@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.
- package/CHANGELOG.md +67 -0
- package/dist/agents/agent-transcript-tool.d.ts +2 -2
- package/dist/agents/cascade.d.ts +4 -5
- package/dist/agents/cascade.js +12 -10
- package/dist/agents/repair-loop.d.ts +7 -5
- package/dist/agents/repair-loop.js +13 -15
- package/dist/agents/retain-ledger.d.ts +2 -3
- package/dist/agents/send-message-tool.d.ts +2 -2
- package/dist/agents/session-util.d.ts +2 -2
- package/dist/agents/subagent.d.ts +27 -46
- 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.d.ts +2 -2
- package/dist/agents/teacher.js +9 -9
- package/dist/agents/team.d.ts +6 -5
- package/dist/agents/team.js +10 -8
- package/dist/agents/verify.d.ts +8 -9
- package/dist/agents/verify.js +17 -17
- package/dist/core/a2a.js +2 -1
- package/dist/core/agent-definition.d.ts +172 -0
- package/dist/core/agent-definition.js +1 -0
- 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/delegation-frames.d.ts +298 -0
- package/dist/core/delegation-frames.js +21 -0
- package/dist/core/engine-notice.d.ts +555 -0
- package/dist/core/engine-notice.js +55 -0
- package/dist/core/gate-fold.d.ts +12 -0
- package/dist/core/gate-fold.js +158 -0
- package/dist/core/gate-lanes.d.ts +93 -0
- package/dist/core/gate-lanes.js +626 -0
- package/dist/core/gate-outcome.d.ts +189 -0
- package/dist/core/gate-outcome.js +70 -0
- package/dist/core/hands-band.d.ts +134 -0
- package/dist/core/hands-band.js +1 -0
- package/dist/core/hooks.d.ts +22 -177
- package/dist/core/hooks.js +53 -851
- package/dist/core/mcp-failure.d.ts +142 -0
- package/dist/core/mcp-failure.js +145 -0
- package/dist/core/mcp-server-spec.d.ts +217 -0
- package/dist/core/mcp-server-spec.js +1 -0
- package/dist/core/mcp.d.ts +21 -77
- package/dist/core/mcp.js +76 -150
- package/dist/core/model-seat.d.ts +99 -0
- package/dist/core/model-seat.js +1 -0
- package/dist/core/pause-registry.d.ts +131 -0
- package/dist/core/pause-registry.js +27 -0
- package/dist/core/reminder-mint.d.ts +10 -0
- package/dist/core/reminder-mint.js +3 -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 +427 -69
- 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 +242 -0
- package/dist/core/runner/gate-exit.js +124 -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.d.ts +2 -7
- package/dist/core/runner/prepare-caps-and-workflow.js +1 -1
- package/dist/core/runner/prepare-delegation-surface.d.ts +2 -7
- 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.d.ts +2 -2
- 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 +20 -102
- package/dist/core/runner/runtask.js +119 -121
- package/dist/core/runner/terminal-projection.d.ts +22 -0
- package/dist/core/runner/terminal-projection.js +28 -0
- package/dist/core/runner-deps.d.ts +1416 -0
- package/dist/core/runner-deps.js +1 -0
- package/dist/core/runtime-caps.d.ts +164 -0
- package/dist/core/runtime-caps.js +1 -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/task-event.d.ts +910 -0
- package/dist/core/task-event.js +1 -0
- package/dist/core/task-limits.d.ts +110 -0
- package/dist/core/task-limits.js +1 -0
- package/dist/core/task-result.d.ts +809 -0
- package/dist/core/task-result.js +1 -0
- package/dist/core/task-spec.d.ts +1370 -0
- package/dist/core/task-spec.js +1 -0
- package/dist/core/task-stream.d.ts +382 -0
- package/dist/core/task-stream.js +1 -0
- 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/tool-spec.d.ts +1174 -0
- package/dist/core/tool-spec.js +1 -0
- package/dist/core/types.d.ts +27 -7789
- package/dist/core/types.js +2 -76
- package/dist/core/warm-resume.d.ts +2 -2
- 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 +12 -8
- package/dist/index.js +9 -6
- package/dist/orchestration/builtin-workflows.d.ts +2 -2
- package/dist/orchestration/builtin-workflows.js +1 -1
- package/dist/orchestration/goal.d.ts +2 -2
- package/dist/orchestration/goal.js +8 -7
- package/dist/orchestration/run-spec.d.ts +2 -2
- package/dist/orchestration/run-spec.js +5 -3
- package/dist/orchestration/run-workflow-tool.d.ts +4 -4
- 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 +15 -4
- 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.d.ts +3 -3
- package/dist/scenarios/scenario-registry.js +1 -1
- package/dist/scenarios/teacher-quickstart.d.ts +2 -2
- package/dist/server/http.d.ts +2 -2
- package/dist/stores/file/fs-atomic.d.ts +88 -12
- package/dist/stores/file/fs-atomic.js +184 -55
- package/dist/stores/file/index.d.ts +1 -0
- package/dist/stores/file/index.js +1 -0
- package/package.json +3 -1
- 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
|
|
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,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
|