@smithers-orchestrator/engine 0.17.0 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smithers-orchestrator/engine",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Concrete Smithers workflow execution engine",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -33,20 +33,20 @@
33
33
  "react": "^19.2.5",
34
34
  "react-dom": "^19.2.5",
35
35
  "zod": "^4.3.6",
36
- "@smithers-orchestrator/agents": "0.17.0",
37
- "@smithers-orchestrator/components": "0.17.0",
38
- "@smithers-orchestrator/driver": "0.17.0",
39
- "@smithers-orchestrator/db": "0.17.0",
40
- "@smithers-orchestrator/errors": "0.17.0",
41
- "@smithers-orchestrator/graph": "0.17.0",
42
- "@smithers-orchestrator/memory": "0.17.0",
43
- "@smithers-orchestrator/react-reconciler": "0.17.0",
44
- "@smithers-orchestrator/observability": "0.17.0",
45
- "@smithers-orchestrator/sandbox": "0.17.0",
46
- "@smithers-orchestrator/scheduler": "0.17.0",
47
- "@smithers-orchestrator/scorers": "0.17.0",
48
- "@smithers-orchestrator/time-travel": "0.17.0",
49
- "@smithers-orchestrator/vcs": "0.17.0"
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"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/bun": "latest",
@@ -72,8 +72,12 @@ function isRetryableBridgeTaskFailure(attempt) {
72
72
  if (meta?.failureRetryable === false) {
73
73
  return false;
74
74
  }
75
+ const errorCode = parseAttemptErrorCode(attempt?.errorJson);
76
+ if (errorCode === "AGENT_CONFIG_INVALID") {
77
+ return false;
78
+ }
75
79
  const kind = typeof meta?.kind === "string" ? meta.kind : null;
76
- return !(kind !== "agent" && parseAttemptErrorCode(attempt?.errorJson) === "INVALID_OUTPUT");
80
+ return !(kind !== "agent" && errorCode === "INVALID_OUTPUT");
77
81
  }
