@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
@@ -87,14 +87,14 @@ export declare function workflowNameProbeOrder(requested: string): string[];
87
87
  * `agent()` members with transcript re-feed + a schema'd finalizer). Deterministic by construction — no
88
88
  * clock/randomness reads (locked by test against `workflowScriptReadsClockOrRandom`).
89
89
  */
90
- export declare const DISCUSSION_SCRIPT = "export const meta = {\n name: \"discussion\",\n description: \"Round-based discussion: configurable members debate a topic across rounds (each member sees the transcript so far), then a finalizer synthesizes a structured verdict.\",\n whenToUse: \"Use for a genuinely contested question that benefits from several perspectives arguing across rounds - design trade-offs, plan or risk reviews, adversarial critique of a proposal. Do NOT use it for a single factual question, a task with one obvious answer, or a budget-sensitive run: every round costs one agent call per member, so a discussion is never cheaper than asking once. args (all optional): { topic, members?: [{ role, prompt?, model? }], rounds?, finalizer?: { prompt?, model? } }. Hard ceilings: members is capped at 6 and rounds is capped at 5 regardless of what you pass; the run reports it via log() and a capped field on the result when a request exceeds either.\",\n phases: [\n { title: \"Discussion\" },\n { title: \"Synthesis\" },\n ],\n};\n// Zero-config runnable (design/140 \u00A76 1c): every arg has an opinionated fallback.\nconst raw = args;\nconst a = raw !== null && typeof raw === \"object\" && !Array.isArray(raw) ? raw : {};\nconst topic =\n typeof a.topic === \"string\" && a.topic.trim() !== \"\"\n ? a.topic\n : typeof raw === \"string\" && raw.trim() !== \"\"\n ? raw // ergonomic form: a bare string args IS the topic\n : \"No topic was provided. Discuss: what information should a caller supply to make a discussion like this productive, and when should they NOT convene one?\";\nconst defaultMembers = [\n { role: \"advocate\", prompt: \"Make the strongest constructive case. Propose concrete options and argue their benefits with specifics.\" },\n { role: \"skeptic\", prompt: \"Stress-test every claim made so far. Surface risks, hidden costs, failure modes, and cheaper alternatives.\" },\n];\nconst rawMembers = Array.isArray(a.members) && a.members.length > 0 ? a.members : defaultMembers;\nconst members = rawMembers.slice(0, 6).map((m, i) => {\n const mm = m !== null && typeof m === \"object\" ? m : {};\n const member = {\n role: typeof mm.role === \"string\" && mm.role.trim() !== \"\" ? mm.role : \"member-\" + (i + 1),\n prompt: typeof mm.prompt === \"string\" && mm.prompt.trim() !== \"\" ? mm.prompt : \"Contribute your own distinct perspective: be concrete, give reasons, and engage with what others said.\",\n };\n if (typeof mm.model === \"string\" && mm.model.trim() !== \"\") member.model = mm.model;\n // Slot-tools carrier (design/140 \u2461-3 + F4): a member may BE a registered agent type ({agent:\"reviewer\"}) \u2014\n // persona/tools/model then come from the deployment's AgentDefinition (role library), args stay thin.\n if (typeof mm.agent === \"string\" && mm.agent.trim() !== \"\") member.agent = mm.agent;\n return member;\n});\n// Deterministic budget truncation (design/140 \u00A71 \u9884\u7B97 row): a HARD rounds ceiling + member cap \u2014 never an\n// evaluator agent. The engine's budget/maxAgents hard stops remain the backstop.\nconst requestedRounds = Math.floor(Number(a.rounds));\nconst normalizedRounds = Number.isFinite(requestedRounds) && requestedRounds >= 1 ? requestedRounds : 2;\nconst rounds = Math.min(normalizedRounds, 5);\n// RB-380 disclosure: the member/round slices above are silent by construction (Array.prototype.slice /\n// Math.min just drop the excess) \u2014 record + surface it instead of a caller finding out only by counting\n// transcript entries. Fires only when a request actually exceeded a ceiling (never on the common path).\nconst capNotes = [];\nif (rawMembers.length > 6) capNotes.push(\"requested \" + rawMembers.length + \" members, capped at 6\");\nif (normalizedRounds > 5) capNotes.push(\"requested \" + normalizedRounds + \" rounds, capped at 5\");\nfor (const note of capNotes) log(\"discussion: \" + note);\nconst fin = a.finalizer !== null && typeof a.finalizer === \"object\" && !Array.isArray(a.finalizer) ? a.finalizer : {};\nconst finalizerPrompt = typeof fin.prompt === \"string\" && fin.prompt.trim() !== \"\"\n ? fin.prompt\n : \"You are the synthesis lead. Read the full discussion transcript and produce the final verdict: the decision/answer, the key supporting points, and the strongest unresolved dissent (if any). Do not introduce new arguments of your own.\";\nconst clip = (s) => { const t = String(s); return t.length > 4000 ? t.slice(0, 4000) + \" ...[truncated]\" : t; };\nconst isBudgetStop = (e) => e !== null && typeof e === \"object\" && e.code === \"workflow.budget_exceeded\";\n\nphase(\"Discussion\");\nconst transcript = [];\nlet truncated = null;\nfor (let r = 1; r <= rounds && truncated === null; r++) {\n // Deterministic early stop on an exhausted budget (a live read of the engine budget; the engine's\n // hard WorkflowBudgetExceededError remains the backstop if a member call itself crosses the line).\n if (budget.total !== null && budget.remaining() <= 0) { truncated = \"budget exhausted before round \" + r; break; }\n for (const m of members) {\n const history = transcript.length === 0 ? \"(none yet - you open the discussion)\" : transcript.join(\"\\n\\n\");\n const spec = {\n objective:\n \"Discussion on: \" + topic + \"\\n\\n\" +\n 'You are \"' + m.role + '\" in round ' + r + \" of \" + rounds + \".\\n\" +\n \"Your brief: \" + m.prompt + \"\\n\\n\" +\n \"Transcript so far:\\n\" + history + \"\\n\\n\" +\n \"Respond to the strongest points others made (do not repeat yourself), then advance your own position. Be concise: a few tight paragraphs at most.\",\n };\n if (m.model !== undefined) spec.modelName = m.model;\n let res;\n try {\n res = await agent(spec, m.agent !== undefined ? { label: m.role + \"-r\" + r, phase: \"Discussion\", agentType: m.agent } : { label: m.role + \"-r\" + r, phase: \"Discussion\" });\n } catch (e) {\n // The engine's budget hard stop: keep what the discussion already produced instead of failing the run.\n if (isBudgetStop(e)) { truncated = \"budget exhausted at \" + m.role + \", round \" + r; break; }\n throw e;\n }\n const text = res && res.status === \"completed\" ? clip(res.result) : \"(no contribution - agent ended \" + (res ? res.status : \"unknown\") + \")\";\n transcript.push(m.role + \" (round \" + r + \"): \" + text);\n }\n}\n\nphase(\"Synthesis\");\nconst finalSpec = {\n objective:\n finalizerPrompt + \"\\n\\nTopic: \" + topic + \"\\n\\nFull transcript:\\n\" +\n (transcript.length === 0 ? \"(the discussion produced no contributions)\" : transcript.join(\"\\n\\n\")) +\n (truncated ? \"\\n\\nNote: the discussion was cut short (\" + truncated + \").\" : \"\"),\n};\nif (typeof fin.model === \"string\" && fin.model.trim() !== \"\") finalSpec.modelName = fin.model;\nlet verdict = null;\ntry {\n verdict = await agent(finalSpec, {\n label: \"finalizer\",\n phase: \"Synthesis\",\n schema: {\n type: \"object\",\n properties: {\n decision: { type: \"string\", description: \"The final answer/decision, one paragraph.\" },\n keyPoints: { type: \"array\", items: { type: \"string\" }, description: \"The strongest supporting points from the discussion.\" },\n dissent: { type: \"string\", description: \"The strongest unresolved counter-position, if any.\" },\n },\n required: [\"decision\", \"keyPoints\"],\n },\n });\n} catch (e) {\n // Budget died before synthesis: return the transcript honestly rather than failing the whole run.\n if (!isBudgetStop(e)) throw e;\n truncated = truncated === null ? \"budget exhausted before synthesis\" : truncated;\n}\n\nreturn {\n topic,\n rounds,\n members: members.map((m) => m.role),\n ...(capNotes.length > 0 ? { capped: capNotes } : {}),\n ...(truncated ? { truncated } : {}),\n transcript,\n verdict: verdict && verdict.structuredOutput !== undefined ? verdict.structuredOutput : (verdict ? verdict.result : null),\n};\n";
90
+ export declare const DISCUSSION_SCRIPT = "export const meta = {\n name: \"discussion\",\n description: \"Round-based discussion: configurable members debate a topic across rounds (each member sees the transcript so far), then a finalizer synthesizes a structured verdict.\",\n whenToUse: \"Use for a genuinely contested question that benefits from several perspectives arguing across rounds - design trade-offs, plan or risk reviews, adversarial critique of a proposal. Do NOT use it for a single factual question, a task with one obvious answer, or a budget-sensitive run: every round costs one agent call per member, so a discussion is never cheaper than asking once. args (all optional): { topic, members?: [{ role, prompt?, model? }], rounds?, finalizer?: { prompt?, model? } }. Hard ceilings: members is capped at 6 and rounds is capped at 5 regardless of what you pass; the run reports it via log() and a capped field on the result when a request exceeds either.\",\n phases: [\n { title: \"Discussion\" },\n { title: \"Synthesis\" },\n ],\n};\n// Zero-config runnable (design/140 \u00A76 1c): every arg has an opinionated fallback.\nconst raw = args;\nconst a = raw !== null && typeof raw === \"object\" && !Array.isArray(raw) ? raw : {};\nconst topic =\n typeof a.topic === \"string\" && a.topic.trim() !== \"\"\n ? a.topic\n : typeof raw === \"string\" && raw.trim() !== \"\"\n ? raw // ergonomic form: a bare string args IS the topic\n : \"No topic was provided. Discuss: what information should a caller supply to make a discussion like this productive, and when should they NOT convene one?\";\nconst defaultMembers = [\n { role: \"advocate\", prompt: \"Make the strongest constructive case. Propose concrete options and argue their benefits with specifics.\" },\n { role: \"skeptic\", prompt: \"Stress-test every claim made so far. Surface risks, hidden costs, failure modes, and cheaper alternatives.\" },\n];\nconst rawMembers = Array.isArray(a.members) && a.members.length > 0 ? a.members : defaultMembers;\nconst members = rawMembers.slice(0, 6).map((m, i) => {\n const mm = m !== null && typeof m === \"object\" ? m : {};\n const member = {\n role: typeof mm.role === \"string\" && mm.role.trim() !== \"\" ? mm.role : \"member-\" + (i + 1),\n prompt: typeof mm.prompt === \"string\" && mm.prompt.trim() !== \"\" ? mm.prompt : \"Contribute your own distinct perspective: be concrete, give reasons, and engage with what others said.\",\n };\n if (typeof mm.model === \"string\" && mm.model.trim() !== \"\") member.model = mm.model;\n // Slot-tools carrier (design/140 \u2461-3 + F4): a member may BE a registered agent type ({agent:\"reviewer\"}) \u2014\n // persona/tools/model then come from the deployment's AgentDefinition (role library), args stay thin.\n if (typeof mm.agent === \"string\" && mm.agent.trim() !== \"\") member.agent = mm.agent;\n return member;\n});\n// Deterministic budget truncation (design/140 \u00A71 \u9884\u7B97 row): a HARD rounds ceiling + member cap \u2014 never an\n// evaluator agent. The engine's budget/maxAgents hard stops remain the backstop.\nconst requestedRounds = Math.floor(Number(a.rounds));\nconst normalizedRounds = Number.isFinite(requestedRounds) && requestedRounds >= 1 ? requestedRounds : 2;\nconst rounds = Math.min(normalizedRounds, 5);\n// RB-380 disclosure: the member/round slices above are silent by construction (Array.prototype.slice /\n// Math.min just drop the excess) \u2014 record + surface it instead of a caller finding out only by counting\n// transcript entries. Fires only when a request actually exceeded a ceiling (never on the common path).\nconst capNotes = [];\nif (rawMembers.length > 6) capNotes.push(\"requested \" + rawMembers.length + \" members, capped at 6\");\nif (normalizedRounds > 5) capNotes.push(\"requested \" + normalizedRounds + \" rounds, capped at 5\");\nfor (const note of capNotes) log(\"discussion: \" + note);\nconst fin = a.finalizer !== null && typeof a.finalizer === \"object\" && !Array.isArray(a.finalizer) ? a.finalizer : {};\nconst finalizerPrompt = typeof fin.prompt === \"string\" && fin.prompt.trim() !== \"\"\n ? fin.prompt\n : \"You are the synthesis lead. Read the full discussion transcript and produce the final verdict: the decision/answer, the key supporting points, and the strongest unresolved dissent (if any). Do not introduce new arguments of your own.\";\nconst clip = (s) => { const t = String(s); return t.length > 4000 ? t.slice(0, 4000) + \" ...[truncated]\" : t; };\nconst isBudgetStop = (e) => e !== null && typeof e === \"object\" && e.code === \"workflow.budget_exceeded\";\n\nphase(\"Discussion\");\nconst transcript = [];\nlet truncated = null;\nfor (let r = 1; r <= rounds && truncated === null; r++) {\n // Deterministic early stop on an exhausted budget (a live read of the engine budget; the engine's\n // hard WorkflowBudgetExceededError remains the backstop if a member call itself crosses the line).\n if (budget.total !== null && budget.remaining() <= 0) { truncated = \"budget exhausted before round \" + r; break; }\n for (const m of members) {\n const history = transcript.length === 0 ? \"(none yet - you open the discussion)\" : transcript.join(\"\\n\\n\");\n const spec = {\n objective:\n \"Discussion on: \" + topic + \"\\n\\n\" +\n 'You are \"' + m.role + '\" in round ' + r + \" of \" + rounds + \".\\n\" +\n \"Your brief: \" + m.prompt + \"\\n\\n\" +\n \"Transcript so far:\\n\" + history + \"\\n\\n\" +\n \"Respond to the strongest points others made (do not repeat yourself), then advance your own position. Be concise: a few tight paragraphs at most.\",\n };\n if (m.model !== undefined) spec.modelName = m.model;\n let res;\n try {\n res = await agent(spec, m.agent !== undefined ? { label: m.role + \"-r\" + r, phase: \"Discussion\", agentType: m.agent } : { label: m.role + \"-r\" + r, phase: \"Discussion\" });\n } catch (e) {\n // The engine's budget hard stop: keep what the discussion already produced instead of failing the run.\n if (isBudgetStop(e)) { truncated = \"budget exhausted at \" + m.role + \", round \" + r; break; }\n throw e;\n }\n const text = res && res.terminal && res.terminal.kind === \"completed\" ? clip(res.result) : \"(no contribution - agent ended \" + (res && res.terminal ? res.terminal.kind : \"unknown\") + \")\";\n transcript.push(m.role + \" (round \" + r + \"): \" + text);\n }\n}\n\nphase(\"Synthesis\");\nconst finalSpec = {\n objective:\n finalizerPrompt + \"\\n\\nTopic: \" + topic + \"\\n\\nFull transcript:\\n\" +\n (transcript.length === 0 ? \"(the discussion produced no contributions)\" : transcript.join(\"\\n\\n\")) +\n (truncated ? \"\\n\\nNote: the discussion was cut short (\" + truncated + \").\" : \"\"),\n};\nif (typeof fin.model === \"string\" && fin.model.trim() !== \"\") finalSpec.modelName = fin.model;\nlet verdict = null;\ntry {\n verdict = await agent(finalSpec, {\n label: \"finalizer\",\n phase: \"Synthesis\",\n schema: {\n type: \"object\",\n properties: {\n decision: { type: \"string\", description: \"The final answer/decision, one paragraph.\" },\n keyPoints: { type: \"array\", items: { type: \"string\" }, description: \"The strongest supporting points from the discussion.\" },\n dissent: { type: \"string\", description: \"The strongest unresolved counter-position, if any.\" },\n },\n required: [\"decision\", \"keyPoints\"],\n },\n });\n} catch (e) {\n // Budget died before synthesis: return the transcript honestly rather than failing the whole run.\n if (!isBudgetStop(e)) throw e;\n truncated = truncated === null ? \"budget exhausted before synthesis\" : truncated;\n}\n\nreturn {\n topic,\n rounds,\n members: members.map((m) => m.role),\n ...(capNotes.length > 0 ? { capped: capNotes } : {}),\n ...(truncated ? { truncated } : {}),\n transcript,\n verdict: verdict && verdict.structuredOutput !== undefined ? verdict.structuredOutput : (verdict ? verdict.result : null),\n};\n";
91
91
  /**
92
92
  * @deprecated Use {@link DISCUSSION_SCRIPT}. Retired spelling retained (C-R14; the original one-minor
93
93
  * window lapsed — removal needs an announced BREAKING window) so a deployment importing the constant by
94
94
  * its old name keeps compiling; the value is the SAME script (its `meta.name`
95
95
  * is the new `discussion`).
96
96
  */
