@sema-agent/core 3.0.0 → 4.0.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 (127) hide show
  1. package/CHANGELOG.md +2345 -0
  2. package/dist/agents/cascade.d.ts +2 -1
  3. package/dist/agents/cascade.js +31 -32
  4. package/dist/agents/cumulative-stats.d.ts +29 -0
  5. package/dist/agents/cumulative-stats.js +29 -0
  6. package/dist/agents/observer.d.ts +4 -0
  7. package/dist/agents/observer.js +11 -10
  8. package/dist/agents/repair-loop.js +27 -19
  9. package/dist/agents/retain-ledger.d.ts +2 -0
  10. package/dist/agents/retain-ledger.js +6 -5
  11. package/dist/agents/roster-store.js +3 -5
  12. package/dist/agents/subagent.js +66 -64
  13. package/dist/agents/teacher.d.ts +1 -9
  14. package/dist/agents/teacher.js +21 -24
  15. package/dist/agents/verify.js +15 -14
  16. package/dist/brain/anthropic.js +24 -21
  17. package/dist/brain/openai.js +24 -21
  18. package/dist/brain/stream-engine.js +5 -2
  19. package/dist/brain/stream-shared.d.ts +1 -0
  20. package/dist/brain/stream-shared.js +14 -0
  21. package/dist/brain/terminal-cause.d.ts +4 -0
  22. package/dist/brain/terminal-cause.js +12 -0
  23. package/dist/brain/tool-call-id.d.ts +1 -0
  24. package/dist/brain/tool-call-id.js +3 -0
  25. package/dist/brain/tool-call-repair.js +2 -1
  26. package/dist/core/auto-compaction.js +16 -25
  27. package/dist/core/background-agent-store.d.ts +22 -22
  28. package/dist/core/background-agent-store.js +19 -23
  29. package/dist/core/checkpoint-store.d.ts +6 -13
  30. package/dist/core/checkpoint-store.js +11 -5
  31. package/dist/core/file-snapshot-store.d.ts +8 -0
  32. package/dist/core/file-snapshot-store.js +4 -3
  33. package/dist/core/hooks.d.ts +3 -1
  34. package/dist/core/hooks.js +8 -18
  35. package/dist/core/lsp.js +4 -3
  36. package/dist/core/mailbox-store.d.ts +7 -10
  37. package/dist/core/mcp.d.ts +7 -10
  38. package/dist/core/mcp.js +7 -3
  39. package/dist/core/memory-engine/engine.d.ts +0 -1
  40. package/dist/core/memory-engine/engine.js +24 -34
  41. package/dist/core/memory-engine/file-backend.d.ts +1 -2
  42. package/dist/core/memory-engine/file-backend.js +25 -36
  43. package/dist/core/memory-engine/layout.d.ts +8 -0
  44. package/dist/core/memory-engine/layout.js +60 -4
  45. package/dist/core/memory-engine/scope-contract.js +9 -3
  46. package/dist/core/memory-engine/types.d.ts +1 -1
  47. package/dist/core/runner/active-skill-scope.d.ts +0 -1
  48. package/dist/core/runner/active-skill-scope.js +1 -15
  49. package/dist/core/runner/assemble-result.d.ts +2 -7
  50. package/dist/core/runner/assemble-result.js +3 -4
  51. package/dist/core/runner/compaction-call-options.d.ts +7 -0
  52. package/dist/core/runner/compaction-call-options.js +30 -0
  53. package/dist/core/runner/prepare-task.d.ts +6 -23
  54. package/dist/core/runner/prepare-task.js +20 -53
  55. package/dist/core/runner/runtask.d.ts +1 -0
  56. package/dist/core/runner/runtask.js +65 -156
  57. package/dist/core/runner/session-rule-policy.js +4 -1
  58. package/dist/core/runner/teardown-bounded.d.ts +7 -0
  59. package/dist/core/runner/teardown-bounded.js +36 -0
  60. package/dist/core/runner/tool-disclosure.d.ts +2 -5
  61. package/dist/core/runner/turn-attachments.d.ts +5 -15
  62. package/dist/core/runner/turn-attachments.js +1 -1
  63. package/dist/core/safe-notify.d.ts +17 -0
  64. package/dist/core/safe-notify.js +59 -0
  65. package/dist/core/sensitive-path-policy.js +2 -1
  66. package/dist/core/store-contracts/checkpoint-store-contract.js +1 -1
  67. package/dist/core/stub-env.d.ts +2 -6
  68. package/dist/core/surrogate-safe-slice.d.ts +4 -0
  69. package/dist/core/surrogate-safe-slice.js +18 -0
  70. package/dist/core/task-registry-monitor.js +8 -13
  71. package/dist/core/task-registry-shared.d.ts +22 -0
  72. package/dist/core/task-registry-shared.js +16 -0
  73. package/dist/core/task-registry.d.ts +1 -0
  74. package/dist/core/task-registry.js +40 -57
  75. package/dist/core/tool-errors.js +5 -4
  76. package/dist/core/tool-name-aliases.js +2 -1
  77. package/dist/core/tool-policy.js +10 -5
  78. package/dist/core/tool-result-store.d.ts +1 -0
  79. package/dist/core/tool-result-store.js +17 -1
  80. package/dist/core/trace.d.ts +2 -9
  81. package/dist/core/types.d.ts +15 -12
  82. package/dist/core/untrusted-egress.js +7 -5
  83. package/dist/core/workflow-journal-store.d.ts +9 -20
  84. package/dist/core/workflow-run-store.d.ts +11 -10
  85. package/dist/core/workflow-run-store.js +22 -16
  86. package/dist/engine/compaction/utils.js +4 -2
  87. package/dist/engine/execution-env/node-execution-env.d.ts +2 -24
  88. package/dist/engine/harness/agent-harness.d.ts +1 -0
  89. package/dist/engine/harness/agent-harness.js +13 -33
  90. package/dist/engine/harness/types.d.ts +29 -67
  91. package/dist/engine/llm/types.d.ts +1 -1
  92. package/dist/engine/loop/agent-loop.js +11 -4
  93. package/dist/engine/loop/types.d.ts +11 -9
  94. package/dist/engine/session/memory-repo.d.ts +2 -6
  95. package/dist/engine/session/session.d.ts +5 -30
  96. package/dist/index.d.ts +3 -1
  97. package/dist/index.js +2 -1
  98. package/dist/internal/harness-types.d.ts +3 -1
  99. package/dist/orchestration/run-spec.js +7 -4
  100. package/dist/orchestration/run-workflow-tool.js +3 -35
  101. package/dist/orchestration/workflow.d.ts +3 -11
  102. package/dist/orchestration/workflow.js +164 -204
  103. package/dist/prompt-assembly/tool-catalog.d.ts +10 -9
  104. package/dist/prompt-assembly/turn-snapshot.d.ts +2 -5
  105. package/dist/stores/file/background-agent-store.d.ts +3 -12
  106. package/dist/stores/file/background-agent-store.js +3 -41
  107. package/dist/stores/file/checkpoint-store.js +6 -6
  108. package/dist/stores/file/mailbox-store.d.ts +2 -6
  109. package/dist/stores/file/session-policy-store.js +15 -1
  110. package/dist/stores/file/session-store.d.ts +2 -6
  111. package/dist/stores/file/workflow-journal-store.d.ts +3 -11
  112. package/dist/stores/file/workflow-run-store.d.ts +3 -7
  113. package/dist/stores/file/workflow-run-store.js +4 -17
  114. package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -0
  115. package/dist/tools/fs/bash-readonly-classifier.js +60 -25
  116. package/dist/tools/fs/fs-bash.js +324 -96
  117. package/dist/tools/fs/fs-pdf.d.ts +2 -12
  118. package/dist/tools/fs/fs-shared.js +2 -2
  119. package/dist/tools/fs/notebook.d.ts +2 -5
  120. package/dist/tools/fs/safety.d.ts +3 -0
  121. package/dist/tools/fs/safety.js +43 -14
  122. package/dist/tools/fs/search.js +5 -5
  123. package/dist/tools/monitor.d.ts +1 -0
  124. package/dist/tools/monitor.js +7 -5
  125. package/dist/tools/worktree.d.ts +2 -7
  126. package/dist/tools/worktree.js +3 -1
  127. package/package.json +6 -2