78
82
  /**
79
83
  * @param {SmithersDb} adapter
package/src/engine.js CHANGED
@@ -25,7 +25,7 @@ import { findVcsRoot } from "@smithers-orchestrator/vcs/find-root";
25
25
  import * as BunContext from "@effect/platform-bun/BunContext";
26
26
  import { eq, getTableName } from "drizzle-orm";
27
27
  import { getTableColumns } from "drizzle-orm/utils";
28
- import { Chunk, Duration, Effect, Fiber, Metric, Queue, Schedule } from "effect";
28
+ import { Cause, Chunk, Duration, Effect, Exit, Fiber, Metric, Queue, Schedule } from "effect";
29
29
  import { attemptDuration, cacheHits, cacheMisses, nodeDuration, promptSizeBytes, responseSizeBytes, runDuration, runsResumedTotal, schedulerConcurrencyUtilization, schedulerQueueDepth, schedulerWaitDuration, trackEvent, } from "@smithers-orchestrator/observability/metrics";
30
30
  import { runScorersAsync } from "@smithers-orchestrator/scorers/run-scorers";
31
31
  import { dirname, resolve } from "node:path";
@@ -287,6 +287,25 @@ function isHeartbeatPayloadValidationError(err) {
287
287
  return (code === "HEARTBEAT_PAYLOAD_NOT_JSON_SERIALIZABLE" ||
288
288
  code === "HEARTBEAT_PAYLOAD_TOO_LARGE");
289
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
+ }
290
309
  /**
291
310
  * @param {Record<string, unknown>} meta
292
311
  * @param {string} engine
@@ -1784,6 +1803,7 @@ function resolveTaskOutputs(tasks, workflow) {
1784
1803
  if (isTimerTask(task)) {
1785
1804
  continue;
1786
1805
  }
1806
+ const hasAmbiguousOutputRef = Boolean(task.outputRef && workflow.ambiguousZodSchemas?.has(task.outputRef));
1787
1807
  // Already resolved (has a table)
1788
1808
  if (task.outputTable) {
1789
1809
  if (!task.outputSchema && task.outputTableName && workflow.schemaRegistry) {
@@ -1807,10 +1827,14 @@ function resolveTaskOutputs(tasks, workflow) {
1807
1827
  }
1808
1828
  }
1809
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
+ }
1810
1833
  throw new SmithersError("UNKNOWN_OUTPUT_SCHEMA", `Task "${task.nodeId}" uses an output ZodObject that is not registered in createSmithers()`);
1811
1834
  }
1812
1835
  }
1813
1836
  const raw = task.outputSchema;
1837
+ const hasAmbiguousOutputSchema = Boolean(raw && typeof raw === "object" && workflow.ambiguousZodSchemas?.has(raw));
1814
1838
  // Resolve ZodObject via outputSchema when no outputRef resolved.
1815
1839
  if (!task.outputTable && raw && typeof raw === "object" && workflow.zodToKeyName) {
1816
1840
  const keyName = workflow.zodToKeyName.get(raw);
@@ -1824,6 +1848,9 @@ function resolveTaskOutputs(tasks, workflow) {
1824
1848
  }
1825
1849
  }
1826
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
+ }
1827
1854
  throw new SmithersError("UNKNOWN_OUTPUT_SCHEMA", `Task "${task.nodeId}" uses an output ZodObject that is not registered in createSmithers()`);
1828
1855
  }
1829
1856
  }
@@ -2554,6 +2581,7 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
2554
2581
  let cacheJjBase = null;
2555
2582
  let responseText = null;
2556
2583
  let effectiveAgent = null;
2584
+ let supportsNativeStructuredOutput = false;
2557
2585
  // Resolve effective root once so both caching and execution share it.
2558
2586
  const taskRoot = desc.worktreePath ?? toolConfig.rootDir;
2559
2587
  const stepCacheEnabled = cacheEnabled || Boolean(desc.cachePolicy);
@@ -2874,7 +2902,8 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
2874
2902
  maybeCompleteHijack();
2875
2903
  };
2876
2904
  let effectivePrompt = desc.prompt ?? "";
2877
- if (desc.outputTable) {
2905
+ supportsNativeStructuredOutput = effectiveAgent.supportsNativeStructuredOutput === true;
2906
+ if (desc.outputTable && !supportsNativeStructuredOutput) {
2878
2907
  const schemaDesc = describeSchemaShape(desc.outputTable, desc.outputSchema);
2879
2908
  const jsonInstructions = [
2880
2909
  "**REQUIRED OUTPUT** — You MUST end your response with a JSON object in a code fence matching this schema:",
@@ -3026,7 +3055,7 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
3026
3055
  // Use fallback agent on retry attempts when available
3027
3056
  let result;
3028
3057
  try {
3029
- result = await Effect.runPromise(withSmithersSpan(smithersSpanNames.agent, Effect.tryPromise({
3058
+ result = await runPromisePreservingFailure(withSmithersSpan(smithersSpanNames.agent, Effect.tryPromise({
3030
3059
  try: () => {
3031
3060
  const agentCall = guidedResumeMessages?.length
3032
3061
  ? {
@@ -3486,17 +3515,25 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
3486
3515
  const zodIssues = validation.error?.issues
3487
3516
  ?.map((iss) => ` - ${(iss.path ?? []).join(".")}: ${iss.message}`)
3488
3517
  .join("\n") ?? "Unknown validation error";
3489
- const schemaRetryPrompt = [
3490
- `Your output didn't match the required schema. Validation errors:`,
3491
- zodIssues,
3492
- ``,
3493
- `Please return valid JSON matching the schema exactly.`,
3494
- ``,
3495
- `You MUST output ONLY a valid JSON object with exactly these fields and types:`,
3496
- schemaDesc,
3497
- ``,
3498
- `Output ONLY the JSON object, no other text.`,
3499
- ].join("\n");
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");
3500
3537
  logInfo("schema validation retry", {
3501
3538
  runId,
3502
3539
  nodeId: desc.nodeId,
@@ -3526,6 +3563,9 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
3526
3563
  recordInternalHeartbeat();
3527
3564
  emitOutput(text, "stderr");
3528
3565
  },
3566
+ ...(supportsNativeStructuredOutput
3567
+ ? { outputSchema: desc.outputSchema }
3568
+ : {}),
3529
3569
  });
3530
3570
  const retryText = (schemaRetryResult.text ?? "").trim();
3531
3571
  responseText = retryText || responseText;
@@ -3547,8 +3587,24 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
3547
3587
  cloneJsonValue(schemaRetryMessages) ?? schemaRetryMessages;
3548
3588
  // Try to parse the retry response
3549
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
+ }
3550
3605
  try {
3551
- if (retryText.startsWith("{") || retryText.startsWith("[")) {
3606
+ if (retryOutput === undefined &&
3607
+ (retryText.startsWith("{") || retryText.startsWith("["))) {
3552
3608
  retryOutput = JSON.parse(retryText);
3553
3609
  }
3554
3610
  }
@@ -3743,9 +3799,9 @@ async function legacyExecuteTask(adapter, db, runId, desc, descriptorMap, inputT
3743
3799
  if (effectiveError &&
3744
3800
  typeof effectiveError === "object" &&
3745
3801
  // @ts-ignore — duck-type on SmithersError shape
3746
- effectiveError.details &&
3747
- // @ts-ignore
3748
- effectiveError.details.failureRetryable === false) {
3802
+ (effectiveError.details?.failureRetryable === false ||
3803
+ // @ts-ignore
3804
+ effectiveError.code === "AGENT_CONFIG_INVALID")) {
3749
3805
  attemptMeta.failureRetryable = false;
3750
3806
  }
3751
3807
  // Honour `discardResumeSession: true` from agent-side errors (e.g. kimi