97
- export declare const TEAM_DISCUSSION_SCRIPT = "export const meta = {\n name: \"discussion\",\n description: \"Round-based discussion: configurable members debate a topic across rounds (each member sees the transcript so far), then a finalizer synthesizes a structured verdict.\",\n whenToUse: \"Use for a genuinely contested question that benefits from several perspectives arguing across rounds - design trade-offs, plan or risk reviews, adversarial critique of a proposal. Do NOT use it for a single factual question, a task with one obvious answer, or a budget-sensitive run: every round costs one agent call per member, so a discussion is never cheaper than asking once. args (all optional): { topic, members?: [{ role, prompt?, model? }], rounds?, finalizer?: { prompt?, model? } }. Hard ceilings: members is capped at 6 and rounds is capped at 5 regardless of what you pass; the run reports it via log() and a capped field on the result when a request exceeds either.\",\n phases: [\n { title: \"Discussion\" },\n { title: \"Synthesis\" },\n ],\n};\n// Zero-config runnable (design/140 \u00A76 1c): every arg has an opinionated fallback.\nconst raw = args;\nconst a = raw !== null && typeof raw === \"object\" && !Array.isArray(raw) ? raw : {};\nconst topic =\n typeof a.topic === \"string\" && a.topic.trim() !== \"\"\n ? a.topic\n : typeof raw === \"string\" && raw.trim() !== \"\"\n ? raw // ergonomic form: a bare string args IS the topic\n : \"No topic was provided. Discuss: what information should a caller supply to make a discussion like this productive, and when should they NOT convene one?\";\nconst defaultMembers = [\n { role: \"advocate\", prompt: \"Make the strongest constructive case. Propose concrete options and argue their benefits with specifics.\" },\n { role: \"skeptic\", prompt: \"Stress-test every claim made so far. Surface risks, hidden costs, failure modes, and cheaper alternatives.\" },\n];\nconst rawMembers = Array.isArray(a.members) && a.members.length > 0 ? a.members : defaultMembers;\nconst members = rawMembers.slice(0, 6).map((m, i) => {\n const mm = m !== null && typeof m === \"object\" ? m : {};\n const member = {\n role: typeof mm.role === \"string\" && mm.role.trim() !== \"\" ? mm.role : \"member-\" + (i + 1),\n prompt: typeof mm.prompt === \"string\" && mm.prompt.trim() !== \"\" ? mm.prompt : \"Contribute your own distinct perspective: be concrete, give reasons, and engage with what others said.\",\n };\n if (typeof mm.model === \"string\" && mm.model.trim() !== \"\") member.model = mm.model;\n // Slot-tools carrier (design/140 \u2461-3 + F4): a member may BE a registered agent type ({agent:\"reviewer\"}) \u2014\n // persona/tools/model then come from the deployment's AgentDefinition (role library), args stay thin.\n if (typeof mm.agent === \"string\" && mm.agent.trim() !== \"\") member.agent = mm.agent;\n return member;\n});\n// Deterministic budget truncation (design/140 \u00A71 \u9884\u7B97 row): a HARD rounds ceiling + member cap \u2014 never an\n// evaluator agent. The engine's budget/maxAgents hard stops remain the backstop.\nconst requestedRounds = Math.floor(Number(a.rounds));\nconst normalizedRounds = Number.isFinite(requestedRounds) && requestedRounds >= 1 ? requestedRounds : 2;\nconst rounds = Math.min(normalizedRounds, 5);\n// RB-380 disclosure: the member/round slices above are silent by construction (Array.prototype.slice /\n// Math.min just drop the excess) \u2014 record + surface it instead of a caller finding out only by counting\n// transcript entries. Fires only when a request actually exceeded a ceiling (never on the common path).\nconst capNotes = [];\nif (rawMembers.length > 6) capNotes.push(\"requested \" + rawMembers.length + \" members, capped at 6\");\nif (normalizedRounds > 5) capNotes.push(\"requested \" + normalizedRounds + \" rounds, capped at 5\");\nfor (const note of capNotes) log(\"discussion: \" + note);\nconst fin = a.finalizer !== null && typeof a.finalizer === \"object\" && !Array.isArray(a.finalizer) ? a.finalizer : {};\nconst finalizerPrompt = typeof fin.prompt === \"string\" && fin.prompt.trim() !== \"\"\n ? fin.prompt\n : \"You are the synthesis lead. Read the full discussion transcript and produce the final verdict: the decision/answer, the key supporting points, and the strongest unresolved dissent (if any). Do not introduce new arguments of your own.\";\nconst clip = (s) => { const t = String(s); return t.length > 4000 ? t.slice(0, 4000) + \" ...[truncated]\" : t; };\nconst isBudgetStop = (e) => e !== null && typeof e === \"object\" && e.code === \"workflow.budget_exceeded\";\n\nphase(\"Discussion\");\nconst transcript = [];\nlet truncated = null;\nfor (let r = 1; r <= rounds && truncated === null; r++) {\n // Deterministic early stop on an exhausted budget (a live read of the engine budget; the engine's\n // hard WorkflowBudgetExceededError remains the backstop if a member call itself crosses the line).\n if (budget.total !== null && budget.remaining() <= 0) { truncated = \"budget exhausted before round \" + r; break; }\n for (const m of members) {\n const history = transcript.length === 0 ? \"(none yet - you open the discussion)\" : transcript.join(\"\\n\\n\");\n const spec = {\n objective:\n \"Discussion on: \" + topic + \"\\n\\n\" +\n 'You are \"' + m.role + '\" in round ' + r + \" of \" + rounds + \".\\n\" +\n \"Your brief: \" + m.prompt + \"\\n\\n\" +\n \"Transcript so far:\\n\" + history + \"\\n\\n\" +\n \"Respond to the strongest points others made (do not repeat yourself), then advance your own position. Be concise: a few tight paragraphs at most.\",\n };\n if (m.model !== undefined) spec.modelName = m.model;\n let res;\n try {\n res = await agent(spec, m.agent !== undefined ? { label: m.role + \"-r\" + r, phase: \"Discussion\", agentType: m.agent } : { label: m.role + \"-r\" + r, phase: \"Discussion\" });\n } catch (e) {\n // The engine's budget hard stop: keep what the discussion already produced instead of failing the run.\n if (isBudgetStop(e)) { truncated = \"budget exhausted at \" + m.role + \", round \" + r; break; }\n throw e;\n }\n const text = res && res.status === \"completed\" ? clip(res.result) : \"(no contribution - agent ended \" + (res ? res.status : \"unknown\") + \")\";\n transcript.push(m.role + \" (round \" + r + \"): \" + text);\n }\n}\n\nphase(\"Synthesis\");\nconst finalSpec = {\n objective:\n finalizerPrompt + \"\\n\\nTopic: \" + topic + \"\\n\\nFull transcript:\\n\" +\n (transcript.length === 0 ? \"(the discussion produced no contributions)\" : transcript.join(\"\\n\\n\")) +\n (truncated ? \"\\n\\nNote: the discussion was cut short (\" + truncated + \").\" : \"\"),\n};\nif (typeof fin.model === \"string\" && fin.model.trim() !== \"\") finalSpec.modelName = fin.model;\nlet verdict = null;\ntry {\n verdict = await agent(finalSpec, {\n label: \"finalizer\",\n phase: \"Synthesis\",\n schema: {\n type: \"object\",\n properties: {\n decision: { type: \"string\", description: \"The final answer/decision, one paragraph.\" },\n keyPoints: { type: \"array\", items: { type: \"string\" }, description: \"The strongest supporting points from the discussion.\" },\n dissent: { type: \"string\", description: \"The strongest unresolved counter-position, if any.\" },\n },\n required: [\"decision\", \"keyPoints\"],\n },\n });\n} catch (e) {\n // Budget died before synthesis: return the transcript honestly rather than failing the whole run.\n if (!isBudgetStop(e)) throw e;\n truncated = truncated === null ? \"budget exhausted before synthesis\" : truncated;\n}\n\nreturn {\n topic,\n rounds,\n members: members.map((m) => m.role),\n ...(capNotes.length > 0 ? { capped: capNotes } : {}),\n ...(truncated ? { truncated } : {}),\n transcript,\n verdict: verdict && verdict.structuredOutput !== undefined ? verdict.structuredOutput : (verdict ? verdict.result : null),\n};\n";
97
+ export declare const TEAM_DISCUSSION_SCRIPT = "export const meta = {\n name: \"discussion\",\n description: \"Round-based discussion: configurable members debate a topic across rounds (each member sees the transcript so far), then a finalizer synthesizes a structured verdict.\",\n whenToUse: \"Use for a genuinely contested question that benefits from several perspectives arguing across rounds - design trade-offs, plan or risk reviews, adversarial critique of a proposal. Do NOT use it for a single factual question, a task with one obvious answer, or a budget-sensitive run: every round costs one agent call per member, so a discussion is never cheaper than asking once. args (all optional): { topic, members?: [{ role, prompt?, model? }], rounds?, finalizer?: { prompt?, model? } }. Hard ceilings: members is capped at 6 and rounds is capped at 5 regardless of what you pass; the run reports it via log() and a capped field on the result when a request exceeds either.\",\n phases: [\n { title: \"Discussion\" },\n { title: \"Synthesis\" },\n ],\n};\n// Zero-config runnable (design/140 \u00A76 1c): every arg has an opinionated fallback.\nconst raw = args;\nconst a = raw !== null && typeof raw === \"object\" && !Array.isArray(raw) ? raw : {};\nconst topic =\n typeof a.topic === \"string\" && a.topic.trim() !== \"\"\n ? a.topic\n : typeof raw === \"string\" && raw.trim() !== \"\"\n ? raw // ergonomic form: a bare string args IS the topic\n : \"No topic was provided. Discuss: what information should a caller supply to make a discussion like this productive, and when should they NOT convene one?\";\nconst defaultMembers = [\n { role: \"advocate\", prompt: \"Make the strongest constructive case. Propose concrete options and argue their benefits with specifics.\" },\n { role: \"skeptic\", prompt: \"Stress-test every claim made so far. Surface risks, hidden costs, failure modes, and cheaper alternatives.\" },\n];\nconst rawMembers = Array.isArray(a.members) && a.members.length > 0 ? a.members : defaultMembers;\nconst members = rawMembers.slice(0, 6).map((m, i) => {\n const mm = m !== null && typeof m === \"object\" ? m : {};\n const member = {\n role: typeof mm.role === \"string\" && mm.role.trim() !== \"\" ? mm.role : \"member-\" + (i + 1),\n prompt: typeof mm.prompt === \"string\" && mm.prompt.trim() !== \"\" ? mm.prompt : \"Contribute your own distinct perspective: be concrete, give reasons, and engage with what others said.\",\n };\n if (typeof mm.model === \"string\" && mm.model.trim() !== \"\") member.model = mm.model;\n // Slot-tools carrier (design/140 \u2461-3 + F4): a member may BE a registered agent type ({agent:\"reviewer\"}) \u2014\n // persona/tools/model then come from the deployment's AgentDefinition (role library), args stay thin.\n if (typeof mm.agent === \"string\" && mm.agent.trim() !== \"\") member.agent = mm.agent;\n return member;\n});\n// Deterministic budget truncation (design/140 \u00A71 \u9884\u7B97 row): a HARD rounds ceiling + member cap \u2014 never an\n// evaluator agent. The engine's budget/maxAgents hard stops remain the backstop.\nconst requestedRounds = Math.floor(Number(a.rounds));\nconst normalizedRounds = Number.isFinite(requestedRounds) && requestedRounds >= 1 ? requestedRounds : 2;\nconst rounds = Math.min(normalizedRounds, 5);\n// RB-380 disclosure: the member/round slices above are silent by construction (Array.prototype.slice /\n// Math.min just drop the excess) \u2014 record + surface it instead of a caller finding out only by counting\n// transcript entries. Fires only when a request actually exceeded a ceiling (never on the common path).\nconst capNotes = [];\nif (rawMembers.length > 6) capNotes.push(\"requested \" + rawMembers.length + \" members, capped at 6\");\nif (normalizedRounds > 5) capNotes.push(\"requested \" + normalizedRounds + \" rounds, capped at 5\");\nfor (const note of capNotes) log(\"discussion: \" + note);\nconst fin = a.finalizer !== null && typeof a.finalizer === \"object\" && !Array.isArray(a.finalizer) ? a.finalizer : {};\nconst finalizerPrompt = typeof fin.prompt === \"string\" && fin.prompt.trim() !== \"\"\n ? fin.prompt\n : \"You are the synthesis lead. Read the full discussion transcript and produce the final verdict: the decision/answer, the key supporting points, and the strongest unresolved dissent (if any). Do not introduce new arguments of your own.\";\nconst clip = (s) => { const t = String(s); return t.length > 4000 ? t.slice(0, 4000) + \" ...[truncated]\" : t; };\nconst isBudgetStop = (e) => e !== null && typeof e === \"object\" && e.code === \"workflow.budget_exceeded\";\n\nphase(\"Discussion\");\nconst transcript = [];\nlet truncated = null;\nfor (let r = 1; r <= rounds && truncated === null; r++) {\n // Deterministic early stop on an exhausted budget (a live read of the engine budget; the engine's\n // hard WorkflowBudgetExceededError remains the backstop if a member call itself crosses the line).\n if (budget.total !== null && budget.remaining() <= 0) { truncated = \"budget exhausted before round \" + r; break; }\n for (const m of members) {\n const history = transcript.length === 0 ? \"(none yet - you open the discussion)\" : transcript.join(\"\\n\\n\");\n const spec = {\n objective:\n \"Discussion on: \" + topic + \"\\n\\n\" +\n 'You are \"' + m.role + '\" in round ' + r + \" of \" + rounds + \".\\n\" +\n \"Your brief: \" + m.prompt + \"\\n\\n\" +\n \"Transcript so far:\\n\" + history + \"\\n\\n\" +\n \"Respond to the strongest points others made (do not repeat yourself), then advance your own position. Be concise: a few tight paragraphs at most.\",\n };\n if (m.model !== undefined) spec.modelName = m.model;\n let res;\n try {\n res = await agent(spec, m.agent !== undefined ? { label: m.role + \"-r\" + r, phase: \"Discussion\", agentType: m.agent } : { label: m.role + \"-r\" + r, phase: \"Discussion\" });\n } catch (e) {\n // The engine's budget hard stop: keep what the discussion already produced instead of failing the run.\n if (isBudgetStop(e)) { truncated = \"budget exhausted at \" + m.role + \", round \" + r; break; }\n throw e;\n }\n const text = res && res.terminal && res.terminal.kind === \"completed\" ? clip(res.result) : \"(no contribution - agent ended \" + (res && res.terminal ? res.terminal.kind : \"unknown\") + \")\";\n transcript.push(m.role + \" (round \" + r + \"): \" + text);\n }\n}\n\nphase(\"Synthesis\");\nconst finalSpec = {\n objective:\n finalizerPrompt + \"\\n\\nTopic: \" + topic + \"\\n\\nFull transcript:\\n\" +\n (transcript.length === 0 ? \"(the discussion produced no contributions)\" : transcript.join(\"\\n\\n\")) +\n (truncated ? \"\\n\\nNote: the discussion was cut short (\" + truncated + \").\" : \"\"),\n};\nif (typeof fin.model === \"string\" && fin.model.trim() !== \"\") finalSpec.modelName = fin.model;\nlet verdict = null;\ntry {\n verdict = await agent(finalSpec, {\n label: \"finalizer\",\n phase: \"Synthesis\",\n schema: {\n type: \"object\",\n properties: {\n decision: { type: \"string\", description: \"The final answer/decision, one paragraph.\" },\n keyPoints: { type: \"array\", items: { type: \"string\" }, description: \"The strongest supporting points from the discussion.\" },\n dissent: { type: \"string\", description: \"The strongest unresolved counter-position, if any.\" },\n },\n required: [\"decision\", \"keyPoints\"],\n },\n });\n} catch (e) {\n // Budget died before synthesis: return the transcript honestly rather than failing the whole run.\n if (!isBudgetStop(e)) throw e;\n truncated = truncated === null ? \"budget exhausted before synthesis\" : truncated;\n}\n\nreturn {\n topic,\n rounds,\n members: members.map((m) => m.role),\n ...(capNotes.length > 0 ? { capped: capNotes } : {}),\n ...(truncated ? { truncated } : {}),\n transcript,\n verdict: verdict && verdict.structuredOutput !== undefined ? verdict.structuredOutput : (verdict ? verdict.result : null),\n};\n";
98
98
  /** One built-in named workflow: the registered name + its self-contained script source. The name is the
99
99
  * routing key of the `{name}` calling surface; the script's `meta.name` matches it (locked by test). */
