pi-ptc-subagents 3.4.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,177 @@ 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.0] - 2026-10-19
9
+
10
+ ### Added
11
+
12
+ - **A dispatch reports the model its child actually ran.** The model used to be resolved inside the
13
+ child and never came back: neither `pi.dispatch`'s `DispatchResult` nor a background
14
+ `TaskRecord` said which provider or model served the call, so a wrong-model delegation could
15
+ only be inferred from a failure after the fact. Both now carry an optional observed-model pair —
16
+ observed by the host from the child's own assistant turns (last turn wins), not taken from the
17
+ request — and the model-facing surfaces state the same fact as a `model: provider/id` line on
18
+ `ptc_subagent` and `ptc_task_output` and as a `model=` fact in a refusal. A transcript that
19
+ carries only a model id reports the id alone; the absence of both means no observed assistant turn
20
+ named the pair — no turn, a supported pi that does not report it, or an old record — not "the
21
+ model is unknown". A background record gains the pair at the same terminal close that writes
22
+ `usage`, so a failed or canceled close that named a model still states it. The pair is named
23
+ `observedProvider` / `observedModel` (ADR-0045): the result's `model` input is the requested
24
+ model, and two different facts no longer share a name. Records written before the rename are
25
+ still read (the field rename itself is breaking for program-visible result shapes — see the
26
+ rename entry under Changed). The rendered value is stripped of ANSI escape sequences and
27
+ control characters and head-capped at 200 characters before it enters model-facing text,
28
+ because the pair is untrusted child wire data. See
29
+ ADR-0044, ADR-0016 §"The child's model", and ADR-0022 §3 as amended.
30
+ - **The `ptc_subagent` call row states the model a generalist dispatch will inherit.** When the
31
+ call names no model and no agent (a generalist child, where a definition cannot pin the tier),
32
+ the request row says `model=inherit` and its expanded view names the provider/id the caller's
33
+ session is on; a named agent gets no such claim, and a pinned `model` is stated as before. This
34
+ is a request fact captured at the call's first render, so a later model switch does not change
35
+ an already-rendered row — and it is not the observed model, which still appears only after the
36
+ child ran. ADR-0044.
37
+
38
+ ### Changed
39
+
40
+ - **BREAKING — the observed-model fields are named `observedProvider` / `observedModel`.**
41
+ `DispatchResult.provider` / `model` and `TaskRecord.provider` / `model` carried the observed
42
+ model, while the input's `model` is the requested model; the shared name made two different
43
+ facts look comparable. The output fields are renamed, the input keeps its name, the child wire
44
+ and the model-facing `model:` text are unchanged, and a record written before the rename is
45
+ normalized on read so its observation is not lost. No aliases: the old result keys are removed,
46
+ which is why this is breaking for program-visible result shapes. See ADR-0045.
47
+ - **BREAKING — a dispatch that names no model now runs on the caller session's current model, not
48
+ pi's global default.** `pi.dispatch` and `ptc_subagent` resolve the child's model first match
49
+ wins: the dispatch's `model` argument, then the agent definition's frontmatter `model:`, then
50
+ the calling session's **current** model, then pi's configured default. A session that switched
51
+ provider or model mid-conversation now carries that switch into its next dispatched child — the
52
+ background-dispatch guide previously said the opposite, and that statement is what this changes.
53
+ Resolution is a snapshot at spawn: a model switched after the child starts does not change it, a
54
+ nested dispatch inherits the child's own session model, and re-running the dispatch is how a
55
+ caller picks up a new model. Tiers 1–2 are unchanged, so pinning the model at the call site or in
56
+ the definition is the explicit opt-out; no config key and no `model: "inherit"` sentinel is
57
+ added. A direct library caller with no context model, an older pi, or a session started without a
58
+ model all fall through to pi's default exactly as before — the session read is structural, so
59
+ the declared 0.86.0 floor gains no new pi type dependency. See ADR-0044 for the decision and its
60
+ rejected alternatives.
61
+
62
+ ### Fixed
63
+
64
+ - **A failed foreground dispatch no longer loses the child's own failure.** The child is a pi
65
+ subprocess: a rate limit or a model error can make it exit 0 after writing
66
+ `stopReason: "error"` and an `errorMessage` on its assistant turn. The foreground loop
67
+ discarded that text and reported the generic `dispatch produced no final text`, while the
68
+ background registry already recorded the child's reason (issue #70's rule). The close decision now
69
+ returns the child's reported error verbatim, using the background registry's rule — the child's
70
+ own error outranks the exit-code and truncation inference — with cancellation still first:
71
+ cancelled, then exit-0-with-text (fulfilled), then the child's error, then exit-0-without-text,
72
+ then truncation. It logs one warning on that newly diagnosed path only. `stderr` is no longer
73
+ described as guaranteed; `errorMessage` is the structured failure channel. This is
74
+ foreground/background parity: one verdict and one sentence for one failure. See ADR-0044
75
+ Decision 3 and ADR-0016 §Decision 3.
76
+ - **`ptc_subagent refused the call: undefined` is gone.** A rejected dispatch without an
77
+ `errorMessage` used to render the literal string `undefined`, telling the model nothing the
78
+ dispatcher knew. The refusal now states the cause plus bounded facts the result already carries —
79
+ whether a child ran or the harness declined to start, the exit code, turns, report channel, and
80
+ the observed model when there is one — tail-bounded to 800 characters so a provider's actionable
81
+ status code survives the cut. See ADR-0044 Decision 3.
82
+
83
+ ## [4.0.0] - 2026-10-19
84
+
85
+ ### Added
86
+
87
+ - **The `ptc_subagent` call row shows the dispatch, not its argument dump.** The tool had no
88
+ renderer, so pi fell back to its raw argument serialisation — one enormous
89
+ `ptc_subagent task="…\\n\\n…"` line of JSON-escaped prose (2026-10-19 field report). It now
90
+ registers a `renderCall`: collapsed it reads as a heading (agent, foreground/background and
91
+ read-only flags, and a pinned `model` when one was passed) with one task preview line and
92
+ `… 还有 N 行`; `ctrl+e` expands `label` / `why` rows and the task block — the first task line
93
+ plus up to the existing `MAX_LOG_LINES_EXPANDED` continuation lines, so no new cap is
94
+ introduced. Follows ADR-0013's "no escaped JSON in the call row" rule.
95
+ - **`whyEnabled`, a default-on switch for the dispatch `why` block.** `~/.pi/agent/ptc.json` takes a
96
+ boolean `whyEnabled`; when it is `false`, a `why` passed to `pi.dispatch` / `ptc_subagent` is
97
+ dropped instead of delivered, and both the foreground result and the background record say that a
98
+ suppression happened. **The switch fails on:** a non-boolean value, an unparseable file, or a file
99
+ that is not a JSON object all leave the feature ON — a broken value must never remove it. A broken
100
+ _key_ is reported once at `session_start` with the path, the key and the type that arrived; a broken
101
+ _file_ is already reported by the default-mode reader, so one typo produces one notice. The
102
+ `why` argument stays on the schema either way, so a running program is never rejected for passing
103
+ an argument whose value the switch drops. `DispatchResult.whySuppressed` and
104
+ `TaskRecord.whySuppressed` record **that a reason was withheld, never what it was** — a background
105
+ dispatch returns a handle rather than a result, so without the record it would have no way at all
106
+ to say so. See ADR-0043.
107
+ - **`/ptc why on|off`**, a session-scoped override for the switch above. It records one session entry
108
+ and never writes `ptc.json` — the difference is "about this conversation" against "about every
109
+ future one". A resumed session restores the last record; a new one starts from the file. Bare
110
+ `/ptc why` reports the value _and_ which of the two is in force, and an argument that is neither
111
+ `on` nor `off` warns and changes nothing. See ADR-0043.
112
+ - **A succeeded background task's completion notification now states its report channel.** The
113
+ `<bg-task-notification>` for a `succeeded` record gains a `<report-channel>` child —
114
+ `tool`, `prompt-json`, `none` or `opted-out`, the same **total** field ADR-0032 defines —
115
+ rendered between `<error-message>` and `<output-ref>`. Before this, a model that wanted to
116
+ know whether a finished child had honoured the report contract had to poll `ptc_task_output`;
117
+ the push it already receives now answers it, once per finished child. The element is absent from
118
+ every other event because only a `succeeded` close declares a channel: a running task has not
119
+ reported _yet_, and a `failed` / `lost` / `canceled` close has no report claim to state
120
+ (`none` and `opted-out` are claims about a finished child, not the absence of one). See
121
+ ADR-0022 §7 as amended (2026-10-19).
122
+
123
+ ### Changed
124
+
125
+ - **BREAKING — a dispatch's `goal` is now `why`.** `pi.dispatch` and `ptc_subagent` took an
126
+ optional `goal` since 3.4.0; that argument is now `why`, and the child's block is now
127
+ `<pi-ptc-why>` instead of `<pi-ptc-goal>`. A PTC program that passes `goal` must pass `why`.
128
+ The block's **wording is unchanged** — it still reads "This goal is context for doing it well" —
129
+ because that sentence was held constant across ADR-0042's 180 runs and renaming the tag does not
130
+ re-test the contrast; rewriting the sentence would change what was held still. See ADR-0043.
131
+ The rename is not cosmetic: in the agent ecosystem `goal` names an objective or a loop, a thing
132
+ worked toward over time, which is not what this argument holds — the reason one slice was cut out.
133
+ `background` was the other candidate and was unavailable, because the dispatch schema already
134
+ carries a `background` argument meaning "detach the child". No compatibility alias is kept: the
135
+ only consumer is a model reading the live schema each session, and a second name for one value
136
+ would cost exactly the comprehension this change buys back.
137
+ - **The background-dispatch guide now says which model a dispatched child runs, and that it is
138
+ not the caller's.** The child is a fresh `pi` subprocess, so its model resolves — first match
139
+ wins — as the dispatch's `model` argument, then the named agent definition's frontmatter
140
+ `model:`, then pi's own global default; the calling session's current model (and therefore a
141
+ provider switched to mid-session) is **not** inherited. Recorded after the 2026-10-19 field
142
+ report: the parent had switched provider, and the child it dispatched afterwards ran on pi's
143
+ default provider and hit a 429. No behaviour change — the resolution order is what the code
144
+ already did, and the guide states it so the failure is also preventable. See the background
145
+ dispatch guide, `docs/usage/bgdispatch.md`.
146
+ - **The TUI task panel now shows why a background task failed.** The `task-output` panel prints
147
+ the record's failure reason as an `error: …` line first in the output block — ahead of the
148
+ preview or the empty-output placeholder — and the `task-list` panel appends one to each
149
+ `failed` / `lost` row whose record carries an `errorMessage`, so a child that died before
150
+ writing output stays legible in the panel instead of as an empty preview, without a
151
+ `ptc_task_output` call. A `lost` record carries the sweep's reason
152
+ (`session_ended_while_running` / `lost_on_session_restart`) and gets the row too; a
153
+ `canceled` task carries no reason and does not. Both rows reuse the panel's existing
154
+ `MAX_ERROR_CHARS` bound (120, first non-blank line), so no new cap or number is added, and
155
+ neither annotation counts against the panel's row caps — see ADR-0022 §11. A settled
156
+ failure's empty-output placeholder also stops claiming `(no output yet)`: with a reason on the
157
+ record it reads `(no output)`, while a page with no recorded reason keeps the `yet` (this
158
+ payload carries no status to tell a live task from an unlabelled close).
159
+
160
+ ### Fixed
161
+
162
+ - **A background task that failed with an empty log now says why — on the notification, in
163
+ `ptc_task_output`, and in the host log.** A 2026-10-19 field report: a child was rate-limited
164
+ (429) before writing anything, its output log was 0 bytes, the `TaskRecord` carried the reason
165
+ in `errorMessage`, and the parent agent received only `status="failed"` — it read the empty
166
+ log as an infrastructure fault and could not act on the real one. Three surfaces now carry the
167
+ cause. A `<bg-task-notification>` whose record carries an `errorMessage` gains an optional
168
+ `<error-message>` (head-truncated to at most 1024 code points, an ellipsis marking the cut, and
169
+ XML-escaped; absent when the record has no such text), so the push that announces the failure
170
+ states it. `ptc_task_output`
171
+ renders the reason as an `error=…` line ahead of a failed/lost task's body and exposes the
172
+ record's `errorMessage` on the codemode projection as `error_message` (and on `details` as
173
+ `errorMessage`), so the pull after the push reads the same reason. The background pump logs a `logger.warn` with the
174
+ task id and the failure reason, so the host operator no longer sees only an empty child log. The
175
+ terminal empty-output fallback also stops saying "yet": a terminal task reads
176
+ `(no output; task <id> is <status>)`, while `running` / `stopping` keep
177
+ `(no output yet; …)`. See ADR-0022 §7 as amended (2026-10-19).
178
+
8
179
  ## [3.4.0] - 2026-10-19
