import { HUD_LAYOUTS } from '../hudLayoutPolicy.js'; // Canonical action arguments. Descriptive wording is supplied separately. import { VOICE_CONTEXT_LAYER_IDS, VOICE_QUERY_LAYER_IDS, VOICE_TOGGLE_LAYER_IDS, } from './layerManifest.js'; // Layer enums come from the voice layer manifest so every shipped layer is // reachable; the manifest test keeps it in step with the layer catalog. const toggleLayerIds = () => [...VOICE_TOGGLE_LAYER_IDS]; const schemas = [ { name: 'fly_to_location', parameters: { type: 'object', additionalProperties: false, properties: { locationId: { type: 'string', enum: [ 'austin', 'sf', 'nyc', 'tokyo', 'london', 'paris', 'dubai', 'dc', ], }, query: { type: 'string', }, latitude: { type: 'number', minimum: -90, maximum: 90, }, longitude: { type: 'number', minimum: -180, maximum: 180, }, viewMode: { type: 'string', enum: ['close', 'overview'], }, rangeM: { type: 'number', minimum: 100, maximum: 20000000, }, waitForArrival: { type: 'boolean', }, referent: { type: 'integer', }, }, }, }, { name: 'select_nearest_aircraft', parameters: { type: 'object', additionalProperties: false, properties: { layerId: { type: 'string', enum: ['flights', 'military'], }, locationId: { type: 'string', enum: [ 'austin', 'sf', 'nyc', 'tokyo', 'london', 'paris', 'dubai', 'dc', ], }, locationQuery: { type: 'string', maxLength: 160, }, latitude: { type: 'number', minimum: -90, maximum: 90, }, longitude: { type: 'number', minimum: -180, maximum: 180, }, }, required: ['layerId'], }, }, { name: 'adjust_camera_zoom', parameters: { type: 'object', additionalProperties: false, properties: { direction: { type: 'string', enum: ['in', 'out'], }, amount: { type: 'string', enum: ['little', 'medium', 'lot'], }, }, required: ['direction', 'amount'], }, }, { name: 'zoom_to_globe', parameters: { type: 'object', additionalProperties: false, properties: {}, }, }, { name: 'set_layer_visibility', parameters: { type: 'object', additionalProperties: false, properties: { layerId: { type: 'string', enum: toggleLayerIds(), }, enabled: { type: 'boolean', }, }, required: ['layerId', 'enabled'], }, }, { name: 'show_data_layers_menu', parameters: { type: 'object', additionalProperties: false, properties: { layerId: { type: 'string', enum: toggleLayerIds(), }, }, }, }, { name: 'set_panel_open', parameters: { type: 'object', additionalProperties: false, properties: { panelId: { type: 'string', enum: [ 'data-panel', 'location-bar', 'control-panel', 'cctv-panel', 'radio-panel', 'scene-panel', 'utilities-panel', 'pp-toggles', 'global-context-panel', 'weather-panel', 'recent-imagery-panel', 'voice-settings', 'voice-preview', 'provider-settings', ], }, open: { type: 'boolean', }, }, required: ['panelId', 'open'], }, }, { name: 'set_voice_inactivity_timeout', parameters: { type: 'object', additionalProperties: false, properties: { minutes: { type: 'integer', minimum: 0, maximum: 60 } }, required: ['minutes'], }, }, { name: 'set_context_mode', parameters: { type: 'object', additionalProperties: false, properties: { mode: { type: 'string', enum: ['off', 'contacts', 'flights', 'space-missions', 'missions'], }, }, required: ['mode'], }, }, { name: 'control_cockpit', parameters: { type: 'object', additionalProperties: false, properties: { action: { type: 'string', enum: ['enter', 'exit', 'previous', 'next', 'prev', 'status'], }, targetLayer: { type: 'string', enum: [ 'flights', 'military', 'ais-live-vessels', 'military-installations', ], }, aircraftClass: { type: 'string', }, voicePanel: { type: 'string', enum: ['preview', 'settings'] }, }, required: ['action'], }, }, { name: 'set_visual_style', parameters: { type: 'object', additionalProperties: false, properties: { style: { type: 'string', enum: ['normal', 'retro', 'surveillance', 'thermal', 'anime', 'noir'], }, }, required: ['style'], }, }, { name: 'get_entity_context', parameters: { type: 'object', additionalProperties: false, properties: { scope: { type: 'string', enum: ['auto', 'selected', 'in_view', 'pointer'], }, referent: { type: 'integer', }, layerId: { type: 'string', enum: [...VOICE_CONTEXT_LAYER_IDS], }, limit: { type: 'number', minimum: 1, maximum: 12, }, }, }, }, { name: 'get_current_view_state', parameters: { type: 'object', additionalProperties: false, properties: {}, }, }, { name: 'set_hud', parameters: { type: 'object', additionalProperties: false, properties: { visible: { type: 'string', enum: ['on', 'off', 'auto'], }, layout: { type: 'string', enum: [...HUD_LAYOUTS], }, }, }, }, { name: 'set_cyber_sonar', parameters: { type: 'object', additionalProperties: false, properties: { enabled: { type: 'boolean' }, rings: { type: 'integer', minimum: 3, maximum: 12 }, rangePct: { type: 'integer', minimum: 60, maximum: 120 }, intensityPct: { type: 'integer', minimum: 0, maximum: 100 }, opacityPct: { type: 'integer', minimum: 35, maximum: 100 }, sectorDeg: { type: 'integer', minimum: 8, maximum: 60 }, }, }, }, { name: 'set_detection', parameters: { type: 'object', additionalProperties: false, properties: { enabled: { type: 'boolean', }, mode: { type: 'string', enum: ['sparse', 'balanced', 'dense'], }, densityPct: { type: 'number', }, allocationStrategy: { type: 'string', enum: ['elastic', 'weighted'], }, }, }, }, { name: 'set_map_stack', parameters: { type: 'object', additionalProperties: false, properties: { stack: { type: 'string', enum: [ 'photoreal', 'bing-aerial', 'bing-labels', 'esri-imagery', 'osm', ], }, }, required: ['stack'], }, }, { name: 'set_post_processing', parameters: { type: 'object', additionalProperties: false, properties: { bloom: { type: 'object', additionalProperties: false, properties: { enabled: { type: 'boolean', }, intensityPct: { type: 'number', }, }, }, sharpen: { type: 'object', additionalProperties: false, properties: { enabled: { type: 'boolean', }, intensityPct: { type: 'number', }, }, }, }, }, }, { name: 'control_scene', parameters: { type: 'object', additionalProperties: false, properties: { action: { type: 'string', enum: ['list', 'play', 'stop', 'next', 'status'], }, sceneId: { type: 'string', }, }, required: ['action'], }, }, { name: 'control_cctv', parameters: { type: 'object', additionalProperties: false, properties: { action: { type: 'string', enum: [ 'enable', 'disable', 'select', 'next', 'prev', 'nearest', 'focus', 'coverage', 'viewshed', 'adjust', 'projection', 'autohop', ], }, cameraQuery: { type: 'string', }, enabled: { type: 'boolean', }, }, required: ['action'], }, }, { name: 'control_radio', parameters: { type: 'object', additionalProperties: false, properties: { action: { type: 'string', enum: [ 'enable', 'disable', 'play', 'resume', 'pause', 'stop', 'next', 'previous', 'volume', 'select', 'status', ], }, volumePct: { type: 'number', minimum: 0, maximum: 100, }, category: { type: 'string', enum: [ 'all', 'news', 'talk', 'weather', 'public-safety', 'aviation-marine', 'traffic-transit', 'music', ], }, locationId: { type: 'string', enum: [ 'austin', 'sf', 'nyc', 'tokyo', 'london', 'paris', 'dubai', 'dc', ], }, locationQuery: { type: 'string', maxLength: 120, }, latitude: { type: 'number', minimum: -90, maximum: 90, }, longitude: { type: 'number', minimum: -180, maximum: 180, }, country: { type: 'string', maxLength: 80, }, stationQuery: { type: 'string', maxLength: 120, }, }, required: ['action'], }, }, { name: 'track_entity', parameters: { type: 'object', additionalProperties: false, properties: { query: { type: 'string', minLength: 1, pattern: '\\S', }, layerId: { type: 'string', }, referent: { type: 'integer', enum: [-1, 1, 2, 3, 4, 5], }, }, anyOf: [ { type: 'object', required: ['query'] }, { type: 'object', required: ['referent'] }, ], }, }, { name: 'stop_tracking', parameters: { type: 'object', additionalProperties: false, properties: {}, }, }, { name: 'frame_overhead', parameters: { type: 'object', additionalProperties: false, properties: { target: { type: 'string', enum: ['flights', 'military', 'satellites', 'vessels'], }, radiusKm: { type: 'number', }, }, required: ['target'], }, }, { name: 'annotate_map', parameters: { type: 'object', additionalProperties: false, properties: { annotations: { type: 'array', minItems: 1, maxItems: 24, items: { type: 'object', additionalProperties: false, properties: { type: { type: 'string', enum: ['pin', 'highlight', 'area', 'arrow', 'route', 'label'], }, target: { type: 'string', maxLength: 200, }, points: { type: 'array', minItems: 2, maxItems: 12, items: { type: 'object', additionalProperties: false, properties: { target: { type: 'string', maxLength: 200, }, latitude: { type: 'number', minimum: -90, maximum: 90, }, longitude: { type: 'number', minimum: -180, maximum: 180, }, screenX: { type: 'number', minimum: 0, maximum: 1, }, screenY: { type: 'number', minimum: 0, maximum: 1, }, }, }, }, mode: { type: 'string', enum: ['walking', 'driving', 'cycling'], }, latitude: { type: 'number', minimum: -90, maximum: 90, }, longitude: { type: 'number', minimum: -180, maximum: 180, }, toTarget: { type: 'string', maxLength: 200, }, toLatitude: { type: 'number', minimum: -90, maximum: 90, }, toLongitude: { type: 'number', minimum: -180, maximum: 180, }, label: { type: 'string', maxLength: 120, }, color: { type: 'string', enum: ['primary', 'amber', 'cyan', 'green', 'red'], }, footprint: { type: 'boolean', }, intent: { type: 'string', enum: ['the_thing', 'around_the_thing'], }, entityKind: { type: 'string', enum: [ 'building', 'compound', 'district', 'street', 'point_feature', ], }, screenX: { type: 'number', minimum: 0, maximum: 1, }, screenY: { type: 'number', minimum: 0, maximum: 1, }, toScreenX: { type: 'number', minimum: 0, maximum: 1, }, toScreenY: { type: 'number', minimum: 0, maximum: 1, }, }, required: ['type'], }, }, flyTo: { type: 'boolean', }, persist: { type: 'boolean', }, }, required: ['annotations'], }, }, { name: 'clear_annotations', parameters: { type: 'object', additionalProperties: false, properties: {}, }, }, { name: 'move_camera', parameters: { type: 'object', additionalProperties: false, properties: { motion: { type: 'string', enum: ['orbit', 'pan', 'tilt', 'rotate', 'rotate_globe', 'stop'], }, direction: { type: 'string', enum: ['left', 'right', 'up', 'down'], }, speed: { type: 'string', enum: ['slow', 'normal', 'fast'], }, mode: { type: 'string', enum: ['once', 'continuous'], }, }, required: ['motion'], }, }, { name: 'fly_route', parameters: { type: 'object', additionalProperties: false, properties: { label: { type: 'string', }, speed: { type: 'string', enum: ['slow', 'normal', 'fast'], }, }, }, }, { name: 'analyst_query', parameters: { type: 'object', additionalProperties: false, properties: { layers: { type: 'array', items: { type: 'string', enum: [...VOICE_QUERY_LAYER_IDS], }, }, scope: { type: 'object', additionalProperties: false, properties: { kind: { type: 'string', enum: ['view', 'region', 'radius', 'anywhere', 'pointer'], }, name: { type: 'string', }, km: { type: 'number', }, center: { type: 'object', additionalProperties: false, properties: { lat: { type: 'number', minimum: -90, maximum: 90, }, lon: { type: 'number', minimum: -180, maximum: 180, }, }, required: ['lat', 'lon'], }, }, }, filters: { type: 'array', items: { type: 'object', additionalProperties: false, properties: { field: { type: 'string', }, op: { type: 'string', enum: ['gt', 'gte', 'lt', 'lte', 'eq', 'neq', 'contains'], }, value: {}, }, required: ['field', 'op', 'value'], }, }, sortBy: { type: 'string', }, sortDir: { type: 'string', enum: ['asc', 'desc'], }, limit: { type: 'number', }, followUp: { type: 'boolean', }, }, }, }, { name: 'next_iss_pass', parameters: { type: 'object', additionalProperties: false, properties: { latitude: { type: 'number', minimum: -90, maximum: 90, }, longitude: { type: 'number', minimum: -180, maximum: 180, }, minElevationDeg: { type: 'number', minimum: 5, maximum: 60, }, }, }, }, { name: 'next_satellite_pass', parameters: { type: 'object', additionalProperties: false, required: ['target'], properties: { target: { type: 'string', minLength: 1, maxLength: 120 }, latitude: { type: 'number', minimum: -90, maximum: 90 }, longitude: { type: 'number', minimum: -180, maximum: 180 }, minElevationDeg: { type: 'number', minimum: 5, maximum: 60 }, visibleOnly: { type: 'boolean' }, }, }, }, ]; function freeze(value) { if (value && typeof value === 'object') { Object.values(value).forEach(freeze); Object.freeze(value); } return value; } /** Immutable action names and argument schemas, without model-facing wording. */ export const GEV_ACTION_SCHEMAS = freeze(schemas); /** Build independent function-tool records with description-only metadata. */ export function createActionTools(descriptions = {}) { for (const name of Object.keys(descriptions)) { if (!schemas.some((schema) => schema.name === name)) throw new TypeError('Unknown action description: ' + name); } return schemas.map((schema) => { const tool = { type: 'function', ...describe(schema, descriptions[schema.name] || {}), }; // Function-calling APIs reject anyOf/oneOf/allOf at the top level of // parameters; the runner and benchmark validate those rules themselves. for (const key of TOP_LEVEL_COMBINATORS) delete tool.parameters?.[key]; return tool; }); } const TOP_LEVEL_COMBINATORS = ['anyOf', 'oneOf', 'allOf']; function describe(schema, metadata, allowDescription = true) { if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) throw new TypeError('Descriptions must be an object'); const result = structuredClone(schema); for (const [key, value] of Object.entries(metadata)) { if (key === '$position' && allowDescription) continue; if (key === 'description' && allowDescription) { if (typeof value !== 'string') throw new TypeError('Description must be text'); Object.defineProperty(result, key, { value, enumerable: true, writable: true, configurable: true, }); } else { if ( !Object.hasOwn(schema, key) || !schema[key] || typeof schema[key] !== 'object' ) throw new TypeError( 'Descriptions cannot change action arguments: ' + key, ); Object.defineProperty(result, key, { value: describe( schema[key], value, !Array.isArray(schema[key]) && (!allowDescription || key !== 'properties'), ), enumerable: true, writable: true, configurable: true, }); } } if ( Object.hasOwn(metadata, '$position') && (!Object.hasOwn(metadata, 'description') || !Number.isInteger(metadata.$position) || metadata.$position < 0 || metadata.$position > Object.keys(schema).length) ) throw new TypeError('Invalid description position'); if (allowDescription && Object.hasOwn(metadata, 'description')) { const entries = Object.entries(result).filter( ([key]) => key !== 'description', ); entries.splice(metadata.$position ?? entries.length, 0, [ 'description', metadata.description, ]); return Object.fromEntries(entries); } return result; }