Routing decisions
The registerIntentLabel() API and custom routing-override surface — the v1.x roadmap for plugin-driven intent registration and per-call routing override.
Status: v1.x roadmap. The
registerIntentLabel()API and the matching plugin-driven routing-override surface are not yet landed. This page documents the intent and scope so plugin authors targeting vertical AI startups can plan against it.The underlying substrate already exists: every routing decision in
@pleach/coregoes throughroutingDecision.ts, which is the host-supplied seam that resolves a model from(ProviderFamily × CallClass × intent). This page describes the plugin-facing API that will sit on top of that seam.
The problem this page solves
Today, intent labels in @pleach/core are a closed enum: a fixed
list of canonical intents the planner recognizes, plus the
substrate's per-intent affinity defaults. Vertical plugins
(legal-contract-review, medical-imaging, regulatory-filing,
code-review, etc.) need to register their own intent labels —
and the routing affinities those intents should resolve to —
without forking the substrate.
The intended surface
Plugins will register intent labels via a top-level helper plus two paired contribution hooks.
// v1.x roadmap — `definePleachPlugin` ships today; `registerIntentLabel`
// is the proposed helper, not yet exported from any `@pleach/core` subpath.
import { definePleachPlugin } from "@pleach/core";
// Proposed v1.x signature (shown here for illustration only):
declare function registerIntentLabel(spec: {
id: string;
description: string;
}): string;
const legalReviewIntent = registerIntentLabel({
id: "legal.contract-review",
description: "Vertical intent for legal-contract review",
});
export const legalReviewPlugin = definePleachPlugin("legal-contract-review", {
// Affinity table — which tools should fire for this intent.
intentToolMap: [
{ intent: legalReviewIntent, tools: ["search_contracts", "fetch_clause"] },
],
_raw: {
version: "0.1.0",
// Per-intent classifier — recognizes user text → this intent.
contributeIntentClassifiers: () => [
{
id: "legal-contract-review",
classify(ctx) {
return /\b(contract|clause|jurisdiction|indemnif)/i.test(ctx.userText)
? [{ intent: legalReviewIntent, confidence: 0.85 }]
: [];
},
},
],
// Routing override — pinning an intent to a specific family + call
// class is the v1.x roadmap surface described above; it is not yet a
// shipping `HarnessPlugin` hook. Today, routing resolves through the
// host-supplied `routingDecision.ts` seam.
},
});The three pieces
| Piece | What it does |
|---|---|
registerIntentLabel({ id, description }) | Returns a typed IntentLabel token. Plugins exchange tokens, not raw strings — the substrate can detect duplicate registrations and emit a warning. |
contributeIntentClassifiers (existing hook) | Returns classifiers that recognize user text → intent label. Already shipped; v1.x widens it to accept intent-label tokens. |
contributeFamilyPivot (existing hook) | First-wins routing override. Already shipped; the v1.x widening lets the pivot read the intent label from the routing context. |
registerIntentLabel() is the new piece. The two
contribute* hooks exist today; they widen to thread the typed
intent label through.
Honest scope-limit — what's covered, what isn't
In scope for the v1.x cut:
- Intent-label registration (the new helper).
- Reading the active intent label in
contributeFamilyPivot. - Reading the active intent label in
contributeIntentToolMapentries. - A capability breadcrumb when no classifier maps user text to a registered intent.
Out of scope:
- Cross-plugin intent inheritance (one plugin registers
legal.contract-review, another registerslegal.contract-review.high-stakesand inherits the parent's routing). The intent registry is flat by default. - Runtime intent registration (registration happens at plugin
construction time, before
SessionRuntimeboots). Late-binding intents at session start are not supported. - Direct override of the seam-level routing (
callClassliterals are still seam-only — see Plugin contract — what a plugin cannot do).
The substrate site
The routing-decision seam lives in the host layer today, not in
@pleach/core itself. The seam takes a
(ProviderFamily, CallClass, model) triple and returns the model
that should fire. Plugins reach the seam through
contributeFamilyPivot — a first-wins override that runs before
the substrate's family-strict cascade.
The v1.x cut wires the intent label into the pivot's input
context (context.intentId) so vertical plugins can branch on
their own registered labels without parsing user text inside the
pivot itself.
The routingDecision seam itself is bounded by structural
invariants: it cannot widen callClass to a value outside the
four canonical classes, and it cannot escape the family lock for
the active session (see
Call classes and
Family lock).
What plugin authors can do today
While the typed registerIntentLabel() API is landing, plugin
authors can already:
- Register intent affinities via
contributeIntentToolMap— the substrate honors these in plan generation. - Register family pivots via
contributeFamilyPivot— the pivot readscontext.callClassandcontext.familyand can already override on those axes. The widening adds anintentIdfield to the context. - Register intent classifiers via
contributeIntentClassifiers— these dispatch alongside the default classifier today; the widening lets them return a typed intent label.
For a working example of all three hooks against the existing
substrate, see
@pleach/recipes verticalAgent factory
— the vertical-agent recipe scaffolds intent registration as a plain
string key today and will lift to typed labels as part of the
v1.x cut.
What this is NOT
- Not a per-call model picker. Plugins can't choose between GPT-4 and Claude on a per-call basis outside the family lock. The family is locked at session start; pivots route within the family.
- Not a cost-aware router. Cost routing lives in
@pleach/gatewayand reads from the substrate's family-strict cascade. Plugins don't override gateway-level cost decisions. - Not an intent-classifier-only hook. Intent classification is
already pluggable today via
contributeIntentClassifiers. This page covers the registration surface — declaring the labels — not the recognition surface.
Where to go next
Family lock
Why model family is locked at session start and how cascade pivots work within it.
Call classes
The four canonical call classes — utility, reasoning, converse, synthesize.
Model resolution matrix
The substrate's (Family × CallClass) → model resolution table.
Authoring
The full HarnessPlugin hook surface.
Stream observers
The contributeStreamObservers hook — verdict ladder, factory pattern, and worked examples for canonicalizing tool-call dialects and stopping on refusal patterns.
Post-tool tier
The contributePostToolTier hook (C10 plugin slot) — uniform enrichment across tool batches for citations, entities, quality scores, and safety flags.