@osolmaz/pi-workflows 0.13.2 → 0.13.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/builtins/autodoc.workflow.d.ts +5 -5
- package/dist/builtins/autoimplement.workflow.d.ts +77 -73
- package/dist/builtins/autoimplement.workflow.js +47 -8
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/builtins/autoplan.workflow.d.ts +8 -8
- package/dist/builtins/autoplan.workflow.js +44 -35
- package/dist/builtins/autoplan.workflow.js.map +1 -1
- package/dist/builtins/catalog.js +2 -2
- package/dist/builtins/change-verification.workflow.d.ts +1 -1
- package/dist/builtins/plain-summary.workflow.d.ts +1 -1
- package/dist/builtins/plan-approval.workflow.d.ts +1 -1
- package/dist/builtins/plan-change.workflow.d.ts +29 -29
- package/dist/builtins/sanity-check.workflow.d.ts +3 -3
- package/dist/builtins/workspace-preparation.workflow.d.ts +1 -1
- package/dist/controllers/index.d.ts +2 -2
- package/dist/controllers/index.js.map +1 -1
- package/dist/controllers/sqlite.js +85 -23
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/controllers/types.d.ts +34 -0
- package/dist/controllers/workflow-engine-scheduler.d.ts +5 -1
- package/dist/controllers/workflow-engine-scheduler.js +83 -1
- package/dist/controllers/workflow-engine-scheduler.js.map +1 -1
- package/dist/controllers/workflows.d.ts +8 -1
- package/dist/controllers/workflows.js +36 -0
- package/dist/controllers/workflows.js.map +1 -1
- package/dist/extension/controller-host.d.ts +2 -1
- package/dist/extension/controller-host.js +1 -1
- package/dist/extension/controller-host.js.map +1 -1
- package/dist/extension/follow-up-coordinator.d.ts +27 -0
- package/dist/extension/follow-up-coordinator.js +131 -0
- package/dist/extension/follow-up-coordinator.js.map +1 -0
- package/dist/extension/index.d.ts +9 -0
- package/dist/extension/index.js +460 -75
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/session-events.d.ts +2 -3
- package/dist/extension/session-events.js +11 -10
- package/dist/extension/session-events.js.map +1 -1
- package/dist/extension/widget.js +9 -0
- package/dist/extension/widget.js.map +1 -1
- package/dist/state/database.d.ts +2 -1
- package/dist/state/database.js +11 -13
- package/dist/state/database.js.map +1 -1
- package/dist/state/json.js +6 -1
- package/dist/state/json.js.map +1 -1
- package/dist/state/mutation.d.ts +1 -1
- package/dist/state/mutation.js.map +1 -1
- package/dist/state/prune.d.ts +18 -0
- package/dist/state/prune.js +373 -0
- package/dist/state/prune.js.map +1 -0
- package/dist/state/schema.d.ts +1 -1
- package/dist/state/schema.js +116 -18
- package/dist/state/schema.js.map +1 -1
- package/dist/viewer/cli.d.ts +3 -1
- package/dist/viewer/cli.js +55 -4
- package/dist/viewer/cli.js.map +1 -1
- package/dist/viewer/render.js +19 -1
- package/dist/viewer/render.js.map +1 -1
- package/dist/workflows/composition.d.ts +2 -0
- package/dist/workflows/composition.js +15 -0
- package/dist/workflows/composition.js.map +1 -1
- package/dist/workflows/definition.d.ts +6 -3
- package/dist/workflows/definition.js +3 -0
- package/dist/workflows/definition.js.map +1 -1
- package/dist/workflows/engine.d.ts +6 -1
- package/dist/workflows/engine.js +301 -35
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/human-decision.d.ts +1 -0
- package/dist/workflows/human-decision.js +25 -9
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +3 -1
- package/dist/workflows/index.js +2 -0
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/json-patch.d.ts +47 -0
- package/dist/workflows/json-patch.js +298 -0
- package/dist/workflows/json-patch.js.map +1 -0
- package/dist/workflows/schema.js +29 -1
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/settings.d.ts +128 -0
- package/dist/workflows/settings.js +199 -0
- package/dist/workflows/settings.js.map +1 -0
- package/dist/workflows/store.d.ts +75 -2
- package/dist/workflows/store.js +1349 -89
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/tool-input.d.ts +22 -0
- package/dist/workflows/tool-input.js +43 -0
- package/dist/workflows/tool-input.js.map +1 -1
- package/dist/workflows/types.d.ts +40 -5
- package/docs/2026-08-25-workflow-follow-ups.md +132 -0
- package/docs/2026-08-25-workflow-settings.md +169 -0
- package/docs/DEFERRED_TURNS.md +6 -0
- package/docs/DESIGN_PHILOSOPHY.md +2 -0
- package/docs/SQLITE_STATE.md +31 -18
- package/docs/WORKFLOW_COMPOSITION.md +16 -1
- package/docs/WORKFLOW_STEP_MESSAGES.md +4 -2
- package/docs/plans/2026-08-25-live-workflow-settings-plan.md +532 -0
- package/docs/plans/2026-08-25-workflow-run-storage-plan.md +67 -0
- package/docs/tui-viewer.md +7 -6
- package/docs/workflows.md +22 -1
- package/examples/workflows/live-settings.workflow.ts +93 -0
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/skills/autoplan/SKILL.md +6 -6
- package/src/builtins/autoimplement.workflow.ts +56 -8
- package/src/builtins/autoplan.workflow.ts +45 -37
- package/src/builtins/catalog.ts +2 -2
- package/src/controllers/index.ts +6 -0
- package/src/controllers/sqlite.ts +129 -35
- package/src/controllers/types.ts +40 -0
- package/src/controllers/workflow-engine-scheduler.ts +103 -1
- package/src/controllers/workflows.ts +68 -0
- package/src/extension/controller-host.ts +6 -1
- package/src/extension/follow-up-coordinator.ts +151 -0
- package/src/extension/index.ts +538 -88
- package/src/extension/session-events.ts +15 -13
- package/src/extension/widget.ts +8 -0
- package/src/state/database.ts +12 -12
- package/src/state/json.ts +6 -1
- package/src/state/mutation.ts +4 -1
- package/src/state/prune.ts +502 -0
- package/src/state/schema.ts +116 -18
- package/src/viewer/cli.ts +52 -5
- package/src/viewer/render.ts +43 -1
- package/src/workflows/composition.ts +19 -0
- package/src/workflows/definition.ts +19 -5
- package/src/workflows/engine.ts +363 -35
- package/src/workflows/human-decision.ts +41 -11
- package/src/workflows/index.ts +43 -0
- package/src/workflows/json-patch.ts +375 -0
- package/src/workflows/schema.ts +31 -1
- package/src/workflows/settings.ts +430 -0
- package/src/workflows/store.ts +1977 -184
- package/src/workflows/tool-input.ts +58 -0
- package/src/workflows/types.ts +43 -4
package/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,38 +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 workflowSourcesHash = state.workflowSources === undefined
|
|
107
|
-
? null
|
|
108
|
-
: this.state.putJson(state.workflowSources, now);
|
|
109
|
-
const humanDecisionHash = state.humanDecision === undefined ? null : this.state.putJson(state.humanDecision, now);
|
|
110
123
|
const finalOutputHash = state.finalOutput === undefined ? null : this.state.putJson(state.finalOutput, now);
|
|
111
124
|
const errorHash = state.error === undefined ? null : this.state.putText(state.error, now);
|
|
112
125
|
this.state.connection
|
|
113
126
|
.prepare(`INSERT INTO runs(
|
|
114
127
|
run_id, resource_id, project_id, parent_run_id, definition_digest,
|
|
115
|
-
workflow_ref,
|
|
116
|
-
|
|
117
|
-
status_detail, input_hash, workflow_sources_hash, human_decision_hash,
|
|
118
|
-
final_output_hash, error_hash, carried_step_count, current_node,
|
|
119
|
-
current_attempt_id, current_node_started_at, waiting_on,
|
|
128
|
+
workflow_ref, launch_options_hash, title, status, paused,
|
|
129
|
+
status_detail, input_hash, final_output_hash, error_hash,
|
|
120
130
|
created_at, updated_at, finished_at
|
|
121
|
-
) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
122
|
-
.run(state.runId, resourceId, state.parentRunId ?? null, definitionDigest, state.workflowName,
|
|
123
|
-
|
|
124
|
-
|
|
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);
|
|
125
135
|
insertRunEvent(this.state, resourceId, 1, at, {
|
|
126
136
|
scope: "run",
|
|
127
137
|
type: "run_created",
|
|
128
138
|
payload: { workflowName: workflow.name },
|
|
129
139
|
});
|
|
140
|
+
this.initializeRunSettingsAndFollowUps(state, options.initialSettings ?? [], now);
|
|
130
141
|
this.syncNodeAttempts(state, snapshot, now);
|
|
131
142
|
});
|
|
132
143
|
this.contexts.set(state.runId, { revision: acceptedRevision, lock: Promise.resolve() });
|
|
133
144
|
return state.runId;
|
|
134
145
|
});
|
|
135
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
|
+
}
|
|
136
579
|
async prepareRunResume(runId) {
|
|
137
580
|
const loaded = this.readRun(runId, { includeTrace: true });
|
|
138
581
|
if (loaded === null)
|
|
@@ -140,6 +583,7 @@ export class WorkflowRunStore {
|
|
|
140
583
|
if (loaded.state.status !== "running") {
|
|
141
584
|
throw new Error(`Cannot resume workflow run ${runId} with status ${loaded.state.status}`);
|
|
142
585
|
}
|
|
586
|
+
this.verifySettingsProjections(runId);
|
|
143
587
|
const revision = this.resourceRevision(runId);
|
|
144
588
|
this.contexts.set(runId, { revision, lock: Promise.resolve() });
|
|
145
589
|
this.finalizeRecordingCaptures(runId, "Workflow host stopped before the run finished");
|
|
@@ -152,9 +596,9 @@ export class WorkflowRunStore {
|
|
|
152
596
|
const at = new Date(now).toISOString();
|
|
153
597
|
this.state.connection
|
|
154
598
|
.prepare(`UPDATE node_attempts
|
|
155
|
-
SET status = '
|
|
599
|
+
SET status = 'interrupted', updated_at = ?
|
|
156
600
|
WHERE run_id = ? AND status IN ('pending', 'running', 'waiting')`)
|
|
157
|
-
.run(now,
|
|
601
|
+
.run(now, runId);
|
|
158
602
|
insertRunEvent(this.state, row.resourceId, nextRevision, at, {
|
|
159
603
|
scope: "run",
|
|
160
604
|
type: "run_resume_prepared",
|
|
@@ -181,6 +625,9 @@ export class WorkflowRunStore {
|
|
|
181
625
|
delete loaded.state.currentNode;
|
|
182
626
|
delete loaded.state.currentAttemptId;
|
|
183
627
|
delete loaded.state.currentNodeStartedAt;
|
|
628
|
+
delete loaded.state.currentSettingsScopeId;
|
|
629
|
+
delete loaded.state.currentSettingsChangeNumber;
|
|
630
|
+
delete loaded.state.currentSettingsHash;
|
|
184
631
|
delete loaded.state.statusDetail;
|
|
185
632
|
delete loaded.state.paused;
|
|
186
633
|
await this.writeSnapshot(runId, loaded.state, {
|
|
@@ -264,12 +711,14 @@ export class WorkflowRunStore {
|
|
|
264
711
|
const revision = context.revision + 1;
|
|
265
712
|
const now = Date.now();
|
|
266
713
|
const at = new Date(now).toISOString();
|
|
714
|
+
const snapshot = this.readDefinition(run.definitionHash);
|
|
715
|
+
this.assertSettingsRouteCurrent(event, snapshot);
|
|
267
716
|
const traceEvent = { seq: revision, at, runId, ...event };
|
|
268
717
|
state.traceSeq = revision;
|
|
269
718
|
state.updatedAt = at;
|
|
270
719
|
insertRunEvent(this.state, run.resourceId, revision, at, traceEvent);
|
|
271
720
|
this.persistRunState(run, state, revision, now);
|
|
272
|
-
|
|
721
|
+
this.transitionFollowUpsForRunState(state, event, now);
|
|
273
722
|
this.syncNodeAttempts(state, snapshot, now);
|
|
274
723
|
context.revision = revision;
|
|
275
724
|
accepted = traceEvent;
|
|
@@ -325,6 +774,11 @@ export class WorkflowRunStore {
|
|
|
325
774
|
VALUES (?, ?, 'interactive', ?)
|
|
326
775
|
ON CONFLICT(run_id) DO NOTHING`)
|
|
327
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);
|
|
328
782
|
const revision = context.revision + 1;
|
|
329
783
|
const at = new Date(now).toISOString();
|
|
330
784
|
insertRunEvent(this.state, run.resourceId, revision, at, {
|
|
@@ -363,7 +817,7 @@ export class WorkflowRunStore {
|
|
|
363
817
|
.prepare("UPDATE session_segments SET entry_count = ? WHERE segment_id = ?")
|
|
364
818
|
.run(sequence, segment.segmentId);
|
|
365
819
|
this.bumpResource(segmentResourceId(segment), revision, now);
|
|
366
|
-
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);
|
|
367
821
|
return sequence;
|
|
368
822
|
});
|
|
369
823
|
}
|
|
@@ -450,6 +904,8 @@ export class WorkflowRunStore {
|
|
|
450
904
|
sessionCapture: flat.capture,
|
|
451
905
|
sessionIntegrity: flat.integrity,
|
|
452
906
|
sessionSegments: segments.filter((segment) => segment.attemptId !== ""),
|
|
907
|
+
settingsScopes: this.settingsScopesForRunView(runId),
|
|
908
|
+
followUpQueue: this.readFollowUpQueue(runId) ?? null,
|
|
453
909
|
};
|
|
454
910
|
}
|
|
455
911
|
listRuns(options = {}) {
|
|
@@ -554,18 +1010,14 @@ export class WorkflowRunStore {
|
|
|
554
1010
|
}
|
|
555
1011
|
persistRunState(run, state, expectedRevision, now) {
|
|
556
1012
|
this.bumpResource(run.resourceId, expectedRevision - 1, now);
|
|
557
|
-
const workflowSourcesHash = state.workflowSources === undefined ? null : this.state.putJson(state.workflowSources, now);
|
|
558
|
-
const humanDecisionHash = state.humanDecision === undefined ? null : this.state.putJson(state.humanDecision, now);
|
|
559
1013
|
const finalOutputHash = state.finalOutput === undefined ? null : this.state.putJson(state.finalOutput, now);
|
|
560
1014
|
const errorHash = state.error === undefined ? null : this.state.putText(state.error, now);
|
|
561
1015
|
const update = this.state.connection
|
|
562
1016
|
.prepare(`UPDATE runs
|
|
563
1017
|
SET title = ?, status = ?, paused = ?, status_detail = ?,
|
|
564
|
-
|
|
565
|
-
error_hash = ?, carried_step_count = ?, current_node = ?, current_attempt_id = ?,
|
|
566
|
-
current_node_started_at = ?, waiting_on = ?, updated_at = ?, finished_at = ?
|
|
1018
|
+
final_output_hash = ?, error_hash = ?, updated_at = ?, finished_at = ?
|
|
567
1019
|
WHERE run_id = ?`)
|
|
568
|
-
.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);
|
|
569
1021
|
if (update.changes !== 1)
|
|
570
1022
|
throw new Error(`Workflow run is missing: ${state.runId}`);
|
|
571
1023
|
if (state.status !== "running") {
|
|
@@ -606,13 +1058,341 @@ export class WorkflowRunStore {
|
|
|
606
1058
|
.run(effectId, effectResourceId, runResourceId, sourceRevision, effectType, idempotencyKey, payloadHash, now, now);
|
|
607
1059
|
insertGenericEvent(this.state, effectResourceId, 1, "effect.created", "system", null, payloadHash, now);
|
|
608
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
|
+
}
|
|
609
1386
|
syncNodeAttempts(state, snapshot, now) {
|
|
610
1387
|
for (const step of state.steps.slice(state.carriedStepCount ?? 0)) {
|
|
611
|
-
const
|
|
612
|
-
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);
|
|
613
1394
|
const outputHash = step.output === undefined ? null : this.state.putJson(step.output, now);
|
|
614
1395
|
const errorHash = step.error === undefined ? null : this.state.putText(step.error, now);
|
|
615
|
-
const promptHash = step.prompt === null ? null : this.state.putJson(step.prompt, now);
|
|
616
1396
|
const existing = this.state.connection
|
|
617
1397
|
.prepare("SELECT attempt_id AS attemptId FROM node_attempts WHERE attempt_id = ?")
|
|
618
1398
|
.get(step.attemptId);
|
|
@@ -621,18 +1401,25 @@ export class WorkflowRunStore {
|
|
|
621
1401
|
this.state.connection
|
|
622
1402
|
.prepare(`INSERT INTO node_attempts(
|
|
623
1403
|
attempt_id, run_id, node_id, attempt_number, node_type, status,
|
|
624
|
-
prompt_hash, output_hash,
|
|
1404
|
+
prompt_hash, output_hash, receipt_hash, error_hash,
|
|
1405
|
+
settings_scope_id, settings_change_number, settings_hash,
|
|
625
1406
|
started_at, finished_at, created_at, updated_at
|
|
626
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
627
|
-
.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);
|
|
628
1409
|
}
|
|
629
1410
|
else {
|
|
630
1411
|
this.state.connection
|
|
631
1412
|
.prepare(`UPDATE node_attempts
|
|
632
|
-
SET status = ?, prompt_hash = ?, output_hash = ?,
|
|
633
|
-
|
|
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 = ?
|
|
634
1416
|
WHERE attempt_id = ?`)
|
|
635
|
-
.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);
|
|
636
1423
|
}
|
|
637
1424
|
}
|
|
638
1425
|
if (state.currentAttemptId !== undefined && state.currentNode !== undefined) {
|
|
@@ -659,16 +1446,9 @@ export class WorkflowRunStore {
|
|
|
659
1446
|
/* istanbul ignore if -- array index follows a checked bound */
|
|
660
1447
|
if (step === undefined)
|
|
661
1448
|
throw new Error("Workflow run step became unavailable");
|
|
662
|
-
const
|
|
663
|
-
.prepare("SELECT output_hash AS outputHash FROM node_attempts WHERE attempt_id = ?")
|
|
664
|
-
.get(step.attemptId);
|
|
665
|
-
if (!isAttemptOutputRow(attempt)) {
|
|
666
|
-
throw new Error(`Workflow node attempt is missing: ${step.attemptId}`);
|
|
667
|
-
}
|
|
1449
|
+
const attemptOutput = this.readAttemptOutput(step.attemptId);
|
|
668
1450
|
const carried = stepIndex < (state.carriedStepCount ?? 0);
|
|
669
|
-
const outputOverrideHash = carried &&
|
|
670
|
-
(attempt.outputHash === null ||
|
|
671
|
-
canonicalJson(this.state.readJson(attempt.outputHash)) !== canonicalJson(step.output))
|
|
1451
|
+
const outputOverrideHash = carried && canonicalJson(attemptOutput) !== canonicalJson(step.output)
|
|
672
1452
|
? this.state.putJson(step.output, now)
|
|
673
1453
|
: null;
|
|
674
1454
|
this.state.connection
|
|
@@ -677,6 +1457,78 @@ export class WorkflowRunStore {
|
|
|
677
1457
|
.run(state.runId, stepIndex, step.attemptId, outputOverrideHash);
|
|
678
1458
|
}
|
|
679
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);
|
|
1531
|
+
}
|
|
680
1532
|
ensureAttempt(state, nodeId, attemptId, now, snapshot) {
|
|
681
1533
|
const existing = this.state.connection
|
|
682
1534
|
.prepare("SELECT attempt_id AS attemptId FROM node_attempts WHERE attempt_id = ?")
|
|
@@ -684,18 +1536,31 @@ export class WorkflowRunStore {
|
|
|
684
1536
|
const status = state.status === "waiting" ? "waiting" : "running";
|
|
685
1537
|
if (existing !== undefined) {
|
|
686
1538
|
this.state.connection
|
|
687
|
-
.prepare(
|
|
688
|
-
|
|
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);
|
|
689
1546
|
return;
|
|
690
1547
|
}
|
|
691
1548
|
const definition = snapshot ?? this.readDefinition(this.requireRunRow(state.runId).definitionHash);
|
|
692
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);
|
|
693
1555
|
this.state.connection
|
|
694
1556
|
.prepare(`INSERT INTO node_attempts(
|
|
695
1557
|
attempt_id, run_id, node_id, attempt_number, node_type, status,
|
|
1558
|
+
settings_scope_id, settings_change_number, settings_hash,
|
|
696
1559
|
started_at, created_at, updated_at
|
|
697
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
698
|
-
.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);
|
|
699
1564
|
}
|
|
700
1565
|
nextAttemptNumber(runId, nodeId) {
|
|
701
1566
|
const row = this.state.connection
|
|
@@ -717,19 +1582,45 @@ export class WorkflowRunStore {
|
|
|
717
1582
|
throw new Error("Could not allocate workflow update sequence");
|
|
718
1583
|
return row.updateSeq;
|
|
719
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
|
+
}
|
|
720
1616
|
readRunRow(runId) {
|
|
721
1617
|
const row = this.state.connection
|
|
722
1618
|
.prepare(`SELECT r.run_id AS runId, r.resource_id AS resourceId,
|
|
723
1619
|
d.definition_hash AS definitionHash, r.definition_digest AS definitionDigest,
|
|
724
|
-
d.workflow_name AS workflowRef, r.
|
|
725
|
-
r.workflow_source_hash AS workflowSourceHash,
|
|
726
|
-
r.workflow_sources_hash AS workflowSourcesHash, r.parent_run_id AS parentRunId,
|
|
1620
|
+
d.workflow_name AS workflowRef, r.parent_run_id AS parentRunId,
|
|
727
1621
|
r.title, r.status, r.paused, r.status_detail AS statusDetail,
|
|
728
|
-
r.input_hash AS inputHash, r.
|
|
729
|
-
r.
|
|
730
|
-
r.carried_step_count AS carriedStepCount, r.current_node AS currentNode,
|
|
731
|
-
r.current_attempt_id AS currentAttemptId,
|
|
732
|
-
r.current_node_started_at AS currentNodeStartedAt, r.waiting_on AS waitingOn,
|
|
1622
|
+
r.input_hash AS inputHash, r.final_output_hash AS finalOutputHash,
|
|
1623
|
+
r.error_hash AS errorHash,
|
|
733
1624
|
r.created_at AS createdAt, r.updated_at AS updatedAt, r.finished_at AS finishedAt
|
|
734
1625
|
FROM runs r
|
|
735
1626
|
JOIN workflow_definitions d ON d.definition_digest = r.definition_digest
|
|
@@ -745,6 +1636,10 @@ export class WorkflowRunStore {
|
|
|
745
1636
|
}
|
|
746
1637
|
readRunState(row, snapshot) {
|
|
747
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);
|
|
748
1643
|
const outputs = {};
|
|
749
1644
|
const results = {};
|
|
750
1645
|
for (const step of steps) {
|
|
@@ -770,15 +1665,11 @@ export class WorkflowRunStore {
|
|
|
770
1665
|
runId: row.runId,
|
|
771
1666
|
workflowName: row.workflowRef,
|
|
772
1667
|
...(row.parentRunId === null ? {} : { parentRunId: row.parentRunId }),
|
|
773
|
-
...(
|
|
1668
|
+
...(carriedStepCount === 0 ? {} : { carriedStepCount }),
|
|
774
1669
|
...(row.title === null ? {} : { runTitle: row.title }),
|
|
775
|
-
...(
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
...(row.workflowSourcesHash === null
|
|
779
|
-
? {}
|
|
780
|
-
: { workflowSources: this.readJsonAs(row.workflowSourcesHash) }),
|
|
781
|
-
...(row.workflowSourcesHash !== null || snapshot.composition?.mounts.length
|
|
1670
|
+
...(sources.root === undefined ? {} : { workflowSource: sources.root }),
|
|
1671
|
+
...(sources.mounted.length === 0 ? {} : { workflowSources: sources.mounted }),
|
|
1672
|
+
...(sources.mounted.length !== 0 || snapshot.composition?.mounts.length
|
|
782
1673
|
? { definitionDigest: `sha256:${row.definitionDigest.toString("hex")}` }
|
|
783
1674
|
: {}),
|
|
784
1675
|
startedAt: new Date(row.createdAt).toISOString(),
|
|
@@ -790,17 +1681,18 @@ export class WorkflowRunStore {
|
|
|
790
1681
|
results,
|
|
791
1682
|
steps,
|
|
792
1683
|
...this.readUpdates(row.runId),
|
|
793
|
-
...(
|
|
794
|
-
...(
|
|
795
|
-
...(
|
|
1684
|
+
...(activeAttempt === undefined ? {} : { currentNode: activeAttempt.nodeId }),
|
|
1685
|
+
...(activeAttempt === undefined ? {} : { currentAttemptId: activeAttempt.attemptId }),
|
|
1686
|
+
...(activeAttempt?.startedAt === null || activeAttempt === undefined
|
|
796
1687
|
? {}
|
|
797
|
-
: { currentNodeStartedAt: new Date(
|
|
1688
|
+
: { currentNodeStartedAt: new Date(activeAttempt.startedAt).toISOString() }),
|
|
1689
|
+
...savedCurrentSettingsBinding(activeAttempt?.settingsScopeId ?? null, activeAttempt?.settingsChangeNumber ?? null, activeAttempt?.settingsHash ?? null),
|
|
798
1690
|
...(row.statusDetail === null ? {} : { statusDetail: row.statusDetail }),
|
|
799
|
-
...(
|
|
800
|
-
? {}
|
|
801
|
-
: { humanDecision: this.readJsonAs(row.humanDecisionHash) }),
|
|
1691
|
+
...(humanDecision === undefined ? {} : { humanDecision }),
|
|
802
1692
|
...(row.paused === 0 ? {} : { paused: true }),
|
|
803
|
-
...(row.
|
|
1693
|
+
...(row.status === "waiting" && steps.at(-1) !== undefined
|
|
1694
|
+
? { waitingOn: steps.at(-1)?.nodeId }
|
|
1695
|
+
: {}),
|
|
804
1696
|
...(row.finalOutputHash === null
|
|
805
1697
|
? {}
|
|
806
1698
|
: { finalOutput: this.state.readJson(row.finalOutputHash) }),
|
|
@@ -810,25 +1702,55 @@ export class WorkflowRunStore {
|
|
|
810
1702
|
readSteps(runId) {
|
|
811
1703
|
const rows = this.state.connection
|
|
812
1704
|
.prepare(`SELECT s.step_index AS stepIndex, a.attempt_id AS attemptId, a.node_id AS nodeId,
|
|
813
|
-
a.node_type AS nodeType, a.status,
|
|
814
|
-
a.
|
|
815
|
-
|
|
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,
|
|
816
1715
|
a.started_at AS startedAt, a.finished_at AS finishedAt
|
|
817
1716
|
FROM run_steps s
|
|
818
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'
|
|
819
1732
|
WHERE s.run_id = ? ORDER BY s.step_index`)
|
|
820
1733
|
.all(runId)
|
|
821
1734
|
.filter(isStepRow);
|
|
822
1735
|
return rows.map((row, index) => {
|
|
823
1736
|
if (row.stepIndex !== index)
|
|
824
1737
|
throw new Error(`Workflow run step sequence has a gap: ${runId}`);
|
|
825
|
-
const
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
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);
|
|
829
1744
|
const outputHash = row.outputOverrideHash ?? row.outputHash;
|
|
830
|
-
const output = 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);
|
|
831
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 };
|
|
832
1754
|
return {
|
|
833
1755
|
attemptId: row.attemptId,
|
|
834
1756
|
nodeId: row.nodeId,
|
|
@@ -839,10 +1761,94 @@ export class WorkflowRunStore {
|
|
|
839
1761
|
prompt,
|
|
840
1762
|
output,
|
|
841
1763
|
...(error === undefined ? {} : { error }),
|
|
842
|
-
...
|
|
1764
|
+
...savedStepSettingsBinding(row.settingsScopeId, row.settingsChangeNumber, row.settingsHash),
|
|
1765
|
+
...receipt,
|
|
1766
|
+
...(conversation === undefined ? {} : { conversation }),
|
|
843
1767
|
};
|
|
844
1768
|
});
|
|
845
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
|
+
}
|
|
846
1852
|
readUpdates(runId) {
|
|
847
1853
|
const rows = this.state.connection
|
|
848
1854
|
.prepare(`SELECT u.update_id AS updateId, u.run_revision AS runRevision,
|
|
@@ -1088,8 +2094,15 @@ function insertRunEvent(state, resourceId, revision, at, event) {
|
|
|
1088
2094
|
insertGenericEvent(state, resourceId, revision, event.type, "system", null, payloadHash, Date.parse(at));
|
|
1089
2095
|
}
|
|
1090
2096
|
function compactTracePayload(eventType, payload) {
|
|
1091
|
-
if (
|
|
1092
|
-
|
|
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")) {
|
|
1093
2106
|
const { output: _output, ...rest } = payload;
|
|
1094
2107
|
return { ...rest, outputStored: true };
|
|
1095
2108
|
}
|
|
@@ -1123,6 +2136,16 @@ function sourceForState(state, definitionDigest) {
|
|
|
1123
2136
|
revision: definitionDigest.toString("hex"),
|
|
1124
2137
|
};
|
|
1125
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
|
+
}
|
|
1126
2149
|
function segmentIdFor(runId, attemptId) {
|
|
1127
2150
|
return `segment-${createHash("sha256")
|
|
1128
2151
|
.update(`${runId}\0${attemptId ?? ""}`)
|
|
@@ -1158,13 +2181,46 @@ function outcomeForStatus(status) {
|
|
|
1158
2181
|
throw new Error(`Workflow step has nonterminal status: ${status}`);
|
|
1159
2182
|
}
|
|
1160
2183
|
}
|
|
1161
|
-
function
|
|
2184
|
+
function attemptReceipt(step) {
|
|
1162
2185
|
return {
|
|
1163
2186
|
...(step.action === undefined ? {} : { action: step.action }),
|
|
1164
2187
|
...(step.assistantMessage === undefined ? {} : { assistantMessage: step.assistantMessage }),
|
|
1165
|
-
...(step.conversation === undefined ? {} : { conversation: step.conversation }),
|
|
1166
2188
|
};
|
|
1167
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
|
+
}
|
|
1168
2224
|
function resultForStep(step) {
|
|
1169
2225
|
return {
|
|
1170
2226
|
attemptId: step.attemptId,
|
|
@@ -1229,6 +2285,143 @@ function validateSessionCapture(capture) {
|
|
|
1229
2285
|
throw new Error("Only failed session capture may contain failure details");
|
|
1230
2286
|
}
|
|
1231
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
|
+
}
|
|
1232
2425
|
function assertValidRunId(runId) {
|
|
1233
2426
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(runId)) {
|
|
1234
2427
|
throw new Error(`Invalid workflow run id: ${JSON.stringify(runId)}`);
|
|
@@ -1240,18 +2433,60 @@ function isRecord(value) {
|
|
|
1240
2433
|
function isRunRow(value) {
|
|
1241
2434
|
return isRecord(value);
|
|
1242
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
|
+
}
|
|
1243
2460
|
function isRunIdRow(value) {
|
|
1244
2461
|
return isRecord(value);
|
|
1245
2462
|
}
|
|
1246
2463
|
function isRunStepIdentityRow(value) {
|
|
1247
2464
|
return isRecord(value);
|
|
1248
2465
|
}
|
|
1249
|
-
function isAttemptOutputRow(value) {
|
|
1250
|
-
return isRecord(value) && (value.outputHash === null || Buffer.isBuffer(value.outputHash));
|
|
1251
|
-
}
|
|
1252
2466
|
function isStepRow(value) {
|
|
1253
2467
|
return isRecord(value);
|
|
1254
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
|
+
}
|
|
1255
2490
|
function isUpdateRow(value) {
|
|
1256
2491
|
return isRecord(value);
|
|
1257
2492
|
}
|
|
@@ -1280,7 +2515,15 @@ function isLeaseAuthorityRow(value) {
|
|
|
1280
2515
|
return isRecord(value);
|
|
1281
2516
|
}
|
|
1282
2517
|
export function createDefinitionSnapshot(workflow) {
|
|
1283
|
-
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
|
+
}));
|
|
1284
2527
|
return {
|
|
1285
2528
|
schema: DEFINITION_SNAPSHOT_SCHEMA,
|
|
1286
2529
|
name: workflow.name,
|
|
@@ -1292,6 +2535,8 @@ export function createDefinitionSnapshot(workflow) {
|
|
|
1292
2535
|
])),
|
|
1293
2536
|
edges: structuredClone(workflow.edges),
|
|
1294
2537
|
...(composition !== undefined ? { composition: structuredClone(composition) } : {}),
|
|
2538
|
+
...(workflow.settings !== undefined ? { settings: snapshotSettings(workflow.settings) } : {}),
|
|
2539
|
+
...(settingsScopes.length > 0 ? { settingsScopes } : {}),
|
|
1295
2540
|
};
|
|
1296
2541
|
}
|
|
1297
2542
|
function snapshotNode(workflow, nodeId, node) {
|
|
@@ -1326,6 +2571,9 @@ function snapshotNode(workflow, nodeId, node) {
|
|
|
1326
2571
|
if (node.nodeType === "agent" && node.expectedOutput !== undefined) {
|
|
1327
2572
|
common.expectedOutput = node.expectedOutput;
|
|
1328
2573
|
}
|
|
2574
|
+
if (node.nodeType === "compute" && node.settingsRoute === true) {
|
|
2575
|
+
common.settingsRoute = true;
|
|
2576
|
+
}
|
|
1329
2577
|
if (node.nodeType === "notify")
|
|
1330
2578
|
common.summary = node.kind ?? "progress";
|
|
1331
2579
|
if (node.nodeType === "checkpoint" && node.summary !== undefined)
|
|
@@ -1346,4 +2594,16 @@ function snapshotNode(workflow, nodeId, node) {
|
|
|
1346
2594
|
common.actionExecution = "exec" in node ? "shell" : "function";
|
|
1347
2595
|
return common;
|
|
1348
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
|
+
}
|
|
1349
2609
|
//# sourceMappingURL=store.js.map
|