// Props
import { DriftDetector } from "smthrs";
type DriftDetectorProps = {
id?: string; // default "drift"; ids {id}-capture, {id}-compare
captureAgent: AgentLike;
compareAgent: AgentLike;
captureOutput: OutputTarget;
compareOutput: OutputTarget; // include `drifted: boolean`
baseline: unknown;
alertIf?: (comparison: any) => boolean; // default: comparison.drifted === true
alert?: ReactElement;
poll?: { intervalMs?: number; maxPolls?: number }; // default maxPolls = 100; intervalMs is reserved, not yet passed to the Loop
skipIf?: boolean;
};
<Workflow name="api-drift-check">
<DriftDetector
captureAgent={snapshotAgent}
compareAgent={diffAgent}
captureOutput={outputs.capture}
compareOutput={outputs.compare}
baseline={{ endpoints: ["/users", "/orders"], schemaHash: "abc123" }}
alert={
<Task id="notify" output={outputs.notify} agent={slackAgent}>
API drift detected. Notify the team.
</Task>
}
/>
</Workflow>
Notes
- Without
pollthe component runs once; withpollit wraps in a Loop. - Given a comparison output,
alertIfdecides whether to renderalert; withoutalertIf, the trigger iscomparison.drifted === true. - Without
alert, the component compares but takes no action.
Source
The<DriftDetector> implementation and the files it imports, straight from the package source. This section is generated; edit the source, not this block.
import React from "react";
import { SmithersContext } from "@smthrs/react-reconciler/context";
import { Task } from "./Task.js";
import { Sequence } from "./Sequence.js";
import { Branch } from "./Branch.js";
import { Loop } from "./Ralph.js";
/** @typedef {import("./DriftDetectorProps.ts").DriftDetectorProps} DriftDetectorProps */
/**
* @param {unknown} value
* @returns {value is Record<string, unknown>}
*/
function isRecord(value) {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
/**
* @param {unknown} comparison
* @param {((comparison: unknown) => boolean) | undefined} alertIf
* @returns {boolean}
*/
function shouldAlert(comparison, alertIf) {
if (comparison == null) {
return false;
}
if (alertIf) {
return Boolean(alertIf(comparison));
}
return isRecord(comparison) && comparison.drifted === true;
}
/**
* @param {DriftDetectorProps} props
*/
export function DriftDetector(props) {
if (props.skipIf) return null;
const prefix = props.id ?? "drift";
const ctx = React.useContext(SmithersContext);
const comparison = ctx?.outputMaybe(props.compareOutput, { nodeId: `${prefix}-compare` });
const drifted = shouldAlert(comparison, props.alertIf);
const captureTask = React.createElement(Task, {
id: `${prefix}-capture`,
output: props.captureOutput,
agent: props.captureAgent,
children: `Capture the current state for drift detection. Baseline reference: ${
typeof props.baseline === "string" ? props.baseline : JSON.stringify(props.baseline)
}`,
});
const compareTask = React.createElement(Task, {
id: `${prefix}-compare`,
output: props.compareOutput,
agent: props.compareAgent,
dependsOn: [`${prefix}-capture`],
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" ? props.baseline : JSON.stringify(props.baseline)
}`,
});
const alertBranch = props.alert
? React.createElement(Branch, {
if: drifted,
then: props.alert,
})
: null;
const sequenceChildren = [captureTask, compareTask];
if (alertBranch) sequenceChildren.push(alertBranch);
const sequence = React.createElement(Sequence, null, ...sequenceChildren);
if (props.poll) {
const maxPolls = props.poll.maxPolls ?? 100;
if (!Number.isFinite(maxPolls)) {
throw new TypeError("DriftDetector poll.maxPolls must be a finite number.");
}
return React.createElement(
Loop,
{
id: `${prefix}-poll`,
until: false,
maxIterations: maxPolls,
onMaxReached: "return-last",
},
sequence,
);
}
return sequence;
}
// @smithers-type-exports-begin
/** @typedef {import("./OutputSnapshot.ts").OutputSnapshot} OutputSnapshot */
/** @typedef {import("./SmithersCtxOptions.ts").SmithersCtxOptions} SmithersCtxOptions */
// @smithers-type-exports-end
import React from "react";
import { SmithersError } from "@smthrs/errors/SmithersError";
export { SmithersCtx } from "@smthrs/driver/SmithersCtx";
/** @type {React.Context<SmithersCtx<any> | null>} */
export const SmithersContext = React.createContext(null);
SmithersContext.displayName = "SmithersContext";
/**
* @template Schema
* @returns {{ SmithersContext: React.Context<SmithersCtx<Schema> | null>, useCtx: () => SmithersCtx<Schema> }}
*/
export function createSmithersContext() {
/** @type {React.Context<SmithersCtx<Schema> | null>} */
const Context = React.createContext(null);
Context.displayName = "SmithersContext";
/**
* @returns {SmithersCtx<Schema>}
*/
function useCtx() {
const ctx = React.useContext(Context);
if (!ctx) {
throw new SmithersError(
"CONTEXT_OUTSIDE_WORKFLOW",
"useCtx() must be called inside a <Workflow> created by createSmithers()",
);
}
return ctx;
}
return { SmithersContext: Context, useCtx };
}
// @smithers-type-exports-begin
/**
* @template D
* @typedef {import("./InferDeps.ts").InferDeps<D>} InferDeps
*/
/** @typedef {import("./OutputTarget.ts").OutputTarget} OutputTarget */
// @smithers-type-exports-end
import { applyCliToolAllowlist } from "./cliToolAllowlist.js";
import { createTaskComponent } from "./taskCore.js";
export { renderPromptToText } from "./taskCore.js";
/**
* The Node/CLI-agent-aware `Task`. Every render-path behavior (deps
* resolution, agent-chain assembly, MDX prompt rendering, static/compute/agent
* branching) lives in `taskCore.js`; this file only supplies the CLI-agent
* tool-allowlist enforcement step (`applyCliToolAllowlist`, which statically
* imports `ClaudeCodeAgent`/`PiAgent`/`GeminiAgent`/`AntigravityAgent` — see
* `cliToolAllowlist.js`). `Task.browser.js` builds the same component with a
* no-op allowlist step instead, so it never pulls those Node-only
* (`node:child_process`-backed) classes into a browser bundle.
*/
export const Task = createTaskComponent({ applyCliToolAllowlist });
import React from "react";
/** @typedef {import("./SequenceProps.ts").SequenceProps} SequenceProps */
/**
* @param {SequenceProps} props
*/
export function Sequence(props) {
if (props.skipIf) return null;
// Sequence carries only a display label; pass a sanitized bag (align with
// the sanitizing structural components) so control props don't leak through.
// `label` names the phase group in run views (graph, the Claude /workflows
// mirror) and is preserved in the persisted frame XML.
const next = {
...(props.label === undefined ? {} : { label: props.label }),
...(props.failurePolicy === undefined ? {} : { failurePolicy: props.failurePolicy }),
};
return React.createElement("smithers:sequence", next, props.children);
}
import React from "react";
import { SmithersError } from "@smthrs/errors/SmithersError";
/** @typedef {import("./BranchProps.ts").BranchProps} BranchProps */
/**
* @param {BranchProps} props
*/
export function Branch(props) {
// <Branch> resolves its subtree from the `then`/`else` props; any JSX children
// would be silently dropped, removing those tasks from the graph with no
// feedback. Fail fast instead. (Checked before skipIf so a stray-children
// mistake still surfaces even on a skipped branch.)
if (props.children !== undefined && props.children !== null) {
throw new SmithersError(
"INVALID_INPUT",
`<Branch> does not take children. Use the "then" and "else" props instead, e.g. ` +
`<Branch if={cond} then={<Task .../>} else={<Task .../>} />. ` +
`Children passed to <Branch> are silently ignored and would drop those tasks from the graph.`,
);
}
if (props.skipIf) return null;
const chosen = props.if ? props.then : (props.else ?? null);
// The branch is resolved to `chosen` at render time, so the host element
// carries no props of its own (align with the sanitizing structural components).
return React.createElement("smithers:branch", {}, chosen);
}
// @smithers-type-exports-begin
/** @typedef {import("./RalphProps.ts").RalphProps} RalphProps */
// @smithers-type-exports-end
import React from "react";
/** @typedef {import("./LoopProps.ts").LoopProps} LoopProps */
/**
* @param {LoopProps} props
*/
export function Loop(props) {
if (props.skipIf) return null;
// Sanitize to the loop's host props (align with other structural components);
// key/skipIf are React/control props and children are passed separately.
const next = {
id: props.id,
until: props.until,
maxIterations: props.maxIterations,
onMaxReached: props.onMaxReached,
continueAsNewEvery: props.continueAsNewEvery,
};
return React.createElement("smithers:ralph", next, props.children);
}
/** @deprecated Use `Loop` instead. */
export const Ralph = Loop;
import type React from "react";
import type { AgentLike } from "@smthrs/agents/AgentLike";
import type { OutputTarget } from "./OutputTarget.ts";
export type DriftDetectorProps = {
/** ID prefix for generated task/component ids. */
id?: string;
/** Agent that captures the current state snapshot. */
captureAgent: AgentLike;
/** Agent that compares current state against the baseline. */
compareAgent: AgentLike;
/** Output schema for the captured state. */
captureOutput: OutputTarget;
/** Output schema for the comparison result. Should include `drifted: boolean` and `significance: string`. */
compareOutput: OutputTarget;
/** Static baseline data, or a function/agent that fetches it. */
baseline: unknown;
/** Condition function that determines whether to fire the alert. If omitted, uses `comparison.drifted === true`. */
alertIf?: (comparison: unknown) => boolean;
/** Element to render when drift is detected (e.g. a Task that sends a notification). */
alert?: React.ReactElement;
/** If set, wraps the detector in a Loop for periodic polling. */
poll?: {
/** Reserved for future delayed polling; maxPolls currently controls Loop iterations. */
intervalMs?: number;
maxPolls?: number;
};
/** Skip the entire component. */
skipIf?: boolean;
};