系統設計
核心模塊、不變量,以及本節所有設計文檔的目錄
👥 本節適合:希望理解 Minara 為何如此構建的研究型開發者,以及需要了解不變量和變更風險邊界的維護者。每個頁面以通俗的"存在意義"說明開頭,以使用示例鏈接收尾,指向對應的用戶功能頁。貢獻者專屬內容均有明確標註。
新人推薦閱讀順序:先讀 Agent 循環(單輪運行的核心),再讀 技能系統(能力的封裝方式),然後根據所關心的功能跳轉到對應子系統。
本節記錄 Minara Agent 的構建方式。先通讀下方的核心模塊概覽,再按目錄跳轉至所需子系統的深度文檔。
目錄
運行時
- Agent 循環:單輪從用戶消息到最終回答的完整流程。
- 技能系統:路由、激活、風險門控,以及完整技能目錄。
- LLM 集成:Provider 抽象、提示詞緩存、模型路由。
- 場景分類器:L0.5 意圖分類,注入流程手冊並預加載技能。
狀態與存儲
- 記憶(子節概覽):四個相互關聯的存儲(會話記憶、個性化、角色反思、學習系統)如何在單輪中協同工作。
- 工作區:Markdown 形式的身份與記憶基準,以及承載持久化對話內容(圖表、報告、上傳文件)的產物與文件存儲。
安全與執行
- 資金安全與沙盒:沙盒、權限等級、鉤子流水線,以及六階段資金安全棧。
I/O 與運維
核心模塊
Agent 由少量模塊組成,通過顯式接口相互協作。本頁逐一介紹每個模塊:其職責範圍、依賴關係,以及所維護的不變量。
app.ts:組合根
apps/agent/src/app.ts 是代碼庫中唯一瞭解所有其他模塊的文件。它導出單一的 createApp() 函數,執行以下操作:
- 以 WAL 模式、啟用 FTS5 的方式在
$dataDir/下打開 SQLite 數據庫。 - 構建
ToolRegistry,安裝權限等級鉤子和"分析 → 交易"邊界鉤子。 - 實例化所有工具工廠(
createReadTools、createTradeTools、createMemoryTools、createFileTools等)並註冊。依賴缺失環境變量的工廠返回[],啟動時不拋出異常。 - 從
BUILTIN_SKILLS及buildExternalDomainSkills()的輸出(apps/agent/src/skills/external/下的外部技能)構建SkillRegistry。 - 連接
AgentLoop,注入 LLM 客戶端、註冊表、沙盒解析器和審計日誌寫入器。 - 返回組裝好的
App對象供網關驅動。
createApp() 是純函數(給定環境變量和配置),CLI REPL 與 HTTP 網關共享完全相同的運行時。若想確認"X 在哪裡接入",答案始終是 app.ts。
core/tool-registry.ts:類型化分發與權限等級
工具註冊表是 Agent 能力的具體表示。每個工具對應一個 ToolEntry:
interface ToolEntry {
name: string;
toolSet: string;
schema: ToolSchema; // JSON Schema for LLM tool-use
handler: ToolHandler; // async (args) => string
permissionTier: PermissionTier;
isAsync: boolean;
description: string;
checkFn?: () => boolean; // optional runtime availability gate
}權限等級
enum PermissionTier {
READ_ONLY = 1, // price, balance, trending, fear_greed, search
CONFIRM_ONCE = 2, // analyze, research, small swap, write_file
ALWAYS_CONFIRM = 3, // perps, large swap, autopilot start/modify
MANUAL_ONLY = 4, // withdraw, external-address send, emergency stop
}等級由 BeforeToolCallHook 強制執行,非建議性約束。鉤子拋出 ToolCallBlockedError 時,Agent 循環捕獲該錯誤,將其作為工具結果返回給 LLM。LLM 將其視為普通工具錯誤;審計日誌對其單獨記錄。
工具集
BUILTIN_TOOL_SETS 將工具分組為具名包(read、trade、perps、file、browser 等),支持可選的 includes 組合。技能按名稱引用工具,而註冊表和網關則以工具集為單位進行管理:更易推理,也更易門控。定時 Autopilot 輪次以 allowedToolSets: ["read", "memory", "web"] 運行,不包含其他工具集。
上下文傳播
註冊表使用 AsyncLocalStorage 在異步工具調用中傳遞 ToolCallContext,包括通過 subagent 發起的子 Agent 調用和技能執行的工具序列。鉤子通過 ToolRegistry.currentContext() 讀取上下文,以強制執行每輪不變量(分析 → 交易邊界、allowedToolSets 白名單、緊急停止開關)。
core/agent-loop.ts:規劃 → 調用 → 觀察 → 決策
Agent 循環是一個普通的 while (iterations < max) 編排器:
- 使用當前
SkillSession狀態,通過prompt-builder.ts構建系統提示詞。 - 以當前激活技能所允許的工具集(與當前輪次的
allowedToolSets取交集)調用 LLM。 - 對每次工具調用執行:
- 驗證聲明的意圖與實際工具調用一致。
- 運行註冊表分發,觸發
BeforeToolCallHook。 - 將調用及結果持久化至審計日誌。
- 將結果作為新用戶消息反饋。
- 在
stop_reason: "end_turn"或達到max_iterations時退出。
輪次賬本(風險上限、信號上下文)存儲於 ToolCallContext 對象中;循環通過 runInContext(ctx, fn) 包裝,使每次工具調用都能訪問同一狀態。
詳見 Agent 循環。
core/prompt-builder.ts:系統提示詞組裝
系統提示詞由聲明式塊構建,而非字符串拼接:
system: [
{ type: "text", text: identityPrompt, cache: true },
{ type: "text", text: skillCatalog, cache: true },
{ type: "text", text: activeSkillPrompts, cache: false },
{ type: "text", text: signalContextBlock, cache: false },
]標記 cache: true 的塊會傳遞給 Anthropic 的提示詞緩存(TTL 5 分鐘,節省 10% 費用),使身份標識和技能目錄保持熱緩存。每輪變化的塊(激活技能、信號上下文、待確認項)不緩存。此設計至關重要:提示詞塊順序不當,會將看似"一次 Claude 調用"變成三次緩存未命中。
buildSystemPrompt() 對需要純字符串的調用方(測試、CLI 的 --dump-prompt 模式)封裝了 buildSystemPromptBlocks()。
skills/:技能層
DomainSkill 是完全聲明式的單元:
{
id: "minara.core",
kind: "domain_skill",
description: "Unified Minara API — markets, portfolio, spot/perps trading, sub-account management",
prompt: "...≤ 800 tokens of instructions...",
tool_names: [
"get_price", "get_trending", "get_fear_greed", "search_tokens",
"minara_account", "lookup_token", "minara_total_balance",
"get_portfolio", "get_perps_positions", "minara_pnl",
"swap_tokens", "buy_token", "sell_token", "transfer_token",
"open_perps_position", "close_perps_position",
"minara_perps_wallets_list", "minara_perps_wallet_sweep", /* ... */
],
activation: "auto",
priority: 50,
routing: {
lifecycle_stages: ["discover", "decide", "manage"],
keywords: ["minara", "balance", "swap", "long", "short", "perp", "perps", "perps sub-account"],
asset_classes: ["crypto_major", "crypto_alt", "crypto_meme", "stablecoin", "perps"],
},
}各組件說明:
SkillRegistry(apps/agent/src/skills/registry.ts):維護技能目錄,在註冊時執行requires_env門控,渲染未路由和已路由的目錄塊,拼接激活 ID 的提示詞片段,並彙總工具白名單。SkillSession(apps/agent/src/skills/session.ts):每輪可變狀態,包括哪些技能已激活、適用的風險上限、待處理的確認項。註冊表無狀態;會話負責激活管理。Router(apps/agent/src/skills/router.ts):運行 L0 確定性預過濾,包括關鍵詞評分、階段分類、資產類別檢測,以及在用戶未確認時短路高風險激活的 L3 風險門控。FinanceTaxonomy(apps/agent/src/skills/finance-taxonomy.ts):路由器與技能共用的詞彙表(LifecycleStage、AssetClass、RiskTier)。
路由算法和激活流程詳見 技能系統。
tools/_shared/sandbox.ts:文件系統邊界
每個文件工具調用 resolveInSandbox(relativePath),執行以下操作:
- 拒絕絕對路徑。
- 在
$dataDir/sandbox/files/下解析路徑。 - 逐段遍歷路徑,跟隨符號鏈接,驗證解析後的真實路徑仍以沙盒根目錄為前綴。
- 返回規範化絕對路徑,或拋出
SandboxEscapeError。
工具處理器從不將原始用戶輸入作為 fs.* 參數使用。工具若嘗試路徑遍歷(本不應如此),解析器會在任何系統調用觸及磁盤之前拒絕該請求。
不要編寫使用不同解析器打開文件的並行輔助函數。 單一入口點是沙盒可防禦性的基礎。
tools/_shared/result.ts:工具結果信封
所有工具處理器返回字符串,但該字符串由 ok({...})、err("...")、errFromThrow(e) 封裝為統一的帶標籤信封,LLM 可可靠解析:
{"ok": true, "data": {...}}
{"ok": false, "error": "..."}設計上刻意保持簡單。每個工具返回相同結構,提示詞邏輯(如"結果為錯誤時,道歉並重試")只需編寫一次,無需針對每個工具單獨處理。
gateway/:兩個入口,一套運行時
cli.ts:將 LLM 輸出流式傳輸到 stdout,通過同一AgentLoop路由工具調用的 REPL。執行的第一行導入config/load-env.ts,確保.env在任何代碼讀取process.env之前已填充。server.ts:暴露 HTTP API,詳見 HTTP 網關。使用同一createApp()輸出,CLI 與 HTTP 行為無偏差。skills-cli.ts:minara skills add/list/upgrade/remove子命令。將外部 SKILL.md 包引入apps/agent/src/skills/external/,並在輸出中展示許可證檢測結果。
config/load-env.ts:環境變量初始化
僅有兩項職責:
- 若
.env存在,調用 Node 22 原生的process.loadEnvFile()加載它。 - 記錄哪些變量來自文件、哪些來自 shell,用於啟動日誌。
作為副作用導入,且必須是每個入口的第一個導入。若新增入口時遺漏此導入,下游會出現無聲的環境變量缺失故障。
架構規則:本頁列出的每個模塊只有一項職責,棧中較高層的模塊只依賴較低層的模塊。 Agent 循環依賴註冊表;註冊表依賴工具層;工具層依賴沙盒。若發現自己向上依賴(例如工具處理器導入 agent-loop.ts),應以重構設計來解決,而非添加反向依賴邊。