@osolmaz/pi-workflows 0.13.2 → 0.13.3
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 -3
- package/dist/builtins/autodoc.workflow.d.ts +5 -5
- package/dist/builtins/autoimplement.workflow.d.ts +65 -61
- package/dist/builtins/autoimplement.workflow.js +47 -8
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/builtins/autoplan.workflow.d.ts +5 -5
- package/dist/builtins/catalog.js +1 -1
- package/dist/builtins/change-verification.workflow.d.ts +1 -1
- package/dist/builtins/plain-summary.workflow.d.ts +1 -1
- package/dist/builtins/plan-approval.workflow.d.ts +1 -1
- package/dist/builtins/plan-change.workflow.d.ts +23 -23
- package/dist/builtins/sanity-check.workflow.d.ts +3 -3
- package/dist/builtins/workspace-preparation.workflow.d.ts +1 -1
- package/dist/controllers/index.d.ts +2 -2
- package/dist/controllers/index.js.map +1 -1
- package/dist/controllers/sqlite.js +85 -23
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/controllers/types.d.ts +34 -0
- package/dist/controllers/workflow-engine-scheduler.d.ts +5 -1
- package/dist/controllers/workflow-engine-scheduler.js +83 -1
- package/dist/controllers/workflow-engine-scheduler.js.map +1 -1
- package/dist/controllers/workflows.d.ts +8 -1
- package/dist/controllers/workflows.js +36 -0
- package/dist/controllers/workflows.js.map +1 -1
- package/dist/extension/controller-host.d.ts +2 -1
- package/dist/extension/controller-host.js +1 -1
- package/dist/extension/controller-host.js.map +1 -1
- package/dist/extension/follow-up-coordinator.d.ts +27 -0
- package/dist/extension/follow-up-coordinator.js +131 -0
- package/dist/extension/follow-up-coordinator.js.map +1 -0
- package/dist/extension/index.d.ts +9 -0
- package/dist/extension/index.js +460 -75
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/session-events.d.ts +2 -3
- package/dist/extension/session-events.js +11 -10
- package/dist/extension/session-events.js.map +1 -1
- package/dist/extension/widget.js +9 -0
- package/dist/extension/widget.js.map +1 -1
- package/dist/state/database.d.ts +2 -1
- package/dist/state/database.js +11 -13
- package/dist/state/database.js.map +1 -1
- package/dist/state/json.js +6 -1
- package/dist/state/json.js.map +1 -1
- package/dist/state/mutation.d.ts +1 -1
- package/dist/state/mutation.js.map +1 -1
- package/dist/state/prune.d.ts +18 -0
- package/dist/state/prune.js +373 -0
- package/dist/state/prune.js.map +1 -0
- package/dist/state/schema.d.ts +1 -1
- package/dist/state/schema.js +116 -18
- package/dist/state/schema.js.map +1 -1
- package/dist/viewer/cli.d.ts +3 -1
- package/dist/viewer/cli.js +55 -4
- package/dist/viewer/cli.js.map +1 -1
- package/dist/viewer/render.js +19 -1
- package/dist/viewer/render.js.map +1 -1
- package/dist/workflows/composition.d.ts +2 -0
- package/dist/workflows/composition.js +15 -0
- package/dist/workflows/composition.js.map +1 -1
- package/dist/workflows/definition.d.ts +6 -3
- package/dist/workflows/definition.js +3 -0
- package/dist/workflows/definition.js.map +1 -1
- package/dist/workflows/engine.d.ts +6 -1
- package/dist/workflows/engine.js +301 -35
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/human-decision.d.ts +1 -0
- package/dist/workflows/human-decision.js +25 -9
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +3 -1
- package/dist/workflows/index.js +2 -0
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/json-patch.d.ts +47 -0
- package/dist/workflows/json-patch.js +298 -0
- package/dist/workflows/json-patch.js.map +1 -0
- package/dist/workflows/schema.js +29 -1
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/settings.d.ts +128 -0
- package/dist/workflows/settings.js +199 -0
- package/dist/workflows/settings.js.map +1 -0
- package/dist/workflows/store.d.ts +75 -2
- package/dist/workflows/store.js +1349 -89
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/tool-input.d.ts +22 -0
- package/dist/workflows/tool-input.js +43 -0
- package/dist/workflows/tool-input.js.map +1 -1
- package/dist/workflows/types.d.ts +40 -5
- package/docs/2026-08-25-workflow-follow-ups.md +132 -0
- package/docs/2026-08-25-workflow-settings.md +169 -0
- package/docs/DEFERRED_TURNS.md +6 -0
- package/docs/DESIGN_PHILOSOPHY.md +2 -0
- package/docs/SQLITE_STATE.md +31 -18
- package/docs/WORKFLOW_COMPOSITION.md +15 -0
- package/docs/WORKFLOW_STEP_MESSAGES.md +4 -2
- package/docs/plans/2026-08-25-live-workflow-settings-plan.md +532 -0
- package/docs/plans/2026-08-25-workflow-run-storage-plan.md +67 -0
- package/docs/tui-viewer.md +7 -6
- package/docs/workflows.md +22 -1
- package/examples/workflows/live-settings.workflow.ts +93 -0
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/src/builtins/autoimplement.workflow.ts +56 -8
- package/src/builtins/catalog.ts +1 -1
- package/src/controllers/index.ts +6 -0
- package/src/controllers/sqlite.ts +129 -35
- package/src/controllers/types.ts +40 -0
- package/src/controllers/workflow-engine-scheduler.ts +103 -1
- package/src/controllers/workflows.ts +68 -0
- package/src/extension/controller-host.ts +6 -1
- package/src/extension/follow-up-coordinator.ts +151 -0
- package/src/extension/index.ts +538 -88
- package/src/extension/session-events.ts +15 -13
- package/src/extension/widget.ts +8 -0
- package/src/state/database.ts +12 -12
- package/src/state/json.ts +6 -1
- package/src/state/mutation.ts +4 -1
- package/src/state/prune.ts +502 -0
- package/src/state/schema.ts +116 -18
- package/src/viewer/cli.ts +52 -5
- package/src/viewer/render.ts +43 -1
- package/src/workflows/composition.ts +19 -0
- package/src/workflows/definition.ts +19 -5
- package/src/workflows/engine.ts +363 -35
- package/src/workflows/human-decision.ts +41 -11
- package/src/workflows/index.ts +43 -0
- package/src/workflows/json-patch.ts +375 -0
- package/src/workflows/schema.ts +31 -1
- package/src/workflows/settings.ts +430 -0
- package/src/workflows/store.ts +1977 -184
- package/src/workflows/tool-input.ts +58 -0
- package/src/workflows/types.ts +43 -4
package/src/workflows/store.ts
CHANGED
|
@@ -1,20 +1,50 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { StateDatabase, workflowStatePath } from "../state/database.js";
|
|
3
|
-
import { canonicalJson } from "../state/json.js";
|
|
4
|
-
import {
|
|
3
|
+
import { canonicalJson, type JsonValue } from "../state/json.js";
|
|
4
|
+
import {
|
|
5
|
+
StateMutationStore,
|
|
6
|
+
StaleResourceError,
|
|
7
|
+
resourceIdFor,
|
|
8
|
+
tokenHash,
|
|
9
|
+
type ActorType,
|
|
10
|
+
type LeaseClaim,
|
|
11
|
+
type MutationActor,
|
|
12
|
+
type OwnerType,
|
|
13
|
+
} from "../state/mutation.js";
|
|
5
14
|
import { compositionMetadata } from "./composition.js";
|
|
15
|
+
import { applyJsonPatch, validateJsonPatch } from "./json-patch.js";
|
|
16
|
+
import {
|
|
17
|
+
applyWorkflowSettingsPatch,
|
|
18
|
+
workflowSettingsScopeId,
|
|
19
|
+
type InitialWorkflowSettingsScope,
|
|
20
|
+
type WorkflowFollowUpQueueRecord,
|
|
21
|
+
type WorkflowFollowUpRecord,
|
|
22
|
+
type WorkflowFollowUpState,
|
|
23
|
+
type WorkflowPresentationState,
|
|
24
|
+
type WorkflowQueueFollowUpRequest,
|
|
25
|
+
type WorkflowRemoveFollowUpRequest,
|
|
26
|
+
type WorkflowSettingsChangeRecord,
|
|
27
|
+
type WorkflowSettingsChangeRequest,
|
|
28
|
+
type WorkflowSettingsChangeResult,
|
|
29
|
+
type WorkflowSettingsDefinition,
|
|
30
|
+
type WorkflowSettingsScopeRecord,
|
|
31
|
+
} from "./settings.js";
|
|
6
32
|
import type {
|
|
7
33
|
WorkflowDefinition,
|
|
8
34
|
WorkflowDefinitionSnapshot,
|
|
35
|
+
HumanDecisionReceipt,
|
|
9
36
|
WorkflowNodeDefinition,
|
|
10
37
|
WorkflowNodeResult,
|
|
11
38
|
WorkflowNodeSnapshot,
|
|
12
39
|
WorkflowRunState,
|
|
40
|
+
WorkflowSource,
|
|
41
|
+
WorkflowMountedSource,
|
|
13
42
|
WorkflowStepRecord,
|
|
14
43
|
WorkflowSessionBinding,
|
|
15
44
|
WorkflowSessionCapture,
|
|
16
45
|
WorkflowSessionEntryRecord,
|
|
17
46
|
WorkflowSessionEventRecord,
|
|
47
|
+
WorkflowSettingsSnapshot,
|
|
18
48
|
WorkflowTraceEvent,
|
|
19
49
|
WorkflowTraceEventDraft,
|
|
20
50
|
WorkflowUpdateInput,
|
|
@@ -29,6 +59,19 @@ export const SESSION_EVENT_SCHEMA = "pi-workflows.session-event.v1" as const;
|
|
|
29
59
|
export const SESSION_CAPTURE_SCHEMA = "pi-workflows.session-capture.v1" as const;
|
|
30
60
|
export const SESSION_EVENT_MAX_BYTES = 1024 * 1024;
|
|
31
61
|
|
|
62
|
+
const FOLLOW_UP_ROW_SELECT = `
|
|
63
|
+
SELECT f.follow_up_id AS followUpId, f.resource_id AS resourceId,
|
|
64
|
+
f.queue_resource_id AS queueResourceId, f.run_id AS runId,
|
|
65
|
+
f.request_id AS requestId, f.order_number AS orderNumber,
|
|
66
|
+
f.target_session_id AS targetSessionId, f.actor_type AS actorType,
|
|
67
|
+
f.actor_id AS actorId, f.source_type AS sourceType,
|
|
68
|
+
f.prompt_hash AS promptHash, f.status,
|
|
69
|
+
f.session_entry_id AS sessionEntryId, f.reason_hash AS reasonHash,
|
|
70
|
+
f.created_at AS createdAt, f.updated_at AS updatedAt, f.sent_at AS sentAt,
|
|
71
|
+
r.revision
|
|
72
|
+
FROM workflow_follow_ups f
|
|
73
|
+
JOIN resources r ON r.resource_id = f.resource_id`;
|
|
74
|
+
|
|
32
75
|
export function workflowStateDatabasePath(homeDir?: string): string {
|
|
33
76
|
return workflowStatePath(homeDir);
|
|
34
77
|
}
|
|
@@ -63,29 +106,27 @@ type RunRow = {
|
|
|
63
106
|
definitionHash: Buffer;
|
|
64
107
|
definitionDigest: Buffer;
|
|
65
108
|
workflowRef: string;
|
|
66
|
-
sourceRef: string;
|
|
67
|
-
workflowSourceHash: Buffer;
|
|
68
|
-
workflowSourcesHash: Buffer | null;
|
|
69
109
|
parentRunId: string | null;
|
|
70
110
|
title: string | null;
|
|
71
111
|
status: string;
|
|
72
112
|
paused: number;
|
|
73
113
|
statusDetail: string | null;
|
|
74
114
|
inputHash: Buffer;
|
|
75
|
-
humanDecisionHash: Buffer | null;
|
|
76
115
|
finalOutputHash: Buffer | null;
|
|
77
116
|
errorHash: Buffer | null;
|
|
78
|
-
carriedStepCount: number;
|
|
79
|
-
currentNode: string | null;
|
|
80
|
-
currentAttemptId: string | null;
|
|
81
|
-
currentNodeStartedAt: number | null;
|
|
82
|
-
waitingOn: string | null;
|
|
83
117
|
createdAt: number;
|
|
84
118
|
updatedAt: number;
|
|
85
119
|
finishedAt: number | null;
|
|
86
120
|
};
|
|
87
121
|
|
|
88
|
-
type
|
|
122
|
+
type RunSourceRow = {
|
|
123
|
+
mountPath: string;
|
|
124
|
+
sourceType: "builtin" | "file";
|
|
125
|
+
sourceRef: string;
|
|
126
|
+
sourceRevision: string;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
type StoredAttemptReceipt = Pick<WorkflowStepRecord, "action" | "assistantMessage">;
|
|
89
130
|
|
|
90
131
|
type StepRow = {
|
|
91
132
|
stepIndex: number;
|
|
@@ -96,8 +137,15 @@ type StepRow = {
|
|
|
96
137
|
promptHash: Buffer | null;
|
|
97
138
|
outputHash: Buffer | null;
|
|
98
139
|
outputOverrideHash: Buffer | null;
|
|
99
|
-
|
|
140
|
+
receiptHash: Buffer | null;
|
|
141
|
+
promptEntryHash: Buffer | null;
|
|
142
|
+
responseEntryHash: Buffer | null;
|
|
143
|
+
firstEntryId: string | null;
|
|
144
|
+
lastEntryId: string | null;
|
|
100
145
|
errorHash: Buffer | null;
|
|
146
|
+
settingsScopeId: string | null;
|
|
147
|
+
settingsChangeNumber: number | null;
|
|
148
|
+
settingsHash: Buffer | null;
|
|
101
149
|
startedAt: number;
|
|
102
150
|
finishedAt: number;
|
|
103
151
|
};
|
|
@@ -120,6 +168,79 @@ type EventRow = {
|
|
|
120
168
|
recordedAt: number;
|
|
121
169
|
};
|
|
122
170
|
|
|
171
|
+
type SettingsScopeRow = {
|
|
172
|
+
scopeId: string;
|
|
173
|
+
resourceId: string;
|
|
174
|
+
originRunId: string;
|
|
175
|
+
activeRunId: string;
|
|
176
|
+
mountPath: string;
|
|
177
|
+
invocation: number;
|
|
178
|
+
currentHash: Buffer;
|
|
179
|
+
revision: number;
|
|
180
|
+
createdAt: number;
|
|
181
|
+
updatedAt: number;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
type SettingsProjectionRow = {
|
|
185
|
+
scopeId: string;
|
|
186
|
+
initialHash: Buffer;
|
|
187
|
+
currentHash: Buffer;
|
|
188
|
+
revision: number;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
type SettingsProjectionChangeRow = {
|
|
192
|
+
changeNumber: number;
|
|
193
|
+
patchHash: Buffer;
|
|
194
|
+
beforeHash: Buffer;
|
|
195
|
+
afterHash: Buffer;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
type SettingsChangeRow = {
|
|
199
|
+
changeId: string;
|
|
200
|
+
scopeId: string;
|
|
201
|
+
requestId: string;
|
|
202
|
+
changeNumber: number;
|
|
203
|
+
actorType: string;
|
|
204
|
+
actorId: string | null;
|
|
205
|
+
sourceType: string;
|
|
206
|
+
patchHash: Buffer;
|
|
207
|
+
beforeHash: Buffer;
|
|
208
|
+
afterHash: Buffer;
|
|
209
|
+
acceptedAt: number;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
type FollowUpQueueRow = {
|
|
213
|
+
runId: string;
|
|
214
|
+
resourceId: string;
|
|
215
|
+
originSessionId: string | null;
|
|
216
|
+
presentationState: string;
|
|
217
|
+
presentationEntryId: string | null;
|
|
218
|
+
presentationAssistantEntryId: string | null;
|
|
219
|
+
presentationReasonHash: Buffer | null;
|
|
220
|
+
revision: number;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
type FollowUpRow = {
|
|
224
|
+
followUpId: string;
|
|
225
|
+
resourceId: string;
|
|
226
|
+
queueResourceId: string;
|
|
227
|
+
runId: string;
|
|
228
|
+
requestId: string;
|
|
229
|
+
orderNumber: number;
|
|
230
|
+
targetSessionId: string;
|
|
231
|
+
actorType: string;
|
|
232
|
+
actorId: string | null;
|
|
233
|
+
sourceType: string;
|
|
234
|
+
promptHash: Buffer;
|
|
235
|
+
status: string;
|
|
236
|
+
sessionEntryId: string | null;
|
|
237
|
+
reasonHash: Buffer | null;
|
|
238
|
+
createdAt: number;
|
|
239
|
+
updatedAt: number;
|
|
240
|
+
sentAt: number | null;
|
|
241
|
+
revision: number;
|
|
242
|
+
};
|
|
243
|
+
|
|
123
244
|
type SegmentRow = {
|
|
124
245
|
segmentId: string;
|
|
125
246
|
runId: string;
|
|
@@ -178,12 +299,18 @@ export type LoadedWorkflowRun = {
|
|
|
178
299
|
sessionCapture: WorkflowSessionCapture | null;
|
|
179
300
|
sessionIntegrity: SessionCaptureIntegrity;
|
|
180
301
|
sessionSegments: SessionCaptureSegment[];
|
|
302
|
+
settingsScopes?: WorkflowSettingsScopeRecord[];
|
|
303
|
+
followUpQueue?: WorkflowFollowUpQueueRecord | null;
|
|
181
304
|
};
|
|
182
305
|
|
|
183
306
|
export type ReadWorkflowRunOptions = {
|
|
184
307
|
includeTrace?: boolean;
|
|
185
308
|
};
|
|
186
309
|
|
|
310
|
+
export type InitializeWorkflowRunOptions = {
|
|
311
|
+
initialSettings?: InitialWorkflowSettingsScope[];
|
|
312
|
+
};
|
|
313
|
+
|
|
187
314
|
export class WorkflowRunStore {
|
|
188
315
|
readonly databasePath: string;
|
|
189
316
|
readonly state: StateDatabase;
|
|
@@ -192,6 +319,7 @@ export class WorkflowRunStore {
|
|
|
192
319
|
| undefined;
|
|
193
320
|
private readonly contexts = new Map<string, RunContext>();
|
|
194
321
|
private readonly ownsState: boolean;
|
|
322
|
+
private readonly mutations: StateMutationStore;
|
|
195
323
|
|
|
196
324
|
constructor(databasePath: string = workflowStatePath(), options: WorkflowRunStoreOptions = {}) {
|
|
197
325
|
this.authorityProvider = options.authorityProvider;
|
|
@@ -204,13 +332,18 @@ export class WorkflowRunStore {
|
|
|
204
332
|
checkLegacyState: databasePath === workflowStatePath(),
|
|
205
333
|
});
|
|
206
334
|
this.databasePath = this.state.filePath;
|
|
335
|
+
this.mutations = new StateMutationStore(this.state);
|
|
207
336
|
}
|
|
208
337
|
|
|
209
338
|
close(): void {
|
|
210
339
|
if (this.ownsState) this.state.close();
|
|
211
340
|
}
|
|
212
341
|
|
|
213
|
-
async initializeRun(
|
|
342
|
+
async initializeRun(
|
|
343
|
+
workflow: WorkflowDefinition,
|
|
344
|
+
state: WorkflowRunState,
|
|
345
|
+
options: InitializeWorkflowRunOptions = {},
|
|
346
|
+
): Promise<string> {
|
|
214
347
|
assertValidRunId(state.runId);
|
|
215
348
|
if (!this.contexts.has(state.runId)) {
|
|
216
349
|
const reserved = this.readRunRow(state.runId);
|
|
@@ -250,7 +383,9 @@ export class WorkflowRunStore {
|
|
|
250
383
|
payload: { workflowName: workflow.name },
|
|
251
384
|
});
|
|
252
385
|
this.persistRunState(reserved, state, revision, now);
|
|
386
|
+
this.initializeRunSettingsAndFollowUps(state, options.initialSettings ?? [], now);
|
|
253
387
|
this.syncNodeAttempts(state, snapshot, now);
|
|
388
|
+
this.syncContinuationIdentity(state);
|
|
254
389
|
context.revision = revision;
|
|
255
390
|
return;
|
|
256
391
|
}
|
|
@@ -276,14 +411,7 @@ export class WorkflowRunStore {
|
|
|
276
411
|
state.traceSeq = 1;
|
|
277
412
|
state.updatedAt = at;
|
|
278
413
|
const inputHash = this.state.putJson(state.input, now);
|
|
279
|
-
const workflowSourceHash = this.state.putJson(state.workflowSource ?? source, now);
|
|
280
414
|
const launchOptionsHash = this.state.putJson({}, now);
|
|
281
|
-
const workflowSourcesHash =
|
|
282
|
-
state.workflowSources === undefined
|
|
283
|
-
? null
|
|
284
|
-
: this.state.putJson(state.workflowSources, now);
|
|
285
|
-
const humanDecisionHash =
|
|
286
|
-
state.humanDecision === undefined ? null : this.state.putJson(state.humanDecision, now);
|
|
287
415
|
const finalOutputHash =
|
|
288
416
|
state.finalOutput === undefined ? null : this.state.putJson(state.finalOutput, now);
|
|
289
417
|
const errorHash = state.error === undefined ? null : this.state.putText(state.error, now);
|
|
@@ -291,13 +419,10 @@ export class WorkflowRunStore {
|
|
|
291
419
|
.prepare(
|
|
292
420
|
`INSERT INTO runs(
|
|
293
421
|
run_id, resource_id, project_id, parent_run_id, definition_digest,
|
|
294
|
-
workflow_ref,
|
|
295
|
-
|
|
296
|
-
status_detail, input_hash, workflow_sources_hash, human_decision_hash,
|
|
297
|
-
final_output_hash, error_hash, carried_step_count, current_node,
|
|
298
|
-
current_attempt_id, current_node_started_at, waiting_on,
|
|
422
|
+
workflow_ref, launch_options_hash, title, status, paused,
|
|
423
|
+
status_detail, input_hash, final_output_hash, error_hash,
|
|
299
424
|
created_at, updated_at, finished_at
|
|
300
|
-
) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
425
|
+
) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
301
426
|
)
|
|
302
427
|
.run(
|
|
303
428
|
state.runId,
|
|
@@ -305,36 +430,26 @@ export class WorkflowRunStore {
|
|
|
305
430
|
state.parentRunId ?? null,
|
|
306
431
|
definitionDigest,
|
|
307
432
|
state.workflowName,
|
|
308
|
-
workflowSourceHash,
|
|
309
433
|
launchOptionsHash,
|
|
310
|
-
source.type,
|
|
311
|
-
source.ref,
|
|
312
|
-
source.revision,
|
|
313
434
|
state.runTitle ?? null,
|
|
314
435
|
state.status,
|
|
315
436
|
state.paused === true ? 1 : 0,
|
|
316
437
|
state.statusDetail ?? null,
|
|
317
438
|
inputHash,
|
|
318
|
-
workflowSourcesHash,
|
|
319
|
-
humanDecisionHash,
|
|
320
439
|
finalOutputHash,
|
|
321
440
|
errorHash,
|
|
322
|
-
state.carriedStepCount ?? 0,
|
|
323
|
-
state.currentNode ?? null,
|
|
324
|
-
state.currentAttemptId ?? null,
|
|
325
|
-
state.currentNodeStartedAt === undefined
|
|
326
|
-
? null
|
|
327
|
-
: Date.parse(state.currentNodeStartedAt),
|
|
328
|
-
state.waitingOn ?? null,
|
|
329
441
|
Date.parse(state.startedAt),
|
|
330
442
|
now,
|
|
331
443
|
state.finishedAt === undefined ? null : Date.parse(state.finishedAt),
|
|
332
444
|
);
|
|
445
|
+
this.insertRunSources(state.runId, source, state.workflowSources ?? []);
|
|
446
|
+
this.syncContinuationIdentity(state);
|
|
333
447
|
insertRunEvent(this.state, resourceId, 1, at, {
|
|
334
448
|
scope: "run",
|
|
335
449
|
type: "run_created",
|
|
336
450
|
payload: { workflowName: workflow.name },
|
|
337
451
|
});
|
|
452
|
+
this.initializeRunSettingsAndFollowUps(state, options.initialSettings ?? [], now);
|
|
338
453
|
this.syncNodeAttempts(state, snapshot, now);
|
|
339
454
|
});
|
|
340
455
|
this.contexts.set(state.runId, { revision: acceptedRevision, lock: Promise.resolve() });
|
|
@@ -342,12 +457,603 @@ export class WorkflowRunStore {
|
|
|
342
457
|
});
|
|
343
458
|
}
|
|
344
459
|
|
|
460
|
+
ensureSettingsScope(options: {
|
|
461
|
+
runId: string;
|
|
462
|
+
mountPath: string;
|
|
463
|
+
invocation: number;
|
|
464
|
+
settings: JsonValue;
|
|
465
|
+
}): WorkflowSettingsScopeRecord {
|
|
466
|
+
const now = Date.now();
|
|
467
|
+
return this.state.transaction(() => {
|
|
468
|
+
this.requireRunAcceptsSettings(options.runId, false);
|
|
469
|
+
const originRunId = this.logicalOriginRunId(options.runId);
|
|
470
|
+
const scopeId = workflowSettingsScopeId(originRunId, options.mountPath, options.invocation);
|
|
471
|
+
const existing = this.settingsScopeRow(scopeId);
|
|
472
|
+
if (existing !== undefined) {
|
|
473
|
+
if (existing.activeRunId !== options.runId) {
|
|
474
|
+
throw new Error(`Workflow settings scope belongs to another active run: ${scopeId}`);
|
|
475
|
+
}
|
|
476
|
+
return this.settingsScopeRecord(existing);
|
|
477
|
+
}
|
|
478
|
+
const resourceId = this.mutations.ensureResource("settings", scopeId, now);
|
|
479
|
+
const settingsHash = this.state.putJson(options.settings, now);
|
|
480
|
+
this.state.connection
|
|
481
|
+
.prepare(
|
|
482
|
+
`INSERT INTO workflow_settings(
|
|
483
|
+
scope_id, resource_id, origin_run_id, active_run_id, mount_path, invocation,
|
|
484
|
+
initial_hash, current_hash, created_at, updated_at
|
|
485
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
486
|
+
)
|
|
487
|
+
.run(
|
|
488
|
+
scopeId,
|
|
489
|
+
resourceId,
|
|
490
|
+
originRunId,
|
|
491
|
+
options.runId,
|
|
492
|
+
options.mountPath,
|
|
493
|
+
options.invocation,
|
|
494
|
+
settingsHash,
|
|
495
|
+
settingsHash,
|
|
496
|
+
now,
|
|
497
|
+
now,
|
|
498
|
+
);
|
|
499
|
+
return this.settingsScopeRecord(this.requireSettingsScopeRow(scopeId));
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
getSettingsScope(scopeId: string): WorkflowSettingsScopeRecord | undefined {
|
|
504
|
+
const row = this.settingsScopeRow(scopeId);
|
|
505
|
+
return row === undefined ? undefined : this.settingsScopeRecord(row);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
getSettingsScopeAtChange(
|
|
509
|
+
scopeId: string,
|
|
510
|
+
changeNumber: number,
|
|
511
|
+
): WorkflowSettingsScopeRecord | undefined {
|
|
512
|
+
if (!Number.isInteger(changeNumber) || changeNumber < 0) {
|
|
513
|
+
throw new Error("Workflow settings change number must be a non-negative integer");
|
|
514
|
+
}
|
|
515
|
+
const row = this.settingsScopeRow(scopeId);
|
|
516
|
+
if (row === undefined) return undefined;
|
|
517
|
+
if (changeNumber > row.revision) {
|
|
518
|
+
throw new Error(`Workflow settings change ${changeNumber} does not exist for ${scopeId}`);
|
|
519
|
+
}
|
|
520
|
+
if (changeNumber === row.revision) return this.settingsScopeRecord(row);
|
|
521
|
+
const saved =
|
|
522
|
+
changeNumber === 0
|
|
523
|
+
? this.state.connection
|
|
524
|
+
.prepare(
|
|
525
|
+
"SELECT initial_hash AS settingsHash FROM workflow_settings WHERE scope_id = ?",
|
|
526
|
+
)
|
|
527
|
+
.get(scopeId)
|
|
528
|
+
: this.state.connection
|
|
529
|
+
.prepare(
|
|
530
|
+
`SELECT after_hash AS settingsHash FROM workflow_setting_changes
|
|
531
|
+
WHERE scope_id = ? AND change_number = ?`,
|
|
532
|
+
)
|
|
533
|
+
.get(scopeId, changeNumber);
|
|
534
|
+
if (!isRecord(saved) || !Buffer.isBuffer(saved.settingsHash)) {
|
|
535
|
+
throw new Error(`Workflow settings change ${changeNumber} is missing for ${scopeId}`);
|
|
536
|
+
}
|
|
537
|
+
return this.settingsScopeRecord(row, changeNumber, saved.settingsHash);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
listSettingsScopes(runId: string): WorkflowSettingsScopeRecord[] {
|
|
541
|
+
return this.state.connection
|
|
542
|
+
.prepare(
|
|
543
|
+
`SELECT s.scope_id AS scopeId, s.resource_id AS resourceId,
|
|
544
|
+
s.origin_run_id AS originRunId, s.active_run_id AS activeRunId,
|
|
545
|
+
s.mount_path AS mountPath, s.invocation,
|
|
546
|
+
s.current_hash AS currentHash, r.revision,
|
|
547
|
+
s.created_at AS createdAt, s.updated_at AS updatedAt
|
|
548
|
+
FROM workflow_settings s
|
|
549
|
+
JOIN resources r ON r.resource_id = s.resource_id
|
|
550
|
+
WHERE s.active_run_id = ?
|
|
551
|
+
ORDER BY s.mount_path, s.invocation`,
|
|
552
|
+
)
|
|
553
|
+
.all(runId)
|
|
554
|
+
.filter(isSettingsScopeRow)
|
|
555
|
+
.map((row) => this.settingsScopeRecord(row));
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
private settingsScopesForRunView(runId: string): WorkflowSettingsScopeRecord[] {
|
|
559
|
+
const originRunId = this.logicalOriginRunId(runId);
|
|
560
|
+
return this.state.connection
|
|
561
|
+
.prepare(
|
|
562
|
+
`SELECT s.scope_id AS scopeId, s.resource_id AS resourceId,
|
|
563
|
+
s.origin_run_id AS originRunId, s.active_run_id AS activeRunId,
|
|
564
|
+
s.mount_path AS mountPath, s.invocation,
|
|
565
|
+
s.current_hash AS currentHash, r.revision,
|
|
566
|
+
s.created_at AS createdAt, s.updated_at AS updatedAt
|
|
567
|
+
FROM workflow_settings s
|
|
568
|
+
JOIN resources r ON r.resource_id = s.resource_id
|
|
569
|
+
WHERE s.origin_run_id = ? OR s.active_run_id = ?
|
|
570
|
+
ORDER BY s.mount_path, s.invocation`,
|
|
571
|
+
)
|
|
572
|
+
.all(originRunId, runId)
|
|
573
|
+
.filter(isSettingsScopeRow)
|
|
574
|
+
.map((row) => this.settingsScopeRecord(row));
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
findSettingsScope(
|
|
578
|
+
runId: string,
|
|
579
|
+
mountPath: string,
|
|
580
|
+
invocation: number,
|
|
581
|
+
): WorkflowSettingsScopeRecord | undefined {
|
|
582
|
+
const row = this.state.connection
|
|
583
|
+
.prepare(
|
|
584
|
+
`SELECT s.scope_id AS scopeId, s.resource_id AS resourceId,
|
|
585
|
+
s.origin_run_id AS originRunId, s.active_run_id AS activeRunId,
|
|
586
|
+
s.mount_path AS mountPath, s.invocation,
|
|
587
|
+
s.current_hash AS currentHash, r.revision,
|
|
588
|
+
s.created_at AS createdAt, s.updated_at AS updatedAt
|
|
589
|
+
FROM workflow_settings s
|
|
590
|
+
JOIN resources r ON r.resource_id = s.resource_id
|
|
591
|
+
WHERE s.active_run_id = ? AND s.mount_path = ? AND s.invocation = ?`,
|
|
592
|
+
)
|
|
593
|
+
.get(runId, mountPath, invocation);
|
|
594
|
+
return isSettingsScopeRow(row) ? this.settingsScopeRecord(row) : undefined;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async changeSettings<TSettings, TInput = unknown>(
|
|
598
|
+
definition: WorkflowSettingsDefinition<TSettings, TInput>,
|
|
599
|
+
request: WorkflowSettingsChangeRequest,
|
|
600
|
+
): Promise<WorkflowSettingsChangeResult> {
|
|
601
|
+
assertRequestId(request.requestId);
|
|
602
|
+
assertSourceType(request.source);
|
|
603
|
+
const patch = validateJsonPatch(request.patch);
|
|
604
|
+
const patchJson = canonicalJson(patch);
|
|
605
|
+
const patchDigest = createHash("sha256").update(patchJson).digest();
|
|
606
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
607
|
+
const prior = this.settingsChangeRow(request.scopeId, request.requestId);
|
|
608
|
+
if (prior !== undefined) {
|
|
609
|
+
this.assertSameSettingsChange(prior, request, patchDigest);
|
|
610
|
+
return {
|
|
611
|
+
scope: this.settingsScopeRecord(this.requireSettingsScopeRow(request.scopeId)),
|
|
612
|
+
change: this.settingsChangeRecord(prior),
|
|
613
|
+
adopted: true,
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
const scope = this.requireSettingsScopeRow(request.scopeId);
|
|
617
|
+
if (scope.activeRunId !== request.runId) {
|
|
618
|
+
throw new Error(`Workflow settings scope is not active for run ${request.runId}`);
|
|
619
|
+
}
|
|
620
|
+
this.requireRunAcceptsSettings(request.runId, true);
|
|
621
|
+
if (
|
|
622
|
+
request.expectedChangeNumber !== undefined &&
|
|
623
|
+
request.expectedChangeNumber !== scope.revision
|
|
624
|
+
) {
|
|
625
|
+
throw new StaleResourceError(
|
|
626
|
+
`Workflow settings changed: expected ${request.expectedChangeNumber}, current ${scope.revision}`,
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
const before = this.state.readJson(scope.currentHash);
|
|
630
|
+
const applied = await applyWorkflowSettingsPatch(
|
|
631
|
+
definition,
|
|
632
|
+
before,
|
|
633
|
+
patch,
|
|
634
|
+
request.actor,
|
|
635
|
+
request.source,
|
|
636
|
+
);
|
|
637
|
+
const beforeHash = scope.currentHash;
|
|
638
|
+
const afterHash = createHash("sha256").update(canonicalJson(applied.json)).digest();
|
|
639
|
+
try {
|
|
640
|
+
const mutation = this.mutations.mutate(
|
|
641
|
+
{
|
|
642
|
+
resourceId: scope.resourceId,
|
|
643
|
+
operation: "settings.change",
|
|
644
|
+
actor: request.actor,
|
|
645
|
+
expectedRevision: scope.revision,
|
|
646
|
+
},
|
|
647
|
+
"settings.changed",
|
|
648
|
+
({ database, nextRevision, now }) => {
|
|
649
|
+
this.requireRunAcceptsSettings(request.runId, true);
|
|
650
|
+
const current = this.requireSettingsScopeRow(request.scopeId);
|
|
651
|
+
if (current.activeRunId !== request.runId || !current.currentHash.equals(beforeHash)) {
|
|
652
|
+
throw new StaleResourceError("Workflow settings changed before the patch committed");
|
|
653
|
+
}
|
|
654
|
+
const patchHash = database.putJson(applied.patch, now);
|
|
655
|
+
const savedAfterHash = database.putJson(applied.json, now);
|
|
656
|
+
const changeId = `setting-change-${randomUUID()}`;
|
|
657
|
+
database.connection
|
|
658
|
+
.prepare(
|
|
659
|
+
`INSERT INTO workflow_setting_changes(
|
|
660
|
+
change_id, scope_id, request_id, change_number,
|
|
661
|
+
actor_type, actor_id, source_type, patch_hash,
|
|
662
|
+
before_hash, after_hash, accepted_at
|
|
663
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
664
|
+
)
|
|
665
|
+
.run(
|
|
666
|
+
changeId,
|
|
667
|
+
request.scopeId,
|
|
668
|
+
request.requestId,
|
|
669
|
+
nextRevision,
|
|
670
|
+
request.actor.type,
|
|
671
|
+
request.actor.id ?? null,
|
|
672
|
+
request.source,
|
|
673
|
+
patchHash,
|
|
674
|
+
beforeHash,
|
|
675
|
+
savedAfterHash,
|
|
676
|
+
now,
|
|
677
|
+
);
|
|
678
|
+
database.connection
|
|
679
|
+
.prepare(
|
|
680
|
+
`UPDATE workflow_settings SET current_hash = ?, updated_at = ? WHERE scope_id = ?`,
|
|
681
|
+
)
|
|
682
|
+
.run(savedAfterHash, now, request.scopeId);
|
|
683
|
+
return changeId;
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
payload: {
|
|
687
|
+
scopeId: request.scopeId,
|
|
688
|
+
requestId: request.requestId,
|
|
689
|
+
source: request.source,
|
|
690
|
+
beforeHash: beforeHash.toString("hex"),
|
|
691
|
+
afterHash: afterHash.toString("hex"),
|
|
692
|
+
},
|
|
693
|
+
},
|
|
694
|
+
);
|
|
695
|
+
const row = this.settingsChangeRow(request.scopeId, request.requestId);
|
|
696
|
+
if (row === undefined || row.changeId !== mutation.value) {
|
|
697
|
+
throw new Error("Accepted workflow settings change is missing");
|
|
698
|
+
}
|
|
699
|
+
return {
|
|
700
|
+
scope: this.settingsScopeRecord(this.requireSettingsScopeRow(request.scopeId)),
|
|
701
|
+
change: this.settingsChangeRecord(row),
|
|
702
|
+
adopted: false,
|
|
703
|
+
};
|
|
704
|
+
} catch (error) {
|
|
705
|
+
if (!(error instanceof StaleResourceError) || request.expectedChangeNumber !== undefined) {
|
|
706
|
+
throw error;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
throw new StaleResourceError("Workflow settings kept changing; retry with an expected number");
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
queueFollowUp(request: WorkflowQueueFollowUpRequest): {
|
|
714
|
+
followUp: WorkflowFollowUpRecord;
|
|
715
|
+
adopted: boolean;
|
|
716
|
+
} {
|
|
717
|
+
assertRequestId(request.requestId);
|
|
718
|
+
assertSourceType(request.source);
|
|
719
|
+
if (typeof request.prompt !== "string" || request.prompt.trim().length === 0) {
|
|
720
|
+
throw new Error("Workflow follow-up prompt must be a non-empty string");
|
|
721
|
+
}
|
|
722
|
+
if (Buffer.byteLength(request.prompt, "utf8") > 64 * 1024) {
|
|
723
|
+
throw new Error("Workflow follow-up prompt cannot exceed 65536 bytes");
|
|
724
|
+
}
|
|
725
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
726
|
+
const prior = this.followUpRowByRequest(request.runId, request.requestId);
|
|
727
|
+
if (prior !== undefined) {
|
|
728
|
+
this.assertSameFollowUp(prior, request);
|
|
729
|
+
return { followUp: this.followUpRecord(prior), adopted: true };
|
|
730
|
+
}
|
|
731
|
+
this.requireRunAcceptsSettings(request.runId, true);
|
|
732
|
+
const queue = this.requireFollowUpQueueRow(request.runId);
|
|
733
|
+
try {
|
|
734
|
+
const result = this.mutations.mutate(
|
|
735
|
+
{
|
|
736
|
+
resourceId: queue.resourceId,
|
|
737
|
+
operation: "follow-up.queue",
|
|
738
|
+
actor: request.actor,
|
|
739
|
+
expectedRevision: queue.revision,
|
|
740
|
+
},
|
|
741
|
+
"follow-up.queued",
|
|
742
|
+
({ database, now }) => {
|
|
743
|
+
this.requireRunAcceptsSettings(request.runId, true);
|
|
744
|
+
const currentQueue = this.requireFollowUpQueueRow(request.runId);
|
|
745
|
+
if (
|
|
746
|
+
currentQueue.originSessionId !== null &&
|
|
747
|
+
currentQueue.originSessionId !== request.targetSessionId
|
|
748
|
+
) {
|
|
749
|
+
throw new Error("Workflow follow-ups must target the run's origin Pi session");
|
|
750
|
+
}
|
|
751
|
+
if (currentQueue.originSessionId === null) {
|
|
752
|
+
database.connection
|
|
753
|
+
.prepare(
|
|
754
|
+
`UPDATE workflow_follow_up_queues
|
|
755
|
+
SET origin_session_id = ?, updated_at = ? WHERE run_id = ?`,
|
|
756
|
+
)
|
|
757
|
+
.run(request.targetSessionId, now, request.runId);
|
|
758
|
+
}
|
|
759
|
+
const order = this.nextFollowUpOrder(request.runId);
|
|
760
|
+
const followUpId = followUpIdFor(request.runId, request.requestId);
|
|
761
|
+
const resourceId = this.mutations.ensureResource("follow_up", followUpId, now);
|
|
762
|
+
const promptHash = database.putText(request.prompt, now);
|
|
763
|
+
database.connection
|
|
764
|
+
.prepare(
|
|
765
|
+
`INSERT INTO workflow_follow_ups(
|
|
766
|
+
follow_up_id, resource_id, queue_resource_id, run_id,
|
|
767
|
+
request_id, order_number, target_session_id,
|
|
768
|
+
actor_type, actor_id, source_type, prompt_hash, status,
|
|
769
|
+
created_at, updated_at
|
|
770
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'queued', ?, ?)`,
|
|
771
|
+
)
|
|
772
|
+
.run(
|
|
773
|
+
followUpId,
|
|
774
|
+
resourceId,
|
|
775
|
+
queue.resourceId,
|
|
776
|
+
request.runId,
|
|
777
|
+
request.requestId,
|
|
778
|
+
order,
|
|
779
|
+
request.targetSessionId,
|
|
780
|
+
request.actor.type,
|
|
781
|
+
request.actor.id ?? null,
|
|
782
|
+
request.source,
|
|
783
|
+
promptHash,
|
|
784
|
+
now,
|
|
785
|
+
now,
|
|
786
|
+
);
|
|
787
|
+
return followUpId;
|
|
788
|
+
},
|
|
789
|
+
{ payload: { runId: request.runId, requestId: request.requestId } },
|
|
790
|
+
);
|
|
791
|
+
return {
|
|
792
|
+
followUp: this.followUpRecord(this.requireFollowUpRow(result.value)),
|
|
793
|
+
adopted: false,
|
|
794
|
+
};
|
|
795
|
+
} catch (error) {
|
|
796
|
+
if (!(error instanceof StaleResourceError)) throw error;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
throw new StaleResourceError("Workflow follow-up queue kept changing; retry the request");
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
removeFollowUp(request: WorkflowRemoveFollowUpRequest): WorkflowFollowUpRecord {
|
|
803
|
+
assertSourceType(request.source);
|
|
804
|
+
const row = this.requireFollowUpRow(request.followUpId);
|
|
805
|
+
if (row.runId !== request.runId) {
|
|
806
|
+
throw new Error(`Workflow follow-up is not part of run ${request.runId}`);
|
|
807
|
+
}
|
|
808
|
+
if (row.status === "removed") return this.followUpRecord(row);
|
|
809
|
+
if (row.status === "sent" || row.status === "cancelled") {
|
|
810
|
+
throw new Error(`Workflow follow-up cannot be removed after it is ${row.status}`);
|
|
811
|
+
}
|
|
812
|
+
this.assertFollowUpRemovalAuthority(row, request.actor, request.source);
|
|
813
|
+
if (this.hasLiveLease(row.resourceId)) {
|
|
814
|
+
throw new Error("Workflow follow-up is being delivered and cannot be removed");
|
|
815
|
+
}
|
|
816
|
+
const reason = "Removed before delivery";
|
|
817
|
+
this.mutations.mutate(
|
|
818
|
+
{
|
|
819
|
+
resourceId: row.resourceId,
|
|
820
|
+
operation: "follow-up.remove",
|
|
821
|
+
actor: request.actor,
|
|
822
|
+
expectedRevision: row.revision,
|
|
823
|
+
},
|
|
824
|
+
"follow-up.removed",
|
|
825
|
+
({ database, now }) => {
|
|
826
|
+
const reasonHash = database.putText(reason, now);
|
|
827
|
+
const update = database.connection
|
|
828
|
+
.prepare(
|
|
829
|
+
`UPDATE workflow_follow_ups
|
|
830
|
+
SET status = 'removed', reason_hash = ?, updated_at = ?
|
|
831
|
+
WHERE follow_up_id = ? AND status IN ('queued', 'pending_presentation', 'ready')`,
|
|
832
|
+
)
|
|
833
|
+
.run(reasonHash, now, request.followUpId);
|
|
834
|
+
if (update.changes !== 1) {
|
|
835
|
+
throw new StaleResourceError("Workflow follow-up changed before removal");
|
|
836
|
+
}
|
|
837
|
+
},
|
|
838
|
+
{ payload: { followUpId: request.followUpId, source: request.source } },
|
|
839
|
+
);
|
|
840
|
+
return this.followUpRecord(this.requireFollowUpRow(request.followUpId));
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
originSessionId(runId: string): string | undefined {
|
|
844
|
+
const row = this.state.connection
|
|
845
|
+
.prepare(
|
|
846
|
+
`SELECT COALESCE(q.origin_session_id, b.origin_session_id) AS originSessionId
|
|
847
|
+
FROM runs r
|
|
848
|
+
LEFT JOIN workflow_follow_up_queues q ON q.run_id = r.run_id
|
|
849
|
+
LEFT JOIN run_bindings b ON b.run_id = r.run_id
|
|
850
|
+
WHERE r.run_id = ?`,
|
|
851
|
+
)
|
|
852
|
+
.get(runId);
|
|
853
|
+
return isRecord(row) && typeof row.originSessionId === "string"
|
|
854
|
+
? row.originSessionId
|
|
855
|
+
: undefined;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
readFollowUpQueue(runId: string): WorkflowFollowUpQueueRecord | undefined {
|
|
859
|
+
const queue = this.followUpQueueRow(runId);
|
|
860
|
+
if (queue === undefined) return undefined;
|
|
861
|
+
const followUps = this.state.connection
|
|
862
|
+
.prepare(`${FOLLOW_UP_ROW_SELECT} WHERE f.run_id = ? ORDER BY f.order_number`)
|
|
863
|
+
.all(runId)
|
|
864
|
+
.filter(isFollowUpRow)
|
|
865
|
+
.map((row) => this.followUpRecord(row));
|
|
866
|
+
return {
|
|
867
|
+
runId,
|
|
868
|
+
...(queue.originSessionId !== null ? { originSessionId: queue.originSessionId } : {}),
|
|
869
|
+
presentationState: assertPresentationState(queue.presentationState),
|
|
870
|
+
...(queue.presentationEntryId !== null
|
|
871
|
+
? { presentationEntryId: queue.presentationEntryId }
|
|
872
|
+
: {}),
|
|
873
|
+
...(queue.presentationAssistantEntryId !== null
|
|
874
|
+
? { presentationAssistantEntryId: queue.presentationAssistantEntryId }
|
|
875
|
+
: {}),
|
|
876
|
+
...(queue.presentationReasonHash !== null
|
|
877
|
+
? { presentationReason: this.readText(queue.presentationReasonHash) }
|
|
878
|
+
: {}),
|
|
879
|
+
followUps,
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
listPendingPresentations(targetSessionId: string): string[] {
|
|
884
|
+
return this.state.connection
|
|
885
|
+
.prepare(
|
|
886
|
+
`SELECT run_id AS runId FROM workflow_follow_up_queues
|
|
887
|
+
WHERE origin_session_id = ? AND presentation_state = 'pending'
|
|
888
|
+
ORDER BY created_at`,
|
|
889
|
+
)
|
|
890
|
+
.all(targetSessionId)
|
|
891
|
+
.flatMap((row) => (isRecord(row) && typeof row.runId === "string" ? [row.runId] : []));
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
settleFollowUpPresentation(options: {
|
|
895
|
+
runId: string;
|
|
896
|
+
state: "settled" | "unavailable";
|
|
897
|
+
presentationEntryId?: string;
|
|
898
|
+
assistantEntryId?: string;
|
|
899
|
+
reason?: string;
|
|
900
|
+
}): WorkflowFollowUpQueueRecord {
|
|
901
|
+
const queue = this.requireFollowUpQueueRow(options.runId);
|
|
902
|
+
if (queue.presentationState === options.state) {
|
|
903
|
+
return this.readFollowUpQueue(options.runId) as WorkflowFollowUpQueueRecord;
|
|
904
|
+
}
|
|
905
|
+
if (queue.presentationState !== "pending") {
|
|
906
|
+
throw new Error(
|
|
907
|
+
`Workflow presentation is ${queue.presentationState}, not pending for ${options.runId}`,
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
if (
|
|
911
|
+
options.state === "settled" &&
|
|
912
|
+
(options.presentationEntryId === undefined || options.assistantEntryId === undefined)
|
|
913
|
+
) {
|
|
914
|
+
throw new Error("Settled workflow presentation requires both session entry IDs");
|
|
915
|
+
}
|
|
916
|
+
if (options.state === "unavailable" && !options.reason?.trim()) {
|
|
917
|
+
throw new Error("Unavailable workflow presentation requires a reason");
|
|
918
|
+
}
|
|
919
|
+
try {
|
|
920
|
+
this.mutations.mutate(
|
|
921
|
+
{
|
|
922
|
+
resourceId: queue.resourceId,
|
|
923
|
+
operation: "follow-up.presentation",
|
|
924
|
+
actor: {
|
|
925
|
+
type: "session",
|
|
926
|
+
...(queue.originSessionId !== null ? { id: queue.originSessionId } : {}),
|
|
927
|
+
},
|
|
928
|
+
expectedRevision: queue.revision,
|
|
929
|
+
},
|
|
930
|
+
`follow-up.presentation-${options.state}`,
|
|
931
|
+
({ database, now }) => {
|
|
932
|
+
const reasonHash =
|
|
933
|
+
options.state === "unavailable"
|
|
934
|
+
? database.putText(options.reason as string, now)
|
|
935
|
+
: null;
|
|
936
|
+
database.connection
|
|
937
|
+
.prepare(
|
|
938
|
+
`UPDATE workflow_follow_up_queues
|
|
939
|
+
SET presentation_state = ?, presentation_entry_id = ?,
|
|
940
|
+
presentation_assistant_entry_id = ?, presentation_reason_hash = ?,
|
|
941
|
+
presentation_updated_at = ?, updated_at = ?
|
|
942
|
+
WHERE run_id = ? AND presentation_state = 'pending'`,
|
|
943
|
+
)
|
|
944
|
+
.run(
|
|
945
|
+
options.state,
|
|
946
|
+
options.presentationEntryId ?? null,
|
|
947
|
+
options.assistantEntryId ?? null,
|
|
948
|
+
reasonHash,
|
|
949
|
+
now,
|
|
950
|
+
now,
|
|
951
|
+
options.runId,
|
|
952
|
+
);
|
|
953
|
+
database.connection
|
|
954
|
+
.prepare(
|
|
955
|
+
`UPDATE workflow_follow_ups SET status = 'ready', updated_at = ?
|
|
956
|
+
WHERE run_id = ? AND status = 'pending_presentation'`,
|
|
957
|
+
)
|
|
958
|
+
.run(now, options.runId);
|
|
959
|
+
},
|
|
960
|
+
{ payload: { runId: options.runId, state: options.state } },
|
|
961
|
+
);
|
|
962
|
+
} catch (error) {
|
|
963
|
+
if (error instanceof StaleResourceError) {
|
|
964
|
+
const current = this.requireFollowUpQueueRow(options.runId);
|
|
965
|
+
if (current.presentationState === options.state) {
|
|
966
|
+
return this.readFollowUpQueue(options.runId) as WorkflowFollowUpQueueRecord;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
throw error;
|
|
970
|
+
}
|
|
971
|
+
return this.readFollowUpQueue(options.runId) as WorkflowFollowUpQueueRecord;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
claimNextFollowUp(
|
|
975
|
+
targetSessionId: string,
|
|
976
|
+
ownerId: string,
|
|
977
|
+
leaseMs = 30_000,
|
|
978
|
+
): { followUp: WorkflowFollowUpRecord; claim: LeaseClaim } | undefined {
|
|
979
|
+
const rows = this.state.connection
|
|
980
|
+
.prepare(
|
|
981
|
+
`${FOLLOW_UP_ROW_SELECT}
|
|
982
|
+
WHERE f.target_session_id = ? AND f.status = 'ready'
|
|
983
|
+
ORDER BY f.created_at, f.order_number LIMIT 16`,
|
|
984
|
+
)
|
|
985
|
+
.all(targetSessionId)
|
|
986
|
+
.filter(isFollowUpRow);
|
|
987
|
+
for (const row of rows) {
|
|
988
|
+
let claim: LeaseClaim | undefined;
|
|
989
|
+
try {
|
|
990
|
+
claim = this.mutations.claim({
|
|
991
|
+
resourceId: row.resourceId,
|
|
992
|
+
ownerType: "session",
|
|
993
|
+
ownerId,
|
|
994
|
+
expectedRevision: row.revision,
|
|
995
|
+
leaseMs,
|
|
996
|
+
});
|
|
997
|
+
} catch (error) {
|
|
998
|
+
if (error instanceof StaleResourceError) continue;
|
|
999
|
+
throw error;
|
|
1000
|
+
}
|
|
1001
|
+
if (claim === undefined) continue;
|
|
1002
|
+
const current = this.requireFollowUpRow(row.followUpId);
|
|
1003
|
+
if (current.status !== "ready") {
|
|
1004
|
+
this.mutations.release(claim, claim.resourceRevision);
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
return { followUp: this.followUpRecord(current), claim };
|
|
1008
|
+
}
|
|
1009
|
+
return undefined;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
markFollowUpSent(
|
|
1013
|
+
followUpId: string,
|
|
1014
|
+
claim: LeaseClaim,
|
|
1015
|
+
sessionEntryId: string,
|
|
1016
|
+
): WorkflowFollowUpRecord {
|
|
1017
|
+
const row = this.requireFollowUpRow(followUpId);
|
|
1018
|
+
if (row.status === "sent") return this.followUpRecord(row);
|
|
1019
|
+
this.mutations.mutate(
|
|
1020
|
+
{
|
|
1021
|
+
resourceId: row.resourceId,
|
|
1022
|
+
operation: "follow-up.sent",
|
|
1023
|
+
actor: { type: "session", id: claim.ownerId },
|
|
1024
|
+
expectedRevision: claim.resourceRevision,
|
|
1025
|
+
lease: claim,
|
|
1026
|
+
},
|
|
1027
|
+
"follow-up.sent",
|
|
1028
|
+
({ database, now }) => {
|
|
1029
|
+
const update = database.connection
|
|
1030
|
+
.prepare(
|
|
1031
|
+
`UPDATE workflow_follow_ups
|
|
1032
|
+
SET status = 'sent', session_entry_id = ?, sent_at = ?, updated_at = ?
|
|
1033
|
+
WHERE follow_up_id = ? AND status = 'ready'`,
|
|
1034
|
+
)
|
|
1035
|
+
.run(sessionEntryId, now, now, followUpId);
|
|
1036
|
+
if (update.changes !== 1) {
|
|
1037
|
+
throw new StaleResourceError("Workflow follow-up changed before delivery completed");
|
|
1038
|
+
}
|
|
1039
|
+
},
|
|
1040
|
+
{ payload: { followUpId, sessionEntryId } },
|
|
1041
|
+
);
|
|
1042
|
+
return this.followUpRecord(this.requireFollowUpRow(followUpId));
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
releaseFollowUpClaim(claim: LeaseClaim): void {
|
|
1046
|
+
const revision = this.requireResourceRevision(claim.resourceId);
|
|
1047
|
+
this.mutations.release(claim, revision);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
345
1050
|
async prepareRunResume(runId: string): Promise<LoadedWorkflowRun> {
|
|
346
1051
|
const loaded = this.readRun(runId, { includeTrace: true });
|
|
347
1052
|
if (loaded === null) throw new Error(`Cannot resume unreadable workflow run: ${runId}`);
|
|
348
1053
|
if (loaded.state.status !== "running") {
|
|
349
1054
|
throw new Error(`Cannot resume workflow run ${runId} with status ${loaded.state.status}`);
|
|
350
1055
|
}
|
|
1056
|
+
this.verifySettingsProjections(runId);
|
|
351
1057
|
const revision = this.resourceRevision(runId);
|
|
352
1058
|
this.contexts.set(runId, { revision, lock: Promise.resolve() });
|
|
353
1059
|
this.finalizeRecordingCaptures(runId, "Workflow host stopped before the run finished");
|
|
@@ -361,10 +1067,10 @@ export class WorkflowRunStore {
|
|
|
361
1067
|
this.state.connection
|
|
362
1068
|
.prepare(
|
|
363
1069
|
`UPDATE node_attempts
|
|
364
|
-
SET status = '
|
|
1070
|
+
SET status = 'interrupted', updated_at = ?
|
|
365
1071
|
WHERE run_id = ? AND status IN ('pending', 'running', 'waiting')`,
|
|
366
1072
|
)
|
|
367
|
-
.run(now,
|
|
1073
|
+
.run(now, runId);
|
|
368
1074
|
insertRunEvent(this.state, row.resourceId, nextRevision, at, {
|
|
369
1075
|
scope: "run",
|
|
370
1076
|
type: "run_resume_prepared",
|
|
@@ -393,6 +1099,9 @@ export class WorkflowRunStore {
|
|
|
393
1099
|
delete loaded.state.currentNode;
|
|
394
1100
|
delete loaded.state.currentAttemptId;
|
|
395
1101
|
delete loaded.state.currentNodeStartedAt;
|
|
1102
|
+
delete loaded.state.currentSettingsScopeId;
|
|
1103
|
+
delete loaded.state.currentSettingsChangeNumber;
|
|
1104
|
+
delete loaded.state.currentSettingsHash;
|
|
396
1105
|
delete loaded.state.statusDetail;
|
|
397
1106
|
delete loaded.state.paused;
|
|
398
1107
|
await this.writeSnapshot(runId, loaded.state, {
|
|
@@ -502,12 +1211,14 @@ export class WorkflowRunStore {
|
|
|
502
1211
|
const revision = context.revision + 1;
|
|
503
1212
|
const now = Date.now();
|
|
504
1213
|
const at = new Date(now).toISOString();
|
|
1214
|
+
const snapshot = this.readDefinition(run.definitionHash);
|
|
1215
|
+
this.assertSettingsRouteCurrent(event, snapshot);
|
|
505
1216
|
const traceEvent: WorkflowTraceEvent = { seq: revision, at, runId, ...event };
|
|
506
1217
|
state.traceSeq = revision;
|
|
507
1218
|
state.updatedAt = at;
|
|
508
1219
|
insertRunEvent(this.state, run.resourceId, revision, at, traceEvent);
|
|
509
1220
|
this.persistRunState(run, state, revision, now);
|
|
510
|
-
|
|
1221
|
+
this.transitionFollowUpsForRunState(state, event, now);
|
|
511
1222
|
this.syncNodeAttempts(state, snapshot, now);
|
|
512
1223
|
context.revision = revision;
|
|
513
1224
|
accepted = traceEvent;
|
|
@@ -593,6 +1304,13 @@ export class WorkflowRunStore {
|
|
|
593
1304
|
ON CONFLICT(run_id) DO NOTHING`,
|
|
594
1305
|
)
|
|
595
1306
|
.run(runId, binding.piSessionId, now);
|
|
1307
|
+
this.state.connection
|
|
1308
|
+
.prepare(
|
|
1309
|
+
`UPDATE workflow_follow_up_queues
|
|
1310
|
+
SET origin_session_id = COALESCE(origin_session_id, ?), updated_at = ?
|
|
1311
|
+
WHERE run_id = ?`,
|
|
1312
|
+
)
|
|
1313
|
+
.run(binding.piSessionId, now, runId);
|
|
596
1314
|
const revision = context.revision + 1;
|
|
597
1315
|
const at = new Date(now).toISOString();
|
|
598
1316
|
insertRunEvent(this.state, run.resourceId, revision, at, {
|
|
@@ -645,7 +1363,7 @@ export class WorkflowRunStore {
|
|
|
645
1363
|
"session.entry_appended",
|
|
646
1364
|
"session",
|
|
647
1365
|
segment.sessionId,
|
|
648
|
-
|
|
1366
|
+
null,
|
|
649
1367
|
now,
|
|
650
1368
|
);
|
|
651
1369
|
return sequence;
|
|
@@ -776,6 +1494,8 @@ export class WorkflowRunStore {
|
|
|
776
1494
|
sessionCapture: flat.capture,
|
|
777
1495
|
sessionIntegrity: flat.integrity,
|
|
778
1496
|
sessionSegments: segments.filter((segment) => segment.attemptId !== ""),
|
|
1497
|
+
settingsScopes: this.settingsScopesForRunView(runId),
|
|
1498
|
+
followUpQueue: this.readFollowUpQueue(runId) ?? null,
|
|
779
1499
|
};
|
|
780
1500
|
}
|
|
781
1501
|
|
|
@@ -894,10 +1614,6 @@ export class WorkflowRunStore {
|
|
|
894
1614
|
now: number,
|
|
895
1615
|
): void {
|
|
896
1616
|
this.bumpResource(run.resourceId, expectedRevision - 1, now);
|
|
897
|
-
const workflowSourcesHash =
|
|
898
|
-
state.workflowSources === undefined ? null : this.state.putJson(state.workflowSources, now);
|
|
899
|
-
const humanDecisionHash =
|
|
900
|
-
state.humanDecision === undefined ? null : this.state.putJson(state.humanDecision, now);
|
|
901
1617
|
const finalOutputHash =
|
|
902
1618
|
state.finalOutput === undefined ? null : this.state.putJson(state.finalOutput, now);
|
|
903
1619
|
const errorHash = state.error === undefined ? null : this.state.putText(state.error, now);
|
|
@@ -905,9 +1621,7 @@ export class WorkflowRunStore {
|
|
|
905
1621
|
.prepare(
|
|
906
1622
|
`UPDATE runs
|
|
907
1623
|
SET title = ?, status = ?, paused = ?, status_detail = ?,
|
|
908
|
-
|
|
909
|
-
error_hash = ?, carried_step_count = ?, current_node = ?, current_attempt_id = ?,
|
|
910
|
-
current_node_started_at = ?, waiting_on = ?, updated_at = ?, finished_at = ?
|
|
1624
|
+
final_output_hash = ?, error_hash = ?, updated_at = ?, finished_at = ?
|
|
911
1625
|
WHERE run_id = ?`,
|
|
912
1626
|
)
|
|
913
1627
|
.run(
|
|
@@ -915,15 +1629,8 @@ export class WorkflowRunStore {
|
|
|
915
1629
|
state.status,
|
|
916
1630
|
state.paused === true ? 1 : 0,
|
|
917
1631
|
state.statusDetail ?? null,
|
|
918
|
-
workflowSourcesHash,
|
|
919
|
-
humanDecisionHash,
|
|
920
1632
|
finalOutputHash,
|
|
921
1633
|
errorHash,
|
|
922
|
-
state.carriedStepCount ?? 0,
|
|
923
|
-
state.currentNode ?? null,
|
|
924
|
-
state.currentAttemptId ?? null,
|
|
925
|
-
state.currentNodeStartedAt === undefined ? null : Date.parse(state.currentNodeStartedAt),
|
|
926
|
-
state.waitingOn ?? null,
|
|
927
1634
|
now,
|
|
928
1635
|
state.finishedAt === undefined ? null : Date.parse(state.finishedAt),
|
|
929
1636
|
state.runId,
|
|
@@ -1001,104 +1708,533 @@ export class WorkflowRunStore {
|
|
|
1001
1708
|
);
|
|
1002
1709
|
}
|
|
1003
1710
|
|
|
1004
|
-
private
|
|
1711
|
+
private initializeRunSettingsAndFollowUps(
|
|
1005
1712
|
state: WorkflowRunState,
|
|
1006
|
-
|
|
1713
|
+
initialSettings: InitialWorkflowSettingsScope[],
|
|
1007
1714
|
now: number,
|
|
1008
1715
|
): void {
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
const
|
|
1012
|
-
|
|
1013
|
-
const outputHash = step.output === undefined ? null : this.state.putJson(step.output, now);
|
|
1014
|
-
const errorHash = step.error === undefined ? null : this.state.putText(step.error, now);
|
|
1015
|
-
const promptHash = step.prompt === null ? null : this.state.putJson(step.prompt, now);
|
|
1016
|
-
const existing = this.state.connection
|
|
1017
|
-
.prepare("SELECT attempt_id AS attemptId FROM node_attempts WHERE attempt_id = ?")
|
|
1018
|
-
.get(step.attemptId);
|
|
1019
|
-
if (existing === undefined) {
|
|
1020
|
-
const attemptNumber = this.nextAttemptNumber(state.runId, step.nodeId);
|
|
1716
|
+
const childQueueResourceId = this.ensureFollowUpQueue(state.runId, now);
|
|
1717
|
+
if (state.parentRunId !== undefined) {
|
|
1718
|
+
const parentQueue = this.followUpQueueRow(state.parentRunId);
|
|
1719
|
+
if (parentQueue !== undefined) {
|
|
1021
1720
|
this.state.connection
|
|
1022
1721
|
.prepare(
|
|
1023
|
-
`
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
started_at, finished_at, created_at, updated_at
|
|
1027
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1722
|
+
`UPDATE workflow_follow_up_queues
|
|
1723
|
+
SET origin_session_id = COALESCE(origin_session_id, ?), updated_at = ?
|
|
1724
|
+
WHERE run_id = ?`,
|
|
1028
1725
|
)
|
|
1029
|
-
.run(
|
|
1030
|
-
step.attemptId,
|
|
1031
|
-
state.runId,
|
|
1032
|
-
step.nodeId,
|
|
1033
|
-
attemptNumber,
|
|
1034
|
-
step.nodeType,
|
|
1035
|
-
outcomeStatus(step.outcome),
|
|
1036
|
-
promptHash,
|
|
1037
|
-
outputHash,
|
|
1038
|
-
stepMetadataHash,
|
|
1039
|
-
errorHash,
|
|
1040
|
-
Date.parse(step.startedAt),
|
|
1041
|
-
Date.parse(step.finishedAt),
|
|
1042
|
-
Date.parse(step.startedAt),
|
|
1043
|
-
now,
|
|
1044
|
-
);
|
|
1045
|
-
} else {
|
|
1726
|
+
.run(parentQueue.originSessionId, now, state.runId);
|
|
1046
1727
|
this.state.connection
|
|
1047
1728
|
.prepare(
|
|
1048
|
-
`UPDATE
|
|
1049
|
-
SET
|
|
1050
|
-
|
|
1051
|
-
WHERE attempt_id = ?`,
|
|
1729
|
+
`UPDATE workflow_follow_ups
|
|
1730
|
+
SET run_id = ?, queue_resource_id = ?, updated_at = ?
|
|
1731
|
+
WHERE run_id = ? AND status = 'queued'`,
|
|
1052
1732
|
)
|
|
1053
|
-
.run(
|
|
1054
|
-
outcomeStatus(step.outcome),
|
|
1055
|
-
promptHash,
|
|
1056
|
-
outputHash,
|
|
1057
|
-
stepMetadataHash,
|
|
1058
|
-
errorHash,
|
|
1059
|
-
Date.parse(step.finishedAt),
|
|
1060
|
-
now,
|
|
1061
|
-
step.attemptId,
|
|
1062
|
-
);
|
|
1733
|
+
.run(state.runId, childQueueResourceId, now, state.parentRunId);
|
|
1063
1734
|
}
|
|
1735
|
+
this.state.connection
|
|
1736
|
+
.prepare(
|
|
1737
|
+
`UPDATE workflow_settings SET active_run_id = ?, updated_at = ? WHERE active_run_id = ?`,
|
|
1738
|
+
)
|
|
1739
|
+
.run(state.runId, now, state.parentRunId);
|
|
1064
1740
|
}
|
|
1065
|
-
|
|
1066
|
-
|
|
1741
|
+
const existing = this.state.connection
|
|
1742
|
+
.prepare("SELECT COUNT(*) AS count FROM workflow_settings WHERE active_run_id = ?")
|
|
1743
|
+
.get(state.runId);
|
|
1744
|
+
const hasSettings = isCountRow(existing) && existing.count > 0;
|
|
1745
|
+
if (hasSettings) return;
|
|
1746
|
+
for (const scope of initialSettings) {
|
|
1747
|
+
const originRunId = this.logicalOriginRunId(state.runId);
|
|
1748
|
+
const scopeId = workflowSettingsScopeId(originRunId, scope.mountPath, scope.invocation);
|
|
1749
|
+
const resourceId = this.mutations.ensureResource("settings", scopeId, now);
|
|
1750
|
+
const settingsHash = this.state.putJson(scope.settings, now);
|
|
1751
|
+
this.state.connection
|
|
1752
|
+
.prepare(
|
|
1753
|
+
`INSERT INTO workflow_settings(
|
|
1754
|
+
scope_id, resource_id, origin_run_id, active_run_id, mount_path, invocation,
|
|
1755
|
+
initial_hash, current_hash, created_at, updated_at
|
|
1756
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1757
|
+
ON CONFLICT(scope_id) DO NOTHING`,
|
|
1758
|
+
)
|
|
1759
|
+
.run(
|
|
1760
|
+
scopeId,
|
|
1761
|
+
resourceId,
|
|
1762
|
+
originRunId,
|
|
1763
|
+
state.runId,
|
|
1764
|
+
scope.mountPath,
|
|
1765
|
+
scope.invocation,
|
|
1766
|
+
settingsHash,
|
|
1767
|
+
settingsHash,
|
|
1768
|
+
now,
|
|
1769
|
+
now,
|
|
1770
|
+
);
|
|
1067
1771
|
}
|
|
1068
|
-
this.syncRunSteps(state, now);
|
|
1069
1772
|
}
|
|
1070
1773
|
|
|
1071
|
-
private
|
|
1072
|
-
const
|
|
1774
|
+
private ensureFollowUpQueue(runId: string, now: number): string {
|
|
1775
|
+
const resourceId = this.mutations.ensureResource("follow_up_queue", runId, now);
|
|
1776
|
+
this.state.connection
|
|
1073
1777
|
.prepare(
|
|
1074
|
-
`
|
|
1075
|
-
|
|
1778
|
+
`INSERT INTO workflow_follow_up_queues(
|
|
1779
|
+
run_id, resource_id, created_at, updated_at
|
|
1780
|
+
) VALUES (?, ?, ?, ?)
|
|
1781
|
+
ON CONFLICT(run_id) DO NOTHING`,
|
|
1076
1782
|
)
|
|
1077
|
-
.
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1783
|
+
.run(runId, resourceId, now, now);
|
|
1784
|
+
return resourceId;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
private logicalOriginRunId(runId: string): string {
|
|
1788
|
+
let current = runId;
|
|
1789
|
+
for (let depth = 0; depth < 100; depth += 1) {
|
|
1790
|
+
const row = this.state.connection
|
|
1791
|
+
.prepare("SELECT parent_run_id AS parentRunId FROM runs WHERE run_id = ?")
|
|
1792
|
+
.get(current);
|
|
1793
|
+
if (!isParentRunRow(row)) return current;
|
|
1794
|
+
if (row.parentRunId === null) return current;
|
|
1795
|
+
current = row.parentRunId;
|
|
1081
1796
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1797
|
+
throw new Error(`Workflow continuation chain is too deep for run ${runId}`);
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
private requireRunAcceptsSettings(runId: string, allowWaiting: boolean): void {
|
|
1801
|
+
const row = this.state.connection
|
|
1802
|
+
.prepare("SELECT status FROM runs WHERE run_id = ?")
|
|
1803
|
+
.get(runId);
|
|
1804
|
+
if (!isStatusRow(row)) throw new Error(`Workflow run is missing: ${runId}`);
|
|
1805
|
+
if (row.status !== "running" && !(allowWaiting && row.status === "waiting")) {
|
|
1806
|
+
throw new Error(`Workflow run ${runId} does not accept changes with status ${row.status}`);
|
|
1086
1807
|
}
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
private verifySettingsProjections(runId: string): void {
|
|
1811
|
+
const scopes = this.state.connection
|
|
1812
|
+
.prepare(
|
|
1813
|
+
`SELECT s.scope_id AS scopeId, s.initial_hash AS initialHash,
|
|
1814
|
+
s.current_hash AS currentHash, r.revision
|
|
1815
|
+
FROM workflow_settings s
|
|
1816
|
+
JOIN resources r ON r.resource_id = s.resource_id
|
|
1817
|
+
WHERE s.active_run_id = ?`,
|
|
1818
|
+
)
|
|
1819
|
+
.all(runId);
|
|
1820
|
+
for (const value of scopes) {
|
|
1821
|
+
if (!isSettingsProjectionRow(value)) {
|
|
1822
|
+
throw new Error("Workflow settings projection row is invalid");
|
|
1096
1823
|
}
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
(
|
|
1101
|
-
|
|
1824
|
+
let current = this.state.readJson(value.initialHash);
|
|
1825
|
+
let currentHash = value.initialHash;
|
|
1826
|
+
const changes = this.state.connection
|
|
1827
|
+
.prepare(
|
|
1828
|
+
`SELECT change_number AS changeNumber, patch_hash AS patchHash,
|
|
1829
|
+
before_hash AS beforeHash, after_hash AS afterHash
|
|
1830
|
+
FROM workflow_setting_changes WHERE scope_id = ? ORDER BY change_number`,
|
|
1831
|
+
)
|
|
1832
|
+
.all(value.scopeId);
|
|
1833
|
+
let expected = 1;
|
|
1834
|
+
for (const change of changes) {
|
|
1835
|
+
if (!isSettingsProjectionChangeRow(change) || change.changeNumber !== expected) {
|
|
1836
|
+
throw new Error(`Workflow settings changes are not contiguous for ${value.scopeId}`);
|
|
1837
|
+
}
|
|
1838
|
+
if (!change.beforeHash.equals(currentHash)) {
|
|
1839
|
+
throw new Error(`Workflow settings change has a wrong old value for ${value.scopeId}`);
|
|
1840
|
+
}
|
|
1841
|
+
current = applyJsonPatch(current, this.state.readJson(change.patchHash));
|
|
1842
|
+
currentHash = createHash("sha256").update(canonicalJson(current)).digest();
|
|
1843
|
+
if (!currentHash.equals(change.afterHash)) {
|
|
1844
|
+
throw new Error(`Workflow settings change has a wrong new value for ${value.scopeId}`);
|
|
1845
|
+
}
|
|
1846
|
+
expected += 1;
|
|
1847
|
+
}
|
|
1848
|
+
if (value.revision !== expected - 1 || !value.currentHash.equals(currentHash)) {
|
|
1849
|
+
throw new Error(
|
|
1850
|
+
`Workflow settings projection does not match saved changes for ${value.scopeId}`,
|
|
1851
|
+
);
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
private settingsScopeRow(scopeId: string): SettingsScopeRow | undefined {
|
|
1857
|
+
const row = this.state.connection
|
|
1858
|
+
.prepare(
|
|
1859
|
+
`SELECT s.scope_id AS scopeId, s.resource_id AS resourceId,
|
|
1860
|
+
s.origin_run_id AS originRunId, s.active_run_id AS activeRunId,
|
|
1861
|
+
s.mount_path AS mountPath, s.invocation,
|
|
1862
|
+
s.current_hash AS currentHash, r.revision,
|
|
1863
|
+
s.created_at AS createdAt, s.updated_at AS updatedAt
|
|
1864
|
+
FROM workflow_settings s
|
|
1865
|
+
JOIN resources r ON r.resource_id = s.resource_id
|
|
1866
|
+
WHERE s.scope_id = ?`,
|
|
1867
|
+
)
|
|
1868
|
+
.get(scopeId);
|
|
1869
|
+
return isSettingsScopeRow(row) ? row : undefined;
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
private requireSettingsScopeRow(scopeId: string): SettingsScopeRow {
|
|
1873
|
+
const row = this.settingsScopeRow(scopeId);
|
|
1874
|
+
if (row === undefined) throw new Error(`Workflow settings scope not found: ${scopeId}`);
|
|
1875
|
+
return row;
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
private settingsScopeRecord(
|
|
1879
|
+
row: SettingsScopeRow,
|
|
1880
|
+
changeNumber = row.revision,
|
|
1881
|
+
settingsHash = row.currentHash,
|
|
1882
|
+
): WorkflowSettingsScopeRecord {
|
|
1883
|
+
return {
|
|
1884
|
+
scopeId: row.scopeId,
|
|
1885
|
+
originRunId: row.originRunId,
|
|
1886
|
+
activeRunId: row.activeRunId,
|
|
1887
|
+
mountPath: row.mountPath,
|
|
1888
|
+
invocation: row.invocation,
|
|
1889
|
+
changeNumber,
|
|
1890
|
+
settings: this.state.readJson(settingsHash),
|
|
1891
|
+
settingsHash: settingsHash.toString("hex"),
|
|
1892
|
+
createdAt: new Date(row.createdAt).toISOString(),
|
|
1893
|
+
updatedAt: new Date(row.updatedAt).toISOString(),
|
|
1894
|
+
};
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
private settingsChangeRow(scopeId: string, requestId: string): SettingsChangeRow | undefined {
|
|
1898
|
+
const row = this.state.connection
|
|
1899
|
+
.prepare(
|
|
1900
|
+
`SELECT change_id AS changeId, scope_id AS scopeId, request_id AS requestId,
|
|
1901
|
+
change_number AS changeNumber, actor_type AS actorType, actor_id AS actorId,
|
|
1902
|
+
source_type AS sourceType, patch_hash AS patchHash,
|
|
1903
|
+
before_hash AS beforeHash, after_hash AS afterHash,
|
|
1904
|
+
accepted_at AS acceptedAt
|
|
1905
|
+
FROM workflow_setting_changes WHERE scope_id = ? AND request_id = ?`,
|
|
1906
|
+
)
|
|
1907
|
+
.get(scopeId, requestId);
|
|
1908
|
+
return isSettingsChangeRow(row) ? row : undefined;
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
private settingsChangeRecord(row: SettingsChangeRow): WorkflowSettingsChangeRecord {
|
|
1912
|
+
return {
|
|
1913
|
+
changeId: row.changeId,
|
|
1914
|
+
scopeId: row.scopeId,
|
|
1915
|
+
requestId: row.requestId,
|
|
1916
|
+
changeNumber: row.changeNumber,
|
|
1917
|
+
actor: {
|
|
1918
|
+
type: assertActorType(row.actorType),
|
|
1919
|
+
...(row.actorId !== null ? { id: row.actorId } : {}),
|
|
1920
|
+
},
|
|
1921
|
+
source: row.sourceType,
|
|
1922
|
+
patch: validateJsonPatch(this.state.readJson(row.patchHash)),
|
|
1923
|
+
beforeHash: row.beforeHash.toString("hex"),
|
|
1924
|
+
afterHash: row.afterHash.toString("hex"),
|
|
1925
|
+
acceptedAt: new Date(row.acceptedAt).toISOString(),
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
private assertSameSettingsChange(
|
|
1930
|
+
row: SettingsChangeRow,
|
|
1931
|
+
request: WorkflowSettingsChangeRequest,
|
|
1932
|
+
patchDigest: Buffer,
|
|
1933
|
+
): void {
|
|
1934
|
+
if (
|
|
1935
|
+
!row.patchHash.equals(patchDigest) ||
|
|
1936
|
+
row.actorType !== request.actor.type ||
|
|
1937
|
+
row.actorId !== (request.actor.id ?? null) ||
|
|
1938
|
+
row.sourceType !== request.source
|
|
1939
|
+
) {
|
|
1940
|
+
throw new Error(`Workflow settings request ID was reused with different content`);
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
private followUpQueueRow(runId: string): FollowUpQueueRow | undefined {
|
|
1945
|
+
const row = this.state.connection
|
|
1946
|
+
.prepare(
|
|
1947
|
+
`SELECT q.run_id AS runId, q.resource_id AS resourceId,
|
|
1948
|
+
q.origin_session_id AS originSessionId,
|
|
1949
|
+
q.presentation_state AS presentationState,
|
|
1950
|
+
q.presentation_entry_id AS presentationEntryId,
|
|
1951
|
+
q.presentation_assistant_entry_id AS presentationAssistantEntryId,
|
|
1952
|
+
q.presentation_reason_hash AS presentationReasonHash,
|
|
1953
|
+
r.revision
|
|
1954
|
+
FROM workflow_follow_up_queues q
|
|
1955
|
+
JOIN resources r ON r.resource_id = q.resource_id
|
|
1956
|
+
WHERE q.run_id = ?`,
|
|
1957
|
+
)
|
|
1958
|
+
.get(runId);
|
|
1959
|
+
return isFollowUpQueueRow(row) ? row : undefined;
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
private requireFollowUpQueueRow(runId: string): FollowUpQueueRow {
|
|
1963
|
+
const row = this.followUpQueueRow(runId);
|
|
1964
|
+
if (row === undefined) throw new Error(`Workflow follow-up queue not found: ${runId}`);
|
|
1965
|
+
return row;
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
private followUpRowByRequest(runId: string, requestId: string): FollowUpRow | undefined {
|
|
1969
|
+
const row = this.state.connection
|
|
1970
|
+
.prepare(`${FOLLOW_UP_ROW_SELECT} WHERE f.run_id = ? AND f.request_id = ?`)
|
|
1971
|
+
.get(runId, requestId);
|
|
1972
|
+
return isFollowUpRow(row) ? row : undefined;
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
private followUpRow(followUpId: string): FollowUpRow | undefined {
|
|
1976
|
+
const row = this.state.connection
|
|
1977
|
+
.prepare(`${FOLLOW_UP_ROW_SELECT} WHERE f.follow_up_id = ?`)
|
|
1978
|
+
.get(followUpId);
|
|
1979
|
+
return isFollowUpRow(row) ? row : undefined;
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
private requireFollowUpRow(followUpId: string): FollowUpRow {
|
|
1983
|
+
const row = this.followUpRow(followUpId);
|
|
1984
|
+
if (row === undefined) throw new Error(`Workflow follow-up not found: ${followUpId}`);
|
|
1985
|
+
return row;
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
private followUpRecord(row: FollowUpRow): WorkflowFollowUpRecord {
|
|
1989
|
+
return {
|
|
1990
|
+
followUpId: row.followUpId,
|
|
1991
|
+
runId: row.runId,
|
|
1992
|
+
requestId: row.requestId,
|
|
1993
|
+
order: row.orderNumber,
|
|
1994
|
+
targetSessionId: row.targetSessionId,
|
|
1995
|
+
actor: {
|
|
1996
|
+
type: assertActorType(row.actorType),
|
|
1997
|
+
...(row.actorId !== null ? { id: row.actorId } : {}),
|
|
1998
|
+
},
|
|
1999
|
+
source: row.sourceType,
|
|
2000
|
+
prompt: this.readText(row.promptHash),
|
|
2001
|
+
state: assertFollowUpState(row.status),
|
|
2002
|
+
...(row.sessionEntryId !== null ? { sessionEntryId: row.sessionEntryId } : {}),
|
|
2003
|
+
...(row.reasonHash !== null ? { reason: this.readText(row.reasonHash) } : {}),
|
|
2004
|
+
createdAt: new Date(row.createdAt).toISOString(),
|
|
2005
|
+
updatedAt: new Date(row.updatedAt).toISOString(),
|
|
2006
|
+
...(row.sentAt !== null ? { sentAt: new Date(row.sentAt).toISOString() } : {}),
|
|
2007
|
+
};
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
private assertSameFollowUp(row: FollowUpRow, request: WorkflowQueueFollowUpRequest): void {
|
|
2011
|
+
const promptHash = createHash("sha256").update(request.prompt).digest();
|
|
2012
|
+
if (
|
|
2013
|
+
!row.promptHash.equals(promptHash) ||
|
|
2014
|
+
row.targetSessionId !== request.targetSessionId ||
|
|
2015
|
+
row.actorType !== request.actor.type ||
|
|
2016
|
+
row.actorId !== (request.actor.id ?? null) ||
|
|
2017
|
+
row.sourceType !== request.source
|
|
2018
|
+
) {
|
|
2019
|
+
throw new Error("Workflow follow-up request ID was reused with different content");
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
private assertFollowUpRemovalAuthority(
|
|
2024
|
+
row: FollowUpRow,
|
|
2025
|
+
actor: MutationActor,
|
|
2026
|
+
source: string,
|
|
2027
|
+
): void {
|
|
2028
|
+
if (actor.type === "human") return;
|
|
2029
|
+
if (
|
|
2030
|
+
(actor.type === "session" || actor.type === "controller") &&
|
|
2031
|
+
row.actorType === actor.type &&
|
|
2032
|
+
row.actorId === (actor.id ?? null) &&
|
|
2033
|
+
row.sourceType === source
|
|
2034
|
+
) {
|
|
2035
|
+
return;
|
|
2036
|
+
}
|
|
2037
|
+
throw new Error("Workflow follow-up can be removed only by its source or a verified human");
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
private hasLiveLease(resourceId: string): boolean {
|
|
2041
|
+
const row = this.state.connection
|
|
2042
|
+
.prepare("SELECT expires_at AS expiresAt FROM leases WHERE resource_id = ?")
|
|
2043
|
+
.get(resourceId);
|
|
2044
|
+
return isLeaseExpiryRow(row) && row.expiresAt !== null && row.expiresAt > Date.now();
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
private nextFollowUpOrder(runId: string): number {
|
|
2048
|
+
const row = this.state.connection
|
|
2049
|
+
.prepare(
|
|
2050
|
+
`SELECT COALESCE(MAX(order_number), 0) + 1 AS nextOrder
|
|
2051
|
+
FROM workflow_follow_ups WHERE run_id = ?`,
|
|
2052
|
+
)
|
|
2053
|
+
.get(runId);
|
|
2054
|
+
if (!isNextOrderRow(row)) throw new Error("Could not allocate workflow follow-up order");
|
|
2055
|
+
return row.nextOrder;
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
private assertSettingsRouteCurrent(
|
|
2059
|
+
event: WorkflowTraceEventDraft,
|
|
2060
|
+
snapshot: WorkflowDefinitionSnapshot,
|
|
2061
|
+
): void {
|
|
2062
|
+
if (event.type !== "node_finished" || event.nodeId === undefined) return;
|
|
2063
|
+
if (snapshot.nodes[event.nodeId]?.settingsRoute !== true) return;
|
|
2064
|
+
const scopeId = event.payload.settingsScopeId;
|
|
2065
|
+
const changeNumber = event.payload.settingsChangeNumber;
|
|
2066
|
+
if (typeof scopeId !== "string" || typeof changeNumber !== "number") {
|
|
2067
|
+
throw new Error(`Settings route ${event.nodeId} has no saved settings binding`);
|
|
2068
|
+
}
|
|
2069
|
+
const scope = this.requireSettingsScopeRow(scopeId);
|
|
2070
|
+
if (scope.revision !== changeNumber) {
|
|
2071
|
+
throw new StaleResourceError(
|
|
2072
|
+
`Settings route ${event.nodeId} used change ${changeNumber}, current change is ${scope.revision}`,
|
|
2073
|
+
);
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
private transitionFollowUpsForRunState(
|
|
2078
|
+
state: WorkflowRunState,
|
|
2079
|
+
event: WorkflowTraceEventDraft,
|
|
2080
|
+
now: number,
|
|
2081
|
+
): void {
|
|
2082
|
+
if (state.status === "running" || state.status === "waiting") return;
|
|
2083
|
+
const queue = this.followUpQueueRow(state.runId);
|
|
2084
|
+
if (queue === undefined || queue.presentationState !== "none") return;
|
|
2085
|
+
let presentationState: WorkflowPresentationState = "not-needed";
|
|
2086
|
+
let followUpState: WorkflowFollowUpState = "cancelled";
|
|
2087
|
+
let reasonHash: Buffer | null = null;
|
|
2088
|
+
if (state.status === "completed") {
|
|
2089
|
+
const required = event.payload.presentationRequired === true;
|
|
2090
|
+
presentationState = required ? "pending" : "not-needed";
|
|
2091
|
+
followUpState = required ? "pending_presentation" : "ready";
|
|
2092
|
+
} else {
|
|
2093
|
+
reasonHash = this.state.putText(
|
|
2094
|
+
state.error ?? `Workflow ended with status ${state.status}`,
|
|
2095
|
+
now,
|
|
2096
|
+
);
|
|
2097
|
+
}
|
|
2098
|
+
this.state.connection
|
|
2099
|
+
.prepare(
|
|
2100
|
+
`UPDATE workflow_follow_up_queues
|
|
2101
|
+
SET presentation_state = ?, presentation_updated_at = ?, updated_at = ?
|
|
2102
|
+
WHERE run_id = ? AND presentation_state = 'none'`,
|
|
2103
|
+
)
|
|
2104
|
+
.run(presentationState, now, now, state.runId);
|
|
2105
|
+
this.state.connection
|
|
2106
|
+
.prepare(
|
|
2107
|
+
`UPDATE workflow_follow_ups
|
|
2108
|
+
SET status = ?, reason_hash = ?, updated_at = ?
|
|
2109
|
+
WHERE run_id = ? AND status IN ('queued', 'pending_presentation', 'ready')`,
|
|
2110
|
+
)
|
|
2111
|
+
.run(followUpState, reasonHash, now, state.runId);
|
|
2112
|
+
const revision = this.requireResourceRevision(queue.resourceId);
|
|
2113
|
+
this.bumpResource(queue.resourceId, revision, now);
|
|
2114
|
+
const payloadHash = this.state.putJson(
|
|
2115
|
+
{ runId: state.runId, state: presentationState, followUps: followUpState },
|
|
2116
|
+
now,
|
|
2117
|
+
);
|
|
2118
|
+
insertGenericEvent(
|
|
2119
|
+
this.state,
|
|
2120
|
+
queue.resourceId,
|
|
2121
|
+
revision + 1,
|
|
2122
|
+
"follow-up.run-finished",
|
|
2123
|
+
"system",
|
|
2124
|
+
null,
|
|
2125
|
+
payloadHash,
|
|
2126
|
+
now,
|
|
2127
|
+
);
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
private syncNodeAttempts(
|
|
2131
|
+
state: WorkflowRunState,
|
|
2132
|
+
snapshot: WorkflowDefinitionSnapshot,
|
|
2133
|
+
now: number,
|
|
2134
|
+
): void {
|
|
2135
|
+
for (const step of state.steps.slice(state.carriedStepCount ?? 0)) {
|
|
2136
|
+
const promptEntryId = this.findAttemptPromptEntry(state.runId, step.attemptId);
|
|
2137
|
+
const promptHash =
|
|
2138
|
+
promptEntryId === undefined && step.prompt !== null
|
|
2139
|
+
? this.state.putText(step.prompt, now)
|
|
2140
|
+
: null;
|
|
2141
|
+
const receipt = attemptReceipt(step);
|
|
2142
|
+
const receiptHash =
|
|
2143
|
+
Object.keys(receipt).length === 0 ? null : this.state.putJson(receipt, now);
|
|
2144
|
+
const outputHash = step.output === undefined ? null : this.state.putJson(step.output, now);
|
|
2145
|
+
const errorHash = step.error === undefined ? null : this.state.putText(step.error, now);
|
|
2146
|
+
const existing = this.state.connection
|
|
2147
|
+
.prepare("SELECT attempt_id AS attemptId FROM node_attempts WHERE attempt_id = ?")
|
|
2148
|
+
.get(step.attemptId);
|
|
2149
|
+
if (existing === undefined) {
|
|
2150
|
+
const attemptNumber = this.nextAttemptNumber(state.runId, step.nodeId);
|
|
2151
|
+
this.state.connection
|
|
2152
|
+
.prepare(
|
|
2153
|
+
`INSERT INTO node_attempts(
|
|
2154
|
+
attempt_id, run_id, node_id, attempt_number, node_type, status,
|
|
2155
|
+
prompt_hash, output_hash, receipt_hash, error_hash,
|
|
2156
|
+
settings_scope_id, settings_change_number, settings_hash,
|
|
2157
|
+
started_at, finished_at, created_at, updated_at
|
|
2158
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2159
|
+
)
|
|
2160
|
+
.run(
|
|
2161
|
+
step.attemptId,
|
|
2162
|
+
state.runId,
|
|
2163
|
+
step.nodeId,
|
|
2164
|
+
attemptNumber,
|
|
2165
|
+
step.nodeType,
|
|
2166
|
+
outcomeStatus(step.outcome),
|
|
2167
|
+
promptHash,
|
|
2168
|
+
outputHash,
|
|
2169
|
+
receiptHash,
|
|
2170
|
+
errorHash,
|
|
2171
|
+
step.settingsScopeId ?? null,
|
|
2172
|
+
step.settingsChangeNumber ?? null,
|
|
2173
|
+
step.settingsHash === undefined ? null : Buffer.from(step.settingsHash, "hex"),
|
|
2174
|
+
Date.parse(step.startedAt),
|
|
2175
|
+
Date.parse(step.finishedAt),
|
|
2176
|
+
Date.parse(step.startedAt),
|
|
2177
|
+
now,
|
|
2178
|
+
);
|
|
2179
|
+
} else {
|
|
2180
|
+
this.state.connection
|
|
2181
|
+
.prepare(
|
|
2182
|
+
`UPDATE node_attempts
|
|
2183
|
+
SET status = ?, prompt_hash = ?, output_hash = ?, receipt_hash = ?, error_hash = ?,
|
|
2184
|
+
settings_scope_id = ?, settings_change_number = ?,
|
|
2185
|
+
settings_hash = ?, finished_at = ?, updated_at = ?
|
|
2186
|
+
WHERE attempt_id = ?`,
|
|
2187
|
+
)
|
|
2188
|
+
.run(
|
|
2189
|
+
outcomeStatus(step.outcome),
|
|
2190
|
+
promptHash,
|
|
2191
|
+
outputHash,
|
|
2192
|
+
receiptHash,
|
|
2193
|
+
errorHash,
|
|
2194
|
+
step.settingsScopeId ?? null,
|
|
2195
|
+
step.settingsChangeNumber ?? null,
|
|
2196
|
+
step.settingsHash === undefined ? null : Buffer.from(step.settingsHash, "hex"),
|
|
2197
|
+
Date.parse(step.finishedAt),
|
|
2198
|
+
now,
|
|
2199
|
+
step.attemptId,
|
|
2200
|
+
);
|
|
2201
|
+
}
|
|
2202
|
+
if (this.syncAttemptEntries(state.runId, step, promptEntryId)) {
|
|
2203
|
+
this.state.connection
|
|
2204
|
+
.prepare("UPDATE node_attempts SET output_hash = NULL WHERE attempt_id = ?")
|
|
2205
|
+
.run(step.attemptId);
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
if (state.currentAttemptId !== undefined && state.currentNode !== undefined) {
|
|
2209
|
+
this.ensureAttempt(state, state.currentNode, state.currentAttemptId, now, snapshot);
|
|
2210
|
+
}
|
|
2211
|
+
this.syncRunSteps(state, now);
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
private syncRunSteps(state: WorkflowRunState, now: number): void {
|
|
2215
|
+
const existingRows = this.state.connection
|
|
2216
|
+
.prepare(
|
|
2217
|
+
`SELECT step_index AS stepIndex, attempt_id AS attemptId
|
|
2218
|
+
FROM run_steps WHERE run_id = ? ORDER BY step_index`,
|
|
2219
|
+
)
|
|
2220
|
+
.all(state.runId)
|
|
2221
|
+
.filter(isRunStepIdentityRow);
|
|
2222
|
+
if (existingRows.length > state.steps.length) {
|
|
2223
|
+
throw new Error(`Workflow run steps cannot shrink: ${state.runId}`);
|
|
2224
|
+
}
|
|
2225
|
+
for (const existing of existingRows) {
|
|
2226
|
+
if (state.steps[existing.stepIndex]?.attemptId !== existing.attemptId) {
|
|
2227
|
+
throw new Error(`Workflow run step history changed at index ${existing.stepIndex}`);
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
for (let stepIndex = existingRows.length; stepIndex < state.steps.length; stepIndex += 1) {
|
|
2231
|
+
const step = state.steps[stepIndex];
|
|
2232
|
+
/* istanbul ignore if -- array index follows a checked bound */
|
|
2233
|
+
if (step === undefined) throw new Error("Workflow run step became unavailable");
|
|
2234
|
+
const attemptOutput = this.readAttemptOutput(step.attemptId);
|
|
2235
|
+
const carried = stepIndex < (state.carriedStepCount ?? 0);
|
|
2236
|
+
const outputOverrideHash =
|
|
2237
|
+
carried && canonicalJson(attemptOutput) !== canonicalJson(step.output)
|
|
1102
2238
|
? this.state.putJson(step.output, now)
|
|
1103
2239
|
: null;
|
|
1104
2240
|
this.state.connection
|
|
@@ -1110,6 +2246,100 @@ export class WorkflowRunStore {
|
|
|
1110
2246
|
}
|
|
1111
2247
|
}
|
|
1112
2248
|
|
|
2249
|
+
private syncAttemptEntries(
|
|
2250
|
+
runId: string,
|
|
2251
|
+
step: WorkflowStepRecord,
|
|
2252
|
+
promptEntryId: string | undefined,
|
|
2253
|
+
): boolean {
|
|
2254
|
+
const links: Array<["prompt" | "response" | "first" | "last", string]> = [];
|
|
2255
|
+
if (promptEntryId !== undefined) links.push(["prompt", promptEntryId]);
|
|
2256
|
+
if (step.conversation !== undefined) {
|
|
2257
|
+
links.push(
|
|
2258
|
+
["first", step.conversation.firstEntryId],
|
|
2259
|
+
["last", step.conversation.lastEntryId],
|
|
2260
|
+
);
|
|
2261
|
+
}
|
|
2262
|
+
if (step.assistantMessage?.entryId !== undefined) {
|
|
2263
|
+
links.push(["response", step.assistantMessage.entryId]);
|
|
2264
|
+
}
|
|
2265
|
+
let responseLinked = false;
|
|
2266
|
+
for (const [role, entryId] of links) {
|
|
2267
|
+
const row = this.state.connection
|
|
2268
|
+
.prepare(
|
|
2269
|
+
`SELECT e.segment_id AS segmentId, e.entry_hash AS entryHash
|
|
2270
|
+
FROM session_entries e
|
|
2271
|
+
JOIN session_segments s ON s.segment_id = e.segment_id
|
|
2272
|
+
WHERE s.run_id = ? AND e.entry_id = ?
|
|
2273
|
+
ORDER BY e.recorded_at DESC LIMIT 1`,
|
|
2274
|
+
)
|
|
2275
|
+
.get(runId, entryId);
|
|
2276
|
+
if (!isSegmentIdentityRow(row)) continue;
|
|
2277
|
+
if (role === "response") {
|
|
2278
|
+
const output = assistantOutputFromEntry(
|
|
2279
|
+
this.state.readJson(row.entryHash),
|
|
2280
|
+
step.assistantMessage?.maxChars,
|
|
2281
|
+
);
|
|
2282
|
+
if (
|
|
2283
|
+
canonicalJson(output) !== canonicalJson(step.output) ||
|
|
2284
|
+
createHash("sha256").update(output).digest("hex") !== step.assistantMessage?.sha256
|
|
2285
|
+
) {
|
|
2286
|
+
throw new Error(`Assistant response entry conflicts with attempt ${step.attemptId}`);
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
this.state.connection
|
|
2290
|
+
.prepare(
|
|
2291
|
+
`INSERT INTO attempt_entries(attempt_id, role, segment_id, entry_id)
|
|
2292
|
+
VALUES (?, ?, ?, ?)
|
|
2293
|
+
ON CONFLICT(attempt_id, role) DO UPDATE SET
|
|
2294
|
+
segment_id = excluded.segment_id, entry_id = excluded.entry_id`,
|
|
2295
|
+
)
|
|
2296
|
+
.run(step.attemptId, role, row.segmentId, entryId);
|
|
2297
|
+
if (role === "response") responseLinked = true;
|
|
2298
|
+
}
|
|
2299
|
+
return responseLinked;
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
private findAttemptPromptEntry(runId: string, attemptId: string): string | undefined {
|
|
2303
|
+
const row = this.state.connection
|
|
2304
|
+
.prepare(
|
|
2305
|
+
`SELECT e.entry_id AS entryId
|
|
2306
|
+
FROM session_entries e
|
|
2307
|
+
JOIN session_segments s ON s.segment_id = e.segment_id
|
|
2308
|
+
JOIN blobs b ON b.blob_hash = e.entry_hash
|
|
2309
|
+
WHERE s.run_id = ?
|
|
2310
|
+
AND json_extract(CAST(b.content AS TEXT), '$.customType') = 'pi-workflows-agent-step'
|
|
2311
|
+
AND json_extract(CAST(b.content AS TEXT), '$.details.contract.attemptId') = ?
|
|
2312
|
+
ORDER BY e.recorded_at DESC LIMIT 1`,
|
|
2313
|
+
)
|
|
2314
|
+
.get(runId, attemptId);
|
|
2315
|
+
return isEntryIdentityRow(row) ? row.entryId : undefined;
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
private readAttemptOutput(attemptId: string): unknown {
|
|
2319
|
+
const row = this.state.connection
|
|
2320
|
+
.prepare(
|
|
2321
|
+
`SELECT a.output_hash AS outputHash, a.receipt_hash AS receiptHash,
|
|
2322
|
+
e.entry_hash AS responseEntryHash
|
|
2323
|
+
FROM node_attempts a
|
|
2324
|
+
LEFT JOIN attempt_entries l ON l.attempt_id = a.attempt_id AND l.role = 'response'
|
|
2325
|
+
LEFT JOIN session_entries e
|
|
2326
|
+
ON e.segment_id = l.segment_id AND e.entry_id = l.entry_id
|
|
2327
|
+
WHERE a.attempt_id = ?`,
|
|
2328
|
+
)
|
|
2329
|
+
.get(attemptId);
|
|
2330
|
+
if (!isAttemptValueRow(row)) {
|
|
2331
|
+
throw new Error(`Workflow node attempt is missing: ${attemptId}`);
|
|
2332
|
+
}
|
|
2333
|
+
if (row.outputHash !== null) return this.state.readJson(row.outputHash);
|
|
2334
|
+
if (row.responseEntryHash === null) return null;
|
|
2335
|
+
const receipt =
|
|
2336
|
+
row.receiptHash === null ? {} : this.readJsonAs<StoredAttemptReceipt>(row.receiptHash);
|
|
2337
|
+
return assistantOutputFromEntry(
|
|
2338
|
+
this.state.readJson(row.responseEntryHash),
|
|
2339
|
+
receipt.assistantMessage?.maxChars,
|
|
2340
|
+
);
|
|
2341
|
+
}
|
|
2342
|
+
|
|
1113
2343
|
private ensureAttempt(
|
|
1114
2344
|
state: WorkflowRunState,
|
|
1115
2345
|
nodeId: string,
|
|
@@ -1123,19 +2353,41 @@ export class WorkflowRunStore {
|
|
|
1123
2353
|
const status = state.status === "waiting" ? "waiting" : "running";
|
|
1124
2354
|
if (existing !== undefined) {
|
|
1125
2355
|
this.state.connection
|
|
1126
|
-
.prepare(
|
|
1127
|
-
|
|
2356
|
+
.prepare(
|
|
2357
|
+
`UPDATE node_attempts
|
|
2358
|
+
SET status = ?, settings_scope_id = ?, settings_change_number = ?,
|
|
2359
|
+
settings_hash = ?, updated_at = ?
|
|
2360
|
+
WHERE attempt_id = ?`,
|
|
2361
|
+
)
|
|
2362
|
+
.run(
|
|
2363
|
+
status,
|
|
2364
|
+
state.currentSettingsScopeId ?? null,
|
|
2365
|
+
state.currentSettingsChangeNumber ?? null,
|
|
2366
|
+
state.currentSettingsHash === undefined
|
|
2367
|
+
? null
|
|
2368
|
+
: Buffer.from(state.currentSettingsHash, "hex"),
|
|
2369
|
+
now,
|
|
2370
|
+
attemptId,
|
|
2371
|
+
);
|
|
1128
2372
|
return;
|
|
1129
2373
|
}
|
|
1130
2374
|
const definition =
|
|
1131
2375
|
snapshot ?? this.readDefinition(this.requireRunRow(state.runId).definitionHash);
|
|
1132
2376
|
const nodeType = definition.nodes[nodeId]?.nodeType ?? "agent";
|
|
2377
|
+
this.state.connection
|
|
2378
|
+
.prepare(
|
|
2379
|
+
`UPDATE node_attempts
|
|
2380
|
+
SET status = 'cancelled', finished_at = COALESCE(finished_at, ?), updated_at = ?
|
|
2381
|
+
WHERE run_id = ? AND status = 'interrupted'`,
|
|
2382
|
+
)
|
|
2383
|
+
.run(now, now, state.runId);
|
|
1133
2384
|
this.state.connection
|
|
1134
2385
|
.prepare(
|
|
1135
2386
|
`INSERT INTO node_attempts(
|
|
1136
2387
|
attempt_id, run_id, node_id, attempt_number, node_type, status,
|
|
2388
|
+
settings_scope_id, settings_change_number, settings_hash,
|
|
1137
2389
|
started_at, created_at, updated_at
|
|
1138
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2390
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1139
2391
|
)
|
|
1140
2392
|
.run(
|
|
1141
2393
|
attemptId,
|
|
@@ -1144,6 +2396,11 @@ export class WorkflowRunStore {
|
|
|
1144
2396
|
this.nextAttemptNumber(state.runId, nodeId),
|
|
1145
2397
|
nodeType,
|
|
1146
2398
|
status,
|
|
2399
|
+
state.currentSettingsScopeId ?? null,
|
|
2400
|
+
state.currentSettingsChangeNumber ?? null,
|
|
2401
|
+
state.currentSettingsHash === undefined
|
|
2402
|
+
? null
|
|
2403
|
+
: Buffer.from(state.currentSettingsHash, "hex"),
|
|
1147
2404
|
state.currentNodeStartedAt === undefined ? now : Date.parse(state.currentNodeStartedAt),
|
|
1148
2405
|
now,
|
|
1149
2406
|
now,
|
|
@@ -1174,20 +2431,59 @@ export class WorkflowRunStore {
|
|
|
1174
2431
|
return row.updateSeq;
|
|
1175
2432
|
}
|
|
1176
2433
|
|
|
2434
|
+
private insertRunSources(
|
|
2435
|
+
runId: string,
|
|
2436
|
+
root: { type: "builtin" | "file"; ref: string; revision: string },
|
|
2437
|
+
mounted: WorkflowMountedSource[],
|
|
2438
|
+
): void {
|
|
2439
|
+
const insert = this.state.connection.prepare(
|
|
2440
|
+
`INSERT INTO run_sources(run_id, mount_path, source_type, source_ref, source_revision)
|
|
2441
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
2442
|
+
);
|
|
2443
|
+
insert.run(runId, "", root.type, root.ref, root.revision);
|
|
2444
|
+
for (const item of mounted) {
|
|
2445
|
+
const source = sourceParts(item.source);
|
|
2446
|
+
insert.run(runId, item.mountPath.join("/"), source.type, source.ref, source.revision);
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
private syncContinuationIdentity(state: WorkflowRunState): void {
|
|
2451
|
+
if (state.parentRunId === undefined || state.humanDecision === undefined) return;
|
|
2452
|
+
const createdAt = Date.parse(state.humanDecision.acceptedAt);
|
|
2453
|
+
this.state.connection
|
|
2454
|
+
.prepare(
|
|
2455
|
+
`INSERT INTO continuations(
|
|
2456
|
+
decision_id, parent_run_id, continuation_run_id, created_at
|
|
2457
|
+
) VALUES (?, ?, ?, ?)
|
|
2458
|
+
ON CONFLICT(decision_id) DO NOTHING`,
|
|
2459
|
+
)
|
|
2460
|
+
.run(state.humanDecision.decisionId, state.parentRunId, state.runId, createdAt);
|
|
2461
|
+
const row = this.state.connection
|
|
2462
|
+
.prepare(
|
|
2463
|
+
`SELECT parent_run_id AS parentRunId, continuation_run_id AS continuationRunId,
|
|
2464
|
+
created_at AS createdAt
|
|
2465
|
+
FROM continuations WHERE decision_id = ?`,
|
|
2466
|
+
)
|
|
2467
|
+
.get(state.humanDecision.decisionId);
|
|
2468
|
+
if (
|
|
2469
|
+
!isContinuationIdentityRow(row) ||
|
|
2470
|
+
row.parentRunId !== state.parentRunId ||
|
|
2471
|
+
row.continuationRunId !== state.runId ||
|
|
2472
|
+
row.createdAt !== createdAt
|
|
2473
|
+
) {
|
|
2474
|
+
throw new Error("Immutable human decision continuation conflicts");
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
|
|
1177
2478
|
private readRunRow(runId: string): RunRow | undefined {
|
|
1178
2479
|
const row = this.state.connection
|
|
1179
2480
|
.prepare(
|
|
1180
2481
|
`SELECT r.run_id AS runId, r.resource_id AS resourceId,
|
|
1181
2482
|
d.definition_hash AS definitionHash, r.definition_digest AS definitionDigest,
|
|
1182
|
-
d.workflow_name AS workflowRef, r.
|
|
1183
|
-
r.workflow_source_hash AS workflowSourceHash,
|
|
1184
|
-
r.workflow_sources_hash AS workflowSourcesHash, r.parent_run_id AS parentRunId,
|
|
2483
|
+
d.workflow_name AS workflowRef, r.parent_run_id AS parentRunId,
|
|
1185
2484
|
r.title, r.status, r.paused, r.status_detail AS statusDetail,
|
|
1186
|
-
r.input_hash AS inputHash, r.
|
|
1187
|
-
r.
|
|
1188
|
-
r.carried_step_count AS carriedStepCount, r.current_node AS currentNode,
|
|
1189
|
-
r.current_attempt_id AS currentAttemptId,
|
|
1190
|
-
r.current_node_started_at AS currentNodeStartedAt, r.waiting_on AS waitingOn,
|
|
2485
|
+
r.input_hash AS inputHash, r.final_output_hash AS finalOutputHash,
|
|
2486
|
+
r.error_hash AS errorHash,
|
|
1191
2487
|
r.created_at AS createdAt, r.updated_at AS updatedAt, r.finished_at AS finishedAt
|
|
1192
2488
|
FROM runs r
|
|
1193
2489
|
JOIN workflow_definitions d ON d.definition_digest = r.definition_digest
|
|
@@ -1205,6 +2501,10 @@ export class WorkflowRunStore {
|
|
|
1205
2501
|
|
|
1206
2502
|
private readRunState(row: RunRow, snapshot: WorkflowDefinitionSnapshot): WorkflowRunState {
|
|
1207
2503
|
const steps = this.readSteps(row.runId);
|
|
2504
|
+
const sources = this.readRunSources(row.runId, snapshot);
|
|
2505
|
+
const carriedStepCount = this.carriedStepCount(row.runId);
|
|
2506
|
+
const activeAttempt = row.status === "running" ? this.readActiveAttempt(row.runId) : undefined;
|
|
2507
|
+
const humanDecision = this.readHumanDecisionReceipt(row.runId);
|
|
1208
2508
|
const outputs: Record<string, unknown> = {};
|
|
1209
2509
|
const results: Record<string, WorkflowNodeResult> = {};
|
|
1210
2510
|
for (const step of steps) {
|
|
@@ -1229,15 +2529,11 @@ export class WorkflowRunStore {
|
|
|
1229
2529
|
runId: row.runId,
|
|
1230
2530
|
workflowName: row.workflowRef,
|
|
1231
2531
|
...(row.parentRunId === null ? {} : { parentRunId: row.parentRunId }),
|
|
1232
|
-
...(
|
|
2532
|
+
...(carriedStepCount === 0 ? {} : { carriedStepCount }),
|
|
1233
2533
|
...(row.title === null ? {} : { runTitle: row.title }),
|
|
1234
|
-
...(
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
...(row.workflowSourcesHash === null
|
|
1238
|
-
? {}
|
|
1239
|
-
: { workflowSources: this.readJsonAs(row.workflowSourcesHash) }),
|
|
1240
|
-
...(row.workflowSourcesHash !== null || snapshot.composition?.mounts.length
|
|
2534
|
+
...(sources.root === undefined ? {} : { workflowSource: sources.root }),
|
|
2535
|
+
...(sources.mounted.length === 0 ? {} : { workflowSources: sources.mounted }),
|
|
2536
|
+
...(sources.mounted.length !== 0 || snapshot.composition?.mounts.length
|
|
1241
2537
|
? { definitionDigest: `sha256:${row.definitionDigest.toString("hex")}` }
|
|
1242
2538
|
: {}),
|
|
1243
2539
|
startedAt: new Date(row.createdAt).toISOString(),
|
|
@@ -1249,17 +2545,22 @@ export class WorkflowRunStore {
|
|
|
1249
2545
|
results,
|
|
1250
2546
|
steps,
|
|
1251
2547
|
...this.readUpdates(row.runId),
|
|
1252
|
-
...(
|
|
1253
|
-
...(
|
|
1254
|
-
...(
|
|
2548
|
+
...(activeAttempt === undefined ? {} : { currentNode: activeAttempt.nodeId }),
|
|
2549
|
+
...(activeAttempt === undefined ? {} : { currentAttemptId: activeAttempt.attemptId }),
|
|
2550
|
+
...(activeAttempt?.startedAt === null || activeAttempt === undefined
|
|
1255
2551
|
? {}
|
|
1256
|
-
: { currentNodeStartedAt: new Date(
|
|
2552
|
+
: { currentNodeStartedAt: new Date(activeAttempt.startedAt).toISOString() }),
|
|
2553
|
+
...savedCurrentSettingsBinding(
|
|
2554
|
+
activeAttempt?.settingsScopeId ?? null,
|
|
2555
|
+
activeAttempt?.settingsChangeNumber ?? null,
|
|
2556
|
+
activeAttempt?.settingsHash ?? null,
|
|
2557
|
+
),
|
|
1257
2558
|
...(row.statusDetail === null ? {} : { statusDetail: row.statusDetail }),
|
|
1258
|
-
...(
|
|
1259
|
-
? {}
|
|
1260
|
-
: { humanDecision: this.readJsonAs(row.humanDecisionHash) }),
|
|
2559
|
+
...(humanDecision === undefined ? {} : { humanDecision }),
|
|
1261
2560
|
...(row.paused === 0 ? {} : { paused: true }),
|
|
1262
|
-
...(row.
|
|
2561
|
+
...(row.status === "waiting" && steps.at(-1) !== undefined
|
|
2562
|
+
? { waitingOn: steps.at(-1)?.nodeId as string }
|
|
2563
|
+
: {}),
|
|
1263
2564
|
...(row.finalOutputHash === null
|
|
1264
2565
|
? {}
|
|
1265
2566
|
: { finalOutput: this.state.readJson(row.finalOutputHash) }),
|
|
@@ -1271,12 +2572,33 @@ export class WorkflowRunStore {
|
|
|
1271
2572
|
const rows = this.state.connection
|
|
1272
2573
|
.prepare(
|
|
1273
2574
|
`SELECT s.step_index AS stepIndex, a.attempt_id AS attemptId, a.node_id AS nodeId,
|
|
1274
|
-
a.node_type AS nodeType, a.status,
|
|
1275
|
-
a.
|
|
1276
|
-
|
|
2575
|
+
a.node_type AS nodeType, a.status,
|
|
2576
|
+
a.prompt_hash AS promptHash, a.output_hash AS outputHash,
|
|
2577
|
+
s.output_override_hash AS outputOverrideHash,
|
|
2578
|
+
a.receipt_hash AS receiptHash, a.error_hash AS errorHash,
|
|
2579
|
+
prompt_entry.entry_hash AS promptEntryHash,
|
|
2580
|
+
response_entry.entry_hash AS responseEntryHash,
|
|
2581
|
+
first_link.entry_id AS firstEntryId, last_link.entry_id AS lastEntryId,
|
|
2582
|
+
a.settings_scope_id AS settingsScopeId,
|
|
2583
|
+
a.settings_change_number AS settingsChangeNumber,
|
|
2584
|
+
a.settings_hash AS settingsHash,
|
|
1277
2585
|
a.started_at AS startedAt, a.finished_at AS finishedAt
|
|
1278
2586
|
FROM run_steps s
|
|
1279
2587
|
JOIN node_attempts a ON a.attempt_id = s.attempt_id
|
|
2588
|
+
LEFT JOIN attempt_entries prompt_link
|
|
2589
|
+
ON prompt_link.attempt_id = a.attempt_id AND prompt_link.role = 'prompt'
|
|
2590
|
+
LEFT JOIN session_entries prompt_entry
|
|
2591
|
+
ON prompt_entry.segment_id = prompt_link.segment_id
|
|
2592
|
+
AND prompt_entry.entry_id = prompt_link.entry_id
|
|
2593
|
+
LEFT JOIN attempt_entries response_link
|
|
2594
|
+
ON response_link.attempt_id = a.attempt_id AND response_link.role = 'response'
|
|
2595
|
+
LEFT JOIN session_entries response_entry
|
|
2596
|
+
ON response_entry.segment_id = response_link.segment_id
|
|
2597
|
+
AND response_entry.entry_id = response_link.entry_id
|
|
2598
|
+
LEFT JOIN attempt_entries first_link
|
|
2599
|
+
ON first_link.attempt_id = a.attempt_id AND first_link.role = 'first'
|
|
2600
|
+
LEFT JOIN attempt_entries last_link
|
|
2601
|
+
ON last_link.attempt_id = a.attempt_id AND last_link.role = 'last'
|
|
1280
2602
|
WHERE s.run_id = ? ORDER BY s.step_index`,
|
|
1281
2603
|
)
|
|
1282
2604
|
.all(runId)
|
|
@@ -1284,14 +2606,29 @@ export class WorkflowRunStore {
|
|
|
1284
2606
|
return rows.map((row, index) => {
|
|
1285
2607
|
if (row.stepIndex !== index)
|
|
1286
2608
|
throw new Error(`Workflow run step sequence has a gap: ${runId}`);
|
|
1287
|
-
const
|
|
1288
|
-
row.
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
2609
|
+
const receipt =
|
|
2610
|
+
row.receiptHash === null ? {} : this.readJsonAs<StoredAttemptReceipt>(row.receiptHash);
|
|
2611
|
+
const prompt =
|
|
2612
|
+
row.promptEntryHash !== null
|
|
2613
|
+
? promptFromEntry(this.state.readJson(row.promptEntryHash))
|
|
2614
|
+
: row.promptHash === null
|
|
2615
|
+
? null
|
|
2616
|
+
: this.readText(row.promptHash);
|
|
1292
2617
|
const outputHash = row.outputOverrideHash ?? row.outputHash;
|
|
1293
|
-
const output =
|
|
2618
|
+
const output =
|
|
2619
|
+
outputHash !== null
|
|
2620
|
+
? this.state.readJson(outputHash)
|
|
2621
|
+
: row.responseEntryHash === null
|
|
2622
|
+
? null
|
|
2623
|
+
: assistantOutputFromEntry(
|
|
2624
|
+
this.state.readJson(row.responseEntryHash),
|
|
2625
|
+
receipt.assistantMessage?.maxChars,
|
|
2626
|
+
);
|
|
1294
2627
|
const error = row.errorHash === null ? undefined : this.readText(row.errorHash);
|
|
2628
|
+
const conversation =
|
|
2629
|
+
row.firstEntryId === null || row.lastEntryId === null
|
|
2630
|
+
? undefined
|
|
2631
|
+
: { firstEntryId: row.firstEntryId, lastEntryId: row.lastEntryId };
|
|
1295
2632
|
return {
|
|
1296
2633
|
attemptId: row.attemptId,
|
|
1297
2634
|
nodeId: row.nodeId,
|
|
@@ -1302,11 +2639,127 @@ export class WorkflowRunStore {
|
|
|
1302
2639
|
prompt,
|
|
1303
2640
|
output,
|
|
1304
2641
|
...(error === undefined ? {} : { error }),
|
|
1305
|
-
...
|
|
2642
|
+
...savedStepSettingsBinding(
|
|
2643
|
+
row.settingsScopeId,
|
|
2644
|
+
row.settingsChangeNumber,
|
|
2645
|
+
row.settingsHash,
|
|
2646
|
+
),
|
|
2647
|
+
...receipt,
|
|
2648
|
+
...(conversation === undefined ? {} : { conversation }),
|
|
1306
2649
|
};
|
|
1307
2650
|
});
|
|
1308
2651
|
}
|
|
1309
2652
|
|
|
2653
|
+
private readRunSources(
|
|
2654
|
+
runId: string,
|
|
2655
|
+
snapshot: WorkflowDefinitionSnapshot,
|
|
2656
|
+
): { root?: WorkflowSource; mounted: WorkflowMountedSource[] } {
|
|
2657
|
+
const rows = this.state.connection
|
|
2658
|
+
.prepare(
|
|
2659
|
+
`SELECT mount_path AS mountPath, source_type AS sourceType,
|
|
2660
|
+
source_ref AS sourceRef, source_revision AS sourceRevision
|
|
2661
|
+
FROM run_sources WHERE run_id = ? ORDER BY mount_path`,
|
|
2662
|
+
)
|
|
2663
|
+
.all(runId)
|
|
2664
|
+
.filter(isRunSourceRow);
|
|
2665
|
+
const rootRow = rows.find((row) => row.mountPath === "");
|
|
2666
|
+
const root =
|
|
2667
|
+
rootRow === undefined ||
|
|
2668
|
+
(rootRow.sourceType === "file" && rootRow.sourceRef.startsWith("inline:"))
|
|
2669
|
+
? undefined
|
|
2670
|
+
: workflowSourceFromRow(rootRow);
|
|
2671
|
+
const mountNames = new Map(
|
|
2672
|
+
(snapshot.composition?.mounts ?? []).map((mount) => [
|
|
2673
|
+
mount.mountPath.join("/"),
|
|
2674
|
+
mount.workflowName,
|
|
2675
|
+
]),
|
|
2676
|
+
);
|
|
2677
|
+
const mounted = rows
|
|
2678
|
+
.filter((row) => row.mountPath !== "")
|
|
2679
|
+
.map((row) => ({
|
|
2680
|
+
mountPath: row.mountPath.split("/"),
|
|
2681
|
+
workflowName: mountNames.get(row.mountPath) ?? row.mountPath,
|
|
2682
|
+
source: workflowSourceFromRow(row),
|
|
2683
|
+
}));
|
|
2684
|
+
return { ...(root === undefined ? {} : { root }), mounted };
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
private carriedStepCount(runId: string): number {
|
|
2688
|
+
const row = this.state.connection
|
|
2689
|
+
.prepare(
|
|
2690
|
+
`SELECT count(*) AS count
|
|
2691
|
+
FROM run_steps s JOIN node_attempts a ON a.attempt_id = s.attempt_id
|
|
2692
|
+
WHERE s.run_id = ? AND a.run_id <> s.run_id`,
|
|
2693
|
+
)
|
|
2694
|
+
.get(runId);
|
|
2695
|
+
return isCountRow(row) ? row.count : 0;
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
private readActiveAttempt(runId: string):
|
|
2699
|
+
| {
|
|
2700
|
+
attemptId: string;
|
|
2701
|
+
nodeId: string;
|
|
2702
|
+
startedAt: number | null;
|
|
2703
|
+
settingsScopeId: string | null;
|
|
2704
|
+
settingsChangeNumber: number | null;
|
|
2705
|
+
settingsHash: Buffer | null;
|
|
2706
|
+
}
|
|
2707
|
+
| undefined {
|
|
2708
|
+
const row = this.state.connection
|
|
2709
|
+
.prepare(
|
|
2710
|
+
`SELECT attempt_id AS attemptId, node_id AS nodeId, started_at AS startedAt,
|
|
2711
|
+
settings_scope_id AS settingsScopeId,
|
|
2712
|
+
settings_change_number AS settingsChangeNumber,
|
|
2713
|
+
settings_hash AS settingsHash
|
|
2714
|
+
FROM node_attempts
|
|
2715
|
+
WHERE run_id = ? AND status IN ('pending', 'running', 'waiting', 'interrupted')`,
|
|
2716
|
+
)
|
|
2717
|
+
.get(runId);
|
|
2718
|
+
return isActiveAttemptRow(row) ? row : undefined;
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
private readHumanDecisionReceipt(runId: string): HumanDecisionReceipt | undefined {
|
|
2722
|
+
const row = this.state.connection
|
|
2723
|
+
.prepare(
|
|
2724
|
+
`SELECT d.request_hash AS requestHash, r.response_hash AS responseHash
|
|
2725
|
+
FROM continuations c
|
|
2726
|
+
JOIN human_decisions d ON d.decision_id = c.decision_id
|
|
2727
|
+
JOIN human_decision_resolutions r ON r.decision_id = c.decision_id
|
|
2728
|
+
WHERE c.continuation_run_id = ? AND r.outcome = 'accepted'`,
|
|
2729
|
+
)
|
|
2730
|
+
.get(runId);
|
|
2731
|
+
if (!isDecisionReceiptRow(row)) return undefined;
|
|
2732
|
+
const request = this.readJsonAs<Record<string, unknown>>(row.requestHash);
|
|
2733
|
+
const decision = this.readJsonAs<Record<string, unknown>>(row.responseHash);
|
|
2734
|
+
if (
|
|
2735
|
+
typeof request.decisionId !== "string" ||
|
|
2736
|
+
typeof request.requestDigest !== "string" ||
|
|
2737
|
+
typeof request.nodeId !== "string" ||
|
|
2738
|
+
typeof decision.provenance !== "string" ||
|
|
2739
|
+
typeof decision.acceptedAt !== "string" ||
|
|
2740
|
+
typeof decision.answerDigest !== "string" ||
|
|
2741
|
+
typeof decision.subjectDigest !== "string" ||
|
|
2742
|
+
typeof decision.presentationDigest !== "string" ||
|
|
2743
|
+
typeof decision.revision !== "number" ||
|
|
2744
|
+
decision.response === undefined
|
|
2745
|
+
) {
|
|
2746
|
+
throw new Error(`Human decision receipt is invalid for run ${runId}`);
|
|
2747
|
+
}
|
|
2748
|
+
return {
|
|
2749
|
+
schema: "pi-workflows.human-decision-receipt.v1",
|
|
2750
|
+
decisionId: request.decisionId,
|
|
2751
|
+
requestDigest: request.requestDigest,
|
|
2752
|
+
nodeId: request.nodeId,
|
|
2753
|
+
response: decision.response as HumanDecisionReceipt["response"],
|
|
2754
|
+
provenance: decision.provenance === "timeout" ? "timeout" : "human",
|
|
2755
|
+
acceptedAt: decision.acceptedAt,
|
|
2756
|
+
answerDigest: decision.answerDigest,
|
|
2757
|
+
subjectDigest: decision.subjectDigest,
|
|
2758
|
+
presentationDigest: decision.presentationDigest,
|
|
2759
|
+
revision: decision.revision,
|
|
2760
|
+
};
|
|
2761
|
+
}
|
|
2762
|
+
|
|
1310
2763
|
private readUpdates(runId: string): Pick<WorkflowRunState, "updates"> {
|
|
1311
2764
|
const rows = this.state.connection
|
|
1312
2765
|
.prepare(
|
|
@@ -1613,10 +3066,22 @@ function compactTracePayload(
|
|
|
1613
3066
|
eventType: string,
|
|
1614
3067
|
payload: Record<string, unknown>,
|
|
1615
3068
|
): Record<string, unknown> {
|
|
1616
|
-
if (
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
3069
|
+
if (eventType === "agent_prompt_sent" && Object.hasOwn(payload, "prompt")) {
|
|
3070
|
+
const { prompt: _prompt, ...rest } = payload;
|
|
3071
|
+
return { ...rest, promptStored: true };
|
|
3072
|
+
}
|
|
3073
|
+
if (eventType === "node_finished" || eventType === "node_failed") {
|
|
3074
|
+
const {
|
|
3075
|
+
output: _output,
|
|
3076
|
+
action: _action,
|
|
3077
|
+
assistantMessage: _assistantMessage,
|
|
3078
|
+
conversation: _conversation,
|
|
3079
|
+
error: _error,
|
|
3080
|
+
...rest
|
|
3081
|
+
} = payload;
|
|
3082
|
+
return { ...rest, ...(Object.hasOwn(payload, "output") ? { outputStored: true } : {}) };
|
|
3083
|
+
}
|
|
3084
|
+
if (eventType === "include_exited" && Object.hasOwn(payload, "output")) {
|
|
1620
3085
|
const { output: _output, ...rest } = payload;
|
|
1621
3086
|
return { ...rest, outputStored: true };
|
|
1622
3087
|
}
|
|
@@ -1675,6 +3140,22 @@ function sourceForState(
|
|
|
1675
3140
|
};
|
|
1676
3141
|
}
|
|
1677
3142
|
|
|
3143
|
+
function sourceParts(source: WorkflowSource): {
|
|
3144
|
+
type: "builtin" | "file";
|
|
3145
|
+
ref: string;
|
|
3146
|
+
revision: string;
|
|
3147
|
+
} {
|
|
3148
|
+
return source.kind === "builtin"
|
|
3149
|
+
? { type: "builtin", ref: source.id, revision: source.revision }
|
|
3150
|
+
: { type: "file", ref: source.path, revision: source.hash };
|
|
3151
|
+
}
|
|
3152
|
+
|
|
3153
|
+
function workflowSourceFromRow(row: RunSourceRow): WorkflowSource {
|
|
3154
|
+
return row.sourceType === "builtin"
|
|
3155
|
+
? { kind: "builtin", id: row.sourceRef, revision: row.sourceRevision }
|
|
3156
|
+
: { kind: "file", path: row.sourceRef, hash: row.sourceRevision };
|
|
3157
|
+
}
|
|
3158
|
+
|
|
1678
3159
|
function segmentIdFor(runId: string, attemptId?: string): string {
|
|
1679
3160
|
return `segment-${createHash("sha256")
|
|
1680
3161
|
.update(`${runId}\0${attemptId ?? ""}`)
|
|
@@ -1714,14 +3195,45 @@ function outcomeForStatus(status: string): WorkflowStepRecord["outcome"] {
|
|
|
1714
3195
|
}
|
|
1715
3196
|
}
|
|
1716
3197
|
|
|
1717
|
-
function
|
|
3198
|
+
function attemptReceipt(step: WorkflowStepRecord): StoredAttemptReceipt {
|
|
1718
3199
|
return {
|
|
1719
3200
|
...(step.action === undefined ? {} : { action: step.action }),
|
|
1720
3201
|
...(step.assistantMessage === undefined ? {} : { assistantMessage: step.assistantMessage }),
|
|
1721
|
-
...(step.conversation === undefined ? {} : { conversation: step.conversation }),
|
|
1722
3202
|
};
|
|
1723
3203
|
}
|
|
1724
3204
|
|
|
3205
|
+
function promptFromEntry(value: unknown): string | null {
|
|
3206
|
+
if (!isRecord(value)) return null;
|
|
3207
|
+
const content = value.content;
|
|
3208
|
+
if (typeof content === "string") return content;
|
|
3209
|
+
if (!Array.isArray(content)) return null;
|
|
3210
|
+
return content
|
|
3211
|
+
.filter(isRecord)
|
|
3212
|
+
.map((part) => (typeof part.text === "string" ? part.text : ""))
|
|
3213
|
+
.filter((text) => text.length !== 0)
|
|
3214
|
+
.join("\n");
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3217
|
+
function assistantOutputFromEntry(value: unknown, maxChars?: number): string {
|
|
3218
|
+
if (!isRecord(value) || !isRecord(value.message)) {
|
|
3219
|
+
throw new Error("Assistant response entry is invalid");
|
|
3220
|
+
}
|
|
3221
|
+
const message = value.message;
|
|
3222
|
+
if (message.role !== "assistant" || !Array.isArray(message.content)) {
|
|
3223
|
+
throw new Error("Assistant response entry does not contain an assistant message");
|
|
3224
|
+
}
|
|
3225
|
+
const text = message.content
|
|
3226
|
+
.filter(isRecord)
|
|
3227
|
+
.filter((part) => part.type === "text" && typeof part.text === "string")
|
|
3228
|
+
.map((part) => part.text as string)
|
|
3229
|
+
.join("\n");
|
|
3230
|
+
if (text.trim().length === 0) throw new Error("Assistant response entry has no visible text");
|
|
3231
|
+
if (maxChars !== undefined && text.length > maxChars) {
|
|
3232
|
+
throw new Error(`Assistant response exceeds its stored limit of ${maxChars} characters`);
|
|
3233
|
+
}
|
|
3234
|
+
return text;
|
|
3235
|
+
}
|
|
3236
|
+
|
|
1725
3237
|
function resultForStep(step: WorkflowStepRecord): WorkflowNodeResult {
|
|
1726
3238
|
return {
|
|
1727
3239
|
attemptId: step.attemptId,
|
|
@@ -1797,6 +3309,178 @@ function validateSessionCapture(capture: WorkflowSessionCapture): void {
|
|
|
1797
3309
|
}
|
|
1798
3310
|
}
|
|
1799
3311
|
|
|
3312
|
+
function followUpIdFor(runId: string, requestId: string): string {
|
|
3313
|
+
const digest = createHash("sha256").update(`${runId}\0${requestId}`).digest("hex").slice(0, 40);
|
|
3314
|
+
return `follow-up-${digest}`;
|
|
3315
|
+
}
|
|
3316
|
+
|
|
3317
|
+
function assertRequestId(requestId: string): void {
|
|
3318
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,511}$/u.test(requestId)) {
|
|
3319
|
+
throw new Error("Workflow request ID must be a stable identifier of at most 512 characters");
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
|
|
3323
|
+
function assertSourceType(source: string): void {
|
|
3324
|
+
if (typeof source !== "string" || source.trim().length === 0 || source.length > 128) {
|
|
3325
|
+
throw new Error("Workflow change source must be a non-empty string of at most 128 characters");
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
|
|
3329
|
+
function assertActorType(value: string): ActorType {
|
|
3330
|
+
if (
|
|
3331
|
+
value !== "session" &&
|
|
3332
|
+
value !== "host" &&
|
|
3333
|
+
value !== "controller" &&
|
|
3334
|
+
value !== "channel" &&
|
|
3335
|
+
value !== "human" &&
|
|
3336
|
+
value !== "policy" &&
|
|
3337
|
+
value !== "control" &&
|
|
3338
|
+
value !== "system"
|
|
3339
|
+
) {
|
|
3340
|
+
throw new Error(`Unknown saved actor type: ${value}`);
|
|
3341
|
+
}
|
|
3342
|
+
return value;
|
|
3343
|
+
}
|
|
3344
|
+
|
|
3345
|
+
function assertFollowUpState(value: string): WorkflowFollowUpState {
|
|
3346
|
+
if (
|
|
3347
|
+
value !== "queued" &&
|
|
3348
|
+
value !== "pending_presentation" &&
|
|
3349
|
+
value !== "ready" &&
|
|
3350
|
+
value !== "sent" &&
|
|
3351
|
+
value !== "removed" &&
|
|
3352
|
+
value !== "cancelled"
|
|
3353
|
+
) {
|
|
3354
|
+
throw new Error(`Unknown workflow follow-up state: ${value}`);
|
|
3355
|
+
}
|
|
3356
|
+
return value;
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3359
|
+
function assertPresentationState(value: string): WorkflowPresentationState {
|
|
3360
|
+
if (
|
|
3361
|
+
value !== "none" &&
|
|
3362
|
+
value !== "not-needed" &&
|
|
3363
|
+
value !== "pending" &&
|
|
3364
|
+
value !== "settled" &&
|
|
3365
|
+
value !== "unavailable"
|
|
3366
|
+
) {
|
|
3367
|
+
throw new Error(`Unknown workflow presentation state: ${value}`);
|
|
3368
|
+
}
|
|
3369
|
+
return value;
|
|
3370
|
+
}
|
|
3371
|
+
|
|
3372
|
+
function isSettingsScopeRow(value: unknown): value is SettingsScopeRow {
|
|
3373
|
+
return (
|
|
3374
|
+
isRecord(value) &&
|
|
3375
|
+
typeof value.scopeId === "string" &&
|
|
3376
|
+
typeof value.resourceId === "string" &&
|
|
3377
|
+
typeof value.originRunId === "string" &&
|
|
3378
|
+
typeof value.activeRunId === "string" &&
|
|
3379
|
+
typeof value.mountPath === "string" &&
|
|
3380
|
+
typeof value.invocation === "number" &&
|
|
3381
|
+
Buffer.isBuffer(value.currentHash) &&
|
|
3382
|
+
typeof value.revision === "number" &&
|
|
3383
|
+
typeof value.createdAt === "number" &&
|
|
3384
|
+
typeof value.updatedAt === "number"
|
|
3385
|
+
);
|
|
3386
|
+
}
|
|
3387
|
+
|
|
3388
|
+
function isSettingsProjectionRow(value: unknown): value is SettingsProjectionRow {
|
|
3389
|
+
return (
|
|
3390
|
+
isRecord(value) &&
|
|
3391
|
+
typeof value.scopeId === "string" &&
|
|
3392
|
+
Buffer.isBuffer(value.initialHash) &&
|
|
3393
|
+
Buffer.isBuffer(value.currentHash) &&
|
|
3394
|
+
typeof value.revision === "number"
|
|
3395
|
+
);
|
|
3396
|
+
}
|
|
3397
|
+
|
|
3398
|
+
function isSettingsProjectionChangeRow(value: unknown): value is SettingsProjectionChangeRow {
|
|
3399
|
+
return (
|
|
3400
|
+
isRecord(value) &&
|
|
3401
|
+
typeof value.changeNumber === "number" &&
|
|
3402
|
+
Buffer.isBuffer(value.patchHash) &&
|
|
3403
|
+
Buffer.isBuffer(value.beforeHash) &&
|
|
3404
|
+
Buffer.isBuffer(value.afterHash)
|
|
3405
|
+
);
|
|
3406
|
+
}
|
|
3407
|
+
|
|
3408
|
+
function isSettingsChangeRow(value: unknown): value is SettingsChangeRow {
|
|
3409
|
+
return (
|
|
3410
|
+
isRecord(value) &&
|
|
3411
|
+
typeof value.changeId === "string" &&
|
|
3412
|
+
typeof value.scopeId === "string" &&
|
|
3413
|
+
typeof value.requestId === "string" &&
|
|
3414
|
+
typeof value.changeNumber === "number" &&
|
|
3415
|
+
typeof value.actorType === "string" &&
|
|
3416
|
+
(typeof value.actorId === "string" || value.actorId === null) &&
|
|
3417
|
+
typeof value.sourceType === "string" &&
|
|
3418
|
+
Buffer.isBuffer(value.patchHash) &&
|
|
3419
|
+
Buffer.isBuffer(value.beforeHash) &&
|
|
3420
|
+
Buffer.isBuffer(value.afterHash) &&
|
|
3421
|
+
typeof value.acceptedAt === "number"
|
|
3422
|
+
);
|
|
3423
|
+
}
|
|
3424
|
+
|
|
3425
|
+
function isFollowUpQueueRow(value: unknown): value is FollowUpQueueRow {
|
|
3426
|
+
return (
|
|
3427
|
+
isRecord(value) &&
|
|
3428
|
+
typeof value.runId === "string" &&
|
|
3429
|
+
typeof value.resourceId === "string" &&
|
|
3430
|
+
(typeof value.originSessionId === "string" || value.originSessionId === null) &&
|
|
3431
|
+
typeof value.presentationState === "string" &&
|
|
3432
|
+
(typeof value.presentationEntryId === "string" || value.presentationEntryId === null) &&
|
|
3433
|
+
(typeof value.presentationAssistantEntryId === "string" ||
|
|
3434
|
+
value.presentationAssistantEntryId === null) &&
|
|
3435
|
+
(Buffer.isBuffer(value.presentationReasonHash) || value.presentationReasonHash === null) &&
|
|
3436
|
+
typeof value.revision === "number"
|
|
3437
|
+
);
|
|
3438
|
+
}
|
|
3439
|
+
|
|
3440
|
+
function isFollowUpRow(value: unknown): value is FollowUpRow {
|
|
3441
|
+
return (
|
|
3442
|
+
isRecord(value) &&
|
|
3443
|
+
typeof value.followUpId === "string" &&
|
|
3444
|
+
typeof value.resourceId === "string" &&
|
|
3445
|
+
typeof value.queueResourceId === "string" &&
|
|
3446
|
+
typeof value.runId === "string" &&
|
|
3447
|
+
typeof value.requestId === "string" &&
|
|
3448
|
+
typeof value.orderNumber === "number" &&
|
|
3449
|
+
typeof value.targetSessionId === "string" &&
|
|
3450
|
+
typeof value.actorType === "string" &&
|
|
3451
|
+
(typeof value.actorId === "string" || value.actorId === null) &&
|
|
3452
|
+
typeof value.sourceType === "string" &&
|
|
3453
|
+
Buffer.isBuffer(value.promptHash) &&
|
|
3454
|
+
typeof value.status === "string" &&
|
|
3455
|
+
(typeof value.sessionEntryId === "string" || value.sessionEntryId === null) &&
|
|
3456
|
+
(Buffer.isBuffer(value.reasonHash) || value.reasonHash === null) &&
|
|
3457
|
+
typeof value.createdAt === "number" &&
|
|
3458
|
+
typeof value.updatedAt === "number" &&
|
|
3459
|
+
(typeof value.sentAt === "number" || value.sentAt === null) &&
|
|
3460
|
+
typeof value.revision === "number"
|
|
3461
|
+
);
|
|
3462
|
+
}
|
|
3463
|
+
|
|
3464
|
+
function isParentRunRow(value: unknown): value is { parentRunId: string | null } {
|
|
3465
|
+
return isRecord(value) && (typeof value.parentRunId === "string" || value.parentRunId === null);
|
|
3466
|
+
}
|
|
3467
|
+
|
|
3468
|
+
function isStatusRow(value: unknown): value is { status: string } {
|
|
3469
|
+
return isRecord(value) && typeof value.status === "string";
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
function isCountRow(value: unknown): value is { count: number } {
|
|
3473
|
+
return isRecord(value) && typeof value.count === "number";
|
|
3474
|
+
}
|
|
3475
|
+
|
|
3476
|
+
function isLeaseExpiryRow(value: unknown): value is { expiresAt: number | null } {
|
|
3477
|
+
return isRecord(value) && (typeof value.expiresAt === "number" || value.expiresAt === null);
|
|
3478
|
+
}
|
|
3479
|
+
|
|
3480
|
+
function isNextOrderRow(value: unknown): value is { nextOrder: number } {
|
|
3481
|
+
return isRecord(value) && typeof value.nextOrder === "number";
|
|
3482
|
+
}
|
|
3483
|
+
|
|
1800
3484
|
function assertValidRunId(runId: string): void {
|
|
1801
3485
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(runId)) {
|
|
1802
3486
|
throw new Error(`Invalid workflow run id: ${JSON.stringify(runId)}`);
|
|
@@ -1811,6 +3495,41 @@ function isRunRow(value: unknown): value is RunRow {
|
|
|
1811
3495
|
return isRecord(value);
|
|
1812
3496
|
}
|
|
1813
3497
|
|
|
3498
|
+
function savedCurrentSettingsBinding(
|
|
3499
|
+
scopeId: string | null,
|
|
3500
|
+
changeNumber: number | null,
|
|
3501
|
+
settingsHash: Buffer | null,
|
|
3502
|
+
): Pick<
|
|
3503
|
+
WorkflowRunState,
|
|
3504
|
+
"currentSettingsScopeId" | "currentSettingsChangeNumber" | "currentSettingsHash"
|
|
3505
|
+
> {
|
|
3506
|
+
if (scopeId === null && changeNumber === null && settingsHash === null) return {};
|
|
3507
|
+
if (scopeId === null || changeNumber === null || settingsHash === null) {
|
|
3508
|
+
throw new Error("Saved current workflow settings binding is incomplete");
|
|
3509
|
+
}
|
|
3510
|
+
return {
|
|
3511
|
+
currentSettingsScopeId: scopeId,
|
|
3512
|
+
currentSettingsChangeNumber: changeNumber,
|
|
3513
|
+
currentSettingsHash: settingsHash.toString("hex"),
|
|
3514
|
+
};
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3517
|
+
function savedStepSettingsBinding(
|
|
3518
|
+
scopeId: string | null,
|
|
3519
|
+
changeNumber: number | null,
|
|
3520
|
+
settingsHash: Buffer | null,
|
|
3521
|
+
): Pick<WorkflowStepRecord, "settingsScopeId" | "settingsChangeNumber" | "settingsHash"> {
|
|
3522
|
+
if (scopeId === null && changeNumber === null && settingsHash === null) return {};
|
|
3523
|
+
if (scopeId === null || changeNumber === null || settingsHash === null) {
|
|
3524
|
+
throw new Error("Saved workflow step settings binding is incomplete");
|
|
3525
|
+
}
|
|
3526
|
+
return {
|
|
3527
|
+
settingsScopeId: scopeId,
|
|
3528
|
+
settingsChangeNumber: changeNumber,
|
|
3529
|
+
settingsHash: settingsHash.toString("hex"),
|
|
3530
|
+
};
|
|
3531
|
+
}
|
|
3532
|
+
|
|
1814
3533
|
function isRunIdRow(value: unknown): value is { runId: string } {
|
|
1815
3534
|
return isRecord(value);
|
|
1816
3535
|
}
|
|
@@ -1822,11 +3541,55 @@ function isRunStepIdentityRow(value: unknown): value is {
|
|
|
1822
3541
|
return isRecord(value);
|
|
1823
3542
|
}
|
|
1824
3543
|
|
|
1825
|
-
function
|
|
1826
|
-
return isRecord(value)
|
|
3544
|
+
function isStepRow(value: unknown): value is StepRow {
|
|
3545
|
+
return isRecord(value);
|
|
1827
3546
|
}
|
|
1828
3547
|
|
|
1829
|
-
function
|
|
3548
|
+
function isRunSourceRow(value: unknown): value is RunSourceRow {
|
|
3549
|
+
return isRecord(value);
|
|
3550
|
+
}
|
|
3551
|
+
|
|
3552
|
+
function isSegmentIdentityRow(value: unknown): value is { segmentId: string; entryHash: Buffer } {
|
|
3553
|
+
return isRecord(value) && typeof value.segmentId === "string" && Buffer.isBuffer(value.entryHash);
|
|
3554
|
+
}
|
|
3555
|
+
|
|
3556
|
+
function isEntryIdentityRow(value: unknown): value is { entryId: string } {
|
|
3557
|
+
return isRecord(value) && typeof value.entryId === "string";
|
|
3558
|
+
}
|
|
3559
|
+
|
|
3560
|
+
function isAttemptValueRow(value: unknown): value is {
|
|
3561
|
+
outputHash: Buffer | null;
|
|
3562
|
+
receiptHash: Buffer | null;
|
|
3563
|
+
responseEntryHash: Buffer | null;
|
|
3564
|
+
} {
|
|
3565
|
+
return isRecord(value);
|
|
3566
|
+
}
|
|
3567
|
+
|
|
3568
|
+
function isActiveAttemptRow(value: unknown): value is {
|
|
3569
|
+
attemptId: string;
|
|
3570
|
+
nodeId: string;
|
|
3571
|
+
startedAt: number | null;
|
|
3572
|
+
settingsScopeId: string | null;
|
|
3573
|
+
settingsChangeNumber: number | null;
|
|
3574
|
+
settingsHash: Buffer | null;
|
|
3575
|
+
} {
|
|
3576
|
+
return isRecord(value);
|
|
3577
|
+
}
|
|
3578
|
+
|
|
3579
|
+
function isDecisionReceiptRow(value: unknown): value is {
|
|
3580
|
+
requestHash: Buffer;
|
|
3581
|
+
responseHash: Buffer;
|
|
3582
|
+
} {
|
|
3583
|
+
return (
|
|
3584
|
+
isRecord(value) && Buffer.isBuffer(value.requestHash) && Buffer.isBuffer(value.responseHash)
|
|
3585
|
+
);
|
|
3586
|
+
}
|
|
3587
|
+
|
|
3588
|
+
function isContinuationIdentityRow(value: unknown): value is {
|
|
3589
|
+
parentRunId: string;
|
|
3590
|
+
continuationRunId: string;
|
|
3591
|
+
createdAt: number;
|
|
3592
|
+
} {
|
|
1830
3593
|
return isRecord(value);
|
|
1831
3594
|
}
|
|
1832
3595
|
|
|
@@ -1873,7 +3636,15 @@ function isLeaseAuthorityRow(value: unknown): value is {
|
|
|
1873
3636
|
}
|
|
1874
3637
|
|
|
1875
3638
|
export function createDefinitionSnapshot(workflow: WorkflowDefinition): WorkflowDefinitionSnapshot {
|
|
1876
|
-
const
|
|
3639
|
+
const metadata = compositionMetadata(workflow);
|
|
3640
|
+
const composition = metadata?.snapshot;
|
|
3641
|
+
const settingsScopes = Object.values(metadata?.scopes ?? {})
|
|
3642
|
+
.filter((scope) => scope.path !== "" && scope.settings !== undefined)
|
|
3643
|
+
.sort((left, right) => left.path.localeCompare(right.path))
|
|
3644
|
+
.map((scope) => ({
|
|
3645
|
+
mountPath: scope.path.split("/"),
|
|
3646
|
+
settings: snapshotSettings(scope.settings!),
|
|
3647
|
+
}));
|
|
1877
3648
|
return {
|
|
1878
3649
|
schema: DEFINITION_SNAPSHOT_SCHEMA,
|
|
1879
3650
|
name: workflow.name,
|
|
@@ -1887,6 +3658,8 @@ export function createDefinitionSnapshot(workflow: WorkflowDefinition): Workflow
|
|
|
1887
3658
|
),
|
|
1888
3659
|
edges: structuredClone(workflow.edges),
|
|
1889
3660
|
...(composition !== undefined ? { composition: structuredClone(composition) } : {}),
|
|
3661
|
+
...(workflow.settings !== undefined ? { settings: snapshotSettings(workflow.settings) } : {}),
|
|
3662
|
+
...(settingsScopes.length > 0 ? { settingsScopes } : {}),
|
|
1890
3663
|
};
|
|
1891
3664
|
}
|
|
1892
3665
|
|
|
@@ -1927,6 +3700,9 @@ function snapshotNode(
|
|
|
1927
3700
|
if (node.nodeType === "agent" && node.expectedOutput !== undefined) {
|
|
1928
3701
|
common.expectedOutput = node.expectedOutput;
|
|
1929
3702
|
}
|
|
3703
|
+
if (node.nodeType === "compute" && node.settingsRoute === true) {
|
|
3704
|
+
common.settingsRoute = true;
|
|
3705
|
+
}
|
|
1930
3706
|
if (node.nodeType === "notify") common.summary = node.kind ?? "progress";
|
|
1931
3707
|
if (node.nodeType === "checkpoint" && node.summary !== undefined) common.summary = node.summary;
|
|
1932
3708
|
if (node.nodeType === "checkpoint" && node.humanDecision !== undefined) {
|
|
@@ -1945,3 +3721,20 @@ function snapshotNode(
|
|
|
1945
3721
|
if (node.nodeType === "action") common.actionExecution = "exec" in node ? "shell" : "function";
|
|
1946
3722
|
return common;
|
|
1947
3723
|
}
|
|
3724
|
+
|
|
3725
|
+
function snapshotSettings(
|
|
3726
|
+
settings: NonNullable<WorkflowDefinition["settings"]>,
|
|
3727
|
+
): WorkflowSettingsSnapshot {
|
|
3728
|
+
return {
|
|
3729
|
+
...(settings.description !== undefined ? { description: settings.description } : {}),
|
|
3730
|
+
paths: settings.paths.map((rule) => ({
|
|
3731
|
+
path: rule.path,
|
|
3732
|
+
permissions: Object.fromEntries(
|
|
3733
|
+
Object.entries(rule.permissions).map(([permission, actors]) => [
|
|
3734
|
+
permission,
|
|
3735
|
+
actors === undefined ? [] : [...actors],
|
|
3736
|
+
]),
|
|
3737
|
+
),
|
|
3738
|
+
})),
|
|
3739
|
+
};
|
|
3740
|
+
}
|