mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
fix: run composite tool sections through the catalog
situation_brief, military_awareness and the HUD caption called other tools directly, so replaced tools and interceptors did not apply to their sections. Tools now reach each other through the catalog, and interceptors see these calls with the calling tool as parent. Surfaces become a view over the composed catalog, so a composite offered on voice keeps sections that voice does not list.
This commit is contained in:
1 parent
7ddee426e7
commit
963c791a5c
9 files changed
+219
-65
No files matched your search
+21
-30
@@ -8,25 +8,17 @@ import {
|
||||
resolveArea,
|
||||
resolvePoint,
|
||||
} from '../area.js';
|
||||
import { aircraftInArea } from './aviation.js';
|
||||
import {
|
||||
findMilitaryInstallations,
|
||||
getCyclones,
|
||||
getWeather,
|
||||
} from './environment.js';
|
||||
import { getActiveFires, getEarthquakes } from './hazards.js';
|
||||
import { vesselsInArea } from './maritime.js';
|
||||
|
||||
const SECTION_LIMIT = 5;
|
||||
const AWARENESS_LIMIT = 10;
|
||||
const AWARENESS_RADIUS_KM = 250;
|
||||
|
||||
/** Brief sections: the tool each one reuses and the services it needs. */
|
||||
/** Brief sections: the tool each one calls and the arguments it passes. */
|
||||
const SECTIONS = [
|
||||
{
|
||||
key: 'weather',
|
||||
label: 'Weather',
|
||||
tool: getWeather,
|
||||
tool: 'get_weather',
|
||||
args: (area, center) => ({
|
||||
location: { lat: center.lat, lon: center.lon },
|
||||
}),
|
||||
@@ -34,31 +26,31 @@ const SECTIONS = [
|
||||
{
|
||||
key: 'earthquakes',
|
||||
label: 'Earthquakes',
|
||||
tool: getEarthquakes,
|
||||
tool: 'get_earthquakes',
|
||||
args: (area) => ({ area: area.argument, limit: SECTION_LIMIT }),
|
||||
},
|
||||
{
|
||||
key: 'fires',
|
||||
label: 'Active fires',
|
||||
tool: getActiveFires,
|
||||
tool: 'get_active_fires',
|
||||
args: (area) => ({ area: area.argument, limit: SECTION_LIMIT }),
|
||||
},
|
||||
{
|
||||
key: 'aircraft',
|
||||
label: 'Aircraft',
|
||||
tool: aircraftInArea,
|
||||
tool: 'aircraft_in_area',
|
||||
args: (area) => ({ area: area.argument, limit: SECTION_LIMIT }),
|
||||
},
|
||||
{
|
||||
key: 'vessels',
|
||||
label: 'Ships',
|
||||
tool: vesselsInArea,
|
||||
tool: 'vessels_in_area',
|
||||
args: (area) => ({ area: area.argument, limit: SECTION_LIMIT }),
|
||||
},
|
||||
{
|
||||
key: 'cyclones',
|
||||
label: 'Tropical cyclones',
|
||||
tool: getCyclones,
|
||||
tool: 'get_cyclones',
|
||||
args: (area) => ({ area: area.argument }),
|
||||
},
|
||||
];
|
||||
@@ -67,7 +59,7 @@ const SECTIONS = [
|
||||
const AWARENESS_SECTIONS = [
|
||||
{
|
||||
key: 'military_aircraft',
|
||||
tool: aircraftInArea,
|
||||
tool: 'aircraft_in_area',
|
||||
args: (area) => ({
|
||||
area: area.argument,
|
||||
military: true,
|
||||
@@ -76,17 +68,17 @@ const AWARENESS_SECTIONS = [
|
||||
},
|
||||
{
|
||||
key: 'aircraft',
|
||||
tool: aircraftInArea,
|
||||
tool: 'aircraft_in_area',
|
||||
args: (area) => ({ area: area.argument, limit: AWARENESS_LIMIT }),
|
||||
},
|
||||
{
|
||||
key: 'vessels',
|
||||
tool: vesselsInArea,
|
||||
tool: 'vessels_in_area',
|
||||
args: (area) => ({ area: area.argument, limit: AWARENESS_LIMIT }),
|
||||
},
|
||||
{
|
||||
key: 'installations',
|
||||
tool: findMilitaryInstallations,
|
||||
tool: 'find_military_installations',
|
||||
args: (area) => ({ area: area.argument, limit: AWARENESS_LIMIT }),
|
||||
},
|
||||
];
|
||||
@@ -108,17 +100,16 @@ function sectionArea(resolved) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Run every section whose services are supplied. A failing section is
|
||||
* reported as unavailable instead of failing the whole answer.
|
||||
* Run every section whose tool the catalog offers, through the catalog so
|
||||
* replaced tools and interceptors apply. A failing section is reported as
|
||||
* unavailable instead of failing the whole answer.
|
||||
*/
|
||||
async function runSections(all, area, { services, signal }) {
|
||||
async function runSections(all, area, { tools, signal }) {
|
||||
const center = areaCenter(area);
|
||||
const sections = all.filter(({ tool }) =>
|
||||
tool.requires.every((key) => services[key] != null),
|
||||
);
|
||||
const sections = all.filter(({ tool }) => tools.has(tool));
|
||||
const results = await Promise.allSettled(
|
||||
sections.map(({ tool, args: build }) =>
|
||||
tool.run(build(area, center), { services, signal }),
|
||||
tools.call(tool, build(area, center)),
|
||||
),
|
||||
);
|
||||
signal?.throwIfAborted();
|
||||
@@ -140,11 +131,11 @@ async function runSections(all, area, { services, signal }) {
|
||||
return { center, answers, lines };
|
||||
}
|
||||
|
||||
async function buildBrief(args, { services, signal }) {
|
||||
async function buildBrief(args, { services, signal, tools }) {
|
||||
// Sections receive the resolved box so a place name is looked up once.
|
||||
const area = sectionArea(await resolveArea(args.area, { services, signal }));
|
||||
const { center, answers, lines } = await runSections(SECTIONS, area, {
|
||||
services,
|
||||
tools,
|
||||
signal,
|
||||
});
|
||||
return { area, center, brief: answers, lines };
|
||||
@@ -241,7 +232,7 @@ export const militaryAwareness = defineTool({
|
||||
additionalProperties: false,
|
||||
},
|
||||
requires: ['military'],
|
||||
async run(args, { services, signal }) {
|
||||
async run(args, { services, signal, tools }) {
|
||||
const point = await resolvePoint(args.location, { services, signal });
|
||||
const radiusKm = args.radius_km ?? AWARENESS_RADIUS_KM;
|
||||
const area = sectionArea(
|
||||
@@ -251,7 +242,7 @@ export const militaryAwareness = defineTool({
|
||||
),
|
||||
);
|
||||
const { answers, lines } = await runSections(AWARENESS_SECTIONS, area, {
|
||||
services,
|
||||
tools,
|
||||
signal,
|
||||
});
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { composeCatalog, coreTools, ToolError } from '../index.js';
|
||||
import {
|
||||
catalogForSurface,
|
||||
composeCatalog,
|
||||
coreTools,
|
||||
defineTool,
|
||||
ToolError,
|
||||
} from '../index.js';
|
||||
|
||||
const conditions = {
|
||||
observedAt: '2026-10-01T19:15:00.000Z',
|
||||
@@ -536,6 +542,19 @@ test('military awareness gathers contacts around a point and marks failures', as
|
||||
reason: 'unavailable right now',
|
||||
});
|
||||
assert.equal(result.data.radius_km, 100);
|
||||
const voice = catalogForSurface(catalog, 'voice');
|
||||
assert.equal(voice.get('aircraft_in_area'), undefined);
|
||||
assert.deepEqual(
|
||||
Object.keys(
|
||||
(
|
||||
await voice.call('military_awareness', {
|
||||
location: { lat: 32.7, lon: -117.2 },
|
||||
radius_km: 100,
|
||||
})
|
||||
).data.sections,
|
||||
),
|
||||
['military_aircraft', 'aircraft', 'installations'],
|
||||
);
|
||||
assert.equal(
|
||||
(await catalog.call('military_awareness', { location: { lat: 0, lon: 0 } }))
|
||||
.data.radius_km,
|
||||
@@ -549,3 +568,36 @@ test('military awareness gathers contacts around a point and marks failures', as
|
||||
/radius_km/,
|
||||
);
|
||||
});
|
||||
|
||||
test('composites reach their sections through the catalog', async () => {
|
||||
const replacement = defineTool({
|
||||
name: 'get_weather',
|
||||
title: 'Weather',
|
||||
description: 'Replaced weather.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: { location: { type: 'object' } },
|
||||
},
|
||||
run: async () => ({ summary: 'Replaced weather.', data: {} }),
|
||||
});
|
||||
const seen = [];
|
||||
const catalog = composeCatalog({
|
||||
tools: [...coreTools, replacement],
|
||||
replace: ['get_weather'],
|
||||
services: { weather, places },
|
||||
interceptors: [
|
||||
(call, next) => {
|
||||
seen.push([call.tool.name, call.parent ?? null]);
|
||||
return next(call);
|
||||
},
|
||||
],
|
||||
});
|
||||
const result = await catalog.call('situation_brief', {
|
||||
area: { place: 'Austin' },
|
||||
});
|
||||
assert.equal(result.data.sections.weather.summary, 'Replaced weather.');
|
||||
assert.deepEqual(seen, [
|
||||
['situation_brief', null],
|
||||
['get_weather', 'situation_brief'],
|
||||
]);
|
||||
});
|
||||
Reference in new issue
Block a user