@sema-agent/core 7.9.1 → 7.10.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.
- package/CHANGELOG.md +54 -0
- package/dist/agents/child-model-seat.d.ts +81 -0
- package/dist/agents/child-model-seat.js +46 -0
- package/dist/agents/subagent.d.ts +5 -3
- package/dist/agents/subagent.js +20 -14
- package/dist/core/ask-unresolvable-notice.d.ts +52 -0
- package/dist/core/ask-unresolvable-notice.js +25 -0
- package/dist/core/auto-mode.d.ts +62 -3
- package/dist/core/auto-mode.js +31 -0
- package/dist/core/checkpoint-store.d.ts +14 -0
- package/dist/core/checkpoint-store.js +2 -1
- package/dist/core/engine-notice.d.ts +28 -7
- package/dist/core/gate-lanes.js +15 -0
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +4 -0
- package/dist/core/hooks.d.ts +24 -1
- package/dist/core/hooks.js +2 -0
- package/dist/core/permission-rule-model.d.ts +51 -16
- package/dist/core/permission-rule-model.js +55 -21
- package/dist/core/permission-rules.d.ts +6 -4
- package/dist/core/permission-rules.js +14 -14
- package/dist/core/roles.d.ts +8 -0
- package/dist/core/runner/contracts.d.ts +29 -4
- package/dist/core/runner/denial-limit-arms.d.ts +14 -3
- package/dist/core/runner/denial-limit-arms.js +15 -5
- package/dist/core/runner/permission-rule-lanes.d.ts +7 -1
- package/dist/core/runner/permission-rule-lanes.js +9 -3
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +1 -1
- package/dist/core/runner/prepare-caps-and-workflow.js +14 -4
- package/dist/core/runner/prepare-gate-stations.d.ts +3 -2
- package/dist/core/runner/prepare-gate-stations.js +3 -0
- package/dist/core/runner/prepare-policy-chain.js +7 -6
- package/dist/core/runner/prepare-wiring-manifest.d.ts +1 -1
- package/dist/core/runner/prepare-wiring-manifest.js +8 -1
- package/dist/core/runner/runtask.d.ts +34 -32
- package/dist/core/runner/runtask.js +64 -34
- package/dist/core/runner-deps.d.ts +9 -2
- package/dist/core/swappable-deps.d.ts +90 -0
- package/dist/core/swappable-deps.js +55 -0
- package/dist/core/tool-policy.d.ts +26 -0
- package/dist/core/tool-policy.js +5 -1
- package/dist/core/wiring-manifest.d.ts +15 -1
- package/dist/core/wiring-manifest.js +10 -2
- package/dist/core/workflow-journal-store.d.ts +21 -2
- package/dist/core/workflow-journal-store.js +1 -1
- package/dist/engine/execution-env/node-execution-env.d.ts +2 -0
- package/dist/engine/execution-env/node-execution-env.js +2 -1
- package/dist/engine/harness/types.d.ts +11 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +5 -3
- package/dist/orchestration/run-workflow-tool.d.ts +17 -0
- package/dist/orchestration/run-workflow-tool.js +12 -0
- package/dist/orchestration/workflow-observe.d.ts +1 -1
- package/dist/orchestration/workflow-observe.js +2 -0
- package/dist/orchestration/workflow-types.d.ts +37 -2
- package/dist/orchestration/workflow-types.js +16 -0
- package/dist/orchestration/workflow.d.ts +41 -2
- package/dist/orchestration/workflow.js +316 -48
- package/dist/stores/file/workflow-journal-store.js +10 -3
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +51 -5
|
@@ -11,14 +11,14 @@ import { combinePolicies, createAllowDenyPolicy } from "../core/tool-policy.js";
|
|
|
11
11
|
import { createSafeNotifier } from "../core/safe-notify.js";
|
|
12
12
|
import { callKeyOrdinal, oversizeJournalResult, journalOversizeTombstone, JOURNAL_OVERSIZE_ERROR_CODE, MAX_JOURNAL_RESULT_BYTES } from "../core/workflow-journal-store.js";
|
|
13
13
|
import { isWorkflowRunActive, closeWorkflowChannel, markWorkflowActive, publishWorkflowEvent } from "./workflow-observe.js";
|
|
14
|
-
import { isDurablePause
|
|
14
|
+
import { isDurablePause } from "../agents/suspend-guard.js";
|
|
15
15
|
import { RUNNING_AGENT_OBSERVE_EVERY_BEATS } from "../config/defaults.js";
|
|
16
16
|
import { boundInputHashOf } from "../core/canonical-json.js";
|
|
17
17
|
import { boundedRedactedSummary } from "../core/untrusted-egress.js";
|
|
18
18
|
import { delimitUntrusted } from "../core/untrusted-text.js";
|
|
19
19
|
import { OUTPUT_TOOL_NAME } from "../core/runner/synthetic-tools.js";
|
|
20
20
|
import { compileOutputSchema } from "../core/runner/strict-output-schema.js";
|
|
21
|
-
import { WorkflowBudgetExceededError, WorkflowNestingError, WorkflowAgentBlockedError, WorkflowAgentSchemaError, WorkflowAgentStalledError, WorkflowMaxAgentsError, WORKFLOW_SPAWN_BLOCKED_ERROR_CODE } from "./workflow-types.js";
|
|
21
|
+
import { WorkflowBudgetExceededError, WorkflowNestingError, WorkflowAgentBlockedError, WorkflowAgentParkedError, WorkflowAgentSchemaError, WorkflowAgentStalledError, WorkflowMaxAgentsError, WORKFLOW_SPAWN_BLOCKED_ERROR_CODE } from "./workflow-types.js";
|
|
22
22
|
import { WorkflowScriptError } from "./workflow-meta.js";
|
|
23
23
|
export * from "./workflow-types.js";
|
|
24
24
|
const MAX_TRANSCRIPT_CHARS = 4000;
|
|
@@ -206,10 +206,9 @@ export class WorkflowJournalIncompatibleError extends Error {
|
|
|
206
206
|
fromRunId;
|
|
207
207
|
ordinal;
|
|
208
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
|
-
"
|
|
212
|
-
"start a fresh run instead of resuming this journal.");
|
|
209
|
+
constructor(fromRunId, ordinal, reason) {
|
|
210
|
+
super(`startWorkflow: resume from "${fromRunId}" was REFUSED — ${reason ?? `journal entry #${ordinal} carries no terminal cause (a journal written before TaskResult.terminal, or a malformed entry)`}. ` +
|
|
211
|
+
"Completed work must not run again; start a fresh run instead of resuming this journal.");
|
|
213
212
|
this.fromRunId = fromRunId;
|
|
214
213
|
this.ordinal = ordinal;
|
|
215
214
|
this.name = "WorkflowJournalIncompatibleError";
|
|
@@ -448,6 +447,12 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
448
447
|
if (opts.resumeFromRunId !== undefined && opts.journalStore === undefined) {
|
|
449
448
|
throw new Error("runWorkflow: resumeFromRunId requires a journalStore to load the prior run's journal");
|
|
450
449
|
}
|
|
450
|
+
if (opts.defaultDurableApproval !== undefined && opts.store === undefined) {
|
|
451
|
+
throw new Error("runWorkflow: defaultDurableApproval requires a WorkflowRunStore — a workflow child's park is recorded on its wa* row, and without a store there is no durable row for a host to route the parked approval by");
|
|
452
|
+
}
|
|
453
|
+
if (opts.parkedResume !== undefined && opts.resumeFromRunId === undefined) {
|
|
454
|
+
throw new Error("runWorkflow: parkedResume drives a PARKED ordinal of a prior run and requires resumeFromRunId");
|
|
455
|
+
}
|
|
451
456
|
const now = opts.now ?? (() => Date.now());
|
|
452
457
|
const scope = opts.scope ?? "default";
|
|
453
458
|
const runId = opts.runId ?? randomUUID();
|
|
@@ -769,6 +774,60 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
769
774
|
let resumeClaim;
|
|
770
775
|
const replayByOrdinal = [];
|
|
771
776
|
let diverged = false;
|
|
777
|
+
let parkedLeg;
|
|
778
|
+
const parkedError = () => {
|
|
779
|
+
const p = parkedLeg;
|
|
780
|
+
return new WorkflowAgentParkedError(p.label, p.paused.terminal.gate.kind, p.paused.terminal.checkpointId);
|
|
781
|
+
};
|
|
782
|
+
const parkedResumeFor = (token) => opts.parkedResume?.find((d) => d.token === token);
|
|
783
|
+
const parkedCallChangedError = (ordinal) => Object.assign(new Error(`workflow resume: ordinal ${ordinal} is PARKED on an approval checkpoint, but the call at that ordinal changed (its call key differs from the journaled one) — ` +
|
|
784
|
+
"the script or its args were edited at a parked call, and a fresh spawn there would run beside the pinned child. Restore the call and resume again, or decide the checkpoint " +
|
|
785
|
+
"through the deployment's approval channel and start a fresh run."), { code: "workflow.parked_call_changed" });
|
|
786
|
+
let inFlightLegs = 0;
|
|
787
|
+
let legsIdleResolve;
|
|
788
|
+
const legStart = () => {
|
|
789
|
+
inFlightLegs += 1;
|
|
790
|
+
};
|
|
791
|
+
const legEnd = () => {
|
|
792
|
+
inFlightLegs -= 1;
|
|
793
|
+
if (inFlightLegs <= 0) {
|
|
794
|
+
inFlightLegs = 0;
|
|
795
|
+
const r = legsIdleResolve;
|
|
796
|
+
legsIdleResolve = undefined;
|
|
797
|
+
r?.();
|
|
798
|
+
}
|
|
799
|
+
};
|
|
800
|
+
const awaitLegsIdle = async (signal) => {
|
|
801
|
+
while (inFlightLegs > 0 && signal?.aborted !== true) {
|
|
802
|
+
await new Promise((resolve) => {
|
|
803
|
+
legsIdleResolve = resolve;
|
|
804
|
+
signal?.addEventListener("abort", () => resolve(), { once: true });
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
const writtenOrdinals = new Set();
|
|
809
|
+
const carryUnresolvedParks = async () => {
|
|
810
|
+
if (opts.resumeFromRunId === undefined || !journalStore)
|
|
811
|
+
return;
|
|
812
|
+
for (const e of replayByOrdinal) {
|
|
813
|
+
if (e?.parked === undefined)
|
|
814
|
+
continue;
|
|
815
|
+
const ord = callKeyOrdinal(e.callKey);
|
|
816
|
+
if (writtenOrdinals.has(ord))
|
|
817
|
+
continue;
|
|
818
|
+
if (workflowSignal.aborted) {
|
|
819
|
+
emitLogLine(`resume-journal: the prior run's PARKED leg at ordinal ${ord} was NOT carried into this run's journal (the run was cancelled or timed out first) — resume from the prior run, not from this one.`);
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
822
|
+
const outcome = await finalizeWithin(timers, journalAppendParked(e.callKey, e.parked).then(() => "ok", (err) => ({ err })), "timeout");
|
|
823
|
+
if (outcome !== "ok") {
|
|
824
|
+
const why = outcome === "timeout" ? `the append did not settle within ${WORKFLOW_RESUME_CLAIM_FINALIZE_TIMEOUT_MS}ms` : outcome.err instanceof Error ? outcome.err.message : String(outcome.err);
|
|
825
|
+
emitLogLine(`resume-journal: the prior run's PARKED leg at ordinal ${ord} could not be carried into this run's journal (${why}) — resume from the prior run, not from this one.`);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
const driveNeverStarted = (r) => r.terminal.kind === "failed" && (r.terminal.code === "resume.reopened_unstarted" || r.terminal.code === "resume.reopen_failed_unstarted");
|
|
830
|
+
const isPausedResult = (r) => isDurablePause(r.terminal);
|
|
772
831
|
let isolationAdvised = false;
|
|
773
832
|
const adviseIsolationIfConcurrent = (opts) => {
|
|
774
833
|
if (isolationAdvised || opts.isolation || sem.active <= 1)
|
|
@@ -783,6 +842,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
783
842
|
};
|
|
784
843
|
let journalTail = Promise.resolve();
|
|
785
844
|
const JOURNAL_DRAIN_MAX_MS = 5_000;
|
|
845
|
+
const durableApprovalInherit = (s) => s.durableApproval === undefined && opts.defaultDurableApproval !== undefined ? { durableApproval: { ...opts.defaultDurableApproval } } : {};
|
|
786
846
|
const journalDurableResult = (result) => {
|
|
787
847
|
const t = result.terminal;
|
|
788
848
|
if (t === undefined || t.kind !== "failed" || typeof t.message !== "string" || t.message === "")
|
|
@@ -821,8 +881,10 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
821
881
|
if (dbg)
|
|
822
882
|
console.error(`[sema:wf-journal] runId=${runId} callKey=${callKey} TOMBSTONE-ERROR ${err instanceof Error ? err.message : String(err)}`);
|
|
823
883
|
}
|
|
884
|
+
writtenOrdinals.add(callKeyOrdinal(callKey));
|
|
824
885
|
return;
|
|
825
886
|
}
|
|
887
|
+
writtenOrdinals.add(callKeyOrdinal(callKey));
|
|
826
888
|
const p = journalStore.append(runId, scope, { callKey, result: stored });
|
|
827
889
|
journalTail = journalTail.then(() => p).catch(() => undefined);
|
|
828
890
|
try {
|
|
@@ -836,6 +898,56 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
836
898
|
throw err;
|
|
837
899
|
}
|
|
838
900
|
};
|
|
901
|
+
const journalAppendParked = async (callKey, paused) => {
|
|
902
|
+
if (!journalStore)
|
|
903
|
+
return;
|
|
904
|
+
writtenOrdinals.add(callKeyOrdinal(callKey));
|
|
905
|
+
let lastErr;
|
|
906
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
907
|
+
const p = journalStore.append(runId, scope, { callKey, parked: paused });
|
|
908
|
+
journalTail = journalTail.then(() => p).catch(() => undefined);
|
|
909
|
+
try {
|
|
910
|
+
await p;
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
catch (err) {
|
|
914
|
+
lastErr = err;
|
|
915
|
+
if (attempt < 2)
|
|
916
|
+
await new Promise((r) => timers.setTimeout(r, 25 * (attempt + 1)));
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
throw lastErr;
|
|
920
|
+
};
|
|
921
|
+
const parkAgentLeg = async (rec, paused, activityTail, chargeLive) => {
|
|
922
|
+
const s = paused.stats;
|
|
923
|
+
accumulateStats(paused, chargeLive);
|
|
924
|
+
rec.status = "parked";
|
|
925
|
+
rec.parkedCheckpointToken = paused.terminal.token;
|
|
926
|
+
rec.taskStatus = terminalProjection(paused.terminal).status;
|
|
927
|
+
if (paused.sessionId)
|
|
928
|
+
rec.sessionId = paused.sessionId;
|
|
929
|
+
setLegRunId(rec, paused);
|
|
930
|
+
rec.endedAt = now();
|
|
931
|
+
rec.stats = { tokens: s.tokens ?? 0, turns: s.turns ?? 0, costMicroUsd: s.costMicroUsd };
|
|
932
|
+
if (s.toolCalls !== undefined)
|
|
933
|
+
rec.toolCalls = s.toolCalls;
|
|
934
|
+
if (activityTail.length > 0)
|
|
935
|
+
rec.activity = activityTail.slice();
|
|
936
|
+
foldLegApprovalReport(rec, paused);
|
|
937
|
+
emit({ type: "agent_end", runId, label: rec.label, phase: rec.phase, ...(rec.groupId !== undefined ? { groupId: rec.groupId } : {}), status: "parked", ...(paused.model !== undefined ? { modelResolved: paused.model } : {}), ts: rec.endedAt });
|
|
938
|
+
void persist("update");
|
|
939
|
+
bceLive.delete(waIdOf(rec.callKey));
|
|
940
|
+
if (parkedLeg === undefined)
|
|
941
|
+
parkedLeg = { label: rec.label, callKey: rec.callKey, paused };
|
|
942
|
+
try {
|
|
943
|
+
await journalAppendParked(rec.callKey, paused);
|
|
944
|
+
}
|
|
945
|
+
catch (err) {
|
|
946
|
+
emitRunLog(`resume-journal: the PARKED leg at ordinal ${callKeyOrdinal(rec.callKey)} ("${rec.label.slice(0, 80)}") could not be recorded (${err instanceof Error ? err.message : String(err)}) — ` +
|
|
947
|
+
"a resume of this run will NOT find the park at its ordinal; decide the checkpoint through the deployment's approval channel and start a fresh run instead of resuming this one.");
|
|
948
|
+
}
|
|
949
|
+
throw parkedError();
|
|
950
|
+
};
|
|
839
951
|
const createActivityCapture = (callKey, label, groupId, rec) => {
|
|
840
952
|
const tail = [];
|
|
841
953
|
let beatCount = 0;
|
|
@@ -1028,13 +1140,22 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1028
1140
|
const phaseInstance = resolveAgentPhase(agentOpts.phase);
|
|
1029
1141
|
const groupId = currentGroup;
|
|
1030
1142
|
const typeDefModel = agentOpts.agentType !== undefined ? agentRegistry.find((d) => d.name === agentOpts.agentType)?.model : undefined;
|
|
1031
|
-
|
|
1143
|
+
let inheritedModelSnap;
|
|
1144
|
+
let inheritedModelError;
|
|
1145
|
+
if (spec.model === undefined && typeDefModel === undefined) {
|
|
1146
|
+
try {
|
|
1147
|
+
inheritedModelSnap = opts.defaultModel?.();
|
|
1148
|
+
}
|
|
1149
|
+
catch (err) {
|
|
1150
|
+
inheritedModelError = err instanceof Error ? err : new Error(String(err));
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1032
1153
|
const specForIdentity = inheritedModelSnap !== undefined ? { ...spec, model: inheritedModelSnap } : spec;
|
|
1033
1154
|
const callKey = workflowAgentCallKey(run.agents.length, specForIdentity, agentOpts);
|
|
1034
1155
|
const prompt = boundedRedactedSummary(spec.systemPrompt ? `${spec.systemPrompt}\n\n${spec.objective}` : spec.objective, MAX_TRANSCRIPT_CHARS);
|
|
1035
1156
|
const specForLabel = spec.model === undefined && typeDefModel !== undefined ? { ...spec, model: typeDefModel } : specForIdentity;
|
|
1036
1157
|
const model = workflowModelLabel(specForLabel, modelCatalog);
|
|
1037
|
-
return { label, phase, phaseInstance, groupId, inheritedModelSnap, callKey, prompt, model };
|
|
1158
|
+
return { label, phase, phaseInstance, groupId, inheritedModelSnap, inheritedModelError, callKey, prompt, model };
|
|
1038
1159
|
};
|
|
1039
1160
|
const reviewSpawnBeforeLaunch = async (lane, label, callKey, runSpec, effectiveSignal) => {
|
|
1040
1161
|
const review = opts.autoModeReview;
|
|
@@ -1181,10 +1302,35 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1181
1302
|
},
|
|
1182
1303
|
async agent(spec, agentOpts = {}) {
|
|
1183
1304
|
const effectiveSignal = assertAgentSpawnAllowed("ctx.agent", spec, agentOpts);
|
|
1184
|
-
const { label, phase, phaseInstance, groupId, inheritedModelSnap, callKey, prompt, model } = prepareAgentCall(spec, agentOpts);
|
|
1305
|
+
const { label, phase, phaseInstance, groupId, inheritedModelSnap, inheritedModelError, callKey, prompt, model } = prepareAgentCall(spec, agentOpts);
|
|
1306
|
+
let drive;
|
|
1307
|
+
const cached = opts.resumeFromRunId !== undefined ? replayByOrdinal[run.agents.length] : undefined;
|
|
1308
|
+
if (cached?.parked !== undefined) {
|
|
1309
|
+
if (cached.callKey !== callKey)
|
|
1310
|
+
throw parkedCallChangedError(run.agents.length);
|
|
1311
|
+
{
|
|
1312
|
+
const decision = parkedResumeFor(cached.parked.terminal.token);
|
|
1313
|
+
if (decision === undefined) {
|
|
1314
|
+
if (finalized)
|
|
1315
|
+
return cached.parked;
|
|
1316
|
+
const at = now();
|
|
1317
|
+
const reparkRec = { label, callKey, ...(groupId !== undefined ? { groupId } : {}), phase, prompt, ...(cached.parked.model !== undefined ? { model: cached.parked.model } : {}), replayed: true, status: "running", queuedAt: at, startedAt: at };
|
|
1318
|
+
if (run.resume)
|
|
1319
|
+
run.resume.replayed += 1;
|
|
1320
|
+
run.agents.push(reparkRec);
|
|
1321
|
+
if (phaseInstance)
|
|
1322
|
+
agentPhaseOf.set(reparkRec, phaseInstance);
|
|
1323
|
+
emit({ type: "agent_start", runId, label, phase, ...(groupId !== undefined ? { groupId } : {}), callKey, prompt, ...(cached.parked.model !== undefined ? { model: cached.parked.model } : {}), replayed: true, ts: at });
|
|
1324
|
+
bceSpawn(callKey, label, agentOpts.agentType, true, cached.parked.sessionId || undefined, cached.parked.model);
|
|
1325
|
+
return await parkAgentLeg(reparkRec, cached.parked, [], false);
|
|
1326
|
+
}
|
|
1327
|
+
drive = { token: decision.token, outcome: decision.outcome, ...(decision.inheritedGate !== undefined ? { inheritedGate: decision.inheritedGate } : {}), sessionId: cached.parked.sessionId || undefined, paused: cached.parked };
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
if (parkedLeg !== undefined && drive === undefined)
|
|
1331
|
+
throw parkedError();
|
|
1185
1332
|
if (opts.resumeFromRunId !== undefined && !diverged) {
|
|
1186
|
-
|
|
1187
|
-
if (cached && cached.callKey === callKey && TERMINAL_CAUSE_IS_REPLAYABLE[cached.result.terminal.kind]) {
|
|
1333
|
+
if (drive === undefined && cached && cached.callKey === callKey && cached.result !== undefined && TERMINAL_CAUSE_IS_REPLAYABLE[cached.result.terminal.kind]) {
|
|
1188
1334
|
if (finalized)
|
|
1189
1335
|
return cached.result;
|
|
1190
1336
|
const r = cached.result;
|
|
@@ -1228,18 +1374,20 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1228
1374
|
}
|
|
1229
1375
|
return r;
|
|
1230
1376
|
}
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
?
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1377
|
+
if (drive === undefined) {
|
|
1378
|
+
noteDivergence(run.agents.length, cached === undefined
|
|
1379
|
+
? run.resume?.journalEntries === 0
|
|
1380
|
+
? "the journal loaded no entries"
|
|
1381
|
+
: run.agents.length >= replayByOrdinal.length
|
|
1382
|
+
? "a new call past the journaled prefix — the prior run ended before this point (the intended extend-the-script resume shape)"
|
|
1383
|
+
: "no journal entry at this ordinal (never recorded, e.g. an oversize result under a pre-tombstone engine, or dropped)"
|
|
1384
|
+
: cached.callKey !== callKey
|
|
1385
|
+
? "the call key changed — the script or its args differ here"
|
|
1386
|
+
: cached.result?.terminal?.kind === "failed" && cached.result.terminal.code === JOURNAL_OVERSIZE_ERROR_CODE
|
|
1387
|
+
? "the prior result exceeded the journal size cap and was never cached"
|
|
1388
|
+
: `the journaled result was ${cached.result?.terminal?.kind ?? "(no terminal record)"}, not completed`);
|
|
1389
|
+
diverged = true;
|
|
1390
|
+
}
|
|
1243
1391
|
}
|
|
1244
1392
|
if (budgetTotal !== null && spent() >= budgetTotal) {
|
|
1245
1393
|
throw new WorkflowBudgetExceededError(spent(), budgetTotal);
|
|
@@ -1251,6 +1399,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1251
1399
|
agentPhaseOf.set(rec, phaseInstance);
|
|
1252
1400
|
emit({ type: "agent_start", runId, label, phase, ...(groupId !== undefined ? { groupId } : {}), callKey, prompt, ...(model !== undefined ? { model } : {}), queuedAt: rec.queuedAt, ts: rec.queuedAt });
|
|
1253
1401
|
void persist("update");
|
|
1402
|
+
legStart();
|
|
1254
1403
|
const release = await sem.acquire();
|
|
1255
1404
|
adviseIsolationIfConcurrent(agentOpts);
|
|
1256
1405
|
let result;
|
|
@@ -1259,19 +1408,24 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1259
1408
|
throw new Error("workflow aborted");
|
|
1260
1409
|
if (finalized)
|
|
1261
1410
|
throw new Error("workflow run already finalized — ctx.agent cannot spawn after the run ended");
|
|
1411
|
+
if (parkedLeg !== undefined && drive === undefined)
|
|
1412
|
+
throw parkedError();
|
|
1262
1413
|
if (budgetTotal !== null && spent() >= budgetTotal) {
|
|
1263
1414
|
throw new WorkflowBudgetExceededError(spent(), budgetTotal);
|
|
1264
1415
|
}
|
|
1265
1416
|
rec.startedAt = now();
|
|
1266
|
-
const bornChildSessionId = resolveChildSessionIdAtSpawn(spec);
|
|
1417
|
+
const bornChildSessionId = drive !== undefined ? drive.sessionId : resolveChildSessionIdAtSpawn(spec);
|
|
1267
1418
|
bceSpawn(callKey, label, agentOpts.agentType, false, bornChildSessionId, model);
|
|
1419
|
+
if (inheritedModelError !== undefined)
|
|
1420
|
+
throw inheritedModelError;
|
|
1268
1421
|
const typedSpec0 = applyWorkflowAgentType(spec, agentOpts.agentType, agentRegistry);
|
|
1269
1422
|
const typedSpec = typedSpec0.model === undefined && inheritedModelSnap !== undefined ? { ...typedSpec0, model: inheritedModelSnap } : typedSpec0;
|
|
1270
1423
|
const framedSpec = withWorkflowChildPersona(typedSpec, agentOpts.schema ?? typedSpec.outputSchema);
|
|
1271
1424
|
const authInherit = framedSpec.getApiKeyAndHeaders === undefined && opts.defaultGetApiKeyAndHeaders !== undefined ? { getApiKeyAndHeaders: opts.defaultGetApiKeyAndHeaders } : {};
|
|
1425
|
+
const parkInherit = durableApprovalInherit(framedSpec);
|
|
1272
1426
|
const preReviewSpec = agentOpts.schema
|
|
1273
|
-
? { ...framedSpec, ...authInherit, signal: effectiveSignal, outputSchema: agentOpts.schema }
|
|
1274
|
-
: { ...framedSpec, ...authInherit, signal: effectiveSignal };
|
|
1427
|
+
? { ...framedSpec, ...authInherit, ...parkInherit, signal: effectiveSignal, outputSchema: agentOpts.schema }
|
|
1428
|
+
: { ...framedSpec, ...authInherit, ...parkInherit, signal: effectiveSignal };
|
|
1275
1429
|
const runSpec = await reviewSpawnBeforeLaunch("ctx.agent", label, callKey, preReviewSpec, effectiveSignal);
|
|
1276
1430
|
const enrichedForward = opts.onForwardEvent !== undefined
|
|
1277
1431
|
? (e) => {
|
|
@@ -1308,7 +1462,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1308
1462
|
const onCallerAbort = () => attemptCtl.abort(new Error("workflow aborted"));
|
|
1309
1463
|
effectiveSignal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
1310
1464
|
armWatchdog();
|
|
1311
|
-
const attemptSessionId = attempts === 1 ? bornChildSessionId : resolveChildSessionIdAtSpawn(runSpec);
|
|
1465
|
+
const attemptSessionId = drive !== undefined ? drive.sessionId : attempts === 1 ? bornChildSessionId : resolveChildSessionIdAtSpawn(runSpec);
|
|
1312
1466
|
const attemptSpec = attemptSessionId !== undefined ? { ...runSpec, sessionId: attemptSessionId, signal: attemptCtl.signal } : { ...runSpec, signal: attemptCtl.signal };
|
|
1313
1467
|
if (attemptSessionId !== undefined && !finalized) {
|
|
1314
1468
|
rec.sessionId = attemptSessionId;
|
|
@@ -1362,10 +1516,15 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1362
1516
|
activeUsageBeats.add(activeBeat);
|
|
1363
1517
|
try {
|
|
1364
1518
|
attemptResult = await workflowDepthStore.run({ depth: depth + 1 }, async () => {
|
|
1365
|
-
if (typeof runner.runTaskStream !== "function") {
|
|
1519
|
+
if (drive === undefined && typeof runner.runTaskStream !== "function") {
|
|
1366
1520
|
return runner.runTask(attemptSpec, attemptInternals);
|
|
1367
1521
|
}
|
|
1368
|
-
const stream =
|
|
1522
|
+
const stream = drive === undefined
|
|
1523
|
+
? runner.runTaskStream(attemptSpec, undefined, attemptInternals)
|
|
1524
|
+
: await (() => {
|
|
1525
|
+
const { objective: _o, sessionId: _s, ...resumeConfig } = attemptSpec;
|
|
1526
|
+
return runner.resumeStream(drive.token, drive.outcome, resumeConfig, { ...attemptInternals, ...(drive.inheritedGate !== undefined ? { inheritedGate: drive.inheritedGate } : {}) });
|
|
1527
|
+
})();
|
|
1369
1528
|
for await (const ev of stream) {
|
|
1370
1529
|
if (ev.type === "text_delta" || ev.type === "reasoning_delta")
|
|
1371
1530
|
onStreamProgress();
|
|
@@ -1398,7 +1557,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1398
1557
|
}
|
|
1399
1558
|
throw new Error("workflow aborted");
|
|
1400
1559
|
}
|
|
1401
|
-
if (stallFired && attemptResult?.terminal.kind !== "completed") {
|
|
1560
|
+
if (drive === undefined && stallFired && attemptResult?.terminal.kind !== "completed") {
|
|
1402
1561
|
if (attemptResult !== undefined && !finalized)
|
|
1403
1562
|
accumulateStats(attemptResult, true);
|
|
1404
1563
|
if (attemptResult !== undefined && !finalized)
|
|
@@ -1427,7 +1586,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1427
1586
|
(result.stats.outputTokens ?? Number.POSITIVE_INFINITY) < 50 &&
|
|
1428
1587
|
lastStopReason === undefined &&
|
|
1429
1588
|
durationMs > stallMs * 0.5;
|
|
1430
|
-
if (degraded && attempts === 1 && !throttleRetried) {
|
|
1589
|
+
if (degraded && attempts === 1 && !throttleRetried && drive === undefined) {
|
|
1431
1590
|
throttleRetried = true;
|
|
1432
1591
|
lastAttemptReason = "throttled";
|
|
1433
1592
|
emitRunLog(`[${label}] throttled response (${(result.result ?? "").trim().length} chars in ${Math.round(durationMs / 1000)}s) — sleeping ${Math.round(throttleBackoffMs / 1000)}s before retry`);
|
|
@@ -1463,6 +1622,10 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1463
1622
|
}
|
|
1464
1623
|
}
|
|
1465
1624
|
catch (err) {
|
|
1625
|
+
if (drive !== undefined && !finalized && !(err instanceof WorkflowAgentParkedError)) {
|
|
1626
|
+
emitRunLog(`[park] the parked leg "${label}" could not be driven — the resume was refused before the checkpoint was consumed (${err instanceof Error ? err.message : String(err)}); the park stands.`);
|
|
1627
|
+
return await parkAgentLeg(rec, drive.paused, activityTail, false);
|
|
1628
|
+
}
|
|
1466
1629
|
if (!finalized) {
|
|
1467
1630
|
rec.status = "failed";
|
|
1468
1631
|
rec.endedAt = now();
|
|
@@ -1490,6 +1653,8 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1490
1653
|
rec.errorCode = err.code;
|
|
1491
1654
|
rec.errorMessage = boundedRedactedSummary(err.message, MAX_TRANSCRIPT_CHARS);
|
|
1492
1655
|
}
|
|
1656
|
+
if (rec.errorMessage === undefined && err instanceof Error && err.message !== "")
|
|
1657
|
+
rec.errorMessage = boundedRedactedSummary(err.message, MAX_TRANSCRIPT_CHARS);
|
|
1493
1658
|
if (err instanceof WorkflowAgentStalledError && err.attempts > 1) {
|
|
1494
1659
|
rec.attempts = err.attempts;
|
|
1495
1660
|
rec.lastAttemptReason = "stalled";
|
|
@@ -1507,7 +1672,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1507
1672
|
},
|
|
1508
1673
|
stats: { turns: 0, tokens: 0, costMicroUsd: 0 },
|
|
1509
1674
|
}, label).catch(() => undefined);
|
|
1510
|
-
if (!(err instanceof WorkflowBudgetExceededError && rec.startedAt === undefined))
|
|
1675
|
+
if (!((err instanceof WorkflowBudgetExceededError || err instanceof WorkflowAgentParkedError) && rec.startedAt === undefined))
|
|
1511
1676
|
await journaled;
|
|
1512
1677
|
bceTerminal(callKey, "failed", rec.output ?? (err instanceof Error ? err.message : String(err)), rec.sessionId, rec.stats);
|
|
1513
1678
|
}
|
|
@@ -1515,11 +1680,16 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1515
1680
|
}
|
|
1516
1681
|
finally {
|
|
1517
1682
|
release();
|
|
1683
|
+
legEnd();
|
|
1518
1684
|
}
|
|
1519
1685
|
if (finalized)
|
|
1520
1686
|
return result;
|
|
1521
|
-
if (
|
|
1522
|
-
|
|
1687
|
+
if (isPausedResult(result)) {
|
|
1688
|
+
return await parkAgentLeg(rec, result, activityTail, true);
|
|
1689
|
+
}
|
|
1690
|
+
if (drive !== undefined && driveNeverStarted(result)) {
|
|
1691
|
+
emitRunLog(`[park] the parked leg "${label}" was driven but the approved action never started (${result.terminal.kind === "failed" ? result.terminal.code : ""}); the park stands.`);
|
|
1692
|
+
return await parkAgentLeg(rec, drive.paused, activityTail, false);
|
|
1523
1693
|
}
|
|
1524
1694
|
await settleAgentResult(rec, result, activityTail, agentOpts, "awaited");
|
|
1525
1695
|
return result;
|
|
@@ -1529,9 +1699,31 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1529
1699
|
if (budgetTotal !== null && spent() >= budgetTotal) {
|
|
1530
1700
|
throw new WorkflowBudgetExceededError(spent(), budgetTotal);
|
|
1531
1701
|
}
|
|
1532
|
-
const { label, phase, phaseInstance, groupId, inheritedModelSnap, callKey, prompt, model } = prepareAgentCall(spec, agentOpts);
|
|
1533
|
-
|
|
1534
|
-
|
|
1702
|
+
const { label, phase, phaseInstance, groupId, inheritedModelSnap, inheritedModelError, callKey, prompt, model } = prepareAgentCall(spec, agentOpts);
|
|
1703
|
+
let drive;
|
|
1704
|
+
const parkedHere = opts.resumeFromRunId !== undefined ? replayByOrdinal[run.agents.length] : undefined;
|
|
1705
|
+
if (parkedHere?.parked !== undefined) {
|
|
1706
|
+
if (parkedHere.callKey !== callKey)
|
|
1707
|
+
throw parkedCallChangedError(run.agents.length);
|
|
1708
|
+
const decision = parkedResumeFor(parkedHere.parked.terminal.token);
|
|
1709
|
+
if (decision === undefined) {
|
|
1710
|
+
const at = now();
|
|
1711
|
+
const reparkRec = { label, callKey, ...(groupId !== undefined ? { groupId } : {}), phase, prompt, ...(parkedHere.parked.model !== undefined ? { model: parkedHere.parked.model } : {}), replayed: true, status: "running", queuedAt: at, startedAt: at };
|
|
1712
|
+
if (run.resume)
|
|
1713
|
+
run.resume.replayed += 1;
|
|
1714
|
+
run.agents.push(reparkRec);
|
|
1715
|
+
if (phaseInstance)
|
|
1716
|
+
agentPhaseOf.set(reparkRec, phaseInstance);
|
|
1717
|
+
emit({ type: "agent_start", runId, label, phase, ...(groupId !== undefined ? { groupId } : {}), callKey, prompt, ...(parkedHere.parked.model !== undefined ? { model: parkedHere.parked.model } : {}), replayed: true, ts: at });
|
|
1718
|
+
bceSpawn(callKey, label, agentOpts.agentType, true, parkedHere.parked.sessionId || undefined, parkedHere.parked.model);
|
|
1719
|
+
return await parkAgentLeg(reparkRec, parkedHere.parked, [], false);
|
|
1720
|
+
}
|
|
1721
|
+
drive = { token: decision.token, outcome: decision.outcome, ...(decision.inheritedGate !== undefined ? { inheritedGate: decision.inheritedGate } : {}), sessionId: parkedHere.parked.sessionId || undefined, paused: parkedHere.parked };
|
|
1722
|
+
}
|
|
1723
|
+
if (drive === undefined) {
|
|
1724
|
+
noteDivergence(run.agents.length, "ctx.agentStream results are never replayed");
|
|
1725
|
+
diverged = true;
|
|
1726
|
+
}
|
|
1535
1727
|
const rec = { label, callKey, ...(groupId !== undefined ? { groupId } : {}), phase, prompt, ...(model !== undefined ? { model } : {}), status: "running", queuedAt: now() };
|
|
1536
1728
|
const { tail: activityTail, onActivity } = createActivityCapture(callKey, label, groupId, rec);
|
|
1537
1729
|
run.agents.push(rec);
|
|
@@ -1539,6 +1731,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1539
1731
|
agentPhaseOf.set(rec, phaseInstance);
|
|
1540
1732
|
emit({ type: "agent_start", runId, label, phase, ...(groupId !== undefined ? { groupId } : {}), callKey, prompt, ...(model !== undefined ? { model } : {}), queuedAt: rec.queuedAt, ts: rec.queuedAt });
|
|
1541
1733
|
void persist("update");
|
|
1734
|
+
legStart();
|
|
1542
1735
|
const release = await sem.acquire();
|
|
1543
1736
|
adviseIsolationIfConcurrent(agentOpts);
|
|
1544
1737
|
let released = false;
|
|
@@ -1556,17 +1749,34 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1556
1749
|
void persist("update");
|
|
1557
1750
|
}
|
|
1558
1751
|
};
|
|
1752
|
+
const releaseLeg = () => {
|
|
1753
|
+
if (!released)
|
|
1754
|
+
legEnd();
|
|
1755
|
+
releaseOnce();
|
|
1756
|
+
};
|
|
1559
1757
|
if (effectiveSignal?.aborted || finalized) {
|
|
1560
1758
|
recordFailed();
|
|
1561
|
-
|
|
1759
|
+
releaseLeg();
|
|
1562
1760
|
throw new Error(finalized ? "workflow run already finalized — ctx.agentStream cannot spawn after the run ended" : "workflow aborted");
|
|
1563
1761
|
}
|
|
1762
|
+
if (parkedLeg !== undefined && drive === undefined) {
|
|
1763
|
+
const parked = parkedError();
|
|
1764
|
+
rec.errorMessage = boundedRedactedSummary(parked.message, MAX_TRANSCRIPT_CHARS);
|
|
1765
|
+
recordFailed();
|
|
1766
|
+
releaseLeg();
|
|
1767
|
+
throw parked;
|
|
1768
|
+
}
|
|
1564
1769
|
if (budgetTotal !== null && spent() >= budgetTotal) {
|
|
1770
|
+
if (drive !== undefined) {
|
|
1771
|
+
emitRunLog(`[park] the parked leg "${label}" could not be driven — the run's token ceiling was reached before its turn; the park stands.`);
|
|
1772
|
+
releaseLeg();
|
|
1773
|
+
return await parkAgentLeg(rec, drive.paused, [], false);
|
|
1774
|
+
}
|
|
1565
1775
|
const refusal = new WorkflowBudgetExceededError(spent(), budgetTotal);
|
|
1566
1776
|
rec.errorCode = refusal.code;
|
|
1567
1777
|
rec.errorMessage = boundedRedactedSummary(refusal.message, MAX_TRANSCRIPT_CHARS);
|
|
1568
1778
|
recordFailed();
|
|
1569
|
-
|
|
1779
|
+
releaseLeg();
|
|
1570
1780
|
void journalAppend(callKey, {
|
|
1571
1781
|
taskId: callKey,
|
|
1572
1782
|
sessionId: "",
|
|
@@ -1577,17 +1787,20 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1577
1787
|
throw refusal;
|
|
1578
1788
|
}
|
|
1579
1789
|
rec.startedAt = now();
|
|
1580
|
-
const childSessionId = resolveChildSessionIdAtSpawn(spec);
|
|
1790
|
+
const childSessionId = drive !== undefined ? drive.sessionId : resolveChildSessionIdAtSpawn(spec);
|
|
1581
1791
|
bceSpawn(callKey, label, agentOpts.agentType, false, childSessionId, model);
|
|
1582
1792
|
let stream;
|
|
1583
1793
|
try {
|
|
1794
|
+
if (inheritedModelError !== undefined)
|
|
1795
|
+
throw inheritedModelError;
|
|
1584
1796
|
const typedSpec0 = applyWorkflowAgentType(spec, agentOpts.agentType, agentRegistry);
|
|
1585
1797
|
const typedSpec = typedSpec0.model === undefined && inheritedModelSnap !== undefined ? { ...typedSpec0, model: inheritedModelSnap } : typedSpec0;
|
|
1586
1798
|
const framedSpec = withWorkflowChildPersona(typedSpec, agentOpts.schema ?? typedSpec.outputSchema);
|
|
1587
1799
|
const authInherit = framedSpec.getApiKeyAndHeaders === undefined && opts.defaultGetApiKeyAndHeaders !== undefined ? { getApiKeyAndHeaders: opts.defaultGetApiKeyAndHeaders } : {};
|
|
1800
|
+
const parkInherit = durableApprovalInherit(framedSpec);
|
|
1588
1801
|
const baseRunSpec = agentOpts.schema
|
|
1589
|
-
? { ...framedSpec, ...authInherit, signal: effectiveSignal, outputSchema: agentOpts.schema }
|
|
1590
|
-
: { ...framedSpec, ...authInherit, signal: effectiveSignal };
|
|
1802
|
+
? { ...framedSpec, ...authInherit, ...parkInherit, signal: effectiveSignal, outputSchema: agentOpts.schema }
|
|
1803
|
+
: { ...framedSpec, ...authInherit, ...parkInherit, signal: effectiveSignal };
|
|
1591
1804
|
const preReviewSpec = childSessionId !== undefined ? { ...baseRunSpec, sessionId: childSessionId } : baseRunSpec;
|
|
1592
1805
|
const runSpec = await reviewSpawnBeforeLaunch("ctx.agentStream", label, callKey, preReviewSpec, effectiveSignal);
|
|
1593
1806
|
const enrichedForwardS = opts.onForwardEvent !== undefined
|
|
@@ -1619,9 +1832,22 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1619
1832
|
}
|
|
1620
1833
|
: {}),
|
|
1621
1834
|
};
|
|
1622
|
-
stream =
|
|
1835
|
+
stream = drive === undefined
|
|
1836
|
+
? workflowDepthStore.run({ depth: depth + 1 }, () => runner.runTaskStream(runSpec, undefined, runInternals))
|
|
1837
|
+
: await workflowDepthStore.run({ depth: depth + 1 }, () => {
|
|
1838
|
+
const d = drive;
|
|
1839
|
+
const { objective: _o, sessionId: _s, ...resumeConfig } = runSpec;
|
|
1840
|
+
return runner.resumeStream(d.token, d.outcome, resumeConfig, { ...runInternals, ...(d.inheritedGate !== undefined ? { inheritedGate: d.inheritedGate } : {}) });
|
|
1841
|
+
});
|
|
1623
1842
|
}
|
|
1624
1843
|
catch (err) {
|
|
1844
|
+
if (drive !== undefined && !finalized) {
|
|
1845
|
+
emitRunLog(`[park] the parked leg "${label}" could not be driven — the resume was refused before the checkpoint was consumed (${err instanceof Error ? err.message : String(err)}); the park stands.`);
|
|
1846
|
+
releaseLeg();
|
|
1847
|
+
return await parkAgentLeg(rec, drive.paused, [], false);
|
|
1848
|
+
}
|
|
1849
|
+
if (!finalized && rec.errorMessage === undefined && err instanceof Error && err.message !== "")
|
|
1850
|
+
rec.errorMessage = boundedRedactedSummary(err.message, MAX_TRANSCRIPT_CHARS);
|
|
1625
1851
|
if (!finalized && err instanceof WorkflowAgentBlockedError) {
|
|
1626
1852
|
rec.errorCode = WORKFLOW_SPAWN_BLOCKED_ERROR_CODE;
|
|
1627
1853
|
rec.errorMessage = boundedRedactedSummary(err.message, MAX_TRANSCRIPT_CHARS);
|
|
@@ -1634,7 +1860,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1634
1860
|
}).catch(() => undefined);
|
|
1635
1861
|
}
|
|
1636
1862
|
recordFailed();
|
|
1637
|
-
|
|
1863
|
+
releaseLeg();
|
|
1638
1864
|
bceTerminal(callKey, "failed", err instanceof Error ? err.message : String(err), undefined, undefined);
|
|
1639
1865
|
throw err;
|
|
1640
1866
|
}
|
|
@@ -1674,7 +1900,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1674
1900
|
accumulateStats({ stats: partialSpend }, true);
|
|
1675
1901
|
}
|
|
1676
1902
|
recordFailed();
|
|
1677
|
-
|
|
1903
|
+
releaseLeg();
|
|
1678
1904
|
void journalAppend(callKey, {
|
|
1679
1905
|
taskId: callKey,
|
|
1680
1906
|
sessionId: "",
|
|
@@ -1687,11 +1913,15 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1687
1913
|
}
|
|
1688
1914
|
rollbackUsageBeat();
|
|
1689
1915
|
activeUsageBeats.delete(activeBeat);
|
|
1690
|
-
|
|
1916
|
+
releaseLeg();
|
|
1691
1917
|
if (finalized)
|
|
1692
1918
|
return result;
|
|
1693
|
-
if (
|
|
1694
|
-
|
|
1919
|
+
if (isPausedResult(result)) {
|
|
1920
|
+
return await parkAgentLeg(rec, result, activityTail, true);
|
|
1921
|
+
}
|
|
1922
|
+
if (drive !== undefined && driveNeverStarted(result)) {
|
|
1923
|
+
emitRunLog(`[park] the parked leg "${label}" was driven but the approved action never started (${result.terminal.kind === "failed" ? result.terminal.code : ""}); the park stands.`);
|
|
1924
|
+
return await parkAgentLeg(rec, drive.paused, activityTail, false);
|
|
1695
1925
|
}
|
|
1696
1926
|
await settleAgentResult(rec, result, activityTail, agentOpts, "best-effort");
|
|
1697
1927
|
return result;
|
|
@@ -1849,12 +2079,40 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1849
2079
|
}
|
|
1850
2080
|
const entries = await journalStore.load(opts.resumeFromRunId, scope);
|
|
1851
2081
|
for (const e of entries) {
|
|
2082
|
+
const parked = e.parked;
|
|
2083
|
+
if (parked !== undefined) {
|
|
2084
|
+
const pt = parked?.terminal;
|
|
2085
|
+
if (pt === null || typeof pt !== "object" || pt.kind !== "paused" || typeof pt.token !== "string" || pt.token.length === 0) {
|
|
2086
|
+
throw new WorkflowJournalIncompatibleError(clampRunIdText(opts.resumeFromRunId), callKeyOrdinal(e.callKey));
|
|
2087
|
+
}
|
|
2088
|
+
replayByOrdinal[callKeyOrdinal(e.callKey)] = e;
|
|
2089
|
+
continue;
|
|
2090
|
+
}
|
|
1852
2091
|
const t = e.result?.terminal;
|
|
1853
2092
|
if (t === null || typeof t !== "object" || !isTerminalCauseKind(t.kind)) {
|
|
1854
2093
|
throw new WorkflowJournalIncompatibleError(clampRunIdText(opts.resumeFromRunId), callKeyOrdinal(e.callKey));
|
|
1855
2094
|
}
|
|
1856
2095
|
replayByOrdinal[callKeyOrdinal(e.callKey)] = e;
|
|
1857
2096
|
}
|
|
2097
|
+
if (opts.store !== undefined) {
|
|
2098
|
+
let prior;
|
|
2099
|
+
try {
|
|
2100
|
+
prior = await opts.store.get(opts.resumeFromRunId);
|
|
2101
|
+
}
|
|
2102
|
+
catch (err) {
|
|
2103
|
+
throw new WorkflowJournalIncompatibleError(clampRunIdText(opts.resumeFromRunId), -1, `the prior run's record could not be read from the run store (${err instanceof Error ? err.message : String(err)}), so a lost park cannot be ruled out`);
|
|
2104
|
+
}
|
|
2105
|
+
if (prior !== null && prior.scope === scope) {
|
|
2106
|
+
for (const row of prior.agents) {
|
|
2107
|
+
if (row.status !== "parked")
|
|
2108
|
+
continue;
|
|
2109
|
+
const ord = callKeyOrdinal(row.callKey);
|
|
2110
|
+
if (replayByOrdinal[ord]?.parked === undefined) {
|
|
2111
|
+
throw new WorkflowJournalIncompatibleError(clampRunIdText(opts.resumeFromRunId), ord, `the prior run's agent #${ord} ("${row.label.slice(0, 80)}") is PARKED on an approval checkpoint but its journal entry is missing (the park was not recorded) — a resume would run that call live beside the pinned child`);
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
1858
2116
|
run.resume = {
|
|
1859
2117
|
fromRunId: clampRunIdText(opts.resumeFromRunId),
|
|
1860
2118
|
journalEntries: entries.length,
|
|
@@ -1895,6 +2153,11 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1895
2153
|
else {
|
|
1896
2154
|
result = await bodyPromise;
|
|
1897
2155
|
}
|
|
2156
|
+
if (parkedLeg !== undefined) {
|
|
2157
|
+
await awaitLegsIdle(workflowSignal);
|
|
2158
|
+
throw parkedError();
|
|
2159
|
+
}
|
|
2160
|
+
await carryUnresolvedParks();
|
|
1898
2161
|
if (maxResultChars !== undefined) {
|
|
1899
2162
|
let size = 0;
|
|
1900
2163
|
try {
|
|
@@ -1927,6 +2190,9 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1927
2190
|
return { result, runId, run };
|
|
1928
2191
|
}
|
|
1929
2192
|
catch (err) {
|
|
2193
|
+
if (parkedLeg !== undefined)
|
|
2194
|
+
await awaitLegsIdle(workflowSignal);
|
|
2195
|
+
await carryUnresolvedParks();
|
|
1930
2196
|
finalized = true;
|
|
1931
2197
|
const unsettledOnFailure = settleActiveUsageBeats();
|
|
1932
2198
|
stampBudgetOvershoot(unsettledOnFailure);
|
|
@@ -1935,6 +2201,8 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1935
2201
|
run.status = "failed";
|
|
1936
2202
|
run.completionId ??= uuidv7();
|
|
1937
2203
|
run.endedAt = now();
|
|
2204
|
+
if (parkedLeg !== undefined && !(err instanceof WorkflowAgentParkedError))
|
|
2205
|
+
err = parkedError();
|
|
1938
2206
|
run.error = err instanceof Error ? err.message : String(err);
|
|
1939
2207
|
closeAbandonedAgents(run.endedAt);
|
|
1940
2208
|
restampPhaseFailures();
|