Merge main into #873

Resolve the CHANGELOG and CURRENT-STATE conflicts: the PR's entries go under
Unreleased, after the 0.2.0 release.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Sameh KhamisandClaude Opus 5.5 committed 2026-10-02 21:18:15 -07:00
commit b33cae4b74
159 files changed
+18509 -1297

No files matched your search

+25 -1
View File
@@ -1,11 +1,35 @@
import { toFunctionOutput } from '../tools/functions.js';
import { GEV_ACTION_SCHEMAS } from './actionSchemas.js';
import { createGevActionRunner } from './gevActions.js';
import { createVoiceCommands } from './commands.js';
export * from './realtimeController.js';
const ACTION_NAMES = new Set(GEV_ACTION_SCHEMAS.map((schema) => schema.name));
/**
* Run app actions through `runner` and every other tool the catalog has
* through the catalog. `loadCatalog` resolves the catalog when first needed.
*/
export function withToolCatalog(runner, loadCatalog) {
if (typeof loadCatalog !== 'function') return runner;
return async function runGevTool(name, args, options = {}) {
if (ACTION_NAMES.has(name)) return runner(name, args, options);
const catalog = await loadCatalog();
if (!catalog?.get(name)) return runner(name, args, options);
const result = await catalog.call(name, args ?? {}, {
signal: options.signal,
});
return toFunctionOutput(name, result);
};
}
/** Compose the standalone action runner with the voice controls. */
export function initGevVoiceCommands(options) {
return createVoiceCommands({
...options,
runner: createGevActionRunner(options),
runner: withToolCatalog(
createGevActionRunner(options),
options.toolCatalog,
),
});
}
+41
View File
@@ -31,6 +31,7 @@ import {
readStoredVoiceLimits,
writeStoredVoiceTier,
writeStoredVoiceLimits,
withToolCatalog,
} from './gevRealtime.js';
import { createVoiceCostTracker } from './voiceCost.js';
@@ -3681,3 +3682,43 @@ test('a genuinely different refused call still gets its own output', async () =>
const testPlaceSearch = () => createStandalonePlaceSearch({ resolveApiKey: () => globalThis.window?.__GOOGLE_MAPS_API_KEY__ });
function createGevActionRunner(options) { return createActionRunner({ placeSearch: testPlaceSearch(), ...options }); }
function controlRadio(viewer, manager, args, options) { return runControlRadio(viewer, manager, args, { placeSearch: testPlaceSearch(), ...options }); }
test('voice runs app actions itself and other tools through the catalog', async () => {
const actions = [];
const runner = async (name, args) => {
actions.push([name, args]);
return { ok: true, action: name };
};
const calls = [];
let loads = 0;
const catalog = {
get: (name) => (name === 'get_wind' ? { name } : undefined),
async call(name, args, options) {
calls.push([name, args, options.signal]);
return { summary: 'Calm.', data: { calm: true } };
},
};
const run = withToolCatalog(runner, async () => {
loads += 1;
return catalog;
});
const signal = new AbortController().signal;
assert.deepEqual(await run('zoom_to_globe', {}), {
ok: true,
action: 'zoom_to_globe',
});
assert.equal(loads, 0);
assert.deepEqual(
await run('get_wind', { location: { place: 'Oslo' } }, { signal }),
{ ok: true, tool: 'get_wind', summary: 'Calm.', data: { calm: true } },
);
assert.deepEqual(calls, [
['get_wind', { location: { place: 'Oslo' } }, signal],
]);
await run('not_a_tool', {});
assert.deepEqual(
actions.map(([name]) => name),
['zoom_to_globe', 'not_a_tool'],
);
assert.equal(withToolCatalog(runner, undefined), runner);
});