@sema-agent/core 5.1.0 → 5.3.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 (66) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/agents/roster-store.js +6 -1
  3. package/dist/bin/sema-tb.js +3 -4
  4. package/dist/brain/openai.js +3 -5
  5. package/dist/brain/terminal-cause.d.ts +1 -1
  6. package/dist/core/a2a-task-state.d.ts +15 -0
  7. package/dist/core/a2a-task-state.js +68 -0
  8. package/dist/core/a2a.d.ts +42 -0
  9. package/dist/core/a2a.js +651 -0
  10. package/dist/core/checkpoint-store.d.ts +6 -1
  11. package/dist/core/checkpoint-store.js +3 -1
  12. package/dist/core/hooks.js +8 -3
  13. package/dist/core/mcp.d.ts +12 -5
  14. package/dist/core/mcp.js +11 -31
  15. package/dist/core/memory-engine/dual-root.js +2 -1
  16. package/dist/core/memory-engine/engine.d.ts +4 -0
  17. package/dist/core/memory-engine/engine.js +25 -6
  18. package/dist/core/memory-engine/file-backend.d.ts +7 -5
  19. package/dist/core/memory-engine/file-backend.js +2 -2
  20. package/dist/core/memory-engine/index.d.ts +1 -1
  21. package/dist/core/memory-engine/index.js +1 -1
  22. package/dist/core/memory-engine/layout.d.ts +6 -2
  23. package/dist/core/memory-engine/layout.js +73 -31
  24. package/dist/core/memory.js +6 -0
  25. package/dist/core/protocol-naming.d.ts +7 -0
  26. package/dist/core/protocol-naming.js +32 -0
  27. package/dist/core/protocol-table.d.ts +13 -8
  28. package/dist/core/protocol-table.js +34 -15
  29. package/dist/core/runner/prepare-memory.js +4 -4
  30. package/dist/core/runner/prepare-task.d.ts +7 -0
  31. package/dist/core/runner/prepare-task.js +73 -28
  32. package/dist/core/runner/runtask.js +42 -9
  33. package/dist/core/runner/tool-disclosure.d.ts +1 -1
  34. package/dist/core/runner/tool-disclosure.js +7 -2
  35. package/dist/core/runner/turn-attachments.d.ts +1 -2
  36. package/dist/core/runner/turn-attachments.js +1 -12
  37. package/dist/core/store-contracts/background-agent-store-contract.d.ts +5 -0
  38. package/dist/core/store-contracts/background-agent-store-contract.js +213 -0
  39. package/dist/core/task-registry-agent.d.ts +14 -1
  40. package/dist/core/task-registry-agent.js +1 -1
  41. package/dist/core/tool-policy.d.ts +1 -0
  42. package/dist/core/tool-policy.js +12 -2
  43. package/dist/core/types.d.ts +16 -2
  44. package/dist/index.d.ts +10 -5
  45. package/dist/index.js +7 -3
  46. package/dist/orchestration/run-workflow-tool.js +5 -1
  47. package/dist/prompt-assembly/assemble.js +0 -1
  48. package/dist/prompt-assembly/event-registry.js +1 -0
  49. package/dist/stores/cc/mailbox-store.js +58 -14
  50. package/dist/stores/file/file-snapshot-store.d.ts +9 -1
  51. package/dist/stores/file/file-snapshot-store.js +28 -5
  52. package/dist/stores/file/fs-atomic.d.ts +4 -1
  53. package/dist/stores/file/fs-atomic.js +2 -1
  54. package/dist/stores/file/index.d.ts +10 -3
  55. package/dist/stores/file/index.js +4 -3
  56. package/dist/stores/file/mailbox-store.d.ts +5 -0
  57. package/dist/stores/file/mailbox-store.js +15 -3
  58. package/dist/stores/file/session-policy-store.d.ts +11 -8
  59. package/dist/stores/file/session-policy-store.js +21 -4
  60. package/dist/stores/file/session-store.d.ts +9 -1
  61. package/dist/stores/file/session-store.js +19 -4
  62. package/dist/tools/fs/fs-search-tools.js +9 -0
  63. package/dist/tools/fs/fs-shared.d.ts +1 -1
  64. package/dist/tools/fs/fs-shared.js +1 -1
  65. package/dist/tools/todo.js +13 -6
  66. package/package.json +1 -1