9
180
 
10
181
  ### Added
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
@@ -169,6 +196,12 @@ per row, with the withheld amount stated in-band. A scalar is one line (`→ 47`
169
196
  bounds ([ADR-0012](./docs/adr/0012-model-facing-result-text.md)): a compact value under 100
170
197
  characters stays on one line, every line caps at 200. Nothing is ever printed as escaped JSON.
171
198
 
199
+ The `ptc_subagent` call row follows the same rule: it reads as a heading — agent, state flags, and
200
+ a pinned model when the caller set one — plus one task preview line and a withheld count, and
201
+ `ctrl+e` expands the `label` / `why` rows and the task block (the first task line plus up to
202
+ `MAX_LOG_LINES_EXPANDED` continuation lines). A dispatch's long prose is never dumped as its
203
+ JSON-escaped argument string.
204
+
172
205
  ## Images
173
206
 
174
207
  An image read _inside_ a program — `await tools.read({ path: "shot.png" })` — is attached to
@@ -186,11 +219,20 @@ and reaches `read` / `bash` / `edit` / `write` / `grep` / `find` / `ls` through
186
219
  …) stay directly callable. Turn it off for one session with `/ptc off` (also `/ptc on`, `/ptc`
187
220
  for status), permanently with `{ "defaultMode": false }` in `~/.pi/agent/ptc.json`.
