Skip to content

Agent teams in Orbit: pi-agents-team feasibility and alternatives #407

Description

@nekrut

Updated after running the spike — see the results comment. Two corrections to what follows:

  1. 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.
  2. 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:

  1. Packaged Orbit has no pi on PATH. The brain ships under process.resourcesPath/loom/, so this either fails outright or finds an unrelated pi.
  2. 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-outdelegate_task tool in extensions/loom/ spawning node bin/loom.js --mode rpc process inherited worker panel none
2. In-process AgentSession fan-outAgentSession 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 embeddedTeamManager 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:

  1. Wire an embedded TeamManager with rpc.command pointing at bin/loom.js.
  2. Delegate one trivial task and confirm a worker comes up as a real Loom brain with Galaxy tools.
  3. Confirm Orbit renders delegate_task / wait_for_agents acceptably with no UI work.
  4. 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?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions