@herbertgao/pi-extensions 2026.9.8 → 2026.9.10

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.
Files changed (144) hide show
  1. package/README.md +8 -4
  2. package/THIRD_PARTY_NOTICES.md +52 -0
  3. package/node_modules/@herbertgao/pi-bark/package.json +2 -2
  4. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -1
  5. package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -1
  6. package/node_modules/@herbertgao/pi-cc-extensions/package.json +4 -4
  7. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +6 -0
  8. package/node_modules/@herbertgao/pi-subagents/README.md +427 -120
  9. package/node_modules/@herbertgao/pi-subagents/docs/rpc.md +184 -0
  10. package/node_modules/@herbertgao/pi-subagents/docs/workflows.md +466 -0
  11. package/node_modules/@herbertgao/pi-subagents/examples/agent-tool-description.md +6 -6
  12. package/node_modules/@herbertgao/pi-subagents/examples/workflows/compose.js +52 -0
  13. package/node_modules/@herbertgao/pi-subagents/examples/workflows/fan-out-audit.js +56 -0
  14. package/node_modules/@herbertgao/pi-subagents/examples/workflows/gated-fix.js +60 -0
  15. package/node_modules/@herbertgao/pi-subagents/examples/workflows/lib/count-child.js +30 -0
  16. package/node_modules/@herbertgao/pi-subagents/examples/workflows/review-panel.js +68 -0
  17. package/node_modules/@herbertgao/pi-subagents/examples/workflows/structured-findings.js +81 -0
  18. package/node_modules/@herbertgao/pi-subagents/package.json +11 -9
  19. package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +52 -12
  20. package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +837 -146
  21. package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +213 -39
  22. package/node_modules/@herbertgao/pi-subagents/src/cross-extension-rpc.ts +73 -14
  23. package/node_modules/@herbertgao/pi-subagents/src/custom-agents.ts +101 -47
  24. package/node_modules/@herbertgao/pi-subagents/src/index.ts +2249 -914
  25. package/node_modules/@herbertgao/pi-subagents/src/invocation-config.ts +13 -0
  26. package/node_modules/@herbertgao/pi-subagents/src/mention-clone.ts +215 -0
  27. package/node_modules/@herbertgao/pi-subagents/src/mention.ts +147 -0
  28. package/node_modules/@herbertgao/pi-subagents/src/model-resolver.ts +9 -1
  29. package/node_modules/@herbertgao/pi-subagents/src/nested-tools.ts +40 -26
  30. package/node_modules/@herbertgao/pi-subagents/src/output-file.ts +18 -8
  31. package/node_modules/@herbertgao/pi-subagents/src/prompts.ts +46 -9
  32. package/node_modules/@herbertgao/pi-subagents/src/schedule.ts +21 -16
  33. package/node_modules/@herbertgao/pi-subagents/src/settings.ts +137 -7
  34. package/node_modules/@herbertgao/pi-subagents/src/structured-output.ts +136 -0
  35. package/node_modules/@herbertgao/pi-subagents/src/types.ts +126 -8
  36. package/node_modules/@herbertgao/pi-subagents/src/ui/agent-mention.ts +274 -0
  37. package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +20 -5
  38. package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +10 -4
  39. package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +167 -22
  40. package/node_modules/@herbertgao/pi-subagents/src/ui/workflow-card.ts +555 -0
  41. package/node_modules/@herbertgao/pi-subagents/src/ui/workflow-dialog.ts +1304 -0
  42. package/node_modules/@herbertgao/pi-subagents/src/ui/workflow-menu.ts +226 -0
  43. package/node_modules/@herbertgao/pi-subagents/src/workflow/collisions.ts +122 -0
  44. package/node_modules/@herbertgao/pi-subagents/src/workflow/entry.ts +47 -0
  45. package/node_modules/@herbertgao/pi-subagents/src/workflow/host.ts +463 -0
  46. package/node_modules/@herbertgao/pi-subagents/src/workflow/journal.ts +164 -0
  47. package/node_modules/@herbertgao/pi-subagents/src/workflow/json-schema.ts +142 -0
  48. package/node_modules/@herbertgao/pi-subagents/src/workflow/meta.ts +401 -0
  49. package/node_modules/@herbertgao/pi-subagents/src/workflow/progress.ts +622 -0
  50. package/node_modules/@herbertgao/pi-subagents/src/workflow/runtime.ts +1399 -0
  51. package/node_modules/@herbertgao/pi-subagents/src/workflow/saved.ts +230 -0
  52. package/node_modules/@herbertgao/pi-subagents/src/workflow/task.ts +333 -0
  53. package/node_modules/@herbertgao/pi-subagents/src/workflow/tool-description.ts +200 -0
  54. package/node_modules/@herbertgao/pi-subagents/src/workflow/worker-source.ts +781 -0
  55. package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +97 -95
  56. package/node_modules/@herbertgao/pi-subagents/src/xml.ts +13 -0
  57. package/node_modules/@herbertgao/resume-from/package.json +2 -2
  58. package/node_modules/@narumitw/pi-btw/README.md +6 -1
  59. package/node_modules/@narumitw/pi-btw/dist/index.ts +209 -23
  60. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +4 -4
  61. package/node_modules/@narumitw/pi-btw/package.json +1 -1
  62. package/node_modules/@narumitw/pi-btw/src/btw.ts +1 -12
  63. package/node_modules/@narumitw/pi-btw/src/menu.ts +244 -15
  64. package/node_modules/@narumitw/pi-btw/src/settings.ts +5 -0
  65. package/node_modules/pi-jev-auto-mode/CHANGELOG.md +110 -0
  66. package/node_modules/pi-jev-auto-mode/LICENSE +21 -0
  67. package/node_modules/pi-jev-auto-mode/README.md +285 -0
  68. package/node_modules/pi-jev-auto-mode/SECURITY.md +32 -0
  69. package/node_modules/pi-jev-auto-mode/docs/calibration.md +154 -0
  70. package/node_modules/pi-jev-auto-mode/docs/design.md +164 -0
  71. package/node_modules/pi-jev-auto-mode/docs/security.md +124 -0
  72. package/node_modules/pi-jev-auto-mode/index.ts +1 -0
  73. package/node_modules/pi-jev-auto-mode/package.json +72 -0
  74. package/node_modules/pi-jev-auto-mode/src/call.ts +180 -0
  75. package/node_modules/pi-jev-auto-mode/src/decide.ts +86 -0
  76. package/node_modules/pi-jev-auto-mode/src/extension.ts +862 -0
  77. package/node_modules/pi-jev-auto-mode/src/intent.ts +71 -0
  78. package/node_modules/pi-jev-auto-mode/src/jev/availability.ts +53 -0
  79. package/node_modules/pi-jev-auto-mode/src/jev/criteria.ts +19 -0
  80. package/node_modules/pi-jev-auto-mode/src/jev/decide.ts +187 -0
  81. package/node_modules/pi-jev-auto-mode/src/jev/engine.ts +165 -0
  82. package/node_modules/pi-jev-auto-mode/src/jev/index.ts +20 -0
  83. package/node_modules/pi-jev-auto-mode/src/jev/questions.ts +273 -0
  84. package/node_modules/pi-jev-auto-mode/src/jev/response.ts +64 -0
  85. package/node_modules/pi-jev-auto-mode/src/jev/state.ts +20 -0
  86. package/node_modules/pi-jev-auto-mode/src/jev/transport.ts +117 -0
  87. package/node_modules/pi-jev-auto-mode/src/jev/types.ts +46 -0
  88. package/node_modules/pi-jev-auto-mode/src/policy.ts +558 -0
  89. package/node_modules/pi-jev-auto-mode/src/records.ts +118 -0
  90. package/node_modules/pi-jev-auto-mode/src/settings.ts +327 -0
  91. package/node_modules/pi-jev-auto-mode/src/ui.ts +233 -0
  92. package/node_modules/pi-lens/CHANGELOG.md +45 -0
  93. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +21 -9
  94. package/node_modules/pi-lens/dist/clients/cascade-format.js +24 -0
  95. package/node_modules/pi-lens/dist/clients/extension-log.js +48 -1
  96. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +11 -12
  97. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +3 -0
  98. package/node_modules/pi-lens/dist/clients/lsp/index.js +41 -7
  99. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +9 -2
  100. package/node_modules/pi-lens/dist/clients/path-utils.js +51 -9
  101. package/node_modules/pi-lens/dist/clients/persistent-reverify.js +271 -0
  102. package/node_modules/pi-lens/dist/clients/pipeline.js +15 -1
  103. package/node_modules/pi-lens/dist/clients/runtime-turn.js +124 -7
  104. package/node_modules/pi-lens/dist/clients/skills-resolver.js +38 -2
  105. package/node_modules/pi-lens/dist/clients/widget-state.js +152 -6
  106. package/node_modules/pi-lens/dist/index.js +729 -344
  107. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +14 -2
  108. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +84 -17
  109. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +32 -5
  110. package/node_modules/pi-lens/package.json +1 -1
  111. package/node_modules/pi-typesafe/LICENSE +21 -0
  112. package/node_modules/pi-typesafe/README.md +153 -0
  113. package/node_modules/pi-typesafe/dist/ask.d.ts +32 -0
  114. package/node_modules/pi-typesafe/dist/ask.js +25 -0
  115. package/node_modules/pi-typesafe/dist/auth.d.ts +56 -0
  116. package/node_modules/pi-typesafe/dist/auth.js +114 -0
  117. package/node_modules/pi-typesafe/dist/batch.d.ts +74 -0
  118. package/node_modules/pi-typesafe/dist/batch.js +116 -0
  119. package/node_modules/pi-typesafe/dist/calibrate.d.ts +106 -0
  120. package/node_modules/pi-typesafe/dist/calibrate.js +157 -0
  121. package/node_modules/pi-typesafe/dist/client.d.ts +69 -0
  122. package/node_modules/pi-typesafe/dist/client.js +188 -0
  123. package/node_modules/pi-typesafe/dist/credentials.d.ts +46 -0
  124. package/node_modules/pi-typesafe/dist/credentials.js +112 -0
  125. package/node_modules/pi-typesafe/dist/errors.d.ts +9 -0
  126. package/node_modules/pi-typesafe/dist/errors.js +31 -0
  127. package/node_modules/pi-typesafe/dist/extension.d.ts +3 -0
  128. package/node_modules/pi-typesafe/dist/extension.js +222 -0
  129. package/node_modules/pi-typesafe/dist/index.d.ts +18 -0
  130. package/node_modules/pi-typesafe/dist/index.js +9 -0
  131. package/node_modules/pi-typesafe/dist/key-prompt.d.ts +3 -0
  132. package/node_modules/pi-typesafe/dist/key-prompt.js +43 -0
  133. package/node_modules/pi-typesafe/dist/login.d.ts +27 -0
  134. package/node_modules/pi-typesafe/dist/login.js +38 -0
  135. package/node_modules/pi-typesafe/dist/schema.d.ts +43 -0
  136. package/node_modules/pi-typesafe/dist/schema.js +152 -0
  137. package/node_modules/pi-typesafe/dist/ui.d.ts +3 -0
  138. package/node_modules/pi-typesafe/dist/ui.js +3 -0
  139. package/node_modules/pi-typesafe/dist/usage.d.ts +73 -0
  140. package/node_modules/pi-typesafe/dist/usage.js +182 -0
  141. package/node_modules/pi-typesafe/examples/decision-extension.ts +41 -0
  142. package/node_modules/pi-typesafe/extensions/index.js +2 -0
  143. package/node_modules/pi-typesafe/package.json +89 -0
  144. package/package.json +16 -9
