fix(voice): preserve analyst result and tracking contracts

keep contacts counts, rows, scope and provenance on one retained snapshot

report capped cohorts as lower bounds

separate sibling tool cancellation from newer user turns

align track_entity validation across runtime and benchmark paths
This commit is contained in:
manjunath22466 committed 2026-10-02 11:15:31 +05:30
1 parent a19897ad15
commit 095981eee7
27 files changed
+1155 -146

No files matched your search

+7 -1
View File
@@ -527,15 +527,21 @@ const schemas = [
properties: {
query: {
type: 'string',
minLength: 1,
pattern: '\\S',
},
layerId: {
type: 'string',
},
referent: {
type: 'integer',
enum: [-1, 1, 2, 3, 4, 5],
},
},
required: ['query'],
anyOf: [
{ type: 'object', required: ['query'] },
{ type: 'object', required: ['referent'] },
],
},
},
{
+21 -1
View File
@@ -3,6 +3,7 @@ import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { GEV_ACTION_SCHEMAS, createActionTools } from './actionSchemas.js';
import { GEV_REALTIME_TOOLS } from '../../server/providers/openai/tools.js';
import { validateArgs } from '../../scripts/voice-bench/grade.mjs';
const stable = (value) =>
Array.isArray(value)
@@ -30,7 +31,7 @@ test('the complete Realtime tool payload pins the manifest-generated layer relea
// Re-derived for the voice layer manifest (generated layer enums and
// aliases), point-and-ask (pointer sentinels, referent args) and the
// consolidated tool wording (each policy stated once).
'087cb623adb90f0f6203aecd6f33345bc4e5616c66ca4f4c55afdba7ff25c8bf',
'a824c2ed0ceb553eb3041350856db05dbfc5abad8a3fa17d29dc378bf7a732d2',
);
});
@@ -110,6 +111,11 @@ test('all legacy action arguments are byte-identical after removing the delibera
value.enum = value.enum.filter((key) => key !== 'pointer');
}
}
const track = legacy.find((tool) => tool.name === 'track_entity').parameters;
delete track.anyOf;
delete track.properties.query.minLength;
delete track.properties.query.pattern;
track.required = ['query'];
const analystScopeKind = legacy.find((tool) => tool.name === 'analyst_query')
.parameters.properties.scope.properties.kind;
analystScopeKind.enum = analystScopeKind.enum.filter(
@@ -131,3 +137,17 @@ test('all legacy action arguments are byte-identical after removing the delibera
'01f14fdb1523eebfcbff8b115e48e6ab99e36fa06f55a3bb13c65e198f79d758',
);
});
test('track_entity accepts a referent alone but rejects an empty target', () => {
const schema = GEV_REALTIME_TOOLS.find(
(tool) => tool.name === 'track_entity',
).parameters;
assert.deepEqual(validateArgs(schema, { referent: 2 }), []);
assert.deepEqual(validateArgs(schema, { referent: -1 }), []);
assert.deepEqual(validateArgs(schema, { query: 'UPS793' }), []);
assert.ok(validateArgs(schema, {}).length > 0);
assert.ok(validateArgs(schema, { query: '' }).length > 0);
assert.ok(validateArgs(schema, { query: ' ' }).length > 0);
assert.ok(validateArgs(schema, { referent: 0 }).length > 0);
assert.ok(validateArgs(schema, { referent: 6 }).length > 0);
});
+20
View File
@@ -147,3 +147,23 @@ test('registry: mirrors the card, records analyst items, clears per session', ()
assert.equal(referents.get(1), null);
assert.deepEqual(referentsFromResult('x', null), []);
});
test('registry: hidden card rows never resolve and last means displayed item five', () => {
const referents = createReferentRegistry();
referents.recordResult('analyst_query', {
ok: true,
count: 6,
scopeLabel: 'anywhere',
coverage: { layersQueried: [{ layerKey: 'flights' }] },
items: Array.from({ length: 6 }, (_, index) => ({
layerKey: 'flights',
id: `F${index + 1}`,
})),
});
assert.deepEqual(
referents.list().map((entry) => entry.n),
[1, 2, 3, 4, 5],
);
assert.equal(referents.get(6), null);
assert.equal(referents.get(-1)?.id, 'F5');
});
+174 -74
View File
@@ -311,8 +311,8 @@ export function createGevActionRunner({
: SPEECH_BUILDERS;
const _layerEnabledAt = new Map();
let analystEngine;
const resolveRegionRing = (name) =>
annotationResolver.resolveRegionRingForQuery(name, undefined, placeSearch);
const resolveRegionRing = (name, signal) =>
annotationResolver.resolveRegionRingForQuery(name, signal, placeSearch);
const ensureAnalystEngine = () =>
(analystEngine ||= createAnalystEngine(
analystProviders(viewer, dataManager, {
@@ -1014,6 +1014,7 @@ export function createGevActionRunner({
args,
_layerEnabledAt,
current,
runOptions.signal,
);
}
@@ -1162,7 +1163,9 @@ export function createGevActionRunner({
}
if (name === 'track_entity') {
return trackEntity(viewer, dataManager, styleManager, args);
return trackEntity(viewer, dataManager, styleManager, args, {
isCurrent: current,
});
}
if (name === 'stop_tracking') {
@@ -2182,7 +2185,20 @@ export function formatTrackedEntityLabel(found, query = '') {
}
/** Finds and tracks/selects an entity by spoken query across layer families. */
async function trackEntity(viewer, dataManager, styleManager, args = {}) {
async function trackEntity(
viewer,
dataManager,
styleManager,
args = {},
{ isCurrent = () => true } = {},
) {
const cancelled = () => ({
ok: false,
cancelled: true,
action: 'track_entity',
error: 'Tracking request was superseded by a newer voice turn',
});
if (!isCurrent()) return cancelled();
const query = String(args.query || '').trim();
if (!query) throw new Error('track_entity needs a query');
@@ -2217,6 +2233,7 @@ async function trackEntity(viewer, dataManager, styleManager, args = {}) {
error: 'The strongest fire has no usable position',
};
}
if (!isCurrent()) return cancelled();
return runManagedVoiceNavigation(
styleManager,
'fire',
@@ -2259,6 +2276,8 @@ async function trackEntity(viewer, dataManager, styleManager, args = {}) {
const found = module.findByQuery(query);
if (!found) continue;
if (!isCurrent()) return cancelled();
if (
family.kind === 'vessel' &&
(!Number.isFinite(found.latitude) || !Number.isFinite(found.longitude))
@@ -3453,7 +3472,12 @@ async function getTargetContext(
* @param {object} result The general engine's result, reused for scope text.
* @returns {object|null} A unified-count payload, or null.
*/
async function aircraftProximityWindowForQuery(dataManager, args, result) {
async function aircraftProximityWindowForQuery(
dataManager,
args,
result,
isCurrent = () => true,
) {
const spec = normalizeAnalystSpec(args, args?.layers || []);
if (!spec) return null;
const scope = args?.scope;
@@ -3461,9 +3485,8 @@ async function aircraftProximityWindowForQuery(dataManager, args, result) {
const layers = Array.isArray(args.layers) ? args.layers : [];
if (!layers.some((layer) => layer === 'flights' || layer === 'military'))
return null;
const snapshot = dataManager?.layers
?.get('military-awareness')
?.module?.getContextSnapshot?.();
const awareness = dataManager?.layers?.get('military-awareness')?.module;
const snapshot = awareness?.getAircraftQuerySnapshot?.();
const subject = snapshot?.subject;
if (!subject?.position) return null;
// An explicit centre only qualifies when it IS the subject; otherwise the
@@ -3473,18 +3496,29 @@ async function aircraftProximityWindowForQuery(dataManager, args, result) {
const radiusM = Number.isFinite(scope.km)
? scope.km * 1000
: snapshot.radiusM || 250_000;
const window = dataManager.layers
.get('military-awareness')
.module.collectAircraftProximityWindow(subject.position, {
radiusM,
subject,
});
if (!window) return null;
// A different radius is a different question. Let the general analyst
// engine answer it instead of relabelling the panel's 250 km snapshot.
if (
Number.isFinite(snapshot.radiusM) &&
Math.abs(radiusM - snapshot.radiusM) > 1
)
return null;
const label = subject.label || subject.id || 'the selected contact';
const radiusKm = Math.round(radiusM / 1000);
const wanted = layers.filter(
(layer) => layer === 'flights' || layer === 'military',
);
// Provenance belongs to the Awareness evaluation, not the current layer
// registry. The panel deliberately retains a cohort between render-cadence
// refreshes, so a live registry read here could already describe a newer
// source state than the count and items the operator is looking at.
const retainedProvenance = wanted
.map((key) => snapshot.cohorts?.[key]?.provenance)
.filter(Boolean);
const feedProvenance = feedProvenanceEnvelope(retainedProvenance);
const sourceSnapshotsById = new Map(
feedProvenance.layers.map((layer) => [layer.id, layer]),
);
// The cohort is the panel's; each contact is joined to its loaded analyst
// record so the same filters, ranking and item shape apply as everywhere
// else. The cohort still owns membership and distance.
@@ -3495,35 +3529,33 @@ async function aircraftProximityWindowForQuery(dataManager, args, result) {
.get(key)
?.module?.getAnalystRecords?.(ANALYST_RECORD_CAP) || [])
if (record?.icao24) records.set(record.icao24, record);
const rows = window[key === 'flights' ? 'flights' : 'military'].map(
(item) => {
const record = records.get(item.icao24);
let lat = record?.lat;
let lon = record?.lon;
if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
const carto = item.position
? Cesium.Cartographic.fromCartesian(item.position)
: null;
lat = carto ? Cesium.Math.toDegrees(carto.latitude) : null;
lon = carto ? Cesium.Math.toDegrees(carto.longitude) : null;
}
const row = {
...(record || {
id: item.id,
icao24: item.icao24,
callsign: item.callsign ?? null,
altitudeM: item.altitudeM ?? null,
speedMps: item.velocityMps ?? null,
aircraftClass: item.aircraftClass ?? null,
}),
lat,
lon,
};
if (Number.isFinite(item.distance))
distanceKm.set(row, Math.round(item.distance / 100) / 10);
return row;
},
);
const rows = (snapshot.cohorts?.[key]?.items || []).map((item) => {
const record = records.get(item.icao24);
let lat = record?.lat;
let lon = record?.lon;
if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
const carto = item.position
? Cesium.Cartographic.fromCartesian(item.position)
: null;
lat = carto ? Cesium.Math.toDegrees(carto.latitude) : null;
lon = carto ? Cesium.Math.toDegrees(carto.longitude) : null;
}
const row = {
...(record || {
id: item.id,
icao24: item.icao24,
callsign: item.callsign ?? null,
altitudeM: item.altitudeM ?? null,
speedMps: item.velocityMps ?? null,
aircraftClass: item.aircraftClass ?? null,
}),
lat,
lon,
};
if (Number.isFinite(item.distance))
distanceKm.set(row, Math.round(item.distance / 100) / 10);
return row;
});
return { key, rows };
});
const selection = await selectAnalystRows(groups, {
@@ -3533,39 +3565,106 @@ async function aircraftProximityWindowForQuery(dataManager, args, result) {
limit: Math.round(clampNumber(args.limit, 1, 50, 12)),
layerKeys: wanted,
distanceOf: (row) => distanceKm.get(row),
isCurrent,
});
if (!selection) return null;
if (!selection || !isCurrent()) return null;
const items = selection.items.map(compactAnalystItem);
return {
const complete = wanted.every((key) => {
const cohort = snapshot.cohorts?.[key];
return Number.isFinite(cohort?.count) && cohort.complete !== false;
});
const genericLayers = new Map(
(result?.coverage?.layersQueried || []).map((layer) => [
layer.layerKey,
layer,
]),
);
const layersQueried = wanted.map((key) => {
const cohort = snapshot.cohorts?.[key] || {};
const generic = genericLayers.get(key) || {};
const sourceSnapshot = sourceSnapshotsById.get(key);
const returned = Number.isFinite(cohort.count)
? cohort.count
: cohort.items?.length || 0;
return {
layerKey: key,
...(generic.status ? { status: generic.status } : {}),
...(generic.note ? { note: generic.note } : {}),
...(generic.window ? { window: generic.window } : {}),
returned,
total: cohort.complete === false ? null : returned,
truncated: cohort.complete === false,
...(cohort.source ? { source: cohort.source } : {}),
...(sourceSnapshot
? {
feedState: sourceSnapshot.feedState,
source: sourceSnapshot.source,
lastUpdate: sourceSnapshot.lastUpdate,
enabled: sourceSnapshot.enabled,
error: sourceSnapshot.error,
}
: {}),
};
});
const coverage = {
layersQueried,
records: {
returned: selection.count,
total: complete ? selection.count : null,
truncated: !complete,
},
scope: `window:${radiusKm}km@${label}`,
followUp: false,
feedProvenance,
note: `Contacts panel snapshot${snapshot.evaluatedAt ? ` at ${new Date(snapshot.evaluatedAt).toISOString()}` : ''} — the same retained cohort supplies the card, speech, and follow-up memory${complete ? '' : '; the count is a lower bound because at least one layer reached its retained limit'}.`,
};
const answer = {
ok: true,
action: 'analyst_query',
count: selection.count,
complete: true,
complete,
scopeLabel: `within ${radiusKm} km of ${label}`,
listed: items.length,
items,
summary: selection.summary,
...(result?.partial
? { partial: true, unanswered: [...(result.unanswered || [])] }
: {}),
display: {
scope: `within ${radiusKm} km of ${label} (Contacts window)`,
},
coverage: {
layersQueried: result?.coverage?.layersQueried || [],
scope: `window:${radiusKm}km@${label}`,
followUp: false,
note: 'Contacts window engine — the same computation and cohort the Contacts panel displays, so an unfiltered count matches the panel exactly — counts cover loaded data; the flights layer loads by viewport.',
},
coverage,
// (D) The answer always says whose window it is and which engine produced it.
window: {
engine: 'contacts-window',
centeredOn: label,
radiusKm,
flights: window.flights.length,
military: window.military.length,
aircraft: window.aircraft,
flights: snapshot.cohorts?.flights?.count ?? 'unknown',
military: snapshot.cohorts?.military?.count ?? 'unknown',
aircraft:
Number.isFinite(snapshot.cohorts?.flights?.count) &&
Number.isFinite(snapshot.cohorts?.military?.count)
? snapshot.cohorts.flights.count + snapshot.cohorts.military.count
: 'unknown',
complete,
evaluatedAt: snapshot.evaluatedAt || null,
},
...(activeContactsWindow(dataManager)
? { contactsWindow: activeContactsWindow(dataManager) }
...(snapshot.contactsWindow
? { contactsWindow: { ...snapshot.contactsWindow } }
: {}),
feedProvenance,
feedState: feedProvenance.overall,
};
return {
answer,
memory: {
matched: selection.matched,
layerKeys: wanted,
coverage,
unanswered: [...(result?.unanswered || [])],
scopeLabel: answer.scopeLabel,
scopeDetail: answer.display.scope,
},
};
}
@@ -4698,7 +4797,9 @@ async function runAnalystQuery(
args = {},
_layerEnabledAt,
isCurrent = () => true,
signal = undefined,
) {
let entityWindow = null;
const result = await analystEngine.query(
{
layers: Array.isArray(args.layers) ? args.layers : undefined,
@@ -4709,7 +4810,20 @@ async function runAnalystQuery(
limit: args.limit,
followUp: Boolean(args.followUp),
},
{ isCurrent },
{
isCurrent,
signal,
beforeCommit: async (candidate) => {
const projected = await aircraftProximityWindowForQuery(
dataManager,
args,
candidate,
isCurrent,
);
entityWindow = projected?.answer || null;
return projected?.memory || null;
},
},
);
if (!result.ok) {
const refusal = {
@@ -4755,22 +4869,8 @@ async function runAnalystQuery(
// differ. The generic record/scope engine still owns explicit regions and
// arbitrary points — only "how many aircraft around <this contact>" is
// unified, because that is the question the panel is already answering.
const entityWindow = await aircraftProximityWindowForQuery(
dataManager,
args,
result,
);
if (entityWindow) {
const provenance = feedProvenanceEnvelope(
layerSnapshots(dataManager.getAll?.() || []).filter(
(layer) => layer.enabled && ['flights', 'military'].includes(layer.id),
),
);
return {
...entityWindow,
feedProvenance: provenance,
feedState: provenance.overall,
};
return entityWindow;
}
const contactsWindow = activeContactsWindow(dataManager);
+315 -8
View File
@@ -21,6 +21,8 @@ import {
} from './gevActions.js';
import { MAP_STACKS } from '../mapStackController.js';
import { GEV_REALTIME_TOOLS } from '../../server/providers/openai/tools.js';
import { presentResult } from './resultDisplay.js';
import { attachVoiceResult } from './speech.js';
test('every live basemap is reachable by its own id — no enum value without a voice alias', () => {
// B1 regression: a stack added to MAP_STACKS (and the set_map_stack enum)
@@ -2819,8 +2821,58 @@ test('a nested Cockpit rollback result is translated too', async () => {
*/
function awarenessSubjectHarness({ subject, flights = [], military = [] }) {
const position = subject ? { __subject: true } : null;
const snapshotFlights = flights.slice(0, 20_000);
const snapshotMilitary = military.slice(0, 20_000);
return {
snapshot: subject ? { subject: { ...subject, position }, radiusM: 250_000, cohorts: [] } : null,
snapshot: subject
? {
subject: { ...subject, position },
evaluatedAt: 1_790_782_400_000,
radiusM: 250_000,
cohorts: [
{
id: 'flights',
count: snapshotFlights.length,
complete: flights.length <= 20_000,
},
{
id: 'military',
count: snapshotMilitary.length,
complete: military.length <= 20_000,
},
],
}
: null,
aircraftSnapshot: subject
? {
subject: { ...subject, position },
evaluatedAt: 1_790_782_400_000,
radiusM: 250_000,
cohorts: {
flights: {
count: snapshotFlights.length,
complete: flights.length <= 20_000,
truncated: flights.length > 20_000,
items: snapshotFlights.map((item) => ({ ...item })),
},
military: {
count: snapshotMilitary.length,
complete: military.length <= 20_000,
truncated: military.length > 20_000,
items: snapshotMilitary.map((item) => ({ ...item })),
},
},
contactsWindow: {
centeredOn: subject.label || subject.id,
radiusKm: 250,
aircraft: snapshotFlights.length + snapshotMilitary.length,
flights: snapshotFlights.length,
military: snapshotMilitary.length,
vessels: 'unknown',
complete: flights.length <= 20_000 && military.length <= 20_000,
},
}
: null,
flights,
military,
};
@@ -2832,11 +2884,40 @@ async function withAwareness(harness, run) {
const militaryLayer = (await import('../data/militaryFlights.js')).default;
const originals = {
snapshot: awareness.getContextSnapshot,
aircraftSnapshot: awareness.getAircraftQuerySnapshot,
flightsNearby: flightsLayer.getNearby,
militaryNearby: militaryLayer.getNearby,
cartoFrom: Cesium.Cartographic.fromCartesian,
};
awareness.getContextSnapshot = () => harness.snapshot;
awareness.getAircraftQuerySnapshot = () => {
const snapshot = harness.aircraftSnapshot;
if (!snapshot) return null;
return {
...snapshot,
subject: { ...snapshot.subject },
contactsWindow: snapshot.contactsWindow
? { ...snapshot.contactsWindow }
: null,
cohorts: Object.fromEntries(
Object.entries(snapshot.cohorts).map(([key, cohort]) => [
key,
{
...cohort,
provenance: cohort.provenance
? {
...cohort.provenance,
stats: cohort.provenance.stats
? { ...cohort.provenance.stats }
: undefined,
}
: null,
items: cohort.items.map((item) => ({ ...item })),
},
]),
),
};
};
flightsLayer.getNearby = () => harness.flights.slice();
militaryLayer.getNearby = () => harness.military.slice();
Cesium.Cartographic.fromCartesian = (value) => (
@@ -2848,13 +2929,14 @@ async function withAwareness(harness, run) {
return await run(awareness);
} finally {
awareness.getContextSnapshot = originals.snapshot;
awareness.getAircraftQuerySnapshot = originals.aircraftSnapshot;
flightsLayer.getNearby = originals.flightsNearby;
militaryLayer.getNearby = originals.militaryNearby;
Cesium.Cartographic.fromCartesian = originals.cartoFrom;
}
}
function analystRunner(awareness) {
function analystRunner(awareness, { getAll } = {}) {
const flights = {
id: 'flights',
// Deliberately a DIFFERENT population from the proximity window: this is
@@ -2878,7 +2960,16 @@ function analystRunner(awareness) {
dataManager: {
layers: new Map([['flights', { module: flights }], ['military-awareness', { module: awareness }]]),
isEnabled: (id) => id === 'flights',
getAll: () => [{ id: 'flights', name: 'Live Flights', enabled: true, stats: { count: 1 } }],
getAll:
getAll ||
(() => [
{
id: 'flights',
name: 'Live Flights',
enabled: true,
stats: { count: 1 },
},
]),
},
});
}
@@ -3426,6 +3517,199 @@ test('the Contacts window ranks, filters and formats like any analyst answer', a
});
});
test('a cancelled Contacts scan returns cancellation and cannot seed follow-up memory', async () => {
globalThis.window = globalThis.window || {
clearTimeout,
setTimeout,
requestIdleCallback: null,
};
const harness = awarenessSubjectHarness({
subject: { id: 'a1b2c3', label: 'N546PC' },
flights: Array.from({ length: 20_000 }, (_, index) => ({
id: `F${index}`,
icao24: `f${index}`,
distance: index,
})),
});
await withAwareness(harness, async (awareness) => {
const runner = analystRunner(awareness);
let current = true;
const pending = runner(
'analyst_query',
{
layers: ['flights'],
scope: { kind: 'radius', km: 250 },
},
{ isCurrent: () => current },
);
current = false;
const stale = await pending;
assert.equal(stale.ok, false);
assert.equal(stale.cancelled, true);
assert.equal(stale.code, 'CANCELLED');
const followUp = await runner('analyst_query', { followUp: true });
assert.equal(followUp.ok, false);
assert.equal(followUp.code, 'NO_RESULT_CONTEXT');
});
});
test('overlapping analyst siblings from one response complete independently', async () => {
globalThis.window = globalThis.window || {
clearTimeout,
setTimeout,
requestIdleCallback: null,
};
const harness = awarenessSubjectHarness({
subject: { id: 'a1b2c3', label: 'N546PC' },
flights: Array.from({ length: 20_000 }, (_, index) => ({
id: `F${index}`,
icao24: `f${index}`,
distance: index,
})),
});
await withAwareness(harness, async (awareness) => {
const runner = analystRunner(awareness);
const older = runner('analyst_query', {
layers: ['flights'],
scope: { kind: 'radius', km: 250 },
});
const sibling = await runner('analyst_query', {
layers: ['flights'],
scope: { kind: 'anywhere' },
});
assert.equal(sibling.ok, true);
assert.deepEqual(
sibling.items.map((item) => item.id),
['STALE1'],
);
assert.equal((await older).ok, true);
});
});
test('a capped Contacts snapshot is a lower bound from one immutable cohort', async () => {
globalThis.window = globalThis.window || {
clearTimeout,
setTimeout,
requestIdleCallback: null,
};
const harness = awarenessSubjectHarness({
subject: { id: 'a1b2c3', label: 'N546PC' },
flights: Array.from({ length: 20_001 }, (_, index) => ({
id: `F${index}`,
icao24: `f${index}`,
distance: index,
})),
});
await withAwareness(harness, async (awareness) => {
let source = 'snapshot-a';
const capture = awareness.getAircraftQuerySnapshot;
awareness.getAircraftQuerySnapshot = () => {
const frozen = capture();
frozen.cohorts.flights.provenance = {
id: 'flights',
name: 'Live Flights',
enabled: true,
feedState: 'nominal',
source,
count: 20_000,
lastUpdate: frozen.evaluatedAt,
ageSec: 0,
ageLabel: 'now',
error: null,
};
harness.aircraftSnapshot.cohorts.flights.items.length = 1;
harness.aircraftSnapshot.cohorts.flights.count = 1;
harness.aircraftSnapshot.contactsWindow.aircraft = 1;
harness.aircraftSnapshot.contactsWindow.flights = 1;
queueMicrotask(() => {
source = 'snapshot-b';
});
return frozen;
};
const pending = analystRunner(awareness, {
// The live registry has already advanced before query entry. The answer
// must still narrate the provenance retained with panel cohort A.
getAll: () => [
{
id: 'flights',
name: 'Live Flights',
enabled: true,
source: 'live-snapshot-b',
stats: { count: 1, source: 'live-snapshot-b' },
},
],
})('analyst_query', {
layers: ['flights'],
scope: { kind: 'radius', km: 250 },
});
const result = await pending;
assert.equal(result.ok, true);
assert.equal(result.count, 20_000);
assert.equal(result.complete, false);
assert.deepEqual(result.coverage.records, {
returned: 20_000,
total: null,
truncated: true,
});
assert.match(result.say, /^At least 20,000 aircraft within 250 km/);
assert.equal(result.window.flights, 20_000);
assert.equal(result.window.complete, false);
assert.equal(result.contactsWindow.aircraft, 20_000);
assert.equal(result.contactsWindow.flights, 20_000);
assert.equal(result.feedProvenance.layers[0].source, 'snapshot-a');
assert.equal(result.coverage.layersQueried[0].source, 'snapshot-a');
});
});
test('Contacts follow-ups filter the cohort that was actually displayed', async () => {
globalThis.window = globalThis.window || {
clearTimeout,
setTimeout,
requestIdleCallback: null,
};
const harness = awarenessSubjectHarness({
subject: { id: 'a1b2c3', label: 'N546PC' },
flights: Array.from({ length: 6 }, (_, index) => ({
id: `F${index + 1}`,
icao24: `f${index + 1}`,
distance: index * 1000,
})),
});
await withAwareness(harness, async (awareness) => {
const runner = analystRunner(awareness);
const first = await runner('analyst_query', {
layers: ['flights'],
scope: { kind: 'radius', km: 250 },
limit: 6,
});
assert.deepEqual(
first.items.map((item) => item.id),
['F1', 'F2', 'F3', 'F4', 'F5', 'F6'],
);
const followUp = await runner('analyst_query', {
followUp: true,
filters: [{ field: 'id', op: 'eq', value: 'F3' }],
});
assert.equal(followUp.count, 1);
assert.equal(followUp.items[0].id, 'F3');
assert.equal(followUp.scopeLabel, 'within 250 km of N546PC');
assert.equal(
followUp.display.scope,
'within 250 km of N546PC (Contacts window)',
);
assert.equal(followUp.coverage.scope, 'window:250km@N546PC');
assert.equal(
presentResult('analyst_query', followUp).display.title,
'1 aircraft within 250 km of N546PC',
);
assert.equal(
attachVoiceResult('analyst_query', followUp).say,
'1 aircraft within 250 km of N546PC.',
);
});
});
test('point-and-ask: the runner resolves pointer and referent targets', async () => {
globalThis.window = globalThis.window || { clearTimeout, setTimeout, requestIdleCallback: null };
const { viewer, styleManager } = createVoiceNavigationHarness();
@@ -3479,14 +3763,30 @@ test('point-and-ask: only accepted, current results become the referent list', a
const { viewer, styleManager } = createVoiceNavigationHarness();
const referents = createReferentRegistry();
let used = 0;
let tracked = 0;
const runner = createActionRunner({
viewer,
styleManager,
dataManager: {
layers: new Map([['flights', { module: {
findByQuery: () => ({ icao24: 'ccc333', callsign: 'AAL9', latitude: 1, longitude: 2 }),
trackById: () => true,
} }]]),
layers: new Map([
[
'flights',
{
module: {
findByQuery: () => ({
icao24: 'ccc333',
callsign: 'AAL9',
latitude: 1,
longitude: 2,
}),
trackById: () => {
tracked++;
return true;
},
},
},
],
]),
isEnabled: () => true,
getAll: () => [],
},
@@ -3509,9 +3809,16 @@ test('point-and-ask: only accepted, current results become the referent list', a
await runner('track_entity', { query: 'pointer' }, { signal: aborted.signal });
assert.equal(referents.get(1), null, 'a cancelled action leaves the registry alone');
assert.equal(used, 0, 'nor does it announce the pointer');
await runner('track_entity', { query: 'pointer' }, { isCurrent: () => false });
assert.equal(tracked, 0, 'nor does it start tracking');
await runner(
'track_entity',
{ query: 'pointer' },
{ isCurrent: () => false },
);
assert.equal(referents.get(1), null, 'a superseded action leaves it alone');
assert.equal(tracked, 0, 'a superseded action does not start tracking');
await runner('track_entity', { query: 'pointer' });
assert.equal(referents.get(1)?.id, 'ccc333');
assert.equal(used, 1, 'a current pointer resolution announces the chip');
assert.equal(tracked, 1);
});
+58
View File
@@ -15,6 +15,7 @@ import { createVoiceCommands } from './sessionCommands.js';
import { harness, tick } from './voiceWorkflowHarness.mjs';
import { realtimeInstructions } from '../../server/providers/openai/instructions.js';
import { GEV_REALTIME_TOOLS } from '../../server/providers/openai/tools.js';
import { attachVoiceResult } from './speech.js';
/** Run one call and fold it through the real voice card. */
async function throughCard(runner, name, args, card = initialVoiceCardState()) {
@@ -47,6 +48,7 @@ test('an analyst count reaches the model and the card with its scope and caveats
});
assert.equal(result.ok, true);
assert.equal(result.complete, true, 'complete survives the projection');
assert.equal(result.say, '3 aircraft anywhere in the loaded data.');
assert.equal(view.visible, true, 'the card shows an analyst result');
assert.equal(view.title, '3 aircraft anywhere in the loaded data');
assert.ok(
@@ -59,6 +61,34 @@ test('an analyst count reaches the model and the card with its scope and caveats
);
});
test('long analyst scope text is bounded identically for speech and card', () => {
const result = attachVoiceResult('analyst_query', {
ok: true,
action: 'analyst_query',
count: 12,
complete: true,
scopeLabel:
'over the extraordinarily long alpine administrative research corridor with a deliberately verbose official name',
coverage: { layersQueried: [{ layerKey: 'flights' }] },
});
const started = reduceVoiceCard(initialVoiceCardState(), {
type: 'action-call',
name: 'analyst_query',
callId: 'long-scope',
arguments: {},
});
const card = reduceVoiceCard(started, {
type: 'action-result',
name: 'analyst_query',
callId: 'long-scope',
result,
});
const title = voiceCardView(card).result.title;
assert.equal(result.say, title);
assert.ok(title.length <= 64);
assert.match(title, /…$/);
});
test('a capped count is a floor everywhere: model, card and caveat', async () => {
const rows = Array.from({ length: 250_001 }, (_, i) => ({
id: `F${i}`,
@@ -76,6 +106,7 @@ test('a capped count is a floor everywhere: model, card and caveat', async () =>
limit: 1,
});
assert.equal(result.complete, false);
assert.match(result.say, /^At least 250,000 aircraft/);
assert.match(view.title, /^At least 250,000 aircraft/);
assert.ok(
view.notes.some((note) => /counted 250,000 of 400,000/.test(note)),
@@ -90,6 +121,7 @@ test('a partial answer names the layers it could not read', async () => {
scope: { kind: 'anywhere' },
});
assert.equal(result.partial, true);
assert.match(result.say, /Partial; earthquakes not answered/);
assert.deepEqual(result.unanswered, ['earthquakes']);
assert.ok(view.chips.includes('partial'));
assert.ok(view.notes.includes('Not answered: earthquakes'));
@@ -105,6 +137,32 @@ test('"the second one" is the second row the card lists', async () => {
assert.equal(referents.get(-1).label, 'SWA3');
});
test('numbered and last referents are exactly the five rows the card displays', async () => {
const flights = {
getStats: () => ({ count: 6, lastUpdate: Date.now() }),
getAnalystRecords: () =>
Array.from({ length: 6 }, (_, index) => ({
id: `F${index + 1}`,
icao24: `f${index + 1}`,
callsign: `CALL${index + 1}`,
lat: 30,
lon: -97,
})),
};
const { runner, referents } = harness({ flights });
const { view } = await throughCard(runner, 'analyst_query', {
layers: ['flights'],
scope: { kind: 'anywhere' },
limit: 6,
});
assert.deepEqual(
view.referents.map((entry) => entry.label),
['CALL1', 'CALL2', 'CALL3', 'CALL4', 'CALL5'],
);
assert.equal(referents.get(6), null);
assert.equal(referents.get(-1)?.label, 'CALL5');
});
test('a follow-up naming other layers than the last answer is refused, not answered from it', async () => {
const { runner } = harness();
await runner('analyst_query', {
+4 -3
View File
@@ -12,7 +12,8 @@
import { presentResult } from './resultDisplay.js';
const MAX_REFERENTS = 12;
/** The numbered rows visible in the voice card and therefore resolvable later. */
export const MAX_DISPLAYED_REFERENTS = 5;
function finite(value) {
if (value === null || value === undefined || value === '') return null;
@@ -47,7 +48,7 @@ export function referentsFromResult(name, result) {
}
/** Numbered referent records ({n, id, label, layerId, lat, lon}). */
export function normalizeReferents(entries, max = MAX_REFERENTS) {
export function normalizeReferents(entries, max = MAX_DISPLAYED_REFERENTS) {
return (Array.isArray(entries) ? entries : [])
.slice(0, max)
.map(normalize)
@@ -55,7 +56,7 @@ export function normalizeReferents(entries, max = MAX_REFERENTS) {
.map((entry, index) => ({ ...entry, n: index + 1 }));
}
export function createReferentRegistry({ max = MAX_REFERENTS } = {}) {
export function createReferentRegistry({ max = MAX_DISPLAYED_REFERENTS } = {}) {
let list = [];
let generation = 0;
return {
+24 -8
View File
@@ -34,26 +34,42 @@ function notesOf(...values) {
.filter(Boolean);
}
function nounFor(layerKeys) {
export function analystNounFor(layerKeys) {
const keys = [...new Set(layerKeys.filter(Boolean))];
if (keys.length !== 1) return 'results';
const key = keys[0];
return LAYER_NOUNS[key] || voiceLayer(key)?.aliases?.[0] || 'results';
}
function analystCard(result) {
if (!result?.ok) return null;
const keys = (result.coverage?.layersQueried || []).map(
/** The count/scope headline shared by the analyst card and spoken answer. */
const ANALYST_HEADLINE_MAX = 64;
function boundedHeadline(value) {
const text = String(value || '')
.replace(/\s+/g, ' ')
.trim();
if (text.length <= ANALYST_HEADLINE_MAX) return text;
return `${text.slice(0, ANALYST_HEADLINE_MAX - 1).trimEnd()}…`;
}
export function analystHeadline(result) {
const keys = (result?.coverage?.layersQueried || []).map(
(layer) => layer.layerKey,
);
const count = Number(result.count) || 0;
const floor = result.complete === false ? 'At least ' : '';
const count = Number(result?.count) || 0;
const floor = result?.complete === false ? 'At least ' : '';
return boundedHeadline(
`${floor}${count.toLocaleString('en-US')} ${analystNounFor(keys)} ${result?.scopeLabel || ''}`,
);
}
function analystCard(result) {
if (!result?.ok) return null;
const display = result.display || {};
const unanswered = Array.isArray(result.unanswered) ? result.unanswered : [];
return {
display: {
title:
`${floor}${count.toLocaleString('en-US')} ${nounFor(keys)} ${result.scopeLabel || ''}`.trim(),
title: analystHeadline(result),
// The precise scope, unless the title already says exactly that.
lines: [
display.scope !== result.scopeLabel ? display.scope : null,
+19 -4
View File
@@ -15,11 +15,11 @@
* continue, not something to read back).
*
* Builders are small pure functions keyed by tool name. Tools without one are
* returned unchanged. analyst_query deliberately has no builder here: its
* result contract is owned by the analyst engine. Supply one through
* `createGevActionRunner({ speechBuilders: { analyst_query: fn } })`.
* returned unchanged.
*/
import { analystHeadline } from './resultDisplay.js';
const LAYER_NOUNS = Object.freeze({
flights: 'aircraft',
military: 'military aircraft',
@@ -398,7 +398,21 @@ function annotateMap(result) {
};
}
/** Default builders by tool name. analyst_query plugs in through the runner. */
function analystQuery(result) {
if (!result?.ok || result.cancelled) return null;
const headline = spokenLabel(analystHeadline(result), 96);
const unanswered = (Array.isArray(result.unanswered) ? result.unanswered : [])
.map((layer) => spokenLabel(layer, 24))
.filter(Boolean);
const missing = unanswered.length
? `Partial; ${listPhrase(unanswered.slice(0, 2))}${unanswered.length > 2 ? ` and ${unanswered.length - 2} more` : ''} not answered.`
: 'Partial answer.';
return {
say: `${sentence(headline)}${result.partial ? ` ${missing}` : ''}`,
};
}
/** Default builders by tool name. */
export const SPEECH_BUILDERS = Object.freeze({
set_layer_visibility: setLayerVisibility,
fly_to_location: flyToLocation,
@@ -407,6 +421,7 @@ export const SPEECH_BUILDERS = Object.freeze({
get_entity_context: getEntityContext,
get_current_view_state: getCurrentViewState,
annotate_map: annotateMap,
analyst_query: analystQuery,
});
/**
+27 -3
View File
@@ -367,10 +367,34 @@ test('annotation speech names only what failed and never announces drawing', ()
assert.deepEqual(pending.display.notes, ['Tracing outlines']);
});
test('builders never break an action and analyst_query has no default builder', () => {
assert.equal(SPEECH_BUILDERS.analyst_query, undefined);
test('analyst speech preserves exact counts, lower bounds and partial status', () => {
const raw = {
ok: true,
action: 'analyst_query',
count: 3,
complete: true,
scopeLabel: 'anywhere in the loaded data',
coverage: { layersQueried: [{ layerKey: 'flights' }] },
};
assert.equal(
attachVoiceResult('analyst_query', raw).say,
'3 aircraft anywhere in the loaded data.',
);
assert.equal(
attachVoiceResult('analyst_query', {
...raw,
count: 250_000,
complete: false,
partial: true,
unanswered: ['military'],
}).say,
'At least 250,000 aircraft anywhere in the loaded data. Partial; military not answered.',
);
});
test('builders never break an action and callers can override analyst speech', () => {
assert.equal(typeof SPEECH_BUILDERS.analyst_query, 'function');
const raw = { ok: true, action: 'analyst_query', count: 3 };
assert.equal(attachVoiceResult('analyst_query', raw), raw);
const hooked = attachVoiceResult(
'analyst_query',
raw,
+6 -4
View File
@@ -1,6 +1,6 @@
import { planStepLabel, progressStepLabel, spokenLabel } from './speech.js';
import { presentResult } from './resultDisplay.js';
import { normalizeReferents } from './referents.js';
import { MAX_DISPLAYED_REFERENTS, normalizeReferents } from './referents.js';
/**
* Pure presentation of the voice card nested in the voice control: captions,
@@ -14,7 +14,6 @@ import { normalizeReferents } from './referents.js';
const MAX_STEPS = 6;
const MAX_LINES = 6;
const MAX_REFERENTS = 5;
const MAX_NOTES = 6;
const MAX_CAPTION = 220;
@@ -205,7 +204,10 @@ export function reduceVoiceCard(state, event) {
...next,
result: {
display: shown.display,
referents: normalizeReferents(shown.referents),
referents: normalizeReferents(
shown.referents,
MAX_DISPLAYED_REFERENTS,
),
},
announce: spokenLabel(shown.display.title, 64),
});
@@ -278,7 +280,7 @@ export function voiceCardView(state) {
.map((line) => spokenLabel(line, 80))
.slice(0, MAX_LINES),
referents: (state.result?.referents || [])
.slice(0, MAX_REFERENTS)
.slice(0, MAX_DISPLAYED_REFERENTS)
.map((ref) => ({ n: ref.n, label: spokenLabel(ref.label, 48) })),
notes,
},