diff --git a/docs/subagent-architecture.html b/docs/subagent-architecture.html new file mode 100644 index 00000000..6e12c449 --- /dev/null +++ b/docs/subagent-architecture.html @@ -0,0 +1,884 @@ + + + + + +Subagent Orchestration Architecture + + + +
+ +

Subagent Orchestration Architecture

+

Super Multica — Parent-child agent spawning, lifecycle management, and result announcement

+ + +

System Architecture

+ + +
+ + +
+
+
+ Agent + Parent Agent (Interactive Session) +
+
+ User-facing agent with full tool access. Can spawn child agents via sessions_spawn tool. + Receives announcement messages when child agents complete. +
+
src/agent/runner.ts → tools: sessions_spawn, exec, glob, web_fetch, ...
+
+
+ + +
+ + + + + + + + + + + + spawn + + + announce + +
+ + +
+
+
+ Singleton + Hub +
+
+ Central coordinator. Creates & manages all agents. Provides createSubagent(), + getAgent(), closeAgent(). Calls registry init on startup, shutdown on exit. +
+
src/hub/hub.ts + hub-singleton.ts
+
+ +
+
+ Module + Subagent Registry +
+
+ In-memory Map + JSON persistence. Tracks run lifecycle (created → started → ended). + Archive sweeper cleans old runs every 60s. Handles crash recovery on restart. +
+
src/agent/subagent/registry.ts
+
+
+ + +
+ + + + + + + + + + + createSubagent() + + watchChildAgent() + +
+ + +
+
+
+ Agent + Child AsyncAgent +
+
+ Isolated agent with isSubagent: true. Restricted tools (no sessions_spawn). + Custom system prompt: stay focused, no user messaging, no nested spawning. +
+
src/agent/async-agent.ts
+
+ +
+
+ Flow + Announce Module +
+
+ Reads child's last assistant reply from session JSONL. Formats announcement message with findings, duration, status. + Delivers to parent via parentAgent.write(). +
+
src/agent/subagent/announce.ts
+
+ +
+
+ Store + Registry Store +
+
+ JSON file persistence at ~/.super-multica/subagents/runs.json. + Schema: { version: 1, runs: {...} }. Survives process restarts. +
+
src/agent/subagent/registry-store.ts
+
+
+ +
+ + +

Call Chain — Spawn & Lifecycle

+ + +
+ +
+
Parent Agent invokes sessions_spawn tool
+
+ Agent calls tool with { task, label?, model?, cleanup?, timeoutSeconds? }. + Guard rejects if isSubagent === true. +
+
sessions-spawn.ts → execute()
+
+ +
+
Generate IDs & build system prompt
+
+ runId = UUIDv7, childSessionId = UUIDv7. + buildSubagentSystemPrompt() creates prompt with task context, rules (no nested spawn, stay focused). +
+
announce.ts → buildSubagentSystemPrompt()
+
+ +
+
Hub creates child AsyncAgent
+
+ hub.createSubagent(childSessionId, { systemPrompt, model }) + creates an AsyncAgent with isSubagent: true. Not persisted to agent store (ephemeral). +
+
hub.ts → createSubagent()
+
+ +
+
Write task to child (non-blocking)
+
+ childAgent.write(task) enqueues the task to the serial queue. + This happens before registration so waitForIdle() observes queued work. +
+
async-agent.ts → write() (enqueues to serial queue)
+
+ +
+
Register run in registry
+
+ registerSubagentRun() saves record to in-memory Map + JSON file. + Sets createdAt, starts archive sweeper. +
+
registry.ts → registerSubagentRun()
+
+ +
+
Start lifecycle watcher & return to parent
+
+ watchChildAgent() sets startedAt. + Attaches childAgent.waitForIdle() (promise resolves when task queue drained) + and childAgent.onClose() callback. Optionally sets timeout timer. + Tool returns { status: "accepted", childSessionId, runId } immediately. +
+
registry.ts → watchChildAgent() → AsyncAgent.waitForIdle() + onClose()
+
+ +
+
Child agent processes task autonomously
+
+ Child runs LLM inference with restricted tools. Uses its own session. + May call exec, glob, web_fetch etc. but NOT sessions_spawn. +
+
runner.ts → Agent.run() (within AsyncAgent queue)
+
+ +
+
Child completes → waitForIdle() resolves
+
+ Task queue drains. Watcher's cleanup callback fires: sets endedAt, outcome: { status: "ok" }. + Persists updated record to JSON. +
+
registry.ts → cleanup() → handleRunCompletion()
+
+ +
+
Announce flow: read child reply & deliver to parent
+
+ readLatestAssistantReply(childSessionId) reads session JSONL, extracts last assistant text. + formatAnnouncementMessage() builds summary with task, status, findings, runtime. + parentAgent.write(message) delivers to parent. +
+
announce.ts → runSubagentAnnounceFlow() → hub.getAgent(parentId).write()
+
+ +
+
Session cleanup & archive
+
+ If cleanup === "delete": removes child session directory + closes agent in Hub. + Schedules archive at now + 60min. Sweeper removes from registry after TTL. +
+
registry.ts → deleteChildSession() + sweep()
+
+ +
+ + +

