Унифицируйте бизнес-контракт, не скрывая семантику API
Хочется спрятать три разных API за одним "универсальным" интерфейсом. Но полезный общий слой знает ровно о том, что действительно общее: tool calls, результаты, usage и непрозрачный provider state. Он НЕ делает вид, что у всех API одинаковые messages или одинаковый способ продолжить разговор, - именно на этой ложной симметрии ломаются самодельные обёртки.
Контракт умещается в несколько типов.
export type ToolCall = {
id: string;
name: string;
arguments: unknown;
};
export type ToolResult = {
callId: string;
name: string;
output: unknown;
isError: boolean;
};
export type ProviderTurn =
| { kind: "tool_calls"; calls: ToolCall[]; state: unknown; usage?: Usage }
| { kind: "text"; text: string; state: unknown; usage?: Usage };
export interface ProviderAdapter {
start(input: AgentInput): Promise<ProviderTurn>;
resume(state: unknown, results: ToolResult[]): Promise<ProviderTurn>;
}
export type AgentInput = {
message: string;
instructions: string;
tools: ToolDefinition[];
traceId: string;
};Ключевая деталь здесь - state: unknown. Общий runtime хранит объект продолжения, но не заглядывает в него: для OpenAI это может быть previous_response_id, для Anthropic - полный массив Messages, для Google - previous_interaction_id. Разводить, что переносится, а что нет, помогает простое деление. Стабильно - имена tools, JSON-схемы, классы риска, outcome, actor context, eval cases. Адаптируется - форма определения tool, call ids, content blocks, способ продолжения. Не переносится - встроенные provider tools, traces, managed sessions, prompt caching и beta-фичи.
Именно поэтому opaque state - это не лень, а осознанное решение.
Opaque state нужен намеренно. Общий runtime хранит объект продолжения, но не интерпретирует его. Как только вы попытаетесь "понять" чужой state и переложить его на другой провайдер, вы получите тихую несовместимость вместо честной границы.
Контракт задаёт форму. Первое, что в него ложится, - это tools, и хороший tool ближе к API-контракту, чем к "функции для модели".