@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.
Files changed (85) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/agents/cascade.d.ts +2 -2
  3. package/dist/agents/cascade.js +12 -10
  4. package/dist/agents/repair-loop.d.ts +5 -3
  5. package/dist/agents/repair-loop.js +13 -15
  6. package/dist/agents/subagent.d.ts +24 -42
  7. package/dist/agents/subagent.js +119 -105
  8. package/dist/agents/suspend-guard.d.ts +31 -19
  9. package/dist/agents/suspend-guard.js +14 -8
  10. package/dist/agents/teacher.js +9 -9
  11. package/dist/agents/team.d.ts +4 -3
  12. package/dist/agents/team.js +10 -8
  13. package/dist/agents/verify.d.ts +3 -3
  14. package/dist/agents/verify.js +17 -17
  15. package/dist/core/a2a.js +2 -1
  16. package/dist/core/ask-origin.d.ts +60 -7
  17. package/dist/core/ask-origin.js +26 -1
  18. package/dist/core/checkpoint-store.d.ts +78 -76
  19. package/dist/core/checkpoint-store.js +17 -1
  20. package/dist/core/gate-outcome.d.ts +189 -0
  21. package/dist/core/gate-outcome.js +70 -0
  22. package/dist/core/hooks.d.ts +18 -92
  23. package/dist/core/hooks.js +88 -85
  24. package/dist/core/mcp-failure.d.ts +104 -0
  25. package/dist/core/mcp-failure.js +128 -0
  26. package/dist/core/mcp.d.ts +21 -77
  27. package/dist/core/mcp.js +76 -150
  28. package/dist/core/pause-registry.d.ts +131 -0
  29. package/dist/core/pause-registry.js +27 -0
  30. package/dist/core/runner/assemble-result.d.ts +32 -41
  31. package/dist/core/runner/assemble-result.js +55 -74
  32. package/dist/core/runner/contracts.d.ts +46 -64
  33. package/dist/core/runner/denial-limit-arms.d.ts +1 -1
  34. package/dist/core/runner/denial-limit-arms.js +3 -3
  35. package/dist/core/runner/gate-exit.d.ts +74 -0
  36. package/dist/core/runner/gate-exit.js +55 -0
  37. package/dist/core/runner/park-commit.d.ts +17 -23
  38. package/dist/core/runner/park-commit.js +14 -15
  39. package/dist/core/runner/prepare-ask-lane.d.ts +0 -3
  40. package/dist/core/runner/prepare-ask-lane.js +3 -5
  41. package/dist/core/runner/prepare-boundary-parks.d.ts +3 -6
  42. package/dist/core/runner/prepare-boundary-parks.js +3 -3
  43. package/dist/core/runner/prepare-caps-and-workflow.js +1 -1
  44. package/dist/core/runner/prepare-gate-stations.d.ts +4 -7
  45. package/dist/core/runner/prepare-gate-stations.js +29 -54
  46. package/dist/core/runner/prepare-inherited-gate.js +1 -1
  47. package/dist/core/runner/prepare-memory.d.ts +44 -26
  48. package/dist/core/runner/prepare-park-ask.d.ts +2 -4
  49. package/dist/core/runner/prepare-park-ask.js +5 -5
  50. package/dist/core/runner/prepare-task.js +8 -9
  51. package/dist/core/runner/prepare-wiring-manifest.d.ts +7 -15
  52. package/dist/core/runner/prepare-wiring-manifest.js +9 -10
  53. package/dist/core/runner/runtask.d.ts +16 -31
  54. package/dist/core/runner/runtask.js +109 -120
  55. package/dist/core/runner/terminal-projection.d.ts +22 -0
  56. package/dist/core/runner/terminal-projection.js +28 -0
  57. package/dist/core/store-contracts/checkpoint-store-contract.d.ts +4 -1
  58. package/dist/core/store-contracts/checkpoint-store-contract.js +8 -2
  59. package/dist/core/terminal-cause.d.ts +137 -0
  60. package/dist/core/terminal-cause.js +9 -0
  61. package/dist/core/tool-policy.d.ts +43 -139
  62. package/dist/core/tool-policy.js +79 -112
  63. package/dist/core/types.d.ts +67 -164
  64. package/dist/core/wiring-manifest.d.ts +6 -3
  65. package/dist/core/workflow-journal-store.js +3 -4
  66. package/dist/engine/harness/agent-harness.d.ts +1 -1
  67. package/dist/index.d.ts +10 -7
  68. package/dist/index.js +8 -5
  69. package/dist/orchestration/builtin-workflows.d.ts +2 -2
  70. package/dist/orchestration/builtin-workflows.js +1 -1
  71. package/dist/orchestration/goal.js +8 -7
  72. package/dist/orchestration/run-spec.js +5 -3
  73. package/dist/orchestration/run-workflow-tool.d.ts +1 -1
  74. package/dist/orchestration/run-workflow-tool.js +4 -4
  75. package/dist/orchestration/workflow-governance.d.ts +4 -4
  76. package/dist/orchestration/workflow-governance.js +4 -2
  77. package/dist/orchestration/workflow-primitives.d.ts +1 -1
  78. package/dist/orchestration/workflow-primitives.js +1 -1
  79. package/dist/orchestration/workflow.d.ts +11 -0
  80. package/dist/orchestration/workflow.js +64 -39
  81. package/dist/prompts/supervisor.d.ts +1 -1
  82. package/dist/prompts/supervisor.js +3 -3
  83. package/dist/scenarios/scenario-registry.js +1 -1
  84. package/package.json +3 -1
  85. package/test/export-surface.snapshot.json +74 -22
