@osolmaz/pi-workflows 0.16.5 → 0.16.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/builtins/autoimplement.workflow.d.ts +5 -1
- package/dist/builtins/autoimplement.workflow.js +29 -10
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/client/client.js +3 -1
- package/dist/client/client.js.map +1 -1
- package/dist/client/protocol.d.ts +1 -1
- package/dist/client/protocol.js +18 -1
- package/dist/client/protocol.js.map +1 -1
- package/dist/client/view.d.ts +7 -5
- package/dist/extension/index.d.ts +2 -1
- package/dist/extension/index.js +116 -166
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/recorder.d.ts +1 -1
- package/dist/extension/recorder.js +2 -2
- package/dist/extension/recorder.js.map +1 -1
- package/dist/extension/step-message.d.ts +1 -1
- package/dist/extension/step-message.js +4 -4
- package/dist/extension/step-message.js.map +1 -1
- package/dist/extension/widget.js +1 -1
- package/dist/extension/widget.js.map +1 -1
- package/dist/extension/workflow-message-coordinator.d.ts +12 -3
- package/dist/extension/workflow-message-coordinator.js +108 -94
- package/dist/extension/workflow-message-coordinator.js.map +1 -1
- package/dist/render/graph-render.js +1 -1
- package/dist/resource-managers/index.d.ts +1 -2
- package/dist/resource-managers/index.js +1 -2
- package/dist/resource-managers/index.js.map +1 -1
- package/dist/resource-managers/sqlite.d.ts +2 -295
- package/dist/resource-managers/sqlite.js +71 -1528
- package/dist/resource-managers/sqlite.js.map +1 -1
- package/dist/resource-managers/types.d.ts +2 -7
- package/dist/server/resource-runner-entry.js +2 -6
- package/dist/server/resource-runner-entry.js.map +1 -1
- package/dist/server/rpc-bridge.js +1 -1
- package/dist/server/rpc-bridge.js.map +1 -1
- package/dist/server/rpc-executor.js +2 -4
- package/dist/server/rpc-executor.js.map +1 -1
- package/dist/server/server-entry.js +3 -0
- package/dist/server/server-entry.js.map +1 -1
- package/dist/server/server.d.ts +9 -8
- package/dist/server/server.js +364 -541
- package/dist/server/server.js.map +1 -1
- package/dist/server/state.d.ts +16 -29
- package/dist/server/state.js +105 -319
- package/dist/server/state.js.map +1 -1
- package/dist/server/view.d.ts +6 -5
- package/dist/server/view.js +68 -51
- package/dist/server/view.js.map +1 -1
- package/dist/server/workflow-runner-entry.d.ts +1 -1
- package/dist/server/workflow-runner-entry.js +1 -49
- package/dist/server/workflow-runner-entry.js.map +1 -1
- package/dist/server/workflow-runner-protocol.d.ts +2 -7
- package/dist/server/workflow-runner-protocol.js +5 -10
- package/dist/server/workflow-runner-protocol.js.map +1 -1
- package/dist/server/workflow-runner-store.d.ts +5 -6
- package/dist/server/workflow-runner-store.js +7 -14
- package/dist/server/workflow-runner-store.js.map +1 -1
- package/dist/state/attempt-time.d.ts +21 -0
- package/dist/state/attempt-time.js +95 -0
- package/dist/state/attempt-time.js.map +1 -0
- package/dist/state/json.d.ts +2 -1
- package/dist/state/json.js.map +1 -1
- package/dist/state/project-store.d.ts +39 -0
- package/dist/state/project-store.js +154 -0
- package/dist/state/project-store.js.map +1 -0
- package/dist/state/prune.js +0 -7
- package/dist/state/prune.js.map +1 -1
- package/dist/state/schema.js +23 -23
- package/dist/state/workflow-messages.d.ts +1 -1
- package/dist/state/workflow-messages.js +3 -1
- package/dist/state/workflow-messages.js.map +1 -1
- package/dist/viewer/backup.js +5 -0
- package/dist/viewer/backup.js.map +1 -1
- package/dist/workflows/diagnostics.d.ts +8 -0
- package/dist/workflows/diagnostics.js +61 -0
- package/dist/workflows/diagnostics.js.map +1 -0
- package/dist/workflows/engine.d.ts +2 -16
- package/dist/workflows/engine.js +128 -252
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/errors.d.ts +1 -1
- package/dist/workflows/errors.js +2 -2
- package/dist/workflows/errors.js.map +1 -1
- package/dist/workflows/human-decision.d.ts +3 -7
- package/dist/workflows/human-decision.js +2 -56
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +2 -1
- package/dist/workflows/index.js +1 -0
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/queue.d.ts +267 -0
- package/dist/workflows/queue.js +1248 -0
- package/dist/workflows/queue.js.map +1 -0
- package/dist/workflows/requests.d.ts +47 -0
- package/dist/workflows/requests.js +158 -0
- package/dist/workflows/requests.js.map +1 -0
- package/dist/workflows/schema.js +2 -4
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/store.d.ts +11 -16
- package/dist/workflows/store.js +177 -174
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/tool-input.d.ts +6 -9
- package/dist/workflows/tool-input.js +15 -12
- package/dist/workflows/tool-input.js.map +1 -1
- package/dist/workflows/transitions.d.ts +38 -0
- package/dist/workflows/transitions.js +276 -0
- package/dist/workflows/transitions.js.map +1 -0
- package/dist/workflows/types.d.ts +9 -42
- package/dist/workflows/workflow-message-content.d.ts +7 -1
- package/dist/workflows/workflow-message-content.js +22 -4
- package/dist/workflows/workflow-message-content.js.map +1 -1
- package/docs/2026-08-25-workflow-follow-ups.md +5 -5
- package/docs/2026-09-02-unify-workflow-messages-plan.md +2 -0
- package/docs/2026-09-04-workflow-run-state-plan.md +2 -0
- package/docs/2026-09-06-durable-execution-plan.md +176 -0
- package/docs/2026-09-07-workflow-handoff-plan.md +84 -0
- package/docs/DEFERRED_TURNS.md +84 -105
- package/docs/HUMAN_DECISIONS.md +17 -19
- package/docs/HUMAN_DECISION_PRESENTATIONS.md +1 -1
- package/docs/SQLITE_STATE.md +68 -29
- package/docs/WORKFLOW_SERVER.md +21 -10
- package/docs/WORKFLOW_STEP_MESSAGES.md +83 -54
- package/docs/plans/2026-08-04-controller-runtime-plan.md +2 -0
- package/docs/plans/2026-08-21-deferred-turn-intents-plan.md +2 -0
- package/docs/plans/2026-08-27-workflow-terminal-restart-plan.md +2 -0
- package/docs/workflows.md +111 -45
- package/examples/workflows/autoresearch.workflow.ts +0 -2
- package/examples/workflows/branch.workflow.ts +0 -4
- package/examples/workflows/echo.workflow.ts +0 -2
- package/examples/workflows/live-settings.workflow.ts +0 -2
- package/examples/workflows/two-turn.workflow.ts +0 -2
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/protocol/fixtures/client-v1.json +1 -1
- package/skills/pi-workflows/SKILL.md +8 -7
- package/src/builtins/autoimplement.workflow.ts +31 -10
- package/src/client/client.ts +3 -1
- package/src/client/protocol.ts +21 -1
- package/src/client/view.ts +9 -5
- package/src/extension/index.ts +118 -190
- package/src/extension/recorder.ts +2 -2
- package/src/extension/step-message.ts +4 -4
- package/src/extension/widget.ts +1 -1
- package/src/extension/workflow-message-coordinator.ts +133 -95
- package/src/render/graph-render.ts +1 -1
- package/src/resource-managers/index.ts +1 -9
- package/src/resource-managers/sqlite.ts +330 -2655
- package/src/resource-managers/types.ts +2 -3
- package/src/server/resource-runner-entry.ts +2 -7
- package/src/server/rpc-bridge.ts +1 -1
- package/src/server/rpc-executor.ts +4 -9
- package/src/server/server-entry.ts +3 -0
- package/src/server/server.ts +416 -641
- package/src/server/state.ts +122 -466
- package/src/server/view.ts +73 -58
- package/src/server/workflow-runner-entry.ts +2 -55
- package/src/server/workflow-runner-protocol.ts +5 -15
- package/src/server/workflow-runner-store.ts +12 -27
- package/src/state/attempt-time.ts +122 -0
- package/src/state/json.ts +2 -1
- package/src/state/project-store.ts +218 -0
- package/src/state/prune.ts +0 -13
- package/src/state/schema.ts +23 -23
- package/src/state/workflow-messages.ts +4 -2
- package/src/viewer/backup.ts +7 -0
- package/src/workflows/diagnostics.ts +78 -0
- package/src/workflows/engine.ts +136 -299
- package/src/workflows/errors.ts +4 -2
- package/src/workflows/human-decision.ts +3 -81
- package/src/workflows/index.ts +8 -2
- package/src/workflows/queue.ts +2091 -0
- package/src/workflows/requests.ts +283 -0
- package/src/workflows/schema.ts +2 -6
- package/src/workflows/store.ts +226 -236
- package/src/workflows/tool-input.ts +20 -12
- package/src/workflows/transitions.ts +330 -0
- package/src/workflows/types.ts +9 -46
- package/src/workflows/workflow-message-content.ts +28 -4
- package/dist/resource-managers/runtime.d.ts +0 -59
- package/dist/resource-managers/runtime.js +0 -423
- package/dist/resource-managers/runtime.js.map +0 -1
- package/src/resource-managers/runtime.ts +0 -576
package/src/workflows/engine.ts
CHANGED
|
@@ -18,8 +18,9 @@ import {
|
|
|
18
18
|
WorkflowSourceChangedError,
|
|
19
19
|
} from "./errors.js";
|
|
20
20
|
import { resolveNext, resolveNextForOutcome, validateWorkflowDefinition } from "./graph.js";
|
|
21
|
-
import { createHumanDecisionRequest
|
|
21
|
+
import { createHumanDecisionRequest } from "./human-decision.js";
|
|
22
22
|
import { extractJsonValue } from "./json.js";
|
|
23
|
+
import { workflowRequestId } from "./requests.js";
|
|
23
24
|
import {
|
|
24
25
|
parseWorkflowSettingsValue,
|
|
25
26
|
resolveInitialWorkflowSettings,
|
|
@@ -33,11 +34,12 @@ import {
|
|
|
33
34
|
createRunId,
|
|
34
35
|
type WorkflowExecutionStore,
|
|
35
36
|
} from "./store.js";
|
|
37
|
+
import { executionTransition } from "./transitions.js";
|
|
36
38
|
import type {
|
|
37
|
-
ResolvedHumanDecision,
|
|
38
39
|
AgentExpectedOutput,
|
|
39
40
|
AgentNodeDefinition,
|
|
40
41
|
AgentStepExecutor,
|
|
42
|
+
AgentStepContract,
|
|
41
43
|
AssistantMessageOutput,
|
|
42
44
|
AssistantMessageReceipt,
|
|
43
45
|
ActionNodeDefinition,
|
|
@@ -54,7 +56,6 @@ import type {
|
|
|
54
56
|
WorkflowNodeOutcome,
|
|
55
57
|
WorkflowNodeResult,
|
|
56
58
|
WorkflowNotificationSink,
|
|
57
|
-
HumanDecisionRequest,
|
|
58
59
|
WorkflowRunResult,
|
|
59
60
|
WorkflowRunState,
|
|
60
61
|
WorkflowSource,
|
|
@@ -63,7 +64,12 @@ import type {
|
|
|
63
64
|
WorkflowUpdateInput,
|
|
64
65
|
WorkflowUpdateReceipt,
|
|
65
66
|
} from "./types.js";
|
|
66
|
-
import {
|
|
67
|
+
import {
|
|
68
|
+
UpdateRateLimiter,
|
|
69
|
+
updateProjection,
|
|
70
|
+
updateReceipt,
|
|
71
|
+
validateWorkflowUpdate,
|
|
72
|
+
} from "./updates.js";
|
|
67
73
|
|
|
68
74
|
const DEFAULT_NODE_TIMEOUT_MS = 15 * 60_000;
|
|
69
75
|
const DEFAULT_MAX_STEPS = 100;
|
|
@@ -101,7 +107,8 @@ type ResumedNodeAttempt = {
|
|
|
101
107
|
nodeId: string;
|
|
102
108
|
attemptId: string;
|
|
103
109
|
startedAt: string;
|
|
104
|
-
|
|
110
|
+
timeoutMs?: number | null;
|
|
111
|
+
elapsedMs?: number;
|
|
105
112
|
settings?: WorkflowSettingsScopeRecord;
|
|
106
113
|
};
|
|
107
114
|
|
|
@@ -119,7 +126,6 @@ export class WorkflowEngine {
|
|
|
119
126
|
private readonly maxSteps: number;
|
|
120
127
|
private readonly onEvent?: WorkflowEngineOptions["onEvent"];
|
|
121
128
|
private readonly onRunStarted?: WorkflowEngineOptions["onRunStarted"];
|
|
122
|
-
private readonly onRunFinishing?: WorkflowEngineOptions["onRunFinishing"];
|
|
123
129
|
private activeAbort: AbortController | null = null;
|
|
124
130
|
private activeAttempt:
|
|
125
131
|
| {
|
|
@@ -136,7 +142,6 @@ export class WorkflowEngine {
|
|
|
136
142
|
private cancelled = false;
|
|
137
143
|
private parked = false;
|
|
138
144
|
private paused = false;
|
|
139
|
-
private presentationRequired = false;
|
|
140
145
|
private wakePause: (() => void) | null = null;
|
|
141
146
|
|
|
142
147
|
constructor(options: WorkflowEngineOptions) {
|
|
@@ -147,7 +152,6 @@ export class WorkflowEngine {
|
|
|
147
152
|
this.maxSteps = options.maxSteps ?? DEFAULT_MAX_STEPS;
|
|
148
153
|
this.onEvent = options.onEvent;
|
|
149
154
|
this.onRunStarted = options.onRunStarted;
|
|
150
|
-
this.onRunFinishing = options.onRunFinishing;
|
|
151
155
|
}
|
|
152
156
|
|
|
153
157
|
get databasePath(): string {
|
|
@@ -190,12 +194,14 @@ export class WorkflowEngine {
|
|
|
190
194
|
limiter.take();
|
|
191
195
|
const { event, record } = await this.store.publishUpdate(
|
|
192
196
|
active.runId,
|
|
193
|
-
active.state,
|
|
194
197
|
active.nodeId,
|
|
195
198
|
active.attemptId,
|
|
196
199
|
update,
|
|
197
200
|
{ signal: active.signal },
|
|
198
201
|
);
|
|
202
|
+
active.state.updates = updateProjection(active.state.updates, record);
|
|
203
|
+
active.state.traceSeq = event.seq;
|
|
204
|
+
active.state.updatedAt = event.at;
|
|
199
205
|
try {
|
|
200
206
|
this.onEvent?.(event, active.state);
|
|
201
207
|
} catch {
|
|
@@ -257,7 +263,6 @@ export class WorkflowEngine {
|
|
|
257
263
|
): Promise<WorkflowRunResult> {
|
|
258
264
|
workflow = isCompiledWorkflow(workflow) ? workflow : compileWorkflowDefinition(workflow);
|
|
259
265
|
validateWorkflowDefinition(workflow);
|
|
260
|
-
this.presentationRequired = workflow.presentationPrompt !== undefined;
|
|
261
266
|
// Fail before any run row exists so bad input cannot leave partial state.
|
|
262
267
|
const suppliedInput = input === undefined ? null : input;
|
|
263
268
|
const normalizedInput = workflow.input ? await workflow.input(suppliedInput) : suppliedInput;
|
|
@@ -324,25 +329,25 @@ export class WorkflowEngine {
|
|
|
324
329
|
runId: string,
|
|
325
330
|
options: {
|
|
326
331
|
workflowSource?: WorkflowSource;
|
|
327
|
-
force?: boolean;
|
|
328
332
|
resumeInteractionAttemptId?: string;
|
|
329
333
|
} = {},
|
|
330
334
|
): Promise<WorkflowRunResult> {
|
|
331
335
|
workflow = isCompiledWorkflow(workflow) ? workflow : compileWorkflowDefinition(workflow);
|
|
332
336
|
validateWorkflowDefinition(workflow);
|
|
333
|
-
this.presentationRequired = workflow.presentationPrompt !== undefined;
|
|
334
337
|
// Reset before any await: a park or cancel landing during preparation
|
|
335
338
|
// must survive, or a server drain would hang while the run executes.
|
|
336
339
|
this.cancelled = false;
|
|
337
340
|
this.paused = false;
|
|
338
341
|
this.parked = false;
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
+
const stored = await this.store.readRunState(runId);
|
|
343
|
+
if (stored === null) throw new Error(`Cannot resume unreadable workflow run: ${runId}`);
|
|
344
|
+
const sourceMismatch = workflowIdentityMismatch(stored, workflow, options.workflowSource);
|
|
345
|
+
if (sourceMismatch) {
|
|
342
346
|
throw new WorkflowSourceChangedError(runId);
|
|
343
347
|
}
|
|
348
|
+
const state = await this.store.prepareRunResume(runId);
|
|
344
349
|
|
|
345
|
-
const point = this.resumePointFor(workflow, state
|
|
350
|
+
const point = this.resumePointFor(workflow, state);
|
|
346
351
|
const interruptedNode =
|
|
347
352
|
state.currentNode !== undefined ? workflow.nodes[state.currentNode] : undefined;
|
|
348
353
|
let resumedAttempt: ResumedNodeAttempt | undefined;
|
|
@@ -350,18 +355,19 @@ export class WorkflowEngine {
|
|
|
350
355
|
state.currentNode !== undefined &&
|
|
351
356
|
state.currentAttemptId !== undefined &&
|
|
352
357
|
state.currentNodeStartedAt !== undefined &&
|
|
353
|
-
interruptedNode?.nodeType === "
|
|
354
|
-
|
|
355
|
-
|
|
358
|
+
(interruptedNode?.nodeType === "checkpoint" ||
|
|
359
|
+
(interruptedNode?.nodeType === "agent" &&
|
|
360
|
+
(assistantMessageConfig(interruptedNode) !== undefined ||
|
|
361
|
+
options.resumeInteractionAttemptId === state.currentAttemptId)))
|
|
356
362
|
) {
|
|
357
363
|
const resumedSettings = await this.persistedSettingsBinding(state);
|
|
358
364
|
resumedAttempt = {
|
|
359
365
|
nodeId: state.currentNode,
|
|
360
366
|
attemptId: state.currentAttemptId,
|
|
361
367
|
startedAt: state.currentNodeStartedAt,
|
|
362
|
-
...(state.
|
|
368
|
+
...(state.currentNodeTimeoutMs === undefined
|
|
363
369
|
? {}
|
|
364
|
-
: {
|
|
370
|
+
: { timeoutMs: state.currentNodeTimeoutMs, elapsedMs: state.currentNodeElapsedMs ?? 0 }),
|
|
365
371
|
...(resumedSettings !== undefined ? { settings: resumedSettings } : {}),
|
|
366
372
|
};
|
|
367
373
|
}
|
|
@@ -374,7 +380,8 @@ export class WorkflowEngine {
|
|
|
374
380
|
delete state.currentNode;
|
|
375
381
|
delete state.currentAttemptId;
|
|
376
382
|
delete state.currentNodeStartedAt;
|
|
377
|
-
delete state.
|
|
383
|
+
delete state.currentNodeTimeoutMs;
|
|
384
|
+
delete state.currentNodeElapsedMs;
|
|
378
385
|
delete state.currentSettingsScopeId;
|
|
379
386
|
delete state.currentSettingsChangeNumber;
|
|
380
387
|
delete state.currentSettingsHash;
|
|
@@ -387,21 +394,13 @@ export class WorkflowEngine {
|
|
|
387
394
|
...(point.nodeId !== null ? { resumeAt: point.nodeId } : {}),
|
|
388
395
|
...(resumedAttempt !== undefined ? { resumedAttemptId: resumedAttempt.attemptId } : {}),
|
|
389
396
|
replayedSteps: state.steps.length,
|
|
390
|
-
...(sourceMismatch ? { workflowSourceMismatch: true, forced: true } : {}),
|
|
391
397
|
},
|
|
392
398
|
});
|
|
393
399
|
await this.onRunStarted?.(runId, state);
|
|
394
400
|
|
|
395
401
|
if (point.nodeId === null) {
|
|
396
|
-
// The last
|
|
397
|
-
|
|
398
|
-
// checkpoint restores its waiting gate rather than completing.
|
|
399
|
-
if (point.waitingOn !== undefined) {
|
|
400
|
-
await this.finishRun(runId, state, "waiting", {
|
|
401
|
-
waitingOn: point.waitingOn,
|
|
402
|
-
finalOutput: point.lastOutput,
|
|
403
|
-
});
|
|
404
|
-
} else if (point.failedResult === undefined) {
|
|
402
|
+
// The last attempt finished before the terminal event was committed.
|
|
403
|
+
if (point.failedResult === undefined) {
|
|
405
404
|
await this.finishRun(runId, state, "completed", { finalOutput: point.lastOutput });
|
|
406
405
|
} else {
|
|
407
406
|
const timedOut = point.failedResult.outcome === "timed_out";
|
|
@@ -433,161 +432,6 @@ export class WorkflowEngine {
|
|
|
433
432
|
return { runId, state };
|
|
434
433
|
}
|
|
435
434
|
|
|
436
|
-
/**
|
|
437
|
-
* Start a continuation run from a checkpointed parent. The new run gets a
|
|
438
|
-
* fresh run and event stream, carries forward the parent's outputs, results,
|
|
439
|
-
* and step accounting, and continues routing after the checkpoint.
|
|
440
|
-
*/
|
|
441
|
-
async continueRun(
|
|
442
|
-
workflow: WorkflowDefinition,
|
|
443
|
-
parentRunId: string,
|
|
444
|
-
input: unknown,
|
|
445
|
-
options: {
|
|
446
|
-
workflowSource?: WorkflowSource;
|
|
447
|
-
runId?: string;
|
|
448
|
-
force?: boolean;
|
|
449
|
-
humanDecision?: ResolvedHumanDecision;
|
|
450
|
-
} = {},
|
|
451
|
-
): Promise<WorkflowRunResult> {
|
|
452
|
-
workflow = isCompiledWorkflow(workflow) ? workflow : compileWorkflowDefinition(workflow);
|
|
453
|
-
validateWorkflowDefinition(workflow);
|
|
454
|
-
this.presentationRequired = workflow.presentationPrompt !== undefined;
|
|
455
|
-
this.cancelled = false;
|
|
456
|
-
this.paused = false;
|
|
457
|
-
this.parked = false;
|
|
458
|
-
const parentState = await this.store.readRunState(parentRunId);
|
|
459
|
-
if (parentState === null) {
|
|
460
|
-
throw new Error(`Cannot continue from unreadable workflow run: ${parentRunId}`);
|
|
461
|
-
}
|
|
462
|
-
if (parentState.status !== "waiting" || parentState.waitingOn === undefined) {
|
|
463
|
-
throw new Error(
|
|
464
|
-
`Cannot continue workflow run ${parentRunId} with status ${parentState.status}`,
|
|
465
|
-
);
|
|
466
|
-
}
|
|
467
|
-
const sourceMismatch = workflowIdentityMismatch(parentState, workflow, options.workflowSource);
|
|
468
|
-
if (sourceMismatch && options.force !== true) {
|
|
469
|
-
throw new WorkflowSourceChangedError(parentRunId);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
const waitingNodeId = parentState.waitingOn;
|
|
473
|
-
const waitingNode = workflow.nodes[waitingNodeId];
|
|
474
|
-
const humanContract =
|
|
475
|
-
waitingNode?.nodeType === "checkpoint" ? waitingNode.humanDecision : undefined;
|
|
476
|
-
let acceptedResponse: unknown;
|
|
477
|
-
let acceptedNodeId: string | undefined;
|
|
478
|
-
let normalizedInput: unknown;
|
|
479
|
-
if (humanContract !== undefined) {
|
|
480
|
-
if (options.humanDecision === undefined) {
|
|
481
|
-
throw new Error(`Checkpoint ${waitingNodeId} requires an accepted verified human decision`);
|
|
482
|
-
}
|
|
483
|
-
const request = parentState.finalOutput as HumanDecisionRequest;
|
|
484
|
-
if (
|
|
485
|
-
request?.schema !== "pi-workflows.human-decision-request.v1" ||
|
|
486
|
-
request.decisionId !== options.humanDecision.decisionId ||
|
|
487
|
-
request.requestDigest !== options.humanDecision.requestDigest
|
|
488
|
-
) {
|
|
489
|
-
throw new Error("Accepted human decision does not match the waiting request");
|
|
490
|
-
}
|
|
491
|
-
const durableDecision = await this.store.readResolvedHumanDecision(request.decisionId);
|
|
492
|
-
if (durableDecision === null || !isDeepStrictEqual(durableDecision, options.humanDecision)) {
|
|
493
|
-
throw new Error("Accepted human decision does not match the durable decision record");
|
|
494
|
-
}
|
|
495
|
-
acceptedResponse = validateHumanDecisionResponse(request, durableDecision.response);
|
|
496
|
-
acceptedNodeId = request.nodeId;
|
|
497
|
-
normalizedInput = structuredClone(parentState.input);
|
|
498
|
-
} else {
|
|
499
|
-
const suppliedInput = input === undefined ? null : input;
|
|
500
|
-
normalizedInput = workflow.input ? await workflow.input(suppliedInput) : suppliedInput;
|
|
501
|
-
}
|
|
502
|
-
assertJsonSerializable(normalizedInput, "Workflow run input");
|
|
503
|
-
if (options.runId !== undefined && !/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(options.runId)) {
|
|
504
|
-
throw new Error(`Invalid workflow run id: ${JSON.stringify(options.runId)}`);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
const state = await this.createRunState(
|
|
508
|
-
workflow,
|
|
509
|
-
normalizedInput,
|
|
510
|
-
options.workflowSource,
|
|
511
|
-
options.runId,
|
|
512
|
-
);
|
|
513
|
-
state.parentRunId = parentRunId;
|
|
514
|
-
state.outputs = structuredClone(parentState.outputs);
|
|
515
|
-
state.results = structuredClone(parentState.results);
|
|
516
|
-
state.steps = structuredClone(parentState.steps);
|
|
517
|
-
if (humanContract !== undefined && options.humanDecision !== undefined) {
|
|
518
|
-
const receipt = {
|
|
519
|
-
decisionId: options.humanDecision.decisionId,
|
|
520
|
-
requestDigest: options.humanDecision.requestDigest,
|
|
521
|
-
nodeId: acceptedNodeId ?? waitingNodeId,
|
|
522
|
-
response: options.humanDecision.response,
|
|
523
|
-
provenance: options.humanDecision.provenance,
|
|
524
|
-
acceptedAt: options.humanDecision.acceptedAt,
|
|
525
|
-
answerDigest: options.humanDecision.answerDigest,
|
|
526
|
-
};
|
|
527
|
-
state.humanDecision = {
|
|
528
|
-
schema: "pi-workflows.human-decision-receipt.v1",
|
|
529
|
-
...receipt,
|
|
530
|
-
subjectDigest: options.humanDecision.subjectDigest,
|
|
531
|
-
presentationDigest: options.humanDecision.presentationDigest,
|
|
532
|
-
revision: options.humanDecision.revision,
|
|
533
|
-
};
|
|
534
|
-
state.outputs[waitingNodeId] = acceptedResponse;
|
|
535
|
-
const priorResult = state.results[waitingNodeId];
|
|
536
|
-
if (priorResult === undefined) {
|
|
537
|
-
throw new Error(`Waiting human decision result is missing for ${waitingNodeId}`);
|
|
538
|
-
}
|
|
539
|
-
state.results[waitingNodeId] = { ...priorResult, output: acceptedResponse };
|
|
540
|
-
const stepIndex = state.steps.findLastIndex((step) => step.nodeId === waitingNodeId);
|
|
541
|
-
if (stepIndex < 0)
|
|
542
|
-
throw new Error(`Waiting human decision step is missing for ${waitingNodeId}`);
|
|
543
|
-
const priorStep = state.steps[stepIndex];
|
|
544
|
-
if (priorStep === undefined)
|
|
545
|
-
throw new Error("Waiting human decision step became unavailable");
|
|
546
|
-
state.steps[stepIndex] = { ...priorStep, output: acceptedResponse };
|
|
547
|
-
}
|
|
548
|
-
state.carriedStepCount = state.steps.length;
|
|
549
|
-
|
|
550
|
-
const runId = await this.store.initializeRun(workflow, state);
|
|
551
|
-
await this.persist(runId, state, {
|
|
552
|
-
scope: "run",
|
|
553
|
-
type: "run_started",
|
|
554
|
-
payload: {
|
|
555
|
-
workflowName: workflow.name,
|
|
556
|
-
...(state.runTitle ? { runTitle: state.runTitle } : {}),
|
|
557
|
-
input: state.input,
|
|
558
|
-
continuedFrom: parentRunId,
|
|
559
|
-
checkpoint: parentState.waitingOn,
|
|
560
|
-
carriedSteps: state.steps.length,
|
|
561
|
-
},
|
|
562
|
-
});
|
|
563
|
-
await this.onRunStarted?.(runId, state);
|
|
564
|
-
|
|
565
|
-
const point = this.resumePointFor(workflow, state, "continue");
|
|
566
|
-
if (point.nodeId === null) {
|
|
567
|
-
// The checkpoint was the final node; the answer completes the chain.
|
|
568
|
-
await this.finishRun(runId, state, "completed", { finalOutput: point.lastOutput });
|
|
569
|
-
return { runId, state };
|
|
570
|
-
}
|
|
571
|
-
try {
|
|
572
|
-
await this.executeGraph(
|
|
573
|
-
workflow,
|
|
574
|
-
state,
|
|
575
|
-
runId,
|
|
576
|
-
point.nodeId,
|
|
577
|
-
countExecutableSteps(workflow, state.steps),
|
|
578
|
-
point.lastOutput,
|
|
579
|
-
);
|
|
580
|
-
} catch (error) {
|
|
581
|
-
if (isClaimLostError(error)) throw error;
|
|
582
|
-
if (isRunParkedError(error) || this.parked) {
|
|
583
|
-
return { runId, state };
|
|
584
|
-
}
|
|
585
|
-
await this.finishAfterError(runId, state, error);
|
|
586
|
-
return { runId, state };
|
|
587
|
-
}
|
|
588
|
-
return { runId, state };
|
|
589
|
-
}
|
|
590
|
-
|
|
591
435
|
/**
|
|
592
436
|
* Find where a resumed run continues. An in-flight node reruns; otherwise
|
|
593
437
|
* routing continues from the last recorded step. A null nodeId means the
|
|
@@ -596,12 +440,10 @@ export class WorkflowEngine {
|
|
|
596
440
|
private resumePointFor(
|
|
597
441
|
workflow: WorkflowDefinition,
|
|
598
442
|
state: WorkflowRunState,
|
|
599
|
-
checkpointBehavior: "wait" | "continue",
|
|
600
443
|
): {
|
|
601
444
|
nodeId: string | null;
|
|
602
445
|
lastOutput?: unknown;
|
|
603
446
|
failedResult?: WorkflowNodeResult;
|
|
604
|
-
waitingOn?: string;
|
|
605
447
|
} {
|
|
606
448
|
if (state.currentNode !== undefined) {
|
|
607
449
|
if (workflow.nodes[state.currentNode] === undefined) {
|
|
@@ -618,19 +460,6 @@ export class WorkflowEngine {
|
|
|
618
460
|
return { nodeId: lastStep.nodeId };
|
|
619
461
|
}
|
|
620
462
|
if (result.outcome === "ok") {
|
|
621
|
-
// A recorded checkpoint means the run should be waiting; a crash
|
|
622
|
-
// before the run_waiting persist restores the gate instead of
|
|
623
|
-
// routing past it. The gate applies to this run's own checkpoint
|
|
624
|
-
// only: a continuation's carried steps end with the parent's
|
|
625
|
-
// already-answered checkpoint, and routing must continue from it.
|
|
626
|
-
const isCarriedStep = state.steps.length <= (state.carriedStepCount ?? 0);
|
|
627
|
-
if (
|
|
628
|
-
checkpointBehavior === "wait" &&
|
|
629
|
-
!isCarriedStep &&
|
|
630
|
-
workflow.nodes[lastStep.nodeId]?.nodeType === "checkpoint"
|
|
631
|
-
) {
|
|
632
|
-
return { nodeId: null, waitingOn: lastStep.nodeId, lastOutput: result.output };
|
|
633
|
-
}
|
|
634
463
|
const next = resolveNext(workflow.edges, lastStep.nodeId, result.output, result);
|
|
635
464
|
return next === null
|
|
636
465
|
? { nodeId: null, lastOutput: result.output }
|
|
@@ -702,9 +531,7 @@ export class WorkflowEngine {
|
|
|
702
531
|
...(await this.resolveTitleBounded(workflow, input)),
|
|
703
532
|
...(workflowSource !== undefined ? { workflowSource } : {}),
|
|
704
533
|
...(composition?.sources.length ? { workflowSources: composition.sources } : {}),
|
|
705
|
-
|
|
706
|
-
? { definitionDigest: definitionDigest(workflow) }
|
|
707
|
-
: {}),
|
|
534
|
+
definitionDigest: definitionDigest(workflow),
|
|
708
535
|
startedAt: now,
|
|
709
536
|
updatedAt: now,
|
|
710
537
|
status: "running",
|
|
@@ -833,6 +660,7 @@ export class WorkflowEngine {
|
|
|
833
660
|
scopeId: latest.scopeId,
|
|
834
661
|
previousChangeNumber: attempt.settings.changeNumber,
|
|
835
662
|
changeNumber: latest.changeNumber,
|
|
663
|
+
settingsHash: latest.settingsHash,
|
|
836
664
|
},
|
|
837
665
|
});
|
|
838
666
|
resumedAttempt = {
|
|
@@ -881,13 +709,6 @@ export class WorkflowEngine {
|
|
|
881
709
|
}
|
|
882
710
|
|
|
883
711
|
lastOutput = attempt.result.output;
|
|
884
|
-
if (node.nodeType === "checkpoint") {
|
|
885
|
-
await this.finishRun(runId, state, "waiting", {
|
|
886
|
-
waitingOn: attempt.result.nodeId,
|
|
887
|
-
finalOutput: lastOutput,
|
|
888
|
-
});
|
|
889
|
-
return;
|
|
890
|
-
}
|
|
891
712
|
currentNodeId = resolveNext(
|
|
892
713
|
workflow.edges,
|
|
893
714
|
attempt.result.nodeId,
|
|
@@ -999,7 +820,8 @@ export class WorkflowEngine {
|
|
|
999
820
|
delete state.currentNode;
|
|
1000
821
|
delete state.currentAttemptId;
|
|
1001
822
|
delete state.currentNodeStartedAt;
|
|
1002
|
-
delete state.
|
|
823
|
+
delete state.currentNodeTimeoutMs;
|
|
824
|
+
delete state.currentNodeElapsedMs;
|
|
1003
825
|
delete state.currentSettingsScopeId;
|
|
1004
826
|
delete state.currentSettingsChangeNumber;
|
|
1005
827
|
delete state.currentSettingsHash;
|
|
@@ -1063,7 +885,8 @@ export class WorkflowEngine {
|
|
|
1063
885
|
node,
|
|
1064
886
|
meta,
|
|
1065
887
|
settings,
|
|
1066
|
-
resumedAttempt?.
|
|
888
|
+
resumedAttempt?.timeoutMs,
|
|
889
|
+
resumedAttempt?.elapsedMs,
|
|
1067
890
|
);
|
|
1068
891
|
return {
|
|
1069
892
|
result: this.createNodeResult(nodeId, node, attemptId, startedAt, "ok", execution.output),
|
|
@@ -1131,7 +954,8 @@ export class WorkflowEngine {
|
|
|
1131
954
|
node: WorkflowNodeDefinition,
|
|
1132
955
|
meta: NodeExecutionMeta,
|
|
1133
956
|
settings?: WorkflowSettingsScopeRecord,
|
|
1134
|
-
|
|
957
|
+
persistedTimeoutMs?: number | null,
|
|
958
|
+
elapsedMs = 0,
|
|
1135
959
|
): Promise<NodeExecution> {
|
|
1136
960
|
const abort = new AbortController();
|
|
1137
961
|
const context = this.createNodeContext(state, abort.signal, settings);
|
|
@@ -1150,39 +974,40 @@ export class WorkflowEngine {
|
|
|
1150
974
|
|
|
1151
975
|
let timeoutMs: number | null;
|
|
1152
976
|
let timeoutErrorMs: number | null;
|
|
1153
|
-
if (
|
|
977
|
+
if (persistedTimeoutMs === undefined) {
|
|
1154
978
|
timeoutMs = await this.resolveNodeTimeout(node, context, abort);
|
|
1155
979
|
timeoutErrorMs = timeoutMs;
|
|
1156
|
-
if (node.nodeType === "agent" && this.executor.
|
|
1157
|
-
state.
|
|
1158
|
-
|
|
980
|
+
if (node.nodeType === "agent" && this.executor.preservesActiveTimeBudget === true) {
|
|
981
|
+
state.currentNodeTimeoutMs = timeoutMs;
|
|
982
|
+
state.currentNodeElapsedMs = 0;
|
|
1159
983
|
await this.persist(runId, state, {
|
|
1160
984
|
scope: "node",
|
|
1161
|
-
type: "
|
|
985
|
+
type: "node_timeout_set",
|
|
1162
986
|
nodeId,
|
|
1163
987
|
attemptId,
|
|
1164
|
-
payload: {
|
|
988
|
+
payload: { timeoutMs },
|
|
1165
989
|
});
|
|
1166
|
-
if (state.currentNodeDeadlineAt !== null && timeoutErrorMs !== null) {
|
|
1167
|
-
timeoutMs = Date.parse(state.currentNodeDeadlineAt) - Date.now();
|
|
1168
|
-
if (timeoutMs <= 0) throw new TimeoutError(timeoutErrorMs);
|
|
1169
|
-
}
|
|
1170
990
|
} else {
|
|
1171
|
-
delete state.
|
|
991
|
+
delete state.currentNodeTimeoutMs;
|
|
992
|
+
delete state.currentNodeElapsedMs;
|
|
1172
993
|
}
|
|
1173
|
-
} else if (
|
|
1174
|
-
state.
|
|
994
|
+
} else if (persistedTimeoutMs === null) {
|
|
995
|
+
state.currentNodeTimeoutMs = null;
|
|
1175
996
|
timeoutMs = null;
|
|
1176
997
|
timeoutErrorMs = null;
|
|
1177
998
|
} else {
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
999
|
+
if (
|
|
1000
|
+
!Number.isFinite(persistedTimeoutMs) ||
|
|
1001
|
+
persistedTimeoutMs <= 0 ||
|
|
1002
|
+
!Number.isFinite(elapsedMs) ||
|
|
1003
|
+
elapsedMs < 0
|
|
1004
|
+
) {
|
|
1005
|
+
throw new Error("Persisted node timeout is invalid");
|
|
1182
1006
|
}
|
|
1183
|
-
state.
|
|
1184
|
-
|
|
1185
|
-
|
|
1007
|
+
state.currentNodeTimeoutMs = persistedTimeoutMs;
|
|
1008
|
+
state.currentNodeElapsedMs = elapsedMs;
|
|
1009
|
+
timeoutMs = persistedTimeoutMs - elapsedMs;
|
|
1010
|
+
timeoutErrorMs = persistedTimeoutMs;
|
|
1186
1011
|
if (timeoutMs <= 0) throw new TimeoutError(timeoutErrorMs);
|
|
1187
1012
|
}
|
|
1188
1013
|
if (abort.signal.aborted) {
|
|
@@ -1333,13 +1158,40 @@ export class WorkflowEngine {
|
|
|
1333
1158
|
}
|
|
1334
1159
|
case "action":
|
|
1335
1160
|
return await this.runActionNode(node, context, nodeId, attemptId, signal, meta);
|
|
1336
|
-
case "checkpoint":
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1161
|
+
case "checkpoint": {
|
|
1162
|
+
const saved = await this.store.readCheckpoint(state.runId, attemptId);
|
|
1163
|
+
if (saved !== undefined) {
|
|
1164
|
+
if (saved.request.status === "cancelled")
|
|
1165
|
+
throw new Error("Checkpoint request is cancelled");
|
|
1166
|
+
if (saved.request.status === "settled") {
|
|
1167
|
+
if (saved.humanDecision !== undefined) state.humanDecision = saved.humanDecision;
|
|
1168
|
+
return { output: saved.output, promptText: null };
|
|
1169
|
+
}
|
|
1170
|
+
} else {
|
|
1171
|
+
const request = await checkpointRequest(node, context, {
|
|
1172
|
+
workflowName: workflow.name,
|
|
1173
|
+
nodeId,
|
|
1174
|
+
attemptId,
|
|
1175
|
+
});
|
|
1176
|
+
const event = await this.store.commitTransition(state.runId, {
|
|
1177
|
+
kind: "waitForInput",
|
|
1178
|
+
...request,
|
|
1179
|
+
event: {
|
|
1180
|
+
scope: "node",
|
|
1181
|
+
type: "checkpoint_requested",
|
|
1182
|
+
nodeId,
|
|
1183
|
+
attemptId,
|
|
1184
|
+
payload: { requestId: request.requestId, requestKind: request.requestKind },
|
|
1185
|
+
},
|
|
1186
|
+
});
|
|
1187
|
+
state.traceSeq = event.seq;
|
|
1188
|
+
state.updatedAt = event.at;
|
|
1189
|
+
}
|
|
1190
|
+
state.status = "waiting";
|
|
1191
|
+
state.waitingOn = nodeId;
|
|
1192
|
+
delete state.finishedAt;
|
|
1193
|
+
throw new RunParkedError();
|
|
1194
|
+
}
|
|
1343
1195
|
}
|
|
1344
1196
|
}
|
|
1345
1197
|
|
|
@@ -1414,11 +1266,12 @@ export class WorkflowEngine {
|
|
|
1414
1266
|
signal: AbortSignal,
|
|
1415
1267
|
settings?: WorkflowSettingsScopeRecord,
|
|
1416
1268
|
): WorkflowNodeContext {
|
|
1269
|
+
const snapshot = deepFreezeJson(structuredClone(state));
|
|
1417
1270
|
return {
|
|
1418
|
-
input:
|
|
1419
|
-
outputs:
|
|
1420
|
-
results:
|
|
1421
|
-
state,
|
|
1271
|
+
input: snapshot.input,
|
|
1272
|
+
outputs: snapshot.outputs,
|
|
1273
|
+
results: snapshot.results,
|
|
1274
|
+
state: snapshot,
|
|
1422
1275
|
...(settings !== undefined
|
|
1423
1276
|
? {
|
|
1424
1277
|
settings: deepFreezeJson(structuredClone(settings.settings)),
|
|
@@ -1468,13 +1321,17 @@ export class WorkflowEngine {
|
|
|
1468
1321
|
// may already be terminal.
|
|
1469
1322
|
throw abortError(signal);
|
|
1470
1323
|
}
|
|
1471
|
-
const
|
|
1472
|
-
|
|
1473
|
-
|
|
1324
|
+
const contract: AgentStepContract = {
|
|
1325
|
+
requestId: workflowRequestId(state.runId, attemptId),
|
|
1326
|
+
runId: state.runId,
|
|
1327
|
+
workflowName: workflow.name,
|
|
1474
1328
|
nodeId,
|
|
1475
1329
|
attemptId,
|
|
1476
|
-
|
|
1477
|
-
|
|
1330
|
+
completion: assistant === undefined ? "submit" : "assistant",
|
|
1331
|
+
...(typeof node.expectedOutput === "string" ? { expectedOutput: node.expectedOutput } : {}),
|
|
1332
|
+
...(assistant?.maxChars !== undefined ? { maxOutputChars: assistant.maxChars } : {}),
|
|
1333
|
+
};
|
|
1334
|
+
const prompt = appendStepContract(basePrompt, contract);
|
|
1478
1335
|
meta.promptText = prompt;
|
|
1479
1336
|
await this.persist(runId, state, {
|
|
1480
1337
|
scope: "agent",
|
|
@@ -1486,17 +1343,7 @@ export class WorkflowEngine {
|
|
|
1486
1343
|
|
|
1487
1344
|
const submission = await this.executor.runAgentStep(
|
|
1488
1345
|
{
|
|
1489
|
-
contract
|
|
1490
|
-
runId: state.runId,
|
|
1491
|
-
workflowName: workflow.name,
|
|
1492
|
-
nodeId,
|
|
1493
|
-
attemptId,
|
|
1494
|
-
completion: assistant === undefined ? "submit" : "assistant",
|
|
1495
|
-
...(typeof node.expectedOutput === "string"
|
|
1496
|
-
? { expectedOutput: node.expectedOutput }
|
|
1497
|
-
: {}),
|
|
1498
|
-
...(assistant?.maxChars !== undefined ? { maxOutputChars: assistant.maxChars } : {}),
|
|
1499
|
-
},
|
|
1346
|
+
contract,
|
|
1500
1347
|
prompt,
|
|
1501
1348
|
...(state.runTitle !== undefined || node.statusDetail !== undefined
|
|
1502
1349
|
? {
|
|
@@ -1632,7 +1479,9 @@ export class WorkflowEngine {
|
|
|
1632
1479
|
state: WorkflowRunState,
|
|
1633
1480
|
event: WorkflowTraceEventDraft,
|
|
1634
1481
|
): Promise<void> {
|
|
1635
|
-
const traceEvent = await this.store.
|
|
1482
|
+
const traceEvent = await this.store.commitTransition(runId, executionTransition(state, event));
|
|
1483
|
+
state.traceSeq = traceEvent.seq;
|
|
1484
|
+
state.updatedAt = traceEvent.at;
|
|
1636
1485
|
try {
|
|
1637
1486
|
this.onEvent?.(traceEvent, state);
|
|
1638
1487
|
} catch {
|
|
@@ -1645,8 +1494,8 @@ export class WorkflowEngine {
|
|
|
1645
1494
|
private async finishRun(
|
|
1646
1495
|
runId: string,
|
|
1647
1496
|
state: WorkflowRunState,
|
|
1648
|
-
status:
|
|
1649
|
-
fields: { error?: string;
|
|
1497
|
+
status: "completed" | "failed" | "timed_out" | "cancelled",
|
|
1498
|
+
fields: { error?: string; finalOutput?: unknown },
|
|
1650
1499
|
): Promise<void> {
|
|
1651
1500
|
if (status === "failed" && state.status === "timed_out") {
|
|
1652
1501
|
status = "timed_out";
|
|
@@ -1656,50 +1505,38 @@ export class WorkflowEngine {
|
|
|
1656
1505
|
if (fields.error !== undefined) {
|
|
1657
1506
|
state.error = fields.error;
|
|
1658
1507
|
}
|
|
1659
|
-
if (fields.waitingOn !== undefined) {
|
|
1660
|
-
state.waitingOn = fields.waitingOn;
|
|
1661
|
-
}
|
|
1662
1508
|
if (fields.finalOutput !== undefined) {
|
|
1663
1509
|
state.finalOutput = fields.finalOutput;
|
|
1664
1510
|
}
|
|
1665
1511
|
delete state.currentNode;
|
|
1666
1512
|
delete state.currentAttemptId;
|
|
1667
1513
|
delete state.currentNodeStartedAt;
|
|
1668
|
-
delete state.
|
|
1514
|
+
delete state.currentNodeTimeoutMs;
|
|
1515
|
+
delete state.currentNodeElapsedMs;
|
|
1669
1516
|
delete state.currentSettingsScopeId;
|
|
1670
1517
|
delete state.currentSettingsChangeNumber;
|
|
1671
1518
|
delete state.currentSettingsHash;
|
|
1672
|
-
// Let observers stop, drain, and prepare terminal delivery from the final
|
|
1673
|
-
// in-memory state before the immutable terminal event is committed.
|
|
1674
|
-
try {
|
|
1675
|
-
await this.onRunFinishing?.(runId, state);
|
|
1676
|
-
} catch {
|
|
1677
|
-
// Finishing the run wins over observer failures.
|
|
1678
|
-
}
|
|
1679
1519
|
await this.persist(runId, state, {
|
|
1680
1520
|
scope: "run",
|
|
1681
1521
|
type: `run_${status}`,
|
|
1682
1522
|
payload: {
|
|
1683
1523
|
status,
|
|
1684
1524
|
...(fields.error !== undefined ? { error: fields.error } : {}),
|
|
1685
|
-
...(fields.waitingOn !== undefined ? { waitingOn: fields.waitingOn } : {}),
|
|
1686
1525
|
...(fields.finalOutput !== undefined ? { finalOutput: fields.finalOutput } : {}),
|
|
1687
|
-
presentationRequired: status === "completed" && this.presentationRequired,
|
|
1688
1526
|
},
|
|
1689
1527
|
});
|
|
1690
1528
|
}
|
|
1691
1529
|
}
|
|
1692
1530
|
|
|
1693
|
-
async function
|
|
1531
|
+
async function checkpointRequest(
|
|
1694
1532
|
node: CheckpointNodeDefinition,
|
|
1695
1533
|
context: WorkflowNodeContext,
|
|
1696
1534
|
execution: {
|
|
1697
|
-
store: WorkflowExecutionStore;
|
|
1698
1535
|
workflowName: string;
|
|
1699
1536
|
nodeId: string;
|
|
1700
1537
|
attemptId: string;
|
|
1701
1538
|
},
|
|
1702
|
-
): Promise<
|
|
1539
|
+
): Promise<{ requestId: string; requestKind: "checkpoint" | "decision"; contract: JsonValue }> {
|
|
1703
1540
|
if (node.humanDecision !== undefined) {
|
|
1704
1541
|
const prompt = await node.humanDecision.request(context);
|
|
1705
1542
|
const audience =
|
|
@@ -1719,11 +1556,18 @@ async function runCheckpointNode(
|
|
|
1719
1556
|
prompt,
|
|
1720
1557
|
...(timeout !== undefined ? { timeout } : {}),
|
|
1721
1558
|
});
|
|
1722
|
-
|
|
1723
|
-
|
|
1559
|
+
return {
|
|
1560
|
+
requestId: request.decisionId,
|
|
1561
|
+
requestKind: "decision",
|
|
1562
|
+
contract: request as unknown as JsonValue,
|
|
1563
|
+
};
|
|
1724
1564
|
}
|
|
1725
1565
|
const output = node.run ? await node.run(context) : { summary: node.summary ?? "checkpoint" };
|
|
1726
|
-
return {
|
|
1566
|
+
return {
|
|
1567
|
+
requestId: workflowRequestId(context.state.runId, execution.attemptId),
|
|
1568
|
+
requestKind: "checkpoint",
|
|
1569
|
+
contract: output as JsonValue,
|
|
1570
|
+
};
|
|
1727
1571
|
}
|
|
1728
1572
|
|
|
1729
1573
|
function shellReceipt(result: ShellActionResult): WorkflowActionReceipt {
|
|
@@ -1930,8 +1774,7 @@ export function workflowIdentityMismatch(
|
|
|
1930
1774
|
const metadata = compositionMetadata(workflow);
|
|
1931
1775
|
const currentSources = metadata?.sources ?? [];
|
|
1932
1776
|
if (!isDeepStrictEqual(state.workflowSources ?? [], currentSources)) return true;
|
|
1933
|
-
|
|
1934
|
-
return state.definitionDigest !== currentDigest;
|
|
1777
|
+
return state.definitionDigest !== definitionDigest(workflow);
|
|
1935
1778
|
}
|
|
1936
1779
|
|
|
1937
1780
|
function definitionDigest(workflow: WorkflowDefinition): string {
|
|
@@ -2040,15 +1883,9 @@ function settingsDefinitionForNode(
|
|
|
2040
1883
|
* The step contract appended to every agent-node prompt. This is the
|
|
2041
1884
|
* documented standard for how the model completes a workflow step.
|
|
2042
1885
|
*/
|
|
2043
|
-
export function appendStepContract(
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
nodeId: string,
|
|
2047
|
-
attemptId: string,
|
|
2048
|
-
expectedOutput: AgentExpectedOutput | undefined,
|
|
2049
|
-
): string {
|
|
2050
|
-
const assistant = assistantMessageOutput(expectedOutput);
|
|
2051
|
-
if (assistant !== undefined) {
|
|
1886
|
+
export function appendStepContract(prompt: string, contract: AgentStepContract): string {
|
|
1887
|
+
const { requestId, workflowName, nodeId, attemptId, expectedOutput } = contract;
|
|
1888
|
+
if (contract.completion === "assistant") {
|
|
2052
1889
|
return [
|
|
2053
1890
|
prompt.trimEnd(),
|
|
2054
1891
|
"",
|
|
@@ -2058,8 +1895,8 @@ export function appendStepContract(
|
|
|
2058
1895
|
"Reply with a normal assistant message.",
|
|
2059
1896
|
"Do not call the workflow tool to complete this step.",
|
|
2060
1897
|
"Your visible reply becomes the workflow step output after the turn settles.",
|
|
2061
|
-
...(
|
|
2062
|
-
? [`Keep the visible reply within ${
|
|
1898
|
+
...(contract.maxOutputChars !== undefined
|
|
1899
|
+
? [`Keep the visible reply within ${contract.maxOutputChars} characters.`]
|
|
2063
1900
|
: []),
|
|
2064
1901
|
].join("\n");
|
|
2065
1902
|
}
|
|
@@ -2070,9 +1907,9 @@ export function appendStepContract(
|
|
|
2070
1907
|
`Workflow step contract (workflow: ${workflowName}, step: ${nodeId}, attempt: ${attemptId})`,
|
|
2071
1908
|
"",
|
|
2072
1909
|
"While this step is active, you may publish non-completing updates with:",
|
|
2073
|
-
`{"action": "update", "
|
|
1910
|
+
`{"action": "update", "requestId": ${JSON.stringify(requestId)}, "update": {"type": "...", "key": "...", "data": {...}}}`,
|
|
2074
1911
|
"Complete this step by calling the `workflow` tool exactly once with:",
|
|
2075
|
-
`{"action": "submit", "
|
|
1912
|
+
`{"action": "submit", "requestId": ${JSON.stringify(requestId)}, "output": <your result>}`,
|
|
2076
1913
|
`Expected output: ${typeof expectedOutput === "string" ? expectedOutput : "a JSON object with your result"}`,
|
|
2077
1914
|
"The step is complete only after the workflow tool accepts the output.",
|
|
2078
1915
|
"If the tool reports a validation error, correct the output and call it again.",
|