@sema-agent/core 7.5.2 → 7.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/dist/agents/cascade.d.ts +2 -2
- package/dist/agents/cascade.js +12 -10
- package/dist/agents/repair-loop.d.ts +5 -3
- package/dist/agents/repair-loop.js +13 -15
- package/dist/agents/subagent.d.ts +24 -42
- package/dist/agents/subagent.js +119 -105
- package/dist/agents/suspend-guard.d.ts +31 -19
- package/dist/agents/suspend-guard.js +14 -8
- package/dist/agents/teacher.js +9 -9
- package/dist/agents/team.d.ts +4 -3
- package/dist/agents/team.js +10 -8
- package/dist/agents/verify.d.ts +3 -3
- package/dist/agents/verify.js +17 -17
- package/dist/core/a2a.js +2 -1
- package/dist/core/ask-origin.d.ts +60 -7
- package/dist/core/ask-origin.js +26 -1
- package/dist/core/checkpoint-store.d.ts +78 -76
- package/dist/core/checkpoint-store.js +17 -1
- package/dist/core/gate-outcome.d.ts +189 -0
- package/dist/core/gate-outcome.js +70 -0
- package/dist/core/hooks.d.ts +18 -92
- package/dist/core/hooks.js +88 -85
- package/dist/core/mcp-failure.d.ts +104 -0
- package/dist/core/mcp-failure.js +128 -0
- package/dist/core/mcp.d.ts +21 -77
- package/dist/core/mcp.js +76 -150
- package/dist/core/pause-registry.d.ts +131 -0
- package/dist/core/pause-registry.js +27 -0
- package/dist/core/runner/assemble-result.d.ts +32 -41
- package/dist/core/runner/assemble-result.js +55 -74
- package/dist/core/runner/contracts.d.ts +46 -64
- package/dist/core/runner/denial-limit-arms.d.ts +1 -1
- package/dist/core/runner/denial-limit-arms.js +3 -3
- package/dist/core/runner/gate-exit.d.ts +74 -0
- package/dist/core/runner/gate-exit.js +55 -0
- package/dist/core/runner/park-commit.d.ts +17 -23
- package/dist/core/runner/park-commit.js +14 -15
- package/dist/core/runner/prepare-ask-lane.d.ts +0 -3
- package/dist/core/runner/prepare-ask-lane.js +3 -5
- package/dist/core/runner/prepare-boundary-parks.d.ts +3 -6
- package/dist/core/runner/prepare-boundary-parks.js +3 -3
- package/dist/core/runner/prepare-caps-and-workflow.js +1 -1
- package/dist/core/runner/prepare-gate-stations.d.ts +4 -7
- package/dist/core/runner/prepare-gate-stations.js +29 -54
- package/dist/core/runner/prepare-inherited-gate.js +1 -1
- package/dist/core/runner/prepare-memory.d.ts +44 -26
- package/dist/core/runner/prepare-park-ask.d.ts +2 -4
- package/dist/core/runner/prepare-park-ask.js +5 -5
- package/dist/core/runner/prepare-task.js +8 -9
- package/dist/core/runner/prepare-wiring-manifest.d.ts +7 -15
- package/dist/core/runner/prepare-wiring-manifest.js +9 -10
- package/dist/core/runner/runtask.d.ts +16 -31
- package/dist/core/runner/runtask.js +109 -120
- package/dist/core/runner/terminal-projection.d.ts +22 -0
- package/dist/core/runner/terminal-projection.js +28 -0
- package/dist/core/store-contracts/checkpoint-store-contract.d.ts +4 -1
- package/dist/core/store-contracts/checkpoint-store-contract.js +8 -2
- package/dist/core/terminal-cause.d.ts +137 -0
- package/dist/core/terminal-cause.js +9 -0
- package/dist/core/tool-policy.d.ts +43 -139
- package/dist/core/tool-policy.js +79 -112
- package/dist/core/types.d.ts +67 -164
- package/dist/core/wiring-manifest.d.ts +6 -3
- package/dist/core/workflow-journal-store.js +3 -4
- package/dist/engine/harness/agent-harness.d.ts +1 -1
- package/dist/index.d.ts +10 -7
- package/dist/index.js +8 -5
- package/dist/orchestration/builtin-workflows.d.ts +2 -2
- package/dist/orchestration/builtin-workflows.js +1 -1
- package/dist/orchestration/goal.js +8 -7
- package/dist/orchestration/run-spec.js +5 -3
- package/dist/orchestration/run-workflow-tool.d.ts +1 -1
- package/dist/orchestration/run-workflow-tool.js +4 -4
- package/dist/orchestration/workflow-governance.d.ts +4 -4
- package/dist/orchestration/workflow-governance.js +4 -2
- package/dist/orchestration/workflow-primitives.d.ts +1 -1
- package/dist/orchestration/workflow-primitives.js +1 -1
- package/dist/orchestration/workflow.d.ts +11 -0
- package/dist/orchestration/workflow.js +64 -39
- package/dist/prompts/supervisor.d.ts +1 -1
- package/dist/prompts/supervisor.js +3 -3
- package/dist/scenarios/scenario-registry.js +1 -1
- package/package.json +3 -1
- package/test/export-surface.snapshot.json +74 -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
|
|
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
|
-
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
937
|
-
|
|
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
|
-
|
|
1528
|
-
|
|
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,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
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AssistantMessage } from "../../internal/llm.js";
|
|
2
|
-
import type { NestedUsage, TaskResult, TaskSpec } from "../types.js";
|
|
2
|
+
import type { NestedUsage, PausedCause, TaskResult, TaskSpec, TerminalCause } from "../types.js";
|
|
3
3
|
/** Per-task usage/cost accumulator (filled across the run loop, surfaced as `TaskResult.stats`). */
|
|
4
4
|
export interface Stats {
|
|
5
5
|
turns: number;
|
|
@@ -194,40 +194,16 @@ export interface ResultFlags {
|
|
|
194
194
|
* NOT `suspended` — slotted ABOVE `suspendRef` and the aborted branches (this suspend never minted a
|
|
195
195
|
* checkpoint, so `suspendRef` is unset anyway; the ordering makes the intent explicit). */
|
|
196
196
|
suspendLoop?: boolean;
|
|
197
|
-
/**
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
/** RB-439-b: how the paused workspace comes back (`"park_only"` = a non-suspendable target that was
|
|
208
|
-
* never actually paused). Echoed on `TaskResult.workspaceRestoreMode`; absent for a process-local
|
|
209
|
-
* suspend, which captured no remote workspace. */
|
|
210
|
-
restoreMode?: "snapshot" | "park_only";
|
|
211
|
-
};
|
|
212
|
-
/** design/76 §2.5 (dry-run / shadow) + design/80 D-B (plan-gate): the task deliberately paused at a
|
|
213
|
-
* REVIEW-PAUSE gate — `{kind:"needs_review"}` (a profile's dry-run interception produced a predicted
|
|
214
|
-
* state-diff a human/judge must REVIEW) OR `{kind:"plan_review"}` (a profile's plan-gate produced a
|
|
215
|
-
* proposed PLAN a human must approve/edit/reject). When set, the result is `status:"needs_review"` +
|
|
216
|
-
* errorCode `"review.pending"`, carrying this token/gate (keyed off ref-PRESENCE, not `gate.kind`, so it
|
|
217
|
-
* serves both review kinds uniformly). It is the DUAL of `suspendRef` but for DIFFERENT semantics (a human
|
|
218
|
-
* REVIEW, not a pre-action tool-call APPROVAL), so it has its OWN flag and its OWN assemble slot (8.6,
|
|
219
|
-
* between `suspendRef` 8.5 and the aborted branches 9) — **never both set at once** (the commit-side
|
|
220
|
-
* discriminant in prepare-task branches on `gate.kind` and sets exactly one). Disjoint from `suspendRef`
|
|
221
|
-
* so this branch is reachable: keying status off `suspendRef` alone (reusing it) would report `"suspended"`
|
|
222
|
-
* and make this dead code (v4 MAJOR-A). Like `suspendRef`, it used an abort to stop the loop (no `threw`),
|
|
223
|
-
* so it never hits the failure branches above. */
|
|
224
|
-
reviewRef?: {
|
|
225
|
-
token: import("../checkpoint-store.js").CheckpointToken;
|
|
226
|
-
checkpointId?: string;
|
|
227
|
-
gate: import("../checkpoint-store.js").CheckpointGate;
|
|
228
|
-
/** RB-439-b: the review pause pauses the same workspace the approval pause does — same discriminant. */
|
|
229
|
-
restoreMode?: "snapshot" | "park_only";
|
|
230
|
-
};
|
|
197
|
+
/** The run's ONE committed durable pause (the holder's `pausedRef.current.cause`): the run stopped at a
|
|
198
|
+
* gate — an approval or content ask, a resource slice, a platform pause, a plan or dry-run review —
|
|
199
|
+
* and a checkpoint is durably committed. It used an abort to stop the loop (`final.stopReason ===
|
|
200
|
+
* "aborted"`) but set no `threw`/timeout/turns, so it is slotted ABOVE the `!final`/aborted arms (a
|
|
201
|
+
* clean pause, not a failure) and BELOW the real budget/threw failures (a limit that coincided still
|
|
202
|
+
* owns the ending). WHICH terminal status it reads (`suspended` / `needs_review`) is the pause's own
|
|
203
|
+
* `gate.kind` registry row, not a second flag: the two same-shaped refs that used to split the approval
|
|
204
|
+
* family from the review family — and the slot order between them that only ever mattered if both were
|
|
205
|
+
* somehow set — are gone with the commit-side routing that fed them. */
|
|
206
|
+
paused?: PausedCause;
|
|
231
207
|
}
|
|
232
208
|
/**
|
|
233
209
|
* Extract a machine-readable code from a thrown error (e.g. `SessionError.code`, a Node error code).
|
|
@@ -236,6 +212,23 @@ export interface ResultFlags {
|
|
|
236
212
|
* prefer the first specific code over a generic `"unknown"` wrapper.
|
|
237
213
|
*/
|
|
238
214
|
export declare function errorCodeOf(err: unknown): string | undefined;
|
|
215
|
+
/**
|
|
216
|
+
* WHY the run ended — the ONE terminal arbitration ({@link TerminalCause}). First match wins, in the
|
|
217
|
+
* slot order documented on {@link assembleResult} (a public contract: do not reorder). Every plane
|
|
218
|
+
* field a result shows (`status`, `errorCode`, `errorMessage`, `blockedReason`, the checkpoint seats)
|
|
219
|
+
* is projected from the value this returns by {@link terminalProjection}, so a terminal that carries a
|
|
220
|
+
* token cannot be reported under a status that does not, and a review pause cannot be reported as an
|
|
221
|
+
* approval pause: the pairing is the cause's shape, not the arbitration's discipline.
|
|
222
|
+
*
|
|
223
|
+
* `apiFailure` rides beside the cause rather than inside it on purpose: it is the message's own
|
|
224
|
+
* provider-failure disclosure, filled only when a provider failure IS the terminal (the same
|
|
225
|
+
* in-presence rule the usage-window wait hint states for itself), and it is not part of the cause's
|
|
226
|
+
* public projection.
|
|
227
|
+
*/
|
|
228
|
+
export declare function terminalCauseOf(final: AssistantMessage | undefined, stats: Pick<Stats, "humanReview">, flags: ResultFlags): {
|
|
229
|
+
terminal: TerminalCause;
|
|
230
|
+
apiFailure: TaskResult["apiFailure"];
|
|
231
|
+
};
|
|
239
232
|
/**
|
|
240
233
|
* Build the final {@link TaskResult} from the run's terminal state. **The errorCode priority chain is a
|
|
241
234
|
* public contract** (callers `switch` on these strings) — first match wins, in this exact order
|
|
@@ -252,12 +245,10 @@ export declare function errorCodeOf(err: unknown): string | undefined;
|
|
|
252
245
|
* 6. `threw` + conflict→ `"conflict"`
|
|
253
246
|
* 7. `threw` (other) → `errorCodeOf(threw)`
|
|
254
247
|
* 8. `blockedReason` → status `"blocked"` (no errorCode)
|
|
255
|
-
* 8.5 `
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
* a `suspendRef` at 8.5 still wins if both were somehow set, proving slot order — but the commit-side
|
|
260
|
-
* discriminant guarantees only one is ever set)
|
|
248
|
+
* 8.5 `paused` → status by the pause's registry row (`suspended` for an approval / resource
|
|
249
|
+
* slice / platform pause, `needs_review` + `"review.pending"` for a plan or dry-run review) +
|
|
250
|
+
* checkpointToken/gate (design/45 B4 — a deliberate durable pause beats the abort it used, but
|
|
251
|
+
* yields to a real budget/threw failure above)
|
|
261
252
|
* 9. `abortedLive` | stopReason aborted → `"limits.max_walltime_exceeded"` | `"limits.max_turns_exceeded"` | undefined
|
|
262
253
|
* (audit A-1: hoisted ABOVE no-final — the loop exits cleanly on abort, so `final` may be a
|
|
263
254
|
* normal message or absent; a plain user interrupt carries NO errorCode — switch on `status`),
|