import { type Config, type EffortLevel, type LadderRung } from "./config-types.js"; import type { HostRoutingState } from "./host-routing.js"; import type { ContextWindowSource, ResolvedContextWindow } from "./metadata.js"; import { type LaneManifest } from "./lane-manifest.js"; import { type DispatchMode } from "./dispatch-lane-stats.js"; import type { TierData } from "./tier-data.js"; /** * The dispatch ladder: which LANE a host agent should hand a delegated task to, in what order, * and what to do when one is spent. * * Why this lives here and not in the routing path: `routing.subagents` decides which provider * serves ONE HTTP turn, and the proxy applies it itself. A ladder rung is a different unit of * work — a whole delegated task — and some rungs are agent CLIs that never traverse this proxy * at all (their quota is client-bound; only the vendor's own binary can spend it). So the relay * owns the ORDER and the live state, and the host executes what it is told. That keeps the proxy * a proxy: it never spawns a process, and it never pretends a CLI answered an HTTP turn. * * Exhaustion is host-reported for every rung kind, deliberately. The relay cannot see an AGY * credit balance or a ChatGPT rate limit, and inventing an availability signal it does not have * would be worse than admitting it: a rung is ready until someone who actually tried it says * otherwise. */ /** Default cooldown for a rung reported spent. Quotas reset on their own schedules; this is * a "try again soon", not a claim about the vendor's reset window. */ export declare const DEFAULT_EXHAUSTED_MS: number; /** * Ceiling on a host-reported cooldown (30 days). Not a policy about vendors — a bound that keeps * `readyAt` a representable date. `Math.max(0, ttlMs)` alone let a caller-supplied `Infinity` * (a legal JSON number: `1e999` parses to it, and `typeof Infinity === "number"` passes every * numeric type guard upstream) reach `new Date(Infinity).toISOString()`, which throws * `RangeError: Invalid time value`. That poisoned the cooldown map permanently: every later * `buildDispatch` threw while rendering the same lane, so one bad exhaustion report took the whole * ladder down until the process restarted. A cooldown is advisory, so clamping is the right * response — refusing the report would lose a real "this lane is spent" signal. */ export declare const MAX_EXHAUSTED_MS: number; /** * Host-reported WHY behind an exhaustion report. Two kinds because they call for different * waits: a rate limit resets on a clock measured in minutes, a spent quota on one measured in * hours (or the vendor's reset boundary). The relay still never invents the signal — the host * says which happened, and an explicit `ttlMs`/`retryAfterMs` always beats the outcome default. */ export type DispatchOutcome = "rate_limited" | "quota_exhausted"; export declare const OUTCOME_DEFAULT_MS: Record; /** * Own failures in a row after which a lane cannot be relied on to answer. `runWalk` will not stop * an earlier lane that is still working in order to reach lanes that are all past this streak. */ export declare const LANE_UNRELIABLE_STREAK = 3; /** Own failures in a row after which a lane is ordered behind the lanes that have none. */ export declare const FAILING_LANE_STREAK = 5; export type LaneState = "ready" | "exhausted" | "disabled" | "not-servable"; /** * Advisory per-lane execution stats for one ladder rung: how often this config took the lane * and how long it took. The median is over the rolling wall-clock window held by * `dispatch-lane-stats.ts` (null when the window is empty — unknown, never 0) and `lastAt` * is epoch ms of the last recorded run (null when the lane never ran here). Present only * when the rung HAS a stats entry; omitted otherwise. Advisory columns only: stats never * change a lane's `state`, never change `next`, and never reorder the ladder. */ export interface DispatchLaneStats { calls: number; successes: number; failures: number; timeouts: number; medianWallClockMs: number | null; /** * 95th percentile of the same window. Reported BESIDE the median rather than instead of it, * because the two answer different questions and the median alone hid the answer an operator * giving up on a lane was actually looking for (median 111.5 s against p95 900 s on the live * store, 2026-09-05). Null when the window is empty — unknown stays null, never 0. */ p95WallClockMs: number | null; lastAt: number | null; } /** * One-line advisory rendering of a lane's stats, shared by `dispatch_lanes` and * `llm-relay dispatch` so the wording cannot drift between the two surfaces: * `stats: 4 calls, 3 ok, 1 failed, 0 timed out, median 24s, p95 91s` (`n/a` when unknown). * Seconds are rounded to one decimal. */ export declare function formatLaneStats(stats: DispatchLaneStats): string; export interface DispatchLane { id: string; kind: "cli" | "relay"; /** 1-based position in the configured ladder — stable regardless of availability. */ position: number; state: LaneState; /** Shared quota bucket, when the rung declares one. Rungs sharing a bucket go down together. */ quota?: string; note?: string; /** * cli rungs: the configured per-MCP-server-process concurrency cap, or `null` when unbounded. * Set on every `cli` lane (never on a `relay` lane, which has no such config field) so a reader * of the ladder can always see whether one is in force, even before any job has run against it — * the live IN-FLIGHT count is a different question this module cannot answer (only the MCP server * process that would spawn a job knows what it is currently running), so `mcp/server.ts` renders * that count itself alongside this figure rather than this module inventing one. */ maxConcurrent?: number | null; /** Highest dispatch tier supported by synced capability evidence; absent means unknown/no limit. */ capability?: EffortLevel; /** Why capability is known, or "unknown" when no evidence-qualified limit can be stated. */ capabilityBasis?: CapabilityBasis; /** When an exhausted rung becomes eligible again (ISO 8601). */ readyAt?: string; /** * The lane's own tool states it does not serve this rung's model. An EXISTENCE fact, so the rung * is removed from selection and its `invoke` is withheld — a command that cannot work must not be * renderable. It stays LISTED with this reason: silently vanishing is its own debugging problem. */ notServable?: string; /** Arguments removed because the lane states (or was observed to state) it rejects them. */ droppedArgs?: string[]; /** * cli rungs: exactly what to run. `args` already has the task substituted when one was given. * `env` is applied by the HOST when spawning: a string value sets the variable, `null` unsets * an inherited one (see `LadderRung.env` for why both directions matter). The task placeholder * is never substituted into env values — they are operator-authored routing, not task content. */ invoke?: { command: string; args: string[]; env?: Record; }; /** relay rungs: the spec to address (`pool/`, `/`, …). */ spec?: string; /** * relay rungs only: with this client's subagent offload OFF, a bare subagent will NOT route to * this spec — the host must put `@relay: ` in the prompt or turn the client rule on. * Surfaced so a host never silently spends primary quota believing it offloaded. * * ⚠ Never set for a bypassed host. There, the directive is not merely insufficient — it is * inert, and reaches the model as literal prompt text. A hint that cannot work is worse than * no hint, because the host acts on it and believes it offloaded. */ requiresDirective?: boolean; /** * This rung was a `relay` rung rendered as a CLI invoke, because the calling host's traffic * does not reach this relay. `spec` is retained alongside `invoke` so the reader can still see * what is being addressed — the transposition is a change of MECHANISM, not of target. */ transposed?: boolean; /** * Context window in tokens that the serving provider PUBLISHES for this lane's spec, when it * published one and (for a pool) every member did. Absent means nobody stated it — never that * it is small. Surfaced so a reader can see whether the rendered command carries a window or * left the child on its own default. */ contextWindow?: number; /** * Where `contextWindow` came from: `provider` (the serving deployment published it) or * `snapshot` (a published figure for the same model id from the synced capability data). Travels * with the number for the same reason `strengthBasis` travels with `strength` — a reader must be * able to tell a first-party measurement from a same-model figure taken elsewhere. * * For a pool this describes the MEMBER that set the minimum, which is the binding constraint. */ contextWindowSource?: ContextWindowSource; /** * How many members of a POOL had no resolvable window. The reported number is the minimum over * the members that DID resolve, so this says how much of the pool that minimum actually covers. * Absent or 0 means every member resolved. */ contextWindowUnknownMembers?: number; /** * Why this rung cannot be used by the calling host as configured. Set when a `relay` rung needs * transposing and no `routing.cliLane` template exists to transpose it with. Such a rung is * never auto-selected as `next` — offering a lane known not to work is the defect being fixed. */ unreachable?: string; /** * Advisory execution stats for this rung (`DispatchLaneStats`), filled from * `dispatch-lane-stats.ts` for every rung that ran under this config and OMITTED otherwise. * Never changes `state`, `next`, or the ladder order — a column, not an input. */ stats?: DispatchLaneStats; /** * This lane answered recently, so it is preferred over its ladder position for a window * (`lane-affinity.ts`). Present only while the pin is live. * * ⚠ **A pin PROMOTES; it never RESURRECTS.** It reorders lanes that are already selectable and * nothing more — a pinned lane that is exhausted, disabled, unreachable or not servable is still * not selected, and this field never appears on one. That is the mirror of "health demotes, * never drops": a memory of past success must not outrank present evidence of unavailability. */ pinned?: { until: string; reason: string; }; /** * This lane was recently abandoned by the dispatch walk after going idle, so ready lanes * carrying no demotion are tried ahead of it for a window (`lane-affinity.ts`). * * ⚠ **It is a FIELD, not a `LaneState` member, and that is load-bearing.** `buildDispatch` * selects on `state === "ready"`, so a `slow` member of that union would REMOVE a slow lane * rather than demote it — breaking "health demotes, never drops" inside the very change that * exists to honour it. Demotion is a TERM in the ordering, exactly as quota demotion is a term * inside `targetUsability` on the HTTP path rather than a state. * * ⚠ **The evidence is first-party and needs no threshold.** `docs/backlog.md` asks for a * calibrated wall-clock statistic and warns, correctly, never to borrow the HTTP path's numbers * — a lane legitimately runs an agent loop for minutes. This carries no statistic at all: "the * walk gave this lane its budget and it did not answer" is a measurement of this lane, by this * relay, moments ago. */ demoted?: { until: string; reason: string; }; /** * The lane's usual time to ANSWER in the requested mode: median and 80th percentile of the * most specific completed-run history window — since 2026-09-10 only a COMPLETED run adds * one, and `restoreLaneStatsRows` empties an older window that provably holds anything else. * Present only when that window holds a duration. A poll renders it (`describeJob` in `mcp/server.ts`), so a caller can tell a slow * lane from a stuck one — 23 of the 182 unanswered dispatches in the 2026-09-10 transcript sweep * ended with the caller simply no longer polling. */ timeToAnswer?: { medianMs: number; p80Ms: number; samples: number; mode: DispatchMode | null; }; /** * Own failures (`failed` or `timed_out`, never a walk abandonment) in a row, read from the same * window as the budget. Present only when above zero. * * ⚠ Evidence the walk reads: a later lane on a streak of `LANE_UNRELIABLE_STREAK` or more cannot * be relied on to answer, so the walk does not stop an earlier lane that is still working in * order to reach it (`runWalk`). Measured 2026-09-10: the walk stopped `free-pool` at 90 s to try * `opencode-muse-spark` (0 of 12), `agy-claude-opus` (0 of 34) and `anthropic` (0 of 21). */ recentFailures?: number; /** * Set at `FAILING_LANE_STREAK` own failures in a row: the lane is ordered behind every lane that * has none, the same band as a walk demotion, until it answers again. Demoted, never dropped. */ failing?: { streak: number; reason: string; }; /** * Not a ladder rung: built for ONE dispatch from a caller-named `model`. Never reported to * `/dispatch/telemetry`, which knows only ladder lane ids; its HTTP traffic is metered by the * relay's own pipeline anyway. */ adHoc?: true; } export interface DispatchView { /** Selected tier-specific ladder, or null when using the legacy single ladder. */ tier: string | null; /** State of the selected client's offload rule, which governs relay-rung hints. */ offload: boolean; /** Originating harness whose rule controls relay-rung directive hints. */ client: string; /** * Whether the CALLING host's traffic reaches this relay, as reported by the caller — the * server cannot observe it (a bypassing host sends nothing here) and must not guess from its * own environment. Governs whether relay rungs are usable as written or transposed. */ host: HostRoutingState; ladder: DispatchLane[]; /** * Lane ids that MAY be selected, best first — the ONE definition of selection order. * `next` is `order[0]` resolved against `ladder`; a dispatch WALK iterates the same list. * * ⚠ It exists so the order has one owner. The alternative — a walking caller re-deriving the * order from `ladder` — puts two definitions of one rule in two files, which is the shape this * repository's history warns about more often than any other (`orderByUsability` versus * `targetUsability`, the pool-failover incident, the two hand-assembled announcement sets). * * ⚠ `ladder` itself stays in CONFIG order, because `position` is documented as stable and a * reader needs to see the configured ladder rather than a re-sorted one. Unselectable rungs * (exhausted, disabled, unreachable, not servable) are absent from `order` entirely — this is * the order of what may be TRIED, not a ranking of everything. */ order: string[]; /** The lane the host should use now, or null when every rung is spent or none configured. */ next: DispatchLane | null; /** Why `next` is what it is — including why it is null. */ reason: string; /** Dispatched task text when one was given. */ task?: string; /** Source of this dispatch view: live daemon, or local fallback when daemon is unreachable. */ source?: "daemon" | "local-fallback"; } export interface DispatchOptions { /** Originating harness (`claude`, `codex`, or a future configured client). */ client?: string; /** Select a named tier-specific ladder (for example low, medium, high, or xhigh). */ tier?: string; /** Substituted for the `{task}` placeholder in a cli rung's args. */ task?: string; /** * Cached lane manifest (`llm-relay lanes --probe`). Passed in rather than loaded here so the * request path never touches the filesystem on our behalf and tests can pin it. Absent ⇒ every * rung is UNKNOWN and nothing is evicted. */ manifest?: LaneManifest | null; /** * Synced capability snapshot used to derive lane capability. Undefined loads the memoized * repository snapshot through getStrength(); null deliberately means "no snapshot" for tests and * embedders. A missing/unmatched model is UNKNOWN and therefore imposes no dispatch ceiling. */ tierData?: TierData | null; /** Host override: return THIS lane as `next`, whatever the order says. */ lane?: string; /** Walk the ladder: pick the first ready rung strictly after this one. */ after?: string; /** * Whether the CALLER's traffic reaches this relay (`src/host-routing.ts`). Supplied by the * caller, never sniffed here: `buildDispatch` runs inside the server as often as not, and the * server's own environment describes the process launched at logon, not the session asking. * Absent means `unknown` — behave exactly as before this existed. */ host?: HostRoutingState; /** Harness name, for messages only (`claude-desktop`). Never used to decide anything. */ entrypoint?: string; /** * Context window a provider PUBLISHES for one of its models, in tokens, or null when it * publishes none. Injected rather than read here so this module keeps no catalog dependency and * stays synchronous — the server backs it with `catalog.cachedLimits()` (which never fetches, so * this cannot become a blocking round-trip), and the CLI with the same on-disk cache. * * Absent means no window is resolved for any lane, which is exactly the behaviour before this * existed. Never guess a number here: see `specContextWindow`. */ publishedContextWindow?: (spec: string) => ResolvedContextWindow | null; /** * WHO is asking, when that changes what can run. `"mcp"` is the `llm-relay mcp` server: it runs * lanes itself and has no `Agent` tool, so a pass-through relay rung — one that forwards the * caller's own Anthropic credential — can never run there. It comes back `unreachable`, never as a * lane the walk tries and fails in 0 s (`docs/history/dispatch-giveup-diagnosis-2026-09-10.md` §4). * Absent ⇒ exactly the behaviour before this existed. */ requester?: "mcp"; /** * The mode the caller will run lanes in, so its time-to-answer history comes from runs of the * SAME mode. Absent ⇒ the mode-less legacy window, as before this existed. */ mode?: DispatchMode; /** * A routing spec (`deepseek/deepseek-flash`, `pool/high`, …) to run as its OWN one-lane view * instead of the ladder. Validated against the configured providers and pools: an unknown spec * yields no lane and a reason. `dispatch` could not name a model before 2026-09-10, so agents that * had to use DeepSeek wrote their own HTTP calls to the relay * (`docs/history/dispatch-giveup-diagnosis-2026-09-10.md` §7). */ model?: string; } /** * Report a rung spent (quota gone, rate-limited, CLI missing). Rungs sharing a `quota` bucket * are cooled down together — that is the whole point of the bucket, since one CLI can meter two * model families against two independent balances and only one of them may be gone. * * Unknown id is not an error: a host walking a ladder it half-remembers should not get a 500. */ export declare function markExhausted(cfg: Config, id: string, ttlMs?: number, tier?: string): boolean; /** Clear one rung's cooldown, or every cooldown for this config when no id is given. */ export declare function clearExhausted(cfg: Config, id?: string, tier?: string): void; /** * What a tier-scoped ladder lookup found: the tier the ladder resolved to (`null` for the legacy * single ladder), the rung when the id names one ON THAT LADDER, and — when `routing.ladders` * exists but holds no ladder under the requested name — that name, so a caller can say so. */ export interface LadderRungLookup { tier: string | null; rung: LadderRung | undefined; missingTier: string | undefined; } /** * The tier-scoped rung lookup — the ONE resolution `markExhausted`, `clearExhausted` and the * operator pin in `routes/admin.ts` share. It answers with the SAME `(tier, rung)` pair * `buildDispatch` annotates a view with (`selectLadder` on both sides), which is what makes a * pin recorded through it land where the next `GET /dispatch` reads. ⚠ Distinct from * `findLadderRung`, which is tier-AGNOSTIC (a telemetry report names a lane, not a tier); a * memory or a cooldown written here is keyed by tier, so the lookup must be too. */ export declare function lookupLadderRung(cfg: Config, laneId: unknown, tier?: string): LadderRungLookup; /** * One exported/persisted cooldown row: the raw map key (`rung:` / `quota:`) and its * absolute expiry in epoch ms. The KEY travels, not the rung, because a bucket outlives any one * ladder rendering — the same `quota:` may appear in several tiers. */ export interface ExhaustedRow { key: string; until: number; } export declare function onExhaustionChanged(cfg: Config, listener: () => void): void; /** Still-future cooldown rows, for persistence and for probe-target selection. */ export declare function exportExhaustedRows(cfg: Config, now?: number): ExhaustedRow[]; /** * Restore persisted rows into this config's live map. Field-validated per row, future-only, and * it NEVER overwrites a cooldown this process already learned — the `restoreState` contract. * An `until` beyond `MAX_EXHAUSTED_MS` from now is clamped, mirroring `normalizeTtl` at write. */ export declare function restoreExhaustedRows(cfg: Config, rows: readonly ExhaustedRow[], now?: number): number; /** Mark one raw bucket key exhausted until an absolute time — the probe path's write. */ export declare function markExhaustedKey(cfg: Config, key: string, untilMs: number, now?: number): void; /** Clear one raw bucket key — the probe path's retraction. */ export declare function clearExhaustedKey(cfg: Config, key: string): void; /** The placeholder a cli rung's args must contain; substituted with the task text. */ export declare const TASK_TOKEN = "{task}"; /** The placeholder a `routing.cliLane` template's args must contain; substituted with the spec. */ export declare const SPEC_TOKEN = "{spec}"; /** * Optional placeholder for the spec's context window, in tokens. Usable in a `cliLane` template's * args AND env values — unlike `{task}`, which is never substituted into env. * * The distinction is not arbitrary. `{task}` carries text a model or a user wrote, so putting it * in the environment of a spawned process would let request content become process configuration. * `{contextWindow}` is a number this relay resolved from the serving provider's own published * metadata; it IS configuration. That is what makes it safe here and `{task}` not. * * Exists because a client cannot be expected to know the window of a model it does not recognise — * the `claude` CLI assumes 200k for an unknown `--model` and compacts against that, so a lane * pointed at a 1M-context model silently throws away four fifths of it. */ export declare const CONTEXT_TOKEN = "{contextWindow}"; /** * Published context window for a spec, in tokens, or null when it cannot be stated. * * ⚠ Null is the common answer and must stay honest. Free providers largely publish no metadata at * all (NIM publishes none), so a pool's members are mostly unknown — measured on this machine, 0 * of 29 members of `pool/high` publish a context length. Guessing a window is strictly worse than * omitting it: the client already has a conservative default, and a number we invented would * override that default with fiction and overflow the real backend. * * For a POOL the MINIMUM across members that resolve is used: failover can land the request on any * member, so the pool's usable window is the smallest one known. * * ⚠ **An unresolvable member does NOT veto the pool.** That was the original rule and it was * wrong twice over. Practically, a single model with no published figure anywhere blanked three of * four pools on the owner's machine — `huggingface/Qwen/Qwen3-235B-A22B-Instruct-2507` alone * blocked `low`, `medium` and `high` while 44 of 49, 38 of 41 and 28 of 29 members resolved fine. * Conceptually, a pool is a ROUTING construct — a ranked candidate list — and membership of one * says nothing about any member's context window; treating "we have no data on one model" as "we * know nothing about this pool" confuses an absent measurement with a measured absence. * * The residual risk — an unmeasured member whose real ceiling is below the reported minimum — is * exactly what the observed rung exists to close: the first over-length rejection from that * deployment states its ceiling, `context-limits.ts` records it, and the next dispatch reports the * corrected floor. `contextWindowUnknownMembers` carries how much of the pool the number covers, * so the gap is visible rather than implied. */ export declare function specContextWindow(spec: string, cfg: Config, published: (spec: string) => ResolvedContextWindow | null): (ResolvedContextWindow & { unknownMembers: number; }) | null; /** * Does `spec` reach only the caller's own pass-through vendor (`reachableWithoutRelay`)? Exported * for the MCP server, which filters such a lane out of a walk itself when its view came from a * daemon older than `requester=mcp` — a version-skew guard, because the daemon started at logon * routinely runs older code than a freshly spawned MCP process. */ export declare function isPassThroughSpec(spec: string, cfg: Config): boolean; /** * Why the MCP server cannot run a pass-through rung — the ONE wording, used for the `unreachable` * verdict here and by the MCP server's own fallback text, so the two never disagree. */ export declare function mcpPassThroughReason(spec: string): string; /** * Resolve command names whose Windows shell semantics differ from their POSIX spelling. * * PowerShell resolves functions and aliases before external applications. Antigravity commonly * installs a PowerShell function named `agy` for opening the IDE alongside the headless * `agy.exe` CLI, so handing a Windows host the bare name can launch the GUI instead of running * the delegated task. Naming the executable extension bypasses that shadowing. Keep the rule * deliberately narrow: explicit paths and every other configured command remain authoritative. */ export declare function normalizeCliCommand(command: string, platform?: NodeJS.Platform): string; /** * Where the windowless-console launcher lives, if the operator has installed one. * * Not shipped by this package or by any of its installers (verified: no `lane-launch.ps1` exists * anywhere under this repository) — it is a hand-maintained artifact documented in CLAUDE.md's AGY * lane notes and `docs/agy-popup-fix-2026-09-07.md`. So absence is the ORDINARY case on a fresh * machine and on every non-Windows host, not a misconfiguration to warn about. Resolved through the * same config-kind XDG base every other operator-authored artifact under this directory uses * (`state-paths.ts`), so an XDG override on the config side moves this alongside `config.json` * itself — matching where every hand-authored `cli` ladder rung already points its own `-File` * argument. (`state-paths.ts` stays the one module naming the XDG variables directly, per * `test/state-paths.test.ts` — this reaches them only through `relayStatePath`.) * * Deliberately the ONE impure seam in this module (the `buildDispatch`/`platform` precedent): every * function below it stays pure over a caller-resolved `launcherPath: string | null`. ⚠ Guarded like * `winenv.ts`/`os-keyring.ts`/`lane-runner.ts`'s default spawner: under vitest, a caller that omits * `exists` gets `null` unconditionally rather than the real filesystem — a suite must never depend * on whether THIS machine happens to have the launcher installed (it does), which is exactly what * broke `routes/admin.ts` and `cli.ts`'s own call sites (neither injects a seam) before this guard * existed. A test that wants the real check injects `exists` itself, same as every seam above it. */ export declare function resolveLaneLauncherPath(platform?: NodeJS.Platform, exists?: (path: string) => boolean): string | null; /** * Every rung across EVERY ladder the config declares — each tiered ladder, then the legacy * single ladder. The ONE ladder walk: `POST /dispatch/telemetry` (unknown-lane 400) and the * CLI's `--by model` lane-id check both read through here or through `findLadderRung`, so a * new ladder shape is fixed once. (`markExhausted` stays tier-scoped via `selectLadder` — an * exhaustion report arrives on a tier's dispatch view.) */ export declare function allLadderRungs(cfg: Config): LadderRung[]; /** * Find one rung by id across every ladder the config declares, whatever tier holds it. * Tier-agnostic on purpose: a telemetry report names a lane, not a tier. `markExhausted` * stays tier-scoped (an exhaustion report arrives on a tier's dispatch view); this is the * shared lookup both branches mean — one walk, not two. */ export declare function findLadderRung(cfg: Config, laneId: string): LadderRung | undefined; /** * Render a caller-supplied id for a `reason` string. The reason is reflected back verbatim in the * JSON response AND printed to a terminal by `llm-relay dispatch`, so echoing raw caller input let * a `?lane=` carrying ESC sequences rewrite the operator's terminal, and an arbitrarily long one * bloat a response about a lane that does not exist. Control characters go, and the echo is capped * — enough to recognise your own typo, not a channel. Exported for `routes/admin.ts`, whose * `POST /dispatch` 400s echo a caller-supplied lane id or tier under the same rule. */ export declare function describeId(id: string): string; export type CapabilityBasis = "snapshot" | "pool-band" | "unknown"; export interface DerivedLaneCapability { tier: EffortLevel | null; basis: CapabilityBasis; /** Routing/model identifier whose evidence was consulted, when one exists. */ model: string | null; } /** * Derive a lane's capability from synced model evidence, never from a hand-authored rung field. * * Dynamic pool rungs use their policy's declared effort band: the band is the pool's requested * capability contract and stays stable even when availability appends a lower-band degradation * tail. Direct relay specs and CLI --model values use the highest evidence-qualified effort band. * Unknown, fuzzy, or under-evidenced models impose NO ceiling — unknown is never treated as weak. */ export declare function derivedCapability(rung: LadderRung, cfg: Config, tierData?: TierData | null): DerivedLaneCapability; export declare function buildDispatch(cfg: Config, rawOpts?: DispatchOptions, platform?: NodeJS.Platform, launcherPath?: string | null): DispatchView; export declare const AUTO_TIERS: readonly ["low", "medium", "high", "xhigh"]; export type AutoTier = (typeof AUTO_TIERS)[number]; export declare function normalizeAutoTier(tier?: string | null): string; export interface AutoSpecResolution { spec: string; tier: string; } /** * Resolves the `auto` model name to a concrete spec and tier: * The spec of the first READY rung of kind `relay` in the dispatch ladder for the given tier. * The tier comes from the `x-llm-relay-tier` request header (low | medium | high | xhigh), else `medium`. * If the ladder for that tier has no ready relay rung, or no ladder is configured, * `auto` falls back to `routing.default`. */ export declare function resolveAutoSpec(cfg: Config, rawTier?: string | null, now?: number): AutoSpecResolution;