import type { Message, PromptResult, Result, SmolConfig, StreamChunk } from "smoltalk"; import * as smoltalk from "smoltalk"; import type { ZodType } from "zod"; import type { HostSettings } from "../host/host.js"; export type ToolDefinition = { name: string; description?: string; schema: ZodType; }; export type ToolCall = { id: string; name: string; arguments: Record; }; export type PromptConfig = { messages: Message[]; tools?: ToolDefinition[]; /** Server-side hosted tools to enable for this call, by capability name * (e.g. ["web_search"]). Distinct from `tools` (client functions): hosted * tools run on the provider and return results inline. A client that does * not support hosted tools simply ignores this field. */ hostedTools?: string[]; maxTokens?: number; temperature?: number; responseFormat?: ZodType; thinking?: { enabled: boolean; budgetTokens?: number; }; reasoningEffort?: "low" | "medium" | "high"; logprobs?: { top?: number; }; /** Per-provider key map (same shape as `SmolConfig["apiKey"]`). No bare-string * shorthand — that would route a key to `openAi` regardless of the actual * provider. See `toSmolConfig`. */ apiKey?: SmolConfig["apiKey"]; model?: string; provider?: string; metadata?: Record; /** The module of the Agency code making the call. The runtime sets it. * The deterministic test client picks its mock queue by it; a real * client ignores it, and it is never sent to a provider. */ moduleId?: string; abortSignal?: AbortSignal; hooks?: Partial<{ onStart: (config: PromptConfig) => void; onToolCall: (toolCall: ToolCall) => void; onEnd: (result: PromptResult) => void; onError: (error: Error) => void; }>; }; /** * Embedding config and result. We reuse smoltalk's types directly here * — there's no benefit to a parallel runtime-neutral shape since * the registered client always speaks one provider's protocol anyway, * and smoltalk's type covers the fields every provider cares about * (provider-specific api keys, dimensions, etc.). * * The LLMClient interface uses `Partial` so the * `model` field can be left unset — the client fills in a sensible * default rather than forcing every caller to specify it. */ export type EmbedConfig = smoltalk.EmbedConfig; export type EmbedResult = smoltalk.EmbedResult; export type ImageConfig = smoltalk.ImageConfig; export type ImageGenResult = smoltalk.ImageGenResult; export type ImageInput = smoltalk.ImageInput; export type ImageRef = smoltalk.ImageRef; export type DecisionState = smoltalk.DecisionState; export type DecisionQuestion = smoltalk.DecisionQuestion; export type DecisionAnswer = smoltalk.DecisionAnswer; /** `moduleId` is the module of the Agency code making the call, as on * `PromptConfig`. It is never sent to a provider. */ export type DecideConfig = smoltalk.DecideConfig & { moduleId?: string; }; export type DecideResult = smoltalk.DecideResult; export type TranscriptionResult = smoltalk.TranscriptionResult; export type SpeechResult = smoltalk.SpeechResult; /** A source for audio bytes: a local path, a URL, or inline bytes/base64. */ export type AudioInput = smoltalk.BlobRef; export type TranscribeConfig = Omit & { model: string; }; export type SpeakConfig = Omit & { model: string; voice: string; format: NonNullable; }; /** * Provider-neutral view of an error thrown by an `LLMClient`. Lets agency's * retry classifier decide policy without importing any provider SDK. The * client adapter (which knows its provider's error shapes) populates this. * * All fields except `message` are optional, so a custom client can supply as * little as it knows. Classification (`lib/runtime/llmRetry.ts`) prefers * `kind` when set, then `status`, then falls back to message-matching on * `message`. */ export type NormalizedLLMError = { /** HTTP status, when the error came from an HTTP response. */ status?: number; /** Server-requested retry delay (ms), already parsed (e.g. from * `retry-after` / `retry-after-ms` headers) by the client. */ retryAfterMs?: number; /** Provider classifications the client recognizes. All are provider-neutral; * a custom client can populate any subset. Retryable: `rateLimit`, * `overloaded`, `requestTimeout`. Terminal: `contentPolicy`, * `contextWindow`, `structuredOutput`, `auth`. Undefined for unclassified * errors — agency falls back to `status`, then `message`. */ kind?: "contentPolicy" | "contextWindow" | "structuredOutput" | "requestTimeout" | "rateLimit" | "overloaded" | "auth"; /** Human-readable message (always present). */ message: string; }; /** * The pluggable LLM transport. Agency ships `SmoltalkClient` as the default, * but anything implementing this shape can be swapped in via * `ctx.llmClient`. The only smoltalk-specific surface is the message / chunk * shapes (`Message`, `PromptResult`, `StreamChunk`, `EmbedConfig`, * `EmbedResult`) which we re-export from smoltalk for convenience. * * `normalizeError` is optional. When absent, agency's retry layer falls back * to `{ message: String(err) }` — meaning it can only classify errors by * matching keywords in the message (`ECONNRESET`, `terminated`, etc.). A * client that wants status-based or typed classification should implement * `normalizeError` and populate the `NormalizedLLMError` fields it knows * about. No retry semantics are baked in here — the policy lives entirely * in `lib/runtime/llmRetry.ts`. */ export type LLMClient = { text(config: PromptConfig): Promise>; textStream(config: PromptConfig): AsyncGenerator; /** Generate embeddings for one or more inputs. Returning a failure * Result lets callers (e.g. memory Tier 2) silently skip rather * than crash when a client doesn't support embeddings. */ embed(input: string | string[], config?: Partial): Promise>; /** Generate an image. Optional — clients that don't support it omit the * method; callers (std::image) surface a failure Result. */ image?(input: ImageInput, config?: Partial): Promise>; /** Speech-to-text. Optional — clients that don't support it omit the method; * the std::speech wrapper throws a clear "unsupported client" error. `config` * is COMPLETE (model required — see TranscribeConfig); the client must not * inject its own defaults. `signal` is the sole cancellation channel: once * dispatch starts, an abort must reject promptly with `signal.reason` (a * resolved failure Result means a non-cancellation failure). */ transcribe?(source: AudioInput, config: TranscribeConfig, signal: AbortSignal): Promise>; /** Text-to-speech. Optional — same contract as `transcribe`: complete config * (model/voice/format required), `signal` the sole cancellation channel. */ speak?(text: string, config: SpeakConfig, signal: AbortSignal): Promise>; /** Ask a decision model (Jev, Laya) typed questions about a state. Optional: * a client with no decision model omits it, and a decision call then fails * with a clear message. Same contract as `transcribe`: complete config, and * `signal` is the only cancellation channel. */ decide?(state: DecisionState, questions: Record, config: DecideConfig, signal: AbortSignal): Promise>; /** Translate an error this client threw into provider-neutral fields for * agency's retry classifier. Optional — agency falls back to `{ message }` * when omitted, which still works (message-pattern matching) but loses * status- and `kind`-based classification. */ normalizeError?(err: unknown): NormalizedLLMError; }; /** The variable each provider's API key is read from, by the slot it * fills in smoltalk's `apiKey` map. The same table smoltalk's own * `resolveApiKey` keeps (`lib/util/provider.ts` there); the test in * `llmClient.test.ts` reads that file and checks the two agree. The * runtime reads these through the host so a browser page, where smoltalk's * own `env()` answers nothing, gets its keys from the variables the app * passed to `browserHost`. */ export declare const PROVIDER_KEY_VARIABLES: Record; /** The keys the host's settings hold, as smoltalk's `apiKey` map. A * variable that is unset or empty leaves its slot out, so a key the * caller passed is never shadowed by nothing. */ export declare function apiKeysFromSettings(settings: HostSettings): NonNullable; /** `config` with the run's host's keys underneath its own: a key from * agency.json or the call wins over the host's. Every model call goes * through this with the run's host before it reaches the client, so a * browser page's keys, which smoltalk's own `env()` cannot see, arrive the * same way a Node process's environment does. * * An empty slot in `config.apiKey` counts as unset. The generated header * reads the same variables through the module's default host when the * module loads and bakes `""` for each one it does not find; in a page * that is every slot, since the app's variables arrive later, on the host * it passes to the node. The config is returned as is when the host holds * no key. */ export declare function withHostKeys(settings: HostSettings, config: T): T; export declare class SmoltalkClient implements LLMClient { text(config: PromptConfig): Promise>; textStream(config: PromptConfig): AsyncGenerator; embed(input: string | string[], config?: Partial): Promise>; image(input: ImageInput, config?: Partial): Promise>; transcribe(source: AudioInput, config: TranscribeConfig, signal: AbortSignal): Promise>; speak(text: string, config: SpeakConfig, signal: AbortSignal): Promise>; decide(state: DecisionState, questions: Record, config: DecideConfig, signal: AbortSignal): Promise>; normalizeError(err: unknown): NormalizedLLMError; } /** Convert agency's PromptConfig into smoltalk's SmolConfig. * The nested `apiKey` map (every provider's key) and `baseUrl` arrive via * `...metadata` (the merged smoltalk client config) and MUST be preserved — * replacing `apiKey` wholesale here would clobber non-OpenAI / hosted-provider * keys. A per-call `apiKey` object merges over the metadata map, overriding * only the provider slots it names. */ export declare function toSmolConfig(config: PromptConfig): Omit;