@osolmaz/pi-workflows 0.12.0 → 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 +56 -31
- package/dist/builtins/autodoc.workflow.d.ts +4 -4
- package/dist/builtins/autoimplement.workflow.d.ts +797 -142
- package/dist/builtins/autoimplement.workflow.js +17 -105
- 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/index.d.ts +4 -2
- package/dist/builtins/index.js +3 -1
- package/dist/builtins/index.js.map +1 -1
- package/dist/builtins/monitor.workflow.d.ts +56 -23
- package/dist/builtins/monitor.workflow.js +508 -329
- package/dist/builtins/monitor.workflow.js.map +1 -1
- package/dist/builtins/pi-agent-group.d.ts +72 -0
- package/dist/builtins/pi-agent-group.js +1087 -0
- package/dist/builtins/pi-agent-group.js.map +1 -0
- 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-approval.workflow.d.ts +39 -5
- package/dist/builtins/plan-approval.workflow.js +92 -14
- package/dist/builtins/plan-approval.workflow.js.map +1 -1
- package/dist/builtins/plan-change.workflow.d.ts +449 -0
- package/dist/builtins/plan-change.workflow.js +256 -0
- package/dist/builtins/plan-change.workflow.js.map +1 -0
- package/dist/builtins/plan-presentation.js +2 -2
- package/dist/builtins/plan-presentation.js.map +1 -1
- package/dist/builtins/sanity-check.workflow.d.ts +50 -6
- package/dist/builtins/sanity-check.workflow.js +150 -28
- 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 +4 -2
- package/dist/extension/decision-channels.js +333 -146
- 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 +209 -127
- 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/session-events.d.ts +2 -2
- 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 +29 -3
- 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 +15 -10
- 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 +9 -23
- 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/catalog.js +7 -2
- package/dist/workflows/catalog.js.map +1 -1
- package/dist/workflows/composition.js +20 -2
- package/dist/workflows/composition.js.map +1 -1
- package/dist/workflows/decision-presentation.d.ts +1 -1
- package/dist/workflows/decision-presentation.js +51 -38
- package/dist/workflows/decision-presentation.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 +7 -7
- package/dist/workflows/engine.js +196 -115
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/errors.d.ts +16 -3
- package/dist/workflows/errors.js +18 -3
- package/dist/workflows/errors.js.map +1 -1
- package/dist/workflows/human-decision.d.ts +45 -8
- package/dist/workflows/human-decision.js +641 -274
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +5 -6
- package/dist/workflows/index.js +4 -5
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/progress.d.ts +1 -0
- package/dist/workflows/progress.js +15 -3
- package/dist/workflows/progress.js.map +1 -1
- package/dist/workflows/schema.js +27 -1
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/store.d.ts +83 -103
- package/dist/workflows/store.js +907 -951
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/types.d.ts +88 -126
- 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 +36 -47
- package/docs/HUMAN_DECISION_PRESENTATIONS.md +14 -24
- package/docs/MONITOR.md +179 -237
- package/docs/SQLITE_STATE.md +219 -0
- package/docs/WORKFLOW_COMPOSITION.md +27 -26
- 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 +322 -0
- package/docs/plans/2026-08-21-sanity-check-plan.md +217 -95
- 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 +127 -53
- package/examples/workflows/approved-plan.workflow.ts +19 -46
- package/examples/workflows/plain-summary.workflow.ts +1 -0
- package/herdr-plugin.toml +1 -1
- package/package.json +7 -7
- package/plugins/herdr/viewer.mjs +1 -11
- package/schemas/human-decision-accepted-v1.schema.json +15 -3
- package/schemas/human-decision-continuation-v1.schema.json +10 -1
- package/schemas/human-decision-delivery-v1.schema.json +8 -0
- package/schemas/human-decision-receipt-v1.schema.json +8 -0
- package/schemas/human-decision-request-v1.schema.json +24 -4
- package/skills/autoimplement/SKILL.md +27 -0
- package/skills/autoplan/SKILL.md +5 -4
- package/skills/monitor/SKILL.md +91 -135
- package/skills/pi-workflows/SKILL.md +9 -8
- package/skills/sanity-check/SKILL.md +44 -0
- package/src/builtins/autoimplement.workflow.ts +19 -118
- package/src/builtins/autoplan.workflow.ts +290 -40
- package/src/builtins/catalog.ts +7 -5
- package/src/builtins/index.ts +18 -1
- package/src/builtins/monitor.workflow.ts +673 -382
- package/src/builtins/pi-agent-group.ts +1407 -0
- package/src/builtins/plain-summary.workflow.ts +185 -0
- package/src/builtins/plan-approval.workflow.ts +157 -24
- package/src/builtins/plan-change.workflow.ts +321 -0
- package/src/builtins/plan-presentation.ts +2 -2
- package/src/builtins/sanity-check.workflow.ts +248 -48
- 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 +456 -203
- package/src/extension/executor.ts +105 -2
- package/src/extension/herdr-viewer.ts +0 -5
- package/src/extension/index.ts +251 -151
- package/src/extension/recorder.ts +13 -22
- package/src/extension/session-events.ts +2 -2
- package/src/extension/step-message.ts +106 -4
- package/src/extension/widget.ts +32 -5
- package/src/host/rpc-executor.ts +4 -0
- package/src/host/runner.ts +93 -41
- package/src/render/graph-render.ts +12 -6
- 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 +19 -30
- 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/catalog.ts +7 -2
- package/src/workflows/composition.ts +21 -3
- package/src/workflows/decision-presentation.ts +56 -43
- package/src/workflows/definition.ts +36 -3
- package/src/workflows/engine.ts +228 -130
- package/src/workflows/errors.ts +27 -3
- package/src/workflows/human-decision.ts +921 -349
- package/src/workflows/index.ts +20 -30
- package/src/workflows/progress.ts +18 -3
- package/src/workflows/schema.ts +36 -1
- package/src/workflows/store.ts +1300 -1163
- package/src/workflows/types.ts +100 -137
- package/dist/builtins/sanity-check-session.d.ts +0 -17
- package/dist/builtins/sanity-check-session.js +0 -168
- package/dist/builtins/sanity-check-session.js.map +0 -1
- 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/schemas/human-decision-accepted-v2.schema.json +0 -50
- package/schemas/human-decision-delivery-v2.schema.json +0 -36
- package/schemas/human-decision-receipt-v2.schema.json +0 -39
- package/schemas/human-decision-request-v2.schema.json +0 -69
- package/schemas/human-decision-resolution-v2.schema.json +0 -27
- package/src/builtins/sanity-check-session.ts +0 -205
- 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(
|
|
@@ -377,7 +402,7 @@ export class WorkflowEngine {
|
|
|
377
402
|
workflowSource?: WorkflowSource;
|
|
378
403
|
runId?: string;
|
|
379
404
|
force?: boolean;
|
|
380
|
-
humanDecision?:
|
|
405
|
+
humanDecision?: ResolvedHumanDecision;
|
|
381
406
|
} = {},
|
|
382
407
|
): Promise<WorkflowRunResult> {
|
|
383
408
|
workflow = isCompiledWorkflow(workflow) ? workflow : compileWorkflowDefinition(workflow);
|
|
@@ -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
|
}
|
|
@@ -412,22 +437,21 @@ export class WorkflowEngine {
|
|
|
412
437
|
}
|
|
413
438
|
const request = parent.state.finalOutput as HumanDecisionRequest;
|
|
414
439
|
if (
|
|
415
|
-
|
|
416
|
-
request?.schema !== "pi-workflows.human-decision-request.v2") ||
|
|
440
|
+
request?.schema !== "pi-workflows.human-decision-request.v1" ||
|
|
417
441
|
request.decisionId !== options.humanDecision.decisionId ||
|
|
418
442
|
request.requestDigest !== options.humanDecision.requestDigest
|
|
419
443
|
) {
|
|
420
444
|
throw new Error("Accepted human decision does not match the waiting request");
|
|
421
445
|
}
|
|
422
|
-
const durableDecision = await new HumanDecisionStore(this.store.
|
|
423
|
-
|
|
424
|
-
);
|
|
446
|
+
const durableDecision = await new HumanDecisionStore(this.store.databasePath, {
|
|
447
|
+
state: this.store.state,
|
|
448
|
+
}).readResolved(request.decisionId);
|
|
425
449
|
if (durableDecision === null || !isDeepStrictEqual(durableDecision, options.humanDecision)) {
|
|
426
450
|
throw new Error("Accepted human decision does not match the durable decision record");
|
|
427
451
|
}
|
|
428
452
|
acceptedResponse = validateHumanDecisionResponse(request, durableDecision.response);
|
|
429
453
|
acceptedNodeId = request.nodeId;
|
|
430
|
-
normalizedInput =
|
|
454
|
+
normalizedInput = structuredClone(parent.state.input);
|
|
431
455
|
} else {
|
|
432
456
|
const suppliedInput = input === undefined ? null : input;
|
|
433
457
|
normalizedInput = workflow.input ? await workflow.input(suppliedInput) : suppliedInput;
|
|
@@ -444,39 +468,26 @@ export class WorkflowEngine {
|
|
|
444
468
|
options.runId,
|
|
445
469
|
);
|
|
446
470
|
state.parentRunId = parentRunId;
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
state.
|
|
450
|
-
parent.state.outputs,
|
|
451
|
-
parent.runDir,
|
|
452
|
-
)) as WorkflowRunState["outputs"];
|
|
453
|
-
state.results = (await resolveArtifacts(
|
|
454
|
-
parent.state.results,
|
|
455
|
-
parent.runDir,
|
|
456
|
-
)) as WorkflowRunState["results"];
|
|
457
|
-
state.steps = (await resolveArtifacts(
|
|
458
|
-
parent.state.steps,
|
|
459
|
-
parent.runDir,
|
|
460
|
-
)) as WorkflowRunState["steps"];
|
|
471
|
+
state.outputs = structuredClone(parent.state.outputs);
|
|
472
|
+
state.results = structuredClone(parent.state.results);
|
|
473
|
+
state.steps = structuredClone(parent.state.steps);
|
|
461
474
|
if (humanContract !== undefined && options.humanDecision !== undefined) {
|
|
462
475
|
const receipt = {
|
|
463
476
|
decisionId: options.humanDecision.decisionId,
|
|
464
477
|
requestDigest: options.humanDecision.requestDigest,
|
|
465
478
|
nodeId: acceptedNodeId ?? waitingNodeId,
|
|
466
479
|
response: options.humanDecision.response,
|
|
480
|
+
provenance: options.humanDecision.provenance,
|
|
467
481
|
acceptedAt: options.humanDecision.acceptedAt,
|
|
468
482
|
answerDigest: options.humanDecision.answerDigest,
|
|
469
483
|
};
|
|
470
|
-
state.humanDecision =
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
revision: options.humanDecision.revision,
|
|
478
|
-
}
|
|
479
|
-
: { schema: "pi-workflows.human-decision-receipt.v1", ...receipt };
|
|
484
|
+
state.humanDecision = {
|
|
485
|
+
schema: "pi-workflows.human-decision-receipt.v1",
|
|
486
|
+
...receipt,
|
|
487
|
+
subjectDigest: options.humanDecision.subjectDigest,
|
|
488
|
+
presentationDigest: options.humanDecision.presentationDigest,
|
|
489
|
+
revision: options.humanDecision.revision,
|
|
490
|
+
};
|
|
480
491
|
state.outputs[waitingNodeId] = acceptedResponse;
|
|
481
492
|
const priorResult = state.results[waitingNodeId];
|
|
482
493
|
if (priorResult === undefined) {
|
|
@@ -493,8 +504,8 @@ export class WorkflowEngine {
|
|
|
493
504
|
}
|
|
494
505
|
state.carriedStepCount = state.steps.length;
|
|
495
506
|
|
|
496
|
-
const
|
|
497
|
-
await this.persist(
|
|
507
|
+
const runId = await this.store.initializeRun(workflow, state);
|
|
508
|
+
await this.persist(runId, state, {
|
|
498
509
|
scope: "run",
|
|
499
510
|
type: "run_started",
|
|
500
511
|
payload: {
|
|
@@ -506,31 +517,31 @@ export class WorkflowEngine {
|
|
|
506
517
|
carriedSteps: state.steps.length,
|
|
507
518
|
},
|
|
508
519
|
});
|
|
509
|
-
await this.onRunStarted?.(
|
|
520
|
+
await this.onRunStarted?.(runId, state);
|
|
510
521
|
|
|
511
522
|
const point = this.resumePointFor(workflow, state, "continue");
|
|
512
523
|
if (point.nodeId === null) {
|
|
513
524
|
// The checkpoint was the final node; the answer completes the chain.
|
|
514
|
-
await this.finishRun(
|
|
515
|
-
return {
|
|
525
|
+
await this.finishRun(runId, state, "completed", { finalOutput: point.lastOutput });
|
|
526
|
+
return { runId, state };
|
|
516
527
|
}
|
|
517
528
|
try {
|
|
518
529
|
await this.executeGraph(
|
|
519
530
|
workflow,
|
|
520
531
|
state,
|
|
521
|
-
|
|
532
|
+
runId,
|
|
522
533
|
point.nodeId,
|
|
523
534
|
countExecutableSteps(workflow, state.steps),
|
|
524
535
|
point.lastOutput,
|
|
525
536
|
);
|
|
526
537
|
} catch (error) {
|
|
527
538
|
if (isRunParkedError(error) || this.parked) {
|
|
528
|
-
return {
|
|
539
|
+
return { runId, state };
|
|
529
540
|
}
|
|
530
|
-
await this.finishAfterError(
|
|
531
|
-
return {
|
|
541
|
+
await this.finishAfterError(runId, state, error);
|
|
542
|
+
return { runId, state };
|
|
532
543
|
}
|
|
533
|
-
return {
|
|
544
|
+
return { runId, state };
|
|
534
545
|
}
|
|
535
546
|
|
|
536
547
|
/**
|
|
@@ -586,17 +597,17 @@ export class WorkflowEngine {
|
|
|
586
597
|
}
|
|
587
598
|
|
|
588
599
|
private async finishAfterError(
|
|
589
|
-
|
|
600
|
+
runId: string,
|
|
590
601
|
state: WorkflowRunState,
|
|
591
602
|
error: unknown,
|
|
592
603
|
): Promise<void> {
|
|
593
604
|
const cancelled = this.cancelled || isAbortLikeError(error);
|
|
594
605
|
try {
|
|
595
|
-
await this.finishRun(
|
|
606
|
+
await this.finishRun(runId, state, cancelled ? "cancelled" : "failed", {
|
|
596
607
|
error: errorMessage(error),
|
|
597
608
|
});
|
|
598
609
|
} catch (finishError) {
|
|
599
|
-
// A fenced-out runner must not touch
|
|
610
|
+
// A fenced-out runner must not touch run state, including terminal
|
|
600
611
|
// projections. Propagate the claim loss instead of the node error.
|
|
601
612
|
if (isClaimLostError(finishError)) {
|
|
602
613
|
throw finishError;
|
|
@@ -664,10 +675,11 @@ export class WorkflowEngine {
|
|
|
664
675
|
private async executeGraph(
|
|
665
676
|
workflow: WorkflowDefinition,
|
|
666
677
|
state: WorkflowRunState,
|
|
667
|
-
|
|
678
|
+
runId: string,
|
|
668
679
|
startNodeId: string | null = workflow.startAt,
|
|
669
680
|
executedStepsBase = 0,
|
|
670
681
|
initialLastOutput?: unknown,
|
|
682
|
+
resumedAttempt?: ResumedNodeAttempt,
|
|
671
683
|
): Promise<void> {
|
|
672
684
|
const maxSteps = workflow.maxSteps ?? this.maxSteps;
|
|
673
685
|
const composition = compositionMetadata(workflow);
|
|
@@ -676,7 +688,7 @@ export class WorkflowEngine {
|
|
|
676
688
|
let lastOutput: unknown = initialLastOutput;
|
|
677
689
|
|
|
678
690
|
while (currentNodeId !== null) {
|
|
679
|
-
await this.holdWhilePaused(state,
|
|
691
|
+
await this.holdWhilePaused(state, runId);
|
|
680
692
|
const isTransition =
|
|
681
693
|
composition?.entries[currentNodeId] !== undefined ||
|
|
682
694
|
composition?.exits[currentNodeId] !== undefined;
|
|
@@ -695,7 +707,16 @@ export class WorkflowEngine {
|
|
|
695
707
|
throw new Error(`Workflow node is missing: ${currentNodeId}`);
|
|
696
708
|
}
|
|
697
709
|
|
|
698
|
-
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
|
+
);
|
|
699
720
|
if (this.parked) {
|
|
700
721
|
// Do not record the aborted attempt: the projection keeps the node
|
|
701
722
|
// as in-flight, and resume reruns it with a fresh attempt.
|
|
@@ -704,7 +725,7 @@ export class WorkflowEngine {
|
|
|
704
725
|
this.recordAttempt(workflow, state, attempt);
|
|
705
726
|
// The terminal node event carries the output, receipt, and conversation
|
|
706
727
|
// linkage so the trace alone is sufficient to reconstruct the run.
|
|
707
|
-
await this.persist(
|
|
728
|
+
await this.persist(runId, state, {
|
|
708
729
|
scope: "node",
|
|
709
730
|
type: attempt.result.outcome === "ok" ? "node_finished" : "node_failed",
|
|
710
731
|
nodeId: attempt.result.nodeId,
|
|
@@ -715,6 +736,9 @@ export class WorkflowEngine {
|
|
|
715
736
|
...(attempt.result.outcome === "ok" ? { output: attempt.result.output ?? null } : {}),
|
|
716
737
|
...(attempt.result.error !== undefined ? { error: attempt.result.error } : {}),
|
|
717
738
|
...(attempt.execution?.action !== undefined ? { action: attempt.execution.action } : {}),
|
|
739
|
+
...(attempt.execution?.assistantMessage !== undefined
|
|
740
|
+
? { assistantMessage: attempt.execution.assistantMessage }
|
|
741
|
+
: {}),
|
|
718
742
|
...(attempt.execution?.conversation !== undefined
|
|
719
743
|
? { conversation: attempt.execution.conversation }
|
|
720
744
|
: {}),
|
|
@@ -729,7 +753,7 @@ export class WorkflowEngine {
|
|
|
729
753
|
const entered = composition?.entries[attempt.result.nodeId];
|
|
730
754
|
if (entered !== undefined) {
|
|
731
755
|
const value = attempt.result.output as { invocation?: number } | undefined;
|
|
732
|
-
await this.persist(
|
|
756
|
+
await this.persist(runId, state, {
|
|
733
757
|
scope: "run",
|
|
734
758
|
type: "include_entered",
|
|
735
759
|
payload: {
|
|
@@ -742,7 +766,7 @@ export class WorkflowEngine {
|
|
|
742
766
|
const exited = composition?.exits[attempt.result.nodeId];
|
|
743
767
|
if (exited !== undefined) {
|
|
744
768
|
const entrySteps = state.steps.filter((step) => step.nodeId === exited.mountPath);
|
|
745
|
-
await this.persist(
|
|
769
|
+
await this.persist(runId, state, {
|
|
746
770
|
scope: "run",
|
|
747
771
|
type: "include_exited",
|
|
748
772
|
payload: {
|
|
@@ -757,7 +781,7 @@ export class WorkflowEngine {
|
|
|
757
781
|
|
|
758
782
|
lastOutput = attempt.result.output;
|
|
759
783
|
if (node.nodeType === "checkpoint") {
|
|
760
|
-
await this.finishRun(
|
|
784
|
+
await this.finishRun(runId, state, "waiting", {
|
|
761
785
|
waitingOn: attempt.result.nodeId,
|
|
762
786
|
finalOutput: lastOutput,
|
|
763
787
|
});
|
|
@@ -771,14 +795,14 @@ export class WorkflowEngine {
|
|
|
771
795
|
);
|
|
772
796
|
}
|
|
773
797
|
|
|
774
|
-
await this.finishRun(
|
|
798
|
+
await this.finishRun(runId, state, "completed", { finalOutput: lastOutput });
|
|
775
799
|
}
|
|
776
800
|
|
|
777
801
|
/**
|
|
778
802
|
* Hold the run at the step boundary while a pause is in effect. Pausing
|
|
779
803
|
* never interrupts a node mid-flight; it only delays the next dispatch.
|
|
780
804
|
*/
|
|
781
|
-
private async holdWhilePaused(state: WorkflowRunState,
|
|
805
|
+
private async holdWhilePaused(state: WorkflowRunState, runId: string): Promise<void> {
|
|
782
806
|
if (this.parked) {
|
|
783
807
|
throw new RunParkedError();
|
|
784
808
|
}
|
|
@@ -789,7 +813,7 @@ export class WorkflowEngine {
|
|
|
789
813
|
return;
|
|
790
814
|
}
|
|
791
815
|
state.paused = true;
|
|
792
|
-
await this.persist(
|
|
816
|
+
await this.persist(runId, state, { scope: "run", type: "run_paused", payload: {} });
|
|
793
817
|
while (this.paused && !this.cancelled && !this.parked) {
|
|
794
818
|
await new Promise<void>((resolve) => {
|
|
795
819
|
this.wakePause = resolve;
|
|
@@ -803,7 +827,7 @@ export class WorkflowEngine {
|
|
|
803
827
|
if (this.cancelled) {
|
|
804
828
|
throw new CancelledError();
|
|
805
829
|
}
|
|
806
|
-
await this.persist(
|
|
830
|
+
await this.persist(runId, state, { scope: "run", type: "run_resumed", payload: {} });
|
|
807
831
|
}
|
|
808
832
|
|
|
809
833
|
private routeAfterFailure(
|
|
@@ -856,6 +880,9 @@ export class WorkflowEngine {
|
|
|
856
880
|
output: attempt.result.output ?? null,
|
|
857
881
|
...(attempt.result.error !== undefined ? { error: attempt.result.error } : {}),
|
|
858
882
|
...(attempt.execution?.action !== undefined ? { action: attempt.execution.action } : {}),
|
|
883
|
+
...(attempt.execution?.assistantMessage !== undefined
|
|
884
|
+
? { assistantMessage: attempt.execution.assistantMessage }
|
|
885
|
+
: {}),
|
|
859
886
|
...(attempt.execution?.conversation !== undefined
|
|
860
887
|
? { conversation: attempt.execution.conversation }
|
|
861
888
|
: {}),
|
|
@@ -870,32 +897,35 @@ export class WorkflowEngine {
|
|
|
870
897
|
private async executeNode(
|
|
871
898
|
workflow: WorkflowDefinition,
|
|
872
899
|
state: WorkflowRunState,
|
|
873
|
-
|
|
900
|
+
runId: string,
|
|
874
901
|
nodeId: string,
|
|
875
902
|
node: WorkflowNodeDefinition,
|
|
903
|
+
resumedAttempt?: ResumedNodeAttempt,
|
|
876
904
|
): Promise<NodeAttempt> {
|
|
877
|
-
const attemptId = randomUUID();
|
|
878
|
-
const startedAt = new Date().toISOString();
|
|
905
|
+
const attemptId = resumedAttempt?.attemptId ?? randomUUID();
|
|
906
|
+
const startedAt = resumedAttempt?.startedAt ?? new Date().toISOString();
|
|
879
907
|
state.currentNode = nodeId;
|
|
880
908
|
state.currentAttemptId = attemptId;
|
|
881
909
|
state.currentNodeStartedAt = startedAt;
|
|
882
910
|
if (node.statusDetail !== undefined) {
|
|
883
911
|
state.statusDetail = node.statusDetail;
|
|
884
912
|
}
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
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
|
+
}
|
|
892
922
|
|
|
893
923
|
const meta: NodeExecutionMeta = { promptText: null };
|
|
894
924
|
try {
|
|
895
925
|
const execution = await this.runNodeWithTimeout(
|
|
896
926
|
workflow,
|
|
897
927
|
state,
|
|
898
|
-
|
|
928
|
+
runId,
|
|
899
929
|
nodeId,
|
|
900
930
|
attemptId,
|
|
901
931
|
node,
|
|
@@ -958,7 +988,7 @@ export class WorkflowEngine {
|
|
|
958
988
|
private async runNodeWithTimeout(
|
|
959
989
|
workflow: WorkflowDefinition,
|
|
960
990
|
state: WorkflowRunState,
|
|
961
|
-
|
|
991
|
+
runId: string,
|
|
962
992
|
nodeId: string,
|
|
963
993
|
attemptId: string,
|
|
964
994
|
node: WorkflowNodeDefinition,
|
|
@@ -988,11 +1018,11 @@ export class WorkflowEngine {
|
|
|
988
1018
|
abort.abort(new TimeoutError(timeoutMs));
|
|
989
1019
|
}, timeoutMs);
|
|
990
1020
|
}
|
|
991
|
-
this.activeAttempt = {
|
|
1021
|
+
this.activeAttempt = { runId, state, nodeId, attemptId, signal: abort.signal };
|
|
992
1022
|
const dispatched = this.dispatchNode(
|
|
993
1023
|
workflow,
|
|
994
1024
|
state,
|
|
995
|
-
|
|
1025
|
+
runId,
|
|
996
1026
|
nodeId,
|
|
997
1027
|
attemptId,
|
|
998
1028
|
node,
|
|
@@ -1009,7 +1039,7 @@ export class WorkflowEngine {
|
|
|
1009
1039
|
const execution = await Promise.race([dispatched, abortRejection(abort.signal)]);
|
|
1010
1040
|
if (execution.output === undefined) {
|
|
1011
1041
|
// JSON cannot represent undefined; normalize so the in-memory state
|
|
1012
|
-
// matches what
|
|
1042
|
+
// matches what persisted canonical JSON round-trips to.
|
|
1013
1043
|
execution.output = null;
|
|
1014
1044
|
}
|
|
1015
1045
|
assertJsonSerializable(execution.output, `Node ${nodeId} output`);
|
|
@@ -1073,7 +1103,7 @@ export class WorkflowEngine {
|
|
|
1073
1103
|
private async dispatchNode(
|
|
1074
1104
|
workflow: WorkflowDefinition,
|
|
1075
1105
|
state: WorkflowRunState,
|
|
1076
|
-
|
|
1106
|
+
runId: string,
|
|
1077
1107
|
nodeId: string,
|
|
1078
1108
|
attemptId: string,
|
|
1079
1109
|
node: WorkflowNodeDefinition,
|
|
@@ -1086,7 +1116,7 @@ export class WorkflowEngine {
|
|
|
1086
1116
|
return await this.runAgentNode(
|
|
1087
1117
|
workflow,
|
|
1088
1118
|
state,
|
|
1089
|
-
|
|
1119
|
+
runId,
|
|
1090
1120
|
nodeId,
|
|
1091
1121
|
attemptId,
|
|
1092
1122
|
node,
|
|
@@ -1144,7 +1174,7 @@ export class WorkflowEngine {
|
|
|
1144
1174
|
private async runAgentNode(
|
|
1145
1175
|
workflow: WorkflowDefinition,
|
|
1146
1176
|
state: WorkflowRunState,
|
|
1147
|
-
|
|
1177
|
+
runId: string,
|
|
1148
1178
|
nodeId: string,
|
|
1149
1179
|
attemptId: string,
|
|
1150
1180
|
node: AgentNodeDefinition,
|
|
@@ -1152,10 +1182,29 @@ export class WorkflowEngine {
|
|
|
1152
1182
|
signal: AbortSignal,
|
|
1153
1183
|
meta: NodeExecutionMeta,
|
|
1154
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
|
+
|
|
1155
1204
|
const basePrompt = await node.prompt(context);
|
|
1156
1205
|
if (signal.aborted) {
|
|
1157
1206
|
// The node timed out or the run was cancelled while the async prompt
|
|
1158
|
-
// builder ran; a late continuation must not write into a
|
|
1207
|
+
// builder ran; a late continuation must not write into a run that
|
|
1159
1208
|
// may already be terminal.
|
|
1160
1209
|
throw abortError(signal);
|
|
1161
1210
|
}
|
|
@@ -1167,12 +1216,12 @@ export class WorkflowEngine {
|
|
|
1167
1216
|
node.expectedOutput,
|
|
1168
1217
|
);
|
|
1169
1218
|
meta.promptText = prompt;
|
|
1170
|
-
await this.persist(
|
|
1219
|
+
await this.persist(runId, state, {
|
|
1171
1220
|
scope: "agent",
|
|
1172
1221
|
type: "agent_prompt_sent",
|
|
1173
1222
|
nodeId,
|
|
1174
1223
|
attemptId,
|
|
1175
|
-
payload: { prompt },
|
|
1224
|
+
payload: { prompt, completion: assistant === undefined ? "submit" : "assistant" },
|
|
1176
1225
|
});
|
|
1177
1226
|
|
|
1178
1227
|
const submission = await this.executor.runAgentStep(
|
|
@@ -1182,7 +1231,11 @@ export class WorkflowEngine {
|
|
|
1182
1231
|
workflowName: workflow.name,
|
|
1183
1232
|
nodeId,
|
|
1184
1233
|
attemptId,
|
|
1185
|
-
|
|
1234
|
+
completion: assistant === undefined ? "submit" : "assistant",
|
|
1235
|
+
...(typeof node.expectedOutput === "string"
|
|
1236
|
+
? { expectedOutput: node.expectedOutput }
|
|
1237
|
+
: {}),
|
|
1238
|
+
...(assistant?.maxChars !== undefined ? { maxOutputChars: assistant.maxChars } : {}),
|
|
1186
1239
|
},
|
|
1187
1240
|
prompt,
|
|
1188
1241
|
...(state.runTitle !== undefined || node.statusDetail !== undefined
|
|
@@ -1202,6 +1255,9 @@ export class WorkflowEngine {
|
|
|
1202
1255
|
return {
|
|
1203
1256
|
output: submission.output,
|
|
1204
1257
|
promptText: prompt,
|
|
1258
|
+
...(submission.assistantMessage !== undefined
|
|
1259
|
+
? { assistantMessage: submission.assistantMessage }
|
|
1260
|
+
: {}),
|
|
1205
1261
|
...(submission.conversation !== undefined ? { conversation: submission.conversation } : {}),
|
|
1206
1262
|
};
|
|
1207
1263
|
}
|
|
@@ -1211,6 +1267,13 @@ export class WorkflowEngine {
|
|
|
1211
1267
|
context: WorkflowNodeContext,
|
|
1212
1268
|
output: unknown,
|
|
1213
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
|
+
}
|
|
1214
1277
|
try {
|
|
1215
1278
|
const normalized = normalizeAgentOutput(output);
|
|
1216
1279
|
const validated = node.validate ? await node.validate(normalized, context) : normalized;
|
|
@@ -1245,11 +1308,11 @@ export class WorkflowEngine {
|
|
|
1245
1308
|
}
|
|
1246
1309
|
|
|
1247
1310
|
private async persist(
|
|
1248
|
-
|
|
1311
|
+
runId: string,
|
|
1249
1312
|
state: WorkflowRunState,
|
|
1250
1313
|
event: WorkflowTraceEventDraft,
|
|
1251
1314
|
): Promise<void> {
|
|
1252
|
-
const traceEvent = await this.store.writeSnapshot(
|
|
1315
|
+
const traceEvent = await this.store.writeSnapshot(runId, state, event);
|
|
1253
1316
|
try {
|
|
1254
1317
|
this.onEvent?.(traceEvent, state);
|
|
1255
1318
|
} catch {
|
|
@@ -1259,7 +1322,7 @@ export class WorkflowEngine {
|
|
|
1259
1322
|
}
|
|
1260
1323
|
|
|
1261
1324
|
private async finishRun(
|
|
1262
|
-
|
|
1325
|
+
runId: string,
|
|
1263
1326
|
state: WorkflowRunState,
|
|
1264
1327
|
status: WorkflowRunState["status"],
|
|
1265
1328
|
fields: { error?: string; waitingOn?: string; finalOutput?: unknown },
|
|
@@ -1268,9 +1331,9 @@ export class WorkflowEngine {
|
|
|
1268
1331
|
status = "timed_out";
|
|
1269
1332
|
}
|
|
1270
1333
|
// Let observers (e.g. the session recorder) stop and drain before the
|
|
1271
|
-
// terminal event exists, so the
|
|
1334
|
+
// terminal event exists, so the terminal fact is immutable from that point on.
|
|
1272
1335
|
try {
|
|
1273
|
-
await this.onRunFinishing?.(
|
|
1336
|
+
await this.onRunFinishing?.(runId, state);
|
|
1274
1337
|
} catch {
|
|
1275
1338
|
// Finishing the run wins over observer failures.
|
|
1276
1339
|
}
|
|
@@ -1288,7 +1351,7 @@ export class WorkflowEngine {
|
|
|
1288
1351
|
delete state.currentNode;
|
|
1289
1352
|
delete state.currentAttemptId;
|
|
1290
1353
|
delete state.currentNodeStartedAt;
|
|
1291
|
-
await this.persist(
|
|
1354
|
+
await this.persist(runId, state, {
|
|
1292
1355
|
scope: "run",
|
|
1293
1356
|
type: `run_${status}`,
|
|
1294
1357
|
payload: {
|
|
@@ -1317,6 +1380,10 @@ async function runCheckpointNode(
|
|
|
1317
1380
|
typeof node.humanDecision.audience === "function"
|
|
1318
1381
|
? await node.humanDecision.audience(context)
|
|
1319
1382
|
: node.humanDecision.audience;
|
|
1383
|
+
const timeout =
|
|
1384
|
+
typeof node.humanDecision.onTimeout === "function"
|
|
1385
|
+
? await node.humanDecision.onTimeout(context)
|
|
1386
|
+
: node.humanDecision.onTimeout;
|
|
1320
1387
|
const request = createHumanDecisionRequest({
|
|
1321
1388
|
runId: context.state.runId,
|
|
1322
1389
|
workflowName: execution.workflowName,
|
|
@@ -1324,8 +1391,11 @@ async function runCheckpointNode(
|
|
|
1324
1391
|
attemptId: execution.attemptId,
|
|
1325
1392
|
contract: { audience, choices: node.humanDecision.choices },
|
|
1326
1393
|
prompt,
|
|
1394
|
+
...(timeout !== undefined ? { timeout } : {}),
|
|
1327
1395
|
});
|
|
1328
|
-
await new HumanDecisionStore(execution.store.
|
|
1396
|
+
await new HumanDecisionStore(execution.store.databasePath, {
|
|
1397
|
+
state: execution.store.state,
|
|
1398
|
+
}).createRequest(request);
|
|
1329
1399
|
return { output: request, promptText: null };
|
|
1330
1400
|
}
|
|
1331
1401
|
const output = node.run ? await node.run(context) : { summary: node.summary ?? "checkpoint" };
|
|
@@ -1456,7 +1526,7 @@ function assertInvocationStepLimit(
|
|
|
1456
1526
|
}
|
|
1457
1527
|
|
|
1458
1528
|
/**
|
|
1459
|
-
* Outputs are persisted to the run
|
|
1529
|
+
* Outputs are persisted to the SQLite run state, so they must be JSON-serializable.
|
|
1460
1530
|
* Failing here turns a bad callback return value into a normal node failure
|
|
1461
1531
|
* instead of corrupting the run state.
|
|
1462
1532
|
*/
|
|
@@ -1525,6 +1595,18 @@ function normalizeAgentOutput(output: unknown): unknown {
|
|
|
1525
1595
|
}
|
|
1526
1596
|
}
|
|
1527
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
|
+
|
|
1528
1610
|
/**
|
|
1529
1611
|
* The step contract appended to every agent-node prompt. This is the
|
|
1530
1612
|
* documented standard for how the model completes a workflow step.
|
|
@@ -1534,8 +1616,24 @@ export function appendStepContract(
|
|
|
1534
1616
|
workflowName: string,
|
|
1535
1617
|
nodeId: string,
|
|
1536
1618
|
attemptId: string,
|
|
1537
|
-
expectedOutput:
|
|
1619
|
+
expectedOutput: AgentExpectedOutput | undefined,
|
|
1538
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
|
+
}
|
|
1539
1637
|
return [
|
|
1540
1638
|
prompt.trimEnd(),
|
|
1541
1639
|
"",
|
|
@@ -1546,7 +1644,7 @@ export function appendStepContract(
|
|
|
1546
1644
|
`{"action": "update", "step": ${JSON.stringify(nodeId)}, "attempt": ${JSON.stringify(attemptId)}, "update": {"type": "...", "key": "...", "data": {...}}}`,
|
|
1547
1645
|
"Complete this step by calling the `workflow` tool exactly once with:",
|
|
1548
1646
|
`{"action": "submit", "step": ${JSON.stringify(nodeId)}, "attempt": ${JSON.stringify(attemptId)}, "output": <your result>}`,
|
|
1549
|
-
`Expected output: ${expectedOutput
|
|
1647
|
+
`Expected output: ${typeof expectedOutput === "string" ? expectedOutput : "a JSON object with your result"}`,
|
|
1550
1648
|
"The step is complete only after the workflow tool accepts the output.",
|
|
1551
1649
|
"If the tool reports a validation error, correct the output and call it again.",
|
|
1552
1650
|
].join("\n");
|