@osolmaz/pi-workflows 0.13.1 → 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 +9 -7
- package/dist/builtins/autodoc.workflow.d.ts +5 -5
- package/dist/builtins/autoimplement.workflow.d.ts +69 -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 +11 -5
- package/dist/builtins/autoplan.workflow.js +68 -32
- package/dist/builtins/autoplan.workflow.js.map +1 -1
- package/dist/builtins/catalog.js +4 -4
- package/dist/builtins/change-verification.workflow.d.ts +1 -1
- package/dist/builtins/plain-summary.workflow.d.ts +1 -1
- package/dist/builtins/plain-summary.workflow.js +35 -24
- package/dist/builtins/plain-summary.workflow.js.map +1 -1
- package/dist/builtins/plan-approval.workflow.d.ts +1 -1
- package/dist/builtins/plan-change.workflow.d.ts +25 -23
- package/dist/builtins/sanity-check.workflow.d.ts +3 -3
- package/dist/builtins/sanity-check.workflow.js +19 -22
- package/dist/builtins/sanity-check.workflow.js.map +1 -1
- 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 +88 -61
- 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/decision-channels.js +45 -7
- package/dist/extension/decision-channels.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 +462 -76
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/recorder.d.ts +1 -1
- package/dist/extension/recorder.js +8 -8
- package/dist/extension/recorder.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 +130 -13
- 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/viewer/session-reducer.d.ts +0 -1
- package/dist/viewer/session-reducer.js +2 -24
- package/dist/viewer/session-reducer.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 +303 -36
- 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 +81 -3
- package/dist/workflows/store.js +1563 -50
- 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 +41 -6
- 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 +36 -17
- package/docs/WORKFLOW_COMPOSITION.md +15 -0
- package/docs/WORKFLOW_STEP_MESSAGES.md +4 -2
- package/docs/plans/2026-08-25-autoplan-user-intent-capture-plan.md +107 -0
- 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/session-event-journal.md +2 -3
- package/docs/tui-viewer.md +7 -6
- package/docs/workflows.md +58 -10
- 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/autoplan.workflow.ts +80 -32
- package/src/builtins/catalog.ts +4 -4
- package/src/builtins/plain-summary.workflow.ts +38 -40
- package/src/builtins/sanity-check.workflow.ts +19 -22
- package/src/controllers/index.ts +6 -0
- package/src/controllers/sqlite.ts +132 -72
- 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/decision-channels.ts +47 -7
- package/src/extension/follow-up-coordinator.ts +151 -0
- package/src/extension/index.ts +540 -89
- package/src/extension/recorder.ts +10 -8
- 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 +130 -13
- package/src/viewer/cli.ts +52 -5
- package/src/viewer/render.ts +43 -1
- package/src/viewer/session-reducer.ts +2 -29
- package/src/workflows/composition.ts +19 -0
- package/src/workflows/definition.ts +19 -5
- package/src/workflows/engine.ts +365 -36
- 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 +2268 -121
- package/src/workflows/tool-input.ts +58 -0
- package/src/workflows/types.ts +43 -5
package/dist/workflows/store.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { StateDatabase, workflowStatePath } from "../state/database.js";
|
|
3
3
|
import { canonicalJson } from "../state/json.js";
|
|
4
|
-
import { resourceIdFor, tokenHash } from "../state/mutation.js";
|
|
4
|
+
import { StateMutationStore, StaleResourceError, resourceIdFor, tokenHash, } from "../state/mutation.js";
|
|
5
5
|
import { compositionMetadata } from "./composition.js";
|
|
6
|
+
import { applyJsonPatch, validateJsonPatch } from "./json-patch.js";
|
|
7
|
+
import { applyWorkflowSettingsPatch, workflowSettingsScopeId, } from "./settings.js";
|
|
6
8
|
import { MAX_CURRENT_UPDATES, createUpdateId, updateProjection } from "./updates.js";
|
|
7
9
|
export const RUN_STATE_SCHEMA = "pi-workflows.run-state.v1";
|
|
8
10
|
export const DEFINITION_SNAPSHOT_SCHEMA = "pi-workflows.definition-snapshot.v1";
|
|
@@ -10,6 +12,18 @@ export const SESSION_BINDING_SCHEMA = "pi-workflows.session-binding.v1";
|
|
|
10
12
|
export const SESSION_EVENT_SCHEMA = "pi-workflows.session-event.v1";
|
|
11
13
|
export const SESSION_CAPTURE_SCHEMA = "pi-workflows.session-capture.v1";
|
|
12
14
|
export const SESSION_EVENT_MAX_BYTES = 1024 * 1024;
|
|
15
|
+
const FOLLOW_UP_ROW_SELECT = `
|
|
16
|
+
SELECT f.follow_up_id AS followUpId, f.resource_id AS resourceId,
|
|
17
|
+
f.queue_resource_id AS queueResourceId, f.run_id AS runId,
|
|
18
|
+
f.request_id AS requestId, f.order_number AS orderNumber,
|
|
19
|
+
f.target_session_id AS targetSessionId, f.actor_type AS actorType,
|
|
20
|
+
f.actor_id AS actorId, f.source_type AS sourceType,
|
|
21
|
+
f.prompt_hash AS promptHash, f.status,
|
|
22
|
+
f.session_entry_id AS sessionEntryId, f.reason_hash AS reasonHash,
|
|
23
|
+
f.created_at AS createdAt, f.updated_at AS updatedAt, f.sent_at AS sentAt,
|
|
24
|
+
r.revision
|
|
25
|
+
FROM workflow_follow_ups f
|
|
26
|
+
JOIN resources r ON r.resource_id = f.resource_id`;
|
|
13
27
|
export function workflowStateDatabasePath(homeDir) {
|
|
14
28
|
return workflowStatePath(homeDir);
|
|
15
29
|
}
|
|
@@ -23,6 +37,7 @@ export class WorkflowRunStore {
|
|
|
23
37
|
authorityProvider;
|
|
24
38
|
contexts = new Map();
|
|
25
39
|
ownsState;
|
|
40
|
+
mutations;
|
|
26
41
|
constructor(databasePath = workflowStatePath(), options = {}) {
|
|
27
42
|
this.authorityProvider = options.authorityProvider;
|
|
28
43
|
this.ownsState = options.state === undefined;
|
|
@@ -34,12 +49,13 @@ export class WorkflowRunStore {
|
|
|
34
49
|
checkLegacyState: databasePath === workflowStatePath(),
|
|
35
50
|
});
|
|
36
51
|
this.databasePath = this.state.filePath;
|
|
52
|
+
this.mutations = new StateMutationStore(this.state);
|
|
37
53
|
}
|
|
38
54
|
close() {
|
|
39
55
|
if (this.ownsState)
|
|
40
56
|
this.state.close();
|
|
41
57
|
}
|
|
42
|
-
async initializeRun(workflow, state) {
|
|
58
|
+
async initializeRun(workflow, state, options = {}) {
|
|
43
59
|
assertValidRunId(state.runId);
|
|
44
60
|
if (!this.contexts.has(state.runId)) {
|
|
45
61
|
const reserved = this.readRunRow(state.runId);
|
|
@@ -79,7 +95,9 @@ export class WorkflowRunStore {
|
|
|
79
95
|
payload: { workflowName: workflow.name },
|
|
80
96
|
});
|
|
81
97
|
this.persistRunState(reserved, state, revision, now);
|
|
98
|
+
this.initializeRunSettingsAndFollowUps(state, options.initialSettings ?? [], now);
|
|
82
99
|
this.syncNodeAttempts(state, snapshot, now);
|
|
100
|
+
this.syncContinuationIdentity(state);
|
|
83
101
|
context.revision = revision;
|
|
84
102
|
return;
|
|
85
103
|
}
|
|
@@ -101,30 +119,463 @@ export class WorkflowRunStore {
|
|
|
101
119
|
state.traceSeq = 1;
|
|
102
120
|
state.updatedAt = at;
|
|
103
121
|
const inputHash = this.state.putJson(state.input, now);
|
|
104
|
-
const workflowSourceHash = this.state.putJson(state.workflowSource ?? source, now);
|
|
105
122
|
const launchOptionsHash = this.state.putJson({}, now);
|
|
106
|
-
const
|
|
123
|
+
const finalOutputHash = state.finalOutput === undefined ? null : this.state.putJson(state.finalOutput, now);
|
|
107
124
|
const errorHash = state.error === undefined ? null : this.state.putText(state.error, now);
|
|
108
125
|
this.state.connection
|
|
109
126
|
.prepare(`INSERT INTO runs(
|
|
110
127
|
run_id, resource_id, project_id, parent_run_id, definition_digest,
|
|
111
|
-
workflow_ref,
|
|
112
|
-
|
|
113
|
-
status_detail, input_hash, output_hash, error_hash,
|
|
128
|
+
workflow_ref, launch_options_hash, title, status, paused,
|
|
129
|
+
status_detail, input_hash, final_output_hash, error_hash,
|
|
114
130
|
created_at, updated_at, finished_at
|
|
115
|
-
) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
116
|
-
.run(state.runId, resourceId, state.parentRunId ?? null, definitionDigest, state.workflowName,
|
|
131
|
+
) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
132
|
+
.run(state.runId, resourceId, state.parentRunId ?? null, definitionDigest, state.workflowName, launchOptionsHash, state.runTitle ?? null, state.status, state.paused === true ? 1 : 0, state.statusDetail ?? null, inputHash, finalOutputHash, errorHash, Date.parse(state.startedAt), now, state.finishedAt === undefined ? null : Date.parse(state.finishedAt));
|
|
133
|
+
this.insertRunSources(state.runId, source, state.workflowSources ?? []);
|
|
134
|
+
this.syncContinuationIdentity(state);
|
|
117
135
|
insertRunEvent(this.state, resourceId, 1, at, {
|
|
118
136
|
scope: "run",
|
|
119
137
|
type: "run_created",
|
|
120
138
|
payload: { workflowName: workflow.name },
|
|
121
139
|
});
|
|
140
|
+
this.initializeRunSettingsAndFollowUps(state, options.initialSettings ?? [], now);
|
|
122
141
|
this.syncNodeAttempts(state, snapshot, now);
|
|
123
142
|
});
|
|
124
143
|
this.contexts.set(state.runId, { revision: acceptedRevision, lock: Promise.resolve() });
|
|
125
144
|
return state.runId;
|
|
126
145
|
});
|
|
127
146
|
}
|
|
147
|
+
ensureSettingsScope(options) {
|
|
148
|
+
const now = Date.now();
|
|
149
|
+
return this.state.transaction(() => {
|
|
150
|
+
this.requireRunAcceptsSettings(options.runId, false);
|
|
151
|
+
const originRunId = this.logicalOriginRunId(options.runId);
|
|
152
|
+
const scopeId = workflowSettingsScopeId(originRunId, options.mountPath, options.invocation);
|
|
153
|
+
const existing = this.settingsScopeRow(scopeId);
|
|
154
|
+
if (existing !== undefined) {
|
|
155
|
+
if (existing.activeRunId !== options.runId) {
|
|
156
|
+
throw new Error(`Workflow settings scope belongs to another active run: ${scopeId}`);
|
|
157
|
+
}
|
|
158
|
+
return this.settingsScopeRecord(existing);
|
|
159
|
+
}
|
|
160
|
+
const resourceId = this.mutations.ensureResource("settings", scopeId, now);
|
|
161
|
+
const settingsHash = this.state.putJson(options.settings, now);
|
|
162
|
+
this.state.connection
|
|
163
|
+
.prepare(`INSERT INTO workflow_settings(
|
|
164
|
+
scope_id, resource_id, origin_run_id, active_run_id, mount_path, invocation,
|
|
165
|
+
initial_hash, current_hash, created_at, updated_at
|
|
166
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
167
|
+
.run(scopeId, resourceId, originRunId, options.runId, options.mountPath, options.invocation, settingsHash, settingsHash, now, now);
|
|
168
|
+
return this.settingsScopeRecord(this.requireSettingsScopeRow(scopeId));
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
getSettingsScope(scopeId) {
|
|
172
|
+
const row = this.settingsScopeRow(scopeId);
|
|
173
|
+
return row === undefined ? undefined : this.settingsScopeRecord(row);
|
|
174
|
+
}
|
|
175
|
+
getSettingsScopeAtChange(scopeId, changeNumber) {
|
|
176
|
+
if (!Number.isInteger(changeNumber) || changeNumber < 0) {
|
|
177
|
+
throw new Error("Workflow settings change number must be a non-negative integer");
|
|
178
|
+
}
|
|
179
|
+
const row = this.settingsScopeRow(scopeId);
|
|
180
|
+
if (row === undefined)
|
|
181
|
+
return undefined;
|
|
182
|
+
if (changeNumber > row.revision) {
|
|
183
|
+
throw new Error(`Workflow settings change ${changeNumber} does not exist for ${scopeId}`);
|
|
184
|
+
}
|
|
185
|
+
if (changeNumber === row.revision)
|
|
186
|
+
return this.settingsScopeRecord(row);
|
|
187
|
+
const saved = changeNumber === 0
|
|
188
|
+
? this.state.connection
|
|
189
|
+
.prepare("SELECT initial_hash AS settingsHash FROM workflow_settings WHERE scope_id = ?")
|
|
190
|
+
.get(scopeId)
|
|
191
|
+
: this.state.connection
|
|
192
|
+
.prepare(`SELECT after_hash AS settingsHash FROM workflow_setting_changes
|
|
193
|
+
WHERE scope_id = ? AND change_number = ?`)
|
|
194
|
+
.get(scopeId, changeNumber);
|
|
195
|
+
if (!isRecord(saved) || !Buffer.isBuffer(saved.settingsHash)) {
|
|
196
|
+
throw new Error(`Workflow settings change ${changeNumber} is missing for ${scopeId}`);
|
|
197
|
+
}
|
|
198
|
+
return this.settingsScopeRecord(row, changeNumber, saved.settingsHash);
|
|
199
|
+
}
|
|
200
|
+
listSettingsScopes(runId) {
|
|
201
|
+
return this.state.connection
|
|
202
|
+
.prepare(`SELECT s.scope_id AS scopeId, s.resource_id AS resourceId,
|
|
203
|
+
s.origin_run_id AS originRunId, s.active_run_id AS activeRunId,
|
|
204
|
+
s.mount_path AS mountPath, s.invocation,
|
|
205
|
+
s.current_hash AS currentHash, r.revision,
|
|
206
|
+
s.created_at AS createdAt, s.updated_at AS updatedAt
|
|
207
|
+
FROM workflow_settings s
|
|
208
|
+
JOIN resources r ON r.resource_id = s.resource_id
|
|
209
|
+
WHERE s.active_run_id = ?
|
|
210
|
+
ORDER BY s.mount_path, s.invocation`)
|
|
211
|
+
.all(runId)
|
|
212
|
+
.filter(isSettingsScopeRow)
|
|
213
|
+
.map((row) => this.settingsScopeRecord(row));
|
|
214
|
+
}
|
|
215
|
+
settingsScopesForRunView(runId) {
|
|
216
|
+
const originRunId = this.logicalOriginRunId(runId);
|
|
217
|
+
return this.state.connection
|
|
218
|
+
.prepare(`SELECT s.scope_id AS scopeId, s.resource_id AS resourceId,
|
|
219
|
+
s.origin_run_id AS originRunId, s.active_run_id AS activeRunId,
|
|
220
|
+
s.mount_path AS mountPath, s.invocation,
|
|
221
|
+
s.current_hash AS currentHash, r.revision,
|
|
222
|
+
s.created_at AS createdAt, s.updated_at AS updatedAt
|
|
223
|
+
FROM workflow_settings s
|
|
224
|
+
JOIN resources r ON r.resource_id = s.resource_id
|
|
225
|
+
WHERE s.origin_run_id = ? OR s.active_run_id = ?
|
|
226
|
+
ORDER BY s.mount_path, s.invocation`)
|
|
227
|
+
.all(originRunId, runId)
|
|
228
|
+
.filter(isSettingsScopeRow)
|
|
229
|
+
.map((row) => this.settingsScopeRecord(row));
|
|
230
|
+
}
|
|
231
|
+
findSettingsScope(runId, mountPath, invocation) {
|
|
232
|
+
const row = this.state.connection
|
|
233
|
+
.prepare(`SELECT s.scope_id AS scopeId, s.resource_id AS resourceId,
|
|
234
|
+
s.origin_run_id AS originRunId, s.active_run_id AS activeRunId,
|
|
235
|
+
s.mount_path AS mountPath, s.invocation,
|
|
236
|
+
s.current_hash AS currentHash, r.revision,
|
|
237
|
+
s.created_at AS createdAt, s.updated_at AS updatedAt
|
|
238
|
+
FROM workflow_settings s
|
|
239
|
+
JOIN resources r ON r.resource_id = s.resource_id
|
|
240
|
+
WHERE s.active_run_id = ? AND s.mount_path = ? AND s.invocation = ?`)
|
|
241
|
+
.get(runId, mountPath, invocation);
|
|
242
|
+
return isSettingsScopeRow(row) ? this.settingsScopeRecord(row) : undefined;
|
|
243
|
+
}
|
|
244
|
+
async changeSettings(definition, request) {
|
|
245
|
+
assertRequestId(request.requestId);
|
|
246
|
+
assertSourceType(request.source);
|
|
247
|
+
const patch = validateJsonPatch(request.patch);
|
|
248
|
+
const patchJson = canonicalJson(patch);
|
|
249
|
+
const patchDigest = createHash("sha256").update(patchJson).digest();
|
|
250
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
251
|
+
const prior = this.settingsChangeRow(request.scopeId, request.requestId);
|
|
252
|
+
if (prior !== undefined) {
|
|
253
|
+
this.assertSameSettingsChange(prior, request, patchDigest);
|
|
254
|
+
return {
|
|
255
|
+
scope: this.settingsScopeRecord(this.requireSettingsScopeRow(request.scopeId)),
|
|
256
|
+
change: this.settingsChangeRecord(prior),
|
|
257
|
+
adopted: true,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
const scope = this.requireSettingsScopeRow(request.scopeId);
|
|
261
|
+
if (scope.activeRunId !== request.runId) {
|
|
262
|
+
throw new Error(`Workflow settings scope is not active for run ${request.runId}`);
|
|
263
|
+
}
|
|
264
|
+
this.requireRunAcceptsSettings(request.runId, true);
|
|
265
|
+
if (request.expectedChangeNumber !== undefined &&
|
|
266
|
+
request.expectedChangeNumber !== scope.revision) {
|
|
267
|
+
throw new StaleResourceError(`Workflow settings changed: expected ${request.expectedChangeNumber}, current ${scope.revision}`);
|
|
268
|
+
}
|
|
269
|
+
const before = this.state.readJson(scope.currentHash);
|
|
270
|
+
const applied = await applyWorkflowSettingsPatch(definition, before, patch, request.actor, request.source);
|
|
271
|
+
const beforeHash = scope.currentHash;
|
|
272
|
+
const afterHash = createHash("sha256").update(canonicalJson(applied.json)).digest();
|
|
273
|
+
try {
|
|
274
|
+
const mutation = this.mutations.mutate({
|
|
275
|
+
resourceId: scope.resourceId,
|
|
276
|
+
operation: "settings.change",
|
|
277
|
+
actor: request.actor,
|
|
278
|
+
expectedRevision: scope.revision,
|
|
279
|
+
}, "settings.changed", ({ database, nextRevision, now }) => {
|
|
280
|
+
this.requireRunAcceptsSettings(request.runId, true);
|
|
281
|
+
const current = this.requireSettingsScopeRow(request.scopeId);
|
|
282
|
+
if (current.activeRunId !== request.runId || !current.currentHash.equals(beforeHash)) {
|
|
283
|
+
throw new StaleResourceError("Workflow settings changed before the patch committed");
|
|
284
|
+
}
|
|
285
|
+
const patchHash = database.putJson(applied.patch, now);
|
|
286
|
+
const savedAfterHash = database.putJson(applied.json, now);
|
|
287
|
+
const changeId = `setting-change-${randomUUID()}`;
|
|
288
|
+
database.connection
|
|
289
|
+
.prepare(`INSERT INTO workflow_setting_changes(
|
|
290
|
+
change_id, scope_id, request_id, change_number,
|
|
291
|
+
actor_type, actor_id, source_type, patch_hash,
|
|
292
|
+
before_hash, after_hash, accepted_at
|
|
293
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
294
|
+
.run(changeId, request.scopeId, request.requestId, nextRevision, request.actor.type, request.actor.id ?? null, request.source, patchHash, beforeHash, savedAfterHash, now);
|
|
295
|
+
database.connection
|
|
296
|
+
.prepare(`UPDATE workflow_settings SET current_hash = ?, updated_at = ? WHERE scope_id = ?`)
|
|
297
|
+
.run(savedAfterHash, now, request.scopeId);
|
|
298
|
+
return changeId;
|
|
299
|
+
}, {
|
|
300
|
+
payload: {
|
|
301
|
+
scopeId: request.scopeId,
|
|
302
|
+
requestId: request.requestId,
|
|
303
|
+
source: request.source,
|
|
304
|
+
beforeHash: beforeHash.toString("hex"),
|
|
305
|
+
afterHash: afterHash.toString("hex"),
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
const row = this.settingsChangeRow(request.scopeId, request.requestId);
|
|
309
|
+
if (row === undefined || row.changeId !== mutation.value) {
|
|
310
|
+
throw new Error("Accepted workflow settings change is missing");
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
scope: this.settingsScopeRecord(this.requireSettingsScopeRow(request.scopeId)),
|
|
314
|
+
change: this.settingsChangeRecord(row),
|
|
315
|
+
adopted: false,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
if (!(error instanceof StaleResourceError) || request.expectedChangeNumber !== undefined) {
|
|
320
|
+
throw error;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
throw new StaleResourceError("Workflow settings kept changing; retry with an expected number");
|
|
325
|
+
}
|
|
326
|
+
queueFollowUp(request) {
|
|
327
|
+
assertRequestId(request.requestId);
|
|
328
|
+
assertSourceType(request.source);
|
|
329
|
+
if (typeof request.prompt !== "string" || request.prompt.trim().length === 0) {
|
|
330
|
+
throw new Error("Workflow follow-up prompt must be a non-empty string");
|
|
331
|
+
}
|
|
332
|
+
if (Buffer.byteLength(request.prompt, "utf8") > 64 * 1024) {
|
|
333
|
+
throw new Error("Workflow follow-up prompt cannot exceed 65536 bytes");
|
|
334
|
+
}
|
|
335
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
336
|
+
const prior = this.followUpRowByRequest(request.runId, request.requestId);
|
|
337
|
+
if (prior !== undefined) {
|
|
338
|
+
this.assertSameFollowUp(prior, request);
|
|
339
|
+
return { followUp: this.followUpRecord(prior), adopted: true };
|
|
340
|
+
}
|
|
341
|
+
this.requireRunAcceptsSettings(request.runId, true);
|
|
342
|
+
const queue = this.requireFollowUpQueueRow(request.runId);
|
|
343
|
+
try {
|
|
344
|
+
const result = this.mutations.mutate({
|
|
345
|
+
resourceId: queue.resourceId,
|
|
346
|
+
operation: "follow-up.queue",
|
|
347
|
+
actor: request.actor,
|
|
348
|
+
expectedRevision: queue.revision,
|
|
349
|
+
}, "follow-up.queued", ({ database, now }) => {
|
|
350
|
+
this.requireRunAcceptsSettings(request.runId, true);
|
|
351
|
+
const currentQueue = this.requireFollowUpQueueRow(request.runId);
|
|
352
|
+
if (currentQueue.originSessionId !== null &&
|
|
353
|
+
currentQueue.originSessionId !== request.targetSessionId) {
|
|
354
|
+
throw new Error("Workflow follow-ups must target the run's origin Pi session");
|
|
355
|
+
}
|
|
356
|
+
if (currentQueue.originSessionId === null) {
|
|
357
|
+
database.connection
|
|
358
|
+
.prepare(`UPDATE workflow_follow_up_queues
|
|
359
|
+
SET origin_session_id = ?, updated_at = ? WHERE run_id = ?`)
|
|
360
|
+
.run(request.targetSessionId, now, request.runId);
|
|
361
|
+
}
|
|
362
|
+
const order = this.nextFollowUpOrder(request.runId);
|
|
363
|
+
const followUpId = followUpIdFor(request.runId, request.requestId);
|
|
364
|
+
const resourceId = this.mutations.ensureResource("follow_up", followUpId, now);
|
|
365
|
+
const promptHash = database.putText(request.prompt, now);
|
|
366
|
+
database.connection
|
|
367
|
+
.prepare(`INSERT INTO workflow_follow_ups(
|
|
368
|
+
follow_up_id, resource_id, queue_resource_id, run_id,
|
|
369
|
+
request_id, order_number, target_session_id,
|
|
370
|
+
actor_type, actor_id, source_type, prompt_hash, status,
|
|
371
|
+
created_at, updated_at
|
|
372
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'queued', ?, ?)`)
|
|
373
|
+
.run(followUpId, resourceId, queue.resourceId, request.runId, request.requestId, order, request.targetSessionId, request.actor.type, request.actor.id ?? null, request.source, promptHash, now, now);
|
|
374
|
+
return followUpId;
|
|
375
|
+
}, { payload: { runId: request.runId, requestId: request.requestId } });
|
|
376
|
+
return {
|
|
377
|
+
followUp: this.followUpRecord(this.requireFollowUpRow(result.value)),
|
|
378
|
+
adopted: false,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
if (!(error instanceof StaleResourceError))
|
|
383
|
+
throw error;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
throw new StaleResourceError("Workflow follow-up queue kept changing; retry the request");
|
|
387
|
+
}
|
|
388
|
+
removeFollowUp(request) {
|
|
389
|
+
assertSourceType(request.source);
|
|
390
|
+
const row = this.requireFollowUpRow(request.followUpId);
|
|
391
|
+
if (row.runId !== request.runId) {
|
|
392
|
+
throw new Error(`Workflow follow-up is not part of run ${request.runId}`);
|
|
393
|
+
}
|
|
394
|
+
if (row.status === "removed")
|
|
395
|
+
return this.followUpRecord(row);
|
|
396
|
+
if (row.status === "sent" || row.status === "cancelled") {
|
|
397
|
+
throw new Error(`Workflow follow-up cannot be removed after it is ${row.status}`);
|
|
398
|
+
}
|
|
399
|
+
this.assertFollowUpRemovalAuthority(row, request.actor, request.source);
|
|
400
|
+
if (this.hasLiveLease(row.resourceId)) {
|
|
401
|
+
throw new Error("Workflow follow-up is being delivered and cannot be removed");
|
|
402
|
+
}
|
|
403
|
+
const reason = "Removed before delivery";
|
|
404
|
+
this.mutations.mutate({
|
|
405
|
+
resourceId: row.resourceId,
|
|
406
|
+
operation: "follow-up.remove",
|
|
407
|
+
actor: request.actor,
|
|
408
|
+
expectedRevision: row.revision,
|
|
409
|
+
}, "follow-up.removed", ({ database, now }) => {
|
|
410
|
+
const reasonHash = database.putText(reason, now);
|
|
411
|
+
const update = database.connection
|
|
412
|
+
.prepare(`UPDATE workflow_follow_ups
|
|
413
|
+
SET status = 'removed', reason_hash = ?, updated_at = ?
|
|
414
|
+
WHERE follow_up_id = ? AND status IN ('queued', 'pending_presentation', 'ready')`)
|
|
415
|
+
.run(reasonHash, now, request.followUpId);
|
|
416
|
+
if (update.changes !== 1) {
|
|
417
|
+
throw new StaleResourceError("Workflow follow-up changed before removal");
|
|
418
|
+
}
|
|
419
|
+
}, { payload: { followUpId: request.followUpId, source: request.source } });
|
|
420
|
+
return this.followUpRecord(this.requireFollowUpRow(request.followUpId));
|
|
421
|
+
}
|
|
422
|
+
originSessionId(runId) {
|
|
423
|
+
const row = this.state.connection
|
|
424
|
+
.prepare(`SELECT COALESCE(q.origin_session_id, b.origin_session_id) AS originSessionId
|
|
425
|
+
FROM runs r
|
|
426
|
+
LEFT JOIN workflow_follow_up_queues q ON q.run_id = r.run_id
|
|
427
|
+
LEFT JOIN run_bindings b ON b.run_id = r.run_id
|
|
428
|
+
WHERE r.run_id = ?`)
|
|
429
|
+
.get(runId);
|
|
430
|
+
return isRecord(row) && typeof row.originSessionId === "string"
|
|
431
|
+
? row.originSessionId
|
|
432
|
+
: undefined;
|
|
433
|
+
}
|
|
434
|
+
readFollowUpQueue(runId) {
|
|
435
|
+
const queue = this.followUpQueueRow(runId);
|
|
436
|
+
if (queue === undefined)
|
|
437
|
+
return undefined;
|
|
438
|
+
const followUps = this.state.connection
|
|
439
|
+
.prepare(`${FOLLOW_UP_ROW_SELECT} WHERE f.run_id = ? ORDER BY f.order_number`)
|
|
440
|
+
.all(runId)
|
|
441
|
+
.filter(isFollowUpRow)
|
|
442
|
+
.map((row) => this.followUpRecord(row));
|
|
443
|
+
return {
|
|
444
|
+
runId,
|
|
445
|
+
...(queue.originSessionId !== null ? { originSessionId: queue.originSessionId } : {}),
|
|
446
|
+
presentationState: assertPresentationState(queue.presentationState),
|
|
447
|
+
...(queue.presentationEntryId !== null
|
|
448
|
+
? { presentationEntryId: queue.presentationEntryId }
|
|
449
|
+
: {}),
|
|
450
|
+
...(queue.presentationAssistantEntryId !== null
|
|
451
|
+
? { presentationAssistantEntryId: queue.presentationAssistantEntryId }
|
|
452
|
+
: {}),
|
|
453
|
+
...(queue.presentationReasonHash !== null
|
|
454
|
+
? { presentationReason: this.readText(queue.presentationReasonHash) }
|
|
455
|
+
: {}),
|
|
456
|
+
followUps,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
listPendingPresentations(targetSessionId) {
|
|
460
|
+
return this.state.connection
|
|
461
|
+
.prepare(`SELECT run_id AS runId FROM workflow_follow_up_queues
|
|
462
|
+
WHERE origin_session_id = ? AND presentation_state = 'pending'
|
|
463
|
+
ORDER BY created_at`)
|
|
464
|
+
.all(targetSessionId)
|
|
465
|
+
.flatMap((row) => (isRecord(row) && typeof row.runId === "string" ? [row.runId] : []));
|
|
466
|
+
}
|
|
467
|
+
settleFollowUpPresentation(options) {
|
|
468
|
+
const queue = this.requireFollowUpQueueRow(options.runId);
|
|
469
|
+
if (queue.presentationState === options.state) {
|
|
470
|
+
return this.readFollowUpQueue(options.runId);
|
|
471
|
+
}
|
|
472
|
+
if (queue.presentationState !== "pending") {
|
|
473
|
+
throw new Error(`Workflow presentation is ${queue.presentationState}, not pending for ${options.runId}`);
|
|
474
|
+
}
|
|
475
|
+
if (options.state === "settled" &&
|
|
476
|
+
(options.presentationEntryId === undefined || options.assistantEntryId === undefined)) {
|
|
477
|
+
throw new Error("Settled workflow presentation requires both session entry IDs");
|
|
478
|
+
}
|
|
479
|
+
if (options.state === "unavailable" && !options.reason?.trim()) {
|
|
480
|
+
throw new Error("Unavailable workflow presentation requires a reason");
|
|
481
|
+
}
|
|
482
|
+
try {
|
|
483
|
+
this.mutations.mutate({
|
|
484
|
+
resourceId: queue.resourceId,
|
|
485
|
+
operation: "follow-up.presentation",
|
|
486
|
+
actor: {
|
|
487
|
+
type: "session",
|
|
488
|
+
...(queue.originSessionId !== null ? { id: queue.originSessionId } : {}),
|
|
489
|
+
},
|
|
490
|
+
expectedRevision: queue.revision,
|
|
491
|
+
}, `follow-up.presentation-${options.state}`, ({ database, now }) => {
|
|
492
|
+
const reasonHash = options.state === "unavailable"
|
|
493
|
+
? database.putText(options.reason, now)
|
|
494
|
+
: null;
|
|
495
|
+
database.connection
|
|
496
|
+
.prepare(`UPDATE workflow_follow_up_queues
|
|
497
|
+
SET presentation_state = ?, presentation_entry_id = ?,
|
|
498
|
+
presentation_assistant_entry_id = ?, presentation_reason_hash = ?,
|
|
499
|
+
presentation_updated_at = ?, updated_at = ?
|
|
500
|
+
WHERE run_id = ? AND presentation_state = 'pending'`)
|
|
501
|
+
.run(options.state, options.presentationEntryId ?? null, options.assistantEntryId ?? null, reasonHash, now, now, options.runId);
|
|
502
|
+
database.connection
|
|
503
|
+
.prepare(`UPDATE workflow_follow_ups SET status = 'ready', updated_at = ?
|
|
504
|
+
WHERE run_id = ? AND status = 'pending_presentation'`)
|
|
505
|
+
.run(now, options.runId);
|
|
506
|
+
}, { payload: { runId: options.runId, state: options.state } });
|
|
507
|
+
}
|
|
508
|
+
catch (error) {
|
|
509
|
+
if (error instanceof StaleResourceError) {
|
|
510
|
+
const current = this.requireFollowUpQueueRow(options.runId);
|
|
511
|
+
if (current.presentationState === options.state) {
|
|
512
|
+
return this.readFollowUpQueue(options.runId);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
throw error;
|
|
516
|
+
}
|
|
517
|
+
return this.readFollowUpQueue(options.runId);
|
|
518
|
+
}
|
|
519
|
+
claimNextFollowUp(targetSessionId, ownerId, leaseMs = 30_000) {
|
|
520
|
+
const rows = this.state.connection
|
|
521
|
+
.prepare(`${FOLLOW_UP_ROW_SELECT}
|
|
522
|
+
WHERE f.target_session_id = ? AND f.status = 'ready'
|
|
523
|
+
ORDER BY f.created_at, f.order_number LIMIT 16`)
|
|
524
|
+
.all(targetSessionId)
|
|
525
|
+
.filter(isFollowUpRow);
|
|
526
|
+
for (const row of rows) {
|
|
527
|
+
let claim;
|
|
528
|
+
try {
|
|
529
|
+
claim = this.mutations.claim({
|
|
530
|
+
resourceId: row.resourceId,
|
|
531
|
+
ownerType: "session",
|
|
532
|
+
ownerId,
|
|
533
|
+
expectedRevision: row.revision,
|
|
534
|
+
leaseMs,
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
catch (error) {
|
|
538
|
+
if (error instanceof StaleResourceError)
|
|
539
|
+
continue;
|
|
540
|
+
throw error;
|
|
541
|
+
}
|
|
542
|
+
if (claim === undefined)
|
|
543
|
+
continue;
|
|
544
|
+
const current = this.requireFollowUpRow(row.followUpId);
|
|
545
|
+
if (current.status !== "ready") {
|
|
546
|
+
this.mutations.release(claim, claim.resourceRevision);
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
return { followUp: this.followUpRecord(current), claim };
|
|
550
|
+
}
|
|
551
|
+
return undefined;
|
|
552
|
+
}
|
|
553
|
+
markFollowUpSent(followUpId, claim, sessionEntryId) {
|
|
554
|
+
const row = this.requireFollowUpRow(followUpId);
|
|
555
|
+
if (row.status === "sent")
|
|
556
|
+
return this.followUpRecord(row);
|
|
557
|
+
this.mutations.mutate({
|
|
558
|
+
resourceId: row.resourceId,
|
|
559
|
+
operation: "follow-up.sent",
|
|
560
|
+
actor: { type: "session", id: claim.ownerId },
|
|
561
|
+
expectedRevision: claim.resourceRevision,
|
|
562
|
+
lease: claim,
|
|
563
|
+
}, "follow-up.sent", ({ database, now }) => {
|
|
564
|
+
const update = database.connection
|
|
565
|
+
.prepare(`UPDATE workflow_follow_ups
|
|
566
|
+
SET status = 'sent', session_entry_id = ?, sent_at = ?, updated_at = ?
|
|
567
|
+
WHERE follow_up_id = ? AND status = 'ready'`)
|
|
568
|
+
.run(sessionEntryId, now, now, followUpId);
|
|
569
|
+
if (update.changes !== 1) {
|
|
570
|
+
throw new StaleResourceError("Workflow follow-up changed before delivery completed");
|
|
571
|
+
}
|
|
572
|
+
}, { payload: { followUpId, sessionEntryId } });
|
|
573
|
+
return this.followUpRecord(this.requireFollowUpRow(followUpId));
|
|
574
|
+
}
|
|
575
|
+
releaseFollowUpClaim(claim) {
|
|
576
|
+
const revision = this.requireResourceRevision(claim.resourceId);
|
|
577
|
+
this.mutations.release(claim, revision);
|
|
578
|
+
}
|
|
128
579
|
async prepareRunResume(runId) {
|
|
129
580
|
const loaded = this.readRun(runId, { includeTrace: true });
|
|
130
581
|
if (loaded === null)
|
|
@@ -132,6 +583,7 @@ export class WorkflowRunStore {
|
|
|
132
583
|
if (loaded.state.status !== "running") {
|
|
133
584
|
throw new Error(`Cannot resume workflow run ${runId} with status ${loaded.state.status}`);
|
|
134
585
|
}
|
|
586
|
+
this.verifySettingsProjections(runId);
|
|
135
587
|
const revision = this.resourceRevision(runId);
|
|
136
588
|
this.contexts.set(runId, { revision, lock: Promise.resolve() });
|
|
137
589
|
this.finalizeRecordingCaptures(runId, "Workflow host stopped before the run finished");
|
|
@@ -144,9 +596,9 @@ export class WorkflowRunStore {
|
|
|
144
596
|
const at = new Date(now).toISOString();
|
|
145
597
|
this.state.connection
|
|
146
598
|
.prepare(`UPDATE node_attempts
|
|
147
|
-
SET status = '
|
|
599
|
+
SET status = 'interrupted', updated_at = ?
|
|
148
600
|
WHERE run_id = ? AND status IN ('pending', 'running', 'waiting')`)
|
|
149
|
-
.run(now,
|
|
601
|
+
.run(now, runId);
|
|
150
602
|
insertRunEvent(this.state, row.resourceId, nextRevision, at, {
|
|
151
603
|
scope: "run",
|
|
152
604
|
type: "run_resume_prepared",
|
|
@@ -173,6 +625,9 @@ export class WorkflowRunStore {
|
|
|
173
625
|
delete loaded.state.currentNode;
|
|
174
626
|
delete loaded.state.currentAttemptId;
|
|
175
627
|
delete loaded.state.currentNodeStartedAt;
|
|
628
|
+
delete loaded.state.currentSettingsScopeId;
|
|
629
|
+
delete loaded.state.currentSettingsChangeNumber;
|
|
630
|
+
delete loaded.state.currentSettingsHash;
|
|
176
631
|
delete loaded.state.statusDetail;
|
|
177
632
|
delete loaded.state.paused;
|
|
178
633
|
await this.writeSnapshot(runId, loaded.state, {
|
|
@@ -230,9 +685,10 @@ export class WorkflowRunStore {
|
|
|
230
685
|
const nextUpdateSeq = this.nextUpdateSequence(attemptId);
|
|
231
686
|
this.state.connection
|
|
232
687
|
.prepare(`INSERT INTO workflow_updates(
|
|
233
|
-
update_id, attempt_id, update_seq,
|
|
234
|
-
|
|
235
|
-
|
|
688
|
+
update_id, attempt_id, update_seq, run_revision,
|
|
689
|
+
update_type, update_key, data_hash, recorded_at
|
|
690
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
691
|
+
.run(updateId, attemptId, nextUpdateSeq, revision, update.type, update.key, dataHash, Date.now());
|
|
236
692
|
insertRunEvent(this.state, run.resourceId, revision, at, event);
|
|
237
693
|
this.persistRunState(run, state, revision, Date.now());
|
|
238
694
|
context.revision = revision;
|
|
@@ -255,12 +711,14 @@ export class WorkflowRunStore {
|
|
|
255
711
|
const revision = context.revision + 1;
|
|
256
712
|
const now = Date.now();
|
|
257
713
|
const at = new Date(now).toISOString();
|
|
714
|
+
const snapshot = this.readDefinition(run.definitionHash);
|
|
715
|
+
this.assertSettingsRouteCurrent(event, snapshot);
|
|
258
716
|
const traceEvent = { seq: revision, at, runId, ...event };
|
|
259
717
|
state.traceSeq = revision;
|
|
260
718
|
state.updatedAt = at;
|
|
261
719
|
insertRunEvent(this.state, run.resourceId, revision, at, traceEvent);
|
|
262
720
|
this.persistRunState(run, state, revision, now);
|
|
263
|
-
|
|
721
|
+
this.transitionFollowUpsForRunState(state, event, now);
|
|
264
722
|
this.syncNodeAttempts(state, snapshot, now);
|
|
265
723
|
context.revision = revision;
|
|
266
724
|
accepted = traceEvent;
|
|
@@ -316,6 +774,11 @@ export class WorkflowRunStore {
|
|
|
316
774
|
VALUES (?, ?, 'interactive', ?)
|
|
317
775
|
ON CONFLICT(run_id) DO NOTHING`)
|
|
318
776
|
.run(runId, binding.piSessionId, now);
|
|
777
|
+
this.state.connection
|
|
778
|
+
.prepare(`UPDATE workflow_follow_up_queues
|
|
779
|
+
SET origin_session_id = COALESCE(origin_session_id, ?), updated_at = ?
|
|
780
|
+
WHERE run_id = ?`)
|
|
781
|
+
.run(binding.piSessionId, now, runId);
|
|
319
782
|
const revision = context.revision + 1;
|
|
320
783
|
const at = new Date(now).toISOString();
|
|
321
784
|
insertRunEvent(this.state, run.resourceId, revision, at, {
|
|
@@ -329,7 +792,7 @@ export class WorkflowRunStore {
|
|
|
329
792
|
...(attemptId !== undefined ? { captureAttemptId: attemptId } : {}),
|
|
330
793
|
},
|
|
331
794
|
});
|
|
332
|
-
const current = this.
|
|
795
|
+
const current = this.readRunState(run, this.readDefinition(run.definitionHash));
|
|
333
796
|
current.traceSeq = revision;
|
|
334
797
|
current.updatedAt = at;
|
|
335
798
|
this.persistRunState(run, current, revision, now);
|
|
@@ -354,7 +817,7 @@ export class WorkflowRunStore {
|
|
|
354
817
|
.prepare("UPDATE session_segments SET entry_count = ? WHERE segment_id = ?")
|
|
355
818
|
.run(sequence, segment.segmentId);
|
|
356
819
|
this.bumpResource(segmentResourceId(segment), revision, now);
|
|
357
|
-
insertGenericEvent(this.state, segmentResourceId(segment), revision + 1, "session.entry_appended", "session", segment.sessionId,
|
|
820
|
+
insertGenericEvent(this.state, segmentResourceId(segment), revision + 1, "session.entry_appended", "session", segment.sessionId, null, now);
|
|
358
821
|
return sequence;
|
|
359
822
|
});
|
|
360
823
|
}
|
|
@@ -385,15 +848,9 @@ export class WorkflowRunStore {
|
|
|
385
848
|
.run(current.segmentId, record.seq, record.type, record.nodeId, record.attemptId, record.turnId ?? null, record.messageId ?? null, record.toolCallId ?? null, payloadHash, Date.parse(record.at));
|
|
386
849
|
expected += 1;
|
|
387
850
|
}
|
|
388
|
-
const now = Date.now();
|
|
389
851
|
this.state.connection
|
|
390
852
|
.prepare("UPDATE session_segments SET event_count = ? WHERE segment_id = ?")
|
|
391
853
|
.run(expected - 1, current.segmentId);
|
|
392
|
-
const resourceId = segmentResourceId(current);
|
|
393
|
-
const revision = this.requireResourceRevision(resourceId);
|
|
394
|
-
this.bumpResource(resourceId, revision, now);
|
|
395
|
-
const payloadHash = this.state.putJson({ count: records.length, lastSeq: expected - 1 }, now);
|
|
396
|
-
insertGenericEvent(this.state, resourceId, revision + 1, "session.events_appended", "session", current.sessionId, payloadHash, now);
|
|
397
854
|
});
|
|
398
855
|
}
|
|
399
856
|
async writeSessionCapture(runId, capture, attemptId) {
|
|
@@ -432,8 +889,8 @@ export class WorkflowRunStore {
|
|
|
432
889
|
const row = this.readRunRow(runId);
|
|
433
890
|
if (row === undefined)
|
|
434
891
|
return null;
|
|
435
|
-
const state = this.readState(row.stateHash);
|
|
436
892
|
const snapshot = this.readDefinition(row.definitionHash);
|
|
893
|
+
const state = this.readRunState(row, snapshot);
|
|
437
894
|
const segments = this.segmentRows(runId).map((segment) => this.loadSegment(segment, state));
|
|
438
895
|
const flat = segments.find((segment) => segment.attemptId === "") ?? emptySegment();
|
|
439
896
|
return {
|
|
@@ -447,6 +904,8 @@ export class WorkflowRunStore {
|
|
|
447
904
|
sessionCapture: flat.capture,
|
|
448
905
|
sessionIntegrity: flat.integrity,
|
|
449
906
|
sessionSegments: segments.filter((segment) => segment.attemptId !== ""),
|
|
907
|
+
settingsScopes: this.settingsScopesForRunView(runId),
|
|
908
|
+
followUpQueue: this.readFollowUpQueue(runId) ?? null,
|
|
450
909
|
};
|
|
451
910
|
}
|
|
452
911
|
listRuns(options = {}) {
|
|
@@ -551,20 +1010,19 @@ export class WorkflowRunStore {
|
|
|
551
1010
|
}
|
|
552
1011
|
persistRunState(run, state, expectedRevision, now) {
|
|
553
1012
|
this.bumpResource(run.resourceId, expectedRevision - 1, now);
|
|
554
|
-
const
|
|
1013
|
+
const finalOutputHash = state.finalOutput === undefined ? null : this.state.putJson(state.finalOutput, now);
|
|
555
1014
|
const errorHash = state.error === undefined ? null : this.state.putText(state.error, now);
|
|
556
1015
|
const update = this.state.connection
|
|
557
1016
|
.prepare(`UPDATE runs
|
|
558
|
-
SET title = ?, status = ?, paused = ?, status_detail = ?,
|
|
559
|
-
error_hash = ?, updated_at = ?, finished_at = ?
|
|
1017
|
+
SET title = ?, status = ?, paused = ?, status_detail = ?,
|
|
1018
|
+
final_output_hash = ?, error_hash = ?, updated_at = ?, finished_at = ?
|
|
560
1019
|
WHERE run_id = ?`)
|
|
561
|
-
.run(state.runTitle ?? null, state.status, state.paused === true ? 1 : 0, state.statusDetail ?? null,
|
|
1020
|
+
.run(state.runTitle ?? null, state.status, state.paused === true ? 1 : 0, state.statusDetail ?? null, finalOutputHash, errorHash, now, state.finishedAt === undefined ? null : Date.parse(state.finishedAt), state.runId);
|
|
562
1021
|
if (update.changes !== 1)
|
|
563
1022
|
throw new Error(`Workflow run is missing: ${state.runId}`);
|
|
564
1023
|
if (state.status !== "running") {
|
|
565
1024
|
this.enqueueRunSettlementEffect(run.resourceId, expectedRevision, state, now);
|
|
566
1025
|
}
|
|
567
|
-
run.stateHash = stateHash;
|
|
568
1026
|
}
|
|
569
1027
|
enqueueRunSettlementEffect(runResourceId, sourceRevision, state, now) {
|
|
570
1028
|
if (this.state.connection.prepare("SELECT 1 FROM run_queue WHERE run_id = ?").get(state.runId) ===
|
|
@@ -600,12 +1058,341 @@ export class WorkflowRunStore {
|
|
|
600
1058
|
.run(effectId, effectResourceId, runResourceId, sourceRevision, effectType, idempotencyKey, payloadHash, now, now);
|
|
601
1059
|
insertGenericEvent(this.state, effectResourceId, 1, "effect.created", "system", null, payloadHash, now);
|
|
602
1060
|
}
|
|
1061
|
+
initializeRunSettingsAndFollowUps(state, initialSettings, now) {
|
|
1062
|
+
const childQueueResourceId = this.ensureFollowUpQueue(state.runId, now);
|
|
1063
|
+
if (state.parentRunId !== undefined) {
|
|
1064
|
+
const parentQueue = this.followUpQueueRow(state.parentRunId);
|
|
1065
|
+
if (parentQueue !== undefined) {
|
|
1066
|
+
this.state.connection
|
|
1067
|
+
.prepare(`UPDATE workflow_follow_up_queues
|
|
1068
|
+
SET origin_session_id = COALESCE(origin_session_id, ?), updated_at = ?
|
|
1069
|
+
WHERE run_id = ?`)
|
|
1070
|
+
.run(parentQueue.originSessionId, now, state.runId);
|
|
1071
|
+
this.state.connection
|
|
1072
|
+
.prepare(`UPDATE workflow_follow_ups
|
|
1073
|
+
SET run_id = ?, queue_resource_id = ?, updated_at = ?
|
|
1074
|
+
WHERE run_id = ? AND status = 'queued'`)
|
|
1075
|
+
.run(state.runId, childQueueResourceId, now, state.parentRunId);
|
|
1076
|
+
}
|
|
1077
|
+
this.state.connection
|
|
1078
|
+
.prepare(`UPDATE workflow_settings SET active_run_id = ?, updated_at = ? WHERE active_run_id = ?`)
|
|
1079
|
+
.run(state.runId, now, state.parentRunId);
|
|
1080
|
+
}
|
|
1081
|
+
const existing = this.state.connection
|
|
1082
|
+
.prepare("SELECT COUNT(*) AS count FROM workflow_settings WHERE active_run_id = ?")
|
|
1083
|
+
.get(state.runId);
|
|
1084
|
+
const hasSettings = isCountRow(existing) && existing.count > 0;
|
|
1085
|
+
if (hasSettings)
|
|
1086
|
+
return;
|
|
1087
|
+
for (const scope of initialSettings) {
|
|
1088
|
+
const originRunId = this.logicalOriginRunId(state.runId);
|
|
1089
|
+
const scopeId = workflowSettingsScopeId(originRunId, scope.mountPath, scope.invocation);
|
|
1090
|
+
const resourceId = this.mutations.ensureResource("settings", scopeId, now);
|
|
1091
|
+
const settingsHash = this.state.putJson(scope.settings, now);
|
|
1092
|
+
this.state.connection
|
|
1093
|
+
.prepare(`INSERT INTO workflow_settings(
|
|
1094
|
+
scope_id, resource_id, origin_run_id, active_run_id, mount_path, invocation,
|
|
1095
|
+
initial_hash, current_hash, created_at, updated_at
|
|
1096
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1097
|
+
ON CONFLICT(scope_id) DO NOTHING`)
|
|
1098
|
+
.run(scopeId, resourceId, originRunId, state.runId, scope.mountPath, scope.invocation, settingsHash, settingsHash, now, now);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
ensureFollowUpQueue(runId, now) {
|
|
1102
|
+
const resourceId = this.mutations.ensureResource("follow_up_queue", runId, now);
|
|
1103
|
+
this.state.connection
|
|
1104
|
+
.prepare(`INSERT INTO workflow_follow_up_queues(
|
|
1105
|
+
run_id, resource_id, created_at, updated_at
|
|
1106
|
+
) VALUES (?, ?, ?, ?)
|
|
1107
|
+
ON CONFLICT(run_id) DO NOTHING`)
|
|
1108
|
+
.run(runId, resourceId, now, now);
|
|
1109
|
+
return resourceId;
|
|
1110
|
+
}
|
|
1111
|
+
logicalOriginRunId(runId) {
|
|
1112
|
+
let current = runId;
|
|
1113
|
+
for (let depth = 0; depth < 100; depth += 1) {
|
|
1114
|
+
const row = this.state.connection
|
|
1115
|
+
.prepare("SELECT parent_run_id AS parentRunId FROM runs WHERE run_id = ?")
|
|
1116
|
+
.get(current);
|
|
1117
|
+
if (!isParentRunRow(row))
|
|
1118
|
+
return current;
|
|
1119
|
+
if (row.parentRunId === null)
|
|
1120
|
+
return current;
|
|
1121
|
+
current = row.parentRunId;
|
|
1122
|
+
}
|
|
1123
|
+
throw new Error(`Workflow continuation chain is too deep for run ${runId}`);
|
|
1124
|
+
}
|
|
1125
|
+
requireRunAcceptsSettings(runId, allowWaiting) {
|
|
1126
|
+
const row = this.state.connection
|
|
1127
|
+
.prepare("SELECT status FROM runs WHERE run_id = ?")
|
|
1128
|
+
.get(runId);
|
|
1129
|
+
if (!isStatusRow(row))
|
|
1130
|
+
throw new Error(`Workflow run is missing: ${runId}`);
|
|
1131
|
+
if (row.status !== "running" && !(allowWaiting && row.status === "waiting")) {
|
|
1132
|
+
throw new Error(`Workflow run ${runId} does not accept changes with status ${row.status}`);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
verifySettingsProjections(runId) {
|
|
1136
|
+
const scopes = this.state.connection
|
|
1137
|
+
.prepare(`SELECT s.scope_id AS scopeId, s.initial_hash AS initialHash,
|
|
1138
|
+
s.current_hash AS currentHash, r.revision
|
|
1139
|
+
FROM workflow_settings s
|
|
1140
|
+
JOIN resources r ON r.resource_id = s.resource_id
|
|
1141
|
+
WHERE s.active_run_id = ?`)
|
|
1142
|
+
.all(runId);
|
|
1143
|
+
for (const value of scopes) {
|
|
1144
|
+
if (!isSettingsProjectionRow(value)) {
|
|
1145
|
+
throw new Error("Workflow settings projection row is invalid");
|
|
1146
|
+
}
|
|
1147
|
+
let current = this.state.readJson(value.initialHash);
|
|
1148
|
+
let currentHash = value.initialHash;
|
|
1149
|
+
const changes = this.state.connection
|
|
1150
|
+
.prepare(`SELECT change_number AS changeNumber, patch_hash AS patchHash,
|
|
1151
|
+
before_hash AS beforeHash, after_hash AS afterHash
|
|
1152
|
+
FROM workflow_setting_changes WHERE scope_id = ? ORDER BY change_number`)
|
|
1153
|
+
.all(value.scopeId);
|
|
1154
|
+
let expected = 1;
|
|
1155
|
+
for (const change of changes) {
|
|
1156
|
+
if (!isSettingsProjectionChangeRow(change) || change.changeNumber !== expected) {
|
|
1157
|
+
throw new Error(`Workflow settings changes are not contiguous for ${value.scopeId}`);
|
|
1158
|
+
}
|
|
1159
|
+
if (!change.beforeHash.equals(currentHash)) {
|
|
1160
|
+
throw new Error(`Workflow settings change has a wrong old value for ${value.scopeId}`);
|
|
1161
|
+
}
|
|
1162
|
+
current = applyJsonPatch(current, this.state.readJson(change.patchHash));
|
|
1163
|
+
currentHash = createHash("sha256").update(canonicalJson(current)).digest();
|
|
1164
|
+
if (!currentHash.equals(change.afterHash)) {
|
|
1165
|
+
throw new Error(`Workflow settings change has a wrong new value for ${value.scopeId}`);
|
|
1166
|
+
}
|
|
1167
|
+
expected += 1;
|
|
1168
|
+
}
|
|
1169
|
+
if (value.revision !== expected - 1 || !value.currentHash.equals(currentHash)) {
|
|
1170
|
+
throw new Error(`Workflow settings projection does not match saved changes for ${value.scopeId}`);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
settingsScopeRow(scopeId) {
|
|
1175
|
+
const row = this.state.connection
|
|
1176
|
+
.prepare(`SELECT s.scope_id AS scopeId, s.resource_id AS resourceId,
|
|
1177
|
+
s.origin_run_id AS originRunId, s.active_run_id AS activeRunId,
|
|
1178
|
+
s.mount_path AS mountPath, s.invocation,
|
|
1179
|
+
s.current_hash AS currentHash, r.revision,
|
|
1180
|
+
s.created_at AS createdAt, s.updated_at AS updatedAt
|
|
1181
|
+
FROM workflow_settings s
|
|
1182
|
+
JOIN resources r ON r.resource_id = s.resource_id
|
|
1183
|
+
WHERE s.scope_id = ?`)
|
|
1184
|
+
.get(scopeId);
|
|
1185
|
+
return isSettingsScopeRow(row) ? row : undefined;
|
|
1186
|
+
}
|
|
1187
|
+
requireSettingsScopeRow(scopeId) {
|
|
1188
|
+
const row = this.settingsScopeRow(scopeId);
|
|
1189
|
+
if (row === undefined)
|
|
1190
|
+
throw new Error(`Workflow settings scope not found: ${scopeId}`);
|
|
1191
|
+
return row;
|
|
1192
|
+
}
|
|
1193
|
+
settingsScopeRecord(row, changeNumber = row.revision, settingsHash = row.currentHash) {
|
|
1194
|
+
return {
|
|
1195
|
+
scopeId: row.scopeId,
|
|
1196
|
+
originRunId: row.originRunId,
|
|
1197
|
+
activeRunId: row.activeRunId,
|
|
1198
|
+
mountPath: row.mountPath,
|
|
1199
|
+
invocation: row.invocation,
|
|
1200
|
+
changeNumber,
|
|
1201
|
+
settings: this.state.readJson(settingsHash),
|
|
1202
|
+
settingsHash: settingsHash.toString("hex"),
|
|
1203
|
+
createdAt: new Date(row.createdAt).toISOString(),
|
|
1204
|
+
updatedAt: new Date(row.updatedAt).toISOString(),
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
settingsChangeRow(scopeId, requestId) {
|
|
1208
|
+
const row = this.state.connection
|
|
1209
|
+
.prepare(`SELECT change_id AS changeId, scope_id AS scopeId, request_id AS requestId,
|
|
1210
|
+
change_number AS changeNumber, actor_type AS actorType, actor_id AS actorId,
|
|
1211
|
+
source_type AS sourceType, patch_hash AS patchHash,
|
|
1212
|
+
before_hash AS beforeHash, after_hash AS afterHash,
|
|
1213
|
+
accepted_at AS acceptedAt
|
|
1214
|
+
FROM workflow_setting_changes WHERE scope_id = ? AND request_id = ?`)
|
|
1215
|
+
.get(scopeId, requestId);
|
|
1216
|
+
return isSettingsChangeRow(row) ? row : undefined;
|
|
1217
|
+
}
|
|
1218
|
+
settingsChangeRecord(row) {
|
|
1219
|
+
return {
|
|
1220
|
+
changeId: row.changeId,
|
|
1221
|
+
scopeId: row.scopeId,
|
|
1222
|
+
requestId: row.requestId,
|
|
1223
|
+
changeNumber: row.changeNumber,
|
|
1224
|
+
actor: {
|
|
1225
|
+
type: assertActorType(row.actorType),
|
|
1226
|
+
...(row.actorId !== null ? { id: row.actorId } : {}),
|
|
1227
|
+
},
|
|
1228
|
+
source: row.sourceType,
|
|
1229
|
+
patch: validateJsonPatch(this.state.readJson(row.patchHash)),
|
|
1230
|
+
beforeHash: row.beforeHash.toString("hex"),
|
|
1231
|
+
afterHash: row.afterHash.toString("hex"),
|
|
1232
|
+
acceptedAt: new Date(row.acceptedAt).toISOString(),
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
assertSameSettingsChange(row, request, patchDigest) {
|
|
1236
|
+
if (!row.patchHash.equals(patchDigest) ||
|
|
1237
|
+
row.actorType !== request.actor.type ||
|
|
1238
|
+
row.actorId !== (request.actor.id ?? null) ||
|
|
1239
|
+
row.sourceType !== request.source) {
|
|
1240
|
+
throw new Error(`Workflow settings request ID was reused with different content`);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
followUpQueueRow(runId) {
|
|
1244
|
+
const row = this.state.connection
|
|
1245
|
+
.prepare(`SELECT q.run_id AS runId, q.resource_id AS resourceId,
|
|
1246
|
+
q.origin_session_id AS originSessionId,
|
|
1247
|
+
q.presentation_state AS presentationState,
|
|
1248
|
+
q.presentation_entry_id AS presentationEntryId,
|
|
1249
|
+
q.presentation_assistant_entry_id AS presentationAssistantEntryId,
|
|
1250
|
+
q.presentation_reason_hash AS presentationReasonHash,
|
|
1251
|
+
r.revision
|
|
1252
|
+
FROM workflow_follow_up_queues q
|
|
1253
|
+
JOIN resources r ON r.resource_id = q.resource_id
|
|
1254
|
+
WHERE q.run_id = ?`)
|
|
1255
|
+
.get(runId);
|
|
1256
|
+
return isFollowUpQueueRow(row) ? row : undefined;
|
|
1257
|
+
}
|
|
1258
|
+
requireFollowUpQueueRow(runId) {
|
|
1259
|
+
const row = this.followUpQueueRow(runId);
|
|
1260
|
+
if (row === undefined)
|
|
1261
|
+
throw new Error(`Workflow follow-up queue not found: ${runId}`);
|
|
1262
|
+
return row;
|
|
1263
|
+
}
|
|
1264
|
+
followUpRowByRequest(runId, requestId) {
|
|
1265
|
+
const row = this.state.connection
|
|
1266
|
+
.prepare(`${FOLLOW_UP_ROW_SELECT} WHERE f.run_id = ? AND f.request_id = ?`)
|
|
1267
|
+
.get(runId, requestId);
|
|
1268
|
+
return isFollowUpRow(row) ? row : undefined;
|
|
1269
|
+
}
|
|
1270
|
+
followUpRow(followUpId) {
|
|
1271
|
+
const row = this.state.connection
|
|
1272
|
+
.prepare(`${FOLLOW_UP_ROW_SELECT} WHERE f.follow_up_id = ?`)
|
|
1273
|
+
.get(followUpId);
|
|
1274
|
+
return isFollowUpRow(row) ? row : undefined;
|
|
1275
|
+
}
|
|
1276
|
+
requireFollowUpRow(followUpId) {
|
|
1277
|
+
const row = this.followUpRow(followUpId);
|
|
1278
|
+
if (row === undefined)
|
|
1279
|
+
throw new Error(`Workflow follow-up not found: ${followUpId}`);
|
|
1280
|
+
return row;
|
|
1281
|
+
}
|
|
1282
|
+
followUpRecord(row) {
|
|
1283
|
+
return {
|
|
1284
|
+
followUpId: row.followUpId,
|
|
1285
|
+
runId: row.runId,
|
|
1286
|
+
requestId: row.requestId,
|
|
1287
|
+
order: row.orderNumber,
|
|
1288
|
+
targetSessionId: row.targetSessionId,
|
|
1289
|
+
actor: {
|
|
1290
|
+
type: assertActorType(row.actorType),
|
|
1291
|
+
...(row.actorId !== null ? { id: row.actorId } : {}),
|
|
1292
|
+
},
|
|
1293
|
+
source: row.sourceType,
|
|
1294
|
+
prompt: this.readText(row.promptHash),
|
|
1295
|
+
state: assertFollowUpState(row.status),
|
|
1296
|
+
...(row.sessionEntryId !== null ? { sessionEntryId: row.sessionEntryId } : {}),
|
|
1297
|
+
...(row.reasonHash !== null ? { reason: this.readText(row.reasonHash) } : {}),
|
|
1298
|
+
createdAt: new Date(row.createdAt).toISOString(),
|
|
1299
|
+
updatedAt: new Date(row.updatedAt).toISOString(),
|
|
1300
|
+
...(row.sentAt !== null ? { sentAt: new Date(row.sentAt).toISOString() } : {}),
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
assertSameFollowUp(row, request) {
|
|
1304
|
+
const promptHash = createHash("sha256").update(request.prompt).digest();
|
|
1305
|
+
if (!row.promptHash.equals(promptHash) ||
|
|
1306
|
+
row.targetSessionId !== request.targetSessionId ||
|
|
1307
|
+
row.actorType !== request.actor.type ||
|
|
1308
|
+
row.actorId !== (request.actor.id ?? null) ||
|
|
1309
|
+
row.sourceType !== request.source) {
|
|
1310
|
+
throw new Error("Workflow follow-up request ID was reused with different content");
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
assertFollowUpRemovalAuthority(row, actor, source) {
|
|
1314
|
+
if (actor.type === "human")
|
|
1315
|
+
return;
|
|
1316
|
+
if ((actor.type === "session" || actor.type === "controller") &&
|
|
1317
|
+
row.actorType === actor.type &&
|
|
1318
|
+
row.actorId === (actor.id ?? null) &&
|
|
1319
|
+
row.sourceType === source) {
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
throw new Error("Workflow follow-up can be removed only by its source or a verified human");
|
|
1323
|
+
}
|
|
1324
|
+
hasLiveLease(resourceId) {
|
|
1325
|
+
const row = this.state.connection
|
|
1326
|
+
.prepare("SELECT expires_at AS expiresAt FROM leases WHERE resource_id = ?")
|
|
1327
|
+
.get(resourceId);
|
|
1328
|
+
return isLeaseExpiryRow(row) && row.expiresAt !== null && row.expiresAt > Date.now();
|
|
1329
|
+
}
|
|
1330
|
+
nextFollowUpOrder(runId) {
|
|
1331
|
+
const row = this.state.connection
|
|
1332
|
+
.prepare(`SELECT COALESCE(MAX(order_number), 0) + 1 AS nextOrder
|
|
1333
|
+
FROM workflow_follow_ups WHERE run_id = ?`)
|
|
1334
|
+
.get(runId);
|
|
1335
|
+
if (!isNextOrderRow(row))
|
|
1336
|
+
throw new Error("Could not allocate workflow follow-up order");
|
|
1337
|
+
return row.nextOrder;
|
|
1338
|
+
}
|
|
1339
|
+
assertSettingsRouteCurrent(event, snapshot) {
|
|
1340
|
+
if (event.type !== "node_finished" || event.nodeId === undefined)
|
|
1341
|
+
return;
|
|
1342
|
+
if (snapshot.nodes[event.nodeId]?.settingsRoute !== true)
|
|
1343
|
+
return;
|
|
1344
|
+
const scopeId = event.payload.settingsScopeId;
|
|
1345
|
+
const changeNumber = event.payload.settingsChangeNumber;
|
|
1346
|
+
if (typeof scopeId !== "string" || typeof changeNumber !== "number") {
|
|
1347
|
+
throw new Error(`Settings route ${event.nodeId} has no saved settings binding`);
|
|
1348
|
+
}
|
|
1349
|
+
const scope = this.requireSettingsScopeRow(scopeId);
|
|
1350
|
+
if (scope.revision !== changeNumber) {
|
|
1351
|
+
throw new StaleResourceError(`Settings route ${event.nodeId} used change ${changeNumber}, current change is ${scope.revision}`);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
transitionFollowUpsForRunState(state, event, now) {
|
|
1355
|
+
if (state.status === "running" || state.status === "waiting")
|
|
1356
|
+
return;
|
|
1357
|
+
const queue = this.followUpQueueRow(state.runId);
|
|
1358
|
+
if (queue === undefined || queue.presentationState !== "none")
|
|
1359
|
+
return;
|
|
1360
|
+
let presentationState = "not-needed";
|
|
1361
|
+
let followUpState = "cancelled";
|
|
1362
|
+
let reasonHash = null;
|
|
1363
|
+
if (state.status === "completed") {
|
|
1364
|
+
const required = event.payload.presentationRequired === true;
|
|
1365
|
+
presentationState = required ? "pending" : "not-needed";
|
|
1366
|
+
followUpState = required ? "pending_presentation" : "ready";
|
|
1367
|
+
}
|
|
1368
|
+
else {
|
|
1369
|
+
reasonHash = this.state.putText(state.error ?? `Workflow ended with status ${state.status}`, now);
|
|
1370
|
+
}
|
|
1371
|
+
this.state.connection
|
|
1372
|
+
.prepare(`UPDATE workflow_follow_up_queues
|
|
1373
|
+
SET presentation_state = ?, presentation_updated_at = ?, updated_at = ?
|
|
1374
|
+
WHERE run_id = ? AND presentation_state = 'none'`)
|
|
1375
|
+
.run(presentationState, now, now, state.runId);
|
|
1376
|
+
this.state.connection
|
|
1377
|
+
.prepare(`UPDATE workflow_follow_ups
|
|
1378
|
+
SET status = ?, reason_hash = ?, updated_at = ?
|
|
1379
|
+
WHERE run_id = ? AND status IN ('queued', 'pending_presentation', 'ready')`)
|
|
1380
|
+
.run(followUpState, reasonHash, now, state.runId);
|
|
1381
|
+
const revision = this.requireResourceRevision(queue.resourceId);
|
|
1382
|
+
this.bumpResource(queue.resourceId, revision, now);
|
|
1383
|
+
const payloadHash = this.state.putJson({ runId: state.runId, state: presentationState, followUps: followUpState }, now);
|
|
1384
|
+
insertGenericEvent(this.state, queue.resourceId, revision + 1, "follow-up.run-finished", "system", null, payloadHash, now);
|
|
1385
|
+
}
|
|
603
1386
|
syncNodeAttempts(state, snapshot, now) {
|
|
604
1387
|
for (const step of state.steps.slice(state.carriedStepCount ?? 0)) {
|
|
605
|
-
const
|
|
1388
|
+
const promptEntryId = this.findAttemptPromptEntry(state.runId, step.attemptId);
|
|
1389
|
+
const promptHash = promptEntryId === undefined && step.prompt !== null
|
|
1390
|
+
? this.state.putText(step.prompt, now)
|
|
1391
|
+
: null;
|
|
1392
|
+
const receipt = attemptReceipt(step);
|
|
1393
|
+
const receiptHash = Object.keys(receipt).length === 0 ? null : this.state.putJson(receipt, now);
|
|
606
1394
|
const outputHash = step.output === undefined ? null : this.state.putJson(step.output, now);
|
|
607
1395
|
const errorHash = step.error === undefined ? null : this.state.putText(step.error, now);
|
|
608
|
-
const promptHash = step.prompt === null ? null : this.state.putJson(step.prompt, now);
|
|
609
1396
|
const existing = this.state.connection
|
|
610
1397
|
.prepare("SELECT attempt_id AS attemptId FROM node_attempts WHERE attempt_id = ?")
|
|
611
1398
|
.get(step.attemptId);
|
|
@@ -614,23 +1401,133 @@ export class WorkflowRunStore {
|
|
|
614
1401
|
this.state.connection
|
|
615
1402
|
.prepare(`INSERT INTO node_attempts(
|
|
616
1403
|
attempt_id, run_id, node_id, attempt_number, node_type, status,
|
|
617
|
-
|
|
1404
|
+
prompt_hash, output_hash, receipt_hash, error_hash,
|
|
1405
|
+
settings_scope_id, settings_change_number, settings_hash,
|
|
618
1406
|
started_at, finished_at, created_at, updated_at
|
|
619
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
620
|
-
.run(step.attemptId, state.runId, step.nodeId, attemptNumber, step.nodeType, outcomeStatus(step.outcome), promptHash, outputHash,
|
|
1407
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
1408
|
+
.run(step.attemptId, state.runId, step.nodeId, attemptNumber, step.nodeType, outcomeStatus(step.outcome), promptHash, outputHash, receiptHash, errorHash, step.settingsScopeId ?? null, step.settingsChangeNumber ?? null, step.settingsHash === undefined ? null : Buffer.from(step.settingsHash, "hex"), Date.parse(step.startedAt), Date.parse(step.finishedAt), Date.parse(step.startedAt), now);
|
|
621
1409
|
}
|
|
622
1410
|
else {
|
|
623
1411
|
this.state.connection
|
|
624
1412
|
.prepare(`UPDATE node_attempts
|
|
625
|
-
SET status = ?,
|
|
626
|
-
|
|
1413
|
+
SET status = ?, prompt_hash = ?, output_hash = ?, receipt_hash = ?, error_hash = ?,
|
|
1414
|
+
settings_scope_id = ?, settings_change_number = ?,
|
|
1415
|
+
settings_hash = ?, finished_at = ?, updated_at = ?
|
|
627
1416
|
WHERE attempt_id = ?`)
|
|
628
|
-
.run(outcomeStatus(step.outcome), promptHash, outputHash,
|
|
1417
|
+
.run(outcomeStatus(step.outcome), promptHash, outputHash, receiptHash, errorHash, step.settingsScopeId ?? null, step.settingsChangeNumber ?? null, step.settingsHash === undefined ? null : Buffer.from(step.settingsHash, "hex"), Date.parse(step.finishedAt), now, step.attemptId);
|
|
1418
|
+
}
|
|
1419
|
+
if (this.syncAttemptEntries(state.runId, step, promptEntryId)) {
|
|
1420
|
+
this.state.connection
|
|
1421
|
+
.prepare("UPDATE node_attempts SET output_hash = NULL WHERE attempt_id = ?")
|
|
1422
|
+
.run(step.attemptId);
|
|
629
1423
|
}
|
|
630
1424
|
}
|
|
631
1425
|
if (state.currentAttemptId !== undefined && state.currentNode !== undefined) {
|
|
632
1426
|
this.ensureAttempt(state, state.currentNode, state.currentAttemptId, now, snapshot);
|
|
633
1427
|
}
|
|
1428
|
+
this.syncRunSteps(state, now);
|
|
1429
|
+
}
|
|
1430
|
+
syncRunSteps(state, now) {
|
|
1431
|
+
const existingRows = this.state.connection
|
|
1432
|
+
.prepare(`SELECT step_index AS stepIndex, attempt_id AS attemptId
|
|
1433
|
+
FROM run_steps WHERE run_id = ? ORDER BY step_index`)
|
|
1434
|
+
.all(state.runId)
|
|
1435
|
+
.filter(isRunStepIdentityRow);
|
|
1436
|
+
if (existingRows.length > state.steps.length) {
|
|
1437
|
+
throw new Error(`Workflow run steps cannot shrink: ${state.runId}`);
|
|
1438
|
+
}
|
|
1439
|
+
for (const existing of existingRows) {
|
|
1440
|
+
if (state.steps[existing.stepIndex]?.attemptId !== existing.attemptId) {
|
|
1441
|
+
throw new Error(`Workflow run step history changed at index ${existing.stepIndex}`);
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
for (let stepIndex = existingRows.length; stepIndex < state.steps.length; stepIndex += 1) {
|
|
1445
|
+
const step = state.steps[stepIndex];
|
|
1446
|
+
/* istanbul ignore if -- array index follows a checked bound */
|
|
1447
|
+
if (step === undefined)
|
|
1448
|
+
throw new Error("Workflow run step became unavailable");
|
|
1449
|
+
const attemptOutput = this.readAttemptOutput(step.attemptId);
|
|
1450
|
+
const carried = stepIndex < (state.carriedStepCount ?? 0);
|
|
1451
|
+
const outputOverrideHash = carried && canonicalJson(attemptOutput) !== canonicalJson(step.output)
|
|
1452
|
+
? this.state.putJson(step.output, now)
|
|
1453
|
+
: null;
|
|
1454
|
+
this.state.connection
|
|
1455
|
+
.prepare(`INSERT INTO run_steps(run_id, step_index, attempt_id, output_override_hash)
|
|
1456
|
+
VALUES (?, ?, ?, ?)`)
|
|
1457
|
+
.run(state.runId, stepIndex, step.attemptId, outputOverrideHash);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
syncAttemptEntries(runId, step, promptEntryId) {
|
|
1461
|
+
const links = [];
|
|
1462
|
+
if (promptEntryId !== undefined)
|
|
1463
|
+
links.push(["prompt", promptEntryId]);
|
|
1464
|
+
if (step.conversation !== undefined) {
|
|
1465
|
+
links.push(["first", step.conversation.firstEntryId], ["last", step.conversation.lastEntryId]);
|
|
1466
|
+
}
|
|
1467
|
+
if (step.assistantMessage?.entryId !== undefined) {
|
|
1468
|
+
links.push(["response", step.assistantMessage.entryId]);
|
|
1469
|
+
}
|
|
1470
|
+
let responseLinked = false;
|
|
1471
|
+
for (const [role, entryId] of links) {
|
|
1472
|
+
const row = this.state.connection
|
|
1473
|
+
.prepare(`SELECT e.segment_id AS segmentId, e.entry_hash AS entryHash
|
|
1474
|
+
FROM session_entries e
|
|
1475
|
+
JOIN session_segments s ON s.segment_id = e.segment_id
|
|
1476
|
+
WHERE s.run_id = ? AND e.entry_id = ?
|
|
1477
|
+
ORDER BY e.recorded_at DESC LIMIT 1`)
|
|
1478
|
+
.get(runId, entryId);
|
|
1479
|
+
if (!isSegmentIdentityRow(row))
|
|
1480
|
+
continue;
|
|
1481
|
+
if (role === "response") {
|
|
1482
|
+
const output = assistantOutputFromEntry(this.state.readJson(row.entryHash), step.assistantMessage?.maxChars);
|
|
1483
|
+
if (canonicalJson(output) !== canonicalJson(step.output) ||
|
|
1484
|
+
createHash("sha256").update(output).digest("hex") !== step.assistantMessage?.sha256) {
|
|
1485
|
+
throw new Error(`Assistant response entry conflicts with attempt ${step.attemptId}`);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
this.state.connection
|
|
1489
|
+
.prepare(`INSERT INTO attempt_entries(attempt_id, role, segment_id, entry_id)
|
|
1490
|
+
VALUES (?, ?, ?, ?)
|
|
1491
|
+
ON CONFLICT(attempt_id, role) DO UPDATE SET
|
|
1492
|
+
segment_id = excluded.segment_id, entry_id = excluded.entry_id`)
|
|
1493
|
+
.run(step.attemptId, role, row.segmentId, entryId);
|
|
1494
|
+
if (role === "response")
|
|
1495
|
+
responseLinked = true;
|
|
1496
|
+
}
|
|
1497
|
+
return responseLinked;
|
|
1498
|
+
}
|
|
1499
|
+
findAttemptPromptEntry(runId, attemptId) {
|
|
1500
|
+
const row = this.state.connection
|
|
1501
|
+
.prepare(`SELECT e.entry_id AS entryId
|
|
1502
|
+
FROM session_entries e
|
|
1503
|
+
JOIN session_segments s ON s.segment_id = e.segment_id
|
|
1504
|
+
JOIN blobs b ON b.blob_hash = e.entry_hash
|
|
1505
|
+
WHERE s.run_id = ?
|
|
1506
|
+
AND json_extract(CAST(b.content AS TEXT), '$.customType') = 'pi-workflows-agent-step'
|
|
1507
|
+
AND json_extract(CAST(b.content AS TEXT), '$.details.contract.attemptId') = ?
|
|
1508
|
+
ORDER BY e.recorded_at DESC LIMIT 1`)
|
|
1509
|
+
.get(runId, attemptId);
|
|
1510
|
+
return isEntryIdentityRow(row) ? row.entryId : undefined;
|
|
1511
|
+
}
|
|
1512
|
+
readAttemptOutput(attemptId) {
|
|
1513
|
+
const row = this.state.connection
|
|
1514
|
+
.prepare(`SELECT a.output_hash AS outputHash, a.receipt_hash AS receiptHash,
|
|
1515
|
+
e.entry_hash AS responseEntryHash
|
|
1516
|
+
FROM node_attempts a
|
|
1517
|
+
LEFT JOIN attempt_entries l ON l.attempt_id = a.attempt_id AND l.role = 'response'
|
|
1518
|
+
LEFT JOIN session_entries e
|
|
1519
|
+
ON e.segment_id = l.segment_id AND e.entry_id = l.entry_id
|
|
1520
|
+
WHERE a.attempt_id = ?`)
|
|
1521
|
+
.get(attemptId);
|
|
1522
|
+
if (!isAttemptValueRow(row)) {
|
|
1523
|
+
throw new Error(`Workflow node attempt is missing: ${attemptId}`);
|
|
1524
|
+
}
|
|
1525
|
+
if (row.outputHash !== null)
|
|
1526
|
+
return this.state.readJson(row.outputHash);
|
|
1527
|
+
if (row.responseEntryHash === null)
|
|
1528
|
+
return null;
|
|
1529
|
+
const receipt = row.receiptHash === null ? {} : this.readJsonAs(row.receiptHash);
|
|
1530
|
+
return assistantOutputFromEntry(this.state.readJson(row.responseEntryHash), receipt.assistantMessage?.maxChars);
|
|
634
1531
|
}
|
|
635
1532
|
ensureAttempt(state, nodeId, attemptId, now, snapshot) {
|
|
636
1533
|
const existing = this.state.connection
|
|
@@ -639,18 +1536,31 @@ export class WorkflowRunStore {
|
|
|
639
1536
|
const status = state.status === "waiting" ? "waiting" : "running";
|
|
640
1537
|
if (existing !== undefined) {
|
|
641
1538
|
this.state.connection
|
|
642
|
-
.prepare(
|
|
643
|
-
|
|
1539
|
+
.prepare(`UPDATE node_attempts
|
|
1540
|
+
SET status = ?, settings_scope_id = ?, settings_change_number = ?,
|
|
1541
|
+
settings_hash = ?, updated_at = ?
|
|
1542
|
+
WHERE attempt_id = ?`)
|
|
1543
|
+
.run(status, state.currentSettingsScopeId ?? null, state.currentSettingsChangeNumber ?? null, state.currentSettingsHash === undefined
|
|
1544
|
+
? null
|
|
1545
|
+
: Buffer.from(state.currentSettingsHash, "hex"), now, attemptId);
|
|
644
1546
|
return;
|
|
645
1547
|
}
|
|
646
1548
|
const definition = snapshot ?? this.readDefinition(this.requireRunRow(state.runId).definitionHash);
|
|
647
1549
|
const nodeType = definition.nodes[nodeId]?.nodeType ?? "agent";
|
|
1550
|
+
this.state.connection
|
|
1551
|
+
.prepare(`UPDATE node_attempts
|
|
1552
|
+
SET status = 'cancelled', finished_at = COALESCE(finished_at, ?), updated_at = ?
|
|
1553
|
+
WHERE run_id = ? AND status = 'interrupted'`)
|
|
1554
|
+
.run(now, now, state.runId);
|
|
648
1555
|
this.state.connection
|
|
649
1556
|
.prepare(`INSERT INTO node_attempts(
|
|
650
1557
|
attempt_id, run_id, node_id, attempt_number, node_type, status,
|
|
1558
|
+
settings_scope_id, settings_change_number, settings_hash,
|
|
651
1559
|
started_at, created_at, updated_at
|
|
652
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
653
|
-
.run(attemptId, state.runId, nodeId, this.nextAttemptNumber(state.runId, nodeId), nodeType, status, state.
|
|
1560
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
1561
|
+
.run(attemptId, state.runId, nodeId, this.nextAttemptNumber(state.runId, nodeId), nodeType, status, state.currentSettingsScopeId ?? null, state.currentSettingsChangeNumber ?? null, state.currentSettingsHash === undefined
|
|
1562
|
+
? null
|
|
1563
|
+
: Buffer.from(state.currentSettingsHash, "hex"), state.currentNodeStartedAt === undefined ? now : Date.parse(state.currentNodeStartedAt), now, now);
|
|
654
1564
|
}
|
|
655
1565
|
nextAttemptNumber(runId, nodeId) {
|
|
656
1566
|
const row = this.state.connection
|
|
@@ -672,11 +1582,46 @@ export class WorkflowRunStore {
|
|
|
672
1582
|
throw new Error("Could not allocate workflow update sequence");
|
|
673
1583
|
return row.updateSeq;
|
|
674
1584
|
}
|
|
1585
|
+
insertRunSources(runId, root, mounted) {
|
|
1586
|
+
const insert = this.state.connection.prepare(`INSERT INTO run_sources(run_id, mount_path, source_type, source_ref, source_revision)
|
|
1587
|
+
VALUES (?, ?, ?, ?, ?)`);
|
|
1588
|
+
insert.run(runId, "", root.type, root.ref, root.revision);
|
|
1589
|
+
for (const item of mounted) {
|
|
1590
|
+
const source = sourceParts(item.source);
|
|
1591
|
+
insert.run(runId, item.mountPath.join("/"), source.type, source.ref, source.revision);
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
syncContinuationIdentity(state) {
|
|
1595
|
+
if (state.parentRunId === undefined || state.humanDecision === undefined)
|
|
1596
|
+
return;
|
|
1597
|
+
const createdAt = Date.parse(state.humanDecision.acceptedAt);
|
|
1598
|
+
this.state.connection
|
|
1599
|
+
.prepare(`INSERT INTO continuations(
|
|
1600
|
+
decision_id, parent_run_id, continuation_run_id, created_at
|
|
1601
|
+
) VALUES (?, ?, ?, ?)
|
|
1602
|
+
ON CONFLICT(decision_id) DO NOTHING`)
|
|
1603
|
+
.run(state.humanDecision.decisionId, state.parentRunId, state.runId, createdAt);
|
|
1604
|
+
const row = this.state.connection
|
|
1605
|
+
.prepare(`SELECT parent_run_id AS parentRunId, continuation_run_id AS continuationRunId,
|
|
1606
|
+
created_at AS createdAt
|
|
1607
|
+
FROM continuations WHERE decision_id = ?`)
|
|
1608
|
+
.get(state.humanDecision.decisionId);
|
|
1609
|
+
if (!isContinuationIdentityRow(row) ||
|
|
1610
|
+
row.parentRunId !== state.parentRunId ||
|
|
1611
|
+
row.continuationRunId !== state.runId ||
|
|
1612
|
+
row.createdAt !== createdAt) {
|
|
1613
|
+
throw new Error("Immutable human decision continuation conflicts");
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
675
1616
|
readRunRow(runId) {
|
|
676
1617
|
const row = this.state.connection
|
|
677
1618
|
.prepare(`SELECT r.run_id AS runId, r.resource_id AS resourceId,
|
|
678
|
-
|
|
679
|
-
r.
|
|
1619
|
+
d.definition_hash AS definitionHash, r.definition_digest AS definitionDigest,
|
|
1620
|
+
d.workflow_name AS workflowRef, r.parent_run_id AS parentRunId,
|
|
1621
|
+
r.title, r.status, r.paused, r.status_detail AS statusDetail,
|
|
1622
|
+
r.input_hash AS inputHash, r.final_output_hash AS finalOutputHash,
|
|
1623
|
+
r.error_hash AS errorHash,
|
|
1624
|
+
r.created_at AS createdAt, r.updated_at AS updatedAt, r.finished_at AS finishedAt
|
|
680
1625
|
FROM runs r
|
|
681
1626
|
JOIN workflow_definitions d ON d.definition_digest = r.definition_digest
|
|
682
1627
|
WHERE r.run_id = ?`)
|
|
@@ -689,12 +1634,259 @@ export class WorkflowRunStore {
|
|
|
689
1634
|
throw new Error(`Workflow run is missing: ${runId}`);
|
|
690
1635
|
return row;
|
|
691
1636
|
}
|
|
692
|
-
|
|
693
|
-
const
|
|
694
|
-
|
|
695
|
-
|
|
1637
|
+
readRunState(row, snapshot) {
|
|
1638
|
+
const steps = this.readSteps(row.runId);
|
|
1639
|
+
const sources = this.readRunSources(row.runId, snapshot);
|
|
1640
|
+
const carriedStepCount = this.carriedStepCount(row.runId);
|
|
1641
|
+
const activeAttempt = row.status === "running" ? this.readActiveAttempt(row.runId) : undefined;
|
|
1642
|
+
const humanDecision = this.readHumanDecisionReceipt(row.runId);
|
|
1643
|
+
const outputs = {};
|
|
1644
|
+
const results = {};
|
|
1645
|
+
for (const step of steps) {
|
|
1646
|
+
const result = resultForStep(step);
|
|
1647
|
+
results[step.nodeId] = result;
|
|
1648
|
+
if (step.outcome === "ok")
|
|
1649
|
+
outputs[step.nodeId] = step.output;
|
|
1650
|
+
else
|
|
1651
|
+
delete outputs[step.nodeId];
|
|
1652
|
+
const node = snapshot.nodes[step.nodeId];
|
|
1653
|
+
const mountPath = node?.includeTransition === "exit" && node.mountPath !== undefined
|
|
1654
|
+
? node.mountPath.join("/")
|
|
1655
|
+
: undefined;
|
|
1656
|
+
if (mountPath !== undefined && step.outcome === "ok") {
|
|
1657
|
+
outputs[mountPath] = step.output;
|
|
1658
|
+
results[mountPath] = { ...result, nodeId: mountPath };
|
|
1659
|
+
}
|
|
696
1660
|
}
|
|
697
|
-
|
|
1661
|
+
const revision = this.requireResourceRevision(row.resourceId);
|
|
1662
|
+
return {
|
|
1663
|
+
schema: RUN_STATE_SCHEMA,
|
|
1664
|
+
traceSeq: revision,
|
|
1665
|
+
runId: row.runId,
|
|
1666
|
+
workflowName: row.workflowRef,
|
|
1667
|
+
...(row.parentRunId === null ? {} : { parentRunId: row.parentRunId }),
|
|
1668
|
+
...(carriedStepCount === 0 ? {} : { carriedStepCount }),
|
|
1669
|
+
...(row.title === null ? {} : { runTitle: row.title }),
|
|
1670
|
+
...(sources.root === undefined ? {} : { workflowSource: sources.root }),
|
|
1671
|
+
...(sources.mounted.length === 0 ? {} : { workflowSources: sources.mounted }),
|
|
1672
|
+
...(sources.mounted.length !== 0 || snapshot.composition?.mounts.length
|
|
1673
|
+
? { definitionDigest: `sha256:${row.definitionDigest.toString("hex")}` }
|
|
1674
|
+
: {}),
|
|
1675
|
+
startedAt: new Date(row.createdAt).toISOString(),
|
|
1676
|
+
...(row.finishedAt === null ? {} : { finishedAt: new Date(row.finishedAt).toISOString() }),
|
|
1677
|
+
updatedAt: new Date(row.updatedAt).toISOString(),
|
|
1678
|
+
status: row.status,
|
|
1679
|
+
input: this.state.readJson(row.inputHash),
|
|
1680
|
+
outputs,
|
|
1681
|
+
results,
|
|
1682
|
+
steps,
|
|
1683
|
+
...this.readUpdates(row.runId),
|
|
1684
|
+
...(activeAttempt === undefined ? {} : { currentNode: activeAttempt.nodeId }),
|
|
1685
|
+
...(activeAttempt === undefined ? {} : { currentAttemptId: activeAttempt.attemptId }),
|
|
1686
|
+
...(activeAttempt?.startedAt === null || activeAttempt === undefined
|
|
1687
|
+
? {}
|
|
1688
|
+
: { currentNodeStartedAt: new Date(activeAttempt.startedAt).toISOString() }),
|
|
1689
|
+
...savedCurrentSettingsBinding(activeAttempt?.settingsScopeId ?? null, activeAttempt?.settingsChangeNumber ?? null, activeAttempt?.settingsHash ?? null),
|
|
1690
|
+
...(row.statusDetail === null ? {} : { statusDetail: row.statusDetail }),
|
|
1691
|
+
...(humanDecision === undefined ? {} : { humanDecision }),
|
|
1692
|
+
...(row.paused === 0 ? {} : { paused: true }),
|
|
1693
|
+
...(row.status === "waiting" && steps.at(-1) !== undefined
|
|
1694
|
+
? { waitingOn: steps.at(-1)?.nodeId }
|
|
1695
|
+
: {}),
|
|
1696
|
+
...(row.finalOutputHash === null
|
|
1697
|
+
? {}
|
|
1698
|
+
: { finalOutput: this.state.readJson(row.finalOutputHash) }),
|
|
1699
|
+
...(row.errorHash === null ? {} : { error: this.readText(row.errorHash) }),
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
readSteps(runId) {
|
|
1703
|
+
const rows = this.state.connection
|
|
1704
|
+
.prepare(`SELECT s.step_index AS stepIndex, a.attempt_id AS attemptId, a.node_id AS nodeId,
|
|
1705
|
+
a.node_type AS nodeType, a.status,
|
|
1706
|
+
a.prompt_hash AS promptHash, a.output_hash AS outputHash,
|
|
1707
|
+
s.output_override_hash AS outputOverrideHash,
|
|
1708
|
+
a.receipt_hash AS receiptHash, a.error_hash AS errorHash,
|
|
1709
|
+
prompt_entry.entry_hash AS promptEntryHash,
|
|
1710
|
+
response_entry.entry_hash AS responseEntryHash,
|
|
1711
|
+
first_link.entry_id AS firstEntryId, last_link.entry_id AS lastEntryId,
|
|
1712
|
+
a.settings_scope_id AS settingsScopeId,
|
|
1713
|
+
a.settings_change_number AS settingsChangeNumber,
|
|
1714
|
+
a.settings_hash AS settingsHash,
|
|
1715
|
+
a.started_at AS startedAt, a.finished_at AS finishedAt
|
|
1716
|
+
FROM run_steps s
|
|
1717
|
+
JOIN node_attempts a ON a.attempt_id = s.attempt_id
|
|
1718
|
+
LEFT JOIN attempt_entries prompt_link
|
|
1719
|
+
ON prompt_link.attempt_id = a.attempt_id AND prompt_link.role = 'prompt'
|
|
1720
|
+
LEFT JOIN session_entries prompt_entry
|
|
1721
|
+
ON prompt_entry.segment_id = prompt_link.segment_id
|
|
1722
|
+
AND prompt_entry.entry_id = prompt_link.entry_id
|
|
1723
|
+
LEFT JOIN attempt_entries response_link
|
|
1724
|
+
ON response_link.attempt_id = a.attempt_id AND response_link.role = 'response'
|
|
1725
|
+
LEFT JOIN session_entries response_entry
|
|
1726
|
+
ON response_entry.segment_id = response_link.segment_id
|
|
1727
|
+
AND response_entry.entry_id = response_link.entry_id
|
|
1728
|
+
LEFT JOIN attempt_entries first_link
|
|
1729
|
+
ON first_link.attempt_id = a.attempt_id AND first_link.role = 'first'
|
|
1730
|
+
LEFT JOIN attempt_entries last_link
|
|
1731
|
+
ON last_link.attempt_id = a.attempt_id AND last_link.role = 'last'
|
|
1732
|
+
WHERE s.run_id = ? ORDER BY s.step_index`)
|
|
1733
|
+
.all(runId)
|
|
1734
|
+
.filter(isStepRow);
|
|
1735
|
+
return rows.map((row, index) => {
|
|
1736
|
+
if (row.stepIndex !== index)
|
|
1737
|
+
throw new Error(`Workflow run step sequence has a gap: ${runId}`);
|
|
1738
|
+
const receipt = row.receiptHash === null ? {} : this.readJsonAs(row.receiptHash);
|
|
1739
|
+
const prompt = row.promptEntryHash !== null
|
|
1740
|
+
? promptFromEntry(this.state.readJson(row.promptEntryHash))
|
|
1741
|
+
: row.promptHash === null
|
|
1742
|
+
? null
|
|
1743
|
+
: this.readText(row.promptHash);
|
|
1744
|
+
const outputHash = row.outputOverrideHash ?? row.outputHash;
|
|
1745
|
+
const output = outputHash !== null
|
|
1746
|
+
? this.state.readJson(outputHash)
|
|
1747
|
+
: row.responseEntryHash === null
|
|
1748
|
+
? null
|
|
1749
|
+
: assistantOutputFromEntry(this.state.readJson(row.responseEntryHash), receipt.assistantMessage?.maxChars);
|
|
1750
|
+
const error = row.errorHash === null ? undefined : this.readText(row.errorHash);
|
|
1751
|
+
const conversation = row.firstEntryId === null || row.lastEntryId === null
|
|
1752
|
+
? undefined
|
|
1753
|
+
: { firstEntryId: row.firstEntryId, lastEntryId: row.lastEntryId };
|
|
1754
|
+
return {
|
|
1755
|
+
attemptId: row.attemptId,
|
|
1756
|
+
nodeId: row.nodeId,
|
|
1757
|
+
nodeType: row.nodeType,
|
|
1758
|
+
outcome: outcomeForStatus(row.status),
|
|
1759
|
+
startedAt: new Date(row.startedAt).toISOString(),
|
|
1760
|
+
finishedAt: new Date(row.finishedAt).toISOString(),
|
|
1761
|
+
prompt,
|
|
1762
|
+
output,
|
|
1763
|
+
...(error === undefined ? {} : { error }),
|
|
1764
|
+
...savedStepSettingsBinding(row.settingsScopeId, row.settingsChangeNumber, row.settingsHash),
|
|
1765
|
+
...receipt,
|
|
1766
|
+
...(conversation === undefined ? {} : { conversation }),
|
|
1767
|
+
};
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
readRunSources(runId, snapshot) {
|
|
1771
|
+
const rows = this.state.connection
|
|
1772
|
+
.prepare(`SELECT mount_path AS mountPath, source_type AS sourceType,
|
|
1773
|
+
source_ref AS sourceRef, source_revision AS sourceRevision
|
|
1774
|
+
FROM run_sources WHERE run_id = ? ORDER BY mount_path`)
|
|
1775
|
+
.all(runId)
|
|
1776
|
+
.filter(isRunSourceRow);
|
|
1777
|
+
const rootRow = rows.find((row) => row.mountPath === "");
|
|
1778
|
+
const root = rootRow === undefined ||
|
|
1779
|
+
(rootRow.sourceType === "file" && rootRow.sourceRef.startsWith("inline:"))
|
|
1780
|
+
? undefined
|
|
1781
|
+
: workflowSourceFromRow(rootRow);
|
|
1782
|
+
const mountNames = new Map((snapshot.composition?.mounts ?? []).map((mount) => [
|
|
1783
|
+
mount.mountPath.join("/"),
|
|
1784
|
+
mount.workflowName,
|
|
1785
|
+
]));
|
|
1786
|
+
const mounted = rows
|
|
1787
|
+
.filter((row) => row.mountPath !== "")
|
|
1788
|
+
.map((row) => ({
|
|
1789
|
+
mountPath: row.mountPath.split("/"),
|
|
1790
|
+
workflowName: mountNames.get(row.mountPath) ?? row.mountPath,
|
|
1791
|
+
source: workflowSourceFromRow(row),
|
|
1792
|
+
}));
|
|
1793
|
+
return { ...(root === undefined ? {} : { root }), mounted };
|
|
1794
|
+
}
|
|
1795
|
+
carriedStepCount(runId) {
|
|
1796
|
+
const row = this.state.connection
|
|
1797
|
+
.prepare(`SELECT count(*) AS count
|
|
1798
|
+
FROM run_steps s JOIN node_attempts a ON a.attempt_id = s.attempt_id
|
|
1799
|
+
WHERE s.run_id = ? AND a.run_id <> s.run_id`)
|
|
1800
|
+
.get(runId);
|
|
1801
|
+
return isCountRow(row) ? row.count : 0;
|
|
1802
|
+
}
|
|
1803
|
+
readActiveAttempt(runId) {
|
|
1804
|
+
const row = this.state.connection
|
|
1805
|
+
.prepare(`SELECT attempt_id AS attemptId, node_id AS nodeId, started_at AS startedAt,
|
|
1806
|
+
settings_scope_id AS settingsScopeId,
|
|
1807
|
+
settings_change_number AS settingsChangeNumber,
|
|
1808
|
+
settings_hash AS settingsHash
|
|
1809
|
+
FROM node_attempts
|
|
1810
|
+
WHERE run_id = ? AND status IN ('pending', 'running', 'waiting', 'interrupted')`)
|
|
1811
|
+
.get(runId);
|
|
1812
|
+
return isActiveAttemptRow(row) ? row : undefined;
|
|
1813
|
+
}
|
|
1814
|
+
readHumanDecisionReceipt(runId) {
|
|
1815
|
+
const row = this.state.connection
|
|
1816
|
+
.prepare(`SELECT d.request_hash AS requestHash, r.response_hash AS responseHash
|
|
1817
|
+
FROM continuations c
|
|
1818
|
+
JOIN human_decisions d ON d.decision_id = c.decision_id
|
|
1819
|
+
JOIN human_decision_resolutions r ON r.decision_id = c.decision_id
|
|
1820
|
+
WHERE c.continuation_run_id = ? AND r.outcome = 'accepted'`)
|
|
1821
|
+
.get(runId);
|
|
1822
|
+
if (!isDecisionReceiptRow(row))
|
|
1823
|
+
return undefined;
|
|
1824
|
+
const request = this.readJsonAs(row.requestHash);
|
|
1825
|
+
const decision = this.readJsonAs(row.responseHash);
|
|
1826
|
+
if (typeof request.decisionId !== "string" ||
|
|
1827
|
+
typeof request.requestDigest !== "string" ||
|
|
1828
|
+
typeof request.nodeId !== "string" ||
|
|
1829
|
+
typeof decision.provenance !== "string" ||
|
|
1830
|
+
typeof decision.acceptedAt !== "string" ||
|
|
1831
|
+
typeof decision.answerDigest !== "string" ||
|
|
1832
|
+
typeof decision.subjectDigest !== "string" ||
|
|
1833
|
+
typeof decision.presentationDigest !== "string" ||
|
|
1834
|
+
typeof decision.revision !== "number" ||
|
|
1835
|
+
decision.response === undefined) {
|
|
1836
|
+
throw new Error(`Human decision receipt is invalid for run ${runId}`);
|
|
1837
|
+
}
|
|
1838
|
+
return {
|
|
1839
|
+
schema: "pi-workflows.human-decision-receipt.v1",
|
|
1840
|
+
decisionId: request.decisionId,
|
|
1841
|
+
requestDigest: request.requestDigest,
|
|
1842
|
+
nodeId: request.nodeId,
|
|
1843
|
+
response: decision.response,
|
|
1844
|
+
provenance: decision.provenance === "timeout" ? "timeout" : "human",
|
|
1845
|
+
acceptedAt: decision.acceptedAt,
|
|
1846
|
+
answerDigest: decision.answerDigest,
|
|
1847
|
+
subjectDigest: decision.subjectDigest,
|
|
1848
|
+
presentationDigest: decision.presentationDigest,
|
|
1849
|
+
revision: decision.revision,
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
readUpdates(runId) {
|
|
1853
|
+
const rows = this.state.connection
|
|
1854
|
+
.prepare(`SELECT u.update_id AS updateId, u.run_revision AS runRevision,
|
|
1855
|
+
a.node_id AS nodeId, u.attempt_id AS attemptId,
|
|
1856
|
+
u.update_type AS updateType, u.update_key AS updateKey,
|
|
1857
|
+
u.data_hash AS dataHash, u.recorded_at AS recordedAt
|
|
1858
|
+
FROM workflow_updates u
|
|
1859
|
+
JOIN node_attempts a ON a.attempt_id = u.attempt_id
|
|
1860
|
+
WHERE a.run_id = ? ORDER BY u.run_revision`)
|
|
1861
|
+
.all(runId)
|
|
1862
|
+
.filter(isUpdateRow);
|
|
1863
|
+
if (rows.length === 0)
|
|
1864
|
+
return {};
|
|
1865
|
+
let updates;
|
|
1866
|
+
for (const row of rows) {
|
|
1867
|
+
updates = updateProjection(updates, {
|
|
1868
|
+
updateId: row.updateId,
|
|
1869
|
+
seq: row.runRevision,
|
|
1870
|
+
at: new Date(row.recordedAt).toISOString(),
|
|
1871
|
+
runId,
|
|
1872
|
+
nodeId: row.nodeId,
|
|
1873
|
+
attemptId: row.attemptId,
|
|
1874
|
+
type: row.updateType,
|
|
1875
|
+
key: row.updateKey,
|
|
1876
|
+
data: this.readJsonAs(row.dataHash),
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1879
|
+
return updates === undefined ? {} : { updates };
|
|
1880
|
+
}
|
|
1881
|
+
readJsonAs(hash) {
|
|
1882
|
+
return this.state.readJson(hash);
|
|
1883
|
+
}
|
|
1884
|
+
readText(hash) {
|
|
1885
|
+
const blob = this.state.readBlob(hash);
|
|
1886
|
+
if (blob === undefined || blob.mediaType !== "text/plain") {
|
|
1887
|
+
throw new Error("Text blob is missing or has the wrong media type");
|
|
1888
|
+
}
|
|
1889
|
+
return blob.content.toString("utf8");
|
|
698
1890
|
}
|
|
699
1891
|
readDefinition(hash) {
|
|
700
1892
|
const value = this.state.readJson(hash);
|
|
@@ -892,14 +2084,38 @@ export function readLastTraceEvent(runId, options = {}) {
|
|
|
892
2084
|
}
|
|
893
2085
|
}
|
|
894
2086
|
function insertRunEvent(state, resourceId, revision, at, event) {
|
|
2087
|
+
const payload = compactTracePayload(event.type, event.payload);
|
|
895
2088
|
const payloadHash = state.putJson({
|
|
896
2089
|
scope: event.scope,
|
|
897
2090
|
...(event.nodeId === undefined ? {} : { nodeId: event.nodeId }),
|
|
898
2091
|
...(event.attemptId === undefined ? {} : { attemptId: event.attemptId }),
|
|
899
|
-
payload
|
|
2092
|
+
payload,
|
|
900
2093
|
}, Date.parse(at));
|
|
901
2094
|
insertGenericEvent(state, resourceId, revision, event.type, "system", null, payloadHash, Date.parse(at));
|
|
902
2095
|
}
|
|
2096
|
+
function compactTracePayload(eventType, payload) {
|
|
2097
|
+
if (eventType === "agent_prompt_sent" && Object.hasOwn(payload, "prompt")) {
|
|
2098
|
+
const { prompt: _prompt, ...rest } = payload;
|
|
2099
|
+
return { ...rest, promptStored: true };
|
|
2100
|
+
}
|
|
2101
|
+
if (eventType === "node_finished" || eventType === "node_failed") {
|
|
2102
|
+
const { output: _output, action: _action, assistantMessage: _assistantMessage, conversation: _conversation, error: _error, ...rest } = payload;
|
|
2103
|
+
return { ...rest, ...(Object.hasOwn(payload, "output") ? { outputStored: true } : {}) };
|
|
2104
|
+
}
|
|
2105
|
+
if (eventType === "include_exited" && Object.hasOwn(payload, "output")) {
|
|
2106
|
+
const { output: _output, ...rest } = payload;
|
|
2107
|
+
return { ...rest, outputStored: true };
|
|
2108
|
+
}
|
|
2109
|
+
if (eventType === "run_started" && Object.hasOwn(payload, "input")) {
|
|
2110
|
+
const { input: _input, ...rest } = payload;
|
|
2111
|
+
return { ...rest, inputStored: true };
|
|
2112
|
+
}
|
|
2113
|
+
if (Object.hasOwn(payload, "finalOutput")) {
|
|
2114
|
+
const { finalOutput: _finalOutput, ...rest } = payload;
|
|
2115
|
+
return { ...rest, finalOutputStored: true };
|
|
2116
|
+
}
|
|
2117
|
+
return payload;
|
|
2118
|
+
}
|
|
903
2119
|
function insertGenericEvent(state, resourceId, revision, eventType, actorType, actorId, payloadHash, now) {
|
|
904
2120
|
state.connection
|
|
905
2121
|
.prepare(`INSERT INTO events(
|
|
@@ -920,6 +2136,16 @@ function sourceForState(state, definitionDigest) {
|
|
|
920
2136
|
revision: definitionDigest.toString("hex"),
|
|
921
2137
|
};
|
|
922
2138
|
}
|
|
2139
|
+
function sourceParts(source) {
|
|
2140
|
+
return source.kind === "builtin"
|
|
2141
|
+
? { type: "builtin", ref: source.id, revision: source.revision }
|
|
2142
|
+
: { type: "file", ref: source.path, revision: source.hash };
|
|
2143
|
+
}
|
|
2144
|
+
function workflowSourceFromRow(row) {
|
|
2145
|
+
return row.sourceType === "builtin"
|
|
2146
|
+
? { kind: "builtin", id: row.sourceRef, revision: row.sourceRevision }
|
|
2147
|
+
: { kind: "file", path: row.sourceRef, hash: row.sourceRevision };
|
|
2148
|
+
}
|
|
923
2149
|
function segmentIdFor(runId, attemptId) {
|
|
924
2150
|
return `segment-${createHash("sha256")
|
|
925
2151
|
.update(`${runId}\0${attemptId ?? ""}`)
|
|
@@ -941,6 +2167,73 @@ function outcomeStatus(outcome) {
|
|
|
941
2167
|
return "failed";
|
|
942
2168
|
}
|
|
943
2169
|
}
|
|
2170
|
+
function outcomeForStatus(status) {
|
|
2171
|
+
switch (status) {
|
|
2172
|
+
case "completed":
|
|
2173
|
+
return "ok";
|
|
2174
|
+
case "timed_out":
|
|
2175
|
+
return "timed_out";
|
|
2176
|
+
case "cancelled":
|
|
2177
|
+
return "cancelled";
|
|
2178
|
+
case "failed":
|
|
2179
|
+
return "failed";
|
|
2180
|
+
default:
|
|
2181
|
+
throw new Error(`Workflow step has nonterminal status: ${status}`);
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
function attemptReceipt(step) {
|
|
2185
|
+
return {
|
|
2186
|
+
...(step.action === undefined ? {} : { action: step.action }),
|
|
2187
|
+
...(step.assistantMessage === undefined ? {} : { assistantMessage: step.assistantMessage }),
|
|
2188
|
+
};
|
|
2189
|
+
}
|
|
2190
|
+
function promptFromEntry(value) {
|
|
2191
|
+
if (!isRecord(value))
|
|
2192
|
+
return null;
|
|
2193
|
+
const content = value.content;
|
|
2194
|
+
if (typeof content === "string")
|
|
2195
|
+
return content;
|
|
2196
|
+
if (!Array.isArray(content))
|
|
2197
|
+
return null;
|
|
2198
|
+
return content
|
|
2199
|
+
.filter(isRecord)
|
|
2200
|
+
.map((part) => (typeof part.text === "string" ? part.text : ""))
|
|
2201
|
+
.filter((text) => text.length !== 0)
|
|
2202
|
+
.join("\n");
|
|
2203
|
+
}
|
|
2204
|
+
function assistantOutputFromEntry(value, maxChars) {
|
|
2205
|
+
if (!isRecord(value) || !isRecord(value.message)) {
|
|
2206
|
+
throw new Error("Assistant response entry is invalid");
|
|
2207
|
+
}
|
|
2208
|
+
const message = value.message;
|
|
2209
|
+
if (message.role !== "assistant" || !Array.isArray(message.content)) {
|
|
2210
|
+
throw new Error("Assistant response entry does not contain an assistant message");
|
|
2211
|
+
}
|
|
2212
|
+
const text = message.content
|
|
2213
|
+
.filter(isRecord)
|
|
2214
|
+
.filter((part) => part.type === "text" && typeof part.text === "string")
|
|
2215
|
+
.map((part) => part.text)
|
|
2216
|
+
.join("\n");
|
|
2217
|
+
if (text.trim().length === 0)
|
|
2218
|
+
throw new Error("Assistant response entry has no visible text");
|
|
2219
|
+
if (maxChars !== undefined && text.length > maxChars) {
|
|
2220
|
+
throw new Error(`Assistant response exceeds its stored limit of ${maxChars} characters`);
|
|
2221
|
+
}
|
|
2222
|
+
return text;
|
|
2223
|
+
}
|
|
2224
|
+
function resultForStep(step) {
|
|
2225
|
+
return {
|
|
2226
|
+
attemptId: step.attemptId,
|
|
2227
|
+
nodeId: step.nodeId,
|
|
2228
|
+
nodeType: step.nodeType,
|
|
2229
|
+
outcome: step.outcome,
|
|
2230
|
+
startedAt: step.startedAt,
|
|
2231
|
+
finishedAt: step.finishedAt,
|
|
2232
|
+
durationMs: Date.parse(step.finishedAt) - Date.parse(step.startedAt),
|
|
2233
|
+
...(step.outcome === "ok" ? { output: step.output } : {}),
|
|
2234
|
+
...(step.error === undefined ? {} : { error: step.error }),
|
|
2235
|
+
};
|
|
2236
|
+
}
|
|
944
2237
|
function traceScope(value) {
|
|
945
2238
|
return value === "node" || value === "agent" || value === "action" || value === "session"
|
|
946
2239
|
? value
|
|
@@ -968,6 +2261,10 @@ function validateSessionEventRecord(record) {
|
|
|
968
2261
|
Array.isArray(record.payload)) {
|
|
969
2262
|
throw new Error("Session event is missing required envelope fields");
|
|
970
2263
|
}
|
|
2264
|
+
if (record.type === "assistant_event" &&
|
|
2265
|
+
["text_delta", "thinking_delta", "toolcall_delta"].includes(String(record.payload.type))) {
|
|
2266
|
+
throw new Error("Incremental assistant events are not durable session facts");
|
|
2267
|
+
}
|
|
971
2268
|
}
|
|
972
2269
|
function validateSessionCapture(capture) {
|
|
973
2270
|
if (capture.schema !== SESSION_CAPTURE_SCHEMA ||
|
|
@@ -988,6 +2285,143 @@ function validateSessionCapture(capture) {
|
|
|
988
2285
|
throw new Error("Only failed session capture may contain failure details");
|
|
989
2286
|
}
|
|
990
2287
|
}
|
|
2288
|
+
function followUpIdFor(runId, requestId) {
|
|
2289
|
+
const digest = createHash("sha256").update(`${runId}\0${requestId}`).digest("hex").slice(0, 40);
|
|
2290
|
+
return `follow-up-${digest}`;
|
|
2291
|
+
}
|
|
2292
|
+
function assertRequestId(requestId) {
|
|
2293
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,511}$/u.test(requestId)) {
|
|
2294
|
+
throw new Error("Workflow request ID must be a stable identifier of at most 512 characters");
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
function assertSourceType(source) {
|
|
2298
|
+
if (typeof source !== "string" || source.trim().length === 0 || source.length > 128) {
|
|
2299
|
+
throw new Error("Workflow change source must be a non-empty string of at most 128 characters");
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
function assertActorType(value) {
|
|
2303
|
+
if (value !== "session" &&
|
|
2304
|
+
value !== "host" &&
|
|
2305
|
+
value !== "controller" &&
|
|
2306
|
+
value !== "channel" &&
|
|
2307
|
+
value !== "human" &&
|
|
2308
|
+
value !== "policy" &&
|
|
2309
|
+
value !== "control" &&
|
|
2310
|
+
value !== "system") {
|
|
2311
|
+
throw new Error(`Unknown saved actor type: ${value}`);
|
|
2312
|
+
}
|
|
2313
|
+
return value;
|
|
2314
|
+
}
|
|
2315
|
+
function assertFollowUpState(value) {
|
|
2316
|
+
if (value !== "queued" &&
|
|
2317
|
+
value !== "pending_presentation" &&
|
|
2318
|
+
value !== "ready" &&
|
|
2319
|
+
value !== "sent" &&
|
|
2320
|
+
value !== "removed" &&
|
|
2321
|
+
value !== "cancelled") {
|
|
2322
|
+
throw new Error(`Unknown workflow follow-up state: ${value}`);
|
|
2323
|
+
}
|
|
2324
|
+
return value;
|
|
2325
|
+
}
|
|
2326
|
+
function assertPresentationState(value) {
|
|
2327
|
+
if (value !== "none" &&
|
|
2328
|
+
value !== "not-needed" &&
|
|
2329
|
+
value !== "pending" &&
|
|
2330
|
+
value !== "settled" &&
|
|
2331
|
+
value !== "unavailable") {
|
|
2332
|
+
throw new Error(`Unknown workflow presentation state: ${value}`);
|
|
2333
|
+
}
|
|
2334
|
+
return value;
|
|
2335
|
+
}
|
|
2336
|
+
function isSettingsScopeRow(value) {
|
|
2337
|
+
return (isRecord(value) &&
|
|
2338
|
+
typeof value.scopeId === "string" &&
|
|
2339
|
+
typeof value.resourceId === "string" &&
|
|
2340
|
+
typeof value.originRunId === "string" &&
|
|
2341
|
+
typeof value.activeRunId === "string" &&
|
|
2342
|
+
typeof value.mountPath === "string" &&
|
|
2343
|
+
typeof value.invocation === "number" &&
|
|
2344
|
+
Buffer.isBuffer(value.currentHash) &&
|
|
2345
|
+
typeof value.revision === "number" &&
|
|
2346
|
+
typeof value.createdAt === "number" &&
|
|
2347
|
+
typeof value.updatedAt === "number");
|
|
2348
|
+
}
|
|
2349
|
+
function isSettingsProjectionRow(value) {
|
|
2350
|
+
return (isRecord(value) &&
|
|
2351
|
+
typeof value.scopeId === "string" &&
|
|
2352
|
+
Buffer.isBuffer(value.initialHash) &&
|
|
2353
|
+
Buffer.isBuffer(value.currentHash) &&
|
|
2354
|
+
typeof value.revision === "number");
|
|
2355
|
+
}
|
|
2356
|
+
function isSettingsProjectionChangeRow(value) {
|
|
2357
|
+
return (isRecord(value) &&
|
|
2358
|
+
typeof value.changeNumber === "number" &&
|
|
2359
|
+
Buffer.isBuffer(value.patchHash) &&
|
|
2360
|
+
Buffer.isBuffer(value.beforeHash) &&
|
|
2361
|
+
Buffer.isBuffer(value.afterHash));
|
|
2362
|
+
}
|
|
2363
|
+
function isSettingsChangeRow(value) {
|
|
2364
|
+
return (isRecord(value) &&
|
|
2365
|
+
typeof value.changeId === "string" &&
|
|
2366
|
+
typeof value.scopeId === "string" &&
|
|
2367
|
+
typeof value.requestId === "string" &&
|
|
2368
|
+
typeof value.changeNumber === "number" &&
|
|
2369
|
+
typeof value.actorType === "string" &&
|
|
2370
|
+
(typeof value.actorId === "string" || value.actorId === null) &&
|
|
2371
|
+
typeof value.sourceType === "string" &&
|
|
2372
|
+
Buffer.isBuffer(value.patchHash) &&
|
|
2373
|
+
Buffer.isBuffer(value.beforeHash) &&
|
|
2374
|
+
Buffer.isBuffer(value.afterHash) &&
|
|
2375
|
+
typeof value.acceptedAt === "number");
|
|
2376
|
+
}
|
|
2377
|
+
function isFollowUpQueueRow(value) {
|
|
2378
|
+
return (isRecord(value) &&
|
|
2379
|
+
typeof value.runId === "string" &&
|
|
2380
|
+
typeof value.resourceId === "string" &&
|
|
2381
|
+
(typeof value.originSessionId === "string" || value.originSessionId === null) &&
|
|
2382
|
+
typeof value.presentationState === "string" &&
|
|
2383
|
+
(typeof value.presentationEntryId === "string" || value.presentationEntryId === null) &&
|
|
2384
|
+
(typeof value.presentationAssistantEntryId === "string" ||
|
|
2385
|
+
value.presentationAssistantEntryId === null) &&
|
|
2386
|
+
(Buffer.isBuffer(value.presentationReasonHash) || value.presentationReasonHash === null) &&
|
|
2387
|
+
typeof value.revision === "number");
|
|
2388
|
+
}
|
|
2389
|
+
function isFollowUpRow(value) {
|
|
2390
|
+
return (isRecord(value) &&
|
|
2391
|
+
typeof value.followUpId === "string" &&
|
|
2392
|
+
typeof value.resourceId === "string" &&
|
|
2393
|
+
typeof value.queueResourceId === "string" &&
|
|
2394
|
+
typeof value.runId === "string" &&
|
|
2395
|
+
typeof value.requestId === "string" &&
|
|
2396
|
+
typeof value.orderNumber === "number" &&
|
|
2397
|
+
typeof value.targetSessionId === "string" &&
|
|
2398
|
+
typeof value.actorType === "string" &&
|
|
2399
|
+
(typeof value.actorId === "string" || value.actorId === null) &&
|
|
2400
|
+
typeof value.sourceType === "string" &&
|
|
2401
|
+
Buffer.isBuffer(value.promptHash) &&
|
|
2402
|
+
typeof value.status === "string" &&
|
|
2403
|
+
(typeof value.sessionEntryId === "string" || value.sessionEntryId === null) &&
|
|
2404
|
+
(Buffer.isBuffer(value.reasonHash) || value.reasonHash === null) &&
|
|
2405
|
+
typeof value.createdAt === "number" &&
|
|
2406
|
+
typeof value.updatedAt === "number" &&
|
|
2407
|
+
(typeof value.sentAt === "number" || value.sentAt === null) &&
|
|
2408
|
+
typeof value.revision === "number");
|
|
2409
|
+
}
|
|
2410
|
+
function isParentRunRow(value) {
|
|
2411
|
+
return isRecord(value) && (typeof value.parentRunId === "string" || value.parentRunId === null);
|
|
2412
|
+
}
|
|
2413
|
+
function isStatusRow(value) {
|
|
2414
|
+
return isRecord(value) && typeof value.status === "string";
|
|
2415
|
+
}
|
|
2416
|
+
function isCountRow(value) {
|
|
2417
|
+
return isRecord(value) && typeof value.count === "number";
|
|
2418
|
+
}
|
|
2419
|
+
function isLeaseExpiryRow(value) {
|
|
2420
|
+
return isRecord(value) && (typeof value.expiresAt === "number" || value.expiresAt === null);
|
|
2421
|
+
}
|
|
2422
|
+
function isNextOrderRow(value) {
|
|
2423
|
+
return isRecord(value) && typeof value.nextOrder === "number";
|
|
2424
|
+
}
|
|
991
2425
|
function assertValidRunId(runId) {
|
|
992
2426
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(runId)) {
|
|
993
2427
|
throw new Error(`Invalid workflow run id: ${JSON.stringify(runId)}`);
|
|
@@ -999,9 +2433,63 @@ function isRecord(value) {
|
|
|
999
2433
|
function isRunRow(value) {
|
|
1000
2434
|
return isRecord(value);
|
|
1001
2435
|
}
|
|
2436
|
+
function savedCurrentSettingsBinding(scopeId, changeNumber, settingsHash) {
|
|
2437
|
+
if (scopeId === null && changeNumber === null && settingsHash === null)
|
|
2438
|
+
return {};
|
|
2439
|
+
if (scopeId === null || changeNumber === null || settingsHash === null) {
|
|
2440
|
+
throw new Error("Saved current workflow settings binding is incomplete");
|
|
2441
|
+
}
|
|
2442
|
+
return {
|
|
2443
|
+
currentSettingsScopeId: scopeId,
|
|
2444
|
+
currentSettingsChangeNumber: changeNumber,
|
|
2445
|
+
currentSettingsHash: settingsHash.toString("hex"),
|
|
2446
|
+
};
|
|
2447
|
+
}
|
|
2448
|
+
function savedStepSettingsBinding(scopeId, changeNumber, settingsHash) {
|
|
2449
|
+
if (scopeId === null && changeNumber === null && settingsHash === null)
|
|
2450
|
+
return {};
|
|
2451
|
+
if (scopeId === null || changeNumber === null || settingsHash === null) {
|
|
2452
|
+
throw new Error("Saved workflow step settings binding is incomplete");
|
|
2453
|
+
}
|
|
2454
|
+
return {
|
|
2455
|
+
settingsScopeId: scopeId,
|
|
2456
|
+
settingsChangeNumber: changeNumber,
|
|
2457
|
+
settingsHash: settingsHash.toString("hex"),
|
|
2458
|
+
};
|
|
2459
|
+
}
|
|
1002
2460
|
function isRunIdRow(value) {
|
|
1003
2461
|
return isRecord(value);
|
|
1004
2462
|
}
|
|
2463
|
+
function isRunStepIdentityRow(value) {
|
|
2464
|
+
return isRecord(value);
|
|
2465
|
+
}
|
|
2466
|
+
function isStepRow(value) {
|
|
2467
|
+
return isRecord(value);
|
|
2468
|
+
}
|
|
2469
|
+
function isRunSourceRow(value) {
|
|
2470
|
+
return isRecord(value);
|
|
2471
|
+
}
|
|
2472
|
+
function isSegmentIdentityRow(value) {
|
|
2473
|
+
return isRecord(value) && typeof value.segmentId === "string" && Buffer.isBuffer(value.entryHash);
|
|
2474
|
+
}
|
|
2475
|
+
function isEntryIdentityRow(value) {
|
|
2476
|
+
return isRecord(value) && typeof value.entryId === "string";
|
|
2477
|
+
}
|
|
2478
|
+
function isAttemptValueRow(value) {
|
|
2479
|
+
return isRecord(value);
|
|
2480
|
+
}
|
|
2481
|
+
function isActiveAttemptRow(value) {
|
|
2482
|
+
return isRecord(value);
|
|
2483
|
+
}
|
|
2484
|
+
function isDecisionReceiptRow(value) {
|
|
2485
|
+
return (isRecord(value) && Buffer.isBuffer(value.requestHash) && Buffer.isBuffer(value.responseHash));
|
|
2486
|
+
}
|
|
2487
|
+
function isContinuationIdentityRow(value) {
|
|
2488
|
+
return isRecord(value);
|
|
2489
|
+
}
|
|
2490
|
+
function isUpdateRow(value) {
|
|
2491
|
+
return isRecord(value);
|
|
2492
|
+
}
|
|
1005
2493
|
function isRevisionRow(value) {
|
|
1006
2494
|
return isRecord(value);
|
|
1007
2495
|
}
|
|
@@ -1027,7 +2515,15 @@ function isLeaseAuthorityRow(value) {
|
|
|
1027
2515
|
return isRecord(value);
|
|
1028
2516
|
}
|
|
1029
2517
|
export function createDefinitionSnapshot(workflow) {
|
|
1030
|
-
const
|
|
2518
|
+
const metadata = compositionMetadata(workflow);
|
|
2519
|
+
const composition = metadata?.snapshot;
|
|
2520
|
+
const settingsScopes = Object.values(metadata?.scopes ?? {})
|
|
2521
|
+
.filter((scope) => scope.path !== "" && scope.settings !== undefined)
|
|
2522
|
+
.sort((left, right) => left.path.localeCompare(right.path))
|
|
2523
|
+
.map((scope) => ({
|
|
2524
|
+
mountPath: scope.path.split("/"),
|
|
2525
|
+
settings: snapshotSettings(scope.settings),
|
|
2526
|
+
}));
|
|
1031
2527
|
return {
|
|
1032
2528
|
schema: DEFINITION_SNAPSHOT_SCHEMA,
|
|
1033
2529
|
name: workflow.name,
|
|
@@ -1039,6 +2535,8 @@ export function createDefinitionSnapshot(workflow) {
|
|
|
1039
2535
|
])),
|
|
1040
2536
|
edges: structuredClone(workflow.edges),
|
|
1041
2537
|
...(composition !== undefined ? { composition: structuredClone(composition) } : {}),
|
|
2538
|
+
...(workflow.settings !== undefined ? { settings: snapshotSettings(workflow.settings) } : {}),
|
|
2539
|
+
...(settingsScopes.length > 0 ? { settingsScopes } : {}),
|
|
1042
2540
|
};
|
|
1043
2541
|
}
|
|
1044
2542
|
function snapshotNode(workflow, nodeId, node) {
|
|
@@ -1073,6 +2571,9 @@ function snapshotNode(workflow, nodeId, node) {
|
|
|
1073
2571
|
if (node.nodeType === "agent" && node.expectedOutput !== undefined) {
|
|
1074
2572
|
common.expectedOutput = node.expectedOutput;
|
|
1075
2573
|
}
|
|
2574
|
+
if (node.nodeType === "compute" && node.settingsRoute === true) {
|
|
2575
|
+
common.settingsRoute = true;
|
|
2576
|
+
}
|
|
1076
2577
|
if (node.nodeType === "notify")
|
|
1077
2578
|
common.summary = node.kind ?? "progress";
|
|
1078
2579
|
if (node.nodeType === "checkpoint" && node.summary !== undefined)
|
|
@@ -1093,4 +2594,16 @@ function snapshotNode(workflow, nodeId, node) {
|
|
|
1093
2594
|
common.actionExecution = "exec" in node ? "shell" : "function";
|
|
1094
2595
|
return common;
|
|
1095
2596
|
}
|
|
2597
|
+
function snapshotSettings(settings) {
|
|
2598
|
+
return {
|
|
2599
|
+
...(settings.description !== undefined ? { description: settings.description } : {}),
|
|
2600
|
+
paths: settings.paths.map((rule) => ({
|
|
2601
|
+
path: rule.path,
|
|
2602
|
+
permissions: Object.fromEntries(Object.entries(rule.permissions).map(([permission, actors]) => [
|
|
2603
|
+
permission,
|
|
2604
|
+
actors === undefined ? [] : [...actors],
|
|
2605
|
+
])),
|
|
2606
|
+
})),
|
|
2607
|
+
};
|
|
2608
|
+
}
|
|
1096
2609
|
//# sourceMappingURL=store.js.map
|