@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.
- package/CHANGELOG.md +2705 -0
- package/LICENSE +21 -0
- package/README.md +503 -0
- package/dist/public.d.ts +331 -0
- package/dist/settings.d.ts +82 -0
- package/docs/architecture/architecture.md +1566 -0
- package/docs/architecture/client-server-opportunities.md +127 -0
- package/docs/architecture/history/phase-1-api-boundary.md +8 -0
- package/docs/architecture/history/phase-10-structural-decomposition.md +141 -0
- package/docs/architecture/history/phase-11-closure-to-class.md +100 -0
- package/docs/architecture/history/phase-12-complexity-test-fixtures.md +55 -0
- package/docs/architecture/history/phase-13-remaining-smells.md +88 -0
- package/docs/architecture/history/phase-14-strip-policy.md +49 -0
- package/docs/architecture/history/phase-15-domain-model-evolution.md +73 -0
- package/docs/architecture/history/phase-16-invert-dependencies.md +144 -0
- package/docs/architecture/history/phase-17-core-consolidation.md +214 -0
- package/docs/architecture/history/phase-18-reconsider-ui.md +166 -0
- package/docs/architecture/history/phase-19-implement-ui-decisions.md +282 -0
- package/docs/architecture/history/phase-2-remove-scheduling.md +9 -0
- package/docs/architecture/history/phase-20-result-delivery.md +245 -0
- package/docs/architecture/history/phase-21-classification-model-boundary.md +107 -0
- package/docs/architecture/history/phase-3-remove-rpc-groupjoin.md +11 -0
- package/docs/architecture/history/phase-4-implement-service.md +8 -0
- package/docs/architecture/history/phase-5-decompose-index.md +42 -0
- package/docs/architecture/history/phase-7-encapsulation.md +173 -0
- package/docs/architecture/history/phase-8-testability.md +103 -0
- package/docs/architecture/history/phase-9-observation-ctx.md +122 -0
- package/docs/comparison-with-upstream.md +77 -0
- package/docs/configuration.md +364 -0
- package/docs/decisions/0001-deferred-patches.md +80 -0
- package/docs/decisions/0002-extensions-on-a-minimal-core.md +125 -0
- package/docs/decisions/0003-publish-bundled-type-declarations.md +71 -0
- package/docs/decisions/0004-reconsider-ui-direction.md +279 -0
- package/docs/decisions/0005-subagent-record-admission-policy.md +106 -0
- package/docs/decisions/0006-inherited-prompt-is-identity-only.md +104 -0
- package/docs/decisions/0007-transcript-viewer-is-not-an-overlay.md +228 -0
- package/docs/decisions/0008-inherited-region-is-shared-parts.md +81 -0
- package/docs/decisions/0009-portable-inheritance-is-provider-scoped.md +116 -0
- package/package.json +91 -0
- package/src/config/agent-types.ts +135 -0
- package/src/config/custom-agents.ts +151 -0
- package/src/config/default-agents.ts +121 -0
- package/src/config/invocation-config.ts +167 -0
- package/src/config/thinking-level.ts +58 -0
- package/src/debug.ts +14 -0
- package/src/handlers/index.ts +3 -0
- package/src/handlers/interrupt.ts +58 -0
- package/src/handlers/lifecycle.ts +71 -0
- package/src/handlers/widget-events.ts +49 -0
- package/src/index.ts +292 -0
- package/src/layered-settings.ts +105 -0
- package/src/lifecycle/child-lifecycle.ts +115 -0
- package/src/lifecycle/child-shutdown.ts +105 -0
- package/src/lifecycle/concurrency-limiter.ts +55 -0
- package/src/lifecycle/create-subagent-session.ts +335 -0
- package/src/lifecycle/parent-snapshot.ts +119 -0
- package/src/lifecycle/run-listeners.ts +37 -0
- package/src/lifecycle/selection-scope.ts +116 -0
- package/src/lifecycle/spawn-selection.ts +259 -0
- package/src/lifecycle/subagent-manager.ts +546 -0
- package/src/lifecycle/subagent-session.ts +347 -0
- package/src/lifecycle/subagent-state.ts +404 -0
- package/src/lifecycle/subagent.ts +885 -0
- package/src/lifecycle/turn-limits.ts +13 -0
- package/src/lifecycle/usage.ts +60 -0
- package/src/lifecycle/workspace-bracket.ts +76 -0
- package/src/lifecycle/workspace.ts +46 -0
- package/src/observation/composite-subagent-observer.ts +74 -0
- package/src/observation/notification.ts +430 -0
- package/src/observation/outcome-delivery.ts +239 -0
- package/src/observation/record-observer.ts +78 -0
- package/src/observation/renderer.ts +161 -0
- package/src/observation/subagent-events-observer.ts +148 -0
- package/src/runtime.ts +137 -0
- package/src/service/service-adapter.ts +201 -0
- package/src/service/service.ts +246 -0
- package/src/session/ask-parent-tool.ts +69 -0
- package/src/session/content-items.ts +53 -0
- package/src/session/context.ts +80 -0
- package/src/session/conversation.ts +49 -0
- package/src/session/env.ts +40 -0
- package/src/session/model-resolver.ts +126 -0
- package/src/session/notify-parent-tool.ts +83 -0
- package/src/session/package-exclusions.ts +75 -0
- package/src/session/prompts.ts +231 -0
- package/src/session/provider-inheritance.ts +56 -0
- package/src/session/selection-catalogue.ts +143 -0
- package/src/session/session-config.ts +202 -0
- package/src/session/session-dir.ts +38 -0
- package/src/settings.ts +447 -0
- package/src/tools/agent-tool.ts +305 -0
- package/src/tools/background-spawner.ts +83 -0
- package/src/tools/foreground-runner.ts +159 -0
- package/src/tools/get-result-renderer.ts +119 -0
- package/src/tools/get-result-report.ts +84 -0
- package/src/tools/get-result-tool.ts +192 -0
- package/src/tools/helpers.ts +118 -0
- package/src/tools/result-renderer.ts +153 -0
- package/src/tools/spawn-config.ts +192 -0
- package/src/tools/steer-tool.ts +109 -0
- package/src/types.ts +143 -0
- package/src/ui/agent-widget.ts +333 -0
- package/src/ui/bounded-lines.ts +45 -0
- package/src/ui/display.ts +180 -0
- package/src/ui/glyphs.ts +62 -0
- package/src/ui/session-navigation.ts +150 -0
- package/src/ui/session-navigator.ts +255 -0
- package/src/ui/subagents-settings.ts +179 -0
- package/src/ui/transcript-content.ts +374 -0
- package/src/ui/widget-renderer.ts +301 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Client-server architecture: opportunities for pi-subagents
|
|
2
|
+
|
|
3
|
+
This note is forward-looking.
|
|
4
|
+
Pi's client-server split is **not on the near-term roadmap** — it is a long-term vision recorded in Mario Zechner's [pi session sync unification][session-sync] plan.
|
|
5
|
+
This document captures what that architecture would let pi-subagents do, so the opportunity is on record alongside the existing [Architecture direction](./architecture.md#architecture-direction) section.
|
|
6
|
+
|
|
7
|
+
It assumes the session-sync plan's shape:
|
|
8
|
+
|
|
9
|
+
- `AgentSession` owns all authoritative intra-session state and emits all sync events.
|
|
10
|
+
- `AgentSessionServer` owns multiple `AgentSession` runtimes, session lifecycle, client subscriptions, snapshots, and deltas.
|
|
11
|
+
- `AgentSessionClient` is the only thing a renderer (interactive mode, web mode) talks to.
|
|
12
|
+
- The sync model is **snapshot plus delta**: a client joins a session, receives one canonical snapshot, then a stream of deltas.
|
|
13
|
+
- Commands go up; state deltas come down.
|
|
14
|
+
- A client can `watch` (read-only) or `join` (interactive) a session, and can hold synchronized caches for many sessions at once.
|
|
15
|
+
- Session lifecycle (`new`, `resume`, `fork`, `import`, `join`, `leave`, `watch`, `unwatch`) is normal server API, not hidden local runtime replacement.
|
|
16
|
+
|
|
17
|
+
## The key realization
|
|
18
|
+
|
|
19
|
+
A subagent **is** a child `AgentSession`. pi-subagents already creates one via `createSubagentSession`, drives its turn loop through `SubagentSession`, and disposes it.
|
|
20
|
+
|
|
21
|
+
Today that child session is an in-process object visible only through machinery pi-subagents had to invent because Pi has no session-sync client:
|
|
22
|
+
|
|
23
|
+
- `record-observer` re-derives live activity from raw session events.
|
|
24
|
+
- the 80 ms `SubagentManager.listAgents()` widget poll.
|
|
25
|
+
- the [#277] Law-of-Demeter accessors (`Subagent.getConversation()`, `.messages`, `.subscribeToUpdates()`, `.getContextPercent()`) that re-expose session internals.
|
|
26
|
+
- the bespoke `ConversationViewer`, and [ADR-0004]'s replacement, native session navigation.
|
|
27
|
+
- [ADR-0004]'s dual-source-by-liveness split (tracked agent → in-memory record; evicted → file snapshot).
|
|
28
|
+
|
|
29
|
+
Every one of those is pi-subagents reinventing an `AgentSessionClient`.
|
|
30
|
+
The session-sync plan supplies the real one.
|
|
31
|
+
If subagent child sessions are registered as first-class sessions in the `AgentSessionServer`, all three target capabilities reduce to the same primitive the operator's own session already uses: `watch`/`join` a session id, receive a snapshot, then a delta stream.
|
|
32
|
+
|
|
33
|
+
## Capability 1 — viewing live subagent sessions
|
|
34
|
+
|
|
35
|
+
`watchSession(subagentSessionId)` gives the operator's client a canonical `SessionSnapshot` (current streaming message, pending tool executions with partial results, queue contents, context usage, active tools) followed by the live delta stream (`message_update`, `tool_execution_start/update/end`, `turn_start/end`).
|
|
36
|
+
|
|
37
|
+
What changes versus today:
|
|
38
|
+
|
|
39
|
+
1. **Late-join correctness.**
|
|
40
|
+
The snapshot carries in-flight state, so an operator attaching to an already-running subagent immediately sees its current streaming message and pending tools — not just future events.
|
|
41
|
+
Today the conversation viewer only catches future deltas plus whatever happens to be in the in-memory record.
|
|
42
|
+
2. **The widget and viewer become thin renderers** of synchronized session state.
|
|
43
|
+
The 80 ms poll, `record-observer`, and the [#277] accessors disappear — they were all substitutes for `subscribeSession`.
|
|
44
|
+
3. **A unified, reconnect-safe live session list.**
|
|
45
|
+
The global event scope (`session_created`, `session_status_changed` idle/busy) means the operator sees every subagent the instant it spawns, with a live status badge per agent — replacing both `listAgents()` polling and the hand-rolled `subagents:*` broadcast tier.
|
|
46
|
+
4. **Multi-session client state** lets one operator client hold synchronized caches for N subagents at once.
|
|
47
|
+
This is the clean foundation for the parallel-agent navigation gesture that [ADR-0004]'s spike (entry-criterion #3) struggled to design — tabbed or split views of multiple live subagents fall out of the model with no redesign.
|
|
48
|
+
|
|
49
|
+
## Capability 2 — viewing suspended subagent sessions
|
|
50
|
+
|
|
51
|
+
Today "suspended" is a dead end: once a background subagent completes and `disposeSession()` fires, the live `AgentSession` is gone, and the only artifact is the persisted JSONL read one-shot via `parseSessionEntries`.
|
|
52
|
+
That is a static file dump with no liveness and no path back to interaction.
|
|
53
|
+
|
|
54
|
+
Under the server model, the live/suspended distinction collapses at the client.
|
|
55
|
+
Session lifecycle is normal server API (`resume_session`, `join_session`, `import_session`), so a suspended subagent is just a session the server can rehydrate from `SessionManager` on demand:
|
|
56
|
+
|
|
57
|
+
1. The operator calls `joinSession(id)` (or the server lazily reloads the `AgentSession` from JSONL when a client joins a dormant session), gets a snapshot, and renders it through the same components as a live session.
|
|
58
|
+
[ADR-0004]'s two-code-path split (in-memory record vs file snapshot) stops being the client's problem — whether the session is resident or rehydrated is the server's private concern.
|
|
59
|
+
2. A genuinely paused subagent (one parked by the `ConcurrencyLimiter` awaiting capacity, or deliberately held) becomes representable: the server holds the runtime idle, the operator can `watch` its frozen state, and then sees it transition to `busy` via `session_status_changed` when capacity frees up.
|
|
60
|
+
3. Subagents can outlive the operator's TUI.
|
|
61
|
+
Because the server is a separate process, if the operator's interactive client restarts it reconnects and re-lists or re-joins subagent sessions that survived — impossible in today's single-process model.
|
|
62
|
+
|
|
63
|
+
## Capability 3 — operators interacting via an editor and submitting messages
|
|
64
|
+
|
|
65
|
+
Today the only inbound channel to a running subagent is `steer_subagent` — a single buffer-or-deliver message through `Subagent.steer`.
|
|
66
|
+
No editor, no takeover, no real conversation.
|
|
67
|
+
|
|
68
|
+
The plan's `join` versus `watch` split is the right primitive:
|
|
69
|
+
|
|
70
|
+
1. **`watch` is read-only** (Capability 1).
|
|
71
|
+
**`join` is interactive participation**: the operator's client sends session commands scoped to the subagent's session id — `prompt`, `abort`, `set model`, `set thinking level`, `set active tools`, `navigate tree`, `compact`, `run bash`.
|
|
72
|
+
2. **Editor flow.**
|
|
73
|
+
The operator opens the subagent session in their client, types in the editor (editor text, focus, and overlays are explicitly client-local UI state, never synchronized), and submits a `prompt` command targeting the subagent's session id.
|
|
74
|
+
The command goes up; the server applies it to the subagent's `AgentSession` (which already owns prompt submission, queued steering, and follow-up messages); canonical deltas flow back down to all subscribers.
|
|
75
|
+
This is precisely "commands go up, state deltas come down," applied to a subagent session instead of the operator's own.
|
|
76
|
+
3. **`steer` generalizes into the full command surface.**
|
|
77
|
+
pi-subagents' bespoke buffer-or-deliver logic (`Subagent.steer` rejecting when not running) is subsumed by `AgentSession`'s queued-steering machinery, and the custom buffering becomes redundant.
|
|
78
|
+
4. **Multiple clients on one session.**
|
|
79
|
+
The plan supports this explicitly, with deltas broadcast to all subscribers including the initiator, so an operator can jump into a subagent the parent agent spawned, inject guidance, and the parent's view stays consistent.
|
|
80
|
+
5. **Fork and rewind, not just append.**
|
|
81
|
+
`navigate tree` plus `fork_session` let an operator rewind a subagent or fork it at an entry — full session editing, far beyond appending a steer message.
|
|
82
|
+
|
|
83
|
+
## What pi-subagents itself becomes
|
|
84
|
+
|
|
85
|
+
This validates and sharpens the direction the architecture doc is already heading. pi-subagents' stated core job — spawn a child session derived from the parent, run the turn loop, track and stream and collect the result, gate concurrency, support resume, and publish its lifecycle — is almost exactly the `AgentSessionServer`'s job for child sessions.
|
|
86
|
+
|
|
87
|
+
- **The observation tier dissolves.**
|
|
88
|
+
This confirms [Phase 18]'s own finding that "the activity/metrics push tier is provisional" — under the server it genuinely does not need to exist in pi-subagents.
|
|
89
|
+
- **The recursion principle gets its real substrate.**
|
|
90
|
+
The architecture doc's "a subagent is a recursive Pi" framing maps directly onto the plan's session model: a subagent session is just another session in the server, with the same snapshot, delta, and command surface.
|
|
91
|
+
- **pi-subagents keeps only its semantic layer** — agent type, description, result text, token totals, concurrency admission, and the recursion guard — the things a generic session server does not know.
|
|
92
|
+
The liveness and activity channel rides on session sync instead of a hand-rolled broadcast.
|
|
93
|
+
|
|
94
|
+
### Bespoke machinery the server would retire
|
|
95
|
+
|
|
96
|
+
| Today (pi-subagents reinvents it) | Under the server architecture |
|
|
97
|
+
| ------------------------------------------------------------------------ | ---------------------------------------------------------- |
|
|
98
|
+
| `record-observer` re-deriving live activity | `subscribeSession` delta stream |
|
|
99
|
+
| 80 ms `listAgents()` widget poll | `session_created` / `session_status_changed` global events |
|
|
100
|
+
| [#277] accessors (`messages`, `subscribeToUpdates`, `getContextPercent`) | `SessionSnapshot` + deltas |
|
|
101
|
+
| `ConversationViewer` / native session navigation | client renders the snapshot through Pi's own components |
|
|
102
|
+
| dual-source-by-liveness split | server rehydrates; client sees one session shape |
|
|
103
|
+
| `steer_subagent` buffer-or-deliver | `join` + `prompt` / queued-steering commands |
|
|
104
|
+
| `subagents:*` activity broadcasts | session sync (semantic events stay; liveness moves) |
|
|
105
|
+
|
|
106
|
+
## Prerequisites and open questions
|
|
107
|
+
|
|
108
|
+
Being honest about the constraints the plan itself raises:
|
|
109
|
+
|
|
110
|
+
1. **Registry-local provider state is a hard prerequisite** (the plan states this explicitly).
|
|
111
|
+
Subagents run in-process under a multi-session server — exactly the scenario where process-global provider registration would corrupt one session's model view from another's extension load. pi-subagents is well-positioned (it already snapshots model and registry into `ParentSnapshot`), but the underlying Pi change must land first.
|
|
112
|
+
2. **The recursion guard stays pi-subagents' invariant.**
|
|
113
|
+
A subagent session created through the server must still have pi-subagents' three tools stripped, and an operator-joined subagent must not be able to spawn observers of itself in a loop.
|
|
114
|
+
3. **Workspace context must travel in the snapshot.**
|
|
115
|
+
Subagents can run in a different cwd or git worktree (`pi-subagents-worktrees`).
|
|
116
|
+
The plan's `SessionSnapshot.cwd` field already accounts for this, so an operator's client renders and interacts in the correct workspace.
|
|
117
|
+
4. **Operator-submitted versus parent-submitted command permissions.**
|
|
118
|
+
Commands an operator joins and submits flow through the subagent's `AgentSession`, so pi-permission-system's in-child gating still applies — but whether an operator's `run bash` or `prompt` should carry a different permission posture than the parent agent's is a genuine open design question, not something to assume.
|
|
119
|
+
|
|
120
|
+
## Net assessment
|
|
121
|
+
|
|
122
|
+
This architecture turns all three capabilities from bespoke, fragile features pi-subagents must build and maintain into thin clients of one uniform session-sync surface — and in doing so lets pi-subagents shed most of the observation infrastructure it only built because that surface did not exist yet.
|
|
123
|
+
|
|
124
|
+
[session-sync]: https://jot.mariozechner.at/s/zgzbq9n4f4mfck
|
|
125
|
+
[ADR-0004]: ../decisions/0004-reconsider-ui-direction.md
|
|
126
|
+
[Phase 18]: ./history/phase-18-reconsider-ui.md
|
|
127
|
+
[#277]: https://github.com/gotgenes/pi-packages/issues/277
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Phase 1: Export SubagentsService from this package
|
|
2
|
+
|
|
3
|
+
Added the `SubagentsService` interface, serializable types, `Symbol.for()` accessor functions, and `SUBAGENT_EVENTS` constants as public exports.
|
|
4
|
+
Wired `service-adapter.ts` to wrap `AgentManager` and call `publishSubagentsService()` at extension init.
|
|
5
|
+
|
|
6
|
+
## Related issues
|
|
7
|
+
|
|
8
|
+
- #48 — Export SubagentsService from this package
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Phase 10: Domain organization, bag decomposition, and complexity reduction
|
|
2
|
+
|
|
3
|
+
Target: reorganize source into domain directories; decompose remaining wide parameter bags into focused value objects; reduce cyclomatic complexity in rendering functions; eliminate production code duplication.
|
|
4
|
+
|
|
5
|
+
## Current smells
|
|
6
|
+
|
|
7
|
+
| Smell | Location | Evidence | Severity |
|
|
8
|
+
| ------------------------------------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | -------- |
|
|
9
|
+
| Flat `src/` directory | `src/` root | 20+ files in a single directory with no domain grouping; hard to see module boundaries | Medium |
|
|
10
|
+
| `ResolvedSpawnConfig` is a 15-field bag | `spawn-config.ts` | Mixes identity (name, slug), execution (model, permissions), and presentation (icon, color) concerns in one interface | Medium |
|
|
11
|
+
| `AgentSpawnConfig` threads parent session fields | `agent-manager.ts` | `parentSessionFile`, `parentSessionId`, `toolCallId` always travel together but are separate parameters | Low |
|
|
12
|
+
| `RunnerIO` is too wide | `foreground-runner.ts` | 9 methods spanning environment concerns (cwd, env) and session factory concerns (create, attach, resume) | Medium |
|
|
13
|
+
| `SessionConfig` mixes tool filtering with session setup | `session-config.ts` | `toolNames`, `disallowedSet`, `extensions` are a cohesive filter group buried in a larger config | Low |
|
|
14
|
+
| `RunOptions` is a 12-field bag | `foreground-runner.ts` | Mixes execution context (exec, registry, cwd) with per-run options (model, prompt, permissions) | Medium |
|
|
15
|
+
| `buildContentLines` high complexity | `ui/build-content-lines.ts` | ~60-line function with a switch over every content type; each branch is an independent formatter | Medium |
|
|
16
|
+
| `renderResult` high complexity | `tools/agent-tool.ts` | ~80-line function with status-based branching; each status is an independent rendering concern | Medium |
|
|
17
|
+
| Duplicated turn-formatting logic | `session/session-runner.ts`, `tools/agent-tool.ts` | 18 lines of identical `ToolCallContent` extraction and `getToolCallName` logic in two production files | Low |
|
|
18
|
+
|
|
19
|
+
## Step 1: Reorganize source into domain directories (#164)
|
|
20
|
+
|
|
21
|
+
Moved files into `config/`, `session/`, `lifecycle/`, `observation/`, and `service/` subdirectories.
|
|
22
|
+
All `src/` internal imports now use `#src/` path aliases.
|
|
23
|
+
Root `src/` reduced to 5 files + 8 directories.
|
|
24
|
+
|
|
25
|
+
Impact: domain boundaries are visible in the directory tree; imports communicate intent via path alias.
|
|
26
|
+
|
|
27
|
+
## Step 2: Decompose ResolvedSpawnConfig (#165)
|
|
28
|
+
|
|
29
|
+
Split the 15-field `ResolvedSpawnConfig` into three focused value objects:
|
|
30
|
+
|
|
31
|
+
- `SpawnIdentity` — name, slug, key
|
|
32
|
+
- `SpawnExecution` — model, permissions, tools, prompt, systemPrompt
|
|
33
|
+
- `SpawnPresentation` — icon, color, description
|
|
34
|
+
|
|
35
|
+
`ResolvedSpawnConfig` composes these three plus the remaining spawn-level fields.
|
|
36
|
+
|
|
37
|
+
Impact: each consumer receives only the fields it needs; adding a presentation field no longer touches execution code.
|
|
38
|
+
|
|
39
|
+
## Step 3: Extract ParentSessionInfo from AgentSpawnConfig (#166)
|
|
40
|
+
|
|
41
|
+
Extracted `parentSessionFile`, `parentSessionId`, and `toolCallId` into `ParentSessionInfo`.
|
|
42
|
+
`AgentSpawnConfig` and `AgentManager.spawn()` accept the single value object instead of three separate parameters.
|
|
43
|
+
|
|
44
|
+
Impact: eliminated parameter co-travel; the three fields that always move together are now a single concept.
|
|
45
|
+
|
|
46
|
+
## Step 4: Narrow RunnerIO (#167)
|
|
47
|
+
|
|
48
|
+
Split `RunnerIO` into two focused interfaces:
|
|
49
|
+
|
|
50
|
+
- `EnvironmentIO` (3 methods) — cwd, env, platform concerns
|
|
51
|
+
- `SessionFactoryIO` (5+1 methods) — create, attach, resume, and related session lifecycle
|
|
52
|
+
|
|
53
|
+
`RunnerIO` kept as a backward-compatible type alias (`EnvironmentIO & SessionFactoryIO`).
|
|
54
|
+
|
|
55
|
+
Impact: consumers declare which IO surface they actually need; test doubles shrink to the relevant subset.
|
|
56
|
+
|
|
57
|
+
## Step 5: Extract ToolFilterConfig from SessionConfig (#168)
|
|
58
|
+
|
|
59
|
+
Grouped `toolNames`, `disallowedSet`, and `extensions` into `ToolFilterConfig`.
|
|
60
|
+
`filterActiveTools` accepts a single `ToolFilterConfig` argument instead of three separate parameters.
|
|
61
|
+
|
|
62
|
+
Impact: tool-filtering concern is encapsulated; `SessionConfig` is narrower and more focused.
|
|
63
|
+
|
|
64
|
+
## Step 6: Extract RunContext from RunOptions (#169)
|
|
65
|
+
|
|
66
|
+
Extracted `exec`, `registry`, `cwd`, and `parentSession` into `RunContext`.
|
|
67
|
+
`RunOptions` reduced from 12 fields to 9 (the 4 extracted fields replaced by a single `context` field, plus the remaining per-run options).
|
|
68
|
+
|
|
69
|
+
Impact: execution context is a reusable value object; `RunOptions` focuses on per-run configuration.
|
|
70
|
+
|
|
71
|
+
## Step 7: Reduce buildContentLines complexity (#170)
|
|
72
|
+
|
|
73
|
+
Extracted per-content-type formatters into `ui/message-formatters.ts`.
|
|
74
|
+
Each content type (text, tool-use, tool-result, image, etc.) has a dedicated pure function.
|
|
75
|
+
`buildContentLines` is now a ~30-line dispatch loop that delegates to the appropriate formatter.
|
|
76
|
+
|
|
77
|
+
Impact: cyclomatic complexity of `buildContentLines` dropped from ~15 to ~5; each formatter is independently testable.
|
|
78
|
+
|
|
79
|
+
## Step 8: Reduce renderResult complexity (#171)
|
|
80
|
+
|
|
81
|
+
Extracted per-status formatters into `tools/result-renderer.ts`.
|
|
82
|
+
Each result status (success, error, timeout, cancelled) has a dedicated pure function.
|
|
83
|
+
`renderResult` reduced from ~80 lines to a 10-line guard that dispatches to the appropriate renderer.
|
|
84
|
+
|
|
85
|
+
Impact: cyclomatic complexity of `renderResult` dropped from ~10 to ~3; status rendering is independently testable.
|
|
86
|
+
|
|
87
|
+
## Step 9: Extract shared turn-formatting logic (#172)
|
|
88
|
+
|
|
89
|
+
Extracted `ToolCallContent`, `getToolCallName`, and `extractAssistantContent` into `session/content-items.ts`.
|
|
90
|
+
Both `session/session-runner.ts` and `tools/agent-tool.ts` import from the shared module.
|
|
91
|
+
|
|
92
|
+
Impact: eliminated 18 lines of production code duplication; single source of truth for turn-content extraction.
|
|
93
|
+
|
|
94
|
+
## Step dependencies
|
|
95
|
+
|
|
96
|
+
```mermaid
|
|
97
|
+
flowchart LR
|
|
98
|
+
subgraph bag["Bag decomposition track"]
|
|
99
|
+
S2["2: Decompose ResolvedSpawnConfig #165"] --> S3["3: Extract ParentSessionInfo #166"]
|
|
100
|
+
S4["4: Narrow RunnerIO #167"]
|
|
101
|
+
S5["5: Extract ToolFilterConfig #168"]
|
|
102
|
+
S6["6: Extract RunContext #169"]
|
|
103
|
+
end
|
|
104
|
+
subgraph complexity["Complexity reduction track"]
|
|
105
|
+
S7["7: Reduce buildContentLines #170"]
|
|
106
|
+
S8["8: Reduce renderResult #171"]
|
|
107
|
+
S9["9: Extract turn-formatting #172"]
|
|
108
|
+
end
|
|
109
|
+
S1["1: Reorganize into domain dirs #164"] --> bag
|
|
110
|
+
S1 --> complexity
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Step 1 unblocked all other steps.
|
|
114
|
+
Within the bag decomposition track, Step 2 enabled Step 3.
|
|
115
|
+
The bag decomposition and complexity reduction tracks were independent of each other.
|
|
116
|
+
|
|
117
|
+
## Impact
|
|
118
|
+
|
|
119
|
+
| Metric | Before | After |
|
|
120
|
+
| ------------------------------ | ------------------------------ | --------------------------------------------- |
|
|
121
|
+
| Health score | ~65 | 75 |
|
|
122
|
+
| `src/` root files | 20+ | 5 files + 8 directories |
|
|
123
|
+
| `ResolvedSpawnConfig` fields | 15 | 3 composed value objects |
|
|
124
|
+
| `RunOptions` fields | 12 | 9 |
|
|
125
|
+
| `RunnerIO` methods | 9 | 3 (`EnvironmentIO`) + 6 (`SessionFactoryIO`) |
|
|
126
|
+
| `buildContentLines` complexity | ~15 | ~5 |
|
|
127
|
+
| `renderResult` lines | ~80 | ~10 |
|
|
128
|
+
| Production duplication | 18 lines | 0 |
|
|
129
|
+
| Test duplication | ~1,400 lines (69 clone groups) | ~1,400 lines (69 clone groups) — not targeted |
|
|
130
|
+
|
|
131
|
+
## Related issues
|
|
132
|
+
|
|
133
|
+
- #164 — Reorganize source into domain directories
|
|
134
|
+
- #165 — Decompose ResolvedSpawnConfig
|
|
135
|
+
- #166 — Extract ParentSessionInfo from AgentSpawnConfig
|
|
136
|
+
- #167 — Narrow RunnerIO
|
|
137
|
+
- #168 — Extract ToolFilterConfig from SessionConfig
|
|
138
|
+
- #169 — Extract RunContext from RunOptions
|
|
139
|
+
- #170 — Reduce buildContentLines complexity
|
|
140
|
+
- #171 — Reduce renderResult complexity
|
|
141
|
+
- #172 — Extract shared turn-formatting logic
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Phase 11: Closure factories to classes
|
|
2
|
+
|
|
3
|
+
Target: convert all closure factories to classes, eliminating adapter closure density in `index.ts` (the package's #1 churn hotspot) and making the composition root a pure construction site.
|
|
4
|
+
|
|
5
|
+
## Root cause
|
|
6
|
+
|
|
7
|
+
The 44 adapter closures in `index.ts` existed because tool factories accepted narrow interfaces that didn't structurally match the real objects.
|
|
8
|
+
Four structural misalignments prevented direct wiring:
|
|
9
|
+
|
|
10
|
+
1. `SubagentRuntime.currentCtx` was typed `{ pi: unknown; ctx: unknown }` — every consumer had to `as any` cast.
|
|
11
|
+
2. Context queries (`buildSnapshot`, `getModelInfo`, `getSessionInfo`) lived as closures in `index.ts` instead of methods on the state holder.
|
|
12
|
+
3. `AgentToolManager` mixed fields from `AgentManager` and `SettingsManager` (source mismatch).
|
|
13
|
+
4. `AgentToolWidget` used different method names than `SubagentRuntime` (name mismatch).
|
|
14
|
+
|
|
15
|
+
## Layer 0: Define SessionContext narrow interface (#192)
|
|
16
|
+
|
|
17
|
+
Defined a `SessionContext` interface capturing the 5 fields `SubagentRuntime` actually needs.
|
|
18
|
+
Replaced the `{ pi: unknown; ctx: unknown }` typing with `SessionContext | undefined`.
|
|
19
|
+
|
|
20
|
+
Impact: typed foundation for all subsequent layers; no `as any` needed by consumers.
|
|
21
|
+
|
|
22
|
+
## Layer 1: SubagentRuntime stores typed context, owns its queries (#193)
|
|
23
|
+
|
|
24
|
+
Changed `currentCtx` to `SessionContext | undefined`.
|
|
25
|
+
The single `as SessionContext` cast moved into `handleSessionStart` — the SDK boundary.
|
|
26
|
+
Added typed methods: `buildSnapshot(inheritContext)`, `getModelInfo()`, `getSessionInfo()`.
|
|
27
|
+
|
|
28
|
+
Impact: 3 closure queries in `index.ts` → 0; 4 `as any` casts eliminated; `SubagentRuntime` is self-sufficient for tool deps.
|
|
29
|
+
|
|
30
|
+
## Layer 2: Align interfaces for structural typing (#194)
|
|
31
|
+
|
|
32
|
+
Three alignment changes:
|
|
33
|
+
|
|
34
|
+
1. Moved `getMaxConcurrent` off `AgentToolManager` — it reads from `SettingsManager`, not `AgentManager`.
|
|
35
|
+
2. Renamed widget methods — aligned `SubagentRuntime` method names with `AgentToolWidget`.
|
|
36
|
+
3. Removed dead re-export `getToolCallName` in `ui/message-formatters.ts`.
|
|
37
|
+
|
|
38
|
+
Impact: `AgentManager` structurally satisfies `AgentToolManager`; `SubagentRuntime` structurally satisfies `AgentToolWidget`; 0 dead exports.
|
|
39
|
+
|
|
40
|
+
## Layer 3: Convert closure factories to classes (#195, #196)
|
|
41
|
+
|
|
42
|
+
All five closure factories converted to classes:
|
|
43
|
+
|
|
44
|
+
| Factory | Class | Issue |
|
|
45
|
+
| -------------------------------- | --------------------- | ----- |
|
|
46
|
+
| `createAgentTool({...})` | `AgentTool` | #195 |
|
|
47
|
+
| `createGetResultTool(...)` | `GetResultTool` | #195 |
|
|
48
|
+
| `createSteerTool(...)` | `SteerTool` | #195 |
|
|
49
|
+
| `createAgentRunner(runnerIO)` | `ConcreteAgentRunner` | #196 |
|
|
50
|
+
| `createAgentsMenuHandler({...})` | `AgentsMenuHandler` | #196 |
|
|
51
|
+
|
|
52
|
+
Each class satisfies the existing interface via structural typing.
|
|
53
|
+
Tool classes expose `toToolDefinition()` for Pi tool registration.
|
|
54
|
+
`getModelLabel` internalized into `AgentsMenuHandler` (was a 7-line closure in `index.ts`).
|
|
55
|
+
|
|
56
|
+
Impact: deps are constructor params (inspectable, testable); no captured closures.
|
|
57
|
+
|
|
58
|
+
## Layer 4: Simplify index.ts (#196)
|
|
59
|
+
|
|
60
|
+
With all factories converted and `AgentManager` satisfying `AgentMenuManager` structurally:
|
|
61
|
+
|
|
62
|
+
- 4 adapter closures eliminated (3 manager method adapters + `getModelLabel`).
|
|
63
|
+
- 4 unused imports removed.
|
|
64
|
+
- Remaining ~15 closures are structural (event registrations, SDK factory callbacks).
|
|
65
|
+
|
|
66
|
+
Impact: adapter closure count halved; `AgentManager` passed directly without wrappers; churn hotspot stabilized.
|
|
67
|
+
|
|
68
|
+
## Step dependencies
|
|
69
|
+
|
|
70
|
+
```mermaid
|
|
71
|
+
flowchart LR
|
|
72
|
+
L0["Layer 0: SessionContext #192"] --> L1["Layer 1: Runtime queries #193"]
|
|
73
|
+
L1 --> L3["Layer 3: Classes #195 #196"]
|
|
74
|
+
L2["Layer 2: Align interfaces #194"] --> L3
|
|
75
|
+
L3 --> L4["Layer 4: Simplify index.ts #196"]
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Layers 0 and 2 were independent.
|
|
79
|
+
Layer 1 depended on Layer 0.
|
|
80
|
+
Layer 3 depended on both Layer 1 and Layer 2.
|
|
81
|
+
Layer 4 depended on Layer 3.
|
|
82
|
+
|
|
83
|
+
## Impact
|
|
84
|
+
|
|
85
|
+
| Metric | Before | After |
|
|
86
|
+
| ------------------------------ | ---------- | --------------------- |
|
|
87
|
+
| Health score | 75/100 (B) | 76/100 (B) |
|
|
88
|
+
| Adapter closures in `index.ts` | 44 | ~15 (structural only) |
|
|
89
|
+
| `as any` casts in `index.ts` | 5 | 1 (SDK boundary) |
|
|
90
|
+
| Dead exports | 1 | 0 |
|
|
91
|
+
| Closure factories | 5 | 0 |
|
|
92
|
+
| `index.ts` fan-out | 25 imports | 21 imports |
|
|
93
|
+
|
|
94
|
+
## Related issues
|
|
95
|
+
|
|
96
|
+
- #192 — Define SessionContext narrow interface
|
|
97
|
+
- #193 — Runtime owns context queries
|
|
98
|
+
- #194 — Align tool interfaces for structural typing
|
|
99
|
+
- #195 — Convert tool factories to classes
|
|
100
|
+
- #196 — Convert AgentRunner and AgentsMenuHandler to classes, simplify index.ts
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Phase 12: Complexity reduction and test fixture extraction
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
Phase 12 decomposed the three remaining high-complexity UI functions and extracted shared test fixtures into `test/helpers/`.
|
|
6
|
+
|
|
7
|
+
## Steps
|
|
8
|
+
|
|
9
|
+
### Step 1: Decompose `renderWidgetLines` (cognitive 44) — [#205]
|
|
10
|
+
|
|
11
|
+
`renderWidgetLines` in `ui/widget-renderer.ts` handled agent-status formatting, tree connectors, overflow, and empty states.
|
|
12
|
+
Extracted per-status renderers and a tree-connector utility.
|
|
13
|
+
|
|
14
|
+
- Target: `src/ui/widget-renderer.ts`
|
|
15
|
+
- Outcome: cognitive complexity < 10
|
|
16
|
+
|
|
17
|
+
### Step 2: Decompose `showAgentDetail` (cognitive 33) — [#206]
|
|
18
|
+
|
|
19
|
+
`showAgentDetail` in `ui/agent-config-editor.ts` handled display, edit, eject, and delete flows.
|
|
20
|
+
Extracted sub-functions per menu action.
|
|
21
|
+
|
|
22
|
+
- Target: `src/ui/agent-config-editor.ts`
|
|
23
|
+
- Outcome: cognitive complexity < 10
|
|
24
|
+
|
|
25
|
+
### Step 3: Decompose `update` in `agent-widget.ts` (cognitive 31) — [#207]
|
|
26
|
+
|
|
27
|
+
`update` mixed timer lifecycle, agent list assembly, render delegation, and visibility state.
|
|
28
|
+
Extracted `assembleWidgetState` (pure) and timer management.
|
|
29
|
+
|
|
30
|
+
- Target: `src/ui/agent-widget.ts`
|
|
31
|
+
- Outcome: cognitive complexity < 10
|
|
32
|
+
|
|
33
|
+
### Step 4: Extract shared test fixtures — [#208]
|
|
34
|
+
|
|
35
|
+
The 3 heaviest clone families:
|
|
36
|
+
|
|
37
|
+
- `agent-runner.test.ts` + `agent-runner-extension-tools.test.ts` (60-line shared setup)
|
|
38
|
+
- `agent-menu.test.ts` + `agent-creation-wizard.test.ts` + `agent-config-editor.test.ts` (54+51+24 lines)
|
|
39
|
+
- `agent-manager.test.ts` (18 internal clone groups, 210 duplicated lines)
|
|
40
|
+
|
|
41
|
+
Extracted shared factories into `test/helpers/runner-io.ts` and `test/helpers/ui-stubs.ts`.
|
|
42
|
+
Test duplication reduced from 71 clone groups (1,424 lines) to 59 clone groups (1,046 lines).
|
|
43
|
+
|
|
44
|
+
## Metrics change
|
|
45
|
+
|
|
46
|
+
| Metric | Before | After |
|
|
47
|
+
| -------------------------- | ---------------------- | ---------------------- |
|
|
48
|
+
| Health score | 75/100 (B) | 78/100 (B) |
|
|
49
|
+
| Fallow refactoring targets | 4 | 1 |
|
|
50
|
+
| Test duplication | 71 groups, 1,424 lines | 59 groups, 1,046 lines |
|
|
51
|
+
|
|
52
|
+
[#205]: https://github.com/gotgenes/pi-packages/issues/205
|
|
53
|
+
[#206]: https://github.com/gotgenes/pi-packages/issues/206
|
|
54
|
+
[#207]: https://github.com/gotgenes/pi-packages/issues/207
|
|
55
|
+
[#208]: https://github.com/gotgenes/pi-packages/issues/208
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Phase 13: Remaining structural smells
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
Phase 13 addressed the remaining fallow refactoring target, oversized methods, production duplication, SDK boundary coupling, and test clone families.
|
|
6
|
+
All six steps are closed: [#214], [#215], [#216], [#217], [#218], [#219].
|
|
7
|
+
|
|
8
|
+
## Steps
|
|
9
|
+
|
|
10
|
+
### Step 1: Convert remaining closure factories to classes — [#214]
|
|
11
|
+
|
|
12
|
+
Three closure factories converted to classes, making their dependencies explicit as constructor parameters.
|
|
13
|
+
|
|
14
|
+
| Factory → Class | File |
|
|
15
|
+
| ------------------------------------------------------ | ----------------------------- |
|
|
16
|
+
| `createAgentConfigEditor()` → `AgentConfigEditor` | `ui/agent-config-editor.ts` |
|
|
17
|
+
| `createAgentCreationWizard()` → `AgentCreationWizard` | `ui/agent-creation-wizard.ts` |
|
|
18
|
+
| `createSubagentsService()` → `SubagentsServiceAdapter` | `service/service-adapter.ts` |
|
|
19
|
+
|
|
20
|
+
- Outcome: 0 remaining closure factories (excluding pure-function factories)
|
|
21
|
+
|
|
22
|
+
### Step 2: Decompose `buildParentContext` (cognitive 30) — [#215]
|
|
23
|
+
|
|
24
|
+
`buildParentContext` in `session/context.ts` was the only remaining fallow refactoring target.
|
|
25
|
+
Extracted per-entry-type formatters: `formatMessageEntry()` and `formatCompactionEntry()`.
|
|
26
|
+
|
|
27
|
+
- Target: `src/session/context.ts`
|
|
28
|
+
- Outcome: cognitive complexity < 10, function < 15 LOC, 0 fallow refactoring targets
|
|
29
|
+
|
|
30
|
+
### Step 3: Decompose `startAgent` in `agent-manager.ts` — [#216]
|
|
31
|
+
|
|
32
|
+
`startAgent` had two mutable closure variables and duplicated finalization logic in `.then()`/`.catch()`.
|
|
33
|
+
Introduced `RunHandle` lifecycle object (private to `agent-manager.ts`) that owns per-run cleanup state.
|
|
34
|
+
`WorktreeState` gained `performCleanup()` to eliminate ask-tell at cleanup sites.
|
|
35
|
+
|
|
36
|
+
Extracted:
|
|
37
|
+
|
|
38
|
+
1. `RunHandle` class — owns `unsub`/`detachFn`, `complete()`, `fail()`, idempotent `fireOnFinished()`.
|
|
39
|
+
2. `finalizeBackgroundRun()` — shared background finalization.
|
|
40
|
+
3. `setupWorktree()` — worktree creation with strict failure.
|
|
41
|
+
4. `flushPendingSteers()` — drain buffered steers on session creation.
|
|
42
|
+
5. `WorktreeState.performCleanup()` — self-cleanup eliminating ask-tell.
|
|
43
|
+
|
|
44
|
+
- Target: `src/lifecycle/agent-manager.ts`, `src/lifecycle/worktree-state.ts`
|
|
45
|
+
- Outcome: `startAgent` reduced to ~40 LOC; zero mutable `let` bindings in `.then()`/`.catch()`
|
|
46
|
+
|
|
47
|
+
### Step 4: Extract overwrite guard from UI — [#217]
|
|
48
|
+
|
|
49
|
+
Extracted the 20-line pattern duplicated between `agent-config-editor.ts` and `agent-creation-wizard.ts` into `writeAgentFile()` in `src/ui/agent-file-writer.ts`.
|
|
50
|
+
|
|
51
|
+
- Target: new `src/ui/agent-file-writer.ts`
|
|
52
|
+
- Outcome: 0 production clone groups (at the time; one internal group re-emerged later in `agent-config-editor.ts`)
|
|
53
|
+
|
|
54
|
+
### Step 5: Push SDK boundary in `settings.ts` — [#218]
|
|
55
|
+
|
|
56
|
+
Injected `agentDir: string` as a constructor parameter to `SettingsManager`, replacing the module-level `getAgentDir()` SDK call.
|
|
57
|
+
|
|
58
|
+
- Target: `src/settings.ts`, `src/index.ts`
|
|
59
|
+
- Outcome: `settings.ts` has 0 Pi SDK imports; `loadSettings`/`saveSettings` fully testable without SDK stubs
|
|
60
|
+
|
|
61
|
+
### Step 6: Reduce test duplication — top 3 clone families — [#219]
|
|
62
|
+
|
|
63
|
+
Extracted shared setup/assertion helpers for the three heaviest test clone families.
|
|
64
|
+
|
|
65
|
+
- Target: `test/lifecycle/agent-manager.test.ts`, `test/conversation-viewer.test.ts`, `test/ui/agent-config-editor.test.ts`
|
|
66
|
+
- Outcome: test duplication significantly reduced
|
|
67
|
+
|
|
68
|
+
## Metrics change
|
|
69
|
+
|
|
70
|
+
| Metric | Before | After |
|
|
71
|
+
| -------------------------- | ---------------------- | --------------------------------- |
|
|
72
|
+
| Health score | 78/100 (B) | 78/100 (B) |
|
|
73
|
+
| Source files | 53 | 56 |
|
|
74
|
+
| Total LOC | 8,180 | 8,382 |
|
|
75
|
+
| Dead code | 0 files, 0 exports | 0 files, 0 exports |
|
|
76
|
+
| Maintainability index | 90.7 | 90.8 |
|
|
77
|
+
| Avg cyclomatic complexity | 1.5 | 1.4 |
|
|
78
|
+
| Fallow refactoring targets | 1 | 0 |
|
|
79
|
+
| Production duplication | 0 clone groups | 1 internal clone group (11 lines) |
|
|
80
|
+
| Test duplication | 59 groups, 1,046 lines | 38 groups, 645 lines (overall) |
|
|
81
|
+
| Churn hotspots | 6 accelerating | 1 accelerating (`index.ts`) |
|
|
82
|
+
|
|
83
|
+
[#214]: https://github.com/gotgenes/pi-packages/issues/214
|
|
84
|
+
[#215]: https://github.com/gotgenes/pi-packages/issues/215
|
|
85
|
+
[#216]: https://github.com/gotgenes/pi-packages/issues/216
|
|
86
|
+
[#217]: https://github.com/gotgenes/pi-packages/issues/217
|
|
87
|
+
[#218]: https://github.com/gotgenes/pi-packages/issues/218
|
|
88
|
+
[#219]: https://github.com/gotgenes/pi-packages/issues/219
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Phase 14: Strip policy from core
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
Phase 14 removed tool and extension policy enforcement from pi-subagents.
|
|
6
|
+
This code duplicated what pi-permission-system already provides with richer semantics (allow/ask/deny vs. binary hide).
|
|
7
|
+
Removing it simplified `runAgent`, shrunk `AgentConfig` and `SessionConfig`, and prepared a cleaner codebase for Phase 15's domain-model work.
|
|
8
|
+
|
|
9
|
+
All four steps are closed: [#237], [#238], [#239], [#242].
|
|
10
|
+
|
|
11
|
+
## Steps
|
|
12
|
+
|
|
13
|
+
### Step 1: Remove `disallowed_tools` — [#237]
|
|
14
|
+
|
|
15
|
+
Removed the `disallowedTools` field from `AgentConfig` and all code that processed it.
|
|
16
|
+
Users migrate to `permission:` frontmatter for tool restrictions.
|
|
17
|
+
|
|
18
|
+
- Target: `types.ts`, `custom-agents.ts`, `session-config.ts`, `agent-runner.ts`, `ui/agent-config-editor.ts`, `ui/agent-creation-wizard.ts`
|
|
19
|
+
- Outcome: single source of truth for access control in pi-permission-system
|
|
20
|
+
|
|
21
|
+
### Step 2: Remove `extensions` filtering — [#238]
|
|
22
|
+
|
|
23
|
+
Removed the `extensions: string[]` allowlist and simplified the field to a boolean.
|
|
24
|
+
The `extensions: false` case (used by `isolated`) was retained for Phase 16.
|
|
25
|
+
|
|
26
|
+
- Target: `types.ts`, `agent-runner.ts`, `session-config.ts`, `ui/agent-config-editor.ts`, `ui/agent-creation-wizard.ts`
|
|
27
|
+
- Outcome: `filterActiveTools` reduced to two concerns: recursion guard and `extensions: false` passthrough
|
|
28
|
+
|
|
29
|
+
### Step 3: Collapse `filterActiveTools` to recursion guard — [#239]
|
|
30
|
+
|
|
31
|
+
With Steps 1–2 complete, `filterActiveTools` was reduced to its essential purpose: filtering `EXCLUDED_TOOL_NAMES` to prevent recursive agent spawning.
|
|
32
|
+
|
|
33
|
+
- Removed `ToolFilterConfig` — the function no longer needs a config bag.
|
|
34
|
+
- Removed the pre-bind filter call — extension tools aren't in the active set pre-bind.
|
|
35
|
+
- Flattened `SessionConfig` — removed `toolFilter: ToolFilterConfig`; `toolNames` and `extensions` are top-level fields.
|
|
36
|
+
- Target: `agent-runner.ts`, `session-config.ts`
|
|
37
|
+
- Outcome: `filterActiveTools` is a one-liner; the pre-bind/post-bind dance is gone
|
|
38
|
+
|
|
39
|
+
### Step 4: Rename `Agent` tool to `subagent` — [#242]
|
|
40
|
+
|
|
41
|
+
Renamed the `Agent` tool to `subagent` to align with Pi's built-in tool naming convention (all lowercase).
|
|
42
|
+
|
|
43
|
+
- Target: `tools/agent-tool.ts`, `lifecycle/agent-runner.ts`, `docs/`, `../pi-permission-system/docs/`
|
|
44
|
+
- Outcome: all three tools use consistent lowercase naming
|
|
45
|
+
|
|
46
|
+
[#237]: https://github.com/gotgenes/pi-packages/issues/237
|
|
47
|
+
[#238]: https://github.com/gotgenes/pi-packages/issues/238
|
|
48
|
+
[#239]: https://github.com/gotgenes/pi-packages/issues/239
|
|
49
|
+
[#242]: https://github.com/gotgenes/pi-packages/issues/242
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Phase 15: Domain model evolution
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
Phase 15 evolved `Agent` from a passive state machine (`AgentRecord`) into an object that **owns its entire execution lifecycle**.
|
|
6
|
+
Before Phase 15, `AgentManager` orchestrated everything: calling the runner, handling session creation, wiring observers, and cleaning up worktrees — reaching into Agent 10+ times across `spawn()` and `startAgent()`.
|
|
7
|
+
After Phase 15, Agent is born complete with all dependencies and configuration, owns `run()` and `resume()`, and manages its own observer and worktree lifecycle.
|
|
8
|
+
|
|
9
|
+
All six steps are closed: [#227], [#228], [#231], [#229], [#230], [#232].
|
|
10
|
+
|
|
11
|
+
## Key changes
|
|
12
|
+
|
|
13
|
+
- `AgentRecord` renamed to `Agent` with full behavioral surface.
|
|
14
|
+
- `Agent.run()` encapsulates the entire execution lifecycle: worktree setup, runner invocation, session-creation handling, observer wiring, worktree cleanup, and status transitions.
|
|
15
|
+
- `Agent.resume()` manages its own observer subscription lifecycle.
|
|
16
|
+
- `startAgent` deleted from `AgentManager` — replaced by `agent.run()`.
|
|
17
|
+
- `ConcurrencyQueue` extracted from `AgentManager` — scheduling is independently testable.
|
|
18
|
+
- `SpawnArgs` deleted — the queue stores agent IDs, not config objects.
|
|
19
|
+
- `onSessionCreated` callback replaced by `AgentLifecycleObserver` passed at construction.
|
|
20
|
+
- `exec` and `registry` relay-only dependencies moved from `AgentManager` to `ConcreteAgentRunner`.
|
|
21
|
+
- `AgentManagerOptions` shrunk from 7 to 5 fields.
|
|
22
|
+
|
|
23
|
+
## Steps
|
|
24
|
+
|
|
25
|
+
### Step 1: Evolve AgentRecord into Agent with behavior — [#227]
|
|
26
|
+
|
|
27
|
+
Renamed `AgentRecord` → `Agent`.
|
|
28
|
+
Moved per-agent behavior from `AgentManager` into the agent: `abort()`, `queueSteer()` / `flushPendingSteers()`, `setupWorktree()`.
|
|
29
|
+
|
|
30
|
+
### Step 2: Convert startAgent to async/await — [#228]
|
|
31
|
+
|
|
32
|
+
Converted `startAgent` to `async` with `try/catch` and dissolved `RunHandle` into `Agent` methods.
|
|
33
|
+
Agent gained run lifecycle methods: `completeRun`, `failRun`, `wireSignal`, `attachObserver`, `releaseListeners`.
|
|
34
|
+
|
|
35
|
+
### Step 3: Push exec/registry relay deps to runner construction — [#231]
|
|
36
|
+
|
|
37
|
+
`exec` and `registry` moved from `AgentManager` to `ConcreteAgentRunner` via `RunnerDeps`.
|
|
38
|
+
`RunContext` shrunk from 4 to 2 per-call fields.
|
|
39
|
+
|
|
40
|
+
### Step 4: Agent born complete — Agent.run() absorbs startAgent — [#229]
|
|
41
|
+
|
|
42
|
+
Agent receives `runner`, `worktrees`, and a lifecycle observer at construction.
|
|
43
|
+
`Agent.run()` encapsulates the entire execution lifecycle.
|
|
44
|
+
`startAgent`, `SpawnArgs`, `onSessionCreated` callback deleted.
|
|
45
|
+
|
|
46
|
+
### Step 5: Extract ConcurrencyQueue from AgentManager — [#230]
|
|
47
|
+
|
|
48
|
+
Extracted `queue[]`, `runningBackground`, `_getMaxConcurrent`, `drainQueue()`, `finalizeBackgroundRun()` into `ConcurrencyQueue`.
|
|
49
|
+
`AgentManager` lost 3 fields and 3 methods (~40 lines).
|
|
50
|
+
|
|
51
|
+
### Step 6: Agent.resume() with internal observer lifecycle — [#232]
|
|
52
|
+
|
|
53
|
+
`Agent.resume(prompt, signal)` manages its own observer subscription lifecycle.
|
|
54
|
+
`AgentManager.resume()` became a one-liner delegation.
|
|
55
|
+
|
|
56
|
+
## Findings summary
|
|
57
|
+
|
|
58
|
+
| Finding | Category | Status |
|
|
59
|
+
| ------------------------------------------------------------------ | ------------ | --------------------- |
|
|
60
|
+
| `AgentRecord` anemic — no behavior, manager reaches in 37× | B: Oversized | ✅ Resolved |
|
|
61
|
+
| Agent cannot run itself — manager orchestrates 10 external touches | C: Coupling | ✅ Resolved |
|
|
62
|
+
| Scheduling tangled into `AgentManager` (3 fields, 3 methods) | A: Coupling | ✅ Resolved |
|
|
63
|
+
| `startAgent` uses `.then()`/`.catch()` instead of async/await | C: Callbacks | ✅ Resolved |
|
|
64
|
+
| `onSessionCreated` callback flows through 3 layers | C: Callbacks | ✅ Subsumed by Step 4 |
|
|
65
|
+
| `resume()` duplicates observer subscribe/unsubscribe pattern | A: Redundant | ✅ Resolved |
|
|
66
|
+
| `exec`/`registry` relay-only deps on `AgentManager` | C: Coupling | ✅ Resolved |
|
|
67
|
+
|
|
68
|
+
[#227]: https://github.com/gotgenes/pi-packages/issues/227
|
|
69
|
+
[#228]: https://github.com/gotgenes/pi-packages/issues/228
|
|
70
|
+
[#229]: https://github.com/gotgenes/pi-packages/issues/229
|
|
71
|
+
[#230]: https://github.com/gotgenes/pi-packages/issues/230
|
|
72
|
+
[#231]: https://github.com/gotgenes/pi-packages/issues/231
|
|
73
|
+
[#232]: https://github.com/gotgenes/pi-packages/issues/232
|