@sema-agent/core 5.2.0 → 5.4.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 (43) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/brain/anthropic.js +12 -2
  3. package/dist/brain/media-degrade.d.ts +14 -0
  4. package/dist/brain/media-degrade.js +30 -0
  5. package/dist/brain/openai.js +9 -11
  6. package/dist/brain/timeout.d.ts +14 -0
  7. package/dist/brain/timeout.js +114 -0
  8. package/dist/core/a2a-task-state.d.ts +15 -0
  9. package/dist/core/a2a-task-state.js +68 -0
  10. package/dist/core/a2a.d.ts +42 -0
  11. package/dist/core/a2a.js +651 -0
  12. package/dist/core/checkpoint-store.d.ts +6 -1
  13. package/dist/core/checkpoint-store.js +3 -1
  14. package/dist/core/exec-output-tail.d.ts +0 -1
  15. package/dist/core/exec-output-tail.js +0 -6
  16. package/dist/core/mcp.d.ts +11 -4
  17. package/dist/core/mcp.js +11 -31
  18. package/dist/core/protocol-naming.d.ts +7 -0
  19. package/dist/core/protocol-naming.js +32 -0
  20. package/dist/core/protocol-table.d.ts +3 -1
  21. package/dist/core/protocol-table.js +37 -32
  22. package/dist/core/runner/prepare-task.d.ts +9 -0
  23. package/dist/core/runner/prepare-task.js +52 -15
  24. package/dist/core/runner/runtask.js +34 -4
  25. package/dist/core/runner/tool-disclosure.d.ts +1 -1
  26. package/dist/core/runner/tool-disclosure.js +1 -1
  27. package/dist/core/runner/turn-attachments.d.ts +1 -2
  28. package/dist/core/runner/turn-attachments.js +1 -12
  29. package/dist/core/store-contracts/background-agent-store-contract.d.ts +5 -0
  30. package/dist/core/store-contracts/background-agent-store-contract.js +213 -0
  31. package/dist/core/task-registry-agent.d.ts +14 -1
  32. package/dist/core/task-registry-agent.js +2 -2
  33. package/dist/core/types.d.ts +18 -2
  34. package/dist/engine/execution-env/node-execution-env.d.ts +0 -1
  35. package/dist/engine/execution-env/node-execution-env.js +12 -30
  36. package/dist/index.d.ts +8 -3
  37. package/dist/index.js +6 -2
  38. package/dist/prompt-assembly/event-registry.js +1 -0
  39. package/dist/tools/fs/fs-read.js +5 -3
  40. package/dist/tools/fs/fs-shared.d.ts +1 -0
  41. package/dist/tools/fs/fs-shared.js +1 -0
  42. package/dist/tools/web.js +12 -4
  43. package/package.json +1 -1
@@ -30,4 +30,3 @@ export declare function sliceStreamIncrement(s: StreamCursorState, terminal: boo
30
30
  droppedBeforeCursor: number;
31
31
  };
32
32
  export declare function markTruncated(text: string, droppedBytes: number): string;
33
- export declare function markLiveSpoolRotations(text: string, liveRotations: number): string;
@@ -73,9 +73,3 @@ export function markTruncated(text, droppedBytes) {
73
73
  ? `[... ${droppedBytes} earlier byte(s) truncated; showing last ${Buffer.byteLength(text, "utf8")} ...]\n${text}`
74
74
  : text;
75
75
  }
76
- export function markLiveSpoolRotations(text, liveRotations) {
77
- if (liveRotations <= 0)
78
- return text;
79
- const note = `[spool reclaimed ${liveRotations} time(s) while the process was still writing; bytes appended during those windows may be missing beyond any counted truncation]`;
80
- return text === "" ? note : `${text}${text.endsWith("\n") ? "" : "\n"}${note}`;
81
- }
@@ -1,3 +1,4 @@
1
+ import { type ProtocolId } from "./protocol-table.js";
1
2
  import type { AgentTool } from "../internal/harness-types.js";
2
3
  import type { ImageContent, TextContent } from "../internal/llm.js";
3
4
  import { type McpImageResizer } from "./image-downsample.js";
@@ -69,14 +70,20 @@ export declare function mcpIdleTimeoutMs(kind: "stdio" | "http"): number;
69
70
  export declare function describeMcpSpecErrorCode(code: unknown): string | undefined;
