@sema-agent/core 5.55.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.
Files changed (81) hide show
  1. package/CHANGELOG.md +140 -0
  2. package/dist/agents/send-message-tool.d.ts +11 -0
  3. package/dist/agents/send-message-tool.js +81 -13
  4. package/dist/agents/subagent.js +250 -89
  5. package/dist/agents/team.d.ts +10 -1
  6. package/dist/agents/team.js +1 -0
  7. package/dist/brain/anthropic.js +15 -5
  8. package/dist/brain/circuit-breaker.js +2 -1
  9. package/dist/brain/degrading.js +4 -1
  10. package/dist/brain/failover.js +16 -1
  11. package/dist/brain/open-responses.js +15 -5
  12. package/dist/brain/openai.js +16 -5
  13. package/dist/brain/request-params.d.ts +30 -27
  14. package/dist/brain/request-params.js +1 -7
  15. package/dist/brain/route-adjudicator.d.ts +190 -0
  16. package/dist/brain/route-adjudicator.js +189 -0
  17. package/dist/brain/route-conformance.d.ts +55 -0
  18. package/dist/brain/route-conformance.js +136 -0
  19. package/dist/brain/routing.js +8 -3
  20. package/dist/core/auto-compaction.d.ts +17 -4
  21. package/dist/core/auto-compaction.js +3 -0
  22. package/dist/core/context-edit.d.ts +55 -6
  23. package/dist/core/context-edit.js +12 -1
  24. package/dist/core/hooks.d.ts +293 -11
  25. package/dist/core/hooks.js +158 -11
  26. package/dist/core/human-input-projection.d.ts +20 -2
  27. package/dist/core/human-input-projection.js +9 -0
  28. package/dist/core/mcp.js +4 -4
  29. package/dist/core/memory-engine/engine.d.ts +15 -5
  30. package/dist/core/memory-engine/engine.js +3 -1
  31. package/dist/core/permission-rule-consent.d.ts +45 -0
  32. package/dist/core/permission-rule-consent.js +40 -11
  33. package/dist/core/permission-rule-model.d.ts +110 -75
  34. package/dist/core/permission-rule-model.js +61 -28
  35. package/dist/core/permission-rules.d.ts +23 -15
  36. package/dist/core/permission-rules.js +40 -31
  37. package/dist/core/runner/prepare-task.d.ts +8 -0
  38. package/dist/core/runner/prepare-task.js +66 -26
  39. package/dist/core/runner/runtask.d.ts +4 -1
  40. package/dist/core/runner/runtask.js +206 -21
  41. package/dist/core/runner/session-rule-policy.js +5 -5
  42. package/dist/core/scheduler.d.ts +5 -0
  43. package/dist/core/session-reconcile.d.ts +32 -0
  44. package/dist/core/session-reconcile.js +15 -0
  45. package/dist/core/side-query.d.ts +12 -5
  46. package/dist/core/task-notification.d.ts +34 -7
  47. package/dist/core/task-notification.js +11 -1
  48. package/dist/core/task-registry-agent.d.ts +20 -3
  49. package/dist/core/task-registry-agent.js +31 -2
  50. package/dist/core/tool-policy.d.ts +14 -9
  51. package/dist/core/tool-policy.js +27 -22
  52. package/dist/core/types.d.ts +69 -11
  53. package/dist/core/untrusted-text.js +8 -0
  54. package/dist/engine/compaction/compaction.d.ts +77 -7
  55. package/dist/engine/compaction/compaction.js +98 -9
  56. package/dist/engine/compaction/utils.d.ts +4 -0
  57. package/dist/engine/compaction/utils.js +6 -0
  58. package/dist/engine/harness/agent-harness.d.ts +84 -0
  59. package/dist/engine/harness/agent-harness.js +114 -13
  60. package/dist/engine/harness/messages.d.ts +4 -2
  61. package/dist/engine/harness/messages.js +7 -2
  62. package/dist/engine/harness/types.d.ts +16 -6
  63. package/dist/engine/llm/types.d.ts +65 -0
  64. package/dist/engine/loop/types.d.ts +7 -0
  65. package/dist/engine/session/import-validate.js +10 -0
  66. package/dist/engine/session/session.js +2 -2
  67. package/dist/index.d.ts +4 -1
  68. package/dist/index.js +3 -1
  69. package/dist/internal/llm.d.ts +1 -1
  70. package/dist/orchestration/run-spec.js +8 -1
  71. package/dist/prompts/default.d.ts +12 -6
  72. package/dist/prompts/default.js +2 -0
  73. package/dist/scenarios/scenario-registry.d.ts +5 -1
  74. package/dist/scenarios/scenario-registry.js +4 -2
  75. package/dist/tools/fs/index.js +8 -1
  76. package/dist/tools/scheduler-tools.js +28 -6
  77. package/dist/tools/web.d.ts +15 -0
  78. package/dist/tools/web.js +8 -2
  79. package/dist/tools/worktree.js +2 -2
  80. package/package.json +1 -1
  81. package/test/export-surface.snapshot.json +19 -1
