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