70
71
  export declare function collapseMcpErrorPrefix(message: string): string;
71
72
  export declare function networkErrorCode(err: unknown, depth?: number): string | undefined;
72
- export declare function normalizeMcpName(name: string): string;
73
- export declare function clampNameSegment(seg: string, max?: number): string;
73
+ export interface HttpTransportFailure {
74
+ condition: string;
75
+ delivered: "no" | "unknown";
76
+ httpStatus?: number;
77
+ }
78
+ export declare function describeHttpTransportFailure(err: unknown): HttpTransportFailure | undefined;
79
+ export { normalizeNameSegment as normalizeMcpName, clampNameSegment } from "./protocol-naming.js";
74
80
  export * from "./image-downsample.js";
75
81
  export declare function inferCompactSchema(value: unknown, depth?: number): string;
76
- export declare function resolveMcpHttpHeaders(t: {
82
+ export interface ProtocolHttpAuth {
77
83
  headers?: Record<string, string>;
78
84
  principalHeader?: string;
79
- }, principal?: string): Record<string, string> | undefined;
85
+ }
86
+ export declare function resolveProtocolHttpHeaders(protocol: ProtocolId, t: ProtocolHttpAuth, principal?: string): Record<string, string> | undefined;
80
87
  interface McpContentItem {
81
88
  type: string;
82
89
  text?: string;
package/dist/core/mcp.js CHANGED
@@ -1,5 +1,5 @@
1
- import { createHash } from "node:crypto";
2
1
  import { MCP_NAMESPACE } from "./protocol-table.js";
2
+ import { mintNamespacePrefix, mintNamespacedToolName, normalizeNameSegment } from "./protocol-naming.js";
3
3
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
4
4
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
5
5
  import { StreamableHTTPClientTransport, StreamableHTTPError } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
@@ -12,7 +12,6 @@ import { sliceHeadSafe, sliceTailSafe } from "./surrogate-safe-slice.js";
12
12
  import { truncateError } from "./tool-errors.js";
13
13
  import { delimitUntrusted, inlineUntrusted, sanitizeUntrustedText } from "./untrusted-text.js";
14
14
  import { validateJsonSchemaShape } from "./runner/strict-output-schema.js";
15
- const NAME_SEP = "__";
16
15
  export const MCP_PREFIX = MCP_NAMESPACE.prefix;
17
16
  const MCP_OUTPUT_TOKENS_DEFAULT = 25_000;
18
17
  const MCP_CHARS_PER_TOKEN = 4;
@@ -198,7 +197,7 @@ export function networkErrorCode(err, depth = 0) {
198
197
  }
199
198
  return networkErrorCode(err.cause, depth + 1);
200
199
  }
