feat(atc): add Live ATC v2 phase machine foundation

- shared/atc/types.ts: All type definitions (Phase, Interaction, EngineState, Transmission, etc.)
- shared/atc/phases/: 9 phase modules (clearance → ground → tower → departure → enroute → approach → landing → taxiIn + emergency)
- shared/atc/phases/index.ts: Phase registry with getPhase(), getPhaseOrder(), getAllPhases()
- shared/atc/templateRenderer.ts: {var} placeholder renderer
- shared/atc/telemetryWatcher.ts: Telemetry condition evaluator for auto-advance

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
itsrubberduck
2026-02-14 19:07:03 +01:00
parent cc7981ed6a
commit f213933db2
13 changed files with 795 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
import type { Phase } from '../types'
export const approachPhase: Phase = {
id: 'approach',
name: 'Approach Control',
frequency: '119.100',
unit: 'APP',
nextPhase: 'landing',
interactions: [
{
id: 'contact_approach',
type: 'pilot_initiates',
pilotIntent: 'Pilot makes initial contact with approach control',
pilotExample: '{callsign}, descending flight level {flight_level}, information {atis_code}',
atcResponse: '{callsign}, radar contact. Expect {approach_type} approach runway {arrival_runway}.',
},
{
id: 'descend_instruction',
type: 'atc_initiates',
pilotIntent: 'ATC instructs pilot to descend to an assigned altitude',
atcResponse: '{callsign}, descend altitude {assigned_alt} feet, QNH {qnh}.',
readback: {
required: ['assigned_alt', 'qnh'],
atcConfirm: 'Correct.',
atcCorrect: 'Negative, descend altitude {assigned_alt} feet, QNH {qnh}.',
},
},
{
id: 'cleared_approach',
type: 'atc_initiates',
pilotIntent: 'ATC clears pilot for the instrument approach',
atcResponse: '{callsign}, cleared {approach_type} approach runway {arrival_runway}.',
readback: {
required: ['approach_type', 'arrival_runway'],
atcConfirm: 'Correct. Contact tower on {tower_freq}.',
atcCorrect: 'Negative, cleared {approach_type} approach runway {arrival_runway}.',
},
handoff: { toPhase: 'landing', say: 'Contact tower on {tower_freq}.' },
},
{
id: 'request_alternate_approach',
type: 'pilot_initiates',
pilotIntent: 'Pilot requests a different approach type than assigned',
pilotExample: '{callsign}, request {requested_approach} approach runway {arrival_runway}',
atcResponse: '{callsign}, approved, expect {requested_approach} approach runway {arrival_runway}.',
updates: { approach_type: '{requested_approach}' },
},
{
id: 'go_around',
type: 'pilot_initiates',
pilotIntent: 'Pilot executes a go-around and reports to approach',
pilotExample: '{callsign}, going around',
atcResponse: '{callsign}, roger, climb altitude {initial_alt} feet, fly runway heading, contact approach on {approach_freq}.',
},
{
id: 'speed_instruction',
type: 'atc_initiates',
pilotIntent: 'ATC instructs pilot to reduce or maintain a specific speed',
atcResponse: '{callsign}, reduce speed {assigned_speed} knots.',
readback: {
required: ['assigned_speed'],
atcConfirm: 'Correct.',
atcCorrect: 'Negative, reduce speed {assigned_speed} knots.',
},
},
],
}

View File

@@ -0,0 +1,32 @@
import type { Phase } from '../types'
export const clearancePhase: Phase = {
id: 'clearance',
name: 'Clearance Delivery',
frequency: '121.900',
unit: 'DEL',
nextPhase: 'ground',
interactions: [
{
id: 'request_clearance',
type: 'pilot_initiates',
pilotIntent: 'Pilot requests IFR clearance to destination',
pilotExample: '{callsign}, request IFR clearance to {dest}, information {atis_code}',
atcResponse: '{callsign}, cleared to {dest} via {sid} departure, runway {runway}, climb initially {initial_alt} feet, expect flight level {flight_level}, squawk {squawk}',
readback: {
required: ['dest', 'sid', 'runway', 'squawk', 'initial_alt'],
atcConfirm: 'Readback correct. Contact ground on {ground_freq}.',
atcCorrect: 'Negative, I say again: cleared to {dest} via {sid}, runway {runway}, climb initially {initial_alt} feet, squawk {squawk}.',
},
updates: { clearance_received: 'true' },
handoff: { toPhase: 'ground', say: 'Contact ground on {ground_freq}.' },
},
{
id: 'request_information',
type: 'pilot_initiates',
pilotIntent: 'Pilot requests ATIS information or QNH',
pilotExample: '{callsign}, request ATIS information',
atcResponse: 'Information {atis_code} is current, QNH {qnh}.',
},
],
}

