@smithers-orchestrator/engine 0.16.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/LICENSE +21 -0
- package/package.json +50 -0
- package/src/AlertHumanRequestOptions.ts +8 -0
- package/src/AlertRuntimeServices.ts +10 -0
- package/src/ChildWorkflowDefinition.ts +5 -0
- package/src/ChildWorkflowExecuteOptions.ts +14 -0
- package/src/ContinuationRequest.ts +3 -0
- package/src/HijackState.ts +19 -0
- package/src/HumanRequestKind.ts +1 -0
- package/src/HumanRequestStatus.ts +1 -0
- package/src/PlanNode.ts +29 -0
- package/src/RalphMeta.ts +7 -0
- package/src/RalphState.ts +4 -0
- package/src/RalphStateMap.ts +3 -0
- package/src/ScheduleResult.ts +15 -0
- package/src/SignalRunOptions.ts +5 -0
- package/src/alert-runtime.js +22 -0
- package/src/approvals.js +220 -0
- package/src/child-workflow.js +163 -0
- package/src/effect/ApprovalDeferredResolution.ts +13 -0
- package/src/effect/ApprovalDurableDeferredResolution.ts +11 -0
- package/src/effect/ApprovalPayload.ts +7 -0
- package/src/effect/ApprovalResult.ts +6 -0
- package/src/effect/BuilderNode.ts +52 -0
- package/src/effect/BuilderStepHandle.ts +47 -0
- package/src/effect/CancelPayload.ts +3 -0
- package/src/effect/CancelResult.ts +4 -0
- package/src/effect/DeferredResolution.ts +7 -0
- package/src/effect/DiffBundle.ts +7 -0
- package/src/effect/ExecuteTaskActivityOptions.ts +7 -0
- package/src/effect/FilePatch.ts +6 -0
- package/src/effect/GetRunPayload.ts +3 -0
- package/src/effect/GetRunResult.ts +3 -0
- package/src/effect/LegacyExecuteTaskFn.ts +24 -0
- package/src/effect/ListRunsPayload.ts +6 -0
- package/src/effect/RunStatusSchema.ts +9 -0
- package/src/effect/RunSummary.ts +23 -0
- package/src/effect/SignalPayload.ts +7 -0
- package/src/effect/SignalResult.ts +6 -0
- package/src/effect/SmithersSqliteOptions.ts +3 -0
- package/src/effect/SqlMessageStorageEventHistoryQuery.ts +7 -0
- package/src/effect/TaggedWorkerError.ts +46 -0
- package/src/effect/TaskActivityContext.ts +4 -0
- package/src/effect/TaskActivityRetryOptions.ts +4 -0
- package/src/effect/TaskBridgeToolConfig.ts +6 -0
- package/src/effect/TaskFailure.ts +3 -0
- package/src/effect/TaskResult.ts +5 -0
- package/src/effect/UnknownWorkerError.ts +5 -0
- package/src/effect/WaitForEventDurableDeferredResolution.ts +11 -0
- package/src/effect/WorkerDispatchKind.ts +1 -0
- package/src/effect/WorkerTask.ts +14 -0
- package/src/effect/WorkerTaskError.ts +4 -0
- package/src/effect/WorkerTaskKind.ts +1 -0
- package/src/effect/WorkflowPatchDecisionRecord.ts +4 -0
- package/src/effect/WorkflowPatchDecisions.ts +1 -0
- package/src/effect/WorkflowVersioningRuntime.ts +7 -0
- package/src/effect/activity-bridge.js +131 -0
- package/src/effect/bridge-utils.js +45 -0
- package/src/effect/builder.js +837 -0
- package/src/effect/compute-task-bridge.js +734 -0
- package/src/effect/deferred-bridge.js +63 -0
- package/src/effect/deferred-state-bridge.js +1343 -0
- package/src/effect/diff-bundle.js +352 -0
- package/src/effect/durable-deferred-bridge.js +282 -0
- package/src/effect/entity-worker.js +154 -0
- package/src/effect/http-runner.js +86 -0
- package/src/effect/rpc-schema.js +101 -0
- package/src/effect/single-runner.js +189 -0
- package/src/effect/sql-message-storage.js +817 -0
- package/src/effect/static-task-bridge.js +308 -0
- package/src/effect/versioning.js +123 -0
- package/src/effect/workflow-bridge.js +260 -0
- package/src/effect/workflow-make-bridge.js +233 -0
- package/src/engine.js +6933 -0
- package/src/events.js +237 -0
- package/src/external/json-schema-to-zod.js +214 -0
- package/src/getDefinedToolMetadata.js +10 -0
- package/src/hot/HotReloadEvent.ts +21 -0
- package/src/hot/HotWorkflowController.js +220 -0
- package/src/hot/OverlayOptions.ts +4 -0
- package/src/hot/WatchTreeOptions.ts +6 -0
- package/src/hot/index.js +9 -0
- package/src/hot/overlay.js +177 -0
- package/src/hot/watch.js +174 -0
- package/src/human-requests.js +120 -0
- package/src/index.d.ts +1597 -0
- package/src/index.js +41 -0
- package/src/runtime-owner.js +36 -0
- package/src/scheduler.js +31 -0
- package/src/signals.js +82 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { Effect, Metric } from "effect";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { buildOutputRow, stripAutoColumns, validateOutput } from "@smithers-orchestrator/db/output";
|
|
4
|
+
import { EventBus } from "../events.js";
|
|
5
|
+
import { makeAbortError, wireAbortSignal } from "./bridge-utils.js";
|
|
6
|
+
import { logDebug, logError, logInfo } from "@smithers-orchestrator/observability/logging";
|
|
7
|
+
import { attemptDuration, nodeDuration } from "@smithers-orchestrator/observability/metrics";
|
|
8
|
+
import { errorToJson } from "@smithers-orchestrator/errors/errorToJson";
|
|
9
|
+
import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
|
|
10
|
+
import { nowMs } from "@smithers-orchestrator/scheduler/nowMs";
|
|
11
|
+
import { getJjPointer } from "@smithers-orchestrator/vcs/jj";
|
|
12
|
+
import * as BunContext from "@effect/platform-bun/BunContext";
|
|
13
|
+
/** @typedef {import("@smithers-orchestrator/db/adapter").SmithersDb} _SmithersDb */
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {{ rootDir: string; }} StaticTaskBridgeToolConfig
|
|
16
|
+
*/
|
|
17
|
+
/** @typedef {import("@smithers-orchestrator/graph/TaskDescriptor").TaskDescriptor} _TaskDescriptor */
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {unknown} err
|
|
21
|
+
* @returns {boolean}
|
|
22
|
+
*/
|
|
23
|
+
function isAbortError(err) {
|
|
24
|
+
if (!err)
|
|
25
|
+
return false;
|
|
26
|
+
if (err.name === "AbortError")
|
|
27
|
+
return true;
|
|
28
|
+
if (typeof DOMException !== "undefined" &&
|
|
29
|
+
err instanceof DOMException &&
|
|
30
|
+
err.name === "AbortError") {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
if (err instanceof Error) {
|
|
34
|
+
return /aborted|abort/i.test(err.message);
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* @param {_TaskDescriptor} desc
|
|
40
|
+
* @param {boolean} cacheEnabled
|
|
41
|
+
* @returns {boolean}
|
|
42
|
+
*/
|
|
43
|
+
export const canExecuteBridgeManagedStaticTask = (desc, cacheEnabled) => {
|
|
44
|
+
if (cacheEnabled || desc.cachePolicy) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
if (desc.agent || desc.computeFn || desc.staticPayload === undefined) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
if (desc.worktreePath) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
return !desc.scorers || Object.keys(desc.scorers).length === 0;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* @param {_SmithersDb} adapter
|
|
57
|
+
* @param {string} runId
|
|
58
|
+
* @param {_TaskDescriptor} desc
|
|
59
|
+
* @param {EventBus} eventBus
|
|
60
|
+
* @param {StaticTaskBridgeToolConfig} toolConfig
|
|
61
|
+
* @param {string} workflowName
|
|
62
|
+
* @param {AbortSignal} [signal]
|
|
63
|
+
* @returns {Promise<void>}
|
|
64
|
+
*/
|
|
65
|
+
export const executeStaticTaskBridge = async (adapter, runId, desc, eventBus, toolConfig, workflowName, signal) => {
|
|
66
|
+
const taskStartMs = performance.now();
|
|
67
|
+
const attempts = await Effect.runPromise(adapter.listAttempts(runId, desc.nodeId, desc.iteration));
|
|
68
|
+
const attemptNo = (attempts[0]?.attempt ?? 0) + 1;
|
|
69
|
+
const taskAbortController = new AbortController();
|
|
70
|
+
const removeAbortForwarder = wireAbortSignal(taskAbortController, signal);
|
|
71
|
+
const taskSignal = taskAbortController.signal;
|
|
72
|
+
const startedAtMs = nowMs();
|
|
73
|
+
const attemptMeta = {
|
|
74
|
+
kind: "static",
|
|
75
|
+
prompt: desc.prompt ?? null,
|
|
76
|
+
staticPayload: desc.staticPayload ?? null,
|
|
77
|
+
label: desc.label ?? null,
|
|
78
|
+
outputTable: desc.outputTableName,
|
|
79
|
+
needsApproval: desc.needsApproval,
|
|
80
|
+
retries: desc.retries,
|
|
81
|
+
timeoutMs: desc.timeoutMs,
|
|
82
|
+
heartbeatTimeoutMs: desc.heartbeatTimeoutMs,
|
|
83
|
+
lastHeartbeat: null,
|
|
84
|
+
agentId: null,
|
|
85
|
+
agentModel: null,
|
|
86
|
+
agentEngine: null,
|
|
87
|
+
agentResume: null,
|
|
88
|
+
agentConversation: null,
|
|
89
|
+
resumedFromSession: null,
|
|
90
|
+
resumedFromConversation: false,
|
|
91
|
+
hijackHandoff: null,
|
|
92
|
+
};
|
|
93
|
+
await adapter.withTransaction("task-start", Effect.gen(function* () {
|
|
94
|
+
yield* adapter.insertAttempt({
|
|
95
|
+
runId,
|
|
96
|
+
nodeId: desc.nodeId,
|
|
97
|
+
iteration: desc.iteration,
|
|
98
|
+
attempt: attemptNo,
|
|
99
|
+
state: "in-progress",
|
|
100
|
+
startedAtMs,
|
|
101
|
+
finishedAtMs: null,
|
|
102
|
+
heartbeatAtMs: null,
|
|
103
|
+
heartbeatDataJson: null,
|
|
104
|
+
errorJson: null,
|
|
105
|
+
jjPointer: null,
|
|
106
|
+
jjCwd: toolConfig.rootDir,
|
|
107
|
+
cached: false,
|
|
108
|
+
metaJson: JSON.stringify(attemptMeta),
|
|
109
|
+
});
|
|
110
|
+
yield* adapter.insertNode({
|
|
111
|
+
runId,
|
|
112
|
+
nodeId: desc.nodeId,
|
|
113
|
+
iteration: desc.iteration,
|
|
114
|
+
state: "in-progress",
|
|
115
|
+
lastAttempt: attemptNo,
|
|
116
|
+
updatedAtMs: nowMs(),
|
|
117
|
+
outputTable: desc.outputTableName,
|
|
118
|
+
label: desc.label ?? null,
|
|
119
|
+
});
|
|
120
|
+
}));
|
|
121
|
+
await Effect.runPromise(eventBus.emitEventWithPersist({
|
|
122
|
+
type: "NodeStarted",
|
|
123
|
+
runId,
|
|
124
|
+
nodeId: desc.nodeId,
|
|
125
|
+
iteration: desc.iteration,
|
|
126
|
+
attempt: attemptNo,
|
|
127
|
+
timestampMs: nowMs(),
|
|
128
|
+
}));
|
|
129
|
+
try {
|
|
130
|
+
if (taskSignal.aborted) {
|
|
131
|
+
throw taskSignal.reason ?? makeAbortError();
|
|
132
|
+
}
|
|
133
|
+
logDebug("bridge-managed static task execution starting", {
|
|
134
|
+
runId,
|
|
135
|
+
nodeId: desc.nodeId,
|
|
136
|
+
iteration: desc.iteration,
|
|
137
|
+
attempt: attemptNo,
|
|
138
|
+
workflowName,
|
|
139
|
+
}, "engine:task");
|
|
140
|
+
let payload = stripAutoColumns(desc.staticPayload);
|
|
141
|
+
const payloadWithKeys = buildOutputRow(desc.outputTable, runId, desc.nodeId, desc.iteration, payload);
|
|
142
|
+
let validation = validateOutput(desc.outputTable, payloadWithKeys);
|
|
143
|
+
if (validation.ok && desc.outputSchema) {
|
|
144
|
+
const zodResult = desc.outputSchema.safeParse(payload);
|
|
145
|
+
if (!zodResult.success) {
|
|
146
|
+
validation = { ok: false, error: zodResult.error };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (!validation.ok) {
|
|
150
|
+
attemptMeta.failureRetryable = false;
|
|
151
|
+
throw new SmithersError("INVALID_OUTPUT", `Task output failed validation for ${desc.outputTableName}`, {
|
|
152
|
+
attempt: attemptNo,
|
|
153
|
+
nodeId: desc.nodeId,
|
|
154
|
+
iteration: desc.iteration,
|
|
155
|
+
outputTable: desc.outputTableName,
|
|
156
|
+
issues: validation.error?.issues,
|
|
157
|
+
}, { cause: validation.error });
|
|
158
|
+
}
|
|
159
|
+
payload = validation.data;
|
|
160
|
+
const completedAtMs = nowMs();
|
|
161
|
+
const jjPointer = await Effect.runPromise(getJjPointer(toolConfig.rootDir).pipe(Effect.provide(BunContext.layer)));
|
|
162
|
+
await adapter.withTransaction("task-completion", Effect.gen(function* () {
|
|
163
|
+
yield* adapter.upsertOutputRow(desc.outputTable, { runId, nodeId: desc.nodeId, iteration: desc.iteration }, payload);
|
|
164
|
+
yield* adapter.updateAttempt(runId, desc.nodeId, desc.iteration, attemptNo, {
|
|
165
|
+
state: "finished",
|
|
166
|
+
finishedAtMs: completedAtMs,
|
|
167
|
+
jjPointer,
|
|
168
|
+
cached: false,
|
|
169
|
+
metaJson: JSON.stringify(attemptMeta),
|
|
170
|
+
responseText: null,
|
|
171
|
+
});
|
|
172
|
+
yield* adapter.insertNode({
|
|
173
|
+
runId,
|
|
174
|
+
nodeId: desc.nodeId,
|
|
175
|
+
iteration: desc.iteration,
|
|
176
|
+
state: "finished",
|
|
177
|
+
lastAttempt: attemptNo,
|
|
178
|
+
updatedAtMs: completedAtMs,
|
|
179
|
+
outputTable: desc.outputTableName,
|
|
180
|
+
label: desc.label ?? null,
|
|
181
|
+
});
|
|
182
|
+
}));
|
|
183
|
+
await Effect.runPromise(eventBus.emitEventWithPersist({
|
|
184
|
+
type: "NodeFinished",
|
|
185
|
+
runId,
|
|
186
|
+
nodeId: desc.nodeId,
|
|
187
|
+
iteration: desc.iteration,
|
|
188
|
+
attempt: attemptNo,
|
|
189
|
+
timestampMs: nowMs(),
|
|
190
|
+
}));
|
|
191
|
+
const taskElapsedMs = performance.now() - taskStartMs;
|
|
192
|
+
void Effect.runPromise(Effect.all([
|
|
193
|
+
Metric.update(nodeDuration, taskElapsedMs),
|
|
194
|
+
Metric.update(attemptDuration, taskElapsedMs),
|
|
195
|
+
], { discard: true }));
|
|
196
|
+
logInfo("bridge-managed static task execution finished", {
|
|
197
|
+
runId,
|
|
198
|
+
nodeId: desc.nodeId,
|
|
199
|
+
iteration: desc.iteration,
|
|
200
|
+
attempt: attemptNo,
|
|
201
|
+
durationMs: Math.round(taskElapsedMs),
|
|
202
|
+
jjPointer,
|
|
203
|
+
}, "engine:task");
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
const aborted = taskSignal.aborted || isAbortError(err);
|
|
207
|
+
const effectiveError = aborted && taskSignal.reason !== undefined
|
|
208
|
+
? taskSignal.reason
|
|
209
|
+
: aborted
|
|
210
|
+
? makeAbortError()
|
|
211
|
+
: err;
|
|
212
|
+
if (aborted) {
|
|
213
|
+
const cancelledAtMs = nowMs();
|
|
214
|
+
await adapter.withTransaction("task-cancel", Effect.gen(function* () {
|
|
215
|
+
yield* adapter.updateAttempt(runId, desc.nodeId, desc.iteration, attemptNo, {
|
|
216
|
+
state: "cancelled",
|
|
217
|
+
finishedAtMs: cancelledAtMs,
|
|
218
|
+
errorJson: JSON.stringify(errorToJson(effectiveError)),
|
|
219
|
+
metaJson: JSON.stringify(attemptMeta),
|
|
220
|
+
responseText: null,
|
|
221
|
+
});
|
|
222
|
+
yield* adapter.insertNode({
|
|
223
|
+
runId,
|
|
224
|
+
nodeId: desc.nodeId,
|
|
225
|
+
iteration: desc.iteration,
|
|
226
|
+
state: "cancelled",
|
|
227
|
+
lastAttempt: attemptNo,
|
|
228
|
+
updatedAtMs: cancelledAtMs,
|
|
229
|
+
outputTable: desc.outputTableName,
|
|
230
|
+
label: desc.label ?? null,
|
|
231
|
+
});
|
|
232
|
+
}));
|
|
233
|
+
await Effect.runPromise(eventBus.emitEventWithPersist({
|
|
234
|
+
type: "NodeCancelled",
|
|
235
|
+
runId,
|
|
236
|
+
nodeId: desc.nodeId,
|
|
237
|
+
iteration: desc.iteration,
|
|
238
|
+
attempt: attemptNo,
|
|
239
|
+
reason: "aborted",
|
|
240
|
+
timestampMs: nowMs(),
|
|
241
|
+
}));
|
|
242
|
+
logInfo("bridge-managed static task execution cancelled", {
|
|
243
|
+
runId,
|
|
244
|
+
nodeId: desc.nodeId,
|
|
245
|
+
iteration: desc.iteration,
|
|
246
|
+
attempt: attemptNo,
|
|
247
|
+
error: effectiveError instanceof Error
|
|
248
|
+
? effectiveError.message
|
|
249
|
+
: String(effectiveError),
|
|
250
|
+
}, "engine:task");
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
logError("bridge-managed static task execution failed", {
|
|
254
|
+
runId,
|
|
255
|
+
nodeId: desc.nodeId,
|
|
256
|
+
iteration: desc.iteration,
|
|
257
|
+
attempt: attemptNo,
|
|
258
|
+
maxAttempts: Number.isFinite(desc.retries) ? desc.retries + 1 : "infinite",
|
|
259
|
+
error: effectiveError instanceof Error
|
|
260
|
+
? effectiveError.message
|
|
261
|
+
: String(effectiveError),
|
|
262
|
+
}, "engine:task");
|
|
263
|
+
const failedAtMs = nowMs();
|
|
264
|
+
await adapter.withTransaction("task-fail", Effect.gen(function* () {
|
|
265
|
+
yield* adapter.updateAttempt(runId, desc.nodeId, desc.iteration, attemptNo, {
|
|
266
|
+
state: "failed",
|
|
267
|
+
finishedAtMs: failedAtMs,
|
|
268
|
+
errorJson: JSON.stringify(errorToJson(effectiveError)),
|
|
269
|
+
metaJson: JSON.stringify(attemptMeta),
|
|
270
|
+
responseText: null,
|
|
271
|
+
});
|
|
272
|
+
yield* adapter.insertNode({
|
|
273
|
+
runId,
|
|
274
|
+
nodeId: desc.nodeId,
|
|
275
|
+
iteration: desc.iteration,
|
|
276
|
+
state: "failed",
|
|
277
|
+
lastAttempt: attemptNo,
|
|
278
|
+
updatedAtMs: failedAtMs,
|
|
279
|
+
outputTable: desc.outputTableName,
|
|
280
|
+
label: desc.label ?? null,
|
|
281
|
+
});
|
|
282
|
+
}));
|
|
283
|
+
await Effect.runPromise(eventBus.emitEventWithPersist({
|
|
284
|
+
type: "NodeFailed",
|
|
285
|
+
runId,
|
|
286
|
+
nodeId: desc.nodeId,
|
|
287
|
+
iteration: desc.iteration,
|
|
288
|
+
attempt: attemptNo,
|
|
289
|
+
error: errorToJson(effectiveError),
|
|
290
|
+
timestampMs: nowMs(),
|
|
291
|
+
}));
|
|
292
|
+
const updatedAttempts = await Effect.runPromise(adapter.listAttempts(runId, desc.nodeId, desc.iteration));
|
|
293
|
+
const failedAttempts = updatedAttempts.filter((attempt) => attempt.state === "failed");
|
|
294
|
+
if (attemptMeta.failureRetryable !== false && failedAttempts.length <= desc.retries) {
|
|
295
|
+
await Effect.runPromise(eventBus.emitEventWithPersist({
|
|
296
|
+
type: "NodeRetrying",
|
|
297
|
+
runId,
|
|
298
|
+
nodeId: desc.nodeId,
|
|
299
|
+
iteration: desc.iteration,
|
|
300
|
+
attempt: attemptNo + 1,
|
|
301
|
+
timestampMs: nowMs(),
|
|
302
|
+
}));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
finally {
|
|
306
|
+
removeAbortForwarder();
|
|
307
|
+
}
|
|
308
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import React from "react";
|
|
3
|
+
/** @typedef {import("./WorkflowPatchDecisionRecord.ts").WorkflowPatchDecisionRecord} WorkflowPatchDecisionRecord */
|
|
4
|
+
/** @typedef {import("./WorkflowPatchDecisions.ts").WorkflowPatchDecisions} WorkflowPatchDecisions */
|
|
5
|
+
/** @typedef {import("./WorkflowVersioningRuntime.ts").WorkflowVersioningRuntime} WorkflowVersioningRuntime */
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {{ baseConfig: Record<string, unknown>; initialDecisions?: WorkflowPatchDecisions; isNewRun: boolean; persist: (config: Record<string, unknown>) => Promise<void>; recordDecision?: (record: WorkflowPatchDecisionRecord) => Promise<void>; }} WorkflowVersioningRuntimeOptions
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const storage = new AsyncLocalStorage();
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} value
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
function normalizePatchId(value) {
|
|
16
|
+
return value.trim();
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* @param {unknown} value
|
|
20
|
+
* @returns {WorkflowPatchDecisions}
|
|
21
|
+
*/
|
|
22
|
+
function normalizePatchDecisions(value) {
|
|
23
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
const decisions = {};
|
|
27
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
28
|
+
const patchId = normalizePatchId(String(key));
|
|
29
|
+
if (!patchId)
|
|
30
|
+
continue;
|
|
31
|
+
if (typeof entry === "boolean") {
|
|
32
|
+
decisions[patchId] = entry;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return decisions;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* @param {WorkflowVersioningRuntimeOptions} options
|
|
39
|
+
* @returns {WorkflowVersioningRuntime}
|
|
40
|
+
*/
|
|
41
|
+
export function createWorkflowVersioningRuntime(options) {
|
|
42
|
+
const decisions = new Map(Object.entries(normalizePatchDecisions(options.initialDecisions)));
|
|
43
|
+
let currentConfig = { ...options.baseConfig };
|
|
44
|
+
let dirty = false;
|
|
45
|
+
const pendingRecords = [];
|
|
46
|
+
return {
|
|
47
|
+
/**
|
|
48
|
+
* @param {string} patchId
|
|
49
|
+
* @returns {boolean}
|
|
50
|
+
*/
|
|
51
|
+
resolve(patchId) {
|
|
52
|
+
const normalized = normalizePatchId(patchId);
|
|
53
|
+
if (!normalized) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
const existing = decisions.get(normalized);
|
|
57
|
+
if (typeof existing === "boolean") {
|
|
58
|
+
return existing;
|
|
59
|
+
}
|
|
60
|
+
const decision = options.isNewRun;
|
|
61
|
+
decisions.set(normalized, decision);
|
|
62
|
+
dirty = true;
|
|
63
|
+
pendingRecords.push({ patchId: normalized, decision });
|
|
64
|
+
return decision;
|
|
65
|
+
},
|
|
66
|
+
async flush() {
|
|
67
|
+
if (!dirty && pendingRecords.length === 0) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const nextConfig = dirty
|
|
71
|
+
? {
|
|
72
|
+
...currentConfig,
|
|
73
|
+
workflowPatches: Object.fromEntries(decisions.entries()),
|
|
74
|
+
}
|
|
75
|
+
: currentConfig;
|
|
76
|
+
if (dirty) {
|
|
77
|
+
await options.persist(nextConfig);
|
|
78
|
+
currentConfig = nextConfig;
|
|
79
|
+
dirty = false;
|
|
80
|
+
}
|
|
81
|
+
if (pendingRecords.length > 0 && options.recordDecision) {
|
|
82
|
+
const records = pendingRecords.slice();
|
|
83
|
+
for (const record of records) {
|
|
84
|
+
await options.recordDecision(record);
|
|
85
|
+
}
|
|
86
|
+
pendingRecords.splice(0, records.length);
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
snapshot() {
|
|
90
|
+
return Object.fromEntries(decisions.entries());
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* @template T
|
|
96
|
+
* @param {WorkflowVersioningRuntime} runtime
|
|
97
|
+
* @param {() => T} execute
|
|
98
|
+
* @returns {T}
|
|
99
|
+
*/
|
|
100
|
+
export function withWorkflowVersioningRuntime(runtime, execute) {
|
|
101
|
+
return storage.run(runtime, execute);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* @returns {| WorkflowVersioningRuntime | undefined}
|
|
105
|
+
*/
|
|
106
|
+
export function getWorkflowVersioningRuntime() {
|
|
107
|
+
return storage.getStore();
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* @param {Record<string, unknown> | null | undefined} config
|
|
111
|
+
* @returns {WorkflowPatchDecisions}
|
|
112
|
+
*/
|
|
113
|
+
export function getWorkflowPatchDecisions(config) {
|
|
114
|
+
return normalizePatchDecisions(config?.workflowPatches);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* @param {string} patchId
|
|
118
|
+
* @returns {boolean}
|
|
119
|
+
*/
|
|
120
|
+
export function usePatched(patchId) {
|
|
121
|
+
const runtime = getWorkflowVersioningRuntime();
|
|
122
|
+
return React.useMemo(() => runtime?.resolve(patchId) ?? false, [runtime, patchId]);
|
|
123
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { SmithersDb } from "@smithers-orchestrator/db/adapter";
|
|
3
|
+
import { EventBus } from "../events.js";
|
|
4
|
+
import { toSmithersError } from "@smithers-orchestrator/errors/toSmithersError";
|
|
5
|
+
import { makeWorkerTask, } from "./entity-worker.js";
|
|
6
|
+
import { executeTaskActivity, makeTaskBridgeKey, RetriableTaskFailure, } from "./activity-bridge.js";
|
|
7
|
+
import { parseAttemptMetaJson } from "./bridge-utils.js";
|
|
8
|
+
import { canExecuteBridgeManagedComputeTask, executeComputeTaskBridge, } from "./compute-task-bridge.js";
|
|
9
|
+
import { canExecuteBridgeManagedStaticTask, executeStaticTaskBridge, } from "./static-task-bridge.js";
|
|
10
|
+
import { dispatchWorkerTask } from "./single-runner.js";
|
|
11
|
+
/** @typedef {import("../HijackState.ts").HijackState} HijackState */
|
|
12
|
+
/** @typedef {import("./LegacyExecuteTaskFn.ts").LegacyExecuteTaskFn} LegacyExecuteTaskFn */
|
|
13
|
+
/** @typedef {import("./TaskBridgeToolConfig.ts").TaskBridgeToolConfig} TaskBridgeToolConfig */
|
|
14
|
+
/** @typedef {import("@smithers-orchestrator/graph/TaskDescriptor").TaskDescriptor} _TaskDescriptor */
|
|
15
|
+
/** @typedef {import("./TaskActivityContext.ts").TaskActivityContext} _TaskActivityContext */
|
|
16
|
+
/** @typedef {import("drizzle-orm/bun-sqlite").BunSQLiteDatabase<Record<string, unknown>>} _BunSQLiteDatabase */
|
|
17
|
+
/** @typedef {import("drizzle-orm/sqlite-core").SQLiteTable} SQLiteTable */
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {"compute" | "static" | "legacy"} BridgeManagedTaskKind
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export { bridgeApprovalResolve, bridgeSignalResolve, bridgeWaitForEventResolve, awaitApprovalDurableDeferred, awaitWaitForEventDurableDeferred, makeApprovalDurableDeferred, makeDurableDeferredBridgeExecutionId, makeWaitForEventDurableDeferred, } from "./durable-deferred-bridge.js";
|
|
23
|
+
export { cancelPendingTimersBridge, isBridgeManagedTimerTask, isBridgeManagedWaitForEventTask, resolveDeferredTaskStateBridge, } from "./deferred-state-bridge.js";
|
|
24
|
+
export { createSchedulerWakeQueue, getWorkflowMakeBridgeRuntime, runWorkflowWithMakeBridge, withWorkflowMakeBridgeRuntime, } from "./workflow-make-bridge.js";
|
|
25
|
+
export { SqlMessageStorage, ensureSqlMessageStorage, ensureSqlMessageStorageEffect, getSqlMessageStorage, } from "./sql-message-storage.js";
|
|
26
|
+
export { SandboxEntity, SandboxEntityExecutor, makeSandboxEntityId, makeSandboxTransportServiceEffect, } from "@smithers-orchestrator/sandbox/effect/sandbox-entity";
|
|
27
|
+
export { CodeplaneSandboxExecutorLive, DockerSandboxExecutorLive, SandboxHttpRunner, } from "./http-runner.js";
|
|
28
|
+
export { BubblewrapSandboxExecutorLive, SandboxSocketRunner, } from "@smithers-orchestrator/sandbox/effect/socket-runner";
|
|
29
|
+
export { isTaskResultFailure, makeWorkerTask, TaskResult, WorkerDispatchKind, WorkerTask, WorkerTaskKind, TaskWorkerEntity, } from "./entity-worker.js";
|
|
30
|
+
export { dispatchWorkerTask, subscribeTaskWorkerDispatches, } from "./single-runner.js";
|
|
31
|
+
/**
|
|
32
|
+
* Phase 0 Seam Adapter
|
|
33
|
+
*
|
|
34
|
+
* This file establishes the interface boundaries for bridging the legacy Smithers engine
|
|
35
|
+
* with the Effect ecosystem.
|
|
36
|
+
*
|
|
37
|
+
* Currently, it delegates to the legacy implementations exactly as they are.
|
|
38
|
+
* In Phase 1, `executeTaskBridge` will be replaced by `Activity.make()`.
|
|
39
|
+
* In subsequent phases, other engine boundaries will be modeled as Workflows.
|
|
40
|
+
*/
|
|
41
|
+
const inflightTaskExecutions = new Map();
|
|
42
|
+
const completedTaskExecutions = new Map();
|
|
43
|
+
/**
|
|
44
|
+
* @template A
|
|
45
|
+
* @param {Effect.Effect<A, unknown, never> | PromiseLike<A> | A} value
|
|
46
|
+
* @returns {Promise<A>}
|
|
47
|
+
*/
|
|
48
|
+
const runEffectOrPromise = async (value) => {
|
|
49
|
+
if (Effect.isEffect?.(value)) {
|
|
50
|
+
return Effect.runPromise(value);
|
|
51
|
+
}
|
|
52
|
+
return await value;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* @param {string | null} [errorJson]
|
|
56
|
+
* @returns {string | null}
|
|
57
|
+
*/
|
|
58
|
+
function parseAttemptErrorCode(errorJson) {
|
|
59
|
+
if (!errorJson)
|
|
60
|
+
return null;
|
|
61
|
+
try {
|
|
62
|
+
const parsed = JSON.parse(errorJson);
|
|
63
|
+
return typeof parsed?.code === "string" ? parsed.code : null;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* @param {{ errorJson?: string | null; metaJson?: string | null } | null} [attempt]
|
|
71
|
+
*/
|
|
72
|
+
function isRetryableBridgeTaskFailure(attempt) {
|
|
73
|
+
const meta = parseAttemptMetaJson(attempt?.metaJson);
|
|
74
|
+
if (meta?.failureRetryable === false) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
const kind = typeof meta?.kind === "string" ? meta.kind : null;
|
|
78
|
+
return !(kind !== "agent" && parseAttemptErrorCode(attempt?.errorJson) === "INVALID_OUTPUT");
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* @param {SmithersDb} adapter
|
|
82
|
+
* @param {string} runId
|
|
83
|
+
* @param {_TaskDescriptor} desc
|
|
84
|
+
* @param {_TaskActivityContext} context
|
|
85
|
+
*/
|
|
86
|
+
const classifyTaskAttempt = async (adapter, runId, desc, context) => {
|
|
87
|
+
const attempts = await runEffectOrPromise(adapter.listAttempts(runId, desc.nodeId, desc.iteration));
|
|
88
|
+
const latest = attempts[0];
|
|
89
|
+
const latestAttempt = latest?.attempt ?? context.attempt;
|
|
90
|
+
const latestState = latest?.state ?? null;
|
|
91
|
+
if (latestState === "failed") {
|
|
92
|
+
const failedAttempts = attempts.filter((attempt) => attempt.state === "failed");
|
|
93
|
+
const hasNonRetryableFailure = failedAttempts.some((attempt) => !isRetryableBridgeTaskFailure(attempt));
|
|
94
|
+
if (!hasNonRetryableFailure && failedAttempts.length <= desc.retries) {
|
|
95
|
+
throw new RetriableTaskFailure(desc.nodeId, latestAttempt);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
state: latestState,
|
|
100
|
+
attempt: latestAttempt,
|
|
101
|
+
idempotencyKey: context.idempotencyKey,
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* @param {SmithersDb} adapter
|
|
106
|
+
* @param {string} runId
|
|
107
|
+
* @param {_TaskDescriptor} desc
|
|
108
|
+
*/
|
|
109
|
+
const getNextTaskActivityAttempt = async (adapter, runId, desc) => {
|
|
110
|
+
const attempts = await runEffectOrPromise(adapter.listAttempts(runId, desc.nodeId, desc.iteration));
|
|
111
|
+
const latestAttempt = attempts[0]?.attempt ?? 0;
|
|
112
|
+
return latestAttempt + 1;
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* @param {SmithersDb} adapter
|
|
116
|
+
* @param {_BunSQLiteDatabase} db
|
|
117
|
+
* @param {string} runId
|
|
118
|
+
* @param {_TaskDescriptor} desc
|
|
119
|
+
* @param {Map<string, _TaskDescriptor>} descriptorMap
|
|
120
|
+
* @param {SQLiteTable} inputTable
|
|
121
|
+
* @param {EventBus} eventBus
|
|
122
|
+
* @param {TaskBridgeToolConfig} toolConfig
|
|
123
|
+
* @param {string} workflowName
|
|
124
|
+
* @param {boolean} cacheEnabled
|
|
125
|
+
* @param {BridgeManagedTaskKind} bridgeManagedExecution
|
|
126
|
+
* @param {_TaskActivityContext} context
|
|
127
|
+
* @param {AbortSignal} [signal]
|
|
128
|
+
* @param {Set<string>} [disabledAgents]
|
|
129
|
+
* @param {AbortController} [runAbortController]
|
|
130
|
+
* @param {HijackState} [hijackState]
|
|
131
|
+
* @param {LegacyExecuteTaskFn} [legacyExecuteTaskFn]
|
|
132
|
+
*/
|
|
133
|
+
const executeBridgeAttempt = async (adapter, db, runId, desc, descriptorMap, inputTable, eventBus, toolConfig, workflowName, cacheEnabled, bridgeManagedExecution, context, signal, disabledAgents, runAbortController, hijackState, legacyExecuteTaskFn) => {
|
|
134
|
+
if (bridgeManagedExecution === "static") {
|
|
135
|
+
await executeStaticTaskBridge(adapter, runId, desc, eventBus, toolConfig, workflowName, signal);
|
|
136
|
+
}
|
|
137
|
+
else if (bridgeManagedExecution === "compute") {
|
|
138
|
+
await executeComputeTaskBridge(adapter, db, runId, desc, eventBus, toolConfig, workflowName, signal);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
await legacyExecuteTaskFn(adapter, db, runId, desc, descriptorMap, inputTable, eventBus, toolConfig, workflowName, cacheEnabled, signal, disabledAgents, runAbortController, hijackState);
|
|
142
|
+
}
|
|
143
|
+
return classifyTaskAttempt(adapter, runId, desc, context);
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* @param {SmithersDb} adapter
|
|
147
|
+
* @param {_BunSQLiteDatabase} db
|
|
148
|
+
* @param {string} runId
|
|
149
|
+
* @param {_TaskDescriptor} desc
|
|
150
|
+
* @param {Map<string, _TaskDescriptor>} descriptorMap
|
|
151
|
+
* @param {SQLiteTable} inputTable
|
|
152
|
+
* @param {EventBus} eventBus
|
|
153
|
+
* @param {TaskBridgeToolConfig} toolConfig
|
|
154
|
+
* @param {string} workflowName
|
|
155
|
+
* @param {boolean} cacheEnabled
|
|
156
|
+
* @param {BridgeManagedTaskKind} bridgeManagedExecution
|
|
157
|
+
* @param {string} bridgeKey
|
|
158
|
+
* @param {AbortSignal} [signal]
|
|
159
|
+
* @param {Set<string>} [disabledAgents]
|
|
160
|
+
* @param {AbortController} [runAbortController]
|
|
161
|
+
* @param {HijackState} [hijackState]
|
|
162
|
+
* @param {LegacyExecuteTaskFn} [legacyExecuteTaskFn]
|
|
163
|
+
*/
|
|
164
|
+
const runTaskBridgeExecution = async (adapter, db, runId, desc, descriptorMap, inputTable, eventBus, toolConfig, workflowName, cacheEnabled, bridgeManagedExecution, bridgeKey, signal, disabledAgents, runAbortController, hijackState, legacyExecuteTaskFn) => {
|
|
165
|
+
const initialAttempt = await getNextTaskActivityAttempt(adapter, runId, desc);
|
|
166
|
+
return dispatchWorkerTask(makeWorkerTask(bridgeKey, workflowName, runId, desc, bridgeManagedExecution), async () => {
|
|
167
|
+
try {
|
|
168
|
+
await executeTaskActivity(adapter, workflowName, runId, desc, (context) => executeBridgeAttempt(adapter, db, runId, desc, descriptorMap, inputTable, eventBus, toolConfig, workflowName, cacheEnabled, bridgeManagedExecution, context, signal, disabledAgents, runAbortController, hijackState, legacyExecuteTaskFn), {
|
|
169
|
+
initialAttempt,
|
|
170
|
+
retry: false,
|
|
171
|
+
});
|
|
172
|
+
return { terminal: true };
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
if (error instanceof RetriableTaskFailure) {
|
|
176
|
+
return { terminal: false };
|
|
177
|
+
}
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
/**
|
|
183
|
+
* @param {SmithersDb} adapter
|
|
184
|
+
* @param {_BunSQLiteDatabase} db
|
|
185
|
+
* @param {string} runId
|
|
186
|
+
* @param {_TaskDescriptor} desc
|
|
187
|
+
* @param {Map<string, _TaskDescriptor>} descriptorMap
|
|
188
|
+
* @param {SQLiteTable} inputTable
|
|
189
|
+
* @param {EventBus} eventBus
|
|
190
|
+
* @param {TaskBridgeToolConfig} toolConfig
|
|
191
|
+
* @param {string} workflowName
|
|
192
|
+
* @param {boolean} cacheEnabled
|
|
193
|
+
* @param {AbortSignal} [signal]
|
|
194
|
+
* @param {Set<string>} [disabledAgents]
|
|
195
|
+
* @param {AbortController} [runAbortController]
|
|
196
|
+
* @param {HijackState} [hijackState]
|
|
197
|
+
* @param {LegacyExecuteTaskFn} [legacyExecuteTaskFn]
|
|
198
|
+
* @returns {Promise<void>}
|
|
199
|
+
*/
|
|
200
|
+
export const executeTaskBridge = (adapter, db, runId, desc, descriptorMap, inputTable, eventBus, toolConfig, workflowName, cacheEnabled, signal, disabledAgents, runAbortController, hijackState, legacyExecuteTaskFn) => {
|
|
201
|
+
const bridgeManagedExecution = canExecuteBridgeManagedComputeTask(desc, cacheEnabled)
|
|
202
|
+
? "compute"
|
|
203
|
+
: canExecuteBridgeManagedStaticTask(desc, cacheEnabled)
|
|
204
|
+
? "static"
|
|
205
|
+
: "legacy";
|
|
206
|
+
if (bridgeManagedExecution === "legacy" && typeof legacyExecuteTaskFn !== "function") {
|
|
207
|
+
return Promise.reject(new TypeError("legacyExecuteTaskFn must be provided"));
|
|
208
|
+
}
|
|
209
|
+
const bridgeKey = makeTaskBridgeKey(adapter, workflowName, runId, desc);
|
|
210
|
+
const completed = completedTaskExecutions.get(bridgeKey);
|
|
211
|
+
if (completed) {
|
|
212
|
+
return completed;
|
|
213
|
+
}
|
|
214
|
+
const existing = inflightTaskExecutions.get(bridgeKey);
|
|
215
|
+
if (existing) {
|
|
216
|
+
return existing;
|
|
217
|
+
}
|
|
218
|
+
const execution = runTaskBridgeExecution(adapter, db, runId, desc, descriptorMap, inputTable, eventBus, toolConfig, workflowName, cacheEnabled, bridgeManagedExecution, bridgeKey, signal, disabledAgents, runAbortController, hijackState, legacyExecuteTaskFn)
|
|
219
|
+
.then((result) => {
|
|
220
|
+
if (!result.terminal) {
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
completedTaskExecutions.set(bridgeKey, execution);
|
|
224
|
+
setTimeout(() => {
|
|
225
|
+
if (completedTaskExecutions.get(bridgeKey) === execution) {
|
|
226
|
+
completedTaskExecutions.delete(bridgeKey);
|
|
227
|
+
}
|
|
228
|
+
}, 0);
|
|
229
|
+
return undefined;
|
|
230
|
+
})
|
|
231
|
+
.finally(() => {
|
|
232
|
+
if (inflightTaskExecutions.get(bridgeKey) === execution) {
|
|
233
|
+
inflightTaskExecutions.delete(bridgeKey);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
inflightTaskExecutions.set(bridgeKey, execution);
|
|
237
|
+
return execution;
|
|
238
|
+
};
|
|
239
|
+
/**
|
|
240
|
+
* @param {SmithersDb} adapter
|
|
241
|
+
* @param {_BunSQLiteDatabase} db
|
|
242
|
+
* @param {string} runId
|
|
243
|
+
* @param {_TaskDescriptor} desc
|
|
244
|
+
* @param {Map<string, _TaskDescriptor>} descriptorMap
|
|
245
|
+
* @param {SQLiteTable} inputTable
|
|
246
|
+
* @param {EventBus} eventBus
|
|
247
|
+
* @param {TaskBridgeToolConfig} toolConfig
|
|
248
|
+
* @param {string} workflowName
|
|
249
|
+
* @param {boolean} cacheEnabled
|
|
250
|
+
* @param {AbortSignal} [signal]
|
|
251
|
+
* @param {Set<string>} [disabledAgents]
|
|
252
|
+
* @param {AbortController} [runAbortController]
|
|
253
|
+
* @param {HijackState} [hijackState]
|
|
254
|
+
* @param {LegacyExecuteTaskFn} [legacyExecuteTaskFn]
|
|
255
|
+
* @returns {Effect.Effect<void, import("@smithers-orchestrator/errors/SmithersError").SmithersError, never>}
|
|
256
|
+
*/
|
|
257
|
+
export const executeTaskBridgeEffect = (adapter, db, runId, desc, descriptorMap, inputTable, eventBus, toolConfig, workflowName, cacheEnabled, signal, disabledAgents, runAbortController, hijackState, legacyExecuteTaskFn) => Effect.tryPromise({
|
|
258
|
+
try: () => executeTaskBridge(adapter, db, runId, desc, descriptorMap, inputTable, eventBus, toolConfig, workflowName, cacheEnabled, signal, disabledAgents, runAbortController, hijackState, legacyExecuteTaskFn),
|
|
259
|
+
catch: (cause) => toSmithersError(cause, "execute task bridge"),
|
|
260
|
+
});
|