@@ -1032,6 +1032,134 @@ export function delegationEntryLedgerFootprint(registry) {
1032
1032
  handles += s.size;
1033
1033
  return { keys: byKey.size, handles };
1034
1034
  }
1035
+ const localDelegationInFlight = new WeakMap();
1036
+ function localDelegationInFlightCount(anchor, key) {
1037
+ return localDelegationInFlight.get(anchor)?.get(key) ?? 0;
1038
+ }
1039
+ function chargeLocalDelegationInFlight(anchor, key) {
1040
+ let byKey = localDelegationInFlight.get(anchor);
1041
+ if (byKey === undefined) {
1042
+ byKey = new Map();
1043
+ localDelegationInFlight.set(anchor, byKey);
1044
+ }
1045
+ byKey.set(key, (byKey.get(key) ?? 0) + 1);
1046
+ let released = false;
1047
+ return () => {
1048
+ if (released)
1049
+ return;
1050
+ released = true;
1051
+ const live = localDelegationInFlight.get(anchor);
1052
+ if (live === undefined)
1053
+ return;
1054
+ const n = (live.get(key) ?? 0) - 1;
1055
+ if (n > 0)
1056
+ live.set(key, n);
1057
+ else
1058
+ live.delete(key);
1059
+ };
1060
+ }
1061
+ const spawnAbortedResult = (wt) => ({
1062
+ isError: true,
1063
+ content: `Sub-agent not started: the delegating call was aborted.${wt ? `\n${wt}` : ""}`,
1064
+ details: { error: "aborted" },
1065
+ });
1066
+ const noopRelease = () => undefined;
1067
+ const DELEGATION_POOL_DEFAULT_SCOPE = "default";
1068
+ async function untilAbort(p, signal) {
1069
+ if (signal === undefined)
1070
+ return await p;
1071
+ if (signal.aborted)
1072
+ throw new Error("delegation entry gate: aborted before the durable floor was read");
1073
+ let onAbort;
1074
+ try {
1075
+ return await Promise.race([
1076
+ p,
1077
+ new Promise((_resolve, reject) => {
1078
+ onAbort = () => reject(new Error("delegation entry gate: aborted while reading the durable floor"));
1079
+ signal.addEventListener("abort", onAbort, { once: true });
1080
+ }),
1081
+ ]);
1082
+ }
1083
+ finally {
1084
+ if (onAbort !== undefined)
1085
+ signal.removeEventListener("abort", onAbort);
1086
+ }
1087
+ }
1088
+ async function openDelegationEntryGate(input) {
1089
+ const { poolAnchor, registry, store, root, caps, revival, signal } = input;
1090
+ if (root === undefined)
1091
+ return { key: undefined, admit: () => ({ admitted: true, release: noopRelease }) };
1092
+ const scope = input.scope ?? DELEGATION_POOL_DEFAULT_SCOPE;
1093
+ const key = JSON.stringify([scope, root]);
1094
+ let storedHandles = [];
1095
+ let storedEnumerationOk = false;
1096
+ if (store !== undefined && !revival) {
1097
+ try {
1098
+ storedHandles = (await untilAbort(store.listBySession(scope, root), signal)).map((r) => r.handle);
1099
+ storedEnumerationOk = true;
1100
+ }
1101
+ catch {
1102
+ storedHandles = [];
1103
+ }
1104
+ }
1105
+ const admit = (kind) => {
1106
+ const activeFace = registry === undefined ? undefined : registry.activeDelegationHandles;
1107
+ const active = typeof activeFace === "function" ? activeFace.call(registry, scope, root) : [];
1108
+ const local = localDelegationInFlightCount(poolAnchor, key);
1109
+ const running = active.length + local;
1110
+ if (running >= caps.maxConcurrent) {
1111
+ return {
1112
+ admitted: false,
1113
+ refusal: {
1114
+ code: "delegation.concurrency_cap",
1115
+ text: `this session tree already has ${running} agents running — the concurrency cap (${caps.maxConcurrent}; RunnerDeps.delegationEntryCaps.maxConcurrent) refuses another. ${kind === "registered" ? "Wait for one to complete (you will be notified) or stop one, then relaunch." : "Wait for one of them to finish or stop one, then try again."}`,
1116
+ },
1117
+ };
1118
+ }
1119
+ if (!revival) {
1120
+ const ledger = delegationEntryLedger(poolAnchor, key);
1121
+ let retained;
1122
+ if (store !== undefined && storedEnumerationOk) {
1123
+ const keep = new Set([...storedHandles, ...active]);
1124
+ if (kind === "registered") {
1125
+ for (const h of [...ledger])
1126
+ if (!keep.has(h))
1127
+ ledger.delete(h);
1128
+ retained = new Set([...storedHandles, ...ledger]).size;
1129
+ }
1130
+ else {
1131
+ const survivors = new Set(storedHandles);
1132
+ for (const h of ledger)
1133
+ if (keep.has(h))
1134
+ survivors.add(h);
1135
+ retained = survivors.size;
1136
+ }
1137
+ }
1138
+ else {
1139
+ retained = ledger.size;
1140
+ }
1141
+ const cumulative = retained + local;
1142
+ if (cumulative >= caps.maxCumulativePerSession) {
1143
+ return {
1144
+ admitted: false,
1145
+ refusal: {
1146
+ code: "delegation.session_cap",
1147
+ text: `this session tree has already launched ${cumulative} agents in its retained window — the cumulative cap (${caps.maxCumulativePerSession}; RunnerDeps.delegationEntryCaps.maxCumulativePerSession) refuses more. Continue an existing agent (SendMessage) instead of launching new ones.`,
1148
+ },
1149
+ };
1150
+ }
1151
+ }
1152
+ return { admitted: true, release: kind === "local" ? chargeLocalDelegationInFlight(poolAnchor, key) : noopRelease };
1153
+ };
1154
+ return { key, admit };
1155
+ }
1156
+ function chargeDelegationEntryHandle(registry, key, store, handle) {
1157
+ if (handle !== undefined)
1158
+ delegationEntryLedger(registry, key).add(handle);
1159
+ bindDelegationLedgerKeyStore(registry, key, store);
1160
+ armDelegationLedgerLifecycle(registry);
1161
+ sweepDelegationLedgerRound(registry, 3);
1162
+ }
1035
1163
  export function normalizeSubagentType(value) {
1036
1164
  return value.normalize("NFKC").toLowerCase().replace(/[\p{White_Space}\p{Pd}_]+/gu, "");
1037
1165
  }