View File

@@ -0,0 +1,61 @@
import type { Phase } from '../types'
export const departurePhase: Phase = {
id: 'departure',
name: 'Departure Control',
frequency: '125.100',
unit: 'DEP',
nextPhase: 'enroute',
autoAdvance: {
parameter: 'altitude_ft',
operator: '>=',
value: 18000,
},
interactions: [
{
id: 'contact_departure',
type: 'pilot_initiates',
pilotIntent: 'Pilot makes initial contact with departure after handoff from tower',
pilotExample: '{callsign}, passing {altitude} feet, climbing {initial_alt}',
atcResponse: '{callsign}, radar contact. Climb flight level {flight_level}.',
readback: {
required: ['flight_level'],
atcConfirm: 'Correct.',
atcCorrect: 'Negative, climb flight level {flight_level}.',
},
},
{
id: 'climb_instruction',
type: 'atc_initiates',
pilotIntent: 'ATC instructs pilot to climb to a new flight level',
atcResponse: '{callsign}, climb flight level {assigned_fl}.',
readback: {
required: ['assigned_fl'],
atcConfirm: 'Correct.',
atcCorrect: 'Negative, climb flight level {assigned_fl}.',
},
},
{
id: 'turn_instruction',
type: 'atc_initiates',
pilotIntent: 'ATC instructs pilot to turn to a specific heading',
atcResponse: '{callsign}, turn {turn_direction} heading {assigned_heading}.',
readback: {
required: ['turn_direction', 'assigned_heading'],
atcConfirm: 'Correct.',
atcCorrect: 'Negative, turn {turn_direction} heading {assigned_heading}.',
},
},
{
id: 'direct_to',
type: 'atc_initiates',
pilotIntent: 'ATC clears pilot to proceed direct to a waypoint',
atcResponse: '{callsign}, proceed direct {waypoint}.',
readback: {
required: ['waypoint'],
atcConfirm: 'Correct.',
atcCorrect: 'Negative, proceed direct {waypoint}.',
},
},
],
}

View File

@@ -0,0 +1,65 @@
import type { Phase } from '../types'
/**
* Emergency phase -- can be activated from ANY other phase.
*
* When the engine detects a MAYDAY or PAN PAN call it should:
* 1. Save `flags.previousPhase` with the current phase id
* 2. Set `flags.emergencyActive = true`
* 3. Transition to this phase
*
* When the emergency is cancelled the engine should:
* 1. Set `flags.emergencyActive = false`
* 2. Restore `currentPhase` to `flags.previousPhase`
* 3. Clear `flags.previousPhase`
*/
export const emergencyPhase: Phase = {
id: 'emergency',
name: 'Emergency',
frequency: '121.500',
unit: 'EMG',
nextPhase: null,
interactions: [
{
id: 'declare_mayday',
type: 'pilot_initiates',
pilotIntent: 'Pilot declares a MAYDAY emergency (distress)',
pilotExample: 'MAYDAY MAYDAY MAYDAY, {callsign}, {emergency_nature}, {pob} persons on board, fuel remaining {fuel_remaining} minutes',
atcResponse: '{callsign}, MAYDAY acknowledged. Roger {emergency_nature}. All stations, stop transmitting, MAYDAY in progress. {callsign}, say intentions.',
updates: { emergency_type: 'mayday', emergency_active: 'true' },
},
{
id: 'declare_panpan',
type: 'pilot_initiates',
pilotIntent: 'Pilot declares a PAN PAN (urgency)',
pilotExample: 'PAN PAN PAN PAN PAN PAN, {callsign}, {emergency_nature}',
atcResponse: '{callsign}, PAN PAN acknowledged. Roger {emergency_nature}. Say intentions.',
updates: { emergency_type: 'panpan', emergency_active: 'true' },
},
{
id: 'cancel_emergency',
type: 'pilot_initiates',
pilotIntent: 'Pilot cancels the emergency, situation resolved',
pilotExample: '{callsign}, cancel {emergency_type}, situation resolved',
atcResponse: '{callsign}, roger, {emergency_type} cancelled. Resume normal operations. Contact {resume_freq}.',
updates: { emergency_active: 'false' },
},
{
id: 'emergency_landing',
type: 'atc_initiates',
pilotIntent: 'ATC provides vectors for an emergency landing',
atcResponse: '{callsign}, turn {turn_direction} heading {assigned_heading}, vectors for {arrival_runway}. Cleared {approach_type} approach runway {arrival_runway}, emergency services standing by.',
readback: {
required: ['assigned_heading', 'arrival_runway'],
atcConfirm: 'Correct, emergency services standing by.',
atcCorrect: 'Negative, turn {turn_direction} heading {assigned_heading}, vectors for {arrival_runway}.',
},
},
{
id: 'souls_on_board',
type: 'atc_initiates',
pilotIntent: 'ATC requests souls on board and fuel remaining for emergency coordination',
atcResponse: '{callsign}, say souls on board and fuel remaining.',
},
],
}

