@osolmaz/pi-workflows 0.11.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -6
- package/dist/builtins/autoimplement-command-batches.d.ts +52 -0
- package/dist/builtins/autoimplement-command-batches.js +258 -0
- package/dist/builtins/autoimplement-command-batches.js.map +1 -0
- package/dist/builtins/autoimplement.workflow.d.ts +29 -3
- package/dist/builtins/autoimplement.workflow.js +876 -231
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/builtins/catalog.js +3 -1
- package/dist/builtins/catalog.js.map +1 -1
- package/dist/builtins/index.d.ts +2 -0
- package/dist/builtins/index.js +1 -0
- package/dist/builtins/index.js.map +1 -1
- package/dist/builtins/sanity-check-session.d.ts +17 -0
- package/dist/builtins/sanity-check-session.js +168 -0
- package/dist/builtins/sanity-check-session.js.map +1 -0
- package/dist/builtins/sanity-check.workflow.d.ts +83 -0
- package/dist/builtins/sanity-check.workflow.js +398 -0
- package/dist/builtins/sanity-check.workflow.js.map +1 -0
- package/dist/controllers/sqlite.d.ts +141 -5
- package/dist/controllers/sqlite.js +568 -46
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/decision-channels.js +7 -7
- package/dist/extension/deferred-turn-coordinator.d.ts +32 -0
- package/dist/extension/deferred-turn-coordinator.js +143 -0
- package/dist/extension/deferred-turn-coordinator.js.map +1 -0
- package/dist/extension/deferred-turn.d.ts +44 -0
- package/dist/extension/deferred-turn.js +110 -0
- package/dist/extension/deferred-turn.js.map +1 -0
- package/dist/extension/index.js +515 -84
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/step-message.d.ts +1 -0
- package/dist/extension/step-message.js.map +1 -1
- package/dist/herdr/setup.js +4 -4
- package/dist/host/runner.js +3 -0
- package/dist/host/runner.js.map +1 -1
- package/dist/workflows/command-batch.d.ts +38 -0
- package/dist/workflows/command-batch.js +176 -0
- package/dist/workflows/command-batch.js.map +1 -0
- package/dist/workflows/engine.js +8 -4
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/index.d.ts +1 -0
- package/dist/workflows/index.js +1 -0
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/migrate-sources.d.ts +1 -1
- package/dist/workflows/migrate-sources.js.map +1 -1
- package/dist/workflows/schema.js +2 -1
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/shell.d.ts +4 -0
- package/dist/workflows/shell.js +6 -0
- package/dist/workflows/shell.js.map +1 -1
- package/dist/workflows/store.js +3 -1
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/tool-input.d.ts +1 -0
- package/dist/workflows/tool-input.js +2 -2
- package/dist/workflows/tool-input.js.map +1 -1
- package/dist/workflows/types.d.ts +4 -3
- package/docs/2026-08-18-herdr-piw-plan.md +1 -1
- package/docs/2026-08-20-durable-workflow-launch-plan.md +445 -0
- package/docs/CONTROLLERS.md +2 -2
- package/docs/DEFERRED_TURNS.md +298 -0
- package/docs/DESIGN_PHILOSOPHY.md +1 -1
- package/docs/HUMAN_DECISIONS.md +7 -7
- package/docs/HUMAN_DECISION_PRESENTATIONS.md +3 -3
- package/docs/MONITOR.md +2 -2
- package/docs/WORKFLOW_COMPOSITION.md +2 -2
- package/docs/WORKFLOW_STEP_MESSAGES.md +5 -5
- package/docs/WORKFLOW_UPDATES.md +28 -5
- package/docs/plans/2026-08-04-controller-runtime-plan.md +1 -1
- package/docs/plans/2026-08-05-always-on-workflows-plan.md +1 -1
- package/docs/plans/2026-08-10-agent-managed-monitor-workflows-plan.md +9 -9
- package/docs/plans/2026-08-13-built-in-workflow-catalog-plan.md +1 -1
- package/docs/plans/2026-08-13-session-addressed-workflow-notifications-plan.md +1 -1
- package/docs/plans/2026-08-16-workflow-updates-plan.md +3 -3
- package/docs/plans/2026-08-17-bundled-skills-plan.md +6 -6
- package/docs/plans/2026-08-19-human-decision-gates-plan.md +5 -5
- package/docs/plans/2026-08-19-human-decision-presentations-plan.md +1 -1
- package/docs/plans/2026-08-19-provider-compatible-workflow-tool-schema-plan.md +1 -1
- package/docs/plans/2026-08-19-workflow-composition-plan.md +6 -6
- package/docs/plans/2026-08-20-autoimplement-blocker-challenge-plan.md +3 -3
- package/docs/plans/2026-08-20-bounded-command-batches-plan.md +206 -0
- package/docs/plans/2026-08-20-herdr-plugin-sync-plan.md +9 -9
- package/docs/plans/2026-08-21-autoimplement-timeout-fallback-plan.md +242 -0
- package/docs/plans/2026-08-21-deferred-turn-intents-plan.md +324 -0
- package/docs/plans/2026-08-21-sanity-check-plan.md +175 -0
- package/docs/run-bundles.md +3 -1
- package/docs/workflows.md +67 -17
- package/examples/workflows/command-batch.workflow.ts +50 -0
- package/examples/workflows/sanity-check.workflow.ts +1 -0
- package/herdr-plugin.toml +3 -3
- package/package.json +1 -1
- package/schemas/decision-presentation-v1.schema.json +1 -1
- package/schemas/human-decision-accepted-v1.schema.json +1 -1
- package/schemas/human-decision-accepted-v2.schema.json +1 -1
- package/schemas/human-decision-answer-attempt-v1.schema.json +1 -1
- package/schemas/human-decision-cancellation-v1.schema.json +1 -1
- package/schemas/human-decision-continuation-v1.schema.json +1 -1
- package/schemas/human-decision-delivery-v1.schema.json +1 -1
- package/schemas/human-decision-delivery-v2.schema.json +1 -1
- package/schemas/human-decision-receipt-v1.schema.json +1 -1
- package/schemas/human-decision-receipt-v2.schema.json +1 -1
- package/schemas/human-decision-request-v1.schema.json +1 -1
- package/schemas/human-decision-request-v2.schema.json +1 -1
- package/schemas/human-decision-resolution-v1.schema.json +1 -1
- package/schemas/human-decision-resolution-v2.schema.json +1 -1
- package/schemas/human-decision-settlement-v1.schema.json +1 -1
- package/skills/autodoc/SKILL.md +33 -2
- package/skills/autoimplement/SKILL.md +66 -12
- package/skills/autoplan/SKILL.md +26 -2
- package/skills/monitor/SKILL.md +35 -21
- package/skills/pi-workflows/SKILL.md +19 -5
- package/src/builtins/autoimplement-command-batches.ts +364 -0
- package/src/builtins/autoimplement.workflow.ts +1065 -261
- package/src/builtins/catalog.ts +3 -1
- package/src/builtins/index.ts +13 -0
- package/src/builtins/sanity-check-session.ts +205 -0
- package/src/builtins/sanity-check.workflow.ts +624 -0
- package/src/controllers/sqlite.ts +878 -51
- package/src/extension/decision-channels.ts +7 -7
- package/src/extension/deferred-turn-coordinator.ts +171 -0
- package/src/extension/deferred-turn.ts +166 -0
- package/src/extension/index.ts +646 -91
- package/src/extension/step-message.ts +1 -0
- package/src/herdr/setup.ts +4 -4
- package/src/host/runner.ts +3 -0
- package/src/workflows/command-batch.ts +254 -0
- package/src/workflows/engine.ts +8 -5
- package/src/workflows/index.ts +15 -0
- package/src/workflows/migrate-sources.ts +5 -1
- package/src/workflows/schema.ts +2 -1
- package/src/workflows/shell.ts +10 -0
- package/src/workflows/store.ts +3 -1
- package/src/workflows/tool-input.ts +5 -2
- package/src/workflows/types.ts +4 -3
|
@@ -34,6 +34,7 @@ const MAX_KEY_LENGTH = 512;
|
|
|
34
34
|
const MAX_ERROR_LENGTH = 8_192;
|
|
35
35
|
const MAX_EVENT_BYTES = 64 * 1024;
|
|
36
36
|
const MAX_RESOURCE_VALUE_BYTES = 1024 * 1024;
|
|
37
|
+
const TURN_INTENT_FACTS_SCHEMA = "pi-workflows.deferred-turn-facts.v1";
|
|
37
38
|
|
|
38
39
|
type ResourceRow = {
|
|
39
40
|
controller: string;
|
|
@@ -83,20 +84,36 @@ type WorkflowRow = {
|
|
|
83
84
|
error: string | null;
|
|
84
85
|
};
|
|
85
86
|
|
|
87
|
+
export type WorkflowRunLaunchStatus =
|
|
88
|
+
| "queued"
|
|
89
|
+
| "starting"
|
|
90
|
+
| "running"
|
|
91
|
+
| "parked"
|
|
92
|
+
| "done"
|
|
93
|
+
| "failed"
|
|
94
|
+
| "cancelled";
|
|
95
|
+
|
|
86
96
|
type WorkflowRunQueueRow = {
|
|
87
97
|
run_id: string;
|
|
88
98
|
workflow_ref: string;
|
|
89
99
|
workflow_path: string;
|
|
100
|
+
workflow_source_json: string;
|
|
101
|
+
definition_digest: string;
|
|
90
102
|
input_json: string;
|
|
91
|
-
|
|
103
|
+
launch_options_json: string;
|
|
104
|
+
status: WorkflowRunLaunchStatus;
|
|
92
105
|
runner_id: string | null;
|
|
93
106
|
claim_token: string | null;
|
|
94
107
|
claim_expires_at: number | null;
|
|
95
108
|
affinity_runner_id: string | null;
|
|
96
109
|
origin_session_id: string | null;
|
|
97
110
|
parent_run_id: string | null;
|
|
111
|
+
error_code: string | null;
|
|
112
|
+
error_message: string | null;
|
|
98
113
|
created_at: string;
|
|
99
114
|
updated_at: string;
|
|
115
|
+
started_at: string | null;
|
|
116
|
+
finished_at: string | null;
|
|
100
117
|
};
|
|
101
118
|
|
|
102
119
|
/** A user-started workflow run tracked by the durable run queue. */
|
|
@@ -106,8 +123,11 @@ export type WorkflowRunQueueRecord = {
|
|
|
106
123
|
workflowName: string;
|
|
107
124
|
/** Canonical source reference used to reopen the run. */
|
|
108
125
|
workflowSourceRef: string;
|
|
126
|
+
workflowSource: unknown;
|
|
127
|
+
definitionDigest: string;
|
|
109
128
|
input: unknown;
|
|
110
|
-
|
|
129
|
+
launchOptions: unknown;
|
|
130
|
+
status: WorkflowRunLaunchStatus;
|
|
111
131
|
runnerId: string | null;
|
|
112
132
|
claimToken: string | null;
|
|
113
133
|
claimExpiresAt: string | null;
|
|
@@ -115,8 +135,12 @@ export type WorkflowRunQueueRecord = {
|
|
|
115
135
|
/** Pi session that owns delivery and interactive execution, or null for detached runs. */
|
|
116
136
|
originSessionId: string | null;
|
|
117
137
|
parentRunId: string | null;
|
|
138
|
+
errorCode: string | null;
|
|
139
|
+
errorMessage: string | null;
|
|
118
140
|
createdAt: string;
|
|
119
141
|
updatedAt: string;
|
|
142
|
+
startedAt: string | null;
|
|
143
|
+
finishedAt: string | null;
|
|
120
144
|
};
|
|
121
145
|
|
|
122
146
|
type WorkflowNotificationRow = {
|
|
@@ -126,7 +150,7 @@ type WorkflowNotificationRow = {
|
|
|
126
150
|
attempt_id: string;
|
|
127
151
|
notification_index: number;
|
|
128
152
|
target_session_id: string;
|
|
129
|
-
kind:
|
|
153
|
+
kind: string;
|
|
130
154
|
content: string;
|
|
131
155
|
created_at: string;
|
|
132
156
|
delivery_claim_token: string | null;
|
|
@@ -148,6 +172,65 @@ export type WorkflowNotificationRecord = {
|
|
|
148
172
|
deliveredAt: string | null;
|
|
149
173
|
};
|
|
150
174
|
|
|
175
|
+
export type WorkflowTurnIntentCause =
|
|
176
|
+
| "agentCancelled"
|
|
177
|
+
| "timedOut"
|
|
178
|
+
| "failed"
|
|
179
|
+
| "launchFailed"
|
|
180
|
+
| "controllerInterrupted"
|
|
181
|
+
| "claimLost";
|
|
182
|
+
|
|
183
|
+
export type WorkflowTurnIntentResolution = "workflowPrompt" | "presentation" | "fallback";
|
|
184
|
+
|
|
185
|
+
export type WorkflowTurnIntentFacts = JsonObject & {
|
|
186
|
+
schema: typeof TURN_INTENT_FACTS_SCHEMA;
|
|
187
|
+
workflowName: string;
|
|
188
|
+
runId: string;
|
|
189
|
+
observedState: string;
|
|
190
|
+
cause: WorkflowTurnIntentCause;
|
|
191
|
+
nodeId: string | null;
|
|
192
|
+
attemptId: string | null;
|
|
193
|
+
reason: string | null;
|
|
194
|
+
handoff: boolean;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
type WorkflowTurnIntentRow = {
|
|
198
|
+
intent_id: string;
|
|
199
|
+
source_event_id: string;
|
|
200
|
+
run_id: string;
|
|
201
|
+
workflow_ref: string;
|
|
202
|
+
target_session_id: string;
|
|
203
|
+
cause: string;
|
|
204
|
+
node_id: string | null;
|
|
205
|
+
attempt_id: string | null;
|
|
206
|
+
fallback_facts_json: string;
|
|
207
|
+
requested_at: string;
|
|
208
|
+
eligible_at: string | null;
|
|
209
|
+
resolved_at: string | null;
|
|
210
|
+
resolution: string | null;
|
|
211
|
+
resolution_message_id: string | null;
|
|
212
|
+
delivery_claim_token: string | null;
|
|
213
|
+
delivery_claim_expires_at: number | null;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
export type WorkflowTurnIntentRecord = {
|
|
217
|
+
intentId: string;
|
|
218
|
+
sourceEventId: string;
|
|
219
|
+
runId: string;
|
|
220
|
+
workflowRef: string;
|
|
221
|
+
targetSessionId: string;
|
|
222
|
+
cause: WorkflowTurnIntentCause;
|
|
223
|
+
nodeId: string | null;
|
|
224
|
+
attemptId: string | null;
|
|
225
|
+
fallbackFacts: WorkflowTurnIntentFacts;
|
|
226
|
+
requestedAt: string;
|
|
227
|
+
eligibleAt: string | null;
|
|
228
|
+
resolvedAt: string | null;
|
|
229
|
+
resolution: WorkflowTurnIntentResolution | null;
|
|
230
|
+
resolutionMessageId: string | null;
|
|
231
|
+
deliveryClaimExpiresAt: string | null;
|
|
232
|
+
};
|
|
233
|
+
|
|
151
234
|
type RunEventRow = {
|
|
152
235
|
seq: number;
|
|
153
236
|
recorded_at: string;
|
|
@@ -799,21 +882,119 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
799
882
|
if (row === undefined) {
|
|
800
883
|
throw new Error("Controller store has no schema identifier");
|
|
801
884
|
}
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
.
|
|
808
|
-
|
|
809
|
-
const queueColumns = this.database.pragma("table_info(workflow_run_queue)") as {
|
|
810
|
-
name: string;
|
|
811
|
-
}[];
|
|
812
|
-
if (!queueColumns.some((column) => column.name === "origin_session_id")) {
|
|
813
|
-
this.database.exec("ALTER TABLE workflow_run_queue ADD COLUMN origin_session_id TEXT");
|
|
885
|
+
} else {
|
|
886
|
+
const existingQueue = this.database
|
|
887
|
+
.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'workflow_run_queue'")
|
|
888
|
+
.get();
|
|
889
|
+
if (existingQueue !== undefined) {
|
|
890
|
+
this.assertAlphaSchemaLayout(false);
|
|
891
|
+
this.assertNoLegacyPendingLaunchNotifications();
|
|
814
892
|
}
|
|
815
|
-
this.
|
|
816
|
-
|
|
893
|
+
this.transaction(() => {
|
|
894
|
+
this.database
|
|
895
|
+
.prepare("INSERT OR IGNORE INTO schema_info (singleton, schema_id) VALUES (1, ?)")
|
|
896
|
+
.run(CONTROLLER_STORE_SCHEMA);
|
|
897
|
+
this.database.exec(SCHEMA_SQL);
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
this.assertAlphaSchemaLayout(true);
|
|
901
|
+
this.assertNoLegacyPendingLaunchNotifications();
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
private assertAlphaSchemaLayout(requireTurnIntents: boolean): void {
|
|
905
|
+
const requiredQueueColumns = new Set([
|
|
906
|
+
"run_id",
|
|
907
|
+
"workflow_ref",
|
|
908
|
+
"workflow_path",
|
|
909
|
+
"workflow_source_json",
|
|
910
|
+
"definition_digest",
|
|
911
|
+
"input_json",
|
|
912
|
+
"launch_options_json",
|
|
913
|
+
"status",
|
|
914
|
+
"runner_id",
|
|
915
|
+
"claim_token",
|
|
916
|
+
"claim_expires_at",
|
|
917
|
+
"affinity_runner_id",
|
|
918
|
+
"origin_session_id",
|
|
919
|
+
"parent_run_id",
|
|
920
|
+
"error_code",
|
|
921
|
+
"error_message",
|
|
922
|
+
"created_at",
|
|
923
|
+
"updated_at",
|
|
924
|
+
"started_at",
|
|
925
|
+
"finished_at",
|
|
926
|
+
]);
|
|
927
|
+
const actual = new Set(
|
|
928
|
+
(
|
|
929
|
+
this.database.pragma("table_info(workflow_run_queue)") as {
|
|
930
|
+
name: string;
|
|
931
|
+
}[]
|
|
932
|
+
).map((column) => column.name),
|
|
933
|
+
);
|
|
934
|
+
const missing = [...requiredQueueColumns].filter((column) => !actual.has(column));
|
|
935
|
+
if (missing.length > 0) {
|
|
936
|
+
throw new Error(
|
|
937
|
+
"Controller store uses an incompatible alpha layout. Preserve needed run bundles, then reset the project controller store.",
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
if (!requireTurnIntents) return;
|
|
941
|
+
const requiredIntentColumns = new Set([
|
|
942
|
+
"intent_id",
|
|
943
|
+
"source_event_id",
|
|
944
|
+
"run_id",
|
|
945
|
+
"workflow_ref",
|
|
946
|
+
"target_session_id",
|
|
947
|
+
"cause",
|
|
948
|
+
"node_id",
|
|
949
|
+
"attempt_id",
|
|
950
|
+
"fallback_facts_json",
|
|
951
|
+
"requested_at",
|
|
952
|
+
"eligible_at",
|
|
953
|
+
"resolved_at",
|
|
954
|
+
"resolution",
|
|
955
|
+
"resolution_message_id",
|
|
956
|
+
"delivery_claim_token",
|
|
957
|
+
"delivery_claim_expires_at",
|
|
958
|
+
]);
|
|
959
|
+
const intentColumns = new Set(
|
|
960
|
+
(
|
|
961
|
+
this.database.pragma("table_info(workflow_turn_intents)") as {
|
|
962
|
+
name: string;
|
|
963
|
+
}[]
|
|
964
|
+
).map((column) => column.name),
|
|
965
|
+
);
|
|
966
|
+
const missingIntentColumns = [...requiredIntentColumns].filter(
|
|
967
|
+
(column) => !intentColumns.has(column),
|
|
968
|
+
);
|
|
969
|
+
if (missingIntentColumns.length > 0) {
|
|
970
|
+
throw new Error(
|
|
971
|
+
"Controller store uses an incompatible alpha turn-intent layout. Preserve needed run bundles, then reset the project controller store.",
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
private assertNoLegacyPendingLaunchNotifications(): void {
|
|
977
|
+
const notificationTable = this.database
|
|
978
|
+
.prepare(
|
|
979
|
+
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'workflow_notifications'",
|
|
980
|
+
)
|
|
981
|
+
.get();
|
|
982
|
+
if (notificationTable === undefined) {
|
|
983
|
+
throw new Error(
|
|
984
|
+
"Controller store uses an incompatible alpha notification layout. Preserve needed run bundles, then reset the project controller store.",
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
const legacy = this.database
|
|
988
|
+
.prepare(
|
|
989
|
+
`SELECT 1 FROM workflow_notifications
|
|
990
|
+
WHERE kind = 'launch_failure' AND delivered_at IS NULL LIMIT 1`,
|
|
991
|
+
)
|
|
992
|
+
.get();
|
|
993
|
+
if (legacy !== undefined) {
|
|
994
|
+
throw new Error(
|
|
995
|
+
"Controller store contains pending alpha launch-failure notifications. Preserve needed run bundles, then reset the project controller store.",
|
|
996
|
+
);
|
|
997
|
+
}
|
|
817
998
|
}
|
|
818
999
|
|
|
819
1000
|
private transaction<T>(task: () => T): T {
|
|
@@ -901,15 +1082,68 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
901
1082
|
return workflow;
|
|
902
1083
|
}
|
|
903
1084
|
|
|
904
|
-
/**
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
1085
|
+
/** Reserve a user-started run before the initiating agent turn settles. */
|
|
1086
|
+
reserveWorkflowRun(options: {
|
|
1087
|
+
runId: string;
|
|
1088
|
+
workflowName: string;
|
|
1089
|
+
workflowSourceRef: string;
|
|
1090
|
+
workflowSource: unknown;
|
|
1091
|
+
definitionDigest: string;
|
|
1092
|
+
input: unknown;
|
|
1093
|
+
launchOptions?: unknown;
|
|
1094
|
+
runnerId: string;
|
|
1095
|
+
originSessionId: string;
|
|
1096
|
+
parentRunId?: string;
|
|
1097
|
+
now?: string;
|
|
1098
|
+
}): WorkflowRunQueueRecord {
|
|
1099
|
+
validateRunId(options.runId);
|
|
1100
|
+
validateKey(options.workflowName, "workflow name");
|
|
1101
|
+
validateKey(options.workflowSourceRef, "workflow source ref");
|
|
1102
|
+
validateKey(options.definitionDigest, "workflow definition digest");
|
|
1103
|
+
validateKey(options.runnerId, "runner id");
|
|
1104
|
+
validateKey(options.originSessionId, "origin session id");
|
|
1105
|
+
const inputJson = canonicalJson(options.input ?? null, "workflow run input");
|
|
1106
|
+
const sourceJson = canonicalJson(options.workflowSource, "workflow run source");
|
|
1107
|
+
const launchJson = canonicalJson(options.launchOptions ?? {}, "workflow launch options");
|
|
1108
|
+
validateJsonSize(inputJson, "Workflow run input", MAX_RESOURCE_VALUE_BYTES);
|
|
1109
|
+
validateJsonSize(sourceJson, "Workflow run source", MAX_EVENT_BYTES);
|
|
1110
|
+
validateJsonSize(launchJson, "Workflow launch options", MAX_EVENT_BYTES);
|
|
1111
|
+
const now = validTimestamp(options.now);
|
|
1112
|
+
this.database
|
|
1113
|
+
.prepare(
|
|
1114
|
+
`INSERT INTO workflow_run_queue (
|
|
1115
|
+
run_id, workflow_ref, workflow_path, workflow_source_json, definition_digest,
|
|
1116
|
+
input_json, launch_options_json, status, runner_id, claim_token, claim_expires_at,
|
|
1117
|
+
affinity_runner_id, origin_session_id, parent_run_id, error_code, error_message,
|
|
1118
|
+
created_at, updated_at, started_at, finished_at
|
|
1119
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, 'queued', NULL, NULL, NULL, ?, ?, ?, NULL, NULL, ?, ?, NULL, NULL)`,
|
|
1120
|
+
)
|
|
1121
|
+
.run(
|
|
1122
|
+
options.runId,
|
|
1123
|
+
options.workflowName,
|
|
1124
|
+
options.workflowSourceRef,
|
|
1125
|
+
sourceJson,
|
|
1126
|
+
options.definitionDigest,
|
|
1127
|
+
inputJson,
|
|
1128
|
+
launchJson,
|
|
1129
|
+
options.runnerId,
|
|
1130
|
+
options.originSessionId,
|
|
1131
|
+
options.parentRunId ?? null,
|
|
1132
|
+
now,
|
|
1133
|
+
now,
|
|
1134
|
+
);
|
|
1135
|
+
return this.requireWorkflowRun(options.runId);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
/** Insert a run that will start immediately outside a live model turn. */
|
|
908
1139
|
enqueueWorkflowRun(options: {
|
|
909
1140
|
runId: string;
|
|
910
1141
|
workflowName: string;
|
|
911
1142
|
workflowSourceRef: string;
|
|
1143
|
+
workflowSource?: unknown;
|
|
1144
|
+
definitionDigest?: string;
|
|
912
1145
|
input: unknown;
|
|
1146
|
+
launchOptions?: unknown;
|
|
913
1147
|
runnerId: string;
|
|
914
1148
|
claimToken: string;
|
|
915
1149
|
leaseMs: number;
|
|
@@ -928,22 +1162,31 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
928
1162
|
validateKey(options.originSessionId, "origin session id");
|
|
929
1163
|
}
|
|
930
1164
|
const inputJson = canonicalJson(options.input ?? null, "workflow run input");
|
|
1165
|
+
const sourceJson = canonicalJson(
|
|
1166
|
+
options.workflowSource ?? { ref: options.workflowSourceRef },
|
|
1167
|
+
"workflow run source",
|
|
1168
|
+
);
|
|
1169
|
+
const launchJson = canonicalJson(options.launchOptions ?? {}, "workflow launch options");
|
|
931
1170
|
validateJsonSize(inputJson, "Workflow run input", MAX_RESOURCE_VALUE_BYTES);
|
|
932
1171
|
const now = validTimestamp(options.now);
|
|
933
1172
|
const expiresAt = epoch(now) + options.leaseMs;
|
|
934
1173
|
this.database
|
|
935
1174
|
.prepare(
|
|
936
1175
|
`INSERT INTO workflow_run_queue (
|
|
937
|
-
run_id, workflow_ref, workflow_path,
|
|
938
|
-
runner_id, claim_token, claim_expires_at,
|
|
939
|
-
origin_session_id, parent_run_id,
|
|
940
|
-
|
|
1176
|
+
run_id, workflow_ref, workflow_path, workflow_source_json, definition_digest,
|
|
1177
|
+
input_json, launch_options_json, status, runner_id, claim_token, claim_expires_at,
|
|
1178
|
+
affinity_runner_id, origin_session_id, parent_run_id, error_code, error_message,
|
|
1179
|
+
created_at, updated_at, started_at, finished_at
|
|
1180
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, 'starting', ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?, NULL)`,
|
|
941
1181
|
)
|
|
942
1182
|
.run(
|
|
943
1183
|
options.runId,
|
|
944
1184
|
options.workflowName,
|
|
945
1185
|
options.workflowSourceRef,
|
|
1186
|
+
sourceJson,
|
|
1187
|
+
options.definitionDigest ?? "unavailable",
|
|
946
1188
|
inputJson,
|
|
1189
|
+
launchJson,
|
|
947
1190
|
options.runnerId,
|
|
948
1191
|
options.claimToken,
|
|
949
1192
|
expiresAt,
|
|
@@ -952,6 +1195,7 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
952
1195
|
options.parentRunId ?? null,
|
|
953
1196
|
now,
|
|
954
1197
|
now,
|
|
1198
|
+
now,
|
|
955
1199
|
);
|
|
956
1200
|
return this.requireWorkflowRun(options.runId);
|
|
957
1201
|
}
|
|
@@ -978,6 +1222,58 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
978
1222
|
return rows.map(workflowRunFromRow);
|
|
979
1223
|
}
|
|
980
1224
|
|
|
1225
|
+
findSessionReservation(sessionId: string): WorkflowRunQueueRecord | undefined {
|
|
1226
|
+
validateKey(sessionId, "session id");
|
|
1227
|
+
const row = this.database
|
|
1228
|
+
.prepare(
|
|
1229
|
+
`SELECT * FROM workflow_run_queue
|
|
1230
|
+
WHERE origin_session_id = ? AND status IN ('queued', 'starting', 'running')
|
|
1231
|
+
ORDER BY created_at LIMIT 1`,
|
|
1232
|
+
)
|
|
1233
|
+
.get(sessionId) as WorkflowRunQueueRow | undefined;
|
|
1234
|
+
return row === undefined ? undefined : workflowRunFromRow(row);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
claimWorkflowRun(options: {
|
|
1238
|
+
runId: string;
|
|
1239
|
+
runnerId: string;
|
|
1240
|
+
claimToken: string;
|
|
1241
|
+
leaseMs: number;
|
|
1242
|
+
now?: string;
|
|
1243
|
+
}): WorkflowRunQueueRecord | undefined {
|
|
1244
|
+
validateRunId(options.runId);
|
|
1245
|
+
validateKey(options.runnerId, "runner id");
|
|
1246
|
+
validateKey(options.claimToken, "claim token");
|
|
1247
|
+
validateDuration(options.leaseMs, "leaseMs");
|
|
1248
|
+
const now = validTimestamp(options.now);
|
|
1249
|
+
const nowMs = epoch(now);
|
|
1250
|
+
const expiresAt = nowMs + options.leaseMs;
|
|
1251
|
+
const result = this.database
|
|
1252
|
+
.prepare(
|
|
1253
|
+
`UPDATE workflow_run_queue
|
|
1254
|
+
SET status = 'starting', runner_id = ?, claim_token = ?, claim_expires_at = ?,
|
|
1255
|
+
started_at = COALESCE(started_at, ?), updated_at = ?
|
|
1256
|
+
WHERE run_id = ? AND (
|
|
1257
|
+
status IN ('queued', 'parked')
|
|
1258
|
+
OR (status = 'starting' AND claim_expires_at IS NOT NULL AND claim_expires_at <= ?)
|
|
1259
|
+
)`,
|
|
1260
|
+
)
|
|
1261
|
+
.run(options.runnerId, options.claimToken, expiresAt, now, now, options.runId, nowMs);
|
|
1262
|
+
return result.changes === 1 ? this.requireWorkflowRun(options.runId) : undefined;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
markWorkflowRunRunning(options: { runId: string; claimToken: string; now?: string }): boolean {
|
|
1266
|
+
validateRunId(options.runId);
|
|
1267
|
+
const now = validTimestamp(options.now);
|
|
1268
|
+
const result = this.database
|
|
1269
|
+
.prepare(
|
|
1270
|
+
`UPDATE workflow_run_queue SET status = 'running', updated_at = ?
|
|
1271
|
+
WHERE run_id = ? AND claim_token = ? AND status = 'starting'`,
|
|
1272
|
+
)
|
|
1273
|
+
.run(now, options.runId, options.claimToken);
|
|
1274
|
+
return result.changes === 1;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
981
1277
|
/**
|
|
982
1278
|
* Claim the oldest claimable run, preferring runs with affinity to this
|
|
983
1279
|
* runner. Parked rows are claimable immediately; claimed rows become
|
|
@@ -1008,11 +1304,11 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1008
1304
|
excluded.length === 0 ? "" : `AND run_id NOT IN (${excluded.map(() => "?").join(", ")})`;
|
|
1009
1305
|
const sessionFilter =
|
|
1010
1306
|
options.sessionId === undefined
|
|
1011
|
-
? ""
|
|
1307
|
+
? "AND status != 'queued' AND (status != 'starting' OR origin_session_id IS NULL)"
|
|
1012
1308
|
: "AND (origin_session_id IS NULL OR origin_session_id = ?)";
|
|
1013
1309
|
const claimable = `(
|
|
1014
|
-
status
|
|
1015
|
-
OR (status
|
|
1310
|
+
status IN ('queued', 'parked')
|
|
1311
|
+
OR (status IN ('starting', 'running') AND claim_expires_at IS NOT NULL AND claim_expires_at <= ?)
|
|
1016
1312
|
)`;
|
|
1017
1313
|
return this.transaction(() => {
|
|
1018
1314
|
const candidate = this.database
|
|
@@ -1036,14 +1332,14 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1036
1332
|
const result = this.database
|
|
1037
1333
|
.prepare(
|
|
1038
1334
|
`UPDATE workflow_run_queue
|
|
1039
|
-
SET status = '
|
|
1040
|
-
claim_expires_at = ?, updated_at = ?
|
|
1335
|
+
SET status = 'starting', runner_id = ?, claim_token = ?,
|
|
1336
|
+
claim_expires_at = ?, started_at = COALESCE(started_at, ?), updated_at = ?
|
|
1041
1337
|
WHERE run_id = ? AND (
|
|
1042
|
-
status
|
|
1043
|
-
OR (status
|
|
1338
|
+
status IN ('queued', 'parked')
|
|
1339
|
+
OR (status IN ('starting', 'running') AND claim_expires_at IS NOT NULL AND claim_expires_at <= ?)
|
|
1044
1340
|
)`,
|
|
1045
1341
|
)
|
|
1046
|
-
.run(options.runnerId, options.claimToken, expiresAt, now, candidate.run_id, nowMs);
|
|
1342
|
+
.run(options.runnerId, options.claimToken, expiresAt, now, now, candidate.run_id, nowMs);
|
|
1047
1343
|
return result.changes === 1 ? this.requireWorkflowRun(candidate.run_id) : undefined;
|
|
1048
1344
|
});
|
|
1049
1345
|
}
|
|
@@ -1062,7 +1358,7 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1062
1358
|
const result = this.database
|
|
1063
1359
|
.prepare(
|
|
1064
1360
|
`UPDATE workflow_run_queue SET claim_expires_at = ?
|
|
1065
|
-
WHERE run_id = ? AND claim_token = ? AND status
|
|
1361
|
+
WHERE run_id = ? AND claim_token = ? AND status IN ('starting', 'running')
|
|
1066
1362
|
AND claim_expires_at > ?`,
|
|
1067
1363
|
)
|
|
1068
1364
|
.run(expiresAt, options.runId, options.claimToken, nowMs);
|
|
@@ -1076,7 +1372,7 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1076
1372
|
const row = this.database
|
|
1077
1373
|
.prepare(
|
|
1078
1374
|
`SELECT 1 AS live FROM workflow_run_queue
|
|
1079
|
-
WHERE run_id = ? AND claim_token = ? AND status
|
|
1375
|
+
WHERE run_id = ? AND claim_token = ? AND status IN ('starting', 'running')
|
|
1080
1376
|
AND claim_expires_at > ?`,
|
|
1081
1377
|
)
|
|
1082
1378
|
.get(options.runId, options.claimToken, nowMs);
|
|
@@ -1092,12 +1388,68 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1092
1388
|
`UPDATE workflow_run_queue
|
|
1093
1389
|
SET status = 'parked', runner_id = NULL, claim_token = NULL,
|
|
1094
1390
|
claim_expires_at = NULL, updated_at = ?
|
|
1095
|
-
WHERE run_id = ? AND claim_token = ? AND status
|
|
1391
|
+
WHERE run_id = ? AND claim_token = ? AND status IN ('starting', 'running')`,
|
|
1096
1392
|
)
|
|
1097
1393
|
.run(now, options.runId, options.claimToken);
|
|
1098
1394
|
return result.changes === 1;
|
|
1099
1395
|
}
|
|
1100
1396
|
|
|
1397
|
+
failWorkflowRun(options: {
|
|
1398
|
+
runId: string;
|
|
1399
|
+
claimToken?: string;
|
|
1400
|
+
errorCode: string;
|
|
1401
|
+
errorMessage: string;
|
|
1402
|
+
now?: string;
|
|
1403
|
+
}): boolean {
|
|
1404
|
+
validateRunId(options.runId);
|
|
1405
|
+
validateKey(options.errorCode, "workflow launch error code");
|
|
1406
|
+
const message = options.errorMessage.trim();
|
|
1407
|
+
if (message.length === 0 || Buffer.byteLength(message) > 2_048) {
|
|
1408
|
+
throw new Error("Workflow launch error message must be between 1 and 2048 bytes");
|
|
1409
|
+
}
|
|
1410
|
+
const now = validTimestamp(options.now);
|
|
1411
|
+
const claimFilter = options.claimToken === undefined ? "" : "AND claim_token = ?";
|
|
1412
|
+
const result = this.database
|
|
1413
|
+
.prepare(
|
|
1414
|
+
`UPDATE workflow_run_queue
|
|
1415
|
+
SET status = 'failed', runner_id = NULL, claim_token = NULL,
|
|
1416
|
+
claim_expires_at = NULL, parent_run_id = NULL, input_json = 'null',
|
|
1417
|
+
launch_options_json = '{}', error_code = ?, error_message = ?,
|
|
1418
|
+
finished_at = ?, updated_at = ?
|
|
1419
|
+
WHERE run_id = ? AND status IN ('queued', 'starting', 'running') ${claimFilter}`,
|
|
1420
|
+
)
|
|
1421
|
+
.run(
|
|
1422
|
+
options.errorCode,
|
|
1423
|
+
message,
|
|
1424
|
+
now,
|
|
1425
|
+
now,
|
|
1426
|
+
options.runId,
|
|
1427
|
+
...(options.claimToken === undefined ? [] : [options.claimToken]),
|
|
1428
|
+
);
|
|
1429
|
+
return result.changes === 1;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
cancelWorkflowRun(options: { runId: string; claimToken?: string; now?: string }): boolean {
|
|
1433
|
+
validateRunId(options.runId);
|
|
1434
|
+
const now = validTimestamp(options.now);
|
|
1435
|
+
const claimFilter = options.claimToken === undefined ? "" : "AND claim_token = ?";
|
|
1436
|
+
const result = this.database
|
|
1437
|
+
.prepare(
|
|
1438
|
+
`UPDATE workflow_run_queue
|
|
1439
|
+
SET status = 'cancelled', runner_id = NULL, claim_token = NULL,
|
|
1440
|
+
claim_expires_at = NULL, input_json = 'null', launch_options_json = '{}',
|
|
1441
|
+
finished_at = ?, updated_at = ?
|
|
1442
|
+
WHERE run_id = ? AND status IN ('queued', 'starting') ${claimFilter}`,
|
|
1443
|
+
)
|
|
1444
|
+
.run(
|
|
1445
|
+
now,
|
|
1446
|
+
now,
|
|
1447
|
+
options.runId,
|
|
1448
|
+
...(options.claimToken === undefined ? [] : [options.claimToken]),
|
|
1449
|
+
);
|
|
1450
|
+
return result.changes === 1;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1101
1453
|
/**
|
|
1102
1454
|
* Delete a claimed row. Used when a continuation fails before its bundle
|
|
1103
1455
|
* exists, so the parent's one-continuation slot is not consumed by a run
|
|
@@ -1119,10 +1471,11 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1119
1471
|
.prepare(
|
|
1120
1472
|
`UPDATE workflow_run_queue
|
|
1121
1473
|
SET status = 'done', runner_id = NULL, claim_token = NULL,
|
|
1122
|
-
claim_expires_at = NULL,
|
|
1123
|
-
|
|
1474
|
+
claim_expires_at = NULL, input_json = 'null', launch_options_json = '{}',
|
|
1475
|
+
finished_at = ?, updated_at = ?
|
|
1476
|
+
WHERE run_id = ? AND claim_token = ? AND status IN ('starting', 'running')`,
|
|
1124
1477
|
)
|
|
1125
|
-
.run(now, options.runId, options.claimToken);
|
|
1478
|
+
.run(now, now, options.runId, options.claimToken);
|
|
1126
1479
|
return result.changes === 1;
|
|
1127
1480
|
}
|
|
1128
1481
|
|
|
@@ -1149,26 +1502,28 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1149
1502
|
const row = this.database
|
|
1150
1503
|
.prepare("SELECT * FROM workflow_run_queue WHERE run_id = ?")
|
|
1151
1504
|
.get(options.runId) as WorkflowRunQueueRow | undefined;
|
|
1152
|
-
if (row === undefined ||
|
|
1505
|
+
if (row === undefined || ["done", "failed", "cancelled"].includes(row.status)) {
|
|
1506
|
+
return false;
|
|
1507
|
+
}
|
|
1153
1508
|
if (row.workflow_path === options.workflowSourceRef && row.status === "parked") {
|
|
1154
1509
|
return "unchanged";
|
|
1155
1510
|
}
|
|
1156
1511
|
const claimable =
|
|
1157
1512
|
row.status === "parked" ||
|
|
1158
|
-
(row.status === "
|
|
1513
|
+
(row.status === "starting" &&
|
|
1159
1514
|
row.claim_token === options.claimToken &&
|
|
1160
1515
|
row.claim_expires_at !== null &&
|
|
1161
1516
|
row.claim_expires_at > nowMs) ||
|
|
1162
|
-
(row.status === "
|
|
1517
|
+
(row.status === "starting" &&
|
|
1163
1518
|
row.claim_expires_at !== null &&
|
|
1164
1519
|
row.claim_expires_at <= nowMs);
|
|
1165
1520
|
if (!claimable) return false;
|
|
1166
1521
|
const result = this.database
|
|
1167
1522
|
.prepare(
|
|
1168
1523
|
`UPDATE workflow_run_queue
|
|
1169
|
-
SET workflow_ref = ?, workflow_path = ?, status = '
|
|
1524
|
+
SET workflow_ref = ?, workflow_path = ?, status = 'starting', runner_id = ?,
|
|
1170
1525
|
claim_token = ?, claim_expires_at = ?, updated_at = ?
|
|
1171
|
-
WHERE run_id = ? AND status
|
|
1526
|
+
WHERE run_id = ? AND status NOT IN ('done', 'failed', 'cancelled')`,
|
|
1172
1527
|
)
|
|
1173
1528
|
.run(
|
|
1174
1529
|
options.workflowName,
|
|
@@ -1208,26 +1563,28 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1208
1563
|
const row = this.database
|
|
1209
1564
|
.prepare("SELECT * FROM workflow_run_queue WHERE run_id = ?")
|
|
1210
1565
|
.get(options.runId) as WorkflowRunQueueRow | undefined;
|
|
1211
|
-
if (row === undefined ||
|
|
1566
|
+
if (row === undefined || ["done", "failed", "cancelled"].includes(row.status)) {
|
|
1567
|
+
return false;
|
|
1568
|
+
}
|
|
1212
1569
|
const sourceMatches =
|
|
1213
1570
|
row.workflow_path === options.oldWorkflowPath ||
|
|
1214
1571
|
row.workflow_path === options.workflowSourceRef;
|
|
1215
1572
|
const claimable =
|
|
1216
1573
|
row.status === "parked" ||
|
|
1217
|
-
(row.status === "
|
|
1574
|
+
(row.status === "starting" &&
|
|
1218
1575
|
row.claim_token === options.claimToken &&
|
|
1219
1576
|
row.claim_expires_at !== null &&
|
|
1220
1577
|
row.claim_expires_at > nowMs) ||
|
|
1221
|
-
(row.status === "
|
|
1578
|
+
(row.status === "starting" &&
|
|
1222
1579
|
row.claim_expires_at !== null &&
|
|
1223
1580
|
row.claim_expires_at <= nowMs);
|
|
1224
1581
|
if (!sourceMatches || !claimable) return false;
|
|
1225
1582
|
const result = this.database
|
|
1226
1583
|
.prepare(
|
|
1227
1584
|
`UPDATE workflow_run_queue
|
|
1228
|
-
SET workflow_ref = ?, workflow_path = ?, status = '
|
|
1585
|
+
SET workflow_ref = ?, workflow_path = ?, status = 'starting', runner_id = ?,
|
|
1229
1586
|
claim_token = ?, claim_expires_at = ?, updated_at = ?
|
|
1230
|
-
WHERE run_id = ? AND status
|
|
1587
|
+
WHERE run_id = ? AND status NOT IN ('done', 'failed', 'cancelled')`,
|
|
1231
1588
|
)
|
|
1232
1589
|
.run(
|
|
1233
1590
|
options.workflowName,
|
|
@@ -1291,6 +1648,287 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1291
1648
|
}));
|
|
1292
1649
|
}
|
|
1293
1650
|
|
|
1651
|
+
ensureWorkflowTurnIntent(options: {
|
|
1652
|
+
intentId: string;
|
|
1653
|
+
sourceEventId: string;
|
|
1654
|
+
runId: string;
|
|
1655
|
+
workflowRef: string;
|
|
1656
|
+
targetSessionId: string;
|
|
1657
|
+
cause: WorkflowTurnIntentCause;
|
|
1658
|
+
nodeId?: string | null;
|
|
1659
|
+
attemptId?: string | null;
|
|
1660
|
+
fallbackFacts: WorkflowTurnIntentFacts;
|
|
1661
|
+
eligible?: boolean;
|
|
1662
|
+
now?: string;
|
|
1663
|
+
}): WorkflowTurnIntentRecord {
|
|
1664
|
+
validateKey(options.intentId, "workflow turn intent id");
|
|
1665
|
+
validateKey(options.sourceEventId, "workflow turn source event id");
|
|
1666
|
+
validateRunId(options.runId);
|
|
1667
|
+
validateKey(options.workflowRef, "workflow turn workflow ref");
|
|
1668
|
+
validateKey(options.targetSessionId, "workflow turn target session id");
|
|
1669
|
+
validateWorkflowTurnIntentCause(options.cause);
|
|
1670
|
+
const nodeId = options.nodeId ?? null;
|
|
1671
|
+
const attemptId = options.attemptId ?? null;
|
|
1672
|
+
if (nodeId !== null) validateKey(nodeId, "workflow turn node id");
|
|
1673
|
+
if (attemptId !== null) validateKey(attemptId, "workflow turn attempt id");
|
|
1674
|
+
const factsJson = workflowTurnIntentFactsJson(options.fallbackFacts);
|
|
1675
|
+
const now = validTimestamp(options.now);
|
|
1676
|
+
const eligibleAt = options.eligible === true ? now : null;
|
|
1677
|
+
this.database
|
|
1678
|
+
.prepare(
|
|
1679
|
+
`INSERT INTO workflow_turn_intents (
|
|
1680
|
+
intent_id, source_event_id, run_id, workflow_ref, target_session_id,
|
|
1681
|
+
cause, node_id, attempt_id, fallback_facts_json, requested_at, eligible_at,
|
|
1682
|
+
resolved_at, resolution, resolution_message_id,
|
|
1683
|
+
delivery_claim_token, delivery_claim_expires_at
|
|
1684
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, NULL, NULL, NULL)
|
|
1685
|
+
ON CONFLICT(intent_id) DO NOTHING`,
|
|
1686
|
+
)
|
|
1687
|
+
.run(
|
|
1688
|
+
options.intentId,
|
|
1689
|
+
options.sourceEventId,
|
|
1690
|
+
options.runId,
|
|
1691
|
+
options.workflowRef,
|
|
1692
|
+
options.targetSessionId,
|
|
1693
|
+
options.cause,
|
|
1694
|
+
nodeId,
|
|
1695
|
+
attemptId,
|
|
1696
|
+
factsJson,
|
|
1697
|
+
now,
|
|
1698
|
+
eligibleAt,
|
|
1699
|
+
);
|
|
1700
|
+
const existing = this.getWorkflowTurnIntent(options.intentId);
|
|
1701
|
+
if (existing === undefined) {
|
|
1702
|
+
throw new Error(`Workflow turn intent was not stored: ${options.intentId}`);
|
|
1703
|
+
}
|
|
1704
|
+
if (
|
|
1705
|
+
existing.sourceEventId !== options.sourceEventId ||
|
|
1706
|
+
existing.runId !== options.runId ||
|
|
1707
|
+
existing.workflowRef !== options.workflowRef ||
|
|
1708
|
+
existing.targetSessionId !== options.targetSessionId ||
|
|
1709
|
+
existing.cause !== options.cause ||
|
|
1710
|
+
existing.nodeId !== nodeId ||
|
|
1711
|
+
existing.attemptId !== attemptId
|
|
1712
|
+
) {
|
|
1713
|
+
throw new Error(`Workflow turn intent identity conflict: ${options.intentId}`);
|
|
1714
|
+
}
|
|
1715
|
+
return existing;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
getWorkflowTurnIntent(intentId: string): WorkflowTurnIntentRecord | undefined {
|
|
1719
|
+
validateKey(intentId, "workflow turn intent id");
|
|
1720
|
+
const row = this.database
|
|
1721
|
+
.prepare("SELECT * FROM workflow_turn_intents WHERE intent_id = ?")
|
|
1722
|
+
.get(intentId) as WorkflowTurnIntentRow | undefined;
|
|
1723
|
+
return row === undefined ? undefined : workflowTurnIntentFromRow(row);
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
findPendingWorkflowTurnIntent(options: {
|
|
1727
|
+
runId: string;
|
|
1728
|
+
targetSessionId: string;
|
|
1729
|
+
}): WorkflowTurnIntentRecord | undefined {
|
|
1730
|
+
validateRunId(options.runId);
|
|
1731
|
+
validateKey(options.targetSessionId, "workflow turn target session id");
|
|
1732
|
+
const row = this.database
|
|
1733
|
+
.prepare(
|
|
1734
|
+
`SELECT * FROM workflow_turn_intents
|
|
1735
|
+
WHERE run_id = ? AND target_session_id = ? AND resolved_at IS NULL
|
|
1736
|
+
ORDER BY requested_at DESC, intent_id DESC LIMIT 1`,
|
|
1737
|
+
)
|
|
1738
|
+
.get(options.runId, options.targetSessionId) as WorkflowTurnIntentRow | undefined;
|
|
1739
|
+
return row === undefined ? undefined : workflowTurnIntentFromRow(row);
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
claimWorkflowTurnIntent(options: {
|
|
1743
|
+
intentId: string;
|
|
1744
|
+
targetSessionId: string;
|
|
1745
|
+
claimToken: string;
|
|
1746
|
+
leaseMs: number;
|
|
1747
|
+
now?: string;
|
|
1748
|
+
}): WorkflowTurnIntentRecord | undefined {
|
|
1749
|
+
validateKey(options.intentId, "workflow turn intent id");
|
|
1750
|
+
validateKey(options.targetSessionId, "workflow turn target session id");
|
|
1751
|
+
validateKey(options.claimToken, "workflow turn claim token");
|
|
1752
|
+
validateDuration(options.leaseMs, "leaseMs");
|
|
1753
|
+
const nowMs = epoch(validTimestamp(options.now));
|
|
1754
|
+
const result = this.database
|
|
1755
|
+
.prepare(
|
|
1756
|
+
`UPDATE workflow_turn_intents
|
|
1757
|
+
SET delivery_claim_token = ?, delivery_claim_expires_at = ?
|
|
1758
|
+
WHERE intent_id = ? AND target_session_id = ? AND resolved_at IS NULL
|
|
1759
|
+
AND (delivery_claim_expires_at IS NULL OR delivery_claim_expires_at <= ?)`,
|
|
1760
|
+
)
|
|
1761
|
+
.run(
|
|
1762
|
+
options.claimToken,
|
|
1763
|
+
nowMs + options.leaseMs,
|
|
1764
|
+
options.intentId,
|
|
1765
|
+
options.targetSessionId,
|
|
1766
|
+
nowMs,
|
|
1767
|
+
);
|
|
1768
|
+
if (result.changes !== 1) return undefined;
|
|
1769
|
+
const row = this.database
|
|
1770
|
+
.prepare("SELECT * FROM workflow_turn_intents WHERE intent_id = ?")
|
|
1771
|
+
.get(options.intentId) as WorkflowTurnIntentRow;
|
|
1772
|
+
return workflowTurnIntentFromRow(row);
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
claimEligibleWorkflowTurnIntents(options: {
|
|
1776
|
+
targetSessionId: string;
|
|
1777
|
+
claimToken: string;
|
|
1778
|
+
leaseMs: number;
|
|
1779
|
+
limit?: number;
|
|
1780
|
+
now?: string;
|
|
1781
|
+
}): WorkflowTurnIntentRecord[] {
|
|
1782
|
+
validateKey(options.targetSessionId, "workflow turn target session id");
|
|
1783
|
+
validateKey(options.claimToken, "workflow turn claim token");
|
|
1784
|
+
validateDuration(options.leaseMs, "leaseMs");
|
|
1785
|
+
const limit = options.limit ?? 20;
|
|
1786
|
+
if (!Number.isSafeInteger(limit) || limit <= 0 || limit > 100) {
|
|
1787
|
+
throw new Error("Workflow turn intent limit must be an integer from 1 through 100");
|
|
1788
|
+
}
|
|
1789
|
+
const now = validTimestamp(options.now);
|
|
1790
|
+
const nowMs = epoch(now);
|
|
1791
|
+
return this.transaction(() => {
|
|
1792
|
+
const ids = this.database
|
|
1793
|
+
.prepare(
|
|
1794
|
+
`SELECT intent_id FROM workflow_turn_intents
|
|
1795
|
+
WHERE target_session_id = ? AND resolved_at IS NULL AND eligible_at IS NOT NULL
|
|
1796
|
+
AND eligible_at <= ?
|
|
1797
|
+
AND (delivery_claim_expires_at IS NULL OR delivery_claim_expires_at <= ?)
|
|
1798
|
+
ORDER BY eligible_at, intent_id LIMIT ?`,
|
|
1799
|
+
)
|
|
1800
|
+
.all(options.targetSessionId, now, nowMs, limit) as { intent_id: string }[];
|
|
1801
|
+
if (ids.length === 0) return [];
|
|
1802
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
1803
|
+
this.database
|
|
1804
|
+
.prepare(
|
|
1805
|
+
`UPDATE workflow_turn_intents
|
|
1806
|
+
SET delivery_claim_token = ?, delivery_claim_expires_at = ?
|
|
1807
|
+
WHERE intent_id IN (${placeholders}) AND resolved_at IS NULL
|
|
1808
|
+
AND (delivery_claim_expires_at IS NULL OR delivery_claim_expires_at <= ?)`,
|
|
1809
|
+
)
|
|
1810
|
+
.run(
|
|
1811
|
+
options.claimToken,
|
|
1812
|
+
nowMs + options.leaseMs,
|
|
1813
|
+
...ids.map((row) => row.intent_id),
|
|
1814
|
+
nowMs,
|
|
1815
|
+
);
|
|
1816
|
+
const rows = this.database
|
|
1817
|
+
.prepare(
|
|
1818
|
+
`SELECT * FROM workflow_turn_intents WHERE delivery_claim_token = ?
|
|
1819
|
+
ORDER BY eligible_at, intent_id`,
|
|
1820
|
+
)
|
|
1821
|
+
.all(options.claimToken) as WorkflowTurnIntentRow[];
|
|
1822
|
+
return rows.map(workflowTurnIntentFromRow);
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
makeWorkflowTurnIntentEligible(options: {
|
|
1827
|
+
intentId: string;
|
|
1828
|
+
fallbackFacts: WorkflowTurnIntentFacts;
|
|
1829
|
+
now?: string;
|
|
1830
|
+
}): boolean {
|
|
1831
|
+
validateKey(options.intentId, "workflow turn intent id");
|
|
1832
|
+
const result = this.database
|
|
1833
|
+
.prepare(
|
|
1834
|
+
`UPDATE workflow_turn_intents
|
|
1835
|
+
SET fallback_facts_json = ?, eligible_at = COALESCE(eligible_at, ?)
|
|
1836
|
+
WHERE intent_id = ? AND resolved_at IS NULL`,
|
|
1837
|
+
)
|
|
1838
|
+
.run(
|
|
1839
|
+
workflowTurnIntentFactsJson(options.fallbackFacts),
|
|
1840
|
+
validTimestamp(options.now),
|
|
1841
|
+
options.intentId,
|
|
1842
|
+
);
|
|
1843
|
+
return result.changes === 1;
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
resolveWorkflowTurnIntent(options: {
|
|
1847
|
+
intentId: string;
|
|
1848
|
+
targetSessionId: string;
|
|
1849
|
+
claimToken: string;
|
|
1850
|
+
resolution: WorkflowTurnIntentResolution;
|
|
1851
|
+
messageId: string;
|
|
1852
|
+
now?: string;
|
|
1853
|
+
}): boolean {
|
|
1854
|
+
validateKey(options.intentId, "workflow turn intent id");
|
|
1855
|
+
validateKey(options.targetSessionId, "workflow turn target session id");
|
|
1856
|
+
validateKey(options.claimToken, "workflow turn claim token");
|
|
1857
|
+
validateWorkflowTurnIntentResolution(options.resolution);
|
|
1858
|
+
validateKey(options.messageId, "workflow turn resolution message id");
|
|
1859
|
+
const result = this.database
|
|
1860
|
+
.prepare(
|
|
1861
|
+
`UPDATE workflow_turn_intents
|
|
1862
|
+
SET resolved_at = ?, resolution = ?, resolution_message_id = ?,
|
|
1863
|
+
delivery_claim_token = NULL, delivery_claim_expires_at = NULL
|
|
1864
|
+
WHERE intent_id = ? AND target_session_id = ? AND resolved_at IS NULL
|
|
1865
|
+
AND delivery_claim_token = ?`,
|
|
1866
|
+
)
|
|
1867
|
+
.run(
|
|
1868
|
+
validTimestamp(options.now),
|
|
1869
|
+
options.resolution,
|
|
1870
|
+
options.messageId,
|
|
1871
|
+
options.intentId,
|
|
1872
|
+
options.targetSessionId,
|
|
1873
|
+
options.claimToken,
|
|
1874
|
+
);
|
|
1875
|
+
return result.changes === 1;
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
releaseWorkflowTurnIntentClaim(options: {
|
|
1879
|
+
intentId: string;
|
|
1880
|
+
targetSessionId: string;
|
|
1881
|
+
claimToken: string;
|
|
1882
|
+
}): boolean {
|
|
1883
|
+
validateKey(options.intentId, "workflow turn intent id");
|
|
1884
|
+
validateKey(options.targetSessionId, "workflow turn target session id");
|
|
1885
|
+
validateKey(options.claimToken, "workflow turn claim token");
|
|
1886
|
+
const result = this.database
|
|
1887
|
+
.prepare(
|
|
1888
|
+
`UPDATE workflow_turn_intents
|
|
1889
|
+
SET delivery_claim_token = NULL, delivery_claim_expires_at = NULL
|
|
1890
|
+
WHERE intent_id = ? AND target_session_id = ? AND resolved_at IS NULL
|
|
1891
|
+
AND delivery_claim_token = ?`,
|
|
1892
|
+
)
|
|
1893
|
+
.run(options.intentId, options.targetSessionId, options.claimToken);
|
|
1894
|
+
return result.changes === 1;
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
listWorkflowTurnIntents(
|
|
1898
|
+
options: {
|
|
1899
|
+
targetSessionId?: string;
|
|
1900
|
+
runId?: string;
|
|
1901
|
+
limit?: number;
|
|
1902
|
+
} = {},
|
|
1903
|
+
): WorkflowTurnIntentRecord[] {
|
|
1904
|
+
if (options.targetSessionId !== undefined) {
|
|
1905
|
+
validateKey(options.targetSessionId, "workflow turn target session id");
|
|
1906
|
+
}
|
|
1907
|
+
if (options.runId !== undefined) validateRunId(options.runId);
|
|
1908
|
+
const limit = options.limit ?? 20;
|
|
1909
|
+
if (!Number.isSafeInteger(limit) || limit <= 0 || limit > 100) {
|
|
1910
|
+
throw new Error("Workflow turn intent limit must be an integer from 1 through 100");
|
|
1911
|
+
}
|
|
1912
|
+
const clauses: string[] = [];
|
|
1913
|
+
const values: unknown[] = [];
|
|
1914
|
+
if (options.targetSessionId !== undefined) {
|
|
1915
|
+
clauses.push("target_session_id = ?");
|
|
1916
|
+
values.push(options.targetSessionId);
|
|
1917
|
+
}
|
|
1918
|
+
if (options.runId !== undefined) {
|
|
1919
|
+
clauses.push("run_id = ?");
|
|
1920
|
+
values.push(options.runId);
|
|
1921
|
+
}
|
|
1922
|
+
const where = clauses.length === 0 ? "" : `WHERE ${clauses.join(" AND ")}`;
|
|
1923
|
+
const rows = this.database
|
|
1924
|
+
.prepare(
|
|
1925
|
+
`SELECT * FROM workflow_turn_intents ${where}
|
|
1926
|
+
ORDER BY requested_at DESC, intent_id DESC LIMIT ?`,
|
|
1927
|
+
)
|
|
1928
|
+
.all(...values, limit) as WorkflowTurnIntentRow[];
|
|
1929
|
+
return rows.map(workflowTurnIntentFromRow);
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1294
1932
|
enqueueWorkflowNotification(options: {
|
|
1295
1933
|
runId: string;
|
|
1296
1934
|
nodeId: string;
|
|
@@ -1483,19 +2121,31 @@ const SCHEMA_SQL = `
|
|
|
1483
2121
|
run_id TEXT PRIMARY KEY,
|
|
1484
2122
|
workflow_ref TEXT NOT NULL,
|
|
1485
2123
|
workflow_path TEXT NOT NULL,
|
|
2124
|
+
workflow_source_json TEXT NOT NULL,
|
|
2125
|
+
definition_digest TEXT NOT NULL,
|
|
1486
2126
|
input_json TEXT NOT NULL,
|
|
1487
|
-
|
|
2127
|
+
launch_options_json TEXT NOT NULL,
|
|
2128
|
+
status TEXT NOT NULL CHECK (
|
|
2129
|
+
status IN ('queued', 'starting', 'running', 'parked', 'done', 'failed', 'cancelled')
|
|
2130
|
+
),
|
|
1488
2131
|
runner_id TEXT,
|
|
1489
2132
|
claim_token TEXT,
|
|
1490
2133
|
claim_expires_at INTEGER,
|
|
1491
2134
|
affinity_runner_id TEXT,
|
|
1492
2135
|
origin_session_id TEXT,
|
|
1493
2136
|
parent_run_id TEXT,
|
|
2137
|
+
error_code TEXT,
|
|
2138
|
+
error_message TEXT,
|
|
1494
2139
|
created_at TEXT NOT NULL,
|
|
1495
|
-
updated_at TEXT NOT NULL
|
|
2140
|
+
updated_at TEXT NOT NULL,
|
|
2141
|
+
started_at TEXT,
|
|
2142
|
+
finished_at TEXT
|
|
1496
2143
|
);
|
|
1497
2144
|
CREATE INDEX IF NOT EXISTS workflow_run_queue_claimable
|
|
1498
2145
|
ON workflow_run_queue(status, claim_expires_at);
|
|
2146
|
+
CREATE UNIQUE INDEX IF NOT EXISTS workflow_run_queue_session_reservation
|
|
2147
|
+
ON workflow_run_queue(origin_session_id)
|
|
2148
|
+
WHERE origin_session_id IS NOT NULL AND status IN ('queued', 'starting', 'running');
|
|
1499
2149
|
-- A checkpointed parent admits exactly one continuation run, across
|
|
1500
2150
|
-- sessions and processes.
|
|
1501
2151
|
CREATE UNIQUE INDEX IF NOT EXISTS workflow_run_queue_parent
|
|
@@ -1531,6 +2181,40 @@ const SCHEMA_SQL = `
|
|
|
1531
2181
|
ON workflow_notifications(target_session_id, delivery_claim_expires_at, created_at)
|
|
1532
2182
|
WHERE delivered_at IS NULL;
|
|
1533
2183
|
|
|
2184
|
+
CREATE TABLE IF NOT EXISTS workflow_turn_intents (
|
|
2185
|
+
intent_id TEXT PRIMARY KEY,
|
|
2186
|
+
source_event_id TEXT NOT NULL,
|
|
2187
|
+
run_id TEXT NOT NULL,
|
|
2188
|
+
workflow_ref TEXT NOT NULL,
|
|
2189
|
+
target_session_id TEXT NOT NULL,
|
|
2190
|
+
cause TEXT NOT NULL CHECK (
|
|
2191
|
+
cause IN ('agentCancelled', 'timedOut', 'failed', 'launchFailed',
|
|
2192
|
+
'controllerInterrupted', 'claimLost')
|
|
2193
|
+
),
|
|
2194
|
+
node_id TEXT,
|
|
2195
|
+
attempt_id TEXT,
|
|
2196
|
+
fallback_facts_json TEXT NOT NULL,
|
|
2197
|
+
requested_at TEXT NOT NULL,
|
|
2198
|
+
eligible_at TEXT,
|
|
2199
|
+
resolved_at TEXT,
|
|
2200
|
+
resolution TEXT CHECK (resolution IN ('workflowPrompt', 'presentation', 'fallback')),
|
|
2201
|
+
resolution_message_id TEXT,
|
|
2202
|
+
delivery_claim_token TEXT,
|
|
2203
|
+
delivery_claim_expires_at INTEGER,
|
|
2204
|
+
CHECK (
|
|
2205
|
+
(resolved_at IS NULL AND resolution IS NULL AND resolution_message_id IS NULL) OR
|
|
2206
|
+
(resolved_at IS NOT NULL AND resolution IS NOT NULL AND resolution_message_id IS NOT NULL)
|
|
2207
|
+
)
|
|
2208
|
+
);
|
|
2209
|
+
CREATE UNIQUE INDEX IF NOT EXISTS workflow_turn_intents_one_pending_run
|
|
2210
|
+
ON workflow_turn_intents(run_id, target_session_id) WHERE resolved_at IS NULL;
|
|
2211
|
+
CREATE INDEX IF NOT EXISTS workflow_turn_intents_pending_run
|
|
2212
|
+
ON workflow_turn_intents(run_id, target_session_id, requested_at)
|
|
2213
|
+
WHERE resolved_at IS NULL;
|
|
2214
|
+
CREATE INDEX IF NOT EXISTS workflow_turn_intents_eligible_session
|
|
2215
|
+
ON workflow_turn_intents(target_session_id, eligible_at, intent_id)
|
|
2216
|
+
WHERE resolved_at IS NULL AND eligible_at IS NOT NULL;
|
|
2217
|
+
|
|
1534
2218
|
CREATE TABLE IF NOT EXISTS effects (
|
|
1535
2219
|
effect_key TEXT NOT NULL,
|
|
1536
2220
|
resource_uid TEXT NOT NULL,
|
|
@@ -1621,7 +2305,56 @@ function workflowFromRow(row: WorkflowRow): ChildWorkflowRecord {
|
|
|
1621
2305
|
};
|
|
1622
2306
|
}
|
|
1623
2307
|
|
|
2308
|
+
function workflowTurnIntentFromRow(row: WorkflowTurnIntentRow): WorkflowTurnIntentRecord {
|
|
2309
|
+
const cause = validateWorkflowTurnIntentCause(row.cause);
|
|
2310
|
+
const resolution =
|
|
2311
|
+
row.resolution === null ? null : validateWorkflowTurnIntentResolution(row.resolution);
|
|
2312
|
+
const fallbackFacts = parseStoredJson<unknown>(
|
|
2313
|
+
row.fallback_facts_json,
|
|
2314
|
+
"workflow turn intent facts",
|
|
2315
|
+
);
|
|
2316
|
+
validateWorkflowTurnIntentFacts(fallbackFacts);
|
|
2317
|
+
validateKey(row.intent_id, "workflow turn intent id");
|
|
2318
|
+
validateKey(row.source_event_id, "workflow turn source event id");
|
|
2319
|
+
validateRunId(row.run_id);
|
|
2320
|
+
validateKey(row.workflow_ref, "workflow turn workflow ref");
|
|
2321
|
+
validateKey(row.target_session_id, "workflow turn target session id");
|
|
2322
|
+
validTimestamp(row.requested_at);
|
|
2323
|
+
if (row.eligible_at !== null) validTimestamp(row.eligible_at);
|
|
2324
|
+
if (row.resolved_at !== null) validTimestamp(row.resolved_at);
|
|
2325
|
+
const allResolutionFieldsNull =
|
|
2326
|
+
row.resolved_at === null && row.resolution === null && row.resolution_message_id === null;
|
|
2327
|
+
const allResolutionFieldsSet =
|
|
2328
|
+
row.resolved_at !== null && row.resolution !== null && row.resolution_message_id !== null;
|
|
2329
|
+
if (!allResolutionFieldsNull && !allResolutionFieldsSet) {
|
|
2330
|
+
throw new Error(`Workflow turn intent ${row.intent_id} has inconsistent resolution fields`);
|
|
2331
|
+
}
|
|
2332
|
+
return {
|
|
2333
|
+
intentId: row.intent_id,
|
|
2334
|
+
sourceEventId: row.source_event_id,
|
|
2335
|
+
runId: row.run_id,
|
|
2336
|
+
workflowRef: row.workflow_ref,
|
|
2337
|
+
targetSessionId: row.target_session_id,
|
|
2338
|
+
cause,
|
|
2339
|
+
nodeId: row.node_id,
|
|
2340
|
+
attemptId: row.attempt_id,
|
|
2341
|
+
fallbackFacts,
|
|
2342
|
+
requestedAt: row.requested_at,
|
|
2343
|
+
eligibleAt: row.eligible_at,
|
|
2344
|
+
resolvedAt: row.resolved_at,
|
|
2345
|
+
resolution,
|
|
2346
|
+
resolutionMessageId: row.resolution_message_id,
|
|
2347
|
+
deliveryClaimExpiresAt:
|
|
2348
|
+
row.delivery_claim_expires_at === null ? null : iso(row.delivery_claim_expires_at),
|
|
2349
|
+
};
|
|
2350
|
+
}
|
|
2351
|
+
|
|
1624
2352
|
function workflowNotificationFromRow(row: WorkflowNotificationRow): WorkflowNotificationRecord {
|
|
2353
|
+
if (row.kind !== "progress" && row.kind !== "final") {
|
|
2354
|
+
throw new Error(
|
|
2355
|
+
"Controller store contains an incompatible pending alpha workflow notification. Preserve needed run bundles, then reset the project controller store.",
|
|
2356
|
+
);
|
|
2357
|
+
}
|
|
1625
2358
|
return {
|
|
1626
2359
|
notificationId: row.notification_id,
|
|
1627
2360
|
runId: row.run_id,
|
|
@@ -1638,12 +2371,102 @@ function workflowNotificationFromRow(row: WorkflowNotificationRow): WorkflowNoti
|
|
|
1638
2371
|
};
|
|
1639
2372
|
}
|
|
1640
2373
|
|
|
2374
|
+
function workflowTurnIntentFactsJson(facts: WorkflowTurnIntentFacts): string {
|
|
2375
|
+
validateWorkflowTurnIntentFacts(facts);
|
|
2376
|
+
const json = canonicalJson(facts, "workflow turn intent facts");
|
|
2377
|
+
validateJsonSize(json, "Workflow turn intent facts", MAX_EVENT_BYTES);
|
|
2378
|
+
return json;
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
function validateWorkflowTurnIntentFacts(facts: unknown): asserts facts is WorkflowTurnIntentFacts {
|
|
2382
|
+
if (facts === null || typeof facts !== "object" || Array.isArray(facts)) {
|
|
2383
|
+
throw new Error("Workflow turn intent facts must be an object");
|
|
2384
|
+
}
|
|
2385
|
+
const candidate = facts as Record<string, unknown>;
|
|
2386
|
+
if (candidate.schema !== TURN_INTENT_FACTS_SCHEMA) {
|
|
2387
|
+
throw new Error(`Workflow turn intent facts schema must be ${TURN_INTENT_FACTS_SCHEMA}`);
|
|
2388
|
+
}
|
|
2389
|
+
const allowedKeys = new Set([
|
|
2390
|
+
"schema",
|
|
2391
|
+
"workflowName",
|
|
2392
|
+
"runId",
|
|
2393
|
+
"observedState",
|
|
2394
|
+
"cause",
|
|
2395
|
+
"nodeId",
|
|
2396
|
+
"attemptId",
|
|
2397
|
+
"reason",
|
|
2398
|
+
"handoff",
|
|
2399
|
+
]);
|
|
2400
|
+
for (const key of Object.keys(candidate)) {
|
|
2401
|
+
if (!allowedKeys.has(key)) throw new Error(`Unknown workflow turn intent facts field: ${key}`);
|
|
2402
|
+
}
|
|
2403
|
+
if (typeof candidate.workflowName !== "string") {
|
|
2404
|
+
throw new Error("Workflow turn intent workflowName must be a string");
|
|
2405
|
+
}
|
|
2406
|
+
if (typeof candidate.runId !== "string") {
|
|
2407
|
+
throw new Error("Workflow turn intent runId must be a string");
|
|
2408
|
+
}
|
|
2409
|
+
if (typeof candidate.observedState !== "string") {
|
|
2410
|
+
throw new Error("Workflow turn intent observedState must be a string");
|
|
2411
|
+
}
|
|
2412
|
+
if (typeof candidate.cause !== "string") {
|
|
2413
|
+
throw new Error("Workflow turn intent cause must be a string");
|
|
2414
|
+
}
|
|
2415
|
+
validateKey(candidate.workflowName, "workflow turn facts workflow name");
|
|
2416
|
+
validateRunId(candidate.runId);
|
|
2417
|
+
validateKey(candidate.observedState, "workflow turn facts observed state");
|
|
2418
|
+
validateWorkflowTurnIntentCause(candidate.cause);
|
|
2419
|
+
if (candidate.nodeId !== null && typeof candidate.nodeId !== "string") {
|
|
2420
|
+
throw new Error("Workflow turn intent nodeId must be a string or null");
|
|
2421
|
+
}
|
|
2422
|
+
if (candidate.attemptId !== null && typeof candidate.attemptId !== "string") {
|
|
2423
|
+
throw new Error("Workflow turn intent attemptId must be a string or null");
|
|
2424
|
+
}
|
|
2425
|
+
if (candidate.reason !== null && typeof candidate.reason !== "string") {
|
|
2426
|
+
throw new Error("Workflow turn intent reason must be a string or null");
|
|
2427
|
+
}
|
|
2428
|
+
if (candidate.nodeId !== null) validateKey(candidate.nodeId, "workflow turn facts node id");
|
|
2429
|
+
if (candidate.attemptId !== null) {
|
|
2430
|
+
validateKey(candidate.attemptId, "workflow turn facts attempt id");
|
|
2431
|
+
}
|
|
2432
|
+
if (candidate.reason !== null && candidate.reason.length > MAX_ERROR_LENGTH) {
|
|
2433
|
+
throw new Error(`Workflow turn intent reason must be at most ${MAX_ERROR_LENGTH} characters`);
|
|
2434
|
+
}
|
|
2435
|
+
if (typeof candidate.handoff !== "boolean") {
|
|
2436
|
+
throw new Error("Workflow turn intent handoff must be a boolean");
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
function validateWorkflowTurnIntentCause(value: string): WorkflowTurnIntentCause {
|
|
2441
|
+
if (
|
|
2442
|
+
value !== "agentCancelled" &&
|
|
2443
|
+
value !== "timedOut" &&
|
|
2444
|
+
value !== "failed" &&
|
|
2445
|
+
value !== "launchFailed" &&
|
|
2446
|
+
value !== "controllerInterrupted" &&
|
|
2447
|
+
value !== "claimLost"
|
|
2448
|
+
) {
|
|
2449
|
+
throw new Error(`Invalid workflow turn intent cause: ${value}`);
|
|
2450
|
+
}
|
|
2451
|
+
return value;
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
function validateWorkflowTurnIntentResolution(value: string): WorkflowTurnIntentResolution {
|
|
2455
|
+
if (value !== "workflowPrompt" && value !== "presentation" && value !== "fallback") {
|
|
2456
|
+
throw new Error(`Invalid workflow turn intent resolution: ${value}`);
|
|
2457
|
+
}
|
|
2458
|
+
return value;
|
|
2459
|
+
}
|
|
2460
|
+
|
|
1641
2461
|
function workflowRunFromRow(row: WorkflowRunQueueRow): WorkflowRunQueueRecord {
|
|
1642
2462
|
return {
|
|
1643
2463
|
runId: row.run_id,
|
|
1644
2464
|
workflowName: row.workflow_ref,
|
|
1645
2465
|
workflowSourceRef: row.workflow_path,
|
|
2466
|
+
workflowSource: parseStoredJson(row.workflow_source_json, "workflow run source"),
|
|
2467
|
+
definitionDigest: row.definition_digest,
|
|
1646
2468
|
input: parseStoredJson(row.input_json, "workflow run input"),
|
|
2469
|
+
launchOptions: parseStoredJson(row.launch_options_json, "workflow launch options"),
|
|
1647
2470
|
status: row.status,
|
|
1648
2471
|
runnerId: row.runner_id,
|
|
1649
2472
|
claimToken: row.claim_token,
|
|
@@ -1651,8 +2474,12 @@ function workflowRunFromRow(row: WorkflowRunQueueRow): WorkflowRunQueueRecord {
|
|
|
1651
2474
|
affinityRunnerId: row.affinity_runner_id,
|
|
1652
2475
|
originSessionId: row.origin_session_id,
|
|
1653
2476
|
parentRunId: row.parent_run_id,
|
|
2477
|
+
errorCode: row.error_code,
|
|
2478
|
+
errorMessage: row.error_message,
|
|
1654
2479
|
createdAt: row.created_at,
|
|
1655
2480
|
updatedAt: row.updated_at,
|
|
2481
|
+
startedAt: row.started_at,
|
|
2482
|
+
finishedAt: row.finished_at,
|
|
1656
2483
|
};
|
|
1657
2484
|
}
|
|
1658
2485
|
|