@ymy88/pi-subagents 0.0.0-stage → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +904 -0
- package/CONTRIBUTING.md +72 -0
- package/LICENSE +21 -0
- package/README.md +1170 -2
- package/SECURITY.md +95 -0
- package/dist/abortable.d.ts +12 -0
- package/dist/abortable.js +42 -0
- package/dist/agent-color.d.ts +35 -0
- package/dist/agent-color.js +123 -0
- package/dist/agent-file-toggle.d.ts +133 -0
- package/dist/agent-file-toggle.js +287 -0
- package/dist/agent-manager.d.ts +552 -0
- package/dist/agent-manager.js +1466 -0
- package/dist/agent-record-entry.d.ts +96 -0
- package/dist/agent-record-entry.js +121 -0
- package/dist/agent-runner.d.ts +310 -0
- package/dist/agent-runner.js +1195 -0
- package/dist/agent-source-badge.d.ts +50 -0
- package/dist/agent-source-badge.js +74 -0
- package/dist/agent-types.d.ts +119 -0
- package/dist/agent-types.js +286 -0
- package/dist/child-context.d.ts +2 -0
- package/dist/child-context.js +12 -0
- package/dist/completion-outbox.d.ts +63 -0
- package/dist/completion-outbox.js +104 -0
- package/dist/context.d.ts +12 -0
- package/dist/context.js +56 -0
- package/dist/cross-extension-rpc.d.ts +72 -0
- package/dist/cross-extension-rpc.js +153 -0
- package/dist/custom-agents.d.ts +77 -0
- package/dist/custom-agents.js +396 -0
- package/dist/default-agents.d.ts +7 -0
- package/dist/default-agents.js +121 -0
- package/dist/enabled-models.d.ts +49 -0
- package/dist/enabled-models.js +145 -0
- package/dist/env.d.ts +6 -0
- package/dist/env.js +28 -0
- package/dist/group-join.d.ts +32 -0
- package/dist/group-join.js +116 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.js +4111 -0
- package/dist/invocation-config.d.ts +108 -0
- package/dist/invocation-config.js +83 -0
- package/dist/memory.d.ts +53 -0
- package/dist/memory.js +165 -0
- package/dist/mention-clone.d.ts +79 -0
- package/dist/mention-clone.js +155 -0
- package/dist/mention.d.ts +81 -0
- package/dist/mention.js +131 -0
- package/dist/model-resolver.d.ts +36 -0
- package/dist/model-resolver.js +95 -0
- package/dist/model-scope.d.ts +49 -0
- package/dist/model-scope.js +48 -0
- package/dist/nested-tools.d.ts +57 -0
- package/dist/nested-tools.js +303 -0
- package/dist/output-file.d.ts +43 -0
- package/dist/output-file.js +153 -0
- package/dist/package-resources.d.ts +240 -0
- package/dist/package-resources.js +519 -0
- package/dist/prompts.d.ts +76 -0
- package/dist/prompts.js +121 -0
- package/dist/schedule-store.d.ts +38 -0
- package/dist/schedule-store.js +155 -0
- package/dist/schedule.d.ts +109 -0
- package/dist/schedule.js +359 -0
- package/dist/settings.d.ts +398 -0
- package/dist/settings.js +282 -0
- package/dist/skill-loader.d.ts +24 -0
- package/dist/skill-loader.js +93 -0
- package/dist/status-note.d.ts +61 -0
- package/dist/status-note.js +85 -0
- package/dist/structured-output.d.ts +61 -0
- package/dist/structured-output.js +112 -0
- package/dist/types.d.ts +391 -0
- package/dist/types.js +5 -0
- package/dist/ui/agent-mention.d.ts +82 -0
- package/dist/ui/agent-mention.js +187 -0
- package/dist/ui/agent-widget.d.ts +219 -0
- package/dist/ui/agent-widget.js +585 -0
- package/dist/ui/conversation-viewer.d.ts +177 -0
- package/dist/ui/conversation-viewer.js +905 -0
- package/dist/ui/fleet-list.d.ts +211 -0
- package/dist/ui/fleet-list.js +489 -0
- package/dist/ui/schedule-menu.d.ts +16 -0
- package/dist/ui/schedule-menu.js +94 -0
- package/dist/ui/select-item.d.ts +27 -0
- package/dist/ui/select-item.js +34 -0
- package/dist/ui/viewer-keys.d.ts +22 -0
- package/dist/ui/viewer-keys.js +18 -0
- package/dist/ui/workflow-card.d.ts +175 -0
- package/dist/ui/workflow-card.js +332 -0
- package/dist/ui/workflow-dialog.d.ts +305 -0
- package/dist/ui/workflow-dialog.js +843 -0
- package/dist/ui/workflow-menu.d.ts +60 -0
- package/dist/ui/workflow-menu.js +147 -0
- package/dist/usage.d.ts +135 -0
- package/dist/usage.js +120 -0
- package/dist/workflow/collisions.d.ts +95 -0
- package/dist/workflow/collisions.js +88 -0
- package/dist/workflow/entry.d.ts +32 -0
- package/dist/workflow/entry.js +29 -0
- package/dist/workflow/host.d.ts +62 -0
- package/dist/workflow/host.js +419 -0
- package/dist/workflow/journal.d.ts +97 -0
- package/dist/workflow/journal.js +120 -0
- package/dist/workflow/json-schema.d.ts +52 -0
- package/dist/workflow/json-schema.js +112 -0
- package/dist/workflow/meta.d.ts +67 -0
- package/dist/workflow/meta.js +317 -0
- package/dist/workflow/progress.d.ts +224 -0
- package/dist/workflow/progress.js +361 -0
- package/dist/workflow/runtime.d.ts +334 -0
- package/dist/workflow/runtime.js +830 -0
- package/dist/workflow/saved.d.ts +102 -0
- package/dist/workflow/saved.js +247 -0
- package/dist/workflow/task.d.ts +136 -0
- package/dist/workflow/task.js +207 -0
- package/dist/workflow/tool-description.d.ts +38 -0
- package/dist/workflow/tool-description.js +199 -0
- package/dist/workflow/worker-source.d.ts +47 -0
- package/dist/workflow/worker-source.js +778 -0
- package/dist/worktree.d.ts +52 -0
- package/dist/worktree.js +164 -0
- package/dist/xml.d.ts +10 -0
- package/dist/xml.js +12 -0
- package/docs/packages.md +211 -0
- package/docs/rpc.md +192 -0
- package/docs/workflows.md +447 -0
- package/examples/agent-tool-description.md +42 -0
- package/examples/workflows/compose.js +51 -0
- package/examples/workflows/fan-out-audit.js +47 -0
- package/examples/workflows/gated-fix.js +60 -0
- package/examples/workflows/lib/count-child.js +27 -0
- package/examples/workflows/review-panel.js +63 -0
- package/examples/workflows/structured-findings.js +78 -0
- package/package.json +74 -4
- package/src/abortable.ts +43 -0
- package/src/agent-color.ts +161 -0
- package/src/agent-file-toggle.ts +297 -0
- package/src/agent-manager.ts +1735 -0
- package/src/agent-record-entry.ts +153 -0
- package/src/agent-runner.ts +1468 -0
- package/src/agent-source-badge.ts +75 -0
- package/src/agent-types.ts +346 -0
- package/src/child-context.ts +15 -0
- package/src/completion-outbox.ts +116 -0
- package/src/context.ts +58 -0
- package/src/cross-extension-rpc.ts +221 -0
- package/src/custom-agents.ts +452 -0
- package/src/default-agents.ts +125 -0
- package/src/enabled-models.ts +180 -0
- package/src/env.ts +33 -0
- package/src/group-join.ts +141 -0
- package/src/index.ts +4433 -0
- package/src/invocation-config.ts +155 -0
- package/src/memory.ts +179 -0
- package/src/mention-clone.ts +195 -0
- package/src/mention.ts +141 -0
- package/src/model-resolver.ts +118 -0
- package/src/model-scope.ts +70 -0
- package/src/nested-tools.ts +426 -0
- package/src/output-file.ts +164 -0
- package/src/package-resources.ts +652 -0
- package/src/prompts.ts +171 -0
- package/src/schedule-store.ts +153 -0
- package/src/schedule.ts +386 -0
- package/src/settings.ts +650 -0
- package/src/skill-loader.ts +102 -0
- package/src/status-note.ts +90 -0
- package/src/structured-output.ts +130 -0
- package/src/types.ts +404 -0
- package/src/ui/agent-mention.ts +216 -0
- package/src/ui/agent-widget.ts +661 -0
- package/src/ui/conversation-viewer.ts +928 -0
- package/src/ui/fleet-list.ts +559 -0
- package/src/ui/schedule-menu.ts +105 -0
- package/src/ui/select-item.ts +45 -0
- package/src/ui/viewer-keys.ts +43 -0
- package/src/ui/workflow-card.ts +470 -0
- package/src/ui/workflow-dialog.ts +1115 -0
- package/src/ui/workflow-menu.ts +193 -0
- package/src/usage.ts +167 -0
- package/src/workflow/collisions.ts +123 -0
- package/src/workflow/entry.ts +47 -0
- package/src/workflow/host.ts +467 -0
- package/src/workflow/journal.ts +164 -0
- package/src/workflow/json-schema.ts +129 -0
- package/src/workflow/meta.ts +325 -0
- package/src/workflow/progress.ts +551 -0
- package/src/workflow/runtime.ts +1219 -0
- package/src/workflow/saved.ts +259 -0
- package/src/workflow/task.ts +302 -0
- package/src/workflow/tool-description.ts +200 -0
- package/src/workflow/worker-source.ts +781 -0
- package/src/worktree.ts +205 -0
- package/src/xml.ts +13 -0
package/docs/rpc.md
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# Driving subagents from another extension
|
|
2
|
+
|
|
3
|
+
Another pi extension can spawn a subagent, listen for subagent completion, read the result, steer and stop the run — all over the `pi.events` bus, without importing this package directly. Five request/reply channels (`subagents:rpc:ping`, `subagents:rpc:spawn`, `subagents:rpc:stop`, `subagents:rpc:consume`, `subagents:rpc:steer`), the lifecycle events in README's [event table](../README.md#events), one per-message usage event (`subagents:usage`), one terminal `subagents:disposed`, and one in-process registry at `Symbol.for("pi-subagents:manager")`.
|
|
4
|
+
|
|
5
|
+
The thing worth understanding up front is that **the bus is in-process.** Every "RPC" call here is a synchronous `pi.events.emit` into the same event loop, and every reply comes back the same way. That single fact explains most of what follows: why `signal` and the `on*` callbacks work on a spawn payload at all, why a `consume` fired inside a `subagents:completed` handler lands *before* the notification decision has been made, and why none of this survives a real process boundary.
|
|
6
|
+
|
|
7
|
+
For the channel list, the reply envelope, the per-channel snippets and the event table, see [`README.md`](../README.md#cross-extension-rpc). This document is the reference README does not have room for: the complete spawn-option surface, every error string, the notification race, the registry, and what protocol version `2` does and does not promise.
|
|
8
|
+
|
|
9
|
+
## Spawn options
|
|
10
|
+
|
|
11
|
+
`subagents:rpc:spawn` forwards `options` to `AgentManager.spawn` — but not verbatim. The manager's `spawn` behind the RPC is `spawnTopLevel` in `src/index.ts`, which deletes internal-only fields first, and then `spawnResolved` overwrites the activity-tracker callbacks with its own. The full interface is `SpawnOptions` in `src/agent-manager.ts`; what a bus caller actually gets is three different things.
|
|
12
|
+
|
|
13
|
+
**Honoured** — set these and they take effect:
|
|
14
|
+
|
|
15
|
+
| Field | Type | Notes |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `description` | string | What the agent is doing. Shown in the widget, FleetView and the completion notification |
|
|
18
|
+
| `name` | string | A memorable second handle (`@auth-audit`). Slugged, never validated — anything unusable degrades rather than failing the spawn |
|
|
19
|
+
| `model` | `Model` **or** `"provider/modelId"` | Strings are resolved at the RPC boundary against `ctx.modelRegistry`. `null` means inherit, not override. Resolution is fuzzy — see [Model Scope](../README.md#model-scope) |
|
|
20
|
+
| `maxTurns` | number | Turn ceiling for the run |
|
|
21
|
+
| `isolated` | boolean | Strips extensions, skills and nested tools. Applies only when the agent file leaves `isolated:` unset — frontmatter is authoritative. **Not** a git worktree — see the trap table below |
|
|
22
|
+
| `inheritContext` | boolean | Fork the parent conversation into the child. Only this option decides — an agent file's `inherit_context:` does not apply to RPC spawns |
|
|
23
|
+
| `thinkingLevel` | ThinkingLevel | Clamped to what the resolved model supports |
|
|
24
|
+
| `isBackground` | boolean | Occupies a `maxConcurrent` slot and queues behind them. Every RPC spawn runs detached regardless; this is what decides whether it is *pooled* |
|
|
25
|
+
| `bypassQueue` | boolean | Starts immediately even when the concurrency limit would queue it. The slot is still counted once running |
|
|
26
|
+
| `structuredOutput` | CompiledSchema | Makes the child report through a `StructuredOutput` tool |
|
|
27
|
+
| `isolation` | `"worktree"` | Temp git worktree, committed to a `pi-agent-*` branch on completion. The agent file's `isolation:` wins: `worktree` applies even when this is omitted, and `off` refuses it |
|
|
28
|
+
| `cwd` | absolute path | The agent's tools operate here; `.pi` config still loads from the parent session's project, and the child reports this directory as untrusted (`ctx.isProjectTrusted()` is `false`) |
|
|
29
|
+
| `invocation` | AgentInvocation | Resolved snapshot used for UI display |
|
|
30
|
+
| `signal` | AbortSignal | Aborting it stops the subagent |
|
|
31
|
+
| `onSpawned` / `onQueued` / `onCompaction` / `onBeforeWorktreeCleanup` | functions | Fire as documented on `SpawnOptions` |
|
|
32
|
+
|
|
33
|
+
**Silently stripped** — set these and nothing happens, with no error and no note. Each deletion is a deliberate guard, and the reasons are worth knowing because they say what the surface refuses to let a caller forge:
|
|
34
|
+
|
|
35
|
+
| Field | Why it is taken away |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `parentAgentId` | Ownership. A forged parent hides your agent under someone else's nested tools |
|
|
38
|
+
| `workflowId` | A forged value would hide an RPC-spawned agent inside someone else's workflow — and take it out of the concurrency pool with it |
|
|
39
|
+
| `depth`, `maxSubagentDepth` | The nesting cap is inherited, not declared |
|
|
40
|
+
| `configCwd` | Config-discovery root; only nested launches may set it |
|
|
41
|
+
| `rootSessionId` | Names a transcript directory, so a forged value is a path-traversal primitive |
|
|
42
|
+
| `resumeSessionFile` | Worse: it names a file to **open and replay** as a conversation. Dispatcher only, and only from a path this extension itself recorded |
|
|
43
|
+
| `reclaim` | Bypasses handle allocation, so a forged value would duplicate a live agent's name and make `@handle` ambiguous |
|
|
44
|
+
| `blocking` | Every spawn through here is detached. A forged `blocking` would charge it to the foreground pool and defer it behind a queue whose gate nobody is holding |
|
|
45
|
+
|
|
46
|
+
**Silently overwritten** — `onToolActivity`, `onTextDelta`, `onTurnEnd`, `onSessionCreated` and `onAssistantUsage` are replaced by the activity tracker's own (`spawnResolved`, `src/index.ts`). Every programmatic spawn passes through one funnel so none can supply half-wired callbacks; a half-wired tracker renders worse than none, which is the bug behind a row that reads `thinking…` for an agent's whole life ([#181](https://github.com/tintinweb/pi-subagents/pull/181)).
|
|
47
|
+
|
|
48
|
+
Four things that are not obvious from the tables:
|
|
49
|
+
|
|
50
|
+
- **Nothing is required at runtime.** `description` is non-optional in TypeScript and never validated. A spawn with no `options` at all is legal and is what "returns agent id on success" in `test/cross-extension-rpc.test.ts` pins.
|
|
51
|
+
- **`bypassQueue` is not stripped.** Its own doc comment scopes it to the scheduler and the `/agents` generator, but a bus caller can set it and skip the `maxConcurrent` check.
|
|
52
|
+
- **`structuredOutput` is documented "set only by the workflow host"** (its field comment on `SpawnOptions`) and is also not stripped.
|
|
53
|
+
- **`signal` and the `on*` callbacks are function values.** They work only because the bus is in-process. A caller that genuinely serializes its payload cannot use them, and they arrive as `undefined` rather than failing.
|
|
54
|
+
|
|
55
|
+
### Names that look right and are not
|
|
56
|
+
|
|
57
|
+
One of these already shipped as a bug in this project's own README example, so it is worth reading the table even if you are sure.
|
|
58
|
+
|
|
59
|
+
| You might write | What it does | What you meant |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| `run_in_background` | Forwarded verbatim and ignored — it is the [`Agent`](../README.md#agent) *tool's* parameter name | `isBackground` |
|
|
62
|
+
| `isolated: true` | Disables extensions, skills and nested tools | `isolation: "worktree"` for a git worktree |
|
|
63
|
+
| `isolation: "worktree"` | Creates a git worktree | `isolated: true` to strip capabilities |
|
|
64
|
+
| `configCwd` | Stripped | `cwd` |
|
|
65
|
+
| `max_turns` / `thinking` / `inherit_context` | Ignored — tool and frontmatter spellings | `maxTurns` / `thinkingLevel` / `inheritContext` |
|
|
66
|
+
| `memory` | Nothing. **There is no such option** | Memory scope comes only from the agent definition's frontmatter |
|
|
67
|
+
|
|
68
|
+
**None of these produce an error.** Option keys are not validated on this path at all — unknown ones are accepted and dropped. (Contrast `agent()` inside a [workflow](workflows.md), which rejects unknown keys by name.)
|
|
69
|
+
|
|
70
|
+
## Errors
|
|
71
|
+
|
|
72
|
+
Every failure reaches the caller as `{ success: false, error }`, where `error` is `err?.message ?? String(err)` (`handleRpc` in `src/cross-extension-rpc.ts`) — so these strings are what you will actually see.
|
|
73
|
+
|
|
74
|
+
| Error | Source |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `No active session` | spawn handler, `src/cross-extension-rpc.ts` — called before the first bound `session_start`, or in a session that excludes pi-subagents |
|
|
77
|
+
| `Model override "<label>" provided but ctx.modelRegistry is unavailable` | spawn handler, `src/cross-extension-rpc.ts` |
|
|
78
|
+
| `Model not found: "<input>".` + available models | `resolveModel`, `src/model-resolver.ts` |
|
|
79
|
+
| `Model not in scope: "<input>".` + allowed models | `checkModelScope`, `src/model-scope.ts` — only with `scopeModels` on, and checked against the *resolved* model |
|
|
80
|
+
| `Unknown or disabled agent type: "<raw>". Available: <list>.` | `resolveSpawnTypeIn`, `src/agent-types.ts` — only under `fallbackSubagent: none` |
|
|
81
|
+
| `No agent type given. Available: <list>.` | `resolveSpawnTypeIn` — same condition |
|
|
82
|
+
| `<reason> The configured fallbackSubagent "<x>" is itself unknown or disabled. Available: <list>.` | `resolveSpawnTypeIn` |
|
|
83
|
+
| `SpawnOptions.cwd must be an absolute path: "<value>"` | `assertValidSpawnCwd`, `src/agent-manager.ts` |
|
|
84
|
+
| `SpawnOptions.cwd does not exist: "<cwd>"` | `assertValidSpawnCwd` |
|
|
85
|
+
| `SpawnOptions.cwd is not a directory: "<cwd>"` | `assertValidSpawnCwd` |
|
|
86
|
+
| ``Cannot run with isolation: "worktree" — not a git repo, no commits yet, or `git worktree add` failed. Initialize git and commit at least once, or omit `isolation`.`` | `AgentManager.startAgent`, surfaced through `awaitStartup`. Every git failure ends here: `createWorktree` (`src/worktree.ts`) catches git's own error and returns nothing |
|
|
87
|
+
| `Agent not found` | stop handler, `src/cross-extension-rpc.ts` |
|
|
88
|
+
| `Agent is owned by another agent or workflow` | stop handler |
|
|
89
|
+
| `Agent is not running` | stop handler. The record exists, so it has already settled |
|
|
90
|
+
| `Agent not found or still running` | consume handler |
|
|
91
|
+
| `Steer message must be a non-empty string` | steer handler — `message` missing, not a string, or whitespace |
|
|
92
|
+
| `Agent not found` / `Agent is owned by another agent or workflow` / `Agent is not running` | steer handler — same lookup, ownership guard and settled check as stop |
|
|
93
|
+
|
|
94
|
+
Three things the table cannot show:
|
|
95
|
+
|
|
96
|
+
- **The failure that is not an error.** With `worktreeIsolation` off project-wide, `isolation: "worktree"` is dropped by the `isWorktreeIsolationEnabled()` check in `AgentManager.startAgent` with no error, no note on the record, and a success envelope on the wire. Your agent runs in the main tree. If you asked for isolation because two agents were going to write the same files, they now collide and nothing told you.
|
|
97
|
+
- **`data` is omitted** when a handler returns nothing, so a successful stop or consume reply is a bare `{ success: true }` and `reply.data.anything` throws.
|
|
98
|
+
- **`requestId` is not validated.** It is interpolated straight into the reply channel, so a caller that omits it gets its reply on the literal channel `subagents:rpc:spawn:reply:undefined` — where every other caller that omitted it is also listening. Send one, and send a unique one.
|
|
99
|
+
|
|
100
|
+
## Ownership
|
|
101
|
+
|
|
102
|
+
`isTopLevelAgent(record)` is `parentAgentId === undefined && workflowId === undefined` (`src/agent-manager.ts`). `subagents:rpc:stop` and `subagents:rpc:steer` enforce it through one shared check (`assertOwned` in `src/cross-extension-rpc.ts`): a nested child or a workflow's agent is owned by something that is *waiting on it*, and aborting or redirecting it out from under that owner turns another extension's request into a failed step. It is defence in depth rather than a live hole — no RPC hands out agent ids, so a caller has no ordinary way to name one it does not own.
|
|
103
|
+
|
|
104
|
+
Two asymmetries to know about, stated as they are:
|
|
105
|
+
|
|
106
|
+
- **Stop takes an id only**: its `getRecord` is the manager's plain lookup. Consume takes an id *or* an `@handle`: its `consumeResult` (both wired in the `session_start` handler, `src/index.ts`) goes through `resolveAgentRef`.
|
|
107
|
+
- **Consume checks `parentAgentId` but not `workflowId`** (`consumeResult`, `src/index.ts`). A workflow-owned agent's result can be marked consumed over the bus even though the same agent cannot be stopped.
|
|
108
|
+
|
|
109
|
+
The same predicate silently scopes the events. **Every lifecycle event is top-level only** — `subagents:started`, `:completed`, `:failed`, `:steered` and `:compacted` all return early for nested and workflow-owned agents (the `isTopLevelAgent` checks in the `AgentManager` callbacks, `src/index.ts`). `subagents:usage` is the one event that is not scoped: it fires for every agent's messages, nested and workflow-owned included, so a ledger sees a workflow's children spend even though it never sees them start or finish.
|
|
110
|
+
|
|
111
|
+
**`subagents:disposed` is the last event of a session.** It is emitted at the end of this extension's `session_shutdown`, after every agent is stopped and every child session has shut down, so no `subagents:usage` can follow it. A listener gets only its synchronous prefix on quit: `pi.events.emit` does not await listeners, and pi calls `process.exit` once the shutdown handlers return. Flush with `writeFileSync`, not `fs.promises`. Fields, and the other events, are in README's [event table](../README.md#events).
|
|
112
|
+
|
|
113
|
+
## The notification race
|
|
114
|
+
|
|
115
|
+
When a background agent finishes, pi-subagents sends the user a completion notification. If you have already shown the model that result yourself, that notification arrives on top of an answer that was already given, and it costs the parent a turn to dismiss. `subagents:rpc:consume` is how you say you have handled it — the bus-side half of what `get_subagent_result` does when it returns a result.
|
|
116
|
+
|
|
117
|
+
**When you send it decides whether it works.** The timeline:
|
|
118
|
+
|
|
119
|
+
1. The agent settles, and the manager's completion callback in `src/index.ts` emits `subagents:completed`.
|
|
120
|
+
2. Next, in the same synchronous callback, it checks `record.resultConsumed` and decides whether to notify at all.
|
|
121
|
+
3. `pi.events` dispatch is synchronous and in-process, so a handler that emits `subagents:rpc:consume` **without awaiting anything** has already set that flag before step 2 evaluates.
|
|
122
|
+
|
|
123
|
+
| When you consume | What happens |
|
|
124
|
+
|---|---|
|
|
125
|
+
| Synchronously, inside your `subagents:completed` handler | The notification is never scheduled. This is the clean path |
|
|
126
|
+
| After an `await`, before the notification is sent | Still suppressed. The notification waits in the outbox (`src/completion-outbox.ts`), and consuming the result cancels it there. It is sent about 200 ms after the completion when the parent is idle (`NOTIFICATION_HOLD_MS`), and at the end of the parent's current tool batch when it is busy |
|
|
127
|
+
| After it was sent | Too late. pi has queued it with `triggerTurn: true`, and it costs the parent a model call |
|
|
128
|
+
|
|
129
|
+
Fire-and-forget is the intended use: the reply carries nothing to act on, and the channel sits outside the `subagents:rpc:ping` version handshake on purpose (the comment on the consume handler, `src/cross-extension-rpc.ts`), so you can send it unconditionally and an older pi-subagents with no handler simply keeps notifying.
|
|
130
|
+
|
|
131
|
+
Consumption is not terminal. An `@handle` steer un-consumes the record (the mention handler, `src/index.ts`) because the agent's reply to that message still needs relaying. A background resume (`AgentManager.resume`) does too, because the record is starting a new run, and so does handing a foreground agent to the background (`AgentManager.sendToBackground`), because its result now has to reach the parent as a notification.
|
|
132
|
+
|
|
133
|
+
One related thing that lives nowhere else: on every top-level settle, pi-subagents writes a session entry — not an event — via `pi.appendEntry("subagents:record", …)` (the manager's completion callback, `src/index.ts`, and the Agent tool's foreground `resume` branch, which settles inline without that callback; the shape is `AgentRecordEntryData` in `src/agent-record-entry.ts`), carrying `id`, `type`, `description`, `status`, `result`, `error`, `startedAt` and `completedAt`, plus `sessionFile`, `handle` and `alias` when the agent has them, and `worktreeLost: true` when its work lived in a worktree that has since been removed. `get_subagent_result` answers from it once the live record is gone, by id or by the entry's handle or alias. `Agent({ resume })` reopens the agent's session from its id, and the entry also serves cross-extension history reconstruction. It is append-only history, not something to react to.
|
|
134
|
+
|
|
135
|
+
## Steering a running agent
|
|
136
|
+
|
|
137
|
+
`subagents:rpc:steer` `{ requestId, agentId, message }` delivers `message` into a running or queued agent's conversation, the bus-side half of the `steer_subagent` tool. It exists for the caller that spawned an agent over the bus and then learns something that agent needs mid-run — a second CI failure on the head it is already fixing, a review that landed while it works — and would otherwise have to choose between spawning a duplicate agent for the same work and dropping the fact.
|
|
138
|
+
|
|
139
|
+
It takes an id only, enforces the same top-level ownership guard as stop, and answers `Agent is not running` for a record that has settled. A message queued before the agent's session exists is flushed when the session is created, exactly as the tool's steer is. A successful steer emits `subagents:steered` `{ id, message }`, as every steer path does. Like `consume`, it is outside the `ping` version handshake: an older build has no handler and the request times out on your side, so give it a timeout and treat expiry as "not available here".
|
|
140
|
+
|
|
141
|
+
## The manager registry
|
|
142
|
+
|
|
143
|
+
`globalThis[Symbol.for("pi-subagents:manager")]` (`MANAGER_KEY` and `registryEntry`, `src/index.ts`) is a second integration surface — the standard Node cross-package singleton pattern, no bus involved:
|
|
144
|
+
|
|
145
|
+
| Member | Signature | Notes |
|
|
146
|
+
|---|---|---|
|
|
147
|
+
| `waitForAll()` | `() => Promise<void>` | Resolves when nothing is running. **All** agents, including ones you did not spawn — a shutdown barrier, not a join |
|
|
148
|
+
| `hasRunning()` | `() => boolean` | |
|
|
149
|
+
| `spawn(pi, ctx, type, prompt, options)` | `=> string` | **Is** `spawnTopLevel`, so the strip list above applies identically |
|
|
150
|
+
| `getRecord(id)` | `=> AgentRecord \| undefined` | Filtered through `isTopLevelAgent`, so someone else's child comes back `undefined` rather than leaking |
|
|
151
|
+
|
|
152
|
+
The slot is claimed by the first activation only; subagent sessions re-activate this extension in the same process, and unconditionally overwriting would point the registry at a short-lived child manager whose shutdown would then delete the root session's entry ([#128](https://github.com/tintinweb/pi-subagents/pull/128)). Child activations leave it alone, and shutdown releases it only if this activation claimed it (`ownsManagerRegistry`, checked in the `session_shutdown` handler).
|
|
153
|
+
|
|
154
|
+
Prefer the bus. The registry has no reply envelope, no version, and no availability event — `globalThis[Symbol.for("pi-subagents:manager")] === undefined` is the only probe you get, and it is also `undefined` in a session that filtered pi-subagents out. Reach for it for the two things the bus has no verb for — *is anything still running*, and *give me a settled record back* — or for a headless host that wants to block on `waitForAll()` before exiting.
|
|
155
|
+
|
|
156
|
+
## Protocol versions
|
|
157
|
+
|
|
158
|
+
`subagents:rpc:ping` replies `{ version: PROTOCOL_VERSION }`, currently `2` (`src/cross-extension-rpc.ts`). The constant was introduced already equal to `2` in 0.5.0; "v1" is a retroactive name for the pre-envelope contract, where spawn replied with a bare `{ id }` or `{ error }`, stop replied `{ success: boolean }` with no message, and each handler caught its own errors.
|
|
159
|
+
|
|
160
|
+
Everything added since shipped **without a bump**, because all of it is additive: stop's ownership refusal, string-`model` resolution ([#59](https://github.com/tintinweb/pi-subagents/pull/59)/[#60](https://github.com/tintinweb/pi-subagents/issues/60)), `scopeModels` enforcement ([#240](https://github.com/tintinweb/pi-subagents/issues/240)), and the whole `consume` and `steer` channels.
|
|
161
|
+
|
|
162
|
+
> A `ping` that answers `2` does not tell you whether `consume` or `steer` exists, whether model scope is enforced, or whether stop checks ownership.
|
|
163
|
+
|
|
164
|
+
So: send `consume` unconditionally and ignore the outcome — an older build has no handler and simply keeps notifying, which is exactly why it was left outside the handshake. And treat every error envelope as authoritative rather than trying to predict which checks are in force.
|
|
165
|
+
|
|
166
|
+
## Availability
|
|
167
|
+
|
|
168
|
+
`subagents:ready` is the discovery signal, and both the RPC handlers and the event itself are wired on the first bound `session_start` (its handler in `src/index.ts`) — deliberately not at factory time. pi runs every extension factory *before* applying an agent's `extensions:` filter and only delivers lifecycle events to the survivors, so a factory-time broadcast made a filtered-out session advertise a spawn service it could never provide: `ping` succeeded and every `spawn` answered `No active session` ([#142](https://github.com/tintinweb/pi-subagents/issues/142)).
|
|
169
|
+
|
|
170
|
+
The consequence is worth stating plainly: **a session that excludes pi-subagents is indistinguishable from pi-subagents not being installed.** It emits no `subagents:ready` and answers nothing. Give discovery a timeout and treat expiry as "not available here" rather than waiting indefinitely. The payload is `{}` — read nothing off it. Handlers are torn down and `rpcHandle` reset in the `session_shutdown` handler, so a later `session_start` re-registers and re-emits.
|
|
171
|
+
|
|
172
|
+
One more trap on the way in: an RPC-spawned agent emits **no `subagents:created`**. The only three emit sites, all in `src/index.ts`, are the `Agent` tool's `run_in_background` branch, a detached resume (`startBackgroundResume`), and a foreground `Agent` call handed to the background with ctrl+b or the viewer's `b` (`sendForegroundToBackground`) — that one fires after the agent's `subagents:started`, as resume does. Your first event for your own agent is `subagents:started` (the manager's start callback), so key your bookkeeping off the id that `spawn` handed you, not off `subagents:created`.
|
|
173
|
+
|
|
174
|
+
## What the tests pin
|
|
175
|
+
|
|
176
|
+
This document has no test of its own, so it is worth knowing which claims are actually held in place:
|
|
177
|
+
|
|
178
|
+
| Test | Level | Pins |
|
|
179
|
+
|---|---|---|
|
|
180
|
+
| `test/cross-extension-rpc.test.ts` | Mocked `SpawnCapable` | Envelope shape, per-channel error strings (including steer's), model resolution and scope enforcement |
|
|
181
|
+
| `test/rpc-lifecycle-gating.test.ts` | Real extension factory | Nothing wired at factory time, everything once at `session_start`, and live widget activity for RPC spawns ([#142](https://github.com/tintinweb/pi-subagents/issues/142)/[#181](https://github.com/tintinweb/pi-subagents/pull/181)) |
|
|
182
|
+
| `test/rpc-result-consumption.test.ts` | Real delivery path | The notification firing, and not firing, around `consume` |
|
|
183
|
+
|
|
184
|
+
Not pinned anywhere, so treat them as descriptions rather than contracts: the `SpawnOptions.cwd` error strings, `subagents:ready`'s `{}` payload, consume's handle resolution, and its missing `workflowId` check.
|
|
185
|
+
|
|
186
|
+
## Reference implementation
|
|
187
|
+
|
|
188
|
+
[**`tintinweb/pi-tasks`**](https://github.com/tintinweb/pi-tasks) is the working integration and the one this surface was shaped by. Its `TaskExecute` drives `subagents:rpc:spawn` — including the serialized `"provider/modelId"` string form that the boundary now resolves — and its `TaskOutput` drives `subagents:rpc:consume`, which exists because pi-tasks joins an agent on `subagents:completed` and reports the result itself ([pi-tasks#62](https://github.com/tintinweb/pi-tasks/issues/62)).
|
|
189
|
+
|
|
190
|
+
Read it for the shape of the whole loop: waiting on `subagents:ready`, keeping an id-keyed map of outstanding spawns, resolving each from the `subagents:completed` / `subagents:failed` handler, and consuming the result in the same synchronous handler that reports it.
|
|
191
|
+
|
|
192
|
+
If what you want is many coordinated agents rather than one, hand a script to [`SubagentWorkflow`](workflows.md) instead of fanning out over `subagents:rpc:spawn` — and note that a workflow's agents are not yours: they emit no lifecycle events — only `subagents:usage` — and `subagents:rpc:stop` refuses them.
|