pi-ptc-subagents 4.0.0 → 5.0.1

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 CHANGED
@@ -5,6 +5,116 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [5.0.1] - 2026-10-20
9
+
10
+ ### Changed
11
+
12
+ - **pi 1.3.0 is now measured, not merely claimed.** The package declares
13
+ `@earendil-works/pi-coding-agent: ">=0.86.0"`, so it has always _claimed_ to work on every pi
14
+ release in that range — but 1.3.0, published on 2026-10-19, had never been measured. The
15
+ differential table's measured versions stopped at 1.2.0 and the compatibility matrix's version
16
+ list did not name it. pi 1.3.0 was installed on its own and the forty codemode-switch cases were
17
+ driven through **its own** `DefaultPackageManager.resolve()`: it answered identically to 1.2.0 on
18
+ all forty, and the static capability probe answered ✓ on all sixteen capability points. The
19
+ 1.3.0 column is that measurement, not an inference from 1.2.0 being older. A user on pi 1.3.0
20
+ now has a version whose behaviour this package has actually observed rather than assumed.
21
+ - **The test toolchain moves to vitest 5, and the compatibility matrix can measure the version the
22
+ repository currently pins.** `vitest` 4.1.11 → 5.0.5, `@vitest/coverage-v8` 4.1.11 → 5.0.5,
23
+ `vite-plus` 0.3.3 → 1.3.0, `oxfmt` 0.70.0 → 0.73.0, `oxlint` 1.85.0 → 1.89.0, `@types/node`
24
+ 22.20.4 → 26.6.5. The coverage provider and the test runner have to move together —
25
+ `@vitest/coverage-v8@5.0.5` declares `vitest: 5.0.5` as a peer, and the mismatched pair is what
26
+ produced 79 unhandled rejections (one per test file) in the upgrade's own CI. Separately,
27
+ `scripts/pi-compat-matrix.mjs` decided "this devDependency entry was not found" by asking
28
+ whether **replacing the version changed the text** — and replacing `1.3.0` with `1.3.0` does
29
+ not change the text, so the matrix could not measure the version the repository currently pins.
30
+ It asks whether the pattern matched now, and a test that _runs_ the function pins that.
31
+ - **The toolchain bump reformats 26 markdown files.** oxfmt 0.73 changed list-continuation
32
+ indentation and thematic-break fences. No prose changed. The files it touched carry a single
33
+ unpaired `---` rather than a YAML frontmatter block, so the site's frontmatter stripping never
34
+ matched them and does not match `***` either; the three files that do carry real frontmatter are
35
+ untouched.
36
+
37
+ ### Fixed
38
+
39
+ - **Nothing in the published package's behaviour changed.** The runtime code, the tool
40
+ descriptions, the argument and result shapes and the declared peer range are all as 5.0.0
41
+ shipped them; this release carries development-side and documentation-side work only.
42
+
43
+ ## [5.0.0] - 2026-10-19
44
+
45
+ ### Added
46
+
47
+ - **A dispatch reports the model its child actually ran.** The model used to be resolved inside the
48
+ child and never came back: neither `pi.dispatch`'s `DispatchResult` nor a background
49
+ `TaskRecord` said which provider or model served the call, so a wrong-model delegation could
50
+ only be inferred from a failure after the fact. Both now carry an optional observed-model pair —
51
+ observed by the host from the child's own assistant turns (last turn wins), not taken from the
52
+ request — and the model-facing surfaces state the same fact as a `model: provider/id` line on
53
+ `ptc_subagent` and `ptc_task_output` and as a `model=` fact in a refusal. A transcript that
54
+ carries only a model id reports the id alone; the absence of both means no observed assistant turn
55
+ named the pair — no turn, a supported pi that does not report it, or an old record — not "the
56
+ model is unknown". A background record gains the pair at the same terminal close that writes
57
+ `usage`, so a failed or canceled close that named a model still states it. The pair is named
58
+ `observedProvider` / `observedModel` (ADR-0045): the result's `model` input is the requested
59
+ model, and two different facts no longer share a name. Records written before the rename are
60
+ still read (the field rename itself is breaking for program-visible result shapes — see the
61
+ rename entry under Changed). The rendered value is stripped of ANSI escape sequences and
62
+ control characters and head-capped at 200 characters before it enters model-facing text,
63
+ because the pair is untrusted child wire data. See
64
+ ADR-0044, ADR-0016 §"The child's model", and ADR-0022 §3 as amended.
65
+ - **The `ptc_subagent` call row states the model a generalist dispatch will inherit.** When the
66
+ call names no model and no agent (a generalist child, where a definition cannot pin the tier),
67
+ the request row says `model=inherit` and its expanded view names the provider/id the caller's
68
+ session is on; a named agent gets no such claim, and a pinned `model` is stated as before. This
69
+ is a request fact captured at the call's first render, so a later model switch does not change
70
+ an already-rendered row — and it is not the observed model, which still appears only after the
71
+ child ran. ADR-0044.
72
+
73
+ ### Changed
74
+
75
+ - **BREAKING — the observed-model fields are named `observedProvider` / `observedModel`.**
76
+ `DispatchResult.provider` / `model` and `TaskRecord.provider` / `model` carried the observed
77
+ model, while the input's `model` is the requested model; the shared name made two different
78
+ facts look comparable. The output fields are renamed, the input keeps its name, the child wire
79
+ and the model-facing `model:` text are unchanged, and a record written before the rename is
80
+ normalized on read so its observation is not lost. No aliases: the old result keys are removed,
81
+ which is why this is breaking for program-visible result shapes. See ADR-0045.
82
+ - **BREAKING — a dispatch that names no model now runs on the caller session's current model, not
83
+ pi's global default.** `pi.dispatch` and `ptc_subagent` resolve the child's model first match
84
+ wins: the dispatch's `model` argument, then the agent definition's frontmatter `model:`, then
85
+ the calling session's **current** model, then pi's configured default. A session that switched
86
+ provider or model mid-conversation now carries that switch into its next dispatched child — the
87
+ background-dispatch guide previously said the opposite, and that statement is what this changes.
88
+ Resolution is a snapshot at spawn: a model switched after the child starts does not change it, a
89
+ nested dispatch inherits the child's own session model, and re-running the dispatch is how a
90
+ caller picks up a new model. Tiers 1–2 are unchanged, so pinning the model at the call site or in
91
+ the definition is the explicit opt-out; no config key and no `model: "inherit"` sentinel is
92
+ added. A direct library caller with no context model, an older pi, or a session started without a
93
+ model all fall through to pi's default exactly as before — the session read is structural, so
94
+ the declared 0.86.0 floor gains no new pi type dependency. See ADR-0044 for the decision and its
95
+ rejected alternatives.
96
+
97
+ ### Fixed
98
+
99
+ - **A failed foreground dispatch no longer loses the child's own failure.** The child is a pi
100
+ subprocess: a rate limit or a model error can make it exit 0 after writing
101
+ `stopReason: "error"` and an `errorMessage` on its assistant turn. The foreground loop
102
+ discarded that text and reported the generic `dispatch produced no final text`, while the
103
+ background registry already recorded the child's reason (issue #70's rule). The close decision now
104
+ returns the child's reported error verbatim, using the background registry's rule — the child's
105
+ own error outranks the exit-code and truncation inference — with cancellation still first:
106
+ cancelled, then exit-0-with-text (fulfilled), then the child's error, then exit-0-without-text,
107
+ then truncation. It logs one warning on that newly diagnosed path only. `stderr` is no longer
108
+ described as guaranteed; `errorMessage` is the structured failure channel. This is
109
+ foreground/background parity: one verdict and one sentence for one failure. See ADR-0044
110
+ Decision 3 and ADR-0016 §Decision 3.
111
+ - **`ptc_subagent refused the call: undefined` is gone.** A rejected dispatch without an
112
+ `errorMessage` used to render the literal string `undefined`, telling the model nothing the
113
+ dispatcher knew. The refusal now states the cause plus bounded facts the result already carries —
114
+ whether a child ran or the harness declined to start, the exit code, turns, report channel, and
115
+ the observed model when there is one — tail-bounded to 800 characters so a provider's actionable
116
+ status code survives the cut. See ADR-0044 Decision 3.
117
+
8
118
  ## [4.0.0] - 2026-10-19
