Updated after running the spike — see the results comment. Two corrections to what follows:
- This analysis missed that Loom already ships an experimental
team_dispatch tool (extensions/loom/teams/). It is a different shape — sequential proposer/critic, no tools — but the real question is how the two relate, which this issue as written does not ask.
- Two further blockers exist beyond the hardcoded binary, and both affect the recommended embedding path: a
--version probe that rejects Loom's version string outright, and Loom's brain starting a greeting turn that makes a worker reject its own task. A "worker mode" for the brain is a prerequisite, not a detail.
The spike itself succeeded end to end, and Orbit needs no UI work. Details in the comment.
Question
Can we offer agent teams in Orbit — an orchestrating session that delegates work to background workers running in parallel — and if so, how? The obvious candidate is the third-party pi extension pi-agents-team (source).
This issue records a source-level feasibility analysis so the design discussion starts from verified facts rather than the package README.
Verified against pi-agents-team@2026.8.5 (config schema v4, repo commit e257585), read from the published tarball and a clone — not the docs. Loom was at pi 0.84.1. Findings may drift as the package evolves.
Short answer: feasible, with one hard blocker that has a clean workaround. But the dependency question and the session-restore semantics deserve a decision before anyone commits.
What Loom already has
- Extensions are injected as
-e <path> pairs in bin/loom.js:567 (mcp-adapter, web-access, loom, orbit-handoff, cli-update, whats-new). Adding one more is a single entry.
- Orbit spawns the brain as
node bin/loom.js --mode rpc (app/src/main/agent.ts:350), with a single AgentManager per window (app/src/main/main.ts:287).
pi.registerTool is already used throughout extensions/loom/ (tools-sync.ts, galaxy-upload.ts).
- pi's version floor for the package (≥0.80.6) is satisfied.
Blocker: the worker binary is hardcoded to pi
dist/src/runtime/worker-process.js:163:
export function spawnWorkerProcess(options) {
const command = options.command ?? "pi";
const args = buildWorkerProcessArgs(options);
const child = spawn(command, args, { cwd: options.cwd, env: options.env, ... });
command comes from DEFAULT_TEAM_CONFIG.rpc (src/config.js:255) — the literal string "pi" with args: ["--mode","rpc","--no-session"] — resolved by PATH lookup, with no require.resolve or absolute-path fallback anywhere in the package.
Two consequences for us:
- Packaged Orbit has no
pi on PATH. The brain ships under process.resourcesPath/loom/, so this either fails outright or finds an unrelated pi.
- Bare
pi workers are not Loom. bin/loom.js is what injects the MCP adapter, the Galaxy MCP spec, the exec-guard, and provider resolution. Workers spawned as plain pi would have no Galaxy tools — which removes the point.
There is no user-facing override. This is structural, not just an absent grep hit: TeamProjectConfigSchema (src/config.ts:108) permits only schemaVersion, version, scaffoldVersion, defaultsVersion, enabled, routingMode, workerAccess, display, roles, and is declared additionalProperties: false — so an rpc block in agents-team.json fails validation rather than taking effect. The loader clones rpc straight from the defaults and never reads it from disk. The package's only env reads are a PATH fingerprint for version caching and PI_AGENT_TEAM_GLOBAL_CONFIG_PATH, which only relocates the config file.
Workaround: embed the library instead of loading the shipped extension
TeamManager is an exported class whose constructor takes a config (src/control-plane/team-manager.js:91): this.config = options?.config ?? DEFAULT_TEAM_CONFIG. The shipped extension never uses that path, but we can:
new TeamManager({ config: { ...DEFAULT_TEAM_CONFIG,
rpc: { ...DEFAULT_TEAM_CONFIG.rpc, command: process.execPath,
args: [LOOM_BIN, "--mode", "rpc", "--no-session"] } } })
Workers then come up as real Loom brains with the full Galaxy stack — no fork, no upstream patch. The cost is that Loom writes a thin extension around the library and owns the tool/command wiring.
Upstreaming an rpc.command config key (or a PI_TEAM_WORKER_CMD env override) would remove the need for this and is probably worth proposing regardless.
Workers can be given extensions — this is real, not advisory
access.extensions on a role becomes literal argv on the worker (src/runtime/worker-process.ts:217):
if (options.extensionMode && options.extensionMode !== "inherit") {
args.push("--no-extensions");
if (options.extensionMode === "worker-minimal") {
for (const source of options.workerExtensions ?? []) args.push("--extension", source);
}
args.push("--no-prompt-templates", "--no-themes", "--no-context-files");
The package's own test (tests/runtime/worker-process.test.ts:88) asserts the exact flag sequence. Notes:
extensionMode: "inherit" is a hard config error in v4 — workers can never inherit the orchestrator's extension set wholesale (deliberate anti-recursion), and an extension source resolving back to pi-agents-team is blocked outright.
- The package has zero MCP awareness — no references anywhere. Galaxy tools reach a worker only via whichever extension is named in
access.extensions, or implicitly if the worker is a Loom brain.
- Worker tool sets are rebuilt per role as an explicit
--tools allowlist, defaulting to ["read","grep","find","ls","bash"]. Galaxy tools need declaring per role — arguably a feature.
options.env is never populated by any call site, so workers inherit the parent's full environment. That's how LOOM_* flags and Galaxy credentials would propagate — for better and worse.
Orbit UI cost is lower than expected
pi defines ExtensionMode = "tui" | "rpc" | "json" | "print" with hasUI: true for both tui and rpc — and Orbit drives the brain in --mode rpc. The package is built and tested for this:
/team is the only command with a full-screen branch, triple-guarded: ctx.mode === "tui" in the command (commands/team.ts:35), a second guard inside the overlay (ui/overlay.ts:1689), and a test asserting ui.custom is never called in RPC mode — it emits plain text instead.
- The other six commands (
/team-copy, /team-result, /team-enable, /team-init, /team-steer, /team-stop) are text in/out.
- All seven tools (
delegate_task, agent_status, agent_result, agent_message, ping_agents, wait_for_agents, agent_cancel) return plain {content, details}. The renderCall field is pi-tui decoration a GUI can ignore.
@earendil-works/pi-tui is imported at module load so it must be resolvable, but all TTY access is inside instance methods — loading in a non-TTY process is safe.
So a minimum viable integration needs no custom Orbit UI. A worker panel becomes an enhancement, not a prerequisite. If we build one: tools are model-invoked rather than push-based, so live status needs its own subscription over the WorkerStatus enum (created, starting, idle, running, waiting_followup, completed, aborted, error, exited), and wait_for_agents can return early with relay_raised — a worker asking a question mid-task — which deserves distinct UI from normal completion.
Two integration details needing a deliberate decision
1. One session write participates in LLM context
- State goes through
pi.appendEntry as customType: "pi-agent-team/state" (type: "custom") — pi documents this as inert and ignored by buildSessionContext. Safe for Loom's notebook to skip.
- Command output goes through
pi.sendMessage as customType: "pi-agent-team/status" (type: "custom_message", role: "custom") — which pi explicitly does fold into the next turn as a user message.
Loom's notebook should recognize and label the latter as extension-originated. A reader that treats any non-assistant entry as user-authored would misattribute dashboard text as something the user typed.
2. Workers do not survive session restore
On session_start, every live worker is force-flipped to exited with "relaunch required for live worker control" (control-plane/persistence.ts:419). Nothing is respawned or reattached. Worker subprocesses run --no-session, so their transcripts are never written anywhere — the package tells the model outright: "Full final answers are live-session-only and are unavailable after session restore." Only compact summaries and usage survive.
Given Orbit's restore behavior (extensions/loom/session-lifecycle.ts), a user reopening Orbit mid-delegation gets dead workers and no deliverables. That is a product decision, not a bug to discover in the field.
Also worth knowing
pathScope is not enforced at the tool layer. From the package's own source comment (safety/launch-policy.ts:58): "Pi DOES NOT currently enforce pathScope at the tool layer — it is an orchestrator-discipline + prompt-convention boundary, NOT an OS sandbox." isPathWithinScope is defined but never called outside tests. Worker containment would be Loom's job via the exec-guard.
access.canSpawnWorkers is parsed and stored but never read at runtime — declared-but-unenforced. Don't rely on it to prevent nested delegation.
- Config resolution: presence of a project
agents-team.json wins over global; if the project file is invalid, the loader falls back to built-in defaults rather than the global file.
Alternatives, if we'd rather not take the dependency
pi ships no built-in team primitive — the extension API is registerTool, registerCommand, registerFlag, registerShortcut, registerProvider, registerNativeProvider, registerMarkdownTransformer, registerMessageRenderer, registerEntryRenderer. Every route builds on one of these seams.
| Approach |
Isolation |
Galaxy tools |
Orbit UI work |
Third-party dep |
1. Loom-native subprocess fan-out — delegate_task tool in extensions/loom/ spawning node bin/loom.js --mode rpc |
process |
inherited |
worker panel |
none |
2. In-process AgentSession fan-out — AgentSession is exported from pi's index |
none |
inherited |
worker panel |
none |
| 3. MCP team server — spawn/delegate tools over the existing adapter |
process |
via adapter |
worker panel |
our own server |
| 4. Orbit tabs — several brains, user-driven |
process |
inherited |
tab UI |
none |
5. pi-agents-team embedded — TeamManager with custom rpc.command |
process |
via rpc.command |
none to start |
yes |
Notes:
- Option 1 reuses machinery Loom already has on both sides:
pi.registerTool in the brain, and app/src/main/agent.ts as a working model of supervising an RPC brain (restart budgets, status transitions, stderr capture).
- Option 2 is cheap and shares MCP connections, but one runaway worker takes the brain down and they contend for a single event loop. Plausible for short read-only workers, risky as the general mechanism.
- Option 4 is a different feature and worth separating: user-driven parallelism (a human opens and steers each session) versus model-driven delegation (the orchestrator decides to fan out). Per
CLAUDE.md, the latter belongs in the brain; tabs are legitimately shell work.
Recommendation
Take option 5 to learn, option 1 to own. Embedding TeamManager is the cheapest way to get a real answer, and its genuinely valuable parts — the role system, launch-policy validation, the relay/wake protocol — are ideas we can lift into a native tool later without changing the model-facing surface.
Proposed spike (~1 day), behind a flag, before any design commitment:
- Wire an embedded
TeamManager with rpc.command pointing at bin/loom.js.
- Delegate one trivial task and confirm a worker comes up as a real Loom brain with Galaxy tools.
- Confirm Orbit renders
delegate_task / wait_for_agents acceptably with no UI work.
- Look at what the notebook actually contains once
pi-agent-team/status messages land in it.
That de-risks every larger decision. Until it runs, I'd hold off on committing to the dependency, given the restore semantics and the question of putting a third party in the agent runtime path.
Open questions for discussion
- Do we want model-driven delegation at all, or is user-driven parallel sessions (option 4) the feature users actually want?
- Altitude check: Galaxy already parallelizes at the job and workflow-step level. Agent teams pay off for parallel reasoning — several independent investigations of a dataset — not parallel compute. Which problem are we solving?
- Third-party dependency in the runtime path: acceptable, vendor, or reimplement?
- Cost and safety: N workers each hold Galaxy credentials and an exec surface, and each spends tokens independently.
LOOM_LOCAL_EXEC is currently decided once per shell (off for the web container, on for desktop) — per-worker semantics need an explicit answer.
- What should happen to in-flight workers when Orbit restarts?
Question
Can we offer agent teams in Orbit — an orchestrating session that delegates work to background workers running in parallel — and if so, how? The obvious candidate is the third-party pi extension
pi-agents-team(source).This issue records a source-level feasibility analysis so the design discussion starts from verified facts rather than the package README.
Verified against
pi-agents-team@2026.8.5(config schema v4, repo commite257585), read from the published tarball and a clone — not the docs. Loom was at pi0.84.1. Findings may drift as the package evolves.Short answer: feasible, with one hard blocker that has a clean workaround. But the dependency question and the session-restore semantics deserve a decision before anyone commits.
What Loom already has
-e <path>pairs inbin/loom.js:567(mcp-adapter, web-access, loom, orbit-handoff, cli-update, whats-new). Adding one more is a single entry.node bin/loom.js --mode rpc(app/src/main/agent.ts:350), with a singleAgentManagerper window (app/src/main/main.ts:287).pi.registerToolis already used throughoutextensions/loom/(tools-sync.ts,galaxy-upload.ts).Blocker: the worker binary is hardcoded to
pidist/src/runtime/worker-process.js:163:commandcomes fromDEFAULT_TEAM_CONFIG.rpc(src/config.js:255) — the literal string"pi"withargs: ["--mode","rpc","--no-session"]— resolved by PATH lookup, with norequire.resolveor absolute-path fallback anywhere in the package.Two consequences for us:
pion PATH. The brain ships underprocess.resourcesPath/loom/, so this either fails outright or finds an unrelatedpi.piworkers are not Loom.bin/loom.jsis what injects the MCP adapter, the Galaxy MCP spec, the exec-guard, and provider resolution. Workers spawned as plainpiwould have no Galaxy tools — which removes the point.There is no user-facing override. This is structural, not just an absent grep hit:
TeamProjectConfigSchema(src/config.ts:108) permits onlyschemaVersion,version,scaffoldVersion,defaultsVersion,enabled,routingMode,workerAccess,display,roles, and is declaredadditionalProperties: false— so anrpcblock inagents-team.jsonfails validation rather than taking effect. The loader clonesrpcstraight from the defaults and never reads it from disk. The package's only env reads are a PATH fingerprint for version caching andPI_AGENT_TEAM_GLOBAL_CONFIG_PATH, which only relocates the config file.Workaround: embed the library instead of loading the shipped extension
TeamManageris an exported class whose constructor takes a config (src/control-plane/team-manager.js:91):this.config = options?.config ?? DEFAULT_TEAM_CONFIG. The shipped extension never uses that path, but we can:Workers then come up as real Loom brains with the full Galaxy stack — no fork, no upstream patch. The cost is that Loom writes a thin extension around the library and owns the tool/command wiring.
Upstreaming an
rpc.commandconfig key (or aPI_TEAM_WORKER_CMDenv override) would remove the need for this and is probably worth proposing regardless.Workers can be given extensions — this is real, not advisory
access.extensionson a role becomes literal argv on the worker (src/runtime/worker-process.ts:217):The package's own test (
tests/runtime/worker-process.test.ts:88) asserts the exact flag sequence. Notes:extensionMode: "inherit"is a hard config error in v4 — workers can never inherit the orchestrator's extension set wholesale (deliberate anti-recursion), and an extension source resolving back topi-agents-teamis blocked outright.access.extensions, or implicitly if the worker is a Loom brain.--toolsallowlist, defaulting to["read","grep","find","ls","bash"]. Galaxy tools need declaring per role — arguably a feature.options.envis never populated by any call site, so workers inherit the parent's full environment. That's howLOOM_*flags and Galaxy credentials would propagate — for better and worse.Orbit UI cost is lower than expected
pi defines
ExtensionMode = "tui" | "rpc" | "json" | "print"withhasUI: truefor both tui and rpc — and Orbit drives the brain in--mode rpc. The package is built and tested for this:/teamis the only command with a full-screen branch, triple-guarded:ctx.mode === "tui"in the command (commands/team.ts:35), a second guard inside the overlay (ui/overlay.ts:1689), and a test assertingui.customis never called in RPC mode — it emits plain text instead./team-copy,/team-result,/team-enable,/team-init,/team-steer,/team-stop) are text in/out.delegate_task,agent_status,agent_result,agent_message,ping_agents,wait_for_agents,agent_cancel) return plain{content, details}. TherenderCallfield is pi-tui decoration a GUI can ignore.@earendil-works/pi-tuiis imported at module load so it must be resolvable, but all TTY access is inside instance methods — loading in a non-TTY process is safe.So a minimum viable integration needs no custom Orbit UI. A worker panel becomes an enhancement, not a prerequisite. If we build one: tools are model-invoked rather than push-based, so live status needs its own subscription over the
WorkerStatusenum (created, starting, idle, running, waiting_followup, completed, aborted, error, exited), andwait_for_agentscan return early withrelay_raised— a worker asking a question mid-task — which deserves distinct UI from normal completion.Two integration details needing a deliberate decision
1. One session write participates in LLM context
pi.appendEntryascustomType: "pi-agent-team/state"(type: "custom") — pi documents this as inert and ignored bybuildSessionContext. Safe for Loom's notebook to skip.pi.sendMessageascustomType: "pi-agent-team/status"(type: "custom_message",role: "custom") — which pi explicitly does fold into the next turn as a user message.Loom's notebook should recognize and label the latter as extension-originated. A reader that treats any non-assistant entry as user-authored would misattribute dashboard text as something the user typed.
2. Workers do not survive session restore
On
session_start, every live worker is force-flipped toexitedwith "relaunch required for live worker control" (control-plane/persistence.ts:419). Nothing is respawned or reattached. Worker subprocesses run--no-session, so their transcripts are never written anywhere — the package tells the model outright: "Full final answers are live-session-only and are unavailable after session restore." Only compact summaries and usage survive.Given Orbit's restore behavior (
extensions/loom/session-lifecycle.ts), a user reopening Orbit mid-delegation gets dead workers and no deliverables. That is a product decision, not a bug to discover in the field.Also worth knowing
pathScopeis not enforced at the tool layer. From the package's own source comment (safety/launch-policy.ts:58): "Pi DOES NOT currently enforce pathScope at the tool layer — it is an orchestrator-discipline + prompt-convention boundary, NOT an OS sandbox."isPathWithinScopeis defined but never called outside tests. Worker containment would be Loom's job via the exec-guard.access.canSpawnWorkersis parsed and stored but never read at runtime — declared-but-unenforced. Don't rely on it to prevent nested delegation.agents-team.jsonwins over global; if the project file is invalid, the loader falls back to built-in defaults rather than the global file.Alternatives, if we'd rather not take the dependency
pi ships no built-in team primitive — the extension API is
registerTool,registerCommand,registerFlag,registerShortcut,registerProvider,registerNativeProvider,registerMarkdownTransformer,registerMessageRenderer,registerEntryRenderer. Every route builds on one of these seams.delegate_tasktool inextensions/loom/spawningnode bin/loom.js --mode rpcAgentSessionfan-out —AgentSessionis exported from pi's indexpi-agents-teamembedded —TeamManagerwith customrpc.commandrpc.commandNotes:
pi.registerToolin the brain, andapp/src/main/agent.tsas a working model of supervising an RPC brain (restart budgets, status transitions, stderr capture).CLAUDE.md, the latter belongs in the brain; tabs are legitimately shell work.Recommendation
Take option 5 to learn, option 1 to own. Embedding
TeamManageris the cheapest way to get a real answer, and its genuinely valuable parts — the role system, launch-policy validation, the relay/wake protocol — are ideas we can lift into a native tool later without changing the model-facing surface.Proposed spike (~1 day), behind a flag, before any design commitment:
TeamManagerwithrpc.commandpointing atbin/loom.js.delegate_task/wait_for_agentsacceptably with no UI work.pi-agent-team/statusmessages land in it.That de-risks every larger decision. Until it runs, I'd hold off on committing to the dependency, given the restore semantics and the question of putting a third party in the agent runtime path.
Open questions for discussion
LOOM_LOCAL_EXECis currently decided once per shell (off for the web container, on for desktop) — per-worker semantics need an explicit answer.