# Routing decisions (/docs/plugins/routing-decisions)



> **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/core` goes through `routingDecision.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 [#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 [#the-intended-surface]

Plugins will register intent labels via a top-level helper plus
two paired contribution hooks.

```typescript
// 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 [#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 [#honest-scope-limit--whats-covered-what-isnt]

**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
  `contributeIntentToolMap` entries.
* 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 registers
  `legal.contract-review.high-stakes` and inherits the parent's
  routing). The intent registry is flat by default.
* Runtime intent registration (registration happens at plugin
  construction time, before `SessionRuntime` boots). Late-binding
  intents at session start are not supported.
* Direct override of the seam-level routing (`callClass`
  literals are still seam-only — see
  [Plugin contract — what a plugin cannot do](/docs/plugin-contract#what-a-plugin-cannot-do)).

## The substrate site [#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](/docs/call-classes) and
[Family lock](/docs/family-lock)).

## What plugin authors can do today [#what-plugin-authors-can-do-today]

While the typed `registerIntentLabel()` API is landing, plugin
authors can already:

1. Register **intent affinities** via `contributeIntentToolMap` —
   the substrate honors these in plan generation.
2. Register **family pivots** via `contributeFamilyPivot` — the
   pivot reads `context.callClass` and `context.family` and can
   already override on those axes. The widening adds an
   `intentId` field to the context.
3. 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](/docs/recipes/vertical-agent)
— 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 [#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/gateway` and 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 [#where-to-go-next]

<Cards>
  <Card title="Family lock" href="/docs/family-lock" description="Why model family is locked at session start and how cascade pivots work within it." />

  <Card title="Call classes" href="/docs/call-classes" description="The four canonical call classes — utility, reasoning, converse, synthesize." />

  <Card title="Model resolution matrix" href="/docs/model-resolution-matrix" description="The substrate's (Family × CallClass) → model resolution table." />

  <Card title="Authoring" href="/docs/plugins/authoring" description="The full HarnessPlugin hook surface." />
</Cards>