@@ -1,14 +1,19 @@
1
- export type ProtocolId = "mcp";
2
- export interface ProtocolNamespace {
1
+ export type ProtocolId = "mcp" | "a2a";
2
+ export interface ProtocolPeerTool {
3
+ peer: string;
4
+ tool: string;
5
+ }
6
+ export interface ProtocolNamespace<P extends string = string> {
3
7
  readonly id: ProtocolId;
4
- readonly prefix: string;
8
+ readonly prefix: P;
5
9
  makeName(peer: string, tool: string): string;
6
- parse(name: string): {
7
- peer: string;
8
- tool: string;
9
- } | undefined;
10
+ parse(name: string): ProtocolPeerTool | undefined;
10
11
  displayGroupKey(name: string): string;
11
12
  }
12
- export declare const MCP_NAMESPACE: ProtocolNamespace;
13
+ declare const MCP_PREFIX_NAME: "mcp__";
14
+ declare const A2A_PREFIX_NAME: "a2a__";
15
+ export declare const MCP_NAMESPACE: ProtocolNamespace<typeof MCP_PREFIX_NAME>;
16
+ export declare const A2A_NAMESPACE: ProtocolNamespace<typeof A2A_PREFIX_NAME>;
13
17
  export declare const PROTOCOL_TABLE: readonly ProtocolNamespace[];
14
18
  export declare function protocolOf(name: string): ProtocolNamespace | undefined;
19
+ export {};
@@ -1,23 +1,42 @@
1
1
  const NAME_SEP = "__";
2
- export const MCP_NAMESPACE = {
3
- id: "mcp",
4
- prefix: `mcp${NAME_SEP}`,
5
- makeName: (peer, tool) => `mcp${NAME_SEP}${peer}${NAME_SEP}${tool}`,
6
- parse(name) {
7
- if (!name.startsWith(this.prefix))
2
+ function makeProtocolNamespace(id, prefix) {
3
+ const parse = (name) => {
4
+ if (!name.startsWith(prefix))
8
5
  return undefined;
9
- const rest = name.slice(this.prefix.length);
6
+ const rest = name.slice(prefix.length);
10
7
  const sep = rest.indexOf(NAME_SEP);
11
8
  if (sep <= 0)
12
9
  return undefined;
13
- return { peer: rest.slice(0, sep), tool: rest.slice(sep + NAME_SEP.length) };
14
- },
15
- displayGroupKey(name) {
16
- const p = this.parse(name);
17
- return p === undefined ? name : `${this.prefix}${p.peer}${NAME_SEP}*`;
18
- },
19
- };
20
- export const PROTOCOL_TABLE = [MCP_NAMESPACE];
10
+ const tool = rest.slice(sep + NAME_SEP.length);
11
+ if (tool.length === 0)
12
+ return undefined;
13
+ return { peer: rest.slice(0, sep), tool };
14
+ };
15
+ const makeName = (peer, tool) => {
16
+ if (peer.length === 0 || tool.length === 0) {
17
+ throw new Error(`protocol table (${id}): peer and tool must both be non-empty (got peer=${JSON.stringify(peer)}, tool=${JSON.stringify(tool)})`);
18
+ }
19
+ if (peer.includes(NAME_SEP)) {
20
+ throw new Error(`protocol table (${id}): peer must not contain the ${NAME_SEP} separator (got ${JSON.stringify(peer)}) — the composed name would parse back as a DIFFERENT peer`);
21
+ }
22
+ return `${prefix}${peer}${NAME_SEP}${tool}`;
23
+ };
24
+ return {
25
+ id,
26
+ prefix,
27
+ makeName,
28
+ parse,
29
+ displayGroupKey: (name) => {
30
+ const p = parse(name);
31
+ return p === undefined ? name : `${prefix}${p.peer}${NAME_SEP}*`;
32
+ },
33
+ };
34
+ }
35
+ const MCP_PREFIX_NAME = `mcp${NAME_SEP}`;
36
+ const A2A_PREFIX_NAME = `a2a${NAME_SEP}`;
37
+ export const MCP_NAMESPACE = makeProtocolNamespace("mcp", MCP_PREFIX_NAME);
38
+ export const A2A_NAMESPACE = makeProtocolNamespace("a2a", A2A_PREFIX_NAME);
39
+ export const PROTOCOL_TABLE = [MCP_NAMESPACE, A2A_NAMESPACE];
21
40
  export function protocolOf(name) {
22
41
  return PROTOCOL_TABLE.find((ns) => name.startsWith(ns.prefix));
23
42
  }
@@ -1,4 +1,4 @@
1
- import { adoptLegacyRepoDirs, deriveControlPlaneDir, deriveProjectControlDir, deriveProjectMemoryDir, deriveRepoMemoryDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, lookupProjectIdHint, recordProjectIdHint, resolveMemoryEngineRoot } from "../memory-engine/layout.js";
1
+ import { adoptLegacyRepoDirs, deriveRepoControlPlaneDir, deriveProjectControlDir, deriveProjectMemoryDir, deriveRepoMemoryDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, lookupProjectIdHint, recordProjectIdHint, resolveMemoryEngineRoot } from "../memory-engine/layout.js";
2
2
  import { classifyScopePlanes, derivePersonalControlDir, derivePersonalMemoryDir, mergeHarvestReports, mergeInjections, needsDualRoots, parsedProjectPlane } from "../memory-engine/dual-root.js";
3
3
  import { normalizeMemorySpec } from "../memory.js";
4
4
  import { MemoryEngine } from "../memory-engine/engine.js";
@@ -38,7 +38,7 @@ export async function prepareMemory(input) {
38
38
  recordProjectIdHint(engineRoot, repoRoot, marker.projectId);
39
39
  if (firstSwitch) {
40
40
  try {
41
- const oldCtl = deriveControlPlaneDir(engineRoot, repoRoot);
41
+ const oldCtl = deriveRepoControlPlaneDir(engineRoot, repoRoot);
42
42
  const newCtl = deriveProjectControlDir(engineRoot, marker.projectId);
43
43
  const drained = drainMemoryAnnouncements(oldCtl);
44
44
  for (const ann of drained.queue)
@@ -100,7 +100,7 @@ export async function prepareMemory(input) {
100
100
  const projectEngine = new MemoryEngine({
101
101
  backend,
102
102
  memoryDir,
103
- controlDir: identityKey !== undefined ? deriveProjectControlDir(engineRoot, identityKey) : deriveControlPlaneDir(engineRoot, repoRoot),
103
+ controlDir: identityKey !== undefined ? deriveProjectControlDir(engineRoot, identityKey) : deriveRepoControlPlaneDir(engineRoot, repoRoot),
104
104
  });
105
105
  const personalEngine = createPersonalEngine();
106
106
  const p = planes;
@@ -140,7 +140,7 @@ export async function prepareMemory(input) {
140
140
  const engine = new MemoryEngine({
141
141
  backend,
142
142
  memoryDir,
143
- controlDir: identityKey !== undefined ? deriveProjectControlDir(engineRoot, identityKey) : deriveControlPlaneDir(engineRoot, repoRoot),
143
+ controlDir: identityKey !== undefined ? deriveProjectControlDir(engineRoot, identityKey) : deriveRepoControlPlaneDir(engineRoot, repoRoot),
144
144
  });
145
145
  const handle = await engine.materialize(memorySpec.scopes, memorySpec.writeScope);
146
146
  writeEngine = engine;
@@ -1,6 +1,7 @@
1
1
  import { AgentHarness, type ThinkingLevel } from "../../internal/harness.js";
2
2
  import type { Model } from "../../internal/llm.js";
3
3
  import { type MaterializedMcp } from "../mcp.js";
4
+ import { type MaterializedA2a } from "../a2a.js";
4
5
  import type { HarvestReport, MemorySessionHandle } from "../memory-engine/types.js";
5
6
  import { StoredSession } from "../session.js";
6
7
  import type { SessionStore } from "../session.js";
@@ -15,6 +16,7 @@ import type { MemoryEngine } from "../memory-engine/engine.js";
15
16
  import { type ToolManifestRow } from "../../prompt-assembly/tool-catalog.js";
16
17
  import type { TaskNotificationPayload } from "../task-notification.js";
17
18
  import { type CwdRef } from "../../tools/fs/index.js";
19
+ import { type WorkflowSizeGuideline } from "../../orchestration/workflow-size-guideline.js";
18
20
  import type { Runner } from "./runtask.js";
19
21
  import { type CheckpointGate, type CheckpointState, type CheckpointToken, type ResourceLedger, type ResourceLimitReason } from "../checkpoint-store.js";
20
22
  import type { ActiveWorktreeSession, AgentMessage, AgentTool, ExecutionEnv } from "../../internal/harness.js";
@@ -36,6 +38,7 @@ export interface Prepared {
36
38
  thinking?: ThinkingLevel;
37
39
  compModel?: Model;
38
40
  mcp: MaterializedMcp;
41
+ a2a?: MaterializedA2a;
39
42
  blockedRef: BlockedRef;
40
43
  outputRef: OutputRef;
41
44
  abortController: AbortController;
@@ -186,6 +189,10 @@ export interface Prepared {
186
189
  path: string;
187
190
  contentHash: string | null;
188
191
  }>;
192
+ workflowSizeGuideline?: {
193
+ legGuideline: WorkflowSizeGuideline;
194
+ current: () => WorkflowSizeGuideline;
195
+ };
189
196
  detectExternalChanges?: (maxFiles: number) => Promise<{
190
197
  changed: Array<{
191
198
  path: string;
@@ -9,6 +9,7 @@ import { buildAutoModePrompt, renderAutoModeAction, renderAutoModeWindow } from
9
9
  import { resolveModel, resolveTaskModel, roleModelIfSet } from "../roles.js";
10
10
  import { primaryActivityArg } from "../arg-summary.js";
11
11
  import { materializeMcpTools } from "../mcp.js";
12
+ import { materializeA2aTools } from "../a2a.js";
12
13
  import { Type } from "typebox";
13
14
  import { brainToRuntime } from "../runtime.js";
14
15
  import { StoredSession, isSessionConflict, hasSessionFork } from "../session.js";
@@ -16,7 +17,7 @@ import { createSubagentWorktreeHelper, forkGovernanceDenial } from "../../agents
16
17
  import { createAgentTranscriptTool, AGENT_TRANSCRIPT_TOOL_NAME } from "../../agents/agent-transcript-tool.js";
17
18
  import { createSendMessageTool, SEND_MESSAGE_TOOL_NAME } from "../../agents/send-message-tool.js";
18
19
  import { SubagentRetainLedger } from "../../agents/retain-ledger.js";
19
- import { combinePolicies, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, resolveAsk, toolPolicyNameSets } from "../tool-policy.js";
20
+ import { combinePolicies, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, refuseOutOfContractDecision, resolveAsk, toolPolicyNameSets } from "../tool-policy.js";
20
21
  import { ActiveSkillScope, createActiveSkillScopePolicy } from "./active-skill-scope.js";
21
22
  import { computeCallCap, createCallCapRef, softExecDeadlineMs, toolCutDeadlineMs, WALLTIME_STALL_CONNECT_MS, WALLTIME_STALL_FIRST_TOKEN_MS, WALLTIME_STALL_IDLE_MS } from "./call-cap.js";
22
23
  import { createCutKillRegistry } from "./cut-kill.js";
@@ -30,7 +31,7 @@ import { CacheBreakDetector, toolsToFingerprintInputs } from "../cache-break-det
30
31
  import { reservedCollisions, reservedFor } from "../../brain/request-params.js";
31
32
  import { defineTool, isDefineToolProduct } from "../tools.js";
32
33
  import { RETIRED_TOOL_NAMES } from "../tool-name-aliases.js";
33
- import { protocolOf } from "../protocol-table.js";
34
+ import { protocolOf, PROTOCOL_TABLE } from "../protocol-table.js";
34
35
  import { pathToUri } from "../lsp-protocol.js";
35
36
  import { DEFAULT_TOOL_RESULT_THRESHOLD_CHARS, createOffloadPersist, firstPartyOffloadPolicy, InMemoryToolResultStore, RunnerSharedToolResultStore, ScopedToolResultStore, isVolatileOffloadStore, OFFLOAD_TOOL_NAME, createReadToolResultTool, withToolResultOffload, } from "../tool-result-store.js";
36
37
  import { OUTPUT_TOOL_NAME, REPORT_FINDINGS_TOOL_NAME, SKILL_CONTENT_MAX_CHARS, SKILL_TOOL_NAME, createOutputTool, createReportBlockedTool, createReportFindingsTool, createSkillTool, normalizeSkills } from "./synthetic-tools.js";
@@ -64,6 +65,7 @@ import { createSchedulerTools } from "../../tools/scheduler-tools.js";
64
65
  import { createPresentPlanTool, createEnterPlanModeTool, PRESENT_PLAN_TOOL_NAME, ENTER_PLAN_MODE_TOOL_NAME } from "../present-plan-tool.js";
65
66
  import { isSelfOrchestrationActive, selfOrchestrationFailClosedReason } from "../../orchestration/workflow-script-runner.js";
66
67
  import { createRunWorkflowTool, RUN_WORKFLOW_TOOL_NAME } from "../../orchestration/run-workflow-tool.js";
68
+ import { resolveWorkflowSizeGuideline } from "../../orchestration/workflow-size-guideline.js";
67
69
  import { createLspTool, gitCheckIgnoreFilter, resolveLspPath } from "../lsp.js";
68
70
  import { resolveKey } from "../../tools/fs/safety.js";
69
71
  import { BINDING_CHECKPOINT_VERSION, mintCheckpointToken, RESOURCE_CHECKPOINT_VERSION, WALLTIME_CHECKPOINT_VERSION, buildRiskDescriptor, debitLedger, remainingBudgetMicroUsd, } from "../checkpoint-store.js";
@@ -82,6 +84,7 @@ function sanitizedTtlMs(ttlMs) {
82
84
  const ungatedWarnedShapes = new WeakMap();
83
85
  const advisedPolicyNames = new WeakMap();
84
86
  const ADVISED_KEYS_CAP = 64;
87
+ const NAMESPACED_NAME_SHAPES = PROTOCOL_TABLE.map((ns) => `${ns.prefix}<peer>__<tool>`).join(", ");
85
88
  export const DEFAULT_IRREVERSIBLE_SCOPE = "irreversible";
86
89
  export function checkpointScopeOf(spec) {
87
90
  return spec.durableApproval?.scope || spec.principal || DEFAULT_IRREVERSIBLE_SCOPE;
@@ -261,7 +264,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
261
264
  let shellGatedMonitor = false;
262
265
  for (const t of spec.tools ?? []) {
263
266
  if (t.name.includes("__")) {
264
- const e = new Error(`Tool name "${t.name}" is invalid: "__" is reserved for the MCP namespace (mcp__<server>__<tool>) and must not appear in a caller tool name.`);
267
+ const e = new Error(`Tool name "${t.name}" is invalid: "__" is reserved for the engine's protocol tool namespaces (${NAMESPACED_NAME_SHAPES}) and must not appear in a caller tool name.`);
265
268
  e.code = "config.tool_name_invalid";
266
269
  throw e;
267
270
  }
@@ -479,7 +482,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
479
482
  return maybeOffload(tool, policy);
480
483
  return maybeOffload(tool, explicitGlobalThreshold === undefined ? policy : undefined);
481
484
  };
482
- const mcpOffload = (tool) => {
485
+ const remoteToolOffload = (tool) => {
483
486
  if (explicitGlobalThreshold !== undefined)
484
487
  return maybeOffload(tool);
485
488
  return maybeOffload(tool, { offloadThresholdChars: tool.mcpMaxResultSizeChars ?? 50_000 });
@@ -492,6 +495,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
492
495
  }
493
496
  let ownedEnv;
494
497
  let mcp;
498
+ let a2a;
495
499
  if (internals?.requestedCwd !== undefined && deps.executionEnvFactory === undefined) {
496
500
  await settleTeardownLeg(() => forgetOnThrow(), "forgetOnThrow (cwd-unsupported leg)", (err) => deps.onError?.(err, { phase: "config", sessionId }));
497
501
  const e = new Error(`Agent cwd "${internals.requestedCwd}" cannot take effect: this deployment has no executionEnvFactory (a static execution environment cannot be re-rooted per agent). Drop the cwd parameter or deploy a factory.`);
@@ -969,8 +973,11 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
969
973
  `(per-principal entitlement governance, not a misconfiguration — the run_workflow tool is not mounted)`), { phase: "config", sessionId });
970
974
  }
971
975
  let workflowToolsActive = false;
976
+ let workflowSizeGuideline;
972
977
  if (selfOrchestrationActive && runnerSelf && deps.workflowScriptRunner && deps.workflowGovernanceBaseline) {
973
978
  workflowToolsActive = true;
979
+ const currentSizeGuideline = () => resolveWorkflowSizeGuideline(deps.workflowLimits?.sizeGuideline).size;
980
+ workflowSizeGuideline = { legGuideline: currentSizeGuideline(), current: currentSizeGuideline };
974
981
  toolEffects.set(RUN_WORKFLOW_TOOL_NAME, "write");
975
982
  tools.push(await createRunWorkflowTool({
976
983
  runner: runnerSelf,
@@ -1093,7 +1100,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1093
1100
  throw e;
1094
1101
  }
1095
1102
  }
1096
- tools.push(...mcp.tools.map((t) => mcpOffload(t)));
1103
+ tools.push(...mcp.tools.map((t) => remoteToolOffload(t)));
1097
1104
  const rebuildHarnessToolsRef = {};
1098
1105
  const toolCallGateArmedRef = { armed: false };
1099
1106
  if (spec.mcp?.length) {
@@ -1121,7 +1128,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1121
1128
  const pushable = r.tools.filter((t) => !excludedSet.has(t.name));
1122
1129
  const excludedNow = r.tools.filter((t) => excludedSet.has(t.name)).map((t) => t.name);
1123
1130
  try {
1124
- foldMcpAxes((r.axes ?? []).filter((a) => !excludedSet.has(a.name)));
1131
+ foldProtocolAxes((r.axes ?? []).filter((a) => !excludedSet.has(a.name)), "MCP");
1125
1132
  }
1126
1133
  catch (foldErr) {
1127
1134
  lines.push(`${r.server}: failed (${foldErr instanceof Error ? foldErr.message : String(foldErr)})`);
@@ -1135,7 +1142,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1135
1142
  tools.splice(i, 1);
1136
1143
  }
1137
1144
  }
1138
- tools.push(...pushable.map((t) => mcpOffload(t)));
1145
+ tools.push(...pushable.map((t) => remoteToolOffload(t)));
1139
1146
  changed = true;
1140
1147
  const detail = [];
1141
1148
  const shownAdded = r.added.filter((n) => !excludedSet.has(n));
@@ -1165,7 +1172,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1165
1172
  });
1166
1173
  toolEffects.set("RefreshMcpTools", "read");
1167
1174
  }
1168
- const foldMcpAxes = (axes) => {
1175
+ const foldProtocolAxes = (axes, protocolLabel) => {
1169
1176
  for (const axis of axes) {
1170
1177
  if (axis.irreversibility === "always") {
1171
1178
  irreversibilityTier.set(axis.name, "always");
@@ -1173,7 +1180,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1173
1180
  }
1174
1181
  if (axis.egress) {
1175
1182
  if (axis.effect !== undefined && axis.effect !== "write") {
1176
- const e = new Error(`MCP tool "${axis.name}" resolves to egress:true with effect:"${axis.effect}" — an egress tool (external write) must have effect:"write". Clear egress (toolAxes egress:false) if it is a pure read, or set effect:"write".`);
1183
+ const e = new Error(`${protocolLabel} tool "${axis.name}" resolves to egress:true with effect:"${axis.effect}" — an egress tool (external write) must have effect:"write". Clear egress (toolAxes egress:false) if it is a pure read, or set effect:"write".`);
1177
1184
  e.code = "config.egress_requires_write_effect";
1178
1185
  throw e;
1179
1186
  }
@@ -1183,7 +1190,25 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1183
1190
  toolEffects.set(axis.name, axis.effect ?? "write");
1184
1191
  }
1185
1192
  };
1186
- foldMcpAxes(mcp.toolAxes);
1193
+ foldProtocolAxes(mcp.toolAxes, "MCP");
1194
+ a2a = spec.a2a?.length
1195
+ ? await materializeA2aTools(spec.a2a, spec.principal, abortController.signal)
1196
+ : { tools: [], toolAxes: [], warnings: [], statuses: [], refresh: async () => [], dispose: async () => { } };
1197
+ for (const w of a2a.warnings)
1198
+ deps.onError?.(w, { phase: "a2a", sessionId });
1199
+ {
1200
+ const callerNames = new Set((spec.tools ?? []).flatMap((t) => [t.name, ...(t.aliases ?? [])]));
1201
+ const clash = a2a.tools.find((t) => callerNames.has(t.name));
1202
+ if (clash) {
1203
+ await a2a.dispose();
1204
+ await mcp.dispose();
1205
+ const e = new Error(`Tool name "${clash.name}" is reserved by an injected A2A tool — a caller tool of the same name would silently shadow it.`);
1206
+ e.code = "config.reserved_tool_name";
1207
+ throw e;
1208
+ }
1209
+ }
1210
+ tools.push(...a2a.tools.map((t) => remoteToolOffload(t)));
1211
+ foldProtocolAxes(a2a.toolAxes, "A2A");
1187
1212
  const callCapOn = spec.limits?.callCapByDeadline !== false;
1188
1213
  const callCapRef = spec.limits?.timeoutSec &&
1189
1214
  spec.limits.timeoutSec > 0 &&
@@ -1861,11 +1886,11 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1861
1886
  }
1862
1887
  }
1863
1888
  const userToolNames = (spec.tools ?? []).map((t) => t.name);
1864
- const mcpToolNames = mcp.tools.map((t) => t.name);
1889
+ const protocolToolNames = [...mcp.tools.map((t) => t.name), ...a2a.tools.map((t) => t.name)];
1865
1890
  const deferred = classifyDeferred({
1866
1891
  specs: spec.tools ?? [],
1867
- mcpToolNames,
1868
- fullTools: tools.filter((t) => userToolNames.includes(t.name) || mcpToolNames.includes(t.name)),
1892
+ protocolToolNames,
1893
+ fullTools: tools.filter((t) => userToolNames.includes(t.name) || protocolToolNames.includes(t.name)),
1869
1894
  deferMode: deps.deferMode,
1870
1895
  model,
1871
1896
  deferNames: (toolFaceSnapshot.defer ?? []).filter((n) => tools.some((t) => t.name === n)),
@@ -2211,7 +2236,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2211
2236
  throw err;
2212
2237
  }
2213
2238
  if (n.includes("__") && protocolOf(n) === undefined) {
2214
- const err = new Error(`tool policy ${kind}-list entry "${n}" is a pre-prefix MCP tool name and matches nothing in this run's roster — MCP tools are named "mcp__<server>__<tool>" and legacy-name normalization was removed (RB-476-A), so this entry would silently guard nothing. Prefix the entry with "mcp__".`);
2239
+ const err = new Error(`tool policy ${kind}-list entry "${n}" carries the "__" namespace separator but no protocol prefix, and matches nothing in this run's roster — protocol tools are named ${NAMESPACED_NAME_SHAPES}, and legacy-name normalization was removed (RB-476-A), so this entry would silently guard nothing. Prefix the entry with the owning protocol's marker.`);
2215
2240
  err.code = "config.legacy_tool_name";
2216
2241
  throw err;
2217
2242
  }
@@ -2364,7 +2389,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2364
2389
  toolName: creq.toolName,
2365
2390
  toolCallId: creq.toolCallId,
2366
2391
  args: editArgs,
2367
- message: re.message ?? re.message ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
2392
+ message: re.message ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
2368
2393
  ...askSourceIdentity(),
2369
2394
  ...(re.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
2370
2395
  }, onAskOf, csignal ?? abortController.signal);
@@ -2410,7 +2435,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2410
2435
  toolName: creq.toolName,
2411
2436
  toolCallId: creq.toolCallId,
2412
2437
  args: presentedArgs,
2413
- message: first.message ?? first.message ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
2438
+ message: first.message ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
2414
2439
  ...askSourceIdentity(),
2415
2440
  ...(first.action === "ask" && first.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
2416
2441
  }, pc.onAsk, csignal ?? abortController.signal);
@@ -2466,7 +2491,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2466
2491
  toolName: creq.toolName,
2467
2492
  toolCallId: creq.toolCallId,
2468
2493
  args: presentedArgs,
2469
- message: decision.message ?? decision.message ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
2494
+ message: decision.message ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
2470
2495
  ...askSourceIdentity(),
2471
2496
  ...(decision.action === "ask" && decision.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
2472
2497
  }, pc.onAsk, csignal ?? abortController.signal);
@@ -2706,7 +2731,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2706
2731
  const preview = approvalPreviewOf(req.toolName, req.args);
2707
2732
  return preview !== undefined ? { preview } : {};
2708
2733
  })(),
2709
- message: decision.message ?? decision.message ?? `approval required for "${req.toolName}"`,
2734
+ message: decision.message ?? `approval required for "${req.toolName}"`,
2710
2735
  ...askSourceIdentity(),
2711
2736
  ...(decision.action === "ask" && decision.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
2712
2737
  }, onAsk, abortController.signal);
@@ -2750,17 +2775,33 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2750
2775
  const checkpointStore = spec.checkpointStore ?? deps.checkpointStore;
2751
2776
  const durableApproval = spec.durableApproval ??
2752
2777
  (runtimeCaps?.forceDurableGate ? { scope: spec.principal || DEFAULT_IRREVERSIBLE_SCOPE } : undefined);
2753
- const serializeCheckpointState = (workspaceHandle) => ({
2778
+ const inFlightSpendMicroUsd = () => {
2779
+ const own = liveSpendRef.get?.().costMicroUsd ?? 0;
2780
+ const seededNested = resume?.seed.nestedStats.costMicroUsd;
2781
+ const nestedDelta = nestedStats.costMicroUsd - (typeof seededNested === "number" && Number.isFinite(seededNested) ? seededNested : 0);
2782
+ return (Number.isFinite(own) ? own : 0) + Math.max(0, Number.isFinite(nestedDelta) ? nestedDelta : 0);
2783
+ };
2784
+ const repairBundleForCheckpoint = (parkedSpendMicroUsd) => {
2785
+ const carried = internals?.repairBundle !== undefined
2786
+ ? structuredClone(internals.repairBundle)
2787
+ : resume?.seed.repairBundle !== undefined
2788
+ ? structuredClone(resume.seed.repairBundle)
2789
+ : undefined;
2790
+ if (carried === undefined)
2791
+ return undefined;
2792
+ if (parkedSpendMicroUsd === undefined || !Number.isFinite(parkedSpendMicroUsd) || parkedSpendMicroUsd <= 0)
2793
+ return carried;
2794
+ const prior = typeof carried.spentMicroUsd === "number" && Number.isFinite(carried.spentMicroUsd) ? Math.max(0, carried.spentMicroUsd) : 0;
2795
+ carried.spentMicroUsd = prior + parkedSpendMicroUsd;
2796
+ return carried;
2797
+ };
2798
+ const serializeCheckpointState = (workspaceHandle, parkedSpendMicroUsd) => ({
2754
2799
  activeTools: [...activeTools],
2755
2800
  outputRef: { value: outputRef.value, set: outputRef.set },
2756
2801
  nestedStats: { ...nestedStats },
2757
2802
  consolidationNotes: undefined,
2758
2803
  readFileState: readFileStateForCheckpoint ? [...readFileStateForCheckpoint.entries()] : undefined,
2759
- repairBundle: internals?.repairBundle !== undefined
2760
- ? structuredClone(internals.repairBundle)
2761
- : resume?.seed.repairBundle !== undefined
2762
- ? structuredClone(resume.seed.repairBundle)
2763
- : undefined,
2804
+ repairBundle: repairBundleForCheckpoint(parkedSpendMicroUsd),
2764
2805
  workspaceHandle,
2765
2806
  handsCwd: handsCwdRef?.current,
2766
2807
  activeWorktree: worktreeSessionRef?.current ? { ...worktreeSessionRef.current } : undefined,
@@ -2978,7 +3019,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2978
3019
  leafId,
2979
3020
  gate,
2980
3021
  pendingAction: { kind: "plan_review" },
2981
- state: serializeCheckpointState(remoteHandle),
3022
+ state: serializeCheckpointState(remoteHandle, inFlightSpendMicroUsd()),
2982
3023
  status: "pending",
2983
3024
  createdAt: mintedAt,
2984
3025
  suspendedAt: now(),
@@ -3092,7 +3133,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3092
3133
  };
3093
3134
  const scope = durableApproval?.scope || checkpointScopeOf({ principal: spec.principal });
3094
3135
  const ttlMs = sanitizedTtlMs(durableApproval?.ttlMs);
3095
- const checkpointState = serializeCheckpointState(remoteHandle);
3136
+ const checkpointState = serializeCheckpointState(remoteHandle, inFlightSpendMicroUsd());
3096
3137
  const approvalLedger = debitLedger(priorLedger, liveSpendRef.get?.() ?? { costMicroUsd: 0, tokens: 0, turns: 0, walltimeMs: 0 }, resourceTotal, { countSlice: false });
3097
3138
  const mintedAt = Date.now();
3098
3139
  cp = {
@@ -3396,7 +3437,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3396
3437
  ? undefined
3397
3438
  : async (path) => {
3398
3439
  try {
3399
- const d = await denyNarrowingPolicy.check({ toolName: "Read", args: { file_path: path }, toolCallId: "changed-files-visibility-probe" }, abortController.signal);
3440
+ const d = refuseOutOfContractDecision(await denyNarrowingPolicy.check({ toolName: "Read", args: { file_path: path }, toolCallId: "changed-files-visibility-probe" }, abortController.signal));
3400
3441
  return d.action === "deny";
3401
3442
  }
3402
3443
  catch {
@@ -3488,12 +3529,16 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3488
3529
  : undefined;
3489
3530
  overheadState.promptChars = systemPrompt.length;
3490
3531
  const preparedHolder = {};
3491
- const buildPrepared = () => ({ harness, session, sessionId, taskRootPath, model, thinking, compModel, mcp: mcp, blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), releaseSignal, cacheBreakDetector, cacheFingerprint, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), ownedEnv, suspendRef, reviewRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(callCapRef ? { callCapRef } : {}), ...(cutKills ? { cutKills } : {}), ...(callCapRef ? { callIssuedAtRef } : {}), suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, promptOverheadTokens, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
3532
+ const buildPrepared = () => ({ harness, session, sessionId, taskRootPath, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), releaseSignal, cacheBreakDetector, cacheFingerprint, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), ownedEnv, suspendRef, reviewRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(callCapRef ? { callCapRef } : {}), ...(cutKills ? { cutKills } : {}), ...(callCapRef ? { callIssuedAtRef } : {}), suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, promptOverheadTokens, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
3492
3533
  const prepared = buildPrepared();
3493
3534
  preparedHolder.current = prepared;
3494
3535
  return prepared;
3495
3536
  }
3496
3537
  catch (prepareErr) {
3538
+ if (a2a) {
3539
+ const a2aHandle = a2a;
3540
+ await settleTeardownLeg(() => a2aHandle.dispose(), "a2a.dispose (prepare-throw leg)", (e) => deps.onError?.(e, { phase: "config", sessionId }));
3541
+ }
3497
3542
  if (mcp) {
3498
3543
  const mcpHandle = mcp;
3499
3544
  await settleTeardownLeg(() => mcpHandle.dispose(), "mcp.dispose (prepare-throw leg)", (e) => deps.onError?.(e, { phase: "config", sessionId }));
@@ -36,10 +36,11 @@ import { delimitUntrusted, inlineUntrusted, sanitizeUntrustedText } from "../unt
36
36
  import { reconcileInterruptedSession } from "../session-reconcile.js";
37
37
  import { RunnerSharedToolResultStore } from "../tool-result-store.js";
38
38
  import { formatDiagnosticsBlock } from "../lsp-diagnostics.js";
39
- import { toolPolicyNameSets } from "../tool-policy.js";
39
+ import { refuseOutOfContractDecision, toolPolicyNameSets } from "../tool-policy.js";
40
40
  import { defaultTaskRegistry } from "../task-registry.js";
41
41
  import { discloseDroppedPending, PendingSessionNotifications, renderTaskNotificationXml, SystemInjectionQueue, taskNotificationDedupKey } from "../task-notification.js";
42
42
  import { ToolDetachHub } from "../tool-detach.js";
43
+ import { workflowSizeGuidelineChangeNotice } from "../../orchestration/workflow-size-guideline.js";
43
44
  import { DEFAULT_MAX_TURNS } from "../../config/defaults.js";
44
45
  import { structuredFrom, toolOutputFrom } from "./tool-output-projection.js";
45
46
  export { DEFAULT_MAX_TURNS };
@@ -51,7 +52,7 @@ function createRunState() {
51
52
  budget: { remainingMicroUsd: undefined, maxCostMicroUsd: undefined, overBudget: () => false, streamCancel: false, callOutputChars: 0, lastStreamBudgetCheck: 0, projectedOverBudget: () => false },
52
53
  turn: { callStartAt: undefined, firstTokenAt: undefined, turnUsage: undefined, turnUsageMissing: false, turnStopReason: undefined, lastTurnHadToolCalls: false, toolBatch: [] },
53
54
  counters: { nudgesSent: 0, nudgeIdx: 0, finalizeInjected: false, walltimeSyncBackstopFired: false, compactionFloor: 0, trimForceBackoff: false, callCutoffs: 0, repetitionCuts: 0, repetitionSpared: 0, repetitionEvents: [], REPETITION_EVENTS_CAP: 0, preemptIgnoredReported: false, wroteThisRun: false, finalVerifyInjections: 0, groundingSignalPreR9: false, groundingSignalPostR9: false, cadenceTurns: 0 },
54
- attach: { attachmentsCfg: undefined, agentListingOn: false, skillsListingOn: false, attachState: undefined, dateState: undefined, instrProbe: undefined, instrState: undefined, attachmentsInjected: 0 },
55
+ attach: { attachmentsCfg: undefined, agentListingOn: false, skillsListingOn: false, attachState: undefined, dateState: undefined, instrProbe: undefined, instrState: undefined, sizeGuidelineState: undefined, attachmentsInjected: 0 },
55
56
  };
56
57
  }
57
58
  const MAX_CONSECUTIVE_COMPACTION_FAILURES = 3;
@@ -314,7 +315,10 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
314
315
  }
315
316
  let boundaryAttachmentBytes = 0;
316
317
  let attachmentsPayload;
317
- if ((rs.attach.dateState !== undefined || rs.attach.instrState !== undefined || rs.attach.attachState !== undefined) &&
318
+ if ((rs.attach.dateState !== undefined ||
319
+ rs.attach.instrState !== undefined ||
320
+ rs.attach.sizeGuidelineState !== undefined ||
321
+ rs.attach.attachState !== undefined) &&
318
322
  !boundarySteered &&
319
323
  !rs.counters.finalizeInjected &&
320
324
  rs.counters.finalVerifyInjections === 0 &&
@@ -457,6 +461,16 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
457
461
  rs.attach.dateState.announcedDate = today;
458
462
  }
459
463
  }
464
+ if (rs.attach.sizeGuidelineState !== undefined && rs.turn.lastTurnHadToolCalls) {
465
+ const guideline = rs.attach.sizeGuidelineState.current();
466
+ if (guideline !== rs.attach.sizeGuidelineState.announcedGuideline) {
467
+ due.splice(due.length > 0 && due[0].source === "date_change" ? 1 : 0, 0, {
468
+ source: "workflow_size_guideline_change",
469
+ body: workflowSizeGuidelineChangeNotice(guideline),
470
+ });
471
+ rs.attach.sizeGuidelineState.announcedGuideline = guideline;
472
+ }
473
+ }
460
474
  if (rs.attach.instrState !== undefined && rs.attach.instrProbe !== undefined && rs.turn.lastTurnHadToolCalls) {
461
475
  let probed = null;
462
476
  try {
@@ -1674,13 +1688,19 @@ export class Runner {
1674
1688
  }
1675
1689
  rs.telemetry.taskStart = Date.now();
1676
1690
  rs.telemetry.taskStartMonotonic = performance.now();
1677
- void Promise.resolve(this.sessions.noteTaskRun?.(prepared.sessionId, rs.telemetry.taskId)).catch((err) => {
1691
+ const discloseNoteTaskRunFailure = (err) => {
1678
1692
  try {
1679
1693
  this.deps.onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "config", sessionId: prepared.sessionId });
1680
1694
  }
1681
1695
  catch {
1682
1696
  }
1683
- });
1697
+ };
1698
+ try {
1699
+ void Promise.resolve(this.sessions.noteTaskRun?.(prepared.sessionId, rs.telemetry.taskId)).catch(discloseNoteTaskRunFailure);
1700
+ }
1701
+ catch (err) {
1702
+ discloseNoteTaskRunFailure(err);
1703
+ }
1684
1704
  rs.degrade.recordDegraded = (info, toModel) => {
1685
1705
  if (rs.degrade.degraded !== undefined)
1686
1706
  return;
@@ -1764,6 +1784,10 @@ export class Runner {
1764
1784
  rs.attach.instrProbe !== undefined && prepared.instructionSources !== undefined && prepared.instructionSources.length > 0
1765
1785
  ? { lastAnnouncedHash: new Map(prepared.instructionSources.map((s) => [s.path, s.contentHash])) }
1766
1786
  : undefined;
1787
+ rs.attach.sizeGuidelineState =
1788
+ prepared.workflowSizeGuideline !== undefined
1789
+ ? { announcedGuideline: prepared.workflowSizeGuideline.legGuideline, current: prepared.workflowSizeGuideline.current }
1790
+ : undefined;
1767
1791
  rs.counters.cadenceTurns = 0;
1768
1792
  rs.turn.lastTurnHadToolCalls = false;
1769
1793
  if (rs.attach.attachState !== undefined && rs.attach.attachmentsCfg?.backgroundTasks === true) {
@@ -2614,6 +2638,7 @@ export class Runner {
2614
2638
  catch (err) {
2615
2639
  if (errorCodeOf(err) === "resume.tool_unavailable") {
2616
2640
  await settleTeardownLeg(() => prepared.mcp.dispose(), "mcp.dispose (tool_unavailable leg)", (e) => this.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
2641
+ await settleTeardownLeg(() => prepared.a2a?.dispose(), "a2a.dispose (tool_unavailable leg)", (e) => this.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
2617
2642
  await settleTeardownLeg(() => (prepared.ownedEnv && hasDestroy(prepared.ownedEnv) ? prepared.ownedEnv.destroy() : undefined), "ownedEnv.destroy (tool_unavailable leg)", (e) => this.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
2618
2643
  throw err;
2619
2644
  }
@@ -3192,12 +3217,12 @@ export class Runner {
3192
3217
  if (outcome.gate === "policy_ask") {
3193
3218
  const boundTo = cp.pendingAction.kind === "tool_approval" ? cp.pendingAction.toolCallId : undefined;
3194
3219
  if (outcome.boundCallId !== boundTo) {
3195
- throw new CheckpointError("checkpoint.invalid_outcome", `resume boundCallId "${outcome.boundCallId}" does not match the checkpoint's pending tool call "${boundTo ?? "(none)"}" — the decision-action binding (design/80 D-1) failed; refusing to apply a decision bound to a different action`);
3220
+ throw new CheckpointError("checkpoint.invalid_outcome", `resume boundCallId "${outcome.boundCallId}" does not match the checkpoint's pending tool call "${boundTo ?? "(none)"}" — the decision-action binding (design/80 D-1) failed; refusing to apply a decision bound to a different action`, { field: "boundCallId" });
3196
3221
  }
3197
3222
  const boundHash = cp.pendingAction.kind === "tool_approval" ? cp.pendingAction.boundInputHash : undefined;
3198
3223
  if (boundHash !== undefined) {
3199
3224
  if (outcome.boundInputHash !== boundHash) {
3200
- throw new CheckpointError("checkpoint.invalid_outcome", "resume boundInputHash does not match the checkpoint's pending tool call input — the decision-action input binding (design/80 D-1 §2) failed; refusing to apply an approval bound to a different input (TOCTOU re-mint guard)");
3225
+ throw new CheckpointError("checkpoint.invalid_outcome", "resume boundInputHash does not match the checkpoint's pending tool call input — the decision-action input binding (design/80 D-1 §2) failed; refusing to apply an approval bound to a different input (TOCTOU re-mint guard)", { field: "boundInputHash" });
3201
3226
  }
3202
3227
  }
3203
3228
  else if (checkpointVersionOf(cp) >= BINDING_CHECKPOINT_VERSION) {
@@ -3347,7 +3372,7 @@ export class Runner {
3347
3372
  return;
3348
3373
  }
3349
3374
  if (outcome.decision === "allow" && outcome.updatedInput !== undefined && prepared.basePolicyForResumeEdit) {
3350
- const rechecked = await prepared.basePolicyForResumeEdit.check({ toolName: pendingAction.toolName, args: resolvedArgs, toolCallId: pendingAction.toolCallId }, prepared.abortController.signal);
3375
+ const rechecked = refuseOutOfContractDecision(await prepared.basePolicyForResumeEdit.check({ toolName: pendingAction.toolName, args: resolvedArgs, toolCallId: pendingAction.toolCallId }, prepared.abortController.signal));
3351
3376
  if (rechecked.action === "deny") {
3352
3377
  emitEnd(true);
3353
3378
  const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, formatHookFeedback(`The approver EDITED this call's input; the edited call is denied by the deployment's tool policy and was not executed${rechecked.message ? `: ${rechecked.message}` : ""}.`), true));
@@ -3356,7 +3381,7 @@ export class Runner {
3356
3381
  }
3357
3382
  }
3358
3383
  if (prepared.denyNarrowingPolicy) {
3359
- const narrowed = await prepared.denyNarrowingPolicy.check({ toolName: pendingAction.toolName, args: resolvedArgs, toolCallId: pendingAction.toolCallId }, prepared.abortController.signal);
3384
+ const narrowed = refuseOutOfContractDecision(await prepared.denyNarrowingPolicy.check({ toolName: pendingAction.toolName, args: resolvedArgs, toolCallId: pendingAction.toolCallId }, prepared.abortController.signal));
3360
3385
  if (narrowed.action === "deny") {
3361
3386
  emitEnd(true);
3362
3387
  const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, formatHookFeedback(`The approved tool call "${pendingAction.toolName}" is now denied by a session rule and was not executed${narrowed.message ? `: ${narrowed.message}` : ""}.`), true));
@@ -3514,6 +3539,14 @@ export class Runner {
3514
3539
  catch (err) {
3515
3540
  this.deps.onError?.(err, { phase: "mcp", sessionId: prepared.sessionId });
3516
3541
  }
3542
+ if (prepared.a2a !== undefined) {
3543
+ try {
3544
+ await prepared.a2a.dispose();
3545
+ }
3546
+ catch (err) {
3547
+ this.deps.onError?.(err, { phase: "a2a", sessionId: prepared.sessionId });
3548
+ }
3549
+ }
3517
3550
  return comp;
3518
3551
  }
3519
3552
  async teardownOwnedEnv(prepared) {
@@ -14,7 +14,7 @@ export declare function deferHint(description: string, max?: number): string;
14
14
  export declare function safeName(name: string): string;
15
15
  export declare function classifyDeferred(opts: {
16
16
  specs: ReadonlyArray<ToolSpec>;
17
- mcpToolNames: ReadonlyArray<string>;
17
+ protocolToolNames: ReadonlyArray<string>;
18
18
  fullTools: ReadonlyArray<ToolFingerprintInput>;
19
19
  deferMode?: "auto";
20
20
  model: Model;
@@ -1,6 +1,6 @@
1
1
  import { Type } from "typebox";
2
2
  import { Value } from "typebox/value";
3
- import { defineTool } from "../tools.js";
3
+ import { defineTool, errorResult } from "../tools.js";
4
4
  export const TOOL_SEARCH_NAME = "ToolSearch";
5
5
  const DEFER_AUTO_FRACTION = 0.1;
6
6
  const CHARS_PER_TOKEN = 4;
@@ -35,7 +35,7 @@ export function classifyDeferred(opts) {
35
35
  if (s.defer === true && !pinned.has(s.name))
36
36
  deferred.add(s.name);
37
37
  }
38
- for (const name of opts.mcpToolNames)
38
+ for (const name of opts.protocolToolNames)
39
39
  if (!pinned.has(name))
40
40
  deferred.add(name);
41
41
  for (const name of opts.deferNames ?? [])
@@ -247,6 +247,11 @@ export function createToolSearchTool(opts) {
247
247
  }),
248
248
  effect: "read",
249
249
  execute: async (raw) => {
250
+ const staleSelect = (raw ?? {});
251
+ if (staleSelect.select !== undefined) {
252
+ return errorResult(`Error (${TOOL_SEARCH_NAME}): \`select\` is not a parameter (the array form is retired) — put the ` +
253
+ `selection in \`query\` instead: {"query":"select:ToolA,ToolB"}. Nothing was activated.`);
254
+ }
250
255
  const args = (raw ?? {});
251
256
  const { matched, missing } = resolveToolSearchDetailed(args, registry);
252
257
  const mounted = mountedNames?.() ?? new Set();