ytdb-slate 0.5.0 → 0.5.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/README.md +2 -2
- package/docs/design-principles.md +3 -1
- package/docs/model-failover.md +5 -5
- package/docs/model-routing.md +182 -44
- package/docs/pr-publishing.md +40 -4
- package/docs/track-workflow.md +29 -0
- package/extension/index.ts +3 -3
- package/extension/mode.ts +1 -1
- package/extension/model-router.ts +14 -11
- package/extension/route.ts +39 -38
- package/extension/state.ts +16 -15
- package/extension/threads.ts +45 -45
- package/extension/tools.ts +40 -33
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ Slate can ride through model API outages. The opt-in `modelFailover` map in `.pi
|
|
|
53
53
|
|
|
54
54
|
## Action-level model routing
|
|
55
55
|
|
|
56
|
-
Slate can pick the model and the effort level per dispatched action rather than running every action on whatever model the orchestrator happens to be on. `router.models` in `.pi/slate.json` (trusted projects only) is a closed list of `provider/id` specs
|
|
56
|
+
Slate can pick the model and the effort level per dispatched action rather than running every action on whatever model the orchestrator happens to be on. `router.models` in `.pi/slate.json` (trusted projects only) is a closed list of `provider/id` specs. Each action uses its `model` and `effort` arguments when present. Otherwise, it uses the thread's base model and a level DERIVED for the model it routes to. That level is the lowest one with a capability measurement, never a fixed default. The list is empty by default. An empty list disables candidate-list enforcement, base seeding, effort derivation, and the router's window, billing, and substitution paths. Per-action `model` and `effort` arguments stay active on both router states. Slate always checks an explicit effort against pi's vocabulary, and against the ladder of the model it routes to whenever it has that model's profile data. An action that omits `model` plans for the thread's pre-router pin, and a thread with no pin gives it no plan target. An action that omits `effort` resolves no level. In both cases the worker session returns to what it opened on. [`docs/model-routing.md`](docs/model-routing.md) holds the full contract, including the known cases where the model or level that runs differs. Only models Slate ships a benchmark profile for can be routed to; entries pi cannot serve, or has no profile for, are dropped with a warning naming them.
|
|
57
57
|
|
|
58
58
|
What the code enforces is deliberately narrower than what the routing data advises. The dispatch guards refuse an unlisted model, a level off the target model's ladder and a level the provider rejects outright; they warn about — and mark — an unmeasured level, refusing it only when `router.allowUnmeasuredEffort` is `false`; and the context-window guard substitutes a wider listed model instead of ever blocking an action. The obligations that come from the profile data itself — keeping review and gate actions on measured levels, and honoring a REFUSE such as `anthropic/claude-fable-5`'s for zero-data-retention work — are stated in the doctrine for the orchestrator to honor; they are **not** code-enforced guards. Full semantics: the `router.*` rows in [Configuration](#configuration) and the shipped [`docs/model-routing.md`](docs/model-routing.md) — if they disagree, that document wins.
|
|
59
59
|
|
|
@@ -86,7 +86,7 @@ Optional config file: `slate.json` in the project's pi config dir (`.pi/slate.js
|
|
|
86
86
|
| `reviewPerspectivesPath` | string | — | Project review charters, each declaring its own finding-ID prefix. The doctrine references this **path**; the orchestrator reads the file alongside the shipped review rules. |
|
|
87
87
|
| `modelFailover` | object (string → string) | — (empty, failover off) | Map of `provider/id` → equal-quality alternative model; on a model API failure the affected site retries once on the mapped model (worker/orchestrator sites only after pi's own retries are exhausted; episode compression has no pi retry loop). Read once at session start — see [`docs/model-failover.md`](docs/model-failover.md). |
|
|
88
88
|
| `preserveGlobalModelDefault` | boolean | `true` | Put pi's global model defaults (`defaultProvider`, `defaultModel`, `defaultThinkingLevel`) back after a Slate-initiated model switch — orchestrator failover and handoff adoption — so the switch stays session-scoped. `false` turns that restore off at both sites (the switches themselves are unaffected), leaving each one persisted in pi's global settings. Best-effort: Slate reverts only a value its own switch produced, and can stand down or give up — limits in [`docs/model-failover.md`](docs/model-failover.md). |
|
|
89
|
-
| `router.models` | string[] | `[]` (empty — routing off) | Closed list of models (`provider/id`) an action may
|
|
89
|
+
| `router.models` | string[] | `[]` (empty — routing off) | Closed list of models (`provider/id`) an action may use. Empty or absent means the router is OFF. Slate enforces no candidate list, seeds no thread base, derives no effort level, and runs no router-only window, billing, or substitution path. Per-action `model` and `effort` arguments stay active on both router states. Slate still checks explicit effort against pi's vocabulary and the routed model's ladder when it has that model's profile data. An action that omits `model` plans for the thread's pre-router pin, and a thread with no pin gives it no plan target. An action that omits `effort` resolves no level. In both cases the worker session returns to what it opened on. The known cases where the model or level that runs differs are listed in [`docs/model-routing.md`](docs/model-routing.md). A listed model becomes routable only if Slate has its benchmark profile, pi registers it, and credentials are configured. Slate drops other entries with a warning. If all entries drop, routing turns off with one summary warning. Slate reads and resolves the list once per session. See [`docs/model-routing.md`](docs/model-routing.md). |
|
|
90
90
|
| `router.allowUnmeasuredEffort` | boolean | `true` | What happens when an explicitly requested effort level is on the target model's ladder but has no capability measurement in Slate's profiles: `true` dispatches it with a ⚠ notice and marks the episode `(unmeasured level)`; `false` refuses the dispatch. Either way a level that is off the model's ladder, or one the provider rejects outright, is refused, and a level Slate DERIVES for an action is always a measured one. |
|
|
91
91
|
|
|
92
92
|
Example `.pi/slate.json` (the `docs/agents/...` paths are placeholders — point them at markdown files that actually exist in **your** project):
|
|
@@ -194,7 +194,9 @@ context budget, also decide per dispatch whether a worker thread still
|
|
|
194
194
|
fits the model it is routed to, in which case the action moves to the
|
|
195
195
|
widest listed model rather than being blocked.
|
|
196
196
|
|
|
197
|
-
|
|
197
|
+
Candidate routing is OFF until `router.models` lists something — the
|
|
198
|
+
per-action `model` and `effort` arguments, and a fallback a failover
|
|
199
|
+
is holding, stay active either way. The feature is
|
|
198
200
|
built on a deliberate split that is worth holding onto when reasoning
|
|
199
201
|
about the architecture: the code ENFORCES less than the routing data
|
|
200
202
|
ADVISES. Dispatch guards refuse what is mechanically wrong — a model
|
package/docs/model-failover.md
CHANGED
|
@@ -89,11 +89,11 @@ Failover applies at three sites:
|
|
|
89
89
|
model and the failed action is re-prompted once with a continuation
|
|
90
90
|
nudge. The transcript is preserved; usage and cost accumulate across
|
|
91
91
|
both attempts. The thread stays on the mapped model while its
|
|
92
|
-
session is live
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
92
|
+
session is live, including across later model-less actions, and the
|
|
93
|
+
`threads` listing marks it `live=<model> (failover)`. The hold, the
|
|
94
|
+
marker and the retry inside the failing dispatch are cases M1, M6
|
|
95
|
+
and M2 of [Known cases where the model or level
|
|
96
|
+
differs](model-routing.md#known-cases-where-the-model-or-level-differs).
|
|
97
97
|
- **Episode compression.** The compression call is retried once with
|
|
98
98
|
the mapped compressor model — only if it is distinct from the
|
|
99
99
|
original, resolvable, and authed. If the retry also fails, the
|
package/docs/model-routing.md
CHANGED
|
@@ -6,14 +6,19 @@ list of models an action may run on; every dispatch then resolves to
|
|
|
6
6
|
one model from that list and one effort level from that model's own
|
|
7
7
|
ladder — chosen to be up to the task and no more, so the bill is
|
|
8
8
|
bounded by the action rather than by the session's current model.
|
|
9
|
-
The list is empty by default
|
|
10
|
-
model list is enforced, no thread gets a seeded base,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
The list is empty by default. With no entries the router is OFF: no
|
|
10
|
+
model list is enforced, no thread gets a seeded base, no effort level
|
|
11
|
+
is derived, and the router's window, billing and substitution paths
|
|
12
|
+
are inert. A dispatch that names no model then plans for the thread's
|
|
13
|
+
pre-router pin, and for no model at all when the thread has none: a
|
|
14
|
+
new worker opens on the host session's model, and a reused one stays
|
|
15
|
+
on the model it opened on. The per-action `model` and `effort`
|
|
16
|
+
arguments are honoured on both router states. What a dispatch plans
|
|
17
|
+
is not always what it runs; the cases known to bite are collected
|
|
18
|
+
under [Known cases where the model or level
|
|
19
|
+
differs](#known-cases-where-the-model-or-level-differs).
|
|
20
|
+
|
|
21
|
+
This document is reference documentation, not workflow doctrine.
|
|
17
22
|
|
|
18
23
|
A model can only be routed to if Slate ships a benchmark profile for
|
|
19
24
|
it. An entry with no profile is named in a warning and excluded —
|
|
@@ -26,36 +31,44 @@ from](#where-the-numbers-come-from-and-how-stale-they-can-be).
|
|
|
26
31
|
|
|
27
32
|
## What routing decides, per action
|
|
28
33
|
|
|
29
|
-
Two
|
|
34
|
+
Two route inputs are per dispatch — on a new thread and on a
|
|
30
35
|
continuation alike:
|
|
31
36
|
|
|
32
37
|
- **the model** — the `model` argument (`"provider/id"`), else the
|
|
33
|
-
thread's base model (router ON), else the thread's pre-router pin
|
|
34
|
-
|
|
38
|
+
thread's base model (router ON), else the thread's pre-router pin,
|
|
39
|
+
else no plan target at all, in which case the session returns to
|
|
40
|
+
the model it opened on (router OFF; M1, M3, M7);
|
|
35
41
|
- **the effort level** — the `effort` argument (one of pi's thinking
|
|
36
|
-
levels), else a level Slate resolves for the model
|
|
37
|
-
|
|
42
|
+
levels), else a level Slate resolves for the model it routes to
|
|
43
|
+
(router ON) or the level the worker session opened on (router
|
|
38
44
|
OFF). See [Effort levels](#effort-levels).
|
|
39
45
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
of them is "whatever the last action asked for":
|
|
46
|
+
The arguments themselves do not become thread defaults. An explicit
|
|
47
|
+
`effort` never governs a later action. An explicit `model` governs
|
|
48
|
+
only its own action too, outside M1, M3, M6 and M7. An omitted `effort`
|
|
49
|
+
is never "whatever the last action asked for":
|
|
45
50
|
|
|
46
51
|
- **router on** — the thread's base effort, re-validated, or a level
|
|
47
|
-
derived for the model
|
|
52
|
+
derived for the model it routes to (see [Effort
|
|
48
53
|
levels](#effort-levels)). A thread based on `luna@medium` whose
|
|
49
54
|
previous action ran at `max` runs the next one at `medium`.
|
|
50
|
-
- **router off** —
|
|
51
|
-
|
|
55
|
+
- **router off** — the worker session is put back to the level it
|
|
56
|
+
opened on, which is pi's own settings default
|
|
52
57
|
(`defaultThinkingLevel`, else pi's built-in default). Slate passes
|
|
53
58
|
that level explicitly at every open, so it is re-clamped against
|
|
54
59
|
the session's current model rather than inherited from the session
|
|
55
60
|
file.
|
|
56
61
|
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
The model axis carries live-session cases the effort axis does not,
|
|
63
|
+
M1 and M3 among them, and they predate action-level routing. The
|
|
64
|
+
effort axis shares only M7. All of them, with the code that
|
|
65
|
+
implements each, are under [Known cases where the model or level
|
|
66
|
+
differs](#known-cases-where-the-model-or-level-differs).
|
|
67
|
+
|
|
68
|
+
A thread's base model and base effort are separate planning state,
|
|
69
|
+
visible in the `threads` listing, and are never set from an action's
|
|
70
|
+
own route. A live failover marker, when present, identifies the
|
|
71
|
+
model that currently overrides that nominal base.
|
|
59
72
|
|
|
60
73
|
Two things can come back instead of a normal episode:
|
|
61
74
|
|
|
@@ -68,6 +81,120 @@ Two things can come back instead of a normal episode:
|
|
|
68
81
|
result — advisory: an evidence gap, a window substitution, a
|
|
69
82
|
long-context billing cliff, a re-seeded base. The action ran.
|
|
70
83
|
|
|
84
|
+
## Known cases where the model or level differs
|
|
85
|
+
|
|
86
|
+
The model and the level an action runs on are not always the ones it
|
|
87
|
+
asked for, or the ones its thread implies. The cases below are the
|
|
88
|
+
ones known to bite, each with the code that implements it. **This
|
|
89
|
+
list is not exhaustive.** `planRoute`, `decideModelSwitch` and
|
|
90
|
+
`decideEffortSwitch` in `extension/route.ts`, applied by `applyRoute`
|
|
91
|
+
in `extension/threads.ts`, are the authority: read them when you need
|
|
92
|
+
certainty rather than a map. This section is the reference for these
|
|
93
|
+
cases. Other sites may name or summarise them; if a summary disagrees,
|
|
94
|
+
this section governs, with the source functions above the ultimate
|
|
95
|
+
authority.
|
|
96
|
+
|
|
97
|
+
Why the MODEL can differ:
|
|
98
|
+
|
|
99
|
+
- **M1 — a live failover holds the thread.** After a successful
|
|
100
|
+
`modelFailover` the worker stays on its fallback, and a dispatch
|
|
101
|
+
that omits `model` keeps it: the revert to the session's opening
|
|
102
|
+
model stands down (BG16). The hold ends when the session is
|
|
103
|
+
disposed, and when a later non-`openOnly` route moves it — an
|
|
104
|
+
explicit `model` on either router state, or the thread's routed
|
|
105
|
+
base while the router is ON. An OFF pre-router pin is `openOnly`
|
|
106
|
+
and does not release it. A reopen ends it too, except in M6.
|
|
107
|
+
*Code:* the `failover-held` keep in `decideModelSwitch`
|
|
108
|
+
(`route.ts`); `applyRoute` clears the marker after a plan-driven
|
|
109
|
+
`setModel`, and `liveFailoverModel` reports it (`threads.ts`).
|
|
110
|
+
- **M2 — a failover fires inside the dispatch.** When the attempt
|
|
111
|
+
fails with a model-API error, Slate switches the live session to
|
|
112
|
+
the mapped model and re-prompts once. One episode covers both
|
|
113
|
+
attempts, and its `ran:` line names the model the action ended on
|
|
114
|
+
rather than the planned one. *Code:* the `isFailoverCandidate`
|
|
115
|
+
block in `runDispatchInner` (`threads.ts`) and `planFailoverSwitch`,
|
|
116
|
+
guard 7 (`route.ts`).
|
|
117
|
+
- **M3 — the revert was attempted and failed.** A dispatch that names
|
|
118
|
+
no model switches the session back to the model it opened on. When
|
|
119
|
+
that spec no longer resolves, or `setModel` throws, Slate warns and
|
|
120
|
+
leaves the session on the model the previous action left it on
|
|
121
|
+
(BG24). A PLAN-driven switch that fails is the opposite case: the
|
|
122
|
+
dispatch aborts, nothing runs and no episode is written. M7 is the
|
|
123
|
+
case where no revert is attempted at all. *Code:* `giveUp` inside
|
|
124
|
+
`applyRoute` (`threads.ts`).
|
|
125
|
+
- **M4 — the window guard substitutes** (router ON). A model that
|
|
126
|
+
cannot hold the thread's context is replaced by the widest listed
|
|
127
|
+
candidate, and only when one is STRICTLY wider than the model it
|
|
128
|
+
would replace. When nothing listed is wider, the action runs on the
|
|
129
|
+
original model and a warning says pi will compact. It never blocks
|
|
130
|
+
the action, and `substitutedFrom` records a replacement. *Code:*
|
|
131
|
+
`strictlyWider` in `planRoute`'s guard 5 (`route.ts`).
|
|
132
|
+
- **M5 — the thread's base was not routable** (router ON). A base
|
|
133
|
+
that is absent or has fallen off the list is re-seeded to a listed
|
|
134
|
+
candidate with a warning, so a model-less dispatch runs on the
|
|
135
|
+
seeded base and not on the value the record carried. *Code:* the
|
|
136
|
+
re-seed branch of `planRoute` with `defaultBase` (`route.ts`),
|
|
137
|
+
written back by `persistReseededBase` (`threads.ts`).
|
|
138
|
+
- **M6 — a reopened session restores a model from its file** (CQ3).
|
|
139
|
+
Slate passes the reopening model explicitly whenever the dispatch
|
|
140
|
+
resolves one, and that overrides the session file. When the
|
|
141
|
+
dispatch resolves no model at all and the host session has none
|
|
142
|
+
either, pi restores the file's last model change instead, which can
|
|
143
|
+
be a fallback M1 was holding. The `live=` marker is cleared on every
|
|
144
|
+
reopen, so in this one shape the fallback outlives its marker.
|
|
145
|
+
*Code:* `opts.model ? resolveModel(ctx, opts.model) : ctx.model` in
|
|
146
|
+
`openWorkerSession` (`worker.ts`); the marker delete in
|
|
147
|
+
`openWorkerFor` and the CQ3 note on `liveFailoverModel`
|
|
148
|
+
(`threads.ts`).
|
|
149
|
+
- **M7 — nothing was captured to return to.** When the worker session
|
|
150
|
+
reported no model as it opened, there is no revert target, and a
|
|
151
|
+
dispatch that names no model leaves the session where it is with no
|
|
152
|
+
switch attempted. The effort axis has the same keep when no opening
|
|
153
|
+
level was captured. *Code:* `NO_SESSION_BASELINE` and the
|
|
154
|
+
`no-baseline` keeps in `decideModelSwitch` and `decideEffortSwitch`,
|
|
155
|
+
fed by `captureSessionBaseline` (`route.ts`).
|
|
156
|
+
|
|
157
|
+
Why the LEVEL can differ. There is no counterpart to M1 here: a
|
|
158
|
+
dispatch that names no `effort` returns the session to the level it
|
|
159
|
+
opened on. M7 and E6 are known cases in which it does not.
|
|
160
|
+
|
|
161
|
+
- **E1 — pi clamps a level Slate cannot judge.** When the ladder for
|
|
162
|
+
the judged model is unreadable — no profile, a throwing lookup, a
|
|
163
|
+
non-array ladder — the guards stand down and the level goes to pi,
|
|
164
|
+
which clamps it to something the model offers. Slate reports
|
|
165
|
+
nothing, because it has no evidence to report. *Code:*
|
|
166
|
+
`checkEffortFor` and `guardEffort`'s `ladderKnown` (`route.ts`).
|
|
167
|
+
- **E2 — M4 moved the model under an explicit level.** A level that
|
|
168
|
+
is not valid on the substituted model is DROPPED with a warning
|
|
169
|
+
rather than refusing the action — a context size must never
|
|
170
|
+
hard-block a dispatch — and the session's opening level applies.
|
|
171
|
+
*Code:* the `soft` branch of `guardEffort` (`route.ts`).
|
|
172
|
+
- **E3 — a stored base level no longer holds.** A base effort is a
|
|
173
|
+
cached derivation over a table that ships with Slate. When today's
|
|
174
|
+
table no longer reads it as `ok`, it is silently re-derived for
|
|
175
|
+
that model: nobody asked for that level, so a stale cache is
|
|
176
|
+
Slate's to correct rather than news to report. *Code:* the stored
|
|
177
|
+
base-effort branch of `planRoute`, with `lowestMeasuredEffort`
|
|
178
|
+
(`route.ts`).
|
|
179
|
+
- **E4 — the action's model is not the base model.** An explicit
|
|
180
|
+
`model`, or M4, drops the inherited base level and derives the new
|
|
181
|
+
model's own lowest measured level instead. A level derived for one
|
|
182
|
+
model is never carried onto another. *Code:* the effort resolution
|
|
183
|
+
in `planRoute`, keyed on `judgedModel` (`route.ts`).
|
|
184
|
+
- **E5 — M2 re-clamps the level.** The failover route requests no
|
|
185
|
+
level, and pi re-clamps the session's current level for the
|
|
186
|
+
fallback model. *Code:* `session.setModel(mapped)` in the failover
|
|
187
|
+
block (`threads.ts`); `openWorkerSession` passes the settings
|
|
188
|
+
default explicitly at every open for the same reason (`worker.ts`).
|
|
189
|
+
- **E6 — the level was judged for one model and another model runs.**
|
|
190
|
+
When M1, M3 or M7 leaves the session on a model other than the plan
|
|
191
|
+
target, the level the plan resolved is still applied, and pi clamps
|
|
192
|
+
it to that model's own ladder with nothing reported.
|
|
193
|
+
`effortJudgedFor` names the model the judgement was about, which is
|
|
194
|
+
then not the model that ran. *Code:* the effort switch in
|
|
195
|
+
`applyRoute`, which runs after a model `keep` (`threads.ts`), and
|
|
196
|
+
`decideEffortSwitch` (`route.ts`).
|
|
197
|
+
|
|
71
198
|
## Configuration
|
|
72
199
|
|
|
73
200
|
`router` lives in `.pi/slate.json` and, like the rest of that file,
|
|
@@ -196,7 +323,8 @@ The vocabulary is pi's ladder and nothing else: `off`, `minimal`,
|
|
|
196
323
|
actually offers is per model — the shipped table records a ladder
|
|
197
324
|
per model id, not a family rule.
|
|
198
325
|
|
|
199
|
-
**An omitted `effort` never resolves to a fixed default.**
|
|
326
|
+
**An omitted `effort` never resolves to a fixed default.** With the
|
|
327
|
+
router ON, in order:
|
|
200
328
|
|
|
201
329
|
1. the thread's stored base effort — but only when the action runs
|
|
202
330
|
on the thread's base model, and only while that stored level
|
|
@@ -205,8 +333,8 @@ per model id, not a family rule.
|
|
|
205
333
|
the ladder, or onto the provider's rejection list) is silently
|
|
206
334
|
re-derived; nobody asked for it, so a stale cache is Slate's
|
|
207
335
|
problem to correct rather than news to report;
|
|
208
|
-
2. otherwise the **lowest measured level of the model
|
|
209
|
-
|
|
336
|
+
2. otherwise the **lowest measured level of the model it routes to**
|
|
337
|
+
— the lowest level on that model's ladder that carries a traced
|
|
210
338
|
capability measurement. A level derived for one model is never
|
|
211
339
|
carried onto another, so an explicit `model`, or a window
|
|
212
340
|
substitution, re-derives it;
|
|
@@ -214,14 +342,19 @@ per model id, not a family rule.
|
|
|
214
342
|
the level the worker session opened on applies, which is pi's own
|
|
215
343
|
settings default — never a level another action left behind.
|
|
216
344
|
|
|
345
|
+
With the router OFF Slate resolves no level at all, so that same
|
|
346
|
+
opening level applies to every action that omits `effort`.
|
|
347
|
+
|
|
217
348
|
So a higher level is only ever reached by naming it: pass `effort`
|
|
218
349
|
explicitly on the dispatch. A derived level is measured by
|
|
219
350
|
construction and therefore cannot trip the effort guards; only an
|
|
220
351
|
explicit one can.
|
|
221
352
|
|
|
222
|
-
An explicit level is judged against the model the action
|
|
223
|
-
|
|
224
|
-
|
|
353
|
+
An explicit level is judged against the model the action routes to,
|
|
354
|
+
which `effortJudgedFor` names. [Known cases where the model or level
|
|
355
|
+
differs](#known-cases-where-the-model-or-level-differs) collects what
|
|
356
|
+
can move either afterwards. The difference between a warning and a
|
|
357
|
+
refusal is:
|
|
225
358
|
|
|
226
359
|
- **evidence gap** (the level is on that model's ladder but no
|
|
227
360
|
traced source reports a capability result there) — advisory. The
|
|
@@ -230,8 +363,8 @@ is:
|
|
|
230
363
|
`router.allowUnmeasuredEffort: false` turns this one case into a
|
|
231
364
|
hard refusal for the whole project;
|
|
232
365
|
- **off the model's ladder** — refused, whenever the ladder is
|
|
233
|
-
known. pi would otherwise clamp the level silently and
|
|
234
|
-
orchestrator would believe the action ran at a level the model
|
|
366
|
+
known, outside E2. pi would otherwise clamp the level silently and
|
|
367
|
+
the orchestrator would believe the action ran at a level the model
|
|
235
368
|
never offered;
|
|
236
369
|
- **rejected outright by the provider** — refused, always. Such a
|
|
237
370
|
level is still on pi's ladder for the model (the table records the
|
|
@@ -300,13 +433,15 @@ model's ladder is unreadable.
|
|
|
300
433
|
its decision to make), the same lines in the live progress output,
|
|
301
434
|
and `details.ranModel` / `details.ranEffort` /
|
|
302
435
|
`details.ranEffortUnmeasured` / `details.warnings` for a renderer.
|
|
303
|
-
- **In the `threads` listing:** `base=<model>@<level>?` —
|
|
304
|
-
dispatch
|
|
436
|
+
- **In the `threads` listing:** `base=<model>@<level>?` — the nominal
|
|
437
|
+
plan target when a dispatch omits `model`. The trailing `?` marks
|
|
305
438
|
the level, not the model, as provisional: it is a stored default,
|
|
306
|
-
re-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
live
|
|
439
|
+
re-validated against the model's current capability data on every
|
|
440
|
+
dispatch and silently re-derived if it no longer holds. `last=` is
|
|
441
|
+
the model and level that the last action actually ran on, with
|
|
442
|
+
`(unmeasured)` where that applies. `live=<model> (failover)` means
|
|
443
|
+
a held fallback currently overrides the nominal base for the live
|
|
444
|
+
session.
|
|
310
445
|
- **At the session level:** the resolution's own warnings, as UI
|
|
311
446
|
notifications (console when no UI is attached).
|
|
312
447
|
- **In the orchestrator's own system prompt, every turn:** the
|
|
@@ -519,13 +654,16 @@ concern; a green suite says nothing about it.
|
|
|
519
654
|
dispatch guard and anything else that consults the router agree.
|
|
520
655
|
A key or provider added mid-session, and an edit to
|
|
521
656
|
`slate.json`, both need a new pi session.
|
|
522
|
-
- **Router OFF
|
|
523
|
-
|
|
524
|
-
seeded base
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
657
|
+
- **Router OFF disables candidate routing, not action arguments or
|
|
658
|
+
failover.** With an empty list there is no model list, window
|
|
659
|
+
guard, billing notice, seeded base or candidate substitution, and
|
|
660
|
+
Slate derives no effort level. A model-less plan targets the
|
|
661
|
+
thread's pre-router pin, else nothing, so the session returns to the
|
|
662
|
+
model it opened on outside M1, M3 and M7, and to the level it opened
|
|
663
|
+
on outside M7 and E6. An explicit `effort` is still judged against
|
|
664
|
+
the shipped profile for the plan target when pi's registry can serve
|
|
665
|
+
that model. The `model` argument itself is passed through
|
|
666
|
+
byte-for-byte, so a malformed spec still produces pi's own error.
|
|
529
667
|
- **A mid-thread model switch costs the prompt cache.** A long
|
|
530
668
|
thread runs overwhelmingly on cache reads; routing a small action
|
|
531
669
|
to a cheaper model on a cold cache can cost more than staying on
|
package/docs/pr-publishing.md
CHANGED
|
@@ -59,6 +59,31 @@ The description becomes the squash-commit body on the default
|
|
|
59
59
|
development branch — it is the permanent git-archaeology record for
|
|
60
60
|
the change. Write it accordingly.
|
|
61
61
|
|
|
62
|
+
Aim to keep the final delivery commit body at or below 16,384 UTF-8
|
|
63
|
+
bytes, excluding the subject. This is a target, not a gate. Measure
|
|
64
|
+
the exact body text as UTF-8 without adding a newline: for a PR, count
|
|
65
|
+
the GitHub API `body` string; for a delivered commit, count the byte
|
|
66
|
+
sequence after the subject separator in `git cat-file commit <sha>`.
|
|
67
|
+
Do not use a formatted `git log` value such as `%b`, which adds an
|
|
68
|
+
output newline.
|
|
69
|
+
|
|
70
|
+
If the body is larger, first remove repetition and merge overlapping
|
|
71
|
+
material. Then record a size exception in Risks & accepted trade-offs
|
|
72
|
+
carrying measurements, not assurances: the overrun, by the canonical
|
|
73
|
+
method above; every top-level section's byte count by that same
|
|
74
|
+
method, largest first; and, for each section that was condensed, its
|
|
75
|
+
byte count before condensing. A section with no before-count is
|
|
76
|
+
visibly untouched, so the user weighs any claim that nothing could be
|
|
77
|
+
removed against the counts beside it, and can refuse the exception. Do
|
|
78
|
+
not remove decisions, risks, verdicts, or evidence needed to
|
|
79
|
+
understand or review the change only to meet the target.
|
|
80
|
+
|
|
81
|
+
By the canonical measurement above, the most recent accepted delivery
|
|
82
|
+
body, from the publishing-enabled path, was 20,957 bytes: 27.9% above
|
|
83
|
+
the target. That precedent establishes that a justified overrun can
|
|
84
|
+
pass; it does not establish the typical size of either delivery path
|
|
85
|
+
or a content-equivalent exception in the publishing-disabled path.
|
|
86
|
+
|
|
62
87
|
## Tracks table
|
|
63
88
|
|
|
64
89
|
The description's Tracks section holds the track table; its
|
|
@@ -102,6 +127,14 @@ executed in order:
|
|
|
102
127
|
description, including the Planned changes section, describes the
|
|
103
128
|
change as implemented, folding in everything added, dropped, or
|
|
104
129
|
reshaped since the draft PR was opened.
|
|
130
|
+
- Resolve every remaining Open Question, or record its user-approved
|
|
131
|
+
deferral or accepted uncertainty in Risks & accepted trade-offs. A
|
|
132
|
+
mention without the user's disposition is not enough.
|
|
133
|
+
- Measure the final PR description by the canonical method in §
|
|
134
|
+
Description rules. If it exceeds the target, verify that Risks &
|
|
135
|
+
accepted trade-offs carries the size exception's measurements: the
|
|
136
|
+
overrun, the per-section counts, and the before-counts of whatever
|
|
137
|
+
was condensed.
|
|
105
138
|
- Last, re-read the whole PR description end-to-end to confirm the
|
|
106
139
|
as-flipped text tells one consistent story.
|
|
107
140
|
|
|
@@ -110,10 +143,13 @@ executed in order:
|
|
|
110
143
|
The user may wait for CI green and/or peer-review completion and ask
|
|
111
144
|
the agent to fix test failures or review observations. The agent lands
|
|
112
145
|
fixes as normal commits, keeps the description in sync, and presents
|
|
113
|
-
agent-landed commits to the user as they land.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
146
|
+
agent-landed commits to the user as they land. After every post-flip
|
|
147
|
+
description change, and again at the final handoff for merge, repeat
|
|
148
|
+
the byte measurement and any over-target size exception required by §
|
|
149
|
+
Description rules. Commits pushed directly by reviewers are visible in
|
|
150
|
+
the PR UI; the agent reconciles the description with them on its next
|
|
151
|
+
task. The user's merge act is the final approval, including acceptance
|
|
152
|
+
of any recorded size exception.
|
|
117
153
|
|
|
118
154
|
## After the merge
|
|
119
155
|
|
package/docs/track-workflow.md
CHANGED
|
@@ -108,6 +108,35 @@ explicitly hands any remaining Open Questions to the user, then
|
|
|
108
108
|
deletes the log file. On abandonment the agent deletes the log file
|
|
109
109
|
after offering its content to the user for archiving.
|
|
110
110
|
|
|
111
|
+
Aim to keep the final delivery commit body at or below 16,384 UTF-8
|
|
112
|
+
bytes, excluding the subject. This is a target, not a gate. Measure
|
|
113
|
+
the exact body text as UTF-8 without adding a newline: count the byte
|
|
114
|
+
sequence after the subject separator in `git cat-file commit <sha>`.
|
|
115
|
+
Do not use a formatted `git log` value such as `%b`, which adds an
|
|
116
|
+
output newline. For a proposed commit, apply the same rule to the
|
|
117
|
+
exact body text before committing.
|
|
118
|
+
|
|
119
|
+
If the body is larger, first remove repetition and merge overlapping
|
|
120
|
+
material. Then add a `Size exception` paragraph to the delivery commit
|
|
121
|
+
body carrying measurements, not assurances: the overrun, by the
|
|
122
|
+
canonical method above; every top-level section's byte count by that
|
|
123
|
+
same method, largest first; and, for each section that was condensed,
|
|
124
|
+
its byte count before condensing. A section with no before-count is
|
|
125
|
+
visibly untouched, so the user weighs any claim that nothing could be
|
|
126
|
+
removed against the counts beside it, and can refuse the exception. Do
|
|
127
|
+
not remove decisions, risks, verdicts, or evidence needed to
|
|
128
|
+
understand or review the change only to meet the target. Immediately
|
|
129
|
+
before presenting the final delivery commit to the user, measure it by
|
|
130
|
+
that method and obtain the user's approval of the body and of any size
|
|
131
|
+
exception; repeat both steps if the body changes before it lands.
|
|
132
|
+
|
|
133
|
+
By this canonical measurement, the most recent accepted delivery body
|
|
134
|
+
from the publishing-enabled path was 20,957 bytes, 27.9% above the
|
|
135
|
+
target. That precedent establishes only that a justified overrun can
|
|
136
|
+
pass in that path. Publishing-enabled descriptions can carry material
|
|
137
|
+
that this path does not, so the figure is neither a direct measurement
|
|
138
|
+
nor an exercised exception for publishing-disabled delivery.
|
|
139
|
+
|
|
111
140
|
### Under-trigger guardrail
|
|
112
141
|
|
|
113
142
|
When shaping the planned-changes statement without a log, state this
|
package/extension/index.ts
CHANGED
|
@@ -101,9 +101,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
101
101
|
// One MEMOIZED model-router resolution per session (model-router.ts), reassigned
|
|
102
102
|
// every session_start below for the same reason as the resolver above: the
|
|
103
103
|
// candidate list is frozen at first use, and a dispatch guard built from a
|
|
104
|
-
// different resolution than the doctrine describes would be a bug. Starts
|
|
105
|
-
//
|
|
106
|
-
//
|
|
104
|
+
// different resolution than the doctrine describes would be a bug. Starts at the
|
|
105
|
+
// shared OFF resolution — no candidate list is available — until session_start
|
|
106
|
+
// has sanitized `router.models`.
|
|
107
107
|
let resolveModelRouterResolution: () => ModelRouterResolution = () => ROUTER_OFF;
|
|
108
108
|
|
|
109
109
|
// One base-model tracker per session (base-model.ts), reassigned every
|
package/extension/mode.ts
CHANGED
|
@@ -339,7 +339,7 @@ ${n}. Route every action to the cheapest model and effort that clears it. Routab
|
|
|
339
339
|
${rows.join("\n")}${legend === "" ? "" : `\n ${legend}.`}
|
|
340
340
|
\`model\` and \`effort\` route THAT action only. Omit \`model\` for the thread's
|
|
341
341
|
base${newThreadBase}; omit \`effort\` for its base
|
|
342
|
-
level, else the FIRST measured level of the model
|
|
342
|
+
level, else the FIRST measured level of the model it routes to — never a higher
|
|
343
343
|
one, so name the level harder work needs. Off-ladder and provider-rejected
|
|
344
344
|
levels are tool errors; an unmeasured one ${gap}.
|
|
345
345
|
Prices are base rates: some models bill a long-context multiplier above a
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Model router: resolving WHICH models an action may be dispatched to, and at
|
|
3
3
|
* which effort levels.
|
|
4
4
|
*
|
|
5
|
-
* The router is OFF by default
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* The router is OFF by default: with no `router.models` configured this resolver
|
|
6
|
+
* returns no candidates and emits no router warning. When a project DOES list
|
|
7
|
+
* models, this module maps that list plus pi's model registry
|
|
8
8
|
* plus the benchmark profiles in model-profiles.ts onto a small, ordered set of
|
|
9
9
|
* routable candidates — and the warnings that make the gaps in that data
|
|
10
10
|
* visible instead of silent.
|
|
@@ -423,9 +423,9 @@ function tierOf(profile: ModelProfile): number {
|
|
|
423
423
|
* Resolve the routable candidate set.
|
|
424
424
|
*
|
|
425
425
|
* An empty model list returns ROUTER_OFF WITHOUT touching the registry or the
|
|
426
|
-
* profile table — the fast path, since the router is off by default and
|
|
427
|
-
*
|
|
428
|
-
*
|
|
426
|
+
* profile table — the fast path, since the router is off by default and there
|
|
427
|
+
* are no candidates to resolve. Every warning is emitted at most once per
|
|
428
|
+
* condition (D58), sanitized, through
|
|
429
429
|
* `warn` (default no-op) and collected on the result.
|
|
430
430
|
*
|
|
431
431
|
* Hostile/raw input is handled here rather than assumed away: `models` may be
|
|
@@ -770,8 +770,11 @@ function frozenResolution(
|
|
|
770
770
|
* decides anything — `router.allowUnmeasuredEffort` and the refusal wording
|
|
771
771
|
* live in the dispatch path.
|
|
772
772
|
*
|
|
773
|
-
* With the router OFF
|
|
774
|
-
*
|
|
773
|
+
* With the router OFF this predicate reports `ok` for every pair, having no
|
|
774
|
+
* candidate list to judge against — a statement about THIS function only, since
|
|
775
|
+
* route.ts feeds it a SYNTHESISED one-candidate resolution built from its
|
|
776
|
+
* injected profile source, so a router-OFF explicit level can still be refused.
|
|
777
|
+
* An omitted/empty effort is also `ok` — the
|
|
775
778
|
* ladder question only arises for a level the caller actually asked for.
|
|
776
779
|
* `listedGap` distinguishes an authored evidence gap from one the table never
|
|
777
780
|
* mentioned (see EffortVerdict/BG9); both are advisory.
|
|
@@ -823,9 +826,9 @@ export function checkEffort(resolution: ModelRouterResolution, spec: string, eff
|
|
|
823
826
|
* The memo holds unconditionally (BG3). Warnings already go through a
|
|
824
827
|
* try/catch, and a throw from `getInput` or from anything else in resolution is
|
|
825
828
|
* caught here and cached as an OFF resolution carrying one warning: the router
|
|
826
|
-
* failing is not a reason to keep re-running it every turn, and
|
|
827
|
-
*
|
|
828
|
-
* second message-level dedup layer — it made the real dedup untestable (CQ4)
|
|
829
|
+
* failing is not a reason to keep re-running it every turn, and an empty
|
|
830
|
+
* candidate result is safe for candidate-dependent guards. There is deliberately
|
|
831
|
+
* NO second message-level dedup layer — it made the real dedup untestable (CQ4)
|
|
829
832
|
* while adding nothing the memo does not already guarantee.
|
|
830
833
|
*
|
|
831
834
|
* CQ7: what is frozen includes the registry and auth SNAPSHOT. A key added to
|
package/extension/route.ts
CHANGED
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
* refused: the fall-through path must always have a valid destination, or
|
|
30
30
|
* configuring a list would strand every thread that predates it.
|
|
31
31
|
*
|
|
32
|
-
* EFFORT — the level
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
32
|
+
* EFFORT — the level the planner resolves is ALWAYS one that belongs to the model
|
|
33
|
+
* it routes to: either the caller named it, in which case it is judged against THAT
|
|
34
|
+
* model's own ladder, or it is derived from that model (its lowest MEASURED level).
|
|
35
|
+
* A level derived for one model is never carried onto another, and a level
|
|
36
36
|
* STORED for this one is only replayed while today's table still reads it as `ok` —
|
|
37
37
|
* a cached derivation must not outlive the evidence it was derived from. `effort`
|
|
38
38
|
* therefore reports one and only one thing, and `effortJudgedFor` names the model
|
|
@@ -43,13 +43,14 @@
|
|
|
43
43
|
* DIFFERENT model, and applying it produced evidence-gap warnings — or, with
|
|
44
44
|
* allowUnmeasuredEffort:false, outright rejections — for a level nobody requested.
|
|
45
45
|
*
|
|
46
|
-
* WITH THE ROUTER OFF
|
|
47
|
-
* billing notice,
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
46
|
+
* WITH THE ROUTER OFF ITS PLANNER-OWNED MECHANISMS ARE INERT. There is no list,
|
|
47
|
+
* window guard, billing notice, seeded or persisted base, or tracker input. The
|
|
48
|
+
* plan target is the `model` ARGUMENT (honoured per action) or else the thread's
|
|
49
|
+
* PRE-ROUTER PIN; the pin only opens a NEW worker session and never instructs a
|
|
50
|
+
* live one to switch (`openOnly`). A later switch decision KEEPS a held failover
|
|
51
|
+
* fallback rather than applying an open-only target (decideModelSwitch). The raw
|
|
52
|
+
* argument still passes through byte-for-byte, so a malformed spec produces pi's
|
|
53
|
+
* own error rather than a router opinion.
|
|
53
54
|
*
|
|
54
55
|
* A FAILURE TO READ EVIDENCE IS NOT EVIDENCE OF A PROBLEM. Every injected data
|
|
55
56
|
* source may be missing, throwing or malformed — a profile lookup, a ladder
|
|
@@ -68,13 +69,13 @@
|
|
|
68
69
|
* is one that is not a STRING at all, since reading a malformed argument as
|
|
69
70
|
* absent would quietly run the action at the thread's base level instead.
|
|
70
71
|
* 1. LIST MEMBERSHIP (router ON only): a resolved model outside the effective
|
|
71
|
-
* candidate list is rejected, naming the list. With the router OFF
|
|
72
|
-
*
|
|
72
|
+
* candidate list is rejected, naming the list. With the router OFF this guard
|
|
73
|
+
* does not enforce a list; the raw model argument continues to pi unchanged.
|
|
73
74
|
* A THREAD'S BASE is exempt by REPAIR, not by exception — see THE ONE RULE
|
|
74
75
|
* above: absent or off-list, it is re-seeded, so only an EXPLICIT off-list
|
|
75
76
|
* model is ever refused.
|
|
76
77
|
* 5. CONTEXT WINDOW: never a hard block, and it runs BEFORE the effort guards so
|
|
77
|
-
* that every effort judgement is about the model
|
|
78
|
+
* that every effort judgement is about the model the planner routes to. The
|
|
78
79
|
* REGISTRY window (which is what a RouterCandidate carries; a profile's own
|
|
79
80
|
* figure is documentation only) reduced by pi's compaction reserve, judged by
|
|
80
81
|
* pi's OWN predicate. A model that cannot hold the thread's context is
|
|
@@ -153,7 +154,7 @@ export interface RoutePlanInput {
|
|
|
153
154
|
requestedModel?: string;
|
|
154
155
|
/** The dispatch's `effort` argument, raw and unvalidated. */
|
|
155
156
|
requestedEffort?: string;
|
|
156
|
-
/** The session's FROZEN router resolution. An off resolution
|
|
157
|
+
/** The session's FROZEN router resolution. An off resolution supplies no router candidates. */
|
|
157
158
|
resolution: ModelRouterResolution;
|
|
158
159
|
/** router.allowUnmeasuredEffort. Default TRUE: only an explicit false refuses an evidence gap. */
|
|
159
160
|
allowUnmeasuredEffort?: boolean;
|
|
@@ -429,7 +430,7 @@ export type EffortSwitchDecision =
|
|
|
429
430
|
* whole rule, in one pure function, mirroring decideModelSwitch:
|
|
430
431
|
*
|
|
431
432
|
* 1. the level the PLAN resolved (an explicit `effort`, the thread's stored default
|
|
432
|
-
* once re-validated, or one derived for the model
|
|
433
|
+
* once re-validated, or one derived for the model the planner routes to);
|
|
433
434
|
* 2. failing that, the level the session was OPENED on — pi's clamped settings
|
|
434
435
|
* default. This is the RESTORE, and it is what makes `effort` per-ACTION: without
|
|
435
436
|
* it a level set by one action silently governs the next (BG18).
|
|
@@ -515,11 +516,10 @@ export type RoutePlanVerdict = RoutePlanProceed | RoutePlanReject;
|
|
|
515
516
|
/**
|
|
516
517
|
* Normalise whatever the session's router resolver handed back.
|
|
517
518
|
*
|
|
518
|
-
* A malformed or half-built resolution
|
|
519
|
-
*
|
|
520
|
-
*
|
|
521
|
-
*
|
|
522
|
-
* the window guard.) Shared with threads.ts so the shape check has ONE definition.
|
|
519
|
+
* A malformed or half-built resolution falls back to the shared ROUTER_OFF value
|
|
520
|
+
* because candidate-dependent planner paths below walk `candidates` directly.
|
|
521
|
+
* (checkEffort tolerates a junk resolution on its own, CQ5.) Shared with threads.ts
|
|
522
|
+
* so the shape check has ONE definition.
|
|
523
523
|
*/
|
|
524
524
|
export function usableResolution(value: unknown): ModelRouterResolution {
|
|
525
525
|
const resolution = value as ModelRouterResolution | undefined;
|
|
@@ -719,8 +719,9 @@ function count(tokens: number): string {
|
|
|
719
719
|
* failover. What remains is the one rule failover itself must obey (never the
|
|
720
720
|
* model that just failed) and a NON-SUBSTITUTING window check that warns and
|
|
721
721
|
* proceeds — substituting here would be the router vetoing failover by another
|
|
722
|
-
* name. Gated on the router being ON so a router-
|
|
723
|
-
* pre-router
|
|
722
|
+
* name. Gated on the router being ON, so a router-OFF failover gets no window
|
|
723
|
+
* warning at all: that is the pre-router behaviour of THIS branch, not of the
|
|
724
|
+
* dispatch path, where a router-OFF action's own `model` still moves a session.
|
|
724
725
|
*/
|
|
725
726
|
function planFailoverSwitch(input: RoutePlanInput, resolution: ModelRouterResolution, target: string | undefined): RoutePlanVerdict {
|
|
726
727
|
const warnings: string[] = [];
|
|
@@ -810,10 +811,10 @@ export function planRoute(input: RoutePlanInput): RoutePlanVerdict {
|
|
|
810
811
|
/** Router OFF only: the thread's pre-router `model` pin, which OPENS a session and never switches one. */
|
|
811
812
|
let pin: string | undefined;
|
|
812
813
|
if (!resolution.on) {
|
|
813
|
-
// ROUTER OFF — this
|
|
814
|
-
// passed ONE thing to the worker opener — the thread's
|
|
815
|
-
// most threads — and never
|
|
816
|
-
//
|
|
814
|
+
// ROUTER OFF — this planner seeds no base and applies no candidate-dependent
|
|
815
|
+
// guard. The pre-router code passed ONE thing to the worker opener — the thread's
|
|
816
|
+
// `model` pin, undefined for most threads — and never used that pin to move a
|
|
817
|
+
// live session. This branch preserves that open-only planning rule:
|
|
817
818
|
//
|
|
818
819
|
// · nothing is SEEDED and nothing is PERSISTED: `baseModel`/`baseEffort` stay
|
|
819
820
|
// undefined, so the caller writes no base onto the record. An earlier version
|
|
@@ -924,9 +925,9 @@ export function planRoute(input: RoutePlanInput): RoutePlanVerdict {
|
|
|
924
925
|
let model = explicit ?? baseModel ?? pin;
|
|
925
926
|
const openOnly = model !== undefined && model === pin && explicit === undefined;
|
|
926
927
|
|
|
927
|
-
// GUARD 1 — list membership. Router ON only; with the router off
|
|
928
|
-
//
|
|
929
|
-
//
|
|
928
|
+
// GUARD 1 — list membership. Router ON only; with the router off this guard does
|
|
929
|
+
// not inspect or reject the raw `model` argument. Validated on the RESOLVED model,
|
|
930
|
+
// but only an EXPLICIT one can trip it: a new thread's base is a
|
|
930
931
|
// listed candidate by construction (D48), and an existing thread's was re-seeded
|
|
931
932
|
// above if it was not — which is what keeps a dispatch that omitted `model` from
|
|
932
933
|
// ever landing here.
|
|
@@ -942,8 +943,8 @@ export function planRoute(input: RoutePlanInput): RoutePlanVerdict {
|
|
|
942
943
|
}
|
|
943
944
|
|
|
944
945
|
// GUARD 5 — CONTEXT WINDOW, and it runs BEFORE the effort guards on purpose: the
|
|
945
|
-
// model this action
|
|
946
|
-
// it, or the judgement is about a model that will be substituted away. Never a
|
|
946
|
+
// model the planner routes this action to must be settled before any level is judged
|
|
947
|
+
// against it, or the judgement is about a model that will be substituted away. Never a
|
|
947
948
|
// hard block, and router ON only: with the router off there is no candidate list
|
|
948
949
|
// to fall back to, and the pre-router behaviour is no check at all.
|
|
949
950
|
let substitutedFrom: string | undefined;
|
|
@@ -994,17 +995,17 @@ export function planRoute(input: RoutePlanInput): RoutePlanVerdict {
|
|
|
994
995
|
}
|
|
995
996
|
}
|
|
996
997
|
|
|
997
|
-
// ---- the EFFORT, for the model
|
|
998
|
+
// ---- the EFFORT, for the model the planner routes to (THE ONE RULE, effort half)
|
|
998
999
|
//
|
|
999
|
-
// The model is final
|
|
1000
|
-
// level can be settled against THAT model and nothing else:
|
|
1000
|
+
// The routed model is final inside planRoute (guard 1 accepted it, guard 5 may have
|
|
1001
|
+
// moved it), so the level can be settled against THAT model and nothing else:
|
|
1001
1002
|
//
|
|
1002
1003
|
// · an EXPLICIT `effort` is judged against it, hard — the caller named a level and
|
|
1003
1004
|
// is entitled to be told it does not exist there rather than silently clamped;
|
|
1004
1005
|
// · a level INHERITED from the thread's base applies only while the base model is
|
|
1005
|
-
// the model
|
|
1006
|
-
// substitution — the inherited level is DROPPED and re-derived for the
|
|
1007
|
-
//
|
|
1006
|
+
// the routed model. The moment they differ — an explicit `model`, or a window
|
|
1007
|
+
// substitution — the inherited level is DROPPED and re-derived for the routed
|
|
1008
|
+
// model (its lowest MEASURED level, or nothing when it has none). Carrying it over
|
|
1008
1009
|
// was BG14: an action explicitly routed to another model inherited a budget
|
|
1009
1010
|
// derived for the base, and was warned about — or, with allowUnmeasuredEffort
|
|
1010
1011
|
// false, REJECTED — for a level nobody had asked for.
|
package/extension/state.ts
CHANGED
|
@@ -36,26 +36,25 @@ export interface ThreadRecord {
|
|
|
36
36
|
status: "idle" | "running";
|
|
37
37
|
/**
|
|
38
38
|
* PRE-ROUTER pin: "provider/id" passed as `model` when the thread was created
|
|
39
|
-
* WITH THE ROUTER OFF. It
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* `model` argument routes ONE action and is deliberately NOT recorded here; see
|
|
43
|
-
* `baseModel`.
|
|
39
|
+
* WITH THE ROUTER OFF. It names what a NEW worker session opens on and never
|
|
40
|
+
* instructs a live one to switch. With the router ON a `model` argument routes
|
|
41
|
+
* ONE action and is deliberately NOT recorded here; see `baseModel`.
|
|
44
42
|
*/
|
|
45
43
|
model?: string;
|
|
46
44
|
/**
|
|
47
|
-
* The thread's DEFAULT model, canonical "provider/id" —
|
|
48
|
-
* omits `model
|
|
45
|
+
* The thread's DEFAULT plan model, canonical "provider/id" — the target when a
|
|
46
|
+
* dispatch omits `model`. Written ONLY while the router is on (with the router off
|
|
49
47
|
* nothing is seeded or persisted), and always one of the effective candidates:
|
|
50
48
|
* a base that is absent or has fallen off the list is re-seeded on the next
|
|
51
49
|
* dispatch (route.ts's THE ONE RULE). DISTINCT from whatever a single action was
|
|
52
|
-
* routed to: a routed action never becomes the thread's base.
|
|
50
|
+
* routed to: a routed action never becomes the thread's base. A live failover may
|
|
51
|
+
* temporarily override it without changing this record. Absent = unknown.
|
|
53
52
|
*/
|
|
54
53
|
baseModel?: string;
|
|
55
54
|
/**
|
|
56
55
|
* The thread's DEFAULT effort level, derived for `baseModel` and valid only for
|
|
57
|
-
* it: a dispatch whose model differs re-derives the level for the model
|
|
58
|
-
*
|
|
56
|
+
* it: a dispatch whose model differs re-derives the level for the model it
|
|
57
|
+
* routes to. Absent = unknown ⇒ the worker session's own opening level.
|
|
59
58
|
*
|
|
60
59
|
* The type is a claim about what slate WROTE, not a guarantee about what it reads
|
|
61
60
|
* back: this record is restored from an unversioned, hand-editable snapshot, so the
|
|
@@ -469,11 +468,13 @@ export interface ContextBudgetObject {
|
|
|
469
468
|
/**
|
|
470
469
|
* Action-level model router (D4/D53). `models` is the CLOSED list of models the
|
|
471
470
|
* router may route an action to, in canonical "provider/id" form; empty or
|
|
472
|
-
* absent means the router is OFF
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
471
|
+
* absent means the router is OFF, so no candidate list or router-owned base,
|
|
472
|
+
* window, billing or substitution mechanism applies. Per-action arguments and
|
|
473
|
+
* pre-existing failover remain outside that feature-off statement.
|
|
474
|
+
* `allowUnmeasuredEffort` (default TRUE) decides what the dispatch path does
|
|
475
|
+
* with an effort level that is ladder-valid but has no capability evidence —
|
|
476
|
+
* an evidence gap is advisory, not a prohibition. Validated by
|
|
477
|
+
* sanitizeRouterConfig in model-router.ts.
|
|
477
478
|
*/
|
|
478
479
|
export interface RouterConfig {
|
|
479
480
|
models?: string[];
|
package/extension/threads.ts
CHANGED
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
* this, `model` was accepted only when a thread was created and silently ignored
|
|
16
16
|
* on every later dispatch. The rules the dispatch path implements:
|
|
17
17
|
*
|
|
18
|
-
* - RESOLUTION: the
|
|
19
|
-
* base model.
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
18
|
+
* - RESOLUTION: the planner's model is the explicit argument, else the thread's
|
|
19
|
+
* base model. Its effort is the explicit argument, else the thread's base effort
|
|
20
|
+
* WHEN THE BASE MODEL IS THE ROUTE TARGET, else a level derived for that target —
|
|
21
|
+
* a level never travels between planner targets (route.ts's THE ONE RULE, effort
|
|
22
|
+
* half). The GUARDS ALWAYS VALIDATE THE RESOLVED PAIR,
|
|
23
23
|
* never only the explicit arguments — an omitted argument must not escape
|
|
24
24
|
* validation.
|
|
25
25
|
*
|
|
@@ -36,27 +36,27 @@
|
|
|
36
36
|
* once, in route.ts's header: with the router ON a thread's base is ALWAYS a
|
|
37
37
|
* listed candidate.
|
|
38
38
|
*
|
|
39
|
-
* - ROUTER OFF
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* level by the next
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
39
|
+
* - ROUTER OFF MAKES ROUTER-OWNED MECHANISMS INERT. No list guard, window guard,
|
|
40
|
+
* billing notice, seeded or persisted base, or consultation of the
|
|
41
|
+
* orchestrator's tracked model: a model-less plan targets the thread's
|
|
42
|
+
* creation-time PIN — which only opens a NEW worker session (`openOnly`) — and
|
|
43
|
+
* nothing at all when there is no pin, so a NEW session opens on the host's
|
|
44
|
+
* current model (worker.ts) and a reused one is reverted to the model it opened
|
|
45
|
+
* on — outside cases M1, M3 and M7 of the doc named below (BG16, BG24).
|
|
46
|
+
* The `model` and `effort` arguments still apply per action: an explicit
|
|
47
|
+
* `effort` is validated against the plan target's ladder whenever that ladder is
|
|
48
|
+
* readable, and a level set by one action is put back to the session's opening
|
|
49
|
+
* level by the next. The MODEL an explicit argument routes to is reverted the same
|
|
50
|
+
* way (BG22), outside those same cases; the failover hold predates the router, and
|
|
51
|
+
* an OFF plan does not undo it. docs/model-routing.md's "Known cases where the
|
|
52
|
+
* model or level differs" collects the ones known to bite, this file's included.
|
|
53
|
+
* It does not claim to be exhaustive, and this code is the authority.
|
|
53
54
|
*
|
|
54
|
-
* An earlier iteration seeded the base from the tracker here and persisted it
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* model
|
|
58
|
-
*
|
|
59
|
-
* episode compressor's last-resort rung.
|
|
55
|
+
* An earlier iteration seeded the base from the tracker here and persisted it.
|
|
56
|
+
* That undid failovers on reused sessions, could strand a thread once its
|
|
57
|
+
* tracked model lost credentials, and stopped a restarted thread following the
|
|
58
|
+
* host's model. The tracker survives only as the episode compressor's
|
|
59
|
+
* last-resort rung.
|
|
60
60
|
*
|
|
61
61
|
* - VALIDATION HAPPENS TWICE. Early, in dispatch(), before any state mutation
|
|
62
62
|
* or session work, so a bad pick is a TOOL ERROR rather than a billed failed
|
|
@@ -266,13 +266,12 @@ export class ThreadManager {
|
|
|
266
266
|
// This session's MEMOIZED model-router resolution (model-router.ts's
|
|
267
267
|
// createModelRouterResolver), bound by value for the same reason as the
|
|
268
268
|
// resolver above: a manager orphaned by a session swap must keep answering
|
|
269
|
-
// with its own session's frozen candidate list. Defaults to the OFF
|
|
270
|
-
// resolution, which
|
|
269
|
+
// with its own session's frozen candidate list. Defaults to the shared OFF
|
|
270
|
+
// resolution, which supplies no candidates or router-owned base.
|
|
271
271
|
private resolveRouter: () => ModelRouterResolution = () => ROUTER_OFF,
|
|
272
272
|
// The orchestrator's base model, EXCLUDING failover fallbacks (base-model.ts).
|
|
273
|
-
// Consulted only
|
|
274
|
-
//
|
|
275
|
-
// behaviour, where the worker opens on the host session's current model.
|
|
273
|
+
// Consulted only for the episode compressor's last-resort model rung; route
|
|
274
|
+
// planning never seeds a worker-thread base from this tracker.
|
|
276
275
|
private baseModelTracker?: BaseModelTracker,
|
|
277
276
|
) {
|
|
278
277
|
this.semaphore = new Semaphore(config.maxConcurrent ?? 4); // default rationale: docs/design-principles.md §5 repo-local note
|
|
@@ -412,11 +411,11 @@ export class ThreadManager {
|
|
|
412
411
|
/**
|
|
413
412
|
* This session's frozen router resolution, defensively.
|
|
414
413
|
*
|
|
415
|
-
* A THROWING resolver, or one that hands back a malformed object,
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
* usableResolution, so the planner and this module cannot
|
|
419
|
-
* usable resolution is.
|
|
414
|
+
* A THROWING resolver, or one that hands back a malformed object, falls back to
|
|
415
|
+
* the shared OFF resolution. That answer supplies no candidates, so list, window,
|
|
416
|
+
* billing and substitution paths cannot walk an unreadable shape. The SHAPE check
|
|
417
|
+
* itself is route.ts's usableResolution, so the planner and this module cannot
|
|
418
|
+
* disagree about what a usable resolution is.
|
|
420
419
|
*/
|
|
421
420
|
private routerResolution(): ModelRouterResolution {
|
|
422
421
|
try {
|
|
@@ -525,11 +524,12 @@ export class ThreadManager {
|
|
|
525
524
|
): RoutePlanInput {
|
|
526
525
|
const resolution = this.routerResolution();
|
|
527
526
|
// CQ17: the window guard is router-ON only, so with the router OFF pi's compaction
|
|
528
|
-
// settings are NOT READ and no predicate is built.
|
|
529
|
-
//
|
|
527
|
+
// settings are NOT READ and no predicate is built. The router-OFF path must add no
|
|
528
|
+
// router-owned SIDE EFFECT, not merely no router-owned decision: the read is a
|
|
530
529
|
// lock-protected disk read, and performing it for a guard that cannot fire is the
|
|
531
|
-
// kind of side effect
|
|
532
|
-
//
|
|
530
|
+
// kind of side effect that rule exists to forbid. (Memoized per manager, so this
|
|
531
|
+
// was never a cost question.) Per-action argument and effort paths are unaffected
|
|
532
|
+
// by it. The router-ON path is untouched, including the
|
|
533
533
|
// `enabled: true` forcing below, which is load-bearing.
|
|
534
534
|
const settings = resolution.on ? this.compactionSettings(ctx) : undefined;
|
|
535
535
|
return {
|
|
@@ -650,7 +650,7 @@ export class ThreadManager {
|
|
|
650
650
|
);
|
|
651
651
|
};
|
|
652
652
|
abortIfBlocked();
|
|
653
|
-
// WHICH MODEL this action
|
|
653
|
+
// WHICH MODEL this action starts on is decided by route.ts's pure `decideModelSwitch`
|
|
654
654
|
// (the plan's model, else the session's opening model as a REVERT, standing down
|
|
655
655
|
// while a failover holds it). Everything it needs is passed explicitly, so the
|
|
656
656
|
// decision — the part with the interesting cases — is checkable without a
|
|
@@ -1169,11 +1169,11 @@ export class ThreadManager {
|
|
|
1169
1169
|
workerEffortUnmeasured: effortIsAsPlanned,
|
|
1170
1170
|
workerEffortJudgedFor: plan?.effortJudgedFor,
|
|
1171
1171
|
configuredModel: this.config.episodeModel,
|
|
1172
|
-
// The compressor's LAST-RESORT rung: the ORCHESTRATOR's base model
|
|
1173
|
-
//
|
|
1174
|
-
//
|
|
1175
|
-
// fire, and a project with no episodeModel and no usable Sonnet
|
|
1176
|
-
// straight to the uncompressed fallback.
|
|
1172
|
+
// The compressor's LAST-RESORT rung: the ORCHESTRATOR's base model from the
|
|
1173
|
+
// tracker (failover fallbacks excluded, spec-validated). Route planning no
|
|
1174
|
+
// longer consumes this tracker. Without it the compressor pin's bottom rung
|
|
1175
|
+
// could never fire, and a project with no episodeModel and no usable Sonnet
|
|
1176
|
+
// would drop straight to the uncompressed fallback.
|
|
1177
1177
|
orchestratorBaseModel: this.trackedBaseModel(),
|
|
1178
1178
|
modelFailover: this.config.modelFailover,
|
|
1179
1179
|
signal: signal?.aborted ? undefined : signal,
|
package/extension/tools.ts
CHANGED
|
@@ -14,21 +14,21 @@ export function registerSlateTools(pi: ExtensionAPI, store: SlateStore, getManag
|
|
|
14
14
|
name: "thread",
|
|
15
15
|
label: "Thread",
|
|
16
16
|
description: [
|
|
17
|
-
"Dispatch ONE bounded action to a persistent worker thread
|
|
18
|
-
"(a compressed, structured record of
|
|
19
|
-
"Omit `thread`
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
17
|
+
"Dispatch ONE bounded action to a persistent worker thread; receive an episode",
|
|
18
|
+
"(a compressed, structured record of its work).",
|
|
19
|
+
"Omit `thread` and give a short `name` to create one; pass its id to continue with all prior-action context.",
|
|
20
|
+
"Use `context` to inject episodes by id from any thread.",
|
|
21
|
+
"Each thread is serial; use DIFFERENT threads in one message for parallel work.",
|
|
22
|
+
"`model` (\"provider/id\") and `effort` (pi thinking level) route THIS ACTION ONLY on new or continued threads.",
|
|
23
|
+
"With routing on, omission plans for the thread's base pair. With routing off, an omitted model",
|
|
24
|
+
"plans for its pre-router pin; no pin gives no target. Omitted effort resolves no level. The session",
|
|
25
|
+
"then returns to what it opened on. See docs/model-routing.md § Known cases where the model or level",
|
|
26
|
+
"differs for what can run instead.",
|
|
27
|
+
"`tools` applies only when creating a thread.",
|
|
28
|
+
"Slate rejects a level the model does not offer and, when configured, a model outside the routable list.",
|
|
29
|
+
"⚠ advisory notices before the episode report evidence gaps, cost cliffs, or window substitution.",
|
|
30
|
+
"STATUS: FAILED means the action failed — read the episode and adapt.",
|
|
31
|
+
"File-changing tasks require the track workflow's pre-implementation gates first.",
|
|
32
32
|
].join(" "),
|
|
33
33
|
promptSnippet: "Dispatch one bounded action to a persistent worker thread; returns a compressed episode",
|
|
34
34
|
promptGuidelines: [
|
|
@@ -43,14 +43,21 @@ export function registerSlateTools(pi: ExtensionAPI, store: SlateStore, getManag
|
|
|
43
43
|
Type.Array(Type.String(), { description: "Episode ids to inject as context (e.g. [\"t1.e2\"])" }),
|
|
44
44
|
),
|
|
45
45
|
model: Type.Optional(
|
|
46
|
-
Type.String({
|
|
46
|
+
Type.String({
|
|
47
|
+
description:
|
|
48
|
+
"Worker model \"provider/id\" for THIS action. Omit it to plan for the thread's base model with " +
|
|
49
|
+
"routing on, or for its pre-router pin with routing off. With no pin the worker session returns to " +
|
|
50
|
+
"the model it opened on. Cases M1, M3 and M7 in docs/model-routing.md § Known cases where the model " +
|
|
51
|
+
"or level differs are when it does not.",
|
|
52
|
+
}),
|
|
47
53
|
),
|
|
48
54
|
effort: Type.Optional(
|
|
49
55
|
Type.String({
|
|
50
56
|
description:
|
|
51
|
-
"Thinking level for THIS action: off, minimal, low, medium, high, xhigh or max " +
|
|
52
|
-
"
|
|
53
|
-
"
|
|
57
|
+
"Thinking level for THIS action: off, minimal, low, medium, high, xhigh or max. " +
|
|
58
|
+
"Slate refuses a level the routed model's known ladder lacks, outside case E2 in " +
|
|
59
|
+
"docs/model-routing.md. Omit it to plan for the thread's default or a measured level with routing " +
|
|
60
|
+
"on, or for the level the worker session opened on with routing off.",
|
|
54
61
|
}),
|
|
55
62
|
),
|
|
56
63
|
tools: Type.Optional(Type.Array(Type.String(), { description: "Worker tool allowlist (new threads only)" })),
|
|
@@ -131,12 +138,11 @@ export function registerSlateTools(pi: ExtensionAPI, store: SlateStore, getManag
|
|
|
131
138
|
name: "threads",
|
|
132
139
|
label: "Threads",
|
|
133
140
|
description:
|
|
134
|
-
"List
|
|
135
|
-
"base=<
|
|
136
|
-
"
|
|
137
|
-
"
|
|
138
|
-
"
|
|
139
|
-
"Use this to decide whether to continue an existing thread or create a new one.",
|
|
141
|
+
"List worker threads, their status, episodes, activity, and models. " +
|
|
142
|
+
"base=<model>@<effort>? is the nominal plan target when `model` is omitted. " +
|
|
143
|
+
"The ? marks effort as provisional because Slate re-checks and may re-derive it. " +
|
|
144
|
+
"last=<model>@<effort> records the last action. live=<model> (failover) shows a held fallback " +
|
|
145
|
+
"that currently overrides the nominal base.",
|
|
140
146
|
promptSnippet: "List worker threads and their episodes",
|
|
141
147
|
parameters: Type.Object({}),
|
|
142
148
|
async execute() {
|
|
@@ -147,15 +153,16 @@ export function registerSlateTools(pi: ExtensionAPI, store: SlateStore, getManag
|
|
|
147
153
|
const lines = threads.map((t) => {
|
|
148
154
|
const episodes = t.episodeIds.length > 0 ? t.episodeIds.join(", ") : "(none)";
|
|
149
155
|
const marks: string[] = [];
|
|
150
|
-
// The thread's
|
|
151
|
-
// (?? t.model:
|
|
152
|
-
//
|
|
153
|
-
//
|
|
156
|
+
// The thread's NOMINAL model — the planner target for a dispatch that omits
|
|
157
|
+
// `model` (?? t.model: an older thread carries only the pre-router pin).
|
|
158
|
+
// Absent means the plan has no model: a NEW session then opens on the host
|
|
159
|
+
// model and a reused one is reverted to the model it opened on. A separate
|
|
160
|
+
// `live=` marker below overrides this display while failover holds the session.
|
|
154
161
|
const base = t.baseModel ?? t.model;
|
|
155
|
-
// CQ19: the MODEL half is authoritative
|
|
156
|
-
// is
|
|
157
|
-
// STORED default: every dispatch re-checks it against the
|
|
158
|
-
// capability data and silently derives a fresh one if the
|
|
162
|
+
// CQ19: the MODEL half is authoritative as nominal planning state (an unroutable
|
|
163
|
+
// base is re-seeded, so this is what an omitted `model` resolves to), but the
|
|
164
|
+
// LEVEL half is only a STORED default: every dispatch re-checks it against the
|
|
165
|
+
// model's current capability data and silently derives a fresh one if the table has moved
|
|
159
166
|
// under it (BG23). Reporting it bare would present a value that may not survive
|
|
160
167
|
// contact with the next action as fact — the `?` says so, and the tool description
|
|
161
168
|
// says what it means. `last=` below carries no such caveat: that one is what ran.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ytdb-slate",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Slate: thread-weaving orchestration for the pi coding agent — bounded worker actions, compressed episodes, mandatory research/design/adversarial/track review workflow, configurable model failover, and orchestrator context-budget handoff.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|