9
119
 
10
120
  ### Added
@@ -443,7 +553,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
443
553
  ([ADR-0025](./docs/adr/0025-extension-surface-is-a-setting.md) §3 as amended).
444
554
 
445
555
  **A pi older than 0.99.0 is unaffected.** `ToolExposure` does not exist there — `grep -r
446
- exposure` over `pi-coding-agent@0.86.1`'s `dist/` returns nothing, and so does
556
+ exposure` over `pi-coding-agent@0.86.1`'s `dist/` returns nothing, and so does
447
557
  `AgentSession._getCallableTools` — so the field is ignored and the tools are declared to the
448
558
  model, which is correct: such a pi ships no `codemode`, resolves to `full` anyway, and has
449
559
  nothing that could call a `codemode`-reach tool. Measured across 0.86.1 / 0.87.1 / 0.99.0 /
@@ -1055,7 +1165,7 @@ exposure` over `pi-coding-agent@0.86.1`'s `dist/` returns nothing, and so does
1055
1165
  immediately instead of queueing.** The dispatcher used to read `maxParallelSubCalls`
1056
1166
  (default 10) and FIFO-queue the overflow; per ADR-0016 §2 the N+1th concurrent
1057
1167
  `pi.dispatch` now resolves at once with `{ status: "rejected", errorMessage:
1058
- "dispatch concurrency limit reached" }`. The cap applies to `pi.dispatch` only and
1168
+ "dispatch concurrency limit reached" }`. The cap applies to `pi.dispatch` only and
1059
1169
  has its own counter; builtin binding fan-out keeps DSH's `maxParallelSubCalls` (10)
1060
1170
  FIFO-queueing semantics (ADR-0004), so in-flight builtin calls never consume
1061
1171
  dispatch slots.
package/README.md CHANGED
@@ -79,11 +79,12 @@ const result = await tools["pi.dispatch"]({
79
79
  // result.status: "fulfilled" | "rejected" (the binding never throws)
80
80
  // result.text: final assistant text
81
81
  // result.usage: { input, output, cacheRead, cacheWrite, cost, turns }
82
+ // result.observedProvider?, result.observedModel?: the provider/model the child actually ran
82
83
  // result.exitCode, result.durationMs, result.stderr?, result.errorMessage?
83
84
  ```