@@ -456,7 +456,7 @@ export async function createRunWorkflowTool(d) {
456
456
  },
457
457
  });
458
458
  }
459
- if (d.notifier) {
459
+ {
460
460
  let notified = false;
461
461
  const fire = (notification) => {
462
462
  if (notified)
@@ -467,6 +467,8 @@ export async function createRunWorkflowTool(d) {
467
467
  }
468
468
  catch {
469
469
  }
470
+ if (!d.notifier)
471
+ return;
470
472
  void Promise.resolve()
471
473
  .then(() => d.notifier.notify({
472
474
  runId,
@@ -507,40 +509,6 @@ export async function createRunWorkflowTool(d) {
507
509
  }))
508
510
  .catch(() => { });
509
511
  }
510
- else {
511
- let notified = false;
512
- const fire = (notification) => {
513
- if (notified)
514
- return;
515
- notified = true;
516
- try {
517
- d.taskNotification?.(notification);
518
- }
519
- catch {
520
- }
521
- };
522
- handle.done
523
- .then((res) => fire({
524
- task_id: runId,
525
- task_type: "workflow",
526
- ...(ctx.toolCallId !== undefined ? { toolUseId: ctx.toolCallId } : {}),
527
- status: "completed",
528
- summary: completionSummary(meta.name, res.run),
529
- result: boundedRedactedSummary(res.result, 4000),
530
- usage: workflowUsageBlock(res.run),
531
- diagnostics: workflowDiagnostics(runId, d.journalStore?.locator?.(runId, res.run.scope), res.run.resume?.journalEntries === 0),
532
- completionId: res.run.completionId,
533
- }), (err) => fire({
534
- task_id: runId,
535
- task_type: "workflow",
536
- ...(ctx.toolCallId !== undefined ? { toolUseId: ctx.toolCallId } : {}),
537
- status: "failed",
538
- summary: failureSummary(err, runId),
539
- diagnostics: workflowDiagnostics(runId, undefined),
540
- ...(completionIdFromRejection(err) !== undefined ? { completionId: completionIdFromRejection(err) } : {}),
541
- }))
542
- .catch(() => { });
543
- }
544
512
  return {
545
513
  details: { type: "workflow-run", status: "backgrounded", workflowRunId: runId },
546
514
  content: JSON.stringify({
@@ -2,7 +2,7 @@ import type { TSchema } from "typebox";
2
2
  import type { Runner } from "../core/runner/runtask.js";
3
3
  import type { AgentDefinition, TaskEvent, TaskResult, TaskSpec } from "../core/types.js";
4
4
  import type { WorkflowRunStore } from "../core/workflow-run-store.js";
5
- import type { WorkflowJournalStore } from "../core/workflow-journal-store.js";
5
+ import type { WorkflowJournalStore, ResumeClaimArgs } from "../core/workflow-journal-store.js";
6
6
  import type { WorkflowRun, WorkflowEvent } from "./workflow-types.js";
7
7
  export * from "./workflow-types.js";
8
8
  export declare const WORKFLOW_SUBAGENT_PROMPT = "You are a subagent spawned by a workflow orchestration script. Use the tools available to complete the task.\n\nCRITICAL: Your final text response is returned **verbatim** as a string to the calling script \u2014 it is your return value, not a message to a human.\n- Output the literal result (data, JSON, text). Do NOT output confirmations like \"Done.\" or \"Sent.\"\n- If asked for JSON, return ONLY the raw JSON \u2014 no code fences, no prose, no markdown.\n- Do NOT address a human user \u2014 there is no user on the other end. Put your answer in your final text response.\n- Do NOT delegate to further sub-agents or start nested workflows; complete the task yourself.\n- Be concise. The script will parse your output.";
@@ -131,19 +131,11 @@ export interface WorkflowTimers {
131
131
  clearTimeout(handle: unknown): void;
132
132
  }
133
133
  export declare const workflowResumeClaimFallback: {
134
- acquire(store: WorkflowJournalStore, input: {
135
- sourceRunId: string;
136
- newRunId: string;
137
- scope: string;
138
- }): {
134
+ acquire(store: WorkflowJournalStore, input: ResumeClaimArgs): {
139
135
  granted: boolean;
140
136
  holder?: string;
141
137
  };
142
- release(store: WorkflowJournalStore, input: {
143
- sourceRunId: string;
144
- newRunId: string;
145
- scope: string;
146
- }): void;
138
+ release(store: WorkflowJournalStore, input: ResumeClaimArgs): void;
147
139
  };
148
140
  export declare function startWorkflow<T>(runner: Runner, fn: (ctx: WorkflowRunContext) => Promise<T>, opts?: RunWorkflowOptions, internals?: WorkflowInternals): WorkflowHandle<T>;
149
141
  export declare function runWorkflow<T>(runner: Runner, fn: (ctx: WorkflowRunContext) => Promise<T>, opts?: RunWorkflowOptions, internals?: WorkflowInternals): Promise<RunWorkflowResult<T>>;
@@ -6,6 +6,7 @@ import { uuidv7 } from "../internal/harness.js";
6
6
  import { builtinAgentDefinitions } from "../agents/builtin-agents.js";
7
7
  import { GENERAL_PURPOSE_SUBAGENT_TYPE } from "../agents/subagent.js";
8
8
  import { combinePolicies, createAllowDenyPolicy } from "../core/tool-policy.js";
9
+ import { createSafeNotifier } from "../core/safe-notify.js";
9
10
  import { callKeyOrdinal, oversizeJournalResult, journalOversizeTombstone, JOURNAL_OVERSIZE_ERROR_CODE, MAX_JOURNAL_RESULT_BYTES } from "../core/workflow-journal-store.js";
10
11
  import { isWorkflowRunActive, closeWorkflowChannel, markWorkflowActive, publishWorkflowEvent } from "./workflow-observe.js";
11
12
  import { isDurablePause, mapNestedSuspend } from "../agents/suspend-guard.js";
@@ -66,8 +67,9 @@ export function applyWorkflowAgentType(spec, agentType, registry) {
66
67
  out.model = def.model;
67
68
  if (out.thinking === undefined && def.thinking !== undefined)
68
69
  out.thinking = def.thinking;
69
- if (out.limits?.maxTurns === undefined && def.maxTurns !== undefined)
70
+ if (out.limits?.maxTurns === undefined && typeof def.maxTurns === "number" && Number.isFinite(def.maxTurns) && def.maxTurns > 0) {
70
71
  out.limits = { ...out.limits, maxTurns: def.maxTurns };
72
+ }
71
73
  if (out.skills === undefined && def.skills !== undefined)
72
74
  out.skills = def.skills;
73
75
  if (out.memory === undefined && def.memory !== undefined)
@@ -124,6 +126,27 @@ function rethrowIfMaxAgents(e) {
124
126
  throw e;
125
127
  return null;
126
128
  }
129
+ function foldFanOutSettled(settled, lane, errorsOut, emitRunLog) {
130
+ let dropped = 0;
131
+ const droppedSlots = [];
132
+ const results = settled.map((s) => {
133
+ if (s.ok)
134
+ return s.v;
135
+ if (s.e instanceof WorkflowBudgetExceededError) {
136
+ dropped++;
137
+ droppedSlots.push(s.i);
138
+ errorsOut?.push(workflowFanOutSlotError(s.i, s.e));
139
+ return null;
140
+ }
141
+ rethrowIfMaxAgents(s.e);
142
+ errorsOut?.push(workflowFanOutSlotError(s.i, s.e));
143
+ emitRunLog(`${lane}[${s.i}] failed: ${boundedRedactedSummary(s.e instanceof Error ? s.e.message : String(s.e), 300)}`);
144
+ return null;
145
+ });
146
+ if (dropped > 0)
147
+ emitRunLog(`${lane}: ${dropped} ${dropped === 1 ? "slot" : "slots"} dropped — token budget exceeded (${dropped === 1 ? "slot" : "slots"} ${droppedSlots.join(", ")})`);
148
+ return results;
149
+ }
127
150
  function workflowFanOutSlotError(index, e) {
128
151
  const code = e?.code;
129
152
  const kind = typeof code === "string" && code.length > 0 ? code : e instanceof Error ? e.name : "Error";
@@ -191,6 +214,14 @@ function normalizeConcurrency(c) {
191
214
  }
192
215
  return Math.floor(c);
193
216
  }
217
+ function normalizeWorkflowHardCap(name, v) {
218
+ if (v === undefined)
219
+ return undefined;
220
+ if (!Number.isFinite(v) || v < 0) {
221
+ throw new Error(`runWorkflow: ${name} must be a finite number ≥ 0 (got ${v})`);
222
+ }
223
+ return v;
224
+ }
194
225
  function createSemaphore(max) {
195
226
  let active = 0;
196
227
  const waiters = [];
@@ -273,8 +304,9 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
273
304
  const concurrency = normalizeConcurrency(opts.concurrency);
274
305
  const agentRegistry = workflowAgentRegistry(opts);
275
306
  const store = opts.store;
276
- const maxAgents = opts.maxAgents;
277
- const maxLogChars = opts.maxLogChars;
307
+ const maxAgents = normalizeWorkflowHardCap("maxAgents", opts.maxAgents);
308
+ const maxLogChars = normalizeWorkflowHardCap("maxLogChars", opts.maxLogChars);
309
+ const maxResultChars = normalizeWorkflowHardCap("maxResultChars", opts.maxResultChars);
278
310
  const stallMs = opts.stallMs ?? WORKFLOW_AGENT_STALL_MS;
279
311
  const agentMaxRetries = opts.agentMaxRetries ?? WORKFLOW_AGENT_MAX_RETRIES;
280
312
  const throttleBackoffMs = opts.throttleBackoffMs ?? WORKFLOW_AGENT_THROTTLE_BACKOFF_MS;
@@ -289,13 +321,10 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
289
321
  const workflowSignal = signalSources.length === 1 ? signalSources[0] : AbortSignal.any(signalSources);
290
322
  let timeoutTimer;
291
323
  let storeRev = 0;
324
+ const notifier = createSafeNotifier();
292
325
  const emit = (e) => {
293
326
  publishWorkflowEvent(runId, e);
294
- try {
295
- opts.onEvent?.(e);
296
- }
297
- catch {
298
- }
327
+ notifier.notify(() => opts.onEvent?.(e), "workflow.onEvent");
299
328
  };
300
329
  let persistTail = Promise.resolve();
301
330
  const persist = (mode) => {
@@ -445,12 +474,9 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
445
474
  };
446
475
  const bceSink = opts.onBackgroundChildEvent;
447
476
  const bceEmit = (e) => {
448
- try {
449
- bceSink?.(e);
450
- }
451
- catch {
452
- }
477
+ notifier.notify(() => bceSink?.(e), "workflow.onBackgroundChildEvent");
453
478
  };
479
+ const bceParentToolCall = opts.parentToolCallId !== undefined ? { parentToolCallId: opts.parentToolCallId } : {};
454
480
  const waIdOf = (callKey) => `wa${createHash("sha256").update(`${runId}:${callKey}`).digest("hex").slice(0, 16)}`;
455
481
  const bceLive = new Map();
456
482
  const bceSpawn = (callKey, label, agentType, replayed, sessionId) => {
@@ -463,6 +489,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
463
489
  taskId: id,
464
490
  sessionScoped: false,
465
491
  owner: runId,
492
+ ...bceParentToolCall,
466
493
  ...(sessionId !== undefined ? { sessionId, transcriptId: sessionId } : {}),
467
494
  ...(scope !== undefined ? { scope } : {}),
468
495
  description: replayed ? `${label} (replayed)` : label,
@@ -494,6 +521,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
494
521
  taskId: id,
495
522
  sessionScoped: false,
496
523
  owner: runId,
524
+ ...bceParentToolCall,
497
525
  workflowRunId: runId,
498
526
  ...(scope !== undefined ? { scope } : {}),
499
527
  ...(row.agentType !== undefined ? { agentType: row.agentType } : { agentType: "workflow-agent" }),
@@ -518,6 +546,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
518
546
  taskId: id,
519
547
  sessionScoped: false,
520
548
  owner: runId,
549
+ ...bceParentToolCall,
521
550
  workflowRunId: runId,
522
551
  ...(scope !== undefined ? { scope } : {}),
523
552
  ...(coord !== undefined ? { sessionId: coord, transcriptId: coord } : {}),
@@ -574,6 +603,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
574
603
  });
575
604
  };
576
605
  let journalTail = Promise.resolve();
606
+ const JOURNAL_DRAIN_MAX_MS = 5_000;
577
607
  const journalAppend = async (callKey, result, label) => {
578
608
  const dbg = typeof process !== "undefined" && process.env?.SEMA_DEBUG_WORKFLOW_JOURNAL === "1";
579
609
  if (!journalStore) {
@@ -712,6 +742,94 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
712
742
  rec.output = output;
713
743
  return output;
714
744
  };
745
+ const assertAgentSpawnAllowed = (lane, spec, agentOpts) => {
746
+ if (finalized) {
747
+ throw new Error(`workflow run already finalized — ${lane} cannot be called after the script returned`);
748
+ }
749
+ const effectiveSignal = spec.signal ?? agentOpts.signal ?? workflowSignal;
750
+ if (effectiveSignal?.aborted)
751
+ throw new Error("workflow aborted");
752
+ if (maxAgents !== undefined && run.agents.length >= maxAgents) {
753
+ throw new WorkflowMaxAgentsError(maxAgents);
754
+ }
755
+ return effectiveSignal;
756
+ };
757
+ const prepareAgentCall = (spec, agentOpts) => {
758
+ if (agentOpts.schema) {
759
+ const compiled = compileOutputSchema(agentOpts.schema);
760
+ if ("error" in compiled)
761
+ throw new TypeError(`agent({schema}) received an invalid JSON Schema: ${compiled.error}`);
762
+ }
763
+ assertSupportedAgentIsolation(agentOpts.isolation);
764
+ const label = agentOpts.label ?? `agent-${run.agents.length + 1}`;
765
+ const phase = agentOpts.phase ?? currentPhase?.title;
766
+ const phaseInstance = resolveAgentPhase(agentOpts.phase);
767
+ const groupId = currentGroup;
768
+ const typeDefModel = agentOpts.agentType !== undefined ? agentRegistry.find((d) => d.name === agentOpts.agentType)?.model : undefined;
769
+ const inheritedModelSnap = spec.model === undefined && typeDefModel === undefined ? opts.defaultModel?.() : undefined;
770
+ const specForIdentity = inheritedModelSnap !== undefined ? { ...spec, model: inheritedModelSnap } : spec;
771
+ const callKey = workflowAgentCallKey(run.agents.length, specForIdentity, agentOpts);
772
+ const prompt = boundedRedactedSummary(spec.systemPrompt ? `${spec.systemPrompt}\n\n${spec.objective}` : spec.objective, MAX_TRANSCRIPT_CHARS);
773
+ const model = workflowModelLabel(specForIdentity);
774
+ return { label, phase, phaseInstance, groupId, inheritedModelSnap, callKey, prompt, model };
775
+ };
776
+ const createUsageBeat = (rec) => {
777
+ let beatTokens = 0;
778
+ let beatTurns = 0;
779
+ let beatCostMicroUsd = 0;
780
+ const rollbackUsageBeat = () => {
781
+ if (beatTurns === 0 && beatTokens === 0 && beatCostMicroUsd === 0)
782
+ return false;
783
+ run.stats.tokens -= beatTokens;
784
+ run.stats.turns -= beatTurns;
785
+ run.stats.costMicroUsd -= beatCostMicroUsd;
786
+ beatTokens = 0;
787
+ beatTurns = 0;
788
+ beatCostMicroUsd = 0;
789
+ return true;
790
+ };
791
+ const onTurnEndUsage = (usage) => {
792
+ if (finalized)
793
+ return;
794
+ const turnTokens = usage ? usage.totalInputTokens + usage.outputTokens : 0;
795
+ const turnCost = usage?.costMicroUsd ?? 0;
796
+ beatTokens += turnTokens;
797
+ beatTurns += 1;
798
+ beatCostMicroUsd += turnCost;
799
+ run.stats.tokens += turnTokens;
800
+ run.stats.turns += 1;
801
+ run.stats.costMicroUsd += turnCost;
802
+ rec.stats = { tokens: beatTokens, turns: beatTurns, costMicroUsd: beatCostMicroUsd };
803
+ void persist("update");
804
+ };
805
+ return { onTurnEndUsage, rollbackUsageBeat };
806
+ };
807
+ const settleAgentResult = async (rec, result, activityTail, agentOpts, journal) => {
808
+ const s = result.stats;
809
+ accumulateStats(result, true);
810
+ rec.status = result.status === "completed" ? "completed" : "failed";
811
+ rec.taskStatus = result.status;
812
+ if (result.sessionId)
813
+ rec.sessionId = result.sessionId;
814
+ rec.endedAt = now();
815
+ rec.stats = { tokens: s.tokens ?? 0, turns: s.turns ?? 0, costMicroUsd: s.costMicroUsd };
816
+ const output = settleAgentError(rec, result, rec.label, boundedRedactedSummary(result.structuredOutput ?? result.result, MAX_TRANSCRIPT_CHARS));
817
+ if (result.stats.toolCalls !== undefined)
818
+ rec.toolCalls = result.stats.toolCalls;
819
+ if (activityTail.length > 0)
820
+ rec.activity = activityTail.slice();
821
+ emit({ type: "agent_end", runId, label: rec.label, phase: rec.phase, ...(rec.groupId !== undefined ? { groupId: rec.groupId } : {}), status: rec.status, output, ...(rec.errorCode !== undefined ? { errorCode: rec.errorCode } : {}), ...(result.stats.toolCalls !== undefined ? { toolCalls: result.stats.toolCalls } : {}), ts: rec.endedAt });
822
+ void persist("update");
823
+ bceTerminal(rec.callKey, rec.status === "completed" ? "completed" : "failed", output, result.sessionId || undefined, rec.stats);
824
+ if (journal === "awaited")
825
+ await journalAppend(rec.callKey, result, rec.label);
826
+ else {
827
+ void journalAppend(rec.callKey, result, rec.label).catch(() => undefined);
828
+ }
829
+ if (agentOpts.schema && result.status === "completed" && result.structuredOutput === undefined) {
830
+ throw new WorkflowAgentSchemaError(rec.label, result);
831
+ }
832
+ };
715
833
  const ctx = {
716
834
  runId,
717
835
  budget,
@@ -720,31 +838,8 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
720
838
  emitRunLog(message);
721
839
  },
722
840
  async agent(spec, agentOpts = {}) {
723
- if (finalized) {
724
- throw new Error("workflow run already finalized ctx.agent cannot be called after the script returned");
725
- }
726
- const effectiveSignal = spec.signal ?? agentOpts.signal ?? workflowSignal;
727
- if (effectiveSignal?.aborted)
728
- throw new Error("workflow aborted");
729
- if (maxAgents !== undefined && run.agents.length >= maxAgents) {
730
- throw new WorkflowMaxAgentsError(maxAgents);
731
- }
732
- if (agentOpts.schema) {
733
- const compiled = compileOutputSchema(agentOpts.schema);
734
- if ("error" in compiled)
735
- throw new TypeError(`agent({schema}) received an invalid JSON Schema: ${compiled.error}`);
736
- }
737
- assertSupportedAgentIsolation(agentOpts.isolation);
738
- const label = agentOpts.label ?? `agent-${run.agents.length + 1}`;
739
- const phase = agentOpts.phase ?? currentPhase?.title;
740
- const phaseInstance = resolveAgentPhase(agentOpts.phase);
741
- const groupId = currentGroup;
742
- const typeDefModel = agentOpts.agentType !== undefined ? agentRegistry.find((d) => d.name === agentOpts.agentType)?.model : undefined;
743
- const inheritedModelSnap = spec.model === undefined && typeDefModel === undefined ? opts.defaultModel?.() : undefined;
744
- const specForIdentity = inheritedModelSnap !== undefined ? { ...spec, model: inheritedModelSnap } : spec;
745
- const callKey = workflowAgentCallKey(run.agents.length, specForIdentity, agentOpts);
746
- const prompt = boundedRedactedSummary(spec.systemPrompt ? `${spec.systemPrompt}\n\n${spec.objective}` : spec.objective, MAX_TRANSCRIPT_CHARS);
747
- const model = workflowModelLabel(specForIdentity);
841
+ const effectiveSignal = assertAgentSpawnAllowed("ctx.agent", spec, agentOpts);
842
+ const { label, phase, phaseInstance, groupId, inheritedModelSnap, callKey, prompt, model } = prepareAgentCall(spec, agentOpts);
748
843
  if (opts.resumeFromRunId !== undefined && !diverged) {
749
844
  const cached = replayByOrdinal[run.agents.length];
750
845
  if (cached && cached.callKey === callKey && cached.result.status === "completed") {
@@ -913,35 +1008,8 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
913
1008
  lastProgressRearm = t;
914
1009
  armWatchdog();
915
1010
  };
916
- let beatTokens = 0;
917
- let beatTurns = 0;
918
- let beatCostMicroUsd = 0;
919
- const rollbackUsageBeat = () => {
920
- if (beatTurns === 0 && beatTokens === 0 && beatCostMicroUsd === 0)
921
- return false;
922
- run.stats.tokens -= beatTokens;
923
- run.stats.turns -= beatTurns;
924
- run.stats.costMicroUsd -= beatCostMicroUsd;
925
- beatTokens = 0;
926
- beatTurns = 0;
927
- beatCostMicroUsd = 0;
928
- return true;
929
- };
1011
+ const { onTurnEndUsage, rollbackUsageBeat } = createUsageBeat(rec);
930
1012
  activeUsageBeats.add(rollbackUsageBeat);
931
- const onTurnEndUsage = (usage) => {
932
- if (finalized)
933
- return;
934
- const turnTokens = usage ? usage.totalInputTokens + usage.outputTokens : 0;
935
- const turnCost = usage?.costMicroUsd ?? 0;
936
- beatTokens += turnTokens;
937
- beatTurns += 1;
938
- beatCostMicroUsd += turnCost;
939
- run.stats.tokens += turnTokens;
940
- run.stats.turns += 1;
941
- run.stats.costMicroUsd += turnCost;
942
- rec.stats = { tokens: beatTokens, turns: beatTurns, costMicroUsd: beatCostMicroUsd };
943
- void persist("update");
944
- };
945
1013
  try {
946
1014
  attemptResult = await workflowDepthStore.run({ depth: depth + 1 }, async () => {
947
1015
  if (typeof runner.runTaskStream !== "function") {
@@ -963,8 +1031,11 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
963
1031
  attemptError = err;
964
1032
  }
965
1033
  finally {
966
- rollbackUsageBeat();
1034
+ const attemptHadSpend = rollbackUsageBeat();
967
1035
  activeUsageBeats.delete(rollbackUsageBeat);
1036
+ if (attemptError !== undefined && attemptHadSpend && !finalized && rec.stats !== undefined) {
1037
+ accumulateStats({ stats: rec.stats }, true);
1038
+ }
968
1039
  disarmWatchdog();
969
1040
  effectiveSignal?.removeEventListener("abort", onCallerAbort);
970
1041
  }
@@ -1073,57 +1144,15 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1073
1144
  if (isDurablePause(result.status)) {
1074
1145
  result = mapNestedSuspend(result);
1075
1146
  }
1076
- const s = result.stats;
1077
- accumulateStats(result, true);
1078
- rec.status = result.status === "completed" ? "completed" : "failed";
1079
- rec.taskStatus = result.status;
1080
- if (result.sessionId)
1081
- rec.sessionId = result.sessionId;
1082
- rec.endedAt = now();
1083
- rec.stats = { tokens: s.tokens ?? 0, turns: s.turns ?? 0, costMicroUsd: s.costMicroUsd };
1084
- const output = settleAgentError(rec, result, label, boundedRedactedSummary(result.structuredOutput ?? result.result, MAX_TRANSCRIPT_CHARS));
1085
- if (result.stats.toolCalls !== undefined)
1086
- rec.toolCalls = result.stats.toolCalls;
1087
- if (activityTail.length > 0)
1088
- rec.activity = activityTail.slice();
1089
- emit({ type: "agent_end", runId, label, phase, ...(groupId !== undefined ? { groupId } : {}), status: rec.status, output, ...(rec.errorCode !== undefined ? { errorCode: rec.errorCode } : {}), ...(result.stats.toolCalls !== undefined ? { toolCalls: result.stats.toolCalls } : {}), ts: rec.endedAt });
1090
- void persist("update");
1091
- bceTerminal(callKey, rec.status === "completed" ? "completed" : "failed", output, result.sessionId || undefined, rec.stats);
1092
- await journalAppend(callKey, result, label);
1093
- if (agentOpts.schema && result.status === "completed" && result.structuredOutput === undefined) {
1094
- throw new WorkflowAgentSchemaError(label, result);
1095
- }
1147
+ await settleAgentResult(rec, result, activityTail, agentOpts, "awaited");
1096
1148
  return result;
1097
1149
  },
1098
1150
  async agentStream(spec, agentOpts = {}) {
1099
- if (finalized) {
1100
- throw new Error("workflow run already finalized — ctx.agentStream cannot be called after the script returned");
1101
- }
1102
- const effectiveSignal = spec.signal ?? agentOpts.signal ?? workflowSignal;
1103
- if (effectiveSignal?.aborted)
1104
- throw new Error("workflow aborted");
1105
- if (maxAgents !== undefined && run.agents.length >= maxAgents) {
1106
- throw new WorkflowMaxAgentsError(maxAgents);
1107
- }
1151
+ const effectiveSignal = assertAgentSpawnAllowed("ctx.agentStream", spec, agentOpts);
1108
1152
  if (budgetTotal !== null && spent() >= budgetTotal) {
1109
1153
  throw new WorkflowBudgetExceededError(spent(), budgetTotal);
1110
1154
  }
1111
- if (agentOpts.schema) {
1112
- const compiled = compileOutputSchema(agentOpts.schema);
1113
- if ("error" in compiled)
1114
- throw new TypeError(`agent({schema}) received an invalid JSON Schema: ${compiled.error}`);
1115
- }
1116
- assertSupportedAgentIsolation(agentOpts.isolation);
1117
- const label = agentOpts.label ?? `agent-${run.agents.length + 1}`;
1118
- const phase = agentOpts.phase ?? currentPhase?.title;
1119
- const phaseInstance = resolveAgentPhase(agentOpts.phase);
1120
- const groupId = currentGroup;
1121
- const typeDefModel = agentOpts.agentType !== undefined ? agentRegistry.find((d) => d.name === agentOpts.agentType)?.model : undefined;
1122
- const inheritedModelSnap = spec.model === undefined && typeDefModel === undefined ? opts.defaultModel?.() : undefined;
1123
- const specForIdentity = inheritedModelSnap !== undefined ? { ...spec, model: inheritedModelSnap } : spec;
1124
- const callKey = workflowAgentCallKey(run.agents.length, specForIdentity, agentOpts);
1125
- const prompt = boundedRedactedSummary(spec.systemPrompt ? `${spec.systemPrompt}\n\n${spec.objective}` : spec.objective, MAX_TRANSCRIPT_CHARS);
1126
- const model = workflowModelLabel(specForIdentity);
1155
+ const { label, phase, phaseInstance, groupId, inheritedModelSnap, callKey, prompt, model } = prepareAgentCall(spec, agentOpts);
1127
1156
  noteDivergence(run.agents.length, "ctx.agentStream results are never replayed");
1128
1157
  diverged = true;
1129
1158
  const rec = { label, callKey, ...(groupId !== undefined ? { groupId } : {}), phase, prompt, ...(model !== undefined ? { model } : {}), status: "running", queuedAt: now() };
@@ -1215,35 +1244,8 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1215
1244
  await stream.steer(framed, { trusted: true });
1216
1245
  return marker;
1217
1246
  };
1218
- let beatTokens = 0;
1219
- let beatTurns = 0;
1220
- let beatCostMicroUsd = 0;
1221
- const rollbackUsageBeat = () => {
1222
- if (beatTurns === 0 && beatTokens === 0 && beatCostMicroUsd === 0)
1223
- return false;
1224
- run.stats.tokens -= beatTokens;
1225
- run.stats.turns -= beatTurns;
1226
- run.stats.costMicroUsd -= beatCostMicroUsd;
1227
- beatTokens = 0;
1228
- beatTurns = 0;
1229
- beatCostMicroUsd = 0;
1230
- return true;
1231
- };
1247
+ const { onTurnEndUsage, rollbackUsageBeat } = createUsageBeat(rec);
1232
1248
  activeUsageBeats.add(rollbackUsageBeat);
1233
- const onTurnEndUsage = (usage) => {
1234
- if (finalized)
1235
- return;
1236
- const turnTokens = usage ? usage.totalInputTokens + usage.outputTokens : 0;
1237
- const turnCost = usage?.costMicroUsd ?? 0;
1238
- beatTokens += turnTokens;
1239
- beatTurns += 1;
1240
- beatCostMicroUsd += turnCost;
1241
- run.stats.tokens += turnTokens;
1242
- run.stats.turns += 1;
1243
- run.stats.costMicroUsd += turnCost;
1244
- rec.stats = { tokens: beatTokens, turns: beatTurns, costMicroUsd: beatCostMicroUsd };
1245
- void persist("update");
1246
- };
1247
1249
  const completion = (async () => {
1248
1250
  let result;
1249
1251
  try {
@@ -1259,14 +1261,18 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1259
1261
  catch (err) {
1260
1262
  rollbackUsageBeat();
1261
1263
  activeUsageBeats.delete(rollbackUsageBeat);
1264
+ const partialSpend = rec.stats;
1265
+ if (!finalized && partialSpend !== undefined && (partialSpend.tokens > 0 || partialSpend.turns > 0 || (partialSpend.costMicroUsd ?? 0) > 0)) {
1266
+ accumulateStats({ stats: partialSpend }, true);
1267
+ }
1262
1268
  recordFailed();
1263
1269
  releaseOnce();
1264
- await journalAppend(callKey, {
1270
+ void journalAppend(callKey, {
1265
1271
  taskId: callKey,
1266
1272
  sessionId: "",
1267
1273
  status: "failed",
1268
1274
  result: boundedRedactedSummary(err instanceof Error ? err.message : String(err), 500),
1269
- stats: { turns: 0, tokens: 0, costMicroUsd: 0 },
1275
+ stats: { turns: partialSpend?.turns ?? 0, tokens: partialSpend?.tokens ?? 0, costMicroUsd: partialSpend?.costMicroUsd ?? 0 },
1270
1276
  }).catch(() => undefined);
1271
1277
  bceTerminal(callKey, "failed", err instanceof Error ? err.message : String(err), undefined, rec.stats);
1272
1278
  throw err;
@@ -1279,26 +1285,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1279
1285
  if (isDurablePause(result.status)) {
1280
1286
  result = mapNestedSuspend(result);
1281
1287
  }
1282
- const s = result.stats;
1283
- accumulateStats(result, true);
1284
- rec.status = result.status === "completed" ? "completed" : "failed";
1285
- rec.taskStatus = result.status;
1286
- if (result.sessionId)
1287
- rec.sessionId = result.sessionId;
1288
- rec.endedAt = now();
1289
- rec.stats = { tokens: s.tokens ?? 0, turns: s.turns ?? 0, costMicroUsd: s.costMicroUsd };
1290
- const output = settleAgentError(rec, result, label, boundedRedactedSummary(result.structuredOutput ?? result.result, MAX_TRANSCRIPT_CHARS));
1291
- if (result.stats.toolCalls !== undefined)
1292
- rec.toolCalls = result.stats.toolCalls;
1293
- if (activityTail.length > 0)
1294
- rec.activity = activityTail.slice();
1295
- emit({ type: "agent_end", runId, label, phase, ...(groupId !== undefined ? { groupId } : {}), status: rec.status, output, ...(rec.errorCode !== undefined ? { errorCode: rec.errorCode } : {}), ...(result.stats.toolCalls !== undefined ? { toolCalls: result.stats.toolCalls } : {}), ts: rec.endedAt });
1296
- void persist("update");
1297
- bceTerminal(callKey, rec.status === "completed" ? "completed" : "failed", output, result.sessionId || undefined, rec.stats);
1298
- await journalAppend(callKey, result, label).catch(() => undefined);
1299
- if (agentOpts.schema && result.status === "completed" && result.structuredOutput === undefined) {
1300
- throw new WorkflowAgentSchemaError(label, result);
1301
- }
1288
+ await settleAgentResult(rec, result, activityTail, agentOpts, "best-effort");
1302
1289
  return result;
1303
1290
  })();
1304
1291
  completion.catch(() => undefined);
@@ -1312,25 +1299,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1312
1299
  const settled = await Promise.all(thunks.map((t, i) => Promise.resolve()
1313
1300
  .then(t)
1314
1301
  .then((v) => ({ ok: true, v }), (e) => ({ ok: false, e, i }))));
1315
- let dropped = 0;
1316
- const droppedSlots = [];
1317
- const results = settled.map((s) => {
1318
- if (s.ok)
1319
- return s.v;
1320
- if (s.e instanceof WorkflowBudgetExceededError) {
1321
- dropped++;
1322
- droppedSlots.push(s.i);
1323
- errorsOut?.push(workflowFanOutSlotError(s.i, s.e));
1324
- return null;
1325
- }
1326
- rethrowIfMaxAgents(s.e);
1327
- errorsOut?.push(workflowFanOutSlotError(s.i, s.e));
1328
- emitRunLog(`parallel[${s.i}] failed: ${boundedRedactedSummary(s.e instanceof Error ? s.e.message : String(s.e), 300)}`);
1329
- return null;
1330
- });
1331
- if (dropped > 0)
1332
- emitRunLog(`parallel: ${dropped} ${dropped === 1 ? "slot" : "slots"} dropped — token budget exceeded (${dropped === 1 ? "slot" : "slots"} ${droppedSlots.join(", ")})`);
1333
- return results;
1302
+ return foldFanOutSettled(settled, "parallel", errorsOut, emitRunLog);
1334
1303
  },
1335
1304
  async pipeline(items, ...stagesAndOpts) {
1336
1305
  if (items.length > MAX_WORKFLOW_ITEMS) {
@@ -1350,25 +1319,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1350
1319
  const settled = await Promise.all(items.map((item, index) => stages
1351
1320
  .reduce((acc, stage) => acc.then((prev) => stage(prev, item, index)), Promise.resolve(item))
1352
1321
  .then((v) => ({ ok: true, v }), (e) => ({ ok: false, e, i: index }))));
1353
- let dropped = 0;
1354
- const droppedSlots = [];
1355
- const results = settled.map((s) => {
1356
- if (s.ok)
1357
- return s.v;
1358
- if (s.e instanceof WorkflowBudgetExceededError) {
1359
- dropped++;
1360
- droppedSlots.push(s.i);
1361
- errorsOut?.push(workflowFanOutSlotError(s.i, s.e));
1362
- return null;
1363
- }
1364
- rethrowIfMaxAgents(s.e);
1365
- errorsOut?.push(workflowFanOutSlotError(s.i, s.e));
1366
- emitRunLog(`pipeline[${s.i}] failed: ${boundedRedactedSummary(s.e instanceof Error ? s.e.message : String(s.e), 300)}`);
1367
- return null;
1368
- });
1369
- if (dropped > 0)
1370
- emitRunLog(`pipeline: ${dropped} ${dropped === 1 ? "slot" : "slots"} dropped — token budget exceeded (${dropped === 1 ? "slot" : "slots"} ${droppedSlots.join(", ")})`);
1371
- return results;
1322
+ return foldFanOutSettled(settled, "pipeline", errorsOut, emitRunLog);
1372
1323
  },
1373
1324
  async phase(title, body) {
1374
1325
  if (finalized)
@@ -1531,7 +1482,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1531
1482
  else {
1532
1483
  result = await bodyPromise;
1533
1484
  }
1534
- if (opts.maxResultChars !== undefined) {
1485
+ if (maxResultChars !== undefined) {
1535
1486
  let size = 0;
1536
1487
  try {
1537
1488
  size = JSON.stringify(result)?.length ?? 0;
@@ -1539,8 +1490,8 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1539
1490
  catch {
1540
1491
  size = 0;
1541
1492
  }
1542
- if (size > opts.maxResultChars)
1543
- throw new WorkflowResultTooLargeError(size, opts.maxResultChars);
1493
+ if (size > maxResultChars)
1494
+ throw new WorkflowResultTooLargeError(size, maxResultChars);
1544
1495
  }
1545
1496
  finalized = true;
1546
1497
  settleActiveUsageBeats();
@@ -1587,7 +1538,16 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1587
1538
  }
1588
1539
  finally {
1589
1540
  finalized = true;
1590
- await journalTail.catch(() => undefined);
1541
+ await new Promise((resolve) => {
1542
+ const t = setTimeout(() => {
1543
+ emitRunLog(`resume-journal: terminal drain did not settle within ${JOURNAL_DRAIN_MAX_MS}ms — tail entries may be missing; a resume from this run re-runs those agents live.`);
1544
+ resolve();
1545
+ }, JOURNAL_DRAIN_MAX_MS);
1546
+ void journalTail.catch(() => undefined).then(() => {
1547
+ clearTimeout(t);
1548
+ resolve();
1549
+ });
1550
+ });
1591
1551
  if (resumeClaim !== undefined) {
1592
1552
  const granted = await finalizeWithin(timers, resumeClaim.verdict.then((v) => v.granted, () => false), false);
1593
1553
  if (granted) {