@cr1ms0n/pi-subagent 0.8.8 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -6
- package/README.md +218 -128
- package/docs/ARCHITECTURE.md +168 -132
- package/docs/COST-ACCOUNTING.md +116 -66
- package/docs/RELEASING.md +32 -32
- package/docs/SECURITY.md +125 -97
- package/docs/UX.md +158 -141
- package/package.json +2 -2
- package/skills/subagent/SKILL.md +142 -121
- package/src/agents.ts +282 -288
- package/src/backends/pi.ts +164 -94
- package/src/child-preflight.ts +166 -0
- package/src/config.ts +254 -252
- package/src/dispatch-preflight.ts +87 -0
- package/src/dispatch-routing.ts +56 -0
- package/src/extension.ts +368 -187
- package/src/format.ts +436 -365
- package/src/jev-router.ts +1036 -0
- package/src/orchestrator.ts +303 -312
- package/src/persistence.ts +643 -335
- package/src/policy.ts +562 -561
- package/src/process-lock.ts +730 -687
- package/src/protocol.ts +320 -290
- package/src/registry.ts +730 -632
- package/src/routing-policy.ts +268 -0
- package/src/routing-types.ts +217 -0
- package/src/runner.ts +1299 -850
- package/src/schema.ts +189 -189
- package/src/startup-check.ts +481 -0
- package/src/types.ts +208 -198
- package/src/usage.ts +316 -274
- package/src/context-policy.ts +0 -169
- package/src/model-policy.ts +0 -169
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -1,132 +1,168 @@
|
|
|
1
|
-
# Architecture contract
|
|
2
|
-
|
|
3
|
-
`pi-subagent` is split by ownership boundary:
|
|
4
|
-
|
|
5
|
-
- `runner.ts`: one child process, Pi RPC protocol (JSONL commands on stdin, events on
|
|
6
|
-
stdout — a superset of `--mode json`), cancellation, process trees, budgets, and a live
|
|
7
|
-
stdin command channel used for mid-run steering. Extension UI dialogs from headless
|
|
8
|
-
children are auto-cancelled so they can never hang a run; stdin is closed after
|
|
9
|
-
`agent_settled` so RPC children shut down cleanly. Budget breaches steer a wrap-up
|
|
10
|
-
message and allow grace turns before SIGTERM (`wrappedUp` marks a clean conclusion).
|
|
11
|
-
A stall watchdog flags protocol silence, probes liveness via `get_state`, and kills
|
|
12
|
-
after a second window so retry can take over. Group kills verify process start-time
|
|
13
|
-
identity (Linux `/proc`, macOS/BSD `ps lstart`) before signalling a possibly-recycled
|
|
14
|
-
PID; transcript joins happen only on message boundaries, not per-chunk ticks.
|
|
15
|
-
- Retry
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
- `
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
- `
|
|
29
|
-
|
|
30
|
-
- `
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
-
|
|
35
|
-
|
|
36
|
-
- `
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
- `
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
- `
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
- `
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
- `
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
and
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
`
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
1
|
+
# Architecture contract
|
|
2
|
+
|
|
3
|
+
`pi-subagent` is split by ownership boundary:
|
|
4
|
+
|
|
5
|
+
- `runner.ts`: one child process, Pi RPC protocol (JSONL commands on stdin, events on
|
|
6
|
+
stdout — a superset of `--mode json`), cancellation, process trees, budgets, and a live
|
|
7
|
+
stdin command channel used for mid-run steering. Extension UI dialogs from headless
|
|
8
|
+
children are auto-cancelled so they can never hang a run; stdin is closed after
|
|
9
|
+
`agent_settled` so RPC children shut down cleanly. Budget breaches steer a wrap-up
|
|
10
|
+
message and allow grace turns before SIGTERM (`wrappedUp` marks a clean conclusion).
|
|
11
|
+
A stall watchdog flags protocol silence, probes liveness via `get_state`, and kills
|
|
12
|
+
after a second window so retry can take over. Group kills verify process start-time
|
|
13
|
+
identity (Linux `/proc`, macOS/BSD `ps lstart`) before signalling a possibly-recycled
|
|
14
|
+
PID; transcript joins happen only on message boundaries, not per-chunk ticks.
|
|
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.
|
|
19
|
+
- `context: "fork"` spawns the child with `--fork <parent session file>` so it starts
|
|
20
|
+
from a real branched copy of the parent conversation. Fail-fast when the parent
|
|
21
|
+
session is not persisted; single-task only.
|
|
22
|
+
- `registry.ts`: one parent-session runtime, run state, snapshots, resume locks, and the
|
|
23
|
+
single LiveRun→snapshot/persisted-result projections used by every consumer.
|
|
24
|
+
- `semaphore.ts`: per-parent-runtime child-process limit.
|
|
25
|
+
- `process-lock.ts`: machine-wide durable coordination under `~/.pi/subagent-locks/` —
|
|
26
|
+
exclusive per-child-session resume locks, global concurrency slots, and run process
|
|
27
|
+
identity records for orphan reconcile.
|
|
28
|
+
- `launch.ts`: resolve the child `pi` invocation via `PI_SUBAGENT_BIN` or
|
|
29
|
+
`process.execPath` + CLI entry (bare PATH name only as last-resort fallback).
|
|
30
|
+
- `persistence.ts`: versioned active-branch event folding, the bounded routing-event decoder
|
|
31
|
+
(`subagent-routing-v1`), and bounded child transcript metadata.
|
|
32
|
+
- `maintenance.ts`: filesystem GC (session files) and abort-race helpers; kept out of persistence.
|
|
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.
|
|
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.
|
|
44
|
+
- `config.ts`: defaults ← `~/.pi/subagent.json` ← `PI_SUBAGENT_*` env overrides.
|
|
45
|
+
- `structured.ts`: structured-output contract (dependency-free JSON-Schema subset
|
|
46
|
+
validation, fenced json:result extraction, contract/repair prompts) and
|
|
47
|
+
conservative double-encoded-arg repair. The runner gates the child's settle on
|
|
48
|
+
validation and runs one steer-based repair round before accepting failure.
|
|
49
|
+
- `agents.ts`: named agent files (`.pi/agents/`, `.agents/agents/`, global agent dir).
|
|
50
|
+
Flat-YAML frontmatter + markdown persona body; resolved in policy with explicit
|
|
51
|
+
params > agent file > profile taskDefaults > parent inheritance. Catalog refreshes
|
|
52
|
+
lazily (5s TTL) so new files work mid-session; symlinks and oversized files skipped.
|
|
53
|
+
- `notifications.ts`: background-run completion batching. Successes group within a
|
|
54
|
+
debounce window (hard cap on hold time); failures bypass batching and flush
|
|
55
|
+
immediately; delivered-state is re-checked at flush time so a consuming `wait`
|
|
56
|
+
suppresses the redundant notification.
|
|
57
|
+
- `ui.ts`: renderers, footer status and `/subagents` inspector. The ambient widget
|
|
58
|
+
(extension-side) shows BACKGROUND runs only — foreground runs render inline as the
|
|
59
|
+
tool result, so widget display would double-render them.
|
|
60
|
+
- `extension.ts`: wiring only; no business logic. Nested children at the depth ceiling do
|
|
61
|
+
not re-register the tool; only top-level parents run maintenance.
|
|
62
|
+
|
|
63
|
+
Invariants:
|
|
64
|
+
|
|
65
|
+
1. A run belongs to exactly one parent session key and cannot update another session.
|
|
66
|
+
2. No more than `maxActiveProcesses` children run per extension runtime, and no more than
|
|
67
|
+
`maxGlobalActive` across every Pi parent process on the machine.
|
|
68
|
+
3. Cancellation prevents queued tasks from spawning.
|
|
69
|
+
4. A child session may have only one direct resume writer at a time, enforced by an
|
|
70
|
+
in-memory lock *and* a durable file lock under `lockDir` that survives crashes and
|
|
71
|
+
coordinates across independent parent processes.
|
|
72
|
+
5. Parallel write-capable tasks need isolated worktrees/distinct cwd or explicit unsafe opt-in.
|
|
73
|
+
6. Every tool response is globally capped to 50 KB / 2,000 lines; full data lives in artifacts/transcripts.
|
|
74
|
+
7. Status is compact; wait is the one-shot deliverable.
|
|
75
|
+
8. On shutdown or tree navigation, child runs are cancelled and awaited for a bounded grace period.
|
|
76
|
+
9. On startup, orphan process groups recorded under `lockDir` are reaped (SIGTERM then SIGKILL)
|
|
77
|
+
before any matching child session is eligible for resume. `$state: "lost"` is a labeling
|
|
78
|
+
that keeps `resumeBlocked` until reconciliation proves death.
|
|
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.
|
|
82
|
+
11. Checkpoint persistence events are lightweight (state, usage, process identity, pointers).
|
|
83
|
+
Full transcripts and final output are persisted exactly once, in the terminal event.
|
|
84
|
+
12. High-frequency registry "changed" events coalesce (trailing window); state transitions,
|
|
85
|
+
new child sessions, billed-usage advances, and terminal events flush immediately.
|
|
86
|
+
13. `wait` is interruptible: aborting a wait returns promptly and does NOT cancel the
|
|
87
|
+
background run. Only `cancel` (or parent shutdown) aborts a run.
|
|
88
|
+
14. Budget stops (`max_turns`/`max_cost`) with at least one completed turn end as `partial`
|
|
89
|
+
and deliver their output normally. Streams truncated after useful assistant output also
|
|
90
|
+
end as `partial`. Timeouts report `state: "timeout"` with `timeoutPhase`.
|
|
91
|
+
15. `timeout_ms` covers the whole task, including semaphore queue time, but the phase
|
|
92
|
+
(queued / starting / running) is recorded so agents can apply the right retry policy.
|
|
93
|
+
16. Worktrees live under a durable root (`~/.pi/subagent-worktrees`), never a purgeable OS
|
|
94
|
+
tmpdir. Startup maintenance (top-level parents only) prunes stale git registrations,
|
|
95
|
+
removes unchanged leftovers, and sweeps changed-but-expired worktrees. Live-run
|
|
96
|
+
worktrees are always shielded. `include_wip` worktrees carry the parent's WIP patch in
|
|
97
|
+
the handle: `diff`/`apply` subtract it when subtraction is clean and otherwise report
|
|
98
|
+
the combined delta with an explicit `[includes parent WIP]` warning — never silently
|
|
99
|
+
wrong; a worktree containing only the untouched WIP patch counts as unchanged.
|
|
100
|
+
17. Process-tree reaping after a clean exit can be disabled per task with `keep_background`
|
|
101
|
+
(for legitimately backgrounded work such as dev servers); forced stops always reap.
|
|
102
|
+
18. Protocol completion prefers Pi's `agent_settled` event. Legacy `agent_end` without
|
|
103
|
+
`willRetry` is accepted for older Pi builds; `agent_end` with `willRetry: true` is
|
|
104
|
+
treated as non-terminal.
|
|
105
|
+
19. Depth and spawn-policy parsing fail closed on malformed values: env scrubbing cannot
|
|
106
|
+
silently reset the depth counter to top-level, and a malformed `PI_SUBAGENT_SPAWNS`
|
|
107
|
+
disables spawning rather than unrestricting it.
|
|
108
|
+
20. Budget breaches (`max_turns`/`max_cost`) steer a wrap-up message and allow grace
|
|
109
|
+
turns before SIGTERM; a child that concludes within grace ends `partial` with
|
|
110
|
+
`wrappedUp: true`. `graceTurns: 0` restores immediate stops.
|
|
111
|
+
21. Transient failures (queued timeout, stall, spawn error, provider error, protocol
|
|
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.
|
|
116
|
+
22. The stall watchdog treats protocol silence as suspect, not fatal: after
|
|
117
|
+
`stallAfterMs` the task is flagged and probed via `get_state` (a live child's
|
|
118
|
+
answer clears the flag); only continued silence for `stallKillAfterMs` more kills
|
|
119
|
+
the child — which is then a transient failure eligible for retry.
|
|
120
|
+
23. Only `async: true` runs notify on completion and appear in the ambient widget.
|
|
121
|
+
Notification delivery respects delivered-once: a `wait` that consumed the run
|
|
122
|
+
suppresses the notification.
|
|
123
|
+
24. Named agent files supply per-field defaults only; explicit request params always
|
|
124
|
+
win, and capability profiles fail closed regardless of what an agent file declares.
|
|
125
|
+
25. Structured-output validation never discards paid work: schema failure after the
|
|
126
|
+
repair round downgrades completed → partial with `structuredError`, and the raw
|
|
127
|
+
text still delivers. Validation is enforced on the parent side of the process
|
|
128
|
+
boundary — the child cannot self-attest.
|
|
129
|
+
26. Arg repair only decodes free-text fields with high-signal escape patterns
|
|
130
|
+
(literal \n or \") and no real newlines; identifier fields, tool lists, and
|
|
131
|
+
Windows-path-like strings are never modified.
|
|
132
|
+
27. Global slots are depth-tiered: `tryAcquireGlobalSlot(runId, depth)` admits only while
|
|
133
|
+
`activeAtOrBelowDepth(depth) < maxGlobalActive - reservedFor(depth)`, holding slots
|
|
134
|
+
back for deeper tiers so a full-width spawn tree cannot deadlock on its own children.
|
|
135
|
+
Slot records without a `depth` field count as depth 0.
|
|
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.
|
package/docs/COST-ACCOUNTING.md
CHANGED
|
@@ -1,66 +1,116 @@
|
|
|
1
|
-
# Cost accounting
|
|
2
|
-
|
|
3
|
-
`pi-subagent` reports
|
|
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
|
-
- **
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
- `
|
|
65
|
-
-
|
|
66
|
-
|
|
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.
|