@@ -1314,7 +1442,6 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1314
1442
  prepareArguments: (args) => foldGeneralPurposeAlias(args, generalPurposeShadowed),
1315
1443
  execute: async (args, ctx) => {
1316
1444
  const a = foldGeneralPurposeAlias(args, generalPurposeShadowed);
1317
- const wantsBackground = opts.background !== undefined && a.run_in_background !== false;
1318
1445
  const reviveClaim = ctx.reviveClaim;
1319
1446
  if (reviveClaim !== undefined && opts.background === undefined) {
1320
1447
  return {
@@ -1358,6 +1485,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1358
1485
  }
1359
1486
  const omitted = rawType === "" || (!generalPurposeShadowed && rawType === GENERAL_PURPOSE_SUBAGENT_TYPE);
1360
1487
  const def = hasAgents && !wantsFork && !omitted ? agentMap.get(rawType) : undefined;
1488
+ const wantsBackground = opts.background !== undefined && (a.run_in_background !== false || def?.background === true);
1361
1489
  const spawnAgentType = wantsFork ? FORK_SUBAGENT_TYPE : omitted ? GENERAL_PURPOSE_SUBAGENT_TYPE : rawType;
1362
1490
  if (!def && !wantsFork && !omitted) {
1363
1491
  return {
@@ -1789,6 +1917,21 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1789
1917
  return { ...(recentSteps ? { recentSteps } : {}), ...(editedFiles ? { editedFiles } : {}) };
1790
1918
  };
1791
1919
  const treeScope = reviveClaim?.row.scope ?? ctx.principal ?? opts.background?.scope;
1920
+ const entryCapPoolAnchor = opts.background?.registry ?? opts;
1921
+ const entryCaps = ctx.delegationEntryCaps ?? { maxConcurrent: DELEGATION_MAX_CONCURRENT_DEFAULT, maxCumulativePerSession: DELEGATION_MAX_PER_SESSION_DEFAULT };
1922
+ const entryCapRoot = reviveClaim !== undefined
1923
+ ? (reviveClaim.row.rootSessionId ?? reviveClaim.row.parentSessionId ?? (reviveClaim.row.sessionScoped ? reviveClaim.row.owner : undefined))
1924
+ : (ctx.rootSessionId ?? ctx.sessionId);
1925
+ const openEntryGate = () => openDelegationEntryGate({
1926
+ poolAnchor: entryCapPoolAnchor,
1927
+ registry: opts.background?.registry,
1928
+ store: opts.background?.agentStore,
1929
+ scope: treeScope,
1930
+ root: entryCapRoot,
1931
+ caps: entryCaps,
1932
+ revival: reviveClaim !== undefined,
1933
+ signal: ctx.signal,
1934
+ });
1792
1935
  const provenanceRequest = ctx.delegationProvenanceForChildren?.();
1793
1936
  const childProvenanceRef = provenanceRequest !== undefined ? { current: newDelegationProvenanceAggregate() } : undefined;
1794
1937
  const childAttestation = (status) => childProvenanceRef !== undefined ? reduceDelegationAttestation(childProvenanceRef.current, { completed: status === "completed" }) : undefined;
@@ -2053,7 +2196,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2053
2196
  if (ctx.signal?.aborted) {
2054
2197
  await cancelObserver();
2055
2198
  const wt = await finishWorktree();
2056
- return { isError: true, content: `Sub-agent not started: the delegating call was aborted.${wt ? `\n${wt}` : ""}`, details: { error: "aborted" } };
2199
+ return spawnAbortedResult(wt);
2057
2200
  }
2058
2201
  if (spawnVerdict.kind === "block") {
2059
2202
  await cancelObserver();
@@ -2106,6 +2249,21 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2106
2249
  const shortDesc = `fork: ${(typeof a.description === "string" && a.description.trim() ? a.description.trim() : prompt).slice(0, 180)}`;
2107
2250
  const bgOwner = sessionScopedBg ? ctx.sessionId : ctx.taskId ?? bg.owner;
2108
2251
  const bgScope = treeScope;
2252
+ const forkEntryGate = await openEntryGate();
2253
+ {
2254
+ const admission = forkEntryGate.admit("registered");
2255
+ if (!admission.admitted) {
2256
+ dropHostAbortListener();
2257
+ try {
2258
+ await (releaseForked ? releaseForked() : opts.runner.sessions.release?.(forkedId));
2259
+ }
2260
+ catch {
2261
+ }
2262
+ await cancelObserver();
2263
+ const wt = await finishWorktree();
2264
+ return errorResult(`Sub-agent not started in background: ${admission.refusal.text}${wt ? `\n${wt}` : ""}`, { error: admission.refusal.code, code: admission.refusal.code });
2265
+ }
2266
+ }
2109
2267
  const forkSettlementSeat = ctx.delegationSettlement?.();
2110
2268
  const forkSettleId = forkSettlementSeat !== undefined ? `bg-${uuidv7()}` : undefined;
2111
2269
  if (forkSettlementSeat !== undefined && forkSettleId !== undefined) {
@@ -2195,6 +2353,8 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2195
2353
  }
2196
2354
  return { isError: true, content: `Sub-agent not started in background: ${e instanceof Error ? e.message : String(e)}${wt ? `\n${wt}` : ""}`, details: { error: "register_failed" } };
2197
2355
  }
2356
+ if (forkEntryGate.key !== undefined)
2357
+ chargeDelegationEntryHandle(bg.registry, forkEntryGate.key, bg.agentStore, taskId);
2198
2358
  childInternals.peerSelfRef?.addAxis("h", taskId);
2199
2359
  bg.registry.bindBackgroundAgentSession(taskId, forkedId);
2200
2360
  const forkBgHangAt = Date.now();
@@ -2507,6 +2667,28 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2507
2667
  const bgIgnoredNote = a.run_in_background === true
2508
2668
  ? `note: run_in_background was ignored for subagent_type "${FORK_SUBAGENT_TYPE}" — the fork ran synchronously (this delegation tool has no background surface configured).`
2509
2669
  : undefined;
2670
+ const syncForkEntryGate = await openEntryGate();
2671
+ if (ctx.signal?.aborted) {
2672
+ try {
2673
+ await (releaseForked ? releaseForked() : opts.runner.sessions.release?.(forkedId));
2674
+ }
2675
+ catch {
2676
+ }
2677
+ await cancelObserver();
2678
+ const wt = await finishWorktree();
2679
+ return spawnAbortedResult(wt);
2680
+ }
2681
+ const syncForkAdmission = syncForkEntryGate.admit("local");
2682
+ if (!syncForkAdmission.admitted) {
2683
+ try {
2684
+ await (releaseForked ? releaseForked() : opts.runner.sessions.release?.(forkedId));
2685
+ }
2686
+ catch {
2687
+ }
2688
+ await cancelObserver();
2689
+ const wt = await finishWorktree();
2690
+ return errorResult(`Sub-agent not started: ${syncForkAdmission.refusal.text}${wt ? `\n${wt}` : ""}`, { error: syncForkAdmission.refusal.code, code: syncForkAdmission.refusal.code });
2691
+ }
2510
2692
  let forkChild;
2511
2693
  try {
2512
2694
  forkChild = await opts.runner.runTask({ ...buildChildSpec(ctx.signal), sessionId: forkedId, requireExistingSession: true, objective: forkObjective }, forkInternals);
@@ -2527,6 +2709,9 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2527
2709
  const wt = await finishWorktree();
2528
2710
  return { isError: true, content: `Fork failed: the forked run threw (${e instanceof Error ? e.message : String(e)}).${wt ? `\n${wt}` : ""}`, details: { error: "fork run failed" } };
2529
2711
  }
2712
+ finally {
2713
+ syncForkAdmission.release();
2714
+ }
2530
2715
  const forkHandbackWarning = await reviewHandback({
2531
2716
  review: ctx.autoModeReview,
2532
2717
  toolCallId: ctx.toolCallId,
@@ -2601,51 +2786,15 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2601
2786
  const shortDesc = reviveRow?.description ?? String(a.description ?? "sub-agent").slice(0, 200);
2602
2787
  const bgOwner = reviveRow !== undefined ? reviveRow.owner : sessionScopedBg ? ctx.sessionId : ctx.taskId ?? bg.owner;
2603
2788
  const bgScope = treeScope;
2604
- const capScope = bgScope;
2605
- const capRoot = reviveRow !== undefined
2606
- ? (reviveRow.rootSessionId ?? reviveRow.parentSessionId ?? (reviveRow.sessionScoped ? reviveRow.owner : undefined))
2607
- : (ctx.rootSessionId ?? ctx.sessionId);
2608
- const entryCaps = ctx.delegationEntryCaps ?? { maxConcurrent: DELEGATION_MAX_CONCURRENT_DEFAULT, maxCumulativePerSession: DELEGATION_MAX_PER_SESSION_DEFAULT };
2609
- const capLedgerKey = capScope !== undefined && capRoot !== undefined ? JSON.stringify([capScope, capRoot]) : undefined;
2610
- if (capScope !== undefined && capRoot !== undefined) {
2611
- let storedHandles = [];
2612
- let storedEnumerationOk = false;
2613
- if (bg.agentStore !== undefined && reviveRow === undefined) {
2614
- try {
2615
- storedHandles = (await bg.agentStore.listBySession(capScope, capRoot)).map((r) => r.handle);
2616
- storedEnumerationOk = true;
2617
- }
2618
- catch {
2619
- storedHandles = [];
2620
- }
2621
- }
2622
- const capRefusal = async (code, text) => {
2789
+ const bgEntryGate = await openEntryGate();
2790
+ const capLedgerKey = bgEntryGate.key;
2791
+ {
2792
+ const admission = bgEntryGate.admit("registered");
2793
+ if (!admission.admitted) {
2623
2794
  dropHostAbortListener();
2624
2795
  await cancelObserver();
2625
2796
  const wt = await finishWorktree();
2626
- return errorResult(`Sub-agent not started in background: ${text}${wt ? `\n${wt}` : ""}`, { error: code, code });
2627
- };
2628
- const activeFace = bg.registry.activeDelegationHandles;
2629
- const active = typeof activeFace === "function" ? activeFace.call(bg.registry, capScope, capRoot) : [];
2630
- if (typeof activeFace === "function" && active.length >= entryCaps.maxConcurrent) {
2631
- return await capRefusal("delegation.concurrency_cap", `this session tree already has ${active.length} background agents running — the concurrency cap (${entryCaps.maxConcurrent}; RunnerDeps.delegationEntryCaps.maxConcurrent) refuses another. Wait for one to complete (you will be notified) or stop one, then relaunch.`);
2632
- }
2633
- if (reviveRow === undefined) {
2634
- const ledger = delegationEntryLedger(bg.registry, capLedgerKey);
2635
- let cumulative;
2636
- if (bg.agentStore !== undefined && storedEnumerationOk) {
2637
- const keep = new Set([...storedHandles, ...active]);
2638
- for (const h of [...ledger])
2639
- if (!keep.has(h))
2640
- ledger.delete(h);
2641
- cumulative = new Set([...storedHandles, ...ledger]).size;
2642
- }
2643
- else {
2644
- cumulative = ledger.size;
2645
- }
2646
- if (cumulative >= entryCaps.maxCumulativePerSession) {
2647
- return await capRefusal("delegation.session_cap", `this session tree has already launched ${cumulative} background agents in its retained window — the cumulative cap (${entryCaps.maxCumulativePerSession}; RunnerDeps.delegationEntryCaps.maxCumulativePerSession) refuses more. Continue an existing agent (SendMessage) instead of launching new ones.`);
2648
- }
2797
+ return errorResult(`Sub-agent not started in background: ${admission.refusal.text}${wt ? `\n${wt}` : ""}`, { error: admission.refusal.code, code: admission.refusal.code });
2649
2798
  }
2650
2799
  }
2651
2800
  const settlementSeat = ctx.delegationSettlement?.();
@@ -2770,13 +2919,8 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2770
2919
  }
2771
2920
  return { isError: true, content: `Sub-agent not started in background: ${e instanceof Error ? e.message : String(e)}${wt ? `\n${wt}` : ""}`, details: { error: "register_failed" } };
2772
2921
  }
2773
- if (capLedgerKey !== undefined && reviveRow === undefined)
2774
- delegationEntryLedger(bg.registry, capLedgerKey).add(taskId);
2775
- if (capLedgerKey !== undefined) {
2776
- bindDelegationLedgerKeyStore(bg.registry, capLedgerKey, bg.agentStore);
2777
- armDelegationLedgerLifecycle(bg.registry);
2778
- sweepDelegationLedgerRound(bg.registry, 3);
2779
- }
2922
+ if (capLedgerKey !== undefined)
2923
+ chargeDelegationEntryHandle(bg.registry, capLedgerKey, bg.agentStore, reviveRow === undefined ? taskId : undefined);
2780
2924
  childInternals.peerSelfRef?.addAxis("h", taskId);
2781
2925
  if (agentName !== undefined) {
2782
2926
  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" }));
@@ -3411,49 +3555,66 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
3411
3555
  };
3412
3556
  }
3413
3557
  let child;
3558
+ const syncEntryGate = await openEntryGate();
3559
+ if (ctx.signal?.aborted) {
3560
+ await cancelObserver();
3561
+ const wt = await finishWorktree();
3562
+ return spawnAbortedResult(wt);
3563
+ }
3564
+ const syncAdmission = syncEntryGate.admit("local");
3565
+ if (!syncAdmission.admitted) {
3566
+ await cancelObserver();
3567
+ const wt = await finishWorktree();
3568
+ return errorResult(`Sub-agent not started: ${syncAdmission.refusal.text}${wt ? `\n${wt}` : ""}`, { error: syncAdmission.refusal.code, code: syncAdmission.refusal.code });
3569
+ }
3414
3570
  const syncStartedAt = Date.now();
3415
3571
  let retainEntry;
3416
- if (ctx.onSubagentSpawn || observerPairing) {
3417
- retainEntry = ctx.onSubagentSpawn ? await tryRetainChild(ctx.subagentRetain) : undefined;
3418
- if (retainEntry)
3419
- stepRecorder.lockTo(retainEntry.childSessionId);
3420
- const stream = opts.runner.runTaskStream({ ...buildChildSpec(ctx.signal), ...(retainEntry ? { sessionId: retainEntry.childSessionId } : {}) }, undefined, childInternals);
3421
- observedSteerRef.steer = (text, o) => stream.steer(text, o);
3422
- let settleResolve;
3423
- const settled = new Promise((r) => { settleResolve = r; });
3424
- if (ctx.onSubagentSpawn) {
3425
- const resume = createSubagentResume({
3426
- ledger: ctx.subagentRetain,
3427
- parentToolCallId: ctx.toolCallId,
3428
- runner: opts.runner,
3429
- ...(opts.background?.notify ? { notify: opts.background.notify } : {}),
3430
- sink: ctx.onSubagentSpawn,
3431
- ...(opts.background ? { registry: opts.background.registry } : {}),
3432
- ...(opts.onObserverError !== undefined ? { onNotifyError: (f) => opts.onObserverError?.(f.error, { site: f.site }) } : {}),
3433
- ...(ctx.onQuestion !== undefined ? { currentOnQuestion: ctx.onQuestion } : {}),
3434
- ...(ctx.autoModeReview !== undefined ? { currentAutoModeReview: ctx.autoModeReview } : {}),
3435
- });
3436
- notifier.notify(() => ctx.onSubagentSpawn?.(createSteerHandle(stream, ctx.toolCallId, childAgentName, settled, {
3437
- resume,
3438
- ...(retainEntry ? { childSessionId: retainEntry.childSessionId } : {}),
3439
- })), "subagent.onSubagentSpawn");
3440
- }
3441
- try {
3442
- for await (const ev of stream)
3443
- observerTap?.record(ev);
3444
- child = await stream.result();
3445
- }
3446
- catch (e) {
3447
- await settleObserver("failed");
3448
- throw e;
3572
+ try {
3573
+ if (ctx.onSubagentSpawn || observerPairing) {
3574
+ retainEntry = ctx.onSubagentSpawn ? await tryRetainChild(ctx.subagentRetain) : undefined;
3575
+ if (retainEntry)
3576
+ stepRecorder.lockTo(retainEntry.childSessionId);
3577
+ const stream = opts.runner.runTaskStream({ ...buildChildSpec(ctx.signal), ...(retainEntry ? { sessionId: retainEntry.childSessionId } : {}) }, undefined, childInternals);
3578
+ observedSteerRef.steer = (text, o) => stream.steer(text, o);
3579
+ let settleResolve;
3580
+ const settled = new Promise((r) => { settleResolve = r; });
3581
+ if (ctx.onSubagentSpawn) {
3582
+ const resume = createSubagentResume({
3583
+ ledger: ctx.subagentRetain,
3584
+ parentToolCallId: ctx.toolCallId,
3585
+ runner: opts.runner,
3586
+ ...(opts.background?.notify ? { notify: opts.background.notify } : {}),
3587
+ sink: ctx.onSubagentSpawn,
3588
+ ...(opts.background ? { registry: opts.background.registry } : {}),
3589
+ ...(opts.onObserverError !== undefined ? { onNotifyError: (f) => opts.onObserverError?.(f.error, { site: f.site }) } : {}),
3590
+ ...(ctx.onQuestion !== undefined ? { currentOnQuestion: ctx.onQuestion } : {}),
3591
+ ...(ctx.autoModeReview !== undefined ? { currentAutoModeReview: ctx.autoModeReview } : {}),
3592
+ });
3593
+ notifier.notify(() => ctx.onSubagentSpawn?.(createSteerHandle(stream, ctx.toolCallId, childAgentName, settled, {
3594
+ resume,
3595
+ ...(retainEntry ? { childSessionId: retainEntry.childSessionId } : {}),
3596
+ })), "subagent.onSubagentSpawn");
3597
+ }
3598
+ try {
3599
+ for await (const ev of stream)
3600
+ observerTap?.record(ev);
3601
+ child = await stream.result();
3602
+ }
3603
+ catch (e) {
3604
+ await settleObserver("failed");
3605
+ throw e;
3606
+ }
3607
+ finally {
3608
+ settleResolve();
3609
+ ctx.subagentRetain?.markSettled(ctx.toolCallId);
3610
+ }
3449
3611
  }
3450
- finally {
3451
- settleResolve();
3452
- ctx.subagentRetain?.markSettled(ctx.toolCallId);
3612
+ else {
3613
+ child = await opts.runner.runTask(buildChildSpec(ctx.signal), childInternals);
3453
3614
  }
3454
3615
  }
3455
- else {
3456
- child = await opts.runner.runTask(buildChildSpec(ctx.signal), childInternals);
3616
+ finally {
3617
+ syncAdmission.release();
3457
3618
  }
3458
3619
  await settleObserver(child.status);
3459
3620
  const handbackWarning = await reviewHandback({
@@ -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
  /**
@@ -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);
@@ -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, stripAuthHeaders, takeHeaderCasefold } from "./request-params.js";
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 apiKey = options?.apiKey ?? config.apiKey;
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
- if (options?.apiKey !== undefined)
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
- return { stream };
156
+ const adjudicateRoute = (model, perModelAuth) => inner.adjudicateRoute?.(model, perModelAuth);
157
+ return { stream, adjudicateRoute };
157
158
  }
@@ -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
- return { stream };
187
+ const adjudicateRoute = (model, perModelAuth) => opts.primary.adjudicateRoute?.(model, perModelAuth);
188
+ return { stream, adjudicateRoute };
186
189
  }
@@ -82,5 +82,20 @@ export function createFailoverBrain(brains) {
82
82
  })();
83
83
  return out;
84
84
  };
85
- return { stream };
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, stripAuthHeaders } from "./request-params.js";
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 apiKey = options?.apiKey ?? config.apiKey;
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
- if (options?.apiKey !== undefined)
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
  }