@sema-agent/core 2.2.0 → 2.4.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 (70) hide show
  1. package/dist/agents/send-message-tool.d.ts +4 -0
  2. package/dist/agents/send-message-tool.js +37 -24
  3. package/dist/agents/subagent.js +275 -127
  4. package/dist/agents/teacher.js +51 -23
  5. package/dist/brain/errors.d.ts +1 -0
  6. package/dist/brain/errors.js +14 -0
  7. package/dist/brain/stream-engine.js +3 -3
  8. package/dist/core/context-edit.js +2 -1
  9. package/dist/core/runner/assemble-result.d.ts +1 -0
  10. package/dist/core/runner/assemble-result.js +12 -8
  11. package/dist/core/runner/prepare-task.js +32 -7
  12. package/dist/core/runner/runtask.js +18 -5
  13. package/dist/core/runner/tool-output-projection.js +2 -1
  14. package/dist/core/store-contracts/checkpoint-store-contract.d.ts +37 -0
  15. package/dist/core/store-contracts/checkpoint-store-contract.js +195 -0
  16. package/dist/core/store-contracts/contract-harness.d.ts +6 -0
  17. package/dist/core/store-contracts/contract-harness.js +16 -0
  18. package/dist/core/store-contracts/contract-kit-version.d.ts +1 -0
  19. package/dist/core/store-contracts/contract-kit-version.js +2 -0
  20. package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +3 -0
  21. package/dist/core/store-contracts/file-snapshot-store-contract.js +126 -0
  22. package/dist/core/store-contracts/mailbox-store-contract.d.ts +6 -0
  23. package/dist/core/store-contracts/mailbox-store-contract.js +193 -0
  24. package/dist/core/store-contracts/session-repo-contract.d.ts +3 -0
  25. package/dist/core/store-contracts/session-repo-contract.js +36 -0
  26. package/dist/core/store-contracts/tool-result-store-contract.d.ts +3 -0
  27. package/dist/core/store-contracts/tool-result-store-contract.js +35 -0
  28. package/dist/core/task-notification.d.ts +2 -0
  29. package/dist/core/task-registry-agent.d.ts +7 -1
  30. package/dist/core/task-registry-agent.js +31 -2
  31. package/dist/core/task-registry-monitor.js +71 -6
  32. package/dist/core/task-registry-shared.d.ts +18 -1
  33. package/dist/core/task-registry-shared.js +5 -2
  34. package/dist/core/task-registry.d.ts +9 -0
  35. package/dist/core/task-registry.js +52 -7
  36. package/dist/core/tool-result-store.d.ts +3 -2
  37. package/dist/core/tool-result-store.js +12 -4
  38. package/dist/core/tools.d.ts +2 -0
  39. package/dist/core/tools.js +9 -0
  40. package/dist/core/trace.d.ts +7 -0
  41. package/dist/core/workflow-journal-store.d.ts +16 -0
  42. package/dist/core/workflow-journal-store.js +28 -0
  43. package/dist/engine/lsp/node-lsp-manager.d.ts +2 -0
  44. package/dist/engine/lsp/node-lsp-manager.js +16 -0
  45. package/dist/index.d.ts +1 -0
  46. package/dist/index.js +1 -0
  47. package/dist/orchestration/builtin-workflows.d.ts +1 -1
  48. package/dist/orchestration/builtin-workflows.js +11 -2
  49. package/dist/orchestration/workflow-governance.d.ts +6 -1
  50. package/dist/orchestration/workflow-governance.js +24 -4
  51. package/dist/orchestration/workflow-primitives.js +7 -1
  52. package/dist/orchestration/workflow.d.ts +16 -0
  53. package/dist/orchestration/workflow.js +94 -21
  54. package/dist/tools/fs/fs-bash.d.ts +7 -1
  55. package/dist/tools/fs/fs-bash.js +51 -20
  56. package/dist/tools/fs/fs-read.js +22 -11
  57. package/dist/tools/fs/fs-search-tools.js +3 -3
  58. package/dist/tools/fs/fs-shared.d.ts +20 -7
  59. package/dist/tools/fs/fs-shared.js +17 -3
  60. package/dist/tools/fs/fs-write.js +4 -4
  61. package/dist/tools/fs/index.d.ts +2 -0
  62. package/dist/tools/fs/index.js +7 -1
  63. package/dist/tools/fs/repo-map.js +2 -2
  64. package/dist/tools/fs/safety.d.ts +10 -0
  65. package/dist/tools/fs/safety.js +15 -1
  66. package/dist/tools/monitor.d.ts +2 -0
  67. package/dist/tools/monitor.js +20 -4
  68. package/dist/tools/web.js +6 -2
  69. package/dist/tools/worktree.js +46 -25
  70. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  import type { NamedWorkflowListing } from "./workflow-script-store.js";
