@smithers-orchestrator/engine 0.20.3 → 0.21.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/package.json +14 -15
- package/src/approvals.js +6 -0
- package/src/cache-policy.js +54 -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 +198 -172
- package/src/hot/HotWorkflowController.js +37 -23
- package/src/hot/overlay.js +42 -24
- package/src/hot/watch.js +162 -4
- package/src/human-requests.js +3 -0
- package/src/workflow-hash.js +157 -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
|
@@ -30,14 +30,13 @@ import { Cause, Chunk, Duration, Effect, Exit, Fiber, Metric, Queue, Schedule }
|
|
|
30
30
|
import { attemptDuration, cacheHits, cacheMisses, nodeDuration, promptSizeBytes, responseSizeBytes, runDuration, runsResumedTotal, schedulerConcurrencyUtilization, schedulerQueueDepth, schedulerWaitDuration, trackEvent, } from "@smithers-orchestrator/observability/metrics";
|
|
31
31
|
import { runScorersAsync } from "@smithers-orchestrator/scorers/run-scorers";
|
|
32
32
|
import { dirname, resolve } from "node:path";
|
|
33
|
-
import { existsSync
|
|
34
|
-
import { readFile } from "node:fs/promises";
|
|
33
|
+
import { existsSync } from "node:fs";
|
|
35
34
|
import { toSmithersError } from "@smithers-orchestrator/errors/toSmithersError";
|
|
36
35
|
import { logDebug, logError, logInfo, logWarning } from "@smithers-orchestrator/observability/logging";
|
|
37
36
|
import { isPidAlive, parseRuntimeOwnerPid } from "./runtime-owner.js";
|
|
38
37
|
import { HotWorkflowController } from "./hot/index.js";
|
|
39
38
|
import { spawn as nodeSpawn } from "node:child_process";
|
|
40
|
-
import {
|
|
39
|
+
import { randomUUID } from "node:crypto";
|
|
41
40
|
import { platform } from "node:os";
|
|
42
41
|
import { annotateSmithersTrace, smithersSpanNames, withSmithersSpan, } from "@smithers-orchestrator/observability";
|
|
43
42
|
import { withTaskRuntime } from "@smithers-orchestrator/driver/task-runtime";
|
|
@@ -45,9 +44,13 @@ import { hashCapabilityRegistry } from "@smithers-orchestrator/agents/capability
|
|
|
45
44
|
import { cancelPendingTimersBridge, executeTaskBridgeEffect, isBridgeManagedTimerTask as isTimerTask, resolveDeferredTaskStateBridge, } from "./effect/workflow-bridge.js";
|
|
46
45
|
import { AlertRuntime } from "./alert-runtime.js";
|
|
47
46
|
import { executeChildWorkflow } from "./child-workflow.js";
|
|
47
|
+
import { executeSandbox } from "@smithers-orchestrator/sandbox/execute";
|
|
48
|
+
import { applyDiffBundle } from "./effect/diff-bundle.js";
|
|
49
|
+
import { buildCacheScopeIdentity, isFreshCacheRow, normalizeCacheScope } from "./cache-policy.js";
|
|
48
50
|
import { runWorkflowWithMakeBridge } from "./effect/workflow-make-bridge.js";
|
|
49
51
|
import { createWorkflowVersioningRuntime, getWorkflowPatchDecisions, withWorkflowVersioningRuntime, } from "./effect/versioning.js";
|
|
50
52
|
import { runWithCorrelationContext, updateCurrentCorrelationContext, withCorrelationContext, } from "@smithers-orchestrator/observability/correlation";
|
|
53
|
+
import { extractWorkflowImportSpecifiers, getWorkflowImportScanLoader, readWorkflowEntryHash, readWorkflowGraphHash, resolveWorkflowImport, sha256Hex, } from "./workflow-hash.js";
|
|
51
54
|
/** @typedef {import("@smithers-orchestrator/graph/GraphSnapshot").GraphSnapshot} GraphSnapshot */
|
|
52
55
|
/** @typedef {import("./HijackState.ts").HijackState} HijackState */
|
|
53
56
|
/** @typedef {import("@smithers-orchestrator/driver/RunOptions").RunOptions} RunOptions */
|
|
@@ -61,13 +64,6 @@ import { runWithCorrelationContext, updateCurrentCorrelationContext, withCorrela
|
|
|
61
64
|
/** @typedef {import("drizzle-orm/bun-sqlite").BunSQLiteDatabase<Record<string, unknown>>} BunSQLiteDatabase */
|
|
62
65
|
/** @typedef {import("drizzle-orm/sqlite-core").SQLiteTable} SQLiteTable */
|
|
63
66
|
|
|
64
|
-
/**
|
|
65
|
-
* @param {string} input
|
|
66
|
-
* @returns {string}
|
|
67
|
-
*/
|
|
68
|
-
function sha256Hex(input) {
|
|
69
|
-
return createHash("sha256").update(input).digest("hex");
|
|
70
|
-
}
|
|
71
67
|
/**
|
|
72
68
|
* Track which worktree paths have already been created this run so we don't
|
|
73
69
|
* re-create them for every task sharing the same worktree.
|
|
@@ -1345,143 +1341,6 @@ function resolveLogDir(rootDir, runId, logDir) {
|
|
|
1345
1341
|
}
|
|
1346
1342
|
return resolve(rootDir, ".smithers", "executions", runId, "logs");
|
|
1347
1343
|
}
|
|
1348
|
-
const STATIC_IMPORT_RE = /\b(?:import|export)\s+(?:[^"'`]*?\s+from\s*)?["']([^"']+)["']/g;
|
|
1349
|
-
const DYNAMIC_IMPORT_RE = /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
1350
|
-
const WORKFLOW_IMPORT_EXTENSIONS = [
|
|
1351
|
-
"",
|
|
1352
|
-
".ts",
|
|
1353
|
-
".tsx",
|
|
1354
|
-
".mts",
|
|
1355
|
-
".cts",
|
|
1356
|
-
".js",
|
|
1357
|
-
".jsx",
|
|
1358
|
-
".mjs",
|
|
1359
|
-
".cjs",
|
|
1360
|
-
];
|
|
1361
|
-
/**
|
|
1362
|
-
* @param {string | null | undefined} sourcePath
|
|
1363
|
-
*/
|
|
1364
|
-
function getWorkflowImportScanLoader(sourcePath) {
|
|
1365
|
-
const lower = sourcePath?.toLowerCase() ?? "";
|
|
1366
|
-
if (lower.endsWith(".tsx"))
|
|
1367
|
-
return "tsx";
|
|
1368
|
-
if (lower.endsWith(".jsx"))
|
|
1369
|
-
return "jsx";
|
|
1370
|
-
if (lower.endsWith(".ts") ||
|
|
1371
|
-
lower.endsWith(".mts") ||
|
|
1372
|
-
lower.endsWith(".cts")) {
|
|
1373
|
-
return "ts";
|
|
1374
|
-
}
|
|
1375
|
-
return "js";
|
|
1376
|
-
}
|
|
1377
|
-
/**
|
|
1378
|
-
* @param {string | null} workflowPath
|
|
1379
|
-
* @returns {Promise<string | null>}
|
|
1380
|
-
*/
|
|
1381
|
-
async function readWorkflowEntryHash(workflowPath) {
|
|
1382
|
-
if (!workflowPath)
|
|
1383
|
-
return null;
|
|
1384
|
-
try {
|
|
1385
|
-
const raw = await readFile(workflowPath, "utf8");
|
|
1386
|
-
return sha256Hex(raw);
|
|
1387
|
-
}
|
|
1388
|
-
catch {
|
|
1389
|
-
return null;
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1392
|
-
/**
|
|
1393
|
-
* @param {string} source
|
|
1394
|
-
* @param {string | null} [sourcePath]
|
|
1395
|
-
* @returns {string[]}
|
|
1396
|
-
*/
|
|
1397
|
-
function extractWorkflowImportSpecifiers(source, sourcePath) {
|
|
1398
|
-
if (typeof Bun !== "undefined" && typeof Bun.Transpiler === "function") {
|
|
1399
|
-
try {
|
|
1400
|
-
const scanned = new Bun.Transpiler({
|
|
1401
|
-
loader: getWorkflowImportScanLoader(sourcePath),
|
|
1402
|
-
}).scanImports(source);
|
|
1403
|
-
const specifiers = new Set();
|
|
1404
|
-
for (const entry of scanned) {
|
|
1405
|
-
const specifier = entry?.path?.trim();
|
|
1406
|
-
if (specifier?.startsWith(".")) {
|
|
1407
|
-
specifiers.add(specifier);
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
return [...specifiers];
|
|
1411
|
-
}
|
|
1412
|
-
catch {
|
|
1413
|
-
// Fall back to regex scanning if Bun's parser cannot handle the source.
|
|
1414
|
-
}
|
|
1415
|
-
}
|
|
1416
|
-
const specifiers = new Set();
|
|
1417
|
-
for (const pattern of [STATIC_IMPORT_RE, DYNAMIC_IMPORT_RE]) {
|
|
1418
|
-
pattern.lastIndex = 0;
|
|
1419
|
-
let match;
|
|
1420
|
-
while ((match = pattern.exec(source)) !== null) {
|
|
1421
|
-
const specifier = match[1]?.trim();
|
|
1422
|
-
if (!specifier?.startsWith("."))
|
|
1423
|
-
continue;
|
|
1424
|
-
specifiers.add(specifier);
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
1427
|
-
return [...specifiers];
|
|
1428
|
-
}
|
|
1429
|
-
/**
|
|
1430
|
-
* @param {string} baseFile
|
|
1431
|
-
* @param {string} specifier
|
|
1432
|
-
* @returns {string | null}
|
|
1433
|
-
*/
|
|
1434
|
-
function resolveWorkflowImport(baseFile, specifier) {
|
|
1435
|
-
const basePath = resolve(dirname(baseFile), specifier);
|
|
1436
|
-
const candidates = [
|
|
1437
|
-
...WORKFLOW_IMPORT_EXTENSIONS.map((ext) => `${basePath}${ext}`),
|
|
1438
|
-
...WORKFLOW_IMPORT_EXTENSIONS
|
|
1439
|
-
.filter((ext) => ext.length > 0)
|
|
1440
|
-
.map((ext) => resolve(basePath, `index${ext}`)),
|
|
1441
|
-
];
|
|
1442
|
-
for (const candidate of candidates) {
|
|
1443
|
-
if (existsSync(candidate) && statSync(candidate).isFile()) {
|
|
1444
|
-
return resolve(candidate);
|
|
1445
|
-
}
|
|
1446
|
-
}
|
|
1447
|
-
return null;
|
|
1448
|
-
}
|
|
1449
|
-
/**
|
|
1450
|
-
* @param {string} workflowPath
|
|
1451
|
-
* @returns {Promise<string[]>}
|
|
1452
|
-
*/
|
|
1453
|
-
async function collectWorkflowModuleHashEntries(workflowPath, visited = new Set()) {
|
|
1454
|
-
const resolvedPath = resolve(workflowPath);
|
|
1455
|
-
if (visited.has(resolvedPath)) {
|
|
1456
|
-
return [];
|
|
1457
|
-
}
|
|
1458
|
-
visited.add(resolvedPath);
|
|
1459
|
-
const source = await readFile(resolvedPath, "utf8");
|
|
1460
|
-
const entries = [`${resolvedPath}:${sha256Hex(source)}`];
|
|
1461
|
-
for (const specifier of extractWorkflowImportSpecifiers(source, resolvedPath)) {
|
|
1462
|
-
const importedPath = resolveWorkflowImport(resolvedPath, specifier);
|
|
1463
|
-
if (!importedPath) {
|
|
1464
|
-
throw new SmithersError("WORKFLOW_HASH_RESOLUTION_FAILED", `Unable to resolve workflow import "${specifier}" from ${resolvedPath}.`, { workflowPath: resolvedPath, specifier });
|
|
1465
|
-
}
|
|
1466
|
-
entries.push(...(await collectWorkflowModuleHashEntries(importedPath, visited)));
|
|
1467
|
-
}
|
|
1468
|
-
return entries;
|
|
1469
|
-
}
|
|
1470
|
-
/**
|
|
1471
|
-
* @param {string | null} workflowPath
|
|
1472
|
-
* @returns {Promise<string | null>}
|
|
1473
|
-
*/
|
|
1474
|
-
async function readWorkflowGraphHash(workflowPath) {
|
|
1475
|
-
if (!workflowPath)
|
|
1476
|
-
return null;
|
|
1477
|
-
try {
|
|
1478
|
-
const entries = await collectWorkflowModuleHashEntries(workflowPath);
|
|
1479
|
-
return sha256Hex(entries.sort().join("|"));
|
|
1480
|
-
}
|
|
1481
|
-
catch {
|
|
1482
|
-
return null;
|
|
1483
|
-
}
|
|
1484
|
-
}
|
|
1485
1344
|
/**
|
|
1486
1345
|
* @param {string} cwd
|
|
1487
1346
|
* @returns {Promise<string | null>}
|
|
@@ -1980,6 +1839,54 @@ function attachSubflowComputeFns(tasks, workflow, opts = {}) {
|
|
|
1980
1839
|
task.meta = persistableMeta;
|
|
1981
1840
|
}
|
|
1982
1841
|
}
|
|
1842
|
+
/**
|
|
1843
|
+
* @param {TaskDescriptor[]} tasks
|
|
1844
|
+
* @param {SmithersWorkflow<any>} workflow
|
|
1845
|
+
* @param {{ rootDir?: string; workflowPath?: string | null }} [opts]
|
|
1846
|
+
*/
|
|
1847
|
+
function attachSandboxComputeFns(tasks, workflow, opts = {}) {
|
|
1848
|
+
for (const task of tasks) {
|
|
1849
|
+
if (!task.meta?.__sandbox || task.computeFn)
|
|
1850
|
+
continue;
|
|
1851
|
+
const sandboxWorkflow = task.meta.__sandboxWorkflow;
|
|
1852
|
+
if (!sandboxWorkflow)
|
|
1853
|
+
continue;
|
|
1854
|
+
const sandboxInput = task.meta.__sandboxInput;
|
|
1855
|
+
const sandboxProvider = task.meta.__sandboxProvider;
|
|
1856
|
+
const sandboxRuntime = task.meta.__sandboxRuntime;
|
|
1857
|
+
const sandboxAllowNetwork = Boolean(task.meta.__sandboxAllowNetwork);
|
|
1858
|
+
const sandboxReviewDiffs = task.meta.__sandboxReviewDiffs;
|
|
1859
|
+
const sandboxAutoAcceptDiffs = task.meta.__sandboxAutoAcceptDiffs;
|
|
1860
|
+
const sandboxAllowNested = Boolean(task.meta.__sandboxAllowNested);
|
|
1861
|
+
const sandboxConfig = task.meta.__sandboxConfig && typeof task.meta.__sandboxConfig === "object"
|
|
1862
|
+
? task.meta.__sandboxConfig
|
|
1863
|
+
: {};
|
|
1864
|
+
task.computeFn = async () => executeSandbox({
|
|
1865
|
+
parentWorkflow: workflow,
|
|
1866
|
+
sandboxId: task.nodeId,
|
|
1867
|
+
provider: sandboxProvider,
|
|
1868
|
+
runtime: sandboxRuntime,
|
|
1869
|
+
workflow: sandboxWorkflow,
|
|
1870
|
+
executeChildWorkflow,
|
|
1871
|
+
applyDiffBundle,
|
|
1872
|
+
input: sandboxInput,
|
|
1873
|
+
rootDir: task.worktreePath ?? opts.rootDir ?? process.cwd(),
|
|
1874
|
+
allowNetwork: sandboxAllowNetwork,
|
|
1875
|
+
maxOutputBytes: 200_000,
|
|
1876
|
+
toolTimeoutMs: 60_000,
|
|
1877
|
+
reviewDiffs: sandboxReviewDiffs,
|
|
1878
|
+
autoAcceptDiffs: sandboxAutoAcceptDiffs,
|
|
1879
|
+
allowNested: sandboxAllowNested,
|
|
1880
|
+
config: sandboxConfig,
|
|
1881
|
+
});
|
|
1882
|
+
const {
|
|
1883
|
+
__sandboxWorkflow: _workflow,
|
|
1884
|
+
__sandboxProvider: _provider,
|
|
1885
|
+
...persistableMeta
|
|
1886
|
+
} = task.meta;
|
|
1887
|
+
task.meta = persistableMeta;
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1983
1890
|
/**
|
|
1984
1891
|
* @param {XmlNode} xml
|
|
1985
1892
|
* @returns {string}
|
|
@@ -2660,6 +2567,9 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
|
|
|
2660
2567
|
const taskRoot = desc.worktreePath ?? toolConfig.rootDir;
|
|
2661
2568
|
const stepCacheEnabled = cacheEnabled || Boolean(desc.cachePolicy);
|
|
2662
2569
|
const cacheAgent = Array.isArray(desc.agent) ? desc.agent[0] : desc.agent;
|
|
2570
|
+
const cachePolicyTtlMs = typeof desc.cachePolicy?.ttlMs === "number" && Number.isFinite(desc.cachePolicy.ttlMs)
|
|
2571
|
+
? Math.max(0, desc.cachePolicy.ttlMs)
|
|
2572
|
+
: null;
|
|
2663
2573
|
let heartbeatWatchdogFiber = null;
|
|
2664
2574
|
try {
|
|
2665
2575
|
if (taskSignal.aborted) {
|
|
@@ -2732,6 +2642,7 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
|
|
|
2732
2642
|
if (desc.cachePolicy) {
|
|
2733
2643
|
let cachePayload = null;
|
|
2734
2644
|
let cacheByOk = true;
|
|
2645
|
+
const cacheScope = normalizeCacheScope(desc.cachePolicy);
|
|
2735
2646
|
try {
|
|
2736
2647
|
const ctx = await buildCacheContext(db, inputTable, runId, desc, descriptorMap, attemptNo);
|
|
2737
2648
|
if (desc.cachePolicy.by) {
|
|
@@ -2752,16 +2663,15 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
|
|
|
2752
2663
|
cacheKeyDisabled = true;
|
|
2753
2664
|
}
|
|
2754
2665
|
cacheBase = {
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
iteration: desc.iteration,
|
|
2758
|
-
outputTableName: desc.outputTableName,
|
|
2666
|
+
cacheScope,
|
|
2667
|
+
...buildCacheScopeIdentity(cacheScope, runId, workflowName, desc),
|
|
2759
2668
|
schemaSig,
|
|
2760
2669
|
outputSchemaSig,
|
|
2761
2670
|
agentSig,
|
|
2762
2671
|
toolsSig,
|
|
2763
2672
|
jjPointer: cacheJjBase,
|
|
2764
2673
|
cacheVersion: desc.cachePolicy.version ?? null,
|
|
2674
|
+
cacheKey: desc.cachePolicy.key ?? null,
|
|
2765
2675
|
cacheBy: cachePayload ?? null,
|
|
2766
2676
|
};
|
|
2767
2677
|
}
|
|
@@ -2797,26 +2707,53 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
|
|
|
2797
2707
|
}
|
|
2798
2708
|
if (cacheKey) {
|
|
2799
2709
|
const cachedRow = await Effect.runPromise(adapter.getCache(cacheKey));
|
|
2800
|
-
if (cachedRow) {
|
|
2801
|
-
const
|
|
2802
|
-
const
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
void Effect.runPromise(Metric.increment(
|
|
2807
|
-
logInfo("cache
|
|
2710
|
+
if (cachedRow && isFreshCacheRow(cachedRow, desc.cachePolicy)) {
|
|
2711
|
+
const createdAtMs = Number(cachedRow.createdAtMs);
|
|
2712
|
+
const expired = cachePolicyTtlMs !== null &&
|
|
2713
|
+
Number.isFinite(createdAtMs) &&
|
|
2714
|
+
nowMs() - createdAtMs >= cachePolicyTtlMs;
|
|
2715
|
+
if (expired) {
|
|
2716
|
+
void Effect.runPromise(Metric.increment(cacheMisses));
|
|
2717
|
+
logInfo("cache entry expired for task output", {
|
|
2808
2718
|
runId,
|
|
2809
2719
|
nodeId: desc.nodeId,
|
|
2810
2720
|
iteration: desc.iteration,
|
|
2811
2721
|
attempt: attemptNo,
|
|
2812
2722
|
cacheKey,
|
|
2723
|
+
ttlMs: cachePolicyTtlMs,
|
|
2813
2724
|
}, "engine:task-cache");
|
|
2814
2725
|
}
|
|
2815
2726
|
else {
|
|
2816
|
-
|
|
2727
|
+
const parsed = JSON.parse(cachedRow.payloadJson);
|
|
2728
|
+
const valid = validateOutput(desc.outputTable, parsed);
|
|
2729
|
+
if (valid.ok) {
|
|
2730
|
+
payload = valid.data;
|
|
2731
|
+
cached = true;
|
|
2732
|
+
void Effect.runPromise(Metric.increment(cacheHits));
|
|
2733
|
+
logInfo("cache hit for task output", {
|
|
2734
|
+
runId,
|
|
2735
|
+
nodeId: desc.nodeId,
|
|
2736
|
+
iteration: desc.iteration,
|
|
2737
|
+
attempt: attemptNo,
|
|
2738
|
+
cacheKey,
|
|
2739
|
+
}, "engine:task-cache");
|
|
2740
|
+
}
|
|
2741
|
+
else {
|
|
2742
|
+
void Effect.runPromise(Metric.increment(cacheMisses));
|
|
2743
|
+
}
|
|
2817
2744
|
}
|
|
2818
2745
|
}
|
|
2819
2746
|
else {
|
|
2747
|
+
if (cachedRow) {
|
|
2748
|
+
logInfo("cache entry expired for task output", {
|
|
2749
|
+
runId,
|
|
2750
|
+
nodeId: desc.nodeId,
|
|
2751
|
+
iteration: desc.iteration,
|
|
2752
|
+
attempt: attemptNo,
|
|
2753
|
+
cacheKey,
|
|
2754
|
+
ttlMs: desc.cachePolicy?.ttlMs,
|
|
2755
|
+
}, "engine:task-cache");
|
|
2756
|
+
}
|
|
2820
2757
|
void Effect.runPromise(Metric.increment(cacheMisses));
|
|
2821
2758
|
}
|
|
2822
2759
|
}
|
|
@@ -2980,14 +2917,14 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
|
|
|
2980
2917
|
if (desc.outputTable && !supportsNativeStructuredOutput) {
|
|
2981
2918
|
const schemaDesc = describeSchemaShape(desc.outputTable, desc.outputSchema);
|
|
2982
2919
|
const jsonInstructions = [
|
|
2983
|
-
"**REQUIRED OUTPUT** — You MUST
|
|
2984
|
-
"```json",
|
|
2920
|
+
"**REQUIRED OUTPUT** — You MUST return ONLY a raw JSON object matching this schema:",
|
|
2985
2921
|
schemaDesc,
|
|
2986
|
-
"
|
|
2987
|
-
"
|
|
2922
|
+
"Do not include prose, markdown, headings, commentary, or code fences.",
|
|
2923
|
+
"The first character of your response must be `{` and the last character must be `}`.",
|
|
2924
|
+
"The workflow will fail unless the entire response is the JSON object.",
|
|
2988
2925
|
].join("\n");
|
|
2989
2926
|
effectivePrompt = [
|
|
2990
|
-
"IMPORTANT: After completing the task below, you MUST output a JSON object
|
|
2927
|
+
"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
2928
|
"",
|
|
2992
2929
|
effectivePrompt,
|
|
2993
2930
|
"",
|
|
@@ -3448,7 +3385,7 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
|
|
|
3448
3385
|
`Now you MUST output ONLY a valid JSON object (no other text) summarizing your work above, with exactly these fields and types:`,
|
|
3449
3386
|
schemaDesc,
|
|
3450
3387
|
``,
|
|
3451
|
-
`Output ONLY the JSON object,
|
|
3388
|
+
`Output ONLY the raw JSON object, with no markdown fences or prose.`,
|
|
3452
3389
|
].join("\n");
|
|
3453
3390
|
const retryResult = await effectiveAgent.generate({
|
|
3454
3391
|
options: undefined,
|
|
@@ -3679,7 +3616,7 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
|
|
|
3679
3616
|
`You MUST output ONLY a valid JSON object with exactly these fields and types:`,
|
|
3680
3617
|
schemaDesc,
|
|
3681
3618
|
``,
|
|
3682
|
-
`Output ONLY the JSON object, no other text.`,
|
|
3619
|
+
`Output ONLY the raw JSON object, with no markdown fences or other text.`,
|
|
3683
3620
|
].join("\n");
|
|
3684
3621
|
logInfo("schema validation retry", {
|
|
3685
3622
|
runId,
|
|
@@ -4132,6 +4069,10 @@ async function renderFrameAsync(workflow, ctx, opts) {
|
|
|
4132
4069
|
rootDir: opts?.baseRootDir,
|
|
4133
4070
|
workflowPath: opts?.workflowPath,
|
|
4134
4071
|
});
|
|
4072
|
+
attachSandboxComputeFns(tasks, workflow, {
|
|
4073
|
+
rootDir: opts?.baseRootDir,
|
|
4074
|
+
workflowPath: opts?.workflowPath,
|
|
4075
|
+
});
|
|
4135
4076
|
return { runId: ctx.runId, frameNo: 0, xml: result.xml, tasks };
|
|
4136
4077
|
}
|
|
4137
4078
|
/**
|
|
@@ -4318,7 +4259,8 @@ async function runWorkflowAsync(workflow, opts) {
|
|
|
4318
4259
|
* @returns {Promise<RunBodyResult>}
|
|
4319
4260
|
*/
|
|
4320
4261
|
async function runWorkflowBody(workflow, opts) {
|
|
4321
|
-
if (
|
|
4262
|
+
if (opts.__smithersEngineMode === "legacy" ||
|
|
4263
|
+
process.env.SMITHERS_LEGACY_ENGINE === "1") {
|
|
4322
4264
|
return runWorkflowBodyLegacy(workflow, opts);
|
|
4323
4265
|
}
|
|
4324
4266
|
return runWorkflowBodyDriver(workflow, opts);
|
|
@@ -5268,10 +5210,14 @@ async function runWorkflowBodyDriver(workflow, opts) {
|
|
|
5268
5210
|
const graph = await withWorkflowVersioningRuntime(workflowVersioning, () => renderer.render(element, renderOpts));
|
|
5269
5211
|
await workflowVersioning.flush();
|
|
5270
5212
|
resolveTaskOutputs(graph.tasks, workflowRef);
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5213
|
+
attachSubflowComputeFns(graph.tasks, workflowRef, {
|
|
5214
|
+
rootDir,
|
|
5215
|
+
workflowPath: resolvedWorkflowPath ?? opts.workflowPath,
|
|
5216
|
+
});
|
|
5217
|
+
attachSandboxComputeFns(graph.tasks, workflowRef, {
|
|
5218
|
+
rootDir,
|
|
5219
|
+
workflowPath: resolvedWorkflowPath ?? opts.workflowPath,
|
|
5220
|
+
});
|
|
5275
5221
|
lastGraph = graph;
|
|
5276
5222
|
descriptorMap = buildDescriptorMap(graph.tasks);
|
|
5277
5223
|
workflowName = getWorkflowNameFromXml(graph.xml);
|
|
@@ -6347,6 +6293,10 @@ async function runWorkflowBodyLegacy(workflow, opts) {
|
|
|
6347
6293
|
rootDir,
|
|
6348
6294
|
workflowPath: resolvedWorkflowPath ?? opts.workflowPath,
|
|
6349
6295
|
});
|
|
6296
|
+
attachSandboxComputeFns(tasks, workflow, {
|
|
6297
|
+
rootDir,
|
|
6298
|
+
workflowPath: resolvedWorkflowPath ?? opts.workflowPath,
|
|
6299
|
+
});
|
|
6350
6300
|
const workflowName = getWorkflowNameFromXml(xml);
|
|
6351
6301
|
updateCurrentCorrelationContext({ workflowName });
|
|
6352
6302
|
const cacheEnabled = workflow.opts.cache ??
|
|
@@ -7229,3 +7179,79 @@ export function runWorkflow(workflow, opts) {
|
|
|
7229
7179
|
root: true,
|
|
7230
7180
|
});
|
|
7231
7181
|
}
|
|
7182
|
+
|
|
7183
|
+
export const __engineInternals = {
|
|
7184
|
+
sha256Hex,
|
|
7185
|
+
isBlockingAgentActionKind,
|
|
7186
|
+
makeAbortError,
|
|
7187
|
+
isAbortError,
|
|
7188
|
+
collectErrorMessages,
|
|
7189
|
+
isStructuredOutputParseFailure,
|
|
7190
|
+
depsTextAccessHint,
|
|
7191
|
+
makeStructuredOutputCompatibilityError,
|
|
7192
|
+
makePlainTextOutputError,
|
|
7193
|
+
abortPromise,
|
|
7194
|
+
parseAttemptMetaJson,
|
|
7195
|
+
asConversationMessages,
|
|
7196
|
+
cloneJsonValue,
|
|
7197
|
+
parseAttemptHeartbeatData,
|
|
7198
|
+
validateHeartbeatValue,
|
|
7199
|
+
serializeHeartbeatPayload,
|
|
7200
|
+
heartbeatTimeoutReasonFromAbort,
|
|
7201
|
+
isHeartbeatPayloadValidationError,
|
|
7202
|
+
runPromisePreservingFailure,
|
|
7203
|
+
extractHijackContinuation,
|
|
7204
|
+
findHijackContinuation,
|
|
7205
|
+
collectDefinedToolMetadata,
|
|
7206
|
+
collectToolResumeWarnings,
|
|
7207
|
+
buildToolResumeWarningMessage,
|
|
7208
|
+
hasToolResumeWarningMessage,
|
|
7209
|
+
appendToolResumeWarningMessage,
|
|
7210
|
+
prependToolResumeWarningMessage,
|
|
7211
|
+
workflowSessionTaskId,
|
|
7212
|
+
workflowSessionTaskIds,
|
|
7213
|
+
summarizeWorkflowSessionDecision,
|
|
7214
|
+
summarizeLegacySchedulerDecision,
|
|
7215
|
+
workflowSessionSummaryKey,
|
|
7216
|
+
coercePositiveInt,
|
|
7217
|
+
buildInputRow,
|
|
7218
|
+
normalizeInputRow,
|
|
7219
|
+
normalizeOutputRow,
|
|
7220
|
+
quoteSqlIdent,
|
|
7221
|
+
toSqlValue,
|
|
7222
|
+
getTableColumnEntries,
|
|
7223
|
+
insertRowWithClient,
|
|
7224
|
+
copyRunScopedRowsWithClient,
|
|
7225
|
+
ralphStateToObject,
|
|
7226
|
+
cloneRalphStateMap,
|
|
7227
|
+
buildCarriedInputRow,
|
|
7228
|
+
continueRunAsNew,
|
|
7229
|
+
resolveRootDir,
|
|
7230
|
+
resolveLogDir,
|
|
7231
|
+
getWorkflowImportScanLoader,
|
|
7232
|
+
extractWorkflowImportSpecifiers,
|
|
7233
|
+
resolveWorkflowImport,
|
|
7234
|
+
buildDurabilityConfig,
|
|
7235
|
+
getStoredDurabilityConfig,
|
|
7236
|
+
compareNullableString,
|
|
7237
|
+
assertResumeDurabilityMetadata,
|
|
7238
|
+
wireAbortSignal,
|
|
7239
|
+
parseRunConfigJson,
|
|
7240
|
+
parseRunAuthContext,
|
|
7241
|
+
isResumableRunStatus,
|
|
7242
|
+
normalizeHotOptions,
|
|
7243
|
+
assertInputObject,
|
|
7244
|
+
validateRunOptions,
|
|
7245
|
+
iterationsToMap,
|
|
7246
|
+
ralphStateFromDriverTransition,
|
|
7247
|
+
resolveTaskOutputs,
|
|
7248
|
+
buildDescriptorMap,
|
|
7249
|
+
buildRalphStateMap,
|
|
7250
|
+
ralphIterationsFromState,
|
|
7251
|
+
ralphIterationsObject,
|
|
7252
|
+
buildRalphDoneMap,
|
|
7253
|
+
parseAttemptErrorCode,
|
|
7254
|
+
isRetryableTaskFailure,
|
|
7255
|
+
cancelInProgress,
|
|
7256
|
+
cancelStaleAttempts,
|
|
7257
|
+
};
|
|
@@ -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
|
+
};
|