/** * Model-router turn routing: the session's per-turn model-selection subsystem — the regex/executor * route resolver, the optional bounded routing judge, the executor lane (Level-0 toolkit direct hit * + speculative brain-refined retry), the per-tier thinking/tool-surface swap around a routed turn, * the cheap-research-turn session buffer with mutating-tool escalation to an expensive retry, and * the router status/diagnostics report. * * Extracted verbatim from agent-session.ts (god-file decomposition). Owns the transient per-turn * route state — the active intent/route, the cheap-turn session buffer, the escalation-requested and * retry-in-flight flags — and the sticky last-decision/last-skip-reason/last-intent used by the * status report. Everything else it needs — the live agent + its state, the current model, the * session/settings managers, the model registry, the agent dir, the session-disposal abort signal for * isolated judge completions, the base system prompt, the isolated-completion primitive, spawned-usage * accounting, the event/telemetry * emitters, and the recently-extracted BackgroundLaneController (resolveLaneModel) / ContextPipeline * (resolveCurationModelIfFit) collaborators — is reached through narrow deps accessors rather than the * whole AgentSession. * * Drive-path boundary (deliberate): the actual agent.prompt()/continue() loop belongs to the * session-owned ForegroundRecoveryController; this controller's parallel routed drive path * ({@link runRoutedTurn}) owns only route decision/escalation/tier bookkeeping and delegates every * agent turn through {@link ModelRouterControllerDeps.runAgentPrompt}, so the drive loop is never duplicated. The * host keeps a one-line delegation at each call-in: the routing prep + routed-turn entry in * _promptUnserialized, the beforeToolCall MUTATION escalation branch ({@link * maybeEscalateToolCall}), the tool-name-agnostic VALIDATION-FAILURE escalation branch for cloud * models ({@link requestValidationFailureEscalation} — de-conflated from the mutation gate; see the * capability-gate spine doctrine, which routes local/managed models to an evidence-gated * native→phone auto-probe on AgentSession instead), the message_end cheap-turn buffering ({@link * captureSessionMessage}), the retry-event suppression ({@link isRetryInFlight}), the public * getModelRouterStatus / autonomy-telemetry reads, and the tier resolution's consultation of the * persisted `/toolprobe` verdict for local/managed tier models ({@link * ModelRouterControllerDeps.getToolProbeVerdict}). */ import type { Agent, AgentMessage } from "@caupulican/pi-agent-core"; import type { SessionManager, SessionMessageBatchEntry } from "@caupulican/pi-agent-core/node"; import type { Api, Model, Usage } from "@caupulican/pi-ai"; import type { AgentSessionEvent, IsolatedCompletionOptions, IsolatedCompletionResult } from "./agent-session-contracts.ts"; import type { RouteDecision } from "./autonomy/contracts.ts"; import { type AutonomyTelemetryEvent } from "./autonomy/telemetry-events.ts"; import type { ModelRegistry } from "./model-registry.ts"; import { type ModelRouterDecisionStatus, type ModelRouterFailoverStatus } from "./model-router/status.ts"; import type { ModelToolProbeVerdict } from "./models/adaptation-store.ts"; import type { SettingsManager } from "./settings-manager.ts"; /** Canonical `provider/id` label for a routed/resolved model, as it appears in decisions and status. */ export declare function formatModelRouterModel(model: Model): string; export interface ModelRouterControllerDeps { /** Live agent — the controller reads/writes agent.state.{model,thinkingLevel,tools,systemPrompt,messages} * for the per-turn tier swap and aborts it on a mutating-tool escalation. */ getAgent(): Agent; /** Current session model, used to decide whether a routed turn actually swaps the model. */ getModel(): Model | undefined; /** Router/executor/judge/thinking settings + capability mode (all opt-in gates). */ getSettingsManager(): SettingsManager; /** Session log: routed-turn message buffering/persistence, decision persistence, recent-decision status. */ getSessionManager(): SessionManager; /** Canonical host-owned mixed message batch persistence; one validated publication for lifecycle linking. */ appendSessionMessageBatch(batch: readonly SessionMessageBatchEntry[]): string[]; /** Resolves configured route/judge/executor model patterns against configured auth. */ getModelRegistry(): ModelRegistry; /** Session-scoped provider/model quota exhaustion guard. */ isModelExhausted(model: Model): boolean; /** Status snapshot for exhausted models and the last failover notice. */ getFailoverStatus(): ModelRouterFailoverStatus; /** Root dir the host-keyed {@link FitnessStore} lives under (executor tool-call fitness gate). */ getAgentDir(): string; /** Aborts the judge's bounded completion when the session is disposed. */ getReflectionSignal(): AbortSignal; /** Base (extension-free) system prompt — the tier swap only sheds tools when the turn is on it. */ getBaseSystemPrompt(): string; /** The session-owned foreground drive loop; the routed path delegates every turn here. */ runAgentPrompt(messages: AgentMessage | AgentMessage[]): Promise; /** Continue from canonical history after a committed cheap-route escalation. */ runAgentContinuation(): Promise; /** Rebuilds the system prompt for a filtered tool surface (routed-model capability shedding). */ buildSystemPromptForToolNames(toolNames: string[]): string; /** Re-resolves the restored model against the registry after a routed turn (provider override safety). */ refreshCurrentModelFromRegistry(): void; /** One-shot, tool-less LLM call — the routing judge and the executor reflex-brain warmup ride this. */ runIsolatedCompletion(opts: IsolatedCompletionOptions): Promise; /** Rolls judge/brain spend into spawned-usage accounting. `reportId` is REQUIRED: every * caller derives a stable id from the work unit's identity so a retry cannot double-count. */ addSpawnedUsage(usage: Usage, opts: { label?: string; sourceSessionId?: string; reportId: string; }): string | undefined; /** Session event stream (executor-miss warning). */ emit(event: AgentSessionEvent): void; /** Autonomy telemetry stream (one route-decision event per user-facing routed turn). */ emitAutonomyTelemetry(event: AutonomyTelemetryEvent): void; /** Resolves the judge model pattern via {@link BackgroundLaneController}. */ resolveLaneModel(pattern: string): Model | undefined; /** Fitness-gated reflex-brain model via {@link ContextPipeline} (executor speculative refinement). */ resolveCurationModelIfFit(): Model | undefined; /** Persisted `/toolprobe` verdict for this model (native / text-protocol / none), or undefined * when never probed. Tier-resolution's consultation reads this ONLY for local/managed models * ({@link isLocalOrManagedRouterModel}); cloud models never call it. */ getToolProbeVerdict(model: Model): ModelToolProbeVerdict | undefined; } /** * Owns the model-router turn routing extracted from {@link AgentSession}. See the module header for the * drive-path boundary that keeps the agent.prompt()/continue() loop in its foreground lifecycle owner. */ export declare class ModelRouterController { /** Active model-router intent for the current transient routed turn, if any. */ private _activeModelRouterIntent?; private _activeModelRouterRoute?; private _modelRouterSessionBuffer?; private _modelRouterEscalationRequested; private _isModelRouterRetry; private _lastModelRouterDecision?; private _lastModelRouterSkipReason?; private _lastModelRouterIntent?; private readonly deps; constructor(deps: ModelRouterControllerDeps); /** True while the escalation retry turn is running, so the host can suppress its duplicate prompt events. */ isRetryInFlight(): boolean; /** Latest completed route decision (sticky), for the autonomy telemetry snapshot. */ getLastDecision(): ModelRouterDecisionStatus | undefined; /** * beforeToolCall escalation gate: a cheap research turn that reaches for a mutating tool aborts the * turn and requests a retry on the expensive model. Returns the block result the host hook forwards, * or undefined when no escalation is required. */ maybeEscalateToolCall(toolName: string, args: unknown): { block: true; reason: string; } | undefined; /** * Tool-name-agnostic validation-failure escalation gate, called from * AgentSession's onToolValidationEscalation handler for a CLOUD model only — see the * capability-gate-spine doctrine (local/managed models never reach this method; they trigger * the evidence-gated native→phone auto-probe instead). Unlike {@link maybeEscalateToolCall} (the * beforeToolCall MUTATION gate, still governed by `shouldEscalateModelRouterTool`/ * READ_ONLY_TOOL_NAMES — a legitimate, unrelated mutation-blast-radius policy), "the model * repeatedly cannot construct valid arguments for this tool" is evidence about the MODEL's * capability, not about the tool's mutation status, so a read-only tool's repeated validation * failure now escalates a cheap routed turn exactly like a mutating tool's would — takes no * tool-name/args input at all (unlike maybeEscalateToolCall), because every repeated validation * failure escalates regardless of which tool or model triggered it. No-op outside an active * cheap-tier routed turn, same scoping as maybeEscalateToolCall. */ requestValidationFailureEscalation(): void; /** * message_end hook: while a cheap routed turn is buffering, capture its messages into the session * buffer instead of persisting them (they are flushed on success or discarded on escalation). * Returns true when the message was buffered, so the host skips its own persistence. */ captureSessionMessage(message: AgentMessage): boolean; /** * Commit a cheap routed turn's buffered messages in their original source order. * * The lifecycle adapter uses this at the tool reservation boundary: once the assistant * message is durable, later tool-result messages must go through the normal message-end * path and an escalation may continue from canonical history instead of splicing it away. * The buffer is marked committed only after every append succeeds; a failed append therefore * rejects the reservation and the tool body is never entered. */ commitSessionBuffer(): Map; /** Commit only the prompt prefix before a provider request; keep the assistant/tool suffix buffered. */ commitSessionBufferPrefix(): Map; /** Whether a cheap routed turn has crossed its durable tool-boundary commit. */ isSessionBufferCommitted(): boolean; private _isModelAvailableAndAuthed; private _evaluateModelFitness; private _formatFitnessFailure; private _routerSurfaceForTier; private _getRouterTierFitnessStatuses; private _resolveExpensiveFallbackRoute; private _resolveExecutorRoute; /** True if a run_toolkit_script tool result since `fromIndex` actually EXECUTED (not error/ambiguous). */ private _executorTurnExecutedScript; /** Ask the reflex brain to refine the last user request into an explicit toolkit instruction. */ private _buildExecutorRefinedPrompt; private _resolveModelRouterTurnRoute; private _resolveModelRouterModelForIntent; resolveConfiguredTierModel(tier: "cheap" | "medium" | "expensive"): Model | undefined; /** * Router resolution with the routing judge (auto-on with the router): the regex classifier's * decision is the baseline; when a judge model resolves (judgeModel, else mediumModel), one * bounded, tool-less completion may move the tier between cheap/medium/expensive — never to * learning. Core rule encoded in the judge prompt: planning is never cheap unless genuinely * trivial. Every fallback stays visible in the decision reasons, and judge spend reports * through spawned-usage accounting. */ resolveTurnRouteJudged(prompt: string, options?: { skipJudge?: boolean; }): Promise<{ decision: RouteDecision; model: Model; } | undefined>; private _resolveModelRouterTurnModel; getStatus(formatLabel?: (label: string) => string): string; runRoutedTurn(messages: AgentMessage | AgentMessage[], routedModel: Model | undefined, routeDecision: RouteDecision | undefined, persistDecision?: boolean, continueFromCanonicalHistory?: boolean): Promise; } //# sourceMappingURL=model-router-controller.d.ts.map