@smithers-orchestrator/engine 0.16.9 → 0.18.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 +25 -16
- package/src/approvals.js +0 -1
- package/src/effect/builder.js +3 -3
- package/src/effect/compute-task-bridge.js +8 -2
- package/src/effect/deferred-state-bridge.js +0 -1
- package/src/effect/diff-bundle.js +1 -1
- package/src/effect/static-task-bridge.js +0 -2
- package/src/effect/workflow-bridge.js +5 -3
- package/src/engine.js +215 -77
- package/src/hot/watch.js +2 -2
- package/src/signals.js +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smithers-orchestrator/engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Concrete Smithers workflow execution engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -20,27 +20,36 @@
|
|
|
20
20
|
"src/"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
+
"@effect/cluster": "^0.58.0",
|
|
24
|
+
"@effect/experimental": "^0.60.0",
|
|
23
25
|
"@effect/platform-bun": "^0.89.0",
|
|
26
|
+
"@effect/rpc": "^0.75.0",
|
|
27
|
+
"@effect/sql": "^0.51.0",
|
|
24
28
|
"@effect/sql-sqlite-bun": "^0.52.0",
|
|
29
|
+
"@effect/workflow": "^0.18.0",
|
|
30
|
+
"diff": "^9.0.0",
|
|
31
|
+
"drizzle-orm": "^0.45.2",
|
|
25
32
|
"effect": "^3.21.1",
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"@smithers-orchestrator/
|
|
30
|
-
"@smithers-orchestrator/
|
|
31
|
-
"@smithers-orchestrator/
|
|
32
|
-
"@smithers-orchestrator/
|
|
33
|
-
"@smithers-orchestrator/
|
|
34
|
-
"@smithers-orchestrator/
|
|
35
|
-
"@smithers-orchestrator/
|
|
36
|
-
"@smithers-orchestrator/scheduler": "0.
|
|
37
|
-
"@smithers-orchestrator/scorers": "0.
|
|
38
|
-
"@smithers-orchestrator/
|
|
39
|
-
"@smithers-orchestrator/
|
|
33
|
+
"react": "^19.2.5",
|
|
34
|
+
"react-dom": "^19.2.5",
|
|
35
|
+
"zod": "^4.3.6",
|
|
36
|
+
"@smithers-orchestrator/agents": "0.18.0",
|
|
37
|
+
"@smithers-orchestrator/db": "0.18.0",
|
|
38
|
+
"@smithers-orchestrator/driver": "0.18.0",
|
|
39
|
+
"@smithers-orchestrator/errors": "0.18.0",
|
|
40
|
+
"@smithers-orchestrator/graph": "0.18.0",
|
|
41
|
+
"@smithers-orchestrator/memory": "0.18.0",
|
|
42
|
+
"@smithers-orchestrator/observability": "0.18.0",
|
|
43
|
+
"@smithers-orchestrator/scheduler": "0.18.0",
|
|
44
|
+
"@smithers-orchestrator/scorers": "0.18.0",
|
|
45
|
+
"@smithers-orchestrator/time-travel": "0.18.0",
|
|
46
|
+
"@smithers-orchestrator/components": "0.18.0",
|
|
47
|
+
"@smithers-orchestrator/react-reconciler": "0.18.0",
|
|
48
|
+
"@smithers-orchestrator/sandbox": "0.18.0",
|
|
49
|
+
"@smithers-orchestrator/vcs": "0.18.0"
|
|
40
50
|
},
|
|
41
51
|
"devDependencies": {
|
|
42
52
|
"@types/bun": "latest",
|
|
43
|
-
"react": "^19.2.5",
|
|
44
53
|
"typescript": "~5.9.3"
|
|
45
54
|
},
|
|
46
55
|
"scripts": {
|
package/src/approvals.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Effect, Metric } from "effect";
|
|
2
2
|
import { nowMs } from "@smithers-orchestrator/scheduler/nowMs";
|
|
3
|
-
import { SmithersDb } from "@smithers-orchestrator/db/adapter";
|
|
4
3
|
import { approvalWaitDuration, trackEvent, updateAsyncExternalWaitPending, } from "@smithers-orchestrator/observability/metrics";
|
|
5
4
|
import { bridgeApprovalResolve } from "./effect/durable-deferred-bridge.js";
|
|
6
5
|
import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
|
package/src/effect/builder.js
CHANGED
|
@@ -298,7 +298,7 @@ function resolveHandleIteration(handle, ctx) {
|
|
|
298
298
|
* @param {Record<string, unknown>} row
|
|
299
299
|
*/
|
|
300
300
|
function stripPersistedKeys(row) {
|
|
301
|
-
const { runId, nodeId, iteration, payload, ...rest } = row;
|
|
301
|
+
const { runId: _runId, nodeId: _nodeId, iteration: _iteration, payload, ...rest } = row;
|
|
302
302
|
if (payload !== undefined)
|
|
303
303
|
return payload;
|
|
304
304
|
return rest;
|
|
@@ -754,7 +754,7 @@ function normalizeExecutionError(result) {
|
|
|
754
754
|
/**
|
|
755
755
|
* @param {{ name: string; input: AnySchema }} options
|
|
756
756
|
*/
|
|
757
|
-
function
|
|
757
|
+
function _createWorkflow(options) {
|
|
758
758
|
return {
|
|
759
759
|
/**
|
|
760
760
|
* @param {($: BuilderApi) => BuilderNode} buildGraph
|
|
@@ -804,7 +804,7 @@ function createWorkflow(options) {
|
|
|
804
804
|
/**
|
|
805
805
|
* @param {{ name: string; params?: Record<string, unknown> }} options
|
|
806
806
|
*/
|
|
807
|
-
function
|
|
807
|
+
function _createComponent(options) {
|
|
808
808
|
return {
|
|
809
809
|
/**
|
|
810
810
|
* @param {($: BuilderApi, params: Record<string, unknown>) => BuilderNode} buildGraph
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { Cause, Duration, Effect, Either, Exit, Metric, Schedule } from "effect";
|
|
2
|
-
import { z } from "zod";
|
|
3
2
|
import { buildOutputRow, stripAutoColumns, validateOutput } from "@smithers-orchestrator/db/output";
|
|
4
3
|
import { TaskHeartbeatTimeout } from "@smithers-orchestrator/errors/TaskHeartbeatTimeout";
|
|
5
4
|
import { TaskTimeout } from "@smithers-orchestrator/errors/TaskTimeout";
|
|
6
|
-
import { EventBus } from "../events.js";
|
|
7
5
|
import { makeAbortError, wireAbortSignal } from "./bridge-utils.js";
|
|
8
6
|
import { withTaskRuntime } from "@smithers-orchestrator/driver/task-runtime";
|
|
9
7
|
import { logDebug, logError, logInfo, logWarning } from "@smithers-orchestrator/observability/logging";
|
|
@@ -623,6 +621,14 @@ export const executeComputeTaskBridge = async (adapter, db, runId, desc, eventBu
|
|
|
623
621
|
if (isHeartbeatPayloadValidationError(effectiveError)) {
|
|
624
622
|
attemptMeta.failureRetryable = false;
|
|
625
623
|
}
|
|
624
|
+
// Propagate non-retryable signal from any thrown SmithersError so the
|
|
625
|
+
// attempt is not retried (e.g. AGENT_CONFIG_INVALID from KimiAgent's
|
|
626
|
+
// expired-credentials check, or auth-failure patterns classified by
|
|
627
|
+
// BaseCliAgent.classifyNonRetryableAgentError).
|
|
628
|
+
if (effectiveError?.details?.failureRetryable === false ||
|
|
629
|
+
effectiveError?.code === "AGENT_CONFIG_INVALID") {
|
|
630
|
+
attemptMeta.failureRetryable = false;
|
|
631
|
+
}
|
|
626
632
|
if (aborted) {
|
|
627
633
|
await waitForHeartbeatWriteDrain();
|
|
628
634
|
await flushHeartbeat(true);
|
|
@@ -3,7 +3,6 @@ import { renderToStaticMarkup } from "react-dom/server";
|
|
|
3
3
|
import { Effect, Exit } from "effect";
|
|
4
4
|
import { buildOutputRow, describeSchemaShape, selectOutputRow, stripAutoColumns, validateExistingOutput, validateOutput, } from "@smithers-orchestrator/db/output";
|
|
5
5
|
import { awaitApprovalDurableDeferred, awaitWaitForEventDurableDeferred, bridgeApprovalResolve, bridgeWaitForEventResolve, } from "./durable-deferred-bridge.js";
|
|
6
|
-
import { EventBus } from "../events.js";
|
|
7
6
|
import { buildHumanRequestId, getHumanTaskPrompt as getStoredHumanTaskPrompt, isHumanTaskMeta, } from "../human-requests.js";
|
|
8
7
|
import { parseAttemptMetaJson } from "./bridge-utils.js";
|
|
9
8
|
import { updateAsyncExternalWaitPending } from "@smithers-orchestrator/observability/metrics";
|
|
@@ -344,7 +344,7 @@ export async function applyDiffBundle(bundle, targetDir) {
|
|
|
344
344
|
await runGit(targetDir, ["apply", "--binary", "--whitespace=nowarn", "--unsafe-paths", "-"], { input: fullPatch });
|
|
345
345
|
return;
|
|
346
346
|
}
|
|
347
|
-
catch
|
|
347
|
+
catch {
|
|
348
348
|
for (const patch of bundle.patches) {
|
|
349
349
|
await applyPatchFallback(patch, targetDir);
|
|
350
350
|
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { Effect, Metric } from "effect";
|
|
2
|
-
import { z } from "zod";
|
|
3
2
|
import { buildOutputRow, stripAutoColumns, validateOutput } from "@smithers-orchestrator/db/output";
|
|
4
|
-
import { EventBus } from "../events.js";
|
|
5
3
|
import { makeAbortError, wireAbortSignal } from "./bridge-utils.js";
|
|
6
4
|
import { logDebug, logError, logInfo } from "@smithers-orchestrator/observability/logging";
|
|
7
5
|
import { attemptDuration, nodeDuration } from "@smithers-orchestrator/observability/metrics";
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { Effect } from "effect";
|
|
2
|
-
import { SmithersDb } from "@smithers-orchestrator/db/adapter";
|
|
3
|
-
import { EventBus } from "../events.js";
|
|
4
2
|
import { toSmithersError } from "@smithers-orchestrator/errors/toSmithersError";
|
|
5
3
|
import { makeWorkerTask, } from "./entity-worker.js";
|
|
6
4
|
import { executeTaskActivity, makeTaskBridgeKey, RetriableTaskFailure, } from "./activity-bridge.js";
|
|
@@ -74,8 +72,12 @@ function isRetryableBridgeTaskFailure(attempt) {
|
|
|
74
72
|
if (meta?.failureRetryable === false) {
|
|
75
73
|
return false;
|
|
76
74
|
}
|
|
75
|
+
const errorCode = parseAttemptErrorCode(attempt?.errorJson);
|
|
76
|
+
if (errorCode === "AGENT_CONFIG_INVALID") {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
77
79
|
const kind = typeof meta?.kind === "string" ? meta.kind : null;
|
|
78
|
-
return !(kind !== "agent" &&
|
|
80
|
+
return !(kind !== "agent" && errorCode === "INVALID_OUTPUT");
|
|
79
81
|
}
|
|
80
82
|
/**
|
|
81
83
|
* @param {SmithersDb} adapter
|
package/src/engine.js
CHANGED
|
@@ -5,7 +5,7 @@ import { SmithersCtx } from "@smithers-orchestrator/driver/SmithersCtx";
|
|
|
5
5
|
import { loadInput, loadOutputs } from "@smithers-orchestrator/db/snapshot";
|
|
6
6
|
import { ensureSmithersTables } from "@smithers-orchestrator/db/ensure";
|
|
7
7
|
import { SmithersDb } from "@smithers-orchestrator/db/adapter";
|
|
8
|
-
import { selectOutputRow, validateOutput, validateExistingOutput,
|
|
8
|
+
import { selectOutputRow, validateOutput, validateExistingOutput, describeSchemaShape, buildOutputRow, stripAutoColumns, } from "@smithers-orchestrator/db/output";
|
|
9
9
|
import { validateInput } from "@smithers-orchestrator/db/input";
|
|
10
10
|
import { schemaSignature } from "@smithers-orchestrator/db/schema-signature";
|
|
11
11
|
import { withSqliteWriteRetry } from "@smithers-orchestrator/db/write-retry";
|
|
@@ -23,10 +23,9 @@ import { EventBus } from "./events.js";
|
|
|
23
23
|
import { getJjPointer, runJj, workspaceAdd } from "@smithers-orchestrator/vcs/jj";
|
|
24
24
|
import { findVcsRoot } from "@smithers-orchestrator/vcs/find-root";
|
|
25
25
|
import * as BunContext from "@effect/platform-bun/BunContext";
|
|
26
|
-
import { z } from "zod";
|
|
27
26
|
import { eq, getTableName } from "drizzle-orm";
|
|
28
27
|
import { getTableColumns } from "drizzle-orm/utils";
|
|
29
|
-
import { Chunk, Duration, Effect, Fiber, Metric, Queue, Schedule } from "effect";
|
|
28
|
+
import { Cause, Chunk, Duration, Effect, Exit, Fiber, Metric, Queue, Schedule } from "effect";
|
|
30
29
|
import { attemptDuration, cacheHits, cacheMisses, nodeDuration, promptSizeBytes, responseSizeBytes, runDuration, runsResumedTotal, schedulerConcurrencyUtilization, schedulerQueueDepth, schedulerWaitDuration, trackEvent, } from "@smithers-orchestrator/observability/metrics";
|
|
31
30
|
import { runScorersAsync } from "@smithers-orchestrator/scorers/run-scorers";
|
|
32
31
|
import { dirname, resolve } from "node:path";
|
|
@@ -288,6 +287,25 @@ function isHeartbeatPayloadValidationError(err) {
|
|
|
288
287
|
return (code === "HEARTBEAT_PAYLOAD_NOT_JSON_SERIALIZABLE" ||
|
|
289
288
|
code === "HEARTBEAT_PAYLOAD_TOO_LARGE");
|
|
290
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* Effect.runPromise rejects with a FiberFailure wrapper. For task execution we
|
|
292
|
+
* need the original failure so retry metadata can read SmithersError fields.
|
|
293
|
+
*
|
|
294
|
+
* @template A
|
|
295
|
+
* @param {Effect.Effect<A, unknown>} effect
|
|
296
|
+
* @returns {Promise<A>}
|
|
297
|
+
*/
|
|
298
|
+
async function runPromisePreservingFailure(effect) {
|
|
299
|
+
const exit = await Effect.runPromiseExit(effect);
|
|
300
|
+
if (Exit.isSuccess(exit)) {
|
|
301
|
+
return exit.value;
|
|
302
|
+
}
|
|
303
|
+
const failure = Cause.failureOption(exit.cause);
|
|
304
|
+
if (failure._tag === "Some") {
|
|
305
|
+
throw failure.value;
|
|
306
|
+
}
|
|
307
|
+
throw Cause.squash(exit.cause);
|
|
308
|
+
}
|
|
291
309
|
/**
|
|
292
310
|
* @param {Record<string, unknown>} meta
|
|
293
311
|
* @param {string} engine
|
|
@@ -448,16 +466,6 @@ function prependToolResumeWarningMessage(prompt, warningMessage) {
|
|
|
448
466
|
}
|
|
449
467
|
return `${warningMessage}\n\n${prompt}`;
|
|
450
468
|
}
|
|
451
|
-
/**
|
|
452
|
-
* @param {HijackCompletion} completion
|
|
453
|
-
* @returns {Error}
|
|
454
|
-
*/
|
|
455
|
-
function buildHijackAbortError(completion) {
|
|
456
|
-
const err = makeAbortError(`Hijack requested for ${completion.engine}`);
|
|
457
|
-
err.code = "RUN_HIJACKED";
|
|
458
|
-
err.hijack = completion;
|
|
459
|
-
return err;
|
|
460
|
-
}
|
|
461
469
|
/**
|
|
462
470
|
* @param {string} cwd
|
|
463
471
|
* @param {string[]} args
|
|
@@ -1526,13 +1534,11 @@ function assertResumeDurabilityMetadata(existingRun, existingConfig, current, wo
|
|
|
1526
1534
|
else if (shouldCheckWorkflowHashes) {
|
|
1527
1535
|
compareNullableString(existingRun.workflowHash, current.entryWorkflowHash, "workflow entry file changed", mismatches);
|
|
1528
1536
|
}
|
|
1529
|
-
compareNullableString(existingRun.vcsType, current.vcsType, "VCS type changed", mismatches);
|
|
1530
1537
|
if ((existingRun.vcsRoot && current.vcsRoot
|
|
1531
1538
|
? resolve(existingRun.vcsRoot) !== resolve(current.vcsRoot)
|
|
1532
1539
|
: (existingRun.vcsRoot ?? null) !== (current.vcsRoot ?? null))) {
|
|
1533
1540
|
mismatches.push("VCS root changed");
|
|
1534
1541
|
}
|
|
1535
|
-
compareNullableString(existingRun.vcsRevision, current.vcsRevision, "VCS revision changed", mismatches);
|
|
1536
1542
|
if (mismatches.length > 0) {
|
|
1537
1543
|
throw new SmithersError("RESUME_METADATA_MISMATCH", `Cannot resume run because durable metadata changed: ${mismatches.join(", ")}`, {
|
|
1538
1544
|
existing: {
|
|
@@ -1797,6 +1803,7 @@ function resolveTaskOutputs(tasks, workflow) {
|
|
|
1797
1803
|
if (isTimerTask(task)) {
|
|
1798
1804
|
continue;
|
|
1799
1805
|
}
|
|
1806
|
+
const hasAmbiguousOutputRef = Boolean(task.outputRef && workflow.ambiguousZodSchemas?.has(task.outputRef));
|
|
1800
1807
|
// Already resolved (has a table)
|
|
1801
1808
|
if (task.outputTable) {
|
|
1802
1809
|
if (!task.outputSchema && task.outputTableName && workflow.schemaRegistry) {
|
|
@@ -1820,10 +1827,14 @@ function resolveTaskOutputs(tasks, workflow) {
|
|
|
1820
1827
|
}
|
|
1821
1828
|
}
|
|
1822
1829
|
if (!task.outputTable) {
|
|
1830
|
+
if (hasAmbiguousOutputRef) {
|
|
1831
|
+
throw new SmithersError("UNKNOWN_OUTPUT_SCHEMA", `Task "${task.nodeId}" uses an output schema that is registered under multiple keys. Use createSmithers(...).outputs.<key> or a string output key instead of the shared raw Zod object.`);
|
|
1832
|
+
}
|
|
1823
1833
|
throw new SmithersError("UNKNOWN_OUTPUT_SCHEMA", `Task "${task.nodeId}" uses an output ZodObject that is not registered in createSmithers()`);
|
|
1824
1834
|
}
|
|
1825
1835
|
}
|
|
1826
1836
|
const raw = task.outputSchema;
|
|
1837
|
+
const hasAmbiguousOutputSchema = Boolean(raw && typeof raw === "object" && workflow.ambiguousZodSchemas?.has(raw));
|
|
1827
1838
|
// Resolve ZodObject via outputSchema when no outputRef resolved.
|
|
1828
1839
|
if (!task.outputTable && raw && typeof raw === "object" && workflow.zodToKeyName) {
|
|
1829
1840
|
const keyName = workflow.zodToKeyName.get(raw);
|
|
@@ -1837,6 +1848,9 @@ function resolveTaskOutputs(tasks, workflow) {
|
|
|
1837
1848
|
}
|
|
1838
1849
|
}
|
|
1839
1850
|
if (!task.outputTable) {
|
|
1851
|
+
if (hasAmbiguousOutputSchema) {
|
|
1852
|
+
throw new SmithersError("UNKNOWN_OUTPUT_SCHEMA", `Task "${task.nodeId}" uses an output schema that is registered under multiple keys. Use createSmithers(...).outputs.<key> or a string output key instead of the shared raw Zod object.`);
|
|
1853
|
+
}
|
|
1840
1854
|
throw new SmithersError("UNKNOWN_OUTPUT_SCHEMA", `Task "${task.nodeId}" uses an output ZodObject that is not registered in createSmithers()`);
|
|
1841
1855
|
}
|
|
1842
1856
|
}
|
|
@@ -2031,8 +2045,15 @@ function isRetryableTaskFailure(attempt) {
|
|
|
2031
2045
|
if (meta?.failureRetryable === false) {
|
|
2032
2046
|
return false;
|
|
2033
2047
|
}
|
|
2048
|
+
const errorCode = parseAttemptErrorCode(attempt?.errorJson);
|
|
2049
|
+
// AGENT_CONFIG_INVALID is a deterministic configuration failure (e.g.
|
|
2050
|
+
// "LLM not set", unknown model). Retrying is guaranteed to fail again
|
|
2051
|
+
// and just multiplies cost — short-circuit immediately.
|
|
2052
|
+
if (errorCode === "AGENT_CONFIG_INVALID") {
|
|
2053
|
+
return false;
|
|
2054
|
+
}
|
|
2034
2055
|
const kind = typeof meta?.kind === "string" ? meta.kind : null;
|
|
2035
|
-
return !(kind !== "agent" &&
|
|
2056
|
+
return !(kind !== "agent" && errorCode === "INVALID_OUTPUT");
|
|
2036
2057
|
}
|
|
2037
2058
|
/**
|
|
2038
2059
|
* @param {SmithersDb} adapter
|
|
@@ -2560,6 +2581,7 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
|
|
|
2560
2581
|
let cacheJjBase = null;
|
|
2561
2582
|
let responseText = null;
|
|
2562
2583
|
let effectiveAgent = null;
|
|
2584
|
+
let supportsNativeStructuredOutput = false;
|
|
2563
2585
|
// Resolve effective root once so both caching and execution share it.
|
|
2564
2586
|
const taskRoot = desc.worktreePath ?? toolConfig.rootDir;
|
|
2565
2587
|
const stepCacheEnabled = cacheEnabled || Boolean(desc.cachePolicy);
|
|
@@ -2766,13 +2788,15 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
|
|
|
2766
2788
|
effectiveAgent.model ??
|
|
2767
2789
|
effectiveAgent.modelId ??
|
|
2768
2790
|
null;
|
|
2769
|
-
const
|
|
2791
|
+
const hijackCapableEngine = typeof effectiveAgent.cliEngine === "string"
|
|
2770
2792
|
? effectiveAgent.cliEngine
|
|
2771
2793
|
: typeof effectiveAgent.hijackEngine === "string"
|
|
2772
2794
|
? effectiveAgent.hijackEngine
|
|
2773
|
-
:
|
|
2774
|
-
|
|
2775
|
-
|
|
2795
|
+
: null;
|
|
2796
|
+
const currentAgentEngine = hijackCapableEngine ??
|
|
2797
|
+
(typeof effectiveAgent.constructor?.name === "string"
|
|
2798
|
+
? effectiveAgent.constructor.name
|
|
2799
|
+
: null);
|
|
2776
2800
|
attemptMeta.agentEngine = currentAgentEngine;
|
|
2777
2801
|
const heartbeatCheckpoint = previousHeartbeat &&
|
|
2778
2802
|
typeof previousHeartbeat === "object" &&
|
|
@@ -2785,15 +2809,25 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
|
|
|
2785
2809
|
const heartbeatCheckpointUsable = !currentAgentEngine ||
|
|
2786
2810
|
!heartbeatCheckpointEngine ||
|
|
2787
2811
|
heartbeatCheckpointEngine === currentAgentEngine;
|
|
2788
|
-
|
|
2789
|
-
|
|
2812
|
+
// If the most recent failed attempt asked us to drop the resume
|
|
2813
|
+
// session (e.g. kimi crashed mid-stream and reported `kimi -r
|
|
2814
|
+
// <uuid>`; that session is now corrupt and re-resuming it just
|
|
2815
|
+
// reproduces the crash), don't reuse the captured agentResume
|
|
2816
|
+
// from the heartbeat. Forces the agent to start a fresh
|
|
2817
|
+
// session on the next attempt.
|
|
2818
|
+
const lastFailedAttempt = attempts.find((a) => a.state === "failed");
|
|
2819
|
+
const lastFailedMeta = parseAttemptMetaJson(lastFailedAttempt?.metaJson);
|
|
2820
|
+
const discardResumeSession = lastFailedMeta?.discardResumeSession === true;
|
|
2821
|
+
const checkpointResumeSession = !discardResumeSession
|
|
2822
|
+
&& heartbeatCheckpointUsable
|
|
2823
|
+
&& typeof heartbeatCheckpoint?.agentResume === "string"
|
|
2790
2824
|
? heartbeatCheckpoint.agentResume
|
|
2791
2825
|
: undefined;
|
|
2792
2826
|
const checkpointResumeMessages = heartbeatCheckpointUsable
|
|
2793
2827
|
? asConversationMessages(heartbeatCheckpoint?.agentConversation)
|
|
2794
2828
|
: undefined;
|
|
2795
|
-
const priorContinuation =
|
|
2796
|
-
? findHijackContinuation(attempts,
|
|
2829
|
+
const priorContinuation = hijackCapableEngine
|
|
2830
|
+
? findHijackContinuation(attempts, hijackCapableEngine)
|
|
2797
2831
|
: undefined;
|
|
2798
2832
|
const resumeSession = priorContinuation?.mode === "native-cli"
|
|
2799
2833
|
? priorContinuation.resume
|
|
@@ -2803,6 +2837,37 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
|
|
|
2803
2837
|
: (cloneJsonValue(checkpointResumeMessages) ??
|
|
2804
2838
|
checkpointResumeMessages);
|
|
2805
2839
|
const guidedResumeMessages = appendToolResumeWarningMessage(resumeMessages, toolResumeWarningMessage);
|
|
2840
|
+
if (desc.hijack) {
|
|
2841
|
+
if (!hijackCapableEngine) {
|
|
2842
|
+
attemptMeta.failureRetryable = false;
|
|
2843
|
+
throw new SmithersError("TASK_HIJACK_UNSUPPORTED", `Task ${desc.nodeId} sets hijack, but its agent is not hijack-capable. Hijack requires an agent with cliEngine or hijackEngine.`, {
|
|
2844
|
+
nodeId: desc.nodeId,
|
|
2845
|
+
agentId: attemptMeta.agentId ?? undefined,
|
|
2846
|
+
});
|
|
2847
|
+
}
|
|
2848
|
+
const shouldAutoHijack = desc.onHijackExit === "reopen" || !priorContinuation;
|
|
2849
|
+
if (shouldAutoHijack && !hijackState) {
|
|
2850
|
+
attemptMeta.failureRetryable = false;
|
|
2851
|
+
throw new SmithersError("TASK_HIJACK_UNSUPPORTED", `Task ${desc.nodeId} cannot auto-hijack in this execution mode.`, {
|
|
2852
|
+
nodeId: desc.nodeId,
|
|
2853
|
+
agentId: attemptMeta.agentId ?? undefined,
|
|
2854
|
+
});
|
|
2855
|
+
}
|
|
2856
|
+
if (shouldAutoHijack && !hijackState.request && !hijackState.completion) {
|
|
2857
|
+
const requestedAtMs = nowMs();
|
|
2858
|
+
hijackState.request = {
|
|
2859
|
+
requestedAtMs,
|
|
2860
|
+
target: hijackCapableEngine,
|
|
2861
|
+
};
|
|
2862
|
+
await Effect.runPromise(adapter.requestRunHijack(runId, requestedAtMs, hijackCapableEngine));
|
|
2863
|
+
await Effect.runPromise(eventBus.emitEventWithPersist({
|
|
2864
|
+
type: "RunHijackRequested",
|
|
2865
|
+
runId,
|
|
2866
|
+
target: hijackCapableEngine,
|
|
2867
|
+
timestampMs: requestedAtMs,
|
|
2868
|
+
}));
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2806
2871
|
if (resumeSession) {
|
|
2807
2872
|
attemptMeta.resumedFromSession = resumeSession;
|
|
2808
2873
|
}
|
|
@@ -2837,7 +2902,8 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
|
|
|
2837
2902
|
maybeCompleteHijack();
|
|
2838
2903
|
};
|
|
2839
2904
|
let effectivePrompt = desc.prompt ?? "";
|
|
2840
|
-
|
|
2905
|
+
supportsNativeStructuredOutput = effectiveAgent.supportsNativeStructuredOutput === true;
|
|
2906
|
+
if (desc.outputTable && !supportsNativeStructuredOutput) {
|
|
2841
2907
|
const schemaDesc = describeSchemaShape(desc.outputTable, desc.outputSchema);
|
|
2842
2908
|
const jsonInstructions = [
|
|
2843
2909
|
"**REQUIRED OUTPUT** — You MUST end your response with a JSON object in a code fence matching this schema:",
|
|
@@ -2989,37 +3055,40 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
|
|
|
2989
3055
|
// Use fallback agent on retry attempts when available
|
|
2990
3056
|
let result;
|
|
2991
3057
|
try {
|
|
2992
|
-
result = await
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3058
|
+
result = await runPromisePreservingFailure(withSmithersSpan(smithersSpanNames.agent, Effect.tryPromise({
|
|
3059
|
+
try: () => {
|
|
3060
|
+
const agentCall = guidedResumeMessages?.length
|
|
3061
|
+
? {
|
|
3062
|
+
messages: guidedResumeMessages,
|
|
3063
|
+
}
|
|
3064
|
+
: {
|
|
3065
|
+
prompt: effectivePrompt,
|
|
3066
|
+
};
|
|
3067
|
+
return effectiveAgent.generate({
|
|
3068
|
+
options: undefined,
|
|
3069
|
+
abortSignal: taskSignal,
|
|
3070
|
+
...agentCall,
|
|
3071
|
+
resumeSession,
|
|
3072
|
+
lastHeartbeat: previousHeartbeat,
|
|
3073
|
+
rootDir: taskRoot,
|
|
3074
|
+
maxOutputBytes: toolConfig.maxOutputBytes,
|
|
3075
|
+
timeout: desc.timeoutMs
|
|
3076
|
+
? { totalMs: desc.timeoutMs }
|
|
3077
|
+
: undefined,
|
|
3078
|
+
onStdout: (text) => {
|
|
3079
|
+
recordInternalHeartbeat();
|
|
3080
|
+
emitOutput(text, "stdout");
|
|
3081
|
+
},
|
|
3082
|
+
onStderr: (text) => {
|
|
3083
|
+
recordInternalHeartbeat();
|
|
3084
|
+
emitOutput(text, "stderr");
|
|
3085
|
+
},
|
|
3086
|
+
onEvent: handleAgentEvent,
|
|
3087
|
+
onStepFinish: handleSdkStepFinish,
|
|
3088
|
+
outputSchema: desc.outputSchema,
|
|
3089
|
+
});
|
|
3090
|
+
},
|
|
3091
|
+
catch: (error) => error,
|
|
3023
3092
|
}), {
|
|
3024
3093
|
...taskSpanContext,
|
|
3025
3094
|
agent: attemptMeta.agentId ??
|
|
@@ -3100,10 +3169,12 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
|
|
|
3100
3169
|
// Fall back to parsing text/steps for JSON
|
|
3101
3170
|
if (output === undefined) {
|
|
3102
3171
|
const text = result.text ?? "";
|
|
3103
|
-
// Try to parse the whole text as JSON first
|
|
3172
|
+
// Try to parse the whole text as JSON first. Strip a leading
|
|
3173
|
+
// UTF-8 BOM and accept either object or array at the root,
|
|
3174
|
+
// since Zod schemas occasionally validate arrays.
|
|
3104
3175
|
try {
|
|
3105
|
-
const trimmed = text.trim();
|
|
3106
|
-
if (trimmed.startsWith("{")) {
|
|
3176
|
+
const trimmed = text.replace(/^\uFEFF/, "").trim();
|
|
3177
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
3107
3178
|
output = JSON.parse(trimmed);
|
|
3108
3179
|
}
|
|
3109
3180
|
}
|
|
@@ -3280,14 +3351,28 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
|
|
|
3280
3351
|
const retryText = retryResult.text ?? "";
|
|
3281
3352
|
responseText = retryText || responseText;
|
|
3282
3353
|
try {
|
|
3283
|
-
const trimmed = retryText.trim();
|
|
3284
|
-
if (trimmed.startsWith("{")) {
|
|
3354
|
+
const trimmed = retryText.replace(/^\uFEFF/, "").trim();
|
|
3355
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
3285
3356
|
output = JSON.parse(trimmed);
|
|
3286
3357
|
}
|
|
3287
3358
|
}
|
|
3288
3359
|
catch {
|
|
3289
3360
|
// Still not valid JSON
|
|
3290
3361
|
}
|
|
3362
|
+
if (output === undefined) {
|
|
3363
|
+
// Try extracting JSON from a markdown code fence
|
|
3364
|
+
// (```json ... ``` or just ``` ... ```).
|
|
3365
|
+
const fenceMatch = retryText.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
3366
|
+
if (fenceMatch) {
|
|
3367
|
+
const inner = fenceMatch[1].trim();
|
|
3368
|
+
try {
|
|
3369
|
+
output = JSON.parse(inner);
|
|
3370
|
+
}
|
|
3371
|
+
catch {
|
|
3372
|
+
// Fall through to balanced extraction
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3291
3376
|
if (output === undefined) {
|
|
3292
3377
|
// Try extracting balanced JSON from retry text
|
|
3293
3378
|
const jsonStr = extractBalancedJson(retryText);
|
|
@@ -3303,8 +3388,6 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
|
|
|
3303
3388
|
}
|
|
3304
3389
|
if (output === undefined) {
|
|
3305
3390
|
// Debug: log what we have
|
|
3306
|
-
const debugSteps = result.steps ?? [];
|
|
3307
|
-
const stepTexts = debugSteps.map((s, i) => `Step ${i}: ${(s?.text ?? "").slice(0, 200)}`);
|
|
3308
3391
|
const finishReason = result.finishReason ?? "unknown";
|
|
3309
3392
|
logDebug("agent response did not contain valid JSON output", {
|
|
3310
3393
|
runId,
|
|
@@ -3319,7 +3402,11 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
|
|
|
3319
3402
|
lastStepText: debugSteps[debugSteps.length - 1]?.text?.slice(0, 500) ??
|
|
3320
3403
|
"none",
|
|
3321
3404
|
}, "engine:task-json");
|
|
3322
|
-
|
|
3405
|
+
const tail = (text ?? "").slice(-200).replace(/\s+/g, " ").trim();
|
|
3406
|
+
const tailHint = tail
|
|
3407
|
+
? ` Last 200 chars of response: ${JSON.stringify(tail)}`
|
|
3408
|
+
: " Agent returned an empty response.";
|
|
3409
|
+
throw new SmithersError("INVALID_OUTPUT", `No valid JSON output found in agent response (finishReason=${finishReason}, textLength=${text.length}).${tailHint}`);
|
|
3323
3410
|
}
|
|
3324
3411
|
}
|
|
3325
3412
|
// Output should already be parsed, but handle string case
|
|
@@ -3327,7 +3414,7 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
|
|
|
3327
3414
|
try {
|
|
3328
3415
|
payload = JSON.parse(output);
|
|
3329
3416
|
}
|
|
3330
|
-
catch
|
|
3417
|
+
catch {
|
|
3331
3418
|
throw new SmithersError("INVALID_OUTPUT", `Failed to parse agent output as JSON. Output starts with: "${output.slice(0, 100)}"`);
|
|
3332
3419
|
}
|
|
3333
3420
|
}
|
|
@@ -3428,17 +3515,25 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
|
|
|
3428
3515
|
const zodIssues = validation.error?.issues
|
|
3429
3516
|
?.map((iss) => ` - ${(iss.path ?? []).join(".")}: ${iss.message}`)
|
|
3430
3517
|
.join("\n") ?? "Unknown validation error";
|
|
3431
|
-
const schemaRetryPrompt =
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3518
|
+
const schemaRetryPrompt = supportsNativeStructuredOutput
|
|
3519
|
+
? [
|
|
3520
|
+
`Your structured output didn't match the required schema. Validation errors:`,
|
|
3521
|
+
zodIssues,
|
|
3522
|
+
``,
|
|
3523
|
+
`Return corrected structured data matching this schema:`,
|
|
3524
|
+
schemaDesc,
|
|
3525
|
+
].join("\n")
|
|
3526
|
+
: [
|
|
3527
|
+
`Your output didn't match the required schema. Validation errors:`,
|
|
3528
|
+
zodIssues,
|
|
3529
|
+
``,
|
|
3530
|
+
`Please return valid JSON matching the schema exactly.`,
|
|
3531
|
+
``,
|
|
3532
|
+
`You MUST output ONLY a valid JSON object with exactly these fields and types:`,
|
|
3533
|
+
schemaDesc,
|
|
3534
|
+
``,
|
|
3535
|
+
`Output ONLY the JSON object, no other text.`,
|
|
3536
|
+
].join("\n");
|
|
3442
3537
|
logInfo("schema validation retry", {
|
|
3443
3538
|
runId,
|
|
3444
3539
|
nodeId: desc.nodeId,
|
|
@@ -3468,6 +3563,9 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
|
|
|
3468
3563
|
recordInternalHeartbeat();
|
|
3469
3564
|
emitOutput(text, "stderr");
|
|
3470
3565
|
},
|
|
3566
|
+
...(supportsNativeStructuredOutput
|
|
3567
|
+
? { outputSchema: desc.outputSchema }
|
|
3568
|
+
: {}),
|
|
3471
3569
|
});
|
|
3472
3570
|
const retryText = (schemaRetryResult.text ?? "").trim();
|
|
3473
3571
|
responseText = retryText || responseText;
|
|
@@ -3489,8 +3587,24 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
|
|
|
3489
3587
|
cloneJsonValue(schemaRetryMessages) ?? schemaRetryMessages;
|
|
3490
3588
|
// Try to parse the retry response
|
|
3491
3589
|
let retryOutput;
|
|
3590
|
+
if (supportsNativeStructuredOutput) {
|
|
3591
|
+
try {
|
|
3592
|
+
if (schemaRetryResult._output !== undefined &&
|
|
3593
|
+
schemaRetryResult._output !== null) {
|
|
3594
|
+
retryOutput = schemaRetryResult._output;
|
|
3595
|
+
}
|
|
3596
|
+
else if (schemaRetryResult.output !== undefined &&
|
|
3597
|
+
schemaRetryResult.output !== null) {
|
|
3598
|
+
retryOutput = schemaRetryResult.output;
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
catch {
|
|
3602
|
+
// Structured output access threw; fall back to text parsing.
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3492
3605
|
try {
|
|
3493
|
-
if (
|
|
3606
|
+
if (retryOutput === undefined &&
|
|
3607
|
+
(retryText.startsWith("{") || retryText.startsWith("["))) {
|
|
3494
3608
|
retryOutput = JSON.parse(retryText);
|
|
3495
3609
|
}
|
|
3496
3610
|
}
|
|
@@ -3679,6 +3793,30 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
|
|
|
3679
3793
|
if (isHeartbeatPayloadValidationError(effectiveError)) {
|
|
3680
3794
|
attemptMeta.failureRetryable = false;
|
|
3681
3795
|
}
|
|
3796
|
+
// Allow agents (e.g. BaseCliAgent on "LLM not set") to flag a failure as
|
|
3797
|
+
// non-retryable via SmithersError details. Without this, the engine would
|
|
3798
|
+
// retry deterministic configuration errors up to desc.retries times.
|
|
3799
|
+
if (effectiveError &&
|
|
3800
|
+
typeof effectiveError === "object" &&
|
|
3801
|
+
// @ts-ignore — duck-type on SmithersError shape
|
|
3802
|
+
(effectiveError.details?.failureRetryable === false ||
|
|
3803
|
+
// @ts-ignore
|
|
3804
|
+
effectiveError.code === "AGENT_CONFIG_INVALID")) {
|
|
3805
|
+
attemptMeta.failureRetryable = false;
|
|
3806
|
+
}
|
|
3807
|
+
// Honour `discardResumeSession: true` from agent-side errors (e.g. kimi
|
|
3808
|
+
// session-loss). The next attempt's resumeSession resolution checks
|
|
3809
|
+
// attemptMeta.discardResumeSession on the most recent failed attempt
|
|
3810
|
+
// and clears the captured agentResume so the agent starts fresh
|
|
3811
|
+
// instead of redundantly trying to resume a corrupt session.
|
|
3812
|
+
if (effectiveError &&
|
|
3813
|
+
typeof effectiveError === "object" &&
|
|
3814
|
+
// @ts-ignore — duck-type on SmithersError shape
|
|
3815
|
+
effectiveError.details &&
|
|
3816
|
+
// @ts-ignore
|
|
3817
|
+
effectiveError.details.discardResumeSession === true) {
|
|
3818
|
+
attemptMeta.discardResumeSession = true;
|
|
3819
|
+
}
|
|
3682
3820
|
if (!heartbeatTimeoutError && (taskSignal.aborted || isAbortError(err))) {
|
|
3683
3821
|
await waitForHeartbeatWriteDrain();
|
|
3684
3822
|
await flushHeartbeat(true);
|
package/src/hot/watch.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { watch } from "node:fs";
|
|
2
|
-
import { readdir
|
|
3
|
-
import { resolve
|
|
2
|
+
import { readdir } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
4
|
import { Effect } from "effect";
|
|
5
5
|
import { toSmithersError } from "@smithers-orchestrator/errors/toSmithersError";
|
|
6
6
|
import { logDebug, logInfo } from "@smithers-orchestrator/observability/logging";
|
package/src/signals.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Effect } from "effect";
|
|
2
|
-
import { SmithersDb } from "@smithers-orchestrator/db/adapter";
|
|
3
2
|
import { bridgeSignalResolve } from "./effect/durable-deferred-bridge.js";
|
|
4
3
|
import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
|
|
5
4
|
import { nowMs } from "@smithers-orchestrator/scheduler/nowMs";
|