@smithers-orchestrator/components 0.24.0 → 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 +10 -10
- package/src/aspects/AspectAccumulator.ts +0 -1
- package/src/aspects/AspectContext.js +2 -3
- package/src/aspects/AspectContextValue.ts +0 -2
- package/src/aspects/LatencySloConfig.ts +5 -4
- package/src/aspects/TokenBudgetConfig.ts +4 -3
- package/src/aspects/TrackingConfig.ts +0 -2
- package/src/components/Approval.js +27 -9
- package/src/components/Aspects.js +3 -5
- package/src/components/AspectsProps.ts +2 -5
- 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/Task.js +4 -6
- 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 +253 -197
- package/src/aspects/CostBudgetConfig.ts +0 -11
- package/src/aspects/index.js +0 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smithers-orchestrator/components",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
28
|
-
"@smithers-orchestrator/
|
|
29
|
-
"@smithers-orchestrator/
|
|
30
|
-
"@smithers-orchestrator/errors": "0.
|
|
31
|
-
"@smithers-orchestrator/
|
|
32
|
-
"@smithers-orchestrator/
|
|
33
|
-
"@smithers-orchestrator/
|
|
34
|
-
"@smithers-orchestrator/
|
|
35
|
-
"@smithers-orchestrator/
|
|
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",
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// @smithers-type-exports-begin
|
|
2
2
|
/** @typedef {import("./AspectAccumulator.ts").AspectAccumulator} AspectAccumulator */
|
|
3
3
|
/** @typedef {import("./AspectContextValue.ts").AspectContextValue} AspectContextValue */
|
|
4
|
-
/** @typedef {import("./CostBudgetConfig.ts").CostBudgetConfig} CostBudgetConfig */
|
|
5
4
|
/** @typedef {import("./LatencySloConfig.ts").LatencySloConfig} LatencySloConfig */
|
|
6
5
|
/** @typedef {import("./TokenBudgetConfig.ts").TokenBudgetConfig} TokenBudgetConfig */
|
|
7
6
|
/** @typedef {import("./TrackingConfig.ts").TrackingConfig} TrackingConfig */
|
|
@@ -10,7 +9,8 @@
|
|
|
10
9
|
import React from "react";
|
|
11
10
|
/**
|
|
12
11
|
* React context that propagates Aspects configuration down the component tree.
|
|
13
|
-
*
|
|
12
|
+
* Tasks read from this context to attach budgets the engine enforces and to
|
|
13
|
+
* track metrics.
|
|
14
14
|
* @type {React.Context<AspectContextValue | null>}
|
|
15
15
|
*/
|
|
16
16
|
export const AspectContext = React.createContext(/** @type {AspectContextValue | null} */ (null));
|
|
@@ -23,7 +23,6 @@ export function createAccumulator() {
|
|
|
23
23
|
return {
|
|
24
24
|
totalTokens: 0,
|
|
25
25
|
totalLatencyMs: 0,
|
|
26
|
-
totalCostUsd: 0,
|
|
27
26
|
taskCount: 0,
|
|
28
27
|
};
|
|
29
28
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { TokenBudgetConfig } from "./TokenBudgetConfig.ts";
|
|
2
2
|
import type { LatencySloConfig } from "./LatencySloConfig.ts";
|
|
3
|
-
import type { CostBudgetConfig } from "./CostBudgetConfig.ts";
|
|
4
3
|
import type { TrackingConfig } from "./TrackingConfig.ts";
|
|
5
4
|
import type { AspectAccumulator } from "./AspectAccumulator.ts";
|
|
6
5
|
|
|
@@ -10,7 +9,6 @@ import type { AspectAccumulator } from "./AspectAccumulator.ts";
|
|
|
10
9
|
export type AspectContextValue = {
|
|
11
10
|
tokenBudget?: TokenBudgetConfig;
|
|
12
11
|
latencySlo?: LatencySloConfig;
|
|
13
|
-
costBudget?: CostBudgetConfig;
|
|
14
12
|
tracking: TrackingConfig;
|
|
15
13
|
accumulator: AspectAccumulator;
|
|
16
14
|
};
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Latency SLO configuration for Aspects.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* The engine enforces the scope-wide `maxMs` wall-clock SLO at task-dispatch
|
|
5
|
+
* time, measured from the run's start.
|
|
5
6
|
*/
|
|
6
7
|
export type LatencySloConfig = {
|
|
7
|
-
/** Maximum total latency in milliseconds across all tasks. */
|
|
8
|
+
/** Maximum total wall-clock latency in milliseconds across all tasks. */
|
|
8
9
|
maxMs: number;
|
|
9
|
-
/** Optional per-task latency limit in milliseconds. */
|
|
10
|
+
/** Optional per-task latency limit in milliseconds. Not enforced yet. */
|
|
10
11
|
perTask?: number;
|
|
11
|
-
/**
|
|
12
|
+
/** Behavior when the SLO is exceeded. Default: "fail". */
|
|
12
13
|
onExceeded?: "fail" | "warn";
|
|
13
14
|
};
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Token budget configuration for Aspects.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* The engine accumulates per-run token usage and enforces `max` at
|
|
5
|
+
* task-dispatch time.
|
|
5
6
|
*/
|
|
6
7
|
export type TokenBudgetConfig = {
|
|
7
8
|
/** Maximum total tokens across all tasks within the Aspects scope. */
|
|
8
9
|
max: number;
|
|
9
|
-
/** Optional per-task token limit. */
|
|
10
|
+
/** Optional per-task token limit. Not enforced yet. */
|
|
10
11
|
perTask?: number;
|
|
11
|
-
/**
|
|
12
|
+
/** Behavior when the budget is exceeded. Default: "fail". */
|
|
12
13
|
onExceeded?: "fail" | "warn" | "skip-remaining";
|
|
13
14
|
};
|
|
@@ -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,
|
|
@@ -9,8 +9,8 @@ import { AspectContext, createAccumulator, } from "../aspects/AspectContext.js";
|
|
|
9
9
|
*
|
|
10
10
|
* Wraps a section of the workflow tree and propagates token budgets,
|
|
11
11
|
* latency SLOs, and cost budgets to all descendant Task components
|
|
12
|
-
* without modifying individual tasks.
|
|
13
|
-
*
|
|
12
|
+
* without modifying individual tasks. The engine enforces the scope-wide
|
|
13
|
+
* budgets at task-dispatch time.
|
|
14
14
|
*
|
|
15
15
|
* ```tsx
|
|
16
16
|
* <Aspects tokenBudget={{ max: 100_000, perTask: 20_000, onExceeded: "warn" }}>
|
|
@@ -21,18 +21,16 @@ import { AspectContext, createAccumulator, } from "../aspects/AspectContext.js";
|
|
|
21
21
|
* @param {AspectsProps} props
|
|
22
22
|
*/
|
|
23
23
|
export function Aspects(props) {
|
|
24
|
-
const { tokenBudget, latencySlo,
|
|
24
|
+
const { tokenBudget, latencySlo, tracking, children } = props;
|
|
25
25
|
// Merge with parent context if nested
|
|
26
26
|
const parentCtx = React.useContext(AspectContext);
|
|
27
27
|
const resolvedTracking = {
|
|
28
28
|
tokens: tracking?.tokens ?? parentCtx?.tracking?.tokens ?? true,
|
|
29
29
|
latency: tracking?.latency ?? parentCtx?.tracking?.latency ?? true,
|
|
30
|
-
cost: tracking?.cost ?? parentCtx?.tracking?.cost ?? true,
|
|
31
30
|
};
|
|
32
31
|
const value = {
|
|
33
32
|
tokenBudget: tokenBudget ?? parentCtx?.tokenBudget,
|
|
34
33
|
latencySlo: latencySlo ?? parentCtx?.latencySlo,
|
|
35
|
-
costBudget: costBudget ?? parentCtx?.costBudget,
|
|
36
34
|
tracking: resolvedTracking,
|
|
37
35
|
accumulator: parentCtx?.accumulator ?? createAccumulator(),
|
|
38
36
|
};
|
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
import type React from "react";
|
|
2
2
|
import type { TokenBudgetConfig } from "../aspects/TokenBudgetConfig.ts";
|
|
3
3
|
import type { LatencySloConfig } from "../aspects/LatencySloConfig.ts";
|
|
4
|
-
import type { CostBudgetConfig } from "../aspects/CostBudgetConfig.ts";
|
|
5
4
|
import type { TrackingConfig } from "../aspects/TrackingConfig.ts";
|
|
6
5
|
|
|
7
6
|
export type AspectsProps = {
|
|
8
|
-
/** Token budget
|
|
7
|
+
/** Token budget — max total tokens, optional per-task limit, and exceeded behavior. */
|
|
9
8
|
tokenBudget?: TokenBudgetConfig;
|
|
10
|
-
/** Latency SLO
|
|
9
|
+
/** Latency SLO — max total wall-clock latency and exceeded behavior. */
|
|
11
10
|
latencySlo?: LatencySloConfig;
|
|
12
|
-
/** Cost budget metadata. Runtime enforcement is not implemented yet. */
|
|
13
|
-
costBudget?: CostBudgetConfig;
|
|
14
11
|
/** Which metrics to track. Defaults to all enabled. */
|
|
15
12
|
tracking?: TrackingConfig;
|
|
16
13
|
/** Workflow content these aspects apply to. */
|
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)
|
package/src/components/Task.js
CHANGED
|
@@ -223,8 +223,8 @@ export function Task(props) {
|
|
|
223
223
|
ctx?.recordDeferredDep?.(props.id, depNodeIds ?? []);
|
|
224
224
|
return null;
|
|
225
225
|
}
|
|
226
|
-
// Build aspect metadata to attach to the task element
|
|
227
|
-
//
|
|
226
|
+
// Build aspect metadata to attach to the task element so the engine can
|
|
227
|
+
// enforce budgets and track metrics at execution time.
|
|
228
228
|
const aspectMeta = aspectCtx ? buildAspectMeta(aspectCtx) : undefined;
|
|
229
229
|
const agentChain = Array.isArray(agent)
|
|
230
230
|
? fallbackAgent
|
|
@@ -287,12 +287,11 @@ export function Task(props) {
|
|
|
287
287
|
}
|
|
288
288
|
/**
|
|
289
289
|
* Build the __aspects metadata object from the current AspectContext.
|
|
290
|
-
* This is attached to the smithers:task element props
|
|
291
|
-
*
|
|
290
|
+
* This is attached to the smithers:task element props so the engine can read
|
|
291
|
+
* budgets and tracking config at execution time.
|
|
292
292
|
* @param {{
|
|
293
293
|
* tokenBudget?: unknown;
|
|
294
294
|
* latencySlo?: unknown;
|
|
295
|
-
* costBudget?: unknown;
|
|
296
295
|
* tracking?: unknown;
|
|
297
296
|
* accumulator?: unknown;
|
|
298
297
|
* }} aspectCtx
|
|
@@ -303,7 +302,6 @@ function buildAspectMeta(aspectCtx) {
|
|
|
303
302
|
__aspects: {
|
|
304
303
|
tokenBudget: aspectCtx.tokenBudget,
|
|
305
304
|
latencySlo: aspectCtx.latencySlo,
|
|
306
|
-
costBudget: aspectCtx.costBudget,
|
|
307
305
|
tracking: aspectCtx.tracking,
|
|
308
306
|
accumulator: aspectCtx.accumulator,
|
|
309
307
|
},
|
|
@@ -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
|
}
|