View File

@@ -0,0 +1,54 @@
import type { Phase } from '../types'
export const enroutePhase: Phase = {
id: 'enroute',
name: 'Center / En-Route Control',
frequency: '132.600',
unit: 'CTR',
nextPhase: 'approach',
interactions: [
{
id: 'contact_center',
type: 'pilot_initiates',
pilotIntent: 'Pilot makes initial contact with center after handoff from departure',
pilotExample: '{callsign}, flight level {flight_level}',
atcResponse: '{callsign}, radar contact. Maintain flight level {flight_level}.',
},
{
id: 'maintain_level',
type: 'atc_initiates',
pilotIntent: 'ATC instructs pilot to maintain current flight level',
atcResponse: '{callsign}, maintain flight level {flight_level}.',
readback: {
required: ['flight_level'],
atcConfirm: 'Correct.',
atcCorrect: 'Negative, maintain flight level {flight_level}.',
},
},
{
id: 'request_level_change',
type: 'pilot_initiates',
pilotIntent: 'Pilot requests a different flight level due to weather or ride comfort',
pilotExample: '{callsign}, request flight level {requested_fl}',
atcResponse: '{callsign}, climb flight level {requested_fl}.',
readback: {
required: ['requested_fl'],
atcConfirm: 'Correct.',
atcCorrect: 'Negative, climb flight level {requested_fl}.',
},
alternatives: [
{
intent: 'Request denied due to traffic',
atcResponse: '{callsign}, unable flight level {requested_fl} due traffic. Maintain flight level {flight_level}.',
},
],
},
{
id: 'position_report',
type: 'pilot_initiates',
pilotIntent: 'Pilot reports current position over a waypoint or fix',
pilotExample: '{callsign}, position {waypoint}, flight level {flight_level}, estimating {next_waypoint} at {est_time}',
atcResponse: '{callsign}, roger, report {next_waypoint}.',
},
],
}

View File

@@ -0,0 +1,59 @@
import type { Phase } from '../types'
export const groundPhase: Phase = {
id: 'ground',
name: 'Ground Control',
frequency: '121.700',
unit: 'GND',
nextPhase: 'tower',
interactions: [
{
id: 'request_pushback',
type: 'pilot_initiates',
pilotIntent: 'Pilot requests pushback and start-up clearance',
pilotExample: '{callsign}, stand {stand}, request pushback and start-up',
atcResponse: '{callsign}, pushback approved, face {push_direction}.',
readback: {
required: ['push_direction'],
atcConfirm: 'Correct.',
atcCorrect: 'Negative, pushback approved, face {push_direction}.',
},
updates: { pushback_approved: 'true' },
},
{
id: 'request_taxi',
type: 'pilot_initiates',
when: 'vars.pushback_approved',
pilotIntent: 'Pilot requests taxi clearance to the runway',
pilotExample: '{callsign}, request taxi',
atcResponse: '{callsign}, taxi to holding point {runway} via {taxi_route}.',
readback: {
required: ['runway', 'taxi_route'],
atcConfirm: 'Correct.',
atcCorrect: 'Negative, taxi to holding point {runway} via {taxi_route}.',
},
updates: { taxi_clearance_received: 'true' },
},
{
id: 'report_holding_short',
type: 'pilot_initiates',
when: 'vars.taxi_clearance_received',
pilotIntent: 'Pilot reports holding short of the runway',
pilotExample: '{callsign}, holding short runway {runway}',
atcResponse: '{callsign}, contact tower on {tower_freq}.',
handoff: { toPhase: 'tower', say: 'Contact tower on {tower_freq}.' },
},
{
id: 'request_cross_runway',
type: 'pilot_initiates',
pilotIntent: 'Pilot requests permission to cross an active runway',
pilotExample: '{callsign}, request cross runway {cross_runway}',
atcResponse: '{callsign}, cross runway {cross_runway}, report vacated.',
readback: {
required: ['cross_runway'],
atcConfirm: 'Correct.',
atcCorrect: 'Negative, cross runway {cross_runway}, report vacated.',
},
},
],
}

