import type { ModelCapability } from '#/llm/capability'; import type { ToolDescription } from '#/llm/message'; import type { ToolDefinition } from '#/tool/tool'; export const SELECT_TOOLS_TOOL_NAME = 'select_tools'; export interface LoadToolsResult { readonly toLoad: readonly string[]; readonly alreadyAvailable: readonly string[]; readonly unknown: readonly string[]; } export interface ToolSelectState { enabled(): boolean; isLoadable(name: string): boolean; isLoaded(name: string): boolean; load(names: readonly string[]): LoadToolsResult; pendingSchemas(): ToolDescription[]; announcement(): string | undefined; markSchemasLanded(): void; markAnnounced(): void; reset(): void; } export interface CreateToolSelectStateOptions { loadable: () => readonly ToolDefinition[]; enabled: () => boolean; } export function isToolSelectEnabled(capability: ModelCapability): boolean { return capability.dynamically_loaded_tools === true && capability.tool_use; } export function renderLoadableToolsAnnouncement( added: readonly string[], removed: readonly string[], ): string { const sections: string[] = []; if (added.length > 0) { sections.push(`\n${added.join('\n')}\n`); } if (removed.length > 0) { sections.push(`\n${removed.join('\n')}\n`); } sections.push( 'Use the select_tools tool with exact names to load full tool definitions before calling them. ' + 'Names listed as removed are no longer loadable — do not select them. ' + 'Fold all announcements in this conversation in order to get the current list.', ); return sections.join('\n\n'); } export function createToolSelectState({ loadable, enabled, }: CreateToolSelectStateOptions): ToolSelectState { const pending = new Set(); const landed = new Set(); const announced = new Set(); let pendingAnnouncement: { added: string[]; removed: string[] } | undefined; const loadableNames = () => new Set(loadable().map((tool) => tool.name)); const schemaOf = (name: string): ToolDescription | undefined => { const tool = loadable().find((entry) => entry.name === name); if (tool === undefined) return undefined; return { name: tool.name, description: tool.description, parameters: tool.parameters }; }; return { enabled, isLoadable: (name) => loadableNames().has(name), isLoaded: (name) => pending.has(name) || landed.has(name), load: (names) => { const loadableSet = loadableNames(); const toLoad: string[] = []; const alreadyAvailable: string[] = []; const unknown: string[] = []; for (const name of new Set(names)) { if (pending.has(name) || landed.has(name)) { alreadyAvailable.push(name); } else if (loadableSet.has(name)) { toLoad.push(name); } else { unknown.push(name); } } for (const name of toLoad) pending.add(name); return { toLoad, alreadyAvailable, unknown }; }, pendingSchemas: () => [...pending] .toSorted((a, b) => a.localeCompare(b)) .flatMap((name) => { const schema = schemaOf(name); return schema === undefined ? [] : [schema]; }), announcement: () => { if (!enabled()) return undefined; const names = loadable() .map((tool) => tool.name) .toSorted((a, b) => a.localeCompare(b)); const namesSet = new Set(names); const added = names.filter((name) => !announced.has(name)); const removed = [...announced] .filter((name) => !namesSet.has(name)) .toSorted((a, b) => a.localeCompare(b)); if (added.length === 0 && removed.length === 0) return undefined; pendingAnnouncement = { added, removed }; return renderLoadableToolsAnnouncement(added, removed); }, markSchemasLanded: () => { for (const name of pending) landed.add(name); pending.clear(); }, markAnnounced: () => { if (pendingAnnouncement === undefined) return; for (const name of pendingAnnouncement.added) announced.add(name); for (const name of pendingAnnouncement.removed) announced.delete(name); pendingAnnouncement = undefined; }, reset: () => { pending.clear(); landed.clear(); announced.clear(); pendingAnnouncement = undefined; }, }; }