84
85
 
85
86
  **Fan out in parallel** with the rest of PTC's tools — dispatch is a binding, not a
86
- model-visible lifecycle tool, so the dispatcher handles concurrency the same way it does for
87
+ model-facing lifecycle tool, so the dispatcher handles concurrency the same way it does for
87
88
  any other tool call:
88
89
 
89
90
  ```ts
@@ -116,6 +117,32 @@ at 20 findings with the withheld count stated in-band.
116
117
  - `signal` — when the parent run is cancelled, every in-flight child gets `SIGTERM` then
117
118
  `SIGKILL` after a 5-second grace window.
118
119
 
120
+ ### The child's model
121
+
122
+ A dispatched child is a fresh `pi` subprocess and resolves its own model, first match wins:
123
+
124
+ 1. the dispatch's `model` argument;
125
+ 2. the named agent definition's frontmatter `model:`;
126
+ 3. **the caller session's current model** — read from the pi session that spawns the child;
127
+ 4. pi's configured default.
128
+
129
+ Tier 3 is the behaviour change of [ADR-0044](./docs/adr/0044-a-dispatch-inherits-the-session-model.md):
130
+ a session that switched provider or model mid-conversation carries that switch into the children it
131
+ dispatches, instead of silently running them on pi's global default. Resolution happens once, at
132
+ spawn — a model switched afterwards does not change a child that is already running, and a nested
133
+ dispatch inherits the child's own session model. Pin the model at the call site or in the agent
134
+ definition to opt out; there is no config key for it, and a direct library caller (or a pi that
135
+ exposes no session model) falls back to pi's default exactly as before.
136
+
137
+ The result says which model the child actually ran: `observedProvider` / `observedModel` on a foreground
138
+ `DispatchResult`, the same optional pair on a background task record (which `ptc_task_output`
139
+ renders as a line), and a `model: provider/id` line in `ptc_subagent`'s text. Absence means no
140
+ observed assistant turn named the pair — no turn, a supported pi that does not report it, or an
141
+ old record — never "unknown model". When the child reported its own failure, a
142
+ rejected dispatch now states that error verbatim as `errorMessage` instead of the generic
143
+ `dispatch produced no final text`, so the foreground failure path matches the background one. Full
144
+ guide: [`docs/usage/bgdispatch.md`](./docs/usage/bgdispatch.md#which-model-the-child-runs).
145
+
119
146
  ### Background dispatch
120
147
 
121
148
  Foreground `pi.dispatch` blocks the program until the child exits. Pass `background: true` to
@@ -205,7 +232,7 @@ Which model-facing tools this package registers is **detected, not configured**
205
232
  setting for it ([ADR-0034](./docs/adr/0034-surface-is-detected-not-set.md)). The short version:
206
233
  a pi whose `codemode` is loaded _and_ callable by the model gets the `subagents` surface
207
234
  (`ptc_subagent` on top; the program pair underneath at `codemode` reach); every other pi gets
208
- `full` (`ptc_run_code` / `ptc_workflow` model-visible). To use the `subagents` surface, put
235
+ `full` (`ptc_run_code` / `ptc_workflow` model-facing). To use the `subagents` surface, put
209
236
  `codemode` in your tool list (`pi --tools read,bash,edit,write,codemode`, or `"defaultTools":
