@sema-agent/core 5.49.0 → 5.50.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 +49 -0
- package/dist/agents/roster-store.js +4 -1
- package/dist/agents/subagent.d.ts +6 -0
- package/dist/agents/subagent.js +126 -1
- package/dist/agents/teacher.js +4 -1
- package/dist/brain/anthropic.js +11 -20
- package/dist/brain/open-responses.js +6 -14
- package/dist/brain/openai.js +6 -18
- package/dist/brain/reasoning.d.ts +100 -8
- package/dist/brain/reasoning.js +39 -15
- package/dist/brain/request-params.d.ts +37 -1
- package/dist/brain/request-params.js +40 -2
- package/dist/core/mcp.d.ts +7 -1
- package/dist/core/mcp.js +64 -8
- package/dist/core/memory-engine/engine.d.ts +30 -1
- package/dist/core/memory-engine/engine.js +219 -18
- package/dist/core/memory-engine/layout.d.ts +43 -0
- package/dist/core/memory-engine/layout.js +59 -0
- package/dist/core/memory-engine/memory-backend-contract.js +87 -0
- package/dist/core/memory-engine/types.d.ts +13 -1
- package/dist/core/runner/prepare-task.js +15 -1
- package/dist/core/task-registry-agent.js +6 -0
- package/dist/core/types.d.ts +17 -0
- package/dist/orchestration/run-workflow-tool.d.ts +12 -0
- package/dist/orchestration/run-workflow-tool.js +1 -1
- package/dist/orchestration/workflow-governance.d.ts +27 -0
- package/dist/orchestration/workflow-governance.js +13 -0
- package/dist/orchestration/workflow-primitives.d.ts +8 -1
- package/dist/orchestration/workflow-primitives.js +11 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.50.0 — 2026-08-21
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **`RunnerDeps.mcpRevocations`** (design/338, the #322 batch-3 hook — mid-turn MCP revocation):
|
|
7
|
+
the host's revocation ledger, probed synchronously at every MCP dispatch (tool call + the three
|
|
8
|
+
resource tools) BEFORE the transport. A revoked server's call settles as the coded refusal
|
|
9
|
+
`mcp.server_revoked` with known-not-executed wording; the ledger is live (revoke/un-revoke take
|
|
10
|
+
effect on the very next call, no engine-side caching); a throwing probe fails OPEN with a
|
|
11
|
+
once-per-run `mcp.revocation_probe_failed` notice. Absent seat = pre-338 semantics.
|
|
12
|
+
- **Machine-readable refusal codes on the wire**: `delegation.concurrency_cap`,
|
|
13
|
+
`delegation.session_cap` and `mcp.server_revoked` now carry a `details.code` twin beside
|
|
14
|
+
`details.error` (same value; the wire errorCode lift reads `code`) — tool_end frames for these
|
|
15
|
+
refusals become classifiable. `HarvestRejectionCode` gains `"deferred"` (additive).
|
|
16
|
+
- Conformance clauses **31 → 34**: the hold×applyPatches protocol (hold-unaware plain write /
|
|
17
|
+
release-replay CAS conflict carrying currentRev / snapshot faces tracking the post-UPDATE tuple)
|
|
18
|
+
and the whitewash-precedence UPDATE spelling. `MemorySessionHandle` gains a keyset gate
|
|
19
|
+
(machine face for consumer obligations).
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- **Workflow mount batch** (merged-scan FAM-3): a generic own-nullish merger
|
|
23
|
+
(`overlayWorktreeBaseline`) replaces the per-key drop list — an overlay's own-undefined keys no
|
|
24
|
+
longer erase base governance (excludeTools/deferTools/toolPolicy/shellGate/…); new keys are safe
|
|
25
|
+
by construction (`checkpointStore: null` is the one registered exception). Host tighten-only
|
|
26
|
+
clamps (`handsReadOnly:true` / `interactiveTools:false`) now ride into workflow children at the
|
|
27
|
+
same injection point as the read-face clamp — a clamped host's workflow child can no longer
|
|
28
|
+
unclamp itself.
|
|
29
|
+
- **Brain effort batch** (FAM-4): the effective-reasoning verdict is minted at the single point
|
|
30
|
+
that writes the wire and echoed back (three single-sources: the carried predicate, the wire
|
|
31
|
+
value mint, the declared-levels shape gate) — a garbled tier (`thinking:"hgih"`) now reports the
|
|
32
|
+
same fact the wire shows instead of a clamped-to-minimal or echo-garbage lie; the anthropic
|
|
33
|
+
budget arm reports its cap-wins drop; truthy non-array effortLevels no longer throw at request
|
|
34
|
+
construction. Per-call headers are case-fold deduplicated (`X-Tenant` + `x-tenant` no longer
|
|
35
|
+
ride the wire as two entries; auth carriers exempt as pinned).
|
|
36
|
+
- **Memory projection write-back debt** (#366 root fix): a committed entry whose id write-back to
|
|
37
|
+
the projection file failed is now a durable DEBT row (`projection-debts.json`, write-ahead
|
|
38
|
+
staged across all four mint lanes) — the next harvest validates and re-binds the id-less file to
|
|
39
|
+
its committed identity instead of re-adopting it as a duplicate; stale rows drop, unvalidatable
|
|
40
|
+
rows defer fail-closed. The old silent swallow is loud in both directions.
|
|
41
|
+
- The durable reap drops the stale terminal in-process handle after a winning row delete (#361) —
|
|
42
|
+
inside the reap-to-restart window a same-process SendMessage answered the pre-deletion "not
|
|
43
|
+
retained" text while a fresh process answered the honest no-transcript form.
|
|
44
|
+
- Delegation entry ledgers get a lifecycle (reap anchor + amortized sweep) — one Map entry per
|
|
45
|
+
(scope, rootSessionId) no longer accumulates for the life of the process.
|
|
46
|
+
|
|
47
|
+
### Notes
|
|
48
|
+
- Conformance 31→34 is a tightening for backend implementers (a hold-unaware backend fails the
|
|
49
|
+
new cases; upgrade-order duty as before). Everything else is additive or a narrowing inside
|
|
50
|
+
existing coded families; no BREAKING changes.
|
|
51
|
+
|
|
3
52
|
## 5.49.0 — 2026-08-20
|
|
4
53
|
|
|
5
54
|
### Added
|
|
@@ -110,7 +110,10 @@ export class FileRosterStore {
|
|
|
110
110
|
if (text.trim() === "")
|
|
111
111
|
return [];
|
|
112
112
|
try {
|
|
113
|
-
const
|
|
113
|
+
const rawU = JSON.parse(text);
|
|
114
|
+
if (typeof rawU !== "object" || rawU === null)
|
|
115
|
+
throw new Error("roster file: not a JSON object");
|
|
116
|
+
const raw = rawU;
|
|
114
117
|
if (!Array.isArray(raw.entries))
|
|
115
118
|
throw new Error("roster file: entries is not an array");
|
|
116
119
|
const kept = raw.entries.filter((e) => typeof e === "object" && e !== null && typeof e.name === "string" && typeof e.agentId === "string" && typeof e.createdAt === "number");
|
|
@@ -484,6 +484,12 @@ export declare function resolveDelegationEntryCaps(caps: {
|
|
|
484
484
|
maxConcurrent?: number;
|
|
485
485
|
maxCumulativePerSession?: number;
|
|
486
486
|
} | undefined): ResolvedDelegationEntryCaps;
|
|
487
|
+
/** @internal — observability seam for the lifecycle pins (tests only; never a public surface):
|
|
488
|
+
* the ledger footprint of one registry. */
|
|
489
|
+
export declare function delegationEntryLedgerFootprint(registry: object): {
|
|
490
|
+
keys: number;
|
|
491
|
+
handles: number;
|
|
492
|
+
};
|
|
487
493
|
/**
|
|
488
494
|
* Options for {@link createSubagentTool}.
|
|
489
495
|
*
|
package/dist/agents/subagent.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { isAbsolute } from "node:path";
|
|
3
3
|
import { withDelegationProvenance } from "../core/tool-policy.js";
|
|
4
|
+
import { errorResult } from "../core/tools.js";
|
|
4
5
|
import { isHighSurrogate, isLowSurrogate } from "../core/surrogate-safe-slice.js";
|
|
5
6
|
import { newDelegationProvenanceAggregate, reduceDelegationAttestation } from "../core/memory-engine/delegation-provenance.js";
|
|
6
7
|
import { registerDelegationLaunch, replayExternalSettlementEffects, settleDelegation } from "../core/memory-engine/delegation-settlement.js";
|
|
@@ -912,6 +913,125 @@ function delegationEntryLedger(registry, key) {
|
|
|
912
913
|
}
|
|
913
914
|
return set;
|
|
914
915
|
}
|
|
916
|
+
const delegationLedgerLifecycles = new WeakMap();
|
|
917
|
+
const delegationLedgerKeyStores = new WeakMap();
|
|
918
|
+
function bindDelegationLedgerKeyStore(registry, key, store) {
|
|
919
|
+
let byKey = delegationLedgerKeyStores.get(registry);
|
|
920
|
+
if (byKey === undefined) {
|
|
921
|
+
byKey = new Map();
|
|
922
|
+
delegationLedgerKeyStores.set(registry, byKey);
|
|
923
|
+
}
|
|
924
|
+
if (byKey.get(key) === null)
|
|
925
|
+
return;
|
|
926
|
+
byKey.set(key, store ?? null);
|
|
927
|
+
}
|
|
928
|
+
async function sweepDelegationEntryLedgerKeys(registry, keys) {
|
|
929
|
+
const byKey = delegationEntryLedgers.get(registry);
|
|
930
|
+
if (byKey === undefined)
|
|
931
|
+
return;
|
|
932
|
+
const keyStores = delegationLedgerKeyStores.get(registry);
|
|
933
|
+
const activeFace = registry.activeDelegationHandles;
|
|
934
|
+
if (typeof activeFace !== "function")
|
|
935
|
+
return;
|
|
936
|
+
for (const key of keys) {
|
|
937
|
+
const set = byKey.get(key);
|
|
938
|
+
if (set === undefined)
|
|
939
|
+
continue;
|
|
940
|
+
const store = keyStores?.get(key);
|
|
941
|
+
if (store === undefined || store === null)
|
|
942
|
+
continue;
|
|
943
|
+
let scopeK;
|
|
944
|
+
let rootK;
|
|
945
|
+
try {
|
|
946
|
+
const parsed = JSON.parse(key);
|
|
947
|
+
if (!Array.isArray(parsed) || typeof parsed[0] !== "string" || typeof parsed[1] !== "string")
|
|
948
|
+
continue;
|
|
949
|
+
scopeK = parsed[0];
|
|
950
|
+
rootK = parsed[1];
|
|
951
|
+
}
|
|
952
|
+
catch {
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
if (activeFace.call(registry, scopeK, rootK).length > 0)
|
|
956
|
+
continue;
|
|
957
|
+
const sizeBefore = set.size;
|
|
958
|
+
let stored;
|
|
959
|
+
try {
|
|
960
|
+
stored = (await store.listBySession(scopeK, rootK)).map((r) => r.handle);
|
|
961
|
+
}
|
|
962
|
+
catch {
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
if (byKey.get(key) !== set || set.size !== sizeBefore || activeFace.call(registry, scopeK, rootK).length > 0)
|
|
966
|
+
continue;
|
|
967
|
+
if (stored.length === 0) {
|
|
968
|
+
byKey.delete(key);
|
|
969
|
+
keyStores?.delete(key);
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
const keep = new Set(stored);
|
|
973
|
+
for (const h of [...set])
|
|
974
|
+
if (!keep.has(h))
|
|
975
|
+
set.delete(h);
|
|
976
|
+
if (set.size === 0) {
|
|
977
|
+
byKey.delete(key);
|
|
978
|
+
keyStores?.delete(key);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
function armDelegationLedgerLifecycle(registry) {
|
|
983
|
+
let life = delegationLedgerLifecycles.get(registry);
|
|
984
|
+
if (life === undefined) {
|
|
985
|
+
life = { cursor: 0, subscribed: false };
|
|
986
|
+
delegationLedgerLifecycles.set(registry, life);
|
|
987
|
+
}
|
|
988
|
+
if (life.subscribed)
|
|
989
|
+
return;
|
|
990
|
+
const reapFace = registry.onSessionReap;
|
|
991
|
+
if (typeof reapFace !== "function")
|
|
992
|
+
return;
|
|
993
|
+
life.subscribed = true;
|
|
994
|
+
reapFace.call(registry, (sessionId, scope) => {
|
|
995
|
+
const byKey = delegationEntryLedgers.get(registry);
|
|
996
|
+
if (byKey === undefined)
|
|
997
|
+
return;
|
|
998
|
+
const keys = [...byKey.keys()].filter((k) => {
|
|
999
|
+
try {
|
|
1000
|
+
const parsed = JSON.parse(k);
|
|
1001
|
+
if (!Array.isArray(parsed) || typeof parsed[0] !== "string" || typeof parsed[1] !== "string")
|
|
1002
|
+
return false;
|
|
1003
|
+
return parsed[1] === sessionId && (scope === undefined || parsed[0] === scope);
|
|
1004
|
+
}
|
|
1005
|
+
catch {
|
|
1006
|
+
return false;
|
|
1007
|
+
}
|
|
1008
|
+
});
|
|
1009
|
+
if (keys.length > 0)
|
|
1010
|
+
void sweepDelegationEntryLedgerKeys(registry, keys).catch(() => undefined);
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
function sweepDelegationLedgerRound(registry, n) {
|
|
1014
|
+
const life = delegationLedgerLifecycles.get(registry);
|
|
1015
|
+
const byKey = delegationEntryLedgers.get(registry);
|
|
1016
|
+
if (life === undefined || byKey === undefined || byKey.size === 0)
|
|
1017
|
+
return;
|
|
1018
|
+
const keys = [...byKey.keys()];
|
|
1019
|
+
const start = life.cursor % keys.length;
|
|
1020
|
+
const slice = [];
|
|
1021
|
+
for (let i = 0; i < Math.min(n, keys.length); i++)
|
|
1022
|
+
slice.push(keys[(start + i) % keys.length]);
|
|
1023
|
+
life.cursor = (start + slice.length) % keys.length;
|
|
1024
|
+
void sweepDelegationEntryLedgerKeys(registry, slice).catch(() => undefined);
|
|
1025
|
+
}
|
|
1026
|
+
export function delegationEntryLedgerFootprint(registry) {
|
|
1027
|
+
const byKey = delegationEntryLedgers.get(registry);
|
|
1028
|
+
if (byKey === undefined)
|
|
1029
|
+
return { keys: 0, handles: 0 };
|
|
1030
|
+
let handles = 0;
|
|
1031
|
+
for (const s of byKey.values())
|
|
1032
|
+
handles += s.size;
|
|
1033
|
+
return { keys: byKey.size, handles };
|
|
1034
|
+
}
|
|
915
1035
|
export function normalizeSubagentType(value) {
|
|
916
1036
|
return value.normalize("NFKC").toLowerCase().replace(/[\p{White_Space}\p{Pd}_]+/gu, "");
|
|
917
1037
|
}
|
|
@@ -2491,7 +2611,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2491
2611
|
dropHostAbortListener();
|
|
2492
2612
|
await cancelObserver();
|
|
2493
2613
|
const wt = await finishWorktree();
|
|
2494
|
-
return
|
|
2614
|
+
return errorResult(`Sub-agent not started in background: ${text}${wt ? `\n${wt}` : ""}`, { error: code, code });
|
|
2495
2615
|
};
|
|
2496
2616
|
const activeFace = bg.registry.activeDelegationHandles;
|
|
2497
2617
|
const active = typeof activeFace === "function" ? activeFace.call(bg.registry, capScope, capRoot) : [];
|
|
@@ -2640,6 +2760,11 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2640
2760
|
}
|
|
2641
2761
|
if (capLedgerKey !== undefined && reviveRow === undefined)
|
|
2642
2762
|
delegationEntryLedger(bg.registry, capLedgerKey).add(taskId);
|
|
2763
|
+
if (capLedgerKey !== undefined) {
|
|
2764
|
+
bindDelegationLedgerKeyStore(bg.registry, capLedgerKey, bg.agentStore);
|
|
2765
|
+
armDelegationLedgerLifecycle(bg.registry);
|
|
2766
|
+
sweepDelegationLedgerRound(bg.registry, 3);
|
|
2767
|
+
}
|
|
2643
2768
|
childInternals.peerSelfRef?.addAxis("h", taskId);
|
|
2644
2769
|
if (agentName !== undefined) {
|
|
2645
2770
|
recordRosterSpawn(ctx.roster, { name: agentName, agentId: taskId, toolUseId: ctx.toolCallId, owner: bgOwner, scope: bgScope, ...((typeof childModel === "string" ? resolveModelDisplayLabel(childModel) : childModel?.id) !== undefined ? { model: typeof childModel === "string" ? childModel : childModel?.id } : {}), ...(reviveRow !== undefined ? ((reviveRow.rootSessionId ?? reviveRow.parentSessionId) !== undefined ? { rootSessionId: reviveRow.rootSessionId ?? reviveRow.parentSessionId } : {}) : (ctx.rootSessionId ?? ctx.sessionId) !== undefined ? { rootSessionId: ctx.rootSessionId ?? ctx.sessionId } : {}), ...(sessionScopedBg ? { sessionScoped: true } : {}), createdAt: reviveRow?.spawnedAt ?? Date.now() }, (err) => opts.onObserverError?.(err, { site: "roster.recordSpawn" }));
|
package/dist/agents/teacher.js
CHANGED
|
@@ -31,7 +31,10 @@ export function parseTeacherAdvice(text) {
|
|
|
31
31
|
const end = text.lastIndexOf("}");
|
|
32
32
|
if (start !== -1 && end > start) {
|
|
33
33
|
try {
|
|
34
|
-
const
|
|
34
|
+
const oU = JSON.parse(text.slice(start, end + 1));
|
|
35
|
+
if (typeof oU !== "object" || oU === null)
|
|
36
|
+
throw new Error("not an object");
|
|
37
|
+
const o = oU;
|
|
35
38
|
return {
|
|
36
39
|
strategy: typeof o.strategy === "string" ? o.strategy : undefined,
|
|
37
40
|
correction: typeof o.correction === "string" ? o.correction : undefined,
|
package/dist/brain/anthropic.js
CHANGED
|
@@ -5,10 +5,9 @@ import { createRepetitionPoll, 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, modelSupportsVision, sendableImages } from "./media-degrade.js";
|
|
8
|
-
import { ANTHROPIC_RESERVED, OUTPUT_CAP_KEYS, applyExtraBody, effectiveOutputCap, stripAuthHeaders } from "./request-params.js";
|
|
9
|
-
import {
|
|
8
|
+
import { ANTHROPIC_RESERVED, OUTPUT_CAP_KEYS, applyExtraBody, effectiveOutputCap, lockHeader, mergeHeaders, stripAuthHeaders, takeHeaderCasefold } from "./request-params.js";
|
|
9
|
+
import { MIN_THINKING_TOKENS, budgetCapSkipsThinking, declaredEffortLevels, reasoningBudgetShare, reasoningRequestCarried, resolveEffort } from "./reasoning.js";
|
|
10
10
|
import { runStreamingBrain } from "./stream-engine.js";
|
|
11
|
-
const MIN_THINKING_TOKENS = 1024;
|
|
12
11
|
function thinkingBudget(maxTokens, share, fixed, hardCap = false) {
|
|
13
12
|
const max = hardCap ? maxTokens : Math.max(maxTokens, MIN_THINKING_TOKENS * 2);
|
|
14
13
|
const want = fixed ?? Math.floor(max * share);
|
|
@@ -261,13 +260,13 @@ export function createAnthropicBrain(config = {}) {
|
|
|
261
260
|
if (options?.temperature !== undefined && anthCompat.supportsTemperature !== false) {
|
|
262
261
|
body.temperature = options.temperature;
|
|
263
262
|
}
|
|
264
|
-
if (model
|
|
263
|
+
if (reasoningRequestCarried(model, options?.reasoning)) {
|
|
265
264
|
if (anthCompat.thinkingMode === "adaptive") {
|
|
266
265
|
body.thinking = { type: "adaptive" };
|
|
267
266
|
}
|
|
268
267
|
else {
|
|
269
268
|
const hardCap = overrides?.maxOutputTokens !== undefined || options?.maxTokens !== undefined;
|
|
270
|
-
if (
|
|
269
|
+
if (budgetCapSkipsThinking(body.max_tokens, hardCap)) {
|
|
271
270
|
body.thinking = undefined;
|
|
272
271
|
delete body.thinking;
|
|
273
272
|
}
|
|
@@ -283,13 +282,9 @@ export function createAnthropicBrain(config = {}) {
|
|
|
283
282
|
}
|
|
284
283
|
const betas = [];
|
|
285
284
|
let sendEffortBeta = false;
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
options?.reasoning &&
|
|
290
|
-
options.reasoning !== "off" &&
|
|
291
|
-
isThinkingLevel(options.reasoning)) {
|
|
292
|
-
const { effective } = resolveEffort(options.reasoning, anthCompat.effortLevels.filter(isThinkingLevel));
|
|
285
|
+
const declaredEffort = declaredEffortLevels(anthCompat.effortLevels);
|
|
286
|
+
if (declaredEffort !== undefined && reasoningRequestCarried(model, options?.reasoning)) {
|
|
287
|
+
const { effective } = resolveEffort(options.reasoning, declaredEffort);
|
|
293
288
|
const wireEffort = effective === "minimal" ? "low" : effective;
|
|
294
289
|
body.output_config = { effort: wireEffort };
|
|
295
290
|
sendEffortBeta = true;
|
|
@@ -303,23 +298,19 @@ export function createAnthropicBrain(config = {}) {
|
|
|
303
298
|
if (anthCompat.interleavedThinking) {
|
|
304
299
|
betas.push("interleaved-thinking-2025-05-14");
|
|
305
300
|
}
|
|
306
|
-
const headers =
|
|
307
|
-
...model.headers,
|
|
308
|
-
...config.headers,
|
|
309
|
-
...options?.headers,
|
|
310
|
-
};
|
|
301
|
+
const headers = mergeHeaders(model.headers, config.headers, options?.headers);
|
|
311
302
|
if (options?.apiKey !== undefined)
|
|
312
303
|
stripAuthHeaders(headers);
|
|
313
304
|
if (betas.length > 0) {
|
|
314
|
-
const existing = (headers
|
|
305
|
+
const existing = (takeHeaderCasefold(headers, "anthropic-beta") ?? "").split(",").map((b) => b.trim()).filter(Boolean);
|
|
315
306
|
for (const beta of betas) {
|
|
316
307
|
if (!existing.includes(beta))
|
|
317
308
|
existing.push(beta);
|
|
318
309
|
}
|
|
319
310
|
headers["anthropic-beta"] = existing.join(",");
|
|
320
311
|
}
|
|
321
|
-
headers
|
|
322
|
-
headers
|
|
312
|
+
lockHeader(headers, "content-type", "application/json");
|
|
313
|
+
lockHeader(headers, "anthropic-version", config.version ?? "2023-06-01");
|
|
323
314
|
if (apiKey)
|
|
324
315
|
headers["x-api-key"] = apiKey;
|
|
325
316
|
const builtThinkingRequested = body.thinking !== undefined;
|
|
@@ -5,8 +5,8 @@ import { createRepetitionPoll, 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";
|
|
8
|
-
import { OUTPUT_CAP_KEYS, RESPONSES_RESERVED, applyExtraBody, effectiveOutputCap, stripAuthHeaders } from "./request-params.js";
|
|
9
|
-
import {
|
|
8
|
+
import { OUTPUT_CAP_KEYS, RESPONSES_RESERVED, applyExtraBody, effectiveOutputCap, lockHeader, mergeHeaders, stripAuthHeaders } from "./request-params.js";
|
|
9
|
+
import { mintEffortWireValue, reasoningRequestCarried } from "./reasoning.js";
|
|
10
10
|
import { runStreamingBrain } from "./stream-engine.js";
|
|
11
11
|
const DEGENERATE_POLL_CHARS = 64;
|
|
12
12
|
const MALFORMED_SAMPLE_CHARS = 160;
|
|
@@ -151,16 +151,12 @@ function toResponsesTools(ctx) {
|
|
|
151
151
|
}));
|
|
152
152
|
}
|
|
153
153
|
function resolveWireEffort(model, reasoning) {
|
|
154
|
-
if (!model
|
|
154
|
+
if (!reasoningRequestCarried(model, reasoning))
|
|
155
155
|
return undefined;
|
|
156
156
|
const compat = responsesCompat(model);
|
|
157
157
|
if (compat.supportsReasoningEffort === false)
|
|
158
158
|
return undefined;
|
|
159
|
-
|
|
160
|
-
const mapped = model.thinkingLevelMap?.[effort];
|
|
161
|
-
if (mapped === null)
|
|
162
|
-
return undefined;
|
|
163
|
-
return mapped ?? effort;
|
|
159
|
+
return mintEffortWireValue(reasoning, model, compat.reasoningEffortLevels).wireValue;
|
|
164
160
|
}
|
|
165
161
|
function computeUsage(model, raw) {
|
|
166
162
|
const input = raw?.input_tokens ?? 0;
|
|
@@ -268,14 +264,10 @@ export function createOpenResponsesBrain(config = {}) {
|
|
|
268
264
|
}
|
|
269
265
|
if (effort !== undefined)
|
|
270
266
|
body.reasoning = { effort };
|
|
271
|
-
const headers =
|
|
272
|
-
...model.headers,
|
|
273
|
-
...config.headers,
|
|
274
|
-
...options?.headers,
|
|
275
|
-
};
|
|
267
|
+
const headers = mergeHeaders(model.headers, config.headers, options?.headers);
|
|
276
268
|
if (options?.apiKey !== undefined)
|
|
277
269
|
stripAuthHeaders(headers);
|
|
278
|
-
headers
|
|
270
|
+
lockHeader(headers, "content-type", "application/json");
|
|
279
271
|
if (apiKey)
|
|
280
272
|
headers["authorization"] = `Bearer ${apiKey}`;
|
|
281
273
|
const wire = applyExtraBody(body, model.extraBody, RESPONSES_RESERVED);
|
package/dist/brain/openai.js
CHANGED
|
@@ -5,8 +5,8 @@ import { createRepetitionPoll, 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";
|
|
8
|
-
import { OPENAI_RESERVED, OUTPUT_CAP_KEYS, applyExtraBody, effectiveOutputCap, stripAuthHeaders } from "./request-params.js";
|
|
9
|
-
import {
|
|
8
|
+
import { OPENAI_RESERVED, OUTPUT_CAP_KEYS, applyExtraBody, effectiveOutputCap, lockHeader, mergeHeaders, stripAuthHeaders } from "./request-params.js";
|
|
9
|
+
import { mintEffortWireValue, reasoningRequestCarried } from "./reasoning.js";
|
|
10
10
|
import { runStreamingBrain } from "./stream-engine.js";
|
|
11
11
|
function closeToolCallAccum(acc) {
|
|
12
12
|
if (acc.closedTc)
|
|
@@ -86,19 +86,11 @@ function applyThinking(body, model, reasoning) {
|
|
|
86
86
|
}
|
|
87
87
|
return;
|
|
88
88
|
}
|
|
89
|
-
if (!model
|
|
89
|
+
if (!reasoningRequestCarried(model, reasoning))
|
|
90
90
|
return;
|
|
91
91
|
const compat = thinkingCompat(model);
|
|
92
92
|
const supportsEffort = compat.supportsReasoningEffort ?? true;
|
|
93
|
-
const
|
|
94
|
-
let effortWire = effort;
|
|
95
|
-
if (model.thinkingLevelMap) {
|
|
96
|
-
const mapped = model.thinkingLevelMap[effort];
|
|
97
|
-
if (mapped === null)
|
|
98
|
-
effortWire = undefined;
|
|
99
|
-
else if (mapped !== undefined)
|
|
100
|
-
effortWire = mapped;
|
|
101
|
-
}
|
|
93
|
+
const effortWire = mintEffortWireValue(reasoning, model, compat.reasoningEffortLevels).wireValue;
|
|
102
94
|
const format = compat.thinkingFormat ?? "openai";
|
|
103
95
|
switch (format) {
|
|
104
96
|
case "openai":
|
|
@@ -311,14 +303,10 @@ export function createOpenAIBrain(config = {}) {
|
|
|
311
303
|
if (options?.stop)
|
|
312
304
|
body.stop = options.stop;
|
|
313
305
|
applyThinking(body, model, options?.reasoning);
|
|
314
|
-
const headers =
|
|
315
|
-
...model.headers,
|
|
316
|
-
...config.headers,
|
|
317
|
-
...options?.headers,
|
|
318
|
-
};
|
|
306
|
+
const headers = mergeHeaders(model.headers, config.headers, options?.headers);
|
|
319
307
|
if (options?.apiKey !== undefined)
|
|
320
308
|
stripAuthHeaders(headers);
|
|
321
|
-
headers
|
|
309
|
+
lockHeader(headers, "content-type", "application/json");
|
|
322
310
|
if (apiKey)
|
|
323
311
|
headers["authorization"] = `Bearer ${apiKey}`;
|
|
324
312
|
const wire = applyExtraBody(body, model.extraBody, OPENAI_RESERVED);
|
|
@@ -16,6 +16,29 @@ import type { ThinkingLevel } from "../internal/harness-types.js";
|
|
|
16
16
|
export type ReasoningIntensity = ThinkingLevel;
|
|
17
17
|
/** Type guard: is `v` one of the 7 {@link ThinkingLevel} tiers? (A legacy/unknown reasoning string is not.) */
|
|
18
18
|
export declare function isThinkingLevel(v: unknown): v is ThinkingLevel;
|
|
19
|
+
/**
|
|
20
|
+
* THE thinking-request ENTRY predicate — does this request enter an applier's emission path at all?
|
|
21
|
+
* One conjunction, three arms: the model declares reasoning (TRUTHINESS — the adapters' own
|
|
22
|
+
* judgment, see the r5 note in {@link resolveReasoning}), the requested value is a real
|
|
23
|
+
* {@link ThinkingLevel} (an out-of-contract value — a typo'd tier, a caller's own enum — reads as
|
|
24
|
+
* ABSENCE, never as a declared tier), and it is not the explicit `"off"`.
|
|
25
|
+
*
|
|
26
|
+
* ENTRY gate, deliberately NOT an emitted-a-key guarantee: past this gate the per-FORMAT arms still
|
|
27
|
+
* decide what (if anything) lands on the wire — `supportsReasoningEffort:false` on a format whose
|
|
28
|
+
* only carrier is the effort key, a `null` levelmap entry, the binary enable keys. Those arms have
|
|
29
|
+
* their own reporting shape (the `graded:false` intent-echo family, each pinned with its rationale
|
|
30
|
+
* where it lives); this predicate only closes the gate the three appliers used to re-spell.
|
|
31
|
+
*
|
|
32
|
+
* Single-sourced here because every wire applier (openai.ts `applyThinking`, anthropic.ts's thinking
|
|
33
|
+
* + effort arms, open-responses.ts `resolveWireEffort`) used to re-spell the same three conjuncts —
|
|
34
|
+
* and the REPORTING resolver ({@link resolveReasoning}) mirrored only ONE of them (`!model.reasoning`),
|
|
35
|
+
* so a garbage tier put ZERO parameters on the wire while the resolution claimed a clamped-to-minimal
|
|
36
|
+
* gradient (or echoed the garbage string as `effective` on binary formats). A predicate the appliers
|
|
37
|
+
* and the reporter both call cannot drift.
|
|
38
|
+
*/
|
|
39
|
+
export declare function reasoningRequestCarried(model: {
|
|
40
|
+
reasoning?: boolean;
|
|
41
|
+
}, reasoning: unknown): reasoning is ThinkingLevel;
|
|
19
42
|
/** Ordinal rank of a level (`off`=0 … `max`=6). */
|
|
20
43
|
export declare function rankOf(level: ThinkingLevel): number;
|
|
21
44
|
/**
|
|
@@ -65,6 +88,66 @@ export interface ReasoningResolution {
|
|
|
65
88
|
/** True when {@link effective} differs from {@link requested} (the request couldn't be honored exactly). */
|
|
66
89
|
clamped: boolean;
|
|
67
90
|
}
|
|
91
|
+
/** Anthropic's hard floor for an extended-thinking budget. Lives HERE (not the anthropic brain) so
|
|
92
|
+
* the cap-wins predicate below and the brain's budget-window math read ONE constant. */
|
|
93
|
+
export declare const MIN_THINKING_TOKENS = 1024;
|
|
94
|
+
/**
|
|
95
|
+
* The anthropic BUDGET path's cap-wins arm (#346, single-sourced; design/119 #2 review codex H4):
|
|
96
|
+
* a HARD per-request output cap (an engine-imposed override or the caller's explicit
|
|
97
|
+
* `options.maxTokens` — the two lanes the brain refuses to raise) too small to host a legal thinking
|
|
98
|
+
* budget (≥ {@link MIN_THINKING_TOKENS}) plus answer room means the CAP WINS and thinking is skipped
|
|
99
|
+
* for the request. Shared by the wire arm (anthropic.ts, which acts on it) and
|
|
100
|
+
* {@link resolveReasoning}'s budget arm (which mirrors it when the caller supplies the request
|
|
101
|
+
* facts), so the skip decision and its report are one predicate — the reporter's budget arm used to
|
|
102
|
+
* claim an unconditional `graded:true` gradient while this arm deleted the thinking block from the
|
|
103
|
+
* very request it described. A soft (model/config-sourced) cap never skips: the brain raises it to
|
|
104
|
+
* host the budget instead (`hardCap === false`).
|
|
105
|
+
*/
|
|
106
|
+
export declare function budgetCapSkipsThinking(outputCapTokens: number, hardCap: boolean): boolean;
|
|
107
|
+
/**
|
|
108
|
+
* OPTIONAL per-request facts for {@link resolveReasoning} — what the wire's budget arm knows at
|
|
109
|
+
* request build that a per-leg eager resolution cannot: the resolved output cap and whether it is a
|
|
110
|
+
* HARD bound. Supplied ⇒ the anthropic budget arm mirrors the wire's cap-wins skip
|
|
111
|
+
* ({@link budgetCapSkipsThinking}); absent ⇒ the budget arm reports the cap-blind gradient it always
|
|
112
|
+
* did (the eager per-leg trace/result mint has no request facts — a capped request's per-attempt skip
|
|
113
|
+
* is visible only to a caller that passes them).
|
|
114
|
+
*/
|
|
115
|
+
export interface ReasoningWireFacts {
|
|
116
|
+
/** The request's resolved output cap (the wire `max_tokens` at the moment the thinking arm judges). */
|
|
117
|
+
outputCapTokens: number;
|
|
118
|
+
/** True when the cap is HARD (engine override / caller `options.maxTokens`) — the lanes the brain
|
|
119
|
+
* refuses to raise; a soft model/config cap is raised to host the budget instead. */
|
|
120
|
+
hardOutputCap: boolean;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The anthropic-`effortLevels` PRESENCE/SHAPE gate, shared by the wire arm (anthropic.ts) and the
|
|
124
|
+
* reporting dispatch below so the two cannot disagree about which family a request rides (#335): the
|
|
125
|
+
* old twin predicates were `x && x.length > 0` on both sides, so a truthy NON-ARRAY (a string —
|
|
126
|
+
* `.length > 0` holds) entered the wire arm and threw a bare TypeError from its `.filter` pre-clean
|
|
127
|
+
* BEFORE {@link resolveEffort}'s centralized non-array fallback could read it as undeclared — while
|
|
128
|
+
* the reporter happily described an effort resolution for the same config. Only the container shape
|
|
129
|
+
* is judged here; MEMBER validity stays {@link resolveEffort}'s job (its element-level sanitization
|
|
130
|
+
* is the single bad-value seat, which is also why the returned array is not member-checked — the cast
|
|
131
|
+
* is checked at runtime by every consumer's `resolveEffort` call).
|
|
132
|
+
*/
|
|
133
|
+
export declare function declaredEffortLevels(v: unknown): readonly ThinkingLevel[] | undefined;
|
|
134
|
+
/**
|
|
135
|
+
* The effort-lane WIRE-VALUE mint — clamp ({@link resolveEffort}) + the `Model.thinkingLevelMap`
|
|
136
|
+
* translation in ONE place, consumed by both completions-family appliers (openai.ts `applyThinking`,
|
|
137
|
+
* open-responses.ts `resolveWireEffort`) AND the reporting dispatch ({@link resolveReasoning}'s
|
|
138
|
+
* arms), so the value that reaches the wire and the resolution a trace/result face claims are two
|
|
139
|
+
* reads of one computation, never parallel re-derivations. Map semantics (the field's contract): a
|
|
140
|
+
* MISSING key ⇒ provider default (the clamped tier name as-is); a STRING ⇒ that provider-specific
|
|
141
|
+
* spelling (same tier, still honored); `null` ⇒ the tier is UNSUPPORTED on this model —
|
|
142
|
+
* `wireValue: undefined`, no effort value on the wire (thinking still enables via a format's own
|
|
143
|
+
* enable key where one exists), which is exactly the tier-not-honored shape the reporter must echo.
|
|
144
|
+
*/
|
|
145
|
+
export declare function mintEffortWireValue(requested: ThinkingLevel, model: {
|
|
146
|
+
thinkingLevelMap?: Readonly<Partial<Record<ThinkingLevel, string | null>>>;
|
|
147
|
+
}, allowed: readonly ThinkingLevel[] | undefined): {
|
|
148
|
+
resolution: ReasoningResolution;
|
|
149
|
+
wireValue: string | undefined;
|
|
150
|
+
};
|
|
68
151
|
/**
|
|
69
152
|
* A {@link ReasoningResolution} enriched with the endpoint discriminant, for observability (design/96 S6).
|
|
70
153
|
* The brain consumes only the {@link ReasoningResolution} fields to shape the request; `format`/`endpoint`
|
|
@@ -77,9 +160,12 @@ export interface ResolvedReasoning extends ReasoningResolution {
|
|
|
77
160
|
/** A coarse endpoint label for the trace (`model.api` — e.g. `openai-completions`, `anthropic-messages`). */
|
|
78
161
|
endpoint: string;
|
|
79
162
|
/**
|
|
80
|
-
* Present (true) only when the
|
|
81
|
-
*
|
|
82
|
-
*
|
|
163
|
+
* Present (true) only when the request never CARRIES at all: {@link reasoningRequestCarried} false
|
|
164
|
+
* on a non-"off" request — the model declares NO reasoning capability (`Model.reasoning` falsy),
|
|
165
|
+
* OR the requested value is not a valid {@link ThinkingLevel} (an out-of-contract tier every wire
|
|
166
|
+
* applier reads as ABSENCE — the same three-conjunct gate all of them share) — or, with
|
|
167
|
+
* {@link ReasoningWireFacts} supplied, the anthropic budget path's cap-wins skip
|
|
168
|
+
* ({@link budgetCapSkipsThinking}), so NO thinking
|
|
83
169
|
* parameter reaches the wire at all — the requested tier is DROPPED entirely, not clamped or
|
|
84
170
|
* downgraded-to-binary. `effective:"off"` here states the ENGINE side of that fact (nothing was
|
|
85
171
|
* requested), NOT a measured gateway state: on the binary enable-only formats (qwen / zai /
|
|
@@ -101,7 +187,10 @@ export interface ResolvedReasoning extends ReasoningResolution {
|
|
|
101
187
|
* brains keep calling {@link resolveEffort}/{@link resolveBinary}/{@link reasoningBudgetShare} on the hot path.
|
|
102
188
|
*
|
|
103
189
|
* - Anthropic (`api === "anthropic-messages"`) → budget-based: the tier sets a budget share, so it's a real
|
|
104
|
-
* gradient (`graded:true`) and never tier-clamped (`clamped:false`); reported as `format:"budget"`.
|
|
190
|
+
* gradient (`graded:true`) and never tier-clamped (`clamped:false`); reported as `format:"budget"`. With
|
|
191
|
+
* the optional {@link ReasoningWireFacts} the arm additionally mirrors the wire's cap-wins skip
|
|
192
|
+
* ({@link budgetCapSkipsThinking}: a hard per-request output cap < 2·{@link MIN_THINKING_TOKENS} deletes
|
|
193
|
+
* the thinking block) as the drop shape — facts absent keeps the historic cap-blind gradient.
|
|
105
194
|
* - Binary enable-only formats (qwen / zai / qwen-chat-template) → `graded:false` (tier not honored).
|
|
106
195
|
* - An effort endpoint with `supportsReasoningEffort:false` → `graded:false` (enable key only, no effort tier).
|
|
107
196
|
* - Otherwise effort-based → clamp DOWN to the endpoint's `reasoningEffortLevels` (default minimal|low|medium|high).
|
|
@@ -111,11 +200,14 @@ export interface ResolvedReasoning extends ReasoningResolution {
|
|
|
111
200
|
* honored. Previously this resolver never read the map and reported such a request as exactly honored
|
|
112
201
|
* (`graded:true`, `clamped:false`) while the wire dropped the value — trace/result-face drift.
|
|
113
202
|
*
|
|
114
|
-
* - A
|
|
115
|
-
*
|
|
203
|
+
* - A request that never CARRIES ({@link reasoningRequestCarried} false on a non-"off" value: the model's
|
|
204
|
+
* `reasoning` capability flag is FALSY, or the requested value is not a valid tier — no brain emits any
|
|
205
|
+
* thinking parameter for either, whatever the format) → `effective:"off"`, `graded:false`, `clamped:true`,
|
|
116
206
|
* `dropped:true` — the loud-drop arm. Previously this resolver described the capability dispatch for such
|
|
117
207
|
* a model (a resolution the request never carried), and the runner's trace guard skipped the frame — the
|
|
118
|
-
* one arm where the request evaporates was the one arm with no disclosure
|
|
208
|
+
* one arm where the request evaporates was the one arm with no disclosure; and an out-of-contract tier
|
|
209
|
+
* was worse still — reported as a clamped-to-minimal gradient (or echoed verbatim as `effective` on the
|
|
210
|
+
* binary formats) while the wire carried nothing.
|
|
119
211
|
*
|
|
120
212
|
* `off`/falsy never enables thinking, so it resolves trivially (no clamp, graded:true) — the caller decides
|
|
121
213
|
* whether to emit at all.
|
|
@@ -125,7 +217,7 @@ export declare function resolveReasoning(requested: ThinkingLevel, model: {
|
|
|
125
217
|
reasoning?: boolean;
|
|
126
218
|
compat?: unknown;
|
|
127
219
|
thinkingLevelMap?: Readonly<Partial<Record<ThinkingLevel, string | null>>>;
|
|
128
|
-
}): ResolvedReasoning;
|
|
220
|
+
}, facts?: ReasoningWireFacts): ResolvedReasoning;
|
|
129
221
|
/**
|
|
130
222
|
* Resolve a requested intensity for an effort-based endpoint (`reasoning_effort` / `reasoning.effort`). Picks
|
|
131
223
|
* the requested tier when supported; otherwise the highest supported tier ≤ requested (clamp DOWN, never
|