2
2
  export declare const TEAM_DISCUSSION_WORKFLOW_NAME = "team-discussion";
3
- export declare const TEAM_DISCUSSION_SCRIPT = "export const meta = {\n name: \"team-discussion\",\n description: \"Round-based team 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? } }.\",\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 team 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 rounds = Math.min(Number.isFinite(requestedRounds) && requestedRounds >= 1 ? requestedRounds : 2, 5);\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 \"Team 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 ...(truncated ? { truncated } : {}),\n transcript,\n verdict: verdict && verdict.structuredOutput !== undefined ? verdict.structuredOutput : (verdict ? verdict.result : null),\n};\n";
3
+ export declare const TEAM_DISCUSSION_SCRIPT = "export const meta = {\n name: \"team-discussion\",\n description: \"Round-based team 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 team 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(\"team-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 \"Team 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";
4
4
  export interface BuiltinWorkflowDefinition {
5
5
  name: string;
6
6
  script: string;
@@ -3,7 +3,7 @@ export const TEAM_DISCUSSION_WORKFLOW_NAME = "team-discussion";
3
3
  export const TEAM_DISCUSSION_SCRIPT = `export const meta = {
4
4
  name: "team-discussion",
5
5
  description: "Round-based team discussion: configurable members debate a topic across rounds (each member sees the transcript so far), then a finalizer synthesizes a structured verdict.",
6
- 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? } }.",
6
+ 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.",
7
7
  phases: [
8
8
  { title: "Discussion" },
9
9
  { title: "Synthesis" },
@@ -38,7 +38,15 @@ const members = rawMembers.slice(0, 6).map((m, i) => {
38
38
  // Deterministic budget truncation (design/140 §1 预算 row): a HARD rounds ceiling + member cap — never an
39
39
  // evaluator agent. The engine's budget/maxAgents hard stops remain the backstop.
40
40
  const requestedRounds = Math.floor(Number(a.rounds));
41
- const rounds = Math.min(Number.isFinite(requestedRounds) && requestedRounds >= 1 ? requestedRounds : 2, 5);
41
+ const normalizedRounds = Number.isFinite(requestedRounds) && requestedRounds >= 1 ? requestedRounds : 2;
42
+ const rounds = Math.min(normalizedRounds, 5);
43
+ // RB-380 disclosure: the member/round slices above are silent by construction (Array.prototype.slice /
44
+ // Math.min just drop the excess) — record + surface it instead of a caller finding out only by counting
45
+ // transcript entries. Fires only when a request actually exceeded a ceiling (never on the common path).
46
+ const capNotes = [];
47
+ if (rawMembers.length > 6) capNotes.push("requested " + rawMembers.length + " members, capped at 6");
48
+ if (normalizedRounds > 5) capNotes.push("requested " + normalizedRounds + " rounds, capped at 5");
49
+ for (const note of capNotes) log("team-discussion: " + note);
42
50
  const fin = a.finalizer !== null && typeof a.finalizer === "object" && !Array.isArray(a.finalizer) ? a.finalizer : {};
43
51
  const finalizerPrompt = typeof fin.prompt === "string" && fin.prompt.trim() !== ""
44
52
  ? fin.prompt
@@ -110,6 +118,7 @@ return {
110
118
  topic,
111
119
  rounds,
112
120
  members: members.map((m) => m.role),
121
+ ...(capNotes.length > 0 ? { capped: capNotes } : {}),
113
122
  ...(truncated ? { truncated } : {}),
114
123
  transcript,
115
124
  verdict: verdict && verdict.structuredOutput !== undefined ? verdict.structuredOutput : (verdict ? verdict.result : null),
@@ -25,5 +25,10 @@ export interface WorkflowChildCaps {
25
25
  childMaxTokens?: number;
26
26
  childMaxTurns?: number;
27
27
  }
28
+ export interface ResourceClampNote {
29
+ field: "maxCostUsd" | "maxTokens" | "timeoutSec" | "maxTurns";
30
+ requested: number | undefined;
31
+ applied: number;
32
+ }
28
33
  export declare function resolveModelName(name: string, allowlist: string[] | undefined, models: Record<string, Model> | undefined): Model;
29
- export declare function buildGovernedChildSpec(scriptSpec: unknown, baseline: WorkflowGovernanceBaseline, models: Record<string, Model> | undefined, caps?: WorkflowChildCaps): TaskSpec;
34
+ export declare function buildGovernedChildSpec(scriptSpec: unknown, baseline: WorkflowGovernanceBaseline, models: Record<string, Model> | undefined, caps?: WorkflowChildCaps, onResourceClamp?: (notes: ResourceClampNote[]) => void): TaskSpec;
@@ -99,26 +99,46 @@ function pickWhitelist(scriptSpec) {
99
99
  return { safe, modelName };
100
100
  }
101
101
  function clampResourceLimits(safe, base, caps) {
102
+ const notes = [];
103
+ const requestedCost = safe.maxCostUsd;
102
104
  const cost = minDefined(safe.maxCostUsd, base.maxCostUsd, caps?.childMaxCostUsd);
103
- if (cost !== undefined)
105
+ if (cost !== undefined) {
106
+ if (cost !== requestedCost)
107
+ notes.push({ field: "maxCostUsd", requested: requestedCost, applied: cost });
104
108
  safe.maxCostUsd = cost;
109
+ }
110
+ const requestedTokens = safe.maxTokens;
105
111
  const tokens = minDefined(safe.maxTokens, base.maxTokens, caps?.childMaxTokens);
106
- if (tokens !== undefined)
112
+ if (tokens !== undefined) {
113
+ if (tokens !== requestedTokens)
114
+ notes.push({ field: "maxTokens", requested: requestedTokens, applied: tokens });
107
115
  safe.maxTokens = tokens;
116
+ }
117
+ const requestedTimeoutSec = safe.limits?.timeoutSec;
118
+ const requestedMaxTurns = safe.limits?.maxTurns;
108
119
  const timeoutSec = minDefined(safe.limits?.timeoutSec, base.limits?.timeoutSec, caps?.perAgentTimeoutSec);
109
120
  const maxTurns = minDefined(safe.limits?.maxTurns, base.limits?.maxTurns, caps?.childMaxTurns);
121
+ if (timeoutSec !== undefined && timeoutSec !== requestedTimeoutSec) {
122
+ notes.push({ field: "timeoutSec", requested: requestedTimeoutSec, applied: timeoutSec });
123
+ }
124
+ if (maxTurns !== undefined && maxTurns !== requestedMaxTurns) {
125
+ notes.push({ field: "maxTurns", requested: requestedMaxTurns, applied: maxTurns });
126
+ }
110
127
  if (timeoutSec !== undefined || maxTurns !== undefined) {
111
128
  safe.limits = {
112
129
  ...(maxTurns !== undefined ? { maxTurns } : {}),
113
130
  ...(timeoutSec !== undefined ? { timeoutSec } : {}),
114
131
  };
115
132
  }
133
+ return notes;
116
134
  }
117
- export function buildGovernedChildSpec(scriptSpec, baseline, models, caps) {
135
+ export function buildGovernedChildSpec(scriptSpec, baseline, models, caps, onResourceClamp) {
118
136
  const { safe, modelName } = pickWhitelist(scriptSpec);
119
137
  if (modelName !== undefined) {
120
138
  safe.model = resolveModelName(modelName, baseline.workflowModelAllowlist, models);
121
139
  }
122
- clampResourceLimits(safe, baseline.base, caps);
140
+ const clampNotes = clampResourceLimits(safe, baseline.base, caps);
141
+ if (clampNotes.length > 0 && onResourceClamp)
142
+ onResourceClamp(clampNotes);
123
143
  return tightenTaskSpec(baseline.base, safe);
124
144
  }
@@ -1,3 +1,4 @@
1
+ import { assertSupportedAgentIsolation } from "./workflow.js";
1
2
  import { buildGovernedChildSpec } from "./workflow-governance.js";
2
3
  function safeAgentOptions(opts) {
3
4
  if (typeof opts !== "object" || opts === null)
@@ -12,10 +13,15 @@ function safeAgentOptions(opts) {
12
13
  out.schema = o.schema;
13
14
  if (typeof o.agentType === "string")
14
15
  out.agentType = o.agentType;
16
+ assertSupportedAgentIsolation(o.isolation);
15
17
  if (o.isolation === "worktree")
16
18
  out.isolation = "worktree";
17
19
  return out;
18
20
  }
21
+ function formatResourceClampNote(notes) {
22
+ const parts = notes.map((n) => `${n.field}: requested ${n.requested === undefined ? "unset" : n.requested} → applied ${n.applied}`);
23
+ return `workflow governance tightened this agent's resource limits (${parts.join("; ")})`;
24
+ }
19
25
  export function buildWorkflowPrimitives(ctx, governance, onAgentSpawn, parentThinking, parentPrincipal) {
20
26
  const agent = (spec, opts) => {
21
27
  if (typeof spec === "string")
@@ -23,7 +29,7 @@ export function buildWorkflowPrimitives(ctx, governance, onAgentSpawn, parentThi
23
29
  const agentOpts = safeAgentOptions(opts);
24
30
  const effectiveBaseline = (b) => agentOpts.isolation === "worktree" && b.worktreeBase !== undefined ? { ...b, base: { ...b.base, ...b.worktreeBase } } : b;
25
31
  const childSpec = governance
26
- ? buildGovernedChildSpec(spec, effectiveBaseline(governance.baseline), governance.models, governance.caps)
32
+ ? buildGovernedChildSpec(spec, effectiveBaseline(governance.baseline), governance.models, governance.caps, (notes) => ctx.log(formatResourceClampNote(notes)))
27
33
  : { ...spec };
28
34
  if (childSpec.thinking === undefined && parentThinking) {
29
35
  const inherited = parentThinking();
@@ -18,6 +18,7 @@ export declare function workflowAgentCallKey(ordinal: number, spec: TaskSpec, op
18
18
  schema?: TSchema;
19
19
  isolation?: "worktree";
20
20
  }): string;
21
+ export declare function assertSupportedAgentIsolation(isolation: unknown): asserts isolation is "worktree" | undefined;
21
22
  export interface WorkflowFanOutSlotError {
22
23
  index: number;
23
24
  kind: string;
@@ -129,5 +130,20 @@ export interface WorkflowTimers {
129
130
  setTimeout(fn: () => void, ms: number): unknown;
130
131
  clearTimeout(handle: unknown): void;
131
132
  }
133
+ export declare const workflowResumeClaimFallback: {
134
+ acquire(store: WorkflowJournalStore, input: {
135
+ sourceRunId: string;
136
+ newRunId: string;
137
+ scope: string;
138
+ }): {
139
+ granted: boolean;
140
+ holder?: string;
141
+ };
142
+ release(store: WorkflowJournalStore, input: {
143
+ sourceRunId: string;
144
+ newRunId: string;
145
+ scope: string;
146
+ }): void;
147
+ };
132
148
  export declare function startWorkflow<T>(runner: Runner, fn: (ctx: WorkflowRunContext) => Promise<T>, opts?: RunWorkflowOptions, internals?: WorkflowInternals): WorkflowHandle<T>;
133
149
  export declare function runWorkflow<T>(runner: Runner, fn: (ctx: WorkflowRunContext) => Promise<T>, opts?: RunWorkflowOptions, internals?: WorkflowInternals): Promise<RunWorkflowResult<T>>;
@@ -20,6 +20,7 @@ const MAX_TRANSCRIPT_CHARS = 4000;
20
20
  const WORKFLOW_RESULT_MAX = 4000;
21
21
  const WORKFLOW_RESULT_FULL_MAX = 200_000;
22
22
  const MAX_ACTIVITY = 30;
23
+ const RUNNING_AGENT_PERSIST_EVERY_BEATS = 4;
23
24
  function workflowModelLabel(spec) {
24
25
  const model = spec.model;
25
26
  if (model === undefined)
@@ -103,6 +104,21 @@ export function workflowAgentCallKey(ordinal, spec, opts) {
103
104
  };
104
105
  return `${ordinal}:${boundInputHashOf(identity)}`;
105
106
  }
107
+ function resolveChildSessionIdAtSpawn(spec) {
108
+ if (spec.sessionId)
109
+ return spec.sessionId;
110
+ if (spec.requireExistingSession === true || spec.resumeAt !== undefined)
111
+ return undefined;
112
+ return randomUUID();
113
+ }
114
+ export function assertSupportedAgentIsolation(isolation) {
115
+ if (isolation !== undefined && isolation !== "worktree") {
116
+ const shown = typeof isolation === "string" ? JSON.stringify(isolation) : String(isolation);
117
+ const e = new Error(`isolation ${shown} is not supported in workflow agents — only "worktree" (omit the option to run in the shared working tree). The agent was not started (fail-closed: an unrecognized isolation value must never silently run in the shared working tree).`);
118
+ e.code = "isolation.invalid";
119
+ throw e;
120
+ }
121
+ }
106
122
  function rethrowIfMaxAgents(e) {
107
123
  if (e instanceof WorkflowMaxAgentsError)
108
124
  throw e;
@@ -206,6 +222,33 @@ function createSemaphore(max) {
206
222
  function clampRunIdText(raw) {
207
223
  return raw.replace(/[^A-Za-z0-9_.:-]/g, "").slice(0, 64);
208
224
  }
225
+ const fallbackResumeClaimTables = new WeakMap();
226
+ function fallbackResumeClaimKey(scope, sourceRunId) {
227
+ return JSON.stringify([scope, sourceRunId]);
228
+ }
229
+ export const workflowResumeClaimFallback = {
230
+ acquire(store, input) {
231
+ let table = fallbackResumeClaimTables.get(store);
232
+ if (table === undefined) {
233
+ table = new Map();
234
+ fallbackResumeClaimTables.set(store, table);
235
+ }
236
+ const key = fallbackResumeClaimKey(input.scope, input.sourceRunId);
237
+ const holder = table.get(key);
238
+ if (holder !== undefined && holder !== input.newRunId)
239
+ return { granted: false, holder };
240
+ table.set(key, input.newRunId);
241
+ return { granted: true };
242
+ },
243
+ release(store, input) {
244
+ const table = fallbackResumeClaimTables.get(store);
245
+ if (table === undefined)
246
+ return;
247
+ const key = fallbackResumeClaimKey(input.scope, input.sourceRunId);
248
+ if (table.get(key) === input.newRunId)
249
+ table.delete(key);
250
+ },
251
+ };
209
252
  export function startWorkflow(runner, fn, opts = {}, internals) {
210
253
  const alsDepth = currentWorkflowDepth();
211
254
  const internalDepth = internals?.workflowDepth;
@@ -566,8 +609,9 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
566
609
  throw err;
567
610
  }
568
611
  };
569
- const createActivityCapture = (callKey, label, groupId) => {
612
+ const createActivityCapture = (callKey, label, groupId, rec) => {
570
613
  const tail = [];
614
+ let beatCount = 0;
571
615
  const onActivity = (a) => {
572
616
  if (finalized)
573
617
  return;
@@ -587,6 +631,13 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
587
631
  tail.push(a);
588
632
  if (tail.length > MAX_ACTIVITY)
589
633
  tail.shift();
634
+ if (a.phase === "start")
635
+ rec.toolCalls = (rec.toolCalls ?? 0) + 1;
636
+ rec.activity = tail;
637
+ beatCount += 1;
638
+ if (beatCount === 1 || beatCount % RUNNING_AGENT_PERSIST_EVERY_BEATS === 0) {
639
+ void persist("update");
640
+ }
590
641
  };
591
642
  return { tail, onActivity };
592
643
  };
@@ -664,6 +715,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
664
715
  if ("error" in compiled)
665
716
  throw new TypeError(`agent({schema}) received an invalid JSON Schema: ${compiled.error}`);
666
717
  }
718
+ assertSupportedAgentIsolation(agentOpts.isolation);
667
719
  const label = agentOpts.label ?? `agent-${run.agents.length + 1}`;
668
720
  const phase = agentOpts.phase ?? currentPhase?.title;
669
721
  const phaseInstance = resolveAgentPhase(agentOpts.phase);
@@ -734,8 +786,8 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
734
786
  if (budgetTotal !== null && spent() >= budgetTotal) {
735
787
  throw new WorkflowBudgetExceededError(spent(), budgetTotal);
736
788
  }
737
- const { tail: activityTail, onActivity } = createActivityCapture(callKey, label, groupId);
738
789
  const rec = { label, callKey, ...(groupId !== undefined ? { groupId } : {}), phase, prompt, ...(model !== undefined ? { model } : {}), status: "running", queuedAt: now() };
790
+ const { tail: activityTail, onActivity } = createActivityCapture(callKey, label, groupId, rec);
739
791
  run.agents.push(rec);
740
792
  if (phaseInstance)
741
793
  agentPhaseOf.set(rec, phaseInstance);
@@ -792,7 +844,12 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
792
844
  const onCallerAbort = () => attemptCtl.abort(new Error("workflow aborted"));
793
845
  effectiveSignal?.addEventListener("abort", onCallerAbort, { once: true });
794
846
  armWatchdog();
795
- const attemptSpec = { ...runSpec, signal: attemptCtl.signal };
847
+ const attemptSessionId = resolveChildSessionIdAtSpawn(runSpec);
848
+ const attemptSpec = attemptSessionId !== undefined ? { ...runSpec, sessionId: attemptSessionId, signal: attemptCtl.signal } : { ...runSpec, signal: attemptCtl.signal };
849
+ if (attemptSessionId !== undefined && !finalized) {
850
+ rec.sessionId = attemptSessionId;
851
+ void persist("update");
852
+ }
796
853
  const attemptInternals = {
797
854
  ...baseInternals,
798
855
  ...(bceSink !== undefined
@@ -1035,6 +1092,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1035
1092
  if ("error" in compiled)
1036
1093
  throw new TypeError(`agent({schema}) received an invalid JSON Schema: ${compiled.error}`);
1037
1094
  }
1095
+ assertSupportedAgentIsolation(agentOpts.isolation);
1038
1096
  const label = agentOpts.label ?? `agent-${run.agents.length + 1}`;
1039
1097
  const phase = agentOpts.phase ?? currentPhase?.title;
1040
1098
  const phaseInstance = resolveAgentPhase(agentOpts.phase);
@@ -1047,8 +1105,8 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1047
1105
  const model = workflowModelLabel(specForIdentity);
1048
1106
  noteDivergence(run.agents.length, "ctx.agentStream results are never replayed");
1049
1107
  diverged = true;
1050
- const { tail: activityTail, onActivity } = createActivityCapture(callKey, label, groupId);
1051
1108
  const rec = { label, callKey, ...(groupId !== undefined ? { groupId } : {}), phase, prompt, ...(model !== undefined ? { model } : {}), status: "running", queuedAt: now() };
1109
+ const { tail: activityTail, onActivity } = createActivityCapture(callKey, label, groupId, rec);
1052
1110
  run.agents.push(rec);
1053
1111
  if (phaseInstance)
1054
1112
  agentPhaseOf.set(rec, phaseInstance);
@@ -1079,14 +1137,17 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1079
1137
  rec.startedAt = now();
1080
1138
  bceSpawn(callKey, label, agentOpts.agentType, false);
1081
1139
  let stream;
1140
+ let childSessionId;
1082
1141
  try {
1083
1142
  const typedSpec0 = applyWorkflowAgentType(spec, agentOpts.agentType, agentRegistry);
1084
1143
  const typedSpec = typedSpec0.model === undefined && inheritedModelSnap !== undefined ? { ...typedSpec0, model: inheritedModelSnap } : typedSpec0;
1085
1144
  const framedSpec = withWorkflowChildPersona(typedSpec, agentOpts.schema ?? typedSpec.outputSchema);
1086
1145
  const authInherit = framedSpec.getApiKeyAndHeaders === undefined && opts.defaultGetApiKeyAndHeaders !== undefined ? { getApiKeyAndHeaders: opts.defaultGetApiKeyAndHeaders } : {};
1087
- const runSpec = agentOpts.schema
1146
+ const baseRunSpec = agentOpts.schema
1088
1147
  ? { ...framedSpec, ...authInherit, signal: effectiveSignal, outputSchema: agentOpts.schema }
1089
1148
  : { ...framedSpec, ...authInherit, signal: effectiveSignal };
1149
+ childSessionId = resolveChildSessionIdAtSpawn(baseRunSpec);
1150
+ const runSpec = childSessionId !== undefined ? { ...baseRunSpec, sessionId: childSessionId } : baseRunSpec;
1090
1151
  const enrichedForwardS = opts.onForwardEvent !== undefined
1091
1152
  ? (e) => {
1092
1153
  opts.onForwardEvent(e.type === "task_progress" ? { ...e, workflowRunId: runId, workflowAgentLabel: label } : e);
@@ -1121,6 +1182,10 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1121
1182
  bceTerminal(callKey, "failed", err instanceof Error ? err.message : String(err), undefined, undefined);
1122
1183
  throw err;
1123
1184
  }
1185
+ if (childSessionId !== undefined && !finalized) {
1186
+ rec.sessionId = childSessionId;
1187
+ void persist("update");
1188
+ }
1124
1189
  const steer = async (content) => {
1125
1190
  const marker = `steer-${++steerMarkerSeq}`;
1126
1191
  const framed = `[operator steer ${marker}] An operator/leader sent guidance for your task. Take it into account on your NEXT step. ` +
@@ -1388,18 +1453,19 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1388
1453
  try {
1389
1454
  const runBody = async () => {
1390
1455
  if (opts.resumeFromRunId !== undefined && journalStore) {
1391
- if (journalStore.resumeClaim) {
1392
- const verdict = journalStore.resumeClaim({ sourceRunId: opts.resumeFromRunId, newRunId: runId, scope });
1393
- resumeClaim = { sourceRunId: opts.resumeFromRunId, verdict };
1394
- const decision = await verdict;
1395
- if (!decision.granted) {
1396
- const holder = decision.holder !== undefined ? clampRunIdText(decision.holder) : "";
1397
- throw new Error(`startWorkflow: resume from "${clampRunIdText(opts.resumeFromRunId)}" was REFUSED — ` +
1398
- `another run already holds the resume claim on it${holder ? ` (holder: ${holder})` : " (holder unknown to the store)"}. ` +
1399
- "Two concurrent resumes of one source run fork its execution: both replay the same prefix and then " +
1400
- "re-run the whole suffix live, duplicating every side effect. Wait for the holder to reach a terminal " +
1401
- "state (or stop it) and resume again.");
1402
- }
1456
+ const usingFallback = journalStore.resumeClaim === undefined;
1457
+ const verdict = usingFallback
1458
+ ? Promise.resolve(workflowResumeClaimFallback.acquire(journalStore, { sourceRunId: opts.resumeFromRunId, newRunId: runId, scope }))
1459
+ : journalStore.resumeClaim({ sourceRunId: opts.resumeFromRunId, newRunId: runId, scope });
1460
+ resumeClaim = { sourceRunId: opts.resumeFromRunId, verdict, fallback: usingFallback };
1461
+ const decision = await verdict;
1462
+ if (!decision.granted) {
1463
+ const holder = decision.holder !== undefined ? clampRunIdText(decision.holder) : "";
1464
+ throw new Error(`startWorkflow: resume from "${clampRunIdText(opts.resumeFromRunId)}" was REFUSED ` +
1465
+ `another run already holds the resume claim on it${holder ? ` (holder: ${holder})` : " (holder unknown to the store)"}. ` +
1466
+ "Two concurrent resumes of one source run fork its execution: both replay the same prefix and then " +
1467
+ "re-run the whole suffix live, duplicating every side effect. Wait for the holder to reach a terminal " +
1468
+ "state (or stop it) and resume again.");
1403
1469
  }
1404
1470
  const entries = await journalStore.load(opts.resumeFromRunId, scope);
1405
1471
  for (const e of entries)
@@ -1501,13 +1567,20 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1501
1567
  finally {
1502
1568
  finalized = true;
1503
1569
  await journalTail.catch(() => undefined);
1504
- if (resumeClaim !== undefined && journalStore?.releaseResumeClaim) {
1570
+ if (resumeClaim !== undefined) {
1505
1571
  const granted = await finalizeWithin(timers, resumeClaim.verdict.then((v) => v.granted, () => false), false);
1506
1572
  if (granted) {
1507
- try {
1508
- await finalizeWithin(timers, journalStore.releaseResumeClaim({ sourceRunId: resumeClaim.sourceRunId, newRunId: runId, scope }), undefined);
1573
+ if (resumeClaim.fallback) {
1574
+ if (journalStore) {
1575
+ workflowResumeClaimFallback.release(journalStore, { sourceRunId: resumeClaim.sourceRunId, newRunId: runId, scope });
1576
+ }
1509
1577
  }
1510
- catch {
1578
+ else if (journalStore?.releaseResumeClaim) {
1579
+ try {
1580
+ await finalizeWithin(timers, journalStore.releaseResumeClaim({ sourceRunId: resumeClaim.sourceRunId, newRunId: runId, scope }), undefined);
1581
+ }
1582
+ catch {
1583
+ }
1511
1584
  }
1512
1585
  }
1513
1586
  }
@@ -26,7 +26,13 @@ export declare function createBashTool(env: ExecutionEnv, rootCanonical: string,
26
26
  execClamp?: ExecClampOption;
27
27
  autoBackgroundOnTimeout?: boolean;
28
28
  oneShot?: boolean;
29
+ additionalRoots?: readonly string[];
30
+ bashDefaultTimeoutMs?: number;
31
+ bashMaxTimeoutMs?: number;
32
+ }): AgentTool;
33
+ export declare function createBashReadonlyTool(env: ExecutionEnv, rootCanonical: string, allow: ReadonlySet<string>, execClamp?: ExecClampOption, timeoutOpts?: {
34
+ bashDefaultTimeoutMs?: number;
35
+ bashMaxTimeoutMs?: number;
29
36
  }): AgentTool;
30
- export declare function createBashReadonlyTool(env: ExecutionEnv, rootCanonical: string, allow: ReadonlySet<string>, execClamp?: ExecClampOption): AgentTool;
31
37
  export declare function createEnvTaskOutputTool(env: ExecutionEnv): AgentTool;
32
38
  export declare function createEnvTaskStopTool(env: ExecutionEnv, registry?: TaskRegistry): AgentTool;