210
237
  ["read", "bash", "edit", "write", "+codemode"]`). To keep this package out of your sessions
211
238
  entirely, run `pi config` and disable its extensions there.
package/dist/index.d.ts CHANGED
@@ -37,6 +37,14 @@ interface ParsedAgentEvent {
37
37
  };
38
38
  };
39
39
  model?: string;
40
+ /**
41
+ * Provider that served this assistant turn. pi >= 1.0.0 puts the resolved provider on the
42
+ * assistant message, so a host can report which model actually ran the child (dispatches
43
+ * otherwise run on pi's configured default, which the caller never sees). Optional and
44
+ * structurally read -- the 0.86.0 floor's type surface may not declare it, and an absent
45
+ * field must mean "not observed" rather than a parse failure.
46
+ */
47
+ provider?: string;
40
48
  stopReason?: string;
41
49
  errorMessage?: string;
42
50
  };
@@ -218,11 +226,13 @@ interface TaskSpawnSource {
218
226
  * Session-level row tracking the lifecycle of one background child. 20 pre-ownership fields since
219
227
  * the 2026-10-16 #199 amendment (19 ADR-0022 §3 originals — its "21 fields" heading is an
220
228
  * authoring miscount — plus `usage`), two optional ADR-0023 owner-identity fields, two ADR-0032
221
- * report fields and ADR-0041's `readOnlyTier`; 25 today, persisted as-is. Optional fields are
229
+ * report fields, ADR-0041's `readOnlyTier`, ADR-0043's `whySuppressed` and the host-observed
230
+ * `observedProvider` / `observedModel` pair (ADR-0045); 28 today, persisted as-is. Optional fields are
222
231
  * absent on records still `running` (e.g. `finishedAt` / `exitCode` / `outputRef` are written at
223
232
  * transition time, not at spawn) and `ownerPid` / `ownerBootMs` are absent on pre-upgrade
224
233
  * legacy records. The `report` / `reportChannel` pair is absent unless the record reached
225
- * `succeeded` (ADR-0032); `usage` is written on every terminal close once a child started.
234
+ * `succeeded` (ADR-0032); `usage` and the observed-model pair are written on every terminal
235
+ * close once a child started.
226
236
  */
227
237
  interface TaskRecord {
228
238
  id: ULID;
@@ -315,6 +325,27 @@ interface TaskRecord {
315
325
  * Absent on records written before this field existed, and on children that never started.
316
326
  */
317
327
  usage?: DispatchUsage;
328
+ /**
329
+ * The provider that actually served this child, observed by the pump off the child's assistant
330
+ * `message_end` events (last turn wins). Persisted at the terminal close, on every outcome the
331
+ * close can produce -- a failed or canceled child still ran a model, and that is the fact the
332
+ * model-observability fix exists to state. Named `observedProvider` because the input's `model`
333
+ * is the requested model, not this one (ADR-0045).
334
+ *
335
+ * ABSENT means no assistant turn NAMED a provider -- not "the child ran on an unknown
336
+ * provider". It covers a child with no observed assistant turn, a supported older pi that
337
+ * does not report the pair on assistant messages (the wire field arrives with pi 1.0.0; the
338
+ * declared floor is 0.86.0), and a record written before these fields existed. A background
339
+ * dispatch that names no model runs on pi's configured default; `ptc_task_output` projects
340
+ * this pair back so a caller can finally see which default that was.
341
+ */
342
+ observedProvider?: string;
343
+ /**
344
+ * The model id the child actually ran, observed like {@link observedProvider}. Present even
345
+ * when `observedProvider` is absent: a transcript may carry a model without a provider, and a
346
+ * half-observation is still an observation.
347
+ */
348
+ observedModel?: string;
318
349
  }
319
350
  /**
320
351
  * Per-subscriber cursor for one TaskRecord (ADR-0022 §5). Cursor is per-subscriber (not per-task)
@@ -425,6 +456,9 @@ type TaskCommand = {
425
456
  report?: ChildReport;
426
457
  reportChannel: ChildReportChannel;
427
458
  usage?: DispatchUsage;
459
+ /** ADR-0045: the observed model names; a legacy on-disk record is normalized on read. */
460
+ observedProvider?: string;
461
+ observedModel?: string;
428
462
  };
429
463
  /** Outcome of one successful command: the persisted record, emitted events, and cursor. */
430
464
  interface TransitionResult {
@@ -492,6 +526,26 @@ interface OutputStorage {
492
526
  outputRef(taskId: ULID): string;
493
527
  }
494
528
  //#endregion
529
+ //#region src/runtime/session-model.d.ts
530
+ /**
531
+ * The caller session's current model, as a dispatch can learn it.
532
+ *
533
+ * ADR-0044: a dispatch that names no model at the call site and whose agent markdown names
534
+ * none inherits the model the caller's session is currently running. That fact lives on pi's
535
+ * tool-execution context (`ctx.model`) rather than in this package, so this module is the one
536
+ * structural reader of it -- kept separate so 0.86.0's type surface (which may not declare the
537
+ * field) cannot turn the read into a compile error on the declared floor.
538
+ *
539
+ * The value is a SNAPSHOT taken when the dispatch starts: a model switch after the spawn does
540
+ * not retro-change a child that is already running, and a nested dispatch inherits the child's
541
+ * own context model (which is what the child runs with, not the grandparent's).
542
+ */
543
+ /** One provider + model id pair, e.g. `deepseek` + `deepseek-flash`. */
544
+ interface SessionModel {
545
+ readonly provider: string;
546
+ readonly id: string;
547
+ }
548
+ //#endregion
495
549
  //#region src/runtime/dispatch.d.ts
496
550
  /**
497
551
  * Per-run in-flight `pi.dispatch` counter (ADR-0016 §2 + ADR-0022 §9). The dispatcher's
@@ -742,6 +796,13 @@ interface BindingContext {
742
796
  * output storage) so the binding's `dispatch()` call shares them.
743
797
  */
744
798
  dispatchDeps?: DispatchDeps;
799
+ /**
800
+ * ADR-0044: the caller session's current model, read by the host off pi's tool-execution
801
+ * context. The `pi.dispatch` binding forwards it into the `DispatchContext` so a dispatch
802
+ * that names no model explicitly inherits the session's. Absent for direct library callers
803
+ * and for a pi too old to pass `ctx.model`: both keep the pre-ADR-0044 behaviour.
804
+ */
805
+ sessionModel?: SessionModel;
745
806
  }
746
807
  interface Binding {
747
808
  readonly name: string;
@@ -1666,6 +1727,14 @@ interface RunPtcProgramOptions {
1666
1727
  * against this run's `dispatchConcurrency` for their whole lifetime.
1667
1728
  */
1668
1729
  dispatchDeps?: DispatchDeps;
1730
+ /**
1731
+ * ADR-0044: the caller session's current model, read by the entrypoint off pi's
1732
+ * tool-execution context. Threaded to every binding call's context so `pi.dispatch` can
1733
+ * inherit it when the call site and the agent markdown both name no model. Absent means
1734
+ * no inheritance -- exactly the pre-ADR-0044 behaviour, which is what every direct library
1735
+ * caller and every pi too old to pass `ctx.model` gets.
1736
+ */
1737
+ sessionModel?: SessionModel;
1669
1738
  /**
1670
1739
  * Optional worker pool (ADR-0017). Absent = spawn a fresh worker and terminate it
1671
1740
  * at run end (the original cold-start path). Present = the dispatcher acquires a