@smithers-orchestrator/components 0.24.2 → 0.25.1
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 +10 -10
- package/src/components/Approval.js +27 -9
- package/src/components/Branch.js +13 -1
- package/src/components/BranchProps.ts +6 -0
- package/src/components/Panel.js +1 -1
- package/src/components/Ralph.js +10 -1
- package/src/components/Saga.js +16 -5
- package/src/components/Sequence.js +3 -1
- package/src/components/Sidecar.js +68 -0
- package/src/components/SidecarDelta.ts +6 -0
- package/src/components/SidecarProps.ts +22 -0
- package/src/components/SuperSmithers.js +15 -14
- package/src/components/TaskProps.ts +4 -0
- package/src/components/Workflow.js +4 -1
- package/src/components/computeSidecarDelta.ts +57 -0
- package/src/components/index.js +5 -1
- package/src/index.d.ts +242 -170
- package/src/aspects/index.js +0 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smithers-orchestrator/components",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.1",
|
|
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/
|
|
28
|
-
"@smithers-orchestrator/
|
|
29
|
-
"@smithers-orchestrator/
|
|
30
|
-
"@smithers-orchestrator/
|
|
31
|
-
"@smithers-orchestrator/
|
|
32
|
-
"@smithers-orchestrator/
|
|
33
|
-
"@smithers-orchestrator/
|
|
34
|
-
"@smithers-orchestrator/
|
|
35
|
-
"@smithers-orchestrator/
|
|
27
|
+
"@smithers-orchestrator/db": "0.25.1",
|
|
28
|
+
"@smithers-orchestrator/errors": "0.25.1",
|
|
29
|
+
"@smithers-orchestrator/driver": "0.25.1",
|
|
30
|
+
"@smithers-orchestrator/agents": "0.25.1",
|
|
31
|
+
"@smithers-orchestrator/graph": "0.25.1",
|
|
32
|
+
"@smithers-orchestrator/memory": "0.25.1",
|
|
33
|
+
"@smithers-orchestrator/scheduler": "0.25.1",
|
|
34
|
+
"@smithers-orchestrator/observability": "0.25.1",
|
|
35
|
+
"@smithers-orchestrator/react-reconciler": "0.25.1"
|
|
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
|
|
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
|
|
190
|
-
(isZodObject(props.output) ? props.output : defaultSchemaForMode(mode)),
|
|
208
|
+
outputSchema,
|
|
191
209
|
dependsOn: props.dependsOn,
|
|
192
210
|
needs: props.needs,
|
|
193
211
|
needsApproval: true,
|
package/src/components/Branch.js
CHANGED
|
@@ -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
|
-
|
|
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
|
};
|
package/src/components/Panel.js
CHANGED
|
@@ -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`,
|
package/src/components/Ralph.js
CHANGED
|
@@ -11,7 +11,16 @@ import React from "react";
|
|
|
11
11
|
export function Loop(props) {
|
|
12
12
|
if (props.skipIf)
|
|
13
13
|
return null;
|
|
14
|
-
|
|
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;
|
package/src/components/Saga.js
CHANGED
|
@@ -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 ||
|
|
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) =>
|
|
66
|
-
|
|
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
|
-
|
|
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,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
|
|
18
|
-
* 3.
|
|
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 =
|
|
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:
|
|
54
|
+
// Task 2: Apply the modifications (or, in a dry run, propose them only)
|
|
55
55
|
const proposeTaskId = `${prefix}-propose`;
|
|
56
|
-
const proposeOutput =
|
|
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
|
-
},
|
|
64
|
-
"
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
+
}
|
package/src/components/index.js
CHANGED
|
@@ -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";
|