@sema-agent/core 7.5.2 → 7.6.1

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 (138) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/dist/agents/agent-transcript-tool.d.ts +2 -2
  3. package/dist/agents/cascade.d.ts +4 -5
  4. package/dist/agents/cascade.js +12 -10
  5. package/dist/agents/repair-loop.d.ts +7 -5
  6. package/dist/agents/repair-loop.js +13 -15
  7. package/dist/agents/retain-ledger.d.ts +2 -3
  8. package/dist/agents/send-message-tool.d.ts +2 -2
  9. package/dist/agents/session-util.d.ts +2 -2
  10. package/dist/agents/subagent.d.ts +27 -46
  11. package/dist/agents/subagent.js +119 -105
  12. package/dist/agents/suspend-guard.d.ts +31 -19
  13. package/dist/agents/suspend-guard.js +14 -8
  14. package/dist/agents/teacher.d.ts +2 -2
  15. package/dist/agents/teacher.js +9 -9
  16. package/dist/agents/team.d.ts +6 -5
  17. package/dist/agents/team.js +10 -8
  18. package/dist/agents/verify.d.ts +8 -9
  19. package/dist/agents/verify.js +17 -17
  20. package/dist/core/a2a.js +2 -1
  21. package/dist/core/agent-definition.d.ts +172 -0
  22. package/dist/core/agent-definition.js +1 -0
  23. package/dist/core/ask-origin.d.ts +60 -7
  24. package/dist/core/ask-origin.js +26 -1
  25. package/dist/core/checkpoint-store.d.ts +78 -76
  26. package/dist/core/checkpoint-store.js +17 -1
  27. package/dist/core/delegation-frames.d.ts +298 -0
  28. package/dist/core/delegation-frames.js +21 -0
  29. package/dist/core/engine-notice.d.ts +555 -0
  30. package/dist/core/engine-notice.js +55 -0
  31. package/dist/core/gate-fold.d.ts +12 -0
  32. package/dist/core/gate-fold.js +158 -0
  33. package/dist/core/gate-lanes.d.ts +93 -0
  34. package/dist/core/gate-lanes.js +626 -0
  35. package/dist/core/gate-outcome.d.ts +189 -0
  36. package/dist/core/gate-outcome.js +70 -0
  37. package/dist/core/hands-band.d.ts +134 -0
  38. package/dist/core/hands-band.js +1 -0
  39. package/dist/core/hooks.d.ts +22 -177
  40. package/dist/core/hooks.js +53 -851
  41. package/dist/core/mcp-failure.d.ts +142 -0
  42. package/dist/core/mcp-failure.js +145 -0
  43. package/dist/core/mcp-server-spec.d.ts +217 -0
  44. package/dist/core/mcp-server-spec.js +1 -0
  45. package/dist/core/mcp.d.ts +21 -77
  46. package/dist/core/mcp.js +76 -150
  47. package/dist/core/model-seat.d.ts +99 -0
  48. package/dist/core/model-seat.js +1 -0
  49. package/dist/core/pause-registry.d.ts +131 -0
  50. package/dist/core/pause-registry.js +27 -0
  51. package/dist/core/reminder-mint.d.ts +10 -0
  52. package/dist/core/reminder-mint.js +3 -0
  53. package/dist/core/runner/assemble-result.d.ts +32 -41
  54. package/dist/core/runner/assemble-result.js +55 -74
  55. package/dist/core/runner/contracts.d.ts +427 -69
  56. package/dist/core/runner/denial-limit-arms.d.ts +1 -1
  57. package/dist/core/runner/denial-limit-arms.js +3 -3
  58. package/dist/core/runner/gate-exit.d.ts +242 -0
  59. package/dist/core/runner/gate-exit.js +124 -0
  60. package/dist/core/runner/park-commit.d.ts +17 -23
  61. package/dist/core/runner/park-commit.js +14 -15
  62. package/dist/core/runner/prepare-ask-lane.d.ts +0 -3
  63. package/dist/core/runner/prepare-ask-lane.js +3 -5
  64. package/dist/core/runner/prepare-boundary-parks.d.ts +3 -6
  65. package/dist/core/runner/prepare-boundary-parks.js +3 -3
  66. package/dist/core/runner/prepare-caps-and-workflow.d.ts +2 -7
  67. package/dist/core/runner/prepare-caps-and-workflow.js +1 -1
  68. package/dist/core/runner/prepare-delegation-surface.d.ts +2 -7
  69. package/dist/core/runner/prepare-gate-stations.d.ts +4 -7
  70. package/dist/core/runner/prepare-gate-stations.js +29 -54
  71. package/dist/core/runner/prepare-inherited-gate.js +1 -1
  72. package/dist/core/runner/prepare-memory.d.ts +44 -26
  73. package/dist/core/runner/prepare-park-ask.d.ts +2 -4
  74. package/dist/core/runner/prepare-park-ask.js +5 -5
  75. package/dist/core/runner/prepare-task.d.ts +2 -2
  76. package/dist/core/runner/prepare-task.js +8 -9
  77. package/dist/core/runner/prepare-wiring-manifest.d.ts +7 -15
  78. package/dist/core/runner/prepare-wiring-manifest.js +9 -10
  79. package/dist/core/runner/runtask.d.ts +20 -102
  80. package/dist/core/runner/runtask.js +119 -121
  81. package/dist/core/runner/terminal-projection.d.ts +22 -0
  82. package/dist/core/runner/terminal-projection.js +28 -0
  83. package/dist/core/runner-deps.d.ts +1416 -0
  84. package/dist/core/runner-deps.js +1 -0
  85. package/dist/core/runtime-caps.d.ts +164 -0
  86. package/dist/core/runtime-caps.js +1 -0
  87. package/dist/core/store-contracts/checkpoint-store-contract.d.ts +4 -1
  88. package/dist/core/store-contracts/checkpoint-store-contract.js +8 -2
  89. package/dist/core/task-event.d.ts +910 -0
  90. package/dist/core/task-event.js +1 -0
  91. package/dist/core/task-limits.d.ts +110 -0
  92. package/dist/core/task-limits.js +1 -0
  93. package/dist/core/task-result.d.ts +809 -0
  94. package/dist/core/task-result.js +1 -0
  95. package/dist/core/task-spec.d.ts +1370 -0
  96. package/dist/core/task-spec.js +1 -0
  97. package/dist/core/task-stream.d.ts +382 -0
  98. package/dist/core/task-stream.js +1 -0
  99. package/dist/core/terminal-cause.d.ts +137 -0
  100. package/dist/core/terminal-cause.js +9 -0
  101. package/dist/core/tool-policy.d.ts +43 -139
  102. package/dist/core/tool-policy.js +79 -112
  103. package/dist/core/tool-spec.d.ts +1174 -0
  104. package/dist/core/tool-spec.js +1 -0
  105. package/dist/core/types.d.ts +27 -7789
  106. package/dist/core/types.js +2 -76
  107. package/dist/core/warm-resume.d.ts +2 -2
  108. package/dist/core/wiring-manifest.d.ts +6 -3
  109. package/dist/core/workflow-journal-store.js +3 -4
  110. package/dist/engine/harness/agent-harness.d.ts +1 -1
  111. package/dist/index.d.ts +12 -8
  112. package/dist/index.js +9 -6
  113. package/dist/orchestration/builtin-workflows.d.ts +2 -2
  114. package/dist/orchestration/builtin-workflows.js +1 -1
  115. package/dist/orchestration/goal.d.ts +2 -2
  116. package/dist/orchestration/goal.js +8 -7
  117. package/dist/orchestration/run-spec.d.ts +2 -2
  118. package/dist/orchestration/run-spec.js +5 -3
  119. package/dist/orchestration/run-workflow-tool.d.ts +4 -4
  120. package/dist/orchestration/run-workflow-tool.js +4 -4
  121. package/dist/orchestration/workflow-governance.d.ts +4 -4
  122. package/dist/orchestration/workflow-governance.js +4 -2
  123. package/dist/orchestration/workflow-primitives.d.ts +1 -1
  124. package/dist/orchestration/workflow-primitives.js +1 -1
  125. package/dist/orchestration/workflow.d.ts +15 -4
  126. package/dist/orchestration/workflow.js +64 -39
  127. package/dist/prompts/supervisor.d.ts +1 -1
  128. package/dist/prompts/supervisor.js +3 -3
  129. package/dist/scenarios/scenario-registry.d.ts +3 -3
  130. package/dist/scenarios/scenario-registry.js +1 -1
  131. package/dist/scenarios/teacher-quickstart.d.ts +2 -2
  132. package/dist/server/http.d.ts +2 -2
  133. package/dist/stores/file/fs-atomic.d.ts +88 -12
  134. package/dist/stores/file/fs-atomic.js +184 -55
  135. package/dist/stores/file/index.d.ts +1 -0
  136. package/dist/stores/file/index.js +1 -0
  137. package/package.json +3 -1
  138. package/test/export-surface.snapshot.json +82 -22
