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;
}