{"version":3,"file":"escalation.d.ts","sourceRoot":"","sources":["../../../src/core/routing/escalation.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEzD,MAAM,WAAW,gBAAgB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAChC,2DAA2D;IAC3D,cAAc,EAAE,MAAM,CAAC;IACvB,iDAAiD;IACjD,gBAAgB,EAAE,MAAM,CAAC;IACzB,0EAA0E;IAC1E,YAAY,EAAE,MAAM,CAAC;CAErB;AAED,eAAO,MAAM,yBAAyB,EAAE,gBAIvC,CAAC;AAEF,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,UAAU,GAAG,YAAY,GAAG,MAAM,CAAC;IACzC,aAAa,CAAC,EAAE,sBAAsB,CAAC;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,QAAQ,EAAE;QAAE,IAAI,EAAE,kCAAkC,GAAG,oCAAoC,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;CAClH;AAoBD,kFAAkF;AAClF,wBAAgB,gBAAgB,CAC/B,OAAO,EAAE,gBAAgB,EAAE,EAC3B,gBAAgB,EAAE,sBAAsB,EACxC,qBAAqB,EAAE,GAAG,CAAC,MAAM,EAAE,sBAAsB,EAAE,CAAC,EAC5D,MAAM,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,MAAM,EACvC,MAAM,8BAA8C,EACpD,OAAO,EAAE;IAAE,eAAe,EAAE,MAAM,CAAC;IAAC,iBAAiB,EAAE,MAAM,CAAC;IAAC,sBAAsB,EAAE,OAAO,CAAA;CAAE,GAC9F,gBAAgB,CA+DlB","sourcesContent":["/**\n * Controlled runtime escalation and de-escalation.\n *\n * Escalation moves to a stronger (more capable / more budgeted) candidate.\n * De-escalation moves to a cheaper/faster candidate. Both are governed by an\n * explicit transition policy, bounded by maximum transitions, and never exceed\n * the operator budget. Configured and resolved model stay distinct; no hidden\n * model substitution occurs.\n */\n\nimport { appendEvent } from \"./store.js\";\nimport type { OrchestrationCandidate } from \"./types.js\";\n\nexport interface EscalationSignal {\n\treasonCode: string;\n\t/** 0..1 severity / confidence that escalation is warranted. */\n\tstrength: number;\n}\n\nexport interface EscalationPolicy {\n\t/** Maximum escalating transitions per run (hard bound). */\n\tmaxEscalations: number;\n\t/** Maximum de-escalating transitions per run. */\n\tmaxDeescalations: number;\n\t/** Model escalation cannot exceed this provider/model capability tier. */\n\tmaxModelTier: number;\n\t/** Budget cannot exceed operator ceiling (checked by caller). */\n}\n\nexport const DEFAULT_ESCALATION_POLICY: EscalationPolicy = {\n\tmaxEscalations: 3,\n\tmaxDeescalations: 3,\n\tmaxModelTier: 5,\n};\n\nexport interface TransitionResult {\n\tkind: \"escalate\" | \"deescalate\" | \"stay\";\n\tnextCandidate?: OrchestrationCandidate;\n\treasonCode: string;\n\tremainingEscalations: number;\n\tremainingDeescalations: number;\n\tdurEvent: { type: \"ORCHESTRATION_ESCALATION_APPLIED\" | \"ORCHESTRATION_DEESCALATION_APPLIED\"; reasonCode: string };\n}\n\nconst ESCALATE_SIGNALS = new Set([\n\t\"repeated_structured_output_failure\",\n\t\"uncertainty_above_threshold\",\n\t\"stall_detected\",\n\t\"failed_focused_validation\",\n\t\"retrieval_confidence_too_low\",\n\t\"reviewer_found_critical_defect\",\n\t\"provider_capability_mismatch\",\n]);\n\nconst DEESCALATE_SIGNALS = new Set([\n\t\"simple_remaining_phase\",\n\t\"resolved_uncertainty\",\n\t\"read_only_synthesis\",\n\t\"budget_pressure\",\n\t\"repetitive_deterministic_work\",\n]);\n\n/** Decide whether to escalate, de-escalate, or stay, given signals and bounds. */\nexport function decideTransition(\n\tsignals: EscalationSignal[],\n\tcurrentCandidate: OrchestrationCandidate,\n\tcandidatesByModelTier: Map<string, OrchestrationCandidate[]>,\n\ttierOf: (candidateId: string) => number,\n\tpolicy: EscalationPolicy = DEFAULT_ESCALATION_POLICY,\n\truntime: { escalationsUsed: number; deescalationsUsed: number; budgetRemainingReserve: boolean },\n): TransitionResult {\n\tconst escalateSignal = signals.find((s) => ESCALATE_SIGNALS.has(s.reasonCode) && s.strength >= 0.5);\n\tconst deescalateSignal = signals.find((s) => DEESCALATE_SIGNALS.has(s.reasonCode) && s.strength >= 0.5);\n\n\t// De-escalation can never remove a required reviewer/validation: only allow\n\t// de-escalation to topologies that retain review when the current one has one.\n\tfunction preservesRequired(constraint: OrchestrationCandidate): boolean {\n\t\tif (currentCandidate.executionTopology === \"single_agent_with_reviewer\") {\n\t\t\treturn constraint.executionTopology === \"single_agent_with_reviewer\";\n\t\t}\n\t\treturn true;\n\t}\n\n\tif (escalateSignal && runtime.escalationsUsed < policy.maxEscalations && runtime.budgetRemainingReserve) {\n\t\tconst currentTier = tierOf(currentCandidate.candidateId);\n\t\tif (currentTier < policy.maxModelTier) {\n\t\t\tconst stronger = pickStronger(currentCandidate, candidatesByModelTier, tierOf);\n\t\t\tif (stronger) {\n\t\t\t\tappendEvent({\n\t\t\t\t\ttype: \"ORCHESTRATION_ESCALATION_APPLIED\",\n\t\t\t\t\trunId: undefined,\n\t\t\t\t\tcandidateId: stronger.candidateId,\n\t\t\t\t\tpayload: { reasonCode: escalateSignal.reasonCode, from: currentCandidate.candidateId },\n\t\t\t\t});\n\t\t\t\treturn {\n\t\t\t\t\tkind: \"escalate\",\n\t\t\t\t\tnextCandidate: stronger,\n\t\t\t\t\treasonCode: escalateSignal.reasonCode,\n\t\t\t\t\tremainingEscalations: policy.maxEscalations - runtime.escalationsUsed - 1,\n\t\t\t\t\tremainingDeescalations: policy.maxDeescalations - runtime.deescalationsUsed,\n\t\t\t\t\tdurEvent: { type: \"ORCHESTRATION_ESCALATION_APPLIED\", reasonCode: escalateSignal.reasonCode },\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t}\n\n\tif (deescalateSignal && runtime.deescalationsUsed < policy.maxDeescalations) {\n\t\tconst cheaper = pickCheaper(currentCandidate, candidatesByModelTier, tierOf, preservesRequired);\n\t\tif (cheaper) {\n\t\t\tappendEvent({\n\t\t\t\ttype: \"ORCHESTRATION_DEESCALATION_APPLIED\",\n\t\t\t\trunId: undefined,\n\t\t\t\tcandidateId: cheaper.candidateId,\n\t\t\t\tpayload: { reasonCode: deescalateSignal.reasonCode, from: currentCandidate.candidateId },\n\t\t\t});\n\t\t\treturn {\n\t\t\t\tkind: \"deescalate\",\n\t\t\t\tnextCandidate: cheaper,\n\t\t\t\treasonCode: deescalateSignal.reasonCode,\n\t\t\t\tremainingEscalations: policy.maxEscalations - runtime.escalationsUsed,\n\t\t\t\tremainingDeescalations: policy.maxDeescalations - runtime.deescalationsUsed - 1,\n\t\t\t\tdurEvent: { type: \"ORCHESTRATION_DEESCALATION_APPLIED\", reasonCode: deescalateSignal.reasonCode },\n\t\t\t};\n\t\t}\n\t}\n\n\treturn {\n\t\tkind: \"stay\",\n\t\treasonCode: \"no_transition_warranted\",\n\t\tremainingEscalations: policy.maxEscalations - runtime.escalationsUsed,\n\t\tremainingDeescalations: policy.maxDeescalations - runtime.deescalationsUsed,\n\t\tdurEvent: { type: \"ORCHESTRATION_ESCALATION_APPLIED\", reasonCode: \"no_transition_warranted\" },\n\t};\n}\n\nfunction pickStronger(\n\tcurrent: OrchestrationCandidate,\n\tbyTier: Map<string, OrchestrationCandidate[]>,\n\ttierOf: (id: string) => number,\n): OrchestrationCandidate | undefined {\n\tconst curTier = tierOf(current.candidateId);\n\tlet best: OrchestrationCandidate | undefined;\n\tfor (const [tier, cands] of byTier) {\n\t\tconst t = Number(tier);\n\t\tif (t > curTier) {\n\t\t\tfor (const c of cands) {\n\t\t\t\tif (!best || c.candidateId.localeCompare(best.candidateId) < 0) best = c;\n\t\t\t}\n\t\t}\n\t}\n\treturn best;\n}\n\nfunction pickCheaper(\n\tcurrent: OrchestrationCandidate,\n\tbyTier: Map<string, OrchestrationCandidate[]>,\n\ttierOf: (id: string) => number,\n\tpreservesRequired: (c: OrchestrationCandidate) => boolean,\n): OrchestrationCandidate | undefined {\n\tconst curTier = tierOf(current.candidateId);\n\tlet best: OrchestrationCandidate | undefined;\n\tfor (const [tier, cands] of byTier) {\n\t\tconst t = Number(tier);\n\t\tif (t < curTier) {\n\t\t\tfor (const c of cands) {\n\t\t\t\tif (!preservesRequired(c)) continue;\n\t\t\t\tif (!best || c.candidateId.localeCompare(best.candidateId) < 0) best = c;\n\t\t\t}\n\t\t}\n\t}\n\treturn best;\n}\n"]}