/** * The server-tool inner loop (gateway-executed web search). * * When the upstream model calls a *server-executed* tool (today: `web_search`), * nobody on the caller's side can answer it — the caller declared the tool * expecting the "server" to run it. This loop makes the gateway that server: * it intercepts server-tool calls from a model step, executes them via a * {@link WebSearchExecutor}, appends the exchange to the chat transcript, and * runs another model step — repeating until a step commits to something the * caller can actually handle (text, client tool calls, or a clean stop). * * The loop operates at the chat-completions layer, around `backend.chat`: * each inner step is an ordinary backend turn, exactly as * if the caller had executed a client tool and come back. The dialect egress * translators stay single-stream: in streaming mode the loop composes the * steps' chat SSE into one continuous stream, suppressing the server-tool * fragments and injecting {@link ServerToolMarker} chunks (an in-process * convention) that the translators render as their dialect's native search * items (`web_search_call` / `server_tool_use` + `web_search_tool_result`). * * Mixed batches (server + client calls in one step) terminate the turn: the * client calls surface and the server calls are dropped un-executed — results * could not be fed back into a turn that just ended, and the upstream model can * simply re-issue the search next turn. */ import { type Reasoning, type ToolResult } from "@velum-labs/routekit-contracts/protocol-ir"; import { type RouteKitPlatform } from "@velum-labs/routekit-runtime/effect"; import { type Context, Effect } from "effect"; import type { BackendRequest } from "../providers/backend.js"; import { type OpenAiChatResponse } from "../providers/protocol.js"; import type { WebSearchExecutor } from "./web-search.js"; /** In-process marker chunk field the loop injects between composed steps. */ export declare const SERVER_TOOL_MARKER_FIELD = "routekit_server_tool"; export type ServerToolMarker = { kind: "web_search"; phase: "start" | "done"; item_id: string; query: string; status?: "completed" | "failed"; /** Anthropic-native result blocks for the Anthropic egress (done phase). */ result_blocks?: unknown[]; }; /** The marker on a parsed chat chunk, if present. */ export declare function serverToolMarkerOf(chunk: unknown): ServerToolMarker | undefined; export type ExecutedSearch = { itemId: string; query: string; status: "completed" | "failed"; result?: ToolResult; }; export type ServerToolLoopEvent = { kind: "reasoning"; details: Reasoning[]; } | { kind: "search"; search: ExecutedSearch; }; export type ServerToolLoopOptions = { /** The translated chat body; the loop appends search exchanges to `messages`. */ chat: Record; runStep: (chat: Record) => BackendRequest; serverToolNames: ReadonlySet; executor: WebSearchExecutor; maxSearches?: number; signal?: AbortSignal; platform?: Context.Context; }; export type BufferedLoopOutcome = { kind: "openai"; openai: OpenAiChatResponse; searches: ExecutedSearch[]; events: ServerToolLoopEvent[]; } | { kind: "upstream_error"; response: Response; }; /** * Run the loop over buffered (non-streaming) model steps. `firstStep` is the * already-awaited first model step (the handler surfaces its HTTP errors * before entering the loop). Returns the terminal step's OpenAI payload (with * any un-executable mixed-batch server calls stripped) plus the searches * executed along the way, for the dialect egress to render as native items. */ export declare function runBufferedServerToolLoop(options: ServerToolLoopOptions & { firstStep: Response; }): Effect.Effect; /** * Compose the loop's model steps into one continuous chat SSE stream. * * `firstStep` is the already-awaited first model step (the handler surfaces * its HTTP errors exactly as the single-step path does). Server-tool call * fragments are suppressed from the forwarded stream; each executed search is * injected as a pair of {@link ServerToolMarker} chunks for the dialect * translator. Per-step usage is withheld and re-emitted summed before the * terminal finish chunk, so the client-visible usage covers the whole loop. */ export declare function composeServerToolStream(options: ServerToolLoopOptions & { firstStep: Response; }): ReadableStream;