View File

@@ -0,0 +1,48 @@
import type { Phase } from '../types'
import { clearancePhase } from './clearance'
import { groundPhase } from './ground'
import { towerPhase } from './tower'
import { departurePhase } from './departure'
import { enroutePhase } from './enroute'
import { approachPhase } from './approach'
import { landingPhase } from './landing'
import { taxiInPhase } from './taxiIn'
import { emergencyPhase } from './emergency'
const phases: Phase[] = [
clearancePhase,
groundPhase,
towerPhase,
departurePhase,
enroutePhase,
approachPhase,
landingPhase,
taxiInPhase,
emergencyPhase,
]
const phaseMap = new Map<string, Phase>(phases.map(p => [p.id, p]))
export function getPhase(id: string): Phase | undefined {
return phaseMap.get(id)
}
export function getPhaseOrder(): string[] {
return phases.filter(p => p.id !== 'emergency').map(p => p.id)
}
export function getAllPhases(): Phase[] {
return [...phases]
}
export {
clearancePhase,
groundPhase,
towerPhase,
departurePhase,
enroutePhase,
approachPhase,
landingPhase,
taxiInPhase,
emergencyPhase,
}

View File

@@ -0,0 +1,45 @@
import type { Phase } from '../types'
export const landingPhase: Phase = {
id: 'landing',
name: 'Tower (Landing)',
frequency: '118.500',
unit: 'TWR',
nextPhase: 'taxiIn',
autoAdvance: {
parameter: 'on_ground',
operator: '==',
value: 1,
},
interactions: [
{
id: 'report_established',
type: 'pilot_initiates',
pilotIntent: 'Pilot reports established on the final approach',
pilotExample: '{callsign}, established {approach_type} runway {arrival_runway}',
atcResponse: '{callsign}, roger, continue approach runway {arrival_runway}.',
},
{
id: 'cleared_land',
type: 'atc_initiates',
pilotIntent: 'ATC clears pilot to land',
atcResponse: '{callsign}, wind {wind}, runway {arrival_runway}, cleared to land.',
readback: {
required: ['arrival_runway'],
atcConfirm: 'Correct.',
atcCorrect: 'Negative, runway {arrival_runway}, cleared to land.',
},
},
{
id: 'go_around_instruction',
type: 'atc_initiates',
pilotIntent: 'ATC instructs pilot to go around due to traffic or runway obstruction',
atcResponse: '{callsign}, go around, I say again go around. Climb altitude {go_around_alt} feet, fly runway heading.',
readback: {
required: ['go_around_alt'],
atcConfirm: 'Correct, contact approach on {approach_freq}.',
atcCorrect: 'Negative, go around, climb altitude {go_around_alt} feet, fly runway heading.',
},
},
],
}

View File

@@ -0,0 +1,42 @@
import type { Phase } from '../types'
export const taxiInPhase: Phase = {
id: 'taxiIn',
name: 'Ground Control (Arrival)',
frequency: '121.700',
unit: 'GND',
nextPhase: null,
interactions: [
{
id: 'contact_ground_arrival',
type: 'pilot_initiates',
pilotIntent: 'Pilot contacts ground after vacating the runway',
pilotExample: '{callsign}, runway {arrival_runway} vacated, request taxi to stand',
atcResponse: '{callsign}, taxi to stand {arrival_stand} via {arrival_taxi_route}.',
readback: {
required: ['arrival_stand', 'arrival_taxi_route'],
atcConfirm: 'Correct.',
atcCorrect: 'Negative, taxi to stand {arrival_stand} via {arrival_taxi_route}.',
},
},
{
id: 'taxi_to_gate',
type: 'atc_initiates',
pilotIntent: 'ATC provides taxi instructions to the assigned stand',
atcResponse: '{callsign}, taxi to stand {arrival_stand} via {arrival_taxi_route}.',
readback: {
required: ['arrival_stand', 'arrival_taxi_route'],
atcConfirm: 'Correct.',
atcCorrect: 'Negative, taxi to stand {arrival_stand} via {arrival_taxi_route}.',
},
},
{
id: 'report_at_gate',
type: 'pilot_initiates',
pilotIntent: 'Pilot reports on stand with engines shut down',
pilotExample: '{callsign}, on stand {arrival_stand}, engines shut down',
atcResponse: '{callsign}, roger, welcome to {dest}. Frequency change approved.',
updates: { session_complete: 'true' },
},
],
}

