@osolmaz/pi-workflows 0.12.1 → 0.13.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/README.md +52 -28
- package/dist/builtins/autodoc.workflow.d.ts +4 -4
- package/dist/builtins/autoimplement.workflow.d.ts +369 -73
- package/dist/builtins/autoplan.workflow.d.ts +126 -13
- package/dist/builtins/autoplan.workflow.js +200 -29
- package/dist/builtins/autoplan.workflow.js.map +1 -1
- package/dist/builtins/catalog.js +6 -4
- package/dist/builtins/catalog.js.map +1 -1
- package/dist/builtins/index.d.ts +2 -1
- package/dist/builtins/index.js +1 -0
- package/dist/builtins/index.js.map +1 -1
- package/dist/builtins/monitor.workflow.d.ts +56 -21
- package/dist/builtins/monitor.workflow.js +497 -216
- package/dist/builtins/monitor.workflow.js.map +1 -1
- package/dist/builtins/plain-summary.workflow.d.ts +32 -0
- package/dist/builtins/plain-summary.workflow.js +134 -0
- package/dist/builtins/plain-summary.workflow.js.map +1 -0
- package/dist/builtins/plan-change.workflow.d.ts +170 -22
- package/dist/builtins/sanity-check.workflow.d.ts +45 -3
- package/dist/builtins/sanity-check.workflow.js +45 -7
- package/dist/builtins/sanity-check.workflow.js.map +1 -1
- package/dist/controllers/effects.d.ts +3 -2
- package/dist/controllers/effects.js +8 -1
- package/dist/controllers/effects.js.map +1 -1
- package/dist/controllers/index.d.ts +1 -1
- package/dist/controllers/index.js +1 -1
- package/dist/controllers/index.js.map +1 -1
- package/dist/controllers/manager.d.ts +1 -0
- package/dist/controllers/manager.js +41 -17
- package/dist/controllers/manager.js.map +1 -1
- package/dist/controllers/sqlite.d.ts +107 -85
- package/dist/controllers/sqlite.js +1611 -1486
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/controllers/store.d.ts +9 -8
- package/dist/controllers/store.js +1 -31
- package/dist/controllers/store.js.map +1 -1
- package/dist/controllers/types.d.ts +3 -0
- package/dist/controllers/workflow-engine-scheduler.d.ts +1 -1
- package/dist/controllers/workflow-engine-scheduler.js +3 -5
- package/dist/controllers/workflow-engine-scheduler.js.map +1 -1
- package/dist/controllers/workflows.d.ts +2 -2
- package/dist/controllers/workflows.js +11 -10
- package/dist/controllers/workflows.js.map +1 -1
- package/dist/extension/controller-host.js +2 -2
- package/dist/extension/controller-host.js.map +1 -1
- package/dist/extension/decision-channels.d.ts +2 -0
- package/dist/extension/decision-channels.js +311 -118
- package/dist/extension/decision-channels.js.map +1 -1
- package/dist/extension/executor.d.ts +9 -2
- package/dist/extension/executor.js +89 -2
- package/dist/extension/executor.js.map +1 -1
- package/dist/extension/herdr-viewer.d.ts +0 -1
- package/dist/extension/herdr-viewer.js +0 -4
- package/dist/extension/herdr-viewer.js.map +1 -1
- package/dist/extension/index.js +156 -103
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/recorder.d.ts +3 -10
- package/dist/extension/recorder.js +13 -22
- package/dist/extension/recorder.js.map +1 -1
- package/dist/extension/step-message.d.ts +7 -2
- package/dist/extension/step-message.js +94 -2
- package/dist/extension/step-message.js.map +1 -1
- package/dist/extension/widget.js +6 -0
- package/dist/extension/widget.js.map +1 -1
- package/dist/host/rpc-executor.d.ts +3 -0
- package/dist/host/rpc-executor.js +2 -0
- package/dist/host/rpc-executor.js.map +1 -1
- package/dist/host/runner.d.ts +3 -6
- package/dist/host/runner.js +83 -32
- package/dist/host/runner.js.map +1 -1
- package/dist/render/graph-render.js +14 -8
- package/dist/render/graph-render.js.map +1 -1
- package/dist/state/database.d.ts +35 -0
- package/dist/state/database.js +287 -0
- package/dist/state/database.js.map +1 -0
- package/dist/state/index.d.ts +4 -0
- package/dist/state/index.js +5 -0
- package/dist/state/index.js.map +1 -0
- package/dist/state/json.d.ts +6 -0
- package/dist/state/json.js +38 -0
- package/dist/state/json.js.map +1 -0
- package/dist/state/mutation.d.ts +57 -0
- package/dist/state/mutation.js +255 -0
- package/dist/state/mutation.js.map +1 -0
- package/dist/state/schema.d.ts +6 -0
- package/dist/state/schema.js +473 -0
- package/dist/state/schema.js.map +1 -0
- package/dist/viewer/cli.d.ts +2 -2
- package/dist/viewer/cli.js +142 -122
- package/dist/viewer/cli.js.map +1 -1
- package/dist/viewer/render.d.ts +4 -4
- package/dist/viewer/render.js +2 -17
- package/dist/viewer/render.js.map +1 -1
- package/dist/viewer/session-reducer.d.ts +1 -1
- package/dist/viewer/session-reducer.js +1 -1
- package/dist/viewer/tui.d.ts +3 -3
- package/dist/viewer/tui.js +15 -13
- package/dist/viewer/tui.js.map +1 -1
- package/dist/viewer/watch.d.ts +2 -5
- package/dist/viewer/watch.js +13 -13
- package/dist/viewer/watch.js.map +1 -1
- package/dist/workflows/composition.js +12 -2
- package/dist/workflows/composition.js.map +1 -1
- package/dist/workflows/definition.d.ts +6 -2
- package/dist/workflows/definition.js +17 -0
- package/dist/workflows/definition.js.map +1 -1
- package/dist/workflows/engine.d.ts +6 -6
- package/dist/workflows/engine.js +183 -103
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/errors.d.ts +3 -3
- package/dist/workflows/errors.js +3 -3
- package/dist/workflows/human-decision.d.ts +29 -4
- package/dist/workflows/human-decision.js +547 -283
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +4 -5
- package/dist/workflows/index.js +3 -4
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/schema.js +17 -1
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/store.d.ts +83 -103
- package/dist/workflows/store.js +902 -951
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/types.d.ts +55 -81
- package/docs/CONTROLLERS.md +10 -8
- package/docs/DEFERRED_TURNS.md +2 -2
- package/docs/DESIGN_PHILOSOPHY.md +4 -2
- package/docs/HUMAN_DECISIONS.md +13 -14
- package/docs/MONITOR.md +179 -231
- package/docs/SQLITE_STATE.md +219 -0
- package/docs/WORKFLOW_COMPOSITION.md +19 -19
- package/docs/WORKFLOW_STEP_MESSAGES.md +20 -18
- package/docs/WORKFLOW_UPDATES.md +10 -9
- package/docs/development.md +9 -9
- package/docs/live-replay-protocol.md +28 -37
- package/docs/plans/2026-08-16-workflow-updates-plan.md +1 -1
- package/docs/plans/2026-08-19-human-decision-gates-plan.md +1 -1
- package/docs/plans/2026-08-21-autoimplement-timeout-fallback-plan.md +1 -1
- package/docs/plans/2026-08-21-plan-change-approval-policy-plan.md +1 -1
- package/docs/plans/2026-08-21-sanity-check-plan.md +30 -16
- package/docs/plans/2026-08-22-goal-finishing-monitor-plan.md +200 -0
- package/docs/plans/2026-08-23-assistant-agent-completion-plan.md +701 -0
- package/docs/plans/2026-08-23-sqlite-state-plan.md +970 -0
- package/docs/plans/replayable-run-bundles-implementation-plan.md +2 -2
- package/docs/plans/session-event-replay-implementation-plan.md +1 -1
- package/docs/plans/tui-viewer-implementation-plan.md +1 -1
- package/docs/session-event-journal.md +63 -434
- package/docs/tui-viewer.md +13 -15
- package/docs/workflows.md +104 -50
- package/examples/workflows/plain-summary.workflow.ts +1 -0
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/plugins/herdr/viewer.mjs +1 -11
- package/skills/autoplan/SKILL.md +5 -4
- package/skills/monitor/SKILL.md +91 -163
- package/skills/pi-workflows/SKILL.md +7 -7
- package/skills/sanity-check/SKILL.md +1 -1
- package/src/builtins/autoplan.workflow.ts +290 -40
- package/src/builtins/catalog.ts +6 -4
- package/src/builtins/index.ts +7 -1
- package/src/builtins/monitor.workflow.ts +663 -249
- package/src/builtins/plain-summary.workflow.ts +185 -0
- package/src/builtins/sanity-check.workflow.ts +62 -7
- package/src/controllers/effects.ts +7 -0
- package/src/controllers/index.ts +0 -6
- package/src/controllers/manager.ts +72 -32
- package/src/controllers/sqlite.ts +2420 -1948
- package/src/controllers/store.ts +17 -38
- package/src/controllers/types.ts +3 -0
- package/src/controllers/workflow-engine-scheduler.ts +4 -8
- package/src/controllers/workflows.ts +45 -21
- package/src/extension/controller-host.ts +1 -2
- package/src/extension/decision-channels.ts +427 -144
- package/src/extension/executor.ts +105 -2
- package/src/extension/herdr-viewer.ts +0 -5
- package/src/extension/index.ts +180 -118
- package/src/extension/recorder.ts +13 -22
- package/src/extension/step-message.ts +106 -4
- package/src/extension/widget.ts +8 -0
- package/src/host/rpc-executor.ts +4 -0
- package/src/host/runner.ts +93 -41
- package/src/render/graph-render.ts +11 -4
- package/src/state/database.ts +375 -0
- package/src/state/index.ts +29 -0
- package/src/state/json.ts +42 -0
- package/src/state/mutation.ts +435 -0
- package/src/state/schema.ts +475 -0
- package/src/viewer/cli.ts +151 -144
- package/src/viewer/render.ts +12 -24
- package/src/viewer/session-reducer.ts +1 -1
- package/src/viewer/tui.ts +18 -17
- package/src/viewer/watch.ts +13 -17
- package/src/workflows/composition.ts +12 -3
- package/src/workflows/definition.ts +36 -3
- package/src/workflows/engine.ts +212 -116
- package/src/workflows/errors.ts +3 -3
- package/src/workflows/human-decision.ts +795 -340
- package/src/workflows/index.ts +15 -19
- package/src/workflows/schema.ts +19 -1
- package/src/workflows/store.ts +1295 -1163
- package/src/workflows/types.ts +61 -81
- package/dist/workflows/artifacts.d.ts +0 -40
- package/dist/workflows/artifacts.js +0 -155
- package/dist/workflows/artifacts.js.map +0 -1
- package/dist/workflows/migrate-sources.d.ts +0 -42
- package/dist/workflows/migrate-sources.js +0 -133
- package/dist/workflows/migrate-sources.js.map +0 -1
- package/docs/run-bundles.md +0 -481
- package/src/workflows/artifacts.ts +0 -188
- package/src/workflows/migrate-sources.ts +0 -178
package/dist/workflows/engine.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { isDeepStrictEqual } from "node:util";
|
|
3
|
-
import { resolveArtifacts } from "./artifacts.js";
|
|
4
3
|
import { compileWorkflowDefinition, compositionMetadata, isCompiledWorkflow, } from "./composition.js";
|
|
5
4
|
import { CancelledError, errorMessage, isAbortLikeError, isClaimLostError, isRunParkedError, RunParkedError, TimeoutError, WorkflowSourceChangedError, } from "./errors.js";
|
|
6
5
|
import { resolveNext, resolveNextForOutcome, validateWorkflowDefinition } from "./graph.js";
|
|
7
6
|
import { HumanDecisionStore, createHumanDecisionRequest, validateHumanDecisionResponse, } from "./human-decision.js";
|
|
8
7
|
import { extractJsonValue } from "./json.js";
|
|
9
8
|
import { runShellAction, shellResultFromError } from "./shell.js";
|
|
10
|
-
import { RUN_STATE_SCHEMA, WorkflowRunStore, createDefinitionSnapshot, createRunId,
|
|
9
|
+
import { RUN_STATE_SCHEMA, WorkflowRunStore, createDefinitionSnapshot, createRunId, } from "./store.js";
|
|
11
10
|
import { UpdateRateLimiter, updateReceipt, validateWorkflowUpdate } from "./updates.js";
|
|
12
11
|
const DEFAULT_NODE_TIMEOUT_MS = 15 * 60_000;
|
|
13
12
|
const DEFAULT_MAX_STEPS = 100;
|
|
@@ -18,8 +17,8 @@ const ABORT_CLEANUP_GRACE_MS = 2_000;
|
|
|
18
17
|
/**
|
|
19
18
|
* Executes a workflow graph step by step. Agent steps are delegated to the
|
|
20
19
|
* configured executor; compute/action/checkpoint nodes run inline. Every
|
|
21
|
-
* state transition is persisted to the run
|
|
22
|
-
* so a live viewer can follow
|
|
20
|
+
* state transition is persisted to the SQLite run state before the engine moves on,
|
|
21
|
+
* so a live viewer can follow committed SQLite events.
|
|
23
22
|
*/
|
|
24
23
|
export class WorkflowEngine {
|
|
25
24
|
executor;
|
|
@@ -41,15 +40,15 @@ export class WorkflowEngine {
|
|
|
41
40
|
constructor(options) {
|
|
42
41
|
this.executor = options.executor;
|
|
43
42
|
this.notificationSink = options.notificationSink;
|
|
44
|
-
this.store = options.store ?? new WorkflowRunStore(options.
|
|
43
|
+
this.store = options.store ?? new WorkflowRunStore(options.databasePath);
|
|
45
44
|
this.defaultNodeTimeoutMs = options.defaultNodeTimeoutMs ?? DEFAULT_NODE_TIMEOUT_MS;
|
|
46
45
|
this.maxSteps = options.maxSteps ?? DEFAULT_MAX_STEPS;
|
|
47
46
|
this.onEvent = options.onEvent;
|
|
48
47
|
this.onRunStarted = options.onRunStarted;
|
|
49
48
|
this.onRunFinishing = options.onRunFinishing;
|
|
50
49
|
}
|
|
51
|
-
get
|
|
52
|
-
return this.store.
|
|
50
|
+
get databasePath() {
|
|
51
|
+
return this.store.databasePath;
|
|
53
52
|
}
|
|
54
53
|
/** Publish a durable update for the currently active attempt without completing it. */
|
|
55
54
|
async publishUpdate(step, attempt, input, idempotencyKey) {
|
|
@@ -76,7 +75,7 @@ export class WorkflowEngine {
|
|
|
76
75
|
this.updateLimiters.set(active.state.runId, limiter);
|
|
77
76
|
}
|
|
78
77
|
limiter.take();
|
|
79
|
-
const { event, record } = await this.store.publishUpdate(active.
|
|
78
|
+
const { event, record } = await this.store.publishUpdate(active.runId, active.state, active.nodeId, active.attemptId, update, { signal: active.signal });
|
|
80
79
|
try {
|
|
81
80
|
this.onEvent?.(event, active.state);
|
|
82
81
|
}
|
|
@@ -126,8 +125,7 @@ export class WorkflowEngine {
|
|
|
126
125
|
async run(workflow, input, options = {}) {
|
|
127
126
|
workflow = isCompiledWorkflow(workflow) ? workflow : compileWorkflowDefinition(workflow);
|
|
128
127
|
validateWorkflowDefinition(workflow);
|
|
129
|
-
// Fail before any
|
|
130
|
-
// on disk or silently change shape when state.json round-trips.
|
|
128
|
+
// Fail before any run row exists so bad input cannot leave partial state.
|
|
131
129
|
const suppliedInput = input === undefined ? null : input;
|
|
132
130
|
const normalizedInput = workflow.input ? await workflow.input(suppliedInput) : suppliedInput;
|
|
133
131
|
assertJsonSerializable(normalizedInput, "Workflow run input");
|
|
@@ -138,8 +136,8 @@ export class WorkflowEngine {
|
|
|
138
136
|
this.paused = false;
|
|
139
137
|
this.parked = false;
|
|
140
138
|
const state = await this.createRunState(workflow, normalizedInput, options.workflowSource, options.runId);
|
|
141
|
-
const
|
|
142
|
-
await this.persist(
|
|
139
|
+
const runId = await this.store.initializeRun(workflow, state);
|
|
140
|
+
await this.persist(runId, state, {
|
|
143
141
|
scope: "run",
|
|
144
142
|
type: "run_started",
|
|
145
143
|
payload: {
|
|
@@ -151,18 +149,18 @@ export class WorkflowEngine {
|
|
|
151
149
|
// Awaited so anything the hook writes (e.g. a session binding and its
|
|
152
150
|
// `session_bound` event) lands before node events and can never trail
|
|
153
151
|
// the terminal event of a fast run.
|
|
154
|
-
await this.onRunStarted?.(
|
|
152
|
+
await this.onRunStarted?.(runId, state);
|
|
155
153
|
try {
|
|
156
|
-
await this.executeGraph(workflow, state,
|
|
154
|
+
await this.executeGraph(workflow, state, runId);
|
|
157
155
|
}
|
|
158
156
|
catch (error) {
|
|
159
157
|
if (isRunParkedError(error) || this.parked) {
|
|
160
|
-
return {
|
|
158
|
+
return { runId, state };
|
|
161
159
|
}
|
|
162
|
-
await this.finishAfterError(
|
|
163
|
-
return {
|
|
160
|
+
await this.finishAfterError(runId, state, error);
|
|
161
|
+
return { runId, state };
|
|
164
162
|
}
|
|
165
|
-
return {
|
|
163
|
+
return { runId, state };
|
|
166
164
|
}
|
|
167
165
|
/**
|
|
168
166
|
* Resume an interrupted run at the node it stopped on. The caller must
|
|
@@ -177,68 +175,83 @@ export class WorkflowEngine {
|
|
|
177
175
|
this.cancelled = false;
|
|
178
176
|
this.paused = false;
|
|
179
177
|
this.parked = false;
|
|
180
|
-
const
|
|
181
|
-
const
|
|
182
|
-
const state = bundle.state;
|
|
178
|
+
const loaded = await this.store.prepareRunResume(runId);
|
|
179
|
+
const state = loaded.state;
|
|
183
180
|
const sourceMismatch = workflowIdentityMismatch(state, workflow, options.workflowSource);
|
|
184
181
|
if (sourceMismatch && options.force !== true) {
|
|
185
182
|
throw new WorkflowSourceChangedError(runId);
|
|
186
183
|
}
|
|
187
184
|
const point = this.resumePointFor(workflow, state, "wait");
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
185
|
+
const interruptedNode = state.currentNode !== undefined ? workflow.nodes[state.currentNode] : undefined;
|
|
186
|
+
const resumedAttempt = state.currentNode !== undefined &&
|
|
187
|
+
state.currentAttemptId !== undefined &&
|
|
188
|
+
state.currentNodeStartedAt !== undefined &&
|
|
189
|
+
interruptedNode?.nodeType === "agent" &&
|
|
190
|
+
assistantMessageConfig(interruptedNode) !== undefined
|
|
191
|
+
? {
|
|
192
|
+
nodeId: state.currentNode,
|
|
193
|
+
attemptId: state.currentAttemptId,
|
|
194
|
+
startedAt: state.currentNodeStartedAt,
|
|
195
|
+
}
|
|
196
|
+
: undefined;
|
|
197
|
+
// A resumed run starts unpaused. Submitted and non-agent nodes discard
|
|
198
|
+
// stale in-flight markers and start a new attempt. Assistant-message
|
|
199
|
+
// nodes keep their attempt id so the origin session can adopt an already
|
|
200
|
+
// visible response without showing it twice.
|
|
191
201
|
delete state.paused;
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
202
|
+
if (resumedAttempt === undefined) {
|
|
203
|
+
delete state.currentNode;
|
|
204
|
+
delete state.currentAttemptId;
|
|
205
|
+
delete state.currentNodeStartedAt;
|
|
206
|
+
delete state.statusDetail;
|
|
207
|
+
}
|
|
208
|
+
await this.persist(runId, state, {
|
|
197
209
|
scope: "run",
|
|
198
210
|
type: "run_resumed",
|
|
199
211
|
payload: {
|
|
200
212
|
...(point.nodeId !== null ? { resumeAt: point.nodeId } : {}),
|
|
213
|
+
...(resumedAttempt !== undefined ? { resumedAttemptId: resumedAttempt.attemptId } : {}),
|
|
201
214
|
replayedSteps: state.steps.length,
|
|
202
215
|
...(sourceMismatch ? { workflowSourceMismatch: true, forced: true } : {}),
|
|
203
216
|
},
|
|
204
217
|
});
|
|
205
|
-
await this.onRunStarted?.(
|
|
218
|
+
await this.onRunStarted?.(runId, state);
|
|
206
219
|
if (point.nodeId === null) {
|
|
207
220
|
// The last recorded transition already finished the graph; the crash
|
|
208
221
|
// happened before the terminal event was written. A finished
|
|
209
222
|
// checkpoint restores its waiting gate rather than completing.
|
|
210
223
|
if (point.waitingOn !== undefined) {
|
|
211
|
-
await this.finishRun(
|
|
224
|
+
await this.finishRun(runId, state, "waiting", {
|
|
212
225
|
waitingOn: point.waitingOn,
|
|
213
226
|
finalOutput: point.lastOutput,
|
|
214
227
|
});
|
|
215
228
|
}
|
|
216
229
|
else if (point.failedResult === undefined) {
|
|
217
|
-
await this.finishRun(
|
|
230
|
+
await this.finishRun(runId, state, "completed", { finalOutput: point.lastOutput });
|
|
218
231
|
}
|
|
219
232
|
else {
|
|
220
233
|
const timedOut = point.failedResult.outcome === "timed_out";
|
|
221
|
-
await this.finishRun(
|
|
234
|
+
await this.finishRun(runId, state, timedOut ? "timed_out" : "failed", {
|
|
222
235
|
error: point.failedResult.error ?? `Workflow node failed: ${point.failedResult.nodeId}`,
|
|
223
236
|
});
|
|
224
237
|
}
|
|
225
|
-
return {
|
|
238
|
+
return { runId, state };
|
|
226
239
|
}
|
|
227
240
|
try {
|
|
228
|
-
await this.executeGraph(workflow, state,
|
|
241
|
+
await this.executeGraph(workflow, state, runId, point.nodeId, countExecutableSteps(workflow, state.steps), point.lastOutput, resumedAttempt);
|
|
229
242
|
}
|
|
230
243
|
catch (error) {
|
|
231
244
|
if (isRunParkedError(error) || this.parked) {
|
|
232
|
-
return {
|
|
245
|
+
return { runId, state };
|
|
233
246
|
}
|
|
234
|
-
await this.finishAfterError(
|
|
235
|
-
return {
|
|
247
|
+
await this.finishAfterError(runId, state, error);
|
|
248
|
+
return { runId, state };
|
|
236
249
|
}
|
|
237
|
-
return {
|
|
250
|
+
return { runId, state };
|
|
238
251
|
}
|
|
239
252
|
/**
|
|
240
253
|
* Start a continuation run from a checkpointed parent. The new run gets a
|
|
241
|
-
* fresh
|
|
254
|
+
* fresh run and event stream, carries forward the parent's outputs, results,
|
|
242
255
|
* and step accounting, and continues routing after the checkpoint.
|
|
243
256
|
*/
|
|
244
257
|
async continueRun(workflow, parentRunId, input, options = {}) {
|
|
@@ -247,7 +260,7 @@ export class WorkflowEngine {
|
|
|
247
260
|
this.cancelled = false;
|
|
248
261
|
this.paused = false;
|
|
249
262
|
this.parked = false;
|
|
250
|
-
const parent =
|
|
263
|
+
const parent = this.store.readRun(parentRunId);
|
|
251
264
|
if (parent === null) {
|
|
252
265
|
throw new Error(`Cannot continue from unreadable workflow run: ${parentRunId}`);
|
|
253
266
|
}
|
|
@@ -274,13 +287,15 @@ export class WorkflowEngine {
|
|
|
274
287
|
request.requestDigest !== options.humanDecision.requestDigest) {
|
|
275
288
|
throw new Error("Accepted human decision does not match the waiting request");
|
|
276
289
|
}
|
|
277
|
-
const durableDecision = await new HumanDecisionStore(this.store.
|
|
290
|
+
const durableDecision = await new HumanDecisionStore(this.store.databasePath, {
|
|
291
|
+
state: this.store.state,
|
|
292
|
+
}).readResolved(request.decisionId);
|
|
278
293
|
if (durableDecision === null || !isDeepStrictEqual(durableDecision, options.humanDecision)) {
|
|
279
294
|
throw new Error("Accepted human decision does not match the durable decision record");
|
|
280
295
|
}
|
|
281
296
|
acceptedResponse = validateHumanDecisionResponse(request, durableDecision.response);
|
|
282
297
|
acceptedNodeId = request.nodeId;
|
|
283
|
-
normalizedInput =
|
|
298
|
+
normalizedInput = structuredClone(parent.state.input);
|
|
284
299
|
}
|
|
285
300
|
else {
|
|
286
301
|
const suppliedInput = input === undefined ? null : input;
|
|
@@ -292,11 +307,9 @@ export class WorkflowEngine {
|
|
|
292
307
|
}
|
|
293
308
|
const state = await this.createRunState(workflow, normalizedInput, options.workflowSource, options.runId);
|
|
294
309
|
state.parentRunId = parentRunId;
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
state.
|
|
298
|
-
state.results = (await resolveArtifacts(parent.state.results, parent.runDir));
|
|
299
|
-
state.steps = (await resolveArtifacts(parent.state.steps, parent.runDir));
|
|
310
|
+
state.outputs = structuredClone(parent.state.outputs);
|
|
311
|
+
state.results = structuredClone(parent.state.results);
|
|
312
|
+
state.steps = structuredClone(parent.state.steps);
|
|
300
313
|
if (humanContract !== undefined && options.humanDecision !== undefined) {
|
|
301
314
|
const receipt = {
|
|
302
315
|
decisionId: options.humanDecision.decisionId,
|
|
@@ -329,8 +342,8 @@ export class WorkflowEngine {
|
|
|
329
342
|
state.steps[stepIndex] = { ...priorStep, output: acceptedResponse };
|
|
330
343
|
}
|
|
331
344
|
state.carriedStepCount = state.steps.length;
|
|
332
|
-
const
|
|
333
|
-
await this.persist(
|
|
345
|
+
const runId = await this.store.initializeRun(workflow, state);
|
|
346
|
+
await this.persist(runId, state, {
|
|
334
347
|
scope: "run",
|
|
335
348
|
type: "run_started",
|
|
336
349
|
payload: {
|
|
@@ -342,24 +355,24 @@ export class WorkflowEngine {
|
|
|
342
355
|
carriedSteps: state.steps.length,
|
|
343
356
|
},
|
|
344
357
|
});
|
|
345
|
-
await this.onRunStarted?.(
|
|
358
|
+
await this.onRunStarted?.(runId, state);
|
|
346
359
|
const point = this.resumePointFor(workflow, state, "continue");
|
|
347
360
|
if (point.nodeId === null) {
|
|
348
361
|
// The checkpoint was the final node; the answer completes the chain.
|
|
349
|
-
await this.finishRun(
|
|
350
|
-
return {
|
|
362
|
+
await this.finishRun(runId, state, "completed", { finalOutput: point.lastOutput });
|
|
363
|
+
return { runId, state };
|
|
351
364
|
}
|
|
352
365
|
try {
|
|
353
|
-
await this.executeGraph(workflow, state,
|
|
366
|
+
await this.executeGraph(workflow, state, runId, point.nodeId, countExecutableSteps(workflow, state.steps), point.lastOutput);
|
|
354
367
|
}
|
|
355
368
|
catch (error) {
|
|
356
369
|
if (isRunParkedError(error) || this.parked) {
|
|
357
|
-
return {
|
|
370
|
+
return { runId, state };
|
|
358
371
|
}
|
|
359
|
-
await this.finishAfterError(
|
|
360
|
-
return {
|
|
372
|
+
await this.finishAfterError(runId, state, error);
|
|
373
|
+
return { runId, state };
|
|
361
374
|
}
|
|
362
|
-
return {
|
|
375
|
+
return { runId, state };
|
|
363
376
|
}
|
|
364
377
|
/**
|
|
365
378
|
* Find where a resumed run continues. An in-flight node reruns; otherwise
|
|
@@ -401,15 +414,15 @@ export class WorkflowEngine {
|
|
|
401
414
|
const next = resolveNextForOutcome(workflow.edges, lastStep.nodeId, result);
|
|
402
415
|
return next === null ? { nodeId: null, failedResult: result } : { nodeId: next };
|
|
403
416
|
}
|
|
404
|
-
async finishAfterError(
|
|
417
|
+
async finishAfterError(runId, state, error) {
|
|
405
418
|
const cancelled = this.cancelled || isAbortLikeError(error);
|
|
406
419
|
try {
|
|
407
|
-
await this.finishRun(
|
|
420
|
+
await this.finishRun(runId, state, cancelled ? "cancelled" : "failed", {
|
|
408
421
|
error: errorMessage(error),
|
|
409
422
|
});
|
|
410
423
|
}
|
|
411
424
|
catch (finishError) {
|
|
412
|
-
// A fenced-out runner must not touch
|
|
425
|
+
// A fenced-out runner must not touch run state, including terminal
|
|
413
426
|
// projections. Propagate the claim loss instead of the node error.
|
|
414
427
|
if (isClaimLostError(finishError)) {
|
|
415
428
|
throw finishError;
|
|
@@ -461,14 +474,14 @@ export class WorkflowEngine {
|
|
|
461
474
|
updates: [],
|
|
462
475
|
};
|
|
463
476
|
}
|
|
464
|
-
async executeGraph(workflow, state,
|
|
477
|
+
async executeGraph(workflow, state, runId, startNodeId = workflow.startAt, executedStepsBase = 0, initialLastOutput, resumedAttempt) {
|
|
465
478
|
const maxSteps = workflow.maxSteps ?? this.maxSteps;
|
|
466
479
|
const composition = compositionMetadata(workflow);
|
|
467
480
|
let currentNodeId = startNodeId;
|
|
468
481
|
let executedSteps = executedStepsBase;
|
|
469
482
|
let lastOutput = initialLastOutput;
|
|
470
483
|
while (currentNodeId !== null) {
|
|
471
|
-
await this.holdWhilePaused(state,
|
|
484
|
+
await this.holdWhilePaused(state, runId);
|
|
472
485
|
const isTransition = composition?.entries[currentNodeId] !== undefined ||
|
|
473
486
|
composition?.exits[currentNodeId] !== undefined;
|
|
474
487
|
if (!isTransition) {
|
|
@@ -482,7 +495,9 @@ export class WorkflowEngine {
|
|
|
482
495
|
if (!node) {
|
|
483
496
|
throw new Error(`Workflow node is missing: ${currentNodeId}`);
|
|
484
497
|
}
|
|
485
|
-
const
|
|
498
|
+
const activeResume = resumedAttempt?.nodeId === currentNodeId ? resumedAttempt : undefined;
|
|
499
|
+
resumedAttempt = undefined;
|
|
500
|
+
const attempt = await this.executeNode(workflow, state, runId, currentNodeId, node, activeResume);
|
|
486
501
|
if (this.parked) {
|
|
487
502
|
// Do not record the aborted attempt: the projection keeps the node
|
|
488
503
|
// as in-flight, and resume reruns it with a fresh attempt.
|
|
@@ -491,7 +506,7 @@ export class WorkflowEngine {
|
|
|
491
506
|
this.recordAttempt(workflow, state, attempt);
|
|
492
507
|
// The terminal node event carries the output, receipt, and conversation
|
|
493
508
|
// linkage so the trace alone is sufficient to reconstruct the run.
|
|
494
|
-
await this.persist(
|
|
509
|
+
await this.persist(runId, state, {
|
|
495
510
|
scope: "node",
|
|
496
511
|
type: attempt.result.outcome === "ok" ? "node_finished" : "node_failed",
|
|
497
512
|
nodeId: attempt.result.nodeId,
|
|
@@ -502,6 +517,9 @@ export class WorkflowEngine {
|
|
|
502
517
|
...(attempt.result.outcome === "ok" ? { output: attempt.result.output ?? null } : {}),
|
|
503
518
|
...(attempt.result.error !== undefined ? { error: attempt.result.error } : {}),
|
|
504
519
|
...(attempt.execution?.action !== undefined ? { action: attempt.execution.action } : {}),
|
|
520
|
+
...(attempt.execution?.assistantMessage !== undefined
|
|
521
|
+
? { assistantMessage: attempt.execution.assistantMessage }
|
|
522
|
+
: {}),
|
|
505
523
|
...(attempt.execution?.conversation !== undefined
|
|
506
524
|
? { conversation: attempt.execution.conversation }
|
|
507
525
|
: {}),
|
|
@@ -514,7 +532,7 @@ export class WorkflowEngine {
|
|
|
514
532
|
const entered = composition?.entries[attempt.result.nodeId];
|
|
515
533
|
if (entered !== undefined) {
|
|
516
534
|
const value = attempt.result.output;
|
|
517
|
-
await this.persist(
|
|
535
|
+
await this.persist(runId, state, {
|
|
518
536
|
scope: "run",
|
|
519
537
|
type: "include_entered",
|
|
520
538
|
payload: {
|
|
@@ -527,7 +545,7 @@ export class WorkflowEngine {
|
|
|
527
545
|
const exited = composition?.exits[attempt.result.nodeId];
|
|
528
546
|
if (exited !== undefined) {
|
|
529
547
|
const entrySteps = state.steps.filter((step) => step.nodeId === exited.mountPath);
|
|
530
|
-
await this.persist(
|
|
548
|
+
await this.persist(runId, state, {
|
|
531
549
|
scope: "run",
|
|
532
550
|
type: "include_exited",
|
|
533
551
|
payload: {
|
|
@@ -541,7 +559,7 @@ export class WorkflowEngine {
|
|
|
541
559
|
}
|
|
542
560
|
lastOutput = attempt.result.output;
|
|
543
561
|
if (node.nodeType === "checkpoint") {
|
|
544
|
-
await this.finishRun(
|
|
562
|
+
await this.finishRun(runId, state, "waiting", {
|
|
545
563
|
waitingOn: attempt.result.nodeId,
|
|
546
564
|
finalOutput: lastOutput,
|
|
547
565
|
});
|
|
@@ -549,13 +567,13 @@ export class WorkflowEngine {
|
|
|
549
567
|
}
|
|
550
568
|
currentNodeId = resolveNext(workflow.edges, attempt.result.nodeId, attempt.result.output, attempt.result);
|
|
551
569
|
}
|
|
552
|
-
await this.finishRun(
|
|
570
|
+
await this.finishRun(runId, state, "completed", { finalOutput: lastOutput });
|
|
553
571
|
}
|
|
554
572
|
/**
|
|
555
573
|
* Hold the run at the step boundary while a pause is in effect. Pausing
|
|
556
574
|
* never interrupts a node mid-flight; it only delays the next dispatch.
|
|
557
575
|
*/
|
|
558
|
-
async holdWhilePaused(state,
|
|
576
|
+
async holdWhilePaused(state, runId) {
|
|
559
577
|
if (this.parked) {
|
|
560
578
|
throw new RunParkedError();
|
|
561
579
|
}
|
|
@@ -566,7 +584,7 @@ export class WorkflowEngine {
|
|
|
566
584
|
return;
|
|
567
585
|
}
|
|
568
586
|
state.paused = true;
|
|
569
|
-
await this.persist(
|
|
587
|
+
await this.persist(runId, state, { scope: "run", type: "run_paused", payload: {} });
|
|
570
588
|
while (this.paused && !this.cancelled && !this.parked) {
|
|
571
589
|
await new Promise((resolve) => {
|
|
572
590
|
this.wakePause = resolve;
|
|
@@ -580,7 +598,7 @@ export class WorkflowEngine {
|
|
|
580
598
|
if (this.cancelled) {
|
|
581
599
|
throw new CancelledError();
|
|
582
600
|
}
|
|
583
|
-
await this.persist(
|
|
601
|
+
await this.persist(runId, state, { scope: "run", type: "run_resumed", payload: {} });
|
|
584
602
|
}
|
|
585
603
|
routeAfterFailure(workflow, state, attempt) {
|
|
586
604
|
if (attempt.result.outcome === "cancelled" || this.cancelled) {
|
|
@@ -624,6 +642,9 @@ export class WorkflowEngine {
|
|
|
624
642
|
output: attempt.result.output ?? null,
|
|
625
643
|
...(attempt.result.error !== undefined ? { error: attempt.result.error } : {}),
|
|
626
644
|
...(attempt.execution?.action !== undefined ? { action: attempt.execution.action } : {}),
|
|
645
|
+
...(attempt.execution?.assistantMessage !== undefined
|
|
646
|
+
? { assistantMessage: attempt.execution.assistantMessage }
|
|
647
|
+
: {}),
|
|
627
648
|
...(attempt.execution?.conversation !== undefined
|
|
628
649
|
? { conversation: attempt.execution.conversation }
|
|
629
650
|
: {}),
|
|
@@ -634,25 +655,27 @@ export class WorkflowEngine {
|
|
|
634
655
|
delete state.currentNodeStartedAt;
|
|
635
656
|
delete state.statusDetail;
|
|
636
657
|
}
|
|
637
|
-
async executeNode(workflow, state,
|
|
638
|
-
const attemptId = randomUUID();
|
|
639
|
-
const startedAt = new Date().toISOString();
|
|
658
|
+
async executeNode(workflow, state, runId, nodeId, node, resumedAttempt) {
|
|
659
|
+
const attemptId = resumedAttempt?.attemptId ?? randomUUID();
|
|
660
|
+
const startedAt = resumedAttempt?.startedAt ?? new Date().toISOString();
|
|
640
661
|
state.currentNode = nodeId;
|
|
641
662
|
state.currentAttemptId = attemptId;
|
|
642
663
|
state.currentNodeStartedAt = startedAt;
|
|
643
664
|
if (node.statusDetail !== undefined) {
|
|
644
665
|
state.statusDetail = node.statusDetail;
|
|
645
666
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
667
|
+
if (resumedAttempt === undefined) {
|
|
668
|
+
await this.persist(runId, state, {
|
|
669
|
+
scope: "node",
|
|
670
|
+
type: "node_started",
|
|
671
|
+
nodeId,
|
|
672
|
+
attemptId,
|
|
673
|
+
payload: { nodeType: node.nodeType },
|
|
674
|
+
});
|
|
675
|
+
}
|
|
653
676
|
const meta = { promptText: null };
|
|
654
677
|
try {
|
|
655
|
-
const execution = await this.runNodeWithTimeout(workflow, state,
|
|
678
|
+
const execution = await this.runNodeWithTimeout(workflow, state, runId, nodeId, attemptId, node, meta);
|
|
656
679
|
return {
|
|
657
680
|
result: this.createNodeResult(nodeId, node, attemptId, startedAt, "ok", execution.output),
|
|
658
681
|
execution,
|
|
@@ -698,7 +721,7 @@ export class WorkflowEngine {
|
|
|
698
721
|
...(output !== undefined ? { output } : {}),
|
|
699
722
|
};
|
|
700
723
|
}
|
|
701
|
-
async runNodeWithTimeout(workflow, state,
|
|
724
|
+
async runNodeWithTimeout(workflow, state, runId, nodeId, attemptId, node, meta) {
|
|
702
725
|
const abort = new AbortController();
|
|
703
726
|
const context = this.createNodeContext(state, abort.signal);
|
|
704
727
|
let timer;
|
|
@@ -722,15 +745,15 @@ export class WorkflowEngine {
|
|
|
722
745
|
abort.abort(new TimeoutError(timeoutMs));
|
|
723
746
|
}, timeoutMs);
|
|
724
747
|
}
|
|
725
|
-
this.activeAttempt = {
|
|
726
|
-
const dispatched = this.dispatchNode(workflow, state,
|
|
748
|
+
this.activeAttempt = { runId, state, nodeId, attemptId, signal: abort.signal };
|
|
749
|
+
const dispatched = this.dispatchNode(workflow, state, runId, nodeId, attemptId, node, context, abort.signal, meta);
|
|
727
750
|
dispatchSettled = dispatched.then(() => undefined, () => undefined);
|
|
728
751
|
// Race the dispatch against the abort signal so timeouts and cancel
|
|
729
752
|
// take effect even for node callbacks that never observe the signal.
|
|
730
753
|
const execution = await Promise.race([dispatched, abortRejection(abort.signal)]);
|
|
731
754
|
if (execution.output === undefined) {
|
|
732
755
|
// JSON cannot represent undefined; normalize so the in-memory state
|
|
733
|
-
// matches what
|
|
756
|
+
// matches what persisted canonical JSON round-trips to.
|
|
734
757
|
execution.output = null;
|
|
735
758
|
}
|
|
736
759
|
assertJsonSerializable(execution.output, `Node ${nodeId} output`);
|
|
@@ -787,10 +810,10 @@ export class WorkflowEngine {
|
|
|
787
810
|
clearTimeout(timer);
|
|
788
811
|
}
|
|
789
812
|
}
|
|
790
|
-
async dispatchNode(workflow, state,
|
|
813
|
+
async dispatchNode(workflow, state, runId, nodeId, attemptId, node, context, signal, meta) {
|
|
791
814
|
switch (node.nodeType) {
|
|
792
815
|
case "agent":
|
|
793
|
-
return await this.runAgentNode(workflow, state,
|
|
816
|
+
return await this.runAgentNode(workflow, state, runId, nodeId, attemptId, node, context, signal, meta);
|
|
794
817
|
case "compute":
|
|
795
818
|
return { output: await node.run(context), promptText: null };
|
|
796
819
|
case "notify": {
|
|
@@ -833,22 +856,40 @@ export class WorkflowEngine {
|
|
|
833
856
|
signal,
|
|
834
857
|
};
|
|
835
858
|
}
|
|
836
|
-
async runAgentNode(workflow, state,
|
|
859
|
+
async runAgentNode(workflow, state, runId, nodeId, attemptId, node, context, signal, meta) {
|
|
860
|
+
const assistant = assistantMessageConfig(node);
|
|
861
|
+
if (assistant !== undefined) {
|
|
862
|
+
if (this.executor.assistantMessageMode === "park") {
|
|
863
|
+
state.statusDetail = "waiting for origin Pi session";
|
|
864
|
+
await this.persist(runId, state, {
|
|
865
|
+
scope: "agent",
|
|
866
|
+
type: "agent_session_required",
|
|
867
|
+
nodeId,
|
|
868
|
+
attemptId,
|
|
869
|
+
payload: { completion: "assistant" },
|
|
870
|
+
});
|
|
871
|
+
this.parked = true;
|
|
872
|
+
throw new RunParkedError();
|
|
873
|
+
}
|
|
874
|
+
if (this.executor.assistantMessageMode !== "visible") {
|
|
875
|
+
throw new Error("Assistant completion requires an origin Pi session");
|
|
876
|
+
}
|
|
877
|
+
}
|
|
837
878
|
const basePrompt = await node.prompt(context);
|
|
838
879
|
if (signal.aborted) {
|
|
839
880
|
// The node timed out or the run was cancelled while the async prompt
|
|
840
|
-
// builder ran; a late continuation must not write into a
|
|
881
|
+
// builder ran; a late continuation must not write into a run that
|
|
841
882
|
// may already be terminal.
|
|
842
883
|
throw abortError(signal);
|
|
843
884
|
}
|
|
844
885
|
const prompt = appendStepContract(basePrompt, workflow.name, nodeId, attemptId, node.expectedOutput);
|
|
845
886
|
meta.promptText = prompt;
|
|
846
|
-
await this.persist(
|
|
887
|
+
await this.persist(runId, state, {
|
|
847
888
|
scope: "agent",
|
|
848
889
|
type: "agent_prompt_sent",
|
|
849
890
|
nodeId,
|
|
850
891
|
attemptId,
|
|
851
|
-
payload: { prompt },
|
|
892
|
+
payload: { prompt, completion: assistant === undefined ? "submit" : "assistant" },
|
|
852
893
|
});
|
|
853
894
|
const submission = await this.executor.runAgentStep({
|
|
854
895
|
contract: {
|
|
@@ -856,7 +897,11 @@ export class WorkflowEngine {
|
|
|
856
897
|
workflowName: workflow.name,
|
|
857
898
|
nodeId,
|
|
858
899
|
attemptId,
|
|
859
|
-
|
|
900
|
+
completion: assistant === undefined ? "submit" : "assistant",
|
|
901
|
+
...(typeof node.expectedOutput === "string"
|
|
902
|
+
? { expectedOutput: node.expectedOutput }
|
|
903
|
+
: {}),
|
|
904
|
+
...(assistant?.maxChars !== undefined ? { maxOutputChars: assistant.maxChars } : {}),
|
|
860
905
|
},
|
|
861
906
|
prompt,
|
|
862
907
|
...(state.runTitle !== undefined || node.statusDetail !== undefined
|
|
@@ -873,10 +918,19 @@ export class WorkflowEngine {
|
|
|
873
918
|
return {
|
|
874
919
|
output: submission.output,
|
|
875
920
|
promptText: prompt,
|
|
921
|
+
...(submission.assistantMessage !== undefined
|
|
922
|
+
? { assistantMessage: submission.assistantMessage }
|
|
923
|
+
: {}),
|
|
876
924
|
...(submission.conversation !== undefined ? { conversation: submission.conversation } : {}),
|
|
877
925
|
};
|
|
878
926
|
}
|
|
879
927
|
async acceptSubmission(node, context, output) {
|
|
928
|
+
if (assistantMessageConfig(node) !== undefined) {
|
|
929
|
+
return {
|
|
930
|
+
ok: false,
|
|
931
|
+
error: "This step completes with a normal assistant response. Do not submit workflow output.",
|
|
932
|
+
};
|
|
933
|
+
}
|
|
880
934
|
try {
|
|
881
935
|
const normalized = normalizeAgentOutput(output);
|
|
882
936
|
const validated = node.validate ? await node.validate(normalized, context) : normalized;
|
|
@@ -902,8 +956,8 @@ export class WorkflowEngine {
|
|
|
902
956
|
const output = await node.run(actionContext);
|
|
903
957
|
return { output, promptText: null, action: { actionType: "function" } };
|
|
904
958
|
}
|
|
905
|
-
async persist(
|
|
906
|
-
const traceEvent = await this.store.writeSnapshot(
|
|
959
|
+
async persist(runId, state, event) {
|
|
960
|
+
const traceEvent = await this.store.writeSnapshot(runId, state, event);
|
|
907
961
|
try {
|
|
908
962
|
this.onEvent?.(traceEvent, state);
|
|
909
963
|
}
|
|
@@ -912,14 +966,14 @@ export class WorkflowEngine {
|
|
|
912
966
|
// correctness; a throwing observer would otherwise fail the run.
|
|
913
967
|
}
|
|
914
968
|
}
|
|
915
|
-
async finishRun(
|
|
969
|
+
async finishRun(runId, state, status, fields) {
|
|
916
970
|
if (status === "failed" && state.status === "timed_out") {
|
|
917
971
|
status = "timed_out";
|
|
918
972
|
}
|
|
919
973
|
// Let observers (e.g. the session recorder) stop and drain before the
|
|
920
|
-
// terminal event exists, so the
|
|
974
|
+
// terminal event exists, so the terminal fact is immutable from that point on.
|
|
921
975
|
try {
|
|
922
|
-
await this.onRunFinishing?.(
|
|
976
|
+
await this.onRunFinishing?.(runId, state);
|
|
923
977
|
}
|
|
924
978
|
catch {
|
|
925
979
|
// Finishing the run wins over observer failures.
|
|
@@ -938,7 +992,7 @@ export class WorkflowEngine {
|
|
|
938
992
|
delete state.currentNode;
|
|
939
993
|
delete state.currentAttemptId;
|
|
940
994
|
delete state.currentNodeStartedAt;
|
|
941
|
-
await this.persist(
|
|
995
|
+
await this.persist(runId, state, {
|
|
942
996
|
scope: "run",
|
|
943
997
|
type: `run_${status}`,
|
|
944
998
|
payload: {
|
|
@@ -968,7 +1022,9 @@ async function runCheckpointNode(node, context, execution) {
|
|
|
968
1022
|
prompt,
|
|
969
1023
|
...(timeout !== undefined ? { timeout } : {}),
|
|
970
1024
|
});
|
|
971
|
-
await new HumanDecisionStore(execution.store.
|
|
1025
|
+
await new HumanDecisionStore(execution.store.databasePath, {
|
|
1026
|
+
state: execution.store.state,
|
|
1027
|
+
}).createRequest(request);
|
|
972
1028
|
return { output: request, promptText: null };
|
|
973
1029
|
}
|
|
974
1030
|
const output = node.run ? await node.run(context) : { summary: node.summary ?? "checkpoint" };
|
|
@@ -1072,7 +1128,7 @@ function assertInvocationStepLimit(metadata, nodeId, steps) {
|
|
|
1072
1128
|
}
|
|
1073
1129
|
}
|
|
1074
1130
|
/**
|
|
1075
|
-
* Outputs are persisted to the run
|
|
1131
|
+
* Outputs are persisted to the SQLite run state, so they must be JSON-serializable.
|
|
1076
1132
|
* Failing here turns a bad callback return value into a normal node failure
|
|
1077
1133
|
* instead of corrupting the run state.
|
|
1078
1134
|
*/
|
|
@@ -1130,11 +1186,35 @@ function normalizeAgentOutput(output) {
|
|
|
1130
1186
|
return output;
|
|
1131
1187
|
}
|
|
1132
1188
|
}
|
|
1189
|
+
function assistantMessageOutput(expectedOutput) {
|
|
1190
|
+
return typeof expectedOutput === "object" && expectedOutput?.kind === "assistant-message"
|
|
1191
|
+
? expectedOutput
|
|
1192
|
+
: undefined;
|
|
1193
|
+
}
|
|
1194
|
+
function assistantMessageConfig(node) {
|
|
1195
|
+
return assistantMessageOutput(node.expectedOutput);
|
|
1196
|
+
}
|
|
1133
1197
|
/**
|
|
1134
1198
|
* The step contract appended to every agent-node prompt. This is the
|
|
1135
1199
|
* documented standard for how the model completes a workflow step.
|
|
1136
1200
|
*/
|
|
1137
1201
|
export function appendStepContract(prompt, workflowName, nodeId, attemptId, expectedOutput) {
|
|
1202
|
+
const assistant = assistantMessageOutput(expectedOutput);
|
|
1203
|
+
if (assistant !== undefined) {
|
|
1204
|
+
return [
|
|
1205
|
+
prompt.trimEnd(),
|
|
1206
|
+
"",
|
|
1207
|
+
"---",
|
|
1208
|
+
`Workflow step contract (workflow: ${workflowName}, step: ${nodeId}, attempt: ${attemptId})`,
|
|
1209
|
+
"",
|
|
1210
|
+
"Reply with a normal assistant message.",
|
|
1211
|
+
"Do not call the workflow tool to complete this step.",
|
|
1212
|
+
"Your visible reply becomes the workflow step output after the turn settles.",
|
|
1213
|
+
...(assistant.maxChars !== undefined
|
|
1214
|
+
? [`Keep the visible reply within ${assistant.maxChars} characters.`]
|
|
1215
|
+
: []),
|
|
1216
|
+
].join("\n");
|
|
1217
|
+
}
|
|
1138
1218
|
return [
|
|
1139
1219
|
prompt.trimEnd(),
|
|
1140
1220
|
"",
|
|
@@ -1145,7 +1225,7 @@ export function appendStepContract(prompt, workflowName, nodeId, attemptId, expe
|
|
|
1145
1225
|
`{"action": "update", "step": ${JSON.stringify(nodeId)}, "attempt": ${JSON.stringify(attemptId)}, "update": {"type": "...", "key": "...", "data": {...}}}`,
|
|
1146
1226
|
"Complete this step by calling the `workflow` tool exactly once with:",
|
|
1147
1227
|
`{"action": "submit", "step": ${JSON.stringify(nodeId)}, "attempt": ${JSON.stringify(attemptId)}, "output": <your result>}`,
|
|
1148
|
-
`Expected output: ${expectedOutput
|
|
1228
|
+
`Expected output: ${typeof expectedOutput === "string" ? expectedOutput : "a JSON object with your result"}`,
|
|
1149
1229
|
"The step is complete only after the workflow tool accepts the output.",
|
|
1150
1230
|
"If the tool reports a validation error, correct the output and call it again.",
|
|
1151
1231
|
].join("\n");
|