188
221
 
222
+ A dispatch may also carry the caller's **reason** for the slice: an optional `why` on
223
+ `pi.dispatch` / `ptc_subagent`, which reaches the child as its own `<pi-ptc-why>` block after the
224
+ host's policy block, and never widens the task. It is on by default. Turn it off for one session
225
+ with `/ptc why off` (`/ptc why` reports the current value and whether it came from the session or
226
+ the file), or permanently with `{ "whyEnabled": false }` in `~/.pi/agent/ptc.json`. A value that is
227
+ not a boolean leaves the block **on** and says so at startup — a default-on switch that could turn
228
+ itself off on a typo is the wrong direction. A `why` that was dropped is reported on the dispatch
229
+ result (`whySuppressed`), never silently ([ADR-0043](./docs/adr/0043-a-why-switch-and-a-name.md)).
230
+
189
231
  Which model-facing tools this package registers is **detected, not configured** — there is no
190
232
  setting for it ([ADR-0034](./docs/adr/0034-surface-is-detected-not-set.md)). The short version:
191
233
  a pi whose `codemode` is loaded _and_ callable by the model gets the `subagents` surface
192
234
  (`ptc_subagent` on top; the program pair underneath at `codemode` reach); every other pi gets
193
- `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
194
236
  `codemode` in your tool list (`pi --tools read,bash,edit,write,codemode`, or `"defaultTools":
