Imported from zhufangzhou/multi-agent-pi (
AGENTS.md). Install upstream withnpx skills add zhufangzhou/multi-agent-pi. Copyright stays with the author.
Repository Guidelines
Project Overview
multi-agent-pi is an oh-my-pi extension (/ma-pi slash command) that
dispatches tasks to a multi-agent team from a full-page TUI dashboard. The
team runtime is built directly on the omp SDK (@oh-my-pi/pi-coding-agent):
- every team member is an omp
AgentSessionwith subagent semantics (taskDepth: 1,parentTaskPrefix: "ma-pi"), tools whitelisted toread/bash/hub, and IRC enabled - inter-agent messaging rides omp's process-global IrcBus via the
built-in
hubtool (P2P by id,to:"all"broadcast, auto-wake of idle recipients) — the old self-builtMessageBusis gone - teams are designed on demand: an orchestrator session (one structured
LLM call, enforced via the SDK's
outputSchema+yieldtool) picks the roles for each task;DEFAULT_TEAMinsrc/team.tsis only the fallback - a fixed reporter agent summarizes the team's final outcome once every worker settles
The runtime runs in a daemon subprocess (bun src/daemon.ts) spawned by
the omp host, isolated from the host's own agent loop and session files;
DaemonClient proxies it over stdio JSON-RPC. Sessions persist per working
directory under ~/.omp/ma-pi/sessions/<cwd-slug>/<runId>/ and are resumable.
Architecture & Data Flow
┌─ omp host ────────────────────────────────────────────────┐
│ extension.ts /ma-pi → ctx.ui.custom(MaPiPage, overlay) │
│ resolveModelOptions: ctx.model + ctx.modelRegistry │
│ page.ts MaPiPage — pi-tui Component, 5-zone layout │
│ client.ts DaemonClient — stdio JSON-RPC proxy │
└──────┬────────────────────────────────────────────────────┘
│ Bun.spawn (MA_PI_MODEL_OPTS, MA_PI_RESUME)
┌──────▼────────────────────────────────────────────────────┐
│ daemon.ts JSON-lines command loop + throttled snapshots│
│ controller.ts DomainController (orchestrator + team) │
│ └─ @oh-my-pi/pi-coding-agent: createAgentSession, │
│ ModelRegistry, SessionManager, IrcBus (hub tool) │
│ persist.ts state.json + <agent>.jsonl under ~/.omp/… │
└────────────────────────────────────────────────────────────┘
Data flow:
/ma-piresolves the current omp session's model (ctx.model,ctx.modelRegistry.getApiKey) →ModelOptionspassed to the daemon viaMA_PI_MODEL_OPTS(env fallback:OPENAI_API_KEY/OPENAI_MODEL/OPENAI_BASE_URL)DaemonClientspawnssrc/daemon.ts(same cwd as the user's project) and sendsinit; the daemon answers{type:"ready"}then pushes throttled (200 ms){type:"snapshot"}eventsDomainController.dispatch(task)has the orchestrator design a team (structuredyieldresult; manual JSON parse only as fallback), spawns oneAgentSessionper member, and runs them in parallel#attachSessionsubscribes to session events (message_start,message_update,tool_execution_*,irc_message,message_end,agent_end) and mirrors them intoDomainSnapshotfor the TUI- A 400 ms polling loop (
startPolling) trackssession.isStreaming, marks auto-woken agents idle, and fires the reporter once all workers settle - Every state change schedules a debounced (400 ms) atomic
state.jsonwrite; agent transcripts are JSONL session files owned by the SDK'sSessionManager(resume =SessionManager.open)
Key Files
| File | Role |
|---|---|
src/extension.ts |
Extension entry: registerCommand("ma-pi"), model/chrome resolution, mounts MaPiPage via ctx.ui.custom(…, { overlay: true }) |
src/page.ts |
MaPiPage — full-page TUI: fleet, workspace (Output/Tools/Comm/Inbox tabs), task console with Editor autocomplete (@agent, skill:, /file-commands), keybindings, SGR mouse, streaming reveal, amber chrome palette; header strip incl. a cumulative Tokens row (in/out/cost); builtin commands incl. export (zip of the run) and exit (leave ma-pi) |
src/controller.ts |
DomainController — team design (orchestrator + TEAM_DESIGN_SCHEMA, roster-visible as a Team Designer agent with its thinking/yield trace), session lifecycle, IRC/trace capture, usage accounting, persist/resume, reporter, abort/destroy |
src/client.ts |
DaemonClient — spawn/proxy over line-delimited JSON; snapshot mirror, resume/listSessions ack tracking, stderr drain to daemon.log |
src/daemon.ts |
Daemon host: init/dispatch/tell/resume/list-sessions/export/abort/clear/new/snapshot/destroy commands; MA_PI_RESUME startup resume |
src/zip.ts |
Dependency-free STORE-method zip writer used by exportRun |
src/team.ts |
TeamMember shape (+ role contract: scope/deliverable/dependsOn), DEFAULT_TEAM, REPORTER_MEMBER, buildDispatchPrompt (roster + goal/done + IRC guidance) |
src/model.ts |
Shared types: DomainSnapshot, AgentView, TraceEntry, RunState (schema 1, incl. goal/done), ModelOptions, Usage; truncation constants |
src/persist.ts |
Runs root (~/.omp/ma-pi/sessions/<cwd-slug>/), atomic state.json writes, listRuns/resolveRunDir (exact → prefix → latest) |
src/smoke.ts |
Headless E2E: dispatch → team design → settle → assert all agents done (real provider) |
src/smoke-resume.ts |
Headless resume E2E: controller resume + daemon/client wire resume + follow-up requiring restored history |
Development Commands
npx tsc --noEmit # typecheck (only static check)
source .env && bun src/smoke.ts # E2E dispatch (needs OPENAI_API_KEY etc.)
source .env && bun src/smoke-resume.ts # E2E resume of the newest saved run
Both smoke scripts are the regression gate after upgrading the SDK or
changing the controller: upgrade the pinned @oh-my-pi/* versions, run
typecheck, then both smokes (they use a real provider, minutes each).
Code Conventions
- Naming: camelCase functions/variables, PascalCase classes/types,
kebab-case filenames; event type discriminants snake_case
(
message_start,tool_execution_end,irc_message) - Structured output: the orchestrator's design call uses the SDK's
outputSchema(requireYieldTool) with a JTD schema (TEAM_DESIGN_SCHEMA, incl. the team goal + success criteria and per-role scope/deliverable/dependsOn); validated payloads arrive ontool_execution_endforyield(result.details.data).parseJsonObjectnormalizeSpecremain as fallback/defense-in-depth
- Team design:
designPromptgrades tasks by difficulty — simple → 2 agents single path, linear → pipeline, open-ended/hard → 2-4 agents in the SAME stage exploring mutually exclusive approaches (diversity is the point), plus a later synthesizer/comparator that depends on ALL of them.dependsOnlists each agent's upstreams (same or earlier stage only);normalizeStagesrepairs violations by pushing a consumer past its latest upstream (bubble until stable) and tags every roster entry[stage N] - Convergence:
MESSAGING_GUIDANCEbans acknowledgement/thanks-only replies and has agents stop once their deliverable is out; the designedgoal/donecriteria are injected into every dispatch prompt and the reporter ends its summary with aCOMPLETION:verdict line - Session lifecycle: team sessions are spawned per run and disposed on
abort/switch; the orchestrator session lives for the controller's
lifetime.
dispose()flushes JSONL — the daemon exit path must await it - Persistence:
state.json(atomic tmp+rename) +<agentId>.jsonl; deliberately NOT~/.omp/agent/sessions/…and alwayssuppressBreadcrumb: true, so ma-pi never hijacks omp's own/resumepicker or--continuebreadcrumb./exportzips the whole run (sessions/*.jsonl,state.json,communication.json|md,manifest.json,handoffs/*.md) into<cwd>/ma-pi-export-<runId>.zip - Cross-stage handoff: after each turn the controller writes the agent's
latest deliverable to
<cwd>/.ma-pi/handoffs/<runId>/<agentId>.md; later-stage prompts inject the file list (path + one-line preview) and the agents read the files with thereadtool, so rework updates are always current. Dispatch prompts annotate the roster with[stage N]tags and forbid messaging later-stage teammates (not yet joined — sends would fail); IRC is reserved for same-stage collaboration and rework loops: an earlier-stage agent stays live and can be asked to fix its handoff, and a later-stage finding triggers a rework round via IRC - Isolation: daemon sessions use
disableExtensionDiscovery: true(no user omp extensions in the team), but the SDK still auto-discovers the project's context files (AGENTS.md), skills, and rules from cwd - Error handling:
runAgent-style retry is not used; the controller relies on SDK auto-retry/fallback. Esc-abort disposes sessions and treats the resulting rejections as expected (#abortRequested)
Dependency Surface (@oh-my-pi/*, pinned exact, currently 17.2.9)
| Import | Used in | Purpose |
|---|---|---|
createAgentSession |
controller.ts |
SDK session factory (model, registry, sessionManager, IRC, taskDepth, outputSchema) |
AgentSession (type) |
controller.ts |
subscribe/prompt/dispose/isStreaming |
ModelRegistry, discoverAuthStorage |
controller.ts |
Provider registry + auth storage; the daemon registers its own ma-pi provider (openai-completions/anthropic) |
SessionManager |
controller.ts |
.open(jsonl, dir, …) per-agent persistence + resume |
ExtensionAPI, ExtensionCommandContext |
extension.ts |
Command registration, ctx.ui.custom/notify, model reuse |
Theme, highlightCode, getActiveSkills |
page.ts |
Content rendering + skill completions |
loadSlashCommands, expandSlashCommand (deep) |
page.ts |
File slash-command completions in the task console |
resolveMermaidAscii (deep) |
page.ts |
Mermaid → ASCII inside markdown rendering |
formatStatusIcon (deep) |
page.ts |
Tool-card status icons |
Editor, Markdown, TUI, KeybindingsManager, matchesKey, parseSgrMouse, text utils (pi-tui) |
page.ts |
All interactive rendering; keybindings reuse omp's own names (app.interrupt, tui.select.*, app.thinking.toggle) |
Deep imports (…/modes/theme/mermaid-cache, …/tools/render-utils,
…/extensibility/slash-commands) are internal paths — re-verify with
typecheck + smokes on every SDK upgrade. page.ts mirrors a few omp
internals (streaming-reveal constants, getMarkdownTheme recipe) — replace
with the public API if omp ever exports them.
Testing & QA
- No test framework — the two smoke scripts are the tests
monitor-test/messaging-test/implicit-testfrom the old multi-agent-pi library are gone (nomulti-agent-pidependency anymore)- Smoke tests require real LLM API keys from
.env(OPENAI_API_KEY,OPENAI_MODEL,OPENAI_BASE_URL)
