@smithers-orchestrator/components 0.24.2 → 0.25.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.24.2",
3
+ "version": "0.25.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.24.2",
28
- "@smithers-orchestrator/db": "0.24.2",
29
- "@smithers-orchestrator/graph": "0.24.2",
30
- "@smithers-orchestrator/errors": "0.24.2",
31
- "@smithers-orchestrator/driver": "0.24.2",
32
- "@smithers-orchestrator/observability": "0.24.2",
33
- "@smithers-orchestrator/react-reconciler": "0.24.2",
34
- "@smithers-orchestrator/scheduler": "0.24.2",
35
- "@smithers-orchestrator/memory": "0.24.2"
27
+ "@smithers-orchestrator/agents": "0.25.0",
28
+ "@smithers-orchestrator/driver": "0.25.0",
29
+ "@smithers-orchestrator/db": "0.25.0",
30
+ "@smithers-orchestrator/errors": "0.25.0",
31
+ "@smithers-orchestrator/memory": "0.25.0",
32
+ "@smithers-orchestrator/scheduler": "0.25.0",
33
+ "@smithers-orchestrator/observability": "0.25.0",
34
+ "@smithers-orchestrator/graph": "0.25.0",
35
+ "@smithers-orchestrator/react-reconciler": "0.25.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@tanstack/react-query": "^5.99.1",
@@ -21,7 +21,10 @@ import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
21
21
 
22
22
  export const approvalDecisionSchema = z.object({
23
23
  approved: z.boolean(),
24
- note: z.string().nullable(),
24
+ // `note` is omitted entirely when no note was provided, so the default
25
+ // decision schema must accept an absent key (optional) as well as the
26
+ // legacy null/string shapes.
27
+ note: z.string().nullable().optional(),
25
28
  decidedBy: z.string().nullable(),
26
29
  decidedAt: z.string().datetime().nullable(),
27
30
  });
@@ -70,6 +73,25 @@ function defaultSchemaForMode(mode) {
70
73
  return approvalDecisionSchema;
71
74
  }
72
75
  }
76
+ /**
77
+ * @param {{ status?: string | null; note?: string | null; decidedBy?: string | null; decidedAtMs?: number | null } | undefined | null} approval
78
+ * @param {import("zod").ZodObject<import("zod").ZodRawShape>} outputSchema
79
+ * @returns {Record<string, unknown>}
80
+ */
81
+ function buildDecisionPayload(approval, outputSchema) {
82
+ const base = {
83
+ approved: approval?.status === "approved",
84
+ decidedBy: approval?.decidedBy ?? null,
85
+ decidedAt: approval?.decidedAtMs != null ? new Date(approval.decidedAtMs).toISOString() : null,
86
+ };
87
+ if (typeof approval?.note === "string") {
88
+ return { ...base, note: approval.note };
89
+ }
90
+ if (outputSchema.safeParse(base).success) {
91
+ return base;
92
+ }
93
+ return { ...base, note: null };
94
+ }
73
95
  /**
74
96
  * @param {ApprovalMode | undefined} mode
75
97
  * @returns {"select" | "rank" | "decision"}
@@ -120,6 +142,8 @@ export function Approval(props) {
120
142
  const mode = props.mode ?? "approve";
121
143
  const approvalMode = normalizeMode(mode);
122
144
  const options = normalizeOptions(props.options);
145
+ const outputSchema = props.outputSchema ??
146
+ (isZodObject(props.output) ? props.output : defaultSchemaForMode(mode));
123
147
  if ((mode === "select" || mode === "rank") && (!options || options.length === 0)) {
124
148
  throw new SmithersError("APPROVAL_OPTIONS_REQUIRED", `Approval ${props.id} requires options when mode="${mode}".`);
125
149
  }
@@ -175,19 +199,13 @@ export function Approval(props) {
175
199
  : approval?.note ?? null,
176
200
  };
177
201
  }
178
- return {
179
- approved: approval?.status === "approved",
180
- note: approval?.note ?? null,
181
- decidedBy: approval?.decidedBy ?? null,
182
- decidedAt: approval?.decidedAtMs != null ? new Date(approval.decidedAtMs).toISOString() : null,
183
- };
202
+ return buildDecisionPayload(approval, outputSchema);
184
203
  };
185
204
  return React.createElement("smithers:task", {
186
205
  id: props.id,
187
206
  key: props.key,
188
207
  output: props.output,
189
- outputSchema: props.outputSchema ??
190
- (isZodObject(props.output) ? props.output : defaultSchemaForMode(mode)),
208
+ outputSchema,
191
209
  dependsOn: props.dependsOn,
192
210
  needs: props.needs,
193
211
  needsApproval: true,
@@ -1,12 +1,24 @@
1
1
  import React from "react";
2
+ import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
2
3
  /** @typedef {import("./BranchProps.ts").BranchProps} BranchProps */