View File

@@ -0,0 +1,59 @@
import type { Phase } from '../types'
export const towerPhase: Phase = {
id: 'tower',
name: 'Tower',
frequency: '118.500',
unit: 'TWR',
nextPhase: 'departure',
autoAdvance: {
parameter: 'altitude_ft',
operator: '>=',
value: 1000,
},
interactions: [
{
id: 'report_ready_departure',
type: 'pilot_initiates',
pilotIntent: 'Pilot reports ready for departure',
pilotExample: '{callsign}, ready for departure runway {runway}',
atcResponse: '{callsign}, hold position, standby.',
},
{
id: 'lineup_and_wait',
type: 'atc_initiates',
pilotIntent: 'ATC instructs pilot to line up on the runway and wait',
atcResponse: '{callsign}, runway {runway}, line up and wait.',
readback: {
required: ['runway'],
atcConfirm: 'Correct.',
atcCorrect: 'Negative, runway {runway}, line up and wait.',
},
},
{
id: 'cleared_takeoff',
type: 'atc_initiates',
pilotIntent: 'ATC clears pilot for takeoff',
atcResponse: '{callsign}, wind {wind}, runway {runway}, cleared for takeoff.',
readback: {
required: ['runway'],
atcConfirm: 'Correct, contact departure on {departure_freq} when airborne.',
atcCorrect: 'Negative, runway {runway}, cleared for takeoff.',
},
updates: { takeoff_clearance: 'true' },
handoff: { toPhase: 'departure', say: 'Contact departure on {departure_freq}.' },
},
{
id: 'cancel_takeoff',
type: 'atc_initiates',
pilotIntent: 'ATC cancels takeoff clearance',
atcResponse: '{callsign}, cancel takeoff, I say again cancel takeoff. Hold position.',
readback: {
required: [],
atcConfirm: 'Correct, hold position.',
atcCorrect: 'Negative, cancel takeoff, hold position.',
},
updates: { takeoff_clearance: 'false' },
},
],
}

View File

@@ -0,0 +1,61 @@
import type { Phase, TelemetryCondition, TelemetryState } from './types'
export interface TelemetryEvent {
type: 'phase_advance' | 'atc_interrupt'
toPhase?: string
interactionId?: string
trigger: {
parameter: string
condition: string
value: number | boolean
}
}
/**
* Evaluates a single telemetry condition against current telemetry state.
* Returns true if the condition is met, false otherwise.
*/
export function evaluateCondition(
telemetry: TelemetryState,
condition: TelemetryCondition
): boolean {
const actual = telemetry[condition.parameter]
if (actual === undefined) return false
const val = condition.value
switch (condition.operator) {
case '>': return (actual as number) > val
case '>=': return (actual as number) >= val
case '<': return (actual as number) < val
case '<=': return (actual as number) <= val
case '==': return actual == val // loose equality for boolean/number comparison (on_ground == 1)
case '!=': return actual != val
default: return false
}
}
/**
* Evaluates telemetry against the current phase's autoAdvance condition.
* If the condition is met and nextPhase is set, returns a phase_advance event.
* Otherwise returns null.
*/
export function evaluateTelemetry(
telemetry: TelemetryState,
currentPhase: Phase
): TelemetryEvent | null {
if (currentPhase.autoAdvance && currentPhase.nextPhase) {
if (evaluateCondition(telemetry, currentPhase.autoAdvance)) {
return {
type: 'phase_advance',
toPhase: currentPhase.nextPhase,
trigger: {
parameter: currentPhase.autoAdvance.parameter,
condition: `${currentPhase.autoAdvance.parameter} ${currentPhase.autoAdvance.operator} ${currentPhase.autoAdvance.value}`,
value: telemetry[currentPhase.autoAdvance.parameter] as number,
},
}
}
}
return null
}

