galaxyproject / galaxyproject/loom
Agent teams in Orbit: pi-agents-team feasibility and alternatives
- Dominant language
- TypeScript
- Stars
- 14
- Forks
- 12
- Avg merge
- 6d 5h
- Merged PRs (30d)
- 17
Description
> **Updated after running the spike — see [the results comment](https://github.com/galaxyproject/loom/issues/407#issuecomment-5254879353).** 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`](https://pi.dev/packages/pi-agents-team) ([source](https://github.com/KristjanPikhof/pi-agents-team)).
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 ` 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`:
```js
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:
```js
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`):
```ts
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:**
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?
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with the updated results comment, then read app/src/main/agent.ts, bin/loom.js, and the existing extensions/loom/teams/ implementation alongside pi-agents-team's TeamManager and worker-process files. Compare the listed integration options, including worker mode and session restore behavior, and document a decided approach with its blockers, ownership boundaries, and acceptance criteria.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- nodejs, typescript
- Domain
- backend-api-design, devtools
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100