Planner
The planning port: cheap observation into a serializable RoutePlan.
Protocol: Planner.plan(request: PlanRequest, loaded: PreloadedSource | None = None) -> PlanResult
Planner is the boundary for inspecting a source and selecting how indx should
process it. It accepts the source, routing constraints, optional pinned
capability snapshot, requested embedding spaces, and signature-detection
preference. It returns a serializable route plan.
The plan identifies routes at document, page, or region scope. Each assignment contains a selected capability and ordered fallbacks, together with public decision reasons. The plan also records its source digest, media type, capability snapshot, policy version, estimates, and whether the request is ready or has unsatisfied constraints.
Separating planning from execution makes routing cheap to inspect before any expensive parser, OCR system, model, or embedder runs. A caller can understand, store, compare, or approve the proposed work independently of execution.
Binding the result to normalized input and versioned decision context also makes identical planning requests reproducible and prevents later execution from quietly choosing a different route.
Contract
Section titled “Contract”The input is
PlanRequest,
which carries a request ID, source, constraints, optional capability snapshot
ID, embedding-space IDs, and signature-detection flag.
loaded is that source already in hand, and it is optional. A caller with
nothing loaded omits it and the planner fetches; a caller that already holds the
bytes hands them over. The executor is the one such caller: a planless encode
loads and observes the source, and before this it then paid for a second
transfer inside plan() and a second SSRF verdict that could disagree with the
one it already had. PreloadedSource is a structural protocol in
indx-interfaces, not a model: the working copy stays private to indx-source,
which indx-interfaces sits below and may not import. Nothing on the wire
carries it, and PlanRequest is unchanged.
The output is
RoutePlan,
which carries:
- A deterministic plan ID and the caller’s request ID.
- The source digest, media type, capability snapshot ID, and policy version.
- Scoped selected routes and ordered fallbacks.
- Decision reasons and optional signature matches.
- Requested embedding spaces and estimated cost, latency, and quality.
- A
readyorunsatisfiedstatus with explicit unsatisfied constraints.
Responsibilities and guarantees
Section titled “Responsibilities and guarantees”- Perform only cheap source observation during planning. When the caller sets
signature_detection, that extends to creating the installedSignatureDetectorimplementations and asking them what they recognize – the one plan-time exception to “planning invokes nothing”, opted into by the caller and bound to the same budget as preflight. - Route over a fixed ladder of kinds – native extraction where a text layer
exists, then OCR, then a vision model, then manual review – never over
capability IDs.
parseris deliberately not a rung: a parser is a specialist that is only correct about a document something recognized first, so signature nomination is its only door, and a capability that generically reads a format declaresnative_extractionhowever specialized its machinery. A media type only a parser declares, planned without signature detection, is an unsatisfied plan naming the parser and the flag rather than a silent manual-review route (POLICY_VERSION0.5.0; the decision for every previously routable input is unchanged). - Treat the ladder and the set of kinds as closed and first-party. Six kinds,
three escalation rungs, one device preference and two preflight signals the
policy reads: none of them grows by installing a distribution, because each is
a decision about what indx routes to rather than a thing to route to. A
distribution whose work is none of the six declares the nearest kind and the
deployment corrects the numbers through
INDX_ROUTING_ECONOMICS; a signal the policy does not read is inert rather than wrong. Adding a kind, a rung, or a meaning is a first-party change with aPOLICY_VERSIONbump. - Nominate from a document-scoped or page-scoped signature only. A region-scoped
match is carried on the plan and routes nothing: it is a claim about part of a
page, there is no region assignment for it to become, and promoting it to its
page read “there is an invoice table in this region” as “this page is an
invoice” (
POLICY_VERSION0.7.0; no first-party parser emits one, so no previously reachable input changes route). - Refuse a requested embedding space against what a run produces, not against a
named lane: a space is unsatisfied when it embeds none of the input execution
hands a document embedder, which
DOCUMENT_EMBEDDING_MODALITIESstates once (POLICY_VERSION0.6.0). A space declaring a document-role image embedder was previously reported as missing a text embedder it had. That list gainedimageonce a run began producing rendered pages, so such a space is now routable rather than unsatisfied, and because one space may then run two embedders, the device rules are applied to each of them rather than the first (POLICY_VERSION0.8.0). - Place a nominated capability ahead of the generic ladder, never in place of it. A signature is a guess, and a guess that displaced the fallbacks would turn a wrong match into a failed document.
- Ask detectors in a fixed order and order the matches by their content, so
discovery order cannot reach
plan_id. - Never let a detector fail a plan. One that will not load, raises, or answers with something that is not a signature contributes nothing and is logged; no signature is already a complete answer.
- Evaluate the request against the selected capability snapshot and routing policy.
- Select only capability and embedding-space IDs present in that snapshot.
- Apply quality, latency, cost, hardware, residency, embedding-space, and snapshot constraints.
- Produce deterministic output for identical normalized inputs, policy, and snapshot.
- Explain why each route was selected and preserve eligible fallbacks.
- Return an explicit unsatisfied plan when constraints cannot be met.
The protocol describes planning behavior, not preflight internals. The evidence
vocabulary is public – PageEvidence, RegionEvidence, TextLayerState and
the signal constants live in indx-interfaces, because an observer ships as its
own distribution and cannot emit evidence it cannot name. The whole preflight
context and the working registry models remain private to the router
implementation: the context carries the source digest a plan is bound to, and
minting it there is what stops an installed observer from naming the identity of
a source it did not fetch. See SourceObserver.
Place in the system
Section titled “Place in the system”The public indx.plan() facade and POST /v1/plan operation delegate to a
planner. The returned PlanResult carries the plan under plan, which can be
inspected directly or supplied to the execution workflow, and beside it the
components that produced it: the loader, the sniffer and the observer, by
distribution, outside the hashed artifact. Encoding without a supplied plan
first obtains a plan through the composed indx service, handing over the bytes
it already loaded, and its trace then names the observer that looked inside the
planner.