// Props
import { ClassifyAndRoute } from "smthrs";
type CategoryConfig = {
agent: AgentLike;
output?: OutputTarget;
prompt?: (item: unknown) => string;
};
type ClassifyAndRouteProps = {
id?: string; // prefix for auto-generated child task IDs; defaults to "classify-and-route"
items: unknown | unknown[];
categories: Record<string, AgentLike | CategoryConfig>;
classifierAgent: AgentLike;
classifierOutput: OutputTarget;
routeOutput: OutputTarget;
classificationResult?: { classifications: Array<{ category: string; itemId?: string }> } | null;
maxConcurrency?: number; // optional; unbounded when omitted
skipIf?: boolean;
children?: ReactNode; // custom classifier prompt
};
const classification = ctx.outputMaybe(outputs.classification, {
nodeId: "classify-and-route-classify",
});
<Workflow name="support-router">
<ClassifyAndRoute
items={ctx.input.tickets}
categories={{ billing: billingAgent, support: supportAgent, sales: salesAgent }}
classifierAgent={classifierAgent}
classifierOutput={outputs.classification}
routeOutput={outputs.handled}
classificationResult={classification}
/>
</Workflow>;
Notes
- Two-phase: the first render classifies; the next uses
classificationResultto mount route handlers. - Each entry’s
categorymust match acategorieskey; unmatched entries are skipped silently. - Route tasks default to
continueOnFail.
Source
The<ClassifyAndRoute> implementation and the files it imports, straight from the package source. This section is generated; edit the source, not this block.
// @smithers-type-exports-begin
/** @typedef {import("./ClassifyAndRouteProps.ts").ClassifyAndRouteProps} ClassifyAndRouteProps */
// @smithers-type-exports-end
import React from "react";
import { Sequence } from "./Sequence.js";
import { Parallel } from "./Parallel.js";
import { Task } from "./Task.js";
/** @typedef {import("@smthrs/agents/AgentLike").AgentLike} AgentLike */
/** @typedef {import("./CategoryConfig.ts").CategoryConfig} CategoryConfig */
/**
* @param {AgentLike | CategoryConfig} value
* @returns {value is CategoryConfig}
*/
function isConfig(value) {
return "agent" in value && typeof value.generate !== "function";
}
/**
* <ClassifyAndRoute> — Classify items then route to category-specific agents.
*
* Composes Sequence, Task, and Parallel. First a classifier Task assigns items
* to categories, then a Parallel block routes each classified item to the
* appropriate category agent.
* @param {ClassifyAndRouteProps} props
*/
export function ClassifyAndRoute(props) {
if (props.skipIf) return null;
const {
id,
items,
categories,
classifierAgent,
classifierOutput,
routeOutput,
classificationResult,
maxConcurrency,
children,
} = props;
const prefix = id ?? "classify-and-route";
const itemList = Array.isArray(items) ? items : [items];
const categoryNames = Object.keys(categories);
// Step 1: Classification task
const classifyTask = React.createElement(Task, {
key: `${prefix}-classify`,
id: `${prefix}-classify`,
output: classifierOutput,
agent: classifierAgent,
label: "Classify items",
children:
children ??
`Classify the following items into categories: ${categoryNames.join(", ")}.\n\nItems:\n${JSON.stringify(itemList, null, 2)}`,
});
// Step 2: Route each classified item to its category agent
const classifications = classificationResult?.classifications ?? [];
const routeElements = classifications
.map((c, idx) => {
const catKey = c.category;
const catEntry = categories[catKey];
if (!catEntry) return null;
const agent = isConfig(catEntry) ? catEntry.agent : catEntry;
const output = isConfig(catEntry) ? (catEntry.output ?? routeOutput) : routeOutput;
const prompt =
isConfig(catEntry) && catEntry.prompt
? catEntry.prompt(c)
: `Handle item classified as "${catKey}":\n${JSON.stringify(c, null, 2)}`;
return React.createElement(Task, {
key: `${prefix}-route-${c.itemId ?? idx}`,
id: `${prefix}-route-${c.itemId ?? idx}`,
output,
agent,
continueOnFail: true,
label: `Route: ${catKey}${c.itemId ? ` (${c.itemId})` : ""}`,
children: prompt,
});
})
.filter(Boolean);
const sequenceChildren = [classifyTask];
if (routeElements.length > 0) {
sequenceChildren.push(
React.createElement(
Parallel,
{
key: `${prefix}-routes`,
id: `${prefix}-routes`,
maxConcurrency,
},
...routeElements,
),
);
}
return React.createElement(Sequence, null, ...sequenceChildren);
}
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";
/** @typedef {import("./ParallelProps.ts").ParallelProps} ParallelProps */
/**
* @param {ParallelProps} props
*/
export function Parallel(props) {
if (props.skipIf) return null;
// Align prop sanitization with other structural components. `label` names
// the phase group in run views (graph, the Claude /workflows mirror).
const next = {
maxConcurrency: props.maxConcurrency,
subtreeConcurrency: props.subtreeConcurrency,
id: props.id,
...(props.label === undefined ? {} : { label: props.label }),
...(props.priority === undefined ? {} : { priority: props.priority }),
...(props.failurePolicy === undefined ? {} : { failurePolicy: props.failurePolicy }),
};
return React.createElement("smithers:parallel", next, props.children);
}
// @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 type React from "react";
import type { AgentLike } from "@smthrs/agents/AgentLike";
import type { CategoryConfig } from "./CategoryConfig.ts";
import type { OutputTarget } from "./OutputTarget.ts";
export type ClassifyAndRouteProps = {
id?: string;
/** Items to classify. A single item or an array of items. */
items: unknown | unknown[];
/** Record mapping category names to agents or config objects. */
categories: Record<string, AgentLike | CategoryConfig>;
/** Agent that classifies items into categories. */
classifierAgent: AgentLike;
/** Output schema for the classification task. */
classifierOutput: OutputTarget;
/** Default output schema for routed work. Can be overridden per-category. */
routeOutput: OutputTarget;
/** Classification result used to drive routing. Typically from ctx.outputMaybe(). */
classificationResult?: {
classifications: Array<{
itemId?: string;
category: string;
[key: string]: unknown;
}>;
} | null;
/** Max parallel routes. */
maxConcurrency?: number;
skipIf?: boolean;
children?: React.ReactNode;
};
import type { AgentLike } from "@smthrs/agents/AgentLike";
import type { OutputTarget } from "./OutputTarget.ts";
export type CategoryConfig = {
agent: AgentLike;
/** Output schema for this category's route handler. Overrides `routeOutput`. */
output?: OutputTarget;
/** Optional prompt for the route handler. Receives the classified item. */
prompt?: (item: unknown) => string;
};