Skip to content

Latest commit

 

History

History
247 lines (173 loc) · 9.51 KB

File metadata and controls

247 lines (173 loc) · 9.51 KB

Multi-Agent System

Package: internal/multiagent
Last updated: 2026-03-26


Overview

The multiagent package extends the single-agent foundation into a multi-agent pipeline. A Supervisor agent orchestrates a Pool of specialised sub-agents via the spawn_agent tool. Sub-agents communicate with the supervisor through Go channels; all execution is in-process.


Components

Supervisor

type Supervisor struct {
    *core.Agent
    Pool   *Pool
    Router Router
}

func New(agent *core.Agent, pool *Pool, router Router, noSpawn bool) *Supervisor

Supervisor embeds *core.Agent so it participates in the full callback and memory pipeline. New appends the spawn_agent tool when noSpawn is false; do not register spawn_agent manually. The agent must not already include spawn_agent in Tools when calling New.

Supervisor.Run delegates to Agent.Run. When the LLM invokes spawn_agent, the tool dispatches a sub-task to the pool asynchronously and returns the result.

Recursive delegation is prevented by design: spawn_agent is registered only on the Supervisor, not on sub-agents.


Pool

type Pool struct { ... }

func NewPool() *Pool
func (p *Pool) Register(name string, agent *core.Agent)
func (p *Pool) Get(name string) (*core.Agent, bool)
func (p *Pool) Snapshot() map[string]*core.Agent
func (p *Pool) Names() []string

Thread-safe, append-friendly registry of named sub-agents. Registrations are expected at startup; Snapshot returns an independent copy of the agent map for safe concurrent reads.


Router Interface

type Router interface {
    Route(ctx context.Context, task string, agents map[string]*core.Agent) (string, error)
}

Two built-in implementations:

Implementation Latency Deterministic Use case
KeywordRouter Zero (no LLM) Yes Well-scoped deployments
LLMRouter +1 LLM call No General/ambiguous task routing

KeywordRouter

router := multiagent.NewKeywordRouter(map[string][]string{
    "code":     {"implement", "fix", "refactor", "code", "build"},
    "research": {"find", "search", "look up", "what is"},
    "shell":    {"run", "execute", "install", "deploy"},
})

Returns the first agent whose keyword list contains a case-insensitive substring match with the task.

LLMRouter

router := multiagent.NewLLMRouter(provider, modelName)

Sends a single low-temperature completion to pick the best agent. Falls back to fuzzy name matching if the model returns extra text. Costs one LLM round-trip per routing decision.


spawn_agent Tool

Registered automatically by New(..., noSpawn=false). Parameters:

Field Type Required Description
task string yes Full description of the sub-task
agent string no Target agent name; omit to let the Router decide

Sub-agent results are delivered via a Go channel:

  • Context cancellation from the supervisor propagates into the sub-agent's Run call
  • The sub-agent runs its own full agent loop (may call tools, multiple LLM turns)
  • The final assistant message is returned as the tool result

Sub-Agents from Configuration

Sub-agents are configured via JSON+Markdown pairs in ~/.micro-agent/agents:

  • ~/.micro-agent/agents/<id>.json — model parameters and tool selection
  • ~/.micro-agent/agents/<id>.md — system prompt for the sub-agent

On startup, internal/app:

  1. Scans ~/.micro-agent/agents for *.json files.
  2. For each <id>.json, loads <id>.md as the prompt.
  3. Builds a *core.Agent using the supervisor's base ModelConfig, overridden by any fields in JSON.
  4. Attaches the tools listed in the JSON tools array.

Minimal JSON schema:

{
  "id": "c-3po",
  "model": "qwen/qwen-2.5-32b-instruct",
  "temperature": 0.7,
  "top_p": 0.8,
  "top_k": 20,
  "min_p": 0.0,
  "presence_penalty": 1.5,
  "repetition_penalty": 1.0,
  "thinking_enabled": false,
  "tools": [
    "read_file",
    "write_file",
    "append_file",
    "edit_file",
    "list_dir",
    "browser_search",
    "browser_content",
    "web_fetch"
  ]
}

Wiring Example

At startup, internal/app builds a multiagent.Pool by calling:

agents, err := multiagent.LoadAgentsFromDir(
    "~/.micro-agent/agents",
    model,
    mem,
    shellExecEnabled,
    browserlessURL,
    telegramToken,
    telegramChatID,
)

Each discovered config becomes a named sub-agent; routing then works as described above. The default supervisor uses multiagent.NewLLMRouter(provider, cfg.Model) and multiagent.New(supervisorAgent, pool, router, noSpawn) where noSpawn comes from the --no-spawn / --safe CLI flags.


Session History Tree

internal/memory.ConversationTree tracks conversation branches. When the user forks the conversation from the CLI, the loop:

  1. Clones the current conversation into a new session ID
  2. Registers both as nodes in the tree (parent → child)
  3. Switches to the new branch

The tree preserves cumulative token counts and tool call counts per branch.

tree := memory.NewConversationTree()
child, err := tree.Fork(parentID, newID)     // clone + register
tree.UpdateTokens(nodeID, inputDelta, outDelta)
tree.IncrToolCalls(nodeID, n)

Logging and Verbosity

Structured logging uses log/slog (standard library). Log output is written to the configured log file, or to stderr if no file is set.

Verbosity Flags

Pass one of the following flags to ua at startup:

Flag slog Level Behaviour
(none) Warn Warnings and errors only
-v Info High-level actions: loop start/end, LLM calls, tool names, token counts
-vv Debug Intermediate detail: truncated content, params, and result summaries
-vvv Trace Full detail: complete params JSON, full LLM responses, full tool results

At -vvv the handler also records source file and line number.

Config File

Configuration is loaded by internal/config.Load() from a nested JSON file and environment (env overrides file). Default file: ~/.micro-agent/config.json. Override with UA_CONFIG. See docs/config.md for the full schema and examples/config.json for an example. LOG_FILE (or default.log_file in the file) controls the log path; if unset, output goes to stderr.

LogCallback (internal/core/log_callback.go)

LogCallback implements core.Callback and is wired into the agent at startup when any verbosity flag is set. It logs:

Hook -v -vv -vvv
BeforeAgentLoop conv ID, message count + +
AfterAgentLoop conv ID, final message count + +
BeforeLLMCall message count + last user message (truncated, 200 chars) + full
AfterLLMCall token counts, tool call count + response summary (truncated) + full content
BeforeToolExecution tool name + params (truncated) + full params JSON
AfterToolExecution tool name + result content (truncated) + full result content

AfterToolExecution always logs at Warn on error, regardless of verbosity level.

LevelTrace (slog.Level(-8)) is exported from the package for use in slog handler configuration.


Memory and Web Tools (supervisor defaults)

Memory Tools (internal/toolsmemorytool)

Tool name Description Required params
memory_save Save/upsert a key-content entry key, content
memory_search Semantic search over Milvus (query embedding) query
memory_delete Delete entry by key key

Registered only when memory.OpenStore succeeds and returns a non-nil core.MemoryStore.

Web and Browser Tools

Tool name Description
browser_search Web search via Browserless (BROWSERLESS_URL required)
browser_content Rendered page text via Browserless
web_fetch HTTP fetch and plain-text extraction (no JS); ~512 KiB download cap, ~10 000 runes of extracted text

browser_screenshot is implemented (tools.NewBrowserScreenshot) but not wired in internal/app/supervisor.go (registration is commented out) and not listed in internal/multiagent buildToolsForConfig, so sub-agent JSON cannot enable it without code changes.

Details and parameters: docs/tools.md.