@@ -1,13 +1,13 @@
1
- import type { TaskResult } from "../core/types.js";
1
+ import type { PausedCause, TaskResult, TerminalCause } from "../core/types.js";
2
2
  /**
3
- * design/45 §11 Q6 — the **durable-suspend hard boundary** (v1). A `status:"suspended"` result is only
4
- * safe to handle at the **top-level `runTask`** boundary: the orchestrators (cascade / verify / teacher /
5
- * team) run their work as *nested* `runTask`s, and none of them can drive a resume from the inside. If a
6
- * nested task suspends on a durable approval gate, the orchestrator must **fail fast** — not silently
7
- * treat it as an ordinary failure (which would escalate/re-run side effects) and not release its pinned
8
- * session (which would orphan the checkpoint). It maps `suspended → failed` + `errorCode:
9
- * "unexpected.suspended"` and **passes the `checkpointToken`/`checkpointGate` straight through** (via the
10
- * spread) so the top-level caller can still resume.
3
+ * design/45 §11 Q6 — the **durable-suspend hard boundary** (v1). A `paused` terminal is only safe to
4
+ * handle at the **top-level `runTask`** boundary: the orchestrators (cascade / verify / teacher / team)
5
+ * run their work as *nested* `runTask`s, and none of them can drive a resume from the inside. If a nested
6
+ * task pauses on a durable gate, the orchestrator must **fail fast** — not silently treat it as an
7
+ * ordinary failure (which would escalate/re-run side effects) and not release its pinned session (which
8
+ * would orphan the checkpoint). It maps `paused → failed` with the code `"unexpected.suspended"` (an
9
+ * approval / resource pause) or `"unexpected.needs_review"` (a review pause) and **carries the pause's own
10
+ * cause on the failed terminal's `nestedPause` seat** so the top-level caller can still resume.
11
11
  *
12
12
  * The structural prevention is upstream — an orchestrator should run nested tasks under a **non-durable**
13
13
  * approval policy (e.g. the verifier already forces `handsReadOnly` + headless auto-deny, DESIGN#5) so a
@@ -15,20 +15,32 @@ import type { TaskResult } from "../core/types.js";
15
15
  * a hard boundary, or a deployment wires durable-suspend into verify and produces silent errors."
16
16
  */
17
17
  export declare const UNEXPECTED_SUSPENDED = "unexpected.suspended";
18
- /** design/80 D-B: the `needs_review` durable-pause family (dry-run `needs_review` AND `plan_review`) hits the
19
- * SAME nested hard boundary as `suspended` — a distinct code so a caller can tell a review pause from an approval. */
18
+ /** design/80 D-B: the review durable-pause family (dry-run `needs_review` AND `plan_review`) hits the SAME
19
+ * nested hard boundary as an approval pause — a distinct code so a caller can tell a review pause from an
20
+ * approval. */
20
21
  export declare const UNEXPECTED_NEEDS_REVIEW = "unexpected.needs_review";
21
22
  /**
22
- * design/80 D-B: the durable-pause hard boundary covers BOTH `suspended` (an approval gate) AND `needs_review`
23
- * (a dry-run review OR a `plan_review` plan-gate). Both persist a checkpoint + PIN their session
24
- * (prepare-task.ts), and a nested orchestrator can't drive their resume — so it must surface the
25
- * `checkpointToken` and NOT release the pinned session (releasing it orphans the checkpoint). The `types.ts`
26
- * TaskStatus doc mandates orchestrators map an unexpected `needs_review` the same hard-boundary way as `suspended`.
23
+ * design/80 D-B: the durable-pause hard boundary covers EVERY `paused` cause — an approval gate, a resource
24
+ * slice, a dry-run review, a `plan_review` plan-gate. All of them persist a checkpoint + PIN their session,
25
+ * and a nested orchestrator can't drive their resume — so it must surface the pause and NOT release the
26
+ * pinned session (releasing it orphans the checkpoint). A type guard, so the caller reads the token/gate
27
+ * off the narrowed cause.
27
28
  */
