@osolmaz/pi-workflows 0.16.4 → 0.16.6
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/README.md +5 -5
- package/dist/builtins/autoimplement.workflow.d.ts +5 -1
- package/dist/builtins/autoimplement.workflow.js +29 -10
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/client/client.js +3 -1
- package/dist/client/client.js.map +1 -1
- package/dist/client/protocol.d.ts +1 -1
- package/dist/client/protocol.js +18 -1
- package/dist/client/protocol.js.map +1 -1
- package/dist/client/view.d.ts +7 -5
- package/dist/extension/index.d.ts +2 -1
- package/dist/extension/index.js +115 -165
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/recorder.d.ts +1 -1
- package/dist/extension/recorder.js +2 -2
- package/dist/extension/recorder.js.map +1 -1
- package/dist/extension/step-message.d.ts +1 -1
- package/dist/extension/step-message.js +4 -4
- package/dist/extension/step-message.js.map +1 -1
- package/dist/extension/workflow-message-coordinator.d.ts +12 -1
- package/dist/extension/workflow-message-coordinator.js +65 -54
- package/dist/extension/workflow-message-coordinator.js.map +1 -1
- package/dist/resource-managers/index.d.ts +1 -2
- package/dist/resource-managers/index.js +1 -2
- package/dist/resource-managers/index.js.map +1 -1
- package/dist/resource-managers/sqlite.d.ts +2 -295
- package/dist/resource-managers/sqlite.js +71 -1528
- package/dist/resource-managers/sqlite.js.map +1 -1
- package/dist/resource-managers/types.d.ts +2 -7
- package/dist/server/resource-runner-entry.js +2 -6
- package/dist/server/resource-runner-entry.js.map +1 -1
- package/dist/server/rpc-bridge.js +1 -1
- package/dist/server/rpc-bridge.js.map +1 -1
- package/dist/server/rpc-executor.js +2 -4
- package/dist/server/rpc-executor.js.map +1 -1
- package/dist/server/server-entry.js +3 -0
- package/dist/server/server-entry.js.map +1 -1
- package/dist/server/server.d.ts +22 -8
- package/dist/server/server.js +498 -543
- package/dist/server/server.js.map +1 -1
- package/dist/server/state.d.ts +16 -29
- package/dist/server/state.js +105 -319
- package/dist/server/state.js.map +1 -1
- package/dist/server/view.d.ts +4 -5
- package/dist/server/view.js +51 -51
- package/dist/server/view.js.map +1 -1
- package/dist/server/workflow-runner-entry.d.ts +1 -1
- package/dist/server/workflow-runner-entry.js +1 -49
- package/dist/server/workflow-runner-entry.js.map +1 -1
- package/dist/server/workflow-runner-protocol.d.ts +2 -7
- package/dist/server/workflow-runner-protocol.js +5 -10
- package/dist/server/workflow-runner-protocol.js.map +1 -1
- package/dist/server/workflow-runner-store.d.ts +5 -6
- package/dist/server/workflow-runner-store.js +7 -14
- package/dist/server/workflow-runner-store.js.map +1 -1
- package/dist/state/attempt-time.d.ts +21 -0
- package/dist/state/attempt-time.js +95 -0
- package/dist/state/attempt-time.js.map +1 -0
- package/dist/state/json.d.ts +2 -1
- package/dist/state/json.js.map +1 -1
- package/dist/state/project-store.d.ts +39 -0
- package/dist/state/project-store.js +154 -0
- package/dist/state/project-store.js.map +1 -0
- package/dist/state/prune.d.ts +28 -0
- package/dist/state/prune.js +278 -83
- package/dist/state/prune.js.map +1 -1
- package/dist/state/schema.js +23 -23
- package/dist/state/workflow-messages.d.ts +1 -1
- package/dist/state/workflow-messages.js +3 -1
- package/dist/state/workflow-messages.js.map +1 -1
- package/dist/viewer/backup.js +5 -0
- package/dist/viewer/backup.js.map +1 -1
- package/dist/workflows/diagnostics.d.ts +8 -0
- package/dist/workflows/diagnostics.js +61 -0
- package/dist/workflows/diagnostics.js.map +1 -0
- package/dist/workflows/engine.d.ts +2 -15
- package/dist/workflows/engine.js +125 -245
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/human-decision.d.ts +3 -7
- package/dist/workflows/human-decision.js +2 -56
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +2 -1
- package/dist/workflows/index.js +1 -0
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/queue.d.ts +267 -0
- package/dist/workflows/queue.js +1248 -0
- package/dist/workflows/queue.js.map +1 -0
- package/dist/workflows/requests.d.ts +47 -0
- package/dist/workflows/requests.js +158 -0
- package/dist/workflows/requests.js.map +1 -0
- package/dist/workflows/schema.js +2 -4
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/store.d.ts +11 -16
- package/dist/workflows/store.js +174 -169
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/tool-input.d.ts +6 -9
- package/dist/workflows/tool-input.js +15 -12
- package/dist/workflows/tool-input.js.map +1 -1
- package/dist/workflows/transitions.d.ts +38 -0
- package/dist/workflows/transitions.js +276 -0
- package/dist/workflows/transitions.js.map +1 -0
- package/dist/workflows/types.d.ts +9 -42
- package/dist/workflows/workflow-message-content.d.ts +7 -1
- package/dist/workflows/workflow-message-content.js +22 -4
- package/dist/workflows/workflow-message-content.js.map +1 -1
- package/docs/2026-08-25-workflow-follow-ups.md +5 -5
- package/docs/2026-09-02-unify-workflow-messages-plan.md +2 -0
- package/docs/2026-09-04-workflow-run-state-plan.md +2 -0
- package/docs/2026-09-06-durable-execution-plan.md +176 -0
- package/docs/DEFERRED_TURNS.md +84 -105
- package/docs/HUMAN_DECISIONS.md +17 -19
- package/docs/HUMAN_DECISION_PRESENTATIONS.md +1 -1
- package/docs/SQLITE_STATE.md +87 -30
- package/docs/WORKFLOW_SERVER.md +50 -11
- package/docs/WORKFLOW_STEP_MESSAGES.md +72 -54
- package/docs/plans/2026-08-04-controller-runtime-plan.md +2 -0
- package/docs/plans/2026-08-21-deferred-turn-intents-plan.md +2 -0
- package/docs/plans/2026-08-27-workflow-terminal-restart-plan.md +2 -0
- package/docs/plans/2026-09-04-automatic-state-retention-plan.md +283 -0
- package/docs/workflows.md +109 -44
- package/examples/workflows/autoresearch.workflow.ts +0 -2
- package/examples/workflows/branch.workflow.ts +0 -4
- package/examples/workflows/echo.workflow.ts +0 -2
- package/examples/workflows/live-settings.workflow.ts +0 -2
- package/examples/workflows/two-turn.workflow.ts +0 -2
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/protocol/fixtures/client-v1.json +1 -1
- package/skills/pi-workflows/SKILL.md +8 -7
- package/src/builtins/autoimplement.workflow.ts +31 -10
- package/src/client/client.ts +3 -1
- package/src/client/protocol.ts +21 -1
- package/src/client/view.ts +9 -5
- package/src/extension/index.ts +117 -189
- package/src/extension/recorder.ts +2 -2
- package/src/extension/step-message.ts +4 -4
- package/src/extension/workflow-message-coordinator.ts +78 -50
- package/src/resource-managers/index.ts +1 -9
- package/src/resource-managers/sqlite.ts +330 -2655
- package/src/resource-managers/types.ts +2 -3
- package/src/server/resource-runner-entry.ts +2 -7
- package/src/server/rpc-bridge.ts +1 -1
- package/src/server/rpc-executor.ts +4 -9
- package/src/server/server-entry.ts +3 -0
- package/src/server/server.ts +568 -644
- package/src/server/state.ts +122 -466
- package/src/server/view.ts +56 -60
- package/src/server/workflow-runner-entry.ts +2 -55
- package/src/server/workflow-runner-protocol.ts +5 -15
- package/src/server/workflow-runner-store.ts +12 -27
- package/src/state/attempt-time.ts +122 -0
- package/src/state/json.ts +2 -1
- package/src/state/project-store.ts +218 -0
- package/src/state/prune.ts +408 -95
- package/src/state/schema.ts +23 -23
- package/src/state/workflow-messages.ts +4 -2
- package/src/viewer/backup.ts +7 -0
- package/src/workflows/diagnostics.ts +78 -0
- package/src/workflows/engine.ts +133 -291
- package/src/workflows/human-decision.ts +3 -81
- package/src/workflows/index.ts +8 -2
- package/src/workflows/queue.ts +2091 -0
- package/src/workflows/requests.ts +283 -0
- package/src/workflows/schema.ts +2 -6
- package/src/workflows/store.ts +223 -231
- package/src/workflows/tool-input.ts +20 -12
- package/src/workflows/transitions.ts +330 -0
- package/src/workflows/types.ts +9 -46
- package/src/workflows/workflow-message-content.ts +28 -4
- package/dist/resource-managers/runtime.d.ts +0 -59
- package/dist/resource-managers/runtime.js +0 -423
- package/dist/resource-managers/runtime.js.map +0 -1
- package/src/resource-managers/runtime.ts +0 -576
package/dist/workflows/engine.js
CHANGED
|
@@ -5,12 +5,14 @@ import { StaleResourceError } from "../state/mutation.js";
|
|
|
5
5
|
import { compileWorkflowDefinition, compositionMetadata, isCompiledWorkflow, } from "./composition.js";
|
|
6
6
|
import { CancelledError, errorMessage, isAbortLikeError, isClaimLostError, isRunParkedError, RunParkedError, TimeoutError, WorkflowSourceChangedError, } from "./errors.js";
|
|
7
7
|
import { resolveNext, resolveNextForOutcome, validateWorkflowDefinition } from "./graph.js";
|
|
8
|
-
import { createHumanDecisionRequest
|
|
8
|
+
import { createHumanDecisionRequest } from "./human-decision.js";
|
|
9
9
|
import { extractJsonValue } from "./json.js";
|
|
10
|
+
import { workflowRequestId } from "./requests.js";
|
|
10
11
|
import { parseWorkflowSettingsValue, resolveInitialWorkflowSettings, } from "./settings.js";
|
|
11
12
|
import { runShellAction, shellResultFromError } from "./shell.js";
|
|
12
13
|
import { RUN_STATE_SCHEMA, WorkflowRunStore, createDefinitionSnapshot, createRunId, } from "./store.js";
|
|
13
|
-
import {
|
|
14
|
+
import { executionTransition } from "./transitions.js";
|
|
15
|
+
import { UpdateRateLimiter, updateProjection, updateReceipt, validateWorkflowUpdate, } from "./updates.js";
|
|
14
16
|
const DEFAULT_NODE_TIMEOUT_MS = 15 * 60_000;
|
|
15
17
|
const DEFAULT_MAX_STEPS = 100;
|
|
16
18
|
const TITLE_TIMEOUT_MS = 30_000;
|
|
@@ -32,7 +34,6 @@ export class WorkflowEngine {
|
|
|
32
34
|
maxSteps;
|
|
33
35
|
onEvent;
|
|
34
36
|
onRunStarted;
|
|
35
|
-
onRunFinishing;
|
|
36
37
|
activeAbort = null;
|
|
37
38
|
activeAttempt;
|
|
38
39
|
updateLimiters = new Map();
|
|
@@ -40,7 +41,6 @@ export class WorkflowEngine {
|
|
|
40
41
|
cancelled = false;
|
|
41
42
|
parked = false;
|
|
42
43
|
paused = false;
|
|
43
|
-
presentationRequired = false;
|
|
44
44
|
wakePause = null;
|
|
45
45
|
constructor(options) {
|
|
46
46
|
this.executor = options.executor;
|
|
@@ -50,7 +50,6 @@ export class WorkflowEngine {
|
|
|
50
50
|
this.maxSteps = options.maxSteps ?? DEFAULT_MAX_STEPS;
|
|
51
51
|
this.onEvent = options.onEvent;
|
|
52
52
|
this.onRunStarted = options.onRunStarted;
|
|
53
|
-
this.onRunFinishing = options.onRunFinishing;
|
|
54
53
|
}
|
|
55
54
|
get databasePath() {
|
|
56
55
|
return this.store.databasePath;
|
|
@@ -80,7 +79,10 @@ export class WorkflowEngine {
|
|
|
80
79
|
this.updateLimiters.set(active.state.runId, limiter);
|
|
81
80
|
}
|
|
82
81
|
limiter.take();
|
|
83
|
-
const { event, record } = await this.store.publishUpdate(active.runId, active.
|
|
82
|
+
const { event, record } = await this.store.publishUpdate(active.runId, active.nodeId, active.attemptId, update, { signal: active.signal });
|
|
83
|
+
active.state.updates = updateProjection(active.state.updates, record);
|
|
84
|
+
active.state.traceSeq = event.seq;
|
|
85
|
+
active.state.updatedAt = event.at;
|
|
84
86
|
try {
|
|
85
87
|
this.onEvent?.(event, active.state);
|
|
86
88
|
}
|
|
@@ -133,7 +135,6 @@ export class WorkflowEngine {
|
|
|
133
135
|
async run(workflow, input, options = {}) {
|
|
134
136
|
workflow = isCompiledWorkflow(workflow) ? workflow : compileWorkflowDefinition(workflow);
|
|
135
137
|
validateWorkflowDefinition(workflow);
|
|
136
|
-
this.presentationRequired = workflow.presentationPrompt !== undefined;
|
|
137
138
|
// Fail before any run row exists so bad input cannot leave partial state.
|
|
138
139
|
const suppliedInput = input === undefined ? null : input;
|
|
139
140
|
const normalizedInput = workflow.input ? await workflow.input(suppliedInput) : suppliedInput;
|
|
@@ -191,34 +192,37 @@ export class WorkflowEngine {
|
|
|
191
192
|
async resumeRun(workflow, runId, options = {}) {
|
|
192
193
|
workflow = isCompiledWorkflow(workflow) ? workflow : compileWorkflowDefinition(workflow);
|
|
193
194
|
validateWorkflowDefinition(workflow);
|
|
194
|
-
this.presentationRequired = workflow.presentationPrompt !== undefined;
|
|
195
195
|
// Reset before any await: a park or cancel landing during preparation
|
|
196
196
|
// must survive, or a server drain would hang while the run executes.
|
|
197
197
|
this.cancelled = false;
|
|
198
198
|
this.paused = false;
|
|
199
199
|
this.parked = false;
|
|
200
|
-
const
|
|
201
|
-
|
|
200
|
+
const stored = await this.store.readRunState(runId);
|
|
201
|
+
if (stored === null)
|
|
202
|
+
throw new Error(`Cannot resume unreadable workflow run: ${runId}`);
|
|
203
|
+
const sourceMismatch = workflowIdentityMismatch(stored, workflow, options.workflowSource);
|
|
202
204
|
if (sourceMismatch && options.force !== true) {
|
|
203
205
|
throw new WorkflowSourceChangedError(runId);
|
|
204
206
|
}
|
|
205
|
-
const
|
|
207
|
+
const state = await this.store.prepareRunResume(runId);
|
|
208
|
+
const point = this.resumePointFor(workflow, state);
|
|
206
209
|
const interruptedNode = state.currentNode !== undefined ? workflow.nodes[state.currentNode] : undefined;
|
|
207
210
|
let resumedAttempt;
|
|
208
211
|
if (state.currentNode !== undefined &&
|
|
209
212
|
state.currentAttemptId !== undefined &&
|
|
210
213
|
state.currentNodeStartedAt !== undefined &&
|
|
211
|
-
interruptedNode?.nodeType === "
|
|
212
|
-
|
|
213
|
-
|
|
214
|
+
(interruptedNode?.nodeType === "checkpoint" ||
|
|
215
|
+
(interruptedNode?.nodeType === "agent" &&
|
|
216
|
+
(assistantMessageConfig(interruptedNode) !== undefined ||
|
|
217
|
+
options.resumeInteractionAttemptId === state.currentAttemptId)))) {
|
|
214
218
|
const resumedSettings = await this.persistedSettingsBinding(state);
|
|
215
219
|
resumedAttempt = {
|
|
216
220
|
nodeId: state.currentNode,
|
|
217
221
|
attemptId: state.currentAttemptId,
|
|
218
222
|
startedAt: state.currentNodeStartedAt,
|
|
219
|
-
...(state.
|
|
223
|
+
...(state.currentNodeTimeoutMs === undefined
|
|
220
224
|
? {}
|
|
221
|
-
: {
|
|
225
|
+
: { timeoutMs: state.currentNodeTimeoutMs, elapsedMs: state.currentNodeElapsedMs ?? 0 }),
|
|
222
226
|
...(resumedSettings !== undefined ? { settings: resumedSettings } : {}),
|
|
223
227
|
};
|
|
224
228
|
}
|
|
@@ -231,7 +235,8 @@ export class WorkflowEngine {
|
|
|
231
235
|
delete state.currentNode;
|
|
232
236
|
delete state.currentAttemptId;
|
|
233
237
|
delete state.currentNodeStartedAt;
|
|
234
|
-
delete state.
|
|
238
|
+
delete state.currentNodeTimeoutMs;
|
|
239
|
+
delete state.currentNodeElapsedMs;
|
|
235
240
|
delete state.currentSettingsScopeId;
|
|
236
241
|
delete state.currentSettingsChangeNumber;
|
|
237
242
|
delete state.currentSettingsHash;
|
|
@@ -249,16 +254,8 @@ export class WorkflowEngine {
|
|
|
249
254
|
});
|
|
250
255
|
await this.onRunStarted?.(runId, state);
|
|
251
256
|
if (point.nodeId === null) {
|
|
252
|
-
// The last
|
|
253
|
-
|
|
254
|
-
// checkpoint restores its waiting gate rather than completing.
|
|
255
|
-
if (point.waitingOn !== undefined) {
|
|
256
|
-
await this.finishRun(runId, state, "waiting", {
|
|
257
|
-
waitingOn: point.waitingOn,
|
|
258
|
-
finalOutput: point.lastOutput,
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
else if (point.failedResult === undefined) {
|
|
257
|
+
// The last attempt finished before the terminal event was committed.
|
|
258
|
+
if (point.failedResult === undefined) {
|
|
262
259
|
await this.finishRun(runId, state, "completed", { finalOutput: point.lastOutput });
|
|
263
260
|
}
|
|
264
261
|
else {
|
|
@@ -283,138 +280,12 @@ export class WorkflowEngine {
|
|
|
283
280
|
}
|
|
284
281
|
return { runId, state };
|
|
285
282
|
}
|
|
286
|
-
/**
|
|
287
|
-
* Start a continuation run from a checkpointed parent. The new run gets a
|
|
288
|
-
* fresh run and event stream, carries forward the parent's outputs, results,
|
|
289
|
-
* and step accounting, and continues routing after the checkpoint.
|
|
290
|
-
*/
|
|
291
|
-
async continueRun(workflow, parentRunId, input, options = {}) {
|
|
292
|
-
workflow = isCompiledWorkflow(workflow) ? workflow : compileWorkflowDefinition(workflow);
|
|
293
|
-
validateWorkflowDefinition(workflow);
|
|
294
|
-
this.presentationRequired = workflow.presentationPrompt !== undefined;
|
|
295
|
-
this.cancelled = false;
|
|
296
|
-
this.paused = false;
|
|
297
|
-
this.parked = false;
|
|
298
|
-
const parentState = await this.store.readRunState(parentRunId);
|
|
299
|
-
if (parentState === null) {
|
|
300
|
-
throw new Error(`Cannot continue from unreadable workflow run: ${parentRunId}`);
|
|
301
|
-
}
|
|
302
|
-
if (parentState.status !== "waiting" || parentState.waitingOn === undefined) {
|
|
303
|
-
throw new Error(`Cannot continue workflow run ${parentRunId} with status ${parentState.status}`);
|
|
304
|
-
}
|
|
305
|
-
const sourceMismatch = workflowIdentityMismatch(parentState, workflow, options.workflowSource);
|
|
306
|
-
if (sourceMismatch && options.force !== true) {
|
|
307
|
-
throw new WorkflowSourceChangedError(parentRunId);
|
|
308
|
-
}
|
|
309
|
-
const waitingNodeId = parentState.waitingOn;
|
|
310
|
-
const waitingNode = workflow.nodes[waitingNodeId];
|
|
311
|
-
const humanContract = waitingNode?.nodeType === "checkpoint" ? waitingNode.humanDecision : undefined;
|
|
312
|
-
let acceptedResponse;
|
|
313
|
-
let acceptedNodeId;
|
|
314
|
-
let normalizedInput;
|
|
315
|
-
if (humanContract !== undefined) {
|
|
316
|
-
if (options.humanDecision === undefined) {
|
|
317
|
-
throw new Error(`Checkpoint ${waitingNodeId} requires an accepted verified human decision`);
|
|
318
|
-
}
|
|
319
|
-
const request = parentState.finalOutput;
|
|
320
|
-
if (request?.schema !== "pi-workflows.human-decision-request.v1" ||
|
|
321
|
-
request.decisionId !== options.humanDecision.decisionId ||
|
|
322
|
-
request.requestDigest !== options.humanDecision.requestDigest) {
|
|
323
|
-
throw new Error("Accepted human decision does not match the waiting request");
|
|
324
|
-
}
|
|
325
|
-
const durableDecision = await this.store.readResolvedHumanDecision(request.decisionId);
|
|
326
|
-
if (durableDecision === null || !isDeepStrictEqual(durableDecision, options.humanDecision)) {
|
|
327
|
-
throw new Error("Accepted human decision does not match the durable decision record");
|
|
328
|
-
}
|
|
329
|
-
acceptedResponse = validateHumanDecisionResponse(request, durableDecision.response);
|
|
330
|
-
acceptedNodeId = request.nodeId;
|
|
331
|
-
normalizedInput = structuredClone(parentState.input);
|
|
332
|
-
}
|
|
333
|
-
else {
|
|
334
|
-
const suppliedInput = input === undefined ? null : input;
|
|
335
|
-
normalizedInput = workflow.input ? await workflow.input(suppliedInput) : suppliedInput;
|
|
336
|
-
}
|
|
337
|
-
assertJsonSerializable(normalizedInput, "Workflow run input");
|
|
338
|
-
if (options.runId !== undefined && !/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(options.runId)) {
|
|
339
|
-
throw new Error(`Invalid workflow run id: ${JSON.stringify(options.runId)}`);
|
|
340
|
-
}
|
|
341
|
-
const state = await this.createRunState(workflow, normalizedInput, options.workflowSource, options.runId);
|
|
342
|
-
state.parentRunId = parentRunId;
|
|
343
|
-
state.outputs = structuredClone(parentState.outputs);
|
|
344
|
-
state.results = structuredClone(parentState.results);
|
|
345
|
-
state.steps = structuredClone(parentState.steps);
|
|
346
|
-
if (humanContract !== undefined && options.humanDecision !== undefined) {
|
|
347
|
-
const receipt = {
|
|
348
|
-
decisionId: options.humanDecision.decisionId,
|
|
349
|
-
requestDigest: options.humanDecision.requestDigest,
|
|
350
|
-
nodeId: acceptedNodeId ?? waitingNodeId,
|
|
351
|
-
response: options.humanDecision.response,
|
|
352
|
-
provenance: options.humanDecision.provenance,
|
|
353
|
-
acceptedAt: options.humanDecision.acceptedAt,
|
|
354
|
-
answerDigest: options.humanDecision.answerDigest,
|
|
355
|
-
};
|
|
356
|
-
state.humanDecision = {
|
|
357
|
-
schema: "pi-workflows.human-decision-receipt.v1",
|
|
358
|
-
...receipt,
|
|
359
|
-
subjectDigest: options.humanDecision.subjectDigest,
|
|
360
|
-
presentationDigest: options.humanDecision.presentationDigest,
|
|
361
|
-
revision: options.humanDecision.revision,
|
|
362
|
-
};
|
|
363
|
-
state.outputs[waitingNodeId] = acceptedResponse;
|
|
364
|
-
const priorResult = state.results[waitingNodeId];
|
|
365
|
-
if (priorResult === undefined) {
|
|
366
|
-
throw new Error(`Waiting human decision result is missing for ${waitingNodeId}`);
|
|
367
|
-
}
|
|
368
|
-
state.results[waitingNodeId] = { ...priorResult, output: acceptedResponse };
|
|
369
|
-
const stepIndex = state.steps.findLastIndex((step) => step.nodeId === waitingNodeId);
|
|
370
|
-
if (stepIndex < 0)
|
|
371
|
-
throw new Error(`Waiting human decision step is missing for ${waitingNodeId}`);
|
|
372
|
-
const priorStep = state.steps[stepIndex];
|
|
373
|
-
if (priorStep === undefined)
|
|
374
|
-
throw new Error("Waiting human decision step became unavailable");
|
|
375
|
-
state.steps[stepIndex] = { ...priorStep, output: acceptedResponse };
|
|
376
|
-
}
|
|
377
|
-
state.carriedStepCount = state.steps.length;
|
|
378
|
-
const runId = await this.store.initializeRun(workflow, state);
|
|
379
|
-
await this.persist(runId, state, {
|
|
380
|
-
scope: "run",
|
|
381
|
-
type: "run_started",
|
|
382
|
-
payload: {
|
|
383
|
-
workflowName: workflow.name,
|
|
384
|
-
...(state.runTitle ? { runTitle: state.runTitle } : {}),
|
|
385
|
-
input: state.input,
|
|
386
|
-
continuedFrom: parentRunId,
|
|
387
|
-
checkpoint: parentState.waitingOn,
|
|
388
|
-
carriedSteps: state.steps.length,
|
|
389
|
-
},
|
|
390
|
-
});
|
|
391
|
-
await this.onRunStarted?.(runId, state);
|
|
392
|
-
const point = this.resumePointFor(workflow, state, "continue");
|
|
393
|
-
if (point.nodeId === null) {
|
|
394
|
-
// The checkpoint was the final node; the answer completes the chain.
|
|
395
|
-
await this.finishRun(runId, state, "completed", { finalOutput: point.lastOutput });
|
|
396
|
-
return { runId, state };
|
|
397
|
-
}
|
|
398
|
-
try {
|
|
399
|
-
await this.executeGraph(workflow, state, runId, point.nodeId, countExecutableSteps(workflow, state.steps), point.lastOutput);
|
|
400
|
-
}
|
|
401
|
-
catch (error) {
|
|
402
|
-
if (isClaimLostError(error))
|
|
403
|
-
throw error;
|
|
404
|
-
if (isRunParkedError(error) || this.parked) {
|
|
405
|
-
return { runId, state };
|
|
406
|
-
}
|
|
407
|
-
await this.finishAfterError(runId, state, error);
|
|
408
|
-
return { runId, state };
|
|
409
|
-
}
|
|
410
|
-
return { runId, state };
|
|
411
|
-
}
|
|
412
283
|
/**
|
|
413
284
|
* Find where a resumed run continues. An in-flight node reruns; otherwise
|
|
414
285
|
* routing continues from the last recorded step. A null nodeId means the
|
|
415
286
|
* graph was already done when the crash hit.
|
|
416
287
|
*/
|
|
417
|
-
resumePointFor(workflow, state
|
|
288
|
+
resumePointFor(workflow, state) {
|
|
418
289
|
if (state.currentNode !== undefined) {
|
|
419
290
|
if (workflow.nodes[state.currentNode] === undefined) {
|
|
420
291
|
throw new Error(`Resume node is missing from the workflow: ${state.currentNode}`);
|
|
@@ -430,17 +301,6 @@ export class WorkflowEngine {
|
|
|
430
301
|
return { nodeId: lastStep.nodeId };
|
|
431
302
|
}
|
|
432
303
|
if (result.outcome === "ok") {
|
|
433
|
-
// A recorded checkpoint means the run should be waiting; a crash
|
|
434
|
-
// before the run_waiting persist restores the gate instead of
|
|
435
|
-
// routing past it. The gate applies to this run's own checkpoint
|
|
436
|
-
// only: a continuation's carried steps end with the parent's
|
|
437
|
-
// already-answered checkpoint, and routing must continue from it.
|
|
438
|
-
const isCarriedStep = state.steps.length <= (state.carriedStepCount ?? 0);
|
|
439
|
-
if (checkpointBehavior === "wait" &&
|
|
440
|
-
!isCarriedStep &&
|
|
441
|
-
workflow.nodes[lastStep.nodeId]?.nodeType === "checkpoint") {
|
|
442
|
-
return { nodeId: null, waitingOn: lastStep.nodeId, lastOutput: result.output };
|
|
443
|
-
}
|
|
444
304
|
const next = resolveNext(workflow.edges, lastStep.nodeId, result.output, result);
|
|
445
305
|
return next === null
|
|
446
306
|
? { nodeId: null, lastOutput: result.output }
|
|
@@ -602,6 +462,7 @@ export class WorkflowEngine {
|
|
|
602
462
|
scopeId: latest.scopeId,
|
|
603
463
|
previousChangeNumber: attempt.settings.changeNumber,
|
|
604
464
|
changeNumber: latest.changeNumber,
|
|
465
|
+
settingsHash: latest.settingsHash,
|
|
605
466
|
},
|
|
606
467
|
});
|
|
607
468
|
resumedAttempt = {
|
|
@@ -647,13 +508,6 @@ export class WorkflowEngine {
|
|
|
647
508
|
});
|
|
648
509
|
}
|
|
649
510
|
lastOutput = attempt.result.output;
|
|
650
|
-
if (node.nodeType === "checkpoint") {
|
|
651
|
-
await this.finishRun(runId, state, "waiting", {
|
|
652
|
-
waitingOn: attempt.result.nodeId,
|
|
653
|
-
finalOutput: lastOutput,
|
|
654
|
-
});
|
|
655
|
-
return;
|
|
656
|
-
}
|
|
657
511
|
currentNodeId = resolveNext(workflow.edges, attempt.result.nodeId, attempt.result.output, attempt.result);
|
|
658
512
|
}
|
|
659
513
|
await this.finishRun(runId, state, "completed", { finalOutput: lastOutput });
|
|
@@ -749,7 +603,8 @@ export class WorkflowEngine {
|
|
|
749
603
|
delete state.currentNode;
|
|
750
604
|
delete state.currentAttemptId;
|
|
751
605
|
delete state.currentNodeStartedAt;
|
|
752
|
-
delete state.
|
|
606
|
+
delete state.currentNodeTimeoutMs;
|
|
607
|
+
delete state.currentNodeElapsedMs;
|
|
753
608
|
delete state.currentSettingsScopeId;
|
|
754
609
|
delete state.currentSettingsChangeNumber;
|
|
755
610
|
delete state.currentSettingsHash;
|
|
@@ -795,7 +650,7 @@ export class WorkflowEngine {
|
|
|
795
650
|
}
|
|
796
651
|
const meta = { promptText: null };
|
|
797
652
|
try {
|
|
798
|
-
const execution = await this.runNodeWithTimeout(workflow, state, runId, nodeId, attemptId, node, meta, settings, resumedAttempt?.
|
|
653
|
+
const execution = await this.runNodeWithTimeout(workflow, state, runId, nodeId, attemptId, node, meta, settings, resumedAttempt?.timeoutMs, resumedAttempt?.elapsedMs);
|
|
799
654
|
return {
|
|
800
655
|
result: this.createNodeResult(nodeId, node, attemptId, startedAt, "ok", execution.output),
|
|
801
656
|
execution,
|
|
@@ -845,7 +700,7 @@ export class WorkflowEngine {
|
|
|
845
700
|
...(output !== undefined ? { output } : {}),
|
|
846
701
|
};
|
|
847
702
|
}
|
|
848
|
-
async runNodeWithTimeout(workflow, state, runId, nodeId, attemptId, node, meta, settings,
|
|
703
|
+
async runNodeWithTimeout(workflow, state, runId, nodeId, attemptId, node, meta, settings, persistedTimeoutMs, elapsedMs = 0) {
|
|
849
704
|
const abort = new AbortController();
|
|
850
705
|
const context = this.createNodeContext(state, abort.signal, settings);
|
|
851
706
|
let timer;
|
|
@@ -862,43 +717,41 @@ export class WorkflowEngine {
|
|
|
862
717
|
}
|
|
863
718
|
let timeoutMs;
|
|
864
719
|
let timeoutErrorMs;
|
|
865
|
-
if (
|
|
720
|
+
if (persistedTimeoutMs === undefined) {
|
|
866
721
|
timeoutMs = await this.resolveNodeTimeout(node, context, abort);
|
|
867
722
|
timeoutErrorMs = timeoutMs;
|
|
868
|
-
if (node.nodeType === "agent" && this.executor.
|
|
869
|
-
state.
|
|
870
|
-
|
|
723
|
+
if (node.nodeType === "agent" && this.executor.preservesActiveTimeBudget === true) {
|
|
724
|
+
state.currentNodeTimeoutMs = timeoutMs;
|
|
725
|
+
state.currentNodeElapsedMs = 0;
|
|
871
726
|
await this.persist(runId, state, {
|
|
872
727
|
scope: "node",
|
|
873
|
-
type: "
|
|
728
|
+
type: "node_timeout_set",
|
|
874
729
|
nodeId,
|
|
875
730
|
attemptId,
|
|
876
|
-
payload: {
|
|
731
|
+
payload: { timeoutMs },
|
|
877
732
|
});
|
|
878
|
-
if (state.currentNodeDeadlineAt !== null && timeoutErrorMs !== null) {
|
|
879
|
-
timeoutMs = Date.parse(state.currentNodeDeadlineAt) - Date.now();
|
|
880
|
-
if (timeoutMs <= 0)
|
|
881
|
-
throw new TimeoutError(timeoutErrorMs);
|
|
882
|
-
}
|
|
883
733
|
}
|
|
884
734
|
else {
|
|
885
|
-
delete state.
|
|
735
|
+
delete state.currentNodeTimeoutMs;
|
|
736
|
+
delete state.currentNodeElapsedMs;
|
|
886
737
|
}
|
|
887
738
|
}
|
|
888
|
-
else if (
|
|
889
|
-
state.
|
|
739
|
+
else if (persistedTimeoutMs === null) {
|
|
740
|
+
state.currentNodeTimeoutMs = null;
|
|
890
741
|
timeoutMs = null;
|
|
891
742
|
timeoutErrorMs = null;
|
|
892
743
|
}
|
|
893
744
|
else {
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
745
|
+
if (!Number.isFinite(persistedTimeoutMs) ||
|
|
746
|
+
persistedTimeoutMs <= 0 ||
|
|
747
|
+
!Number.isFinite(elapsedMs) ||
|
|
748
|
+
elapsedMs < 0) {
|
|
749
|
+
throw new Error("Persisted node timeout is invalid");
|
|
898
750
|
}
|
|
899
|
-
state.
|
|
900
|
-
|
|
901
|
-
|
|
751
|
+
state.currentNodeTimeoutMs = persistedTimeoutMs;
|
|
752
|
+
state.currentNodeElapsedMs = elapsedMs;
|
|
753
|
+
timeoutMs = persistedTimeoutMs - elapsedMs;
|
|
754
|
+
timeoutErrorMs = persistedTimeoutMs;
|
|
902
755
|
if (timeoutMs <= 0)
|
|
903
756
|
throw new TimeoutError(timeoutErrorMs);
|
|
904
757
|
}
|
|
@@ -1010,13 +863,42 @@ export class WorkflowEngine {
|
|
|
1010
863
|
}
|
|
1011
864
|
case "action":
|
|
1012
865
|
return await this.runActionNode(node, context, nodeId, attemptId, signal, meta);
|
|
1013
|
-
case "checkpoint":
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
866
|
+
case "checkpoint": {
|
|
867
|
+
const saved = await this.store.readCheckpoint(state.runId, attemptId);
|
|
868
|
+
if (saved !== undefined) {
|
|
869
|
+
if (saved.request.status === "cancelled")
|
|
870
|
+
throw new Error("Checkpoint request is cancelled");
|
|
871
|
+
if (saved.request.status === "settled") {
|
|
872
|
+
if (saved.humanDecision !== undefined)
|
|
873
|
+
state.humanDecision = saved.humanDecision;
|
|
874
|
+
return { output: saved.output, promptText: null };
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
else {
|
|
878
|
+
const request = await checkpointRequest(node, context, {
|
|
879
|
+
workflowName: workflow.name,
|
|
880
|
+
nodeId,
|
|
881
|
+
attemptId,
|
|
882
|
+
});
|
|
883
|
+
const event = await this.store.commitTransition(state.runId, {
|
|
884
|
+
kind: "waitForInput",
|
|
885
|
+
...request,
|
|
886
|
+
event: {
|
|
887
|
+
scope: "node",
|
|
888
|
+
type: "checkpoint_requested",
|
|
889
|
+
nodeId,
|
|
890
|
+
attemptId,
|
|
891
|
+
payload: { requestId: request.requestId, requestKind: request.requestKind },
|
|
892
|
+
},
|
|
893
|
+
});
|
|
894
|
+
state.traceSeq = event.seq;
|
|
895
|
+
state.updatedAt = event.at;
|
|
896
|
+
}
|
|
897
|
+
state.status = "waiting";
|
|
898
|
+
state.waitingOn = nodeId;
|
|
899
|
+
delete state.finishedAt;
|
|
900
|
+
throw new RunParkedError();
|
|
901
|
+
}
|
|
1020
902
|
}
|
|
1021
903
|
}
|
|
1022
904
|
async captureSettingsBinding(workflow, state, nodeId) {
|
|
@@ -1072,11 +954,12 @@ export class WorkflowEngine {
|
|
|
1072
954
|
return scope;
|
|
1073
955
|
}
|
|
1074
956
|
createNodeContext(state, signal, settings) {
|
|
957
|
+
const snapshot = deepFreezeJson(structuredClone(state));
|
|
1075
958
|
return {
|
|
1076
|
-
input:
|
|
1077
|
-
outputs:
|
|
1078
|
-
results:
|
|
1079
|
-
state,
|
|
959
|
+
input: snapshot.input,
|
|
960
|
+
outputs: snapshot.outputs,
|
|
961
|
+
results: snapshot.results,
|
|
962
|
+
state: snapshot,
|
|
1080
963
|
...(settings !== undefined
|
|
1081
964
|
? {
|
|
1082
965
|
settings: deepFreezeJson(structuredClone(settings.settings)),
|
|
@@ -1114,7 +997,17 @@ export class WorkflowEngine {
|
|
|
1114
997
|
// may already be terminal.
|
|
1115
998
|
throw abortError(signal);
|
|
1116
999
|
}
|
|
1117
|
-
const
|
|
1000
|
+
const contract = {
|
|
1001
|
+
requestId: workflowRequestId(state.runId, attemptId),
|
|
1002
|
+
runId: state.runId,
|
|
1003
|
+
workflowName: workflow.name,
|
|
1004
|
+
nodeId,
|
|
1005
|
+
attemptId,
|
|
1006
|
+
completion: assistant === undefined ? "submit" : "assistant",
|
|
1007
|
+
...(typeof node.expectedOutput === "string" ? { expectedOutput: node.expectedOutput } : {}),
|
|
1008
|
+
...(assistant?.maxChars !== undefined ? { maxOutputChars: assistant.maxChars } : {}),
|
|
1009
|
+
};
|
|
1010
|
+
const prompt = appendStepContract(basePrompt, contract);
|
|
1118
1011
|
meta.promptText = prompt;
|
|
1119
1012
|
await this.persist(runId, state, {
|
|
1120
1013
|
scope: "agent",
|
|
@@ -1124,17 +1017,7 @@ export class WorkflowEngine {
|
|
|
1124
1017
|
payload: { prompt, completion: assistant === undefined ? "submit" : "assistant" },
|
|
1125
1018
|
});
|
|
1126
1019
|
const submission = await this.executor.runAgentStep({
|
|
1127
|
-
contract
|
|
1128
|
-
runId: state.runId,
|
|
1129
|
-
workflowName: workflow.name,
|
|
1130
|
-
nodeId,
|
|
1131
|
-
attemptId,
|
|
1132
|
-
completion: assistant === undefined ? "submit" : "assistant",
|
|
1133
|
-
...(typeof node.expectedOutput === "string"
|
|
1134
|
-
? { expectedOutput: node.expectedOutput }
|
|
1135
|
-
: {}),
|
|
1136
|
-
...(assistant?.maxChars !== undefined ? { maxOutputChars: assistant.maxChars } : {}),
|
|
1137
|
-
},
|
|
1020
|
+
contract,
|
|
1138
1021
|
prompt,
|
|
1139
1022
|
...(state.runTitle !== undefined || node.statusDetail !== undefined
|
|
1140
1023
|
? {
|
|
@@ -1251,7 +1134,9 @@ export class WorkflowEngine {
|
|
|
1251
1134
|
}
|
|
1252
1135
|
}
|
|
1253
1136
|
async persist(runId, state, event) {
|
|
1254
|
-
const traceEvent = await this.store.
|
|
1137
|
+
const traceEvent = await this.store.commitTransition(runId, executionTransition(state, event));
|
|
1138
|
+
state.traceSeq = traceEvent.seq;
|
|
1139
|
+
state.updatedAt = traceEvent.at;
|
|
1255
1140
|
try {
|
|
1256
1141
|
this.onEvent?.(traceEvent, state);
|
|
1257
1142
|
}
|
|
@@ -1270,41 +1155,29 @@ export class WorkflowEngine {
|
|
|
1270
1155
|
if (fields.error !== undefined) {
|
|
1271
1156
|
state.error = fields.error;
|
|
1272
1157
|
}
|
|
1273
|
-
if (fields.waitingOn !== undefined) {
|
|
1274
|
-
state.waitingOn = fields.waitingOn;
|
|
1275
|
-
}
|
|
1276
1158
|
if (fields.finalOutput !== undefined) {
|
|
1277
1159
|
state.finalOutput = fields.finalOutput;
|
|
1278
1160
|
}
|
|
1279
1161
|
delete state.currentNode;
|
|
1280
1162
|
delete state.currentAttemptId;
|
|
1281
1163
|
delete state.currentNodeStartedAt;
|
|
1282
|
-
delete state.
|
|
1164
|
+
delete state.currentNodeTimeoutMs;
|
|
1165
|
+
delete state.currentNodeElapsedMs;
|
|
1283
1166
|
delete state.currentSettingsScopeId;
|
|
1284
1167
|
delete state.currentSettingsChangeNumber;
|
|
1285
1168
|
delete state.currentSettingsHash;
|
|
1286
|
-
// Let observers stop, drain, and prepare terminal delivery from the final
|
|
1287
|
-
// in-memory state before the immutable terminal event is committed.
|
|
1288
|
-
try {
|
|
1289
|
-
await this.onRunFinishing?.(runId, state);
|
|
1290
|
-
}
|
|
1291
|
-
catch {
|
|
1292
|
-
// Finishing the run wins over observer failures.
|
|
1293
|
-
}
|
|
1294
1169
|
await this.persist(runId, state, {
|
|
1295
1170
|
scope: "run",
|
|
1296
1171
|
type: `run_${status}`,
|
|
1297
1172
|
payload: {
|
|
1298
1173
|
status,
|
|
1299
1174
|
...(fields.error !== undefined ? { error: fields.error } : {}),
|
|
1300
|
-
...(fields.waitingOn !== undefined ? { waitingOn: fields.waitingOn } : {}),
|
|
1301
1175
|
...(fields.finalOutput !== undefined ? { finalOutput: fields.finalOutput } : {}),
|
|
1302
|
-
presentationRequired: status === "completed" && this.presentationRequired,
|
|
1303
1176
|
},
|
|
1304
1177
|
});
|
|
1305
1178
|
}
|
|
1306
1179
|
}
|
|
1307
|
-
async function
|
|
1180
|
+
async function checkpointRequest(node, context, execution) {
|
|
1308
1181
|
if (node.humanDecision !== undefined) {
|
|
1309
1182
|
const prompt = await node.humanDecision.request(context);
|
|
1310
1183
|
const audience = typeof node.humanDecision.audience === "function"
|
|
@@ -1322,11 +1195,18 @@ async function runCheckpointNode(node, context, execution) {
|
|
|
1322
1195
|
prompt,
|
|
1323
1196
|
...(timeout !== undefined ? { timeout } : {}),
|
|
1324
1197
|
});
|
|
1325
|
-
|
|
1326
|
-
|
|
1198
|
+
return {
|
|
1199
|
+
requestId: request.decisionId,
|
|
1200
|
+
requestKind: "decision",
|
|
1201
|
+
contract: request,
|
|
1202
|
+
};
|
|
1327
1203
|
}
|
|
1328
1204
|
const output = node.run ? await node.run(context) : { summary: node.summary ?? "checkpoint" };
|
|
1329
|
-
return {
|
|
1205
|
+
return {
|
|
1206
|
+
requestId: workflowRequestId(context.state.runId, execution.attemptId),
|
|
1207
|
+
requestKind: "checkpoint",
|
|
1208
|
+
contract: output,
|
|
1209
|
+
};
|
|
1330
1210
|
}
|
|
1331
1211
|
function shellReceipt(result) {
|
|
1332
1212
|
return {
|
|
@@ -1578,9 +1458,9 @@ function settingsDefinitionForNode(workflow, nodeId) {
|
|
|
1578
1458
|
* The step contract appended to every agent-node prompt. This is the
|
|
1579
1459
|
* documented standard for how the model completes a workflow step.
|
|
1580
1460
|
*/
|
|
1581
|
-
export function appendStepContract(prompt,
|
|
1582
|
-
const
|
|
1583
|
-
if (
|
|
1461
|
+
export function appendStepContract(prompt, contract) {
|
|
1462
|
+
const { requestId, workflowName, nodeId, attemptId, expectedOutput } = contract;
|
|
1463
|
+
if (contract.completion === "assistant") {
|
|
1584
1464
|
return [
|
|
1585
1465
|
prompt.trimEnd(),
|
|
1586
1466
|
"",
|
|
@@ -1590,8 +1470,8 @@ export function appendStepContract(prompt, workflowName, nodeId, attemptId, expe
|
|
|
1590
1470
|
"Reply with a normal assistant message.",
|
|
1591
1471
|
"Do not call the workflow tool to complete this step.",
|
|
1592
1472
|
"Your visible reply becomes the workflow step output after the turn settles.",
|
|
1593
|
-
...(
|
|
1594
|
-
? [`Keep the visible reply within ${
|
|
1473
|
+
...(contract.maxOutputChars !== undefined
|
|
1474
|
+
? [`Keep the visible reply within ${contract.maxOutputChars} characters.`]
|
|
1595
1475
|
: []),
|
|
1596
1476
|
].join("\n");
|
|
1597
1477
|
}
|
|
@@ -1602,9 +1482,9 @@ export function appendStepContract(prompt, workflowName, nodeId, attemptId, expe
|
|
|
1602
1482
|
`Workflow step contract (workflow: ${workflowName}, step: ${nodeId}, attempt: ${attemptId})`,
|
|
1603
1483
|
"",
|
|
1604
1484
|
"While this step is active, you may publish non-completing updates with:",
|
|
1605
|
-
`{"action": "update", "
|
|
1485
|
+
`{"action": "update", "requestId": ${JSON.stringify(requestId)}, "update": {"type": "...", "key": "...", "data": {...}}}`,
|
|
1606
1486
|
"Complete this step by calling the `workflow` tool exactly once with:",
|
|
1607
|
-
`{"action": "submit", "
|
|
1487
|
+
`{"action": "submit", "requestId": ${JSON.stringify(requestId)}, "output": <your result>}`,
|
|
1608
1488
|
`Expected output: ${typeof expectedOutput === "string" ? expectedOutput : "a JSON object with your result"}`,
|
|
1609
1489
|
"The step is complete only after the workflow tool accepts the output.",
|
|
1610
1490
|
"If the tool reports a validation error, correct the output and call it again.",
|