pleach
Plugins

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/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

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

PieceWhat 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 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).

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:

  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 — 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/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

On this page