@smithers-orchestrator/components 0.16.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/LICENSE +21 -0
- package/package.json +47 -0
- package/src/SmithersWorkflow.ts +1 -0
- package/src/aspects/AspectAccumulator.ts +9 -0
- package/src/aspects/AspectContext.js +29 -0
- package/src/aspects/AspectContextValue.ts +16 -0
- package/src/aspects/CostBudgetConfig.ts +9 -0
- package/src/aspects/LatencySloConfig.ts +11 -0
- package/src/aspects/TokenBudgetConfig.ts +11 -0
- package/src/aspects/TrackingConfig.ts +11 -0
- package/src/aspects/index.js +10 -0
- package/src/components/Approval.js +211 -0
- package/src/components/ApprovalAutoApprove.ts +8 -0
- package/src/components/ApprovalDecision.ts +4 -0
- package/src/components/ApprovalGate.js +45 -0
- package/src/components/ApprovalGateProps.ts +22 -0
- package/src/components/ApprovalMode.ts +1 -0
- package/src/components/ApprovalOption.ts +6 -0
- package/src/components/ApprovalProps.ts +42 -0
- package/src/components/ApprovalRanking.ts +4 -0
- package/src/components/ApprovalRequest.ts +5 -0
- package/src/components/ApprovalSelection.ts +4 -0
- package/src/components/Aspects.js +39 -0
- package/src/components/AspectsProps.ts +18 -0
- package/src/components/Branch.js +12 -0
- package/src/components/BranchProps.ts +8 -0
- package/src/components/CategoryConfig.ts +10 -0
- package/src/components/CheckConfig.ts +8 -0
- package/src/components/CheckSuite.js +71 -0
- package/src/components/CheckSuiteProps.ts +12 -0
- package/src/components/ClassifyAndRoute.js +75 -0
- package/src/components/ClassifyAndRouteProps.ts +30 -0
- package/src/components/ColumnDef.ts +19 -0
- package/src/components/ContentPipeline.js +38 -0
- package/src/components/ContentPipelineProps.ts +12 -0
- package/src/components/ContentPipelineStage.ts +13 -0
- package/src/components/ContinueAsNew.js +27 -0
- package/src/components/ContinueAsNewProps.ts +6 -0
- package/src/components/Debate.js +63 -0
- package/src/components/DebateProps.ts +15 -0
- package/src/components/DecisionRule.ts +10 -0
- package/src/components/DecisionTable.js +42 -0
- package/src/components/DecisionTableProps.ts +14 -0
- package/src/components/DepsSpec.ts +3 -0
- package/src/components/DriftDetector.js +54 -0
- package/src/components/DriftDetectorProps.ts +29 -0
- package/src/components/EscalationChain.js +99 -0
- package/src/components/EscalationChainProps.ts +20 -0
- package/src/components/EscalationLevel.ts +13 -0
- package/src/components/GatherAndSynthesize.js +69 -0
- package/src/components/GatherAndSynthesizeProps.ts +24 -0
- package/src/components/HumanTask.js +94 -0
- package/src/components/HumanTaskProps.ts +27 -0
- package/src/components/InferDeps.ts +8 -0
- package/src/components/Kanban.js +68 -0
- package/src/components/KanbanProps.ts +27 -0
- package/src/components/Loop.js +6 -0
- package/src/components/LoopProps.ts +11 -0
- package/src/components/MergeQueue.js +16 -0
- package/src/components/MergeQueueProps.ts +12 -0
- package/src/components/Optimizer.js +52 -0
- package/src/components/OptimizerProps.ts +25 -0
- package/src/components/OutputTarget.ts +6 -0
- package/src/components/Panel.js +69 -0
- package/src/components/PanelProps.ts +17 -0
- package/src/components/PanelistConfig.ts +7 -0
- package/src/components/Parallel.js +16 -0
- package/src/components/ParallelProps.ts +8 -0
- package/src/components/Poller.js +69 -0
- package/src/components/PollerProps.ts +24 -0
- package/src/components/Ralph.js +17 -0
- package/src/components/RalphProps.ts +4 -0
- package/src/components/ReviewLoop.js +51 -0
- package/src/components/ReviewLoopProps.ts +23 -0
- package/src/components/Runbook.js +91 -0
- package/src/components/RunbookProps.ts +19 -0
- package/src/components/RunbookStep.ts +17 -0
- package/src/components/Saga.js +77 -0
- package/src/components/SagaProps.ts +10 -0
- package/src/components/SagaStepDef.ts +8 -0
- package/src/components/SagaStepProps.ts +7 -0
- package/src/components/Sandbox.js +48 -0
- package/src/components/SandboxProps.ts +46 -0
- package/src/components/SandboxRuntime.ts +1 -0
- package/src/components/SandboxVolumeMount.ts +5 -0
- package/src/components/SandboxWorkspaceSpec.ts +6 -0
- package/src/components/ScanFixVerify.js +60 -0
- package/src/components/ScanFixVerifyProps.ts +30 -0
- package/src/components/Sequence.js +11 -0
- package/src/components/SequenceProps.ts +6 -0
- package/src/components/Signal.js +48 -0
- package/src/components/SignalProps.ts +21 -0
- package/src/components/SourceDef.ts +12 -0
- package/src/components/Subflow.js +32 -0
- package/src/components/SubflowProps.ts +33 -0
- package/src/components/SuperSmithers.js +102 -0
- package/src/components/SuperSmithersProps.ts +20 -0
- package/src/components/Supervisor.js +86 -0
- package/src/components/SupervisorProps.ts +28 -0
- package/src/components/Task.js +319 -0
- package/src/components/TaskProps.ts +57 -0
- package/src/components/Timer.js +42 -0
- package/src/components/TimerProps.ts +21 -0
- package/src/components/TryCatchFinally.js +35 -0
- package/src/components/TryCatchFinallyProps.ts +12 -0
- package/src/components/WaitForEvent.js +37 -0
- package/src/components/WaitForEventProps.ts +28 -0
- package/src/components/Workflow.js +10 -0
- package/src/components/WorkflowProps.ts +7 -0
- package/src/components/Worktree.js +17 -0
- package/src/components/WorktreeProps.ts +11 -0
- package/src/components/control-flow-utils.js +37 -0
- package/src/components/index.js +121 -0
- package/src/index.d.ts +1579 -0
- package/src/index.js +62 -0
- package/src/markdownComponents.js +44 -0
- package/src/renderMdx.js +26 -0
- package/src/types/react-dom-server.d.ts +1 -0
- package/src/types.ts +22 -0
- package/src/zod-to-example.js +87 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// @smithers-type-exports-begin
|
|
2
|
+
/** @typedef {import("./CheckSuiteProps.ts").CheckSuiteProps} CheckSuiteProps */
|
|
3
|
+
// @smithers-type-exports-end
|
|
4
|
+
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { Sequence } from "./Sequence.js";
|
|
7
|
+
import { Parallel } from "./Parallel.js";
|
|
8
|
+
import { Task } from "./Task.js";
|
|
9
|
+
/** @typedef {import("./CheckConfig.ts").CheckConfig} CheckConfig */
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {CheckConfig[] | Record<string, Omit<CheckConfig, "id">>} checks
|
|
13
|
+
* @returns {CheckConfig[]}
|
|
14
|
+
*/
|
|
15
|
+
function normalizeChecks(checks) {
|
|
16
|
+
if (Array.isArray(checks))
|
|
17
|
+
return checks;
|
|
18
|
+
return Object.entries(checks).map(([key, cfg]) => ({
|
|
19
|
+
id: key,
|
|
20
|
+
...cfg,
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* <CheckSuite> — Parallel checks with auto-aggregated pass/fail verdict.
|
|
25
|
+
*
|
|
26
|
+
* Composes: Sequence > Parallel[Task per check] > Task(verdict aggregator)
|
|
27
|
+
* @param {CheckSuiteProps} props
|
|
28
|
+
*/
|
|
29
|
+
export function CheckSuite(props) {
|
|
30
|
+
if (props.skipIf)
|
|
31
|
+
return null;
|
|
32
|
+
const { id, checks, verdictOutput, strategy = "all-pass", maxConcurrency, continueOnFail = true, } = props;
|
|
33
|
+
const prefix = id ?? "checksuite";
|
|
34
|
+
const normalized = normalizeChecks(checks);
|
|
35
|
+
// Build parallel check tasks
|
|
36
|
+
const checkTasks = normalized.map((check) => {
|
|
37
|
+
const taskId = `${prefix}-${check.id}`;
|
|
38
|
+
const childContent = check.command
|
|
39
|
+
? `Run check: ${check.command}`
|
|
40
|
+
: `Run check: ${check.label ?? check.id}`;
|
|
41
|
+
const taskProps = {
|
|
42
|
+
key: taskId,
|
|
43
|
+
id: taskId,
|
|
44
|
+
output: verdictOutput,
|
|
45
|
+
continueOnFail,
|
|
46
|
+
label: check.label ?? check.id,
|
|
47
|
+
};
|
|
48
|
+
if (check.agent) {
|
|
49
|
+
taskProps.agent = check.agent;
|
|
50
|
+
}
|
|
51
|
+
return React.createElement(Task, taskProps, childContent);
|
|
52
|
+
});
|
|
53
|
+
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.";
|
|
65
|
+
const verdictTask = React.createElement(Task, {
|
|
66
|
+
id: `${prefix}-verdict`,
|
|
67
|
+
output: verdictOutput,
|
|
68
|
+
needs,
|
|
69
|
+
}, `Aggregate check results into a pass/fail verdict.\n\nStrategy: ${strategyDesc}`);
|
|
70
|
+
return React.createElement(Sequence, null, parallelEl, verdictTask);
|
|
71
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CheckConfig } from "./CheckConfig.ts";
|
|
2
|
+
import type { OutputTarget } from "./OutputTarget.ts";
|
|
3
|
+
|
|
4
|
+
export type CheckSuiteProps = {
|
|
5
|
+
id?: string;
|
|
6
|
+
checks: CheckConfig[] | Record<string, Omit<CheckConfig, "id">>;
|
|
7
|
+
verdictOutput: OutputTarget;
|
|
8
|
+
strategy?: "all-pass" | "majority" | "any-pass";
|
|
9
|
+
maxConcurrency?: number;
|
|
10
|
+
continueOnFail?: boolean;
|
|
11
|
+
skipIf?: boolean;
|
|
12
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// @smithers-type-exports-begin
|
|
2
|
+
/** @typedef {import("./ClassifyAndRouteProps.ts").ClassifyAndRouteProps} ClassifyAndRouteProps */
|
|
3
|
+
// @smithers-type-exports-end
|
|
4
|
+
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { Sequence } from "./Sequence.js";
|
|
7
|
+
import { Parallel } from "./Parallel.js";
|
|
8
|
+
import { Task } from "./Task.js";
|
|
9
|
+
/** @typedef {import("@smithers-orchestrator/agents/AgentLike").AgentLike} AgentLike */
|
|
10
|
+
/** @typedef {import("./CategoryConfig.ts").CategoryConfig} CategoryConfig */
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {AgentLike | CategoryConfig} value
|
|
14
|
+
* @returns {value is CategoryConfig}
|
|
15
|
+
*/
|
|
16
|
+
function isConfig(value) {
|
|
17
|
+
return "agent" in value && typeof value.generate !== "function";
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* <ClassifyAndRoute> — Classify items then route to category-specific agents.
|
|
21
|
+
*
|
|
22
|
+
* Composes Sequence, Task, and Parallel. First a classifier Task assigns items
|
|
23
|
+
* to categories, then a Parallel block routes each classified item to the
|
|
24
|
+
* appropriate category agent.
|
|
25
|
+
* @param {ClassifyAndRouteProps} props
|
|
26
|
+
*/
|
|
27
|
+
export function ClassifyAndRoute(props) {
|
|
28
|
+
if (props.skipIf)
|
|
29
|
+
return null;
|
|
30
|
+
const { id, items, categories, classifierAgent, classifierOutput, routeOutput, classificationResult, maxConcurrency, children, } = props;
|
|
31
|
+
const prefix = id ?? "classify-and-route";
|
|
32
|
+
const itemList = Array.isArray(items) ? items : [items];
|
|
33
|
+
const categoryNames = Object.keys(categories);
|
|
34
|
+
// Step 1: Classification task
|
|
35
|
+
const classifyTask = React.createElement(Task, {
|
|
36
|
+
key: `${prefix}-classify`,
|
|
37
|
+
id: `${prefix}-classify`,
|
|
38
|
+
output: classifierOutput,
|
|
39
|
+
agent: classifierAgent,
|
|
40
|
+
label: "Classify items",
|
|
41
|
+
children: children ??
|
|
42
|
+
`Classify the following items into categories: ${categoryNames.join(", ")}.\n\nItems:\n${JSON.stringify(itemList, null, 2)}`,
|
|
43
|
+
});
|
|
44
|
+
// Step 2: Route each classified item to its category agent
|
|
45
|
+
const classifications = classificationResult?.classifications ?? [];
|
|
46
|
+
const routeElements = classifications.map((c, idx) => {
|
|
47
|
+
const catKey = c.category;
|
|
48
|
+
const catEntry = categories[catKey];
|
|
49
|
+
if (!catEntry)
|
|
50
|
+
return null;
|
|
51
|
+
const agent = isConfig(catEntry) ? catEntry.agent : catEntry;
|
|
52
|
+
const output = isConfig(catEntry) ? (catEntry.output ?? routeOutput) : routeOutput;
|
|
53
|
+
const prompt = isConfig(catEntry) && catEntry.prompt
|
|
54
|
+
? catEntry.prompt(c)
|
|
55
|
+
: `Handle item classified as "${catKey}":\n${JSON.stringify(c, null, 2)}`;
|
|
56
|
+
return React.createElement(Task, {
|
|
57
|
+
key: `${prefix}-route-${c.itemId ?? idx}`,
|
|
58
|
+
id: `${prefix}-route-${c.itemId ?? idx}`,
|
|
59
|
+
output,
|
|
60
|
+
agent,
|
|
61
|
+
continueOnFail: true,
|
|
62
|
+
label: `Route: ${catKey}${c.itemId ? ` (${c.itemId})` : ""}`,
|
|
63
|
+
children: prompt,
|
|
64
|
+
});
|
|
65
|
+
}).filter(Boolean);
|
|
66
|
+
const sequenceChildren = [classifyTask];
|
|
67
|
+
if (routeElements.length > 0) {
|
|
68
|
+
sequenceChildren.push(React.createElement(Parallel, {
|
|
69
|
+
key: `${prefix}-routes`,
|
|
70
|
+
id: `${prefix}-routes`,
|
|
71
|
+
maxConcurrency,
|
|
72
|
+
}, ...routeElements));
|
|
73
|
+
}
|
|
74
|
+
return React.createElement(Sequence, null, ...sequenceChildren);
|
|
75
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
import type { AgentLike } from "@smithers-orchestrator/agents/AgentLike";
|
|
3
|
+
import type { CategoryConfig } from "./CategoryConfig.ts";
|
|
4
|
+
import type { OutputTarget } from "./OutputTarget.ts";
|
|
5
|
+
|
|
6
|
+
export type ClassifyAndRouteProps = {
|
|
7
|
+
id?: string;
|
|
8
|
+
/** Items to classify. A single item or an array of items. */
|
|
9
|
+
items: unknown | unknown[];
|
|
10
|
+
/** Record mapping category names to agents or config objects. */
|
|
11
|
+
categories: Record<string, AgentLike | CategoryConfig>;
|
|
12
|
+
/** Agent that classifies items into categories. */
|
|
13
|
+
classifierAgent: AgentLike;
|
|
14
|
+
/** Output schema for the classification task. */
|
|
15
|
+
classifierOutput: OutputTarget;
|
|
16
|
+
/** Default output schema for routed work. Can be overridden per-category. */
|
|
17
|
+
routeOutput: OutputTarget;
|
|
18
|
+
/** Classification result used to drive routing. Typically from ctx.outputMaybe(). */
|
|
19
|
+
classificationResult?: {
|
|
20
|
+
classifications: Array<{
|
|
21
|
+
itemId?: string;
|
|
22
|
+
category: string;
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
}>;
|
|
25
|
+
} | null;
|
|
26
|
+
/** Max parallel routes. */
|
|
27
|
+
maxConcurrency?: number;
|
|
28
|
+
skipIf?: boolean;
|
|
29
|
+
children?: React.ReactNode;
|
|
30
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { AgentLike } from "@smithers-orchestrator/agents/AgentLike";
|
|
2
|
+
import type { TaskProps } from "./TaskProps.ts";
|
|
3
|
+
import type { OutputTarget } from "./OutputTarget.ts";
|
|
4
|
+
|
|
5
|
+
type ColumnTaskProps = Omit<Partial<TaskProps<unknown>>, "agent" | "children" | "id" | "key" | "output" | "smithersContext">;
|
|
6
|
+
|
|
7
|
+
export type ColumnDef = {
|
|
8
|
+
name: string;
|
|
9
|
+
agent: AgentLike;
|
|
10
|
+
/** Output schema for tasks in this column. */
|
|
11
|
+
output: OutputTarget;
|
|
12
|
+
/** Prompt template. Receives `{ item, column }` and returns a string. */
|
|
13
|
+
prompt?: (ctx: {
|
|
14
|
+
item: unknown;
|
|
15
|
+
column: string;
|
|
16
|
+
}) => string;
|
|
17
|
+
/** Optional Task props applied to each generated item task in this column. */
|
|
18
|
+
task?: ColumnTaskProps;
|
|
19
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// @smithers-type-exports-begin
|
|
2
|
+
/** @typedef {import("./ContentPipelineProps.ts").ContentPipelineProps} ContentPipelineProps */
|
|
3
|
+
/** @typedef {import("./ContentPipelineStage.ts").ContentPipelineStage} ContentPipelineStage */
|
|
4
|
+
// @smithers-type-exports-end
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { Sequence } from "./Sequence.js";
|
|
8
|
+
import { Task } from "./Task.js";
|
|
9
|
+
/**
|
|
10
|
+
* Progressive content refinement: outline -> draft -> edit -> publish.
|
|
11
|
+
*
|
|
12
|
+
* Composes Sequence and Task to create a typed waterfall where each
|
|
13
|
+
* stage is explicitly defined. Each Task uses `needs` to depend on
|
|
14
|
+
* the previous stage, passing output forward through the pipeline.
|
|
15
|
+
* @param {ContentPipelineProps} props
|
|
16
|
+
*/
|
|
17
|
+
export function ContentPipeline(props) {
|
|
18
|
+
if (props.skipIf)
|
|
19
|
+
return null;
|
|
20
|
+
const { stages, children } = props;
|
|
21
|
+
const taskElements = stages.map((stage, index) => {
|
|
22
|
+
const taskProps = {
|
|
23
|
+
id: stage.id,
|
|
24
|
+
output: stage.output,
|
|
25
|
+
agent: stage.agent,
|
|
26
|
+
label: stage.label,
|
|
27
|
+
};
|
|
28
|
+
if (index === 0) {
|
|
29
|
+
// First stage receives the initial prompt.
|
|
30
|
+
return React.createElement(Task, taskProps, children);
|
|
31
|
+
}
|
|
32
|
+
// Subsequent stages depend on the previous stage.
|
|
33
|
+
const prevStage = stages[index - 1];
|
|
34
|
+
taskProps.needs = { previous: prevStage.id };
|
|
35
|
+
return React.createElement(Task, taskProps, `Continue from the previous stage's output. Perform: ${stage.label ?? stage.id}`);
|
|
36
|
+
});
|
|
37
|
+
return React.createElement(Sequence, null, ...taskElements);
|
|
38
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
import type { ContentPipelineStage } from "./ContentPipelineStage.ts";
|
|
3
|
+
|
|
4
|
+
export type ContentPipelineProps = {
|
|
5
|
+
id?: string;
|
|
6
|
+
/** Pipeline stages executed in order. Each stage receives the previous stage's output. */
|
|
7
|
+
stages: ContentPipelineStage[];
|
|
8
|
+
/** Skip the entire pipeline. */
|
|
9
|
+
skipIf?: boolean;
|
|
10
|
+
/** Initial prompt/content for the first stage (string or ReactNode). */
|
|
11
|
+
children: string | React.ReactNode;
|
|
12
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AgentLike } from "@smithers-orchestrator/agents/AgentLike";
|
|
2
|
+
import type { OutputTarget } from "./OutputTarget.ts";
|
|
3
|
+
|
|
4
|
+
export type ContentPipelineStage = {
|
|
5
|
+
/** Unique identifier for this stage. */
|
|
6
|
+
id: string;
|
|
7
|
+
/** Agent that performs this stage's work. */
|
|
8
|
+
agent: AgentLike;
|
|
9
|
+
/** Output schema for this stage. */
|
|
10
|
+
output: OutputTarget;
|
|
11
|
+
/** Human-readable label for the stage (used as task label). */
|
|
12
|
+
label?: string;
|
|
13
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
/** @typedef {import("./ContinueAsNewProps.ts").ContinueAsNewProps} ContinueAsNewProps */
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {unknown} state
|
|
6
|
+
* @returns {string | undefined}
|
|
7
|
+
*/
|
|
8
|
+
function serializeState(state) {
|
|
9
|
+
if (state === undefined)
|
|
10
|
+
return undefined;
|
|
11
|
+
return JSON.stringify(state);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* @param {ContinueAsNewProps} props
|
|
15
|
+
*/
|
|
16
|
+
export function ContinueAsNew(props) {
|
|
17
|
+
return React.createElement("smithers:continue-as-new", {
|
|
18
|
+
stateJson: serializeState(props.state),
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Convenience helper for conditional continuation inside workflow JSX:
|
|
23
|
+
* `{shouldContinue ? continueAsNew({ cursor }) : null}`
|
|
24
|
+
*/
|
|
25
|
+
export function continueAsNew(state) {
|
|
26
|
+
return React.createElement(ContinueAsNew, { state });
|
|
27
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// @smithers-type-exports-begin
|
|
2
|
+
/** @typedef {import("./DebateProps.ts").DebateProps} DebateProps */
|
|
3
|
+
// @smithers-type-exports-end
|
|
4
|
+
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { Sequence } from "./Sequence.js";
|
|
7
|
+
import { Parallel } from "./Parallel.js";
|
|
8
|
+
import { Loop } from "./Ralph.js";
|
|
9
|
+
import { Task } from "./Task.js";
|
|
10
|
+
/**
|
|
11
|
+
* <Debate> — Adversarial rounds with rebuttals, followed by a judge verdict.
|
|
12
|
+
*
|
|
13
|
+
* Composes: Sequence > Loop[Parallel(proposer, opponent)] > Task(judge)
|
|
14
|
+
* @param {DebateProps} props
|
|
15
|
+
*/
|
|
16
|
+
export function Debate(props) {
|
|
17
|
+
if (props.skipIf)
|
|
18
|
+
return null;
|
|
19
|
+
const { id, proposer, opponent, judge, rounds = 2, argumentOutput, verdictOutput, topic, } = props;
|
|
20
|
+
const prefix = id ?? "debate";
|
|
21
|
+
// Build round tasks inside a loop
|
|
22
|
+
// Each round: proposer and opponent argue in parallel
|
|
23
|
+
const proposerTask = React.createElement(Task, {
|
|
24
|
+
id: `${prefix}-proposer`,
|
|
25
|
+
output: argumentOutput,
|
|
26
|
+
agent: proposer,
|
|
27
|
+
label: "Proposer",
|
|
28
|
+
children: React.createElement(React.Fragment, null, "Argue FOR the following topic:\n\n", topic),
|
|
29
|
+
});
|
|
30
|
+
const opponentTask = React.createElement(Task, {
|
|
31
|
+
id: `${prefix}-opponent`,
|
|
32
|
+
output: argumentOutput,
|
|
33
|
+
agent: opponent,
|
|
34
|
+
label: "Opponent",
|
|
35
|
+
children: React.createElement(React.Fragment, null, "Argue AGAINST the following topic:\n\n", topic),
|
|
36
|
+
});
|
|
37
|
+
const roundParallel = React.createElement(Parallel, null, proposerTask, opponentTask);
|
|
38
|
+
const roundSequence = React.createElement(Sequence, null, roundParallel);
|
|
39
|
+
// Loop wraps the round sequence
|
|
40
|
+
// `until` is always false here — the runtime re-renders the tree each frame,
|
|
41
|
+
// so the caller controls iteration via their own ctx-based condition.
|
|
42
|
+
// We set maxIterations to cap the rounds.
|
|
43
|
+
const loopEl = React.createElement(Loop, {
|
|
44
|
+
id: `${prefix}-loop`,
|
|
45
|
+
until: false,
|
|
46
|
+
maxIterations: rounds,
|
|
47
|
+
onMaxReached: "return-last",
|
|
48
|
+
}, roundSequence);
|
|
49
|
+
// Judge verdict after all rounds
|
|
50
|
+
const judgeNeeds = {
|
|
51
|
+
[`${prefix}-proposer`]: `${prefix}-proposer`,
|
|
52
|
+
[`${prefix}-opponent`]: `${prefix}-opponent`,
|
|
53
|
+
};
|
|
54
|
+
const judgeTask = React.createElement(Task, {
|
|
55
|
+
id: `${prefix}-judge`,
|
|
56
|
+
output: verdictOutput,
|
|
57
|
+
agent: judge,
|
|
58
|
+
needs: judgeNeeds,
|
|
59
|
+
label: "Judge",
|
|
60
|
+
children: React.createElement(React.Fragment, null, "Review all arguments from both sides and render a verdict on:\n\n", topic),
|
|
61
|
+
});
|
|
62
|
+
return React.createElement(Sequence, null, loopEl, judgeTask);
|
|
63
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
import type { AgentLike } from "@smithers-orchestrator/agents/AgentLike";
|
|
3
|
+
import type { OutputTarget } from "./OutputTarget.ts";
|
|
4
|
+
|
|
5
|
+
export type DebateProps = {
|
|
6
|
+
id?: string;
|
|
7
|
+
proposer: AgentLike;
|
|
8
|
+
opponent: AgentLike;
|
|
9
|
+
judge: AgentLike;
|
|
10
|
+
rounds?: number;
|
|
11
|
+
argumentOutput: OutputTarget;
|
|
12
|
+
verdictOutput: OutputTarget;
|
|
13
|
+
topic: string | React.ReactNode;
|
|
14
|
+
skipIf?: boolean;
|
|
15
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
|
|
3
|
+
export type DecisionRule = {
|
|
4
|
+
/** Condition evaluated at render time. */
|
|
5
|
+
when: boolean;
|
|
6
|
+
/** Element to render when this rule matches. */
|
|
7
|
+
then: React.ReactElement;
|
|
8
|
+
/** Optional display label for the rule. */
|
|
9
|
+
label?: string;
|
|
10
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// @smithers-type-exports-begin
|
|
2
|
+
/** @typedef {import("./DecisionRule.ts").DecisionRule} DecisionRule */
|
|
3
|
+
/** @typedef {import("./DecisionTableProps.ts").DecisionTableProps} DecisionTableProps */
|
|
4
|
+
// @smithers-type-exports-end
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { Branch } from "./Branch.js";
|
|
8
|
+
import { Parallel } from "./Parallel.js";
|
|
9
|
+
/**
|
|
10
|
+
* Structured deterministic routing. Replaces deeply nested Branches with a
|
|
11
|
+
* flat, declarative rule table.
|
|
12
|
+
*
|
|
13
|
+
* - `"first-match"` builds nested Branch elements so the first matching rule wins.
|
|
14
|
+
* - `"all-match"` gathers all matching rules' `then` elements into a Parallel.
|
|
15
|
+
*
|
|
16
|
+
* Composes Branch and Parallel internally.
|
|
17
|
+
* @param {DecisionTableProps} props
|
|
18
|
+
*/
|
|
19
|
+
export function DecisionTable(props) {
|
|
20
|
+
if (props.skipIf)
|
|
21
|
+
return null;
|
|
22
|
+
const { rules, strategy = "first-match" } = props;
|
|
23
|
+
if (strategy === "all-match") {
|
|
24
|
+
const matching = rules.filter((r) => r.when).map((r) => r.then);
|
|
25
|
+
if (matching.length === 0) {
|
|
26
|
+
return props.default ?? null;
|
|
27
|
+
}
|
|
28
|
+
return React.createElement(Parallel, { id: props.id }, ...matching);
|
|
29
|
+
}
|
|
30
|
+
// "first-match": build nested Branches from the last rule backward.
|
|
31
|
+
// The innermost else is the default fallback.
|
|
32
|
+
let fallback = props.default ?? null;
|
|
33
|
+
for (let i = rules.length - 1; i >= 0; i--) {
|
|
34
|
+
const rule = rules[i];
|
|
35
|
+
fallback = React.createElement(Branch, {
|
|
36
|
+
if: rule.when,
|
|
37
|
+
then: rule.then,
|
|
38
|
+
else: fallback,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return fallback;
|
|
42
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
import type { DecisionRule } from "./DecisionRule.ts";
|
|
3
|
+
|
|
4
|
+
export type DecisionTableProps = {
|
|
5
|
+
/** ID prefix for generated wrapper nodes. */
|
|
6
|
+
id?: string;
|
|
7
|
+
/** Ordered list of rules. Each rule has a `when` condition and a `then` element. */
|
|
8
|
+
rules: DecisionRule[];
|
|
9
|
+
/** Fallback element rendered when no rules match. */
|
|
10
|
+
default?: React.ReactElement;
|
|
11
|
+
/** `"first-match"` (default): first matching rule wins. `"all-match"`: all matching rules run in parallel. */
|
|
12
|
+
strategy?: "first-match" | "all-match";
|
|
13
|
+
skipIf?: boolean;
|
|
14
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Task } from "./Task.js";
|
|
3
|
+
import { Sequence } from "./Sequence.js";
|
|
4
|
+
import { Branch } from "./Branch.js";
|
|
5
|
+
import { Loop } from "./Ralph.js";
|
|
6
|
+
/** @typedef {import("./DriftDetectorProps.ts").DriftDetectorProps} DriftDetectorProps */
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {DriftDetectorProps} props
|
|
10
|
+
*/
|
|
11
|
+
export function DriftDetector(props) {
|
|
12
|
+
if (props.skipIf)
|
|
13
|
+
return null;
|
|
14
|
+
const prefix = props.id ?? "drift";
|
|
15
|
+
// Determine if drift was detected from comparison output.
|
|
16
|
+
// At render time, comparison may not exist yet, so default to false.
|
|
17
|
+
const drifted = false; // Resolved at runtime via reactive re-render
|
|
18
|
+
const captureTask = React.createElement(Task, {
|
|
19
|
+
id: `${prefix}-capture`,
|
|
20
|
+
output: props.captureOutput,
|
|
21
|
+
agent: props.captureAgent,
|
|
22
|
+
children: `Capture the current state for drift detection. Baseline reference: ${typeof props.baseline === "string"
|
|
23
|
+
? props.baseline
|
|
24
|
+
: JSON.stringify(props.baseline)}`,
|
|
25
|
+
});
|
|
26
|
+
const compareTask = React.createElement(Task, {
|
|
27
|
+
id: `${prefix}-compare`,
|
|
28
|
+
output: props.compareOutput,
|
|
29
|
+
agent: props.compareAgent,
|
|
30
|
+
dependsOn: [`${prefix}-capture`],
|
|
31
|
+
children: `Compare the captured current state against the baseline and determine if meaningful drift has occurred. Include a "drifted" boolean and "significance" string in your response. Baseline: ${typeof props.baseline === "string"
|
|
32
|
+
? props.baseline
|
|
33
|
+
: JSON.stringify(props.baseline)}`,
|
|
34
|
+
});
|
|
35
|
+
const alertBranch = props.alert
|
|
36
|
+
? React.createElement(Branch, {
|
|
37
|
+
if: drifted,
|
|
38
|
+
then: props.alert,
|
|
39
|
+
})
|
|
40
|
+
: null;
|
|
41
|
+
const sequenceChildren = [captureTask, compareTask];
|
|
42
|
+
if (alertBranch)
|
|
43
|
+
sequenceChildren.push(alertBranch);
|
|
44
|
+
const sequence = React.createElement(Sequence, null, ...sequenceChildren);
|
|
45
|
+
if (props.poll) {
|
|
46
|
+
return React.createElement(Loop, {
|
|
47
|
+
id: `${prefix}-poll`,
|
|
48
|
+
until: false,
|
|
49
|
+
maxIterations: props.poll.maxPolls ?? 100,
|
|
50
|
+
onMaxReached: "return-last",
|
|
51
|
+
}, sequence);
|
|
52
|
+
}
|
|
53
|
+
return sequence;
|
|
54
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
import type { AgentLike } from "@smithers-orchestrator/agents/AgentLike";
|
|
3
|
+
import type { OutputTarget } from "./OutputTarget.ts";
|
|
4
|
+
|
|
5
|
+
export type DriftDetectorProps = {
|
|
6
|
+
/** ID prefix for generated task/component ids. */
|
|
7
|
+
id?: string;
|
|
8
|
+
/** Agent that captures the current state snapshot. */
|
|
9
|
+
captureAgent: AgentLike;
|
|
10
|
+
/** Agent that compares current state against the baseline. */
|
|
11
|
+
compareAgent: AgentLike;
|
|
12
|
+
/** Output schema for the captured state. */
|
|
13
|
+
captureOutput: OutputTarget;
|
|
14
|
+
/** Output schema for the comparison result. Should include `drifted: boolean` and `significance: string`. */
|
|
15
|
+
compareOutput: OutputTarget;
|
|
16
|
+
/** Static baseline data, or a function/agent that fetches it. */
|
|
17
|
+
baseline: unknown;
|
|
18
|
+
/** Condition function that determines whether to fire the alert. If omitted, uses the `drifted` field from comparison output. */
|
|
19
|
+
alertIf?: (comparison: unknown) => boolean;
|
|
20
|
+
/** Element to render when drift is detected (e.g. a Task that sends a notification). */
|
|
21
|
+
alert?: React.ReactElement;
|
|
22
|
+
/** If set, wraps the detector in a Loop for periodic polling. */
|
|
23
|
+
poll?: {
|
|
24
|
+
intervalMs: number;
|
|
25
|
+
maxPolls?: number;
|
|
26
|
+
};
|
|
27
|
+
/** Skip the entire component. */
|
|
28
|
+
skipIf?: boolean;
|
|
29
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// @smithers-type-exports-begin
|
|
2
|
+
/** @typedef {import("./EscalationChainProps.ts").EscalationChainProps} EscalationChainProps */
|
|
3
|
+
/** @typedef {import("./EscalationLevel.ts").EscalationLevel} EscalationLevel */
|
|
4
|
+
// @smithers-type-exports-end
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { Sequence } from "./Sequence.js";
|
|
8
|
+
import { Branch } from "./Branch.js";
|
|
9
|
+
import { Task } from "./Task.js";
|
|
10
|
+
import { Approval } from "./Approval.js";
|
|
11
|
+
/**
|
|
12
|
+
* Escalation chain: tries agents in order, escalating on failure or when
|
|
13
|
+
* `escalateIf` returns `true`. Optionally ends with a human approval fallback.
|
|
14
|
+
*
|
|
15
|
+
* Composes Sequence + Task (with `continueOnFail`) + Branch + Approval.
|
|
16
|
+
* @param {EscalationChainProps} props
|
|
17
|
+
*/
|
|
18
|
+
export function EscalationChain(props) {
|
|
19
|
+
if (props.skipIf)
|
|
20
|
+
return null;
|
|
21
|
+
const prefix = props.id ?? "escalation";
|
|
22
|
+
const { levels, children, humanFallback, humanRequest, escalationOutput } = props;
|
|
23
|
+
// Build the chain from the last level forward, nesting each level inside a
|
|
24
|
+
// Branch that gates on the previous level's escalation condition.
|
|
25
|
+
// We construct the elements bottom-up so the final element is a single
|
|
26
|
+
// Sequence that evaluates top-down at runtime.
|
|
27
|
+
const levelElements = [];
|
|
28
|
+
for (let i = 0; i < levels.length; i++) {
|
|
29
|
+
const level = levels[i];
|
|
30
|
+
const levelId = `${prefix}-level-${i}`;
|
|
31
|
+
const isFirst = i === 0;
|
|
32
|
+
const taskEl = React.createElement(Task, {
|
|
33
|
+
id: levelId,
|
|
34
|
+
output: level.output,
|
|
35
|
+
agent: level.agent,
|
|
36
|
+
continueOnFail: true,
|
|
37
|
+
label: level.label ?? `Escalation level ${i}`,
|
|
38
|
+
children: children,
|
|
39
|
+
});
|
|
40
|
+
if (isFirst) {
|
|
41
|
+
// First level always runs.
|
|
42
|
+
levelElements.push(taskEl);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
// 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.
|
|
54
|
+
const prevLevel = levels[i - 1];
|
|
55
|
+
const checkId = `${prefix}-check-${i - 1}`;
|
|
56
|
+
const checkTask = React.createElement(Task, {
|
|
57
|
+
id: checkId,
|
|
58
|
+
output: escalationOutput,
|
|
59
|
+
continueOnFail: true,
|
|
60
|
+
label: `Check escalation from level ${i - 1}`,
|
|
61
|
+
children: () => {
|
|
62
|
+
// This compute function runs at task execution time.
|
|
63
|
+
// It evaluates the previous level's escalateIf predicate.
|
|
64
|
+
return {
|
|
65
|
+
escalated: true,
|
|
66
|
+
fromLevel: i - 1,
|
|
67
|
+
toLevel: i,
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
// Gate the current level: it always mounts when we reach this point
|
|
72
|
+
// in the sequence because the previous level had continueOnFail.
|
|
73
|
+
// The Branch uses `true` here because the sequence only reaches this
|
|
74
|
+
// point if the previous task failed or escalateIf was configured.
|
|
75
|
+
const gatedLevel = React.createElement(Branch, {
|
|
76
|
+
if: true,
|
|
77
|
+
then: taskEl,
|
|
78
|
+
});
|
|
79
|
+
levelElements.push(checkTask);
|
|
80
|
+
levelElements.push(gatedLevel);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Append human fallback if requested.
|
|
84
|
+
if (humanFallback) {
|
|
85
|
+
const humanId = `${prefix}-human-fallback`;
|
|
86
|
+
const request = humanRequest ?? {
|
|
87
|
+
title: "Escalation requires human review",
|
|
88
|
+
summary: `All ${levels.length} automated levels have been exhausted.`,
|
|
89
|
+
};
|
|
90
|
+
levelElements.push(React.createElement(Approval, {
|
|
91
|
+
id: humanId,
|
|
92
|
+
output: escalationOutput,
|
|
93
|
+
request,
|
|
94
|
+
continueOnFail: true,
|
|
95
|
+
label: request.title,
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
return React.createElement(Sequence, {}, ...levelElements);
|
|
99
|
+
}
|