package/dist/core/mcp.js CHANGED
@@ -3,7 +3,8 @@ import { MCP_NAMESPACE } from "./protocol-table.js";
3
3
  import { findNamespacePrefixCollision, mintNamespacePrefix, mintNamespacedToolName, normalizeNameSegment } from "./protocol-naming.js";
4
4
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
5
5
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
6
- import { StreamableHTTPClientTransport, StreamableHTTPError } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
6
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
7
+ import { classifyMcpFailure, describeHttpTransportFailure } from "./mcp-failure.js";
7
8
  import { lstat, mkdir, writeFile } from "node:fs/promises";
8
9
  import { tmpdir } from "node:os";
9
10
  import { join } from "node:path";
@@ -204,6 +205,10 @@ function armMcpIdleWatchdog(health, idleMs, outerSignal) {
204
205
  export function mcpStartupTimeoutMs() {
205
206
  return parseEnvMs("MCP_TIMEOUT");
206
207
  }
208
+ function markMcpFailure(e, details) {
209
+ e.details = details;
210
+ return e;
211
+ }
207
212
  const MCP_SPEC_ERROR_CODE_NAMES = new Map([
208
213
  [-32020, "header mismatch (the server saw an HTTP header that disagreed with the tool parameter mapped onto it)"],
209
214
  [-32021, "missing required client capability (the server requires a capability this client does not declare)"],
@@ -237,90 +242,6 @@ function remoteErrorText(err, opts) {
237
242
  const raw = err instanceof Error ? err.message : String(err);
238
243
  return redactSecrets(opts?.collapse === true ? collapseMcpErrorPrefix(raw) : raw);
239
244
  }
240
- function isTransportLost(err) {
241
- if (err instanceof McpError && err.code === ErrorCode.ConnectionClosed)
242
- return true;
243
- return err instanceof Error && /not connected|connection closed/i.test(err.message);
244
- }
245
- const NETWORK_CODES_NEVER_DELIVERED = new Set([
246
- "ECONNREFUSED",
247
- "ENOTFOUND",
248
- "EAI_AGAIN",
249
- "EHOSTUNREACH",
250
- "ENETUNREACH",
251
- "UND_ERR_CONNECT_TIMEOUT",
252
- ]);
253
- export function networkErrorCode(err, depth = 0) {
254
- if (depth > 5 || !(err instanceof Error))
255
- return undefined;
256
- const code = err.code;
257
- if (typeof code === "string" && /^(?:E[A-Z_]+|UND_ERR_[A-Z_]+)$/.test(code))
258
- return code;
259
- if (err instanceof AggregateError) {
260
- for (const inner of err.errors) {
261
- const found = networkErrorCode(inner, depth + 1);
262
- if (found !== undefined)
263
- return found;
264
- }
265
- }
266
- return networkErrorCode(err.cause, depth + 1);
267
- }
268
- export function describeHttpTransportFailure(err) {
269
- if (err instanceof McpError)
270
- return undefined;
271
- if (err instanceof StreamableHTTPError) {
272
- const status = typeof err.code === "number" && err.code > 0 ? err.code : undefined;
273
- return {
274
- condition: status !== undefined
275
- ? `its HTTP endpoint answered ${status} instead of an MCP response`
276
- : "its HTTP endpoint answered something that is not an MCP response",
277
- delivered: "unknown",
278
- ...(status !== undefined ? { httpStatus: status } : {}),
279
- };
280
- }
281
- const code = networkErrorCode(err);
282
- if (code !== undefined) {
283
- return NETWORK_CODES_NEVER_DELIVERED.has(code)
284
- ? { condition: `its HTTP endpoint could not be reached (${code})`, delivered: "no" }
285
- : { condition: `the connection to its HTTP endpoint failed (${code})`, delivered: "unknown" };
286
- }
287
- if (err instanceof TypeError && /fetch failed|terminated|network/i.test(err.message)) {
288
- return { condition: "the HTTP request to its endpoint failed at the network layer", delivered: "unknown" };
289
- }
290
- return undefined;
291
- }
292
- export const MCP_FAILURE_CODES = ["connect_refused", "connection_failed", "network", "not_mcp_response", "spawn_failed", "connection_closed", "timeout", "protocol", "invalid_config", "unknown"];
293
- export function mcpFailureCodeOf(err, transport) {
294
- if (err instanceof McpError) {
295
- if (err.code === ErrorCode.ConnectionClosed)
296
- return "connection_closed";
297
- if (err.code === ErrorCode.RequestTimeout)
298
- return "timeout";
299
- return "protocol";
300
- }
301
- const http = describeHttpTransportFailure(err);
302
- if (http !== undefined) {
303
- if (http.httpStatus !== undefined)
304
- return `http_${http.httpStatus}`;
305
- if (err instanceof StreamableHTTPError)
306
- return "not_mcp_response";
307
- const code = networkErrorCode(err);
308
- if (code === undefined)
309
- return "network";
310
- if (code === "ERR_INVALID_URL")
311
- return "invalid_config";
312
- if (code.startsWith("ERR_"))
313
- return "unknown";
314
- if (transport === "stdio")
315
- return "spawn_failed";
316
- if (transport === undefined)
317
- return "connection_failed";
318
- return http.delivered === "no" ? "connect_refused" : "connection_failed";
319
- }
320
- if (isTransportLost(err))
321
- return "connection_closed";
322
- return "unknown";
323
- }
324
245
  function writeEffectWarning(writeEffect) {
325
246
  return writeEffect
326
247
  ? " This tool is write-capable: treat its side effects as POSSIBLY APPLIED and verify the actual state before retrying."
@@ -341,72 +262,63 @@ function rethrowHonestMcpError(err, ctx) {
341
262
  sanitizeMcpErrorTextInPlace(err);
342
263
  if (ctx.idle?.signal.reason === IDLE_WATCHDOG_ABORT_REASON) {
343
264
  const serverLabel = inlineUntrusted(ctx.server);
344
- const e = new Error(`${ctx.what} on MCP server "${serverLabel}" received no response for ${ctx.idle.idleMs}ms (idle watchdog — ` +
265
+ finish(markMcpFailure(new Error(`${ctx.what} on MCP server "${serverLabel}" received no response for ${ctx.idle.idleMs}ms (idle watchdog — ` +
345
266
  `distinct from the ${ctx.timeoutMs}ms total ceiling above). The server may still be alive but is not ` +
346
267
  `responding; the client stopped waiting.${writeEffectWarning(ctx.writeEffect)} (Set MCP_IDLE_TIMEOUT_STDIO ` +
347
- `/ MCP_IDLE_TIMEOUT_HTTP (ms) to change this bound.)`, { cause: err });
348
- e.errorKind = "timeout";
349
- e.details = { timedOut: true, timeoutMs: ctx.idle.idleMs, idleTimeout: true, server: ctx.server };
350
- finish(e);
268
+ `/ MCP_IDLE_TIMEOUT_HTTP (ms) to change this bound.)`, { cause: err }), { code: "timeout", delivered: "unknown", server: ctx.server, timeoutMs: ctx.idle.idleMs, idleTimeout: true }));
351
269
  }
352
270
  if (ctx.signal?.aborted)
353
271
  finish(err);
354
272
  const serverLabel = inlineUntrusted(ctx.server);
355
273
  const fenceServerText = (label, raw) => delimitUntrusted(label, truncateMcpErrorText(raw));
356
- if (err instanceof McpError && err.code === ErrorCode.RequestTimeout) {
357
- const data = err.data;
358
- const totalMs = typeof data?.maxTotalTimeout === "number" ? data.maxTotalTimeout : undefined;
359
- const e = new Error(totalMs !== undefined
360
- ? `${ctx.what} on MCP server "${serverLabel}" exceeded its total time ceiling of ${totalMs}ms (the call stayed alive — e.g. via progress notifications — but ran past the total wall-clock budget). The client stopped waiting, but the server may still be executing the request — the outcome is unknown.${writeEffectWarning(ctx.writeEffect)} (Set the MCP_TOOL_TIMEOUT_TOTAL environment variable (ms) to change this ceiling; MCP_TOOL_TIMEOUT only tunes the per-call timer, currently ${ctx.timeoutMs}ms.)`
361
- : `${ctx.what} on MCP server "${serverLabel}" timed out after ${ctx.timeoutMs}ms. The client stopped waiting, but the server may still be executing the request — the outcome is unknown.${writeEffectWarning(ctx.writeEffect)} (Set the MCP_TOOL_TIMEOUT environment variable (ms) to change this limit.)`, { cause: err });
362
- e.errorKind = "timeout";
363
- e.details =
364
- totalMs !== undefined
365
- ? { timedOut: true, timeoutMs: totalMs, totalTimeout: true, server: ctx.server }
366
- : { timedOut: true, timeoutMs: ctx.timeoutMs, server: ctx.server };
367
- finish(e);
368
- }
369
- if (isTransportLost(err)) {
370
- const e = new Error(`The connection to MCP server "${serverLabel}" was lost while ${ctx.what} was in flight. The request may or may not have executed on the server — the outcome is unknown.${writeEffectWarning(ctx.writeEffect)}`, { cause: err });
371
- e.errorKind = "transport_lost";
372
- e.details = { transportLost: true, server: ctx.server };
373
- finish(e);
374
- }
375
- const httpFailure = describeHttpTransportFailure(err);
376
- if (httpFailure !== undefined) {
377
- const detail = remoteErrorText(err);
378
- const fenced = `\nThe transport error follows as external/untrusted data:\n${fenceServerText(`${ctx.server} transport error`, detail)}`;
379
- const e = new Error(httpFailure.delivered === "no"
380
- ? `${ctx.what} could not reach MCP server "${serverLabel}": ${httpFailure.condition}. The request was not delivered, so the server did not execute it. This server's tools and resources will keep failing until its endpoint is reachable again — do not retry them; use an alternative if one exists.${fenced}`
381
- : `${ctx.what} failed at the transport layer of MCP server "${serverLabel}": ${httpFailure.condition}. The request may or may not have executed on the server — the outcome is unknown.${writeEffectWarning(ctx.writeEffect)}${fenced}`, { cause: err });
382
- e.errorKind = httpFailure.delivered === "no" ? "server_disconnected" : "transport_lost";
383
- e.details = {
384
- server: ctx.server,
385
- ...(httpFailure.delivered === "no" ? {} : { transportLost: true }),
386
- ...(httpFailure.httpStatus !== undefined ? { httpStatus: httpFailure.httpStatus } : {}),
387
- };
388
- finish(e);
389
- }
390
- if (err instanceof McpError) {
391
- const condition = describeMcpSpecErrorCode(err.code);
392
- if (condition !== undefined) {
393
- const e = new Error(`${ctx.what} on MCP server "${serverLabel}" was rejected with MCP protocol error ${err.code} — ${condition}. The server's error text follows as external/untrusted data:\n${fenceServerText(`${ctx.server} error`, err.message)}`, { cause: err });
394
- e.errorKind = "protocol_error";
395
- e.details = { server: ctx.server, specErrorCode: err.code };
396
- finish(e);
274
+ const failure = classifyMcpFailure(err, { phase: "request" });
275
+ const details = (extra) => ({ code: failure.kind, delivered: failure.delivered, server: ctx.server, ...extra });
276
+ switch (failure.kind) {
277
+ case "timeout": {
278
+ const data = err.data;
279
+ const totalMs = typeof data?.maxTotalTimeout === "number" ? data.maxTotalTimeout : undefined;
280
+ finish(markMcpFailure(new Error(totalMs !== undefined
281
+ ? `${ctx.what} on MCP server "${serverLabel}" exceeded its total time ceiling of ${totalMs}ms (the call stayed alive — e.g. via progress notifications — but ran past the total wall-clock budget). The client stopped waiting, but the server may still be executing the request — the outcome is unknown.${writeEffectWarning(ctx.writeEffect)} (Set the MCP_TOOL_TIMEOUT_TOTAL environment variable (ms) to change this ceiling; MCP_TOOL_TIMEOUT only tunes the per-call timer, currently ${ctx.timeoutMs}ms.)`
282
+ : `${ctx.what} on MCP server "${serverLabel}" timed out after ${ctx.timeoutMs}ms. The client stopped waiting, but the server may still be executing the request — the outcome is unknown.${writeEffectWarning(ctx.writeEffect)} (Set the MCP_TOOL_TIMEOUT environment variable (ms) to change this limit.)`, { cause: err }), totalMs !== undefined ? details({ timeoutMs: totalMs, totalTimeout: true }) : details({ timeoutMs: ctx.timeoutMs })));
283
+ }
284
+ case "connection_closed":
285
+ finish(markMcpFailure(new Error(`The connection to MCP server "${serverLabel}" was lost while ${ctx.what} was in flight. The request may or may not have executed on the server — the outcome is unknown.${writeEffectWarning(ctx.writeEffect)}`, { cause: err }), details()));
286
+ case "http_status":
287
+ case "not_mcp_response":
288
+ case "connect_refused":
289
+ case "connection_failed": {
290
+ const httpFailure = describeHttpTransportFailure(err);
291
+ const detail = remoteErrorText(err);
292
+ const fenced = `\nThe transport error follows as external/untrusted data:\n${fenceServerText(`${ctx.server} transport error`, detail)}`;
293
+ finish(markMcpFailure(new Error(failure.delivered === "no"
294
+ ? `${ctx.what} could not reach MCP server "${serverLabel}": ${httpFailure.condition}. The request was not delivered, so the server did not execute it. This server's tools and resources will keep failing until its endpoint is reachable again — do not retry them; use an alternative if one exists.${fenced}`
295
+ : `${ctx.what} failed at the transport layer of MCP server "${serverLabel}": ${httpFailure.condition}. The request may or may not have executed on the server — the outcome is unknown.${writeEffectWarning(ctx.writeEffect)}${fenced}`, { cause: err }), details(failure.httpStatus !== undefined ? { httpStatus: failure.httpStatus } : undefined)));
296
+ }
297
+ case "protocol": {
298
+ const code = err.code;
299
+ const condition = describeMcpSpecErrorCode(code);
300
+ if (condition !== undefined) {
301
+ finish(markMcpFailure(new Error(`${ctx.what} on MCP server "${serverLabel}" was rejected with MCP protocol error ${code} — ${condition}. The server's error text follows as external/untrusted data:\n${fenceServerText(`${ctx.server} error`, err.message)}`, { cause: err }), details({ specErrorCode: code })));
302
+ }
303
+ if (ctx.attributeServer) {
304
+ finish(markMcpFailure(new Error(`${ctx.what} on MCP server "${serverLabel}" failed. The server's error text follows as external/untrusted data:\n${fenceServerText(`${ctx.server} error`, remoteErrorText(err))}`, { cause: err }), details()));
305
+ }
306
+ finish(markMcpFailure(err, details()));
307
+ }
308
+ case "spawn_failed":
309
+ case "invalid_config":
310
+ case "unknown": {
311
+ if (ctx.attributeServer) {
312
+ finish(markMcpFailure(new Error(`${ctx.what} on MCP server "${serverLabel}" failed. The server's error text follows as external/untrusted data:\n${fenceServerText(`${ctx.server} error`, remoteErrorText(err))}`, { cause: err }), details()));
313
+ }
314
+ if (err instanceof Error)
315
+ finish(markMcpFailure(err, details()));
316
+ finish(err);
397
317
  }
398
318
  }
399
- if (ctx.attributeServer) {
400
- const msg = remoteErrorText(err);
401
- finish(new Error(`${ctx.what} on MCP server "${serverLabel}" failed. The server's error text follows as external/untrusted data:\n${fenceServerText(`${ctx.server} error`, msg)}`, { cause: err }));
402
- }
403
- finish(err);
404
319
  }
405
320
  function throwDeadServer(server, what) {
406
- const e = new Error(`MCP server "${inlineUntrusted(server)}" is disconnected (its transport closed earlier in this task). ${what} was not attempted. This server's tools and resources will keep failing until the server is available again — do not retry them; use an alternative if one exists.`);
407
- e.errorKind = "server_disconnected";
408
- e.details = { server };
409
- throw e;
321
+ throw markMcpFailure(new Error(`MCP server "${inlineUntrusted(server)}" is disconnected (its transport closed earlier in this task). ${what} was not attempted. This server's tools and resources will keep failing until the server is available again — do not retry them; use an alternative if one exists.`), { code: "connection_closed", delivered: "no", server });
410
322
  }
411
323
  export { normalizeNameSegment as normalizeMcpName, clampNameSegment } from "./protocol-naming.js";
412
324
  export * from "./image-downsample.js";
@@ -516,6 +428,7 @@ export function resolveProtocolHttpHeaders(protocol, t, principal) {
516
428
  }
517
429
  return t.headers;
518
430
  }
431
+ export const declaredEndpointFetch = (url, init) => fetch(url, { ...init, redirect: "manual" });
519
432
  function buildTransport(spec, principal, kind) {
520
433
  const t = spec.transport;
521
434
  if (kind === "stdio") {
@@ -532,6 +445,7 @@ function buildTransport(spec, principal, kind) {
532
445
  const headers = resolveProtocolHttpHeaders(MCP_NAMESPACE.id, t, principal);
533
446
  return new StreamableHTTPClientTransport(new URL(t.url), {
534
447
  requestInit: headers ? { headers } : undefined,
448
+ fetch: declaredEndpointFetch,
535
449
  });
536
450
  }
537
451
  let autoResizer;
@@ -893,7 +807,8 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
893
807
  }
894
808
  else {
895
809
  warnings.push(asServerWarning(spec, r.reason));
896
- statuses.push({ name: spec.name, status: "failed", errorCode: mcpFailureCodeOf(r.reason, dialKinds[i]), error: namedMcpFailureText(r.reason) });
810
+ const { kind, delivered, httpStatus } = classifyMcpFailure(r.reason, { phase: "connect", transport: dialKinds[i] });
811
+ statuses.push({ name: spec.name, status: "failed", errorCode: kind, delivered, ...(httpStatus !== undefined ? { httpStatus } : {}), error: namedMcpFailureText(r.reason) });
897
812
  }
898
813
  }
899
814
  const resourceTools = buildResourceTools(resourceServers, isServerRevoked);
@@ -933,8 +848,8 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
933
848
  const advertised = new Set(listed.tools.map((t) => t.name));
934
849
  const retainedRaw = listed.incomplete !== undefined ? h.listedRaw.filter((t) => !advertised.has(t.name)) : [];
935
850
  const mergedRaw = retainedRaw.length > 0 ? [...listed.tools, ...retainedRaw] : listed.tools;
936
- cacheMcpToolMetadata(h.client, retainedRaw.length > 0 ? [...retainedRaw, ...listed.tools] : listed.tools);
937
- const { serverTools, serverAxes, dropped } = intakeListedTools({ tools: mergedRaw }, h.spec, h.client, h.health, imageResizer, mcpDisclosure, isServerRevoked, h.transportKind);
851
+ const { serverTools, serverAxes, dropped, accepted } = intakeListedTools({ tools: mergedRaw }, h.spec, h.client, h.health, imageResizer, mcpDisclosure, isServerRevoked, h.transportKind);
852
+ cacheMcpToolMetadata(h.client, accepted);
938
853
  const priorNames = h.tools.map((t) => t.name);
939
854
  const newNames = serverTools.map((t) => t.name);
940
855
  const added = newNames.filter((n) => !priorNames.includes(n));
@@ -1305,7 +1220,7 @@ function buildResourceTools(resourceServers, isServerRevoked = () => false) {
1305
1220
  if (isServerRevoked(server)) {
1306
1221
  return {
1307
1222
  content: [{ type: "text", text: `${what} was refused: MCP server "${server}" was revoked by the operator mid-session. The request was NOT sent. This server stays on this run's tool roster — every call to it is refused the same way, so don't retry it.` }],
1308
- details: { error: "mcp.server_revoked", code: "mcp.server_revoked", server },
1223
+ details: { error: "mcp.server_revoked", code: "mcp.server_revoked", delivered: "no", server },
1309
1224
  terminate: false,
1310
1225
  isError: true,
1311
1226
  };
@@ -1364,7 +1279,7 @@ function buildResourceTools(resourceServers, isServerRevoked = () => false) {
1364
1279
  if (isServerRevoked(server)) {
1365
1280
  return {
1366
1281
  content: [{ type: "text", text: `${what} was refused: MCP server "${server}" was revoked by the operator mid-session. The request was NOT sent. This server stays on this run's tool roster — every call to it is refused the same way, so don't retry it.` }],
1367
- details: { error: "mcp.server_revoked", code: "mcp.server_revoked", server },
1282
+ details: { error: "mcp.server_revoked", code: "mcp.server_revoked", delivered: "no", server },
1368
1283
  terminate: false,
1369
1284
  isError: true,
1370
1285
  };
@@ -1524,8 +1439,8 @@ async function connectServer(spec, principal, onElicit, imageResizer, reminderDi
1524
1439
  announce.fn?.();
1525
1440
  };
1526
1441
  const listed = await listToolsLenient(client, startupOpts);
1527
- cacheMcpToolMetadata(client, listed.tools);
1528
- const { serverTools, serverAxes, dropped, advisories } = intakeListedTools(listed, spec, client, health, imageResizer, reminderDisclosure, isServerRevoked, transportKind);
1442
+ const { serverTools, serverAxes, dropped, advisories, accepted } = intakeListedTools(listed, spec, client, health, imageResizer, reminderDisclosure, isServerRevoked, transportKind);
1443
+ cacheMcpToolMetadata(client, accepted);
1529
1444
  const caps = client.getServerCapabilities();
1530
1445
  const resourceInfo = caps?.resources
1531
1446
  ? {
@@ -1567,6 +1482,7 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
1567
1482
  const serverTools = [];
1568
1483
  const serverAxes = [];
1569
1484
  const dropped = [];
1485
+ const accepted = [];
1570
1486
  const advisories = [];
1571
1487
  const mintedNames = new Map();
1572
1488
  for (const t of listed.tools) {
@@ -1607,6 +1523,7 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
1607
1523
  advisories.push({ tool: inlineUntrusted(t.name), reason: inlineUntrusted(schemaAdvisory, 240) });
1608
1524
  }
1609
1525
  const remoteName = t.name;
1526
+ const taskRequired = t.execution !== null && typeof t.execution === "object" && t.execution.taskSupport === "required";
1610
1527
  const namespacedName = mintNamespacedToolName(MCP_NAMESPACE, spec.name, remoteName);
1611
1528
  const mintedBy = mintedNames.get(namespacedName);
1612
1529
  if (mintedBy !== undefined) {
@@ -1639,7 +1556,15 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
1639
1556
  if (isServerRevoked?.(spec.name) === true) {
1640
1557
  return {
1641
1558
  content: [{ type: "text", text: `The call to MCP server "${spec.name}" was refused: the server was revoked by the operator mid-session. The call was NOT sent, so the server did not execute it. This server stays on this run's tool roster — every call to it is refused the same way, so don't retry it.` }],
1642
- details: { error: "mcp.server_revoked", code: "mcp.server_revoked", server: spec.name },
1559
+ details: { error: "mcp.server_revoked", code: "mcp.server_revoked", delivered: "no", server: spec.name },
1560
+ terminate: false,
1561
+ isError: true,
1562
+ };
1563
+ }
1564
+ if (taskRequired) {
1565
+ return {
1566
+ content: [{ type: "text", text: "The call was refused: this tool requires task-based execution (MCP tasks), which this client does not drive. The call was NOT sent, so the server did not execute it. Every call to this tool is refused the same way, so don't retry it." }],
1567
+ details: { error: "mcp.task_required", code: "mcp.task_required", delivered: "no", server: spec.name },
1643
1568
  terminate: false,
1644
1569
  isError: true,
1645
1570
  };
@@ -1693,7 +1618,7 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
1693
1618
  if (reminderDisclosure !== undefined) {
1694
1619
  observeReminderMarkEcho({ text: msg, mark: reminderDisclosure.mark, outlet: "mcp", counts: reminderDisclosure.counts });
1695
1620
  }
1696
- throw new Error(msg);
1621
+ throw Object.assign(new Error(msg), { details: { code: "mcp.tool_error", delivered: "yes", server: spec.name } });
1697
1622
  }
1698
1623
  const content = gateMcpOutput(mapped);
1699
1624
  const sc = res.structuredContent;
@@ -1731,8 +1656,9 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
1731
1656
  },
1732
1657
  };
1733
1658
  serverTools.push(declaredContentOrigin !== undefined ? withContentOrigin(mounted, declaredContentOrigin) : mounted);
1659
+ accepted.push(t);
1734
1660
  }
1735
- return { serverTools, serverAxes, dropped, advisories };
1661
+ return { serverTools, serverAxes, dropped, advisories, accepted };
1736
1662
  }
1737
1663
  function asServerWarning(spec, err) {
1738
1664
  const warning = new Error(`mcp: server "${spec.name}" failed to connect — skipped (${namedMcpFailureText(err)})`, { cause: sanitizedCause(err) });
@@ -0,0 +1,99 @@
1
+ /**
2
+ * The MODEL seat: how a task names a model (`ModelRef`), the roles a deployment binds models to
3
+ * (`ModelRole` / `RoleSpec` / `ModelRoles`), and the provider seat a ref resolves to (`Brain`). Layer 0
4
+ * vocabulary beside `terminal-cause.ts` and `gate-outcome.ts` — no import above this layer, so any
5
+ * module may read the words. `types.ts` re-exports every name below, so no consumer's import changes.
6
+ */
7
+ import type { ThinkingLevel } from "../internal/harness.js";
8
+ import type { CompleteSimpleFn, Model, StreamFn } from "../internal/llm.js";
9
+ /** A model reference: either a fully-formed Model, or a `"provider/id"` ref resolved via a catalog. */
10
+ export type ModelRef = string | Model;
11
+ /**
12
+ * Logical model roles, so a deployment declares its models once and every subsystem asks for its
13
+ * role instead of repeating a model name. Resolved against `RunnerDeps.roles` / `TaskSpec.roles`
14
+ * with a fallback chain (e.g. `summarize → default`, `synthesize → team → default`).
15
+ * - `default` — the main task model when `TaskSpec.model` is omitted.
16
+ * - `summarize` — between-turn compaction/summarization. Absent = the MAIN task model, which is the
17
+ * correct default baseline: the summary request FORKS the main conversation's already-paid
18
+ * prompt-cache prefix (design/169-A CC form — same system prompt, same message prefix, one
19
+ * appended instruction), so the marginal cost is a cache read plus one summary's output, not a
20
+ * full-price re-prefill. Configuring a separate cheap model here is an explicit deployment
21
+ * tradeoff (quality for price): an independent model cannot share the main prefix, so it pays the
22
+ * full independent-request serialization every boundary AND summarizes with a weaker model.
23
+ * - `subagent` — delegated sub-runs (`createSubagentTool` without an explicit model).
24
+ * - `team` / `synthesize` — team members / the neutral synthesizer.
25
+ * - `advisor` — the strong "teacher" a cheap student escalates to (`runWithTeacher`).
26
+ * - `classifier` — the auto-mode permission classifier (a roster-cheap tier; falls back
27
+ * `summarize → default`, tier default `flash` on tier-expanded catalogs).
28
+ * - `consolidate` — the memory-consolidation distillation driver (design/376). Falls back to
29
+ * `summarize` and then REFUSES LOUDLY — deliberately never to `default`: one consolidation run
30
+ * is a whole-library read (~10⁵ prompt tokens), so silently escalating to the most expensive
31
+ * main-task model is the bad-value-silence family, not a convenience. The divergence from the
32
+ * classifier family's `→ default` tail is calibrated on call volume: a classifier resolves one
33
+ * short classification, this role resolves a full-library batch. An explicit
34
+ * `roles.consolidate = <main model>` is a legal deployment choice; the summarize hop is kept
35
+ * because a deployment that declared a cheap summarize tier has already made the cost choice
36
+ * this role wants (note the summarize DEFAULT rationale — prompt-cache forking — does not
37
+ * apply here: consolidation is an offline independent request with no shared prefix).
38
+ */
39
+ export type ModelRole = "default" | "summarize" | "subagent" | "team" | "synthesize" | "advisor" | "verifier" | "classifier" | "consolidate";
40
+ /**
41
+ * A role's model: a `ModelRef`, or a config object that pins a `model`, **selects** one by
42
+ * capability/price (`select`, design/11 Layer 2), and/or sets a default `thinking` level.
43
+ */
44
+ export type RoleSpec = ModelRef | {
45
+ model?: ModelRef;
46
+ select?: import("./select-model.js").ModelCriteria;
47
+ thinking?: ThinkingLevel;
48
+ /**
49
+ * Optional preset system prompt for this role (e.g. {@link CODE_AGENT_PROMPT} for a dev role).
50
+ * Used only when the task does not supply its own `TaskSpec.systemPrompt`. Lets coding roles run
51
+ * a coding prompt while other roles keep the neutral default — without touching the global default.
52
+ */
53
+ systemPrompt?: string;
54
+ };
55
+ /** Map of role → model. `TaskSpec.roles` overrides `RunnerDeps.roles` per task/scenario. */
56
+ export type ModelRoles = Partial<Record<ModelRole, RoleSpec>>;
57
+ /** The "external brain": a streaming completion function plus a non-streaming one (auto-derived if omitted). */
58
+ export interface Brain {
59
+ /** Streaming completion. Must follow the AssistantMessageEvent stream contract (never throw post-invocation).
60
+ *
61
+ * NOTE: the return type is a union — `Stream | Promise<Stream>`. Third-party direct callers must
62
+ * normalize first (`const s = await brain.stream(...)`) before iterating or reading `.result()`,
63
+ * otherwise TS reports TS2339 on the union. The Runner already handles this internally. */
64
+ stream: StreamFn;
65
+ /**
66
+ * Optional non-streaming completion used by compaction/summarization.
67
+ * If omitted, it is derived from `stream` by consuming the stream to its final result.
68
+ *
69
+ * This is exactly the runtime's `completeSimple` type: the engine forwards the value you supply
70
+ * as-is, with no adapting wrapper in between.
71
+ *
72
+ * CONTRACT BEYOND WHAT THE TYPE STATES — every field named here is OPTIONAL on
73
+ * {@link AssistantMessage}, so a message rebuilt from a field whitelist still typechecks while
74
+ * silently disabling recovery:
75
+ * - return the AssistantMessage with ALL fields intact, in particular `errorKind` (the compaction
76
+ * summary path keys its budget-escalation recovery on it) and `errorMessage`;
77
+ * - honor `options.signal` — the summary call runs under a soft deadline that aborts through it;
78
+ * - honor `options.maxTokens` — that is the escalating budget the recovery re-issues the call with.
79
+ */
80
+ complete?: CompleteSimpleFn;
81
+ /**
82
+ * Optional key↔URL pairing judge (see `src/brain/route-adjudicator.ts` for the law). Answers, for a
83
+ * model this brain would serve, whether the credential the request would carry belongs to the URL
84
+ * it would target — WITHOUT sending anything. `perModelAuth` is the caller's already-resolved
85
+ * per-model auth (the `getApiKeyAndHeaders` result), so a resolution seat can pre-flight the exact
86
+ * request it is about to make. First-party brains implement it against their own config; the
87
+ * decorators (routing/failover/degrading/circuit-breaker) re-dispatch it the way their `stream`
88
+ * would. `undefined` = this brain cannot judge (a custom brain without the face) — callers must
89
+ * treat that as "no judgment", never as OK or as broken.
90
+ *
91
+ * The same law runs again inside the first-party brains' request build (single source, called
92
+ * twice): a broken pairing that skips the pre-flight still refuses loudly at the request instead
93
+ * of sending a credential to a host it is not paired with.
94
+ */
95
+ adjudicateRoute?: (model: Model, perModelAuth?: {
96
+ apiKey?: string;
97
+ headers?: Record<string, string>;
98
+ }) => import("../internal/llm.js").RouteAdjudication | undefined;
99
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,131 @@
1
+ /**
2
+ * The pause registry — ONE table for the three faces of a durable pause (why it stopped / what it left
3
+ * hanging / what brings it back), the three word types and the resume match DERIVED from it, and the
4
+ * registry-bound `gate` × `pendingAction` pair with its checked constructor. A vocabulary module on the
5
+ * floor: it types against `checkpoint-store.ts` (type-only, no runtime edge) and `checkpoint-store.ts`
6
+ * reaches down for the pair type.
7
+ */
8
+ import type { AssertAllKeysHandled } from "./ask-origin.js";
9
+ import type { Checkpoint, CheckpointGate, CheckpointRow, PendingAction, ResumeOutcome } from "./checkpoint-store.js";
10
+ /**
11
+ * THE registry of pauses — one row per way a run can stop and wait, and the ONE place the three faces
12
+ * of that pause are declared together: WHY it stopped (`CheckpointGate.kind`, the row key), WHAT it left
13
+ * hanging (`pending` — the {@link PendingAction} kind the row carries), and WHAT BRINGS IT BACK (`resume`
14
+ * — the {@link ResumeOutcome} gate word a caller must answer with). The three vocabularies used to be
15
+ * three independent `as const` lists whose correspondence lived in a six-arm disjunction in the resume
16
+ * entry and two FACET comments; adding a kind of pause meant three declarations, one disjunction arm and
17
+ * two comments, and missing any of them was silent. Now the three word types, the resume match and the
18
+ * facet are DERIVED from this table: adding a pause = adding a row, and the fences below red the build
19
+ * until every declaration agrees with it.
20
+ *
21
+ * - `facet` — the deadline semantic a deployment's reaper applies (see {@link Checkpoint.deadline}):
22
+ * `A` = approval gates, whose `deadline` is an SLA resolve-deny; `B` = unattended-TTL gates, whose
23
+ * `deadline` is an abandonment TTL (`expire`/`reap` only — never a resolve-deny; these pauses produce
24
+ * no `tool_end` and therefore no settlement); `-` = the non-gate park (`task_done`).
25
+ * - `taskStatus` — the `TaskResult.status` word a run assembles when it stops at this gate (approvals and
26
+ * resource slices read `suspended`; the two review pauses read `needs_review`).
27
+ * - `terminal` — `task_done` is the 1C handle / deployment-park convention, not a run pause core mints.
28
+ */
29
+ export declare const PAUSE_REGISTRY: {
30
+ readonly human: {
31
+ readonly pending: "tool_approval";
32
+ readonly resume: "policy_ask";
33
+ readonly facet: "A";
34
+ readonly taskStatus: "suspended";
35
+ readonly terminal: false;
36
+ };
37
+ readonly irreversible_ask: {
38
+ readonly pending: "tool_approval";
39
+ readonly resume: "policy_ask";
40
+ readonly facet: "A";
41
+ readonly taskStatus: "suspended";
42
+ readonly terminal: false;
43
+ };
44
+ readonly resource_limit: {
45
+ readonly pending: "resource_limit";
46
+ readonly resume: "resource_limit";
47
+ readonly facet: "B";
48
+ readonly taskStatus: "suspended";
49
+ readonly terminal: false;
50
+ };
51
+ readonly needs_review: {
52
+ readonly pending: "review";
53
+ readonly resume: "dry_run_review";
54
+ readonly facet: "B";
55
+ readonly taskStatus: "needs_review";
56
+ readonly terminal: false;
57
+ };
58
+ readonly plan_review: {
59
+ readonly pending: "plan_review";
60
+ readonly resume: "plan_review";
61
+ readonly facet: "B";
62
+ readonly taskStatus: "needs_review";
63
+ readonly terminal: false;
64
+ };
65
+ readonly task_done: {
66
+ readonly pending: "task_done";
67
+ readonly resume: "task_done";
68
+ readonly facet: "-";
69
+ readonly taskStatus: "suspended";
70
+ readonly terminal: true;
71
+ };
72
+ };
73
+ /** The pause kinds — the row keys of {@link PAUSE_REGISTRY}; `CheckpointGate.kind` is fenced to equal it. */
74
+ export type GateKind = keyof typeof PAUSE_REGISTRY;
75
+ /** The pending-action kinds — the `pending` column; `PendingAction.kind` is fenced to equal it. */
76
+ export type PendingKind = (typeof PAUSE_REGISTRY)[GateKind]["pending"];
77
+ /** The resume gate words — the `resume` column plus `wake` (the un-park verb, which is not a gate decision
78
+ * and so has no registry row); `ResumeOutcome.gate` is fenced to equal it. */
79
+ export type ResumeGate = (typeof PAUSE_REGISTRY)[GateKind]["resume"] | "wake";
80
+ /** Runtime twin of {@link GateKind}. */
81
+ export declare function isGateKind(v: unknown): v is GateKind;
82
+ /** The one resume-match predicate: does this outcome gate word answer a checkpoint of this kind? `wake`
83
+ * answers any (it passed its own, stricter, non-gate validation upstream); every other word must be the
84
+ * row's `resume` column. Replaces a six-arm hand-written disjunction. */
85
+ export declare function resumeGateMatches(gateKind: GateKind, outcomeGate: ResumeGate): boolean;
86
+ /**
87
+ * The one CONSTRUCTOR of a registry-bound pause pair from a gate and a pending action that arrive
88
+ * separately typed (a mint site that chooses its gate in a ternary, a store deserializing a row, a test
89
+ * fixture taking overrides). The generic binds the pending action's kind to the gate's registry row at
90
+ * the call site; the runtime check is the belt for the wide call (`CheckpointGate` × `PendingAction`) and
91
+ * for a deserialized row, where the type says nothing about the bytes. A pair the registry does not pair
92
+ * is refused loudly here rather than persisted and discovered at resume.
93
+ */
94
+ export declare function pauseOf<K extends GateKind>(gate: Extract<CheckpointGate, {
95
+ kind: K;
96
+ }>, pendingAction: Extract<PendingAction, {
97
+ kind: (typeof PAUSE_REGISTRY)[K]["pending"];
98
+ }>): CheckpointPause<K>;
99
+ /** The loose input shape of {@link checkpointFrom}: a row's fields with its gate and pending action typed
100
+ * independently (as a deserializer or a fixture holds them). */
101
+ export type CheckpointFields = CheckpointRow & {
102
+ gate: CheckpointGate;
103
+ pendingAction: PendingAction;
104
+ };
105
+ /** Build a {@link Checkpoint} from independently typed fields — {@link pauseOf} binds (and checks) the pair. */
106
+ export declare function checkpointFrom(fields: CheckpointFields): Checkpoint;
107
+ /** Compile-time fences (each `never` while the declaration and the registry agree, in BOTH directions). */
108
+ export type GateDeclarationCoversEveryRegistryRow = AssertAllKeysHandled<Exclude<GateKind, CheckpointGate["kind"]>>;
109
+ export type RegistryCoversEveryGateDeclaration = AssertAllKeysHandled<Exclude<CheckpointGate["kind"], GateKind>>;
110
+ export type PendingDeclarationCoversEveryRegistryColumn = AssertAllKeysHandled<Exclude<PendingKind, PendingAction["kind"]>>;
111
+ export type RegistryCoversEveryPendingDeclaration = AssertAllKeysHandled<Exclude<PendingAction["kind"], PendingKind>>;
112
+ export type ResumeDeclarationCoversEveryRegistryColumn = AssertAllKeysHandled<Exclude<ResumeGate, ResumeOutcome["gate"]>>;
113
+ export type RegistryCoversEveryResumeDeclaration = AssertAllKeysHandled<Exclude<ResumeOutcome["gate"], ResumeGate>>;
114
+ /**
115
+ * One row's `gate` × `pendingAction` pair, BOUND by {@link PAUSE_REGISTRY}: a `needs_review` gate carries a
116
+ * `review` pending action and nothing else, an approval gate a `tool_approval`, and so on. Distributed over
117
+ * the registry rows so that a row literal pairing a gate with another kind's pending action does not
118
+ * type-check — the invariant that used to be a sentence in a comment is now the shape of the type.
119
+ */
120
+ type PairOf<K extends GateKind> = {
121
+ gate: Extract<CheckpointGate, {
122
+ kind: K;
123
+ }>;
124
+ pendingAction: Extract<PendingAction, {
125
+ kind: (typeof PAUSE_REGISTRY)[K]["pending"];
126
+ }>;
127
+ };
128
+ export type CheckpointPause<K extends GateKind = GateKind> = {
129
+ [P in K]: PairOf<P>;
130
+ }[K];
131
+ export {};
@@ -0,0 +1,27 @@
1
+ export const PAUSE_REGISTRY = {
2
+ human: { pending: "tool_approval", resume: "policy_ask", facet: "A", taskStatus: "suspended", terminal: false },
3
+ irreversible_ask: { pending: "tool_approval", resume: "policy_ask", facet: "A", taskStatus: "suspended", terminal: false },
4
+ resource_limit: { pending: "resource_limit", resume: "resource_limit", facet: "B", taskStatus: "suspended", terminal: false },
5
+ needs_review: { pending: "review", resume: "dry_run_review", facet: "B", taskStatus: "needs_review", terminal: false },
6
+ plan_review: { pending: "plan_review", resume: "plan_review", facet: "B", taskStatus: "needs_review", terminal: false },
7
+ task_done: { pending: "task_done", resume: "task_done", facet: "-", taskStatus: "suspended", terminal: true },
8
+ };
9
+ export function isGateKind(v) {
10
+ return typeof v === "string" && Object.prototype.hasOwnProperty.call(PAUSE_REGISTRY, v);
11
+ }
12
+ export function resumeGateMatches(gateKind, outcomeGate) {
13
+ return outcomeGate === "wake" || PAUSE_REGISTRY[gateKind].resume === outcomeGate;
14
+ }
15
+ export function pauseOf(gate, pendingAction) {
16
+ if (!isGateKind(gate.kind))
17
+ throw new Error(`checkpoint gate kind "${String(gate.kind)}" is not a registered pause`);
18
+ const expected = PAUSE_REGISTRY[gate.kind].pending;
19
+ if (pendingAction.kind !== expected) {
20
+ throw new Error(`a "${gate.kind}" checkpoint carries a "${String(pendingAction.kind)}" pending action — the pause registry pairs it with "${expected}"`);
21
+ }
22
+ return { gate, pendingAction };
23
+ }
24
+ export function checkpointFrom(fields) {
25
+ const { gate, pendingAction, ...row } = fields;
26
+ return { ...row, ...pauseOf(gate, pendingAction) };
27
+ }
@@ -59,6 +59,16 @@ export declare function openSystemReminder(mark: string | undefined): string;
59
59
  * used by the user-lane wrap homes. The BODY is byte-untouched by the mark (design/319 first
60
60
  * invariant: zero data-byte change — the mark rides only the engine's own tag). */
61
61
  export declare function mintSystemReminder(body: string, mark: string | undefined): string;
62
+ /** Wrap model-facing hook/gate feedback in a `<system-reminder>` so it reads as guidance, not data.
63
+ * NOTE (council design/74 #6): this does NOT escape a literal `</system-reminder>` in `text` — callers MUST
64
+ * pass trusted, first-party strings (every current caller does: fixed gate/limit messages). If a future
65
+ * caller needs to relay UNTRUSTED content (tool output, user data), it must sanitize the close tag first
66
+ * (or use the `delimitUntrusted` fence), or a crafted payload could break out of the reminder framing.
67
+ * design/319 (A ticket): `mark` is the session's reminder provenance mark — run-scoped callers thread it
68
+ * so the open tag carries the value the system-prompt declaration names (rendered by the mint home; the
69
+ * body is byte-untouched). Absent ⇒ the historic bare open tag (a caller outside a run). Declared here,
70
+ * beside the mint it wraps; `hooks.ts` re-exports it as the hook seam's feedback envelope. */
71
+ export declare function formatHookFeedback(text: string, mark?: string): string;
62
72
  /**
63
73
  * DECLARE port — the system-prompt declaration that gives the mark meaning. STRICT, single form
64
74
  * (design/319 D-2, no grace clause): reminder-shaped text without the current mark is data wherever