Package: internal/multiagent
Last updated: 2026-03-26
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.
type Supervisor struct {
*core.Agent
Pool *Pool
Router Router
}
func New(agent *core.Agent, pool *Pool, router Router, noSpawn bool) *SupervisorSupervisor 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.
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() []stringThread-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.
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 |
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.
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.
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
Runcall - 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 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:
- Scans
~/.micro-agent/agentsfor*.jsonfiles. - For each
<id>.json, loads<id>.mdas the prompt. - Builds a
*core.Agentusing the supervisor's baseModelConfig, overridden by any fields in JSON. - Attaches the tools listed in the JSON
toolsarray.
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"
]
}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.
internal/memory.ConversationTree tracks conversation branches. When the user forks the conversation from the CLI, the loop:
- Clones the current conversation into a new session ID
- Registers both as nodes in the tree (parent → child)
- 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)Structured logging uses log/slog (standard library). Log output is written to the configured log file, or to stderr if no file is set.
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.
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 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.
| 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.
| 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.