# Lighter > A lightweight TypeScript AI agent framework. Currently in alpha. --- Source: /docs/ # Lighter Lighter is a lightweight TypeScript framework for building AI agents. It handles the loop between model calls and tool execution so you can build an agent with just a few lines of code. Inspired by [Pi](https://pi.dev/), Lighter keeps its core small while leaving room for customization. Its straightforward API makes it easy to get started, whatever your experience level. Use it to: - **Call models** through a shared interface for OpenAI, Claude, or DeepSeek. - **Run tools** by giving the model access to your own functions for queries, calculations, and other tasks. - **Build interactions** with streaming replies and conversation history maintained by your application. - **Customize behavior** with lifecycle hooks that log requests, modify inputs, or process replies. ## Getting started Follow the [quickstart](/docs/getting-started/quickstart/) to install Lighter, connect a model, and run your first agent. You need Node.js, an API key for a model provider, and an available model ID. ## Core components The smallest Lighter setup is `Provider → Model → Agent`: | Component | Responsibility | | --- | --- | | Provider | Communicates with the model service and converts requests and responses | | Model | Wraps model calls and runs hooks before and after them | | Agent | Organizes messages and runs the loop between the model and tools | Tools are optional. Without tools, the agent finishes after receiving a model reply. When the model requests tools, the agent executes them, returns their results, and calls the model again until it stops requesting tools. ## Documentation | Section | Topics | | --- | --- | | Getting started | [Quickstart](/docs/getting-started/quickstart/) · [Core concepts](/docs/getting-started/concepts/) | | Guides | [Configuration](/docs/guides/configuration/) · [Tools](/docs/guides/tools/) · [Conversation history](/docs/guides/conversations/) · [Streaming](/docs/guides/streaming/) · [Hooks](/docs/guides/hooks/) · [Errors](/docs/guides/errors/) | | Providers | [Built-in providers](/docs/providers/) · [Custom providers](/docs/providers/custom/) | | Advanced | [Use Model directly](/docs/advanced/model/) · [Runtime limits](/docs/advanced/limits/) | | API reference | [Agent](/docs/reference/agent/) · [Model and Provider](/docs/reference/model-and-providers/) · [Messages](/docs/reference/messages/) · [Tool](/docs/reference/tools/) · [Hooks](/docs/reference/hooks/) · [Errors](/docs/reference/errors/) | AI agents can use [llms.txt](/docs/llms.txt) to find documentation on demand, or read the [complete documentation](/docs/llms-full.txt). ## Version and scope This documentation covers **Lighter 0.1.0**, currently in alpha. Breaking API changes are possible. Lighter provides the core loop. Your application handles conversation storage, loop limits, business permissions, and tool isolation. [Start: run your first agent →](/docs/getting-started/quickstart/) --- Source: /docs/getting-started/quickstart/ # Quickstart Connect to a model and get your first reply with a minimal example. This guide uses OpenAI and requires Node.js 22+ and a TypeScript project configured for ESM. ## Install dependencies ```sh npm install @yong_xie/lighter npm install --save-dev tsx ``` These examples use `tsx` to run TypeScript. Skip it if your project already has a TypeScript runner. ## Set your API key In your terminal, set an API key and a model ID available to your account: ```sh export OPENAI_API_KEY="your-api-key" export OPENAI_MODEL="your-model-id" ``` ## Create and run an agent Create `hello.ts`: ```ts import { Agent, Model, OpenAIProvider, UserMessage } from "@yong_xie/lighter"; // Connect to OpenAI with the API key and model ID configured above. const provider = new OpenAIProvider( process.env.OPENAI_API_KEY!, process.env.OPENAI_MODEL!, ); // Wrap the provider in a Model, then create an Agent. const model = new Model(provider); const agent = new Agent({ model }); // Represent user input with UserMessage. run() waits for the run to finish. const result = await agent.run({ messages: [new UserMessage("Name a bookstore on the moon. Return only the name.")], }); if (result.status === "failed") { // Read error on failure and exit with a failure status. console.error("Run failed:", result.error); process.exitCode = 1; } else { // There are no tools here, so the last new message is the assistant reply. const reply = result.messages.at(-1); if (reply?.role === "assistant") { console.log(reply.content); } } ``` `Provider` connects to the model service, `Model` wraps model calls, and `Agent` accepts messages and runs the loop. Call `run()` to get the result. Run it in the same terminal: ```sh node --import tsx hello.ts ``` The output will look something like this. The actual name is generated by the model: ```text Moonlight Books ``` ## Understand the result - `status` is `"completed"` when the run completes. When it is `"failed"`, inspect `error` for the cause. - `messages` contains only messages added during this run, not the input history. - This example has no tools. On success, the last new message is the assistant reply, with its text in `content`. [← Introduction](/docs/) · [Next: core concepts →](/docs/getting-started/concepts/) --- Source: /docs/getting-started/concepts/ # Core concepts Six components cover the main ideas in Lighter: `Provider`, `Model`, `Agent`, `Message`, `Tool`, and `Hook`. ## From model to agent | Component | Responsibility | When to use it | | --- | --- | --- | | Provider | Talks to a model service and converts requests, responses, and errors | Choose OpenAI, Claude, DeepSeek, or connect another service | | Model | Wraps a provider and runs hooks before and after model calls | Create an agent or make a single model call | | Agent | Organizes messages and runs the model–tool loop | Let a model use tools to complete tasks | An Agent uses one Model, and a Model uses one Provider. Built-in providers take an API key and model ID, then are passed to Model: ```ts import { Agent, Model, OpenAIProvider } from "@yong_xie/lighter"; // Compose the three components. No request is sent during construction. const provider = new OpenAIProvider( process.env.OPENAI_API_KEY!, process.env.OPENAI_MODEL!, ); const model = new Model(provider); const agent = new Agent({ model }); ``` ## What happens during a run When you call `agent.run({ messages })`, the agent: 1. Combines its system prompt and the input messages into context. 2. Calls the model to obtain an assistant message. 3. If the message includes tool calls, executes them sequentially, adds the results to context, and calls the model again. 4. If there are no tool calls, ends the loop and returns the messages added during this run. ```mermaid flowchart TD A[System prompt and input messages] --> B[Call the model] B --> C{Tool calls requested?} C -->|Yes| D[Execute tools sequentially] D --> E[Add tool results to context] E --> B C -->|No| F[Return the run result] ``` Without tools, a normal run calls the model once. If the model keeps requesting tools, the loop continues. There is currently no built-in maximum number of rounds. ## Messages, tools, and hooks A **Message** carries context: system instructions, user input, assistant replies, or tool results. Assistant messages can include tool calls, so they are not always text intended for the user. A **Tool** is a function the model can call. You define its name, description, parameter schema, and execution logic. Agent handles the call and passes the result back. A **Hook** runs at the start or end of a run, or before or after a model call. Use hooks to log activity or modify requests and replies. ## Runs and conversations A single `run()` may contain multiple model calls, but it does not automatically remember earlier runs. Your application must save conversation history and pass it into the next request. `run()` and `stream()` share the same core loop. The former waits for the final result; the latter yields incremental text while running, followed by the complete result. [← Quickstart](/docs/getting-started/quickstart/) · [Next: configure an agent →](/docs/guides/configuration/) --- Source: /docs/guides/configuration/ # Configure an agent Only `model` is required when creating an Agent. Add tools, a system prompt, and lifecycle hooks as needed. ## Set a system prompt This example uses the environment variables and runner from the [quickstart](/docs/getting-started/quickstart/): ```ts import { Agent, Model, OpenAIProvider, SystemMessage, UserMessage, } from "@yong_xie/lighter"; const model = new Model(new OpenAIProvider( process.env.OPENAI_API_KEY!, process.env.OPENAI_MODEL!, )); const agent = new Agent({ model, // Set the role, task scope, and response style with SystemMessage. systemPrompt: new SystemMessage("You are a bookstore advisor. Answer questions briefly in English."), }); const result = await agent.run({ // Put the question for each run in messages. messages: [new UserMessage("Suggest three events for a bookstore on the moon.")], }); console.log(result); ``` If you omit `systemPrompt`, the default is `You are a helpful assistant.` A custom value replaces this default. Agent automatically prepends the system prompt to the context for each run. You do not need to add it again when maintaining conversation history. ## Where configuration belongs | Setting | Where to configure it | | --- | --- | | API key, model ID, provider-specific options | Provider constructor | | Model, tools, system prompt, hooks | `new Agent({ ... })` | | User messages and existing conversation history | `run({ messages })` or `stream({ messages })` | You cannot currently override the model or tools in an individual Agent request. Create another Agent when you need a different configuration. ## Extend as needed - Use [Tool](/docs/guides/tools/) to let an agent run your functions. - Use [Hook](/docs/guides/hooks/) to observe or modify a run. - Adjust output length, reasoning, and other options in the [provider configuration](/docs/providers/). See [Agent API](/docs/reference/agent/) for all parameters. [← Core concepts](/docs/getting-started/concepts/) · [Next: add tools →](/docs/guides/tools/) --- Source: /docs/guides/tools/ # Add tools Tools let the model call functions you define. You describe the tool and implement its logic; Agent validates arguments, runs the function, and returns the result to the model. ## Define and register a tool Install Zod to define parameter schemas: ```sh npm install zod@4 ``` This example uses the environment variables and runner from the [quickstart](/docs/getting-started/quickstart/): ```ts import { Agent, Model, OpenAIProvider, Tool, UserMessage } from "@yong_xie/lighter"; import * as z from "zod"; const model = new Model(new OpenAIProvider( process.env.OPENAI_API_KEY!, process.env.OPENAI_MODEL!, )); const add = new Tool({ name: "add", description: "Calculate the sum of two numbers.", // The schema is sent to the model and used for local validation before execution. parametersZodSchema: z.strictObject({ a: z.number(), b: z.number() }), // The TypeScript types of a and b are inferred from the schema. func: ({ a, b }) => a + b, }); const agent = new Agent({ model, tools: [add] }); const result = await agent.run({ messages: [new UserMessage("Use the add tool to calculate 19 + 23.")], }); // Inspect tool calls, tool results, and the subsequent reply. console.log(result); ``` When the model requests `add`, Agent executes the function, obtains `42`, and sends the result back so the model can generate a reply. The model decides whether to call a tool. ## Asynchronous tools `func` can also be asynchronous. This simulated lookup does not access real inventory: ```ts import { Tool } from "@yong_xie/lighter"; import * as z from "zod"; const getStock = new Tool({ name: "get_stock", description: "Look up stock for the specified book.", parametersZodSchema: z.strictObject({ bookId: z.string() }), func: async ({ bookId }) => { // Replace this with a database query or external API request in your app. const stock = await Promise.resolve({ bookId, available: 12 }); return stock; }, }); ``` Add `getStock` to the Agent's `tools` to make it available. Multiple tool calls are awaited sequentially; they do not currently run in parallel. ## Arguments, results, and failures - Names should be unique. Descriptions should explain when and why to use the tool. - Parameters must use a Zod object schema. After validation, `func` receives the value parsed by Zod. - Return JSON-serializable values. Successful results are converted to `toolOutput` using `JSON.stringify()`, so string results include JSON quotation marks. - If a tool is missing, argument JSON is invalid, validation fails, or the function throws, Agent sends the model a tool message with `isError: true` and continues the loop. A tool failure does not necessarily fail the whole run. The model may retry with different arguments or reply directly. Tools execute with application permissions, so implement business permission checks inside them. See [Tool API](/docs/reference/tools/) for all parameters and methods. [← Configure an agent](/docs/guides/configuration/) · [Next: conversation history →](/docs/guides/conversations/) --- Source: /docs/guides/conversations/ # Manage messages and conversation history Lighter does not automatically save conversations. Pass existing history into each run, then append the new messages from its result to continue the conversation. ## Continue a conversation This example uses the environment variables and runner from the [quickstart](/docs/getting-started/quickstart/): ```ts import { Agent, Model, OpenAIProvider, UserMessage, type BaseMessages, } from "@yong_xie/lighter"; const model = new Model(new OpenAIProvider( process.env.OPENAI_API_KEY!, process.env.OPENAI_MODEL!, )); const agent = new Agent({ model }); const history: BaseMessages = [new UserMessage("Name a bookstore on the moon.")]; const first = await agent.run({ messages: history }); if (first.status === "failed") throw first.error; // The result contains only new messages. Append them to preserve context. history.push(...first.messages); history.push(new UserMessage("Give me three more names in a similar style.")); const second = await agent.run({ messages: history }); if (second.status === "failed") throw second.error; history.push(...second.messages); console.log(second.messages); ``` Agent does not append to the input array. Saving only the final reply loses tool calls and results, so preserve all new messages when tools are involved. ## Read messages by role | `role` | Message type | Main fields | | --- | --- | --- | | `system` | `SystemMessage` | System instructions in `content` | | `user` | `UserMessage` | User input in `content` | | `assistant` | `AssistantMessage` | Reply `content`, `toolCalls`, and optional `reasoning` | | `tool` | `ToolMessage` | `toolOutput`, call identifier `callId`, and error flag `isError` | Tool calls may have no assistant text. Check `role` before reading a message. See [Messages API](/docs/reference/messages/) for the full field definitions. ## Multiple conversations One Agent can handle independent runs, but each conversation should have its own history array. Your application must still coordinate concurrent access to shared state in tools, hooks, or application code. ## Persistence and recovery boundaries Your application can save history to a database. Messages returned by OpenAI and Claude may also carry native context required by subsequent requests. Ordinary JSON serialization, cloning, or recreating messages does not preserve all of that information. For tool round trips that depend on reasoning context, prefer the original message instances returned by the provider. Persistent recovery needs additional application design: saving `content` and `reasoning` alone does not guarantee that native context can be restored. [← Add tools](/docs/guides/tools/) · [Next: streaming →](/docs/guides/streaming/) --- Source: /docs/guides/streaming/ # Stream responses Use `stream()` instead of `run()` to receive text as the model generates it. Both methods execute the same tool loop. ## Receive text and the final result This example uses the environment variables and runner from the [quickstart](/docs/getting-started/quickstart/): ```ts import { Agent, Model, OpenAIProvider, UserMessage } from "@yong_xie/lighter"; const model = new Model(new OpenAIProvider( process.env.OPENAI_API_KEY!, process.env.OPENAI_MODEL!, )); const agent = new Agent({ model }); for await (const event of agent.stream({ messages: [new UserMessage("Describe a bookstore on the moon in three sentences.")], })) { // The final result has status, unlike incremental events. if ("status" in event) { process.stdout.write("\n"); if (event.status === "failed") { console.error("Run failed:", event.error); process.exitCode = 1; } // Use complete messages from the final result when saving history. console.log(event.messages); } else if (event.type === "text_delta") { process.stdout.write(event.text); } } ``` ## Events you receive | Content | Shape | Purpose | | --- | --- | --- | | Text delta | `{ type: "text_delta", text }` | Display the reply incrementally | | Reasoning delta | `{ type: "reasoning_delta", text }` | Display reasoning content or summaries provided by the provider; not always available | | Final result | `{ status, messages, error? }` | Check completion and read complete messages or the failure cause | The final result is yielded, so a `for await` loop receives it when the stream is consumed to completion. Agent consumes the provider's `message` event internally rather than forwarding it as a separate event. With tools, text deltas may come from different model rounds. Tool messages and complete assistant messages are all included in the final result. Do not append the final reply text again to text you have already displayed incrementally. ## Failures and early stopping If the stream is interrupted, Agent produces a failed result that preserves completed messages from the run. Partial text that has not become a complete message is not included in `messages`. Use `break` to stop consuming the stream. This closes the iterator and prevents subsequent tool and model calls. You will not receive a final result after stopping early, and this does not cancel tools already running. The `afterModel` hook processes the complete assistant message. If it modifies the reply, the final message may differ from the deltas already displayed. [← Conversation history](/docs/guides/conversations/) · [Next: lifecycle hooks →](/docs/guides/hooks/) --- Source: /docs/guides/hooks/ # Use lifecycle hooks Hooks let you read or modify requests and results at specific points. Use them for logging, input preprocessing, and response checks. ## Log model requests This example uses the environment variables and runner from the [quickstart](/docs/getting-started/quickstart/): ```ts import { Agent, BeforeModelHook, Model, OpenAIProvider, UserMessage, } from "@yong_xie/lighter"; const model = new Model(new OpenAIProvider( process.env.OPENAI_API_KEY!, process.env.OPENAI_MODEL!, )); const logRequest = new BeforeModelHook({ hookName: "log-model-request", hookFunction: (request) => { // Runs before every model call, including rounds after tool results. console.log("Message count:", request.messages.length); // Return nothing to keep the current request. }, }); const agent = new Agent({ model, hooks: { beforeModel: logRequest } }); const result = await agent.run({ messages: [new UserMessage("Hello!")] }); console.log(result); ``` ## When hooks run | Configuration | Hook class | Input | Effect of a replacement value | | --- | --- | --- | --- | | `beforeAgent` | `BeforeAgentHook` | `AgentRequest` | Replaces input messages for this run | | `beforeModel` | `BeforeModelHook` | `ModelRequest` | Replaces this model request, including messages and tool definitions | | `afterModel` | `AfterModelHook` | `AssistantMessage` | Replaces the assistant message used for context and tool execution | | `afterAgent` | `AfterAgentHook` | `BaseMessages` | Replaces the final array of new messages | A run calls `beforeAgent` first. Every model round executes `beforeModel → Provider → afterModel`. After the loop completes successfully, `afterAgent` runs. The request passed to `beforeAgent` does not yet include Agent's system prompt. Context passed to `beforeModel` already includes it. ## Modify a request Replace `logRequest` from the earlier example with this hook: ```ts import { BeforeModelHook, SystemMessage } from "@yong_xie/lighter"; const requireEnglish = new BeforeModelHook({ hookName: "require-english", hookFunction: (request) => ({ ...request, // Return a new request with an extra instruction for this model call only. messages: [...request.messages, new SystemMessage("Please reply in English.")], }), }); ``` All hooks support synchronous or asynchronous functions. Returning `undefined` keeps the current value; returning a new value replaces it. Each configuration field accepts one hook instance. Compose multiple operations within the callback when needed. ## Things to keep in mind - A hook exception makes Agent return a failed result. When calling Model directly, model-hook exceptions propagate to the caller. - Currently, `afterAgent` does not run when a model call fails or the stream is stopped early. Put essential resource cleanup in your application's `finally` block. - Modifying tool definitions in `beforeModel` does not register new executable tools with Agent. - Separate request arrays do not imply deep copies of message objects. Prefer returning new arrays over mutating shared messages. - Original OpenAI and Claude assistant messages carry native context. Replacing, cloning, or modifying them may lose reasoning replay information. Preserve original messages when you only need to log them. See [Hooks API](/docs/reference/hooks/) for complete callback types. [← Streaming](/docs/guides/streaming/) · [Next: error handling →](/docs/guides/errors/) --- Source: /docs/guides/errors/ # Handle errors Agent uses `status` to indicate whether a run completed. When a model or hook fails, check the result before reading `error`. ## Check the run result This example uses the environment variables and runner from the [quickstart](/docs/getting-started/quickstart/): ```ts import { Agent, Model, ModelError, OpenAIProvider, UserMessage } from "@yong_xie/lighter"; const agent = new Agent({ model: new Model(new OpenAIProvider( process.env.OPENAI_API_KEY!, process.env.OPENAI_MODEL!, )), }); const result = await agent.run({ messages: [new UserMessage("Hello!")] }); if (result.status === "failed") { // error is unknown. Narrow its type before reading error fields. if (result.error instanceof ModelError) { console.error(result.error.code, result.error.message); } else { console.error("Run failed:", result.error); } // Completed messages may remain here if tools have already run. console.log(result.messages); process.exitCode = 1; } else { console.log(result.messages); } ``` Built-in providers classify recognized SDK errors as `ModelError`. Other values thrown by applications or hooks are preserved as-is. They are not guaranteed to be `Error` instances and may even be `undefined`. ## Tool failures and run failures When a tool is missing, arguments are invalid, or a tool function throws, Agent creates a `ToolMessage` with `isError: true` and lets the model handle it. This does not directly change the run status to `failed`. Therefore, `completed` means the loop ended normally. It does not guarantee that every tool succeeded or that the answer is correct. ## Preserve completed progress If the model fails in a later round, the result's `messages` still contains completed assistant and tool messages from this run. Your application can save them before deciding whether to continue. Keep complete tool-call/result pairs when continuing. Do not rerun the entire task solely because its status is failed: doing so may repeat tools that have already produced side effects. ## Troubleshooting | Situation | What to check | | --- | --- | | `auth_error` | API key and account permissions | | `invalid_request` | Model ID, parameters, messages, and tool definitions | | `rate_limit` | Request rate, concurrency, and rate-limit details returned by the service | | `network_error` | Network, proxy, and service connectivity | | `bad_response` | Incomplete generation, stream interruption, or unsupported responses | | Module loading failure | ESM configuration and the TypeScript runner | Lighter has no independent retry policy. The underlying SDK may implement its own retries. See [Errors API](/docs/reference/errors/) for all error codes. Calling `Model` or a Provider directly throws errors. Invalid provider configuration may also throw during construction. Handle these cases with `try/catch` in your application. [← Lifecycle hooks](/docs/guides/hooks/) · [Next: choose a provider →](/docs/providers/) --- Source: /docs/providers/ # Choose and configure a provider Providers connect to model services. After switching providers, you still use the same message and tool interfaces through `Model` and `Agent`. ## Built-in providers | Class | `name` | Current integration | Optional configuration | | --- | --- | --- | --- | | `OpenAIProvider` | `"openai"` | OpenAI Responses API | `reasoning`, `maxOutputTokens` | | `ClaudeProvider` | `"claude"` | Anthropic Messages API | `thinking`, `maxOutputTokens` | | `DeepSeekProvider` | `"deepseek"` | DeepSeek Responses interface through the OpenAI SDK | No third configuration argument | The following examples create a Model only. Pass your chosen `model` to `new Agent({ model })` to use it in an agent. ## OpenAI ```ts import { Model, OpenAIProvider } from "@yong_xie/lighter"; const model = new Model(new OpenAIProvider( process.env.OPENAI_API_KEY!, process.env.OPENAI_MODEL!, // Optional: limit the output length of this model call. { maxOutputTokens: 2048 }, )); ``` `maxOutputTokens` maps to `max_output_tokens`. If omitted, the service decides the limit. The provider always sends `store: false`. `reasoning` uses the underlying SDK's configuration type. To request a reasoning summary, set `{ reasoning: { summary: "auto" } }` with a model that supports it. The adapter extracts `reasoning` and `reasoning_delta` from summaries; reasoning content may not be available when a summary is not configured. ## Claude Set `ANTHROPIC_API_KEY` and `ANTHROPIC_MODEL`, then create the provider: ```ts import { ClaudeProvider, Model } from "@yong_xie/lighter"; const model = new Model(new ClaudeProvider( process.env.ANTHROPIC_API_KEY!, process.env.ANTHROPIC_MODEL!, { maxOutputTokens: 8192, // Use only with models that support this configuration; the budget must be below the output limit. thinking: { type: "enabled", budget_tokens: 2048 }, }, )); ``` `maxOutputTokens` defaults to `8192` and must be a positive safe integer. When explicitly enabling `thinking`, `budget_tokens` must be a safe integer of at least `1024` and less than the output limit. Otherwise, the constructor immediately throws `ModelError`. Omit `thinking` when you do not need it. Option availability depends on the selected model. ## DeepSeek Set `DEEPSEEK_API_KEY` first: ```ts import { DeepSeekProvider, Model } from "@yong_xie/lighter"; const model = new Model(new DeepSeekProvider( process.env.DEEPSEEK_API_KEY!, // The current constructor type accepts this ID and deepseek-v4-pro. "deepseek-flash", )); ``` The current implementation fixes the base URL to `https://api.deepseek.com` and calls the SDK's `responses.create()`. The constructor's model ID type is limited to `"deepseek-flash" | "deepseek-v4-pro"`. These details describe the current adapter and type constraints. They do not establish compatibility for every account or live service request. Built-in provider tests use mocked HTTP rather than real model services. ## History and reasoning context OpenAI and Claude preserve native response information on returned messages for replay in later requests. This path is available only while the original messages and their relevant contents remain unchanged. Cloning, replacing, reconstructing serialized messages, or modifying assistant messages causes requests to be rebuilt from public fields instead, which may lose native reasoning context. In particular, Claude's signed thinking blocks cannot be reconstructed from `reasoning` text, and subsequent tool-result requests may fail. The shared interface remains available when switching providers, but provider-specific context in existing history is not guaranteed to work across services. See [Model and Provider API](/docs/reference/model-and-providers/) for complete signatures. To connect another service, see [custom providers](/docs/providers/custom/). [← Error handling](/docs/guides/errors/) · [Next: custom providers →](/docs/providers/custom/) --- Source: /docs/providers/custom/ # Custom providers Implement the `Provider` interface and pass it to `new Model(provider)` to connect another model service. Agent does not need to know the service's specific API. ## Minimal implementation This `EchoProvider` repeats the latest user message without making a network request. Save it as `echo.ts` and run it using the [quickstart](/docs/getting-started/quickstart/) instructions. No API key is needed. ```ts import { Agent, AssistantMessage, Model, ModelError, UserMessage, type Provider, type ProviderRequest, type ProviderStreamEvent, } from "@yong_xie/lighter"; export class EchoProvider implements Provider { readonly name = "echo"; async invoke(request: ProviderRequest): Promise { // This example supports text only. Reject unsupported capabilities explicitly. if (request.toolSpecs?.length) { throw new ModelError("EchoProvider does not support tools.", "invalid_request"); } const lastUser = [...request.messages].reverse() .find((message) => message.role === "user"); return new AssistantMessage(`Received: ${lastUser?.content ?? ""}`); } async *stream(request: ProviderRequest): AsyncGenerator { const message = await this.invoke(request); // Simulate a text delta. A real provider should forward text as it arrives. yield { type: "text_delta", text: message.content }; // Yield the complete assistant message last so Agent can advance the loop. yield { type: "message", message }; } } const agent = new Agent({ model: new Model(new EchoProvider()) }); const result = await agent.run({ messages: [new UserMessage("The moon bookstore is open!")] }); console.log(result.messages); ``` ## Connect a real service | Part | What to implement | | --- | --- | | `name` | Provide a stable service identifier | | `invoke()` | Convert input messages and tool definitions, then return a complete `AssistantMessage` | | `stream()` | Forward text and optional reasoning deltas, then yield one complete `message` event | | Tool calls | Convert service calls to `callId`, `toolName`, and JSON-string `arguments` | | Tool results | Convert `ToolMessage` to the matching call result, preserving the call ID and error information | | Errors | Detect failed or incomplete responses and throw an appropriate `ModelError` | Providers handle communication and format conversion only. Agent executes tools. Do not store mutable history for an individual conversation in a shared Provider. ## Stream completion rules `text_delta` and `reasoning_delta` are incremental events, not substitutes for the final `message`. If a stream ends without a complete message, Agent produces a `bad_response` failure. Calling Model's stream directly does not add this validation. Custom providers should therefore also check whether the service ended normally rather than treating a dropped connection as success. ## Validate your integration At a minimum, test ordinary replies, tool-call/result round trips, streaming deltas and completion events, service failures, and interrupted streams. When supporting tools, also check that IDs and arguments remain complete when a response contains multiple tool calls. See [Model and Provider API](/docs/reference/model-and-providers/) for interface signatures. [← Built-in providers](/docs/providers/) · [Next: use Model directly →](/docs/advanced/model/) --- Source: /docs/advanced/model/ # Use Model directly Use `Model` directly when you need a single model call or want to control tool execution yourself. ## Get a complete reply This example uses the environment variables and runner from the [quickstart](/docs/getting-started/quickstart/): ```ts import { Model, OpenAIProvider, SystemMessage, UserMessage } from "@yong_xie/lighter"; const model = new Model(new OpenAIProvider( process.env.OPENAI_API_KEY!, process.env.OPENAI_MODEL!, )); try { const message = await model.invoke({ // Model does not add a system prompt; include one when needed. messages: [ new SystemMessage("You are a bookstore advisor. Keep your answers brief."), new UserMessage("Name a bookstore on the moon."), ], }); console.log(message.content); } catch (error) { // Model throws errors directly instead of returning an AgentResponse with status. console.error("Model call failed:", error); process.exitCode = 1; } ``` `invoke()` returns one `AssistantMessage`. If the model requests tools, the calls are in `toolCalls`. Model does not execute tools or start another round automatically. ## Stream a reply ```ts import { Model, OpenAIProvider, UserMessage } from "@yong_xie/lighter"; const model = new Model(new OpenAIProvider( process.env.OPENAI_API_KEY!, process.env.OPENAI_MODEL!, )); try { for await (const event of model.stream({ messages: [new UserMessage("Hello!")] })) { if (event.type === "text_delta") { process.stdout.write(event.text); } else if (event.type === "message") { // Unlike Agent, Model yields the complete message event directly. console.log("\nComplete message:", event.message); } } } catch (error) { console.error("Streaming failed:", error); process.exitCode = 1; } ``` Model streams `text_delta`, `reasoning_delta`, and `message` events. It does not yield a final `AgentResponse`. ## Tools and hooks Pass `tool.getSpec()` in the request's `toolSpecs` to describe tools to the model. After receiving a call, your application handles parameter parsing, execution, and the next request. Use Agent to automate these steps. The second argument of `invoke(request, hooks?)` and `stream(request, hooks?)` accepts `beforeModel` and `afterModel`, just like [Agent's model hooks](/docs/guides/hooks/). Calling a Provider directly bypasses these hooks. [← Custom providers](/docs/providers/custom/) · [Next: runtime limits →](/docs/advanced/limits/) --- Source: /docs/advanced/limits/ # Runtime limits and application responsibilities Lighter handles the core loop between the model and tools. Your application adds storage, execution limits, permissions, and monitoring as needed. ## Current boundaries | Requirement | Current behavior | | --- | --- | | Conversation persistence and history trimming | The application maintains and supplies history | | Maximum number of model calls | No built-in limit; use application logic or hooks | | Request timeouts and cancellation | No extra unified control interface; behavior depends on the underlying SDK | | Automatic retries | No Lighter-specific retry policy; the underlying SDK may retry | | Parallel tool execution | Tools are currently awaited sequentially | | Tool isolation and permissions | Tools run with application permissions; no built-in sandbox | | Logging, tracing, and usage statistics | No built-in monitoring panel or unified usage field; integrate through hooks | ## Limit model calls The following application-level limit creates a separate counter on every function call, so requests do not interfere with one another: ```ts import { Agent, BeforeModelHook, Model, OpenAIProvider, type BaseMessages, type Tool, } from "@yong_xie/lighter"; const model = new Model(new OpenAIProvider( process.env.OPENAI_API_KEY!, process.env.OPENAI_MODEL!, )); export async function runWithLimit(messages: BaseMessages, tools: Tool[] = []) { let calls = 0; const beforeModel = new BeforeModelHook({ hookName: "limit-model-calls", hookFunction: () => { // Throw before the ninth call; Agent marks this run as failed. if (++calls > 8) throw new Error("The limit of 8 model calls has been reached."); }, }); const agent = new Agent({ model, tools, hooks: { beforeModel } }); return agent.run({ messages }); } ``` This limits only the number of model calls, not the duration of a request or tool execution. Reaching the limit does not undo side effects from tools that have already run. ## Tool permissions and side effects Zod validates parameter shape only. Before accessing databases, files, or external services, tools must also check the current user's business permissions. Run tools in a separate process or service when isolation is required. For side effects such as payments or sending messages, your application designs confirmation flows, idempotency, and audit records. ## Concurrency, stopping, and recovery Each Agent run has its own message array, but tools, hooks, and application state may be shared. Your application must coordinate access to shared counters, database connections, and mutable data. Stopping stream consumption prevents subsequent work but does not guarantee cancellation of a tool already running. Failed results preserve completed messages, but do not provide crash recovery or exactly-once side effects. Persistence also needs to account for provider-native message context. See [conversation history](/docs/guides/conversations/). [← Use Model directly](/docs/advanced/model/) · [Next: Agent API →](/docs/reference/agent/) --- Source: /docs/reference/agent/ # Agent API This page covers Lighter 0.1.0. For usage, see [configure an agent](/docs/guides/configuration/) and [streaming](/docs/guides/streaming/). ## Constructor `new Agent(config: AgentConfig)` | Setting | Type | Required | Default | | --- | --- | --- | --- | | `model` | `Model` | Yes | None | | `tools` | `Tool[]` | No | `[]` | | `systemPrompt` | `SystemMessage` | No | `new SystemMessage("You are a helpful assistant.")` | | `hooks` | Hook configuration object, shown below | No | `{}` | | `hooks` field | Type | | --- | --- | | `beforeAgent` | `BeforeAgentHook` | | `beforeModel` | `BeforeModelHook` | | `afterModel` | `AfterModelHook` | | `afterAgent` | `AfterAgentHook` | All four hook fields are optional. Each accepts one instance. The constructor only stores configuration; it does not send model requests. ## `run()` ```text run(request: AgentRequest): Promise ``` `AgentRequest` has one required field: `messages: BaseMessages`. Agent combines its system prompt and these messages into context, then runs the loop. The public `AgentResponse` type is: ```ts import type { BaseMessages } from "@yong_xie/lighter"; type AgentResponse = { status: "completed" | "failed"; messages: BaseMessages; error?: unknown; }; ``` | Field | Behavior | | --- | --- | | `status` | `completed` when the loop and final hook succeed; `failed` when a model or hook exception is caught | | `messages` | New messages only, excluding input history and the automatically prepended system prompt; `afterAgent` can replace this array | | `error` | The original caught value on failure, typed as `unknown`; it may also be `undefined` | This type is not a discriminated union with status-specific fields. Even after checking `status`, narrow the type of `error` before using it. Agent does not append to the input message array, but message objects are not deep-copied. Tool errors are returned to the model through `ToolMessage` and do not directly set the run status to `failed`. ## `stream()` ```text stream(request: AgentRequest): AsyncGenerator ``` Execution starts only when the asynchronous iterator is consumed. When consumed normally to completion, it yields incremental events followed by one `AgentResponse`. The public type is `AgentStreamEvent = ProviderStreamEvent | AgentResponse`. Although `ProviderStreamEvent` includes a `message` branch, Agent currently consumes that branch internally. The events actually exposed are: - `{ type: "text_delta", text: string }` - `{ type: "reasoning_delta", text: string }`, depending on provider and model configuration. - `{ status, messages, error? }`, the final result of this run. Use `"status" in event` to identify the final result. Complete assistant and tool messages are included in that result. Partial incremental text is not saved as a complete message. Stopping iteration early provides no final result and does not guarantee cancellation of tools already running. ## Minimal example ```ts import { Agent, Model, OpenAIProvider, UserMessage } from "@yong_xie/lighter"; const agent = new Agent({ model: new Model(new OpenAIProvider( process.env.OPENAI_API_KEY!, process.env.OPENAI_MODEL!, )), }); const result = await agent.run({ messages: [new UserMessage("Hello!")] }); // Check the status before using the new messages. if (result.status === "completed") console.log(result.messages); else console.error(result.error); ``` [← Runtime limits](/docs/advanced/limits/) · [Next: Model and Provider API →](/docs/reference/model-and-providers/) --- Source: /docs/reference/model-and-providers/ # Model and Provider API For usage, see [use Model directly](/docs/advanced/model/), [built-in providers](/docs/providers/), and [custom providers](/docs/providers/custom/). ## Model `new Model(provider: Provider)` | Member | Type or signature | Description | | --- | --- | --- | | `provider` | `readonly Provider` | The Provider passed to the constructor | | `invoke()` | `invoke(request: ModelRequest, hooks?): Promise` | One call returning a complete assistant message | | `stream()` | `stream(request: ModelRequest, hooks?): AsyncGenerator` | Forwards provider streaming events | `hooks` is an optional object with `beforeModel?: BeforeModelHook` and `afterModel?: AfterModelHook`. Both methods await the relevant hooks. Streaming `afterModel` processes complete `message` events only. Model does not add a default system prompt, execute tools, maintain a loop, or wrap exceptions in Agent results. Provider or hook exceptions propagate to the caller. ## Request types `ModelRequest` is a type alias for `ProviderRequest`: | Field | Type | Required | Description | | --- | --- | --- | --- | | `messages` | `BaseMessages` | Yes | Complete context for this request | | `toolSpecs` | `ToolSpec[]` | No | Tool definitions for the model, without execution functions | ## Provider interface ```ts import type { AssistantMessage, ProviderRequest, ProviderStreamEvent, } from "@yong_xie/lighter"; interface Provider { readonly name: string; invoke(request: ProviderRequest): Promise; stream(request: ProviderRequest): AsyncGenerator; } ``` Calling Provider methods bypasses Model hooks. The provider is responsible for detecting incomplete or failed responses and throwing errors. ## ProviderStreamEvent | `type` | Other fields | Meaning | | --- | --- | --- | | `text_delta` | `text: string` | Text increment | | `reasoning_delta` | `text: string` | Reasoning or summary increment, depending on capabilities and configuration | | `message` | `message: AssistantMessage` | Complete assistant message with full text and tool calls | A provider stream should yield one complete `message` event before ending normally. Agent uses it to advance the loop. Model forwards it without additional validation for missing messages. ## OpenAIProvider ```text new OpenAIProvider(apiKey, modelID, options?: OpenAIProviderOptions) ``` | Parameter | Type | Description | | --- | --- | --- | | `apiKey` | `string` | API key | | `modelID` | Non-nullable OpenAI SDK `ResponseCreateParamsNonStreaming["model"]` | Model ID | | `options.reasoning` | OpenAI SDK `ResponseCreateParamsNonStreaming["reasoning"]` | Optional reasoning configuration | | `options.maxOutputTokens` | `number` | Optional; maps to `max_output_tokens` | `options` defaults to `{}` and `name` is `"openai"`. This provider uses the Responses API and always sends `store: false`. Reasoning text comes from summaries. See the [provider guide](/docs/providers/) for native context preservation rules. ## ClaudeProvider ```text new ClaudeProvider(apiKey: string, modelID: string, options?: ClaudeProviderOptions) ``` | Option | Type | Default or constraint | | --- | --- | --- | | `maxOutputTokens` | `number` | Defaults to `8192`; must be a positive safe integer | | `thinking` | Anthropic SDK `MessageCreateParamsNonStreaming["thinking"]` | Omitted by default; availability depends on the model | `options` defaults to `{}` and `name` is `"claude"`. When `thinking.type` is `"enabled"`, `budget_tokens` must be a safe integer of at least `1024` and less than `maxOutputTokens`. Otherwise, construction throws `ModelError("...", "invalid_request")`. System messages are merged into the request's `system` field. The accepted stop reasons are currently `end_turn`, `tool_use`, `stop_sequence`, and `refusal`. Other reasons, including output-limit exhaustion and `pause_turn`, throw `bad_response` and are preserved in `responseStatus`. ## DeepSeekProvider ```text new DeepSeekProvider(apiKey: string, modelID: "deepseek-flash" | "deepseek-v4-pro") ``` `name` is `"deepseek"`. The base URL is fixed to `https://api.deepseek.com`. This provider uses the OpenAI SDK's Responses interface and exposes no third options argument. The model ID type describes values accepted by the current code. It does not establish compatibility with the live service. All three built-in providers implement the same `invoke()` and `stream()` interface. `OpenAIProviderOptions` and `ClaudeProviderOptions` are public exported types. Internal model-ID type aliases are not exported separately. [← Agent API](/docs/reference/agent/) · [Next: Messages API →](/docs/reference/messages/) --- Source: /docs/reference/messages/ # Messages API Messages use `role` to identify their purpose. For usage, see [conversation history](/docs/guides/conversations/). ## SystemMessage and UserMessage ```text new SystemMessage(content: string) new UserMessage(content: string) ``` | Class | `role` | Other fields | | --- | --- | --- | | `SystemMessage` | `"system"` | `content: string` | | `UserMessage` | `"user"` | `content: string` | Both `role` and `content` are readonly properties. ## AssistantMessage ```text new AssistantMessage(content: string, options?: { reasoning?: string; toolCalls?: ToolCall[]; }) ``` | Field | Type | Default or description | | --- | --- | --- | | `role` | `"assistant"` | Fixed role | | `content` | `string` | Reply text; may be empty for tool calls | | `reasoning` | `string \| undefined` | Optional reasoning content or summary supplied by the provider | | `toolCalls` | `ToolCall[]` | Defaults to `[]`; tool calls requested by the model | These properties are declared readonly, but that does not deeply freeze arrays or their objects. Original messages may also carry provider-specific context. Avoid modifying or cloning them casually. ## ToolCall | Field | Type | Description | | --- | --- | --- | | `callId` | `string` | Identifies the call and matches it to a tool result | | `toolName` | `string` | Tool name | | `arguments` | `string` | JSON string containing arguments that have not yet been parsed or validated | All three fields are readonly. `arguments` is not an argument object: Agent first calls `JSON.parse()`, then passes the result to the tool for validation. ## ToolMessage ```text new ToolMessage({ callId: string, toolName: string, toolOutput: string, isError?: boolean, }) ``` | Field | Type | Default or description | | --- | --- | --- | | `role` | `"tool"` | Fixed role | | `callId` | `string` | Matches `ToolCall.callId` | | `toolName` | `string` | Tool name | | `toolOutput` | `string` | Output text; successful tool results are JSON-serialized | | `isError` | `boolean` | Defaults to `false`; `true` means tool execution failed | All these properties are readonly. ## BaseMessage and BaseMessages ```ts import type { SystemMessage, UserMessage, AssistantMessage, ToolMessage, } from "@yong_xie/lighter"; type BaseMessage = SystemMessage | UserMessage | AssistantMessage | ToolMessage; type BaseMessages = BaseMessage[]; ``` `BaseMessage` is a type union, not a class you can instantiate. ## Construct messages This manually constructed tool round trip illustrates the message structure. In normal runs, providers and Agent usually generate assistant and tool messages. ```ts import { AssistantMessage, ToolMessage, UserMessage, type BaseMessages, } from "@yong_xie/lighter"; const messages: BaseMessages = [ new UserMessage("Calculate 19 + 23."), new AssistantMessage("", { toolCalls: [{ callId: "call_add_1", toolName: "add", arguments: JSON.stringify({ a: 19, b: 23 }), }], }), // callId links the tool result to the earlier call. new ToolMessage({ callId: "call_add_1", toolName: "add", toolOutput: "42" }), new AssistantMessage("19 + 23 = 42."), ]; for (const message of messages) { // Read toolOutput for tool messages and content for other messages. console.log(message.role, message.role === "tool" ? message.toolOutput : message.content); } ``` [← Model and Provider API](/docs/reference/model-and-providers/) · [Next: Tool API →](/docs/reference/tools/) --- Source: /docs/reference/tools/ # Tool API `Tool` wraps parameter validation, function execution, and tool results. For usage, see [add tools](/docs/guides/tools/). ## Constructor parameters `new Tool({ name, description, parametersZodSchema, func })` | Field | Type | Description | | --- | --- | --- | | `name` | `string` | Tool name; should be unique within an Agent | | `description` | `string` | Explains the purpose and conditions for using the tool to the model | | `parametersZodSchema` | `ZSchema extends z.ZodObject` | Zod object schema used for JSON Schema generation and local validation | | `func` | `(args: z.infer) => unknown \| Promise` | Synchronous or asynchronous execution function | All four fields are required. Tool uses generics to infer the parameter type of `func`. The internal constructor type `ToolConfig` is not exported separately. An instance's `name`, `description`, `parametersZodSchema`, and `func` are public readonly properties. ## `execute()` ```text execute(args: unknown, options: { callId: string }): Promise ``` Execution order: validate with Zod's synchronous `parse()` → await `func()` → serialize the result with `JSON.stringify()`. - Success returns a `ToolMessage` with `isError: false`. - If validation, execution, or serialization throws, the returned `ToolMessage` has `isError: true`, with `String(error)` as `toolOutput`. - The caller provides `callId`. It is preserved in the result to associate it with the tool call. Return values that `JSON.stringify()` can turn into strings. Avoid `undefined`, functions, Symbols, and other values that do not produce valid output text. BigInt and circular references can cause serialization failures. Agent normally calls this method automatically. To execute a tool directly: ```ts import { Tool } from "@yong_xie/lighter"; import * as z from "zod"; const add = new Tool({ name: "add", description: "Calculate the sum of two numbers.", parametersZodSchema: z.strictObject({ a: z.number(), b: z.number() }), func: ({ a, b }) => a + b, }); // Validate before executing. toolOutput in the result is the string "42". const result = await add.execute({ a: 19, b: 23 }, { callId: "call_add_1" }); console.log(result); ``` ## `getSpec()` ```text getSpec(): ToolSpec ``` Converts the parameter schema to JSON Schema and returns a tool definition for the model, without the execution function. Conversion uses Zod's input mode. Unsupported schemas may throw here. `ToolSpec` contains: | Field | Type | Current `getSpec()` value | | --- | --- | --- | | `type` | `"function"` | `"function"` | | `name` | `string` | Tool name | | `description` | `string` | Tool description | | `parameters` | `Record` | JSON Schema generated by Zod | | `strict` | `boolean` | Always `false` | `strict: false` is a definition option sent to the provider. It does not disable local Zod validation before tool execution. [← Messages API](/docs/reference/messages/) · [Next: Hooks API →](/docs/reference/hooks/) --- Source: /docs/reference/hooks/ # Hooks API All four hook classes share the same construction pattern. For a working example, see [lifecycle hooks](/docs/guides/hooks/). ## Shared constructor parameters `new BeforeModelHook({ hookName, description?, hookFunction })` The other hook classes accept the same three fields: | Field | Required | Description | | --- | --- | --- | | `hookName: string` | Yes | Hook identifier; also a public readonly instance property | | `description?: string` | No | Description; also a public readonly instance property | | `hookFunction` | Yes | Callback, with the types listed below | ## Callback types | Class | Input | Allowed return values | | --- | --- | --- | | `BeforeAgentHook` | `AgentRequest` | `AgentRequest \| void \| Promise` | | `BeforeModelHook` | `ModelRequest` | `ModelRequest \| void \| Promise` | | `AfterModelHook` | `AssistantMessage` | `AssistantMessage \| void \| Promise` | | `AfterAgentHook` | `BaseMessages` | `BaseMessages \| void \| Promise` | All callbacks are awaited. Returning `undefined` preserves the current value; returning a replacement uses it instead. The implementation currently uses nullish coalescing, but the types do not permit returning `null`. `AfterAgentHook` receives and returns a message array, not a complete `AgentResponse`. ## `execute()` | Class | Public method | | --- | --- | | `BeforeAgentHook` | `execute(request: AgentRequest): Promise` | | `BeforeModelHook` | `execute(request: ModelRequest): Promise` | | `AfterModelHook` | `execute(message: AssistantMessage): Promise` | | `AfterAgentHook` | `execute(messages: BaseMessages): Promise` | Agent or Model calls these methods automatically. When you call them directly, callback exceptions propagate to the caller. ## Timing and failures - `beforeAgent`: once at the start of each run, before Agent adds its system prompt. - `beforeModel`: before every model request, including calls after tool results. - `afterModel`: after each complete assistant message, before Agent executes its tool calls. Streaming deltas do not pass through this hook. - `afterAgent`: currently once after the loop completes successfully, before Agent wraps the final result. Agent catches hook exceptions and returns or yields a failed result. Model-hook exceptions propagate when calling Model directly. `afterAgent` is currently skipped on model failure or early stream termination, so do not rely on it for guaranteed resource cleanup. ## Example ```ts import { AfterAgentHook, AssistantMessage } from "@yong_xie/lighter"; const appendNotice = new AfterAgentHook({ hookName: "append-notice", hookFunction: (messages) => [ ...messages, // Return a message array for messages in the final Agent response. new AssistantMessage("This response is for reference only."), ], }); ``` Pass this instance to `new Agent({ model, hooks: { afterAgent: appendNotice } })`. This hook affects only the final returned messages. It does not trigger another model call or send the added content as a stream delta. [← Tool API](/docs/reference/tools/) · [Next: Errors API →](/docs/reference/errors/) --- Source: /docs/reference/errors/ # Errors API For usage, see [error handling](/docs/guides/errors/). Built-in providers use `ModelError` for recognized model-call errors. ## ModelError ```text new ModelError(message: string, code: ModelErrorCode, options?: { httpStatus?: number; responseStatus?: string; }) ``` `ModelError` extends `Error` and provides these readonly fields: | Field | Type | Description | | --- | --- | --- | | `name` | `"ModelError"` | Fixed name | | `code` | `ModelErrorCode` | Unified error category | | `httpStatus` | `number \| undefined` | HTTP error status returned by the service | | `responseStatus` | `string \| undefined` | Generation status or stop reason, such as `incomplete` or `max_tokens` | `message`, `stack`, and related fields are inherited from `Error`. `httpStatus` and `responseStatus` are independent. Network errors may have neither. ## ModelErrorCode `ModelErrorCode` is a union of these strings: | Code | Meaning | | --- | --- | | `auth_error` | Authentication or access-permission failure | | `rate_limit` | Service rate limit | | `invalid_request` | Invalid request format, parameters, model, or related settings | | `network_error` | Connection or network error | | `server_error` | Model-service failure | | `bad_response` | Incomplete, failed, unparseable, or unsupported response | | `aborted` | Request cancellation or a cancelled status returned by the service | | `unknown` | SDK error not classified elsewhere | ## Use in a custom provider ```ts import { ModelError } from "@yong_xie/lighter"; // Example: create and inspect an incomplete-generation error. const error = new ModelError("The model reached the output limit before generation completed.", "bad_response", { responseStatus: "incomplete", }); console.log(error.code, error.responseStatus, error.message); ``` Custom providers should throw suitable errors when the service fails. Unified categories let applications handle common failures across providers using `code`. ## Relationship to AgentResponse `AgentResponse.error` remains an optional `unknown`. Only read `code`, `httpStatus`, and related fields after confirming `error instanceof ModelError`. Errors propagate when calling Model or a Provider directly. When running through Agent, caught model and hook errors are included in the failed result. Errors inside tool functions are represented by `ToolMessage.isError` instead. [← Hooks API](/docs/reference/hooks/) · [Back to introduction](/docs/)