3
4
 
4
5
  /**
5
6
  * @param {BranchProps} props
6
7
  */
7
8
  export function Branch(props) {
9
+ // <Branch> resolves its subtree from the `then`/`else` props; any JSX children
10
+ // would be silently dropped, removing those tasks from the graph with no
11
+ // feedback. Fail fast instead. (Checked before skipIf so a stray-children
12
+ // mistake still surfaces even on a skipped branch.)
13
+ if (props.children !== undefined && props.children !== null) {
14
+ throw new SmithersError("INVALID_INPUT", `<Branch> does not take children. Use the "then" and "else" props instead, e.g. ` +
15
+ `<Branch if={cond} then={<Task .../>} else={<Task .../>} />. ` +
16
+ `Children passed to <Branch> are silently ignored and would drop those tasks from the graph.`);
17
+ }
8
18
  if (props.skipIf)
9
19
  return null;
10
20
  const chosen = props.if ? props.then : (props.else ?? null);
11
- return React.createElement("smithers:branch", props, chosen);
21
+ // The branch is resolved to `chosen` at render time, so the host element
22
+ // carries no props of its own (align with the sanitizing structural components).
23
+ return React.createElement("smithers:branch", {}, chosen);
12
24
  }
@@ -5,4 +5,10 @@ export type BranchProps = {
5
5
  then: React.ReactElement;
6
6
  else?: React.ReactElement | null;
7
7
  skipIf?: boolean;
8
+ /**
9
+ * `<Branch>` resolves its subtree from `then`/`else`; it takes no children.
10
+ * Typed as `never` so passing JSX children is a compile-time error (the
11
+ * runtime also throws — children would otherwise be silently dropped).
12
+ */
13
+ children?: never;
8
14
  };
