@jopqior/pi-subagents 1.0.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.
Files changed (110) hide show
  1. package/CHANGELOG.md +2705 -0
  2. package/LICENSE +21 -0
  3. package/README.md +503 -0
  4. package/dist/public.d.ts +331 -0
  5. package/dist/settings.d.ts +82 -0
  6. package/docs/architecture/architecture.md +1566 -0
  7. package/docs/architecture/client-server-opportunities.md +127 -0
  8. package/docs/architecture/history/phase-1-api-boundary.md +8 -0
  9. package/docs/architecture/history/phase-10-structural-decomposition.md +141 -0
  10. package/docs/architecture/history/phase-11-closure-to-class.md +100 -0
  11. package/docs/architecture/history/phase-12-complexity-test-fixtures.md +55 -0
  12. package/docs/architecture/history/phase-13-remaining-smells.md +88 -0
  13. package/docs/architecture/history/phase-14-strip-policy.md +49 -0
  14. package/docs/architecture/history/phase-15-domain-model-evolution.md +73 -0
  15. package/docs/architecture/history/phase-16-invert-dependencies.md +144 -0
  16. package/docs/architecture/history/phase-17-core-consolidation.md +214 -0
  17. package/docs/architecture/history/phase-18-reconsider-ui.md +166 -0
  18. package/docs/architecture/history/phase-19-implement-ui-decisions.md +282 -0
  19. package/docs/architecture/history/phase-2-remove-scheduling.md +9 -0
  20. package/docs/architecture/history/phase-20-result-delivery.md +245 -0
  21. package/docs/architecture/history/phase-21-classification-model-boundary.md +107 -0
  22. package/docs/architecture/history/phase-3-remove-rpc-groupjoin.md +11 -0
  23. package/docs/architecture/history/phase-4-implement-service.md +8 -0
  24. package/docs/architecture/history/phase-5-decompose-index.md +42 -0
  25. package/docs/architecture/history/phase-7-encapsulation.md +173 -0
  26. package/docs/architecture/history/phase-8-testability.md +103 -0
  27. package/docs/architecture/history/phase-9-observation-ctx.md +122 -0
  28. package/docs/comparison-with-upstream.md +77 -0
  29. package/docs/configuration.md +364 -0
  30. package/docs/decisions/0001-deferred-patches.md +80 -0
  31. package/docs/decisions/0002-extensions-on-a-minimal-core.md +125 -0
  32. package/docs/decisions/0003-publish-bundled-type-declarations.md +71 -0
  33. package/docs/decisions/0004-reconsider-ui-direction.md +279 -0
  34. package/docs/decisions/0005-subagent-record-admission-policy.md +106 -0
  35. package/docs/decisions/0006-inherited-prompt-is-identity-only.md +104 -0
  36. package/docs/decisions/0007-transcript-viewer-is-not-an-overlay.md +228 -0
  37. package/docs/decisions/0008-inherited-region-is-shared-parts.md +81 -0
  38. package/docs/decisions/0009-portable-inheritance-is-provider-scoped.md +116 -0
  39. package/package.json +91 -0
  40. package/src/config/agent-types.ts +135 -0
  41. package/src/config/custom-agents.ts +151 -0
  42. package/src/config/default-agents.ts +121 -0
  43. package/src/config/invocation-config.ts +167 -0
  44. package/src/config/thinking-level.ts +58 -0
  45. package/src/debug.ts +14 -0
  46. package/src/handlers/index.ts +3 -0
  47. package/src/handlers/interrupt.ts +58 -0
  48. package/src/handlers/lifecycle.ts +71 -0
  49. package/src/handlers/widget-events.ts +49 -0
  50. package/src/index.ts +292 -0
  51. package/src/layered-settings.ts +105 -0
  52. package/src/lifecycle/child-lifecycle.ts +115 -0
  53. package/src/lifecycle/child-shutdown.ts +105 -0
  54. package/src/lifecycle/concurrency-limiter.ts +55 -0
  55. package/src/lifecycle/create-subagent-session.ts +335 -0
  56. package/src/lifecycle/parent-snapshot.ts +119 -0
  57. package/src/lifecycle/run-listeners.ts +37 -0
  58. package/src/lifecycle/selection-scope.ts +116 -0
  59. package/src/lifecycle/spawn-selection.ts +259 -0
  60. package/src/lifecycle/subagent-manager.ts +546 -0
  61. package/src/lifecycle/subagent-session.ts +347 -0
  62. package/src/lifecycle/subagent-state.ts +404 -0
  63. package/src/lifecycle/subagent.ts +885 -0
  64. package/src/lifecycle/turn-limits.ts +13 -0
  65. package/src/lifecycle/usage.ts +60 -0
  66. package/src/lifecycle/workspace-bracket.ts +76 -0
  67. package/src/lifecycle/workspace.ts +46 -0
  68. package/src/observation/composite-subagent-observer.ts +74 -0
  69. package/src/observation/notification.ts +430 -0
  70. package/src/observation/outcome-delivery.ts +239 -0
  71. package/src/observation/record-observer.ts +78 -0
  72. package/src/observation/renderer.ts +161 -0
  73. package/src/observation/subagent-events-observer.ts +148 -0
  74. package/src/runtime.ts +137 -0
  75. package/src/service/service-adapter.ts +201 -0
  76. package/src/service/service.ts +246 -0
  77. package/src/session/ask-parent-tool.ts +69 -0
  78. package/src/session/content-items.ts +53 -0
  79. package/src/session/context.ts +80 -0
  80. package/src/session/conversation.ts +49 -0
  81. package/src/session/env.ts +40 -0
  82. package/src/session/model-resolver.ts +126 -0
  83. package/src/session/notify-parent-tool.ts +83 -0
  84. package/src/session/package-exclusions.ts +75 -0
  85. package/src/session/prompts.ts +231 -0
  86. package/src/session/provider-inheritance.ts +56 -0
  87. package/src/session/selection-catalogue.ts +143 -0
  88. package/src/session/session-config.ts +202 -0
  89. package/src/session/session-dir.ts +38 -0
  90. package/src/settings.ts +447 -0
  91. package/src/tools/agent-tool.ts +305 -0
  92. package/src/tools/background-spawner.ts +83 -0
  93. package/src/tools/foreground-runner.ts +159 -0
  94. package/src/tools/get-result-renderer.ts +119 -0
  95. package/src/tools/get-result-report.ts +84 -0
  96. package/src/tools/get-result-tool.ts +192 -0
  97. package/src/tools/helpers.ts +118 -0
  98. package/src/tools/result-renderer.ts +153 -0
  99. package/src/tools/spawn-config.ts +192 -0
  100. package/src/tools/steer-tool.ts +109 -0
  101. package/src/types.ts +143 -0
  102. package/src/ui/agent-widget.ts +333 -0
  103. package/src/ui/bounded-lines.ts +45 -0
  104. package/src/ui/display.ts +180 -0
  105. package/src/ui/glyphs.ts +62 -0
  106. package/src/ui/session-navigation.ts +150 -0
  107. package/src/ui/session-navigator.ts +255 -0
  108. package/src/ui/subagents-settings.ts +179 -0
  109. package/src/ui/transcript-content.ts +374 -0
  110. package/src/ui/widget-renderer.ts +301 -0
