@smithers-orchestrator/graph 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smithers-orchestrator/graph",
3
- "version": "0.16.9",
3
+ "version": "0.18.0",
4
4
  "description": "Framework-neutral Smithers workflow graph model and extraction helpers",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -20,8 +20,10 @@
20
20
  "src/"
21
21
  ],
22
22
  "dependencies": {
23
- "@smithers-orchestrator/agents": "0.16.9",
24
- "@smithers-orchestrator/errors": "0.16.9"
23
+ "drizzle-orm": "^0.45.2",
24
+ "zod": "^4.3.6",
25
+ "@smithers-orchestrator/agents": "0.18.0",
26
+ "@smithers-orchestrator/errors": "0.18.0"
25
27
  },
26
28
  "devDependencies": {
27
29
  "@types/bun": "latest",
@@ -24,11 +24,28 @@ import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
24
24
  // differ, so replacing this implementation would not produce identical output
25
25
  // for all inputs.
26
26
  const loadRuntimeModule = new Function("specifier", "return import(specifier)");
27
- // CLI agents (Claude Code, Codex, etc.) can spend minutes reading files and
28
- // thinking without producing stdout. 60s was too aggressive and caused
29
- // spurious aborts on complex spec/research/plan generation tasks.
30
- const DEFAULT_LOCAL_TASK_HEARTBEAT_TIMEOUT_MS = 300_000;
31
- const DEFAULT_SANDBOX_TASK_HEARTBEAT_TIMEOUT_MS = 300_000;
27
+ // CLI agents (Claude Code, Codex, Gemini, Kimi) can spend many minutes reading
28
+ // files and thinking without producing stdout. 5 min was still too aggressive:
29
+ // reviewer agents on substantive diffs were getting killed mid-review and
30
+ // breaking ValidationLoop. 10 min matches the explicit per-task overrides used
31
+ // by implement/validate tasks.
32
+ //
33
+ // The default can be overridden at runtime via the SMITHERS_TASK_HEARTBEAT_MS
34
+ // environment variable.
35
+ const HEARTBEAT_DEFAULT_MS = 600_000;
36
+ function envHeartbeatTimeoutMs() {
37
+ const raw = typeof process !== "undefined" && process?.env
38
+ ? process.env.SMITHERS_TASK_HEARTBEAT_MS
39
+ : undefined;
40
+ if (typeof raw !== "string" || raw.length === 0)
41
+ return HEARTBEAT_DEFAULT_MS;
42
+ const parsed = Number(raw);
43
+ if (!Number.isFinite(parsed) || parsed <= 0)
44
+ return HEARTBEAT_DEFAULT_MS;
45
+ return Math.floor(parsed);
46
+ }
47
+ const DEFAULT_LOCAL_TASK_HEARTBEAT_TIMEOUT_MS = envHeartbeatTimeoutMs();
48
+ const DEFAULT_SANDBOX_TASK_HEARTBEAT_TIMEOUT_MS = envHeartbeatTimeoutMs();
32
49
  /**
33
50
  * @param {unknown} value
34
51
  * @returns {boolean}
@@ -101,17 +118,24 @@ function getRalphIteration(opts, id) {
101
118
  /**
102
119
  * @param {Record<string, unknown>} raw
103
120
  */
104
- function resolveRetryConfig(raw) {
121
+ function resolveRetryConfig(raw, isAgent = false) {
105
122
  const noRetry = Boolean(raw.noRetry);
106
123
  const continueOnFail = Boolean(raw.continueOnFail);
107
124
  const hasExplicitRetries = typeof raw.retries === "number" && !Number.isNaN(raw.retries);
108
125
  const hasExplicitRetryPolicy = Boolean(raw.retryPolicy && typeof raw.retryPolicy === "object");
109
126
  const defaultNoRetryForContinueOnFail = continueOnFail && !hasExplicitRetries && !hasExplicitRetryPolicy;
110
- const retries = noRetry || defaultNoRetryForContinueOnFail
127
+ // Agent tasks (CLI agents like Codex/Claude) can hit transient upstream
128
+ // failures (e.g. "thread <uuid> not found"). Give them at least one free
129
+ // retry by default — even when the user opted into continueOnFail —
130
+ // unless they explicitly requested noRetry.
131
+ const baseRetries = noRetry
111
132
  ? 0
112
- : hasExplicitRetries
113
- ? /** @type {number} */ (raw.retries)
114
- : Infinity;
133
+ : defaultNoRetryForContinueOnFail
134
+ ? (isAgent ? 1 : 0)
135
+ : hasExplicitRetries
136
+ ? /** @type {number} */ (raw.retries)
137
+ : Infinity;
138
+ const retries = baseRetries;
115
139
  const retryPolicy = hasExplicitRetryPolicy
116
140
  ? /** @type {import("../RetryPolicy.ts").RetryPolicy} */ (raw.retryPolicy)
117
141
  : retries > 0
@@ -404,7 +428,13 @@ export function extractFromHost(root, opts) {
404
428
  prompt: undefined,
405
429
  staticPayload: undefined,
406
430
  computeFn: async () => {
407
- const { executeSandbox } = await loadRuntimeModule("@smithers-orchestrator/sandbox/execute");
431
+ const [
432
+ { executeSandbox },
433
+ { executeChildWorkflow },
434
+ ] = await Promise.all([
435
+ loadRuntimeModule("@smithers-orchestrator/sandbox/execute"),
436
+ loadRuntimeModule("@smithers-orchestrator/engine/child-workflow"),
437
+ ]);
408
438
  if (!workflowDef) {
409
439
  throw new SmithersError("INVALID_INPUT", `Sandbox ${nodeId} is missing workflow definition.`, { nodeId });
410
440
  }
@@ -417,6 +447,7 @@ export function extractFromHost(root, opts) {
417
447
  ? runtime
418
448
  : "bubblewrap",
419
449
  workflow: workflowDef,
450
+ executeChildWorkflow,
420
451
  input: raw.__smithersSandboxInput ?? raw.input,
421
452
  rootDir: topWorktree?.path ?? process.cwd(),
422
453
  allowNetwork: Boolean(raw.allowNetwork),
@@ -703,14 +734,14 @@ export function extractFromHost(root, opts) {
703
734
  }
704
735
  : undefined;
705
736
  const skipIf = Boolean(raw.skipIf);
706
- const { retries, retryPolicy } = resolveRetryConfig(raw);
737
+ const agent = raw.agent;
738
+ const kind = raw.__smithersKind;
739
+ const isAgent = kind === "agent" || Boolean(agent);
740
+ const { retries, retryPolicy } = resolveRetryConfig(raw, isAgent);
707
741
  const timeoutMs = typeof raw.timeoutMs === "number" ? raw.timeoutMs : null;
708
742
  const parsedHeartbeatTimeoutMs = parseHeartbeatTimeoutMs(raw);
709
743
  const continueOnFail = Boolean(raw.continueOnFail);
710
744
  const cachePolicy = raw.cache && typeof raw.cache === "object" ? raw.cache : undefined;
711
- const agent = raw.agent;
712
- const kind = raw.__smithersKind;
713
- const isAgent = kind === "agent" || Boolean(agent);
714
745
  const heartbeatTimeoutMs = parsedHeartbeatTimeoutMs ??
715
746
  (isAgent ? DEFAULT_LOCAL_TASK_HEARTBEAT_TIMEOUT_MS : null);
716
747
  const prompt = isAgent ? String(raw.children ?? "") : undefined;
@@ -761,6 +792,8 @@ export function extractFromHost(root, opts) {
761
792
  heartbeatTimeoutMs,
762
793
  continueOnFail,
763
794
  cachePolicy,
795
+ hijack: Boolean(raw.hijack),
796
+ onHijackExit: raw.onHijackExit === "reopen" ? "reopen" : "complete",
764
797
  agent,
765
798
  prompt,
766
799
  staticPayload,
package/src/extract.js CHANGED
@@ -8,8 +8,25 @@ import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
8
8
 
9
9
  const DEFAULT_MERGE_QUEUE_CONCURRENCY = 1;
10
10
  const WORKTREE_EMPTY_PATH_ERROR = "<Worktree> requires a non-empty path prop";
11
- const DEFAULT_LOCAL_TASK_HEARTBEAT_TIMEOUT_MS = 300_000;
12
- const DEFAULT_SANDBOX_TASK_HEARTBEAT_TIMEOUT_MS = 300_000;
11
+ // Default per-task heartbeat timeout. 10 min is the floor for agent-backed
12
+ // tasks: LLM CLIs (claude, codex, gemini, kimi) can sit silent for several
13
+ // minutes during long deliberation or large-context reads. The previous
14
+ // 5-minute default killed reviewer agents mid-review and broke ValidationLoop.
15
+ // Overridable at runtime via SMITHERS_TASK_HEARTBEAT_MS.
16
+ const HEARTBEAT_DEFAULT_MS = 600_000;
17
+ function envHeartbeatTimeoutMs() {
18
+ const raw = typeof process !== "undefined" && process?.env
19
+ ? process.env.SMITHERS_TASK_HEARTBEAT_MS
20
+ : undefined;
21
+ if (typeof raw !== "string" || raw.length === 0)
22
+ return HEARTBEAT_DEFAULT_MS;
23
+ const parsed = Number(raw);
24
+ if (!Number.isFinite(parsed) || parsed <= 0)
25
+ return HEARTBEAT_DEFAULT_MS;
26
+ return Math.floor(parsed);
27
+ }
28
+ const DEFAULT_LOCAL_TASK_HEARTBEAT_TIMEOUT_MS = envHeartbeatTimeoutMs();
29
+ const DEFAULT_SANDBOX_TASK_HEARTBEAT_TIMEOUT_MS = envHeartbeatTimeoutMs();
13
30
  /**
14
31
  * @param {string} prefix
15
32
  * @param {readonly number[]} path
@@ -102,17 +119,23 @@ function parseHeartbeatTimeoutMs(raw) {
102
119
  /**
103
120
  * @param {Record<string, unknown>} raw
104
121
  */
105
- function resolveRetryConfig(raw) {
122
+ function resolveRetryConfig(raw, isAgent = false) {
106
123
  const noRetry = Boolean(raw.noRetry);
107
124
  const continueOnFail = Boolean(raw.continueOnFail);
108
125
  const hasExplicitRetries = typeof raw.retries === "number" && !Number.isNaN(raw.retries);
109
126
  const hasExplicitRetryPolicy = Boolean(raw.retryPolicy && typeof raw.retryPolicy === "object");
110
127
  const defaultNoRetryForContinueOnFail = continueOnFail && !hasExplicitRetries && !hasExplicitRetryPolicy;
111
- const retries = noRetry || defaultNoRetryForContinueOnFail
128
+ // Agent tasks (CLI agents like Codex/Claude) can hit transient upstream
129
+ // failures (e.g. "thread <uuid> not found"). Give them at least one free
130
+ // retry by default — even when the user opted into continueOnFail —
131
+ // unless they explicitly requested noRetry.
132
+ const retries = noRetry
112
133
  ? 0
113
- : hasExplicitRetries
114
- ? raw.retries
115
- : Infinity;
134
+ : defaultNoRetryForContinueOnFail
135
+ ? (isAgent ? 1 : 0)
136
+ : hasExplicitRetries
137
+ ? raw.retries
138
+ : Infinity;
116
139
  const retryPolicy = hasExplicitRetryPolicy
117
140
  ? raw.retryPolicy
118
141
  : retries > 0
@@ -538,9 +561,9 @@ export function extractGraph(root, opts) {
538
561
  raw.approvalOnDeny === "fail"
539
562
  ? raw.approvalOnDeny
540
563
  : undefined;
541
- const { retries, retryPolicy } = resolveRetryConfig(raw);
542
564
  const kind = raw.__smithersKind;
543
565
  const isAgent = kind === "agent" || Boolean(raw.agent);
566
+ const { retries, retryPolicy } = resolveRetryConfig(raw, isAgent);
544
567
  const isCompute = kind === "compute" && typeof raw.__smithersComputeFn === "function";
545
568
  const parsedHeartbeatTimeoutMs = parseHeartbeatTimeoutMs(raw);
546
569
  const heartbeatTimeoutMs = parsedHeartbeatTimeoutMs ??
@@ -569,6 +592,8 @@ export function extractGraph(root, opts) {
569
592
  cachePolicy: raw.cache && typeof raw.cache === "object"
570
593
  ? raw.cache
571
594
  : undefined,
595
+ hijack: Boolean(raw.hijack),
596
+ onHijackExit: raw.onHijackExit === "reopen" ? "reopen" : "complete",
572
597
  agent: raw.agent,
573
598
  prompt,
574
599
  staticPayload: isAgent || isCompute
package/src/index.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { z } from 'zod';
2
- import { AgentCapabilityRegistry } from '@smithers-orchestrator/agents/capability-registry';
3
2
 
4
3
  type XmlNode$1 = XmlElement$1 | XmlText$1;
5
4
  type XmlElement$1 = {
@@ -42,7 +41,19 @@ type CachePolicy$1<Ctx = unknown> = {
42
41
  type AgentLike$1 = {
43
42
  id?: string;
44
43
  tools?: Record<string, unknown>;
45
- capabilities?: AgentCapabilityRegistry;
44
+ supportsNativeStructuredOutput?: boolean;
45
+ capabilities?: {
46
+ version: 1;
47
+ engine: string;
48
+ runtimeTools: Record<string, {
49
+ description?: string;
50
+ source?: string;
51
+ }>;
52
+ mcp: Record<string, unknown>;
53
+ skills: Record<string, unknown>;
54
+ humanInteraction: Record<string, unknown>;
55
+ builtIns: string[];
56
+ };
46
57
  generate: (args: unknown) => Promise<unknown>;
47
58
  };
48
59
  type ScoreResult$1 = {
@@ -138,6 +149,8 @@ type TaskDescriptor$1 = {
138
149
  heartbeatTimeoutMs: number | null;
139
150
  continueOnFail: boolean;
140
151
  cachePolicy?: CachePolicy$1;
152
+ hijack?: boolean;
153
+ onHijackExit?: "complete" | "reopen";
141
154
  agent?: AgentLike$1 | AgentLike$1[];
142
155
  prompt?: string;
143
156
  staticPayload?: unknown;
package/src/types.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import type { z } from "zod";
2
- import type { AgentCapabilityRegistry } from "@smithers-orchestrator/agents/capability-registry";
3
2
 
4
3
  export type XmlNode = XmlElement | XmlText;
5
4
 
@@ -51,7 +50,16 @@ export type CachePolicy<Ctx = unknown> = {
51
50
  export type AgentLike = {
52
51
  id?: string;
53
52
  tools?: Record<string, unknown>;
54
- capabilities?: AgentCapabilityRegistry;
53
+ supportsNativeStructuredOutput?: boolean;
54
+ capabilities?: {
55
+ version: 1;
56
+ engine: string;
57
+ runtimeTools: Record<string, { description?: string; source?: string }>;
58
+ mcp: Record<string, unknown>;
59
+ skills: Record<string, unknown>;
60
+ humanInteraction: Record<string, unknown>;
61
+ builtIns: string[];
62
+ };
55
63
  generate: (args: unknown) => Promise<unknown>;
56
64
  };
57
65
 
@@ -155,6 +163,8 @@ export type TaskDescriptor = {
155
163
  heartbeatTimeoutMs: number | null;
156
164
  continueOnFail: boolean;
157
165
  cachePolicy?: CachePolicy;
166
+ hijack?: boolean;
167
+ onHijackExit?: "complete" | "reopen";
158
168
  agent?: AgentLike | AgentLike[];
159
169
  prompt?: string;
160
170
  staticPayload?: unknown;