@@ -56,7 +56,7 @@ export function Panel(props) {
56
56
  ? `\n\nStrategy: VOTE. Count how many panelists agree. ${minAgree ? `Minimum agreement required: ${minAgree}.` : ""}`
57
57
  : strategy === "consensus"
58
58
  ? `\n\nStrategy: CONSENSUS. All panelists must converge. ${minAgree ? `Minimum agreement required: ${minAgree}.` : ""}`
59
- : `\n\nStrategy: SYNTHESIZE. Combine all panelist outputs into a single coherent result.`;
59
+ : `\n\nStrategy: SYNTHESIZE. Combine all panelist outputs into a single coherent result. Preserve each panelist's concrete, grounded findings verbatim (specific file paths, line numbers, identifiers, prior-PR references, and what already exists); reconcile disagreements with evidence. Do not over-generalize, drop specifics, or change the scope the panelists analyzed.`;
60
60
  const moderatorChildren = `Synthesize the following panelist outputs.${strategyPrompt}`;
61
61
  const moderatorTask = React.createElement(Task, {
62
62
  id: `${prefix}-moderator`,
@@ -11,7 +11,16 @@ import React from "react";
11
11
  export function Loop(props) {
12
12
  if (props.skipIf)
13
13
  return null;
14
- return React.createElement("smithers:ralph", props, props.children);
14
+ // Sanitize to the loop's host props (align with other structural components);
15
+ // key/skipIf are React/control props and children are passed separately.
16
+ const next = {
17
+ id: props.id,
18
+ until: props.until,
19
+ maxIterations: props.maxIterations,
20
+ onMaxReached: props.onMaxReached,
21
+ continueAsNewEvery: props.continueAsNewEvery,
22
+ };
23
+ return React.createElement("smithers:ralph", next, props.children);
15
24
  }
16
25
  /** @deprecated Use `Loop` instead. */
17
26
  export const Ralph = Loop;
@@ -4,14 +4,18 @@
4
4
  // @smithers-type-exports-end
5
5
 
6
6
  import React from "react";
7
+ import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
7
8
  import { forceContinueOnFail } from "./control-flow-utils.js";
8
9
  /** @typedef {import("./SagaStepProps.ts").SagaStepProps} SagaStepProps */
9
10
 
10
11
  /**
12
+ * Declarative marker for a Saga step. Exported as a value so `import { SagaStep }`
13
+ * works; also available as `<Saga.Step>`.
14
+ *
11
15
  * @param {SagaStepProps} _props
12
16
  * @returns {React.ReactElement | null}
13
17
  */
14
- function SagaStep(_props) {
18
+ export function SagaStep(_props) {
15
19
  // SagaStep is a declarative marker — the Saga component reads its props
16
20
  // directly from the children array. It does not render on its own.
17
21
  return null;
@@ -39,7 +43,8 @@ export function Saga(props) {
39
43
  const childArr = React.Children.toArray(children);
40
44
  for (const child of childArr) {
41
45
  if (React.isValidElement(child) &&
42
- (child.type === SagaStep || child.type.__isSagaStep)) {
46
+ (child.type === SagaStep ||
47
+ (typeof child.type === "function" && child.type.__isSagaStep))) {
43
48
  const stepProps = child.props;
44
49
  resolvedSteps.push({
45
50
  id: stepProps.id,
@@ -62,9 +67,15 @@ export function Saga(props) {
62
67
  const actionChildren = resolvedSteps.map((step) => React.cloneElement(forceContinueOnFail(step.action), {
63
68
  key: `saga-action-${step.id}`,
64
69
  }));
65
- const compensationChildren = resolvedSteps.map((step) => React.cloneElement(step.compensation, {
66
- key: `saga-compensation-${step.id}`,
67
- }));
70
+ const compensationChildren = resolvedSteps.map((step) => {
71
+ if (!React.isValidElement(step.compensation)) {
72
+ throw new SmithersError("INVALID_INPUT", `<Saga id="${id}"> step "${step.id}" has an invalid compensation. Each step needs a single ` +
73
+ `element (e.g. a <Task/>) that undoes its action; a missing or inert compensation leaves dirty state on rollback.`);
74
+ }
75
+ return React.cloneElement(step.compensation, {
76
+ key: `saga-compensation-${step.id}`,
77
+ });
78
+ });
68
79
  // Store compensation elements on the host props for the engine.
69
80
  sagaProps.__sagaCompensations = resolvedSteps.reduce((acc, step) => {
70
81
  acc[step.id] = step.compensation;
@@ -7,5 +7,7 @@ import React from "react";
7
7
  export function Sequence(props) {
8
8
  if (props.skipIf)
9
9
  return null;
10
- return React.createElement("smithers:sequence", props, props.children);
10
+ // Sequence carries no host props of its own; pass an empty bag (align with
11
+ // the sanitizing structural components) so control props don't leak through.
12
+ return React.createElement("smithers:sequence", {}, props.children);
11
13
  }
@@ -0,0 +1,68 @@
1
+ // @smithers-type-exports-begin
2
+ /** @typedef {import("./SidecarProps.ts").SidecarProps} SidecarProps */
3
+ // @smithers-type-exports-end
4
+
5
+ import React from "react";
6
+ import { Parallel } from "./Parallel.js";
7
+ import { Task } from "./Task.js";
8
+
9
+ /**
10
+ * Runs a primary task and a cheap shadow task over the same prompt.
11
+ *
12
+ * The primary task keeps the component id so downstream `needs` can consume it.
13
+ * The sidecar task is continue-on-fail and writes its own scorer rows.
14
+ *
15
+ * @param {SidecarProps} props
16
+ */
17
+ export function Sidecar(props) {
18
+ if (props.skipIf) return null;
19
+ const {
20
+ id = "sidecar",
21
+ agent,
22
+ sidecar,
23
+ output,
24
+ sidecarOutput,
25
+ scorers,
26
+ prompt,
27
+ input,
28
+ maxConcurrency,
29
+ groundTruth,
30
+ context,
31
+ primaryLabel,
32
+ sidecarLabel,
33
+ children,
34
+ } = props;
35
+ const promptNode = prompt ?? input ?? children;
36
+ const shadowId = `${id}-sidecar`;
37
+ return React.createElement(
38
+ Parallel,
39
+ { id: `${id}-parallel`, maxConcurrency },
40
+ React.createElement(
41
+ Task,
42
+ {
43
+ id,
44
+ output,
45
+ agent,
46
+ scorers,
47
+ groundTruth,
48
+ context,
49
+ label: primaryLabel,
50
+ },
51
+ promptNode,
52
+ ),
53
+ React.createElement(
54
+ Task,
55
+ {
56
+ id: shadowId,
57
+ output: sidecarOutput ?? output,
58
+ agent: sidecar,
59
+ continueOnFail: true,
60
+ scorers,
61
+ groundTruth,
62
+ context,
63
+ label: sidecarLabel,
64
+ },
65
+ promptNode,
66
+ ),
67
+ );
68
+ }
@@ -0,0 +1,6 @@
1
+ export type SidecarDelta = {
2
+ primaryScore: number | null;
3
+ sidecarScore: number | null;
4
+ delta: number | null;
5
+ cheaperWins: boolean;
6
+ };
@@ -0,0 +1,22 @@
1
+ import type React from "react";
2
+ import type { AgentLike } from "@smithers-orchestrator/agents/AgentLike";
3
+ import type { ScorersMap } from "@smithers-orchestrator/graph/types";
4
+ import type { OutputTarget } from "./OutputTarget.ts";
5
+
6
+ export type SidecarProps = {
7
+ id?: string;
8
+ agent: AgentLike;
9
+ sidecar: AgentLike;
10
+ output: OutputTarget;
11
+ sidecarOutput?: OutputTarget;
12
+ scorers?: ScorersMap;
13
+ prompt?: string | React.ReactNode;
14
+ input?: string | React.ReactNode;
15
+ maxConcurrency?: number;
16
+ groundTruth?: unknown;
17
+ context?: unknown;
18
+ primaryLabel?: string;
19
+ sidecarLabel?: string;
20
+ skipIf?: boolean;
21
+ children?: string | React.ReactNode;
22
+ };
@@ -14,8 +14,8 @@ import React from "react";
14
14
  *
15
15
  * Internally expands to a sequence of tasks:
16
16
  * 1. Agent reads the strategy doc and target files
17
- * 2. Agent proposes modifications
18
- * 3. (If not dryRun) Compute task writes modifications to disk
17
+ * 2. (If not dryRun) Agent applies the modifications directly to disk
18
+ * 3. A compute marker records the apply and triggers the hot-reload system
19
19
  * 4. Agent generates a report of what changed
20
20
  *
21
21
  * ```tsx
@@ -36,7 +36,7 @@ export function SuperSmithers(props) {
36
36
  const prefix = idPrefix ?? "super-smithers";
37
37
  // Task 1: Read strategy and target files
38
38
  const readTaskId = `${prefix}-read`;
39
- const readOutput = reportOutput ?? "super-smithers-read";
39
+ const readOutput = "super-smithers-read";
40
40
  const strategyText = typeof strategy === "string" ? strategy : undefined;
41
41
  const strategyElement = typeof strategy !== "string" ? strategy : undefined;
42
42
  const readPrompt = strategyText
@@ -51,22 +51,26 @@ export function SuperSmithers(props) {
51
51
  agent,
52
52
  __smithersKind: "agent",
53
53
  }, readChildren);
54
- // Task 2: Propose modifications
54
+ // Task 2: Apply the modifications (or, in a dry run, propose them only)
55
55
  const proposeTaskId = `${prefix}-propose`;
56
- const proposeOutput = reportOutput ?? "super-smithers-propose";
56
+ const proposeOutput = "super-smithers-propose";
57
57
  const proposeTask = React.createElement("smithers:task", {
58
58
  id: proposeTaskId,
59
59
  output: proposeOutput,
60
60
  agent,
61
61
  dependsOn: [readTaskId],
62
62
  __smithersKind: "agent",
63
- }, "Based on your analysis, propose specific code modifications. " +
64
- "For each file, provide the exact changes needed as a list of edits. " +
65
- "Include the file path, the original code, and the replacement code for each change. " +
66
- (dryRun ? "This is a DRY RUN do not apply changes, only report them." : ""));
67
- // Task 3: Apply modifications (only if not dryRun)
63
+ }, dryRun
64
+ ? "Based on your analysis, propose specific code modifications. This is a DRY RUN — do NOT modify any files. " +
65
+ "For each file, provide the exact changes needed as a list of edits: the file path, the original code, and the replacement code."
66
+ : "Based on your analysis, apply the necessary code modifications directly to the target files using your file-editing tools. " +
67
+ "Make each edit on disk now. After applying them, list each file you changed with a short description of the change.");
68
+ // Task 3: Sync marker after the apply agent has written its edits (skipped on
69
+ // dry runs). The edits themselves are made by the agent in Task 2; this compute
70
+ // step is a dependency barrier so the report below only runs once the apply
71
+ // task has settled (and its settled write triggers the hot-reload system).
68
72
  const applyTaskId = `${prefix}-apply`;
69
- const applyOutput = reportOutput ?? "super-smithers-apply";
73
+ const applyOutput = "super-smithers-apply";
70
74
  const applyTask = !dryRun
71
75
  ? React.createElement("smithers:task", {
72
76
  id: applyTaskId,
@@ -74,9 +78,6 @@ export function SuperSmithers(props) {
74
78
  dependsOn: [proposeTaskId],
75
79
  __smithersKind: "compute",
76
80
  __smithersComputeFn: async () => {
77
- // The compute function has access to the proposed modifications
78
- // from the previous task via the engine context. The actual file
79
- // writes trigger the hot reload system.
80
81
  return { applied: true };
81
82
  },
82
83
  }, null)
@@ -54,6 +54,10 @@ export type TaskProps<Row, Output extends OutputTarget = OutputTarget, D extends
54
54
  cache?: CachePolicy;
55
55
  /** Optional scorers to evaluate this task's output after completion. */
56
56
  scorers?: ScorersMap;
57
+ /** Expected output supplied to scorers that compare against a reference answer. */
58
+ groundTruth?: unknown;
59
+ /** Additional source context supplied to scorers such as faithfulnessScorer. */
60
+ context?: unknown;
57
61
  /** Optional cross-run memory configuration. */
58
62
  memory?: TaskMemoryConfig;
59
63
  /** Request an immediate hijack handoff as soon as the task starts running. */
@@ -6,5 +6,8 @@ import React from "react";
6
6
  * @returns {React.DOMElement<WorkflowProps, Element>}
7
7
  */
8
8
  export function Workflow(props) {
9
- return React.createElement("smithers:workflow", props, props.children);
9
+ // Sanitize host props (align with other structural components): pass only the
10
+ // fields the host element carries, not the React children/control props.
11
+ const next = { name: props.name, cache: props.cache };
12
+ return React.createElement("smithers:workflow", next, props.children);
10
13
  }
@@ -0,0 +1,57 @@
1
+ import type { SidecarDelta } from "./SidecarDelta.ts";
2
+
3
+ type RowLike = {
4
+ nodeId?: string;
5
+ node_id?: string;
6
+ scorerId?: string;
7
+ scorer_id?: string;
8
+ score?: number;
9
+ scoredAtMs?: number;
10
+ scored_at_ms?: number;
11
+ } & Record<string, unknown>;
12
+
13
+ type ComputeSidecarDeltaOptions = {
14
+ primaryNodeId: string;
15
+ sidecarNodeId: string;
16
+ scorerId?: string;
17
+ };
18
+
19
+ function getNodeId(row: RowLike): string | undefined {
20
+ return typeof row.nodeId === "string" ? row.nodeId : typeof row.node_id === "string" ? row.node_id : undefined;
21
+ }
22
+
23
+ function getScorerId(row: RowLike): string | undefined {
24
+ return typeof row.scorerId === "string"
25
+ ? row.scorerId
26
+ : typeof row.scorer_id === "string"
27
+ ? row.scorer_id
28
+ : undefined;
29
+ }
30
+
31
+ function getScoredAtMs(row: RowLike): number {
32
+ const value = row.scoredAtMs ?? row.scored_at_ms;
33
+ return typeof value === "number" ? value : 0;
34
+ }
35
+
36
+ function getScore(row: RowLike | undefined): number | null {
37
+ return typeof row?.score === "number" ? row.score : null;
38
+ }
39
+
40
+ function latestMatching(rows: RowLike[], nodeId: string, scorerId?: string): RowLike | undefined {
41
+ return rows
42
+ .filter((row) => getNodeId(row) === nodeId && (!scorerId || getScorerId(row) === scorerId))
43
+ .sort((a, b) => getScoredAtMs(b) - getScoredAtMs(a))[0];
44
+ }
45
+
46
+ export function computeSidecarDelta(rows: RowLike[], opts: ComputeSidecarDeltaOptions): SidecarDelta {
47
+ const primaryScore = getScore(latestMatching(rows, opts.primaryNodeId, opts.scorerId));
48
+ const sidecarScore = getScore(latestMatching(rows, opts.sidecarNodeId, opts.scorerId));
49
+ const delta =
50
+ primaryScore == null || sidecarScore == null ? null : Number((primaryScore - sidecarScore).toFixed(12));
51
+ return {
52
+ primaryScore,
53
+ sidecarScore,
54
+ delta,
55
+ cheaperWins: primaryScore != null && sidecarScore != null && sidecarScore >= primaryScore,
56
+ };
57
+ }
@@ -59,6 +59,8 @@
59
59
  /** @typedef {import("./ScanFixVerifyProps.ts").ScanFixVerifyProps} ScanFixVerifyProps */
60
60
  /** @typedef {import("@smithers-orchestrator/graph/types").ScorersMap} ScorersMap */
61
61
  /** @typedef {import("./SequenceProps.ts").SequenceProps} SequenceProps */
62
+ /** @typedef {import("./SidecarDelta.ts").SidecarDelta} SidecarDelta */
63
+ /** @typedef {import("./SidecarProps.ts").SidecarProps} SidecarProps */
62
64
  /**
63
65
  * @template Schema
64
66
  * @typedef {import("./SignalProps.ts").SignalProps<Schema>} SignalProps
@@ -108,6 +110,8 @@ export { ScanFixVerify } from "./ScanFixVerify.js";
108
110
  export { Poller } from "./Poller.js";
109
111
  export { Supervisor } from "./Supervisor.js";
110
112
  export { Runbook } from "./Runbook.js";
113
+ export { Sidecar } from "./Sidecar.js";
114
+ export { computeSidecarDelta } from "./computeSidecarDelta.ts";
111
115
  // --- Engine-Backed Primitives ---
112
116
  export { Subflow } from "./Subflow.js";
113
117
  export { Sandbox } from "./Sandbox.js";
@@ -115,7 +119,7 @@ export { WaitForEvent } from "./WaitForEvent.js";
115
119
  export { Signal } from "./Signal.js";
116
120
  export { Timer } from "./Timer.js";
117
121
  export { HumanTask } from "./HumanTask.js";
118
- export { Saga } from "./Saga.js";
122
+ export { Saga, SagaStep } from "./Saga.js";
119
123
  export { TryCatchFinally } from "./TryCatchFinally.js";
120
124
  // --- Core Enhancements ---
121
125
  export { Aspects } from "./Aspects.js";