100
100
  export interface BuiltinWorkflowDefinition {
@@ -95,7 +95,7 @@ for (let r = 1; r <= rounds && truncated === null; r++) {
95
95
  if (isBudgetStop(e)) { truncated = "budget exhausted at " + m.role + ", round " + r; break; }
96
96
  throw e;
97
97
  }
98
- const text = res && res.status === "completed" ? clip(res.result) : "(no contribution - agent ended " + (res ? res.status : "unknown") + ")";
98
+ const text = res && res.terminal && res.terminal.kind === "completed" ? clip(res.result) : "(no contribution - agent ended " + (res && res.terminal ? res.terminal.kind : "unknown") + ")";
99
99
  transcript.push(m.role + " (round " + r + "): " + text);
100
100
  }
101
101
  }
@@ -2,7 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import { Type } from "typebox";
3
3
  import { delimitUntrusted } from "../core/untrusted-text.js";
4
4
  import { OUTPUT_TOOL_NAME } from "../core/runner/synthetic-tools.js";
5
- import { isDurablePause } from "../agents/suspend-guard.js";
5
+ import { isDurablePause, pauseFamily } from "../agents/suspend-guard.js";
6
6
  export const DECLARE_DONE_TOOL_NAME = "DeclareDone";
7
7
  function createDeclareDoneTool(ref) {
8
8
  return {
@@ -143,20 +143,21 @@ export async function runGoal(runner, spec) {
143
143
  cumulativeTokens += (result.stats.tokens ?? 0) + (result.stats.nested?.tokens ?? 0);
144
144
  spentCostMicroUsd += (result.stats.costMicroUsd ?? 0) + (result.stats.nested?.costMicroUsd ?? 0);
145
145
  spentTurns += (result.stats.turns ?? 0) + (result.stats.nested?.turns ?? 0);
146
- if (isDurablePause(result.status)) {
147
- return mk(result.status, iteration, { result, checkpointToken: result.checkpointToken });
146
+ if (isDurablePause(result.terminal)) {
147
+ return mk(pauseFamily(result.terminal), iteration, { result, checkpointToken: result.terminal.token });
148
148
  }
149
149
  const midStop = classifyStop();
150
150
  if (midStop)
151
151
  return mk(midStop.status, iteration, { result, budgetCause: midStop.budgetCause });
152
- if (result.status === "blocked")
152
+ if (result.terminal.kind === "blocked")
153
153
  return mk("blocked", iteration, { result });
154
- if (result.status === "failed") {
155
- if (result.errorCode !== undefined && result.errorCode.startsWith("limits.max_")) {
154
+ if (result.terminal.kind === "failed") {
155
+ const code = result.terminal.code;
156
+ if (code !== undefined && code.startsWith("limits.max_")) {
156
157
  lastFeedback = TIMEOUT_FEEDBACK;
157
158
  continue;
158
159
  }
159
- return mk("failed", iteration, { result, errorCode: result.errorCode });
160
+ return mk("failed", iteration, { result, errorCode: code });
160
161
  }
161
162
  const gate1 = hasOutputSchema ? result.structuredOutput !== undefined : doneRef.declared === true;
162
163
  if (!gate1) {
@@ -4,6 +4,7 @@ import { isAbsolute, resolve, sep } from "node:path";
4
4
  import { combinePolicies } from "../core/tool-policy.js";
5
5
  import { delimitUntrusted } from "../core/untrusted-text.js";
6
6
  import { isDurablePause } from "../agents/suspend-guard.js";
7
+ import { terminalProjection } from "../core/runner/terminal-projection.js";
7
8
  import { isAbsolutePathForm, writeTargetPath } from "../tools/fs/safety.js";
8
9
  import { captureBaseline, resolveFrozenPaths, restoreFrozenPaths, runOracle, snapshotFrozenPaths, specError, } from "../core/spec-contract.js";
9
10
  const WRITE_TOOLS = new Set(["Write", "Edit", "NotebookEdit"]);
@@ -183,15 +184,16 @@ export async function runSpec(runner, contract, opts) {
183
184
  oracleRedRuns++;
184
185
  }
185
186
  const oracle = { final: final.verdict, perGate: final.gates, baseline };
186
- if (isDurablePause(result.status))
187
+ if (isDurablePause(result.terminal))
187
188
  return { result, oracle, guardRestores };
189
+ const terminalFace = terminalProjection(result.terminal);
188
190
  const outcome = {
189
191
  taskSignature: contract.taskSignature ?? `spec:${createHash("sha256").update(specText).digest("hex").slice(0, 16)}`,
190
192
  runId: result.taskId,
191
193
  model: result.model,
192
194
  verificationKind: "mechanical",
193
- status: result.status,
194
- errorCode: result.errorCode,
195
+ status: terminalFace.status,
196
+ errorCode: terminalFace.errorCode,
195
197
  oracle: {
196
198
  green: final.verdict === "green",
197
199
  finalVerdict: final.verdict,
@@ -332,7 +332,7 @@ export interface RunWorkflowToolDeps {
332
332
  autoModeReview?: () => {
333
333
  decider: import("../core/auto-mode.js").AutoModeDecider;
334
334
  } | undefined;
335
- /** 5.30 merge-rescan round 2 — the HOST task's durable OFF SWITCH (`spec.checkpointStore === null`,
335
+ /** 5.30 merge-rescan round 2 — the HOST task's durable OFF SWITCH (`spec.checkpointStore === "disabled"`,
336
336
  * ruled 2026-08-04 to inherit into every spawned agent, see `buildWorkflowPrimitives`'s call
337
337
  * above). Known at prepare-time (no lazy getter needed — `spec` is available at mount). A dep for
338
338
  * the same reason as `forwardEvent`/`inheritedGateForChildren`: the auto-mounted tool's execute
@@ -322,10 +322,10 @@ export async function createRunWorkflowTool(d) {
322
322
  "• Research/audit workflows: document and file claims are HYPOTHESES, not evidence — add a VERIFY " +
323
323
  "stage whose agents EXECUTE (run the command, the test, the reproducer) and cross-check claims " +
324
324
  "against actual output before the synthesis stage reports them as fact.\n" +
325
- "• r.status: agent() does NOT throw when the sub-agent FAILS — it RETURNS the result with r.status " +
326
- "('completed' on success; 'failed'/'blocked'/etc otherwise, with the failure text in r.result). ALWAYS " +
327
- "check r.status before building on a result, and GATE later phases on it — e.g. " +
328
- "`const impl = await agent({ objective: '…' }, { label: 'implement-fix' }); if (impl.status !== 'completed') return { failed: 'implement', why: impl.result }` " +
325
+ "• r.terminal.kind: agent() does NOT throw when the sub-agent FAILS — it RETURNS the result with r.terminal " +
326
+ "({ kind: 'completed' } on success; kind 'failed' (with r.terminal.code / r.terminal.message) / 'blocked' (r.terminal.reason) / 'paused' otherwise, " +
327
+ "with any failure text also in r.result). ALWAYS check r.terminal.kind before building on a result, and GATE later phases on it — e.g. " +
328
+ "`const impl = await agent({ objective: '…' }, { label: 'implement-fix' }); if (impl.terminal.kind !== 'completed') return { failed: 'implement', why: impl.result }` " +
329
329
  "— running later phases on a failed prerequisite burns tokens on garbage.\n" +
330
330
  "• phase (two forms, both fine): bare marker — phase('Scan'); groups every later agent() under 'Scan' " +
331
331
  "until the next phase() call. Scoped — await phase('Fix', async () => { ...agents... }); groups just the " +
@@ -194,10 +194,10 @@ export interface ResourceClampNote {
194
194
  * `restoreGatedTools` / `readFace` / … — for exactly the children an untrusted script can route to
195
195
  * the overlay (`isolation: "worktree"`), own-`undefined` keys included (a spread copies them; the
196
196
  * old `=== null` face drop missed that half). One rule at the one merge point: a new baseline key
197
- * is safe with no list to extend. The single exception is {@link NULL_VALUED_BASELINE_KEYS}
198
- * (`checkpointStore: null` the deployment disarming durable suspend for isolated children is a
199
- * capability removal it must be able to spell). Explicit non-nullish overlay values keep winning
200
- * wholesale — the documented deployment override.
197
+ * is safe with no list to extend, and there is no exception list the one key that used to give
198
+ * `null` a meaning of its own (`checkpointStore`, the durable off switch) now spells it as the word
199
+ * `"disabled"`, an ordinary non-nullish value the rule already keeps. Explicit non-nullish overlay
200
+ * values keep winning wholesale — the documented deployment override.
201
201
  *
202
202
  * ACCEPTED COST, stated (adversarial-review round 2): the rule is direction-blind — a CAPABILITY
203
203
  * key's out-of-type overlay `null` used to WITHHOLD the base value by the same spread accident
@@ -4,6 +4,7 @@ import { sanitizeUntrustedText } from "../core/untrusted-text.js";
4
4
  import { compileReadDeny } from "../tools/fs/read-deny.js";
5
5
  import { WorkflowScriptError } from "./workflow-meta.js";
6
6
  import { uuidv7 } from "../engine/session/uuid.js";
7
+ import { retiredCheckpointStoreNull } from "../core/checkpoint-store.js";
7
8
  export class WorkflowModelNotAllowedError extends Error {
8
9
  modelName;
9
10
  code = "workflow.model_not_allowed";
@@ -79,12 +80,13 @@ export function emitIgnoredAgentOptionsNotice(total, sample, accepted, onNotice)
79
80
  }
80
81
  }
81
82
  const VALID_THINKING = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
82
- const NULL_VALUED_BASELINE_KEYS = new Set(["checkpointStore"]);
83
83
  export function overlayWorktreeBaseline(base, worktreeBase) {
84
84
  let overlay = worktreeBase;
85
85
  for (const key of Object.keys(worktreeBase)) {
86
86
  const v = worktreeBase[key];
87
- if (v !== undefined && (v !== null || NULL_VALUED_BASELINE_KEYS.has(key)))
87
+ if (key === "checkpointStore" && v === null)
88
+ throw retiredCheckpointStoreNull();
89
+ if (v !== undefined && v !== null)
88
90
  continue;
89
91
  if (overlay === worktreeBase)
90
92
  overlay = { ...worktreeBase };
@@ -33,7 +33,7 @@ export interface WorkflowGovernance {
33
33
  * `agent` primitive is GOVERNED when `governance` is set (LLM-authored), else a trusted pass-through.
34
34
  */
35
35
  export declare function buildWorkflowPrimitives(ctx: WorkflowRunContext, governance?: WorkflowGovernance, onAgentSpawn?: (handle: WorkflowAgentHandle) => void, parentThinking?: () => TaskSpec["thinking"], parentPrincipal?: string,
36
- /** ruled 2026-08-04 — the host run set `TaskSpec.checkpointStore: null` (the per-run durable off
36
+ /** ruled 2026-08-04 — the host run set `TaskSpec.checkpointStore: "disabled"` (the per-run durable off
37
37
  * switch). Every agent this workflow spawns inherits it; see the injection below. */
38
38
  parentCheckpointStoreDisabled?: boolean,
39
39
  /** 5.30 merge-rescan (design/199 parity gap) — the host run's RESOLVED read-face containment
@@ -83,7 +83,7 @@ export function buildWorkflowPrimitives(ctx, governance, onAgentSpawn, parentThi
83
83
  childSpec.principal = parentPrincipal;
84
84
  }
85
85
  if (parentCheckpointStoreDisabled === true) {
86
- childSpec.checkpointStore = null;
86
+ childSpec.checkpointStore = "disabled";
87
87
  }
88
88
  if (parentReadFace) {
89
89
  const pf = parentReadFace();
@@ -111,6 +111,17 @@ export declare class WorkflowResultTooLargeError extends Error {
111
111
  readonly code = "workflow.result_too_large";
112
112
  constructor(size: number, max: number);
113
113
  }
114
+ /** A resume journal entry whose result carries no terminal cause — a journal written before
115
+ * `TaskResult.terminal` existed, or a malformed entry — is INCOMPATIBLE, and the resume is refused
116
+ * before any work is dispatched. The alternative (letting the entry diverge into a live re-run) would
117
+ * execute the already-completed prefix again, side effects included; a schema mismatch is never a
118
+ * permission to rerun finished work. Start a fresh run instead of resuming such a journal. */
119
+ export declare class WorkflowJournalIncompatibleError extends Error {
120
+ readonly fromRunId: string;
121
+ readonly ordinal: number;
122
+ readonly code = "workflow.journal_incompatible";
123
+ constructor(fromRunId: string, ordinal: number);
124
+ }
114
125
  /**
115
126
  * A TRUSTED, run-scoped internal channel carrying the workflow **nesting depth** across a process boundary
116
127
  * (design/98 §0.1 BLOCKER3). It is NOT a `TaskSpec` field and NOT a `run_workflow` tool param — a worker or
@@ -3,6 +3,8 @@ import { AsyncLocalStorage } from "node:async_hooks";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import { availableParallelism } from "node:os";
5
5
  import { uuidv7 } from "../internal/harness.js";
6
+ import { TERMINAL_CAUSE_IS_REPLAYABLE, isTerminalCauseKind } from "../core/terminal-cause.js";
7
+ import { terminalProjection } from "../core/runner/terminal-projection.js";
6
8
  import { builtinAgentDefinitions } from "../agents/builtin-agents.js";
7
9
  import { GENERAL_PURPOSE_SUBAGENT_TYPE, markerFragment } from "../agents/subagent.js";
8
10
  import { combinePolicies, createAllowDenyPolicy } from "../core/tool-policy.js";
@@ -200,6 +202,19 @@ export class WorkflowResultTooLargeError extends Error {
200
202
  this.name = "WorkflowResultTooLargeError";
201
203
  }
202
204
  }
205
+ export class WorkflowJournalIncompatibleError extends Error {
206
+ fromRunId;
207
+ ordinal;
208
+ code = "workflow.journal_incompatible";
209
+ constructor(fromRunId, ordinal) {
210
+ super(`startWorkflow: resume from "${fromRunId}" was REFUSED — journal entry #${ordinal} carries no terminal cause ` +
211
+ "(a journal written before TaskResult.terminal, or a malformed entry). Completed work must not run again; " +
212
+ "start a fresh run instead of resuming this journal.");
213
+ this.fromRunId = fromRunId;
214
+ this.ordinal = ordinal;
215
+ this.name = "WorkflowJournalIncompatibleError";
216
+ }
217
+ }
203
218
  const workflowDepthStore = new AsyncLocalStorage();
204
219
  function currentWorkflowDepth() {
205
220
  return workflowDepthStore.getStore()?.depth ?? 0;
@@ -768,9 +783,12 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
768
783
  };
769
784
  let journalTail = Promise.resolve();
770
785
  const JOURNAL_DRAIN_MAX_MS = 5_000;
771
- const journalDurableResult = (result) => result.status !== "completed" && typeof result.errorMessage === "string" && result.errorMessage !== ""
772
- ? { ...result, errorMessage: boundedRedactedSummary(result.errorMessage, MAX_TRANSCRIPT_CHARS) }
773
- : result;
786
+ const journalDurableResult = (result) => {
787
+ const t = result.terminal;
788
+ if (t === undefined || t.kind !== "failed" || typeof t.message !== "string" || t.message === "")
789
+ return result;
790
+ return { ...result, terminal: { ...t, message: boundedRedactedSummary(t.message, MAX_TRANSCRIPT_CHARS) } };
791
+ };
774
792
  const journalAppend = async (callKey, result, label) => {
775
793
  const dbg = typeof process !== "undefined" && process.env?.SEMA_DEBUG_WORKFLOW_JOURNAL === "1";
776
794
  if (!journalStore) {
@@ -972,16 +990,17 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
972
990
  }
973
991
  emitRunLog(`resume: replay stopped at agent #${ordinal} (${reason}) — this call and every later call run live.`);
974
992
  };
975
- const settleAgentError = (rec, result, label, rawOutput) => {
976
- if (result.status !== "completed") {
977
- if (result.errorCode !== undefined)
978
- rec.errorCode = result.errorCode;
979
- if (result.errorMessage !== undefined && result.errorMessage !== "") {
980
- rec.errorMessage = boundedRedactedSummary(result.errorMessage, MAX_TRANSCRIPT_CHARS);
993
+ const settleAgentError = (rec, terminal, label, rawOutput) => {
994
+ const face = terminalProjection(terminal);
995
+ if (terminal.kind !== "completed") {
996
+ if (face.errorCode !== undefined)
997
+ rec.errorCode = face.errorCode;
998
+ if (face.errorMessage !== undefined && face.errorMessage !== "") {
999
+ rec.errorMessage = boundedRedactedSummary(face.errorMessage, MAX_TRANSCRIPT_CHARS);
981
1000
  }
982
- emitRunLog(`[${label}] ${result.status}${result.errorCode !== undefined ? ` (${result.errorCode})` : ""}${rec.errorMessage !== undefined ? `: ${rec.errorMessage.slice(0, 300)}` : ""}`);
1001
+ emitRunLog(`[${label}] ${face.status}${face.errorCode !== undefined ? ` (${face.errorCode})` : ""}${rec.errorMessage !== undefined ? `: ${rec.errorMessage.slice(0, 300)}` : ""}`);
983
1002
  }
984
- const output = rawOutput || (result.status !== "completed" ? (rec.errorMessage ?? "") : "");
1003
+ const output = rawOutput || (terminal.kind !== "completed" ? (rec.errorMessage ?? "") : "");
985
1004
  rec.output = output;
986
1005
  return output;
987
1006
  };
@@ -1128,14 +1147,14 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1128
1147
  const settleAgentResult = async (rec, result, activityTail, agentOpts, journal) => {
1129
1148
  const s = result.stats;
1130
1149
  accumulateStats(result, true);
1131
- rec.status = result.status === "completed" ? "completed" : "failed";
1132
- rec.taskStatus = result.status;
1150
+ rec.status = result.terminal.kind === "completed" ? "completed" : "failed";
1151
+ rec.taskStatus = terminalProjection(result.terminal).status;
1133
1152
  if (result.sessionId)
1134
1153
  rec.sessionId = result.sessionId;
1135
1154
  setLegRunId(rec, result);
1136
1155
  rec.endedAt = now();
1137
1156
  rec.stats = { tokens: s.tokens ?? 0, turns: s.turns ?? 0, costMicroUsd: s.costMicroUsd };
1138
- const output = settleAgentError(rec, result, rec.label, boundedRedactedSummary(result.structuredOutput ?? result.result, MAX_TRANSCRIPT_CHARS));
1157
+ const output = settleAgentError(rec, result.terminal, rec.label, boundedRedactedSummary(result.structuredOutput ?? result.result, MAX_TRANSCRIPT_CHARS));
1139
1158
  if (result.stats.toolCalls !== undefined)
1140
1159
  rec.toolCalls = result.stats.toolCalls;
1141
1160
  if (activityTail.length > 0)
@@ -1149,7 +1168,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1149
1168
  else {
1150
1169
  void journalAppend(rec.callKey, result, rec.label).catch(() => undefined);
1151
1170
  }
1152
- if (agentOpts.schema && result.status === "completed" && result.structuredOutput === undefined) {
1171
+ if (agentOpts.schema && result.terminal.kind === "completed" && result.structuredOutput === undefined) {
1153
1172
  throw new WorkflowAgentSchemaError(rec.label, result);
1154
1173
  }
1155
1174
  };
@@ -1165,7 +1184,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1165
1184
  const { label, phase, phaseInstance, groupId, inheritedModelSnap, callKey, prompt, model } = prepareAgentCall(spec, agentOpts);
1166
1185
  if (opts.resumeFromRunId !== undefined && !diverged) {
1167
1186
  const cached = replayByOrdinal[run.agents.length];
1168
- if (cached && cached.callKey === callKey && cached.result.status === "completed") {
1187
+ if (cached && cached.callKey === callKey && TERMINAL_CAUSE_IS_REPLAYABLE[cached.result.terminal.kind]) {
1169
1188
  if (finalized)
1170
1189
  return cached.result;
1171
1190
  const r = cached.result;
@@ -1181,8 +1200,8 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1181
1200
  prompt,
1182
1201
  ...(replayModel !== undefined ? { model: replayModel } : {}),
1183
1202
  replayed: true,
1184
- status: r.status === "completed" ? "completed" : "failed",
1185
- taskStatus: r.status,
1203
+ status: r.terminal.kind === "completed" ? "completed" : "failed",
1204
+ taskStatus: terminalProjection(r.terminal).status,
1186
1205
  ...(r.sessionId ? { sessionId: r.sessionId } : {}),
1187
1206
  ...(r.runId ? { taskRunId: r.runId } : {}),
1188
1207
  queuedAt: at,
@@ -1204,7 +1223,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1204
1223
  accumulateStats(r, false);
1205
1224
  void persist("update");
1206
1225
  await journalAppend(callKey, r);
1207
- if (agentOpts.schema && r.status === "completed" && r.structuredOutput === undefined) {
1226
+ if (agentOpts.schema && r.terminal.kind === "completed" && r.structuredOutput === undefined) {
1208
1227
  throw new WorkflowAgentSchemaError(label, r);
1209
1228
  }
1210
1229
  return r;
@@ -1217,9 +1236,9 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1217
1236
  : "no journal entry at this ordinal (never recorded, e.g. an oversize result under a pre-tombstone engine, or dropped)"
1218
1237
  : cached.callKey !== callKey
1219
1238
  ? "the call key changed — the script or its args differ here"
1220
- : cached.result.errorCode === JOURNAL_OVERSIZE_ERROR_CODE
1239
+ : cached.result.terminal?.kind === "failed" && cached.result.terminal.code === JOURNAL_OVERSIZE_ERROR_CODE
1221
1240
  ? "the prior result exceeded the journal size cap and was never cached"
1222
- : `the journaled result was ${cached.result.status}, not completed`);
1241
+ : `the journaled result was ${cached.result.terminal?.kind ?? "(no terminal record)"}, not completed`);
1223
1242
  diverged = true;
1224
1243
  }
1225
1244
  if (budgetTotal !== null && spent() >= budgetTotal) {
@@ -1379,7 +1398,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1379
1398
  }
1380
1399
  throw new Error("workflow aborted");
1381
1400
  }
1382
- if (stallFired && attemptResult?.status !== "completed") {
1401
+ if (stallFired && attemptResult?.terminal.kind !== "completed") {
1383
1402
  if (attemptResult !== undefined && !finalized)
1384
1403
  accumulateStats(attemptResult, true);
1385
1404
  if (attemptResult !== undefined && !finalized)
@@ -1403,7 +1422,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1403
1422
  throw attemptError;
1404
1423
  result = attemptResult;
1405
1424
  const durationMs = now() - attemptStart;
1406
- const degraded = result.status === "completed" &&
1425
+ const degraded = result.terminal.kind === "completed" &&
1407
1426
  result.structuredOutput === undefined &&
1408
1427
  (result.stats.outputTokens ?? Number.POSITIVE_INFINITY) < 50 &&
1409
1428
  lastStopReason === undefined &&
@@ -1451,14 +1470,15 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1451
1470
  if (salvaged?.runId === undefined)
1452
1471
  delete rec.taskRunId;
1453
1472
  if (salvaged !== undefined) {
1454
- rec.taskStatus = salvaged.status;
1473
+ const salvagedFace = terminalProjection(salvaged.terminal);
1474
+ rec.taskStatus = salvagedFace.status;
1455
1475
  if (salvaged.sessionId)
1456
1476
  rec.sessionId = salvaged.sessionId;
1457
1477
  setLegRunId(rec, salvaged);
1458
- if (salvaged.errorCode !== undefined)
1459
- rec.errorCode = salvaged.errorCode;
1460
- if (salvaged.errorMessage !== undefined && salvaged.errorMessage !== "")
1461
- rec.errorMessage = boundedRedactedSummary(salvaged.errorMessage, MAX_TRANSCRIPT_CHARS);
1478
+ if (salvagedFace.errorCode !== undefined)
1479
+ rec.errorCode = salvagedFace.errorCode;
1480
+ if (salvagedFace.errorMessage !== undefined && salvagedFace.errorMessage !== "")
1481
+ rec.errorMessage = boundedRedactedSummary(salvagedFace.errorMessage, MAX_TRANSCRIPT_CHARS);
1462
1482
  rec.output = boundedRedactedSummary(salvaged.structuredOutput ?? salvaged.result, MAX_TRANSCRIPT_CHARS) || (rec.errorMessage ?? "");
1463
1483
  rec.stats = { tokens: salvaged.stats.tokens ?? 0, turns: salvaged.stats.turns ?? 0, costMicroUsd: salvaged.stats.costMicroUsd };
1464
1484
  }
@@ -1479,10 +1499,12 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1479
1499
  const journaled = journalAppend(callKey, salvaged ?? {
1480
1500
  taskId: callKey,
1481
1501
  sessionId: "",
1482
- status: "failed",
1483
1502
  result: boundedRedactedSummary(err instanceof Error ? err.message : String(err), 500),
1484
- ...(err instanceof WorkflowAgentBlockedError ? { errorCode: WORKFLOW_SPAWN_BLOCKED_ERROR_CODE } : {}),
1485
- ...(err instanceof WorkflowBudgetExceededError ? { errorCode: err.code } : {}),
1503
+ terminal: {
1504
+ kind: "failed",
1505
+ ...(err instanceof WorkflowAgentBlockedError ? { code: WORKFLOW_SPAWN_BLOCKED_ERROR_CODE } : err instanceof WorkflowBudgetExceededError ? { code: err.code } : {}),
1506
+ message: boundedRedactedSummary(err instanceof Error ? err.message : String(err), 500),
1507
+ },
1486
1508
  stats: { turns: 0, tokens: 0, costMicroUsd: 0 },
1487
1509
  }, label).catch(() => undefined);
1488
1510
  if (!(err instanceof WorkflowBudgetExceededError && rec.startedAt === undefined))
@@ -1496,7 +1518,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1496
1518
  }
1497
1519
  if (finalized)
1498
1520
  return result;
1499
- if (isDurablePause(result.status)) {
1521
+ if (isDurablePause(result.terminal)) {
1500
1522
  result = mapNestedSuspend(result);
1501
1523
  }
1502
1524
  await settleAgentResult(rec, result, activityTail, agentOpts, "awaited");
@@ -1548,9 +1570,8 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1548
1570
  void journalAppend(callKey, {
1549
1571
  taskId: callKey,
1550
1572
  sessionId: "",
1551
- status: "failed",
1573
+ terminal: { kind: "failed", code: refusal.code, message: boundedRedactedSummary(refusal.message, 500) },
1552
1574
  result: boundedRedactedSummary(refusal.message, 500),
1553
- errorCode: refusal.code,
1554
1575
  stats: { turns: 0, tokens: 0, costMicroUsd: 0 },
1555
1576
  }).catch(() => undefined);
1556
1577
  throw refusal;
@@ -1607,9 +1628,8 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1607
1628
  void journalAppend(callKey, {
1608
1629
  taskId: callKey,
1609
1630
  sessionId: "",
1610
- status: "failed",
1631
+ terminal: { kind: "failed", code: WORKFLOW_SPAWN_BLOCKED_ERROR_CODE, message: boundedRedactedSummary(err.message, 500) },
1611
1632
  result: boundedRedactedSummary(err.message, 500),
1612
- errorCode: WORKFLOW_SPAWN_BLOCKED_ERROR_CODE,
1613
1633
  stats: { turns: 0, tokens: 0, costMicroUsd: 0 },
1614
1634
  }).catch(() => undefined);
1615
1635
  }
@@ -1658,7 +1678,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1658
1678
  void journalAppend(callKey, {
1659
1679
  taskId: callKey,
1660
1680
  sessionId: "",
1661
- status: "failed",
1681
+ terminal: { kind: "failed", message: boundedRedactedSummary(err instanceof Error ? err.message : String(err), 500) },
1662
1682
  result: boundedRedactedSummary(err instanceof Error ? err.message : String(err), 500),
1663
1683
  stats: { turns: partialSpend?.turns ?? 0, tokens: partialSpend?.tokens ?? 0, costMicroUsd: partialSpend?.costMicroUsd ?? 0 },
1664
1684
  }).catch(() => undefined);
@@ -1670,7 +1690,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1670
1690
  releaseOnce();
1671
1691
  if (finalized)
1672
1692
  return result;
1673
- if (isDurablePause(result.status)) {
1693
+ if (isDurablePause(result.terminal)) {
1674
1694
  result = mapNestedSuspend(result);
1675
1695
  }
1676
1696
  await settleAgentResult(rec, result, activityTail, agentOpts, "best-effort");
@@ -1828,8 +1848,13 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1828
1848
  "state (or stop it) and resume again.");
1829
1849
  }
1830
1850
  const entries = await journalStore.load(opts.resumeFromRunId, scope);
1831
- for (const e of entries)
1851
+ for (const e of entries) {
1852
+ const t = e.result?.terminal;
1853
+ if (t === null || typeof t !== "object" || !isTerminalCauseKind(t.kind)) {
1854
+ throw new WorkflowJournalIncompatibleError(clampRunIdText(opts.resumeFromRunId), callKeyOrdinal(e.callKey));
1855
+ }
1832
1856
  replayByOrdinal[callKeyOrdinal(e.callKey)] = e;
1857
+ }
1833
1858
  run.resume = {
1834
1859
  fromRunId: clampRunIdText(opts.resumeFromRunId),
1835
1860
  journalEntries: entries.length,
@@ -49,7 +49,7 @@ export declare const SUPERVISOR_PROMPT = "You are a supervisor \u2014 the delega
49
49
  * This block is strategy-blind (it is composed from the prompt context, which carries no strategy
50
50
  * fact), so it must not promise the arm that only one strategy delivers. */
51
51
  export declare const ORCHESTRATION_GUIDANCE_DEFERRED = "You can author and run your own WORKFLOW via the Workflow tool (multi-agent orchestration). Its schema and how-to are deferred: when a task genuinely needs orchestration, activate the tool (see the deferred-tools note) and work from what the activation returns.";
52
- export declare const ORCHESTRATION_GUIDANCE = "You can author and run your own WORKFLOW via the Workflow tool \u2014 a\ndeterministic JS script that spawns and coordinates sub-agents. Use it to be more thorough (decompose and\ncover in parallel), more confident (independent perspectives + adversarial checks before committing), or to\nhandle scale one context can't hold. This is a power tool: reach for it on a SUBSTANTIAL task that genuinely\ndecomposes \u2014 for a simple or sequential task, just do the work directly. Over-orchestrating a trivial task\nwastes tokens and adds latency.\n\nHow a workflow script works (the contract):\n- It begins with `export const meta = { name, description, phases }` \u2014 a PURE LITERAL (no variables, calls,\n or template strings). Use the same phase titles in meta.phases as in your phase() calls and in each\n agent's opts `phase`.\n- \uD83D\uDD34 After the meta line, write the body as TOP-LEVEL async statements \u2014 the primitives are already in\n scope. Do NOT wrap the body in `export default`, a function, or a `body()` method; do NOT use\n `import`/`require`; do NOT put the script inside markdown code fences. End with `return <value>`.\n The script IS the function body. A complete example \u2014 copy this SHAPE exactly:\n\n export const meta = { name: 'risk-scan', description: 'list risks in parallel', phases: [{ title: 'scan' }] }\n const results = await parallel([\n () => agent({ objective: 'Name one risk of X. Reply in one short sentence.' }, { label: 'scan-risk-a', phase: 'scan' }),\n () => agent({ objective: 'Name a DIFFERENT risk of X. Reply in one short sentence.' }, { label: 'scan-risk-b', phase: 'scan' }),\n ])\n return results.filter((r) => r && r.status === 'completed').map((r) => r.result)\n\n- The body is async and uses these injected primitives:\n - agent(spec, opts?) \u2014 run one sub-agent. spec is { objective: string (USE `objective`, not `goal`),\n modelName?, thinking?, systemPrompt? }; opts is { schema?, label?, phase?, isolation? } (schema goes in\n OPTS, not in spec). ALWAYS pass a short kebab-case `label` naming what THIS agent does (e.g.\n { label: 'find-dead-code' }) \u2014 label/phase go in OPTS, never inside spec (a spec-side label is ignored);\n unlabeled agents render as anonymous agent-N rows in the monitor. Set opts `phase` to one of your\n meta.phases titles so the agent groups under its stage.\n `isolation: \"worktree\"` runs the agent in its own isolated git worktree \u2014 use it ONLY\n when concurrent agents WRITE THE SAME repo/files and must not clobber each other (a separate working copy,\n not merely several agents). Returns the task result \u2014 read `r.result` (text) or `r.structuredOutput`\n (when you passed {schema}). agent() does NOT throw when the sub-agent fails \u2014 it RETURNS the result\n with `r.status` set; ALWAYS check `r.status` and GATE later phases on it (the Workflow tool card\n shows the full gate pattern).\n - parallel(thunks) \u2014 run thunks concurrently; BARRIER (awaits all); a thrown thunk resolves to null\n (filter before use). Use when you need all results together.\n - pipeline(items, ...stages) \u2014 each item flows through all stages independently, NO barrier between stages\n (item A can be in stage 3 while B is in stage 1). DEFAULT for multi-stage work. Each stage gets\n (prevResult, originalItem, index). A stage that throws drops that item to null.\n - phase(title, body) \u2014 group work under a named phase (shows in /workflows).\n - budget \u2014 { total, spent(), remaining() }; once spend reaches total, agent() throws. Loop on\n budget.remaining() for budget-scaled depth \u2014 but GUARD the loop on budget.total: with no budget set,\n remaining() returns Infinity and the loop runs straight into the agent cap (add a hard iteration cap).\n spent() moves when an agent SETTLES (authoritative accounting); the live per-turn figures you may see\n in run observability are display-only and never charge the budget gate.\n - log(message) \u2014 emit a progress line.\n - args \u2014 the JSON value passed to Workflow.\n- The script returns a value; you are notified when it completes and can read the result + the run via the\n workflow observability.\n\nDiscipline (this is where orchestration earns its cost):\n- DEFAULT TO pipeline(). Only use parallel() (a barrier) when a stage genuinely needs ALL prior results at\n once (dedup/merge across the full set, early-exit on zero, cross-item comparison). Otherwise pipeline so a\n fast item isn't blocked by a slow one.\n- Give each sub-agent a CLEAR goal + output spec + boundary, so they don't duplicate or conflict. A vague\n delegation produces duplicated or off-scope work. Detailed sub-task instructions matter.\n- Be confident, not just fast: for findings that must be right, spawn INDEPENDENT verifiers prompted to\n REFUTE (default to refuted if uncertain) and keep a finding only if it survives. Diverse lenses\n (correctness / security / does-it-reproduce) catch failure modes redundancy can't. When workers fan out, a\n single bad conclusion gets amplified \u2014 verify before you commit to it.\n- Scale to the task: a quick check needs a couple of agents; \"be comprehensive / audit thoroughly\" warrants a\n larger finder pool + an adversarial verify pass. Don't fan out wider than the task needs.\n\nYou operate under hard caps (a runaway script is bounded, not trusted): a token budget, a concurrency limit,\nper-agent and total timeouts, a max agent count, and a nesting limit of ONE level (a workflow's agent cannot\nitself start another workflow). Every sub-agent you spawn runs under the deployment's permission/approval/\nsafety policy \u2014 you may inherit or TIGHTEN it for a sub-agent, never loosen it. Work within these; they are\nthe safety net that lets you be trusted with this power.";
52
+ export declare const ORCHESTRATION_GUIDANCE = "You can author and run your own WORKFLOW via the Workflow tool \u2014 a\ndeterministic JS script that spawns and coordinates sub-agents. Use it to be more thorough (decompose and\ncover in parallel), more confident (independent perspectives + adversarial checks before committing), or to\nhandle scale one context can't hold. This is a power tool: reach for it on a SUBSTANTIAL task that genuinely\ndecomposes \u2014 for a simple or sequential task, just do the work directly. Over-orchestrating a trivial task\nwastes tokens and adds latency.\n\nHow a workflow script works (the contract):\n- It begins with `export const meta = { name, description, phases }` \u2014 a PURE LITERAL (no variables, calls,\n or template strings). Use the same phase titles in meta.phases as in your phase() calls and in each\n agent's opts `phase`.\n- \uD83D\uDD34 After the meta line, write the body as TOP-LEVEL async statements \u2014 the primitives are already in\n scope. Do NOT wrap the body in `export default`, a function, or a `body()` method; do NOT use\n `import`/`require`; do NOT put the script inside markdown code fences. End with `return <value>`.\n The script IS the function body. A complete example \u2014 copy this SHAPE exactly:\n\n export const meta = { name: 'risk-scan', description: 'list risks in parallel', phases: [{ title: 'scan' }] }\n const results = await parallel([\n () => agent({ objective: 'Name one risk of X. Reply in one short sentence.' }, { label: 'scan-risk-a', phase: 'scan' }),\n () => agent({ objective: 'Name a DIFFERENT risk of X. Reply in one short sentence.' }, { label: 'scan-risk-b', phase: 'scan' }),\n ])\n return results.filter((r) => r && r.terminal.kind === 'completed').map((r) => r.result)\n\n- The body is async and uses these injected primitives:\n - agent(spec, opts?) \u2014 run one sub-agent. spec is { objective: string (USE `objective`, not `goal`),\n modelName?, thinking?, systemPrompt? }; opts is { schema?, label?, phase?, isolation? } (schema goes in\n OPTS, not in spec). ALWAYS pass a short kebab-case `label` naming what THIS agent does (e.g.\n { label: 'find-dead-code' }) \u2014 label/phase go in OPTS, never inside spec (a spec-side label is ignored);\n unlabeled agents render as anonymous agent-N rows in the monitor. Set opts `phase` to one of your\n meta.phases titles so the agent groups under its stage.\n `isolation: \"worktree\"` runs the agent in its own isolated git worktree \u2014 use it ONLY\n when concurrent agents WRITE THE SAME repo/files and must not clobber each other (a separate working copy,\n not merely several agents). Returns the task result \u2014 read `r.result` (text) or `r.structuredOutput`\n (when you passed {schema}). agent() does NOT throw when the sub-agent fails \u2014 it RETURNS the result\n with `r.terminal` set (`r.terminal.kind` is 'completed' | 'failed' | 'blocked' | 'paused'); ALWAYS check\n `r.terminal.kind` and GATE later phases on it (the Workflow tool card shows the full gate pattern).\n - parallel(thunks) \u2014 run thunks concurrently; BARRIER (awaits all); a thrown thunk resolves to null\n (filter before use). Use when you need all results together.\n - pipeline(items, ...stages) \u2014 each item flows through all stages independently, NO barrier between stages\n (item A can be in stage 3 while B is in stage 1). DEFAULT for multi-stage work. Each stage gets\n (prevResult, originalItem, index). A stage that throws drops that item to null.\n - phase(title, body) \u2014 group work under a named phase (shows in /workflows).\n - budget \u2014 { total, spent(), remaining() }; once spend reaches total, agent() throws. Loop on\n budget.remaining() for budget-scaled depth \u2014 but GUARD the loop on budget.total: with no budget set,\n remaining() returns Infinity and the loop runs straight into the agent cap (add a hard iteration cap).\n spent() moves when an agent SETTLES (authoritative accounting); the live per-turn figures you may see\n in run observability are display-only and never charge the budget gate.\n - log(message) \u2014 emit a progress line.\n - args \u2014 the JSON value passed to Workflow.\n- The script returns a value; you are notified when it completes and can read the result + the run via the\n workflow observability.\n\nDiscipline (this is where orchestration earns its cost):\n- DEFAULT TO pipeline(). Only use parallel() (a barrier) when a stage genuinely needs ALL prior results at\n once (dedup/merge across the full set, early-exit on zero, cross-item comparison). Otherwise pipeline so a\n fast item isn't blocked by a slow one.\n- Give each sub-agent a CLEAR goal + output spec + boundary, so they don't duplicate or conflict. A vague\n delegation produces duplicated or off-scope work. Detailed sub-task instructions matter.\n- Be confident, not just fast: for findings that must be right, spawn INDEPENDENT verifiers prompted to\n REFUTE (default to refuted if uncertain) and keep a finding only if it survives. Diverse lenses\n (correctness / security / does-it-reproduce) catch failure modes redundancy can't. When workers fan out, a\n single bad conclusion gets amplified \u2014 verify before you commit to it.\n- Scale to the task: a quick check needs a couple of agents; \"be comprehensive / audit thoroughly\" warrants a\n larger finder pool + an adversarial verify pass. Don't fan out wider than the task needs.\n\nYou operate under hard caps (a runaway script is bounded, not trusted): a token budget, a concurrency limit,\nper-agent and total timeouts, a max agent count, and a nesting limit of ONE level (a workflow's agent cannot\nitself start another workflow). Every sub-agent you spawn runs under the deployment's permission/approval/\nsafety policy \u2014 you may inherit or TIGHTEN it for a sub-agent, never loosen it. Work within these; they are\nthe safety net that lets you be trusted with this power.";
53
53
  /**
54
54
  * Goal-mode completion guidance, composed when goal mode is enabled (design/96 C 节 LLM-self-report gate).
55
55
  * Red line (G1, design/96 §定.0): declaring "done" STOPS iteration and surfaces for review (the completion
@@ -53,7 +53,7 @@ How a workflow script works (the contract):
53
53
  () => agent({ objective: 'Name one risk of X. Reply in one short sentence.' }, { label: 'scan-risk-a', phase: 'scan' }),
54
54
  () => agent({ objective: 'Name a DIFFERENT risk of X. Reply in one short sentence.' }, { label: 'scan-risk-b', phase: 'scan' }),
55
55
  ])
56
- return results.filter((r) => r && r.status === 'completed').map((r) => r.result)
56
+ return results.filter((r) => r && r.terminal.kind === 'completed').map((r) => r.result)
57
57
 
58
58
  - The body is async and uses these injected primitives:
59
59
  - agent(spec, opts?) — run one sub-agent. spec is { objective: string (USE \`objective\`, not \`goal\`),
@@ -66,8 +66,8 @@ How a workflow script works (the contract):
66
66
  when concurrent agents WRITE THE SAME repo/files and must not clobber each other (a separate working copy,
67
67
  not merely several agents). Returns the task result — read \`r.result\` (text) or \`r.structuredOutput\`
68
68
  (when you passed {schema}). agent() does NOT throw when the sub-agent fails — it RETURNS the result
69
- with \`r.status\` set; ALWAYS check \`r.status\` and GATE later phases on it (the ${RUN_WORKFLOW_TOOL_NAME} tool card
70
- shows the full gate pattern).
69
+ with \`r.terminal\` set (\`r.terminal.kind\` is 'completed' | 'failed' | 'blocked' | 'paused'); ALWAYS check
70
+ \`r.terminal.kind\` and GATE later phases on it (the ${RUN_WORKFLOW_TOOL_NAME} tool card shows the full gate pattern).
71
71
  - parallel(thunks) — run thunks concurrently; BARRIER (awaits all); a thrown thunk resolves to null
72
72
  (filter before use). Use when you need all results together.
73
73
  - pipeline(items, ...stages) — each item flows through all stages independently, NO barrier between stages
@@ -125,7 +125,7 @@ async function runCodeReviewVerify(opts) {
125
125
  const reviewed = {
126
126
  taskId: "code-review",
127
127
  sessionId: "code-review",
128
- status: "completed",
128
+ terminal: { kind: "completed" },
129
129
  result: opts.objective,
130
130
  stats: { turns: 0, tokens: 0 },
131
131
  };