@sema-agent/core 5.56.0 → 5.57.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 +74 -0
- package/dist/agents/send-message-tool.d.ts +11 -0
- package/dist/agents/send-message-tool.js +34 -12
- package/dist/agents/team.d.ts +10 -1
- package/dist/agents/team.js +1 -0
- package/dist/brain/anthropic.js +15 -5
- package/dist/brain/circuit-breaker.js +2 -1
- package/dist/brain/degrading.js +4 -1
- package/dist/brain/failover.js +16 -1
- package/dist/brain/open-responses.js +15 -5
- package/dist/brain/openai.js +16 -5
- package/dist/brain/request-params.d.ts +30 -27
- package/dist/brain/request-params.js +1 -7
- package/dist/brain/route-adjudicator.d.ts +190 -0
- package/dist/brain/route-adjudicator.js +189 -0
- package/dist/brain/route-conformance.d.ts +55 -0
- package/dist/brain/route-conformance.js +136 -0
- package/dist/brain/routing.js +8 -3
- package/dist/core/mcp.js +4 -4
- package/dist/core/memory-engine/engine.d.ts +15 -5
- package/dist/core/memory-engine/engine.js +3 -1
- package/dist/core/permission-rule-consent.d.ts +45 -0
- package/dist/core/permission-rule-consent.js +40 -11
- package/dist/core/permission-rule-model.d.ts +110 -75
- package/dist/core/permission-rule-model.js +61 -28
- package/dist/core/runner/prepare-task.js +33 -4
- package/dist/core/runner/runtask.d.ts +4 -1
- package/dist/core/runner/runtask.js +48 -0
- package/dist/core/scheduler.d.ts +5 -0
- package/dist/core/side-query.d.ts +12 -5
- package/dist/core/types.d.ts +32 -0
- package/dist/engine/harness/agent-harness.js +26 -1
- package/dist/engine/harness/types.d.ts +5 -1
- package/dist/engine/llm/types.d.ts +65 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -1
- package/dist/internal/llm.d.ts +1 -1
- package/dist/prompts/default.d.ts +2 -2
- package/dist/prompts/default.js +2 -0
- package/dist/scenarios/scenario-registry.d.ts +5 -1
- package/dist/scenarios/scenario-registry.js +4 -2
- package/dist/tools/fs/index.js +8 -1
- package/dist/tools/scheduler-tools.js +28 -6
- package/dist/tools/web.d.ts +15 -0
- package/dist/tools/web.js +8 -2
- package/dist/tools/worktree.js +2 -2
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +19 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,79 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.57.0 — 2026-08-23
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **key×URL pairing adjudicator** (#309 cross-repo case, [5060]-[5063] consensus): `adjudicateModelRoute`
|
|
7
|
+
is the single source of the credential-pairing law. Per-model credential = paired by construction;
|
|
8
|
+
the deployment-config credential passes only on the deployment's own declared root; absent credential
|
|
9
|
+
is legal keyless except for an entry declaring its own off-root URL. Codes `route.credential_mismatch`
|
|
10
|
+
/ `route.credential_missing` (refusal detail carries entry id + both URL halves + a one-line fixHint,
|
|
11
|
+
fail-closed by ruling). The three first-party brains gate inside buildRequest (refusal terminates
|
|
12
|
+
before any fetch) and expose `Brain.adjudicateRoute`; routing/failover/degrading/circuit-breaker
|
|
13
|
+
re-judge per leg (failover serves from the paired leg; a broken degrading hop is skipped). Two-leg
|
|
14
|
+
disposition: a NAMED model surfaces the refusal; a DERIVED model (compaction summarize, auto-mode
|
|
15
|
+
classifier, suggestions) pre-flights and falls back to the primary with a `route.fallback_to_primary`
|
|
16
|
+
notice; `route.base_url_changed_key_unchanged` mints at swapModels. `ROUTE_ADJUDICATION_CONFORMANCE_CORPUS`
|
|
17
|
+
(16 vectors) exported as the cross-codebase anchor. **Narrowings (each with a fixHint)**: declared-root
|
|
18
|
+
deployments' off-root entries now refuse instead of silently borrowing the gateway credential; an
|
|
19
|
+
entry-declared auth header now wins over the deployment key. Preserved byte-identical: unpinned
|
|
20
|
+
quick-start, keyless single-gateway, header-only boot token, gateway dual-auth.
|
|
21
|
+
- **Per-model auth seats for WebFetch summarizer and team surfaces** (#417): `getApiKeyAndHeaders`
|
|
22
|
+
on `WebFetchSummarizerOptions` / `TeamDiscussionOptions` / `runScenario` (absent seat = byte-identical).
|
|
23
|
+
- **Compound-prefix permission rules** (#364 case B, ruled; registered divergence): `Bash(cd /tmp && adb pull:*)`
|
|
24
|
+
— leading segments byte-exact, only the final segment takes arguments. The validator accepts the
|
|
25
|
+
chain form (a `*` in any other position still refuses); the matcher admits on segment-count equality
|
|
26
|
+
+ byte-prefix at a word boundary (a single-command prefix still never admits a compound; the segment
|
|
27
|
+
deny/ask fence stays strictly ahead of the allow lane); the suggester mints the compound-prefix
|
|
28
|
+
sibling at index 1 when the final segment fits the same grammar as the simple seat; the card-edit
|
|
29
|
+
face accepts the respelling; org tighten gains the same grammar (a compound-prefix deny is enforceable
|
|
30
|
+
instead of bricking its snapshot). **Widenings**: validator accepts the new form; eligible compounds
|
|
31
|
+
offer 2 seats; card edit + import accept it; org snapshots carrying it are enforceable. **Mixed-fleet
|
|
32
|
+
note**: older engines quarantine compound-prefix rows (bytes preserved, disclosed) — do not publish
|
|
33
|
+
compound-prefix org rules before the fleet is level.
|
|
34
|
+
- **Generic prefix grammar for rule suggestions** (#364 R1, CC 2.1.223 iNo corpus): the prefix arm
|
|
35
|
+
uses the corpus rule `^[a-z][a-z0-9]*(-[a-z0-9]+)*$` with the interpreter/wrapper blacklist; the
|
|
36
|
+
125-line lexicon demotes to a bonus for multi-word deepening; ASCII-closed head-name screen.
|
|
37
|
+
- **`precheckEditedRuleText(text, command)`** (#428, server pickup): the card-edit face's three text
|
|
38
|
+
gates (spelling normalization → grammar → coverage bound) extracted to one shared body and exported
|
|
39
|
+
(`EditedRuleTextPrecheck`). `ok` means SUBMITTABLE (record-level gates stay with `confirmRuleApproval`);
|
|
40
|
+
`canonicalRule` may differ from the submitted bytes; refusal words match `edit_rejected.detail`
|
|
41
|
+
verbatim (`code` rides validator refusals only). The entry screens unrenderable command bytes loudly
|
|
42
|
+
(`config.invalid_argument`).
|
|
43
|
+
- **ScheduleWakeup honesty family** (#424 A-1/A-4): the wakeup intent carries `lifetime:"session"`
|
|
44
|
+
where the backend vouched for the reap contract; an undeclared backend keeps the historic durable
|
|
45
|
+
intent with receipt + description disclosure; `reason` rides `details.reason`. An incomplete
|
|
46
|
+
replace (stale wakeup possibly still armed) is disclosed on the receipt.
|
|
47
|
+
- **SendMessage completion promises follow mount truth** (#424 A-6): `notificationWired?` / `oneShot?`
|
|
48
|
+
seats (defaults preserve wired behavior byte-identical); the unwired arm teaches retrieval, the
|
|
49
|
+
one-shot arm instructs blocking retrieval now; ten sites across two wordings guarded.
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
- **Derived-route pre-flight can never sink the main task** (pre-ship rescan P1): the judge call and
|
|
53
|
+
the whole seat (including URL normalization of garbage catalog values) now abstain on throw instead
|
|
54
|
+
of failing task preparation and leaking the acquired session view; both derived seats share the
|
|
55
|
+
guarded helper.
|
|
56
|
+
- **Route URL comparison is wire-equivalent** (rescan): scheme/host case-fold + default-port elision
|
|
57
|
+
(path half stays byte-ordered; parse failure falls back to raw) — case-only respellings of the same
|
|
58
|
+
endpoint no longer refuse as `credential_mismatch`.
|
|
59
|
+
- **MCP revocation copy tells the standing truth at all four outlets** (rescan; #424 A-5 completed):
|
|
60
|
+
the ListMcpResources arm joins the other three ("stays on this run's roster; every call refused
|
|
61
|
+
the same way"), with an extinction tripwire for the old sentence.
|
|
62
|
+
- **AgentHarness auth carriers refuse loudly at the two host seats** (rescan hardening): a
|
|
63
|
+
deployment-wide auth header in `streamOptions.headers` is refused with a migration hint instead of
|
|
64
|
+
silently riding beside per-model credentials; `stripAuthHeaders` retired (zero callers).
|
|
65
|
+
- **ExitWorktree parameter texts describe this tool** (#424 A-8): detached worktrees have no branch;
|
|
66
|
+
a changed tree is KEPT (not refused) — CC originals and reasoning recorded at the schema.
|
|
67
|
+
- **Read-only memory notice restores CC's recall-framing paragraph** (#424 A-9, byte-compared).
|
|
68
|
+
- **MCP/scheduler small truths** (rescan): the wakeup mount note is led by the `supportsSessionWakeup:false`
|
|
69
|
+
arm; an explicitly-declared `supportsSessionLifetime:false` backend behaves like an undeclared one
|
|
70
|
+
(seam defines the two spellings as one state — the refusal arm from an unshipped interim build rolled back).
|
|
71
|
+
|
|
72
|
+
### Bench (not part of the package surface)
|
|
73
|
+
- LLM distillation arm for the memory benchmark (three-clause instruction arc: retained 20.8%→100%,
|
|
74
|
+
convergence restored; distillation quality flat with the hand-authored ceiling on this corpus's
|
|
75
|
+
measurable axes); live provider key no longer travels to the loopback observation plane.
|
|
76
|
+
|
|
3
77
|
## 5.56.0 — 2026-08-23
|
|
4
78
|
|
|
5
79
|
### Fixed
|
|
@@ -33,6 +33,17 @@ export interface SendMessageToolOptions {
|
|
|
33
33
|
notify?: (n: TaskNotificationPayload, opts?: {
|
|
34
34
|
priority?: "now" | "next" | "later";
|
|
35
35
|
}) => void;
|
|
36
|
+
/** Whether a completion notification really reaches the sender — drives every "you will be notified"
|
|
37
|
+
* sentence on this face (description + four receipts), mirroring TaskOutput's `notificationWired`.
|
|
38
|
+
* Default = `notify !== undefined`, which is the truth on both the first-party mount and a direct
|
|
39
|
+
* mount that wires its own sink here. Pass `true` only when the deployment announces a finished
|
|
40
|
+
* agent through a channel of its own (this tool then keeps the wired wording); `false` states the
|
|
41
|
+
* degraded truth explicitly. */
|
|
42
|
+
notificationWired?: boolean;
|
|
43
|
+
/** RB-220 mirror of {@link import("../core/types.js").TaskSpec.oneShot} (the Runner mount fills it):
|
|
44
|
+
* this run's process exits when the turn ends, so no completion notification can ever land — checked
|
|
45
|
+
* AHEAD of {@link notificationWired}, since no amount of wiring makes a later turn exist. */
|
|
46
|
+
oneShot?: boolean;
|
|
36
47
|
/** Steer-handle sink: the revived run re-emits a FRESH handle (design/122 risk-table contract). */
|
|
37
48
|
sink?: (handle: SubagentSteerHandle) => void;
|
|
38
49
|
/**
|
|
@@ -46,6 +46,21 @@ function targetLaneKey(scope, targetId) {
|
|
|
46
46
|
const OPERATOR_CONTINUATION_CTX = Symbol("sema.operator_continuation");
|
|
47
47
|
export function createSendMessageTool(opts) {
|
|
48
48
|
const tier3Capable = opts.agentStore !== undefined && opts.mailbox !== undefined && opts.reviveSpawn !== undefined;
|
|
49
|
+
const completionMode = opts.oneShot === true ? "one_shot" : (opts.notificationWired ?? opts.notify !== undefined) ? "notified" : "silent";
|
|
50
|
+
const notifyWired = completionMode === "notified";
|
|
51
|
+
const NO_COMPLETION_NOTICE = completionMode === "one_shot"
|
|
52
|
+
? "this is a ONE-SHOT submission — there is no later turn for a completion notification to land in, so retrieve its result NOW with TaskOutput(task_id, block: true) rather than ending your turn"
|
|
53
|
+
: "its completion is NOT announced on this mount — retrieve its status and result with TaskOutput(task_id) where mounted";
|
|
54
|
+
const AWAIT_COMPLETION = notifyWired
|
|
55
|
+
? "Wait for its completion notification"
|
|
56
|
+
: completionMode === "one_shot"
|
|
57
|
+
? "Wait for it NOW with TaskOutput(task_id, block: true) — this one-shot submission ends with this turn"
|
|
58
|
+
: "Check for its completion with TaskOutput(task_id) where mounted";
|
|
59
|
+
const awaitCompletion = notifyWired
|
|
60
|
+
? "wait for its completion notification"
|
|
61
|
+
: completionMode === "one_shot"
|
|
62
|
+
? "wait for it NOW with TaskOutput(task_id, block: true) — this one-shot submission ends with this turn"
|
|
63
|
+
: "check for its completion with TaskOutput(task_id) where mounted";
|
|
49
64
|
const sendMessagePins = new Map();
|
|
50
65
|
const pinGuard = (targetId, targetName, rung, to) => {
|
|
51
66
|
if (rung === "other")
|
|
@@ -86,8 +101,11 @@ export function createSendMessageTool(opts) {
|
|
|
86
101
|
`background run with its full prior conversation preserved, so don't re-explain what it already knows. Names ` +
|
|
87
102
|
`keep working after an agent completes; but if a name you already messaged is later taken by a NEWER agent, the ` +
|
|
88
103
|
`send is refused rather than silently redirected — re-send with the task_id of whichever agent you meant. ` +
|
|
89
|
-
|
|
90
|
-
|
|
104
|
+
(notifyWired
|
|
105
|
+
? `You will be notified automatically when it completes — prefer ending your turn; do not poll. `
|
|
106
|
+
: completionMode === "one_shot"
|
|
107
|
+
? `This is a ONE-SHOT submission: there is no later turn for a completion notification to land in, so do NOT end your turn expecting one — wait actively with TaskOutput(task_id, block: true) when you need a continued agent's result. `
|
|
108
|
+
: `A finished agent is NOT announced on this mount — check on it with TaskOutput(task_id) where mounted rather than ending your turn to wait for a notification that never comes. `) +
|
|
91
109
|
(tier3Capable
|
|
92
110
|
? `Continuing a finished agent works for agents with a durable record — by name or task_id, even across restarts — and for runs that retain sub-agent sessions; when neither covers it you get an honest error and should launch a new agent with the needed context instead.`
|
|
93
111
|
: `Continuing a finished agent requires the run to retain sub-agent sessions; when the session was not ` +
|
|
@@ -255,7 +273,7 @@ export function createSendMessageTool(opts) {
|
|
|
255
273
|
if (row.status === "running") {
|
|
256
274
|
if (row.writerId === opts.registry.writerId) {
|
|
257
275
|
return {
|
|
258
|
-
content: `Message not sent: ${whoT3} is currently running in this process but its mid-run delivery channel is not reachable from here.
|
|
276
|
+
content: `Message not sent: ${whoT3} is currently running in this process but its mid-run delivery channel is not reachable from here. ${AWAIT_COMPLETION}, then send again to continue it.`,
|
|
259
277
|
details: { error: "still_running", to },
|
|
260
278
|
isError: true,
|
|
261
279
|
};
|
|
@@ -268,7 +286,7 @@ export function createSendMessageTool(opts) {
|
|
|
268
286
|
};
|
|
269
287
|
}
|
|
270
288
|
return {
|
|
271
|
-
content: `Message not sent: ${whoT3} is running on another host — this process has no delivery channel to it.
|
|
289
|
+
content: `Message not sent: ${whoT3} is running on another host — this process has no delivery channel to it. ${AWAIT_COMPLETION}, then send again to continue it.`,
|
|
272
290
|
details: { error: "running_elsewhere", to },
|
|
273
291
|
isError: true,
|
|
274
292
|
};
|
|
@@ -501,7 +519,9 @@ export function createSendMessageTool(opts) {
|
|
|
501
519
|
const priorCount = lease.messages.length - 1;
|
|
502
520
|
return {
|
|
503
521
|
content: `Message sent — ${whoT3} was revived in the background with its prior context intact${priorCount > 0 ? ` (${priorCount} earlier pending message(s) delivered with it)` : ""}. ` +
|
|
504
|
-
|
|
522
|
+
(notifyWired
|
|
523
|
+
? `You will be notified automatically when it completes. Continue with other work — do not poll.`
|
|
524
|
+
: `${NO_COMPLETION_NOTICE.charAt(0).toUpperCase()}${NO_COMPLETION_NOTICE.slice(1)}.`),
|
|
505
525
|
details: { type: "send-message", status: "revived", to, task_id: handle, seq: nextSeq, ...(priorCount > 0 ? { priorMessages: priorCount } : {}) },
|
|
506
526
|
};
|
|
507
527
|
}
|
|
@@ -628,10 +648,10 @@ export function createSendMessageTool(opts) {
|
|
|
628
648
|
}, { priority: "next" });
|
|
629
649
|
if (delivered.ok) {
|
|
630
650
|
const receiptText = delivered.disposition === "parked"
|
|
631
|
-
? `Message parked for ${who}: the agent finished before reading it — it will be delivered when the agent is next continued. You will be notified of its completion; continue with other work.`
|
|
651
|
+
? `Message parked for ${who}: the agent finished before reading it — it will be delivered when the agent is next continued. ${notifyWired ? "You will be notified of its completion; continue with other work." : `Note: ${NO_COMPLETION_NOTICE}.`}`
|
|
632
652
|
: delivered.disposition === "pending"
|
|
633
|
-
? `Message accepted for ${who} but delivery is UNCONFIRMED (its channel did not confirm within the wait window) — it stays queued and will deliver if the channel binds. You will be notified of the agent's completion either way; resend then if it went unanswered. ${DEDUP_RETRY_NOTE}`
|
|
634
|
-
: `Message queued for delivery to ${who} at its next turn. If it finishes before reading it, the message may not survive — you will be notified of its completion either way; resend then if it went unanswered. Continue with other work; do not poll. ${DEDUP_RETRY_NOTE}`;
|
|
653
|
+
? `Message accepted for ${who} but delivery is UNCONFIRMED (its channel did not confirm within the wait window) — it stays queued and will deliver if the channel binds. ${notifyWired ? "You will be notified of the agent's completion either way" : `Note: ${NO_COMPLETION_NOTICE}`}; resend then if it went unanswered. ${DEDUP_RETRY_NOTE}`
|
|
654
|
+
: `Message queued for delivery to ${who} at its next turn. If it finishes before reading it, the message may not survive — ${notifyWired ? "you will be notified of its completion either way" : NO_COMPLETION_NOTICE}; resend then if it went unanswered. ${notifyWired ? "Continue with other work; do not poll. " : ""}${DEDUP_RETRY_NOTE}`;
|
|
635
655
|
return {
|
|
636
656
|
content: receiptText,
|
|
637
657
|
details: { type: "send-message", status: "delivered_running", disposition: delivered.disposition, to, task_id: targetId, summary },
|
|
@@ -639,7 +659,7 @@ export function createSendMessageTool(opts) {
|
|
|
639
659
|
}
|
|
640
660
|
if (delivered.reason === "no_channel") {
|
|
641
661
|
return {
|
|
642
|
-
content: `Message not sent: ${who} is still running and this deployment has no mid-run delivery channel for it.
|
|
662
|
+
content: `Message not sent: ${who} is still running and this deployment has no mid-run delivery channel for it. ${AWAIT_COMPLETION}, then SendMessage to continue it. ${DEDUP_RETRY_NOTE}`,
|
|
643
663
|
details: { error: "still_running", to },
|
|
644
664
|
isError: true,
|
|
645
665
|
};
|
|
@@ -705,7 +725,7 @@ export function createSendMessageTool(opts) {
|
|
|
705
725
|
}
|
|
706
726
|
if (ledger.get(resumeToolUseId)?.running === true) {
|
|
707
727
|
return {
|
|
708
|
-
content: `Message not sent: ${who} (or a prior follow-up to it) is still running —
|
|
728
|
+
content: `Message not sent: ${who} (or a prior follow-up to it) is still running — ${awaitCompletion}.`,
|
|
709
729
|
details: { error: "steering.still_running", to },
|
|
710
730
|
isError: true,
|
|
711
731
|
};
|
|
@@ -761,7 +781,9 @@ export function createSendMessageTool(opts) {
|
|
|
761
781
|
const marker = await resume(`${fromPrefix}[${safeSummary}] ${safeMessage}`);
|
|
762
782
|
return {
|
|
763
783
|
content: `Message sent — ${who} resumed in the background with its prior context intact (correlation marker [${marker}]).\n` +
|
|
764
|
-
|
|
784
|
+
(notifyWired
|
|
785
|
+
? `You will be notified automatically when it completes; its reply will carry [${marker}]. Continue with other work — do not poll.`
|
|
786
|
+
: `Its reply will carry [${marker}], but ${NO_COMPLETION_NOTICE}.`),
|
|
765
787
|
details: { type: "send-message", status: "resumed", to, task_id: targetId, marker, summary },
|
|
766
788
|
};
|
|
767
789
|
}
|
|
@@ -778,7 +800,7 @@ export function createSendMessageTool(opts) {
|
|
|
778
800
|
? `${who} reached its resume cap (${SUBAGENT_RESUME_CAP} follow-ups per agent) — relaunch a new agent instead.`
|
|
779
801
|
: code === "steering.still_running"
|
|
780
802
|
?
|
|
781
|
-
`${who} (or a prior follow-up to it) is still running —
|
|
803
|
+
`${who} (or a prior follow-up to it) is still running — ${awaitCompletion}. ${DEDUP_RETRY_NOTE}`
|
|
782
804
|
: code === "resume.row_recycling"
|
|
783
805
|
?
|
|
784
806
|
`${who}'s registry row is being adjudicated right now (a revival claim or a reap sweep holds it) — send again in a moment. ${DEDUP_RETRY_NOTE}`
|
package/dist/agents/team.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Runner } from "../core/runner/runtask.js";
|
|
2
|
-
import type { McpServerSpec, ModelRef, ModelRole, TaskResult, ToolSpec } from "../core/types.js";
|
|
2
|
+
import type { McpServerSpec, ModelRef, ModelRole, TaskResult, TaskSpec, ToolSpec } from "../core/types.js";
|
|
3
3
|
export interface TeamMember {
|
|
4
4
|
/** The member's role / specialty, e.g. "安全评审" or "performance". */
|
|
5
5
|
role: string;
|
|
@@ -99,6 +99,15 @@ export interface TeamDiscussionOptions {
|
|
|
99
99
|
denyTools?: string[];
|
|
100
100
|
mcp?: McpServerSpec[];
|
|
101
101
|
};
|
|
102
|
+
/**
|
|
103
|
+
* Per-model auth — MIRRORS {@link TaskSpec.getApiKeyAndHeaders} (forwarded into every member /
|
|
104
|
+
* summary / synthesizer run, which resolve it against their own resolved models). Without this
|
|
105
|
+
* seat a team whose member/synthesizer models live on per-model-credential routes had NO way to
|
|
106
|
+
* carry the resolver: every nested run fell to the brain's construction-time credential, which the
|
|
107
|
+
* key↔URL pairing gate refuses off the deployment root — the teacher/verify delegation family
|
|
108
|
+
* carries the same seat for the same reason.
|
|
109
|
+
*/
|
|
110
|
+
getApiKeyAndHeaders?: TaskSpec["getApiKeyAndHeaders"];
|
|
102
111
|
/** Progress callback. */
|
|
103
112
|
onEvent?: (e: TeamEvent) => void;
|
|
104
113
|
/**
|
package/dist/agents/team.js
CHANGED
|
@@ -117,6 +117,7 @@ export async function runTeamDiscussion(opts) {
|
|
|
117
117
|
limits: memberLimits,
|
|
118
118
|
signal: opts.signal,
|
|
119
119
|
...(opts.principal !== undefined ? { principal: opts.principal } : {}),
|
|
120
|
+
...(opts.getApiKeyAndHeaders !== undefined ? { getApiKeyAndHeaders: opts.getApiKeyAndHeaders } : {}),
|
|
120
121
|
}, { isDelegatedChild: true });
|
|
121
122
|
tokens += res.stats.tokens + (res.stats.nested?.tokens ?? 0);
|
|
122
123
|
turns += res.stats.turns + (res.stats.nested?.turns ?? 0);
|
package/dist/brain/anthropic.js
CHANGED
|
@@ -5,7 +5,8 @@ import { createRepetitionPoll, parseStreamedToolArgs } from "./stream-shared.js"
|
|
|
5
5
|
import { mintFallbackToolCallId } from "./tool-call-id.js";
|
|
6
6
|
import { emitBrainTelemetry, reportReasoningWireFacts } 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, lockHeader, mergeHeaders,
|
|
8
|
+
import { ANTHROPIC_RESERVED, OUTPUT_CAP_KEYS, applyExtraBody, effectiveOutputCap, lockHeader, mergeHeaders, takeHeaderCasefold } from "./request-params.js";
|
|
9
|
+
import { adjudicateModelRoute, applyRouteCredentialHeaders, createBrainRouteJudge, resolveRouteCredential, routeRefusalText } from "./route-adjudicator.js";
|
|
9
10
|
import { MIN_THINKING_TOKENS, budgetCapSkipsThinking, declaredEffortLevels, reasoningBudgetShare, reasoningRequestCarried, resolveEffort } from "./reasoning.js";
|
|
10
11
|
import { runStreamingBrain } from "./stream-engine.js";
|
|
11
12
|
function thinkingBudget(maxTokens, share, fixed, hardCap = false) {
|
|
@@ -234,7 +235,17 @@ export function createAnthropicBrain(config = {}) {
|
|
|
234
235
|
httpLabel: "anthropic",
|
|
235
236
|
stallTimeouts: options?.stallTimeouts,
|
|
236
237
|
buildRequest: (overrides) => {
|
|
237
|
-
const
|
|
238
|
+
const routeCredential = resolveRouteCredential({
|
|
239
|
+
optionsApiKey: options?.apiKey,
|
|
240
|
+
optionsHeaders: options?.headers,
|
|
241
|
+
modelHeaders: model.headers,
|
|
242
|
+
configApiKey: config.apiKey,
|
|
243
|
+
configHeaders: config.headers,
|
|
244
|
+
});
|
|
245
|
+
const routeVerdict = adjudicateModelRoute(model, routeCredential, { baseUrl: config.baseUrl });
|
|
246
|
+
if (!routeVerdict.ok)
|
|
247
|
+
throw new BrainError("invalid_request", routeRefusalText(routeVerdict));
|
|
248
|
+
const apiKey = routeCredential.apiKey;
|
|
238
249
|
const root = (model.baseUrl || config.baseUrl || "https://api.anthropic.com").replace(/\/+$/, "");
|
|
239
250
|
const nonEmptyBlocks = context.systemBlocks?.filter((b) => b.text.length > 0);
|
|
240
251
|
const system = nonEmptyBlocks?.length
|
|
@@ -302,8 +313,7 @@ export function createAnthropicBrain(config = {}) {
|
|
|
302
313
|
betas.push("interleaved-thinking-2025-05-14");
|
|
303
314
|
}
|
|
304
315
|
const headers = mergeHeaders(model.headers, config.headers, options?.headers);
|
|
305
|
-
|
|
306
|
-
stripAuthHeaders(headers);
|
|
316
|
+
applyRouteCredentialHeaders(headers, routeCredential, { model: model.headers, options: options?.headers });
|
|
307
317
|
if (betas.length > 0) {
|
|
308
318
|
const existing = (takeHeaderCasefold(headers, "anthropic-beta") ?? "").split(",").map((b) => b.trim()).filter(Boolean);
|
|
309
319
|
for (const beta of betas) {
|
|
@@ -680,5 +690,5 @@ export function createAnthropicBrain(config = {}) {
|
|
|
680
690
|
},
|
|
681
691
|
});
|
|
682
692
|
};
|
|
683
|
-
return { stream };
|
|
693
|
+
return { stream, adjudicateRoute: createBrainRouteJudge(config) };
|
|
684
694
|
}
|
|
@@ -153,5 +153,6 @@ export function createCircuitBreakerBrain(inner, opts = {}) {
|
|
|
153
153
|
})();
|
|
154
154
|
return out;
|
|
155
155
|
};
|
|
156
|
-
|
|
156
|
+
const adjudicateRoute = (model, perModelAuth) => inner.adjudicateRoute?.(model, perModelAuth);
|
|
157
|
+
return { stream, adjudicateRoute };
|
|
157
158
|
}
|
package/dist/brain/degrading.js
CHANGED
|
@@ -144,6 +144,8 @@ export function createDegradingBrain(opts) {
|
|
|
144
144
|
break;
|
|
145
145
|
if (hop.model.id === failedModelId)
|
|
146
146
|
continue;
|
|
147
|
+
if (hop.brain.adjudicateRoute?.(hop.model)?.ok === false)
|
|
148
|
+
continue;
|
|
147
149
|
chainPath.push(hop.model.id);
|
|
148
150
|
degradeInfo = {
|
|
149
151
|
from: model.id,
|
|
@@ -182,5 +184,6 @@ export function createDegradingBrain(opts) {
|
|
|
182
184
|
})();
|
|
183
185
|
return out;
|
|
184
186
|
};
|
|
185
|
-
|
|
187
|
+
const adjudicateRoute = (model, perModelAuth) => opts.primary.adjudicateRoute?.(model, perModelAuth);
|
|
188
|
+
return { stream, adjudicateRoute };
|
|
186
189
|
}
|
package/dist/brain/failover.js
CHANGED
|
@@ -82,5 +82,20 @@ export function createFailoverBrain(brains) {
|
|
|
82
82
|
})();
|
|
83
83
|
return out;
|
|
84
84
|
};
|
|
85
|
-
|
|
85
|
+
const adjudicateRoute = (model, perModelAuth) => {
|
|
86
|
+
let firstRefusal;
|
|
87
|
+
let sawUnjudged = false;
|
|
88
|
+
for (const b of brains) {
|
|
89
|
+
const verdict = b.adjudicateRoute?.(model, perModelAuth);
|
|
90
|
+
if (verdict === undefined) {
|
|
91
|
+
sawUnjudged = true;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (verdict.ok)
|
|
95
|
+
return verdict;
|
|
96
|
+
firstRefusal ??= verdict;
|
|
97
|
+
}
|
|
98
|
+
return sawUnjudged ? undefined : firstRefusal;
|
|
99
|
+
};
|
|
100
|
+
return { stream, adjudicateRoute };
|
|
86
101
|
}
|
|
@@ -5,7 +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, lockHeader, mergeHeaders
|
|
8
|
+
import { OUTPUT_CAP_KEYS, RESPONSES_RESERVED, applyExtraBody, effectiveOutputCap, lockHeader, mergeHeaders } from "./request-params.js";
|
|
9
|
+
import { adjudicateModelRoute, applyRouteCredentialHeaders, createBrainRouteJudge, resolveRouteCredential, routeRefusalText } from "./route-adjudicator.js";
|
|
9
10
|
import { mintEffortWireValue, reasoningRequestCarried } from "./reasoning.js";
|
|
10
11
|
import { runStreamingBrain } from "./stream-engine.js";
|
|
11
12
|
const DEGENERATE_POLL_CHARS = 64;
|
|
@@ -228,7 +229,17 @@ export function createOpenResponsesBrain(config = {}) {
|
|
|
228
229
|
httpLabel: "responses",
|
|
229
230
|
stallTimeouts: options?.stallTimeouts,
|
|
230
231
|
buildRequest: (overrides) => {
|
|
231
|
-
const
|
|
232
|
+
const routeCredential = resolveRouteCredential({
|
|
233
|
+
optionsApiKey: options?.apiKey,
|
|
234
|
+
optionsHeaders: options?.headers,
|
|
235
|
+
modelHeaders: model.headers,
|
|
236
|
+
configApiKey: config.apiKey,
|
|
237
|
+
configHeaders: config.headers,
|
|
238
|
+
});
|
|
239
|
+
const routeVerdict = adjudicateModelRoute(model, routeCredential, { baseUrl: config.baseUrl });
|
|
240
|
+
if (!routeVerdict.ok)
|
|
241
|
+
throw new BrainError("invalid_request", routeRefusalText(routeVerdict));
|
|
242
|
+
const apiKey = routeCredential.apiKey;
|
|
232
243
|
const root = (model.baseUrl || config.baseUrl || "").replace(/\/+$/, "");
|
|
233
244
|
if (!root) {
|
|
234
245
|
throw new Error("createOpenResponsesBrain: no baseUrl configured (set config.baseUrl or model.baseUrl)");
|
|
@@ -265,8 +276,7 @@ export function createOpenResponsesBrain(config = {}) {
|
|
|
265
276
|
if (effort !== undefined)
|
|
266
277
|
body.reasoning = { effort };
|
|
267
278
|
const headers = mergeHeaders(model.headers, config.headers, options?.headers);
|
|
268
|
-
|
|
269
|
-
stripAuthHeaders(headers);
|
|
279
|
+
applyRouteCredentialHeaders(headers, routeCredential, { model: model.headers, options: options?.headers });
|
|
270
280
|
lockHeader(headers, "content-type", "application/json");
|
|
271
281
|
if (apiKey)
|
|
272
282
|
headers["authorization"] = `Bearer ${apiKey}`;
|
|
@@ -738,5 +748,5 @@ export function createOpenResponsesBrain(config = {}) {
|
|
|
738
748
|
},
|
|
739
749
|
});
|
|
740
750
|
};
|
|
741
|
-
return { stream };
|
|
751
|
+
return { stream, adjudicateRoute: createBrainRouteJudge(config) };
|
|
742
752
|
}
|
package/dist/brain/openai.js
CHANGED
|
@@ -5,7 +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, imagesOmittedNoVisionNote, modelSupportsVision, sendableImages } from "./media-degrade.js";
|
|
8
|
-
import { OPENAI_RESERVED, OUTPUT_CAP_KEYS, applyExtraBody, effectiveOutputCap, lockHeader, mergeHeaders
|
|
8
|
+
import { OPENAI_RESERVED, OUTPUT_CAP_KEYS, applyExtraBody, effectiveOutputCap, lockHeader, mergeHeaders } from "./request-params.js";
|
|
9
|
+
import { BrainError } from "./errors.js";
|
|
10
|
+
import { adjudicateModelRoute, applyRouteCredentialHeaders, createBrainRouteJudge, resolveRouteCredential, routeRefusalText } from "./route-adjudicator.js";
|
|
9
11
|
import { mintEffortWireValue, reasoningRequestCarried } from "./reasoning.js";
|
|
10
12
|
import { runStreamingBrain } from "./stream-engine.js";
|
|
11
13
|
function closeToolCallAccum(acc) {
|
|
@@ -270,7 +272,17 @@ export function createOpenAIBrain(config = {}) {
|
|
|
270
272
|
httpLabel: "gateway",
|
|
271
273
|
stallTimeouts: options?.stallTimeouts,
|
|
272
274
|
buildRequest: (overrides) => {
|
|
273
|
-
const
|
|
275
|
+
const routeCredential = resolveRouteCredential({
|
|
276
|
+
optionsApiKey: options?.apiKey,
|
|
277
|
+
optionsHeaders: options?.headers,
|
|
278
|
+
modelHeaders: model.headers,
|
|
279
|
+
configApiKey: config.apiKey,
|
|
280
|
+
configHeaders: config.headers,
|
|
281
|
+
});
|
|
282
|
+
const routeVerdict = adjudicateModelRoute(model, routeCredential, { baseUrl: config.baseUrl });
|
|
283
|
+
if (!routeVerdict.ok)
|
|
284
|
+
throw new BrainError("invalid_request", routeRefusalText(routeVerdict));
|
|
285
|
+
const apiKey = routeCredential.apiKey;
|
|
274
286
|
const root = (model.baseUrl || config.baseUrl || "").replace(/\/+$/, "");
|
|
275
287
|
if (!root) {
|
|
276
288
|
throw new Error("createOpenAIBrain: no baseUrl configured (set config.baseUrl or model.baseUrl)");
|
|
@@ -304,8 +316,7 @@ export function createOpenAIBrain(config = {}) {
|
|
|
304
316
|
body.stop = options.stop;
|
|
305
317
|
applyThinking(body, model, options?.reasoning);
|
|
306
318
|
const headers = mergeHeaders(model.headers, config.headers, options?.headers);
|
|
307
|
-
|
|
308
|
-
stripAuthHeaders(headers);
|
|
319
|
+
applyRouteCredentialHeaders(headers, routeCredential, { model: model.headers, options: options?.headers });
|
|
309
320
|
lockHeader(headers, "content-type", "application/json");
|
|
310
321
|
if (apiKey)
|
|
311
322
|
headers["authorization"] = `Bearer ${apiKey}`;
|
|
@@ -618,5 +629,5 @@ export function createOpenAIBrain(config = {}) {
|
|
|
618
629
|
},
|
|
619
630
|
});
|
|
620
631
|
};
|
|
621
|
-
return { stream };
|
|
632
|
+
return { stream, adjudicateRoute: createBrainRouteJudge(config) };
|
|
622
633
|
}
|
|
@@ -22,17 +22,16 @@ export declare function reservedFor(api: string): ReadonlySet<string>;
|
|
|
22
22
|
* deployment with no `extraBody` is unaffected.
|
|
23
23
|
*/
|
|
24
24
|
export declare function applyExtraBody(body: Record<string, unknown>, extraBody: Record<string, unknown> | undefined, reserved: ReadonlySet<string>): Record<string, unknown>;
|
|
25
|
-
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
|
|
35
|
-
export declare function stripAuthHeaders(headers: Record<string, string>): void;
|
|
25
|
+
/** The auth-bearing header names (case-fold), single-sourced for the credential enforcement
|
|
26
|
+
* (`applyRouteCredentialHeaders`, route-adjudicator.ts — the ONE authority over auth spelling: a
|
|
27
|
+
* per-model winner strips every carrier, any case, and re-asserts the winning bag's own),
|
|
28
|
+
* {@link mergeHeaders} (which EXEMPTS them — see there), and the route credential resolver (which
|
|
29
|
+
* reads carrier PRESENCE per bag to classify a headers-borne credential). The former per-call
|
|
30
|
+
* strip helper that lived here (`stripAuthHeaders` — "per-call auth replaces construction-time
|
|
31
|
+
* auth", triggered on `options.apiKey` presence) is retired: the three brains now run the
|
|
32
|
+
* route-credential resolution instead, whose per-model arm strips a strict superset of what the
|
|
33
|
+
* helper did. */
|
|
34
|
+
export declare const AUTH_CARRIER_NAMES: ReadonlySet<string>;
|
|
36
35
|
/**
|
|
37
36
|
* #343 — the shared USER-HEADER merge layer (`model.headers` → construction `config.headers` →
|
|
38
37
|
* per-call `options.headers`, later bag wins), CASE-FOLD deduplicated: HTTP header field names are
|
|
@@ -45,26 +44,29 @@ export declare function stripAuthHeaders(headers: Record<string, string>): void;
|
|
|
45
44
|
* deployment — is byte-identical on the wire).
|
|
46
45
|
*
|
|
47
46
|
* EXEMPT: the auth carriers (`authorization` / `x-api-key`, any case) pass through with the exact
|
|
48
|
-
* legacy spread semantics (same-spelling override only, no case-fold dedup), so that
|
|
49
|
-
*
|
|
50
|
-
* second, subtly different one.
|
|
47
|
+
* legacy spread semantics (same-spelling override only, no case-fold dedup), so that the credential
|
|
48
|
+
* enforcement (`applyRouteCredentialHeaders`, route-adjudicator.ts) stays the ONE authority over
|
|
49
|
+
* auth spelling and this layer never becomes a second, subtly different one.
|
|
51
50
|
*
|
|
52
51
|
* RE-RULED, because the exemption used to be justified by a reason that does not hold: the
|
|
53
52
|
* note claimed it protected "the header-only ANTHROPIC_AUTH_TOKEN shape, which must survive under its
|
|
54
53
|
* own capital-A spelling". Dedup would not endanger that shape — it keeps the WINNER'S spelling, and a
|
|
55
54
|
* lone `Authorization` has nothing to be deduped against, so it survives either way; nor does the
|
|
56
|
-
*
|
|
57
|
-
* spelling present. Measured, not reasoned:
|
|
58
|
-
* BOTH, and the platform `Headers` fold
|
|
55
|
+
* credential-replaces flow depend on the exemption, since the enforcement already deletes every
|
|
56
|
+
* spelling present when a per-model credential wins. Measured, not reasoned:
|
|
57
|
+
* `mergeHeaders({Authorization:A},{authorization:B})` keeps BOTH, and the platform `Headers` fold
|
|
58
|
+
* sends `authorization: A, B`.
|
|
59
59
|
*
|
|
60
|
-
* STATED RESIDUAL (deliberately not fixed here)
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
60
|
+
* STATED RESIDUAL (deliberately not fixed here), NARROWED by the credential enforcement: where a
|
|
61
|
+
* PER-MODEL credential wins (an options- or entry-bag carrier), every carrier is stripped and only
|
|
62
|
+
* the winning bag's re-asserted, so a cross-layer double spelling now folds to the winner. What
|
|
63
|
+
* remains is the DEPLOYMENT-CONFIG arm — its bytes ride untouched by design — so a config bag that
|
|
64
|
+
* itself spells the SAME auth carrier two ways still ships both, comma-folded. It is held, not
|
|
65
|
+
* denied, on severity: no server accepts a comma-joined credential, so the failure is a LOUD 401
|
|
66
|
+
* attributable to the misconfiguration, whereas the non-auth case this function exists for produced
|
|
67
|
+
* a silently WRONG value (`X-Tenant: a, b` — neither writer's, the later layer's documented
|
|
68
|
+
* override defeated). Tightening it changes which credential reaches the wire, so it belongs in a
|
|
69
|
+
* window that discloses an auth-face behavior change.
|
|
68
70
|
*/
|
|
69
71
|
/**
|
|
70
72
|
* #343 (review r4) — assign a STRUCTURAL locked header under its canonical lowercase name, deleting
|
|
@@ -72,8 +74,9 @@ export declare function stripAuthHeaders(headers: Record<string, string>): void;
|
|
|
72
74
|
* the user-bag merge precisely so they "can NEVER be overridden" (council design/40) — but a valid
|
|
73
75
|
* user bag carrying `Content-Type: text/plain` survived BESIDE the lowercase lock, and the platform
|
|
74
76
|
* `Headers` fold turns the pair into `text/plain, application/json` on the wire: the lock decided
|
|
75
|
-
* nothing. Auth carriers are deliberately NOT routed through here (
|
|
76
|
-
* header-only boot flow are
|
|
77
|
+
* nothing. Auth carriers are deliberately NOT routed through here (credential replacement + the
|
|
78
|
+
* header-only boot flow are the pinned jurisdiction of `applyRouteCredentialHeaders`,
|
|
79
|
+
* route-adjudicator.ts).
|
|
77
80
|
*/
|
|
78
81
|
export declare function lockHeader(headers: Record<string, string>, lowerName: string, value: string): void;
|
|
79
82
|
/**
|
|
@@ -54,13 +54,7 @@ export function applyExtraBody(body, extraBody, reserved) {
|
|
|
54
54
|
}
|
|
55
55
|
return { ...passthrough, ...body };
|
|
56
56
|
}
|
|
57
|
-
const AUTH_CARRIER_NAMES = new Set(["authorization", "x-api-key"]);
|
|
58
|
-
export function stripAuthHeaders(headers) {
|
|
59
|
-
for (const k of Object.keys(headers)) {
|
|
60
|
-
if (AUTH_CARRIER_NAMES.has(k.toLowerCase()))
|
|
61
|
-
delete headers[k];
|
|
62
|
-
}
|
|
63
|
-
}
|
|
57
|
+
export const AUTH_CARRIER_NAMES = new Set(["authorization", "x-api-key"]);
|
|
64
58
|
export function lockHeader(headers, lowerName, value) {
|
|
65
59
|
for (const k of Object.keys(headers)) {
|
|
66
60
|
if (k !== lowerName && k.toLowerCase() === lowerName)
|