28
- export declare function isDurablePause(status: TaskResult["status"]): boolean;
29
+ export declare function isDurablePause(terminal: TerminalCause): terminal is PausedCause;
30
+ /** A result whose terminal is known to be a pause — what {@link isDurablePause} narrows a result's
31
+ * `terminal` to, spelled as a type so an orchestrator can HOLD a paused result without re-checking. */
32
+ export type PausedResult = TaskResult & {
33
+ terminal: PausedCause;
34
+ };
29
35
  /**
30
- * If `result` durably paused (suspended OR needs_review), return it mapped to a `failed` result carrying the
31
- * checkpoint token; otherwise return it unchanged. The orchestrator should also **stop** (not escalate/re-run)
36
+ * Which family a pause belongs to its gate kind's registry row: `"suspended"` (an approval gate, a
37
+ * resource slice, a platform park) or `"needs_review"` (a dry-run or plan review). The same row the
38
+ * status word is projected from; read here by the orchestrators that name the family in their own faces.
39
+ */
40
+ export declare function pauseFamily(t: PausedCause): "suspended" | "needs_review";
41
+ /**
42
+ * If `result` durably paused, return it mapped to a `failed` result whose terminal carries the pause on
43
+ * `nestedPause`; otherwise return it unchanged. The orchestrator should also **stop** (not escalate/re-run)
32
44
  * and **not release** the session when this maps a pause — the checkpoint still references that session.
33
45
  */
34
46
  export declare function mapNestedSuspend(result: TaskResult): TaskResult;
@@ -1,18 +1,24 @@
1
+ import { PAUSE_REGISTRY } from "../core/pause-registry.js";
1
2
  export const UNEXPECTED_SUSPENDED = "unexpected.suspended";
2
3
  export const UNEXPECTED_NEEDS_REVIEW = "unexpected.needs_review";