Sequence Diagram

+ + +
+ + + + + + Parent Agent + + + sessions_spawn + + + Hub + + + Registry + + + Child Agent + + + + + + + + + + + execute({ task, label }) + + + + createSubagent(id, opts) + + + + new AsyncAgent({ isSubagent: true }) + + + + childAgent.write(task) + + + + registerSubagentRun(params) + + + + waitForIdle() + onClose() + + + + { status: "accepted", runId } + + + + + async (non-blocking) + + + + LLM + inference + + + + idle (resolved) + + + + announce + + + + parentAgent.write(announcement) + + + + deleteChildSession() + + + + archive 60m + + + + + + + + + + + + + + + + + + + + + + + +
+ + +

Run State Machine

+ + +
+
+ + created +
+
+ + + + + + + + + startedAt +
+
+ + started +
+
+ + + + + + + + + endedAt +
+
+ + ended +
+
+ + + + + + + + + announce +
+
+ + cleanup done +
+
+ + + + + + + + + 60 min +
+
+ + archived +
+
+ +
+
Outcome Status Values
+
+ ok — Task completed normally (waitForIdle resolved) + error — Child agent threw an error + timeout — Exceeded timeoutSeconds limit + unknown — Process crash or Hub shutdown +
+
+ + +

Module Map

+ + +
+
+
src/agent/subagent/types.ts
+
Core type definitions
+
+ export SubagentRunOutcome
+ export SubagentRunRecord
+ export RegisterSubagentRunParams
+ export SubagentAnnounceParams
+ export SubagentSystemPromptParams +
+
+ +
+
src/agent/subagent/registry.ts
+
In-memory registry + lifecycle watcher
+
+ export initSubagentRegistry()
+ export registerSubagentRun()
+ export listSubagentRuns()
+ export releaseSubagentRun()
+ export getSubagentRun()
+ export shutdownSubagentRegistry() +
+
+ +
+
src/agent/subagent/registry-store.ts
+
JSON file persistence
+
+ export loadSubagentRuns()
+ export saveSubagentRuns()
+ export getSubagentStorePath() +
+
+ +
+
src/agent/subagent/announce.ts
+
Result propagation child → parent
+
+ export buildSubagentSystemPrompt()
+ export readLatestAssistantReply()
+ export formatAnnouncementMessage()
+ export runSubagentAnnounceFlow() +
+
+ +
+
src/agent/tools/sessions-spawn.ts
+
Tool definition for parent agents
+
+ export createSessionsSpawnTool()
+ schema: { task, label?, model?,
+   cleanup?, timeoutSeconds? } +
+
+ +
+
src/hub/hub-singleton.ts
+
Global Hub access for tools & registry
+
+ export setHub(hub)
+ export getHub()
+ export isHubInitialized() +
+
+
+ + +

Key Design Decisions

+ + +
+
+
waitForIdle() vs stream consumption
+
+ Channel is single-reader. Hub.consumeAgent() already reads the stream for forwarding events. + Registry uses waitForIdle() (promise on internal task queue) to detect completion without competing for the stream. +
+
+
+
Singleton Hub access
+
+ Tools and registry modules cannot receive Hub via constructor injection (tools are created before Hub exists). + A module-level singleton (setHub/getHub) bridges this gap, with isHubInitialized() guard for test safety. +
+
+
+
Crash recovery
+
+ Runs are persisted to JSON after every state change. On restart, initSubagentRegistry() + loads persisted runs: completed-but-unannounced runs trigger announce flow; unfinished runs are marked as + status: "unknown". +
+
+
+
Subagent isolation
+
+ Child agents have isSubagent: true which applies tool deny-list (blocks sessions_spawn). + System prompt explicitly forbids nested spawning, direct user communication, and off-topic work. + Sessions are ephemeral — deleted after announce unless cleanup: "keep". +
+
+
+ +
+ Super Multica — Subagent Orchestration System — Branch: subagent-orchestration +
+ +
+ +