Documentation v1.0.9

Architecture

A high-level view of how Agenvoy fits together. For per-module diagrams, sequence flows, and the tool-dispatch state machine, jump into the topic-specific pages.

Overview

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 · user-registered MCP server"]
        K["kura mcp<br/>one entry in 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/>absent when the server is not registered| Kura
    Execute <-->|confirm/ask| Pending
    Sub <-->|subagent ask_user| Pending
    Execute <--> Memory
    Execute <--> Session

Layers

Layer Package Responsibility
Entry cmd/app argv dispatch (stop / update / --daemon); MCP server on non-TTY stdin; init sandbox, filesystem policy, MCP manager
App services internal/app services shared by daemon and TUI: agent registry build, config.json watcher that reloads the registry and Telegram / Discord, session watcher, MCP setup, chat push hooks, skill runs, daemon spawn
Startup internal/startup start-on-login via a launchd plist (macOS) or systemd service (Linux), toggled from /config; moved from internal/runtime/startup, which no longer exists
Notes internal/note operator notes in the SQLite note table with an FTS5 trigram index; the old file and ToriiDB copies are migrated in at startup
Runtime singleton internal/runtime server-mode UID lock; SIGTERM prior server on startup
Engine internal/agents/exec Prepare / Start (skill match, named skill, model resolution); iteration loop; tool dispatch; dispatcher routing with tiers and priority (selectAgent.go); cooldown and retry (retryHandler); subagent host (execWithSubagent.go, at most 3 concurrent)
Compaction internal/agents/exec/compact per-model threshold, tool-history and old-history folding, trim and raw-tool fallbacks
Providers go-llm-router v0.6.0 (external module) unified Agent.Send() across 13 provider entries plus any OpenAI-compatible endpoint; reasoning and fast-mode normalization. Speech-to-text and text-to-speech route separately, and only OpenAI and Gemini back them
Tools internal/tools + internal/runtime/toolAdapter built-in / API / script / extension tool definitions
MCP internal/runtime/mcp client and server on the official modelcontextprotocol/go-sdk, one package
Sandbox go-pkg/sandbox OS-native isolation, single entry Wrap()
Filesystem go-pkg/filesystem (+ reader/) + internal/filesystem policy-aware writes; ToriiDB pathing
Session internal/session history.json / summary / action.log / pending metadata / fsnotify observer; session config lives in the SQLite session table, and liveness is a ToriiDB online marker rather than a persisted state row
Dashboard page/ + internal/runtime/routes Web UI embedded into the binary and served at /; AGENVOY_PAGE_DIR serves it from disk instead. On startup the daemon also installs a Chrome app launcher pointed at that URL — a bundle under ~/Applications on macOS, a .desktop entry on Linux, a Windows shortcut under WSL — so the dashboard opens as its own window. It is written once and skipped thereafter; a missing Chrome only logs a warning. Since v1.0.1 it also runs offline: /sw.js precaches the embedded assets in a service worker and /vendor/* serves the third-party libraries from ~/.config/agenvoy/vendor/, filled once at daemon startup by internal/runtime/webapp.SyncAsset and refreshed when the binary version changes
Pending internal/runtime/pending.go prefix-routed confirm/ask listener registry; per-front-end listener via RegisterListener(prefix), claim via PickNext(prefix) / PickNextMatch(prefix, accept) (PickNextFor was removed)
Memory go-sqlkit (SQLite: session, message_meta, note + FTS5, messages + FTS5, action_history, file_history, usage) + ToriiDB (db_0 tool and provider-quota cache, db_1 chat vectors, db_2 error memory, db_3 online markers) full-text archive (trigram tokenizer), operator notes, per-model usage, file snapshots, semantic search, 90-day error memory. The state table was dropped in v0.35.3, when liveness moved to ToriiDB online markers; ToriiDB is reached over its own socket since v0.35.3 (now ToriiDB v0.7.0), so the /v1/toriidb HTTP gate is gone
Scheduler internal/runtime scheduler + fsnotify watcher cron / one-shot tasks bound to scheduler skills; hot-reload on {tasks,crons}.json change
TUI internal/runtime/tui bubbletea inline-chat front-end; single-package by design

Cross-cutting principles

TUI design choices

Per pardn chiu: "bubbletea isn't designed to be split into separate modules that reference each other — splitting it would make the lifecycle a mess. I don't have the bandwidth to handle it right now." This module is intentionally kept undivided.

The TUI lives in a single package (internal/runtime/tui) and is not split into subpackages. Every file under internal/runtime/tui/ follows this principle.

Why bubbletea (not tview / tcell)

The previous TUI used rivo/tview. It was replaced because:

The cost is that bubbletea is a Go port of The Elm Architecture — its tea.Model interface is monolithic by design.

Why a single package

tea.Model requires Update(tea.Msg) (tea.Model, tea.Cmd) to be a method on the model type. Methods must live in the same package as the type. This forces:

A real Go-style TUI would build per-domain widget packages (each owning its state struct, render method, and event handler) with bubbletea acting only as event loop. That refactor is a 600-800 LOC rewrite split into 4 phases. For the current TUI (about 10.7k non-test LOC across internal/runtime/tui) maintained by one developer, the gain doesn't justify the cost.

When to revisit

Switch to per-domain widget packages when any one of:


[!NOTE] This document was auto-generated by Claude after reading the full source code.

中文