@asterxsk/kiln 0.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/LICENSE +21 -0
- package/README.md +170 -0
- package/agent/AGENTS.md +67 -0
- package/agent/README.md +5 -0
- package/agent/extensions/AGENTS.md +68 -0
- package/agent/extensions/ask-user/index.ts +418 -0
- package/agent/extensions/ask-user/install.ps1 +23 -0
- package/agent/extensions/ask-user/install.sh +21 -0
- package/agent/extensions/ask-user/package-lock.json +769 -0
- package/agent/extensions/ask-user/package.json +19 -0
- package/agent/extensions/ask-user/prompt.ts +45 -0
- package/agent/extensions/ask-user/tsconfig.json +7 -0
- package/agent/extensions/background-terminals/docs/implementation-guide.md +942 -0
- package/agent/extensions/background-terminals/index.ts +627 -0
- package/agent/extensions/background-terminals/install.ps1 +23 -0
- package/agent/extensions/background-terminals/install.sh +21 -0
- package/agent/extensions/background-terminals/manager.test.ts +735 -0
- package/agent/extensions/background-terminals/output.test.ts +109 -0
- package/agent/extensions/background-terminals/package-lock.json +769 -0
- package/agent/extensions/background-terminals/package.json +17 -0
- package/agent/extensions/background-terminals/prompt.test.ts +125 -0
- package/agent/extensions/background-terminals/ps.test.ts +82 -0
- package/agent/extensions/background-terminals/result-delivery.test.ts +44 -0
- package/agent/extensions/background-terminals/src/domain.ts +87 -0
- package/agent/extensions/background-terminals/src/manager.ts +907 -0
- package/agent/extensions/background-terminals/src/output.ts +84 -0
- package/agent/extensions/background-terminals/src/prompt.ts +142 -0
- package/agent/extensions/background-terminals/src/result-delivery.ts +27 -0
- package/agent/extensions/background-terminals/src/runtime.ts +36 -0
- package/agent/extensions/background-terminals/src/ui/output-view.ts +79 -0
- package/agent/extensions/background-terminals/src/ui/ps.ts +621 -0
- package/agent/extensions/background-terminals/tsconfig.json +7 -0
- package/agent/extensions/destructive/README.md +31 -0
- package/agent/extensions/destructive/index.ts +88 -0
- package/agent/extensions/destructive/install.ps1 +23 -0
- package/agent/extensions/destructive/install.sh +21 -0
- package/agent/extensions/file-search/index.spec.ts +443 -0
- package/agent/extensions/file-search/index.ts +459 -0
- package/agent/extensions/file-search/install.ps1 +23 -0
- package/agent/extensions/file-search/install.sh +21 -0
- package/agent/extensions/file-search/package-lock.json +2253 -0
- package/agent/extensions/file-search/package.json +23 -0
- package/agent/extensions/file-search/src/args.ts +122 -0
- package/agent/extensions/file-search/src/binaries.ts +422 -0
- package/agent/extensions/file-search/src/output.ts +126 -0
- package/agent/extensions/file-search/src/process.ts +146 -0
- package/agent/extensions/file-search/src/prompt.ts +52 -0
- package/agent/extensions/file-search/tsconfig.json +7 -0
- package/agent/extensions/goal/README.md +50 -0
- package/agent/extensions/goal/index.ts +155 -0
- package/agent/extensions/goal/install.ps1 +23 -0
- package/agent/extensions/goal/install.sh +21 -0
- package/agent/extensions/modelconf/PLAN.md +915 -0
- package/agent/extensions/modelconf/README.md +66 -0
- package/agent/extensions/modelconf/index.ts +296 -0
- package/agent/extensions/modelconf/install.ps1 +23 -0
- package/agent/extensions/modelconf/install.sh +21 -0
- package/agent/extensions/modelconf/package-lock.json +1809 -0
- package/agent/extensions/modelconf/package.json +13 -0
- package/agent/extensions/modelconf/src/fuzzy.ts +26 -0
- package/agent/extensions/modelconf/src/glob.ts +13 -0
- package/agent/extensions/modelconf/src/persistence.test.ts +46 -0
- package/agent/extensions/modelconf/src/persistence.ts +164 -0
- package/agent/extensions/modelconf/src/ui/ModelConfView.test.ts +75 -0
- package/agent/extensions/modelconf/src/ui/ModelConfView.ts +1101 -0
- package/agent/extensions/modelconf/tsconfig.json +15 -0
- package/agent/extensions/pi-web-access/CHANGELOG.md +690 -0
- package/agent/extensions/pi-web-access/LICENSE +21 -0
- package/agent/extensions/pi-web-access/README.md +470 -0
- package/agent/extensions/pi-web-access/SECURITY.md +5 -0
- package/agent/extensions/pi-web-access/activity.ts +101 -0
- package/agent/extensions/pi-web-access/auth-fetch.ts +148 -0
- package/agent/extensions/pi-web-access/banner.png +0 -0
- package/agent/extensions/pi-web-access/brightdata-unlocker.ts +272 -0
- package/agent/extensions/pi-web-access/chrome-cookies.ts +669 -0
- package/agent/extensions/pi-web-access/content-find.ts +139 -0
- package/agent/extensions/pi-web-access/credential-source.ts +191 -0
- package/agent/extensions/pi-web-access/data-uri-sanitize.ts +406 -0
- package/agent/extensions/pi-web-access/datalab-pdf-extract.ts +568 -0
- package/agent/extensions/pi-web-access/declared-web-links.ts +173 -0
- package/agent/extensions/pi-web-access/evidence/CONTRACT-EVIDENCE.md +496 -0
- package/agent/extensions/pi-web-access/evidence/contract-probe.mjs +140 -0
- package/agent/extensions/pi-web-access/exa.ts +526 -0
- package/agent/extensions/pi-web-access/extract.ts +1196 -0
- package/agent/extensions/pi-web-access/feature-config.ts +29 -0
- package/agent/extensions/pi-web-access/fetch-params.ts +111 -0
- package/agent/extensions/pi-web-access/gemini-adc.ts +298 -0
- package/agent/extensions/pi-web-access/gemini-api.ts +353 -0
- package/agent/extensions/pi-web-access/gemini-pdf-extract.ts +108 -0
- package/agent/extensions/pi-web-access/gemini-search.ts +21 -0
- package/agent/extensions/pi-web-access/gemini-url-context.ts +128 -0
- package/agent/extensions/pi-web-access/gemini-web-config.ts +101 -0
- package/agent/extensions/pi-web-access/gemini-web.ts +487 -0
- package/agent/extensions/pi-web-access/github-api.ts +197 -0
- package/agent/extensions/pi-web-access/github-extract.ts +746 -0
- package/agent/extensions/pi-web-access/github-issue-pr.ts +700 -0
- package/agent/extensions/pi-web-access/index.ts +1737 -0
- package/agent/extensions/pi-web-access/package-lock.json +5808 -0
- package/agent/extensions/pi-web-access/package.json +64 -0
- package/agent/extensions/pi-web-access/page-query.ts +96 -0
- package/agent/extensions/pi-web-access/pdf-extract.ts +409 -0
- package/agent/extensions/pi-web-access/pi-web-fetch-demo.mp4 +0 -0
- package/agent/extensions/pi-web-access/promise-try.d.ts +7 -0
- package/agent/extensions/pi-web-access/query-rewrite.ts +51 -0
- package/agent/extensions/pi-web-access/render-search-error.ts +170 -0
- package/agent/extensions/pi-web-access/rsc-extract.ts +338 -0
- package/agent/extensions/pi-web-access/search-types.ts +20 -0
- package/agent/extensions/pi-web-access/source-check.ts +282 -0
- package/agent/extensions/pi-web-access/ssrf-protection.ts +526 -0
- package/agent/extensions/pi-web-access/storage.ts +521 -0
- package/agent/extensions/pi-web-access/summary-model-scope.ts +125 -0
- package/agent/extensions/pi-web-access/test/auth-fetch.test.mjs +208 -0
- package/agent/extensions/pi-web-access/test/brightdata-unlocker.test.mjs +840 -0
- package/agent/extensions/pi-web-access/test/chrome-cookie-extraction.test.mjs +441 -0
- package/agent/extensions/pi-web-access/test/config-path.test.mjs +283 -0
- package/agent/extensions/pi-web-access/test/content-find.test.mjs +25 -0
- package/agent/extensions/pi-web-access/test/credential-source.test.mjs +118 -0
- package/agent/extensions/pi-web-access/test/data-uri-sanitize.test.mjs +210 -0
- package/agent/extensions/pi-web-access/test/datalab-pdf-extract.test.mjs +552 -0
- package/agent/extensions/pi-web-access/test/declared-web-links.test.mjs +212 -0
- package/agent/extensions/pi-web-access/test/fetch-answer-storage.test.mjs +40 -0
- package/agent/extensions/pi-web-access/test/fetch-cache-storage.test.mjs +334 -0
- package/agent/extensions/pi-web-access/test/fetch-content-domain-policy.test.mjs +95 -0
- package/agent/extensions/pi-web-access/test/fetch-modes.test.mjs +53 -0
- package/agent/extensions/pi-web-access/test/fetch-not-found-guidance.test.mjs +92 -0
- package/agent/extensions/pi-web-access/test/fetch-params.test.mjs +86 -0
- package/agent/extensions/pi-web-access/test/fetch-render-call.test.mjs +34 -0
- package/agent/extensions/pi-web-access/test/fetch-routing.test.mjs +173 -0
- package/agent/extensions/pi-web-access/test/gemini-adc-auth.test.mjs +257 -0
- package/agent/extensions/pi-web-access/test/gemini-api-transport.test.mjs +170 -0
- package/agent/extensions/pi-web-access/test/gemini-pdf-extract.test.mjs +133 -0
- package/agent/extensions/pi-web-access/test/gemini-web-cookie-opt-in.test.mjs +178 -0
- package/agent/extensions/pi-web-access/test/gemini-web-header-overflow.test.mjs +148 -0
- package/agent/extensions/pi-web-access/test/get-search-content.test.mjs +223 -0
- package/agent/extensions/pi-web-access/test/github-extract.test.mjs +378 -0
- package/agent/extensions/pi-web-access/test/github-issue-pr.test.mjs +565 -0
- package/agent/extensions/pi-web-access/test/inline-content-config.test.mjs +99 -0
- package/agent/extensions/pi-web-access/test/lazy-extract-load.test.mjs +118 -0
- package/agent/extensions/pi-web-access/test/local-video-oversize.test.mjs +52 -0
- package/agent/extensions/pi-web-access/test/package-typebox-dependency.test.mjs +50 -0
- package/agent/extensions/pi-web-access/test/page-query.test.mjs +51 -0
- package/agent/extensions/pi-web-access/test/pdf-config.test.mjs +140 -0
- package/agent/extensions/pi-web-access/test/pdf-extract.test.mjs +500 -0
- package/agent/extensions/pi-web-access/test/proxy-transport.test.mjs +286 -0
- package/agent/extensions/pi-web-access/test/query-rewrite.test.mjs +52 -0
- package/agent/extensions/pi-web-access/test/rsc-fallback.test.mjs +102 -0
- package/agent/extensions/pi-web-access/test/search-error-render.test.mjs +152 -0
- package/agent/extensions/pi-web-access/test/search-providers.test.mjs +274 -0
- package/agent/extensions/pi-web-access/test/source-check.test.mjs +179 -0
- package/agent/extensions/pi-web-access/test/ssrf-allow-ranges-config.test.mjs +205 -0
- package/agent/extensions/pi-web-access/test/ssrf-protection.test.mjs +456 -0
- package/agent/extensions/pi-web-access/test/summary-model-scope.test.mjs +106 -0
- package/agent/extensions/pi-web-access/test/tool-registration-config.test.mjs +182 -0
- package/agent/extensions/pi-web-access/test/web-search-answer-render.test.mjs +66 -0
- package/agent/extensions/pi-web-access/test/youtube-extract-errors.test.mjs +64 -0
- package/agent/extensions/pi-web-access/tsconfig.json +11 -0
- package/agent/extensions/pi-web-access/utils.ts +451 -0
- package/agent/extensions/pi-web-access/video-extract.ts +392 -0
- package/agent/extensions/pi-web-access/youtube-extract.ts +328 -0
- package/agent/extensions/shared/activity-status.ts +31 -0
- package/agent/extensions/shared/child-session.test.ts +270 -0
- package/agent/extensions/shared/child-session.ts +148 -0
- package/agent/extensions/shared/context-utilization.test.ts +48 -0
- package/agent/extensions/shared/context-utilization.ts +47 -0
- package/agent/extensions/shared/dashboard-state.ts +99 -0
- package/agent/extensions/shared/install.ps1 +23 -0
- package/agent/extensions/shared/install.sh +21 -0
- package/agent/extensions/shared/tool-call-timeout.test.ts +117 -0
- package/agent/extensions/shared/tool-call-timeout.ts +104 -0
- package/agent/extensions/skillsconf/README.md +74 -0
- package/agent/extensions/skillsconf/index.ts +92 -0
- package/agent/extensions/skillsconf/install.ps1 +23 -0
- package/agent/extensions/skillsconf/install.sh +21 -0
- package/agent/extensions/skillsconf/package-lock.json +1809 -0
- package/agent/extensions/skillsconf/package.json +18 -0
- package/agent/extensions/skillsconf/src/delete-skill.test.ts +64 -0
- package/agent/extensions/skillsconf/src/delete-skill.ts +45 -0
- package/agent/extensions/skillsconf/src/filter.test.ts +80 -0
- package/agent/extensions/skillsconf/src/filter.ts +74 -0
- package/agent/extensions/skillsconf/src/fuzzy.ts +26 -0
- package/agent/extensions/skillsconf/src/persistence.test.ts +72 -0
- package/agent/extensions/skillsconf/src/persistence.ts +169 -0
- package/agent/extensions/skillsconf/src/ui/SkillConfView.test.ts +337 -0
- package/agent/extensions/skillsconf/src/ui/SkillConfView.ts +758 -0
- package/agent/extensions/skillsconf/src/ui/text-input.ts +91 -0
- package/agent/extensions/skillsconf/src/ui/tui-helpers.ts +144 -0
- package/agent/extensions/skillsconf/tsconfig.json +15 -0
- package/agent/extensions/status line/index.ts +282 -0
- package/agent/extensions/status line/install.ps1 +23 -0
- package/agent/extensions/status line/install.sh +21 -0
- package/agent/extensions/subagents/by-the-way.test.ts +29 -0
- package/agent/extensions/subagents/claude.test.ts +119 -0
- package/agent/extensions/subagents/codex.test.ts +102 -0
- package/agent/extensions/subagents/context-usage.test.ts +107 -0
- package/agent/extensions/subagents/docs/design-plan.md +568 -0
- package/agent/extensions/subagents/docs/effect-v4-extension-guide.md +354 -0
- package/agent/extensions/subagents/docs/effect-v4-notes.md +571 -0
- package/agent/extensions/subagents/index.ts +779 -0
- package/agent/extensions/subagents/install.ps1 +23 -0
- package/agent/extensions/subagents/install.sh +21 -0
- package/agent/extensions/subagents/manager.test.ts +276 -0
- package/agent/extensions/subagents/package-lock.json +2244 -0
- package/agent/extensions/subagents/package.json +19 -0
- package/agent/extensions/subagents/result-delivery.test.ts +27 -0
- package/agent/extensions/subagents/src/backend.ts +73 -0
- package/agent/extensions/subagents/src/backends/claude.ts +701 -0
- package/agent/extensions/subagents/src/backends/codex.ts +1060 -0
- package/agent/extensions/subagents/src/backends/pi.ts +575 -0
- package/agent/extensions/subagents/src/backends/stub.ts +300 -0
- package/agent/extensions/subagents/src/by-the-way.ts +21 -0
- package/agent/extensions/subagents/src/domain.ts +253 -0
- package/agent/extensions/subagents/src/format.ts +74 -0
- package/agent/extensions/subagents/src/manager.ts +736 -0
- package/agent/extensions/subagents/src/prompt.ts +92 -0
- package/agent/extensions/subagents/src/result-delivery.ts +20 -0
- package/agent/extensions/subagents/src/runtime.ts +53 -0
- package/agent/extensions/subagents/src/ui/takeover.ts +583 -0
- package/agent/extensions/subagents/src/ui/transcript.ts +201 -0
- package/agent/extensions/subagents/takeover.test.ts +29 -0
- package/agent/extensions/subagents/tsconfig.json +7 -0
- package/agent/extensions/taste/index.ts +443 -0
- package/agent/extensions/taste/install.ps1 +23 -0
- package/agent/extensions/taste/install.sh +21 -0
- package/agent/extensions/todo/AGENTS.md +38 -0
- package/agent/extensions/todo/LICENSE +21 -0
- package/agent/extensions/todo/config.ts +55 -0
- package/agent/extensions/todo/index.ts +151 -0
- package/agent/extensions/todo/install.ps1 +23 -0
- package/agent/extensions/todo/install.sh +21 -0
- package/agent/extensions/todo/locales/de.json +17 -0
- package/agent/extensions/todo/locales/en.json +15 -0
- package/agent/extensions/todo/locales/es.json +17 -0
- package/agent/extensions/todo/locales/fr.json +17 -0
- package/agent/extensions/todo/locales/pt-BR.json +17 -0
- package/agent/extensions/todo/locales/pt.json +17 -0
- package/agent/extensions/todo/locales/ru.json +17 -0
- package/agent/extensions/todo/locales/uk.json +17 -0
- package/agent/extensions/todo/locales/zh.json +17 -0
- package/agent/extensions/todo/package-lock.json +3358 -0
- package/agent/extensions/todo/package.json +67 -0
- package/agent/extensions/todo/state/i18n-bridge.ts +64 -0
- package/agent/extensions/todo/state/invariants.ts +20 -0
- package/agent/extensions/todo/state/replay.ts +38 -0
- package/agent/extensions/todo/state/selectors.ts +107 -0
- package/agent/extensions/todo/state/state-reducer.ts +326 -0
- package/agent/extensions/todo/state/state.ts +18 -0
- package/agent/extensions/todo/state/store.ts +82 -0
- package/agent/extensions/todo/state/task-graph.ts +57 -0
- package/agent/extensions/todo/todo-overlay.ts +200 -0
- package/agent/extensions/todo/todo.ts +155 -0
- package/agent/extensions/todo/tool/response-envelope.ts +109 -0
- package/agent/extensions/todo/tool/types.ts +206 -0
- package/agent/extensions/todo/verify-ref-system.js +0 -0
- package/agent/extensions/todo/view/format.ts +177 -0
- package/agent/extensions/trim-context/README.md +54 -0
- package/agent/extensions/trim-context/index.ts +487 -0
- package/agent/extensions/trim-context/install.ps1 +23 -0
- package/agent/extensions/trim-context/install.sh +21 -0
- package/agent/install.ps1 +527 -0
- package/agent/install.sh +511 -0
- package/agent/keybindings.json +7 -0
- package/bin/kiln.js +359 -0
- package/package.json +21 -0
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
# subagents — Design Plan
|
|
2
|
+
|
|
3
|
+
A pi extension that fires off background subagents from a parent pi session, where each
|
|
4
|
+
subagent can be powered by one of three backends — **pi** (in-process SDK session),
|
|
5
|
+
**Claude Code** (`@anthropic-ai/claude-agent-sdk`), or **Codex** (`codex app-server`) —
|
|
6
|
+
unified behind a single Effect v4 service interface.
|
|
7
|
+
|
|
8
|
+
> **Status:** this document describes the original v1 plan (stubbed backends). All
|
|
9
|
+
> three backends are now REAL implementations — see `src/backends/{pi,claude,codex}.ts`.
|
|
10
|
+
> The stub machinery survives in `src/backends/stub.ts` for the manager test registry.
|
|
11
|
+
|
|
12
|
+
**Scope of the first version:** interface design + stubbed backend internals + the v1 UI
|
|
13
|
+
carried over. No real Claude/Codex process integration yet; the pi backend may also stay
|
|
14
|
+
stubbed initially so the manager/UI/tool loop can be exercised end to end with zero
|
|
15
|
+
external dependencies.
|
|
16
|
+
|
|
17
|
+
**Location:** `/Users/davis/.pi/agent/extensions/subagents/` — fully self-contained
|
|
18
|
+
(no imports from `../shared` or `../subagents`; the handful of shared helpers v1 uses are
|
|
19
|
+
copied in).
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 1. V1 inventory (what must be preserved)
|
|
24
|
+
|
|
25
|
+
Source: `/Users/davis/.pi/agent/extensions/subagents/` (`index.ts`, `manager.ts`,
|
|
26
|
+
`prompt.ts`, `result-delivery.ts`, `takeover.ts`) plus `../shared/` helpers.
|
|
27
|
+
|
|
28
|
+
### 1.1 Tools exposed to the parent LLM
|
|
29
|
+
|
|
30
|
+
| Tool | Parameters | Behavior |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| `subagent_spawn` | `prompt`, `title`, `working_dir?`, `model?`, `provider?`, `reasoning_effort?` | Fire-and-forget spawn. Returns immediately with an id (`sa-N`). Enforces `MAX_RUNNING = 4` with a synchronous reservation so parallel tool calls can't race past the cap. Validates `working_dir`, resolves model against the registry (inherit parent model/thinking level by default), truncates title to 160 chars. |
|
|
33
|
+
| `subagent_wait` | `ids[]` (max 64) | Blocks until all listed subagents settle; respects the tool `AbortSignal`; streams `Waiting for ...` via `onUpdate`. Marks the awaited results "consumed" so they are not also auto-delivered. Output budgets: 48KB total, 16KB per agent, with per-section fallbacks (`[omitted: ...]`). Errors on unknown ids (lists known ids). |
|
|
34
|
+
| `subagent_cancel` | `ids[]` | Aborts running subagents (marks consumed first to avoid duplicate delivery), waits for settlement, reports per-id `Cancelled ...` / `was already <status>`. Partial transcripts remain on disk. |
|
|
35
|
+
| `subagent_check` | `id` | Non-blocking peek: status line, turn count, error text, up to 2KB/20 lines of latest output (includes the live streaming assistant message). Does not consume the result. |
|
|
36
|
+
| `subagent_list` | — | One `describeSubagent()` line per agent: `id [status] "title" (provider/model, ctx%, elapsed, cwd)`. |
|
|
37
|
+
|
|
38
|
+
Prompt metadata (all strings live in `prompt.ts`): `subagent_spawn` has a
|
|
39
|
+
`promptSnippet` and two `promptGuidelines` (delegate self-contained tasks; don't block on
|
|
40
|
+
`subagent_wait` unless necessary). Tool descriptions explain fire-and-forget semantics,
|
|
41
|
+
the concurrency cap, and that children can't orchestrate/see the parent conversation.
|
|
42
|
+
|
|
43
|
+
### 1.2 State tracking (v1 `SubagentManager`)
|
|
44
|
+
|
|
45
|
+
- Plain class with `Map<string, Subagent>`; each `Subagent` = `{ id, title, prompt, cwd,
|
|
46
|
+
session: AgentSession, status: "running" | "done" | "error", createdAt, settledAt?,
|
|
47
|
+
errorText?, unsubscribeLifecycle }`.
|
|
48
|
+
- Children are **in-process pi `AgentSession`s** created via the SDK
|
|
49
|
+
(`createAgentSession` + `SessionManager.create(cwd)` → real session files visible in
|
|
50
|
+
`/resume`), with child resources loaded per-cwd (`DefaultResourceLoader`, trust-gated
|
|
51
|
+
project resources) and a tool denylist (`excludeTools`: the subagent_* tools,
|
|
52
|
+
`workflow`, `ask_user`).
|
|
53
|
+
- Settlement is driven by session lifecycle events (`agent_start` re-marks running;
|
|
54
|
+
`agent_settled` settles). Failure detection: thrown prompt error, last assistant
|
|
55
|
+
`stopReason === "error" | "aborted"`, error text bounded to 4096 chars.
|
|
56
|
+
- Change notification: `addChangeListener()` + `nextChange(signal)` promise — used by
|
|
57
|
+
`waitFor`, the footer status, and the dashboard.
|
|
58
|
+
- `waitFor(ids, signal, onPending)` keeps a `waitInterest` refcount per id so settles
|
|
59
|
+
during an active wait are marked consumed.
|
|
60
|
+
- `send(sub, text)`: steer via `session.steer()` while streaming, else start a fresh
|
|
61
|
+
`prompt()` run (used by takeover).
|
|
62
|
+
- Caps and cleanup: `MAX_RUNNING = 4`, `MAX_TRACKED = 64` with LRU pruning of settled
|
|
63
|
+
agents, `STOP_TIMEOUT_MS = 5s` bounded aborts, force-dispose fallback, idempotent
|
|
64
|
+
`disposeAll()` on `session_shutdown`.
|
|
65
|
+
|
|
66
|
+
### 1.3 Result delivery back to the parent
|
|
67
|
+
|
|
68
|
+
- When a child settles **unconsumed**, `onSettled` defers it into a tiny
|
|
69
|
+
`createDeferredResultDelivery` buffer (defer/consume/drain/clear keyed by id).
|
|
70
|
+
- Flush happens when the parent goes idle: immediately if `sessionContext.isIdle()`,
|
|
71
|
+
otherwise on the parent's `agent_settled` event. A later `subagent_wait` can still
|
|
72
|
+
consume a deferred result before flush (that's why it is a buffer, not an immediate
|
|
73
|
+
send).
|
|
74
|
+
- Delivery = `pi.sendMessage({ customType: "subagent-result", content, display: true,
|
|
75
|
+
details: { id, title, status } }, { deliverAs: "followUp", triggerTurn: true })`.
|
|
76
|
+
Content is built by `buildSubagentResultMessage` (`Subagent sa-N "title"
|
|
77
|
+
finished/failed.` + optional `Error:` line + output truncated to 24KB/600 lines with a
|
|
78
|
+
pointer to the child session file for the full transcript).
|
|
79
|
+
|
|
80
|
+
### 1.4 UI (carried over into v2 essentially as-is)
|
|
81
|
+
|
|
82
|
+
1. **Footer status** (`ctx.ui.setStatus("subagents", ...)`): `subagents: ■ 2 running ·
|
|
83
|
+
■ 1 done · ■ 1 failed · /subagents to view` (warning/success/error colored squares;
|
|
84
|
+
cleared when no subagents). Driven by manager change listener.
|
|
85
|
+
2. **`subagent-result` message renderer**: status icon (`■`/`x`) + bold accent header
|
|
86
|
+
`subagent sa-N · title · finished/failed`; collapsed = first 8 body lines +
|
|
87
|
+
`... (ctrl+o to expand)`; expanded = header + `Markdown` component render of the body.
|
|
88
|
+
3. **`/subagents` command** → `openSubagentPicker` loop (TUI mode only; notifies and
|
|
89
|
+
bails in non-TUI or when there are no subagents):
|
|
90
|
+
- **SubagentDashboard** — fullscreen overlay (`anchor: "center", width: "100%",
|
|
91
|
+
maxHeight: "100%"`), bordered list panel titled `agents · settled/total`. Each row:
|
|
92
|
+
selection marker `❯`, status glyph, title, dim id on the left; model id · context
|
|
93
|
+
utilization (`%/capacity`) · elapsed · status word on the right. Scroll window
|
|
94
|
+
centered on the selection with `... N more` markers. 1Hz ticker re-render for
|
|
95
|
+
elapsed/token columns + manager change subscription. Keys: `tui.select.up/down`
|
|
96
|
+
**and** `j`/`k` to move, `tui.select.confirm` to take over, `x` to abort the
|
|
97
|
+
selected running agent, `tui.select.cancel` to close. Hint line shows the
|
|
98
|
+
*configured* keys via `keybindings.getKeys()`.
|
|
99
|
+
- **TakeoverView** — fullscreen overlay for one subagent: header line (status glyph,
|
|
100
|
+
`id · title · status · elapsed · provider/model · ctx%`), fixed-height transcript
|
|
101
|
+
viewport (error line and scroll indicator consume viewport rows so height never
|
|
102
|
+
jumps), an `Input` line, and a hint row. Keys: `tui.input.submit` send (steer if
|
|
103
|
+
streaming, new run if idle), `app.interrupt`/`tui.select.cancel` back to dashboard,
|
|
104
|
+
`app.clear` abort run, `tui.editor.cursorUp/Down` scroll ±6 lines,
|
|
105
|
+
`tui.editor.pageUp/Down` page. Renders are throttled to 50ms because streaming can
|
|
106
|
+
emit per-token events.
|
|
107
|
+
- **Transcript rendering** (`buildTranscriptLines`): sanitizes ANSI/tabs/control
|
|
108
|
+
chars; user messages as `> ` accent-prefixed wrapped lines; assistant text wrapped
|
|
109
|
+
plain; thinking as dim italic `~ ` lines; tool calls as `→ toolname {args}`; tool
|
|
110
|
+
results as one dim `output:`/red `error:` first line. Includes the **live streaming
|
|
111
|
+
assistant message**, **live tool executions** (running/done/error marker + first
|
|
112
|
+
output line preview, tracked from `tool_execution_*` events until the final tool
|
|
113
|
+
result message lands), and **queued steering/follow-up messages** (`> [queued
|
|
114
|
+
steer] ...`) so Enter visibly acknowledges input.
|
|
115
|
+
|
|
116
|
+
**V2 requirement:** all of the above renders from a *normalized* per-subagent view
|
|
117
|
+
instead of poking at `sub.session` directly — that is the main UI refactor, everything
|
|
118
|
+
else ports over mostly verbatim.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## 2. Backend integration facts
|
|
123
|
+
|
|
124
|
+
These shape the interface even though v1-of-v2 stubs the internals. (Traced from the
|
|
125
|
+
"T3 Code" codebase which integrates Codex and Claude Code.)
|
|
126
|
+
|
|
127
|
+
| | Interactive sessions | One-shot tasks | Event shape | Interrupt | Steering |
|
|
128
|
+
|---|---|---|---|---|---|
|
|
129
|
+
| **pi** | In-process `createAgentSession()` (pi SDK); real session files; `session.subscribe()` | `session.prompt()` and read final assistant message (or `pi -p` subprocess, not needed) | `AgentSessionEvent` (message_start/update/end, tool_execution_*, agent_start/settled, queue_update, ...) | `session.abort()` | `session.steer()` / `followUp()` |
|
|
130
|
+
| **Claude Code** | `@anthropic-ai/claude-agent-sdk` `query()` — SDK launches the `claude` executable and streams JSON messages (assistant/user/result/system, streaming partials, tool_use blocks) | `claude -p --output-format json` | SDK message stream (async iterable) | `query.interrupt()` / abort controller | streaming-input mode: push more user messages into the input iterable |
|
|
131
|
+
| **Codex** | spawn `codex app-server` child process, JSON-RPC over stdin/stdout (`newConversation` / `sendUserTurn`, notifications: `agentMessageDelta`, `execCommandBegin/End`, `taskComplete`, `tokenCount`, ...) | `codex exec` (prints result to stdout, `--json` for events) | JSON-RPC notifications | `interruptConversation` request | send another `sendUserTurn` on the same conversation |
|
|
132
|
+
|
|
133
|
+
Common denominator all three can supply:
|
|
134
|
+
|
|
135
|
+
- an async event stream with: lifecycle (started/turn/settled), assistant text
|
|
136
|
+
(deltas and/or completed messages), reasoning text, tool execution begin/update/end,
|
|
137
|
+
token usage, errors;
|
|
138
|
+
- a way to send a follow-up/steering user message into a live session;
|
|
139
|
+
- an interrupt operation;
|
|
140
|
+
- a final result text per run;
|
|
141
|
+
- metadata: backend name, model identifier, session/log file path (pi session file,
|
|
142
|
+
Claude session id + projects dir JSONL, Codex rollout path), working dir.
|
|
143
|
+
|
|
144
|
+
That is exactly what the normalized event model below encodes.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## 3. Architecture
|
|
149
|
+
|
|
150
|
+
### 3.1 Effect v4 conventions used
|
|
151
|
+
|
|
152
|
+
- Single `effect` package (v4 beta). Services defined with
|
|
153
|
+
`ServiceMap.Service<T>()("id", ...)` / `ServiceMap.Key`; wiring via `Layer`;
|
|
154
|
+
`ManagedRuntime.make(layer)` at the extension edge with `runtime.runPromise(effect,
|
|
155
|
+
{ signal })` inside `async execute()` tool handlers and `await runtime.dispose()` on
|
|
156
|
+
`session_shutdown`.
|
|
157
|
+
- `Effect.gen` generators throughout the internals. `async`/`Promise` appears **only**
|
|
158
|
+
in: tool `execute()` bodies, `pi.on(...)` handlers, the `/subagents` command handler,
|
|
159
|
+
and the imperative TUI component classes (which are callback-driven, not effectful).
|
|
160
|
+
- Streams: `Stream<SubagentEvent>` per subagent, produced by backends
|
|
161
|
+
(`Stream.callback` for push-based sources like JSON-RPC notifications / SDK
|
|
162
|
+
iterables), consumed by a manager fiber per subagent.
|
|
163
|
+
- Errors: tagged error classes (`Data.TaggedError`) — `SpawnError`, `BackendUnavailable`,
|
|
164
|
+
`SubagentNotFound`, `ConcurrencyLimitError`, `SendError`, `InterruptTimeout`. Tool
|
|
165
|
+
handlers map these to thrown `Error`s with the same user-facing messages v1 uses.
|
|
166
|
+
|
|
167
|
+
### 3.2 Domain model (`src/domain.ts`)
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
type BackendName = "pi" | "claude" | "codex";
|
|
171
|
+
type SubagentStatus = "running" | "done" | "error"; // unchanged from v1
|
|
172
|
+
|
|
173
|
+
interface SpawnTask {
|
|
174
|
+
prompt: string;
|
|
175
|
+
title: string;
|
|
176
|
+
cwd: string;
|
|
177
|
+
// Generic model hint; each backend interprets/validates it its own way.
|
|
178
|
+
model?: string; // pi: "provider/model-id"; claude: model alias; codex: model slug
|
|
179
|
+
reasoningEffort?: string; // pi thinking level; codex reasoning effort; claude: ignored/mapped
|
|
180
|
+
parentContext: { // resolved by the tool layer, passed opaquely
|
|
181
|
+
parentCwd: string;
|
|
182
|
+
projectTrusted: boolean;
|
|
183
|
+
inheritedModelRef?: { provider: string; id: string }; // pi only
|
|
184
|
+
inheritedThinkingLevel?: string;
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
interface SubagentMeta {
|
|
189
|
+
backend: BackendName;
|
|
190
|
+
modelLabel?: string; // "anthropic/claude-opus-4-5", "gpt-5-codex", ...
|
|
191
|
+
contextWindow?: number; // for utilization %, when known
|
|
192
|
+
sessionFilePath?: string; // pi session file / claude JSONL / codex rollout path
|
|
193
|
+
nativeSessionId?: string; // claude session id, codex conversation id
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### 3.3 Normalized event model (`src/domain.ts`)
|
|
198
|
+
|
|
199
|
+
One discriminated union covers everything the v1 UI and manager need. Backends translate
|
|
200
|
+
their native streams into this; nothing downstream knows which backend produced it.
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
type SubagentEvent =
|
|
204
|
+
// lifecycle
|
|
205
|
+
| { _tag: "RunStarted" } // pi agent_start / claude init / codex turn start
|
|
206
|
+
| { _tag: "RunSettled"; outcome: RunOutcome } // terminal per run (a session can run again via send())
|
|
207
|
+
// transcript building blocks
|
|
208
|
+
| { _tag: "UserMessage"; text: string } // initial prompt + takeover sends, echoed by backend
|
|
209
|
+
| { _tag: "AssistantDelta"; kind: "text" | "thinking"; delta: string }
|
|
210
|
+
| { _tag: "AssistantMessage"; parts: TranscriptPart[] } // finalized message (replaces live buffer)
|
|
211
|
+
| { _tag: "ToolStart"; toolId: string; name: string; argsPreview?: string }
|
|
212
|
+
| { _tag: "ToolUpdate"; toolId: string; outputPreview?: string }
|
|
213
|
+
| { _tag: "ToolEnd"; toolId: string; isError: boolean; outputPreview?: string }
|
|
214
|
+
// bookkeeping
|
|
215
|
+
| { _tag: "QueueChanged"; queued: ReadonlyArray<{ text: string; kind: "steer" | "follow-up" }> }
|
|
216
|
+
| { _tag: "UsageChanged"; tokens?: number; contextWindow?: number }
|
|
217
|
+
| { _tag: "MetaChanged"; meta: Partial<SubagentMeta> } // model switched, session file known, ...
|
|
218
|
+
| { _tag: "BackendError"; message: string }; // non-fatal diagnostics (fatal → RunSettled outcome)
|
|
219
|
+
|
|
220
|
+
type RunOutcome =
|
|
221
|
+
| { _tag: "Completed"; finalText: string }
|
|
222
|
+
| { _tag: "Failed"; errorText: string; partialText?: string }
|
|
223
|
+
| { _tag: "Interrupted"; partialText?: string };
|
|
224
|
+
|
|
225
|
+
type TranscriptPart =
|
|
226
|
+
| { type: "text"; text: string }
|
|
227
|
+
| { type: "thinking"; text: string; redacted?: boolean }
|
|
228
|
+
| { type: "toolCall"; toolId: string; name: string; argsPreview?: string };
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Mapping sanity check against what the v1 UI renders:
|
|
232
|
+
|
|
233
|
+
| v1 render source | v2 event source |
|
|
234
|
+
|---|---|
|
|
235
|
+
| `sub.session.messages` (user/assistant/toolResult) | fold of `UserMessage` / `AssistantMessage` / `ToolEnd` into the snapshot transcript |
|
|
236
|
+
| streaming message (`agent.state.streamingMessage`) | live buffer fed by `AssistantDelta` (cleared on `AssistantMessage`/`RunSettled`) |
|
|
237
|
+
| live tool map from `tool_execution_*` | `ToolStart/Update/End` (entries drop when the finalized assistant/tool item lands, same as v1) |
|
|
238
|
+
| queued steer/follow-up messages | `QueueChanged` |
|
|
239
|
+
| status / errorText / settledAt | `RunStarted` / `RunSettled` |
|
|
240
|
+
| model + context utilization columns | `MetaChanged` + `UsageChanged` |
|
|
241
|
+
| `finalOutput` / `latestOutput` for check/wait/result delivery | `RunOutcome.finalText` (+ live buffer for `latestOutput`) |
|
|
242
|
+
|
|
243
|
+
Previews (`argsPreview`, `outputPreview`) are pre-flattened single-line strings because
|
|
244
|
+
the UI only ever shows one sanitized line — this avoids leaking three different native
|
|
245
|
+
"tool result" shapes through the interface.
|
|
246
|
+
|
|
247
|
+
### 3.4 The `SubagentBackend` service (`src/backend.ts`)
|
|
248
|
+
|
|
249
|
+
One interface; three implementations; a registry keyed by `BackendName`.
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
interface SubagentBackend {
|
|
253
|
+
readonly name: BackendName;
|
|
254
|
+
readonly capabilities: {
|
|
255
|
+
steering: boolean; // can send() into a live run (all three eventually; stubs: true)
|
|
256
|
+
modelSelection: boolean;
|
|
257
|
+
reasoningEffort: boolean;
|
|
258
|
+
};
|
|
259
|
+
/** Probe availability (binary on PATH, SDK importable, API key). Cheap + cached. */
|
|
260
|
+
readonly available: Effect.Effect<boolean>;
|
|
261
|
+
/**
|
|
262
|
+
* Spawn a session. Scoped: releasing the scope interrupts/kills the underlying
|
|
263
|
+
* session/process. Returns a live handle immediately (fire-and-forget semantics
|
|
264
|
+
* live in the manager, not here).
|
|
265
|
+
*/
|
|
266
|
+
spawn(task: SpawnTask): Effect.Effect<SubagentSession, SpawnError, Scope.Scope>;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
interface SubagentSession {
|
|
270
|
+
readonly meta: Effect.Effect<SubagentMeta>; // snapshot; also updated via MetaChanged
|
|
271
|
+
/** All activity. Single consumer (the manager). Ends after the final RunSettled on close. */
|
|
272
|
+
readonly events: Stream.Stream<SubagentEvent, never>;
|
|
273
|
+
/** Steer while running, or start a fresh run when idle (v1 `manager.send` semantics). */
|
|
274
|
+
send(text: string): Effect.Effect<void, SendError>;
|
|
275
|
+
/** Interrupt the active run; resolves when the backend acknowledges. Bounded by the caller. */
|
|
276
|
+
readonly interrupt: Effect.Effect<void>;
|
|
277
|
+
/** Resolves with the outcome of the most recent run (mirrors the last RunSettled). */
|
|
278
|
+
readonly awaitSettled: Effect.Effect<RunOutcome>;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Registry: a plain ServiceMap.Key holding a ReadonlyMap<BackendName, SubagentBackend>,
|
|
282
|
+
// built by a Layer that collects the three backend layers. Adding a 4th backend = one
|
|
283
|
+
// new file + one line in the registry layer.
|
|
284
|
+
class BackendRegistry extends ServiceMap.Key<BackendRegistry,
|
|
285
|
+
ReadonlyMap<BackendName, SubagentBackend>>()("subagents/BackendRegistry") {}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Design choices worth calling out:
|
|
289
|
+
|
|
290
|
+
- **`spawn` is scoped, not paired with an explicit `dispose`.** The manager opens one
|
|
291
|
+
`Scope` per subagent and closes it on cancel/prune/disposeAll — this replaces v1's
|
|
292
|
+
`shutdownAndDisposeChildSession` + WeakMap-idempotence machinery with Effect's own
|
|
293
|
+
guaranteed-once finalization. Timeout-bounded teardown (`5s`, then force) is a
|
|
294
|
+
finalizer concern inside each backend.
|
|
295
|
+
- **`send` unifies steer/new-run.** v1's `manager.send` already had these semantics; the
|
|
296
|
+
interface keeps the decision inside the backend because "is a run active" is
|
|
297
|
+
backend-native state.
|
|
298
|
+
- **Events, not message arrays, are the contract.** The manager folds events into
|
|
299
|
+
snapshots; backends never expose native message types. The pi backend has the richest
|
|
300
|
+
native data and simply down-converts.
|
|
301
|
+
- **No `exec()` one-shot operation in v1 of this extension** — see Open Questions; the
|
|
302
|
+
interface deliberately leaves room to add `exec(task): Effect<RunOutcome>` later
|
|
303
|
+
without touching the manager.
|
|
304
|
+
|
|
305
|
+
### 3.5 `SubagentManager` service (`src/manager.ts`)
|
|
306
|
+
|
|
307
|
+
Owns the registry of running/finished subagents. Effect service; internally a `Ref` (or
|
|
308
|
+
plain mutable map guarded by the single-threaded JS model) of entries plus a
|
|
309
|
+
**synchronous read model** for the TUI.
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
interface SubagentEntry {
|
|
313
|
+
id: string; // "sa-N", same scheme as v1
|
|
314
|
+
backend: BackendName;
|
|
315
|
+
title: string; prompt: string; cwd: string;
|
|
316
|
+
scope: Scope.Closeable; // owns the SubagentSession
|
|
317
|
+
session: SubagentSession;
|
|
318
|
+
eventPump: Fiber.Fiber<void>; // folds events → snapshot, fires settle hooks
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
interface SubagentSnapshot { // what the UI and tools read; plain immutable data
|
|
322
|
+
id: string; backend: BackendName; title: string; cwd: string;
|
|
323
|
+
status: SubagentStatus;
|
|
324
|
+
createdAt: number; settledAt?: number;
|
|
325
|
+
errorText?: string;
|
|
326
|
+
meta: SubagentMeta;
|
|
327
|
+
usage: { tokens?: number; contextWindow?: number };
|
|
328
|
+
transcript: ReadonlyArray<TranscriptItem>; // finalized items
|
|
329
|
+
liveAssistant?: { text: string; thinking: string };
|
|
330
|
+
liveTools: ReadonlyArray<LiveToolState>; // v1's LiveToolEvent, verbatim
|
|
331
|
+
queued: ReadonlyArray<{ text: string; kind: "steer" | "follow-up" }>;
|
|
332
|
+
finalText: string; // last Completed finalText (v1 finalOutput)
|
|
333
|
+
latestText: string; // finalText or live buffer (v1 latestOutput)
|
|
334
|
+
turns: number; // count of AssistantMessage events (for subagent_check)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
class SubagentManager extends ServiceMap.Key<SubagentManager, {
|
|
338
|
+
spawn(backend: BackendName, task: SpawnTask):
|
|
339
|
+
Effect.Effect<SubagentSnapshot, SpawnError | ConcurrencyLimitError | BackendUnavailable>;
|
|
340
|
+
waitFor(ids: string[], onPending?: (pending: string[]) => void):
|
|
341
|
+
Effect.Effect<void, SubagentNotFound>; // interruption = tool signal abort
|
|
342
|
+
cancel(ids: string[]): Effect.Effect<CancelReport, SubagentNotFound>;
|
|
343
|
+
send(id: string, text: string): Effect.Effect<void, SubagentNotFound | SendError>;
|
|
344
|
+
get(id: string): Effect.Effect<SubagentSnapshot | undefined>;
|
|
345
|
+
list: Effect.Effect<ReadonlyArray<SubagentSnapshot>>;
|
|
346
|
+
disposeAll: Effect.Effect<void>;
|
|
347
|
+
/** Synchronous read model for the TUI (see 3.6). */
|
|
348
|
+
readonly view: SubagentReadModel;
|
|
349
|
+
}>()("subagents/SubagentManager") {}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Behavior preserved from v1, expressed in Effect terms:
|
|
353
|
+
|
|
354
|
+
- **Concurrency cap**: `MAX_RUNNING = 4` enforced with a synchronous
|
|
355
|
+
reserve-before-first-yield counter (same race-avoidance rationale as v1); the cap
|
|
356
|
+
counts *running* agents across all backends (see Open Questions for per-backend caps).
|
|
357
|
+
- **Settlement**: the per-subagent event pump fiber updates the snapshot on every event;
|
|
358
|
+
on `RunSettled` it computes `status`/`errorText` (bounded to 4096 chars) and invokes
|
|
359
|
+
the settle hook with `consumed = waitInterest > 0`. `waitFor` keeps the same
|
|
360
|
+
wait-interest refcounts (a `Ref<Map<string, number>>`) and wakes on snapshot changes
|
|
361
|
+
(a `Latch`/`PubSub`-based "next change" primitive replacing v1's resolver array).
|
|
362
|
+
- **Cancel**: mark consumed → `session.interrupt` with 5s bound → close scope on
|
|
363
|
+
timeout → wait for settle. Same "already \<status\>" reporting.
|
|
364
|
+
- **Pruning**: `MAX_TRACKED = 64`, oldest settled non-wait-interested entries pruned by
|
|
365
|
+
closing their scopes; cleanup tracked so `disposeAll` can await it.
|
|
366
|
+
- **Settle → result delivery hook**: the manager exposes `onSettled` wiring identical in
|
|
367
|
+
spirit to v1: the extension layer registers a callback that defers into the
|
|
368
|
+
`result-delivery` buffer and flushes on parent idle / `agent_settled`. The
|
|
369
|
+
`createDeferredResultDelivery` module is copied over unchanged (it is pure and already
|
|
370
|
+
has a test).
|
|
371
|
+
|
|
372
|
+
### 3.6 Synchronous read model for the TUI (`src/read-model.ts`)
|
|
373
|
+
|
|
374
|
+
The TUI components (`Component` classes with `render(width)`/`handleInput(data)`) are
|
|
375
|
+
imperative and render synchronously — they cannot `yield*` effects. Bridge:
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
interface SubagentReadModel {
|
|
379
|
+
list(): ReadonlyArray<SubagentSnapshot>; // sync snapshot reads
|
|
380
|
+
get(id: string): SubagentSnapshot | undefined;
|
|
381
|
+
subscribe(listener: () => void): () => void; // any-change notification (dashboard, footer)
|
|
382
|
+
subscribeTo(id: string, l: () => void): () => void; // per-agent (takeover view)
|
|
383
|
+
// sync fire-and-forget commands, executed via the ManagedRuntime under the hood:
|
|
384
|
+
requestSend(id: string, text: string): void; // TakeoverView input submit
|
|
385
|
+
requestAbort(id: string): void; // dashboard `x`, takeover app.clear
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
The manager's event pump writes each new snapshot into this store (plain mutable map +
|
|
390
|
+
listener set) as its last step, so the UI is always at most one microtask behind the
|
|
391
|
+
Effect world and v1's dashboard/takeover code ports with only these substitutions:
|
|
392
|
+
|
|
393
|
+
| v1 | v2 |
|
|
394
|
+
|---|---|
|
|
395
|
+
| `manager.list()` / `manager.get(id)` | `view.list()` / `view.get(id)` |
|
|
396
|
+
| `manager.addChangeListener` | `view.subscribe` |
|
|
397
|
+
| `sub.session.subscribe(handleSessionEvent)` + local live-tool map | `view.subscribeTo(id, ...)` + read `snapshot.liveTools` / `liveAssistant` (the fold moved into the manager) |
|
|
398
|
+
| `manager.send(sub, text)` / `manager.abort(sub)` | `view.requestSend(id, text)` / `view.requestAbort(id)` |
|
|
399
|
+
| `buildTranscriptLines(sub, ...)` reading `session.messages` | `buildTranscriptLines(snapshot, ...)` reading `snapshot.transcript` + live state + `snapshot.queued` |
|
|
400
|
+
|
|
401
|
+
Keybindings, layout math, scroll behavior, 1Hz ticker, 50ms render throttle, sanitize
|
|
402
|
+
logic: copied as-is.
|
|
403
|
+
|
|
404
|
+
### 3.7 Extension edge (`index.ts` + `src/runtime.ts`)
|
|
405
|
+
|
|
406
|
+
```
|
|
407
|
+
Layer graph:
|
|
408
|
+
PiBackendStub.layer ─┐
|
|
409
|
+
ClaudeBackendStub.layer ─┼→ BackendRegistry.layer ─→ SubagentManager.layer ─→ AppLayer
|
|
410
|
+
CodexBackendStub.layer ─┘
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
- `const runtime = ManagedRuntime.make(AppLayer)` — created lazily on first use (per the
|
|
414
|
+
extension-docs guidance to not start background resources in the factory;
|
|
415
|
+
`ManagedRuntime` builds its layer on first run, which satisfies this, but we still
|
|
416
|
+
gate creation behind `session_start`). `session_shutdown`: `runtime.runPromise(
|
|
417
|
+
manager.disposeAll)` then `await runtime.dispose()`, then recreate on the next
|
|
418
|
+
`session_start` (handles `/new`, `/resume`, `/reload`).
|
|
419
|
+
- **Tool handlers are the async boundary.** Each `execute(toolCallId, params, signal,
|
|
420
|
+
onUpdate, ctx)` builds one `Effect.gen` program and runs it with
|
|
421
|
+
`runtime.runPromise(program, { signal })`; tool-visible errors are converted from
|
|
422
|
+
tagged errors to `Error` messages matching v1 wording. `onUpdate` and `ctx`
|
|
423
|
+
(model registry, cwd, trust) are captured into the program as plain values/callbacks.
|
|
424
|
+
- **Tool schema change:** `subagent_spawn` gains
|
|
425
|
+
`agent: StringEnum(["pi", "claude", "codex"])` (optional, default `"pi"`), and
|
|
426
|
+
`model`/`provider`/`reasoning_effort` keep their v1 shapes but are documented as
|
|
427
|
+
backend-interpreted (pi validates against the registry; claude/codex validate against
|
|
428
|
+
their own known-model rules — stubs accept anything). `describeSubagent` lines and the
|
|
429
|
+
dashboard gain the backend name (e.g. `sa-3 [running] "title" (codex, gpt-5-codex,
|
|
430
|
+
41%/272k, 1m32s, /repo)`).
|
|
431
|
+
- `pi.registerMessageRenderer("subagent-result", ...)`, `pi.registerCommand(
|
|
432
|
+
"subagents", ...)`, footer status updates, and the result-delivery flush hooks
|
|
433
|
+
(`agent_settled`, idle-check on settle) are wired exactly like v1 — these all live
|
|
434
|
+
outside the runtime and call into it only via `runPromise`/the read model.
|
|
435
|
+
|
|
436
|
+
### 3.8 What the stubs do (v1 of this extension)
|
|
437
|
+
|
|
438
|
+
All three backends share a `createStubSession(profile)` helper (`src/backends/stub.ts`)
|
|
439
|
+
that fakes a plausible session so the manager, tools, result delivery, and both TUI
|
|
440
|
+
views are exercised end to end:
|
|
441
|
+
|
|
442
|
+
- **spawn**: emits `MetaChanged` (backend-flavored model label + fake session file path
|
|
443
|
+
under `os.tmpdir()`, e.g. `.../subagents-stub/sa-1.jsonl`, actually written with the
|
|
444
|
+
transcript so "full transcript in session file" pointers resolve), then `RunStarted`,
|
|
445
|
+
then a scripted turn: 2–3 `AssistantDelta` batches on a timer (~200ms cadence so
|
|
446
|
+
streaming is visible), one fake `ToolStart/Update/End` cycle (`bash` with an args
|
|
447
|
+
preview), `UsageChanged` ramping tokens, a final `AssistantMessage`, and `RunSettled`
|
|
448
|
+
with `Completed` — final text echoes the task: `"[stub:claude] completed: <first 200
|
|
449
|
+
chars of prompt>"`. Total runtime ~3–6s (configurable per profile) so `subagent_wait`,
|
|
450
|
+
the footer counters, and the dashboard's running→done transition are observable.
|
|
451
|
+
- **send**: emits `UserMessage` + `QueueChanged` (briefly, to exercise the queued-line
|
|
452
|
+
rendering) and runs another scripted turn — so takeover steering works.
|
|
453
|
+
- **interrupt**: stops the script timer and settles with `Interrupted` (→ status
|
|
454
|
+
`error`, errorText `"Run was aborted"`, matching v1) — so `subagent_cancel` and the
|
|
455
|
+
`x`/`app.clear` keybindings work.
|
|
456
|
+
- **failure path**: a magic prompt prefix (e.g. `FAIL:`) makes the run settle with
|
|
457
|
+
`Failed` — so error rendering, `errorText` rows, and failed result delivery are
|
|
458
|
+
testable without real backends.
|
|
459
|
+
- **Backend differentiation**: per-backend profiles vary the model label
|
|
460
|
+
(`anthropic/claude-opus-4-5` vs `claude-sonnet-4-5` vs `gpt-5-codex`), fake context
|
|
461
|
+
window, tool names, and delta cadence — enough to verify the UI treats backends
|
|
462
|
+
uniformly. `available` returns `true` for stubs (real impls will probe binaries/SDK).
|
|
463
|
+
- The **pi stub** can later be swapped for the real in-process SDK implementation by
|
|
464
|
+
porting v1's `manager.ts` session code behind the same `SubagentSession` shape; that
|
|
465
|
+
port is the first post-stub milestone.
|
|
466
|
+
|
|
467
|
+
---
|
|
468
|
+
|
|
469
|
+
## 4. File/module layout
|
|
470
|
+
|
|
471
|
+
```
|
|
472
|
+
/Users/davis/.pi/agent/extensions/subagents/
|
|
473
|
+
├── package.json # name, "effect": "^4.0.0-beta.x"; pi extension entry via pi.extensions
|
|
474
|
+
├── package-lock.json / node_modules/ (after npm install)
|
|
475
|
+
├── docs/
|
|
476
|
+
│ └── design-plan.md # this document
|
|
477
|
+
├── index.ts # extension factory: runtime lifecycle, 5 tools, /subagents
|
|
478
|
+
│ # command, message renderer, footer status, result flush hooks
|
|
479
|
+
└── src/
|
|
480
|
+
├── domain.ts # BackendName, SubagentStatus, SpawnTask, SubagentEvent,
|
|
481
|
+
│ # RunOutcome, TranscriptItem/Part, SubagentSnapshot, tagged errors
|
|
482
|
+
├── backend.ts # SubagentBackend + SubagentSession interfaces, BackendRegistry
|
|
483
|
+
│ # key + registry layer
|
|
484
|
+
├── backends/
|
|
485
|
+
│ ├── stub.ts # shared scripted fake-session machinery
|
|
486
|
+
│ ├── pi.ts # PiBackend layer (v1: stub profile; later: real pi SDK sessions)
|
|
487
|
+
│ ├── claude.ts # ClaudeBackend layer (v1: stub; later: @anthropic-ai/claude-agent-sdk)
|
|
488
|
+
│ └── codex.ts # CodexBackend layer (v1: stub; later: codex app-server JSON-RPC)
|
|
489
|
+
├── manager.ts # SubagentManager service + layer: registry, cap, waitFor,
|
|
490
|
+
│ # cancel, prune, settle hook, event-fold into snapshots
|
|
491
|
+
├── read-model.ts # sync SubagentReadModel bridge for the TUI
|
|
492
|
+
├── runtime.ts # AppLayer composition + ManagedRuntime create/dispose helpers
|
|
493
|
+
├── result-delivery.ts # deferred delivery buffer (copied from v1, unchanged)
|
|
494
|
+
├── result-delivery.test.ts
|
|
495
|
+
├── prompt.ts # all model-facing strings (v1 copy + `agent` param description)
|
|
496
|
+
├── format.ts # elapsed/context-utilization/activity-status formatting
|
|
497
|
+
│ # (merged copies of ../shared/{context-utilization,activity-status}.ts)
|
|
498
|
+
└── ui/
|
|
499
|
+
├── transcript.ts # sanitize + buildTranscriptLines over SubagentSnapshot
|
|
500
|
+
└── takeover.ts # SubagentDashboard + TakeoverView + openSubagentPicker (ported)
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
Notes:
|
|
504
|
+
- `package.json` is needed because `effect` is an npm dependency (extension-with-deps
|
|
505
|
+
style from the extension docs). Everything else avoids new dependencies.
|
|
506
|
+
- v1's `child-session.ts` trust/tool-policy helpers are **not** copied in v1 of v2 (the
|
|
507
|
+
stubs don't need them); the real pi backend will bring the needed subset into
|
|
508
|
+
`backends/pi.ts` when implemented. The `resolveStandaloneChildProjectTrust` logic *is*
|
|
509
|
+
still referenced by the design (SpawnTask.parentContext.projectTrusted) so the tool
|
|
510
|
+
layer computes trust the same way v1 does.
|
|
511
|
+
- Suggested project scripts (per house rules, to be added): `check` (`tsc --noEmit`),
|
|
512
|
+
`test` (`node --test` or vitest for `result-delivery` + manager fold tests against
|
|
513
|
+
stub backends).
|
|
514
|
+
|
|
515
|
+
---
|
|
516
|
+
|
|
517
|
+
## 5. Migration/coexistence note
|
|
518
|
+
|
|
519
|
+
v1 and v2 register the same tool names (`subagent_spawn`, ...) and the same
|
|
520
|
+
`/subagents` command. While both live in `~/.pi/agent/extensions/`, pi will suffix
|
|
521
|
+
duplicate commands (`/subagents:1`, `/subagents:2`) and both tool sets would be
|
|
522
|
+
registered. During development, either (a) v2 uses temporary names
|
|
523
|
+
(`subagent2_spawn`, `/subagents2`), or (b) v1 is moved out of the auto-discovery dir.
|
|
524
|
+
Recommendation: (a) during development, rename to final names when v2 replaces v1.
|
|
525
|
+
|
|
526
|
+
---
|
|
527
|
+
|
|
528
|
+
## 6. Open questions (need user input)
|
|
529
|
+
|
|
530
|
+
1. **Per-backend spawn options.** v1's `model`/`provider`/`reasoning_effort` are
|
|
531
|
+
pi-shaped. Options: (a) keep one generic `model` string + `reasoning_effort` that
|
|
532
|
+
each backend interprets (proposed above — simplest for the LLM); (b) add a
|
|
533
|
+
`backend_options` free-form object; (c) per-backend defaults in a config file with no
|
|
534
|
+
per-spawn override. Which surface do you want the parent LLM to have?
|
|
535
|
+
2. **One-shot exec mode.** Should the interface expose a separate cheap
|
|
536
|
+
`exec(task): Effect<RunOutcome>` (mapping to `codex exec` / `claude -p
|
|
537
|
+
--output-format json` / a fresh in-memory pi session), or is one interactive-session
|
|
538
|
+
path enough? Exec would forfeit takeover/steering for that subagent — is a
|
|
539
|
+
`mode: "session" | "exec"` spawn parameter desirable, or backend-internal
|
|
540
|
+
optimization only?
|
|
541
|
+
3. **Permissions/sandboxing for Claude/Codex children.** Subagents are headless, so
|
|
542
|
+
interactive permission prompts are impossible. Do we run Claude with
|
|
543
|
+
`bypassPermissions`/`--dangerously-skip-permissions` and Codex with
|
|
544
|
+
`--full-auto`-style sandbox + never-ask approval policy? Should this be a global
|
|
545
|
+
extension setting, per-spawn, or hardcoded? (Pi children inherit v1's trust-store
|
|
546
|
+
logic — keep that as-is?)
|
|
547
|
+
4. **Concurrency cap scope.** Keep one global `MAX_RUNNING = 4`, or per-backend caps
|
|
548
|
+
(e.g. 4 pi + 2 claude + 2 codex)? Global is proposed as default.
|
|
549
|
+
5. **Steering support parity in real backends.** Codex steering means
|
|
550
|
+
interrupt-then-new-turn or queued `sendUserTurn`; Claude requires streaming-input
|
|
551
|
+
mode from the start. OK to declare `capabilities.steering` and have the TakeoverView
|
|
552
|
+
input show "(steering not supported)" if a backend can't, or is steering a hard
|
|
553
|
+
requirement for all three?
|
|
554
|
+
6. **Model/thinking inheritance across backends.** When `agent: "claude"` and no model
|
|
555
|
+
given, what's the default (e.g. always `opus`/`sonnet`)? Inheriting the parent pi
|
|
556
|
+
model is meaningless cross-backend. Proposal: per-backend default model in a small
|
|
557
|
+
config block; confirm.
|
|
558
|
+
7. **Binary/SDK discovery + failure UX.** When `codex`/`claude` isn't installed or has
|
|
559
|
+
no credentials, should `subagent_spawn` fail fast with a clear tool error (proposed),
|
|
560
|
+
or should the backends be hidden from the `agent` enum dynamically?
|
|
561
|
+
8. **Result truncation budgets.** Keep v1's numbers (24KB result message, 48KB wait
|
|
562
|
+
total, 16KB per agent, 2KB check preview) unchanged?
|
|
563
|
+
9. **Effect version pinning.** Effect v4 is beta — pin an exact `4.0.0-beta.x` and
|
|
564
|
+
accept manual bumps, or track the beta dist-tag?
|
|
565
|
+
10. **Persistence across reloads.** v1 loses all subagents on `session_shutdown`
|
|
566
|
+
(disposeAll). Codex/Claude children are external processes that *could* outlive a
|
|
567
|
+
pi reload — should v2 keep v1's kill-everything behavior (proposed for v1 of v2) or
|
|
568
|
+
plan for reattach later?
|