@@ -0,0 +1,184 @@
1
+ # Driving subagents from another extension
2
+
3
+ Another pi extension can spawn a subagent, listen for subagent completion, read the result and stop the run — all over the `pi.events` bus, without importing this package directly. Four request/reply channels (`subagents:rpc:ping`, `subagents:rpc:spawn`, `subagents:rpc:stop`, `subagents:rpc:consume`), eleven lifecycle events, 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` (`src/index.ts:698-721`), which deletes internal-only fields first, and then `spawnResolved` (`src/index.ts:666-696`) overwrites the activity-tracker callbacks with its own. The full interface is `SpawnOptions` at `src/agent-manager.ts:177-313`; 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. **Not** a git worktree — see the trap table below |
22
+ | `inheritContext` | boolean | Fork the parent conversation into the child |
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 |
28
+ | `cwd` | absolute path | The agent's tools operate here; `.pi` config still loads from the parent session's project |
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 (`src/index.ts:693`). 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 `test/cross-extension-rpc.test.ts:81-94` 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"** (`src/agent-manager.ts:239-242`) 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)` (`src/cross-extension-rpc.ts:87`) — so these strings are what you will actually see.
73
+
74
+ | Error | Source |
75
+ | ------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
76
+ | `No active session` | `src/cross-extension-rpc.ts:107` — 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` | `src/cross-extension-rpc.ts:126` |
78
+ | `Model not found: "<input>".` + available models | `src/model-resolver.ts:117` |
79
+ | `Model not in scope: "<input>".` + allowed models | `src/model-scope.ts:62` — only with `scopeModels` on, and checked against the _resolved_ model |
80
+ | `Unknown or disabled agent type: "<raw>". Available: <list>.` | `src/agent-types.ts:187` — only under `fallbackSubagent: none` |
81
+ | `No agent type given. Available: <list>.` | `src/agent-types.ts:187-194` — same condition |
82
+ | `<reason> The configured fallbackSubagent "<x>" is itself unknown or disabled. Available: <list>.` | `src/agent-types.ts:205-207` |
83
+ | `SpawnOptions.cwd must be an absolute path: "<value>"` | `src/agent-manager.ts:93` |
84
+ | `SpawnOptions.cwd does not exist: "<cwd>"` | `src/agent-manager.ts:99` |
85
+ | `SpawnOptions.cwd is not a directory: "<cwd>"` | `src/agent-manager.ts:102` |
86
+ | `Cannot run with isolation: "worktree" — not a git repo, no commits yet, or 'git worktree add' failed.` | `src/agent-manager.ts:724-727`, surfaced through `awaitStartup` |
87
+ | `Worktree cleanup failed: <reason>` + retained recovery path | `src/agent-manager.ts:76-81`; the agent becomes an error instead of losing the worktree |
88
+ | git plumbing failures | `src/worktree.ts:78` |
89
+ | `Agent not found` | stop — `src/cross-extension-rpc.ts:170` |
90
+ | `Agent is owned by another agent or workflow` | stop — `:178` |
91
+ | `Agent is not running` | stop — `:182`. The record exists, so it has already settled |
92
+ | `Agent not found or still running` | consume — `:193` |
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 at `src/agent-manager.ts:720` 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:130-134`). `subagents:rpc:stop` enforces it (`src/cross-extension-rpc.ts:178`): a nested child or a workflow's agent is owned by something that is _waiting on it_, and aborting it out from under that owner turns another extension's stop 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** (`src/index.ts:806`). Consume takes an id _or_ an `@handle`, through `resolveAgentRef` (`src/index.ts:816` → `:731-736`).
107
+ - **Consume checks `parentAgentId` but not `workflowId`** (`src/index.ts:816`). 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` and `:compacted` all return early for nested and workflow-owned agents (`src/index.ts:573`, `:615`, `:631`). A workflow's children are invisible on the bus: you will see the workflow's own agents come and go without a single event.
110
+
111
+ ## The notification race
112
+
113
+ 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.
114
+
115
+ **When you send it decides whether it works.** The timeline:
116
+
117
+ 1. The agent settles and `subagents:completed` is emitted — `src/index.ts:581`.
118
+ 2. Eleven lines later, at `src/index.ts:592`, the code checks `record.resultConsumed` and decides whether to notify at all.
119
+ 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.
120
+
121
+ | When you consume | What happens |
122
+ | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
123
+ | Synchronously, inside your `subagents:completed` handler | The notification is never scheduled. This is the clean path |
124
+ | After an `await`, within 200 ms | Still suppressed. The nudge is held for `NUDGE_HOLD_MS` (`src/index.ts:451`), `consume` cancels the pending timer (`:819`), and there is a re-check at send time (`:474`) |
125
+ | After 200 ms | Too late. The follow-up has fired with `triggerTurn: true` and cost the parent a turn |
126
+
127
+ 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 (`src/cross-extension-rpc.ts:190`), so you can send it unconditionally and an older pi-subagents with no handler simply keeps notifying.
128
+
129
+ Consumption is not terminal. An `@handle` steer un-consumes the record (`src/index.ts:920`) because the agent's reply to that message still needs relaying, and so does a background resume (`src/agent-manager.ts:1148`) because the record is starting a new run.
130
+
131
+ 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", …)` (`src/index.ts:585`), carrying `id`, `type`, `description`, `status`, `result`, `error`, `startedAt` and `completedAt`. It exists for cross-extension history reconstruction. It is append-only history, not something to react to.
132
+
133
+ ## The manager registry
134
+
135
+ `globalThis[Symbol.for("pi-subagents:manager")]` (`src/index.ts:649-659`) is a second integration surface — the standard Node cross-package singleton pattern, no bus involved:
136
+
137
+ | Member | Signature | Notes |
138
+ | --------------------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------- |
139
+ | `waitForAll()` | `() => Promise<void>` | Resolves when nothing is running. **All** agents, including ones you did not spawn — a shutdown barrier, not a join |
140
+ | `hasRunning()` | `() => boolean` | |
141
+ | `spawn(pi, ctx, type, prompt, options)` | `=> string` | **Is** `spawnTopLevel`, so the strip list above applies identically |
142
+ | `getRecord(id)` | `=> AgentRecord \| undefined` | Filtered through `isTopLevelAgent`, so someone else's child comes back `undefined` rather than leaking |
143
+
144
+ 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 (`src/index.ts:747-750`, `:1105-1107`).
145
+
146
+ 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.
147
+
148
+ ## Protocol versions
149
+
150
+ `subagents:rpc:ping` replies `{ version: PROTOCOL_VERSION }`, currently `2` (`src/cross-extension-rpc.ts:33`). 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.
151
+
152
+ 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` channel.
153
+
154
+ > A `ping` that answers `2` does not tell you whether `consume` exists, whether model scope is enforced, or whether stop checks ownership.
155
+
156
+ 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.
157
+
158
+ ## Availability
159
+
160
+ `subagents:ready` is the discovery signal, and both the RPC handlers and the event itself are wired on the first bound `session_start` (`src/index.ts:789`, `:799`, `:827`) — 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)).
161
+
162
+ 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 the flag reset on `session_shutdown` (`src/index.ts:1100-1103`), so a later `session_start` re-registers and re-emits.
163
+
164
+ One more trap on the way in: an RPC-spawned agent emits **no `subagents:created`**. The only two emit sites are the `Agent` tool's background branch (`src/index.ts:2104`) and detached resume (`:1350`). Your first event for your own agent is `subagents:started` (`:625`), so key your bookkeeping off the id that `spawn` handed you, not off `subagents:created`.
165
+
166
+ ## What the tests pin
167
+
168
+ This document has no test of its own, so it is worth knowing which claims are actually held in place:
169
+
170
+ | Test | Level | Pins |
171
+ | ------------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
172
+ | `test/cross-extension-rpc.test.ts` | Mocked `SpawnCapable` | Envelope shape, per-channel error strings, model resolution and scope enforcement |
173
+ | `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)) |
174
+ | `test/rpc-result-consumption.test.ts` | Real delivery path | The notification firing, and not firing, around `consume` |
175
+
176
+ 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.
177
+
178
+ ## Reference implementation
179
+
180
+ [**`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)).
181
+
182
+ 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.
183
+
184
+ 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, and `subagents:rpc:stop` refuses them.