@cr1ms0n/pi-subagent 0.8.9 → 0.10.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.
@@ -0,0 +1,132 @@
1
+ [English](README.md) | [简体中文](README.zh-CN.md)
2
+
3
+ # Pi Smart Subagents
4
+
5
+ 在 [Pi](https://pi.dev/) 中运行独立子代理,由 Jev 为每项任务选择执行模型和具体工具。
6
+
7
+ npm 包名为 `@cr1ms0n/pi-subagent`。本项目是 Luke Parke 的 `@parke.dev/pi-subagent` 0.8.0 的独立社区分支,上游来自 [LukasParke/pi-extensions](https://github.com/LukasParke/pi-extensions/tree/main/packages/pi-subagent),并非上游官方发行版。原始 MIT 许可证和版权声明均予以保留。
8
+
9
+ 上游扩展提供子进程引擎、命名代理、后台任务、工作树和用量统计。本分支增加了强制 Jev 模型与工具选择,并在发送任务前核验子进程实际采用的模型和工具权限。
10
+
11
+ ---
12
+
13
+ <a id="quick-start"></a>
14
+ ### 安装与快速开始
15
+
16
+ 需要 Node.js 22.19.0 或更高版本,以及已安装、已配置可用模型提供方的 Pi CLI。Pi 0.86.0 是已验证的宿主基线,能够执行内置工具、扩展工具和延迟注册工具的允许列表。若宿主无法核验所选能力,扩展会拒绝启动,不会扩大工具权限。
17
+
18
+ **1. 安装已发布的软件包。**
19
+
20
+ 安装精确的 `0.10.0` 版本。较早的 `0.9.0` 使用旧的 `apiKeyEnv` 配置契约,不接受 `apiKey`:
21
+
22
+ ```bash
23
+ pi install npm:@cr1ms0n/pi-subagent@0.10.0
24
+ ```
25
+
26
+ Pi 会直接加载这个软件包。不要同时启用本扩展的其他副本或 `@parke.dev/pi-subagent`,它们会注册同名工具。本包提供 `subagent`、`subagent_wait`、`/subagents`、`/subagent-cost` 和 `/btw`。
27
+
28
+ **2. 将 TypeSafe 凭据保存到私有配置。**
29
+
30
+ 在用户级 `~/.pi/subagent.json` 中设置 `jevRouting.apiKey`,格式如下。如果从 `0.9.0` 升级,请在启动新任务前将 `jevRouting.apiKeyEnv` 中的现有值移到 `jevRouting.apiKey`,并删除旧字段。不要把密钥放进聊天或仓库文件。配置文件以明文保存密钥,需要限制文件访问权限并保护备份。迁移与安全说明见[凭据配置](docs/REFERENCE.md#credential-setup)。子模型所需的提供方认证需要另行在 Pi 中配置。
31
+
32
+ **3. 配置候选模型。**
33
+
34
+ 将下面的配置加入 `~/.pi/subagent.json`,保留其他已有设置。把示例模型 ID 替换为当前 Pi 中可用的精确 `provider/model-id`,并自行描述模型特点。如果存在旧的 `modelPolicy` 配置,需要将其移除;扩展不会自动迁移。
35
+
36
+ ```json
37
+ {
38
+ "jevRouting": {
39
+ "selectorModel": "jev-latest",
40
+ "apiKey": "<your-typesafe-api-key>",
41
+ "timeoutMs": 15000,
42
+ "models": [
43
+ {
44
+ "model": "<provider/model-id>",
45
+ "description": "Describe this model's strengths and the tasks you want it to handle."
46
+ }
47
+ ]
48
+ }
49
+ }
50
+ ```
51
+
52
+ 将 `apiKey` 占位符替换为自己的 TypeSafe 密钥,并移除旧的 `apiKeyEnv` 字段。扩展不会回退读取环境变量,也不会自动迁移。模型描述可以使用中文。可选的 thinking 默认值、profile 默认值和限制见[配置参考](docs/REFERENCE.md#configuration)。
53
+
54
+ Jev 选择可能产生 TypeSafe 费用。它会接收委派任务文本、模型 ID 与描述、候选工具名称与描述,以及必要约束;不会自动上传仓库文件或对话历史,但任务中主动包含的文本仍可能泄露敏感信息。`action: "plan"` 同样会调用 Jev,之后实际执行时还会重新选择。
55
+
56
+ **4. 启动 Pi,委派一个只读任务。**
57
+
58
+ 启动 Pi;如果刚切换扩展代码,需要重新加载或重启。加载 `0.10.0` 后,每次新任务都会重新读取配置,修改 `apiKey` 不需要更新 shell 环境变量。
59
+
60
+ ```bash
61
+ pi
62
+ ```
63
+
64
+ 让主代理使用 `subagent`,例如传入下面的请求:
65
+
66
+ ```json
67
+ {
68
+ "task": "Read README.md and summarize what this package does.",
69
+ "description": "Summarize the README",
70
+ "profile": "explore",
71
+ "tools": ["read"],
72
+ "max_turns": 4,
73
+ "timeout_ms": 120000,
74
+ "max_retries": 0
75
+ }
76
+ ```
77
+
78
+ 不要传入 `model` 或 `fallback_models`。Jev 从配置的模型列表和允许的工具中进行选择;路由失败会阻止本次新任务启动,不会改用兜底方案。已有任务的管理操作不依赖路由凭据。
79
+
80
+ ---
81
+
82
+ <a id="delegation"></a>
83
+ ### 任务委派
84
+
85
+ - **模型与工具路由:**本分支让 Jev 根据任务和模型描述进行匹配,逐个选择工具;本地权限检查和子进程启动核验负责落实选择结果。
86
+ - **命名代理与并行工作:**上游引擎支持可复用的代理角色和并发子进程。本分支为每个新子代理执行 Jev 路由,代理文件不能固定其模型或工具选择。
87
+ - **后台任务:**上游引擎支持状态查询、可中断等待、取消和中途指导。本分支会显示所选模型,并在展开结果中展示工具详情。
88
+ - **隔离修改:**上游工作树机制支持检查、应用或丢弃改动,避免并行代理共用同一个可写工作区。
89
+ - **结构化结果与预算:**上游引擎在父进程中校验结构化输出,并保留部分工作成果。本分支始终使用已选定的模型和工具集进行重试,单独统计选择器 token。
90
+
91
+ 后台任务设置 `async: true`,之后使用 `subagent_wait` 或 `action: "wait"` 收取结果。中断等待或等待超时不会取消子代理,需要停止任务时使用 `action: "cancel"`。通过 `/subagents` 检查任务,通过 `/subagent-cost` 查看用量。
92
+
93
+ [使用参考](docs/REFERENCE.md#quick-usage)涵盖并行任务、结果汇总、恢复与分叉、结构化输出、预算,以及工作树的 diff/apply/discard 操作。[TUI 指南](docs/UX.md)介绍任务查看器和键盘操作。
94
+
95
+ ---
96
+
97
+ <a id="permissions-and-costs"></a>
98
+ ### 权限与费用
99
+
100
+ | Profile | 工具选择 | 修改项目文件 |
101
+ | --- | --- | --- |
102
+ | `explore` | Jev 选择的本地允许的只读工具,加上可用的 Pi 上下文控制工具 | 不允许 |
103
+ | `review` | 与 explore 相同的只读策略 | 不允许 |
104
+ | `general` | Jev 选择的本地允许的工具,加上可用的 Pi 上下文控制工具 | 选中可写工具时可以修改 |
105
+
106
+ 单任务默认使用 `general`,并行任务默认使用 `explore`。显式传入的 `tools` 列表限定候选工具范围。即使传入 `tools: []`,本地仍会补充可用的 Pi 上下文管理工具。工具选择为空绝不表示允许所有工具。
107
+
108
+ Profile 是工具选择策略,不是操作系统沙箱。子进程继承主进程环境,也能读取同一用户有权访问的文件,包括私有配置。工作树只隔离代码工作区。委派不可信任务前,请阅读[安全模型](docs/SECURITY.md)。
109
+
110
+ 用量账本分别记录主代理、子代理、路由和合计用量。TypeSafe 只报告路由 token,不报告金额,因此选择器费用标记为**未报告**,不代表免费。`max_cost` 限制子代理执行时由提供方报告的费用,不限制 TypeSafe 费用。结果交付、重试和会话分支的统计规则见[费用统计](docs/COST-ACCOUNTING.md)。
111
+
112
+ 扩展管理的新任务只支持 Pi 后端,原生 Codex/Claude 后端请求会被拒绝。[底层 SDK](docs/REFERENCE.md#using-the-runner-as-a-library)是另一套显式任务规格 API,不会自动调用 Jev,嵌入方需要自行负责模型和工具选择。
113
+
114
+ ---
115
+
116
+ <a id="development"></a>
117
+ ### 开发
118
+
119
+ 源码是使用 peer dependencies 的独立 TypeScript 包,没有构建步骤,也没有随仓库提供的测试运行器或类型检查脚本。本仓库支持的检查方式见[开发与验证](docs/DEVELOPMENT.md)。语法转换不等于语义类型检查;`npm pack --dry-run --ignore-scripts --json` 用于检查打包内容,不会发布包。
120
+
121
+ [架构约定](docs/ARCHITECTURE.md)记录模块职责和不变量。[发布维护](docs/RELEASING.md)说明如何选择性更新源码,以及需要单独明确授权的 npm 发布流程。
122
+
123
+ ---
124
+
125
+ <a id="license"></a>
126
+ ### 许可证
127
+
128
+ [MIT](LICENSE)。Copyright (c) 2026 Luke Parke。社区分支由 cr1ms0n(awoaCrim)维护。重新分发时请保留原始版权声明和许可证。
129
+
130
+ 译自 [README.md](README.md),英文文件 blob:`02294faabd946a50be23551e43e694451628bc39`。中英文内容如有差异,以英文为准。
131
+
132
+ 感谢 [Linux.do](https://linux.do/)。
@@ -12,9 +12,10 @@
12
12
  after a second window so retry can take over. Group kills verify process start-time
13
13
  identity (Linux `/proc`, macOS/BSD `ps lstart`) before signalling a possibly-recycled
14
14
  PID; transcript joins happen only on message boundaries, not per-chunk ticks.
15
- - Retry with model fallback lives in `orchestrator.ts` (`isTransientFailure`): queue
16
- timeouts, stalls, spawn errors, and provider errors re-run the same spec on the next
17
- fallback model with accumulated usage; task-quality failures never retry.
15
+ - Retry lives in `orchestrator.ts` (`isTransientFailure`): queue timeouts, stalls, spawn
16
+ errors, and provider errors re-run the same already-selected spec (model and tool set)
17
+ with accumulated usage; there is no fallback-model escalation, and task-quality failures
18
+ never retry.
18
19
  - `context: "fork"` spawns the child with `--fork <parent session file>` so it starts
19
20
  from a real branched copy of the parent conversation. Fail-fast when the parent
20
21
  session is not persisted; single-task only.
@@ -26,10 +27,20 @@
26
27
  identity records for orphan reconcile.
27
28
  - `launch.ts`: resolve the child `pi` invocation via `PI_SUBAGENT_BIN` or
28
29
  `process.execPath` + CLI entry (bare PATH name only as last-resort fallback).
29
- - `persistence.ts`: versioned active-branch event folding and bounded child transcript metadata.
30
+ - `persistence.ts`: versioned active-branch event folding, the bounded routing-event decoder
31
+ (`subagent-routing-v1`), and bounded child transcript metadata.
30
32
  - `maintenance.ts`: filesystem GC (session files) and abort-race helpers; kept out of persistence.
31
- - `usage.ts`: provider-reported root/subagent/combined accounting.
33
+ - `usage.ts`: provider-reported root/subagent/combined accounting, plus a separate
34
+ once-per-request routing-token category whose currency is reported as unreported.
32
35
  - `policy.ts` / `schema.ts`: discriminated request validation and safe capability profiles. `schema.ts` retains the canonical TypeBox validators and derives provider-safe tool-schema projections; `extension.ts` registers those projections while validating calls with the originals. Pi context-management control-plane tools remain available to child allowlists without granting project-file write access.
36
+ - `routing-types.ts` / `routing-policy.ts` / `jev-router.ts` / `dispatch-routing.ts`:
37
+ the mandatory Jev route. `routing-types.ts` owns the selector DTOs, decision/receipt
38
+ shapes and local resource limits; `routing-policy.ts` owns the strict `jevRouting`
39
+ parser, the candidate intersection with locally available models, and the injected
40
+ model-facing guidance; `jev-router.ts` owns the injectable TypeSafe transport,
41
+ response validation, deadlines and per-request receipts; `dispatch-routing.ts`
42
+ resolves every worker before any launch and refuses a partially selected fanout. The
43
+ router has no engine imports and makes no parent UI calls.
33
44
  - `config.ts`: defaults ← `~/.pi/subagent.json` ← `PI_SUBAGENT_*` env overrides.
34
45
  - `structured.ts`: structured-output contract (dependency-free JSON-Schema subset
35
46
  validation, fenced json:result extraction, contract/repair prompts) and
@@ -65,7 +76,9 @@ Invariants:
65
76
  9. On startup, orphan process groups recorded under `lockDir` are reaped (SIGTERM then SIGKILL)
66
77
  before any matching child session is eligible for resume. `$state: "lost"` is a labeling
67
78
  that keeps `resumeBlocked` until reconciliation proves death.
68
- 10. Billed usage is folded once per root message and once per full child run UUID.
79
+ 10. Billed execution usage is folded once per root message and once per full child run UUID.
80
+ Selector usage is a separate category folded once per selector request ID, with currency
81
+ reported as unreported rather than inferred.
69
82
  11. Checkpoint persistence events are lightweight (state, usage, process identity, pointers).
70
83
  Full transcripts and final output are persisted exactly once, in the terminal event.
71
84
  12. High-frequency registry "changed" events coalesce (trailing window); state transitions,
@@ -96,10 +109,10 @@ Invariants:
96
109
  turns before SIGTERM; a child that concludes within grace ends `partial` with
97
110
  `wrappedUp: true`. `graceTurns: 0` restores immediate stops.
98
111
  21. Transient failures (queued timeout, stall, spawn error, provider error, protocol
99
- truncation) retry up to `maxRetries` extra attempts, escalating through
100
- `fallback_models`; usage accumulates across attempts and `attemptedModels` is
101
- recorded. Task-quality failures (nonzero exit with complete protocol, cancellation,
102
- budget stop, running timeout) never retry.
112
+ truncation) retry up to `maxRetries` extra attempts on the already selected model and
113
+ tool set; there is no fallback-model escalation and no reselection. Usage accumulates
114
+ across attempts. Task-quality failures (nonzero exit with complete protocol,
115
+ cancellation, budget stop, running timeout) never retry.
103
116
  22. The stall watchdog treats protocol silence as suspect, not fatal: after
104
117
  `stallAfterMs` the task is flagged and probed via `get_state` (a live child's
105
118
  answer clears the flag); only continued silence for `stallKillAfterMs` more kills
@@ -120,6 +133,36 @@ Invariants:
120
133
  `activeAtOrBelowDepth(depth) < maxGlobalActive - reservedFor(depth)`, holding slots
121
134
  back for deeper tiers so a full-width spawn tree cannot deadlock on its own children.
122
135
  Slot records without a `depth` field count as depth 0.
123
- 28. `action: "plan"` is a truth oracle: it runs the exact validation and preflights of a
124
- real spawn and returns the resolved plan without spawning never a softer check, and
125
- never a registry entry.
136
+ 28. `action: "plan"` is a truth oracle: it runs the exact validation, Jev selection and
137
+ local preflights of a real spawn and returns the resolved plan and its selector usage
138
+ without spawning. It creates no registry entry, and its fee-bearing selection is not
139
+ cached for a later dispatch.
140
+ 29. Every new extension-managed launch (`task`/`tasks[]`, `action:"plan"`, `/btw`,
141
+ resume, fork, nested dispatch and the optional synthesis child) crosses one selector
142
+ interface before any child starts. The dedicated candidate list intersected with
143
+ locally available models is the only source of execution models; the full locally
144
+ permitted tool catalog is the only candidate source. Legacy `model`/`fallback_models`
145
+ fields are rejected on new work, and an empty selected tool set never becomes
146
+ inheritance or "all tools".
147
+ 30. The finalized tool subset is passed to the child as Pi's `--tools` allowlist
148
+ (`--no-tools` when empty). Pi 0.86.0 is the verified baseline for built-in, extension
149
+ and late-registered tool enforcement; a host that cannot honor the allowlist is
150
+ refused rather than silently weakened, and no older release is advertised as
151
+ equivalent.
152
+ Startup verification is the enforcement companion: the Pi adapter supplies a
153
+ package-local preflight extension plus a bounded non-secret expectation, verifies that
154
+ the nonce-specific bootstrap command exists from the expected package source, then
155
+ requires the child to acknowledge the exact selected model and finalized tool names
156
+ (including nested-tool source) before the real task prompt is sent. Missing or
157
+ mismatched acknowledgement is a capability/startup diagnostic, never compensated by
158
+ broadening tools or choosing another model.
159
+ 31. An absolute task deadline is created before preflight/selection, and routing, setup,
160
+ queue and retries all count against it. Pending selector work is tracked per session
161
+ runtime, aborted on cancellation, shutdown or session switch, and every post-await
162
+ transition re-checks captured runtime/session ownership so a late response cannot
163
+ launch into a replaced session.
164
+ 32. Before any paid selection, plan and dispatch share a side-effect-free direct-resume
165
+ availability check (in-memory owner, `resumeBlocked`, durable lock ownership and
166
+ staleness) that acquires, renews or reaps nothing. Dispatch still takes the
167
+ authoritative lock atomically at the existing launch point, and forked resumes skip
168
+ the exclusive direct-resume check.
@@ -1,66 +1,116 @@
1
- # Cost accounting
2
-
3
- `pi-subagent` reports three independent ledgers:
4
-
5
- - **root** — provider-reported usage from assistant messages on the active parent-session branch.
6
- - **subagents** — provider-reported cumulative usage from child-run checkpoints and terminal events on that same branch.
7
- - **combined** root + subagents.
8
-
9
- These totals appear in `subagent { action: "status" }`, per-run status, the `/subagent-cost` command, and the `/subagents` overlay header. The footer stays terse (running/ready counts only) because Pi's native footer already shows session cost — including subagent spend, on Pi builds with native tool-result usage accounting (see below).
10
-
11
- ## Source of truth
12
-
13
- The extension does not estimate prices. It uses Pi's normalized provider response:
14
-
15
- ```ts
16
- message.usage.cost.total
17
- ```
18
-
19
- It also retains provider-reported input/output/cache category costs, token counts, reasoning tokens, context size, and completed turn count when supplied.
20
-
21
- ## Native Pi usage accounting
22
-
23
- Pi builds after v0.80.10 persist an optional `usage` field on tool-result messages ([pi#6671](https://github.com/earendil-works/pi/pull/6671)) and fold it into the native footer total, `/session` statistics (as `Tools/summaries`), and RPC `get_state` totals.
24
-
25
- The extension participates in both directions:
26
-
27
- - **Upward** — the tool result that *delivers* a run (foreground completion, or the first `wait`) carries the run's total provider usage as native `usage`. Attachment is gated on the same delivered-flag transition as output delivery, so it happens exactly once per run UUID. Status, replayed waits, steer, diff/apply/discard, and plan responses never attach usage. Older Pi hosts copy only `content`/`details` from tool results and silently ignore the field — safe on every version this package supports.
28
- - **Downward** a child's event stream may contain tool-result messages that themselves carry nested usage (for example, a grandchild subagent on a new-Pi child). The parent folds that into the run's cumulative usage, so `max_cost` budgets and both ledgers see true subtree spend. Pre-#6671 children simply never emit the field.
29
-
30
- Known undercounts in the **native** total (the extension ledger still counts these from persisted entries):
31
-
32
- - A background run dismissed in the overlay (or via status) without a delivering `wait` never produces a tool result, so its spend reaches only the extension ledger.
33
- - A failed or lost run raises an error instead of returning a tool result; any pre-failure usage likewise reaches only the extension ledger.
34
-
35
- Because the native footer counts parent assistant messages plus delivered tool-result usage, and the extension's **combined** counts the same runs by UUID, the two agree whenever every terminal run was delivered.
36
-
37
- ## Deduplication rules
38
-
39
- 1. Root assistant messages are counted once by session-entry ID.
40
- 2. Each subagent run is counted once by full run UUID; the newest live/checkpoint/terminal cumulative value replaces older values.
41
- 3. Delivery, dismissal, status, and checkpoint events never add cost.
42
- 4. If an old run is evicted from in-memory UI history, its latest persisted usage still contributes to the session ledger.
43
- 5. Active and immediately completed runs supplement or replace stale persisted checkpoints until newer session entries become visible; the full run UUID prevents double counting afterward.
44
- 6. Resumed and forked invocations are distinct billed runs. Their new provider usage is counted once, even though they reuse prior context.
45
- 7. Retry attempts (transient-failure retries and model fallbacks) accumulate into their
46
- run's single usage record every attempt's billed usage counts once, under one run
47
- UUID, with `attemptedModels` recording the escalation path.
48
- 8. The optional parallel `synthesis` child bills into the same run as an extra result.
49
- 9. Native `usage` on the delivering tool result mirrors rule 2's run totals and is attached at most once per run (delivered-flag gated), so Pi-side totals cannot double count a run either.
50
-
51
- ## Branch semantics
52
-
53
- Only `sessionManager.getBranch()` is used. Costs from abandoned sibling branches are excluded. When a parent session is forked, its inherited active-branch terminal entries remain part of that fork's historical total; new runs are added to the fork independently.
54
-
55
- ## Failure and cancellation
56
-
57
- Any usage reported before a failure, timeout, budget stop, cancellation, or parent crash is retained in a cumulative checkpoint/terminal record. A run with no provider response contributes zero rather than an estimate.
58
-
59
- ## Provider limitations
60
-
61
- Accounting is only as precise as the provider data normalized by Pi:
62
-
63
- - Some providers may report zero or incomplete costs.
64
- - `reasoning` is a subset of output tokens and is not added to output again.
65
- - `contextTokens` is the latest turn's context size, not an additive billed-token field.
66
- - The extension deliberately does not infer missing prices from a local model table.
1
+ # Cost accounting
2
+
3
+ `pi-subagent` reports four independent ledgers:
4
+
5
+ - **root** — provider-reported usage from assistant messages on the active parent-session branch.
6
+ - **subagents** — provider-reported cumulative usage from child-run checkpoints and terminal events on that same branch.
7
+ - **routing**: usage reported by the Jev/TypeSafe selector, one record per selector HTTP request.
8
+ - **combined**: root + subagents + routing.
9
+
10
+ These totals appear in `subagent { action: "status" }`, per-run status, the
11
+ `/subagent-cost` command, and the `/subagents` overlay header. The footer stays
12
+ terse (running/ready counts only) because Pi's native footer already shows
13
+ session cost, including subagent spend, on Pi builds with native tool-result
14
+ usage accounting (see below).
15
+
16
+ ## Source of truth
17
+
18
+ The extension does not estimate prices. It uses Pi's normalized provider response:
19
+
20
+ ```ts
21
+ message.usage.cost.total
22
+ ```
23
+
24
+ It also retains provider-reported input/output/cache category costs, token counts, reasoning tokens, context size, and completed turn count when supplied.
25
+
26
+ ## Routing (selector) accounting
27
+
28
+ Jev selection is billed separately from child execution:
29
+
30
+ - TypeSafe reports input and output **tokens**, not billed currency. The routing
31
+ ledger therefore reports tokens and marks its currency as **unreported**. Do not
32
+ read the numeric `0` currency placeholder in the native usage schema as the
33
+ selector being free: it is the API-required number for an unreported value.
34
+ - Numeric dollar totals for root/subagent/combined exclude unreported routing
35
+ spend. The status line says so explicitly rather than silently omitting it.
36
+ - `max_cost` remains the provider-reported execution ceiling for the child and
37
+ its subtree. It cannot cap TypeSafe charges, so a run can stay under `max_cost`
38
+ while still incurring selector fees.
39
+ - No selector price is inferred from a public price page or a local model table.
40
+
41
+ A routing record exists per selector HTTP request, not per logical selection: a
42
+ model question plus one or more packed tool-question requests each produce their
43
+ own record, and all of them count once by full request ID. Plan selections and
44
+ pre-spawn failures are included, because no child run exists to carry them.
45
+
46
+ Successful route metadata (decision ID, selector model and reported version(s),
47
+ selected execution model/tools, locally added control-plane tools, confidence,
48
+ success outcome, latency and receipt IDs) travels with the run and both registry
49
+ projections. Per-request failure outcomes and safe error codes stay in selector receipts. It carries no descriptions, raw request bodies, headers,
50
+ credentials or invented rationale.
51
+
52
+ Receipts pending append visibility remain in a bounded session-local overlay until the
53
+ active branch exposes the matching record. Persistence retries reuse the receipt ID,
54
+ not the paid selector request. If persistence cannot be confirmed, new child launch is
55
+ blocked and a branch change is cancelled; a forced shutdown reports that durable usage
56
+ may be incomplete. This cannot repair an unavailable storage adapter after process exit.
57
+
58
+ ## Native Pi usage accounting
59
+
60
+ Pi builds after v0.80.10 persist an optional `usage` field on tool-result messages ([pi#6671](https://github.com/earendil-works/pi/pull/6671)) and fold it into the native footer total, `/session` statistics (as `Tools/summaries`), and RPC `get_state` totals.
61
+
62
+ The extension participates in both directions:
63
+
64
+ - **Upward**: the tool result that *delivers* a run (foreground completion, or the first `wait`) carries the run's total provider usage as native `usage`. Attachment is gated on the same delivered-flag transition as output delivery, so it happens exactly once per run UUID. Status, replayed waits, steer, diff/apply/discard, and plan responses never attach run execution usage; plan may report its own selector usage through its result instead (rule 10). Older Pi hosts copy only `content`/`details` from tool results and silently ignore the field; that is safe on every version this package supports.
65
+ - **Downward**: a child's event stream may contain tool-result messages that themselves carry nested usage (for example, a grandchild subagent on a new-Pi child). The parent folds that into the run's cumulative usage, so `max_cost` budgets and the execution/routing ledgers see true subtree spend. Pre-#6671 children simply never emit the field.
66
+
67
+ Known undercounts in the **native** total (the extension ledger still counts these from persisted entries):
68
+
69
+ - A background run dismissed in the overlay (or via status) without a delivering `wait` never produces a tool result, so its spend reaches only the extension ledger.
70
+ - A failed or lost run raises an error instead of returning a tool result; any pre-failure usage likewise reaches only the extension ledger. The same native limitation applies to selector usage on a thrown error: the extension ledger retains it, but a thrown tool call cannot attach a native usage object. This is documented rather than converted into a success.
71
+
72
+ Because the native footer counts parent assistant messages plus delivered tool-result usage, and the extension's **combined** counts the same runs by UUID, the reported token totals agree when every run and selector receipt was successfully delivered as native usage. Thrown errors and undelivered plan/dispatch failures can leave selector usage only in the extension ledger; currency totals never include unreported selector fees.
73
+
74
+ ## Deduplication rules
75
+
76
+ 1. Root assistant messages are counted once by session-entry ID.
77
+ 2. Each subagent run is counted once by full run UUID; the newest live/checkpoint/terminal cumulative value replaces older values.
78
+ 3. Delivery, dismissal, status, and checkpoint events never add cost.
79
+ 4. If an old run is evicted from in-memory UI history, its latest persisted usage still contributes to the session ledger.
80
+ 5. Active and immediately completed runs supplement or replace stale persisted checkpoints until newer session entries become visible; the full run UUID prevents double counting afterward.
81
+ 6. Resumed and forked invocations are distinct billed runs. Their new provider usage is counted once, even though they reuse prior context.
82
+ 7. Retry attempts (transient-failure retries on the already selected model and tool set)
83
+ accumulate into their run's single usage record: every attempt's billed usage counts once
84
+ under one run UUID. There is no fallback-model escalation, so `attemptedModels` repeats the
85
+ selected model rather than recording a route change.
86
+ 8. The optional parallel `synthesis` child bills into the same run as an extra result.
87
+ 9. Selector requests are counted once by full selector request ID, including plan
88
+ requests and pre-spawn rejected decisions. Route references inside task results
89
+ never add selector usage again, and each receipt is folded once across
90
+ replay/status/repeated wait.
91
+ 10. Native `usage` on the delivering tool result mirrors rule 2's run totals and is attached at most once per run (delivered-flag gated), so Pi-side totals cannot double count a run either. Worker pre-spawn routing tokens attach once at async start; the first delivery/wait attaches child execution usage plus any deferred-synthesis routing tokens not yet delivered; foreground completion attaches all invocation routing tokens plus execution tokens. Plan attaches only its own routing usage. Missing usage on an interrupted or invalid response stays **unknown**, never an invented zero. A partial or malformed token report retains individually validated counts while marking completeness unknown; for example, valid input with invalid output is not a complete report.
92
+
93
+ Native routing attachment commits atomically: foreground/wait use the run's single
94
+ `delivered` event for linked receipts; plan and async-start use one `native-delivery`
95
+ request-ID batch on the routing event stream. A throwing append does not consume a
96
+ prefix or set the in-memory run delivered flag. Transient persistence failures retry
97
+ without another selector call. A batch holds at most 1024 receipt IDs; larger plan or
98
+ background requests must be split. Background requests check this bound before any
99
+ child/run registration. Already incurred selector usage stays in the ledger.
100
+
101
+ ## Branch semantics
102
+
103
+ Only `sessionManager.getBranch()` is used. Costs from abandoned sibling branches are excluded. When a parent session is forked, its inherited active-branch terminal entries remain part of that fork's historical total; new runs are added to the fork independently.
104
+
105
+ ## Failure and cancellation
106
+
107
+ Any usage reported before a failure, timeout, budget stop, cancellation, or parent crash is retained in a cumulative checkpoint/terminal record. A run with no provider response contributes zero rather than an estimate.
108
+
109
+ ## Provider limitations
110
+
111
+ Accounting is only as precise as the provider data normalized by Pi:
112
+
113
+ - Some providers may report zero or incomplete costs.
114
+ - `reasoning` is a subset of output tokens and is not added to output again.
115
+ - `contextTokens` is the latest turn's context size, not an additive billed-token field.
116
+ - The extension deliberately does not infer missing prices from a local model table, and TypeSafe's token-only reports are never converted into a dollar estimate.
@@ -0,0 +1,124 @@
1
+ # Development and verification
2
+
3
+ This is a standalone ESM TypeScript package loaded by Pi, not the upstream monorepo. Start with the [README](../README.md) for installation and the [architecture contract](ARCHITECTURE.md) before changing engine behavior.
4
+
5
+ ---
6
+
7
+ ### Checkout and runtime
8
+
9
+ [package.json](../package.json) declares Node.js 22.19.0 or newer and Pi/TypeBox peer dependencies. There is no compiled distribution or build step. The extension entry is [extensions/subagent.ts](../extensions/subagent.ts); the supported SDK exports are in [src/index.ts](../src/index.ts).
10
+
11
+ To use a local checkout, replace the example path with your own:
12
+
13
+ ```bash
14
+ pi install /absolute/path/to/pi-smart-subagents
15
+ ```
16
+
17
+ Pi registers a local path without copying it. Do not enable that checkout and another copy of the same extension together. Reload or restart Pi after package changes. Keep user settings, provider credentials and real session files outside the repository.
18
+
19
+ The product skill at [skills/subagent/SKILL.md](../skills/subagent/SKILL.md) is distributed with the package. Maintainer-specific agents, workflows, task records and local verification artifacts are not required to use or inspect the public source.
20
+
21
+ ---
22
+
23
+ ### Source ownership
24
+
25
+ | Responsibility | Owner |
26
+ | --- | --- |
27
+ | Pi registration and lifecycle wiring | [extension.ts](../src/extension.ts) |
28
+ | Request schema, permission checks and normalization | [schema.ts](../src/schema.ts), [policy.ts](../src/policy.ts), [config.ts](../src/config.ts), [agents.ts](../src/agents.ts) |
29
+ | Jev transport, candidate policy and dispatch | [routing-types.ts](../src/routing-types.ts), [routing-policy.ts](../src/routing-policy.ts), [jev-router.ts](../src/jev-router.ts), [dispatch-routing.ts](../src/dispatch-routing.ts) |
30
+ | Local preflight and child capability verification | [dispatch-preflight.ts](../src/dispatch-preflight.ts), [startup-check.ts](../src/startup-check.ts), [child-preflight.ts](../src/child-preflight.ts) |
31
+ | Child processes, retries and protocol | [runner.ts](../src/runner.ts), [orchestrator.ts](../src/orchestrator.ts), [protocol.ts](../src/protocol.ts), [backend adapters](../src/backends/) |
32
+ | Concurrency, ownership and durable worktrees | [semaphore.ts](../src/semaphore.ts), [process-lock.ts](../src/process-lock.ts), [registry.ts](../src/registry.ts), [worktree.ts](../src/worktree.ts) |
33
+ | Persistence, accounting and output | [persistence.ts](../src/persistence.ts), [usage.ts](../src/usage.ts), [output.ts](../src/output.ts), [structured.ts](../src/structured.ts) |
34
+ | TUI, notifications and transcript display | [format.ts](../src/format.ts), [ui.ts](../src/ui.ts), [notifications.ts](../src/notifications.ts), [transcript.ts](../src/transcript.ts) |
35
+
36
+ Keep engine rules in their owning modules. The extension is a composition root, and renderers consume narrow projections rather than owning a second run store. The architecture document covers the remaining modules and invariants.
37
+
38
+ ---
39
+
40
+ ### Checks available in this checkout
41
+
42
+ There are no npm scripts, devDependencies, TypeScript project configuration or bundled test suite. `npm test`, `npm run typecheck` and the upstream release-check scripts are not available here. Installing dependencies alone does not create those commands. The historical [plan](PLAN.md) and [roadmap](ROADMAP.md) refer to upstream tooling and previous release work.
43
+
44
+ #### Documentation and whitespace
45
+
46
+ Review the English and Chinese READMEs together: commands, config keys, feature claims, language links, anchors and license references must agree. Check links against the intended public Git tree, not only files that happen to exist in a maintainer's checkout. Keep the English-source blob reference in the Chinese README current when changing the translation.
47
+
48
+ For tracked changes:
49
+
50
+ ```bash
51
+ git diff --check
52
+ ```
53
+
54
+ For a prepared commit:
55
+
56
+ ```bash
57
+ git diff --cached --check
58
+ ```
59
+
60
+ These commands do not check Markdown links, translation accuracy or untracked files. Review those separately. Documentation-only changes do not require a provider call or a claim that engine tests passed.
61
+
62
+ #### Offline TypeScript syntax check
63
+
64
+ When the global Pi installation includes esbuild, the following Bash/Git Bash command enumerates production TypeScript files, parses and strips their types, and writes only to a unique temporary directory. It removes that directory afterward. It does not download a tool or modify source files.
65
+
66
+ ```bash
67
+ node --input-type=module - "$(npm root -g)" <<'NODE'
68
+ import * as fs from "node:fs";
69
+ import * as os from "node:os";
70
+ import * as path from "node:path";
71
+ import { spawnSync } from "node:child_process";
72
+
73
+ const esbuild = path.join(process.argv[2], "@earendil-works/pi-coding-agent/node_modules/esbuild/bin/esbuild");
74
+ if (!fs.existsSync(esbuild)) throw new Error("Global Pi esbuild is unavailable; no syntax check was run.");
75
+ const files = [];
76
+ function walk(dir) {
77
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
78
+ const file = path.join(dir, entry.name);
79
+ if (entry.isDirectory()) walk(file);
80
+ else if (file.endsWith(".ts")) files.push(file);
81
+ }
82
+ }
83
+ walk("src");
84
+ walk("extensions");
85
+ const outputDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-syntax-"));
86
+ try {
87
+ for (const file of files.sort()) {
88
+ const result = spawnSync(process.execPath, [esbuild, file, "--loader:.ts=ts", "--format=esm",
89
+ "--target=node22", `--outfile=${path.join(outputDir, "check.js")}`], { stdio: "inherit" });
90
+ if (result.error) throw result.error;
91
+ if (result.status !== 0) throw new Error(`Syntax transform failed for ${file}`);
92
+ }
93
+ process.stdout.write(`Parsed ${files.length} TypeScript files; no semantic typechecking performed.\n`);
94
+ } finally {
95
+ fs.rmSync(outputDir, { recursive: true, force: true });
96
+ }
97
+ NODE
98
+ ```
99
+
100
+ This catches malformed TypeScript only. It does not resolve imports, check types or validate peer APIs. If your Pi installation does not expose that esbuild path, report the missing check rather than claiming success or installing new tooling silently.
101
+
102
+ A semantic typecheck requires a separately configured TypeScript environment and compatible peers. There is no repository-owned command for it here. Any external harness or isolated fixture run must be reported with its actual setup, scope and limitations; private maintainer fixtures are not a test suite shipped in this checkout.
103
+
104
+ #### Package contents
105
+
106
+ Review the npm file list without generating or publishing a tarball:
107
+
108
+ ```bash
109
+ npm pack --dry-run --ignore-scripts --json
110
+ ```
111
+
112
+ The output should contain production source, the distributed skill, public documentation, both READMEs, changelog, license and package metadata. It must not contain local settings, agent instructions, tasks, backups, transcripts, credentials, tarballs or generated test bundles. Git exclusions and the package `files` allowlist are separate controls; inspect both when changing packaging.
113
+
114
+ A dry run verifies packaging, not application behavior. For an actual release, follow the additional artifact and installation checks in [release maintenance](RELEASING.md).
115
+
116
+ ---
117
+
118
+ ### Behavior verification
119
+
120
+ For engine changes, trace the affected architecture invariants and exercise the owned boundary with an injected transport, fake process or isolated host when such a harness is available. State what was exercised and keep paid partial output/accounting semantics intact.
121
+
122
+ A real `subagent` call and `action: "plan"` both invoke Jev and may incur charges. A plan avoids spawning a child; it is not an offline test. Obtain explicit permission before live routing/provider smoke tests, use synthetic task data and keep real user sessions out of fixtures.
123
+
124
+ Report syntax transforms, semantic typechecks, package checks, fixture assertions and live-provider checks separately. Do not summarize them as “tests passed” when no test suite was run.
package/docs/PLAN.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Execution plan — remaining roadmap phases
2
2
 
3
+ > **Historical upstream plan.** Retained as design rationale for phases that shipped before this fork's Jev routing changes. Version targets, test paths and `npm run release:check` below describe the upstream monorepo, not tools available in this standalone checkout. See [the current reference](REFERENCE.md), [architecture contract](ARCHITECTURE.md) and [development checks](DEVELOPMENT.md) for the maintained behavior and verification instructions. Historical gate statements are not verification results for the current checkout.
4
+
3
5
  Formalization of [ROADMAP.md](./ROADMAP.md) phases 2–4 into implementable work
4
6
  items. Where the roadmap holds rationale and design sketches, this document
5
7
  holds the execution contract: exact scope, file-level work breakdown,