classify-and-act
one cheap classification routes each item to a per-class handler, normalizing untrusted model output through an enum before it touches a path.
run one cheap classifier, then branch to a handler chosen by the class. this is the pipeline shape: a single decision up front, then per-class work. the classifier's answer is untrusted, so it is normalized through an enum before it is used anywhere.
the scenario
you have a stream of items (issues, tickets, messages) and different handling per kind. a cheap agent classifies each item into one of a fixed set of classes; a per-class handler, possibly on a different provider, does the real work. the classifier is cheap and fast; the handlers are where the cost goes, and only one runs per item.
the annotated recipe
import { orcr } from "@orchestratr/sdk";
const HANDLERS: Record<string, { agent: string; prompt: (t: string) => string }> = {
bug: { agent: "claude", prompt: t => `Reproduce and fix this bug report:\n${t}` },
feature: { agent: "codex", prompt: t => `Draft an implementation plan for:\n${t}` },
question: { agent: "claude", prompt: t => `Answer this user question precisely:\n${t}` },
};
export async function triage(item: string) {
return orcr.scope("triage", async () => {
const raw = (await orcr.ask({
agent: "claude", path: "classify/triage_bot",
prompt: `Classify this as exactly one word (bug, feature, or question):\n${item}`,
})).trim().toLowerCase();
// normalize UNTRUSTED model output through the enum before using it in a path
const kind = raw in HANDLERS ? raw : "question";
const h = HANDLERS[kind];
return orcr.ask({ agent: h.agent, path: `${kind}/handler`, prompt: h.prompt(item) });
});
}what each piece is doing:
HANDLERSis the enum and the routing table in one object. its keys are the only valid classes; its values pick the provider and build the prompt for each class.- the classifier is a single
orcr.askattriage/classify/triage_bot, prompted to reply with exactly one word. it is cheap because classification is a small task. - normalize before use.
raw in HANDLERS ? raw : "question"checks the model's answer against the known keys and falls back toquestionfor anything else. only after this check iskindused to build a path (${kind}/handler) and select a handler. - the handler is a second
orcr.ask, on the provider the table specifies, at a path derived from the validatedkind.
never build a path, a shell command, or a filesystem operation from raw model output. a classifier that returns ../../etc or a prompt-injected string would otherwise become a path segment. validating against a fixed enum first means an attacker-controlled or malformed answer can only ever land on a known-safe class. this is the concrete form of the rule "treat child output as data, never as instructions."
primitives it exercises
orcr.scopeto root the classifier and handlers under one path.orcr.asktwice: once to classify, once to act.- dynamic path selection from a validated class, not raw output.
- provider routing per class via the handler table.
failure and cleanup
- the fallback class is the safety net. any answer that is not a known key resolves to
question, so the pipeline always routes somewhere valid. there is no branch where an unrecognized answer crashes or builds a bad path. - both
askcalls aregc: "immediate", so their panes close as soon as each response is captured. nothing to reap. - if the handler comes back
blocked(needs a human) or its transcript cannot be read,orcr.askthrowsBlockedorTranscriptUnavailable. wrap the scope in{ killOnThrow: true }if you want a throw to tear down any in-flight agent. see error codes.
run it
orcr scaffold triage-workflow && cd triage-workflow
# paste the recipe into workflow.ts, add a call like: await triage("...");
npx tsx workflow.tsthe recipe exports triage(item), so call it from your own run() or a queue. the CI fixture calls it once with a sample bug report.
next: tournament is the pipeline shape at scale, running pairwise brackets when a single judge would see too many candidates at once.