3
- export function isDurablePause(status) {
4
- return status === "suspended" || status === "needs_review";
4
+ export function isDurablePause(terminal) {
5
+ return terminal.kind === "paused";
6
+ }
7
+ export function pauseFamily(t) {
8
+ return PAUSE_REGISTRY[t.gate.kind].taskStatus;
5
9
  }
6
10
  export function mapNestedSuspend(result) {
7
- if (!isDurablePause(result.status)) {
11
+ const t = result.terminal;
12
+ if (!isDurablePause(t)) {
8
13
  return result;
9
14
  }
15
+ const word = pauseFamily(t);
16
+ const code = word === "needs_review" ? UNEXPECTED_NEEDS_REVIEW : UNEXPECTED_SUSPENDED;
17
+ const message = `a nested task durably paused ("${word}") on a HITL gate; durable suspend/resume and plan/` +
18
+ "dry-run review are only supported at the top-level runTask (design/45 v1 / design/80). Resume it via " +
19
+ "the pause's token, or run the orchestrated task under a non-durable policy so it auto-adjudicates instead of pausing.";
10
20
  return {
11
21
  ...result,
12
- status: "failed",
13
- errorCode: result.status === "needs_review" ? UNEXPECTED_NEEDS_REVIEW : UNEXPECTED_SUSPENDED,
14
- errorMessage: `a nested task durably paused ("${result.status}") on a HITL gate; durable suspend/resume and plan/` +
15
- "dry-run review are only supported at the top-level runTask (design/45 v1 / design/80). Resume it via " +
16
- "checkpointToken, or run the orchestrated task under a non-durable policy so it auto-adjudicates instead of pausing.",
22
+ terminal: { kind: "failed", code, message, nestedPause: t },
17
23
  };
18
24
  }
@@ -119,7 +119,7 @@ export async function runWithTeacher(runner, studentSpec, teacher = {}) {
119
119
  }
120
120
  }
121
121
  const result = await runTeacherCore(runner, spec, teacher);
122
- if (store && scope && result.status === "completed" && result.escalations.length > 0) {
122
+ if (store && scope && result.terminal.kind === "completed" && result.escalations.length > 0) {
123
123
  const advice = result.escalations[result.escalations.length - 1].teacher;
124
124
  if (advice.strategy && advice.confidence >= (teacher.minConfidenceToStore ?? 2)) {
125
125
  try {
@@ -271,7 +271,7 @@ async function runTeacherCore(runner, studentSpec, teacher) {
271
271
  ...(studentSpec.restoreGatedTools !== undefined
272
272
  ? { restoreGatedTools: Array.isArray(studentSpec.restoreGatedTools) ? [...studentSpec.restoreGatedTools] : studentSpec.restoreGatedTools }
273
273
  : {}),
274
- ...(studentSpec.checkpointStore === null ? { checkpointStore: null } : {}),
274
+ ...(studentSpec.checkpointStore === "disabled" ? { checkpointStore: "disabled" } : {}),
275
275
  };
276
276
  const helperBase = () => teacher.helperModel
277
277
  ? { ...inheritedDeploymentConfig, model: teacher.helperModel }
@@ -407,9 +407,8 @@ async function runTeacherCore(runner, studentSpec, teacher) {
407
407
  const syntheticAborted = (sid) => ({
408
408
  taskId: sid ?? "aborted",
409
409
  sessionId: sid ?? "aborted",
410
- status: "failed",
410
+ terminal: { kind: "failed", message: "student run did not settle promptly after stuck-abort" },
411
411
  result: "",
412
- errorMessage: "student run did not settle promptly after stuck-abort",
413
412
  stats: { turns: 0, tokens: 0 },
414
413
  });
415
414
  const abortFallback = (sid) => {
@@ -451,7 +450,7 @@ async function runTeacherCore(runner, studentSpec, teacher) {
451
450
  toolLog.length = 0;
452
451
  const { result, stuck, recent } = await runStudent();
453
452
  sessionId = result.sessionId;
454
- if (isDurablePause(result.status)) {
453
+ if (isDurablePause(result.terminal)) {
455
454
  return finalize(mapNestedSuspend(result));
456
455
  }
457
456
  let trigger;
@@ -459,10 +458,10 @@ async function runTeacherCore(runner, studentSpec, teacher) {
459
458
  if (stuck) {
460
459
  trigger = "stuck";
461
460
  }
462
- else if (result.status === "blocked") {
461
+ else if (result.terminal.kind === "blocked") {
463
462
  trigger = "blocked";
464
463
  }
465
- else if (result.status === "failed") {
464
+ else if (result.terminal.kind === "failed") {
466
465
  trigger = "failed";
467
466
  }
468
467
  else if (!verifyOutput) {
@@ -482,7 +481,8 @@ async function runTeacherCore(runner, studentSpec, teacher) {
482
481
  return finalize(result);
483
482
  }
484
483
  if (trigger === "failed" &&
485
- /truncat|max[ _-]?tokens?|maximum tokens?|token (limit|budget)|context (length|window)|no answer text|(consumed|exceeded).{0,12}budget/i.test(result.errorMessage ?? "")) {
484
+ result.terminal.kind === "failed" &&
485
+ /truncat|max[ _-]?tokens?|maximum tokens?|token (limit|budget)|context (length|window)|no answer text|(consumed|exceeded).{0,12}budget/i.test(result.terminal.message ?? "")) {
486
486
  return finalize(result);
487
487
  }
488
488
  if (escalations.length >= maxEscalations) {
@@ -528,7 +528,7 @@ async function runTeacherCore(runner, studentSpec, teacher) {
528
528
  signal: extSignal,
529
529
  });
530
530
  addTeacher(r);
531
- if (isDurablePause(r.status)) {
531
+ if (isDurablePause(r.terminal)) {
532
532
  return finalize(mapNestedSuspend(r));
533
533
  }
534
534
  return finalize(r);
@@ -1,5 +1,6 @@
1
1
  import type { Runner } from "../core/runner/runtask.js";
2
- import type { McpServerSpec, ModelRef, ModelRole, TaskResult, TaskSpec, ToolSpec } from "../core/types.js";
2
+ import type { McpServerSpec, ModelRef, ModelRole, TaskSpec, ToolSpec } from "../core/types.js";
3
+ import type { CheckpointGate, CheckpointToken } from "../core/checkpoint-store.js";
3
4
  export interface TeamMember {
4
5
  /** The member's role / specialty, e.g. "安全评审" or "performance". */
5
6
  role: string;
@@ -206,8 +207,8 @@ export interface TeamResult {
206
207
  * the discussion STOPS (no synthesis on a half-done team) and surfaces the resume capability so the caller
207
208
  * can resume the paused member via the token, then re-run. `conclusionValid` is false in this case. */
208
209
  durablePause?: boolean;
209
- checkpointToken?: TaskResult["checkpointToken"];
210
- checkpointGate?: TaskResult["checkpointGate"];
210
+ checkpointToken?: CheckpointToken;
211
+ checkpointGate?: CheckpointGate;
211
212
  }
212
213
  /**
213
214
  * Run a multi-round discussion across several role-specialized agents, then synthesize a conclusion.
@@ -1,5 +1,6 @@
1
1
  import { resolveToolSubset } from "./tool-filter.js";
2
2
  import { mapNestedSuspend, isDurablePause } from "./suspend-guard.js";
3
+ import { terminalProjection } from "../core/runner/terminal-projection.js";
3
4
  import { sanitizeUntrustedText } from "../core/untrusted-text.js";
4
5
  const DISCUSSION_WRAPPER_TAGS = ["statement", "discussion", "discussion-final", "earlier-discussion-summary", "previous-summary", "topic"];
5
6
  const RETIRED_WRAPPER_TAGS = ["team-discussion", "team-discussion-final"];
@@ -87,7 +88,7 @@ export async function runTeamDiscussion(opts) {
87
88
  return {
88
89
  status: "failed",
89
90
  result: conclusion,
90
- errorCode: p.errorCode,
91
+ errorCode: terminalProjection(mapNestedSuspend(p).terminal).errorCode,
91
92
  conclusion,
92
93
  conclusionValid: false,
93
94
  transcript,
@@ -96,8 +97,8 @@ export async function runTeamDiscussion(opts) {
96
97
  failures,
97
98
  transcriptCompactions,
98
99
  durablePause: true,
99
- checkpointToken: p.checkpointToken,
100
- checkpointGate: p.checkpointGate,
100
+ checkpointToken: p.terminal.token,
101
+ checkpointGate: p.terminal.gate,
101
102
  };
102
103
  };
103
104
  const runTurn = async (objective, systemPrompt, modelRef, role, tools, mcp) => {
@@ -122,16 +123,17 @@ export async function runTeamDiscussion(opts) {
122
123
  tokens += res.stats.tokens + (res.stats.nested?.tokens ?? 0);
123
124
  turns += res.stats.turns + (res.stats.nested?.turns ?? 0);
124
125
  costMicroUsd += (res.stats.costMicroUsd ?? 0) + (res.stats.nested?.costMicroUsd ?? 0);
125
- if (isDurablePause(res.status)) {
126
- const m = mapNestedSuspend(res);
127
- pendingPause = m;
126
+ if (isDurablePause(res.terminal)) {
127
+ pendingPause = { ...res, terminal: res.terminal };
128
+ const m = terminalProjection(mapNestedSuspend(res).terminal);
128
129
  return { text: `[unavailable: ${m.status}/${m.errorCode}]`, ok: false };
129
130
  }
130
131
  await opts.runner.sessions.release(res.sessionId).catch(() => undefined);
131
- if (res.status === "completed" && res.result.trim()) {
132
+ if (res.terminal.kind === "completed" && res.result.trim()) {
132
133
  return { text: res.result, ok: true };
133
134
  }
134
- last = `[unavailable: ${res.status}${res.errorCode ? `/${res.errorCode}` : ""}${res.errorMessage ? ` — ${res.errorMessage}` : ""}]`;
135
+ const face = terminalProjection(res.terminal);
136
+ last = `[unavailable: ${face.status}${face.errorCode ? `/${face.errorCode}` : ""}${face.errorMessage ? ` — ${face.errorMessage}` : ""}]`;
135
137
  }
136
138
  return { text: last || "[unavailable]", ok: false };
137
139
  };
@@ -2,7 +2,7 @@ import { Type, type Static } from "typebox";
2
2
  import type { Runner, ResumeTaskConfig } from "../core/runner/runtask.js";
3
3
  import type { RunInternals } from "../core/runner/prepare-task.js";
4
4
  import type { CheckpointGate, CheckpointToken, ResumeOutcome } from "../core/checkpoint-store.js";
5
- import type { ModelRef, TaskResult, TaskSpec, ToolSpec } from "../core/types.js";
5
+ import type { ModelRef, TaskResult, TaskSpec, TaskStatus, ToolSpec } from "../core/types.js";
6
6
  /**
7
7
  * Verification gate (developer mode, design/28 §4). An **independent falsification-style verifier** runs after
8
8
  * an implementation task and tries to BREAK it — read-only, evidence-required, returning a structured
@@ -131,7 +131,7 @@ export interface VerifyConfig {
131
131
  */
132
132
  onVerifierPause?: (info: {
133
133
  round: number;
134
- status: TaskResult["status"];
134
+ status: TaskStatus;
135
135
  sessionId: string;
136
136
  checkpointToken?: CheckpointToken;
137
137
  checkpointGate?: CheckpointGate;
@@ -200,7 +200,7 @@ export interface VerificationOutcome {
200
200
  */
201
201
  verifierPause?: {
202
202
  /** The verifier run's terminal pause status (`"suspended"` or `"needs_review"`). */
203
- status: TaskResult["status"];
203
+ status: TaskStatus;
204
204
  /** The paused verifier's own session id (independent of the implementation's). */
205
205
  sessionId: string;
206
206
  /** The resume capability for the verifier's checkpoint. Undefined only if the pause carried none. */
@@ -1,7 +1,7 @@
1
1
  import { Type } from "typebox";
2
2
  import { CODE_AGENT_PROMPT } from "../prompts/default.js";
3
3
  import { releaseSession } from "./session-util.js";
4
- import { mapNestedSuspend, isDurablePause } from "./suspend-guard.js";
4
+ import { mapNestedSuspend, isDurablePause, pauseFamily } from "./suspend-guard.js";
5
5
  import { delimitUntrusted, sanitizeUntrustedText } from "../core/untrusted-text.js";
6
6
  import { createSafeNotifier } from "../core/safe-notify.js";
7
7
  import { createCumulativeStatsTracker } from "./cumulative-stats.js";
@@ -102,7 +102,7 @@ function verifierObjective(originalObjective, impl, evidence) {
102
102
  }
103
103
  export async function verifyCompleted(runner, result, specBase, objective, config, internals) {
104
104
  refuseUnhonorableInternals(internals, "door");
105
- if (result.status !== "completed") {
105
+ if (result.terminal.kind !== "completed") {
106
106
  return { ...result, verification: { verdict: "unverified", unverifiedReason: "impl_incomplete", rounds: 0, findings: [] } };
107
107
  }
108
108
  const maxRounds = Number.isFinite(config.maxRounds) ? Math.max(1, Math.floor(config.maxRounds)) : 2;
@@ -168,10 +168,10 @@ export async function verifyCompleted(runner, result, specBase, objective, confi
168
168
  }, legInternals());
169
169
  try {
170
170
  const cost = (v.stats.costMicroUsd ?? 0) + (v.stats.nested?.costMicroUsd ?? 0);
171
- return { verdict: v.structuredOutput, cost, ...(isDurablePause(v.status) ? { paused: v } : {}) };
171
+ return { verdict: v.structuredOutput, cost, ...(isDurablePause(v.terminal) ? { paused: { ...v, terminal: v.terminal } } : {}) };
172
172
  }
173
173
  finally {
174
- if (!isDurablePause(v.status))
174
+ if (!isDurablePause(v.terminal))
175
175
  await releaseSession(runner, v.sessionId);
176
176
  }
177
177
  };
@@ -202,18 +202,18 @@ export async function verifyCompleted(runner, result, specBase, objective, confi
202
202
  findings: [],
203
203
  verifierCost: verifierSpend,
204
204
  verifierPause: {
205
- status: v.paused.status,
206
- sessionId: v.paused.sessionId,
207
- ...(v.paused.checkpointToken ? { checkpointToken: v.paused.checkpointToken } : {}),
208
- ...(v.paused.checkpointGate ? { checkpointGate: v.paused.checkpointGate } : {}),
205
+ status: pauseFamily(paused.terminal),
206
+ sessionId: paused.sessionId,
207
+ checkpointToken: paused.terminal.token,
208
+ checkpointGate: paused.terminal.gate,
209
209
  },
210
210
  };
211
211
  notifier.notify(() => config.onVerifierPause?.({
212
212
  round,
213
- status: paused.status,
213
+ status: pauseFamily(paused.terminal),
214
214
  sessionId: paused.sessionId,
215
- ...(paused.checkpointToken ? { checkpointToken: paused.checkpointToken } : {}),
216
- ...(paused.checkpointGate ? { checkpointGate: paused.checkpointGate } : {}),
215
+ checkpointToken: paused.terminal.token,
216
+ checkpointGate: paused.terminal.gate,
217
217
  }), VERIFY_ON_VERIFIER_PAUSE_SITE);
218
218
  notifier.notify(() => config.onRound?.({ round, verdict: outcome.verdict, findings: outcome.findings }), VERIFY_ON_ROUND_SITE);
219
219
  break;
@@ -248,10 +248,10 @@ export async function verifyCompleted(runner, result, specBase, objective, confi
248
248
  }, legInternals());
249
249
  spend += (current.stats.costMicroUsd ?? 0) + (current.stats.nested?.costMicroUsd ?? 0);
250
250
  implAccount.add(current.stats);
251
- if (isDurablePause(current.status)) {
251
+ if (isDurablePause(current.terminal)) {
252
252
  return { ...mapNestedSuspend(carryFrozenPosture(withImplAccount(current))), verification: outcome };
253
253
  }
254
- if (current.status !== "completed")
254
+ if (current.terminal.kind !== "completed")
255
255
  break;
256
256
  }
257
257
  return { ...carryFrozenPosture(withImplAccount(current)), verification: outcome };
@@ -259,8 +259,8 @@ export async function verifyCompleted(runner, result, specBase, objective, confi
259
259
  export async function runWithVerification(runner, implSpec, config = {}, internals) {
260
260
  refuseUnhonorableInternals(internals, "door");
261
261
  const result = await runner.runTask(implSpec, internals);
262
- if (isDurablePause(result.status)) {
263
- return { ...mapNestedSuspend(result), verification: { verdict: "unverified", unverifiedReason: result.status === "needs_review" ? "needs_review" : "suspended", rounds: 0, findings: [] } };
262
+ if (isDurablePause(result.terminal)) {
263
+ return { ...mapNestedSuspend(result), verification: { verdict: "unverified", unverifiedReason: pauseFamily(result.terminal), rounds: 0, findings: [] } };
264
264
  }
265
265
  refuseUnhonorableInternals(internals, "before_leg");
266
266
  return verifyCompleted(runner, result, implSpec, implSpec.objective, config, internals);
@@ -268,8 +268,8 @@ export async function runWithVerification(runner, implSpec, config = {}, interna
268
268
  export async function resumeWithVerification(runner, token, outcome, taskConfig, objective, config = {}, internals) {
269
269
  refuseUnhonorableInternals(internals, "door");
270
270
  const result = await runner.resume(token, outcome, taskConfig, internals);
271
- if (isDurablePause(result.status)) {
272
- return { ...mapNestedSuspend(result), verification: { verdict: "unverified", unverifiedReason: result.status === "needs_review" ? "needs_review" : "suspended", rounds: 0, findings: [] } };
271
+ if (isDurablePause(result.terminal)) {
272
+ return { ...mapNestedSuspend(result), verification: { verdict: "unverified", unverifiedReason: pauseFamily(result.terminal), rounds: 0, findings: [] } };
273
273
  }
274
274
  refuseUnhonorableInternals(internals, "before_leg");
275
275
  return verifyCompleted(runner, result, taskConfig, objective, config, internals);
package/dist/core/a2a.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { Type } from "typebox";
3
3
  import { A2A_TASK_STATES } from "./a2a-task-state.js";
4
- import { describeHttpTransportFailure, resolveProtocolHttpHeaders } from "./mcp.js";
4
+ import { resolveProtocolHttpHeaders } from "./mcp.js";
5
+ import { describeHttpTransportFailure } from "./mcp-failure.js";
5
6
  import { findNamespacePrefixCollision, mintNamespacePrefix, mintNamespacedToolName } from "./protocol-naming.js";
6
7
  import { A2A_NAMESPACE } from "./protocol-table.js";
7
8
  import { delimitUntrusted, inlineUntrusted } from "./untrusted-text.js";
@@ -47,11 +47,21 @@ import type { PermissionResult } from "./tool-policy.js";
47
47
  * an instruction rather than hesitation.
48
48
  * - `denial_limit_fallback` — the auto-mode classifier reached its denial bound and handed the call
49
49
  * to a person; the classifier stands BEFORE this ask, never after it.
50
- * - `policy` — everything else: a deployment `ToolPolicy` ask, a mode-default ask, or one of the
51
- * engine's own post-fold tightens (egress / irreversibility / peer referral / write protection).
52
- * This is the hesitation family the one an armed classifier exists to resolve.
50
+ * - `shell_gate_tighten` — the gate's own irreversibility tighten raised the ask over a tier the COARSE
51
+ * `shellGate` doctrine installed (a shell command under `shellGate: "classify"`/`"always"`), not an
52
+ * explicit per-tool mark. The gate records that its own tighten spoke; a policy stamping
53
+ * `decisionReason: "safety"` on its own ask does not become one.
54
+ * - `safety_tighten` — the gate's own post-fold tightens over an EXPLICIT fact of the call: the tool's
55
+ * egress mark, its irreversibility mark, a peer-message referral in auto mode, a write onto the
56
+ * write-protected table. Same recording rule as above.
57
+ * - `policy` — everything else: a deployment `ToolPolicy` ask or a mode-default ask. This is the
58
+ * hesitation family — the one an armed classifier exists to resolve. The two tighten words are
59
+ * classifier-eligible too (arming auto mode is the deployment's explicit choice to let the
60
+ * classifier be the explicit ask-resolution for those classes); they are separate words so a deny
61
+ * observer or an approval card can say WHICH engine layer raised the question, which one word
62
+ * could not.
53
63
  */
54
- export declare const ASK_ORIGINS: readonly ["content_question", "unresolvable", "org_unavailable", "org_rule", "hook", "ask_rule", "denial_limit_fallback", "policy"];
64
+ export declare const ASK_ORIGINS: readonly ["content_question", "unresolvable", "org_unavailable", "org_rule", "hook", "ask_rule", "denial_limit_fallback", "shell_gate_tighten", "safety_tighten", "policy"];
55
65
  export type AskOrigin = (typeof ASK_ORIGINS)[number];
56
66
  /** Whether a value is a member of the closed origin set. Exported because the word crosses process
57
67
  * boundaries: a host validating one it received must not hand-roll the set. */
@@ -77,6 +87,10 @@ export interface AskOriginFacts {
77
87
  * read-back off the decision (a policy can put `decisionReason: "org_rule"` on its own ask; the
78
88
  * gate's cell cannot be written by a policy). */
79
89
  readonly org: "rule" | "unavailable" | undefined;
90
+ /** Which of the gate's OWN post-fold tightens minted the standing ask, if one did — again the gate's
91
+ * record of its own layer (a tighten replaces an `allow` wholesale, so at most one fires per pass),
92
+ * never a read-back of `decisionReason: "safety"` off a decision a policy could have composed. */
93
+ readonly tightened: "shell_gate" | "safety" | undefined;
80
94
  }
81
95
  /**
82
96
  * The ONE derivation of an ask's origin, from the surviving decision plus the gate's own facts. The
@@ -89,6 +103,39 @@ export interface AskOriginFacts {
89
103
  export declare function askOriginOf(decision: Extract<PermissionResult, {
90
104
  action: "ask";
91
105
  }>, facts: AskOriginFacts): AskOrigin;
106
+ /**
107
+ * Which origins IMPLY the `requiresRealApproval` bit, and WHERE that implication is verified — the
108
+ * consistency table {@link askOriginOf} reads. An ask of a must-carry origin exists so that no automatic
109
+ * lane clears it (the org's asks: "no configuration clears this, a person nods"; the classifier's
110
+ * fallback: no automatic lane may re-clear what the classifier already refused), so the bit must ride
111
+ * it. The bit is NOT derived from this table: the write sites each have their own reason, and a
112
+ * table-derived bit would have to either strip a safety policy's mandate or make every hook ask
113
+ * non-budgetable. The table only refuses the one shape that is a defect: a must-carry origin without
114
+ * the bit — and says where that refusal can honestly happen:
115
+ * - `"at_derivation"` — the origin is derived from a GATE fact only the engine writes (`facts.org`),
116
+ * so a missing bit is an engine defect and {@link askOriginOf} fails loud;
117
+ * - `"at_mint"` — the origin is derived from a decision MEMBER a policy may compose
118
+ * (`denialLimitFallback`: a self-declared one opts the policy's own ask out of automatic
119
+ * resolution, the safe direction, not a defect), so the invariant is pinned at the engine's two
120
+ * mint sites (the gate's fallback re-mint and the inherited station's) rather than thrown at
121
+ * derivation, where a policy's own object could trip it;
122
+ * - `false` — the origin says nothing about the bit: a `policy` ask may carry it (a safety policy's
123
+ * own mandate) or not, and both are legal.
124
+ */
125
+ export declare const ORIGIN_IMPLIES_REAL_APPROVAL: {
126
+ readonly content_question: false;
127
+ readonly unresolvable: false;
128
+ readonly org_unavailable: "at_derivation";
129
+ readonly org_rule: "at_derivation";
130
+ readonly hook: false;
131
+ readonly ask_rule: false;
132
+ readonly denial_limit_fallback: "at_mint";
133
+ readonly shell_gate_tighten: false;
134
+ readonly safety_tighten: false;
135
+ readonly policy: false;
136
+ };
137
+ /** The fence over the consistency table: `never` while every origin has a row. */
138
+ export type RealApprovalTableCoversEveryOrigin = AssertAllKeysHandled<Exclude<AskOrigin, keyof typeof ORIGIN_IMPLIES_REAL_APPROVAL>>;
92
139
  /**
93
140
  * The auto-mode CLASSIFIER's table: may a live classifier verdict resolve an ask of this origin?
94
141
  *
@@ -107,9 +154,13 @@ export declare function askOriginOf(decision: Extract<PermissionResult, {
107
154
  * whether the person is asked at all, which is the rule's whole content.
108
155
  * - `denial_limit_fallback`: the classifier already spoke (its bound is what minted this ask); an
109
156
  * allow here would un-say the fallback, a block would count it twice.
110
- * - `policy`: YES — including the engine's own safety tightens and a safety policy's
111
- * `requiresRealApproval` ask (a live verdict is judgment, and arming auto mode is the deployment's
112
- * explicit choice to let the classifier be the explicit ask-resolution for those classes).
157
+ * - `shell_gate_tighten` / `safety_tighten`: YES — the engine's own tightens are hesitation the
158
+ * deployment armed the classifier to resolve (the classify doctrine's whole point for the shell
159
+ * tier; the safety marks make the ask NON-BUDGETABLE at the durable park, a different axis from who
160
+ * may answer it live).
161
+ * - `policy`: YES — including a safety policy's `requiresRealApproval` ask (a live verdict is
162
+ * judgment, and arming auto mode is the deployment's explicit choice to let the classifier be the
163
+ * explicit ask-resolution for those classes).
113
164
  * Exclusion is skip-not-deny: the ask flows the original chain (park / approver / headless deny) —
114
165
  * narrower autonomy, never a lost ask.
115
166
  */
@@ -121,6 +172,8 @@ declare const CLASSIFIER_MAY_ANSWER: {
121
172
  readonly hook: false;
122
173
  readonly ask_rule: false;
123
174
  readonly denial_limit_fallback: false;
175
+ readonly shell_gate_tighten: true;
176
+ readonly safety_tighten: true;
124
177
  readonly policy: true;
125
178
  };
126
179
  /** The fence over the classifier table: `never` while every origin has a row (pinned by a type test). */
@@ -1,9 +1,16 @@
1
- export const ASK_ORIGINS = ["content_question", "unresolvable", "org_unavailable", "org_rule", "hook", "ask_rule", "denial_limit_fallback", "policy"];
1
+ export const ASK_ORIGINS = ["content_question", "unresolvable", "org_unavailable", "org_rule", "hook", "ask_rule", "denial_limit_fallback", "shell_gate_tighten", "safety_tighten", "policy"];
2
2
  const ASK_ORIGIN_SET = new Set(ASK_ORIGINS);
3
3
  export function isAskOrigin(v) {
4
4
  return ASK_ORIGIN_SET.has(v);
5
5
  }
6
6
  export function askOriginOf(decision, facts) {
7
+ const origin = originLadder(decision, facts);
8
+ if (ORIGIN_IMPLIES_REAL_APPROVAL[origin] === "at_derivation" && decision.requiresRealApproval !== true) {
9
+ throw new Error(`ask origin "${origin}" requires the requiresRealApproval bit on the decision it was derived from, and the decision carries none — an engine mint site dropped the bit`);
10
+ }
11
+ return origin;
12
+ }
13
+ function originLadder(decision, facts) {
7
14
  if (facts.contentQuestion)
8
15
  return "content_question";
9
16
  if (facts.markedUnresolvable)
@@ -18,8 +25,24 @@ export function askOriginOf(decision, facts) {
18
25
  return "ask_rule";
19
26
  if (decision.denialLimitFallback !== undefined)
20
27
  return "denial_limit_fallback";
28
+ if (facts.tightened === "shell_gate")
29
+ return "shell_gate_tighten";
30
+ if (facts.tightened === "safety")
31
+ return "safety_tighten";
21
32
  return "policy";
22
33
  }
34
+ export const ORIGIN_IMPLIES_REAL_APPROVAL = {
35
+ content_question: false,
36
+ unresolvable: false,
37
+ org_unavailable: "at_derivation",
38
+ org_rule: "at_derivation",
39
+ hook: false,
40
+ ask_rule: false,
41
+ denial_limit_fallback: "at_mint",
42
+ shell_gate_tighten: false,
43
+ safety_tighten: false,
44
+ policy: false,
45
+ };
23
46
  const CLASSIFIER_MAY_ANSWER = {
24
47
  content_question: false,
25
48
  unresolvable: false,
@@ -28,6 +51,8 @@ const CLASSIFIER_MAY_ANSWER = {
28
51
  hook: false,
29
52
  ask_rule: false,
30
53
  denial_limit_fallback: false,
54
+ shell_gate_tighten: true,
55
+ safety_tighten: true,
31
56
  policy: true,
32
57
  };
33
58
  export function classifierMayAnswer(origin) {