@osolmaz/pi-workflows 0.12.1 → 0.13.1
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 +59 -33
- package/dist/builtins/autodoc.workflow.d.ts +194 -7
- package/dist/builtins/autodoc.workflow.js +156 -30
- package/dist/builtins/autodoc.workflow.js.map +1 -1
- package/dist/builtins/autoimplement-command-batches.d.ts +1 -0
- package/dist/builtins/autoimplement-command-batches.js +29 -31
- package/dist/builtins/autoimplement-command-batches.js.map +1 -1
- package/dist/builtins/autoimplement.workflow.d.ts +1619 -97
- package/dist/builtins/autoimplement.workflow.js +416 -153
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- 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 +7 -5
- package/dist/builtins/catalog.js.map +1 -1
- package/dist/builtins/change-verification.workflow.d.ts +110 -0
- package/dist/builtins/change-verification.workflow.js +860 -0
- package/dist/builtins/change-verification.workflow.js.map +1 -0
- 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 +499 -217
- 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 +527 -26
- package/dist/builtins/plan-change.workflow.js +26 -0
- package/dist/builtins/plan-change.workflow.js.map +1 -1
- 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/builtins/workspace-preparation.workflow.d.ts +75 -0
- package/dist/builtins/workspace-preparation.workflow.js +498 -0
- package/dist/builtins/workspace-preparation.workflow.js.map +1 -0
- 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 +90 -6
- 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 -117
- 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 +27 -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/2026-08-24-change-scoped-verification-plan.md +419 -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 +111 -62
- 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/autodoc/SKILL.md +7 -0
- package/skills/autoimplement/SKILL.md +4 -0
- 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/autodoc.workflow.ts +184 -33
- package/src/builtins/autoimplement-command-batches.ts +39 -33
- package/src/builtins/autoimplement.workflow.ts +483 -175
- package/src/builtins/autoplan.workflow.ts +290 -40
- package/src/builtins/catalog.ts +7 -5
- package/src/builtins/change-verification.workflow.ts +1143 -0
- package/src/builtins/index.ts +7 -1
- package/src/builtins/monitor.workflow.ts +667 -250
- package/src/builtins/plain-summary.workflow.ts +185 -0
- package/src/builtins/plan-change.workflow.ts +35 -0
- package/src/builtins/sanity-check.workflow.ts +62 -7
- package/src/builtins/workspace-preparation.workflow.ts +668 -0
- 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 +106 -6
- package/src/extension/herdr-viewer.ts +0 -5
- package/src/extension/index.ts +180 -132
- 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/src/workflows/engine.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { isDeepStrictEqual } from "node:util";
|
|
3
|
-
import { resolveArtifacts } from "./artifacts.js";
|
|
4
3
|
import {
|
|
5
4
|
compileWorkflowDefinition,
|
|
6
5
|
compositionMetadata,
|
|
@@ -29,12 +28,14 @@ import {
|
|
|
29
28
|
WorkflowRunStore,
|
|
30
29
|
createDefinitionSnapshot,
|
|
31
30
|
createRunId,
|
|
32
|
-
readRunBundle,
|
|
33
31
|
} from "./store.js";
|
|
34
32
|
import type {
|
|
35
33
|
ResolvedHumanDecision,
|
|
34
|
+
AgentExpectedOutput,
|
|
36
35
|
AgentNodeDefinition,
|
|
37
36
|
AgentStepExecutor,
|
|
37
|
+
AssistantMessageOutput,
|
|
38
|
+
AssistantMessageReceipt,
|
|
38
39
|
ActionNodeDefinition,
|
|
39
40
|
CheckpointNodeDefinition,
|
|
40
41
|
ConversationRange,
|
|
@@ -71,6 +72,7 @@ type NodeExecution = {
|
|
|
71
72
|
output: unknown;
|
|
72
73
|
promptText: string | null;
|
|
73
74
|
action?: WorkflowActionReceipt;
|
|
75
|
+
assistantMessage?: AssistantMessageReceipt;
|
|
74
76
|
conversation?: ConversationRange;
|
|
75
77
|
};
|
|
76
78
|
|
|
@@ -89,11 +91,17 @@ type NodeAttempt = {
|
|
|
89
91
|
error?: unknown;
|
|
90
92
|
};
|
|
91
93
|
|
|
94
|
+
type ResumedNodeAttempt = {
|
|
95
|
+
nodeId: string;
|
|
96
|
+
attemptId: string;
|
|
97
|
+
startedAt: string;
|
|
98
|
+
};
|
|
99
|
+
|
|
92
100
|
/**
|
|
93
101
|
* Executes a workflow graph step by step. Agent steps are delegated to the
|
|
94
102
|
* configured executor; compute/action/checkpoint nodes run inline. Every
|
|
95
|
-
* state transition is persisted to the run
|
|
96
|
-
* so a live viewer can follow
|
|
103
|
+
* state transition is persisted to the SQLite run state before the engine moves on,
|
|
104
|
+
* so a live viewer can follow committed SQLite events.
|
|
97
105
|
*/
|
|
98
106
|
export class WorkflowEngine {
|
|
99
107
|
private readonly executor: AgentStepExecutor;
|
|
@@ -107,7 +115,7 @@ export class WorkflowEngine {
|
|
|
107
115
|
private activeAbort: AbortController | null = null;
|
|
108
116
|
private activeAttempt:
|
|
109
117
|
| {
|
|
110
|
-
|
|
118
|
+
runId: string;
|
|
111
119
|
state: WorkflowRunState;
|
|
112
120
|
nodeId: string;
|
|
113
121
|
attemptId: string;
|
|
@@ -124,7 +132,7 @@ export class WorkflowEngine {
|
|
|
124
132
|
constructor(options: WorkflowEngineOptions) {
|
|
125
133
|
this.executor = options.executor;
|
|
126
134
|
this.notificationSink = options.notificationSink;
|
|
127
|
-
this.store = options.store ?? new WorkflowRunStore(options.
|
|
135
|
+
this.store = options.store ?? new WorkflowRunStore(options.databasePath);
|
|
128
136
|
this.defaultNodeTimeoutMs = options.defaultNodeTimeoutMs ?? DEFAULT_NODE_TIMEOUT_MS;
|
|
129
137
|
this.maxSteps = options.maxSteps ?? DEFAULT_MAX_STEPS;
|
|
130
138
|
this.onEvent = options.onEvent;
|
|
@@ -132,8 +140,8 @@ export class WorkflowEngine {
|
|
|
132
140
|
this.onRunFinishing = options.onRunFinishing;
|
|
133
141
|
}
|
|
134
142
|
|
|
135
|
-
get
|
|
136
|
-
return this.store.
|
|
143
|
+
get databasePath(): string {
|
|
144
|
+
return this.store.databasePath;
|
|
137
145
|
}
|
|
138
146
|
|
|
139
147
|
/** Publish a durable update for the currently active attempt without completing it. */
|
|
@@ -171,7 +179,7 @@ export class WorkflowEngine {
|
|
|
171
179
|
}
|
|
172
180
|
limiter.take();
|
|
173
181
|
const { event, record } = await this.store.publishUpdate(
|
|
174
|
-
active.
|
|
182
|
+
active.runId,
|
|
175
183
|
active.state,
|
|
176
184
|
active.nodeId,
|
|
177
185
|
active.attemptId,
|
|
@@ -235,8 +243,7 @@ export class WorkflowEngine {
|
|
|
235
243
|
): Promise<WorkflowRunResult> {
|
|
236
244
|
workflow = isCompiledWorkflow(workflow) ? workflow : compileWorkflowDefinition(workflow);
|
|
237
245
|
validateWorkflowDefinition(workflow);
|
|
238
|
-
// Fail before any
|
|
239
|
-
// on disk or silently change shape when state.json round-trips.
|
|
246
|
+
// Fail before any run row exists so bad input cannot leave partial state.
|
|
240
247
|
const suppliedInput = input === undefined ? null : input;
|
|
241
248
|
const normalizedInput = workflow.input ? await workflow.input(suppliedInput) : suppliedInput;
|
|
242
249
|
assertJsonSerializable(normalizedInput, "Workflow run input");
|
|
@@ -253,8 +260,8 @@ export class WorkflowEngine {
|
|
|
253
260
|
options.workflowSource,
|
|
254
261
|
options.runId,
|
|
255
262
|
);
|
|
256
|
-
const
|
|
257
|
-
await this.persist(
|
|
263
|
+
const runId = await this.store.initializeRun(workflow, state);
|
|
264
|
+
await this.persist(runId, state, {
|
|
258
265
|
scope: "run",
|
|
259
266
|
type: "run_started",
|
|
260
267
|
payload: {
|
|
@@ -266,18 +273,18 @@ export class WorkflowEngine {
|
|
|
266
273
|
// Awaited so anything the hook writes (e.g. a session binding and its
|
|
267
274
|
// `session_bound` event) lands before node events and can never trail
|
|
268
275
|
// the terminal event of a fast run.
|
|
269
|
-
await this.onRunStarted?.(
|
|
276
|
+
await this.onRunStarted?.(runId, state);
|
|
270
277
|
|
|
271
278
|
try {
|
|
272
|
-
await this.executeGraph(workflow, state,
|
|
279
|
+
await this.executeGraph(workflow, state, runId);
|
|
273
280
|
} catch (error) {
|
|
274
281
|
if (isRunParkedError(error) || this.parked) {
|
|
275
|
-
return {
|
|
282
|
+
return { runId, state };
|
|
276
283
|
}
|
|
277
|
-
await this.finishAfterError(
|
|
278
|
-
return {
|
|
284
|
+
await this.finishAfterError(runId, state, error);
|
|
285
|
+
return { runId, state };
|
|
279
286
|
}
|
|
280
|
-
return {
|
|
287
|
+
return { runId, state };
|
|
281
288
|
}
|
|
282
289
|
|
|
283
290
|
/**
|
|
@@ -297,76 +304,94 @@ export class WorkflowEngine {
|
|
|
297
304
|
this.cancelled = false;
|
|
298
305
|
this.paused = false;
|
|
299
306
|
this.parked = false;
|
|
300
|
-
const
|
|
301
|
-
const
|
|
302
|
-
const state = bundle.state;
|
|
307
|
+
const loaded = await this.store.prepareRunResume(runId);
|
|
308
|
+
const state = loaded.state;
|
|
303
309
|
const sourceMismatch = workflowIdentityMismatch(state, workflow, options.workflowSource);
|
|
304
310
|
if (sourceMismatch && options.force !== true) {
|
|
305
311
|
throw new WorkflowSourceChangedError(runId);
|
|
306
312
|
}
|
|
307
313
|
|
|
308
314
|
const point = this.resumePointFor(workflow, state, "wait");
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
315
|
+
const interruptedNode =
|
|
316
|
+
state.currentNode !== undefined ? workflow.nodes[state.currentNode] : undefined;
|
|
317
|
+
const resumedAttempt: ResumedNodeAttempt | undefined =
|
|
318
|
+
state.currentNode !== undefined &&
|
|
319
|
+
state.currentAttemptId !== undefined &&
|
|
320
|
+
state.currentNodeStartedAt !== undefined &&
|
|
321
|
+
interruptedNode?.nodeType === "agent" &&
|
|
322
|
+
assistantMessageConfig(interruptedNode) !== undefined
|
|
323
|
+
? {
|
|
324
|
+
nodeId: state.currentNode,
|
|
325
|
+
attemptId: state.currentAttemptId,
|
|
326
|
+
startedAt: state.currentNodeStartedAt,
|
|
327
|
+
}
|
|
328
|
+
: undefined;
|
|
329
|
+
// A resumed run starts unpaused. Submitted and non-agent nodes discard
|
|
330
|
+
// stale in-flight markers and start a new attempt. Assistant-message
|
|
331
|
+
// nodes keep their attempt id so the origin session can adopt an already
|
|
332
|
+
// visible response without showing it twice.
|
|
312
333
|
delete state.paused;
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
334
|
+
if (resumedAttempt === undefined) {
|
|
335
|
+
delete state.currentNode;
|
|
336
|
+
delete state.currentAttemptId;
|
|
337
|
+
delete state.currentNodeStartedAt;
|
|
338
|
+
delete state.statusDetail;
|
|
339
|
+
}
|
|
340
|
+
await this.persist(runId, state, {
|
|
318
341
|
scope: "run",
|
|
319
342
|
type: "run_resumed",
|
|
320
343
|
payload: {
|
|
321
344
|
...(point.nodeId !== null ? { resumeAt: point.nodeId } : {}),
|
|
345
|
+
...(resumedAttempt !== undefined ? { resumedAttemptId: resumedAttempt.attemptId } : {}),
|
|
322
346
|
replayedSteps: state.steps.length,
|
|
323
347
|
...(sourceMismatch ? { workflowSourceMismatch: true, forced: true } : {}),
|
|
324
348
|
},
|
|
325
349
|
});
|
|
326
|
-
await this.onRunStarted?.(
|
|
350
|
+
await this.onRunStarted?.(runId, state);
|
|
327
351
|
|
|
328
352
|
if (point.nodeId === null) {
|
|
329
353
|
// The last recorded transition already finished the graph; the crash
|
|
330
354
|
// happened before the terminal event was written. A finished
|
|
331
355
|
// checkpoint restores its waiting gate rather than completing.
|
|
332
356
|
if (point.waitingOn !== undefined) {
|
|
333
|
-
await this.finishRun(
|
|
357
|
+
await this.finishRun(runId, state, "waiting", {
|
|
334
358
|
waitingOn: point.waitingOn,
|
|
335
359
|
finalOutput: point.lastOutput,
|
|
336
360
|
});
|
|
337
361
|
} else if (point.failedResult === undefined) {
|
|
338
|
-
await this.finishRun(
|
|
362
|
+
await this.finishRun(runId, state, "completed", { finalOutput: point.lastOutput });
|
|
339
363
|
} else {
|
|
340
364
|
const timedOut = point.failedResult.outcome === "timed_out";
|
|
341
|
-
await this.finishRun(
|
|
365
|
+
await this.finishRun(runId, state, timedOut ? "timed_out" : "failed", {
|
|
342
366
|
error: point.failedResult.error ?? `Workflow node failed: ${point.failedResult.nodeId}`,
|
|
343
367
|
});
|
|
344
368
|
}
|
|
345
|
-
return {
|
|
369
|
+
return { runId, state };
|
|
346
370
|
}
|
|
347
371
|
|
|
348
372
|
try {
|
|
349
373
|
await this.executeGraph(
|
|
350
374
|
workflow,
|
|
351
375
|
state,
|
|
352
|
-
|
|
376
|
+
runId,
|
|
353
377
|
point.nodeId,
|
|
354
378
|
countExecutableSteps(workflow, state.steps),
|
|
355
379
|
point.lastOutput,
|
|
380
|
+
resumedAttempt,
|
|
356
381
|
);
|
|
357
382
|
} catch (error) {
|
|
358
383
|
if (isRunParkedError(error) || this.parked) {
|
|
359
|
-
return {
|
|
384
|
+
return { runId, state };
|
|
360
385
|
}
|
|
361
|
-
await this.finishAfterError(
|
|
362
|
-
return {
|
|
386
|
+
await this.finishAfterError(runId, state, error);
|
|
387
|
+
return { runId, state };
|
|
363
388
|
}
|
|
364
|
-
return {
|
|
389
|
+
return { runId, state };
|
|
365
390
|
}
|
|
366
391
|
|
|
367
392
|
/**
|
|
368
393
|
* Start a continuation run from a checkpointed parent. The new run gets a
|
|
369
|
-
* fresh
|
|
394
|
+
* fresh run and event stream, carries forward the parent's outputs, results,
|
|
370
395
|
* and step accounting, and continues routing after the checkpoint.
|
|
371
396
|
*/
|
|
372
397
|
async continueRun(
|
|
@@ -385,7 +410,7 @@ export class WorkflowEngine {
|
|
|
385
410
|
this.cancelled = false;
|
|
386
411
|
this.paused = false;
|
|
387
412
|
this.parked = false;
|
|
388
|
-
const parent =
|
|
413
|
+
const parent = this.store.readRun(parentRunId);
|
|
389
414
|
if (parent === null) {
|
|
390
415
|
throw new Error(`Cannot continue from unreadable workflow run: ${parentRunId}`);
|
|
391
416
|
}
|
|
@@ -418,15 +443,15 @@ export class WorkflowEngine {
|
|
|
418
443
|
) {
|
|
419
444
|
throw new Error("Accepted human decision does not match the waiting request");
|
|
420
445
|
}
|
|
421
|
-
const durableDecision = await new HumanDecisionStore(this.store.
|
|
422
|
-
|
|
423
|
-
);
|
|
446
|
+
const durableDecision = await new HumanDecisionStore(this.store.databasePath, {
|
|
447
|
+
state: this.store.state,
|
|
448
|
+
}).readResolved(request.decisionId);
|
|
424
449
|
if (durableDecision === null || !isDeepStrictEqual(durableDecision, options.humanDecision)) {
|
|
425
450
|
throw new Error("Accepted human decision does not match the durable decision record");
|
|
426
451
|
}
|
|
427
452
|
acceptedResponse = validateHumanDecisionResponse(request, durableDecision.response);
|
|
428
453
|
acceptedNodeId = request.nodeId;
|
|
429
|
-
normalizedInput =
|
|
454
|
+
normalizedInput = structuredClone(parent.state.input);
|
|
430
455
|
} else {
|
|
431
456
|
const suppliedInput = input === undefined ? null : input;
|
|
432
457
|
normalizedInput = workflow.input ? await workflow.input(suppliedInput) : suppliedInput;
|
|
@@ -443,20 +468,9 @@ export class WorkflowEngine {
|
|
|
443
468
|
options.runId,
|
|
444
469
|
);
|
|
445
470
|
state.parentRunId = parentRunId;
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
state.
|
|
449
|
-
parent.state.outputs,
|
|
450
|
-
parent.runDir,
|
|
451
|
-
)) as WorkflowRunState["outputs"];
|
|
452
|
-
state.results = (await resolveArtifacts(
|
|
453
|
-
parent.state.results,
|
|
454
|
-
parent.runDir,
|
|
455
|
-
)) as WorkflowRunState["results"];
|
|
456
|
-
state.steps = (await resolveArtifacts(
|
|
457
|
-
parent.state.steps,
|
|
458
|
-
parent.runDir,
|
|
459
|
-
)) as WorkflowRunState["steps"];
|
|
471
|
+
state.outputs = structuredClone(parent.state.outputs);
|
|
472
|
+
state.results = structuredClone(parent.state.results);
|
|
473
|
+
state.steps = structuredClone(parent.state.steps);
|
|
460
474
|
if (humanContract !== undefined && options.humanDecision !== undefined) {
|
|
461
475
|
const receipt = {
|
|
462
476
|
decisionId: options.humanDecision.decisionId,
|
|
@@ -490,8 +504,8 @@ export class WorkflowEngine {
|
|
|
490
504
|
}
|
|
491
505
|
state.carriedStepCount = state.steps.length;
|
|
492
506
|
|
|
493
|
-
const
|
|
494
|
-
await this.persist(
|
|
507
|
+
const runId = await this.store.initializeRun(workflow, state);
|
|
508
|
+
await this.persist(runId, state, {
|
|
495
509
|
scope: "run",
|
|
496
510
|
type: "run_started",
|
|
497
511
|
payload: {
|
|
@@ -503,31 +517,31 @@ export class WorkflowEngine {
|
|
|
503
517
|
carriedSteps: state.steps.length,
|
|
504
518
|
},
|
|
505
519
|
});
|
|
506
|
-
await this.onRunStarted?.(
|
|
520
|
+
await this.onRunStarted?.(runId, state);
|
|
507
521
|
|
|
508
522
|
const point = this.resumePointFor(workflow, state, "continue");
|
|
509
523
|
if (point.nodeId === null) {
|
|
510
524
|
// The checkpoint was the final node; the answer completes the chain.
|
|
511
|
-
await this.finishRun(
|
|
512
|
-
return {
|
|
525
|
+
await this.finishRun(runId, state, "completed", { finalOutput: point.lastOutput });
|
|
526
|
+
return { runId, state };
|
|
513
527
|
}
|
|
514
528
|
try {
|
|
515
529
|
await this.executeGraph(
|
|
516
530
|
workflow,
|
|
517
531
|
state,
|
|
518
|
-
|
|
532
|
+
runId,
|
|
519
533
|
point.nodeId,
|
|
520
534
|
countExecutableSteps(workflow, state.steps),
|
|
521
535
|
point.lastOutput,
|
|
522
536
|
);
|
|
523
537
|
} catch (error) {
|
|
524
538
|
if (isRunParkedError(error) || this.parked) {
|
|
525
|
-
return {
|
|
539
|
+
return { runId, state };
|
|
526
540
|
}
|
|
527
|
-
await this.finishAfterError(
|
|
528
|
-
return {
|
|
541
|
+
await this.finishAfterError(runId, state, error);
|
|
542
|
+
return { runId, state };
|
|
529
543
|
}
|
|
530
|
-
return {
|
|
544
|
+
return { runId, state };
|
|
531
545
|
}
|
|
532
546
|
|
|
533
547
|
/**
|
|
@@ -583,17 +597,17 @@ export class WorkflowEngine {
|
|
|
583
597
|
}
|
|
584
598
|
|
|
585
599
|
private async finishAfterError(
|
|
586
|
-
|
|
600
|
+
runId: string,
|
|
587
601
|
state: WorkflowRunState,
|
|
588
602
|
error: unknown,
|
|
589
603
|
): Promise<void> {
|
|
590
604
|
const cancelled = this.cancelled || isAbortLikeError(error);
|
|
591
605
|
try {
|
|
592
|
-
await this.finishRun(
|
|
606
|
+
await this.finishRun(runId, state, cancelled ? "cancelled" : "failed", {
|
|
593
607
|
error: errorMessage(error),
|
|
594
608
|
});
|
|
595
609
|
} catch (finishError) {
|
|
596
|
-
// A fenced-out runner must not touch
|
|
610
|
+
// A fenced-out runner must not touch run state, including terminal
|
|
597
611
|
// projections. Propagate the claim loss instead of the node error.
|
|
598
612
|
if (isClaimLostError(finishError)) {
|
|
599
613
|
throw finishError;
|
|
@@ -661,10 +675,11 @@ export class WorkflowEngine {
|
|
|
661
675
|
private async executeGraph(
|
|
662
676
|
workflow: WorkflowDefinition,
|
|
663
677
|
state: WorkflowRunState,
|
|
664
|
-
|
|
678
|
+
runId: string,
|
|
665
679
|
startNodeId: string | null = workflow.startAt,
|
|
666
680
|
executedStepsBase = 0,
|
|
667
681
|
initialLastOutput?: unknown,
|
|
682
|
+
resumedAttempt?: ResumedNodeAttempt,
|
|
668
683
|
): Promise<void> {
|
|
669
684
|
const maxSteps = workflow.maxSteps ?? this.maxSteps;
|
|
670
685
|
const composition = compositionMetadata(workflow);
|
|
@@ -673,7 +688,7 @@ export class WorkflowEngine {
|
|
|
673
688
|
let lastOutput: unknown = initialLastOutput;
|
|
674
689
|
|
|
675
690
|
while (currentNodeId !== null) {
|
|
676
|
-
await this.holdWhilePaused(state,
|
|
691
|
+
await this.holdWhilePaused(state, runId);
|
|
677
692
|
const isTransition =
|
|
678
693
|
composition?.entries[currentNodeId] !== undefined ||
|
|
679
694
|
composition?.exits[currentNodeId] !== undefined;
|
|
@@ -692,7 +707,16 @@ export class WorkflowEngine {
|
|
|
692
707
|
throw new Error(`Workflow node is missing: ${currentNodeId}`);
|
|
693
708
|
}
|
|
694
709
|
|
|
695
|
-
const
|
|
710
|
+
const activeResume = resumedAttempt?.nodeId === currentNodeId ? resumedAttempt : undefined;
|
|
711
|
+
resumedAttempt = undefined;
|
|
712
|
+
const attempt = await this.executeNode(
|
|
713
|
+
workflow,
|
|
714
|
+
state,
|
|
715
|
+
runId,
|
|
716
|
+
currentNodeId,
|
|
717
|
+
node,
|
|
718
|
+
activeResume,
|
|
719
|
+
);
|
|
696
720
|
if (this.parked) {
|
|
697
721
|
// Do not record the aborted attempt: the projection keeps the node
|
|
698
722
|
// as in-flight, and resume reruns it with a fresh attempt.
|
|
@@ -701,7 +725,7 @@ export class WorkflowEngine {
|
|
|
701
725
|
this.recordAttempt(workflow, state, attempt);
|
|
702
726
|
// The terminal node event carries the output, receipt, and conversation
|
|
703
727
|
// linkage so the trace alone is sufficient to reconstruct the run.
|
|
704
|
-
await this.persist(
|
|
728
|
+
await this.persist(runId, state, {
|
|
705
729
|
scope: "node",
|
|
706
730
|
type: attempt.result.outcome === "ok" ? "node_finished" : "node_failed",
|
|
707
731
|
nodeId: attempt.result.nodeId,
|
|
@@ -712,6 +736,9 @@ export class WorkflowEngine {
|
|
|
712
736
|
...(attempt.result.outcome === "ok" ? { output: attempt.result.output ?? null } : {}),
|
|
713
737
|
...(attempt.result.error !== undefined ? { error: attempt.result.error } : {}),
|
|
714
738
|
...(attempt.execution?.action !== undefined ? { action: attempt.execution.action } : {}),
|
|
739
|
+
...(attempt.execution?.assistantMessage !== undefined
|
|
740
|
+
? { assistantMessage: attempt.execution.assistantMessage }
|
|
741
|
+
: {}),
|
|
715
742
|
...(attempt.execution?.conversation !== undefined
|
|
716
743
|
? { conversation: attempt.execution.conversation }
|
|
717
744
|
: {}),
|
|
@@ -726,7 +753,7 @@ export class WorkflowEngine {
|
|
|
726
753
|
const entered = composition?.entries[attempt.result.nodeId];
|
|
727
754
|
if (entered !== undefined) {
|
|
728
755
|
const value = attempt.result.output as { invocation?: number } | undefined;
|
|
729
|
-
await this.persist(
|
|
756
|
+
await this.persist(runId, state, {
|
|
730
757
|
scope: "run",
|
|
731
758
|
type: "include_entered",
|
|
732
759
|
payload: {
|
|
@@ -739,7 +766,7 @@ export class WorkflowEngine {
|
|
|
739
766
|
const exited = composition?.exits[attempt.result.nodeId];
|
|
740
767
|
if (exited !== undefined) {
|
|
741
768
|
const entrySteps = state.steps.filter((step) => step.nodeId === exited.mountPath);
|
|
742
|
-
await this.persist(
|
|
769
|
+
await this.persist(runId, state, {
|
|
743
770
|
scope: "run",
|
|
744
771
|
type: "include_exited",
|
|
745
772
|
payload: {
|
|
@@ -754,7 +781,7 @@ export class WorkflowEngine {
|
|
|
754
781
|
|
|
755
782
|
lastOutput = attempt.result.output;
|
|
756
783
|
if (node.nodeType === "checkpoint") {
|
|
757
|
-
await this.finishRun(
|
|
784
|
+
await this.finishRun(runId, state, "waiting", {
|
|
758
785
|
waitingOn: attempt.result.nodeId,
|
|
759
786
|
finalOutput: lastOutput,
|
|
760
787
|
});
|
|
@@ -768,14 +795,14 @@ export class WorkflowEngine {
|
|
|
768
795
|
);
|
|
769
796
|
}
|
|
770
797
|
|
|
771
|
-
await this.finishRun(
|
|
798
|
+
await this.finishRun(runId, state, "completed", { finalOutput: lastOutput });
|
|
772
799
|
}
|
|
773
800
|
|
|
774
801
|
/**
|
|
775
802
|
* Hold the run at the step boundary while a pause is in effect. Pausing
|
|
776
803
|
* never interrupts a node mid-flight; it only delays the next dispatch.
|
|
777
804
|
*/
|
|
778
|
-
private async holdWhilePaused(state: WorkflowRunState,
|
|
805
|
+
private async holdWhilePaused(state: WorkflowRunState, runId: string): Promise<void> {
|
|
779
806
|
if (this.parked) {
|
|
780
807
|
throw new RunParkedError();
|
|
781
808
|
}
|
|
@@ -786,7 +813,7 @@ export class WorkflowEngine {
|
|
|
786
813
|
return;
|
|
787
814
|
}
|
|
788
815
|
state.paused = true;
|
|
789
|
-
await this.persist(
|
|
816
|
+
await this.persist(runId, state, { scope: "run", type: "run_paused", payload: {} });
|
|
790
817
|
while (this.paused && !this.cancelled && !this.parked) {
|
|
791
818
|
await new Promise<void>((resolve) => {
|
|
792
819
|
this.wakePause = resolve;
|
|
@@ -800,7 +827,7 @@ export class WorkflowEngine {
|
|
|
800
827
|
if (this.cancelled) {
|
|
801
828
|
throw new CancelledError();
|
|
802
829
|
}
|
|
803
|
-
await this.persist(
|
|
830
|
+
await this.persist(runId, state, { scope: "run", type: "run_resumed", payload: {} });
|
|
804
831
|
}
|
|
805
832
|
|
|
806
833
|
private routeAfterFailure(
|
|
@@ -853,6 +880,9 @@ export class WorkflowEngine {
|
|
|
853
880
|
output: attempt.result.output ?? null,
|
|
854
881
|
...(attempt.result.error !== undefined ? { error: attempt.result.error } : {}),
|
|
855
882
|
...(attempt.execution?.action !== undefined ? { action: attempt.execution.action } : {}),
|
|
883
|
+
...(attempt.execution?.assistantMessage !== undefined
|
|
884
|
+
? { assistantMessage: attempt.execution.assistantMessage }
|
|
885
|
+
: {}),
|
|
856
886
|
...(attempt.execution?.conversation !== undefined
|
|
857
887
|
? { conversation: attempt.execution.conversation }
|
|
858
888
|
: {}),
|
|
@@ -867,32 +897,35 @@ export class WorkflowEngine {
|
|
|
867
897
|
private async executeNode(
|
|
868
898
|
workflow: WorkflowDefinition,
|
|
869
899
|
state: WorkflowRunState,
|
|
870
|
-
|
|
900
|
+
runId: string,
|
|
871
901
|
nodeId: string,
|
|
872
902
|
node: WorkflowNodeDefinition,
|
|
903
|
+
resumedAttempt?: ResumedNodeAttempt,
|
|
873
904
|
): Promise<NodeAttempt> {
|
|
874
|
-
const attemptId = randomUUID();
|
|
875
|
-
const startedAt = new Date().toISOString();
|
|
905
|
+
const attemptId = resumedAttempt?.attemptId ?? randomUUID();
|
|
906
|
+
const startedAt = resumedAttempt?.startedAt ?? new Date().toISOString();
|
|
876
907
|
state.currentNode = nodeId;
|
|
877
908
|
state.currentAttemptId = attemptId;
|
|
878
909
|
state.currentNodeStartedAt = startedAt;
|
|
879
910
|
if (node.statusDetail !== undefined) {
|
|
880
911
|
state.statusDetail = node.statusDetail;
|
|
881
912
|
}
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
913
|
+
if (resumedAttempt === undefined) {
|
|
914
|
+
await this.persist(runId, state, {
|
|
915
|
+
scope: "node",
|
|
916
|
+
type: "node_started",
|
|
917
|
+
nodeId,
|
|
918
|
+
attemptId,
|
|
919
|
+
payload: { nodeType: node.nodeType },
|
|
920
|
+
});
|
|
921
|
+
}
|
|
889
922
|
|
|
890
923
|
const meta: NodeExecutionMeta = { promptText: null };
|
|
891
924
|
try {
|
|
892
925
|
const execution = await this.runNodeWithTimeout(
|
|
893
926
|
workflow,
|
|
894
927
|
state,
|
|
895
|
-
|
|
928
|
+
runId,
|
|
896
929
|
nodeId,
|
|
897
930
|
attemptId,
|
|
898
931
|
node,
|
|
@@ -955,7 +988,7 @@ export class WorkflowEngine {
|
|
|
955
988
|
private async runNodeWithTimeout(
|
|
956
989
|
workflow: WorkflowDefinition,
|
|
957
990
|
state: WorkflowRunState,
|
|
958
|
-
|
|
991
|
+
runId: string,
|
|
959
992
|
nodeId: string,
|
|
960
993
|
attemptId: string,
|
|
961
994
|
node: WorkflowNodeDefinition,
|
|
@@ -985,11 +1018,11 @@ export class WorkflowEngine {
|
|
|
985
1018
|
abort.abort(new TimeoutError(timeoutMs));
|
|
986
1019
|
}, timeoutMs);
|
|
987
1020
|
}
|
|
988
|
-
this.activeAttempt = {
|
|
1021
|
+
this.activeAttempt = { runId, state, nodeId, attemptId, signal: abort.signal };
|
|
989
1022
|
const dispatched = this.dispatchNode(
|
|
990
1023
|
workflow,
|
|
991
1024
|
state,
|
|
992
|
-
|
|
1025
|
+
runId,
|
|
993
1026
|
nodeId,
|
|
994
1027
|
attemptId,
|
|
995
1028
|
node,
|
|
@@ -1006,7 +1039,7 @@ export class WorkflowEngine {
|
|
|
1006
1039
|
const execution = await Promise.race([dispatched, abortRejection(abort.signal)]);
|
|
1007
1040
|
if (execution.output === undefined) {
|
|
1008
1041
|
// JSON cannot represent undefined; normalize so the in-memory state
|
|
1009
|
-
// matches what
|
|
1042
|
+
// matches what persisted canonical JSON round-trips to.
|
|
1010
1043
|
execution.output = null;
|
|
1011
1044
|
}
|
|
1012
1045
|
assertJsonSerializable(execution.output, `Node ${nodeId} output`);
|
|
@@ -1070,7 +1103,7 @@ export class WorkflowEngine {
|
|
|
1070
1103
|
private async dispatchNode(
|
|
1071
1104
|
workflow: WorkflowDefinition,
|
|
1072
1105
|
state: WorkflowRunState,
|
|
1073
|
-
|
|
1106
|
+
runId: string,
|
|
1074
1107
|
nodeId: string,
|
|
1075
1108
|
attemptId: string,
|
|
1076
1109
|
node: WorkflowNodeDefinition,
|
|
@@ -1083,7 +1116,7 @@ export class WorkflowEngine {
|
|
|
1083
1116
|
return await this.runAgentNode(
|
|
1084
1117
|
workflow,
|
|
1085
1118
|
state,
|
|
1086
|
-
|
|
1119
|
+
runId,
|
|
1087
1120
|
nodeId,
|
|
1088
1121
|
attemptId,
|
|
1089
1122
|
node,
|
|
@@ -1141,7 +1174,7 @@ export class WorkflowEngine {
|
|
|
1141
1174
|
private async runAgentNode(
|
|
1142
1175
|
workflow: WorkflowDefinition,
|
|
1143
1176
|
state: WorkflowRunState,
|
|
1144
|
-
|
|
1177
|
+
runId: string,
|
|
1145
1178
|
nodeId: string,
|
|
1146
1179
|
attemptId: string,
|
|
1147
1180
|
node: AgentNodeDefinition,
|
|
@@ -1149,10 +1182,29 @@ export class WorkflowEngine {
|
|
|
1149
1182
|
signal: AbortSignal,
|
|
1150
1183
|
meta: NodeExecutionMeta,
|
|
1151
1184
|
): Promise<NodeExecution> {
|
|
1185
|
+
const assistant = assistantMessageConfig(node);
|
|
1186
|
+
if (assistant !== undefined) {
|
|
1187
|
+
if (this.executor.assistantMessageMode === "park") {
|
|
1188
|
+
state.statusDetail = "waiting for origin Pi session";
|
|
1189
|
+
await this.persist(runId, state, {
|
|
1190
|
+
scope: "agent",
|
|
1191
|
+
type: "agent_session_required",
|
|
1192
|
+
nodeId,
|
|
1193
|
+
attemptId,
|
|
1194
|
+
payload: { completion: "assistant" },
|
|
1195
|
+
});
|
|
1196
|
+
this.parked = true;
|
|
1197
|
+
throw new RunParkedError();
|
|
1198
|
+
}
|
|
1199
|
+
if (this.executor.assistantMessageMode !== "visible") {
|
|
1200
|
+
throw new Error("Assistant completion requires an origin Pi session");
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1152
1204
|
const basePrompt = await node.prompt(context);
|
|
1153
1205
|
if (signal.aborted) {
|
|
1154
1206
|
// The node timed out or the run was cancelled while the async prompt
|
|
1155
|
-
// builder ran; a late continuation must not write into a
|
|
1207
|
+
// builder ran; a late continuation must not write into a run that
|
|
1156
1208
|
// may already be terminal.
|
|
1157
1209
|
throw abortError(signal);
|
|
1158
1210
|
}
|
|
@@ -1164,12 +1216,12 @@ export class WorkflowEngine {
|
|
|
1164
1216
|
node.expectedOutput,
|
|
1165
1217
|
);
|
|
1166
1218
|
meta.promptText = prompt;
|
|
1167
|
-
await this.persist(
|
|
1219
|
+
await this.persist(runId, state, {
|
|
1168
1220
|
scope: "agent",
|
|
1169
1221
|
type: "agent_prompt_sent",
|
|
1170
1222
|
nodeId,
|
|
1171
1223
|
attemptId,
|
|
1172
|
-
payload: { prompt },
|
|
1224
|
+
payload: { prompt, completion: assistant === undefined ? "submit" : "assistant" },
|
|
1173
1225
|
});
|
|
1174
1226
|
|
|
1175
1227
|
const submission = await this.executor.runAgentStep(
|
|
@@ -1179,7 +1231,11 @@ export class WorkflowEngine {
|
|
|
1179
1231
|
workflowName: workflow.name,
|
|
1180
1232
|
nodeId,
|
|
1181
1233
|
attemptId,
|
|
1182
|
-
|
|
1234
|
+
completion: assistant === undefined ? "submit" : "assistant",
|
|
1235
|
+
...(typeof node.expectedOutput === "string"
|
|
1236
|
+
? { expectedOutput: node.expectedOutput }
|
|
1237
|
+
: {}),
|
|
1238
|
+
...(assistant?.maxChars !== undefined ? { maxOutputChars: assistant.maxChars } : {}),
|
|
1183
1239
|
},
|
|
1184
1240
|
prompt,
|
|
1185
1241
|
...(state.runTitle !== undefined || node.statusDetail !== undefined
|
|
@@ -1199,6 +1255,9 @@ export class WorkflowEngine {
|
|
|
1199
1255
|
return {
|
|
1200
1256
|
output: submission.output,
|
|
1201
1257
|
promptText: prompt,
|
|
1258
|
+
...(submission.assistantMessage !== undefined
|
|
1259
|
+
? { assistantMessage: submission.assistantMessage }
|
|
1260
|
+
: {}),
|
|
1202
1261
|
...(submission.conversation !== undefined ? { conversation: submission.conversation } : {}),
|
|
1203
1262
|
};
|
|
1204
1263
|
}
|
|
@@ -1208,6 +1267,13 @@ export class WorkflowEngine {
|
|
|
1208
1267
|
context: WorkflowNodeContext,
|
|
1209
1268
|
output: unknown,
|
|
1210
1269
|
): Promise<{ ok: true; value: unknown } | { ok: false; error: string }> {
|
|
1270
|
+
if (assistantMessageConfig(node) !== undefined) {
|
|
1271
|
+
return {
|
|
1272
|
+
ok: false,
|
|
1273
|
+
error:
|
|
1274
|
+
"This step completes with a normal assistant response. Do not submit workflow output.",
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1211
1277
|
try {
|
|
1212
1278
|
const normalized = normalizeAgentOutput(output);
|
|
1213
1279
|
const validated = node.validate ? await node.validate(normalized, context) : normalized;
|
|
@@ -1242,11 +1308,11 @@ export class WorkflowEngine {
|
|
|
1242
1308
|
}
|
|
1243
1309
|
|
|
1244
1310
|
private async persist(
|
|
1245
|
-
|
|
1311
|
+
runId: string,
|
|
1246
1312
|
state: WorkflowRunState,
|
|
1247
1313
|
event: WorkflowTraceEventDraft,
|
|
1248
1314
|
): Promise<void> {
|
|
1249
|
-
const traceEvent = await this.store.writeSnapshot(
|
|
1315
|
+
const traceEvent = await this.store.writeSnapshot(runId, state, event);
|
|
1250
1316
|
try {
|
|
1251
1317
|
this.onEvent?.(traceEvent, state);
|
|
1252
1318
|
} catch {
|
|
@@ -1256,7 +1322,7 @@ export class WorkflowEngine {
|
|
|
1256
1322
|
}
|
|
1257
1323
|
|
|
1258
1324
|
private async finishRun(
|
|
1259
|
-
|
|
1325
|
+
runId: string,
|
|
1260
1326
|
state: WorkflowRunState,
|
|
1261
1327
|
status: WorkflowRunState["status"],
|
|
1262
1328
|
fields: { error?: string; waitingOn?: string; finalOutput?: unknown },
|
|
@@ -1265,9 +1331,9 @@ export class WorkflowEngine {
|
|
|
1265
1331
|
status = "timed_out";
|
|
1266
1332
|
}
|
|
1267
1333
|
// Let observers (e.g. the session recorder) stop and drain before the
|
|
1268
|
-
// terminal event exists, so the
|
|
1334
|
+
// terminal event exists, so the terminal fact is immutable from that point on.
|
|
1269
1335
|
try {
|
|
1270
|
-
await this.onRunFinishing?.(
|
|
1336
|
+
await this.onRunFinishing?.(runId, state);
|
|
1271
1337
|
} catch {
|
|
1272
1338
|
// Finishing the run wins over observer failures.
|
|
1273
1339
|
}
|
|
@@ -1285,7 +1351,7 @@ export class WorkflowEngine {
|
|
|
1285
1351
|
delete state.currentNode;
|
|
1286
1352
|
delete state.currentAttemptId;
|
|
1287
1353
|
delete state.currentNodeStartedAt;
|
|
1288
|
-
await this.persist(
|
|
1354
|
+
await this.persist(runId, state, {
|
|
1289
1355
|
scope: "run",
|
|
1290
1356
|
type: `run_${status}`,
|
|
1291
1357
|
payload: {
|
|
@@ -1327,7 +1393,9 @@ async function runCheckpointNode(
|
|
|
1327
1393
|
prompt,
|
|
1328
1394
|
...(timeout !== undefined ? { timeout } : {}),
|
|
1329
1395
|
});
|
|
1330
|
-
await new HumanDecisionStore(execution.store.
|
|
1396
|
+
await new HumanDecisionStore(execution.store.databasePath, {
|
|
1397
|
+
state: execution.store.state,
|
|
1398
|
+
}).createRequest(request);
|
|
1331
1399
|
return { output: request, promptText: null };
|
|
1332
1400
|
}
|
|
1333
1401
|
const output = node.run ? await node.run(context) : { summary: node.summary ?? "checkpoint" };
|
|
@@ -1458,7 +1526,7 @@ function assertInvocationStepLimit(
|
|
|
1458
1526
|
}
|
|
1459
1527
|
|
|
1460
1528
|
/**
|
|
1461
|
-
* Outputs are persisted to the run
|
|
1529
|
+
* Outputs are persisted to the SQLite run state, so they must be JSON-serializable.
|
|
1462
1530
|
* Failing here turns a bad callback return value into a normal node failure
|
|
1463
1531
|
* instead of corrupting the run state.
|
|
1464
1532
|
*/
|
|
@@ -1527,6 +1595,18 @@ function normalizeAgentOutput(output: unknown): unknown {
|
|
|
1527
1595
|
}
|
|
1528
1596
|
}
|
|
1529
1597
|
|
|
1598
|
+
function assistantMessageOutput(
|
|
1599
|
+
expectedOutput: AgentExpectedOutput | undefined,
|
|
1600
|
+
): AssistantMessageOutput | undefined {
|
|
1601
|
+
return typeof expectedOutput === "object" && expectedOutput?.kind === "assistant-message"
|
|
1602
|
+
? expectedOutput
|
|
1603
|
+
: undefined;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
function assistantMessageConfig(node: AgentNodeDefinition): AssistantMessageOutput | undefined {
|
|
1607
|
+
return assistantMessageOutput(node.expectedOutput);
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1530
1610
|
/**
|
|
1531
1611
|
* The step contract appended to every agent-node prompt. This is the
|
|
1532
1612
|
* documented standard for how the model completes a workflow step.
|
|
@@ -1536,8 +1616,24 @@ export function appendStepContract(
|
|
|
1536
1616
|
workflowName: string,
|
|
1537
1617
|
nodeId: string,
|
|
1538
1618
|
attemptId: string,
|
|
1539
|
-
expectedOutput:
|
|
1619
|
+
expectedOutput: AgentExpectedOutput | undefined,
|
|
1540
1620
|
): string {
|
|
1621
|
+
const assistant = assistantMessageOutput(expectedOutput);
|
|
1622
|
+
if (assistant !== undefined) {
|
|
1623
|
+
return [
|
|
1624
|
+
prompt.trimEnd(),
|
|
1625
|
+
"",
|
|
1626
|
+
"---",
|
|
1627
|
+
`Workflow step contract (workflow: ${workflowName}, step: ${nodeId}, attempt: ${attemptId})`,
|
|
1628
|
+
"",
|
|
1629
|
+
"Reply with a normal assistant message.",
|
|
1630
|
+
"Do not call the workflow tool to complete this step.",
|
|
1631
|
+
"Your visible reply becomes the workflow step output after the turn settles.",
|
|
1632
|
+
...(assistant.maxChars !== undefined
|
|
1633
|
+
? [`Keep the visible reply within ${assistant.maxChars} characters.`]
|
|
1634
|
+
: []),
|
|
1635
|
+
].join("\n");
|
|
1636
|
+
}
|
|
1541
1637
|
return [
|
|
1542
1638
|
prompt.trimEnd(),
|
|
1543
1639
|
"",
|
|
@@ -1548,7 +1644,7 @@ export function appendStepContract(
|
|
|
1548
1644
|
`{"action": "update", "step": ${JSON.stringify(nodeId)}, "attempt": ${JSON.stringify(attemptId)}, "update": {"type": "...", "key": "...", "data": {...}}}`,
|
|
1549
1645
|
"Complete this step by calling the `workflow` tool exactly once with:",
|
|
1550
1646
|
`{"action": "submit", "step": ${JSON.stringify(nodeId)}, "attempt": ${JSON.stringify(attemptId)}, "output": <your result>}`,
|
|
1551
|
-
`Expected output: ${expectedOutput
|
|
1647
|
+
`Expected output: ${typeof expectedOutput === "string" ? expectedOutput : "a JSON object with your result"}`,
|
|
1552
1648
|
"The step is complete only after the workflow tool accepts the output.",
|
|
1553
1649
|
"If the tool reports a validation error, correct the output and call it again.",
|
|
1554
1650
|
].join("\n");
|