@smithers-orchestrator/engine 0.20.1 → 0.20.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -15
- package/src/approvals.js +6 -0
- package/src/child-workflow.js +7 -0
- package/src/effect/builder.js +48 -13
- package/src/effect/compute-task-bridge.js +42 -47
- package/src/effect/deferred-state-bridge.js +37 -0
- package/src/effect/diff-bundle.js +24 -12
- package/src/effect/durable-deferred-bridge.js +16 -4
- package/src/effect/http-runner.js +2 -86
- package/src/effect/single-runner.js +26 -1
- package/src/effect/sql-message-storage.js +2 -817
- package/src/effect/static-task-bridge.js +6 -2
- package/src/effect/workflow-bridge.js +15 -4
- package/src/effect/workflow-make-bridge.js +10 -3
- package/src/engine.js +85 -8
- package/src/hot/HotWorkflowController.js +37 -23
- package/src/hot/overlay.js +42 -24
- package/src/human-requests.js +3 -0
|
@@ -21,13 +21,13 @@ import * as BunContext from "@effect/platform-bun/BunContext";
|
|
|
21
21
|
function isAbortError(err) {
|
|
22
22
|
if (!err)
|
|
23
23
|
return false;
|
|
24
|
-
if (err.name === "AbortError")
|
|
25
|
-
return true;
|
|
26
24
|
if (typeof DOMException !== "undefined" &&
|
|
27
25
|
err instanceof DOMException &&
|
|
28
26
|
err.name === "AbortError") {
|
|
29
27
|
return true;
|
|
30
28
|
}
|
|
29
|
+
if (err.name === "AbortError")
|
|
30
|
+
return true;
|
|
31
31
|
if (err instanceof Error) {
|
|
32
32
|
return /aborted|abort/i.test(err.message);
|
|
33
33
|
}
|
|
@@ -304,3 +304,7 @@ export const executeStaticTaskBridge = async (adapter, runId, desc, eventBus, to
|
|
|
304
304
|
removeAbortForwarder();
|
|
305
305
|
}
|
|
306
306
|
};
|
|
307
|
+
|
|
308
|
+
export const __staticTaskBridgeInternals = {
|
|
309
|
+
isAbortError,
|
|
310
|
+
};
|
|
@@ -113,6 +113,12 @@ const getNextTaskActivityAttempt = async (adapter, runId, desc) => {
|
|
|
113
113
|
const latestAttempt = attempts[0]?.attempt ?? 0;
|
|
114
114
|
return latestAttempt + 1;
|
|
115
115
|
};
|
|
116
|
+
function taskBridgeResultForError(error) {
|
|
117
|
+
if (error instanceof RetriableTaskFailure) {
|
|
118
|
+
return { terminal: false };
|
|
119
|
+
}
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
116
122
|
/**
|
|
117
123
|
* @param {SmithersDb} adapter
|
|
118
124
|
* @param {_BunSQLiteDatabase} db
|
|
@@ -174,10 +180,7 @@ const runTaskBridgeExecution = async (adapter, db, runId, desc, descriptorMap, i
|
|
|
174
180
|
return { terminal: true };
|
|
175
181
|
}
|
|
176
182
|
catch (error) {
|
|
177
|
-
|
|
178
|
-
return { terminal: false };
|
|
179
|
-
}
|
|
180
|
-
throw error;
|
|
183
|
+
return taskBridgeResultForError(error);
|
|
181
184
|
}
|
|
182
185
|
});
|
|
183
186
|
};
|
|
@@ -260,3 +263,11 @@ export const executeTaskBridgeEffect = (adapter, db, runId, desc, descriptorMap,
|
|
|
260
263
|
try: () => executeTaskBridge(adapter, db, runId, desc, descriptorMap, inputTable, eventBus, toolConfig, workflowName, cacheEnabled, signal, disabledAgents, runAbortController, hijackState, legacyExecuteTaskFn),
|
|
261
264
|
catch: (cause) => toSmithersError(cause, "execute task bridge"),
|
|
262
265
|
});
|
|
266
|
+
export const __workflowBridgeInternals = {
|
|
267
|
+
classifyTaskAttempt,
|
|
268
|
+
getNextTaskActivityAttempt,
|
|
269
|
+
isRetryableBridgeTaskFailure,
|
|
270
|
+
parseAttemptErrorCode,
|
|
271
|
+
runEffectOrPromise,
|
|
272
|
+
taskBridgeResultForError,
|
|
273
|
+
};
|
|
@@ -175,9 +175,6 @@ export function createSchedulerWakeQueue() {
|
|
|
175
175
|
}
|
|
176
176
|
return new Promise((resolve) => {
|
|
177
177
|
resolver = () => {
|
|
178
|
-
if (pending > 0) {
|
|
179
|
-
pending -= 1;
|
|
180
|
-
}
|
|
181
178
|
resolve();
|
|
182
179
|
};
|
|
183
180
|
});
|
|
@@ -231,3 +228,13 @@ export async function runWorkflowWithMakeBridge(workflow, opts, executeBody) {
|
|
|
231
228
|
}
|
|
232
229
|
}
|
|
233
230
|
}
|
|
231
|
+
|
|
232
|
+
export const __workflowMakeBridgeInternals = {
|
|
233
|
+
createWorkflowExecutionEffect,
|
|
234
|
+
createWorkflowMakeBridgeRuntime,
|
|
235
|
+
executeRegisteredChildWorkflow,
|
|
236
|
+
getWorkflowNamespace,
|
|
237
|
+
isSuspendingStatus,
|
|
238
|
+
makeBridgeWorkflow,
|
|
239
|
+
registerBridgeWorkflow,
|
|
240
|
+
};
|
package/src/engine.js
CHANGED
|
@@ -2980,14 +2980,14 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
|
|
|
2980
2980
|
if (desc.outputTable && !supportsNativeStructuredOutput) {
|
|
2981
2981
|
const schemaDesc = describeSchemaShape(desc.outputTable, desc.outputSchema);
|
|
2982
2982
|
const jsonInstructions = [
|
|
2983
|
-
"**REQUIRED OUTPUT** — You MUST
|
|
2984
|
-
"```json",
|
|
2983
|
+
"**REQUIRED OUTPUT** — You MUST return ONLY a raw JSON object matching this schema:",
|
|
2985
2984
|
schemaDesc,
|
|
2986
|
-
"
|
|
2987
|
-
"
|
|
2985
|
+
"Do not include prose, markdown, headings, commentary, or code fences.",
|
|
2986
|
+
"The first character of your response must be `{` and the last character must be `}`.",
|
|
2987
|
+
"The workflow will fail unless the entire response is the JSON object.",
|
|
2988
2988
|
].join("\n");
|
|
2989
2989
|
effectivePrompt = [
|
|
2990
|
-
"IMPORTANT: After completing the task below, you MUST output a JSON object
|
|
2990
|
+
"IMPORTANT: After completing the task below, you MUST output ONLY a raw JSON object. Do NOT wrap it in markdown or add any prose — the workflow fails without it.",
|
|
2991
2991
|
"",
|
|
2992
2992
|
effectivePrompt,
|
|
2993
2993
|
"",
|
|
@@ -3448,7 +3448,7 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
|
|
|
3448
3448
|
`Now you MUST output ONLY a valid JSON object (no other text) summarizing your work above, with exactly these fields and types:`,
|
|
3449
3449
|
schemaDesc,
|
|
3450
3450
|
``,
|
|
3451
|
-
`Output ONLY the JSON object,
|
|
3451
|
+
`Output ONLY the raw JSON object, with no markdown fences or prose.`,
|
|
3452
3452
|
].join("\n");
|
|
3453
3453
|
const retryResult = await effectiveAgent.generate({
|
|
3454
3454
|
options: undefined,
|
|
@@ -3679,7 +3679,7 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
|
|
|
3679
3679
|
`You MUST output ONLY a valid JSON object with exactly these fields and types:`,
|
|
3680
3680
|
schemaDesc,
|
|
3681
3681
|
``,
|
|
3682
|
-
`Output ONLY the JSON object, no other text.`,
|
|
3682
|
+
`Output ONLY the raw JSON object, with no markdown fences or other text.`,
|
|
3683
3683
|
].join("\n");
|
|
3684
3684
|
logInfo("schema validation retry", {
|
|
3685
3685
|
runId,
|
|
@@ -4318,7 +4318,8 @@ async function runWorkflowAsync(workflow, opts) {
|
|
|
4318
4318
|
* @returns {Promise<RunBodyResult>}
|
|
4319
4319
|
*/
|
|
4320
4320
|
async function runWorkflowBody(workflow, opts) {
|
|
4321
|
-
if (
|
|
4321
|
+
if (opts.__smithersEngineMode === "legacy" ||
|
|
4322
|
+
process.env.SMITHERS_LEGACY_ENGINE === "1") {
|
|
4322
4323
|
return runWorkflowBodyLegacy(workflow, opts);
|
|
4323
4324
|
}
|
|
4324
4325
|
return runWorkflowBodyDriver(workflow, opts);
|
|
@@ -7229,3 +7230,79 @@ export function runWorkflow(workflow, opts) {
|
|
|
7229
7230
|
root: true,
|
|
7230
7231
|
});
|
|
7231
7232
|
}
|
|
7233
|
+
|
|
7234
|
+
export const __engineInternals = {
|
|
7235
|
+
sha256Hex,
|
|
7236
|
+
isBlockingAgentActionKind,
|
|
7237
|
+
makeAbortError,
|
|
7238
|
+
isAbortError,
|
|
7239
|
+
collectErrorMessages,
|
|
7240
|
+
isStructuredOutputParseFailure,
|
|
7241
|
+
depsTextAccessHint,
|
|
7242
|
+
makeStructuredOutputCompatibilityError,
|
|
7243
|
+
makePlainTextOutputError,
|
|
7244
|
+
abortPromise,
|
|
7245
|
+
parseAttemptMetaJson,
|
|
7246
|
+
asConversationMessages,
|
|
7247
|
+
cloneJsonValue,
|
|
7248
|
+
parseAttemptHeartbeatData,
|
|
7249
|
+
validateHeartbeatValue,
|
|
7250
|
+
serializeHeartbeatPayload,
|
|
7251
|
+
heartbeatTimeoutReasonFromAbort,
|
|
7252
|
+
isHeartbeatPayloadValidationError,
|
|
7253
|
+
runPromisePreservingFailure,
|
|
7254
|
+
extractHijackContinuation,
|
|
7255
|
+
findHijackContinuation,
|
|
7256
|
+
collectDefinedToolMetadata,
|
|
7257
|
+
collectToolResumeWarnings,
|
|
7258
|
+
buildToolResumeWarningMessage,
|
|
7259
|
+
hasToolResumeWarningMessage,
|
|
7260
|
+
appendToolResumeWarningMessage,
|
|
7261
|
+
prependToolResumeWarningMessage,
|
|
7262
|
+
workflowSessionTaskId,
|
|
7263
|
+
workflowSessionTaskIds,
|
|
7264
|
+
summarizeWorkflowSessionDecision,
|
|
7265
|
+
summarizeLegacySchedulerDecision,
|
|
7266
|
+
workflowSessionSummaryKey,
|
|
7267
|
+
coercePositiveInt,
|
|
7268
|
+
buildInputRow,
|
|
7269
|
+
normalizeInputRow,
|
|
7270
|
+
normalizeOutputRow,
|
|
7271
|
+
quoteSqlIdent,
|
|
7272
|
+
toSqlValue,
|
|
7273
|
+
getTableColumnEntries,
|
|
7274
|
+
insertRowWithClient,
|
|
7275
|
+
copyRunScopedRowsWithClient,
|
|
7276
|
+
ralphStateToObject,
|
|
7277
|
+
cloneRalphStateMap,
|
|
7278
|
+
buildCarriedInputRow,
|
|
7279
|
+
continueRunAsNew,
|
|
7280
|
+
resolveRootDir,
|
|
7281
|
+
resolveLogDir,
|
|
7282
|
+
getWorkflowImportScanLoader,
|
|
7283
|
+
extractWorkflowImportSpecifiers,
|
|
7284
|
+
resolveWorkflowImport,
|
|
7285
|
+
buildDurabilityConfig,
|
|
7286
|
+
getStoredDurabilityConfig,
|
|
7287
|
+
compareNullableString,
|
|
7288
|
+
assertResumeDurabilityMetadata,
|
|
7289
|
+
wireAbortSignal,
|
|
7290
|
+
parseRunConfigJson,
|
|
7291
|
+
parseRunAuthContext,
|
|
7292
|
+
isResumableRunStatus,
|
|
7293
|
+
normalizeHotOptions,
|
|
7294
|
+
assertInputObject,
|
|
7295
|
+
validateRunOptions,
|
|
7296
|
+
iterationsToMap,
|
|
7297
|
+
ralphStateFromDriverTransition,
|
|
7298
|
+
resolveTaskOutputs,
|
|
7299
|
+
buildDescriptorMap,
|
|
7300
|
+
buildRalphStateMap,
|
|
7301
|
+
ralphIterationsFromState,
|
|
7302
|
+
ralphIterationsObject,
|
|
7303
|
+
buildRalphDoneMap,
|
|
7304
|
+
parseAttemptErrorCode,
|
|
7305
|
+
isRetryableTaskFailure,
|
|
7306
|
+
cancelInProgress,
|
|
7307
|
+
cancelStaleAttempts,
|
|
7308
|
+
};
|
|
@@ -17,6 +17,37 @@ import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
|
|
|
17
17
|
|
|
18
18
|
const DEFAULT_MAX_GENERATIONS = 3;
|
|
19
19
|
const DEFAULT_DEBOUNCE_MS = 100;
|
|
20
|
+
|
|
21
|
+
function makeHotReloadFailureEvent(err, ctx) {
|
|
22
|
+
const { entryPath, generation, changedFiles } = ctx;
|
|
23
|
+
if (err instanceof Error && err.message?.includes("Schema change detected")) {
|
|
24
|
+
logWarning("hot workflow reload marked unsafe", {
|
|
25
|
+
entryPath,
|
|
26
|
+
generation,
|
|
27
|
+
changedFileCount: changedFiles.length,
|
|
28
|
+
reason: err.message,
|
|
29
|
+
}, "hot:reload");
|
|
30
|
+
return {
|
|
31
|
+
type: "unsafe",
|
|
32
|
+
generation,
|
|
33
|
+
changedFiles,
|
|
34
|
+
reason: err.message,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
logWarning("hot workflow reload failed", {
|
|
38
|
+
entryPath,
|
|
39
|
+
generation,
|
|
40
|
+
changedFileCount: changedFiles.length,
|
|
41
|
+
error: err instanceof Error ? err.message : String(err),
|
|
42
|
+
}, "hot:reload");
|
|
43
|
+
return {
|
|
44
|
+
type: "failed",
|
|
45
|
+
generation,
|
|
46
|
+
changedFiles,
|
|
47
|
+
error: err,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
20
51
|
export class HotWorkflowController {
|
|
21
52
|
entryPath;
|
|
22
53
|
hotRoot;
|
|
@@ -160,32 +191,11 @@ export class HotWorkflowController {
|
|
|
160
191
|
};
|
|
161
192
|
}).pipe(Effect.catchAll((err) => Effect.gen(function* () {
|
|
162
193
|
yield* Metric.increment(hotReloadFailures);
|
|
163
|
-
|
|
164
|
-
logWarning("hot workflow reload marked unsafe", {
|
|
165
|
-
entryPath,
|
|
166
|
-
generation: gen,
|
|
167
|
-
changedFileCount: changedFiles.length,
|
|
168
|
-
reason: err.message,
|
|
169
|
-
}, "hot:reload");
|
|
170
|
-
return {
|
|
171
|
-
type: "unsafe",
|
|
172
|
-
generation: gen,
|
|
173
|
-
changedFiles,
|
|
174
|
-
reason: err.message,
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
logWarning("hot workflow reload failed", {
|
|
194
|
+
return makeHotReloadFailureEvent(err, {
|
|
178
195
|
entryPath,
|
|
179
196
|
generation: gen,
|
|
180
|
-
changedFileCount: changedFiles.length,
|
|
181
|
-
error: err instanceof Error ? err.message : String(err),
|
|
182
|
-
}, "hot:reload");
|
|
183
|
-
return {
|
|
184
|
-
type: "failed",
|
|
185
|
-
generation: gen,
|
|
186
197
|
changedFiles,
|
|
187
|
-
|
|
188
|
-
};
|
|
198
|
+
});
|
|
189
199
|
})), Effect.annotateLogs({
|
|
190
200
|
entryPath,
|
|
191
201
|
hotRoot,
|
|
@@ -218,3 +228,7 @@ export class HotWorkflowController {
|
|
|
218
228
|
}), Effect.withLogSpan("hot:close"));
|
|
219
229
|
}
|
|
220
230
|
}
|
|
231
|
+
|
|
232
|
+
export const __hotWorkflowControllerInternals = {
|
|
233
|
+
makeHotReloadFailureEvent,
|
|
234
|
+
};
|
package/src/hot/overlay.js
CHANGED
|
@@ -13,6 +13,17 @@ const DEFAULT_EXCLUDE = [
|
|
|
13
13
|
".smithers",
|
|
14
14
|
".DS_Store",
|
|
15
15
|
];
|
|
16
|
+
|
|
17
|
+
function hotOverlayError(cause, operation, details) {
|
|
18
|
+
return toSmithersError(cause, operation, {
|
|
19
|
+
code: "HOT_OVERLAY_FAILED",
|
|
20
|
+
details,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function hotOverlayErrorMapper(operation, details) {
|
|
25
|
+
return (cause) => hotOverlayError(cause, operation, details);
|
|
26
|
+
}
|
|
16
27
|
/**
|
|
17
28
|
* Build a generation overlay by hardlinking (or copying) the hot root
|
|
18
29
|
* tree into a new generation directory.
|
|
@@ -31,9 +42,10 @@ export function buildOverlayEffect(hotRoot, outDir, generation, opts) {
|
|
|
31
42
|
return Effect.gen(function* () {
|
|
32
43
|
yield* Effect.tryPromise({
|
|
33
44
|
try: () => mkdir(genDir, { recursive: true }),
|
|
34
|
-
catch: (
|
|
35
|
-
|
|
36
|
-
|
|
45
|
+
catch: hotOverlayErrorMapper("create hot overlay generation dir", {
|
|
46
|
+
hotRoot,
|
|
47
|
+
outDir,
|
|
48
|
+
generation,
|
|
37
49
|
}),
|
|
38
50
|
});
|
|
39
51
|
yield* mirrorTreeEffect(hotRoot, genDir, exclude);
|
|
@@ -63,9 +75,9 @@ function mirrorTreeEffect(src, dest, exclude) {
|
|
|
63
75
|
return Effect.gen(function* () {
|
|
64
76
|
const entries = yield* Effect.tryPromise({
|
|
65
77
|
try: () => readdir(src, { withFileTypes: true }),
|
|
66
|
-
catch: (
|
|
67
|
-
|
|
68
|
-
|
|
78
|
+
catch: hotOverlayErrorMapper("read hot overlay source dir", {
|
|
79
|
+
src,
|
|
80
|
+
dest,
|
|
69
81
|
}),
|
|
70
82
|
});
|
|
71
83
|
for (const entry of entries) {
|
|
@@ -78,9 +90,9 @@ function mirrorTreeEffect(src, dest, exclude) {
|
|
|
78
90
|
if (entry.isDirectory()) {
|
|
79
91
|
yield* Effect.tryPromise({
|
|
80
92
|
try: () => mkdir(destPath, { recursive: true }),
|
|
81
|
-
catch: (
|
|
82
|
-
|
|
83
|
-
|
|
93
|
+
catch: hotOverlayErrorMapper("create mirrored hot overlay dir", {
|
|
94
|
+
srcPath,
|
|
95
|
+
destPath,
|
|
84
96
|
}),
|
|
85
97
|
});
|
|
86
98
|
yield* mirrorTreeEffect(srcPath, destPath, exclude);
|
|
@@ -88,24 +100,24 @@ function mirrorTreeEffect(src, dest, exclude) {
|
|
|
88
100
|
else if (entry.isFile()) {
|
|
89
101
|
const linked = yield* Effect.either(Effect.tryPromise({
|
|
90
102
|
try: () => link(srcPath, destPath),
|
|
91
|
-
catch: (
|
|
92
|
-
|
|
93
|
-
|
|
103
|
+
catch: hotOverlayErrorMapper("hardlink overlay file", {
|
|
104
|
+
srcPath,
|
|
105
|
+
destPath,
|
|
94
106
|
}),
|
|
95
107
|
}));
|
|
96
108
|
if (linked._tag === "Left") {
|
|
97
109
|
yield* Effect.tryPromise({
|
|
98
110
|
try: () => mkdir(dirname(destPath), { recursive: true }),
|
|
99
|
-
catch: (
|
|
100
|
-
|
|
101
|
-
|
|
111
|
+
catch: hotOverlayErrorMapper("create overlay file parent dir", {
|
|
112
|
+
srcPath,
|
|
113
|
+
destPath,
|
|
102
114
|
}),
|
|
103
115
|
});
|
|
104
116
|
yield* Effect.tryPromise({
|
|
105
117
|
try: () => copyFile(srcPath, destPath),
|
|
106
|
-
catch: (
|
|
107
|
-
|
|
108
|
-
|
|
118
|
+
catch: hotOverlayErrorMapper("copy overlay file", {
|
|
119
|
+
srcPath,
|
|
120
|
+
destPath,
|
|
109
121
|
}),
|
|
110
122
|
});
|
|
111
123
|
}
|
|
@@ -126,9 +138,9 @@ export function cleanupGenerationsEffect(outDir, keepLast) {
|
|
|
126
138
|
return;
|
|
127
139
|
const entries = yield* Effect.tryPromise({
|
|
128
140
|
try: () => readdir(outDir, { withFileTypes: true }),
|
|
129
|
-
catch: (
|
|
130
|
-
|
|
131
|
-
|
|
141
|
+
catch: hotOverlayErrorMapper("read hot overlay generations", {
|
|
142
|
+
outDir,
|
|
143
|
+
keepLast,
|
|
132
144
|
}),
|
|
133
145
|
});
|
|
134
146
|
const genDirs = entries
|
|
@@ -143,9 +155,9 @@ export function cleanupGenerationsEffect(outDir, keepLast) {
|
|
|
143
155
|
for (const dir of toRemove) {
|
|
144
156
|
yield* Effect.either(Effect.tryPromise({
|
|
145
157
|
try: () => rm(join(outDir, dir.name), { recursive: true, force: true }),
|
|
146
|
-
catch: (
|
|
147
|
-
|
|
148
|
-
|
|
158
|
+
catch: hotOverlayErrorMapper("remove stale hot overlay generation", {
|
|
159
|
+
outDir,
|
|
160
|
+
generationDir: dir.name,
|
|
149
161
|
}),
|
|
150
162
|
}));
|
|
151
163
|
}
|
|
@@ -175,3 +187,9 @@ export function resolveOverlayEntry(entryPath, hotRoot, genDir) {
|
|
|
175
187
|
const rel = relative(hotRoot, entryPath);
|
|
176
188
|
return resolve(genDir, rel);
|
|
177
189
|
}
|
|
190
|
+
|
|
191
|
+
export const __overlayInternals = {
|
|
192
|
+
hotOverlayError,
|
|
193
|
+
hotOverlayErrorMapper,
|
|
194
|
+
mirrorTreeEffect,
|
|
195
|
+
};
|