@sema-agent/core 7.28.0 → 7.29.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 +114 -1
- package/dist/brain/anthropic.js +4 -2
- package/dist/brain/open-responses.js +4 -3
- package/dist/brain/openai.js +2 -1
- package/dist/brain/stream-engine.js +6 -0
- package/dist/brain/stream-shared.d.ts +8 -0
- package/dist/brain/stream-shared.js +8 -0
- package/dist/core/engine-notice.d.ts +7 -0
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +2 -0
- package/dist/core/message-utils.d.ts +13 -0
- package/dist/core/message-utils.js +6 -0
- package/dist/core/runner/assemble-result.js +3 -1
- package/dist/core/runner/contracts.d.ts +5 -0
- package/dist/core/runner/initial-run-state.js +1 -1
- package/dist/core/runner/response-model-seat.d.ts +16 -0
- package/dist/core/runner/response-model-seat.js +27 -0
- package/dist/core/runner/run-harness-handlers.js +8 -1
- package/dist/core/runner/runtask.js +1 -1
- package/dist/core/side-query.d.ts +6 -5
- package/dist/core/side-query.js +3 -1
- package/dist/core/task-event.d.ts +8 -0
- package/dist/core/task-result.d.ts +13 -3
- package/package.json +1 -1
- package/wire-changes/{core@7.28.0.json → core@7.28.1.json} +3 -3
- package/wire-changes/core@7.29.0.json +185 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,119 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 7.
|
|
3
|
+
## 7.29.0 — 2026-09-23
|
|
4
|
+
|
|
5
|
+
### Added — the provider's own model name reaches the user face (#1029)
|
|
6
|
+
|
|
7
|
+
An assistant message has always carried `model`: the **engine-selected** catalogue id the request was
|
|
8
|
+
sent with. What the PROVIDER answered with — an alias resolved gateway-side, a pool member, a
|
|
9
|
+
substitution — was read on one of three wire adapters and reached no output face at all. It now has its
|
|
10
|
+
own seat beside `model`, and `model` is untouched.
|
|
11
|
+
|
|
12
|
+
- **All three adapters fill `AssistantMessage.responseModel`**, through one shared admission rule
|
|
13
|
+
(`noteResponseModel`): openai-completions reads `chunk.model` (the `OAChunk` type gains the field,
|
|
14
|
+
and the read sits ahead of the `choices` guard so a usage-only frame still names the model),
|
|
15
|
+
anthropic reads `message_start.message.model` beside the usage it already took, and Open Responses —
|
|
16
|
+
which already read `response.created` — now reads the `response.completed` / `response.incomplete` /
|
|
17
|
+
`response.failed` envelopes too. The last frame that NAMES a model decides; a frame naming the
|
|
18
|
+
requested id RETRACTS an earlier substitution; a frame naming nothing is not an update. The engine's
|
|
19
|
+
own terminal shell (connect failure, retries exhausted, abort) carries whatever the attempt observed.
|
|
20
|
+
- **Two new wire keys, both optional and both additive**: `turn_end.responseModel` (the turn's last
|
|
21
|
+
assistant message) and `TaskResult.responseModel` (the run's final one). **Present only when the
|
|
22
|
+
provider named a model that DIFFERS from the id the request carried.** Absence is ONE reading — "it
|
|
23
|
+
named the requested id" and "it named nothing" are not distinguished — which is also all the
|
|
24
|
+
reference implementation's own mismatch latch keeps. Never present on the engine's synthetic
|
|
25
|
+
messages. `SideQueryResult.responseModel` now admits on the same terms as those two.
|
|
26
|
+
- **One new operator notice**: `EngineNotice.code === "route.response_model_mismatch"`, audience
|
|
27
|
+
`operator`, `detail: { requested, reported, sessionId, runId }`, fired ONCE PER LEG PER
|
|
28
|
+
(requested, reported) pair. Advisory, never a refusal — an alias or a pool looks exactly like this.
|
|
29
|
+
`ENGINE_NOTICE_CODES` gains that one member.
|
|
30
|
+
- **Consumer flips.** No existing key changes meaning and nothing is removed (`handoff:diff v7.28.1` =
|
|
31
|
+
+0 / −0 exports). A consumer that renders "served by X" should prefer `responseModel` when present
|
|
32
|
+
and `model` when not; a consumer that keys anything on `model` must NOT switch to the new key — that
|
|
33
|
+
field is what a resume re-seeds from, what session import validates, and where the synthetic-record
|
|
34
|
+
sentinel lives. A consumer with a closed list of notice codes gains one row. **@cli L-507** (the
|
|
35
|
+
"served by" line and the notice row) and **@client-core** (the frame/result projection) are the two
|
|
36
|
+
named consumers.
|
|
37
|
+
- The release live gate's PASSED line now ends in `served=…`, read off `TaskResult.responseModel`
|
|
38
|
+
through a sidecar the live suite writes — the engine's own seat, not a second question to the provider.
|
|
39
|
+
|
|
40
|
+
### The release post's dist diff is read off the published tarballs (#1031; repository tooling — `scripts/` and `test/` only, nothing in the tarball; +0 exports)
|
|
41
|
+
|
|
42
|
+
`ship-post-lint`'s leg B answered "what changed for consumers?" by rebuilding two commits with
|
|
43
|
+
`tsc -p tsconfig.build.json` and diffing the two `dist` trees. That is a second implementation of the
|
|
44
|
+
package build, and it had drifted: for 7.28.1 it reported **10** changed files where the two tarballs
|
|
45
|
+
consumers installed differ in **28**. Two measured causes, neither patchable in a way that would stop
|
|
46
|
+
the next drift — the rebuild ran ONE tsc pass, and `tsconfig.build.json` sets `removeComments: true`,
|
|
47
|
+
which strips declaration output too, so every published `.d.ts` reached the comparison already
|
|
48
|
+
comment-free and the 19 files that differ in JSDoc alone compared equal; and the packaged-extras fold
|
|
49
|
+
skipped `files` entries that are DIRECTORIES, leaving `wire-changes/` (which changes every release by
|
|
50
|
+
construction) outside the comparison, with `package.json` outside it as well.
|
|
51
|
+
|
|
52
|
+
- **Leg B compares artifacts.** The previous release is fetched from the registry (`npm pack <spec>`)
|
|
53
|
+
and checked against the shasum the registry publishes for it; the release being cut is produced by
|
|
54
|
+
`npm pack` on a scratch checkout of the RC, which runs the package's own `prepare` and therefore the
|
|
55
|
+
package's own build. Measured: packing `v7.28.1` this way reproduces the published tarball's shasum
|
|
56
|
+
`0a04a998…` exactly.
|
|
57
|
+
- **Two readings, and the filter is stated.** RAW (every byte — the publisher's self-check) and
|
|
58
|
+
COMMENTS STRIPPED (the consumer's reading: what can change behaviour or types on a re-pin). Only
|
|
59
|
+
comments are removed, only from the files TypeScript parses, identified by the PARSER — whitespace is
|
|
60
|
+
not normalised, nothing is re-printed, and `package.json`, the markdown and every JSON manifest are
|
|
61
|
+
compared raw in both. The gap between the two readings is reported as its own list.
|
|
62
|
+
- **A missing artifact is a refusal, never a delta.** An unreachable registry, a tarball whose bytes do
|
|
63
|
+
not match its published checksum, or a tree that will not pack ⇒ `[gate-could-not-judge]` and exit 2,
|
|
64
|
+
with no numbers printed. There is deliberately no fallback to a local rebuild: that fallback is the
|
|
65
|
+
defect. `--tree-fingerprint` anchors the tarball for the same reason.
|
|
66
|
+
- **A release that ships no new source must say why it exists.** When the RC publishes the same `src`
|
|
67
|
+
tree as the previous release — 7.28.1's shape — the post must carry a line beginning
|
|
68
|
+
`Renumber reason:` / `补版原因:`. No delta can answer "why is there a second number for the same
|
|
69
|
+
code?", so the post answers it in words.
|
|
70
|
+
- **`consumersHint` no longer counts bookkeeping as consumption.** Files whose SUBJECT is this
|
|
71
|
+
package's surface (`docs/core-consumption/`, `docs/wire-consumption/`, `*LEDGER*.json`) name every
|
|
72
|
+
export by construction; the 7.28.1 manifest's only neighbour hit was one such row that itself said
|
|
73
|
+
`state: "skip"`. Those paths are now scanned SEPARATELY and reported in `consumersHint.note` —
|
|
74
|
+
"N name(s) matched ONLY in bookkeeping files", listed — so excluding is visible, not silent. A
|
|
75
|
+
neighbour's other docs, audits and examples are not excluded: the rule is about the file's subject,
|
|
76
|
+
not about it being documentation.
|
|
77
|
+
|
|
78
|
+
Who is hurt: nobody at runtime — `src/` is untouched and the public surface is unchanged. Release
|
|
79
|
+
engineering pays two prices: leg B now needs the registry (and refuses loudly without it), and the
|
|
80
|
+
numbers it prints for a release will be larger than the old ones, because they are the real ones.
|
|
81
|
+
`docs/KNOWN-LIMITS.md` KL-lint-a is closed by this. No version is minted for this change on its own:
|
|
82
|
+
`scripts/` and `test/` are not in `files`, so it is disclosed here and ships with the next package
|
|
83
|
+
release.
|
|
84
|
+
|
|
85
|
+
### Gates refuse instead of reporting a count they could not take (#1030; repository tooling — `scripts/` and `test/` only, nothing in the tarball; +0 exports)
|
|
86
|
+
|
|
87
|
+
A non-zero exit from `git` carried three different facts — the binary cannot run, this is not a
|
|
88
|
+
repository, or git answered "no match" — and roughly twenty call sites under `scripts/` and `test/`
|
|
89
|
+
folded them into one `catch`. Measured on this tree with a `git` that exits 1 for everything: 25 of 27
|
|
90
|
+
static gates read PASS, `gate:compensation` printed "0 live compensation(s) — the goal state" with an
|
|
91
|
+
unregistered marker sitting in `src/index.ts`, and the two reds both named the wrong cause (96 present
|
|
92
|
+
commits reported as "does not exist in this repo").
|
|
93
|
+
|
|
94
|
+
- **One seam**, `scripts/git-cli.mjs`: `gitAvailable()` proves the binary by resolving it on PATH and
|
|
95
|
+
running `--version` on that exact file; `gitRun()` then hands back git's own verdict, so a non-zero
|
|
96
|
+
status is data the caller reads. `repoRootOf()` / `NotARepoError` make "not a repository" an answer a
|
|
97
|
+
caller chooses out loud, and `requireGit()` / `requireRepo()` are a gate's first line.
|
|
98
|
+
- **A gate that cannot consult git exits 2 with one sentence and no counts.** `gate:all` prints
|
|
99
|
+
`REFUSED` for a child that says so and ends the run with "this run certifies nothing" — the marker,
|
|
100
|
+
not the bare exit code, because 2 already means "bad invocation" in two scripts.
|
|
101
|
+
- Two readings were being taken through commands that cannot express them, and now are not:
|
|
102
|
+
`gate:ledger` asks `rev-parse --verify --quiet` (real git answers 128 — not 1 — to
|
|
103
|
+
`cat-file -e <missing>^{commit}`, so an absent commit was indistinguishable from an unreadable
|
|
104
|
+
repository), and the blackbox gate's freshness pre-check establishes a missing snapshot with a
|
|
105
|
+
successful `ls-tree` rather than a failed `git show`.
|
|
106
|
+
- Same class, adjacent binaries: `car:gate` no longer prints PASS for a step killed by a signal, and
|
|
107
|
+
the `noUncheckedIndexedAccess` ratchet no longer parses a compiler's partial output as a full scan.
|
|
108
|
+
|
|
109
|
+
Who is hurt: nobody at runtime — `src/` is untouched and the public surface is unchanged. A checkout
|
|
110
|
+
whose git is broken now gets an exit 2 from four scripts where it previously got a number. No version was minted for this change on its own: the tarball's contents
|
|
111
|
+
(`dist/`, the export snapshot, `wire-changes/`) are byte-identical to 7.28.1, so it is disclosed here and ships
|
|
112
|
+
with the next package release.
|
|
113
|
+
|
|
114
|
+
## 7.28.1 — 2026-09-22
|
|
115
|
+
|
|
116
|
+
> **7.28.0 and 7.28.1 are the same tree** (tag `v7.28.0` = `e797e47f`, tag `v7.28.1` = `35c47a4d`; the only differences are this note, the version string, the criteria document's file name and the wire manifest's name). What happened: the first `npm publish` of 7.28.0 was cut off on the publisher's side, the registry held the upload as a *staged* version and answered every retry with `E409 Cannot publish over previously staged version`, so the release was renumbered — and then the registry finalized the staged 7.28.0 on its own about five minutes later. An earlier wording of this note ("7.28.0 was never published") was wrong and is corrected here; both versions are valid, `7.28.1` is `latest`.
|
|
4
117
|
|
|
5
118
|
### One shell lexer mints word FACTS; the thirteen ANSI-C letters fold; an unterminated quote is a parse failure (#994 S0; behaviour WIDENING ×1 and NARROWING ×1; +0 exports; @cli @test)
|
|
6
119
|
|
package/dist/brain/anthropic.js
CHANGED
|
@@ -2,7 +2,7 @@ import {} from "../internal/llm.js";
|
|
|
2
2
|
import { BrainError } from "./errors.js";
|
|
3
3
|
import { startsWithInputTooLongAnchor } from "./input-too-long.js";
|
|
4
4
|
import { DEGENERATE_MESSAGE, trimDegenerateTail } from "./repetition.js";
|
|
5
|
-
import { STREAM_TORN_WARNING, createRepetitionPoll, parseStreamedToolArgs } from "./stream-shared.js";
|
|
5
|
+
import { STREAM_TORN_WARNING, createRepetitionPoll, noteResponseModel, parseStreamedToolArgs } from "./stream-shared.js";
|
|
6
6
|
import { mintFallbackToolCallId } from "./tool-call-id.js";
|
|
7
7
|
import { emitBrainTelemetry, reportReasoningWireFacts } from "./status-sink.js";
|
|
8
8
|
import { errorResultMediaNote, IMAGE_OMITTED_NO_VISION, modelSupportsVision, sendableImages } from "./media-degrade.js";
|
|
@@ -380,7 +380,9 @@ export function createAnthropicBrain(config = {}) {
|
|
|
380
380
|
const handleEvent = (evt) => {
|
|
381
381
|
switch (evt.type) {
|
|
382
382
|
case "message_start": {
|
|
383
|
-
const
|
|
383
|
+
const msg = evt.message;
|
|
384
|
+
noteResponseModel(partial, msg?.model, model.id);
|
|
385
|
+
const u = msg?.usage;
|
|
384
386
|
if (u) {
|
|
385
387
|
usageRaw.input_tokens = u.input_tokens;
|
|
386
388
|
usageRaw.cache_read_input_tokens = u.cache_read_input_tokens;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {} from "../internal/llm.js";
|
|
2
2
|
import { BrainError } from "./errors.js";
|
|
3
3
|
import { DEGENERATE_MESSAGE, trimDegenerateTail } from "./repetition.js";
|
|
4
|
-
import { STREAM_TORN_WARNING, createRepetitionPoll, parseStreamedToolArgs } from "./stream-shared.js";
|
|
4
|
+
import { STREAM_TORN_WARNING, createRepetitionPoll, noteResponseModel, parseStreamedToolArgs } from "./stream-shared.js";
|
|
5
5
|
import { mintFallbackToolCallId } from "./tool-call-id.js";
|
|
6
6
|
import { emitBrainTelemetry } from "./status-sink.js";
|
|
7
7
|
import { errorResultMediaNote, IMAGE_OMITTED_NO_VISION, imagesOmittedNoVisionNote, modelSupportsVision, sendableImages } from "./media-degrade.js";
|
|
@@ -423,8 +423,7 @@ export function createOpenResponsesBrain(config = {}) {
|
|
|
423
423
|
if (env) {
|
|
424
424
|
if (env.id !== undefined)
|
|
425
425
|
responseId = env.id;
|
|
426
|
-
|
|
427
|
-
partial.responseModel = env.model;
|
|
426
|
+
noteResponseModel(partial, env.model, model.id);
|
|
428
427
|
auditEcho(env);
|
|
429
428
|
}
|
|
430
429
|
return;
|
|
@@ -552,9 +551,11 @@ export function createOpenResponsesBrain(config = {}) {
|
|
|
552
551
|
usageRaw = env.usage;
|
|
553
552
|
if (env?.id !== undefined)
|
|
554
553
|
responseId = env.id;
|
|
554
|
+
noteResponseModel(partial, env?.model, model.id);
|
|
555
555
|
return;
|
|
556
556
|
}
|
|
557
557
|
case "response.failed": {
|
|
558
|
+
noteResponseModel(partial, frame.response?.model, model.id);
|
|
558
559
|
const err = frame.response?.error;
|
|
559
560
|
throw new BrainError(responsesErrorCode(err?.code), `responses stream failed (${err?.code ?? "unknown"}): ${err?.message ?? ""}`, undefined, { apiError: true });
|
|
560
561
|
}
|
package/dist/brain/openai.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {} from "../internal/llm.js";
|
|
2
2
|
import { repairTextToolCalls } from "./tool-call-repair.js";
|
|
3
3
|
import { DEGENERATE_MESSAGE, trimDegenerateTail } from "./repetition.js";
|
|
4
|
-
import { STREAM_TORN_WARNING, createRepetitionPoll, parseStreamedToolArgs } from "./stream-shared.js";
|
|
4
|
+
import { STREAM_TORN_WARNING, createRepetitionPoll, noteResponseModel, parseStreamedToolArgs } from "./stream-shared.js";
|
|
5
5
|
import { mintFallbackToolCallId } from "./tool-call-id.js";
|
|
6
6
|
import { emitBrainTelemetry } from "./status-sink.js";
|
|
7
7
|
import { errorResultMediaNote, IMAGE_OMITTED_NO_VISION, imagesOmittedNoVisionNote, modelSupportsVision, sendableImages } from "./media-degrade.js";
|
|
@@ -464,6 +464,7 @@ export function createOpenAIBrain(config = {}) {
|
|
|
464
464
|
}
|
|
465
465
|
if (chunk.usage)
|
|
466
466
|
usageRaw = chunk.usage;
|
|
467
|
+
noteResponseModel(partial, chunk.model, model.id);
|
|
467
468
|
const choice = chunk.choices?.[0];
|
|
468
469
|
if (!choice)
|
|
469
470
|
return;
|
|
@@ -265,6 +265,7 @@ export function runStreamingBrain(args) {
|
|
|
265
265
|
let terminalRetryPhase = "recovered";
|
|
266
266
|
let terminalRetryDetail = "recovered after retrying";
|
|
267
267
|
let requestIdSeen;
|
|
268
|
+
let attemptPartial;
|
|
268
269
|
void run()
|
|
269
270
|
.catch((err) => {
|
|
270
271
|
const aborted = signal?.aborted === true || isAbortError(err);
|
|
@@ -290,6 +291,9 @@ export function runStreamingBrain(args) {
|
|
|
290
291
|
}
|
|
291
292
|
}
|
|
292
293
|
errorMsg.usageMissing = true;
|
|
294
|
+
const seenResponseModel = attemptPartial?.responseModel;
|
|
295
|
+
if (seenResponseModel !== undefined)
|
|
296
|
+
errorMsg.responseModel = seenResponseModel;
|
|
293
297
|
out.push({ type: "error", reason: aborted ? "aborted" : "error", error: errorMsg });
|
|
294
298
|
})
|
|
295
299
|
.finally(() => {
|
|
@@ -330,6 +334,7 @@ export function runStreamingBrain(args) {
|
|
|
330
334
|
let r;
|
|
331
335
|
let netErr;
|
|
332
336
|
requestIdSeen = undefined;
|
|
337
|
+
attemptPartial = undefined;
|
|
333
338
|
try {
|
|
334
339
|
r = await doFetch(req.url, { method: "POST", headers: req.headers, body: req.body, signal: cc.signal });
|
|
335
340
|
requestIdSeen = readProviderRequestId(r);
|
|
@@ -478,6 +483,7 @@ export function runStreamingBrain(args) {
|
|
|
478
483
|
}
|
|
479
484
|
}
|
|
480
485
|
const partial = emptyAssistant(model);
|
|
486
|
+
attemptPartial = partial;
|
|
481
487
|
if (!startEmitted) {
|
|
482
488
|
startEmitted = true;
|
|
483
489
|
out.push({ type: "start", partial: { ...partial } });
|
|
@@ -43,3 +43,11 @@ export interface RepetitionPoll {
|
|
|
43
43
|
* boot block doesn't log once per poll. Attached to the final message as `repetition`.
|
|
44
44
|
*/
|
|
45
45
|
export declare function createRepetitionPoll(): RepetitionPoll;
|
|
46
|
+
/** Record the provider's OWN name for the model that answered, on the shared partial (hence on the final
|
|
47
|
+
* message every lane spreads `...partial` into) — ONE rule for the three lanes' spellings (`chunk.model` /
|
|
48
|
+
* `message_start.message.model` / `response.model`), WRITE twin of `providerReportedModel`
|
|
49
|
+
* (`core/message-utils.ts`). The LAST frame that NAMES a model decides and the seat carries it only when it
|
|
50
|
+
* differs; naming nothing is not an update, naming the REQUESTED id RETRACTS an earlier substitution. */
|
|
51
|
+
export declare function noteResponseModel(partial: {
|
|
52
|
+
responseModel?: string;
|
|
53
|
+
}, reported: unknown, requestedId: string): void;
|
|
@@ -40,3 +40,11 @@ export function createRepetitionPoll() {
|
|
|
40
40
|
},
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
|
+
export function noteResponseModel(partial, reported, requestedId) {
|
|
44
|
+
if (typeof reported !== "string" || reported === "")
|
|
45
|
+
return;
|
|
46
|
+
if (reported === requestedId)
|
|
47
|
+
delete partial.responseModel;
|
|
48
|
+
else
|
|
49
|
+
partial.responseModel = reported;
|
|
50
|
+
}
|
|
@@ -222,6 +222,13 @@ export interface EngineNotice {
|
|
|
222
222
|
* occurrenceId }` (rendered list bounded, total always exact); the `occurrenceId` is minted at this
|
|
223
223
|
* announcement, independently of the `config.models_swapped` line's own, because the bounded render
|
|
224
224
|
* makes two swaps moving the same first entries identical. Dedup key: (code, occurrenceId).
|
|
225
|
+
* - `"route.response_model_mismatch"` — the provider answered a turn naming a model that is NOT the id
|
|
226
|
+
* the request was sent with (an alias, a pool or a gateway-side substitution looks exactly like this),
|
|
227
|
+
* so it is advisory, never a refusal: the engine keeps asking for the catalog's id and reports the
|
|
228
|
+
* served name beside it (`turn_end.responseModel` / `TaskResult.responseModel`). ONCE PER LEG PER
|
|
229
|
+
* (requested, reported) PAIR — a catalog fact, not a turn fact. Audience `operator`;
|
|
230
|
+
* `detail: { requested, reported, sessionId, runId }`. Absent ⇒ the provider echoed the requested id
|
|
231
|
+
* or named nothing — one reading, not two.
|
|
225
232
|
* - `"config.read_face_deployment_clamped"` (#237) — a deployment-wide `readFace: "open"` is not
|
|
226
233
|
* in force beside a read-only (verifier) mount: it clamps to "roots" without throwing
|
|
227
234
|
* (stricter-wins; the clamp verdict stands, only its occurrence was undisclosed). Announced
|
|
@@ -119,7 +119,7 @@ export type NoticeAudience = "user" | "operator";
|
|
|
119
119
|
* the union's catalog ({@link EngineNotice.code}). The groups below say only which family a row joins,
|
|
120
120
|
* so the catalog is not written a second time here and the two cannot drift.
|
|
121
121
|
*/
|
|
122
|
-
export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_swapped", "config.read_face_deployment_clamped", "config.write_face_swapped", "config.write_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.artifact_host_invalid", "config.task_root_not_canonical", "config.write_protection_target_view_absent", "config.write_protection_unresolved", "config.execution_env_capability_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.forward_sink_failed", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "classifier.fallback", "classifier.role_defaulted", "classifier.contract_broken", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "delegation.ask_unresolvable", "mcp.injection_dropped", "mcp.revocation_probe_failed", "mcp.server_redialed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "memory.restore_withheld", "memory.project_scope_absent", "route.fallback_to_primary", "route.classifier_unroutable", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "task.interrupt_unconsumed", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "skills.listing_truncated", "agents.listing_truncated", "tool_result.offload_put_failed"];
|
|
122
|
+
export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_swapped", "config.read_face_deployment_clamped", "config.write_face_swapped", "config.write_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.artifact_host_invalid", "config.task_root_not_canonical", "config.write_protection_target_view_absent", "config.write_protection_unresolved", "config.execution_env_capability_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.forward_sink_failed", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "classifier.fallback", "classifier.role_defaulted", "classifier.contract_broken", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "delegation.ask_unresolvable", "mcp.injection_dropped", "mcp.revocation_probe_failed", "mcp.server_redialed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "memory.restore_withheld", "memory.project_scope_absent", "route.fallback_to_primary", "route.classifier_unroutable", "route.base_url_changed_key_unchanged", "route.response_model_mismatch", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "task.interrupt_unconsumed", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "skills.listing_truncated", "agents.listing_truncated", "tool_result.offload_put_failed"];
|
|
123
123
|
/** A code this engine mints (see {@link ENGINE_NOTICE_CODES}). NOT the type of
|
|
124
124
|
* `EngineNotice.code`, which stays `string` — a host forwarding its own notices through the same
|
|
125
125
|
* sink is a supported shape, and narrowing that field would break it. */
|
|
@@ -157,6 +157,7 @@ export const ENGINE_NOTICE_CODES = [
|
|
|
157
157
|
"route.fallback_to_primary",
|
|
158
158
|
"route.classifier_unroutable",
|
|
159
159
|
"route.base_url_changed_key_unchanged",
|
|
160
|
+
"route.response_model_mismatch",
|
|
160
161
|
"task.user_steer_undrained",
|
|
161
162
|
"task.user_followup_undrained",
|
|
162
163
|
"steering.parked_input_blocked",
|
|
@@ -235,6 +236,7 @@ const NOTICE_AUDIENCE_TABLE = {
|
|
|
235
236
|
"route.fallback_to_primary": "operator",
|
|
236
237
|
"route.classifier_unroutable": "operator",
|
|
237
238
|
"route.base_url_changed_key_unchanged": "operator",
|
|
239
|
+
"route.response_model_mismatch": "operator",
|
|
238
240
|
"skills.listing_truncated": "operator",
|
|
239
241
|
"agents.listing_truncated": "operator",
|
|
240
242
|
"tool_result.offload_put_failed": "operator",
|
|
@@ -3,6 +3,19 @@ import type { ToolResultMessage } from "../internal/llm.js";
|
|
|
3
3
|
/** Read the (possibly absent) `role` off an AgentMessage defensively — the union doesn't expose it
|
|
4
4
|
* uniformly, so context passes narrow by role with a single cast in one place. */
|
|
5
5
|
export declare function messageRole(m: AgentMessage): string | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* The PROVIDER's own name for the model that answered this assistant message, when it differs from the
|
|
8
|
+
* ENGINE-SELECTED id the message is stamped with — the ONE read of `AssistantMessage.responseModel`, so
|
|
9
|
+
* the per-turn frame, the result face, the side-query projection and the mismatch notice admit it on the
|
|
10
|
+
* same terms rather than on four. The READ twin of `noteResponseModel` (`brain/stream-shared.ts`), which
|
|
11
|
+
* states the same law on the write side; this side re-tests rather than trusting, because a HOST's own
|
|
12
|
+
* Brain mints these messages too. `undefined` ⇒ the provider named the stamped id, named nothing, or
|
|
13
|
+
* named something unreadable — ONE reading, never three.
|
|
14
|
+
*/
|
|
15
|
+
export declare function providerReportedModel(m: {
|
|
16
|
+
readonly model?: unknown;
|
|
17
|
+
readonly responseModel?: unknown;
|
|
18
|
+
}): string | undefined;
|
|
6
19
|
/** True if the message is a tool-result message (shared by the context-edit and context-guard passes).
|
|
7
20
|
* A TYPE PREDICATE, not a bare boolean: `role: "toolResult"` belongs to exactly one member of the
|
|
8
21
|
* union, so callers that check it can read `.content`/`.toolName` and rebuild the message directly
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
export function messageRole(m) {
|
|
2
2
|
return m.role;
|
|
3
3
|
}
|
|
4
|
+
export function providerReportedModel(m) {
|
|
5
|
+
const reported = m.responseModel;
|
|
6
|
+
if (typeof reported !== "string" || reported === "")
|
|
7
|
+
return undefined;
|
|
8
|
+
return reported === m.model ? undefined : reported;
|
|
9
|
+
}
|
|
4
10
|
export function isToolResult(m) {
|
|
5
11
|
return messageRole(m) === "toolResult";
|
|
6
12
|
}
|
|
@@ -2,6 +2,7 @@ import { isDegenerateCutMessage } from "../../brain/terminal-cause.js";
|
|
|
2
2
|
import { extractErrorCode, stripErrorCodePrefix } from "../../brain/errors.js";
|
|
3
3
|
import { terminalProjection } from "./terminal-projection.js";
|
|
4
4
|
import { haltConsumed } from "./halt-attribution.js";
|
|
5
|
+
import { providerReportedModel } from "../message-utils.js";
|
|
5
6
|
import { isErrorLike, readMember } from "../thrown-value.js";
|
|
6
7
|
import { describeRejectedValue } from "../tool-search-setting.js";
|
|
7
8
|
const SALVAGE_ELIGIBLE_TERMINALS = new Set([
|
|
@@ -153,6 +154,7 @@ export function terminalCauseOf(final, stats, flags) {
|
|
|
153
154
|
export function assembleResult(spec, sessionId, final, stats, flags) {
|
|
154
155
|
const taskId = spec.taskId ?? sessionId;
|
|
155
156
|
const text = final ? assistantText(final) : "";
|
|
157
|
+
const responseModel = final === undefined ? undefined : providerReportedModel(final);
|
|
156
158
|
if (!flags.unpricedSpend) {
|
|
157
159
|
const compactionMicroUsd = stats.compactionMicroUsd ?? 0;
|
|
158
160
|
const nestedSubagentMicroUsd = stats.nested?.costMicroUsd ?? 0;
|
|
@@ -176,5 +178,5 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
|
|
|
176
178
|
if (flags.unpricedSpend)
|
|
177
179
|
delete publicStats.costMicroUsd;
|
|
178
180
|
const stampHaltedByUser = haltConsumed(flags.haltConsumption);
|
|
179
|
-
return { taskId, ...(flags.runId !== undefined ? { runId: flags.runId } : {}), sessionId, terminal, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, ...(apiFailure !== undefined ? { apiFailure } : {}), ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.editedFiles !== undefined && flags.editedFiles.length > 0 ? { editedFiles: flags.editedFiles } : {}), ...(flags.haltedOnUserRejection === true ? { haltedOnUserRejection: true } : {}), ...(stampHaltedByUser ? { haltedByUser: true } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: [...flags.remoteEnvFailures] } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), ...(flags.effectiveReadFace !== undefined ? { effectiveReadFace: flags.effectiveReadFace } : {}), ...(flags.effectiveWriteFace !== undefined ? { effectiveWriteFace: flags.effectiveWriteFace } : {}), ...(flags.effectiveReadDenyPatterns !== undefined && flags.effectiveReadDenyPatterns.length > 0 ? { effectiveReadDenyPatterns: flags.effectiveReadDenyPatterns } : {}), ...(flags.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: flags.effectiveMemoryScopes } : {}), ...(flags.effectiveReasoning !== undefined ? { effectiveReasoning: flags.effectiveReasoning } : {}), stats: publicStats };
|
|
181
|
+
return { taskId, ...(flags.runId !== undefined ? { runId: flags.runId } : {}), sessionId, terminal, ...(flags.model !== undefined ? { model: flags.model } : {}), ...(responseModel !== undefined ? { responseModel } : {}), result: result.trim(), salvagedOutput, ...(apiFailure !== undefined ? { apiFailure } : {}), ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.editedFiles !== undefined && flags.editedFiles.length > 0 ? { editedFiles: flags.editedFiles } : {}), ...(flags.haltedOnUserRejection === true ? { haltedOnUserRejection: true } : {}), ...(stampHaltedByUser ? { haltedByUser: true } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: [...flags.remoteEnvFailures] } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), ...(flags.effectiveReadFace !== undefined ? { effectiveReadFace: flags.effectiveReadFace } : {}), ...(flags.effectiveWriteFace !== undefined ? { effectiveWriteFace: flags.effectiveWriteFace } : {}), ...(flags.effectiveReadDenyPatterns !== undefined && flags.effectiveReadDenyPatterns.length > 0 ? { effectiveReadDenyPatterns: flags.effectiveReadDenyPatterns } : {}), ...(flags.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: flags.effectiveMemoryScopes } : {}), ...(flags.effectiveReasoning !== undefined ? { effectiveReasoning: flags.effectiveReasoning } : {}), stats: publicStats };
|
|
180
182
|
}
|
|
@@ -2340,6 +2340,10 @@ export interface RunState {
|
|
|
2340
2340
|
} | undefined;
|
|
2341
2341
|
turnUsageMissing: boolean;
|
|
2342
2342
|
turnStopReason: string | undefined;
|
|
2343
|
+
/** The PROVIDER-REPORTED model name of this turn's LAST assistant message, when it differed from the
|
|
2344
|
+
* requested id — surfaced on `turn_end.responseModel`, reset with the rest of the per-turn
|
|
2345
|
+
* bookkeeping. Same shape as `turnStopReason`: last-message-wins, absent by default. */
|
|
2346
|
+
turnResponseModel: string | undefined;
|
|
2343
2347
|
lastTurnHadToolCalls: boolean;
|
|
2344
2348
|
/** B15 三期使能:postToolBatch 的本回合聚集批(每回合边界消费并重置 — 回合语义,故归 turn)。 */
|
|
2345
2349
|
toolBatch: PostToolBatchCall[];
|
|
@@ -2608,6 +2612,7 @@ export interface HarnessHandlersDeps {
|
|
|
2608
2612
|
batchArgs: Map<string, unknown> | undefined;
|
|
2609
2613
|
runnerHooks: {
|
|
2610
2614
|
onError: RunnerDeps["onError"];
|
|
2615
|
+
onNotice: RunnerDeps["onNotice"];
|
|
2611
2616
|
};
|
|
2612
2617
|
/** The run's ONE isolation scope for the {@link RunInternals} observation sinks, minted by the
|
|
2613
2618
|
* identity-wiring lane (which calls the first of them) and shared with these handlers, so the
|
|
@@ -4,7 +4,7 @@ export function createRunState() {
|
|
|
4
4
|
degrade: { degradeToModel: undefined, degraded: undefined, outputErrorStreak: 0, outputInvalid: false, recordDegraded: () => { } },
|
|
5
5
|
limits: { turnsExceeded: false, budgetHit: undefined, budgetAxis: undefined, platformTerminal: undefined, outputRetryCap: 0, effectiveMaxTurns: undefined },
|
|
6
6
|
budget: { remainingMicroUsd: undefined, maxCostMicroUsd: undefined, remainingTokens: undefined, maxTokensWindow: undefined, overBudget: () => undefined, streamCancel: false, callOutputChars: 0, lastStreamBudgetCheck: 0, projectedOverBudget: () => undefined },
|
|
7
|
-
turn: { callStartAt: undefined, firstTokenAt: undefined, turnUsage: undefined, turnUsageMissing: false, turnStopReason: undefined, lastTurnHadToolCalls: false, toolBatch: [] },
|
|
7
|
+
turn: { callStartAt: undefined, firstTokenAt: undefined, turnUsage: undefined, turnUsageMissing: false, turnStopReason: undefined, turnResponseModel: undefined, lastTurnHadToolCalls: false, toolBatch: [] },
|
|
8
8
|
counters: { approachNoticesSent: 0, walltimeSyncBackstopFired: false, compactionFloor: 0, trimForceBackoff: false, repetitionCuts: 0, repetitionSpared: 0, repetitionEvents: [], REPETITION_EVENTS_CAP: 0, preemptIgnoredReported: false, wroteThisRun: false, finalVerifyInjections: 0, groundingSignalPreR9: false, groundingSignalPostR9: false, cadenceTurns: 0 },
|
|
9
9
|
attach: { attachmentsCfg: undefined, agentListingOn: false, skillsListingOn: false, attachState: undefined, dateState: undefined, instrProbe: undefined, instrState: undefined, sizeGuidelineState: undefined, attachmentsInjected: 0 },
|
|
10
10
|
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { EngineNotice } from "../engine-notice.js";
|
|
2
|
+
/** The seat the harness handlers hold for the run. */
|
|
3
|
+
export interface ResponseModelSeat {
|
|
4
|
+
/** Observe one assistant message: returns the provider-reported name when it carries one (the value the
|
|
5
|
+
* turn's frame reports), `undefined` otherwise, announcing a pair the leg has not announced yet. */
|
|
6
|
+
observe(message: {
|
|
7
|
+
readonly model?: unknown;
|
|
8
|
+
readonly responseModel?: unknown;
|
|
9
|
+
}): string | undefined;
|
|
10
|
+
}
|
|
11
|
+
/** Ids on the notice, so an operator can attribute it without joining on stream order. */
|
|
12
|
+
export interface ResponseModelSeatIds {
|
|
13
|
+
sessionId: string;
|
|
14
|
+
runId: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function createResponseModelSeat(onNotice: ((notice: EngineNotice) => void) | undefined, ids: ResponseModelSeatIds): ResponseModelSeat;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { deliverEngineNotice } from "../engine-notice.js";
|
|
2
|
+
import { providerReportedModel } from "../message-utils.js";
|
|
3
|
+
export function createResponseModelSeat(onNotice, ids) {
|
|
4
|
+
const announced = new Set();
|
|
5
|
+
return {
|
|
6
|
+
observe(message) {
|
|
7
|
+
const reported = providerReportedModel(message);
|
|
8
|
+
if (reported === undefined)
|
|
9
|
+
return undefined;
|
|
10
|
+
const requested = typeof message.model === "string" ? message.model : "";
|
|
11
|
+
const pair = `${requested}\u0000${reported}`;
|
|
12
|
+
if (!announced.has(pair)) {
|
|
13
|
+
announced.add(pair);
|
|
14
|
+
deliverEngineNotice(onNotice, {
|
|
15
|
+
code: "route.response_model_mismatch",
|
|
16
|
+
message: `The provider answered with model "${reported}" for a request sent as "${requested || "(unnamed)"}". ` +
|
|
17
|
+
`The run is unchanged — the engine keeps asking for the id the catalog names, and the served name is ` +
|
|
18
|
+
`reported beside it on every turn (turn_end.responseModel) and on the result (TaskResult.responseModel). ` +
|
|
19
|
+
`This is what an alias, a pool or a gateway-side substitution looks like from here; if the two were ` +
|
|
20
|
+
`meant to be the same model, the catalog id and the gateway disagree about which one that is.`,
|
|
21
|
+
detail: { requested, reported, sessionId: ids.sessionId, runId: ids.runId },
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return reported;
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -10,8 +10,10 @@ import { advanceCadenceClock, reduceToolEnd } from "./turn-attachments.js";
|
|
|
10
10
|
import { gatedCallIdOf } from "./park-commit.js";
|
|
11
11
|
import { hasVerifiableStructureSignal } from "./grounding-signal.js";
|
|
12
12
|
import { batchResponseDigest, toolEndBodyFrom } from "./tool-end-body.js";
|
|
13
|
+
import { createResponseModelSeat } from "./response-model-seat.js";
|
|
13
14
|
export function createHarnessHandlers(input) {
|
|
14
15
|
const { prepared, stats, rs, deps } = input;
|
|
16
|
+
const responseModelSeat = createResponseModelSeat(deps.runnerHooks.onNotice, { sessionId: prepared.sessionId, runId: prepared.runId });
|
|
15
17
|
const { spec, queue, internals, ident, parentToolCallId, subagentName, pushContent, emitCommitted, startedToolCallIds, toolStartAt, toolProgress, writeFamilyOf, toolLabels, postToolBatchHook, batchArgs, internalsNotifier } = deps;
|
|
16
18
|
let lastWorkspaceCwd = prepared.cwdRef?.current;
|
|
17
19
|
const announceWorkspaceMove = () => {
|
|
@@ -143,6 +145,9 @@ export function createHarnessHandlers(input) {
|
|
|
143
145
|
if (m.role === "assistant" && typeof m.stopReason === "string") {
|
|
144
146
|
rs.turn.turnStopReason = m.stopReason;
|
|
145
147
|
}
|
|
148
|
+
if (m.role === "assistant") {
|
|
149
|
+
rs.turn.turnResponseModel = responseModelSeat.observe(m);
|
|
150
|
+
}
|
|
146
151
|
if (m.role === "assistant") {
|
|
147
152
|
const rep = m.repetition;
|
|
148
153
|
if (rep !== undefined) {
|
|
@@ -291,13 +296,15 @@ export function createHarnessHandlers(input) {
|
|
|
291
296
|
if (prepared.abortController.signal.aborted)
|
|
292
297
|
prepared.workCutRef.abortedBeforeTurnEnd = true;
|
|
293
298
|
const stopExtra = rs.turn.turnStopReason !== undefined ? { stopReason: rs.turn.turnStopReason } : {};
|
|
299
|
+
const servedExtra = rs.turn.turnResponseModel !== undefined ? { responseModel: rs.turn.turnResponseModel } : {};
|
|
294
300
|
const usage = rs.turn.turnUsage ?? { inputTokens: 0, totalInputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, costMicroUsd: 0 };
|
|
295
301
|
const turnUsageUnknown = rs.turn.turnUsage === undefined || rs.turn.turnUsageMissing;
|
|
296
302
|
if (turnUsageUnknown)
|
|
297
303
|
stats.usageMissing = true;
|
|
298
|
-
queue.push({ type: "turn_end", usage, ...(turnUsageUnknown ? { usageMissing: true } : {}), ...stopExtra, ...ident() });
|
|
304
|
+
queue.push({ type: "turn_end", usage, ...(turnUsageUnknown ? { usageMissing: true } : {}), ...stopExtra, ...servedExtra, ...ident() });
|
|
299
305
|
rs.turn.turnUsageMissing = false;
|
|
300
306
|
rs.turn.turnStopReason = undefined;
|
|
307
|
+
rs.turn.turnResponseModel = undefined;
|
|
301
308
|
if (parentToolCallId !== undefined) {
|
|
302
309
|
const progress = {
|
|
303
310
|
type: "task_progress",
|
|
@@ -443,7 +443,7 @@ export class Runner {
|
|
|
443
443
|
spec, queue, internals, ident, parentToolCallId, subagentName,
|
|
444
444
|
pushContent, emitCommitted, startedToolCallIds, toolStartAt, toolProgress, writeFamilyOf,
|
|
445
445
|
toolLabels, postToolBatchHook, batchArgs, internalsNotifier,
|
|
446
|
-
runnerHooks: { onError: (e, info) => this.deps.onError?.(e, info) },
|
|
446
|
+
runnerHooks: { onError: (e, info) => this.deps.onError?.(e, info), onNotice: this.deps.onNotice },
|
|
447
447
|
},
|
|
448
448
|
});
|
|
449
449
|
const { compactionBrain, withinTaskCompaction, compactionBreaker, windowSafetyOptions } = runCompactionMachinery({ spec, prepared, rs, stats, noteUnevaluablePriceTable, runner: this.depsSeat });
|
|
@@ -83,15 +83,16 @@ export interface SideQueryResult {
|
|
|
83
83
|
args: unknown;
|
|
84
84
|
}>;
|
|
85
85
|
/** The RESOLVED pool model id the query was ROUTED to (the caller's ref resolved). See
|
|
86
|
-
* `servedModel` for the
|
|
86
|
+
* `servedModel` for the engine-selected id the reply came back stamped with. */
|
|
87
87
|
model: string;
|
|
88
|
-
/** The
|
|
89
|
-
*
|
|
90
|
-
*
|
|
88
|
+
/** The ENGINE-SELECTED id the reply came back stamped with (a degrading deployment may fall back
|
|
89
|
+
* mid-query — attribution follows the entry that ran, not the routing intent); equals `model` when
|
|
90
|
+
* nothing degraded. Still THIS catalog's id: what the PROVIDER called it is `responseModel` below. */
|
|
91
91
|
servedModel: string;
|
|
92
92
|
/** Structured degradation marker when a fallback model served (from/to/reason/chain). */
|
|
93
93
|
degraded?: DegradationInfo;
|
|
94
|
-
/**
|
|
94
|
+
/** The PROVIDER's own name for the model that answered, when it differs from `servedModel` above.
|
|
95
|
+
* Absent ⇒ it named the same id or named none — one reading, as on `AssistantMessage.responseModel`. */
|
|
95
96
|
responseModel?: string;
|
|
96
97
|
/** Usage with `input` NORMALIZED to the uncached prompt quantity (the Anthropic-protocol
|
|
97
98
|
* `input_tokens` meaning) for EVERY provider family — the same gate the run loop's accounting
|
package/dist/core/side-query.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolveTaskModel } from "./roles.js";
|
|
2
2
|
import { readDegradation } from "../brain/degrading.js";
|
|
3
3
|
import { cacheFamilyOf, uncachedInputTokensOf } from "./runner/usage-accounting.js";
|
|
4
|
+
import { providerReportedModel } from "./message-utils.js";
|
|
4
5
|
function normalizeSideQueryUsage(model, u) {
|
|
5
6
|
const input = uncachedInputTokensOf(cacheFamilyOf(model), u);
|
|
6
7
|
return input === u.input ? u : { ...u, input };
|
|
@@ -63,13 +64,14 @@ export async function runSideQuery(spec, deps) {
|
|
|
63
64
|
.map((c) => ({ id: c.id, name: c.name, args: c.arguments }));
|
|
64
65
|
const degraded = readDegradation(msg);
|
|
65
66
|
const servedModel = typeof msg.model === "string" && msg.model.length > 0 ? msg.model : resolved.model.id;
|
|
67
|
+
const reportedModel = providerReportedModel(msg);
|
|
66
68
|
return {
|
|
67
69
|
text,
|
|
68
70
|
toolCalls,
|
|
69
71
|
model: resolved.model.id,
|
|
70
72
|
servedModel,
|
|
71
73
|
...(degraded !== undefined ? { degraded } : {}),
|
|
72
|
-
...(
|
|
74
|
+
...(reportedModel !== undefined ? { responseModel: reportedModel } : {}),
|
|
73
75
|
usage: normalizeSideQueryUsage(resolved.model, msg.usage),
|
|
74
76
|
...(msg.usageMissing === true ? { usageMissing: true } : {}),
|
|
75
77
|
stopReason: msg.stopReason,
|
|
@@ -581,6 +581,14 @@ export type TaskEvent = ({
|
|
|
581
581
|
* no assistant message. Typed `string` (not the internal `StopReason`) to keep the engine's union
|
|
582
582
|
* private, but the value set is exactly those five tokens. */
|
|
583
583
|
stopReason?: string;
|
|
584
|
+
/** The PROVIDER'S OWN name for the model that answered this turn, when it DIFFERS from the id the
|
|
585
|
+
* request was sent with — a SECOND fact beside `TaskResult.model` / the trace's `brain.call.model`,
|
|
586
|
+
* which stay the ENGINE-SELECTED pool id a resume re-seeds and a catalog resolves, never an
|
|
587
|
+
* overwrite of either. PRESENCE: the turn's LAST assistant message carried one
|
|
588
|
+
* (`AssistantMessage.responseModel`). ABSENT ⇒ the provider named the requested id or named nothing —
|
|
589
|
+
* ONE reading, not two, as on the reference mismatch latch, also written only for a disagreeing pair.
|
|
590
|
+
* A mismatch is additionally announced once per leg per pair as `route.response_model_mismatch`. */
|
|
591
|
+
responseModel?: string;
|
|
584
592
|
} & TaskEventIdentity) | ({
|
|
585
593
|
/** design/99 MF-18: a compaction boundary (or, since design/119 #1, mid-turn when the prompt-too-long recovery force-compacts between a failed request and its retry). `trigger` = what caused it: `"auto"` (the context-threshold
|
|
586
594
|
* auto-compaction), `"manual"` (the `TaskStream.compact` / `/compact` path), or `"forced"` (RB-398-b
|
|
@@ -198,10 +198,20 @@ export interface TaskResult {
|
|
|
198
198
|
* {@link import("./runner/terminal-projection.js").terminalProjection} — the one cause→word derivation.
|
|
199
199
|
*/
|
|
200
200
|
terminal: TerminalCause;
|
|
201
|
-
/** design/99 MF-25: the EFFECTIVE model id
|
|
202
|
-
* `TaskSpec.model` ref (
|
|
203
|
-
*
|
|
201
|
+
/** design/99 MF-25: the EFFECTIVE model id this task ran on — the ENGINE-SELECTED, RESOLVED `Model.id`, not
|
|
202
|
+
* the requested `TaskSpec.model` ref (a role / name / `@mention`). A mid-run degradation is observed
|
|
203
|
+
* separately (the degraded-model fields); what the PROVIDER called it rides {@link responseModel}. */
|
|
204
204
|
model?: string;
|
|
205
|
+
/** The PROVIDER'S OWN name for the model that answered this run's FINAL turn, when it DIFFERS from the
|
|
206
|
+
* id the request was sent with — the result-face twin of `turn_end.responseModel`. {@link model} above
|
|
207
|
+
* stays the ENGINE-SELECTED id a resume re-seeds and a catalog resolves; a UI echoing "served by X"
|
|
208
|
+
* prints this one when it is here and {@link model} when it is not. **In-presence condition**: the final
|
|
209
|
+
* assistant message carried one. ABSENT ⇒ the provider named the requested id or named nothing (one
|
|
210
|
+
* reading, not two), there was no final assistant message, or the final turn AGREED while an earlier one
|
|
211
|
+
* did not — the per-turn frames are the complete record, this is the terminal one. Never present on the
|
|
212
|
+
* engine's synthetic messages, which carry no provider report; a mismatch is announced either way as
|
|
213
|
+
* `route.response_model_mismatch`. */
|
|
214
|
+
responseModel?: string;
|
|
205
215
|
/**
|
|
206
216
|
* The leg's effective REASONING resolution: the result-face twin of {@link model} for the thinking
|
|
207
217
|
* knob. How the requested tier resolved against the serving model's real capability
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"repo": "core",
|
|
3
|
-
"version": "7.28.
|
|
3
|
+
"version": "7.28.1",
|
|
4
4
|
"from": "cba36ff92ab9b8a3912e984f6a20263bd6a4c16b",
|
|
5
|
-
"to": "v7.28.
|
|
5
|
+
"to": "v7.28.1",
|
|
6
6
|
"added": [],
|
|
7
7
|
"removed": [],
|
|
8
8
|
"changed": [],
|
|
@@ -590,5 +590,5 @@
|
|
|
590
590
|
},
|
|
591
591
|
"breakingCount": 0,
|
|
592
592
|
"breakingItems": [],
|
|
593
|
-
"toResolvedAt": "
|
|
593
|
+
"toResolvedAt": "228eabc7adbae3961b847fb9b598dcbee4593b86"
|
|
594
594
|
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
{
|
|
2
|
+
"repo": "core",
|
|
3
|
+
"version": "7.29.0",
|
|
4
|
+
"from": "35c47a4d8e574c93501b5bfe7c7d3c41a707d53a",
|
|
5
|
+
"to": "v7.29.0",
|
|
6
|
+
"added": [],
|
|
7
|
+
"removed": [],
|
|
8
|
+
"changed": [],
|
|
9
|
+
"narrowed": [],
|
|
10
|
+
"closedSetMembers": [
|
|
11
|
+
{
|
|
12
|
+
"set": "ENGINE_NOTICE_CODES",
|
|
13
|
+
"at": [
|
|
14
|
+
"src/core/governance-codes.ts:315"
|
|
15
|
+
],
|
|
16
|
+
"newSet": false,
|
|
17
|
+
"added": [
|
|
18
|
+
"route.response_model_mismatch"
|
|
19
|
+
],
|
|
20
|
+
"removed": [],
|
|
21
|
+
"breaking": false,
|
|
22
|
+
"criteriaCovered": true
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"closedSetsUnreadable": [],
|
|
26
|
+
"typeKeys": [
|
|
27
|
+
{
|
|
28
|
+
"type": "CascadeRunResult",
|
|
29
|
+
"at": [
|
|
30
|
+
"src/agents/cascade.ts:130"
|
|
31
|
+
],
|
|
32
|
+
"added": [
|
|
33
|
+
"responseModel"
|
|
34
|
+
],
|
|
35
|
+
"removed": [],
|
|
36
|
+
"breaking": false,
|
|
37
|
+
"criteriaCovered": false
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"type": "RepairResult",
|
|
41
|
+
"at": [
|
|
42
|
+
"src/agents/repair-loop.ts:214"
|
|
43
|
+
],
|
|
44
|
+
"added": [
|
|
45
|
+
"responseModel"
|
|
46
|
+
],
|
|
47
|
+
"removed": [],
|
|
48
|
+
"breaking": false,
|
|
49
|
+
"criteriaCovered": false
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"type": "TaskEvent",
|
|
53
|
+
"at": [
|
|
54
|
+
"src/core/task-event.ts:265"
|
|
55
|
+
],
|
|
56
|
+
"added": [
|
|
57
|
+
"turn_end.responseModel"
|
|
58
|
+
],
|
|
59
|
+
"removed": [],
|
|
60
|
+
"breaking": false,
|
|
61
|
+
"criteriaCovered": true
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"type": "TaskResult",
|
|
65
|
+
"at": [
|
|
66
|
+
"src/core/task-result.ts:169"
|
|
67
|
+
],
|
|
68
|
+
"added": [
|
|
69
|
+
"responseModel"
|
|
70
|
+
],
|
|
71
|
+
"removed": [],
|
|
72
|
+
"breaking": false,
|
|
73
|
+
"criteriaCovered": true
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"type": "TeacherRunResult",
|
|
77
|
+
"at": [
|
|
78
|
+
"src/agents/teacher.ts:156"
|
|
79
|
+
],
|
|
80
|
+
"added": [
|
|
81
|
+
"responseModel"
|
|
82
|
+
],
|
|
83
|
+
"removed": [],
|
|
84
|
+
"breaking": false,
|
|
85
|
+
"criteriaCovered": false
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"type": "VerificationResult",
|
|
89
|
+
"at": [
|
|
90
|
+
"src/agents/verify.ts:262"
|
|
91
|
+
],
|
|
92
|
+
"added": [
|
|
93
|
+
"responseModel"
|
|
94
|
+
],
|
|
95
|
+
"removed": [],
|
|
96
|
+
"breaking": false,
|
|
97
|
+
"criteriaCovered": false
|
|
98
|
+
}
|
|
99
|
+
],
|
|
100
|
+
"typeKeysUnreadableArms": [
|
|
101
|
+
{
|
|
102
|
+
"type": "TaskEvent",
|
|
103
|
+
"at": "src/core/task-event.ts:265",
|
|
104
|
+
"reasons": [
|
|
105
|
+
"a union carrying 2 arm(s) whose keys this reader does not follow (a qualified reference (`TaskEventIdentity`); a qualified reference (`BrainStatus`)) — a key missing from this type at `to` is still reported REMOVED, carrying `uncertain` so the writer checks whether it moved there"
|
|
106
|
+
]
|
|
107
|
+
}
|
|
108
|
+
],
|
|
109
|
+
"typeKeysFlattenedCount": 213,
|
|
110
|
+
"declarationMoves": [],
|
|
111
|
+
"mintMoves": [],
|
|
112
|
+
"criteriaObserves": [
|
|
113
|
+
{
|
|
114
|
+
"kind": "engineNoticeCode",
|
|
115
|
+
"name": "route.response_model_mismatch",
|
|
116
|
+
"assertions": [
|
|
117
|
+
"G3.a"
|
|
118
|
+
]
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"kind": "taskEventField",
|
|
122
|
+
"name": "turn_end.responseModel",
|
|
123
|
+
"assertions": [
|
|
124
|
+
"G2.a"
|
|
125
|
+
]
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"kind": "typeMember",
|
|
129
|
+
"name": "AssistantMessage.model",
|
|
130
|
+
"assertions": [
|
|
131
|
+
"G1.a",
|
|
132
|
+
"G4.a"
|
|
133
|
+
]
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"kind": "typeMember",
|
|
137
|
+
"name": "AssistantMessage.responseModel",
|
|
138
|
+
"assertions": [
|
|
139
|
+
"G1.a"
|
|
140
|
+
]
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"kind": "typeMember",
|
|
144
|
+
"name": "TaskResult.model",
|
|
145
|
+
"assertions": [
|
|
146
|
+
"G2.a",
|
|
147
|
+
"G4.a"
|
|
148
|
+
]
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"kind": "typeMember",
|
|
152
|
+
"name": "TaskResult.responseModel",
|
|
153
|
+
"assertions": [
|
|
154
|
+
"G2.a"
|
|
155
|
+
]
|
|
156
|
+
}
|
|
157
|
+
],
|
|
158
|
+
"consumersHint": {
|
|
159
|
+
"cli": [
|
|
160
|
+
"CascadeRunResult",
|
|
161
|
+
"ENGINE_NOTICE_CODES",
|
|
162
|
+
"RepairResult",
|
|
163
|
+
"TaskEvent",
|
|
164
|
+
"TaskResult",
|
|
165
|
+
"TeacherRunResult",
|
|
166
|
+
"VerificationResult"
|
|
167
|
+
],
|
|
168
|
+
"server": [
|
|
169
|
+
"ENGINE_NOTICE_CODES",
|
|
170
|
+
"RepairResult",
|
|
171
|
+
"TaskEvent",
|
|
172
|
+
"TaskResult",
|
|
173
|
+
"VerificationResult"
|
|
174
|
+
],
|
|
175
|
+
"sdk": [
|
|
176
|
+
"ENGINE_NOTICE_CODES",
|
|
177
|
+
"TaskEvent",
|
|
178
|
+
"TaskResult"
|
|
179
|
+
],
|
|
180
|
+
"note": "scanned cli@9bf54265 server@acc76c5f sdk@1ba8942 — a name listed is MENTIONED there today, which is evidence of consumption, not proof of it. Ledger files (**/docs/core-consumption/**, **/docs/wire-consumption/**, **/*LEDGER*.json) are not evidence of use — server: 2 name(s) matched ONLY in bookkeeping files and were set aside (CascadeRunResult, TeacherRunResult)"
|
|
181
|
+
},
|
|
182
|
+
"breakingCount": 0,
|
|
183
|
+
"breakingItems": [],
|
|
184
|
+
"toResolvedAt": "00ea23133f3ab421c44449851f44587ca5330d97"
|
|
185
|
+
}
|