import { err, ok, type Result } from "../result.ts"; import type { Agent, AssistantMessage, LoweredIR } from "../llm-ir.ts"; import type { LoadedTools, ToolMap } from "../tool-def.ts"; import { JsonFixResponse } from "../../prompts/autofix-prompts.ts"; import { Transport } from "../../transports/transport-common.ts"; export type CompilerIR> = LoweredIR; export type CompilerTokenType = [Tools] extends [undefined] ? "reasoning" | "content" : "reasoning" | "content" | "tool"; export type CompilerModality = "text" | "vision"; export type CompilerModalities = readonly CompilerModality[]; export type CompilerUsage = { input: { cached: number; uncached: number; total: number; }; output: number; }; export function compilerUsage( inputTotal: number, output: number, cached: number = 0, ): CompilerUsage { return { input: { cached, uncached: Math.max(0, inputTotal - cached), total: inputTotal, }, output, }; } export function compilerUsageHasTokens(usage: CompilerUsage): boolean { return usage.input.total !== 0 || usage.output !== 0; } export type CompilerError = | { type: "auth-error"; authError: string; } | { type: "request-error"; requestError: string; curl: string; headers?: Headers; } | { type: "stream-error"; requestError: string; curl: string; usage: CompilerUsage; headers: Headers; } | { type: "payment-error"; requestError: string; curl: string; headers: Headers; } | { type: "rate-limit-error"; requestError: string; curl: string; headers: Headers; } | { type: "unexpected-tool-call"; requestError: string; curl: string; usage: CompilerUsage; headers: Headers; }; export function unexpectedToolCallError( curl: string, usage: CompilerUsage, headers: Headers, ): CompilerError { return { type: "unexpected-tool-call", requestError: "Model returned tool calls even though no tools were provided.", curl, usage, headers, }; } export type AssistantMessageWithoutToolCalls> = Omit< AssistantMessage, "toolCalls" > & { toolCalls?: never; }; type AssistantMessageForTools, Tools> = [Tools] extends [undefined] ? AssistantMessageWithoutToolCalls : AssistantMessage; export type CompilerResultData, Tools> = { output: AssistantMessageForTools; curl: string; headers: Headers; usage: CompilerUsage; }; export type CompilerResult, Tools = unknown> = Result< CompilerResultData, CompilerError >; export type CompilerResultWithoutToolCalls> = CompilerResult< A, undefined >; export type CompilerSuccessData> = { output: AssistantMessage; curl: string; headers: Headers; usage: CompilerUsage; }; type CompilerParamsBase, Model> = { systemPrompt?: () => Promise; model: Model; irs: Array>; abortSignal: AbortSignal; transport: Transport; autofixJson?: (badJson: string, signal: AbortSignal) => Promise; }; export type CompilerParams< A extends Agent, Model, Tools extends Partial> | undefined = undefined, > = CompilerParamsBase & { onTokens: (t: string, type: CompilerTokenType) => any; tools?: Tools; }; export type CompilerParamsWithoutTools, Model> = CompilerParamsBase< A, Model > & { onTokens: (t: string, type: CompilerTokenType) => any; tools?: undefined; }; export type CompilerParamsWithTools< A extends Agent, Model, Tools extends Partial> = Partial>, > = CompilerParamsBase & { onTokens: (t: string, type: "reasoning" | "content" | "tool") => any; tools: Tools; }; export type CompilerParamsImplementation, Model> = | CompilerParamsWithoutTools | CompilerParamsWithTools; const compilerFinished = Symbol("compilerFinished"); type CompilerFinishedData> = CompilerSuccessData & { readonly [compilerFinished]: true; }; type CompilerImplementationResult> = Result< CompilerFinishedData, CompilerError >; export function compilerParamsHaveTools, Model>( params: CompilerParamsImplementation, ): params is CompilerParamsWithTools { return params.tools !== undefined; } export type CompilerImplementationParams, Model> = Omit< CompilerParamsImplementation, "onTokens" > & { onTokens: (t: string, type: "reasoning" | "content" | "tool") => any; finish: (args: { curl: string; headers: Headers; usage: CompilerUsage; abortedOutput: AssistantMessage; parsedOutput: () => AssistantMessage | Promise>; }) => Promise>; }; type CompilerImplementation = >( params: CompilerImplementationParams, ) => Promise>; function compilerSuccess, Model>( params: CompilerParamsImplementation, data: CompilerSuccessData, ): CompilerResult | CompilerResult { if ( !compilerParamsHaveTools(params) && data.output.toolCalls && data.output.toolCalls.length > 0 ) { return err(unexpectedToolCallError(data.curl, data.usage, data.headers)); } if (!compilerParamsHaveTools(params)) { const { toolCalls: _toolCalls, ...output } = data.output; return ok({ ...data, output, }); } return ok(data); } export type Compiler = < A extends Agent, Tools extends Partial> | undefined = undefined, >( params: CompilerParams, ) => Promise>; // defineCompiler keeps the "were tools offered?" bookkeeping local to libocto. Concrete compiler // implementations get a broad token callback that they can call with any provider event, plus a // finish(...) callback that wraps final assistant construction. If a provider emits tool tokens when // the caller did not supply tools, finish(...) returns an unexpected-tool-call error before running // the parsedOutput callback, so compiler implementations do not need to duplicate that guard around // every tool parsing path. finish(...) also skips the parsedOutput callback after aborts, because // compiler parsedOutput callbacks are where expensive or invalid-on-abort tool parsing usually // happens. export function defineCompiler( implementation: CompilerImplementation, ): Compiler { async function compiler>( params: CompilerParamsWithoutTools, ): Promise>; async function compiler>( params: CompilerParamsWithTools, ): Promise>; async function compiler>( params: CompilerParamsImplementation, ): Promise | CompilerResult> { let unexpectedToolCall = false; const onTokens: CompilerImplementationParams["onTokens"] = (tokens, type) => { if (type === "tool") { if (!compilerParamsHaveTools(params)) { unexpectedToolCall = true; return; } if (tokens === "") return; params.onTokens(tokens, type); return; } params.onTokens(tokens, type); }; const finish: CompilerImplementationParams["finish"] = async ({ curl, headers, usage, abortedOutput, parsedOutput, }) => { if (unexpectedToolCall) return err(unexpectedToolCallError(curl, usage, headers)); const assistantMessage = params.abortSignal.aborted ? abortedOutput : await parsedOutput(); return ok({ output: assistantMessage, curl, headers, usage, [compilerFinished]: true, }); }; const compiled = await implementation({ ...params, onTokens, finish, }); if (!compiled.success) return compiled; return compilerSuccess(params, compiled.data); } return compiler; }