View File

@@ -0,0 +1,11 @@
/**
* Simple template renderer that replaces {variable} placeholders with values.
* Renders a template string like "{callsign}, cleared to {dest} via {sid}"
* by replacing {key} placeholders with values from the vars object.
* Unreplaced placeholders are left as-is (useful for debugging).
*/
export function renderTemplate(template: string, vars: Record<string, string>): string {
return template.replace(/\{(\w+)\}/g, (match, key) => {
return vars[key] ?? match
})
}

191
shared/atc/types.ts Normal file
View File

@@ -0,0 +1,191 @@
// Phase definition
export interface Phase {
id: string
name: string
frequency: string
unit: string
interactions: Interaction[]
autoAdvance?: TelemetryCondition
nextPhase: string | null
}
export interface Interaction {
id: string
type: 'pilot_initiates' | 'atc_initiates' | 'readback_check'
when?: string // Condition expression
pilotIntent: string // For LLM prompt
pilotExample?: string // Template with {vars}
atcResponse: string // Template with {vars}
readback?: ReadbackSpec
updates?: Record<string, any>
handoff?: { toPhase: string; say?: string }
alternatives?: AlternativeResponse[]
}
export interface ReadbackSpec {
required: string[]
atcConfirm: string
atcCorrect: string
}
export interface AlternativeResponse {
intent: string
atcResponse: string
updates?: Record<string, any>
}
export interface TelemetryCondition {
parameter: string
operator: '>' | '>=' | '<' | '<=' | '==' | '!='
value: number
holdMs?: number
}
// Flight variables
export interface FlightVars {
callsign: string
aircraft: string
dep: string
dest: string
stand: string
runway: string
sid: string
squawk: string
atis_code: string
initial_alt: string
flight_level: string
qnh: string
taxi_route: string
ground_freq: string
tower_freq: string
departure_freq: string
approach_freq: string
center_freq: string
atis_freq: string
wind: string
arrival_runway: string
arrival_stand: string
arrival_taxi_route: string
star: string
approach_type: string
[key: string]: string
}
// Telemetry from SimBridge
export interface TelemetryState {
altitude_ft: number
speed_kts: number
groundspeed_kts: number
vertical_speed_fpm: number
heading_deg: number
latitude_deg: number
longitude_deg: number
on_ground: boolean
[key: string]: number | boolean
}
// Engine state
export interface EngineState {
currentPhase: string
currentInteraction: string | null
waitingFor: 'pilot' | 'readback' | 'none'
vars: FlightVars
flags: {
inAir: boolean
emergencyActive: boolean
previousPhase: string | null
}
telemetry: TelemetryState
sessionId: string
transmissions: Transmission[]
}
// Transmission with debug info
export interface Transmission {
id: string
timestamp: Date
speaker: 'pilot' | 'atc' | 'system'
message: string
normalized?: string
phase: string
frequency: string
debug: TransmissionDebug
}
export interface TransmissionDebug {
sttRaw?: string
llmRequest?: {
currentPhase: string
currentInteraction: string | null
pilotSaid: string
candidates: { id: string; intent: string }[]
contextSent: Record<string, any>
}
llmResponse?: {
chosenInteraction: string
confidence: 'high' | 'medium' | 'low'
reason: string
tokensUsed: number
durationMs: number
model: string
}
engineAction?: {
templateUsed: string
variablesUpdated: Record<string, any>
handoff?: { from: string; to: string }
phaseChanged?: { from: string; to: string }
}
telemetryTrigger?: {
parameter: string
condition: string
value: number
}
readbackResult?: {
complete: boolean
missing?: string[]
}
}
// Flight plan input
export interface FlightPlan {
callsign: string
aircraft?: string
dep: string
arr: string
route?: string
altitude?: string
squawk?: string
id?: number
assignedsquawk?: string
}
// LLM Router types
export interface RouteRequest {
pilotSaid: string
phase: string
interaction: string | null
waitingFor: 'pilot' | 'readback' | 'none'
candidates: RouteCandidate[]
vars: Record<string, any>
recentTransmissions: string[]
}
export interface RouteCandidate {
id: string
intent: string
example?: string
}
export interface RouteResponse {
chosen: string
reason: string
pilotIntent: string
confidence: 'high' | 'medium' | 'low'
tokensUsed: number
durationMs: number
model: string
readbackResult?: {
complete: boolean
missing?: string[]
}
}