架構
Agenvoy 整體如何組合的高階視角。各模組圖表、序列流程與 tool-dispatch 狀態機請跳至各主題專頁。
概覽
graph TB
subgraph Entry["Entry · cmd/app"]
App["agen · TUI + daemon<br/>agen stop · update · --daemon<br/>non-TTY stdin · MCP server"]
Web["Web dashboard · page/<br/>embedded in the binary<br/>served at 127.0.0.1:17989/"]
end
subgraph Engine["Engine · internal/agents/exec"]
Run["exec.Prepare · exec.Start<br/>skill match → named skill → ResolveAgent<br/>session model · dispatcher · tiers · priority"]
Execute["exec.Execute · ≤128 iterations<br/>3-pass parallel tool calls"]
Sub["ExecWithSubagent<br/>in-process · ≤3 concurrent"]
Compact["compact · threshold → tool history<br/>→ old history → trim fallback"]
end
subgraph Pending["Pending · internal/runtime"]
Reg["Prefix-routed listener registry<br/>per-entry buffered=1 reply ch<br/>TUI cli- · web chat- · Telegram tg- · Discord dc-"]
end
subgraph Providers["go-llm-router v0.6.0 · 13 provider entries + compat"]
P["OpenAI · Codex · Claude · Gemini<br/>Grok · Grok (xAI) · Copilot · DeepSeek<br/>Mistral · NVIDIA · Ollama Cloud · OpenRouter<br/>Cloudflare · + any OpenAI-compatible endpoint"]
end
subgraph Tools["Tool Subsystem · internal/tools"]
T["find_files · read_files · edit_file · file_history<br/>search_web · fetch_page · http_request<br/>subagents · schedules · run_skill · ask_user<br/>chat_history · error_history · find_note<br/>reasoning_guide · write_report"]
MCP["MCP · internal/runtime/mcp<br/>client + server · go-sdk"]
Adapter["toolAdapter · internal/runtime<br/>script_* · api_* · ext_*"]
end
subgraph Security["Security · go-pkg/sandbox"]
S["bwrap (Linux) / sandbox-exec (macOS)<br/>filesystem.Policy · DeniedMap"]
end
subgraph Session["Session · internal/session"]
SL["history.json · summary.json<br/>action.log · pending/<br/>fsnotify watch"]
end
subgraph Memory["Memory · SQLite + ToriiDB"]
M["history.db · session · message_meta · note<br/>messages + FTS5 · action_history<br/>file_history · usage<br/>db_0..db_3 · error_memory 90d TTL"]
end
subgraph Kura["KuraDB · 使用者自行註冊的 MCP server"]
K["kura mcp<br/>mcp.json 中的一筆設定<br/>mcp__kura__list_rag · mcp__kura__search_rag"]
end
App --> Run
Web --> Run
Run --> Execute
Execute -->|subagents| Sub
Sub --> Execute
Execute --> Compact
Execute -->|Send| Providers
Execute -->|tool calls| Tools
Tools --> MCP
Tools --> Adapter
Tools --> Security
Tools -.->|mcp__kura__*<br/>未註冊該伺服器時完全不存在| Kura
Execute <-->|confirm/ask| Pending
Sub <-->|subagent ask_user| Pending
Execute <--> Memory
Execute <--> Session
分層
| 層 | 套件 | 職責 |
|---|---|---|
| Entry | cmd/app |
argv dispatch(stop / update / --daemon);stdin 非 TTY 時作為 MCP server;初始化 sandbox、filesystem policy、MCP manager |
| App services | internal/app |
daemon 與 TUI 共用的服務:建立 agent registry、監看 config.json 並重新載入 registry 與 Telegram / Discord、session watcher、MCP 設定、聊天推送 hook、skill 執行、daemon 啟動 |
| Startup | internal/startup |
以 launchd plist(macOS)或 systemd service(Linux)登入時啟動,由 /config 切換;由 internal/runtime/startup 移入,原路徑已不存在 |
| Runtime singleton | internal/runtime |
server-mode UID lock;啟動時對先前 server 送 SIGTERM |
| Engine | internal/agents/exec |
Prepare / Start(skill 比對、指名 skill、模型解析);iteration loop;tool dispatch;結合 tier 與優先序的 dispatcher 路由(selectAgent.go);cooldown 與重試(retryHandler);subagent host(execWithSubagent.go,最多 3 個並行) |
| Compaction | internal/agents/exec/compact |
各模型門檻、tool 歷史與舊歷史折疊、trim 與 raw-tool fallback |
| Providers | go-llm-router v0.6.0(外部模組) |
跨 13 個 provider 條目及任何 OpenAI 相容端點的統一 Agent.Send();reasoning 與 fast 模式正規化。speech-to-text 與 text-to-speech 各自獨立路由,且僅由 OpenAI 與 Gemini 支援 |
| Tools | internal/tools + internal/runtime/toolAdapter |
built-in / API / script / extension tool 定義 |
| MCP | internal/runtime/mcp |
建構於官方 modelcontextprotocol/go-sdk 的 client 與 server,單一套件 |
| Sandbox | go-pkg/sandbox |
OS-native 隔離,單一入口 Wrap() |
| Filesystem | go-pkg/filesystem(+ reader/)+ internal/filesystem |
policy-aware 寫入;ToriiDB pathing |
| Session | internal/session |
history.json / summary / action.log / pending metadata / fsnotify observer;session 設定存於 SQLite session 表,存活狀態改由 ToriiDB online marker 判定,不再持久化 state |
| Dashboard | page/ + internal/runtime/routes |
Web UI 內嵌進執行檔並於 / 提供;AGENVOY_PAGE_DIR 改為從磁碟提供。daemon 啟動時另會安裝一個指向該 URL 的 Chrome app 啟動器 —— macOS 為 ~/Applications 下的 bundle、Linux 為 .desktop 項目、WSL 下為 Windows 捷徑 —— 讓儀表板以獨立視窗開啟。只寫入一次,之後略過;找不到 Chrome 僅記錄警告。自 v1.0.1 起亦可離線運作:/sw.js 以 service worker 預快取內嵌資產,/vendor/* 則從 ~/.config/agenvoy/vendor/ 提供第三方函式庫,該目錄由 daemon 啟動時的 internal/runtime/webapp.SyncAsset 填入,執行檔版本變動時更新 |
| Pending | internal/runtime/pending.go |
prefix-routed 的 confirm/ask listener registry;各前端 listener 透過 RegisterListener(prefix),透過 PickNext(prefix) / PickNextMatch(prefix, accept) 認領(PickNextFor 已移除) |
| Memory | go-sqlkit(SQLite:session、message_meta、note + FTS5、messages + FTS5、action_history、file_history、usage)+ ToriiDB(db_0 工具與供應商額度快取、db_1 對話向量、db_2 錯誤記憶、db_3 online marker) |
全文封存(trigram tokenizer)、操作者筆記、各模型用量、檔案快照、語意搜尋、90 天錯誤記憶。state 表於 v0.35.3 移除,存活狀態改由 ToriiDB online marker 承載;同版起改走 ToriiDB 自有 socket(目前 ToriiDB v0.7.0),/v1/toriidb HTTP 入口已不存在 |
| Scheduler | internal/runtime scheduler 與 fsnotify watcher |
綁定 scheduler skill 的 cron / one-shot 任務;{tasks,crons}.json 變更時 hot-reload |
| TUI | internal/runtime/tui |
bubbletea inline-chat 前端;設計上為單一 package |
橫切原則
- OS-native sandbox 優於 Go 側 filter — 安全 policy 在 OS 邊界強制執行;新限制放進
go-pkg/sandbox,不進 agenvoy caller - Prompt 即 policy — permission mode、敏感操作與 system-prompt 保護住在
configs/prompts/;新增類別意味編輯 prompt,而非 engine - subagent 走 in-process 而非 HTTP —
subagents(mode=invoke)直接呼叫exec.Execute,共用同一組 provider client、sandbox、pending registry 與 memory 層;AllowAll與WorkDir透過 ctx 流動 - read tool 扇出、write tool 序列化 — 並行為 opt-in,且同時要求「無副作用」與「上游允許並行」
- 每個關注點一層 config — provider 憑證在 OS keychain、已註冊 model、優先序、tier 與 limits 在
config.json、MCP 在mcp.json、session 設定與 persona 在 SQLitesession表;每位 tool 作者/使用者最多動一個地方 - 單一 HTTP 介面 — TUI、瀏覽器儀表板、聊天通道與外部呼叫端全部經由同一個
127.0.0.1:17989daemon;儀表板為內嵌而非外部託管,因此沒有第二個來源需要同步 - 廠商程式碼留在上游 — provider adapter 已移至
go-llm-router、MCP 協定改用官方 go-sdk;修正應進上游模組,而非在本地加 shim
TUI 設計取捨
引 pardn chiu:「bubbletea 的設計不是要拆成互相 reference 的獨立模組——拆了會讓 lifecycle 一團亂。我現在沒餘力處理它。」 此模組刻意保持不切分。
TUI 住在單一 package(internal/runtime/tui),不拆成子 package。internal/runtime/tui/ 底下每個檔案都遵循此原則。
為何選 bubbletea(而非 tview / tcell)
先前的 TUI 用 rivo/tview,被替換的原因:
- Inline scrollback:bubbletea 的
tea.Println寫出的行會捲進 input box 上方 terminal 原生 buffer。tview 佔據整個螢幕,無法與 shell scrollback 共存。 - lipgloss styling primitives:border、padding、foreground/background 組合乾淨。tview style 為 tag-based,跨 component 重用較難。
- bubbles 生態系:
textarea、spinner、cursor是即插即用的 component,與 charm-bracelet 風格其餘部分一致。
代價是 bubbletea 為 The Elm Architecture 的 Go port——其 tea.Model 介面設計上即為 monolithic。
為何單一 package
tea.Model 要求 Update(tea.Msg) (tea.Model, tea.Cmd) 為 model type 上的 method。Method 必須與 type 住在同一 package。這強制:
- 所有
Update邏輯與 model 同 package - 拆成子 package 需要在第三個(root)package 建 wrapper,並匯出每一個 model field 讓子 package 能讀寫 state
- 目前
popupState、commandPickerState、viewMode等unexportedtype 必須變成 exported,形成一個internal/runtime/tui外部永遠不會消費的「API」 send()與program atomic.Pointer[tea.Program]要嘛移進子 package(root 透過 setter API 設定),要嘛留在 root 並強迫 handler import root,這會造成第二個 cycle
真正 Go 風格的 TUI 會建 per-domain widget package(各自持有 state struct、render method 與 event handler),bubbletea 僅作為 event loop。該重構為 600-800 LOC 的重寫,切成 4 個 phase。對目前約 10.7k 非測試 LOC(internal/runtime/tui)、由單一開發者維護的 TUI,收益不足以正當化成本。
何時重新檢視
當任一條件成立時,切換為 per-domain widget package:
- TUI 超過約 3k LOC,且 code review 持續卡在「這該歸屬哪裡」
- 多位開發者常態觸碰 TUI 並互相踩 state
- 特定 widget 需針對凍結 state 做獨立 unit test——目前不實例化整個
Model就辦不到
[!NOTE] 本文件由 Claude 讀完完整原始碼後自動生成。