import { Schema } from "effect" import { JsonSchema, MessageRole, ProviderMetadata } from "./ids" import { CacheHint, CachePolicy, GenerationOptions, HttpOptions, ModelSchema, ProviderOptions } from "./options" import { isRecord } from "../utils/record" const systemPartSchema = Schema.Struct({ type: Schema.Literal("text"), text: Schema.String, cache: Schema.optional(CacheHint), metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), }).annotate({ identifier: "LLM.SystemPart" }) export type SystemPart = Schema.Schema.Type const makeSystemPart = (text: string): SystemPart => ({ type: "text", text }) export const SystemPart = Object.assign(systemPartSchema, { make: makeSystemPart, content: (input?: string | SystemPart | ReadonlyArray) => { if (input === undefined) return [] return typeof input === "string" ? [makeSystemPart(input)] : Array.isArray(input) ? [...input] : [input] }, }) export const TextPart = Schema.Struct({ type: Schema.Literal("text"), text: Schema.String, cache: Schema.optional(CacheHint), metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), providerMetadata: Schema.optional(ProviderMetadata), }).annotate({ identifier: "LLM.Content.Text" }) export type TextPart = Schema.Schema.Type export const MediaPart = Schema.Struct({ type: Schema.Literal("media"), mediaType: Schema.String, data: Schema.Union([Schema.String, Schema.Uint8Array]), filename: Schema.optional(Schema.String), metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), }).annotate({ identifier: "LLM.Content.Media" }) export type MediaPart = Schema.Schema.Type export const ToolTextContent = Schema.Struct({ type: Schema.Literal("text"), text: Schema.String, }).annotate({ identifier: "Tool.TextContent" }) export type ToolTextContent = typeof ToolTextContent.Type export const ToolFileContent = Schema.Struct({ type: Schema.Literal("file"), uri: Schema.String, mime: Schema.String, name: Schema.optional(Schema.String), }).annotate({ identifier: "Tool.FileContent" }) export type ToolFileContent = typeof ToolFileContent.Type /** Ordered, provider-independent content shown to models and UIs after a tool succeeds. */ export const ToolContent = Schema.Union([ToolTextContent, ToolFileContent]).pipe(Schema.toTaggedUnion("type")) export type ToolContent = Schema.Schema.Type const isToolResultValue = (value: unknown): value is ToolResultValue => isRecord(value) && (value.type === "text" || value.type === "json" || value.type === "error" || value.type === "content") && "value" in value export const ToolResultValue = Object.assign( Schema.Union([ Schema.Struct({ type: Schema.Literal("json"), value: Schema.Unknown, }), Schema.Struct({ type: Schema.Literal("text"), value: Schema.Unknown, }), Schema.Struct({ type: Schema.Literal("error"), value: Schema.Unknown, }), Schema.Struct({ type: Schema.Literal("content"), value: Schema.Array(ToolContent), }), ]).annotate({ identifier: "LLM.ToolResult" }), { is: isToolResultValue, make: (value: unknown, type: ToolResultValue["type"] = "json"): ToolResultValue => { if (isToolResultValue(value)) return value if (type === "content") return { type, value: Array.isArray(value) ? value : [] } return { type, value } }, }, ) export type ToolResultValue = Schema.Schema.Type export interface ToolOutput { readonly structured: unknown readonly content: ReadonlyArray } export const ToolOutput = Object.assign( Schema.Struct({ structured: Schema.Unknown, content: Schema.Array(ToolContent), }).annotate({ identifier: "LLM.ToolOutput" }), { make: (structured: unknown, content: ReadonlyArray = []): ToolOutput => ({ structured, content }), fromResultValue: (result: ToolResultValue): ToolOutput | undefined => { switch (result.type) { case "json": return { structured: result.value, content: [] } case "text": return { structured: {}, content: [{ type: "text", text: toolResultText(result.value) }] } case "content": return { structured: {}, content: result.value } case "error": return undefined } }, toResultValue: (output: ToolOutput): ToolResultValue => { if (output.content.length === 0) return { type: "json", value: output.structured } if (output.content.length === 1 && output.content[0]?.type === "text") return { type: "text", value: output.content[0].text } return { type: "content", value: output.content } }, }, ) const toolResultText = (value: unknown) => { if (typeof value === "string") return value try { return JSON.stringify(value) ?? String(value) } catch { return String(value) } } export const ToolCallPart = Object.assign( Schema.Struct({ type: Schema.Literal("tool-call"), id: Schema.String, name: Schema.String, input: Schema.Unknown, providerExecuted: Schema.optional(Schema.Boolean), metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), providerMetadata: Schema.optional(ProviderMetadata), }).annotate({ identifier: "LLM.Content.ToolCall" }), { make: (input: Omit): ToolCallPart => ({ type: "tool-call", ...input }), }, ) export type ToolCallPart = Schema.Schema.Type export const ToolResultPart = Object.assign( Schema.Struct({ type: Schema.Literal("tool-result"), id: Schema.String, name: Schema.String, result: ToolResultValue, providerExecuted: Schema.optional(Schema.Boolean), cache: Schema.optional(CacheHint), metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), providerMetadata: Schema.optional(ProviderMetadata), }).annotate({ identifier: "LLM.Content.ToolResult" }), { make: ( input: Omit & { readonly result: unknown readonly resultType?: ToolResultValue["type"] }, ): ToolResultPart => ({ type: "tool-result", id: input.id, name: input.name, result: ToolResultValue.make(input.result, input.resultType), providerExecuted: input.providerExecuted, cache: input.cache, metadata: input.metadata, providerMetadata: input.providerMetadata, }), }, ) export type ToolResultPart = Schema.Schema.Type export const ReasoningPart = Schema.Struct({ type: Schema.Literal("reasoning"), text: Schema.String, encrypted: Schema.optional(Schema.String), metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), providerMetadata: Schema.optional(ProviderMetadata), }).annotate({ identifier: "LLM.Content.Reasoning" }) export type ReasoningPart = Schema.Schema.Type export const ContentPart = Schema.Union([TextPart, MediaPart, ToolCallPart, ToolResultPart, ReasoningPart]).pipe( Schema.toTaggedUnion("type"), ) export type ContentPart = Schema.Schema.Type export class Message extends Schema.Class("LLM.Message")({ id: Schema.optional(Schema.String), role: MessageRole, content: Schema.Array(ContentPart), metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), native: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), }) {} export namespace Message { export type ContentInput = string | ContentPart | ReadonlyArray export type SystemContentInput = string | TextPart | ReadonlyArray export type Input = Omit[0], "content"> & { readonly content: ContentInput } export const text = (value: string): ContentPart => ({ type: "text", text: value }) export const content = (input: ContentInput) => typeof input === "string" ? [text(input)] : Array.isArray(input) ? [...input] : [input] export const make = (input: Message | Input) => { if (input instanceof Message) return input return new Message({ ...input, content: content(input.content) }) } export const user = (content: ContentInput) => make({ role: "user", content }) export const assistant = (content: ContentInput) => make({ role: "assistant", content }) /** * Add an operator-authored instruction at this chronological point in the * conversation. This is distinct from the initial `LLMRequest.system` * prompt. Keep raw retrieved, tool, and web content out of privileged system * updates; pass that untrusted content through ordinary user/tool channels. */ export const system = (content: SystemContentInput) => make({ role: "system", content }) export const tool = (result: ToolResultPart | Parameters[0]) => make({ role: "tool", content: ["type" in result ? result : ToolResultPart.make(result)] }) } export class ToolDefinition extends Schema.Class("LLM.ToolDefinition")({ name: Schema.String, description: Schema.String, inputSchema: JsonSchema, outputSchema: Schema.optional(JsonSchema), cache: Schema.optional(CacheHint), metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), native: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), }) {} export namespace ToolDefinition { export type Input = ToolDefinition | ConstructorParameters[0] /** Normalize tool definition input into the canonical `ToolDefinition` class. */ export const make = (input: Input) => (input instanceof ToolDefinition ? input : new ToolDefinition(input)) } export class ToolChoice extends Schema.Class("LLM.ToolChoice")({ type: Schema.Literals(["auto", "none", "required", "tool"]), name: Schema.optional(Schema.String), }) {} export namespace ToolChoice { export type Mode = Exclude export type Input = ToolChoice | ConstructorParameters[0] | ToolDefinition | string const isMode = (value: string): value is Mode => value === "auto" || value === "none" || value === "required" /** Select a specific named tool. */ export const named = (value: string) => new ToolChoice({ type: "tool", name: value }) /** Normalize ergonomic tool-choice inputs into the canonical `ToolChoice` class. */ export const make = (input: Input) => { if (input instanceof ToolChoice) return input if (input instanceof ToolDefinition) return named(input.name) if (typeof input === "string") return isMode(input) ? new ToolChoice({ type: input }) : named(input) return new ToolChoice(input) } } export const ResponseFormat = Schema.Union([ Schema.Struct({ type: Schema.Literal("text") }), Schema.Struct({ type: Schema.Literal("json"), schema: JsonSchema }), Schema.Struct({ type: Schema.Literal("tool"), tool: ToolDefinition }), ]).pipe(Schema.toTaggedUnion("type")) export type ResponseFormat = Schema.Schema.Type export class LLMRequest extends Schema.Class("LLM.Request")({ id: Schema.optional(Schema.String), model: ModelSchema, system: Schema.Array(SystemPart), messages: Schema.Array(Message), tools: Schema.Array(ToolDefinition), toolChoice: Schema.optional(ToolChoice), generation: Schema.optional(GenerationOptions), providerOptions: Schema.optional(ProviderOptions), http: Schema.optional(HttpOptions), responseFormat: Schema.optional(ResponseFormat), cache: Schema.optional(CachePolicy), metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), }) {} export namespace LLMRequest { export type Input = ConstructorParameters[0] export const input = (request: LLMRequest): Input => ({ id: request.id, model: request.model, system: request.system, messages: request.messages, tools: request.tools, toolChoice: request.toolChoice, generation: request.generation, providerOptions: request.providerOptions, http: request.http, responseFormat: request.responseFormat, cache: request.cache, metadata: request.metadata, }) export const update = (request: LLMRequest, patch: Partial) => { if (Object.keys(patch).length === 0) return request return new LLMRequest({ ...input(request), ...patch, model: patch.model ?? request.model, }) } }