// Anthropic native tool-calling gather. Non-streaming /v1/messages loop using // tool_use / tool_result content blocks. import { PROVIDERS } from "../askProvider"; import { abortError, EMPTY_PARAMS, FETCH_BUDGET, FETCH_PARAMS, FETCH_TOOL_DESCRIPTION, FETCH_TOOL_NAME, FINISH_TOOL_DESCRIPTION, FINISH_TOOL_NAME, type ParsedToolCall, postJson, REPORT_BUDGET, REPORT_PARAMS, REPORT_TOOL_DESCRIPTION, REPORT_TOOL_NAME, SEARCH_PARAMS, SEARCH_TOOL_DESCRIPTION, SEARCH_TOOL_NAME, type NativeGatherContext, } from "./shared"; // The fetch_context tool is offered only when the turn can read transcripts // (expand mode with a pinned set); update_report only in report mode; search + // finish are always present. function buildTools(includeFetch: boolean, includeReport: boolean) { const tools: { name: string; description: string; input_schema: unknown }[] = [ { name: SEARCH_TOOL_NAME, description: SEARCH_TOOL_DESCRIPTION, input_schema: SEARCH_PARAMS, }, ]; if (includeFetch) { tools.push({ name: FETCH_TOOL_NAME, description: FETCH_TOOL_DESCRIPTION, input_schema: FETCH_PARAMS, }); } if (includeReport) { tools.push({ name: REPORT_TOOL_NAME, description: REPORT_TOOL_DESCRIPTION, input_schema: REPORT_PARAMS, }); } tools.push({ name: FINISH_TOOL_NAME, description: FINISH_TOOL_DESCRIPTION, input_schema: EMPTY_PARAMS, }); return tools; } // Pure: extract tool_use blocks from an Anthropic message response. export function parseAnthropicToolUses(json: unknown): ParsedToolCall[] { const content = (json as { content?: unknown }).content; if (!Array.isArray(content)) return []; const out: ParsedToolCall[] = []; for (const block of content) { const b = block as { type?: string; id?: string; name?: string; input?: { query?: unknown; video?: unknown; aroundSeconds?: unknown; section?: unknown; content?: unknown; }; }; if (b.type === "tool_use" && typeof b.name === "string") { out.push({ id: b.id ?? "", name: b.name, query: typeof b.input?.query === "string" ? b.input.query : "", video: typeof b.input?.video === "string" ? b.input.video : "", aroundSeconds: typeof b.input?.aroundSeconds === "number" ? b.input.aroundSeconds : undefined, ...(typeof b.input?.section === "string" ? { section: b.input.section } : {}), ...(typeof b.input?.content === "string" ? { content: b.input.content } : {}), }); } } return out; } export async function anthropicGather(ctx: NativeGatherContext): Promise { const canFetch = !!ctx.runFetchContext; const canReport = !!ctx.runUpdateReport; const tools = buildTools(canFetch, canReport); const messages: { role: string; content: unknown }[] = [ ...ctx.history.map((m) => ({ role: m.role, content: m.content })), { role: "user", content: ctx.question }, ]; let searchesRun = 0; let fetchesRun = 0; let reportsRun = 0; const maxRounds = ctx.budget + 1 + (canFetch ? FETCH_BUDGET : 0) + (canReport ? REPORT_BUDGET : 0); for (let round = 0; round <= maxRounds; round++) { if (ctx.signal?.aborted) throw abortError(); const json = await postJson( "https://api.anthropic.com/v1/messages", { "x-api-key": ctx.apiKey, "anthropic-version": "2023-06-01", "anthropic-dangerous-direct-browser-access": "true", }, { model: ctx.model || PROVIDERS.anthropic.defaultModel, max_tokens: 512, system: ctx.system, tools, messages, }, "Anthropic", ctx.signal, { onDebug: ctx.onDebug, model: ctx.model }, ); const calls = parseAnthropicToolUses(json); const wantsSearch = calls.some( (c) => c.name === SEARCH_TOOL_NAME && c.query.trim() !== "", ); const wantsFetch = canFetch && calls.some((c) => c.name === FETCH_TOOL_NAME && c.video.trim() !== ""); const wantsReport = canReport && calls.some( (c) => c.name === REPORT_TOOL_NAME && (c.section ?? "").trim() !== "", ); // No actionable tool call → the model finished (or answered directly). Done. if (!wantsSearch && !wantsFetch && !wantsReport) return; // Replay the assistant's tool_use turn verbatim, then answer each tool_use. messages.push({ role: "assistant", content: (json as { content?: unknown[] }).content ?? [], }); const toolResults: unknown[] = []; for (const call of calls) { let content: string; if (call.name === SEARCH_TOOL_NAME && call.query.trim() !== "") { if (searchesRun < ctx.budget) { searchesRun += 1; content = await ctx.runSearch(call.query); } else { content = "Search budget reached — answer with the excerpts gathered so far."; } } else if ( canFetch && call.name === FETCH_TOOL_NAME && call.video.trim() !== "" ) { if (fetchesRun < FETCH_BUDGET) { fetchesRun += 1; content = await ctx.runFetchContext!(call.video, call.aroundSeconds); } else { content = "Reached the limit on transcript reads this turn."; } } else if ( canReport && call.name === REPORT_TOOL_NAME && (call.section ?? "").trim() !== "" ) { if (reportsRun < REPORT_BUDGET) { reportsRun += 1; content = await ctx.runUpdateReport!( call.section!, call.content ?? "", ); } else { content = "Reached the limit on report updates this turn."; } } else { // finish (or any other tool): acknowledge so the block is satisfied. content = "Acknowledged."; } toolResults.push({ type: "tool_result", tool_use_id: call.id, content, }); } messages.push({ role: "user", content: toolResults }); } }