fix: tighten tool answers found in local testing

Unknown aircraft tracks read as empty instead of a server error, aircraft
info names the aircraft and flight, the HUD caption sends the app's caption
context, and satellite pass, satellites overhead and nearby places take a
place name or point like the other tools.
This commit is contained in:
Sameh Khamis committed 2026-10-01 14:01:25 -07:00
1 parent 1a03e7483d
commit ec1adbdc19
9 files changed
+119 -64

No files matched your search

+9 -5
View File
@@ -200,9 +200,13 @@ export const getAircraftTrack = defineTool({
requires: ['aircraft'],
async run(args, { services, signal }) {
const icao24 = args.icao24.toLowerCase();
const track = await feedFor(services, args.military).getTrack(icao24, {
signal,
});
// A track server answers 404 for an aircraft it has no positions for.
const track = await feedFor(services, args.military)
.getTrack(icao24, { signal })
.catch((error) => {
if (error?.status === 404) return { records: [] };
throw error;
});
const points = [...track.records].sort(
(a, b) => a.observedAtMs - b.observedAtMs,
);
@@ -284,13 +288,13 @@ export const getAircraftInfo = defineTool({
if (icao24)
parts.push(
aircraft
? `${icao24} is ${aircraft.type_name || aircraft.type_code || 'an aircraft of unknown type'}${aircraft.registration ? ` (${aircraft.registration})` : ''}`
? `aircraft ${icao24} is ${aircraft.type_name || aircraft.type_code || 'an aircraft of unknown type'}${aircraft.registration ? ` (${aircraft.registration})` : ''}`
: `no type is known for ${icao24}`,
);
if (callsign)
parts.push(
flight
? `${callsign} flies ${flight.origin?.code || flight.origin?.name} to ${flight.destination?.code || flight.destination?.name}${flight.airline ? ` (${flight.airline})` : ''}`
? `flight ${callsign} flies ${flight.origin?.code || flight.origin?.name} to ${flight.destination?.code || flight.destination?.name}${flight.airline ? ` (${flight.airline})` : ''}`
: `no route is known for ${callsign}`,
);
const summary = parts.join('; ');
+25 -1
View File
@@ -215,6 +215,30 @@ test('tracks are ordered and thinned to 200 points', async () => {
(await empty.call('get_aircraft_track', { icao24: 'aaa111' })).summary,
'No recent track is available for aaa111.',
);
const failing = (status) => ({
getSnapshot: async () => snapshot([]),
getTrack: async () => {
throw new LiveSourceError('unavailable', `OpenSky HTTP ${status}`, {
status,
});
},
});
const unknown = composeCatalog({
tools: coreTools,
services: { aircraft: failing(404) },
});
assert.equal(
(await unknown.call('get_aircraft_track', { icao24: 'abcdef' })).summary,
'No recent track is available for abcdef.',
);
const down = composeCatalog({
tools: coreTools,
services: { aircraft: failing(502) },
});
await assert.rejects(
down.call('get_aircraft_track', { icao24: 'abcdef' }),
(error) => error.code === 'unavailable',
);
});
test('aircraft info combines type and route lookups', async () => {
@@ -241,7 +265,7 @@ test('aircraft info combines type and route lookups', async () => {
});
assert.equal(
both.summary,
'Aaa111 is Boeing 737-800 (N12345); UAL123 flies SFO to EWR (United Airlines).',
'Aircraft aaa111 is Boeing 737-800 (N12345); flight UAL123 flies SFO to EWR (United Airlines).',
);
assert.equal(both.data.route.destination.name, 'Newark');
const unknown = await catalog.call('get_aircraft_info', { callsign: 'XYZ9' });
+29 -2
View File
@@ -25,6 +25,7 @@ const AWARENESS_RADIUS_KM = 250;
const SECTIONS = [
{
key: 'weather',
label: 'Weather',
tool: getWeather,
args: (area, center) => ({
location: { lat: center.lat, lon: center.lon },
@@ -32,26 +33,31 @@ const SECTIONS = [
},
{
key: 'earthquakes',
label: 'Earthquakes',
tool: getEarthquakes,
args: (area) => ({ area: area.argument, limit: SECTION_LIMIT }),
},
{
key: 'fires',
label: 'Active fires',
tool: getActiveFires,
args: (area) => ({ area: area.argument, limit: SECTION_LIMIT }),
},
{
key: 'aircraft',
label: 'Aircraft',
tool: aircraftInArea,
args: (area) => ({ area: area.argument, limit: SECTION_LIMIT }),
},
{
key: 'vessels',
label: 'Ships',
tool: vesselsInArea,
args: (area) => ({ area: area.argument, limit: SECTION_LIMIT }),
},
{
key: 'cyclones',
label: 'Tropical cyclones',
tool: getCyclones,
args: (area) => ({ area: area.argument }),
},
@@ -181,9 +187,30 @@ export const getHudCaption = defineTool({
},
requires: ['weather', 'summary'],
async run(args, context) {
const { area, center, lines } = await buildBrief(args, context);
const { area, center, brief, lines } = await buildBrief(args, context);
// The app's caption context: place labels and the layers that answered,
// with their feed state.
const answered = SECTIONS.filter(
({ key }) => brief[key] && !brief[key].unavailable,
);
const layers = answered.map(({ key, label }) => ({
id: key,
name: label,
feedState: 'nominal',
}));
const response = await context.services.summary.summarize(
{ location: area.label, center, observations: lines },
{
placeLabels: [area.label],
location: area.label,
center,
observations: lines,
enabledLayerLabels: layers.map((layer) => layer.name),
enabledLayers: layers,
feedProvenance: {
overall: layers.length ? 'nominal' : null,
layers,
},
},
{ signal: context.signal },
);
const caption = response?.data?.summary;
+3
View File
@@ -438,6 +438,9 @@ test('the HUD caption summarizes the brief through the summary service', async (
);
assert.equal(result.summary, 'Overcast skies over Austin');
assert.equal(sent[0].location, 'Austin, Texas');
assert.deepEqual(sent[0].placeLabels, ['Austin, Texas']);
assert.deepEqual(sent[0].enabledLayerLabels, ['Weather']);
assert.equal(sent[0].feedProvenance.overall, 'nominal');
assert.match(sent[0].observations[0], /^Weather at /);
const failing = {
summarize: async () => ({
+5 -5
View File
@@ -82,25 +82,25 @@ export const placesNearby = defineTool({
inputSchema: {
type: 'object',
properties: {
lat: { type: 'number', minimum: -90, maximum: 90 },
lon: { type: 'number', minimum: -180, maximum: 180 },
location: POINT_SCHEMA,
radius_m: { type: 'number', minimum: 10, maximum: 5000 },
limit: LIMIT_SCHEMA,
},
required: ['lat', 'lon'],
required: ['location'],
additionalProperties: false,
},
requires: ['placeSearch'],
async run(args, { services, signal }) {
const point = await resolvePoint(args.location, { services, signal });
const radiusM = Math.round(args.radius_m ?? 250);
const result = await services.placeSearch.nearby(
{ latitude: args.lat, longitude: args.lon, radiusM },
{ latitude: point.lat, longitude: point.lon, radiusM },
{ signal },
);
if (!result.configured) throw new ToolError('unavailable', NOT_CONFIGURED);
const rows = result.places.map(placeRow);
return {
summary: `${countNoun(rows.length, 'place')} within ${radiusM} m.`,
summary: `${countNoun(rows.length, 'place')} within ${radiusM} m of ${point.label}.`,
data: capRows(rows, args.limit),
};
},
+2 -3
View File
@@ -93,11 +93,10 @@ test('nearby places use the given radius', async () => {
services: { placeSearch: search },
});
const result = await catalog.call('places_nearby', {
lat: 30.27,
lon: -97.74,
location: { lat: 30.27, lon: -97.74 },
radius_m: 400,
});
assert.equal(result.summary, '1 place within 400 m.');
assert.equal(result.summary, '1 place within 400 m of 30.2700, -97.7400.');
assert.deepEqual(search.calls[0], {
latitude: 30.27,
longitude: -97.74,
+26 -26
View File
@@ -6,6 +6,7 @@ import {
lookAnglesAt,
} from '../../data/satellitePass.js';
import { parseTleText, tleCatalogNumber } from '../../sources/tle.js';
import { POINT_SCHEMA, resolvePoint } from '../area.js';
import { defineTool, ToolError } from '../catalog.js';
import { LIMIT_SCHEMA, capRows, countNoun, isoTime } from '../results.js';
@@ -83,20 +84,6 @@ const COMPASS = [
'NW',
'NNW',
];
const OBSERVER = {
lat: {
type: 'number',
minimum: -90,
maximum: 90,
description: 'Observer latitude.',
},
lon: {
type: 'number',
minimum: -180,
maximum: 180,
description: 'Observer longitude.',
},
};
const GROUP_SCHEMA = {
type: 'string',
enum: GROUPS,
@@ -154,7 +141,7 @@ export const nextSatellitePass = defineTool({
inputSchema: {
type: 'object',
properties: {
...OBSERVER,
location: POINT_SCHEMA,
satellite: {
type: 'string',
minLength: 1,
@@ -170,11 +157,12 @@ export const nextSatellitePass = defineTool({
},
hours: { type: 'number', minimum: 1, maximum: 72 },
},
required: ['lat', 'lon'],
required: ['location'],
additionalProperties: false,
},
requires: ['satellites'],
async run(args, { services, signal }) {
const point = await resolvePoint(args.location, { services, signal });
const group = args.group ?? 'stations';
const entries = await readCatalog(services, group, signal);
const satellite = matchSatellite(entries, args.satellite);
@@ -186,8 +174,8 @@ export const nextSatellitePass = defineTool({
const hours = args.hours ?? 24;
const pass = findNextSatellitePass({
satrec: satellite.satrec,
latDeg: args.lat,
lonDeg: args.lon,
latDeg: point.lat,
lonDeg: point.lon,
fromMs: nowMs(services),
minElevDeg: args.min_elevation_deg ?? 10,
horizonHours: hours,
@@ -196,15 +184,21 @@ export const nextSatellitePass = defineTool({
const name = satellite.name;
if (!pass)
return {
summary: `${name} has no ${args.visible_only ? 'visible ' : ''}pass over this location in the next ${hours} hours.`,
data: { satellite: name, norad: satellite.norad, pass: null },
summary: `${name} has no ${args.visible_only ? 'visible ' : ''}pass over ${point.label} in the next ${hours} hours.`,
data: {
location: point,
satellite: name,
norad: satellite.norad,
pass: null,
},
};
return {
summary:
`${name} next rises ${isoTime(pass.riseMs)} in the ${compass(pass.riseAzDeg)}, ` +
`${name} next rises over ${point.label} at ${isoTime(pass.riseMs)} in the ${compass(pass.riseAzDeg)}, ` +
`peaking at ${Math.round(pass.maxElevDeg)}°` +
`${pass.visible ? '; visible to the naked eye' : ''}.`,
data: {
location: point,
satellite: name,
norad: satellite.norad,
pass: {
@@ -230,22 +224,23 @@ export const satellitesOverhead = defineTool({
inputSchema: {
type: 'object',
properties: {
...OBSERVER,
location: POINT_SCHEMA,
group: GROUP_SCHEMA,
min_elevation_deg: { type: 'number', minimum: 0, maximum: 90 },
limit: LIMIT_SCHEMA,
},
required: ['lat', 'lon'],
required: ['location'],
additionalProperties: false,
},
requires: ['satellites'],
async run(args, { services, signal }) {
const point = await resolvePoint(args.location, { services, signal });
const group = args.group ?? 'stations';
const minimum = args.min_elevation_deg ?? 10;
const at = nowMs(services);
const rows = (await readCatalog(services, group, signal))
.flatMap((entry) => {
const look = lookAnglesAt(entry.satrec, at, args.lat, args.lon);
const look = lookAnglesAt(entry.satrec, at, point.lat, point.lon);
return look && look.elevDeg >= minimum
? [
{
@@ -260,8 +255,13 @@ export const satellitesOverhead = defineTool({
})
.sort((a, b) => b.elevation_deg - a.elevation_deg);
return {
summary: `${countNoun(rows.length, 'satellite')} from the ${group} group ${rows.length === 1 ? 'is' : 'are'} at least ${minimum}° above this location at ${isoTime(at)}.`,
data: { ...capRows(rows, args.limit), group, at: isoTime(at) },
summary: `${countNoun(rows.length, 'satellite')} from the ${group} group ${rows.length === 1 ? 'is' : 'are'} at least ${minimum}° above ${point.label} at ${isoTime(at)}.`,
data: {
...capRows(rows, args.limit),
location: point,
group,
at: isoTime(at),
},
};
},
});
+17 -19
View File
@@ -36,8 +36,7 @@ test('the next pass defaults to the ISS and reports times, peak and direction',
services: { satellites: satellites(), clock: CLOCK },
});
const result = await catalog.call('next_satellite_pass', {
lat: 40.7,
lon: -74,
location: { lat: 40.7, lon: -74 },
});
const { pass } = result.data;
assert.equal(result.data.satellite, 'ISS (ZARYA)');
@@ -50,7 +49,7 @@ test('the next pass defaults to the ISS and reports times, peak and direction',
assert.match(
result.summary,
new RegExp(
`^ISS \\(ZARYA\\) next rises ${pass.rise} in the ${pass.rise_direction}, peaking at \\d+°`,
`^ISS \\(ZARYA\\) next rises over 40.7000, -74.0000 at ${pass.rise} in the ${pass.rise_direction}, peaking at \\d+°`,
),
);
});
@@ -62,14 +61,12 @@ test('satellites are matched by name or catalog number within the group', async
services: { satellites: source, clock: CLOCK },
});
const byName = await catalog.call('next_satellite_pass', {
lat: 0,
lon: 0,
location: { lat: 0, lon: 0 },
satellite: 'tianhe',
});
assert.equal(byName.data.norad, 48274);
const byNumber = await catalog.call('next_satellite_pass', {
lat: 0,
lon: 0,
location: { lat: 0, lon: 0 },
satellite: '48274',
group: 'visual',
});
@@ -77,14 +74,16 @@ test('satellites are matched by name or catalog number within the group', async
assert.deepEqual(source.calls, ['stations', 'visual']);
await assert.rejects(
catalog.call('next_satellite_pass', {
lat: 0,
lon: 0,
location: { lat: 0, lon: 0 },
satellite: 'Hubble',
}),
/No satellite matching "Hubble" is in the stations group/,
);
await assert.rejects(
catalog.call('next_satellite_pass', { lat: 0, lon: 0, group: 'weather' }),
catalog.call('next_satellite_pass', {
location: { lat: 0, lon: 0 },
group: 'weather',
}),
/must be one of/,
);
});
@@ -96,21 +95,20 @@ test('a pass that never comes is a normal answer, and catalog failures are unava
});
// The ISS orbit is inclined 51.6°, so it never rises 10° above the pole.
const none = await catalog.call('next_satellite_pass', {
lat: 89.9,
lon: 0,
location: { lat: 89.9, lon: 0 },
hours: 6,
});
assert.equal(none.data.pass, null);
assert.equal(
none.summary,
'ISS (ZARYA) has no pass over this location in the next 6 hours.',
'ISS (ZARYA) has no pass over 89.9000, 0.0000 in the next 6 hours.',
);
const down = composeCatalog({
tools: coreTools,
services: { satellites: satellites('', false), clock: CLOCK },
});
await assert.rejects(
down.call('next_satellite_pass', { lat: 0, lon: 0 }),
down.call('next_satellite_pass', { location: { lat: 0, lon: 0 } }),
(error) => error.code === 'unavailable' && /HTTP 503/.test(error.message),
);
});
@@ -120,7 +118,9 @@ test('satellites overhead are those above the elevation, highest first', async (
const services = { satellites: source, clock: CLOCK };
const catalog = composeCatalog({ tools: coreTools, services });
const pass = (
await catalog.call('next_satellite_pass', { lat: 40.7, lon: -74 })
await catalog.call('next_satellite_pass', {
location: { lat: 40.7, lon: -74 },
})
).data.pass;
const atPeak = {
satellites: source,
@@ -130,8 +130,7 @@ test('satellites overhead are those above the elevation, highest first', async (
tools: coreTools,
services: atPeak,
}).call('satellites_overhead', {
lat: 40.7,
lon: -74,
location: { lat: 40.7, lon: -74 },
});
assert.equal(overhead.data.rows[0].name, 'ISS (ZARYA)');
assert.ok(
@@ -142,8 +141,7 @@ test('satellites overhead are those above the elevation, highest first', async (
tools: coreTools,
services: atPeak,
}).call('satellites_overhead', {
lat: 40.7,
lon: -74,
location: { lat: 40.7, lon: -74 },
min_elevation_deg: 90,
});
assert.equal(strict.data.total, 0);