201
- function describeHttpTransportFailure(err) {
200
+ export function describeHttpTransportFailure(err) {
202
201
  if (err instanceof McpError)
203
202
  return undefined;
204
203
  if (err instanceof StreamableHTTPError) {
@@ -297,19 +296,7 @@ function throwDeadServer(server, what) {
297
296
  e.details = { server };
298
297
  throw e;
299
298
  }
300
- export function normalizeMcpName(name) {
301
- const charset = name.replace(/[^a-zA-Z0-9_-]/g, "_");
302
- return clampNameSegment(charset);
303
- }
304
- export function clampNameSegment(seg, max = 64) {
305
- if (seg === "")
306
- return "_";
307
- if (seg.length <= max)
308
- return seg;
309
- const digest = createHash("sha256").update(seg).digest("hex").slice(0, 8);
310
- const keep = max - digest.length - 1;
311
- return keep > 0 ? `${seg.slice(0, keep)}_${digest}` : digest.slice(0, max);
312
- }
299
+ export { normalizeNameSegment as normalizeMcpName, clampNameSegment } from "./protocol-naming.js";
313
300
  export * from "./image-downsample.js";
314
301
  const MCP_BLOB_DIR = join(tmpdir(), "sema-mcp-blobs");
315
302
  function extensionForMime(mimeType) {
@@ -345,7 +332,7 @@ function extensionForMime(mimeType) {
345
332
  async function persistMcpBlob(base64Data, mimeType, server) {
346
333
  const bytes = Buffer.from(base64Data, "base64");
347
334
  const ext = extensionForMime(mimeType);
348
- const filePath = join(MCP_BLOB_DIR, `mcp-${normalizeMcpName(server ?? "server")}-blob-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${ext}`);
335
+ const filePath = join(MCP_BLOB_DIR, `mcp-${normalizeNameSegment(server ?? "server")}-blob-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${ext}`);
349
336
  try {
350
337
  await mkdir(MCP_BLOB_DIR, { recursive: true, mode: 0o700 });
351
338
  const st = await lstat(MCP_BLOB_DIR);
@@ -409,10 +396,10 @@ export function inferCompactSchema(value, depth = 2) {
409
396
  }
410
397
  return typeof value;
411
398
  }
412
- export function resolveMcpHttpHeaders(t, principal) {
399
+ export function resolveProtocolHttpHeaders(protocol, t, principal) {
413
400
  if (t.principalHeader && principal !== undefined) {
414
401
  if (/[\r\n]/.test(principal))
415
- throw new Error("mcp: principal must not contain CR/LF (header injection guard)");
402
+ throw new Error(`${protocol}: principal must not contain CR/LF (header injection guard)`);
416
403
  return { ...t.headers, [t.principalHeader]: principal };
417
404
  }
418
405
  return t.headers;
@@ -426,7 +413,7 @@ function buildTransport(spec, principal) {
426
413
  env: t.env,
427
414
  });
428
415
  }
429
- const headers = resolveMcpHttpHeaders(t, principal);
416
+ const headers = resolveProtocolHttpHeaders(MCP_NAMESPACE.id, t, principal);
430
417
  return new StreamableHTTPClientTransport(new URL(t.url), {
431
418
  requestInit: headers ? { headers } : undefined,
432
419
  });
@@ -667,7 +654,7 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
667
654
  droppedTools,
668
655
  statuses,
669
656
  refresh: async (server) => {
670
- const prefixOf = (name) => `${MCP_PREFIX}${normalizeMcpName(name)}${NAME_SEP}`;
657
+ const prefixOf = (name) => mintNamespacePrefix(MCP_NAMESPACE, name);
671
658
  const targets = server !== undefined ? serverHandles.filter((h) => h.name === server) : serverHandles;
672
659
  const results = [];
673
660
  if (server !== undefined && targets.length === 0) {
@@ -1149,14 +1136,14 @@ function intakeListedTools(listed, spec, client, health, imageResizer) {
1149
1136
  }
1150
1137
  const normalized = normalizeMcpToolSchema(t.inputSchema);
1151
1138
  let effectiveInputSchema = t.inputSchema;
1152
- let effectiveDescription = t.description;
1139
+ let effectiveDescription = t.description !== undefined ? sanitizeUntrustedText(t.description) : undefined;
1153
1140
  if (normalized.outcome === "drop") {
1154
1141
  dropped.push({ tool: inlineUntrusted(t.name), reason: inlineUntrusted(normalized.reason, 240) });
1155
1142
  continue;
1156
1143
  }
1157
1144
  if (normalized.outcome === "normalized") {
1158
1145
  effectiveInputSchema = normalized.schema;
1159
- effectiveDescription = normalized.note + (t.description ? `\n\n${t.description}` : "");
1146
+ effectiveDescription = normalized.note + (effectiveDescription ? `\n\n${effectiveDescription}` : "");
1160
1147
  }
1161
1148
  const schemaProblem = mcpToolSchemaProblem(effectiveInputSchema);
1162
1149
  if (schemaProblem !== undefined) {
@@ -1164,14 +1151,7 @@ function intakeListedTools(listed, spec, client, health, imageResizer) {
1164
1151
  continue;
1165
1152
  }
1166
1153
  const remoteName = t.name;
1167
- const namespacedName = (() => {
1168
- const TOOL_MIN = 16;
1169
- const fixed = MCP_PREFIX.length + NAME_SEP.length;
1170
- const serverBudget = Math.max(1, 64 - fixed - TOOL_MIN);
1171
- const server = clampNameSegment(normalizeMcpName(spec.name), serverBudget);
1172
- const prefix = `${MCP_PREFIX}${server}${NAME_SEP}`;
1173
- return `${prefix}${clampNameSegment(normalizeMcpName(remoteName), Math.max(1, 64 - prefix.length))}`;
1174
- })();
1154
+ const namespacedName = mintNamespacedToolName(MCP_NAMESPACE, spec.name, remoteName);
1175
1155
  const hintAxis = mcpAxisFor(namespacedName, t.annotations);
1176
1156
  const axis = applyCallerAxisOverride(namespacedName, hintAxis, spec.toolAxes?.[remoteName]);
1177
1157
  if (axis)
@@ -0,0 +1,7 @@
1
+ import type { ProtocolNamespace } from "./protocol-table.js";
2
+ export declare const TOOL_NAME_MAX_CHARS = 64;
3
+ export declare const MINTED_TOOL_SEGMENT_MIN_CHARS = 16;
4
+ export declare function normalizeNameSegment(name: string): string;
5
+ export declare function clampNameSegment(seg: string, max?: number): string;
6
+ export declare function mintNamespacePrefix(ns: ProtocolNamespace, peer: string): string;
7
+ export declare function mintNamespacedToolName(ns: ProtocolNamespace, peer: string, tool: string): string;
@@ -0,0 +1,32 @@
1
+ import { createHash } from "node:crypto";
2
+ export const TOOL_NAME_MAX_CHARS = 64;
3
+ export const MINTED_TOOL_SEGMENT_MIN_CHARS = 16;
4
+ const SEGMENT_DIGEST_CHARS = 8;
5
+ const NAME_SEP = "__";
6
+ export function normalizeNameSegment(name) {
7
+ return clampNameSegment(name.replace(/[^a-zA-Z0-9_-]/g, "_"));
8
+ }
9
+ const SEGMENT_DIGEST_JOIN = "-";
10
+ export function clampNameSegment(seg, max = TOOL_NAME_MAX_CHARS) {
11
+ if (seg === "")
12
+ return "_";
13
+ if (seg.length <= max)
14
+ return seg;
15
+ const digest = createHash("sha256").update(seg).digest("hex").slice(0, SEGMENT_DIGEST_CHARS);
16
+ const keep = max - digest.length - SEGMENT_DIGEST_JOIN.length;
17
+ return keep > 0 ? `${seg.slice(0, keep)}${SEGMENT_DIGEST_JOIN}${digest}` : digest.slice(0, max);
18
+ }
19
+ function settlePeerSegment(clamped) {
20
+ const folded = clamped.replace(/_{2,}/g, "_").replace(/_+$/, "");
21
+ if (folded !== "")
22
+ return folded;
23
+ return createHash("sha256").update(clamped).digest("hex").slice(0, SEGMENT_DIGEST_CHARS);
24
+ }
25
+ export function mintNamespacePrefix(ns, peer) {
26
+ const peerBudget = Math.max(1, TOOL_NAME_MAX_CHARS - ns.prefix.length - NAME_SEP.length - MINTED_TOOL_SEGMENT_MIN_CHARS);
27
+ return `${ns.prefix}${settlePeerSegment(clampNameSegment(normalizeNameSegment(peer), peerBudget))}${NAME_SEP}`;
28
+ }
29
+ export function mintNamespacedToolName(ns, peer, tool) {
30
+ const prefix = mintNamespacePrefix(ns, peer);
31
+ return `${prefix}${clampNameSegment(normalizeNameSegment(tool), Math.max(1, TOOL_NAME_MAX_CHARS - prefix.length))}`;
32
+ }
@@ -1,4 +1,4 @@
1
- export type ProtocolId = "mcp";
1
+ export type ProtocolId = "mcp" | "a2a";
2
2
  export interface ProtocolPeerTool {
3
3
  peer: string;
4
4
  tool: string;
@@ -11,7 +11,9 @@ export interface ProtocolNamespace<P extends string = string> {
11
11
  displayGroupKey(name: string): string;
12
12
  }
13
13
  declare const MCP_PREFIX_NAME: "mcp__";
14
+ declare const A2A_PREFIX_NAME: "a2a__";
14
15
  export declare const MCP_NAMESPACE: ProtocolNamespace<typeof MCP_PREFIX_NAME>;
16
+ export declare const A2A_NAMESPACE: ProtocolNamespace<typeof A2A_PREFIX_NAME>;
15
17
  export declare const PROTOCOL_TABLE: readonly ProtocolNamespace[];
16
18
  export declare function protocolOf(name: string): ProtocolNamespace | undefined;
17
19
  export {};
@@ -1,37 +1,42 @@
1
1
  const NAME_SEP = "__";
2
+ function makeProtocolNamespace(id, prefix) {
3
+ const parse = (name) => {
4
+ if (!name.startsWith(prefix))
5
+ return undefined;
6
+ const rest = name.slice(prefix.length);
7
+ const sep = rest.indexOf(NAME_SEP);
8
+ if (sep <= 0)
9
+ return undefined;
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
+ }
2
35
  const MCP_PREFIX_NAME = `mcp${NAME_SEP}`;
3
- const parseMcpName = (name) => {
4
- if (!name.startsWith(MCP_PREFIX_NAME))
5
- return undefined;
6
- const rest = name.slice(MCP_PREFIX_NAME.length);
7
- const sep = rest.indexOf(NAME_SEP);
8
- if (sep <= 0)
9
- return undefined;
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 makeMcpName = (peer, tool) => {
16
- if (peer.length === 0 || tool.length === 0) {
17
- throw new Error(`protocol table (mcp): 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 (mcp): 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 `${MCP_PREFIX_NAME}${peer}${NAME_SEP}${tool}`;
23
- };
24
- export const MCP_NAMESPACE = {
25
- id: "mcp",
26
- prefix: MCP_PREFIX_NAME,
27
- makeName: makeMcpName,
28
- parse: parseMcpName,
29
- displayGroupKey: (name) => {
30
- const p = parseMcpName(name);
31
- return p === undefined ? name : `${MCP_PREFIX_NAME}${p.peer}${NAME_SEP}*`;
32
- },
33
- };
34
- export const PROTOCOL_TABLE = [MCP_NAMESPACE];
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];
35
40
  export function protocolOf(name) {
36
41
  return PROTOCOL_TABLE.find((ns) => name.startsWith(ns.prefix));
37
42
  }
@@ -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";
@@ -10,11 +11,13 @@ import { type ActiveSkillFrame } from "./active-skill-scope.js";
10
11
  import { type CutKillRegistry } from "./cut-kill.js";
11
12
  import type { SessionPermissionRules } from "../session-policy-store.js";
12
13
  import { CacheBreakDetector, type ToolFingerprintInput } from "../cache-break-detector.js";
14
+ import { type BrainCallGuardrailRef } from "../../brain/timeout.js";
13
15
  import { type OutputRef, type BlockedRef, type SkillListingEntry } from "./synthetic-tools.js";
14
16
  import type { MemoryEngine } from "../memory-engine/engine.js";
15
17
  import { type ToolManifestRow } from "../../prompt-assembly/tool-catalog.js";
16
18
  import type { TaskNotificationPayload } from "../task-notification.js";
17
19
  import { type CwdRef } from "../../tools/fs/index.js";
20
+ import { type WorkflowSizeGuideline } from "../../orchestration/workflow-size-guideline.js";
18
21
  import type { Runner } from "./runtask.js";
19
22
  import { type CheckpointGate, type CheckpointState, type CheckpointToken, type ResourceLedger, type ResourceLimitReason } from "../checkpoint-store.js";
20
23
  import type { ActiveWorktreeSession, AgentMessage, AgentTool, ExecutionEnv } from "../../internal/harness.js";
@@ -36,6 +39,7 @@ export interface Prepared {
36
39
  thinking?: ThinkingLevel;
37
40
  compModel?: Model;
38
41
  mcp: MaterializedMcp;
42
+ a2a?: MaterializedA2a;
39
43
  blockedRef: BlockedRef;
40
44
  outputRef: OutputRef;
41
45
  abortController: AbortController;
@@ -133,6 +137,7 @@ export interface Prepared {
133
137
  callIssuedAtRef?: {
134
138
  current?: number;
135
139
  };
140
+ brainCallGuardrailRef: BrainCallGuardrailRef;
136
141
  reviewRequestRef: {
137
142
  pending?: {
138
143
  reason?: string;
@@ -186,6 +191,10 @@ export interface Prepared {
186
191
  path: string;
187
192
  contentHash: string | null;
188
193
  }>;
194
+ workflowSizeGuideline?: {
195
+ legGuideline: WorkflowSizeGuideline;
196
+ current: () => WorkflowSizeGuideline;
197
+ };
189
198
  detectExternalChanges?: (maxFiles: number) => Promise<{
190
199
  changed: Array<{
191
200
  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";
@@ -28,9 +29,10 @@ import { cloneObserverInput, formatHookFeedback, runToolGate } from "../hooks.js
28
29
  import { reconcileInterruptedSession } from "../session-reconcile.js";
29
30
  import { CacheBreakDetector, toolsToFingerprintInputs } from "../cache-break-detector.js";
30
31
  import { reservedCollisions, reservedFor } from "../../brain/request-params.js";
32
+ import { resolveBrainCallGuardrailMs, withBrainCallGuardrail } from "../../brain/timeout.js";
31
33
  import { defineTool, isDefineToolProduct } from "../tools.js";
32
34
  import { RETIRED_TOOL_NAMES } from "../tool-name-aliases.js";
33
- import { protocolOf } from "../protocol-table.js";
35
+ import { protocolOf, PROTOCOL_TABLE } from "../protocol-table.js";
34
36
  import { pathToUri } from "../lsp-protocol.js";
35
37
  import { DEFAULT_TOOL_RESULT_THRESHOLD_CHARS, createOffloadPersist, firstPartyOffloadPolicy, InMemoryToolResultStore, RunnerSharedToolResultStore, ScopedToolResultStore, isVolatileOffloadStore, OFFLOAD_TOOL_NAME, createReadToolResultTool, withToolResultOffload, } from "../tool-result-store.js";
36
38
  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 +66,7 @@ import { createSchedulerTools } from "../../tools/scheduler-tools.js";
64
66
  import { createPresentPlanTool, createEnterPlanModeTool, PRESENT_PLAN_TOOL_NAME, ENTER_PLAN_MODE_TOOL_NAME } from "../present-plan-tool.js";
65
67
  import { isSelfOrchestrationActive, selfOrchestrationFailClosedReason } from "../../orchestration/workflow-script-runner.js";
66
68
  import { createRunWorkflowTool, RUN_WORKFLOW_TOOL_NAME } from "../../orchestration/run-workflow-tool.js";
69
+ import { resolveWorkflowSizeGuideline } from "../../orchestration/workflow-size-guideline.js";
67
70
  import { createLspTool, gitCheckIgnoreFilter, resolveLspPath } from "../lsp.js";
68
71
  import { resolveKey } from "../../tools/fs/safety.js";
69
72
  import { BINDING_CHECKPOINT_VERSION, mintCheckpointToken, RESOURCE_CHECKPOINT_VERSION, WALLTIME_CHECKPOINT_VERSION, buildRiskDescriptor, debitLedger, remainingBudgetMicroUsd, } from "../checkpoint-store.js";
@@ -82,6 +85,7 @@ function sanitizedTtlMs(ttlMs) {
82
85
  const ungatedWarnedShapes = new WeakMap();
83
86
  const advisedPolicyNames = new WeakMap();
84
87
  const ADVISED_KEYS_CAP = 64;
88
+ const NAMESPACED_NAME_SHAPES = PROTOCOL_TABLE.map((ns) => `${ns.prefix}<peer>__<tool>`).join(", ");
85
89
  export const DEFAULT_IRREVERSIBLE_SCOPE = "irreversible";
86
90
  export function checkpointScopeOf(spec) {
87
91
  return spec.durableApproval?.scope || spec.principal || DEFAULT_IRREVERSIBLE_SCOPE;
@@ -233,6 +237,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
233
237
  e.code = "config.empty_objective";
234
238
  throw e;
235
239
  }
240
+ const brainCallGuardrailRef = {};
241
+ const brainCallGuardrailMs = resolveBrainCallGuardrailMs(spec.limits?.brainCallGuardrailMs ?? deps.brainCallGuardrailMs);
236
242
  if (spec.agents !== undefined && spec.agents.length > 0) {
237
243
  const pool = spec.tools ?? [];
238
244
  if (!pool.some((t) => typeof t.withAgents === "function")) {
@@ -261,7 +267,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
261
267
  let shellGatedMonitor = false;
262
268
  for (const t of spec.tools ?? []) {
263
269
  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.`);
270
+ 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
271
  e.code = "config.tool_name_invalid";
266
272
  throw e;
267
273
  }
@@ -479,7 +485,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
479
485
  return maybeOffload(tool, policy);
480
486
  return maybeOffload(tool, explicitGlobalThreshold === undefined ? policy : undefined);
481
487
  };
482
- const mcpOffload = (tool) => {
488
+ const remoteToolOffload = (tool) => {
483
489
  if (explicitGlobalThreshold !== undefined)
484
490
  return maybeOffload(tool);
485
491
  return maybeOffload(tool, { offloadThresholdChars: tool.mcpMaxResultSizeChars ?? 50_000 });
@@ -492,6 +498,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
492
498
  }
493
499
  let ownedEnv;
494
500
  let mcp;
501
+ let a2a;
495
502
  if (internals?.requestedCwd !== undefined && deps.executionEnvFactory === undefined) {
496
503
  await settleTeardownLeg(() => forgetOnThrow(), "forgetOnThrow (cwd-unsupported leg)", (err) => deps.onError?.(err, { phase: "config", sessionId }));
497
504
  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 +976,11 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
969
976
  `(per-principal entitlement governance, not a misconfiguration — the run_workflow tool is not mounted)`), { phase: "config", sessionId });
970
977
  }
971
978
  let workflowToolsActive = false;
979
+ let workflowSizeGuideline;
972
980
  if (selfOrchestrationActive && runnerSelf && deps.workflowScriptRunner && deps.workflowGovernanceBaseline) {
973
981
  workflowToolsActive = true;
982
+ const currentSizeGuideline = () => resolveWorkflowSizeGuideline(deps.workflowLimits?.sizeGuideline).size;
983
+ workflowSizeGuideline = { legGuideline: currentSizeGuideline(), current: currentSizeGuideline };
974
984
  toolEffects.set(RUN_WORKFLOW_TOOL_NAME, "write");
975
985
  tools.push(await createRunWorkflowTool({
976
986
  runner: runnerSelf,
@@ -1093,7 +1103,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1093
1103
  throw e;
1094
1104
  }
1095
1105
  }
1096
- tools.push(...mcp.tools.map((t) => mcpOffload(t)));
1106
+ tools.push(...mcp.tools.map((t) => remoteToolOffload(t)));
1097
1107
  const rebuildHarnessToolsRef = {};
1098
1108
  const toolCallGateArmedRef = { armed: false };
1099
1109
  if (spec.mcp?.length) {
@@ -1121,7 +1131,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1121
1131
  const pushable = r.tools.filter((t) => !excludedSet.has(t.name));
1122
1132
  const excludedNow = r.tools.filter((t) => excludedSet.has(t.name)).map((t) => t.name);
1123
1133
  try {
1124
- foldMcpAxes((r.axes ?? []).filter((a) => !excludedSet.has(a.name)));
1134
+ foldProtocolAxes((r.axes ?? []).filter((a) => !excludedSet.has(a.name)), "MCP");
1125
1135
  }
1126
1136
  catch (foldErr) {
1127
1137
  lines.push(`${r.server}: failed (${foldErr instanceof Error ? foldErr.message : String(foldErr)})`);
@@ -1135,7 +1145,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1135
1145
  tools.splice(i, 1);
1136
1146
  }
1137
1147
  }
1138
- tools.push(...pushable.map((t) => mcpOffload(t)));
1148
+ tools.push(...pushable.map((t) => remoteToolOffload(t)));
1139
1149
  changed = true;
1140
1150
  const detail = [];
1141
1151
  const shownAdded = r.added.filter((n) => !excludedSet.has(n));
@@ -1165,7 +1175,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1165
1175
  });
1166
1176
  toolEffects.set("RefreshMcpTools", "read");
1167
1177
  }
1168
- const foldMcpAxes = (axes) => {
1178
+ const foldProtocolAxes = (axes, protocolLabel) => {
1169
1179
  for (const axis of axes) {
1170
1180
  if (axis.irreversibility === "always") {
1171
1181
  irreversibilityTier.set(axis.name, "always");
@@ -1173,7 +1183,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1173
1183
  }
1174
1184
  if (axis.egress) {
1175
1185
  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".`);
1186
+ 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
1187
  e.code = "config.egress_requires_write_effect";
1178
1188
  throw e;
1179
1189
  }
@@ -1183,7 +1193,25 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1183
1193
  toolEffects.set(axis.name, axis.effect ?? "write");
1184
1194
  }
1185
1195
  };
1186
- foldMcpAxes(mcp.toolAxes);
1196
+ foldProtocolAxes(mcp.toolAxes, "MCP");
1197
+ a2a = spec.a2a?.length
1198
+ ? await materializeA2aTools(spec.a2a, spec.principal, abortController.signal)
1199
+ : { tools: [], toolAxes: [], warnings: [], statuses: [], refresh: async () => [], dispose: async () => { } };
1200
+ for (const w of a2a.warnings)
1201
+ deps.onError?.(w, { phase: "a2a", sessionId });
1202
+ {
1203
+ const callerNames = new Set((spec.tools ?? []).flatMap((t) => [t.name, ...(t.aliases ?? [])]));
1204
+ const clash = a2a.tools.find((t) => callerNames.has(t.name));
1205
+ if (clash) {
1206
+ await a2a.dispose();
1207
+ await mcp.dispose();
1208
+ 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.`);
1209
+ e.code = "config.reserved_tool_name";
1210
+ throw e;
1211
+ }
1212
+ }
1213
+ tools.push(...a2a.tools.map((t) => remoteToolOffload(t)));
1214
+ foldProtocolAxes(a2a.toolAxes, "A2A");
1187
1215
  const callCapOn = spec.limits?.callCapByDeadline !== false;
1188
1216
  const callCapRef = spec.limits?.timeoutSec &&
1189
1217
  spec.limits.timeoutSec > 0 &&
@@ -1861,11 +1889,11 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1861
1889
  }
1862
1890
  }
1863
1891
  const userToolNames = (spec.tools ?? []).map((t) => t.name);
1864
- const mcpToolNames = mcp.tools.map((t) => t.name);
1892
+ const protocolToolNames = [...mcp.tools.map((t) => t.name), ...a2a.tools.map((t) => t.name)];
1865
1893
  const deferred = classifyDeferred({
1866
1894
  specs: spec.tools ?? [],
1867
- mcpToolNames,
1868
- fullTools: tools.filter((t) => userToolNames.includes(t.name) || mcpToolNames.includes(t.name)),
1895
+ protocolToolNames,
1896
+ fullTools: tools.filter((t) => userToolNames.includes(t.name) || protocolToolNames.includes(t.name)),
1869
1897
  deferMode: deps.deferMode,
1870
1898
  model,
1871
1899
  deferNames: (toolFaceSnapshot.defer ?? []).filter((n) => tools.some((t) => t.name === n)),
@@ -2173,7 +2201,12 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2173
2201
  systemPrompt,
2174
2202
  ...(assembled.systemBlocks ? { systemBlocks: assembled.systemBlocks } : {}),
2175
2203
  getApiKeyAndHeaders: spec.getApiKeyAndHeaders,
2176
- runtime: brainToRuntime(deps.brain),
2204
+ runtime: brainToRuntime(brainCallGuardrailMs === undefined
2205
+ ? deps.brain
2206
+ : {
2207
+ ...deps.brain,
2208
+ stream: withBrainCallGuardrail((m, c, o) => deps.brain.stream(m, c, o), brainCallGuardrailMs, brainCallGuardrailRef),
2209
+ }),
2177
2210
  });
2178
2211
  harnessRef.current = harness;
2179
2212
  let releaseSignal = () => undefined;
@@ -2211,7 +2244,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2211
2244
  throw err;
2212
2245
  }
2213
2246
  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__".`);
2247
+ 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
2248
  err.code = "config.legacy_tool_name";
2216
2249
  throw err;
2217
2250
  }
@@ -3504,12 +3537,16 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3504
3537
  : undefined;
3505
3538
  overheadState.promptChars = systemPrompt.length;
3506
3539
  const preparedHolder = {};
3507
- 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 } : {}) });
3540
+ 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 } : {}), brainCallGuardrailRef, 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 } : {}) });
3508
3541
  const prepared = buildPrepared();
3509
3542
  preparedHolder.current = prepared;
3510
3543
  return prepared;
3511
3544
  }
3512
3545
  catch (prepareErr) {
3546
+ if (a2a) {
3547
+ const a2aHandle = a2a;
3548
+ await settleTeardownLeg(() => a2aHandle.dispose(), "a2a.dispose (prepare-throw leg)", (e) => deps.onError?.(e, { phase: "config", sessionId }));
3549
+ }
3513
3550
  if (mcp) {
3514
3551
  const mcpHandle = mcp;
3515
3552
  await settleTeardownLeg(() => mcpHandle.dispose(), "mcp.dispose (prepare-throw leg)", (e) => deps.onError?.(e, { phase: "config", sessionId }));