@gotgenes/pi-subagents 15.0.2 → 16.1.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 +23 -0
- package/README.md +24 -24
- package/docs/architecture/architecture.md +111 -18
- package/docs/plans/0381-replace-concurrency-queue-with-limiter.md +267 -0
- package/docs/plans/0400-include-parent-prompt-in-replace-mode.md +199 -0
- package/docs/retro/0381-replace-concurrency-queue-with-limiter.md +49 -0
- package/docs/retro/0400-include-parent-prompt-in-replace-mode.md +84 -0
- package/package.json +1 -1
- package/src/index.ts +8 -15
- package/src/lifecycle/concurrency-limiter.ts +55 -0
- package/src/lifecycle/subagent-manager.ts +38 -35
- package/src/lifecycle/subagent.ts +2 -1
- package/src/session/prompts.ts +25 -20
- package/src/lifecycle/concurrency-queue.ts +0 -63
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [16.1.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v16.0.0...pi-subagents-v16.1.0) (2026-06-14)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **pi-subagents:** add ConcurrencyLimiter ([#381](https://github.com/gotgenes/pi-packages/issues/381)) ([26f4203](https://github.com/gotgenes/pi-packages/commit/26f420337094d81d39bcc3e0522e12262c7767b7))
|
|
14
|
+
|
|
15
|
+
## [16.0.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v15.0.2...pi-subagents-v16.0.0) (2026-06-14)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### ⚠ BREAKING CHANGES
|
|
19
|
+
|
|
20
|
+
* replace-mode subagents (built-in Explore/Plan and any custom prompt_mode: replace agent) now inherit the parent system prompt as their base instead of a thin standalone header. The custom prompt is appended last and retains full control; the <sub_agent_context> bridge and <agent_instructions> wrapper are still omitted in replace mode.
|
|
21
|
+
|
|
22
|
+
### Performance Improvements
|
|
23
|
+
|
|
24
|
+
* include parent system prompt in replace mode ([#400](https://github.com/gotgenes/pi-packages/issues/400)) ([1cc25cf](https://github.com/gotgenes/pi-packages/commit/1cc25cf0106cbfe3015ceb69a820c745c07038e2))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Documentation
|
|
28
|
+
|
|
29
|
+
* describe replace-mode parent inheritance ([#400](https://github.com/gotgenes/pi-packages/issues/400)) ([6b6e61d](https://github.com/gotgenes/pi-packages/commit/6b6e61d649582c26d2c36edf67dfd1e35d87a802))
|
|
30
|
+
|
|
8
31
|
## [15.0.2](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v15.0.1...pi-subagents-v15.0.2) (2026-06-12)
|
|
9
32
|
|
|
10
33
|
|
package/README.md
CHANGED
|
@@ -113,14 +113,14 @@ The LLM receives structured `<task-notification>` XML for parsing, while the use
|
|
|
113
113
|
|
|
114
114
|
## Default Agent Types
|
|
115
115
|
|
|
116
|
-
| Type | Tools | Model | Prompt Mode | Description
|
|
117
|
-
| ----------------- | -------------------------- | ----------------------------- | ---------------------- |
|
|
118
|
-
| `general-purpose` | all 7 | inherit | `append` (parent twin) | Inherits the parent's full system prompt — same rules, CLAUDE.md, project conventions
|
|
119
|
-
| `Explore` | read, bash, grep, find, ls | haiku (falls back to inherit) | `replace`
|
|
120
|
-
| `Plan` | read, bash, grep, find, ls | inherit | `replace`
|
|
116
|
+
| Type | Tools | Model | Prompt Mode | Description |
|
|
117
|
+
| ----------------- | -------------------------- | ----------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------ |
|
|
118
|
+
| `general-purpose` | all 7 | inherit | `append` (parent twin) | Inherits the parent's full system prompt — same rules, CLAUDE.md, project conventions |
|
|
119
|
+
| `Explore` | read, bash, grep, find, ls | haiku (falls back to inherit) | `replace` | Fast codebase exploration (read-only); inherits the parent prompt as a base |
|
|
120
|
+
| `Plan` | read, bash, grep, find, ls | inherit | `replace` | Software architect for implementation planning (read-only); inherits the parent prompt as a base |
|
|
121
121
|
|
|
122
122
|
The `general-purpose` agent is a **parent twin** — it receives the parent's entire system prompt plus a sub-agent context bridge, so it follows the same rules the parent does.
|
|
123
|
-
Explore and Plan use
|
|
123
|
+
Explore and Plan use `replace` mode: the parent prompt is the cacheable base and their specialist read-only instructions are appended last, giving them the final say.
|
|
124
124
|
|
|
125
125
|
Default agents can be **ejected** (`/agents` → select agent → Eject) to export them as `.md` files for customization, **overridden** by creating a `.md` file with the same name (e.g. `.pi/agents/general-purpose.md`), or **disabled** per-project with `enabled: false` frontmatter.
|
|
126
126
|
|
|
@@ -172,23 +172,23 @@ subagent({ subagent_type: "auditor", prompt: "Review the auth module", descripti
|
|
|
172
172
|
|
|
173
173
|
All fields are optional — sensible defaults for everything.
|
|
174
174
|
|
|
175
|
-
| Field | Default | Description
|
|
176
|
-
| ------------------- | -------------- |
|
|
177
|
-
| `description` | filename | Agent description shown in tool listings
|
|
178
|
-
| `display_name` | — | Display name for UI (e.g. widget, agent list)
|
|
179
|
-
| `tools` | all 7 | Comma-separated built-in tools: read, bash, edit, write, grep, find, ls. `none` for no tools
|
|
180
|
-
| `extensions` | `true` | `true` to inherit all MCP/extension tools, `false` to disable
|
|
181
|
-
| `skills` | `true` | Inherit skills from parent. Can be a comma-separated list of skill names to preload (see [Skill Preloading](#skill-preloading) for discovery locations)
|
|
182
|
-
| `memory` | — | Persistent agent memory scope: `project`, `local`, or `user`. Auto-detects read-only agents
|
|
183
|
-
| `isolation` | — | Set to `worktree` to run in an isolated git worktree
|
|
184
|
-
| `model` | inherit parent | Model — `provider/modelId` or fuzzy name (`"haiku"`, `"sonnet"`)
|
|
185
|
-
| `thinking` | inherit | off, minimal, low, medium, high, xhigh
|
|
186
|
-
| `max_turns` | unlimited | Max agentic turns before graceful shutdown. `0` or omit for unlimited
|
|
187
|
-
| `prompt_mode` | `append` | `replace`:
|
|
188
|
-
| `inherit_context` | `false` | Fork parent conversation into agent
|
|
189
|
-
| `run_in_background` | `false` | Run in background by default
|
|
190
|
-
| `isolated` | `false` | No extension/MCP tools, only built-in
|
|
191
|
-
| `enabled` | `true` | Set to `false` to disable an agent (useful for hiding a default agent per-project)
|
|
175
|
+
| Field | Default | Description |
|
|
176
|
+
| ------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
177
|
+
| `description` | filename | Agent description shown in tool listings |
|
|
178
|
+
| `display_name` | — | Display name for UI (e.g. widget, agent list) |
|
|
179
|
+
| `tools` | all 7 | Comma-separated built-in tools: read, bash, edit, write, grep, find, ls. `none` for no tools |
|
|
180
|
+
| `extensions` | `true` | `true` to inherit all MCP/extension tools, `false` to disable |
|
|
181
|
+
| `skills` | `true` | Inherit skills from parent. Can be a comma-separated list of skill names to preload (see [Skill Preloading](#skill-preloading) for discovery locations) |
|
|
182
|
+
| `memory` | — | Persistent agent memory scope: `project`, `local`, or `user`. Auto-detects read-only agents |
|
|
183
|
+
| `isolation` | — | Set to `worktree` to run in an isolated git worktree |
|
|
184
|
+
| `model` | inherit parent | Model — `provider/modelId` or fuzzy name (`"haiku"`, `"sonnet"`) |
|
|
185
|
+
| `thinking` | inherit | off, minimal, low, medium, high, xhigh |
|
|
186
|
+
| `max_turns` | unlimited | Max agentic turns before graceful shutdown. `0` or omit for unlimited |
|
|
187
|
+
| `prompt_mode` | `append` | `replace`: parent prompt is the cacheable base; body is appended last with full control (no `<sub_agent_context>` bridge, no `<agent_instructions>` wrapper). `append`: parent prompt is the base; body is wrapped in `<agent_instructions>` and a sub-agent context bridge is injected (agent acts as a "parent twin") |
|
|
188
|
+
| `inherit_context` | `false` | Fork parent conversation into agent |
|
|
189
|
+
| `run_in_background` | `false` | Run in background by default |
|
|
190
|
+
| `isolated` | `false` | No extension/MCP tools, only built-in |
|
|
191
|
+
| `enabled` | `true` | Set to `false` to disable an agent (useful for hiding a default agent per-project) |
|
|
192
192
|
|
|
193
193
|
Frontmatter is authoritative.
|
|
194
194
|
If an agent file sets `model`, `thinking`, `max_turns`, `inherit_context`, `run_in_background`, `isolated`, or `isolation`, those values are locked for that agent.
|
|
@@ -491,7 +491,7 @@ Each has a corresponding upstream PR:
|
|
|
491
491
|
Upstream PR: [tintinweb/pi-subagents#71](https://github.com/tintinweb/pi-subagents/pull/71).
|
|
492
492
|
2. **Post-`bindExtensions` active-tool re-filter** (`src/agent-runner.ts`) — `runAgent` re-runs its active-tool filter after `session.bindExtensions(...)` so the `EXCLUDED_TOOL_NAMES` recursion guard applies to extension-registered tools (which join the active set during `bindExtensions`).
|
|
493
493
|
Upstream PR: [tintinweb/pi-subagents#72](https://github.com/tintinweb/pi-subagents/pull/72).
|
|
494
|
-
3. **`<active_agent>` system-prompt tag** (`src/prompts.ts`) — `buildAgentPrompt`
|
|
494
|
+
3. **`<active_agent>` system-prompt tag** (`src/prompts.ts`) — `buildAgentPrompt` includes `<active_agent name="${config.name}"/>` in every assembled child system prompt (both `replace` and `append` modes); the tag follows the cacheable parent-prompt prefix in both modes.
|
|
495
495
|
Downstream extensions like [`@gotgenes/pi-permission-system`](https://github.com/gotgenes/pi-permission-system) parse this tag to resolve per-agent `permission:` frontmatter inside the child session.
|
|
496
496
|
Upstream PR: [tintinweb/pi-subagents#73](https://github.com/tintinweb/pi-subagents/pull/73).
|
|
497
497
|
4. **Child-execution lifecycle events** (`src/lifecycle/child-lifecycle.ts`) — the child-session execution lifecycle is published as ordered events on `pi.events` (`subagents:child:spawning`, `session-created`, `completed`, `disposed`).
|
|
@@ -53,7 +53,7 @@ flowchart TB
|
|
|
53
53
|
subgraph lifecycle["Lifecycle domain"]
|
|
54
54
|
direction TB
|
|
55
55
|
SubagentManager["SubagentManager<br/>(spawn, abort, collection)"]
|
|
56
|
-
|
|
56
|
+
ConcurrencyLimiter["ConcurrencyLimiter<br/>(thunk admission gate)"]
|
|
57
57
|
CreateSubagentSession["createSubagentSession<br/>(assembly factory)"]
|
|
58
58
|
SubagentSession["SubagentSession<br/>(turn loop, steer, dispose)"]
|
|
59
59
|
Subagent["Subagent<br/>(status, behavior: abort/steer/run lifecycle)"]
|
|
@@ -283,7 +283,7 @@ src/
|
|
|
283
283
|
│ ├── subagent-session.ts born-complete child session: turn loop, steer, dispose
|
|
284
284
|
│ ├── turn-limits.ts normalizeMaxTurns (turn-count policy)
|
|
285
285
|
│ ├── subagent.ts owns full execution lifecycle (run, abort, steer, workspace)
|
|
286
|
-
│ ├── concurrency-
|
|
286
|
+
│ ├── concurrency-limiter.ts background admission gate: schedules run thunks FIFO against the limit
|
|
287
287
|
│ ├── parent-snapshot.ts immutable spawn-time parent state
|
|
288
288
|
│ ├── child-lifecycle.ts child-execution lifecycle event publisher
|
|
289
289
|
│ ├── workspace.ts workspace provider seam (generative extension surface)
|
|
@@ -360,7 +360,7 @@ They declare this package as an optional peer dependency and use dynamic import
|
|
|
360
360
|
|
|
361
361
|
- The three tools: `subagent` (née `Agent`), `get_subagent_result`, `steer_subagent`.
|
|
362
362
|
- `SubagentManager` — spawn, abort, resume, collection management, observer wiring.
|
|
363
|
-
- `
|
|
363
|
+
- `ConcurrencyLimiter` — background admission gate: schedules run thunks FIFO against a configurable concurrency limit.
|
|
364
364
|
- `createSubagentSession` — assembly factory: session creation and extension binding; returns a born-complete `SubagentSession`.
|
|
365
365
|
- `SubagentSession` — the born-complete child session: drives the turn loop (`runTurnLoop`/`resumeTurnLoop`), steers, and disposes (firing `disposed` at true session disposal, so resume executions are registry-detected).
|
|
366
366
|
- `child-lifecycle` — publishes the child-execution lifecycle (`spawning`, `session-created` before `bindExtensions()`, `completed`, `disposed`) on `pi.events`.
|
|
@@ -492,6 +492,10 @@ The governing rule — **no vacant hooks**: the architecture must _admit_ a seam
|
|
|
492
492
|
A provider seam with no consumer is a speculative abstraction that taxes every reader and that `fallow` flags as dead.
|
|
493
493
|
Latent extensibility is the deliverable; a vacant hook is not.
|
|
494
494
|
|
|
495
|
+
The [first-principles refinement](#first-principles-refinement-the-deeper-target) below sharpens this two-surface split.
|
|
496
|
+
The awaited, behavior-affecting lifecycle events (notably `session-created` before `bindExtensions`) are _hooks_ — the child's own extension surface applied recursively, generative because the core waits on the handler before deciding what to do next.
|
|
497
|
+
The observational surface then carries only fire-and-forget broadcasts of immutable snapshots, which no consumer can use to change the core.
|
|
498
|
+
|
|
495
499
|
### Core responsibilities (keep)
|
|
496
500
|
|
|
497
501
|
- **Agent definitions** — name, model, thinking, system prompt, tools list.
|
|
@@ -522,12 +526,90 @@ In the target state, pi-subagents publishes events and a provider seam; other pa
|
|
|
522
526
|
|
|
523
527
|
- **pi-permission-system** (observational) subscribes to child-session lifecycle events, detects subagent execution context in the child, and gates tool calls at runtime.
|
|
524
528
|
- **pi-subagents-worktrees** (generative) registers a `WorkspaceProvider` that prepares a git worktree at run-start and tears it down after, supplying the child's cwd.
|
|
525
|
-
- **pi-subagents-ui** (future) subscribes to the
|
|
529
|
+
- **pi-subagents-ui** (future, under reconsideration — see the [first-principles refinement](#first-principles-refinement-the-deeper-target)) subscribes to the broadcast and the query/behavior interfaces; whether the inherited widget, conversation viewer, and `/agents` menu survive is judged on our principles, not preserved by default.
|
|
526
530
|
- **Any future extension** (OTel, auditing, cost tracking) subscribes to the same events without pi-subagents knowing.
|
|
527
531
|
|
|
528
532
|
Composition test: 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.
|
|
529
533
|
|
|
530
|
-
This is achieved across phases: Phase 14 (strip policy), Phase 16 (invert dependencies — extensions on a minimal core), and Phase 18 (
|
|
534
|
+
This is achieved across phases: Phase 14 (strip policy), Phase 16 (invert dependencies — extensions on a minimal core), and Phase 18 (reconsider UI).
|
|
535
|
+
|
|
536
|
+
### First-principles refinement (the deeper target)
|
|
537
|
+
|
|
538
|
+
The two-surface model above is correct but coarse.
|
|
539
|
+
Pushing it against our own principles — construct complete, state owns its mutations, tell-don't-ask, dependency inversion — surfaces sharper boundaries that the current code draws through the middle of classes.
|
|
540
|
+
This subsection records the deeper target; the steps that realize it are sequenced in later phases.
|
|
541
|
+
|
|
542
|
+
#### `Subagent` is four conflated domains
|
|
543
|
+
|
|
544
|
+
The construction duality that motivates Phase 17 — a class that is simultaneously a passive record and an executor — is only the two most visible of four domains fused into one class.
|
|
545
|
+
Pulling each apart by asking "who changes this, how often, and who needs to know" surfaces:
|
|
546
|
+
|
|
547
|
+
1. **Lifecycle state** — status, result, error, timestamps.
|
|
548
|
+
Owned by the subagent; transitions are rare and meaningful; the right outward shape is an immutable snapshot announced on change.
|
|
549
|
+
2. **Metrics** — tool uses, token usage, compaction count.
|
|
550
|
+
These are not lifecycle state; they are a projection aggregated over the child session's event stream.
|
|
551
|
+
`record-observer` already computes them — its only error is writing the aggregate back onto the subagent.
|
|
552
|
+
3. **The hook surface** — the points where an extension alters or augments the child before and around its run.
|
|
553
|
+
This is the child session's own extension binding (see below), not data on the subagent.
|
|
554
|
+
4. **Result delivery** — whether the parent has consumed the result, when to nudge, how the result reaches the caller.
|
|
555
|
+
The homeless `notification.resultConsumed` field belongs to this domain, not to execution.
|
|
556
|
+
|
|
557
|
+
The ~20 optional constructor fields and the runtime `run()` throws are the pressure these four domains exert on one class.
|
|
558
|
+
Separating them is what makes the Phase 17 steps fall out rather than fight back.
|
|
559
|
+
|
|
560
|
+
#### The subagent is a recursive Pi
|
|
561
|
+
|
|
562
|
+
A subagent is a child Pi session: created with `createAgentSession`, then `bindExtensions`.
|
|
563
|
+
Its extension surface is therefore Pi's extension surface applied recursively — not a bespoke event bus.
|
|
564
|
+
What the current doc calls "awaited, ordered lifecycle events" are not observations; they are **hooks**, structurally identical to Pi's own (`session_start`, `tool_execution_start`).
|
|
565
|
+
The tell is the awaiting: the core waits for the handler because the handler's completion changes what the core does next — an extension registers before the child binds.
|
|
566
|
+
A handler that can change subsequent behavior is generative, not observational, whatever we name the channel.
|
|
567
|
+
|
|
568
|
+
This splits the current "lifecycle events" surface cleanly in two:
|
|
569
|
+
|
|
570
|
+
1. **Broadcast** (observational, fire-and-forget) — "this happened; react if you want; you cannot change anything."
|
|
571
|
+
Carries immutable snapshots for telemetry, notification, and any renderer.
|
|
572
|
+
No consumer holds a live `Subagent`.
|
|
573
|
+
2. **Hooks** (generative, awaited, ordered) — the recursive Pi extension surface where workspace, permissions, and future concerns attach to the child.
|
|
574
|
+
The `WorkspaceProvider` is one _typed_ hook; the general form is "be an extension of the child session."
|
|
575
|
+
|
|
576
|
+
The "no vacant hooks" rule still governs the generative side: admit the surface, ship a hook only when a real consumer exists.
|
|
577
|
+
|
|
578
|
+
#### Reactive versus discrete (not internal versus external)
|
|
579
|
+
|
|
580
|
+
The axis that decides push versus pull is whether a need is reactive or discrete — never whether the consumer is in-package or out.
|
|
581
|
+
|
|
582
|
+
- **Reactive** (ambient state that changes underneath you) → subscribe to the broadcast; be told.
|
|
583
|
+
The state-owner announces; the consumer maintains its own read-model; nobody pulls.
|
|
584
|
+
- **Discrete** (a one-shot question: current value, full transcript) → pull a query.
|
|
585
|
+
`get_subagent_result`, opening a transcript, and the external `SubagentsService.getRecord` are queries by nature and stay pull, in-package or not.
|
|
586
|
+
|
|
587
|
+
Behavior is a third interface: **tell by id, with outcomes**.
|
|
588
|
+
`steer` and `abort` own their own rules — a non-running agent rejects a steer from inside `steer`, not via a caller's status pre-check — so coordinators never ask-then-tell.
|
|
589
|
+
|
|
590
|
+
#### Consequences
|
|
591
|
+
|
|
592
|
+
Two consequences fall straight out, and both cut scope.
|
|
593
|
+
|
|
594
|
+
1. **The activity/metrics push tier is provisional.**
|
|
595
|
+
Its only reactive consumer is the inherited widget.
|
|
596
|
+
Treated from first principles, metrics are accumulated by an observer, exposed as a discrete query, and folded into the completion snapshot — so the high-frequency stream may not need to exist at all.
|
|
597
|
+
We do not contort the core's event design to feed an inherited consumer.
|
|
598
|
+
2. **Phase 18 is "reconsider the UI," not "extract the UI."**
|
|
599
|
+
The widget and `/agents` menu predate the fork; they are consumers to be judged on our principles, not requirements to preserve.
|
|
600
|
+
If a UI survives, it survives as a reactive consumer of the broadcast and a caller of the query/behavior interfaces — built on our terms, possibly smaller, possibly removed.
|
|
601
|
+
|
|
602
|
+
#### Sibling packages follow the same discipline
|
|
603
|
+
|
|
604
|
+
`@gotgenes/pi-permission-system` is one of these hooks, and it is subject to the same scrutiny.
|
|
605
|
+
Its boundaries deserve the same first-principles treatment: surface its conflated domains, distinguish what it observes from what it injects, and prefer being told over asking.
|
|
606
|
+
The recursion principle means a consumer's internal design is not exempt because it lives in another package — the same axes (reactive versus discrete, hook versus broadcast, construct complete) apply across the seam.
|
|
607
|
+
|
|
608
|
+
#### How we find these boundaries
|
|
609
|
+
|
|
610
|
+
The boundaries above were not deduced top-down; they were surfaced by friction.
|
|
611
|
+
Each place the target got _harder_ to test marked a domain seam drawn through the middle of a class.
|
|
612
|
+
That method — testability friction as a boundary probe, with its limits — is recorded in the `improvement-discovery` skill so it outlives this phase.
|
|
531
613
|
|
|
532
614
|
## Current structural analysis
|
|
533
615
|
|
|
@@ -768,9 +850,13 @@ See [phase-16-invert-dependencies.md](history/phase-16-invert-dependencies.md) f
|
|
|
768
850
|
|
|
769
851
|
## Improvement roadmap (Phase 17 — core consolidation)
|
|
770
852
|
|
|
771
|
-
Phase 17 consolidates the core's remaining structural debt before the UI
|
|
853
|
+
Phase 17 consolidates the core's remaining structural debt before the UI reconsideration (now Phase 18).
|
|
772
854
|
The findings come from the standard discovery pass — fallow suite, entry-point trace, design-review checklist, and test-constructibility audit — run after Phase 16 landed.
|
|
773
855
|
|
|
856
|
+
Phase 17 is the consolidation slice of the [first-principles refinement](#first-principles-refinement-the-deeper-target), not the full domain split.
|
|
857
|
+
It lands the first cut of the lifecycle-state domain (Step 2's `SubagentState`) plus the wiring, queue, and duplication cleanups.
|
|
858
|
+
The fuller four-domain split — metrics as a projection, result delivery as its own domain, the hook/broadcast reclassification, and the push/pull (DIP) inversion — is recorded in the refinement and sequenced into later phases.
|
|
859
|
+
|
|
774
860
|
### Findings summary
|
|
775
861
|
|
|
776
862
|
Updated health metrics (fallow, package-wide including tests):
|
|
@@ -793,6 +879,7 @@ The syntactic metrics are healthy and stable — the remaining debt is structura
|
|
|
793
879
|
`SubagentInit` carries ~20 fields, nearly all optional with "required for run(), optional for tests" semantics, and `run()` compensates with runtime throws ("not configured for execution").
|
|
794
880
|
This violates principle 8 (construct complete): the class is simultaneously a passive record (tests build display-only snapshots) and an executor (production wires factory, observer, run config, workspace provider).
|
|
795
881
|
The symptoms are in the tests: external writes `record.promise = …` (manager, queue callback, four test files) and `record.notification = new NotificationState(…)` (seven test sites) are output-argument smells on fields the object should own.
|
|
882
|
+
This duality is the two most visible of four domains fused into `Subagent`; Phase 17 resolves it (Step 2) and defers the remaining split (metrics, result delivery) to a later phase per the [first-principles refinement](#first-principles-refinement-the-deeper-target).
|
|
796
883
|
2. **Wiring debt in `index.ts`.**
|
|
797
884
|
Two forward references (settings → queue, queue → manager) are replicated with an `eslint-disable prefer-const` dance in `test/lifecycle/subagent-manager.test.ts`; the queue's start callback (`record.promise = record.run()` after a status check) is duplicated verbatim between `index.ts` and the test helper.
|
|
798
885
|
A ~70-line inline `SubagentManagerObserver` literal mixes three concerns (event emission, `appendEntry` persistence, notification dispatch).
|
|
@@ -809,7 +896,7 @@ Priority = Impact × (6 − Risk).
|
|
|
809
896
|
| Step | Title | Category | Impact | Risk | Priority |
|
|
810
897
|
| ---- | ------------------------------------------------------------------------------------ | -------- | ------ | ---- | -------- |
|
|
811
898
|
| 1 | Replace ConcurrencyQueue with a thunk-based ConcurrencyLimiter | A/C | 4 | 2 | 16 |
|
|
812
|
-
| 2 |
|
|
899
|
+
| 2 | Extract `SubagentState`; make `Subagent` execution deps mandatory | B/D | 4 | 3 | 12 |
|
|
813
900
|
| 3 | Encapsulate run start and notification attachment on Subagent | C | 3 | 2 | 12 |
|
|
814
901
|
| 4 | Extract run-listener and workspace-bracket collaborators from Subagent | B/C | 3 | 2 | 12 |
|
|
815
902
|
| 5 | Extract the manager observer from index.ts into a class | B/E | 3 | 2 | 12 |
|
|
@@ -818,7 +905,7 @@ Priority = Impact × (6 − Risk).
|
|
|
818
905
|
| 8 | Consolidate UI and tools test fixtures | D | 2 | 1 | 10 |
|
|
819
906
|
| 9 | Resolve the cross-package settings-loader duplication | A | 2 | 2 | 8 |
|
|
820
907
|
|
|
821
|
-
#### Step 1 — Replace ConcurrencyQueue with a thunk-based ConcurrencyLimiter ([#381])
|
|
908
|
+
#### Step 1 — Replace ConcurrencyQueue with a thunk-based ConcurrencyLimiter ([#381]) ✅ Complete
|
|
822
909
|
|
|
823
910
|
- Targets: `src/lifecycle/concurrency-queue.ts` (→ `concurrency-limiter.ts`), `src/lifecycle/subagent-manager.ts`, `src/index.ts`, `test/lifecycle/concurrency-queue.test.ts`, `test/lifecycle/subagent-manager.test.ts`.
|
|
824
911
|
- Smell: Category C (forward references: the queue's ID-registry design forces a start callback that reaches back into the manager, duplicated between `index.ts` and the test helper) and Category A (dual counting: the queue's `running` counter is fed by `markStarted`/`markFinished` relays in the manager's observer, mirroring state the agents already carry).
|
|
@@ -827,19 +914,25 @@ Priority = Impact × (6 − Risk).
|
|
|
827
914
|
The settings `onMaxConcurrentChanged` hook wires to `limiter.recheck()` in `index.ts`; `dispose()` calls `limiter.clear()` to drop pending thunks.
|
|
828
915
|
- Outcome: dependency direction is strictly manager → limiter (no callback back-edge; the `prefer-const` eslint-disable in the test helper is deleted); the observer's two queue relays are gone; every spawned agent has a `promise` at spawn, collapsing `waitForAll`'s `while (true)` drain loop and its eslint-disable.
|
|
829
916
|
|
|
830
|
-
#### Step 2 —
|
|
917
|
+
#### Step 2 — Extract `SubagentState`; make `Subagent` execution deps mandatory ([#373])
|
|
831
918
|
|
|
832
|
-
- Targets: `src/lifecycle/subagent.ts` (
|
|
919
|
+
- Targets: `src/lifecycle/subagent.ts` (state fields, transition/accumulation methods, constructor, `run()` guards), `src/lifecycle/subagent-manager.ts` (`spawn`), `test/helpers/make-subagent.ts`, `test/lifecycle/subagent.test.ts`, `test/observation/record-observer.test.ts`.
|
|
833
920
|
- Smell: Category B (god interface — ~20 fields) and Category D (constructibility: "optional for tests" fields with compensating runtime throws).
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
921
|
+
The record/executor duality is the two most visible of the four conflated domains (see [First-principles refinement](#first-principles-refinement-the-deeper-target)).
|
|
922
|
+
- Change: extract the passive-record state — status, result, error, timestamps, and the stats (toolUses, lifetimeUsage, compactionCount) — into a `SubagentState` value object that owns the transition and accumulation methods.
|
|
923
|
+
`Subagent` holds one privately; its existing getters and `markX`/`incrementX`/`addUsage` methods become one-line delegations, so the ~40 read sites and the mutation callers are unchanged.
|
|
924
|
+
This is not reach-through: `SubagentState` is a private owned value, not a foreign collaborator (contrast [#277], which removed reach-through to the raw SDK session).
|
|
925
|
+
With the readable state extracted, the remaining execution inputs (snapshot, prompt, model, maxTurns, thinkingLevel, parentSession, signal, createSubagentSession, observer, getRunConfig, getWorkspaceProvider, baseCwd) collapse into a single **mandatory** `SubagentExecution` collaborator: production always supplies it (the one `spawn()` site), the passive-record construction moves entirely into `make-subagent.ts`, and `run()`'s two "not configured" throws vanish by construction.
|
|
926
|
+
- Outcome: state-machine and observer tests target `SubagentState` directly (no stub execution); `Subagent` is construct-complete with no optional execution fields and no runtime throws (grep-verifiable: no "not configured for execution" in `subagent.ts`); the record-vs-executor duality is resolved, not type-encoded.
|
|
927
|
+
- Scope boundary: stats stay on `SubagentState` for now.
|
|
928
|
+
Hoisting **metrics** into a projection over the child session's event stream and extracting **result delivery** (`notification`/`resultConsumed`) into its own domain are the remaining two of the four domains, deferred to a later phase per the refinement.
|
|
929
|
+
- The issue ([#373]) is filed under the prior "decompose `SubagentInit` into present-or-absent bags" framing; update its description to this stronger target before implementation.
|
|
837
930
|
|
|
838
931
|
#### Step 3 — Encapsulate run start and notification attachment on Subagent ([#374])
|
|
839
932
|
|
|
840
933
|
- Targets: `src/lifecycle/subagent.ts`, `src/lifecycle/subagent-manager.ts`, `test/tools/get-result-tool.test.ts`, `test/lifecycle/subagent-manager.test.ts`, `test/service/service-adapter.test.ts`, `test/observation/notification.test.ts`, `test/helpers/make-subagent.ts`.
|
|
841
934
|
- Smell: Category C — output arguments: external writes to `record.promise` (3 production/test sites) and `record.notification` (7 test sites).
|
|
842
|
-
- Change: add `Subagent.start()` that runs and stores its own promise (plus an awaitable accessor for `spawnAndWait`/`waitForAll`); make `promise` and `notification` externally read-only; tests attach notification state through `
|
|
935
|
+
- Change: add `Subagent.start()` that runs and stores its own promise (plus an awaitable accessor for `spawnAndWait`/`waitForAll`); make `promise` and `notification` externally read-only; tests attach notification state through `SubagentExecution.parentSession.toolCallId` or a dedicated options field.
|
|
843
936
|
- Outcome: zero external writes to `Subagent` fields outside its own methods (grep-verifiable: `\.promise =` and `\.notification =` appear only inside `subagent.ts`).
|
|
844
937
|
|
|
845
938
|
#### Step 4 — Extract run-listener and workspace-bracket collaborators from Subagent ([#375])
|
|
@@ -865,7 +958,7 @@ Priority = Impact × (6 − Risk).
|
|
|
865
958
|
|
|
866
959
|
#### Step 7 — Consolidate lifecycle test fixtures ([#378])
|
|
867
960
|
|
|
868
|
-
- Targets: `test/lifecycle/subagent-manager.test.ts` (766 LOC), `test/lifecycle/subagent.test.ts`, `test/lifecycle/subagent-session.test.ts`, `test/lifecycle/create-subagent-session.test.ts`, `test/lifecycle/create-subagent-session-extension-tools.test.ts`, `test/lifecycle/concurrency-
|
|
961
|
+
- Targets: `test/lifecycle/subagent-manager.test.ts` (766 LOC), `test/lifecycle/subagent.test.ts`, `test/lifecycle/subagent-session.test.ts`, `test/lifecycle/create-subagent-session.test.ts`, `test/lifecycle/create-subagent-session-extension-tools.test.ts`, `test/lifecycle/concurrency-limiter.test.ts`, `test/helpers/`.
|
|
869
962
|
- Smell: Category D — fallow reports five clone families across the lifecycle tests.
|
|
870
963
|
- Change: extract the repeated spawn/run/factory arrangements into shared helpers, migrating incrementally (lift-and-shift, never a single-step rewrite of a large test file).
|
|
871
964
|
- Outcome: lifecycle clone families 5 → ≤ 1; package test duplication below 600 lines.
|
|
@@ -890,7 +983,7 @@ Priority = Impact × (6 − Risk).
|
|
|
890
983
|
```mermaid
|
|
891
984
|
flowchart TB
|
|
892
985
|
S1["Step 1 (#381)<br/>ConcurrencyLimiter replacement"]
|
|
893
|
-
S2["Step 2 (#373)<br/>
|
|
986
|
+
S2["Step 2 (#373)<br/>SubagentState extraction"]
|
|
894
987
|
S3["Step 3 (#374)<br/>Encapsulate start + notification"]
|
|
895
988
|
S4["Step 4 (#375)<br/>Run collaborators extraction"]
|
|
896
989
|
S5["Step 5 (#376)<br/>Observer class from index.ts"]
|
|
@@ -933,7 +1026,7 @@ Detailed records are preserved in per-phase history files:
|
|
|
933
1026
|
| 3 | Remove group-join, RPC; replace output-file | Complete | [phase-3-remove-rpc-groupjoin.md](history/phase-3-remove-rpc-groupjoin.md) |
|
|
934
1027
|
| 4 | Implement and publish SubagentsService | Complete | [phase-4-implement-service.md](history/phase-4-implement-service.md) |
|
|
935
1028
|
| 5 | Decompose index.ts | Complete | [phase-5-decompose-index.md](history/phase-5-decompose-index.md) |
|
|
936
|
-
| 6 | Extract UI to separate package | Deferred → Phase
|
|
1029
|
+
| 6 | Extract UI to separate package | Deferred → Phase 18 | — |
|
|
937
1030
|
| 7 | Encapsulation and dependency narrowing | Complete | [phase-7-encapsulation.md](history/phase-7-encapsulation.md) |
|
|
938
1031
|
| 8 | Testability, display extraction, menu decomposition | Complete | [phase-8-testability.md](history/phase-8-testability.md) |
|
|
939
1032
|
| 9 | Observation consolidation, ctx elimination | Complete | [phase-9-observation-ctx.md](history/phase-9-observation-ctx.md) |
|
|
@@ -945,7 +1038,7 @@ Detailed records are preserved in per-phase history files:
|
|
|
945
1038
|
| 15 | Domain model evolution | Complete | [phase-15-domain-model-evolution.md](history/phase-15-domain-model-evolution.md) |
|
|
946
1039
|
| 16 | Invert dependencies (extensions on a minimal core) | Complete | [phase-16-invert-dependencies.md](history/phase-16-invert-dependencies.md) |
|
|
947
1040
|
| 17 | Core consolidation | Planned | — |
|
|
948
|
-
| 18 |
|
|
1041
|
+
| 18 | Reconsider UI (first principles) | Planned | — |
|
|
949
1042
|
|
|
950
1043
|
### Structural refactoring issues
|
|
951
1044
|
|