195
237
  ["read", "bash", "edit", "write", "+codemode"]`). To keep this package out of your sessions
196
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;
@@ -291,6 +301,21 @@ interface TaskRecord {
291
301
  * tier was asked for, which is the same claim the foreground result's absent field makes.
292
302
  */
293
303
  readOnlyTier?: ReadOnlyTier;
304
+ /**
305
+ * ADR-0043: `true` when the caller passed a `why` and this session's switch dropped it, so the
306
+ * child's prompt carried no `<pi-ptc-why>` block. Written at SPAWN, alongside `readOnlyTier`,
307
+ * because it too is a fact about the request rather than about the child's output.
308
+ *
309
+ * **The dropped value is deliberately not here.** ADR-0042 declined to persist `why` on the
310
+ * TaskRecord because the value is a caller-held fact, not a host-measured one. That reasoning
311
+ * reaches the value and stops at the event: "a suppression happened" is observed by the host and
312
+ * is the only thing a background caller can otherwise not learn, since a background dispatch
313
+ * returns a handle and has no result to put it on.
314
+ *
315
+ * Absent means no `why` was passed, which is the same claim the foreground
316
+ * `DispatchResult.whySuppressed`'s absence makes — the two fronts state one fact, not two.
317
+ */
318
+ whySuppressed?: true;
294
319
  /**
295
320
  * Host-observed token usage for this child (ADR-0032's counter, moved to its own carrier).
296
321
  * Written on every terminal close for any child the host actually started — independent of
@@ -300,6 +325,27 @@ interface TaskRecord {
300
325
  * Absent on records written before this field existed, and on children that never started.
301
326
  */
302
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;
303
349
  }
304
350
  /**
305
351
  * Per-subscriber cursor for one TaskRecord (ADR-0022 §5). Cursor is per-subscriber (not per-task)
@@ -410,6 +456,9 @@ type TaskCommand = {
410
456
  report?: ChildReport;
411
457
  reportChannel: ChildReportChannel;
412
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;
413
462
  };
414
463
  /** Outcome of one successful command: the persisted record, emitted events, and cursor. */
415
464
  interface TransitionResult {
@@ -477,6 +526,26 @@ interface OutputStorage {
477
526
  outputRef(taskId: ULID): string;
478
527
  }
479
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
480
549
  //#region src/runtime/dispatch.d.ts
481
550
  /**
482
551
  * Per-run in-flight `pi.dispatch` counter (ADR-0016 §2 + ADR-0022 §9). The dispatcher's
@@ -524,6 +593,23 @@ interface DispatchDeps {
524
593
  * can dereference the bytes (BG-07).
525
594
  */
526
595
  outputStorage?: OutputStorage;
596
+ /**
597
+ * ADR-0043: whether the caller may hand the child a `why`. Absent means ON, which is this
598
+ * package's default and the state ADR-0042's measurement was taken in — so every existing
599
+ * zero-argument call site keeps the behaviour it had before the switch existed.
600
+ *
601
+ * The FILE is read ONCE, at extension load, and carried here rather than consulted per call for
602
+ * the reason #73 already records for `optInBindings`: a switch read per call would let the third
603
+ * dispatch of a run see a different answer from the first, with nothing in the transcript
604
+ * recording that it changed.
605
+ *
606
+ * "Read once" is about this file, not about the value being frozen for the session — `/ptc why`
607
+ * overwrites this exact field mid-session by design (spec D8, ADR-0043), and the `whyConfig`
608
+ * JSDoc in `index.ts` carries that half. What keeps the two together is that the write is
609
+ * recorded: `/ptc why` appends a session entry, so a program that saw the value change knows
610
+ * from the transcript that a user changed it rather than finding it had moved underneath a run.
611
+ */
612
+ whyEnabled?: boolean;
527
613
  }
528
614
  /**
529
615
  * ADR-0041: the two read-only tiers. `strict` narrows what the child can be handed;
@@ -710,6 +796,13 @@ interface BindingContext {
710
796
  * output storage) so the binding's `dispatch()` call shares them.
711
797
  */
712
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;
713
806
  }
714
807
  interface Binding {
715
808
  readonly name: string;
@@ -819,6 +912,43 @@ interface DefaultModeConfig {
819
912
  * on** — the caller should surface the problem rather than silently changing behavior.
820
913
  */
821
914
  export declare function readDefaultModeConfig(agentDir: string): DefaultModeConfig;
915
+ /** Result of reading the `why` switch, with enough detail to warn about a broken file. */
916
+ interface WhyEnabledConfig {
917
+ whyEnabled: boolean;
918
+ source: "file" | "default" | "invalid";
919
+ error?: string;
920
+ }
921
+ /**
922
+ * Read `whyEnabled` from the agent-dir config file — ADR-0043's switch for the dispatch caller's
923
+ * reason.
924
+ *
925
+ * The three outcomes are borrowed from {@link readDefaultModeConfig} rather than invented, because
926
+ * this package already decided what a broken boolean in this file means and a second answer for the
927
+ * same file would be a second failure vocabulary:
928
+ *
929
+ * - Absent file, absent key → **on**. This package is default-on (CONTEXT.md), and ADR-0042's
930
+ * measurement is why the block exists at all.
931
+ * - A non-boolean value, or a file that is not a JSON object, or unparseable JSON → reported as
932
+ * `invalid` **and still resolved to on**.
933
+ *
934
+ * That last line is the one worth stating plainly: a broken value must never be able to take the
935
+ * feature away. A switch whose parse failure silently removes a block the user never asked to
936
+ * remove is worse than one that ignores the typo and says so — the failure here is LOUD (a
937
+ * `session_start` warning naming the path and the key) and the behaviour is UNCHANGED.
938
+ *
939
+ * **A file too broken to parse is not this function's to report**, and an earlier draft got that
940
+ * wrong in a way `tests/extension.test.ts` caught: this reader raised its own "not valid JSON" and
941
+ * `readDefaultModeConfig` raised the identical sentence for the same file, so one typo produced
942
+ * two notices. Two identical warnings train a user to ignore warnings, which defeats the loudness
943
+ * the previous paragraph is buying. The file's parse belongs to the reader that owns
944
+ * `defaultMode`; this one reports only what is wrong with the `whyEnabled` KEY, exactly as
945
+ * `readOptInBindingsConfig` draws the same boundary for `optInBindings`.
946
+ *
947
+ * What this key does NOT do: it stays off the model-facing schema either way. The parameter keeps
948
+ * its place in `DISPATCH_PARAMETERS` so a running program is never rejected for passing an argument
949
+ * whose value this switch decides to drop.
950
+ */
951
+ export declare function readWhyEnabledConfig(agentDir: string): WhyEnabledConfig;
822
952
  /** Result of reading the opt-in binding config, with enough detail to warn about a broken file. */
823
953
  interface OptInBindingsConfig {
824
954
  optInBindings: readonly BuiltinBindingName[];
@@ -1597,6 +1727,14 @@ interface RunPtcProgramOptions {
1597
1727
  * against this run's `dispatchConcurrency` for their whole lifetime.
1598
1728
  */
1599
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;
1600
1738
  /**
1601
1739
  * Optional worker pool (ADR-0017). Absent = spawn a fresh worker and terminate it
1602
1740
  * at run end (the original cold-start path). Present = the dispatcher acquires a
@@ -1736,6 +1874,19 @@ export interface PtcSubagentsOptions {
1736
1874
  codemodeActivation?: CodemodeActivationResolution;
1737
1875
  }
1738
1876
  export default function ptcSubagents(pi: ExtensionAPI, options?: PtcSubagentsOptions): void;
1877
+ /**
1878
+ * `customType` for this session's `why` record (`pi.appendEntry`).
1879
+ *
1880
+ * Separate from `PTC_MODE_ENTRY_TYPE` and it has to be: both readers scan the SAME entry list
1881
+ * backwards for their own type, so a shared one would let `/ptc why off` restore as a mode record
1882
+ * on the next `session_start` -- and `/ptc off` restore as a `why` decision. Two entry types, two
1883
+ * independent switches, and neither recoverable as the other.
1884
+ *
1885
+ * Declared here rather than beside `PTC_MODE_ENTRY_TYPE` in `src/mode/ptc-mode.ts` because that
1886
+ * module is PTC MODE's state machine; this switch has nothing to say about the loadout, and
1887
+ * everything it touches lives in this file.
1888
+ */
1889
+ export declare const PTC_WHY_ENTRY_TYPE = "ptc-why";
1739
1890
  //#endregion
1740
1891
  export type { Binding, BindingContext, BindingTable, CodemodePresence, CodemodeSwitch, CodemodeSwitchResolution, CodemodeSwitchSource, CreateBuiltinBindingsOptions, DefaultModeConfig, DetectedSurface, LegacySurfaceKey, ModeBlockReason, ModeEntryDecision, ModeEntryInput, ModeHideStrategy, OptInBindingName, OptInBindingsConfig, PersistedModeState, PtcConfig, PtcErrorKind, PtcErrorShape, PtcImage, PtcJsonValue, PtcModeState, PtcRunOutcome, PtcSurface, RunPtcProgramOptions, TurnPoolsOptions, WorkerPoolOptions, WorkerPoolStats, WorkerPoolWorkerOptions };
1741
1892
  //# sourceMappingURL=index.d.ts.map