@@ -0,0 +1,125 @@
1
+ ---
2
+ status: accepted
3
+ date: 2026-05-29
4
+ ---
5
+
6
+ # 0002 — Workspaces and permissions are extensions on a minimal core
7
+
8
+ ## Status
9
+
10
+ Accepted.
11
+ Supersedes the "agent collaborator architecture" framing of Phase 16 (an abandoned exploration) and the work shipped under it: issue #256 (`WorktreeIsolation` as an `Agent` collaborator) and issue #257 (`ChildSessionFactory` extraction, parked at planning).
12
+ Reclaims Phase 16's original intent — "invert dependencies" — and extends it to evict worktree isolation from the core.
13
+
14
+ ## Context
15
+
16
+ The core question that triggered this decision: a single-method `ChildSessionFactory` with a `create(cwd?)` method (planned for #257) looked like it wanted to be a function, and the `cwd` parameter was late-bound.
17
+ Pulling that thread exposed progressively more rudimentary issues.
18
+
19
+ 1. `cwd` is late-bound because `WorktreeIsolation.setup()` is called lazily inside `Agent.run()`, after construction — a two-phase `construct-then-setup()` that violates design principle 8 ("Construct complete").
20
+ 2. The worktree is *ready* only at dequeue (a concurrency slot is held and `git worktree add` has run).
21
+ "Construct when ready" therefore means constructing the worktree at run-start, not at spawn — which dissolves the lazy `setup()` and makes `cwd` knowable at construction.
22
+ 3. The worktree and the child session share one lifespan: both are born at run-start and torn down at completion (the worktree's cleanup saves a branch; the session is disposed).
23
+ Resources with one lifetime are one resource, not sibling collaborators that `Agent` must sequence.
24
+ The `create(cwd?)` parameter only existed because we split one run-scoped resource (the worktree) out and made `Agent` relay its output back in.
25
+ 4. Worktrees are not intrinsic to what makes subagents useful.
26
+ The maintainer never uses them (WIP-of-1, trunk-based, CI/CD).
27
+ Git worktree isolation is one *strategy* for answering "where does this child run, and what brackets the run?"
28
+ — a container, a throwaway tmpdir, or a remote sandbox are others.
29
+ The core needs only *a working directory and a disposal hook*; the default (the parent's cwd, no setup/teardown) is always correct.
30
+ 5. This mirrors Phase 14, which evicted tool/extension *policy* (`disallowed_tools`, `extensions` filtering) to `@gotgenes/pi-permission-system`.
31
+ Worktrees are *environment* policy; they belong outside the core for the same reason.
32
+
33
+ Permissions and workspaces are orthogonal concerns that must compose as independent extensions on the core, never knowing about each other.
34
+
35
+ ## Decision
36
+
37
+ pi-subagents is a minimal orchestrator: it spawns a child session derived from the parent, runs the turn loop, tracks and streams and collects the result, gates concurrency, supports resume, and **publishes its lifecycle**.
38
+ Everything else attaches through exactly two extension surfaces, distinguished by the direction of information flow.
39
+
40
+ ### Two extension surfaces
41
+
42
+ 1. **Lifecycle events (observational) — unlimited.**
43
+ The core emits awaited, ordered events for the child-execution lifecycle (`spawning`, `session-created` pre-`bindExtensions`, `bound` post-`bindExtensions`, `completed`, `disposed`).
44
+ Any number of extensions subscribe; handlers return nothing.
45
+ Reactive concerns live here: permission detection, telemetry, UI, notifications.
46
+ Adding a reactive concern never modifies the core.
47
+
48
+ 2. **Provider seams (generative) — rationed.**
49
+ The rare concern that must *inject* a value the core consumes synchronously registers a provider the core consults.
50
+ Today there is exactly one: the **workspace provider** (it returns the child's working directory plus bracketed setup/teardown).
51
+ A provider seam is the only place the core is "open," so the list is kept as small as possible.
52
+
53
+ ### The discriminator
54
+
55
+ When deciding how a concern attaches:
56
+
57
+ - It only needs to **know** what happened → subscribe to a lifecycle event (observational, unlimited).
58
+ - It must **return a value the core consumes** → register a provider (generative, rationed).
59
+
60
+ Permissions are observational: the core does not enforce policy; it publishes the child's identity at the pre-bind instant so the permission extension (loaded in the child) can detect "am I a subagent?"
61
+ and gate tool calls at runtime.
62
+ Workspaces are generative: the core cannot default the cwd away when an isolation strategy is requested, so the provider hands it back.
63
+
64
+ ### The governing rule: no vacant hooks
65
+
66
+ The architecture must *admit* a seam without *shipping* it until a concrete consumer exists.
67
+ A provider seam with no consumer is not extensibility — it is a speculative abstraction that taxes every reader, and `fallow` flags it as dead.
68
+ Latent extensibility (the design can host the seam additively) is the deliverable; a vacant hook is not.
69
+
70
+ ### What leaves the core
71
+
72
+ - **Worktree isolation** (`worktree.ts`, `worktree-isolation.ts`, `GitWorktreeManager`, the `isolation: "worktree"` spawn mode) → a new package, `@gotgenes/pi-subagents-worktrees`, that implements the workspace provider and owns the git plumbing and the "saved to branch" result.
73
+ - **`permission-bridge.ts`** → retired.
74
+ The core stops reaching *out* to `Symbol.for("@gotgenes/pi-permission-system:service")` and instead *emits* lifecycle events the permission system subscribes to.
75
+ - **`isolated` / `extensions: false` / `noSkills`** → removed.
76
+ Deny-at-use (the in-child permission layer blocking disallowed tool calls) covers what `isolated` pretended to do for tools.
77
+ Prevent-load (refusing to bind an extension because of load-time side effects, cost, or true sandboxing) is genuinely generative and cannot be reduced to observation, so it is left as a *latent* (un-built) provider seam, added only if a real consumer needs it.
78
+
79
+ ### What stays in the core (not policy)
80
+
81
+ - The **recursion guard** (stripping the core's own `subagent` / `get_subagent_result` / `steer_subagent` tools from children).
82
+ It defends the core's own invariant — a subagent must not recursively spawn — keyed off the core's own tool names.
83
+ With `isolated` gone, children always load the parent's resources, so the guard becomes unconditional rather than gated on `cfg.extensions`.
84
+
85
+ ### Composition test
86
+
87
+ Install neither extension, only permissions, only workspaces, or both: the core is byte-for-byte identical in all four cases, and the two extensions never reference each other.
88
+ Permissions depend only on the core's events; workspaces depend only on the core's provider seam; the core depends on neither.
89
+
90
+ ## Consequences
91
+
92
+ - The "agent collaborator architecture" Phase 16 (give `Agent` a worktree collaborator + a session factory) is abandoned.
93
+ #256 is superseded (worktree was placed in the wrong layer); #257 is parked (it polished a subsystem slated for eviction).
94
+ - A new package `@gotgenes/pi-subagents-worktrees` is introduced; the core spawn API drops `isolation` and `isolated`.
95
+ - `permission-bridge.ts` is removed; `@gotgenes/pi-permission-system` migrates from a published-service lookup to lifecycle-event subscription, which requires the core to emit an awaited, ordered `session-created` event before `bindExtensions()`.
96
+ Confirming Pi's event model supports awaited pre-bind emission is the first investigation of the reclaimed phase.
97
+ - Once the cwd is resolved through the provider seam rather than relayed by `Agent`, child-session creation can construct a born-complete execution and the "runner" concept dissolves — recovering the structural goal of the abandoned collaborator steps by a cleaner route.
98
+ - The reclaimed Phase 16 roadmap and step issues live in [`docs/architecture/architecture.md`](../architecture/architecture.md).
99
+
100
+ ## Amendment: prevent-load ships as a settings key, not a provider seam (#696)
101
+
102
+ This decision reserved prevent-load as a *latent* provider seam, "added only if a real consumer needs it."
103
+ A real consumer arrived: in-process children reloaded `@cortexkit/pi-magic-context`, whose per-session initialization scans the whole Pi session store, and four concurrent children exhausted the V8 heap in the shared process.
104
+
105
+ The seam was nonetheless **not** built.
106
+ The governing rule above — no vacant hooks — decides it: a provider seam is warranted when an *extension* must inject a value the core consumes, and no extension wants to supply a prevent-load policy.
107
+ The only policy source is the operator's configuration, so a registerable provider whose sole consumer would be pi-subagents' own settings reader is precisely the speculative abstraction this ADR forbids.
108
+
109
+ The core therefore reads an `excludedExtensionPackages` list from the layered `subagents.json` and filters the child's package view before resource loading.
110
+ This is narrower than the per-agent policy Phase 14 evicted: it is global/project scope only, never per agent type; it names packages rather than individual extensions or tools; and it carries no tool-permission semantics, which remain deny-at-use in `@gotgenes/pi-permission-system`.
111
+ Default inheritance is unchanged — an absent or empty list reproduces prior behavior exactly.
112
+
113
+ Latent extensibility is preserved rather than spent.
114
+ The filtering is applied at the composition root, so if a sandboxing extension ever needs to supply the policy generatively, a provider seam can be added additively without disturbing the settings key or the assembly factory.
115
+
116
+ ## Amendment: the child-announcement contract is specified downstream (#789)
117
+
118
+ This decision required the core to emit an awaited, ordered `session-created` event before `bindExtensions()`, and the channel names and payload shapes were then declared independently in this package and in `@gotgenes/pi-permission-system`, each with a comment asking that they stay in sync.
119
+
120
+ That arrangement ends here.
121
+ The contract is now named — the **subagent adapter convention** — and specified in one place: [Subagent Integration](https://github.com/gotgenes/pi-packages/blob/main/packages/pi-permission-system/docs/subagent-integration.md#the-subagent-adapter-convention), per pi-permission-system's [ADR 0012](https://github.com/gotgenes/pi-packages/blob/main/packages/pi-permission-system/docs/decisions/0012-cross-node-extension-contract.md) decision 5.
122
+ Read it there rather than reconstructing it from this record.
123
+
124
+ Nothing about this package's obligations changed: the core still publishes its lifecycle and knows nothing about its consumers, and the inverted dependency this decision established is untouched.
125
+ What changed is that the convention now has a canonical home, and any implementation — not only this one — can conform to it.
@@ -0,0 +1,71 @@
1
+ ---
2
+ status: accepted
3
+ date: 2026-05-29
4
+ ---
5
+
6
+ # 0003 — Publish a bundled `.d.ts` for the public surface
7
+
8
+ ## Status
9
+
10
+ Accepted.
11
+ Introduces the repository's first build step, scoped to type declarations only.
12
+
13
+ ## Context
14
+
15
+ `@gotgenes/pi-subagents` could not be imported by another TypeScript package in this workspace.
16
+ Issue #263 (extract worktree isolation to `@gotgenes/pi-subagents-worktrees`) is the first intra-repo consumer: it must `implements WorkspaceProvider` and call `getSubagentsService().registerWorkspaceProvider(...)`, both of which require importing the package by name.
17
+
18
+ A `tsc --traceResolution` of a sibling consuming the package surfaced two compounding failures.
19
+
20
+ 1. `package.json` `exports["."]` pointed at `./src/service.ts`, which does not exist — the real module is `./src/service/service.ts`.
21
+ A latent bug, unnoticed because nothing in-repo imported the package by name.
22
+ 2. Once corrected, the public entry's internal alias imports cascade.
23
+ `service/service.ts` imports `type LifetimeUsage` and `type WorkspaceProvider` via the `#src/*` alias.
24
+ When a sibling's `tsc` follows the symlink, the consumer's own `paths` (`#src/*` → `./src/*`) intercept first and resolve into the *consumer's* `src/` — a global-`paths` collision, since both packages define `#src/*`.
25
+ The fallback to the publisher's `package.json` `imports` field also fails: `tsc` cannot resolve the extensionless `.ts` target under Node `imports` semantics ("Import specifier '#src/lifecycle/usage' does not exist in package.json scope").
26
+
27
+ The public entry's type closure is deeply entangled: `WorkspaceProvider` (in `lifecycle/workspace.ts`) reaches `AgentStatus` in the 510-line `lifecycle/agent.ts`, plus `SubagentType`/`AgentInvocation` from `types.ts` (which itself re-exports the `Agent` class).
28
+ A shallow alias-free entry is therefore not achievable without a substantial source restructure.
29
+
30
+ This collides with the ship-source model ([ADR-0002]): every package ships raw `.ts` executed directly by Pi, with no build step.
31
+
32
+ ## Decision
33
+
34
+ Emit a single, self-contained `dist/public.d.ts` for the public surface and advertise it through a `types` export condition, while the runtime entry continues to serve `.ts` source.
35
+
36
+ ```jsonc
37
+ "exports": {
38
+ ".": {
39
+ "types": "./dist/public.d.ts",
40
+ "default": "./src/service/service.ts"
41
+ }
42
+ }
43
+ ```
44
+
45
+ - `rollup-plugin-dts` rolls the declaration graph rooted at `src/service/service.ts` into one file, inlining the internal `#src/*` types and keeping peer-dependency types (`@earendil-works/*`, `@sinclair/typebox`) external.
46
+ We ship `.ts` source, so only the declaration bundle is emitted — no JS.
47
+ - The bundle is generated at `prepack` time and shipped via a `files` allowlist; it is gitignored and never committed.
48
+ - `default` → `./src/service/service.ts` fixes the stale path and serves runtime consumers; its `import type` lines erase, so no runtime `#src/*` resolution is needed.
49
+ - A `pnpm pack` → throwaway-consumer → `tsc` harness proves external consumability with no publish round-trip and no workspace privileges.
50
+
51
+ This is the repository's first build step.
52
+ It is deliberately narrow: it produces type declarations only and changes nothing about how Pi loads the extension from source (`pi.extensions: ["./src/index.ts"]` is untouched).
53
+
54
+ ## Alternatives considered
55
+
56
+ - Alias-free public entry (restructure the source so the entry's full type closure resolves via same-directory `./` imports).
57
+ Mechanically possible, but it requires moving the `AgentStatus`/`SubagentType`/`AgentInvocation`/`WorkspaceProvider` definitions and untangling the `agent.ts`/`types.ts` graph, with care that inner layers do not import the outer service layer.
58
+ `eslint`'s `no-parent-relative-imports` rule (which forbids `../`) narrows the options further.
59
+ Larger blast radius than emitting a `.d.ts`, and it churns the domain model to serve a packaging concern.
60
+ - A self-contained entry that re-declares the public types inline, guarded by a conformance test.
61
+ Avoids a build step but duplicates the seam/usage/status type definitions, which drift over time.
62
+
63
+ ## Consequences
64
+
65
+ - The repository now has a build step, but it is type-only and isolated to this package; the ship-source model is otherwise intact.
66
+ - Consumers (including `@gotgenes/pi-subagents-worktrees` in #263) consume the packaged public interface like any external developer — no `workspace:*` privileges.
67
+ - The `types` condition points at a build-time artifact; an in-repo workspace-linked consumer that imported the package would need `dist/public.d.ts` present.
68
+ This is acceptable because no in-repo package imports the surface yet; #263 consumes the built artifact from the published tarball.
69
+ - Sequencing: #270 must be published (its release-please PR merged) before #263 edits `pi-subagents` core, so #263's changes do not batch into the same `pi-subagents` release.
70
+
71
+ [ADR-0002]: ./0002-extensions-on-a-minimal-core.md
@@ -0,0 +1,279 @@
1
+ ---
2
+ status: accepted
3
+ date: 2026-06-18
4
+ ---
5
+
6
+ # 0004 — Reconsider the UI direction from first principles
7
+
8
+ ## Status
9
+
10
+ Accepted.
11
+ Completes Phase 18 (reconsider the UI) and gateways Phase 19 (implement the recorded decisions).
12
+ Decision-only: this ADR changes no runtime code.
13
+ The inherited UI stays live until Phase 19 acts on these decisions.
14
+
15
+ ## Context
16
+
17
+ Phase 18's spine (Steps 1–7, #420 through #426) disentangled the activity tier from the core.
18
+ The core now owns all run state in one place (`SubagentState`), the widget self-drives from lifecycle events, the LLM-facing `subagent` tool no longer depends on the widget, and the public event contract's declared channels equal its emitted channels.
19
+ The UI is therefore a pure reactive consumer of the broadcast-plus-query surface — _substitutable_.
20
+
21
+ This final step decides the UI's _direction and distribution_, not whether substitution is possible.
22
+ The goal is **substitutable, not optional**: a human needs some surface, but the specific UI is replaceable — the way Pi ships a default TUI built on the same public API any extension targets.
23
+ The disentangled core stays byte-for-byte identical whether or not a given UI consumer is installed (the composition invariant), so a replacement UI is a downstream concern even though _some_ UI is not.
24
+
25
+ Unlike the worktrees provider seam (generative, rationed — one provider the core consults), the UI is an observational consumer (unlimited, the core never waits on it).
26
+ That asymmetry is why packaging the UI is the secondary question and decoupling it was the real win.
27
+
28
+ Three operator-framed concerns shape the per-component judgment.
29
+
30
+ 1. **Foreground progress is already shown by the tool call.**
31
+ In foreground the `subagent` tool's inline `onUpdate` stream renders progress well; the above-editor widget duplicates it.
32
+ 2. **Background agents have no tool-call display.**
33
+ When agents run in the background there is no inline stream, so _something_ must indicate their state — and multiple subagents can run in parallel, so that surface must represent N concurrent agents at once.
34
+ 3. **Operator visibility into a subagent's session is a distinct, richer need.**
35
+ "Switch into a subagent's session, scroll/read it, switch between subagents, and exit back to root" is a navigation interaction, not a live overlay.
36
+ The core already persists each child as a standalone Pi session JSONL at `Subagent.outputFile`, and `Subagent.messages` exposes the full history — so the data was never the limit; the bespoke, width-capped `ConversationViewer` overlay was.
37
+
38
+ ### Relevant Pi SDK surface
39
+
40
+ Verified against `@earendil-works/pi-coding-agent@0.79.1`:
41
+
42
+ - `ExtensionActions.switchSession(sessionPath, { withSession })` switches the **active** session to a different session file.
43
+ It is a full active-session takeover: it fires `session_before_switch` / `session_shutdown`, invalidates the current session context (`setBeforeSessionInvalidate` exists for host-owned UI teardown), and returns `{ cancelled }`.
44
+ The switched-to session is fully interactive — `ReplacedSessionContext` exposes `sendUserMessage`.
45
+ - `session-manager` exports `loadEntriesFromFile(filePath)` / `parseSessionEntries(content)`, which read a session file's entries without switching — the read-only alternative to a full takeover.
46
+
47
+ ## Decision
48
+
49
+ Judge each UI component on the first principles above, then record the distribution.
50
+
51
+ ### A — Foreground widget: shrink to background agents only
52
+
53
+ The above-editor widget duplicates the foreground tool's inline `onUpdate` stream.
54
+ The widget survives **only** as the background-agent status surface (concern 2): foreground runs suppress it, the inline stream is authoritative there, and the background surface keeps the widget's existing per-agent tree so it represents N parallel agents at once.
55
+ The change is _when_ the widget shows (background-only), not _what_ it shows.
56
+
57
+ ### B — Conversation viewer: replace the bespoke overlay with native session navigation
58
+
59
+ Remove the bespoke `ConversationViewer` overlay.
60
+ Operator visibility (concern 3) is served by Pi's own session machinery applied to the already-persisted child session file, not a hand-rolled transcript renderer — the recursive-Pi insight applied to `Subagent.outputFile`.
61
+
62
+ The illustrative call shape (Phase 19, not final):
63
+
64
+ ```typescript
65
+ // "View running agents" → pick a child → switch into its persisted session
66
+ const child = manager.getRecord(id);
67
+ if (child?.outputFile) {
68
+ await ctx.switchSession(child.outputFile);
69
+ // operator reads/scrolls in Pi's native viewer; a later switch returns to root
70
+ }
71
+ ```
72
+
73
+ This is Tell-Don't-Ask (hand Pi the session path; Pi owns the viewer) and keeps the core free of transcript-rendering code.
74
+
75
+ This decision records the _direction_ (native session machinery over a bespoke renderer), not the _mechanism_.
76
+ `switchSession` is a full active-session takeover and is interactive, so the operator UX is gated on a Phase 19 spike that chooses between (i) true `switchSession` round-trips and (ii) a read-only transcript built from `loadEntriesFromFile` that renders Pi-standard entries without leaving the root session.
77
+ See "Phase 19 entry criteria."
78
+
79
+ ### C — `/agents` menu: dissolve the monolithic command into focused surfaces
80
+
81
+ The single `/agents` command bundles four unrelated jobs; split them, and do not keep all in one command.
82
+ Managing agent _definitions_ through the menu earns no keep — creating or editing agents is better done with other tools (directly in Pi, or a real text editor / IDE).
83
+
84
+ - **Create new agent (wizard)** → **remove.**
85
+ An operator generates a new agent `.md` by asking a Pi agent directly (more capable than a fixed wizard) or by writing the file in an editor.
86
+ - **Agent types (list + config editor)** → **remove.**
87
+ Viewing and editing agent definitions is better served by opening the `.md` files directly in an editor/IDE.
88
+ - **Running agents (visibility)** → **keep the responsibility, re-home it.**
89
+ _Something_ must own running-agent visibility; it moves onto the background widget (Decision A) plus the native session navigation (Decision B), not a bespoke in-menu overlay.
90
+ - **Settings (concurrency / max turns / grace turns)** → **extract to a focused command** (e.g. `/subagents:settings`).
91
+ Some value, but it does not belong bundled with agent management.
92
+
93
+ ### D — Distribution: keep the surviving UI in-core (substitutable, not extracted)
94
+
95
+ The spine already made the UI substitutable; a replacement UI is a downstream concern that targets the public broadcast-plus-query surface.
96
+ The surviving UI — the background widget, a focused settings command, and the session-navigation glue — **stays in-core** as a reactive consumer.
97
+ Extraction to a separate `@gotgenes/pi-subagents-ui` package is **not** chosen now.
98
+
99
+ This answers the issue's headline question — the UI's _distribution_ — with "keep in core, substitutable," recorded explicitly rather than left implicit.
100
+ Extraction remains an available future option precisely because the composition invariant holds: the core is byte-for-byte identical with or without a given UI consumer.
101
+ It would be revisited if a second, materially different UI consumer appears, or if the in-core UI starts to pull SDK or rendering concerns back into core modules.
102
+
103
+ ## Consequences
104
+
105
+ - The inherited UI is no longer preserved by default; each component now has a recorded fate (shrink / replace / dissolve) motivated by the first principles, not by inheritance.
106
+ - Phase 18 is complete.
107
+ This ADR gateways Phase 19, which implements the decisions (background-only widget, native session navigation, `/agents` decomposition, `/subagents:settings` extraction) under its own plan and issues.
108
+ - No interim regression: this ADR removes nothing.
109
+ The widget, the `ConversationViewer`, and the full `/agents` menu stay live until Phase 19 replaces them.
110
+ - Phase 19 must preserve the spine's invariants when it acts on these decisions: the runtime holds zero UI state (#422), the widget is a reactive consumer with no inbound calls from core spawn tools (#423), the LLM tool depends only on manager/runtime/settings/registry (#424), and declared event channels equal emitted channels with no vacant hook (#425).
111
+ These are pinned today by the existing observer/widget/event-contract suites, which Phase 19 inherits.
112
+
113
+ ## Phase 19 entry criteria
114
+
115
+ The following are open and must be resolved by a Phase 19 spike before committing to a mechanism; they are deliberately not decided here.
116
+
117
+ - **Root-continuity during a session switch.**
118
+ `switchSession` invalidates the current session context — does the root's in-flight turn survive a switch-out-and-return, and what is the correct "return to root" gesture?
119
+ Resolve before committing to true `switchSession` round-trips.
120
+ - **View-only vs interactive.**
121
+ A switched-to child session is interactive (`sendUserMessage`).
122
+ Decide whether steering a child from its own session is desirable, or whether the viewer should be strictly read-only (favoring the `loadEntriesFromFile` transcript path).
123
+ - **Parallel-agent navigation.**
124
+ With N background agents running, decide the operator's gesture to pick which child to view and to cycle between them — driven from the background widget, a dedicated command, or both.
125
+ - **Settings command namespace.**
126
+ Confirm the final command name/namespace for the extracted settings surface (`/subagents:settings` vs another form) against how sibling packages register namespaced commands.
127
+
128
+ The agent create/edit surfaces are **not** open questions: both are removed (Decision C).
129
+
130
+ ## Addendum (2026-06-20): Phase 19 entry-criteria answers ([#446])
131
+
132
+ The Phase 19 Step 1 spike ([#446]) resolved all four entry criteria.
133
+ Evidence comes from the bundled `@earendil-works/pi-coding-agent` SDK surface (`packages/pi-subagents/node_modules/@earendil-works/pi-coding-agent/dist`) and a throwaway vitest harness run against a **real child session JSONL** (a 43-entry subagent session: 1 `session` header carrying a `parentSession` backref, 1 `model_change`, 1 `thinking_level_change`, 40 `message` entries).
134
+ The harness was discarded after observation; no production source changed.
135
+
136
+ ### Finding 0 — `loadEntriesFromFile` is not part of the package's public surface
137
+
138
+ The original "Relevant Pi SDK surface" section cited `loadEntriesFromFile` as the read-only alternative to a switch.
139
+ The spike found it is **not reachable** from `@earendil-works/pi-coding-agent`, and that this is not a types/runtime mismatch — the type barrel and the runtime barrel agree, both omitting it.
140
+ `loadEntriesFromFile` is defined in the deep module `core/session-manager.ts` (annotated `/** Exported for testing */`), but the public barrel `src/index.ts` (→ `dist/index.d.ts` + `dist/index.js`) re-exports only a curated subset of that module — including `parseSessionEntries` but **not** `loadEntriesFromFile`.
141
+ The `package.json` `exports` map exposes only `"."` → the barrel, so the deep import `@earendil-works/pi-coding-agent/dist/core/session-manager.js` is not a supported entry point either.
142
+ `tsc` correctly rejects `import { loadEntriesFromFile } from "@earendil-works/pi-coding-agent"` with `TS2305: Module … has no exported member 'loadEntriesFromFile'`; the throwaway Vitest harness only reached a runtime `is not a function` because esbuild strips types without type-checking (the package's own `pnpm run check` would have caught it at compile time).
143
+ This is not version-specific: the barrel omits it identically in both the pinned `0.79.1` and the latest `0.79.8`, so an SDK upgrade does not surface it — Step 4 should not chase one.
144
+ The viable read-only path is therefore `parseSessionEntries(readFileSync(outputFile, "utf8"))` — `parseSessionEntries` _is_ public (both types and runtime) — which the harness confirmed returns the full `FileEntry[]` transcript with no session switch and no active-session mutation.
145
+ Step 4 ([#445]) should read the file itself and call `parseSessionEntries`, not `loadEntriesFromFile`.
146
+
147
+ Upstream references:
148
+
149
+ - Barrel that omits it: [`packages/coding-agent/src/index.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/index.ts).
150
+ - Test-annotated definition: [`packages/coding-agent/src/core/session-manager.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/session-manager.ts).
151
+
152
+ ### Finding 1 — the read-only transcript renders entirely on public Pi APIs (no bespoke renderer)
153
+
154
+ The read-only path returns raw `FileEntry[]`, so _something_ must render them.
155
+ The spike found that every piece needed to turn those entries into a Pi-standard transcript is already re-exported through the public root barrel — so Step 4 wires Pi's own machinery rather than re-implementing the formatting the bespoke `ConversationViewer`/`message-formatters.ts` carried.
156
+ This matters because ADR-0004 Decision B's `switchSession` sketch implied "Pi owns the viewer"; the read-only path keeps that property at the _component_ level (Pi renders each entry) without the active-session takeover.
157
+
158
+ The verified public pipeline (all symbols confirmed present in `dist/index.d.ts`, the root barrel):
159
+
160
+ 1. **Load** — `parseSessionEntries(readFileSync(record.outputFile, "utf8")): FileEntry[]` (Finding 0).
161
+ 2. **Bridge** — `buildSessionContext(entries, leafId?, byId?): SessionContext` turns the entries into `{ messages: AgentMessage[], thinkingLevel, model }`, handling tree traversal, compaction, and branch summaries along the path.
162
+ Note `buildSessionContext` takes `SessionEntry[]`, so drop the leading `SessionHeader` (`type: "session"`) that `parseSessionEntries` includes.
163
+ 3. **Render** — one of:
164
+ - **Text:** `serializeConversation(messages: Message[]): string` (from `core/compaction`) for a plain-text dump.
165
+ - **TUI:** the per-entry components `AssistantMessageComponent`, `UserMessageComponent`, `ToolExecutionComponent`, `BashExecutionComponent`, `CompactionSummaryMessageComponent`, `BranchSummaryMessageComponent`, `CustomMessageComponent`, `SkillInvocationMessageComponent` (from `modes/interactive/components`), plus `renderDiff`, for a scrollable native transcript.
166
+
167
+ Unlike `loadEntriesFromFile`, all of these _are_ public (both types and runtime).
168
+ So the read-only viewer is buildable end-to-end on supported APIs, and ADR-0004 Decision B's "keep the core free of transcript-rendering code" holds — the rendering is Pi's, imported, not hand-rolled.
169
+
170
+ Upstream references:
171
+
172
+ - Component barrel: [`packages/coding-agent/src/modes/interactive/components/index.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/modes/interactive/components/index.ts).
173
+ - `serializeConversation`: [`packages/coding-agent/src/core/compaction/utils.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/compaction/utils.ts).
174
+
175
+ ### Criterion 1 — Root-continuity during a session switch: avoid the switch
176
+
177
+ `switchSession` is a full active-session takeover: it fires `session_before_switch` (cancellable) and then tears the current runtime down via `session_shutdown` (whose `targetSessionFile` field marks a replacement-driven shutdown).
178
+ The root's in-flight turn does **not** survive the takeover — the runtime that owns that turn is invalidated — and a "return to root" would require a second `switchSession(rootSessionFile)` that re-incurs the teardown on the way back.
179
+ Because background agents run precisely while the operator keeps working at root, a true `switchSession` round-trip is hostile to a root with a turn in flight.
180
+
181
+ **Answer:** do not use `switchSession` for navigation.
182
+ The read-only transcript path (Criterion 2) sidesteps root-continuity entirely — it never touches the active session, so there is no return gesture to get wrong.
183
+
184
+ ### Criterion 2 — View-only vs interactive: read-only
185
+
186
+ `ReplacedSessionContext` (handed to a `switchSession` `withSession` callback) extends `ExtensionCommandContext` and exposes `sendUserMessage`/`sendMessage`, so a switched-to child session is interactive.
187
+ But operator visibility (concern 3) is framed as "switch in, scroll/read, switch between, exit back to root" — a navigation interaction, not a live steering overlay — and steering already has a home (`steer_subagent` tool / the widget).
188
+ Adding in-session steering would create a second, redundant steering surface.
189
+
190
+ **Answer:** the viewer is strictly **read-only**, loaded via `parseSessionEntries(readFileSync(record.outputFile))` (Finding 0) and rendered through Pi's public entry components (Finding 1) without leaving the root session.
191
+ This also resolves Criterion 1 by construction.
192
+
193
+ ### Criterion 3 — Parallel-agent navigation: command-first
194
+
195
+ With N background agents running, the operator needs a gesture to pick which child to view.
196
+ The background widget (Decision A, [#444]) already represents N parallel agents as a per-agent tree, making it the natural eventual selection surface; a flat command gives a non-widget entry point that lists running background agents and lets the operator pick one keyed on `record.outputFile`.
197
+
198
+ **Answer:** Step 4 ([#445]) ships a **command** as the primary, unit-testable selection surface (list background agents → pick → render that child's transcript read-only), with a widget gesture as an optional later enhancement.
199
+ "Both" remains the eventual target; command-first is the Step 4 starting point because it does not depend on the widget shrink ([#444]) landing first.
200
+
201
+ ### Criterion 4 — Settings command name: `/subagents-settings`
202
+
203
+ Sibling packages register flat, hyphenated command names with no `:` namespace: `registerCommand("agents", …)` (this package), `"colgrep-reindex"`, `"permission-system"`.
204
+ A `/subagents:settings` form would be inconsistent with every existing command in the repo, and `/agents-settings` wrongly implies it manages agent definitions (which Decision C removes).
205
+
206
+ **Answer:** confirm **`/subagents-settings`** (flat, hyphenated) for Step 2 ([#447]).
207
+ Reject the tentative `/subagents:settings` and the `/agents-settings` alternative.
208
+
209
+ ### Net mechanism for Phase 19
210
+
211
+ - Session navigation (Step 4, [#445]): a read-only transcript, surfaced through a flat command; no `switchSession`, no `loadEntriesFromFile`.
212
+ The pipeline is `parseSessionEntries(readFileSync(record.outputFile, "utf8"))` → drop the `SessionHeader` → `buildSessionContext(...).messages` → render via Pi's public entry components (`AssistantMessageComponent` / `ToolExecutionComponent` / … ) or `serializeConversation` (Findings 0 and 1).
213
+ - Settings command (Step 2, [#447]): `/subagents-settings`.
214
+
215
+ This keeps bespoke transcript rendering out of the package (rendering is Pi's own public components), adds no inbound call from the UI to the core, and preserves the Phase 18 spine invariants (#422–#425).
216
+
217
+ ## Addendum 2 (2026-06-20): Step 4 sourcing — live record plus file snapshot ([#445])
218
+
219
+ This addendum revises the first addendum's Criterion 2 answer for Step 4 ([#445]).
220
+ The "strictly read-only, file-only" sourcing and the "not a live overlay" framing are superseded by the dual-source decision below; everything else from the spike stands.
221
+
222
+ **Why revise.**
223
+ The spike's mechanism — `parseSessionEntries(readFileSync(record.outputFile))` read once — serves a _completed_ subagent well but is a frozen snapshot for a _running_ one: it shows only what was flushed to disk at open time and does not stream.
224
+ The clarified desired behavior is to see the **live** activity of any subagent _and_ the activity of a completed subagent.
225
+ The bespoke `ConversationViewer` being removed in Step 5 already delivers both — it subscribes to the live in-memory record (`record.subscribeToUpdates()`) and renders `record.messages` plus a streaming indicator built from `record.activeTools` / `record.responseText`.
226
+ That liveness comes from the in-memory record, not the persisted file, so a file-only viewer drops it.
227
+
228
+ **Decision — dual-source, one renderer.**
229
+ Step 4 sources the transcript by liveness and renders both sources through the same Pi public entry components (Finding 1 holds — no bespoke renderer):
230
+
231
+ - **Tracked agent (still in `manager.listAgents()`)** — render from the live in-memory record: `record.messages` for history, `record.subscribeToUpdates()` to re-render on streaming updates, and `record.activeTools` / `record.responseText` for the running-agent streaming indicator.
232
+ This is live.
233
+ - **Evicted / untracked agent** — render from the file snapshot: `parseSessionEntries(readFileSync(record.outputFile, "utf8"))` → drop the `SessionHeader` → `buildSessionContext(...).messages` (Findings 0 and 1).
234
+
235
+ Both sources yield `AgentMessage[]`, so a single Pi-component renderer serves both.
236
+
237
+ **Type-boundary note.**
238
+ `SubagentSession.messages` is deliberately widened to `readonly unknown[]` at the core boundary (`src/lifecycle/subagent-session.ts`), even though the underlying `_session.messages` is the SDK's `AgentMessage[]`.
239
+ Step 4 should add a typed accessor that returns `AgentMessage[]` (or narrow at the boundary) rather than feeding `unknown[]` into Pi's components.
240
+ This is a read accessor on the existing record — it adds no inbound call from the UI to the core and does not regress the Phase 18 spine invariants.
241
+
242
+ **Still read-only (non-interactive).**
243
+ The viewer remains strictly non-interactive: Criterion 2's anti-redundant-steering rationale stands (steering lives in the `steer_subagent` tool and the widget), and Criterion 1's rejection of `switchSession` is unchanged — "live" here means the in-memory subscription, not an active-session takeover.
244
+
245
+ **Candidate set (revises Criterion 3).**
246
+ The selection command lists **any subagent with a live record or a persisted session file** — foreground agents included, not only running background agents.
247
+ This matches the bespoke viewer's current reach (gated on `record.isSessionReady()`, never background-filtered) and avoids an interim regression.
248
+ A foreground agent is navigable only _after_ it completes — while it runs, the root turn is blocked on it — whereas a background agent is navigable live.
249
+ The background widget ([#444]) remains the optional secondary selection gesture for background agents; the command is the primary, unit-testable surface.
250
+
251
+ **Evicted-agent candidate set ([#463]).**
252
+ Step 4b realizes the file-snapshot branch for fully-evicted agents.
253
+ The candidate set is broadened via **manager-retained descriptors**, not a directory scan of the tasks directory: the persisted child session carries no subagent `type`/`description` (those live only on the in-memory record), so a scan yields degraded labels and parses every file per picker open.
254
+ The cleanup sweep instead stashes a lightweight `EvictedSubagent` descriptor (label fields + `outputFile`, no messages) before disposing a record, preserving rich labels and bounded memory.
255
+ This covers in-session evictions — the sweep's only targets, since a fresh manager per session never reloads prior-process subagents.
256
+ The "render from the file snapshot" mechanism for an evicted agent (above) is unchanged; only the candidate-set _enumeration_ is pinned to descriptors.
257
+
258
+ ## Addendum 3 (2026-06-23): adopt the `subagents:` colon namespace, superseding Criterion 4
259
+
260
+ This addendum reverses **Criterion 4**, which confirmed flat, hyphenated command names (`/subagents-settings`) and rejected `/subagents:settings`.
261
+ The two registered commands are renamed: `/subagents-settings` → `/subagents:settings` and `/subagent-sessions` → `/subagents:sessions`.
262
+ This is a breaking change to the command surface.
263
+
264
+ **Why revise.**
265
+ Criterion 4 surveyed only in-repo siblings (`agents`, `colgrep-reindex`, `permission-system`) and concluded a `:` namespace would be inconsistent with every existing command.
266
+ That survey missed the broader Pi ecosystem: `@eko24ive/pi-ask` establishes the colon convention with `answer:again` and `ask:replay`, grouping a package's commands under a shared prefix.
267
+ The colon namespace reads as a deliberate grouping gesture (`subagents:settings`, `subagents:sessions` clearly belong to one package) where the hyphen form blurs into an ordinary command name.
268
+ The newer ecosystem signal outweighs the original in-repo-only consistency argument.
269
+
270
+ **Scope.**
271
+ This change is pi-subagents-only.
272
+ Whether the colon convention becomes repo-wide (renaming `colgrep-reindex`, `permission-system`, etc.) is deferred — not decided here.
273
+ `/agents` is intentionally left flat: it is slated for removal in the Phase 18 / Step 5 menu retirement, so namespacing it would be churn on a command being deleted.
274
+
275
+ [#444]: https://github.com/gotgenes/pi-packages/issues/444
276
+ [#445]: https://github.com/gotgenes/pi-packages/issues/445
277
+ [#446]: https://github.com/gotgenes/pi-packages/issues/446
278
+ [#447]: https://github.com/gotgenes/pi-packages/issues/447
279
+ [#463]: https://github.com/gotgenes/pi-packages/issues/463
@@ -0,0 +1,106 @@
1
+ ---
2
+ status: accepted
3
+ date: 2026-08-29
4
+ ---
5
+
6
+ # 0005 — What the public `SubagentRecord` snapshot exposes
7
+
8
+ ## Status
9
+
10
+ Accepted.
11
+ Settles the admission policy for the public snapshot and its contract direction; the lifecycle **event payloads** keep their own guarantee unstated.
12
+
13
+ ## Context
14
+
15
+ `SubagentRecord` is what `SubagentsService.getRecord()` and `.listAgents()` return.
16
+ It is produced by `toSubagentRecord()` in `src/service/service-adapter.ts`, which copies a named set of fields and drops everything else — a deliberate allowlist that keeps live session objects out of a snapshot a consumer may serialize.
17
+
18
+ The allowlist had no stated admission policy, so every proposed widening re-litigated the same trade-off from scratch.
19
+ Pull request [#748] proposed `turnCount` and `activeTools` as required fields plus an optional `outputFile`, and flagged the required-versus-optional hazard itself.
20
+ [#724] deferred `isBackground` here rather than deciding it in passing.
21
+ Several more fields — `maxTurns`, `responseText`, `consumedAt`, `stoppedWhileQueued` — exist on the live `Subagent`/`SubagentState` and were dropped with no recorded reason.
22
+
23
+ Three facts shaped the decision.
24
+
25
+ 1. **The record has no in-repo consumer.**
26
+ Outside `service-adapter.ts` and its tests, nothing in this repository reads a `SubagentRecord`; the background widget reads live `Subagent` objects through `manager.listAgents()`.
27
+ The policy is therefore a judgment about consumers in other extensions, not a response to local demand.
28
+ 2. **`architecture.md` already supplies the axis.**
29
+ Its "Reactive versus discrete (not internal versus external)" refinement rules `SubagentsService.getRecord` a query by nature, in-package or not.
30
+ A snapshot is the discrete half of that split, and what belongs in it follows from that rather than from a field-by-field vote.
31
+ 3. **Whether the required-versus-optional question is even a semver question depends on the contract direction.**
32
+ TypeScript is structurally typed: adding a required property to a type breaks code that *builds* the value, never code that *reads* it.
33
+
34
+ ## Decision
35
+
36
+ ### Admission rules
37
+
38
+ A field is admitted to `SubagentRecord` when **all four** hold.
39
+
40
+ 1. **Serializable by value** — a JSON primitive, array, or plain object.
41
+ Never a live object, a function, or a `Map`.
42
+ 2. **Discrete, not momentary** — identity, a resolved spawn decision, a cumulative metric, or a pointer to a durable artifact.
43
+ State whose value is stale the instant it is read is reactive by nature, and the discrete-query half of the split does not serve it.
44
+ 3. **Meaningful outside this package** — not bookkeeping the core keeps in order to run its own sweeps.
45
+ 4. **Stable in meaning** — the package can keep producing it without re-deriving it from a display snapshot or a UI concern.
46
+
47
+ Four exclusion classes follow, and a proposal is answered by naming one of them rather than by reopening the debate:
48
+
49
+ - **live objects** (rule 1) — `subagentSession`, `abortController`, `promise`, `execution`;
50
+ - **momentary activity** (rule 2) — `activeTools`, `responseText`;
51
+ - **internal bookkeeping** (rule 3) — `consumedAt`, `stoppedWhileQueued`;
52
+ - **display snapshots** (rule 4) — `invocation`, which only the tool door built.
53
+ It was removed from `Subagent` outright by [#828], so this class currently has no live instance; the rule stands on its own and answers the next display snapshot proposed.
54
+
55
+ ### `SubagentRecord` is produced, not implemented
56
+
57
+ The package constructs every `SubagentRecord` that exists; consumers read them.
58
+ `SubagentRecord` and `SubagentsService` are not contracts third parties satisfy — a consumer's test double should be a cast or a `Partial<>`, not an implementation.
59
+
60
+ The semver consequence is therefore fixed once, here:
61
+
62
+ - **adding a field is semver-minor**, required or optional, because structural typing leaves every reader unaffected;
63
+ - **removing a field, renaming one, or narrowing its type is semver-major.**
64
+
65
+ A field is optional only when its underlying value is genuinely absent in a real state — never as a compatibility hedge.
66
+
67
+ ### A snapshot is by value
68
+
69
+ `getRecord()` and `listAgents()` return data that no later mutation of the agent can alter, and that no consumer mutation can write back into the agent.
70
+ This was true of every field except `lifetimeUsage`, which was assigned by reference to the object `SubagentState.addUsage()` mutates in place — so a held record drifted, and a consumer could write into a running agent's token totals.
71
+ `toSubagentRecord` now copies it.
72
+
73
+ ### Dispositions
74
+
75
+ | Field | Disposition | Basis |
76
+ | ---------------------------------------------- | ------------------------------------ | --------------------------------------------------------------- |
77
+ | `id`, `type`, `description`, `status` | admitted (already present) | identity and lifecycle status |
78
+ | `result`, `error`, `completedAt` | admitted, optional (already present) | terminal facts, absent until the agent ends |
79
+ | `toolUses`, `lifetimeUsage`, `compactionCount` | admitted (already present) | cumulative metrics |
80
+ | `startedAt` | admitted (already present) | resolved lifecycle timestamp |
81
+ | `isBackground` | admitted, required | resolved spawn fact, known from the choke point onward ([#724]) |
82
+ | `turnCount` | admitted, required | cumulative metric; parity with `toolUses` and `compactionCount` |
83
+ | `maxTurns` | admitted, optional | spawn-time configuration; genuinely absent when unset |
84
+ | `outputFile` | admitted, optional | pointer to the child's durable session transcript |
85
+ | `activeTools` | declined | rule 2 — momentary set, and a `Map` on the live record |
86
+ | `responseText` | declined | rule 2 — momentary and unbounded in size |
87
+ | `consumedAt` | declined | rule 3 — result-delivery bookkeeping behind the retention sweep |
88
+ | `stoppedWhileQueued` | declined | rule 3 — internal marker selecting a never-started result text |
89
+
90
+ ## Consequences
91
+
92
+ - The public snapshot answers "what is this agent, how is it progressing, and how did it end?"
93
+ A consumer can render progress (`turnCount` against `maxTurns`), filter the roster (`isBackground`) without reconstructing the mode from a display snapshot, and open the child's transcript (`outputFile`) — the pipeline [ADR 0004](0004-reconsider-ui-direction.md) established against the in-package record.
94
+ - Pull request [#748]'s widening is **partially adopted**: `turnCount` and `outputFile` are in, `activeTools` is declined under rule 2.
95
+ - Live agent activity has no external path at all, since no broadcast channel carries it either.
96
+ That is the intended state of a "no vacant hooks" core, not an oversight.
97
+ **Revisit condition:** a named consumer plus a reactive channel for momentary state.
98
+ Whoever reopens it should add the channel, not widen the snapshot — a pulled `activeTools` would be stale on arrival.
99
+ - Both halves of the policy are pinned by tests in `test/service/service-adapter.test.ts`: exact `toEqual` assertions on the admitted set, and a test that populates every declined field on the source and asserts none of them reaches the output.
100
+ A future widening fails those tests by design; that failure is the moment the proposal meets this policy.
101
+ - `SubagentRecord.lifetimeUsage` stays declared mutable.
102
+ Once the value is a copy, a consumer mutating it harms nothing, and retyping a shipped public property as `Readonly` would be a breaking change bought for nothing.
103
+
104
+ [#724]: https://github.com/gotgenes/pi-packages/issues/724
105
+ [#748]: https://github.com/gotgenes/pi-packages/pull/748
106
+ [#828]: https://github.com/gotgenes/pi-packages/issues/828