@smithers-orchestrator/components 0.20.4 → 0.22.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/components",
3
- "version": "0.20.4",
3
+ "version": "0.22.0",
4
4
  "description": "React components for Smithers workflows",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -24,15 +24,15 @@
24
24
  "react": "^19.2.5",
25
25
  "react-dom": "^19.2.5",
26
26
  "zod": "^4.3.6",
27
- "@smithers-orchestrator/agents": "0.20.4",
28
- "@smithers-orchestrator/db": "0.20.4",
29
- "@smithers-orchestrator/driver": "0.20.4",
30
- "@smithers-orchestrator/errors": "0.20.4",
31
- "@smithers-orchestrator/graph": "0.20.4",
32
- "@smithers-orchestrator/react-reconciler": "0.20.4",
33
- "@smithers-orchestrator/memory": "0.20.4",
34
- "@smithers-orchestrator/scheduler": "0.20.4",
35
- "@smithers-orchestrator/observability": "0.20.4"
27
+ "@smithers-orchestrator/db": "0.22.0",
28
+ "@smithers-orchestrator/agents": "0.22.0",
29
+ "@smithers-orchestrator/errors": "0.22.0",
30
+ "@smithers-orchestrator/driver": "0.22.0",
31
+ "@smithers-orchestrator/memory": "0.22.0",
32
+ "@smithers-orchestrator/graph": "0.22.0",
33
+ "@smithers-orchestrator/observability": "0.22.0",
34
+ "@smithers-orchestrator/react-reconciler": "0.22.0",
35
+ "@smithers-orchestrator/scheduler": "0.22.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@tanstack/react-query": "^5.99.1",
@@ -123,16 +123,18 @@ export function Approval(props) {
123
123
  if ((mode === "select" || mode === "rank") && (!options || options.length === 0)) {
124
124
  throw new SmithersError("APPROVAL_OPTIONS_REQUIRED", `Approval ${props.id} requires options when mode="${mode}".`);
125
125
  }
126
+ const conditionMet = props.autoApprove
127
+ ? evaluateBooleanCallback(props.autoApprove.condition, ctx)
128
+ : undefined;
129
+ const revertOnMet = props.autoApprove
130
+ ? evaluateBooleanCallback(props.autoApprove.revertOn, ctx)
131
+ : undefined;
126
132
  const autoApprove = props.autoApprove
127
133
  ? {
128
134
  ...(typeof props.autoApprove.after === "number" ? { after: props.autoApprove.after } : {}),
129
135
  audit: props.autoApprove.audit !== false,
130
- ...(evaluateBooleanCallback(props.autoApprove.condition, ctx) !== undefined
131
- ? { conditionMet: evaluateBooleanCallback(props.autoApprove.condition, ctx) }
132
- : {}),
133
- ...(evaluateBooleanCallback(props.autoApprove.revertOn, ctx) !== undefined
134
- ? { revertOnMet: evaluateBooleanCallback(props.autoApprove.revertOn, ctx) }
135
- : {}),
136
+ ...(conditionMet !== undefined ? { conditionMet } : {}),
137
+ ...(revertOnMet !== undefined ? { revertOnMet } : {}),
136
138
  }
137
139
  : undefined;
138
140
  const requestMeta = {
@@ -177,7 +179,7 @@ export function Approval(props) {
177
179
  approved: approval?.status === "approved",
178
180
  note: approval?.note ?? null,
179
181
  decidedBy: approval?.decidedBy ?? null,
180
- decidedAt: null,
182
+ decidedAt: approval?.decidedAtMs != null ? new Date(approval.decidedAtMs).toISOString() : null,
181
183
  };
182
184
  };
183
185
  return React.createElement("smithers:task", {
@@ -3,11 +3,46 @@
3
3
  // @smithers-type-exports-end
4
4
 
5
5
  import React from "react";
6
+ import { SmithersContext } from "@smithers-orchestrator/react-reconciler/context";
6
7
  import { Sequence } from "./Sequence.js";
7
8
  import { Parallel } from "./Parallel.js";
8
9
  import { Task } from "./Task.js";
9
10
  /** @typedef {import("./CheckConfig.ts").CheckConfig} CheckConfig */
10
11
 
12
+ /**
13
+ * Whether a single check's output row counts as a pass. A missing row (the
14
+ * check never produced output) or an explicit failure signal counts as a fail.
15
+ * @param {unknown} row
16
+ * @returns {boolean}
17
+ */
18
+ function checkPassed(row) {
19
+ if (row == null)
20
+ return false;
21
+ if (typeof row === "object") {
22
+ const r = /** @type {Record<string, unknown>} */ (row);
23
+ if (r.passed === false || r.ok === false || r.failed === true)
24
+ return false;
25
+ if (r.error != null && r.error !== false)
26
+ return false;
27
+ }
28
+ return true;
29
+ }
30
+
31
+ /**
32
+ * Resolve the overall pass/fail verdict from the per-check pass count.
33
+ * @param {"all-pass" | "majority" | "any-pass"} strategy
34
+ * @param {number} passCount
35
+ * @param {number} total
36
+ * @returns {boolean}
37
+ */
38
+ function resolveVerdict(strategy, passCount, total) {
39
+ if (strategy === "any-pass")
40
+ return passCount > 0;
41
+ if (strategy === "majority")
42
+ return passCount * 2 > total;
43
+ return total > 0 && passCount === total;
44
+ }
45
+
11
46
  /**
12
47
  * @param {CheckConfig[] | Record<string, Omit<CheckConfig, "id">>} checks
13
48
  * @returns {CheckConfig[]}
@@ -29,6 +64,7 @@ function normalizeChecks(checks) {
29
64
  export function CheckSuite(props) {
30
65
  if (props.skipIf)
31
66
  return null;
67
+ const ctx = React.useContext(SmithersContext);
32
68
  const { id, checks, verdictOutput, strategy = "all-pass", maxConcurrency, continueOnFail = true, } = props;
33
69
  const prefix = id ?? "checksuite";
34
70
  const normalized = normalizeChecks(checks);
@@ -51,21 +87,38 @@ export function CheckSuite(props) {
51
87
  return React.createElement(Task, taskProps, childContent);
52
88
  });
53
89
  const parallelEl = React.createElement(Parallel, { maxConcurrency }, ...checkTasks);
54
- // Build needs map so the verdict task depends on all checks
55
- const needs = {};
56
- normalized.forEach((check) => {
57
- const taskId = `${prefix}-${check.id}`;
58
- needs[taskId] = taskId;
59
- });
60
- const strategyDesc = strategy === "all-pass"
61
- ? "ALL checks must pass for an overall pass verdict."
62
- : strategy === "majority"
63
- ? "A MAJORITY of checks must pass for an overall pass verdict."
64
- : "ANY single check passing is sufficient for an overall pass verdict.";
90
+ // The verdict depends on every check. We use dependsOn (the mechanism the
91
+ // graph extractor honors) so the verdict only runs once all checks have
92
+ // produced output — a `needs` map alone is ignored when no `deps` are set.
93
+ const checkIds = normalized.map((check) => `${prefix}-${check.id}`);
94
+ // Compute the aggregate verdict from the per-check outputs. Reads are taken
95
+ // from the workflow context at render time and captured in the closure; the
96
+ // component re-renders reactively as each check's output becomes available,
97
+ // and the engine defers execution until every dependency has completed.
65
98
  const verdictTask = React.createElement(Task, {
66
99
  id: `${prefix}-verdict`,
67
100
  output: verdictOutput,
68
- needs,
69
- }, `Aggregate check results into a pass/fail verdict.\n\nStrategy: ${strategyDesc}`);
101
+ dependsOn: checkIds,
102
+ label: "verdict",
103
+ }, () => {
104
+ let passCount = 0;
105
+ const results = {};
106
+ for (const check of normalized) {
107
+ const checkId = `${prefix}-${check.id}`;
108
+ const row = ctx?.outputMaybe(verdictOutput, { nodeId: checkId });
109
+ const passed = checkPassed(row);
110
+ results[check.id] = passed;
111
+ if (passed)
112
+ passCount += 1;
113
+ }
114
+ const total = normalized.length;
115
+ return {
116
+ passed: resolveVerdict(strategy, passCount, total),
117
+ passCount,
118
+ total,
119
+ strategy,
120
+ results,
121
+ };
122
+ });
70
123
  return React.createElement(Sequence, null, parallelEl, verdictTask);
71
124
  }
@@ -4,10 +4,42 @@
4
4
  // @smithers-type-exports-end
5
5
 
6
6
  import React from "react";
7
+ import { SmithersContext } from "@smithers-orchestrator/react-reconciler/context";
7
8
  import { Sequence } from "./Sequence.js";
8
9
  import { Branch } from "./Branch.js";
9
10
  import { Task } from "./Task.js";
10
11
  import { Approval } from "./Approval.js";
12
+ /**
13
+ * Default escalation predicate: escalate when the previous level has no result
14
+ * yet, or its result signals a failure (`error`/`failed` truthy or `ok === false`).
15
+ * @param {unknown} result
16
+ * @returns {boolean}
17
+ */
18
+ function defaultEscalateIf(result) {
19
+ if (result == null)
20
+ return true;
21
+ if (typeof result === "object") {
22
+ const row = /** @type {Record<string, unknown>} */ (result);
23
+ if (row.error != null && row.error !== false)
24
+ return true;
25
+ if (row.failed === true)
26
+ return true;
27
+ if (row.ok === false)
28
+ return true;
29
+ }
30
+ return false;
31
+ }
32
+ /**
33
+ * Resolve whether the previous level escalated by invoking its `escalateIf`
34
+ * predicate (or the default) against its actual result.
35
+ * @param {EscalationLevel} prevLevel
36
+ * @param {unknown} prevResult
37
+ * @returns {boolean}
38
+ */
39
+ function didEscalate(prevLevel, prevResult) {
40
+ const predicate = prevLevel.escalateIf ?? defaultEscalateIf;
41
+ return Boolean(predicate(prevResult));
42
+ }
11
43
  /**
12
44
  * Escalation chain: tries agents in order, escalating on failure or when
13
45
  * `escalateIf` returns `true`. Optionally ends with a human approval fallback.
@@ -18,6 +50,7 @@ import { Approval } from "./Approval.js";
18
50
  export function EscalationChain(props) {
19
51
  if (props.skipIf)
20
52
  return null;
53
+ const ctx = React.useContext(SmithersContext);
21
54
  const prefix = props.id ?? "escalation";
22
55
  const { levels, children, humanFallback, humanRequest, escalationOutput } = props;
23
56
  // Build the chain from the last level forward, nesting each level inside a
@@ -43,14 +76,14 @@ export function EscalationChain(props) {
43
76
  }
44
77
  else {
45
78
  // Subsequent levels are gated by a Branch that checks whether the
46
- // previous level needs escalation. The `if` condition is `true` at
47
- // render time when the previous level's `escalateIf` would trigger,
48
- // but since we cannot evaluate the runtime result at component-render
49
- // time, we rely on `continueOnFail` and use a compute Task to check
50
- // the escalation predicate, then Branch on its output.
51
- //
52
- // For the composite pattern we wrap each subsequent level so it only
53
- // mounts when the prior level signals escalation.
79
+ // previous level needs escalation. The chain re-renders reactively as
80
+ // outputs become available, so we read the previous level's actual
81
+ // result from the workflow context and run its `escalateIf` predicate
82
+ // (or the default failure predicate) to decide whether this level runs.
83
+ const prevLevel = levels[i - 1];
84
+ const prevLevelId = `${prefix}-level-${i - 1}`;
85
+ const prevResult = ctx?.outputMaybe(prevLevel.output, { nodeId: prevLevelId });
86
+ const escalated = didEscalate(prevLevel, prevResult);
54
87
  const checkId = `${prefix}-check-${i - 1}`;
55
88
  const checkTask = React.createElement(Task, {
56
89
  id: checkId,
@@ -58,40 +91,51 @@ export function EscalationChain(props) {
58
91
  continueOnFail: true,
59
92
  label: `Check escalation from level ${i - 1}`,
60
93
  children: () => {
61
- // This compute function runs at task execution time.
62
- // It evaluates the previous level's escalateIf predicate.
94
+ // Record the escalation decision for the prior level so it is
95
+ // visible in the escalation output stream.
63
96
  return {
64
- escalated: true,
97
+ escalated,
65
98
  fromLevel: i - 1,
66
99
  toLevel: i,
67
100
  };
68
101
  },
69
102
  });
70
- // Gate the current level: it always mounts when we reach this point
71
- // in the sequence because the previous level had continueOnFail.
72
- // The Branch uses `true` here because the sequence only reaches this
73
- // point if the previous task failed or escalateIf was configured.
103
+ // Gate the current level on the previous level's escalation decision:
104
+ // it only mounts when the prior level actually escalated.
74
105
  const gatedLevel = React.createElement(Branch, {
75
- if: true,
106
+ if: escalated,
76
107
  then: taskEl,
77
108
  });
78
109
  levelElements.push(checkTask);
79
110
  levelElements.push(gatedLevel);
80
111
  }
81
112
  }
82
- // Append human fallback if requested.
83
- if (humanFallback) {
113
+ // Append human fallback if requested. It only mounts when every automated
114
+ // level escalated (i.e. all automated levels were exhausted). A single
115
+ // level resolving without escalation stops the chain and the fallback, even
116
+ // if later levels never ran and therefore have no recorded result.
117
+ if (humanFallback && levels.length > 0) {
84
118
  const humanId = `${prefix}-human-fallback`;
85
119
  const request = humanRequest ?? {
86
120
  title: "Escalation requires human review",
87
121
  summary: `All ${levels.length} automated levels have been exhausted.`,
88
122
  };
89
- levelElements.push(React.createElement(Approval, {
123
+ const allEscalated = levels.every((level, idx) => {
124
+ const levelResult = ctx?.outputMaybe(level.output, {
125
+ nodeId: `${prefix}-level-${idx}`,
126
+ });
127
+ return didEscalate(level, levelResult);
128
+ });
129
+ const approvalEl = React.createElement(Approval, {
90
130
  id: humanId,
91
131
  output: escalationOutput,
92
132
  request,
93
133
  continueOnFail: true,
94
134
  label: request.title,
135
+ });
136
+ levelElements.push(React.createElement(Branch, {
137
+ if: allEscalated,
138
+ then: approvalEl,
95
139
  }));
96
140
  }
97
141
  return React.createElement(Sequence, {}, ...levelElements);
@@ -17,10 +17,12 @@ export function Sandbox(props) {
17
17
  id: props.id,
18
18
  key: props.key,
19
19
  output: props.output,
20
- runtime: props.runtime ?? "bubblewrap",
20
+ provider: props.provider,
21
+ runtime: props.runtime,
21
22
  allowNetwork: props.allowNetwork,
22
23
  reviewDiffs: props.reviewDiffs,
23
24
  autoAcceptDiffs: props.autoAcceptDiffs,
25
+ allowNested: props.allowNested,
24
26
  image: props.image,
25
27
  env: props.env,
26
28
  ports: props.ports,
@@ -40,9 +42,11 @@ export function Sandbox(props) {
40
42
  needs: props.needs,
41
43
  label: props.label ?? props.id,
42
44
  meta: props.meta,
45
+ __smithersSandboxProvider: props.provider,
43
46
  __smithersSandboxWorkflow: props.workflow,
44
47
  __smithersSandboxInput: props.input,
45
- __smithersSandboxRuntime: props.runtime ?? "bubblewrap",
48
+ __smithersSandboxRuntime: props.runtime,
49
+ __smithersSandboxAllowNested: props.allowNested,
46
50
  __smithersSandboxChildren: props.children,
47
51
  });
48
52
  }
@@ -14,10 +14,15 @@ export type SandboxProps = {
14
14
  /** Input passed to the child workflow. */
15
15
  input?: unknown;
16
16
  output: OutputTarget;
17
+ /** Injectable sandbox provider object or a provider id registered with the sandbox package. */
18
+ provider?: unknown;
19
+ /** @deprecated Prefer provider. Kept for legacy local transports. */
17
20
  runtime?: SandboxRuntime;
18
21
  allowNetwork?: boolean;
19
22
  reviewDiffs?: boolean;
20
23
  autoAcceptDiffs?: boolean;
24
+ /** Allow this sandbox to execute while already inside another sandbox. Disabled by default. */
25
+ allowNested?: boolean;
21
26
  image?: string;
22
27
  env?: Record<string, string>;
23
28
  ports?: Array<{
@@ -53,6 +53,7 @@ export function ScanFixVerify(props) {
53
53
  const reportTask = React.createElement(Task, {
54
54
  id: `${prefix}-report`,
55
55
  output: props.reportOutput,
56
+ agent: props.verifier,
56
57
  dependsOn: [`${prefix}-verify`],
57
58
  children: "Produce a final summary report of all scan-fix-verify cycles, including what was found, what was fixed, and the final verification status.",
58
59
  });
@@ -78,6 +78,7 @@ export function Supervisor(props) {
78
78
  const finalTask = React.createElement(Task, {
79
79
  id: `${prefix}-final`,
80
80
  output: props.finalOutput,
81
+ agent: props.boss,
81
82
  needs: { review: `${prefix}-review`, plan: `${prefix}-plan` },
82
83
  label: "Supervisor summary",
83
84
  children: "Summarize the overall results from all delegation cycles.",
@@ -13,6 +13,7 @@ import { zodSchemaToJsonExample } from "../zod-to-example.js";
13
13
  import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
14
14
  import { SmithersContext } from "@smithers-orchestrator/react-reconciler/context";
15
15
  import { AspectContext } from "../aspects/AspectContext.js";
16
+ import { AntigravityAgent } from "@smithers-orchestrator/agents/AntigravityAgent";
16
17
  import { ClaudeCodeAgent } from "@smithers-orchestrator/agents/ClaudeCodeAgent";
17
18
  import { GeminiAgent } from "@smithers-orchestrator/agents/GeminiAgent";
18
19
  import { PiAgent } from "@smithers-orchestrator/agents/PiAgent";
@@ -174,6 +175,13 @@ function applyCliToolAllowlist(agent, allowTools) {
174
175
  allowedTools: [...allowTools],
175
176
  });
176
177
  }
178
+ if (agent instanceof AntigravityAgent) {
179
+ const opts = { ...agent.opts };
180
+ return new AntigravityAgent({
181
+ ...opts,
182
+ allowedTools: [...allowTools],
183
+ });
184
+ }
177
185
  return agent;
178
186
  }
179
187
  /**
@@ -31,6 +31,14 @@ export type TaskProps<Row, Output extends OutputTarget = OutputTarget, D extends
31
31
  needs?: Record<string, string>;
32
32
  /** Render-time typed dependencies. Keys resolve from task ids of the same name, or from matching `needs` entries. */
33
33
  deps?: D;
34
+ /**
35
+ * Start this agent task from a copy of another task's final agent session context.
36
+ * The fork source becomes an implicit dependency: this task waits for it to complete,
37
+ * then copies its conversation snapshot into a fresh, independent session and submits
38
+ * its own prompt. The source is never mutated. Inside a `<Loop>`, resolves to the
39
+ * latest completed snapshot for that task id. Requires an agent task.
40
+ */
41
+ fork?: string;
34
42
  skipIf?: boolean;
35
43
  needsApproval?: boolean;
36
44
  /** When paired with `needsApproval`, do not block unrelated downstream flow while the approval is pending. */
package/src/index.d.ts CHANGED
@@ -316,10 +316,15 @@ type SandboxProps$2 = {
316
316
  /** Input passed to the child workflow. */
317
317
  input?: unknown;
318
318
  output: OutputTarget$1;
319
+ /** Injectable sandbox provider object or a provider id registered with the sandbox package. */
320
+ provider?: unknown;
321
+ /** @deprecated Prefer provider. Kept for legacy local transports. */
319
322
  runtime?: SandboxRuntime$1;
320
323
  allowNetwork?: boolean;
321
324
  reviewDiffs?: boolean;
322
325
  autoAcceptDiffs?: boolean;
326
+ /** Allow this sandbox to execute while already inside another sandbox. Disabled by default. */
327
+ allowNested?: boolean;
323
328
  image?: string;
324
329
  env?: Record<string, string>;
325
330
  ports?: Array<{
@@ -1201,10 +1206,12 @@ declare function Sandbox(props: SandboxProps$1): React.ReactElement<{
1201
1206
  id: string;
1202
1207
  key: string | undefined;
1203
1208
  output: OutputTarget$1;
1204
- runtime: SandboxRuntime$1;
1209
+ provider: unknown;
1210
+ runtime: SandboxRuntime$1 | undefined;
1205
1211
  allowNetwork: boolean | undefined;
1206
1212
  reviewDiffs: boolean | undefined;
1207
1213
  autoAcceptDiffs: boolean | undefined;
1214
+ allowNested: boolean | undefined;
1208
1215
  image: string | undefined;
1209
1216
  env: Record<string, string> | undefined;
1210
1217
  ports: {
@@ -1230,9 +1237,11 @@ declare function Sandbox(props: SandboxProps$1): React.ReactElement<{
1230
1237
  needs: Record<string, string> | undefined;
1231
1238
  label: string;
1232
1239
  meta: Record<string, unknown> | undefined;
1240
+ __smithersSandboxProvider: unknown;
1233
1241
  __smithersSandboxWorkflow: _smithers_driver.WorkflowDefinition<unknown> | undefined;
1234
1242
  __smithersSandboxInput: unknown;
1235
- __smithersSandboxRuntime: SandboxRuntime$1;
1243
+ __smithersSandboxRuntime: SandboxRuntime$1 | undefined;
1244
+ __smithersSandboxAllowNested: boolean | undefined;
1236
1245
  __smithersSandboxChildren: React.ReactNode;
1237
1246
  }, string | React.JSXElementConstructor<any>> | null;
1238
1247
  type SandboxProps$1 = SandboxProps$2;