This is the full developer documentation for AgentKit by Inngest # AgentKit > A TypeScript library to create and orchestrate AI Agents. AgentKit is a framework to build AI Agents, from single model inference calls to multi-agent systems that use tools. Designed with orchestration at its core, AgentKit enables developers to build, test, and deploy reliable AI applications at scale. With AgentKit, you get: ✨ **Simple and composable primitives** to build from simple Support Agents to semi-autonomous Coding Agents. 🧠 **Support for [OpenAI, Anthropic, Gemini](/concepts/models)** and all OpenAI API compatible models. 🛠️ **Powerful tools building API** with support for [MCP as tools](/advanced-patterns/mcp). 🔌 **Integrates** with your favorite AI libraries and products (ex: [E2B](/integrations/e2b), [Browserbase](/integrations/browserbase), [Smithery](/integrations/smithery), [Daytona](/integrations/daytona)). ⚡ **Stream live updates** to your UI with [UI Streaming](/advanced-patterns/legacy-ui-streaming). 📊 **[Local Live traces](/getting-started/local-development) and input/output logs** when combined with the Inngest Dev Server. New to AI Agents? Follow our [Guided Tour](/guided-tour/overview) to learn how to build your first AgentKit application. All the above sounds familiar? Check our **[Getting started section](#getting-started)** or the **[“How AgentKit works” section](#how-agentkit-works)** to learn more about AgentKit’s architecture. ## Getting started [Section titled “Getting started”](#getting-started) [Quick start ](/getting-started/quick-start)Jump into the action by building your first AgentKit application. [Examples ](/examples/overview)Looking for inspiration? Check out our examples to see how AgentKit can be used. [Concepts ](/concepts/agents)Learn the core concepts of AgentKit. [SDK Reference ](/reference/introduction)Ready to dive into the code? Browse the SDK reference to learn more about AgentKit's primitives. ## How AgentKit works [Section titled “How AgentKit works”](#how-agentkit-works) AgentKit enables developers to compose simple single-agent systems or entire *systems of agents* in which multiple agents can work together. **[Agents](/concepts/agents)** are combined into **[Networks](concepts/networks)** which include a **[Router](concepts/routers)** to determine which Agent should be called. Their system’s memory is recorded as Network **[State](concepts/state)** which can be used by the Router, Agents or **[Tools](concepts/tools)** to collaborate on tasks. ![A diagram with the components of AgentKit in an AgentKit Network](/graphics/system.svg) The entire system is orchestration-aware and allows for customization at runtime for dynamic, powerful AI workflows and agentic systems. Here is what a simple Network looks like in code: ```ts import { createNetwork, createAgent, openai, anthropic, } from "@inngest/agent-kit"; import { searchWebTool } from "./tools"; const navigator = createAgent({ name: "Navigator", system: "You are a navigator...", tools: [searchWebTool], }); const classifier = createAgent({ name: "Classifier", system: "You are a classifier...", model: openai("gpt-3.5-turbo"), }); const summarizer = createAgent({ model: anthropic("claude-3-5-haiku-latest"), name: "Summarizer", system: "You are a summarizer...", }); const network = createNetwork({ agents: [navigator, classifier, summarizer], defaultModel: openai({ model: "gpt-4o" }), }); const input = `Classify then summarize the latest 10 blog posts on https://www.deeplearning.ai/blog/`; const result = await network.run(input, ({ network }) => { return defaultRoutingAgent; }); ``` ## `llms.txt` [Section titled “llms.txt”](#llmstxt) You can access the entire AgentKit docs in markdown format at [agentkit.inngest.com/llms-full.txt](https://agentkit.inngest.com/llms-full.txt). This is useful for passing the entire docs to an LLM, AI-enabled IDE, or similar tool to answer questions about AgentKit. If your context window is too small to pass the entire docs, you can use the shorter [agentkit.inngest.com/llms.txt](https://agentkit.inngest.com/llms.txt) file which offers a table of contents for LLMs or other developer tools to index the docs more easily. # Human in the Loop > Enable your Agents to wait for human input. Agents such as Support Agents, Coding or Research Agents might require human oversight. By combining AgentKit with Inngest, you can create [Tools](/concepts/tools) that can wait for human input. ## Creating a “Human in the Loop” tool [Section titled “Creating a “Human in the Loop” tool”](#creating-a-human-in-the-loop-tool) “Human in the Loop” tools are implemented using Inngest’s [`waitForEvent()`](https://www.inngest.com/docs/features/inngest-functions/steps-workflows/wait-for-event) step method: ```ts import { createTool } from "@inngest/agent-kit"; createTool({ name: "ask_developer", description: "Ask a developer for input on a technical issue", parameters: z.object({ question: z.string().describe("The technical question for the developer"), context: z.string().describe("Additional context about the issue"), }), handler: async ({ question, context }, { step }) => { if (!step) { return { error: "This tool requires step context" }; } // Example: Send a Slack message to the developer // Wait for developer response event const developerResponse = await step.waitForEvent("developer.response", { event: "app/support.ticket.developer-response", timeout: "4h", match: "data.ticketId", }); if (!developerResponse) { return { error: "No developer response provided" }; } return { developerResponse: developerResponse.data.answer, responseTime: developerResponse.data.timestamp, }; }, }); ``` The `ask_developer` tool will wait up to 4 hours for a `"developer.response"` event to be received, pausing the execution of the AgentKit network. The incoming `"developer.response"` event will be matched against the `data.ticketId` field of the event that trigger the AgentKit network. For this reason, the AgentKit network will need to be wrapped in an Inngest function as demonstrated in the next section. ## Example: Support Agent with Human in the Loop [Section titled “Example: Support Agent with Human in the Loop”](#example-support-agent-with-human-in-the-loop) Let’s consider a Support Agent Network automously triaging and solving tickets: ```tsx const customerSupportAgent = createAgent({ name: "Customer Support", description: "I am a customer support agent that helps customers with their inquiries.", system: `You are a helpful customer support agent. Your goal is to assist customers with their questions and concerns. Be professional, courteous, and thorough in your responses.`, model: anthropic({ model: "claude-3-5-haiku-latest", max_tokens: 1000, }), tools: [ searchKnowledgeBase, // ... ], }); const technicalSupportAgent = createAgent({ name: "Technical Support", description: "I am a technical support agent that helps critical tickets.", system: `You are a technical support specialist. Your goal is to help resolve critical tickets. Use your expertise to diagnose problems and suggest solutions. If you need developer input, use the ask_developer tool.`, model: anthropic({ model: "claude-3-5-haiku-latest", max_tokens: 1000, }), tools: [ searchLatestReleaseNotes, // ... ], }); const supervisorRoutingAgent = createRoutingAgent({ // ... }); // Create a network with the agents and default router const supportNetwork = createNetwork({ name: "Support Network", agents: [customerSupportAgent, technicalSupportAgent], defaultModel: anthropic({ model: "claude-3-5-haiku-latest", max_tokens: 1000, }), router: supervisorRoutingAgent, }); ``` Note You can find the complete example code in the [examples/support-agent-human-in-the-loop](https://github.com/inngest/agent-kit/tree/main/examples/support-agent-human-in-the-loop) directory. To avoid the Support Agent to be stuck or classifying tickets incorrectly, we’ll implement a “Human in the Loop” tool to enable a human to add some context. To implement a “Human in the Loop” tool, we’ll need to embed our AgentKit network into an Inngest function. ### Transforming your AgentKit network into an Inngest function [Section titled “Transforming your AgentKit network into an Inngest function”](#transforming-your-agentkit-network-into-an-inngest-function) First, you’ll need to create an Inngest Client: ```ts import { Inngest } from "inngest"; const inngest = new Inngest({ id: "my-agentkit-network", }); ``` Then, transform your AgentKit network into an Inngest function as follows: ```ts import { createAgent, createNetwork, openai } from "@inngest/agent-kit"; import { createServer } from "@inngest/agent-kit/server"; const customerSupportAgent = createAgent({ name: "Customer Support", // .. }); const technicalSupportAgent = createAgent({ name: "Technical Support", // .. }); // Create a network with the agents and default router const supportNetwork = createNetwork({ name: "Support Network", agents: [customerSupportAgent, technicalSupportAgent], // .. }); const supportAgentWorkflow = inngest.createFunction( { id: "support-agent-workflow", }, { event: "app/support.ticket.created", }, async ({ step, event }) => { const ticket = await step.run("get_ticket_details", async () => { const ticket = await getTicketDetails(event.data.ticketId); return ticket; }); if (!ticket || "error" in ticket) { throw new NonRetriableError(`Ticket not found: ${ticket.error}`); } const response = await supportNetwork.run(ticket.title); return { response, ticket, }; } ); // Create and start the server const server = createServer({ functions: [supportAgentWorkflow as any], }); server.listen(3010, () => console.log("Support Agent demo server is running on port 3010") ); ``` The `network.run()` is now performed by the Inngest function. Don’t forget to register the function with `createServer`’s `functions` property. ### Add a `ask_developer` tool to the network [Section titled “Add a ask\_developer tool to the network”](#add-a-ask_developer-tool-to-the-network) Our AgentKit network is now ran inside an Inngest function triggered by the `"app/support.ticket.created"` event which carries the `data.ticketId` field. The `Technical Support` Agent will now use the `ask_developer` tool to ask a developer for input on a technical issue: ```ts import { createTool } from "@inngest/agent-kit"; createTool({ name: "ask_developer", description: "Ask a developer for input on a technical issue", parameters: z.object({ question: z.string().describe("The technical question for the developer"), context: z.string().describe("Additional context about the issue"), }), handler: async ({ question, context }, { step }) => { if (!step) { return { error: "This tool requires step context" }; } // Example: Send a Slack message to the developer // Wait for developer response event const developerResponse = await step.waitForEvent("developer.response", { event: "app/support.ticket.developer-response", timeout: "4h", match: "data.ticketId", }); if (!developerResponse) { return { error: "No developer response provided" }; } return { developerResponse: developerResponse.data.answer, responseTime: developerResponse.data.timestamp, }; }, }); ``` Our `ask_developer` tool will now wait for a `"developer.response"` event to be received (ex: from a Slack message), and match it against the `data.ticketId` field. The result of the `ask_developer` tool will be returned to the `Technical Support` Agent. Look at the Inngest [`step.waitForEvent()`](https://www.inngest.com/docs/features/inngest-functions/steps-workflows/wait-for-event) documentation for more details and examples. ### Try it out [Section titled “Try it out”](#try-it-out) [Support Agent with "Human in the loop" ](https://github.com/inngest/agent-kit/tree/main/examples/support-agent-human-in-the-loop#readme)This Support AgentKit Network is composed of two Agents (Customer Support and Technical Support) and a Supervisor Agent that routes the ticket to the correct Agent. The Technical Support Agent can wait for a developer response when facing complex technical issues. # UI Streaming with useAgent > Stream AgentKit events to your UI with the useAgent hook. The `useAgent` hook is a powerful client-side hook for React that manages real-time, multi-threaded conversations with an AgentKit network. It encapsulates the entire lifecycle of agent interactions, including sending messages, receiving streaming events, handling out-of-order event sequences, and managing connection state. While `useChat` is the recommended high-level hook for building chat interfaces, `useAgent` provides the low-level building blocks for more customized implementations where you need direct control over the event stream and state management. [use-agent Example ](https://github.com/inngest/agent-kit/tree/main/examples/use-agent)Find the complete source code for a Next.js chat application using useAgent on GitHub. ## How it Works [Section titled “How it Works”](#how-it-works) `useAgent` builds upon Inngest’s Realtime capabilities to create a persistent, unified stream of events for a user. Here’s a step-by-step breakdown of the data flow: 1. **Client Initialization**: The `useAgent` hook is initialized in your React component. It uses the `useInngestSubscription` hook from `@inngest/realtime/hooks` to establish a WebSocket connection. 2. **Authentication**: To connect, it calls a backend API route (e.g., `/api/realtime/token`) to get a short-lived subscription token. This token authorizes the client to listen to events on a specific user channel. 3. **Sending a Message**: The user types a message and the UI calls the `sendMessage` function returned by the hook. 4. **API Request**: `sendMessage` makes a `POST` request to a backend API route (e.g., `/api/chat`). This request contains the message content, the current `threadId`, and the conversation history. 5. **Triggering Inngest**: The chat API route receives the request and sends an `agent/chat.requested` event to Inngest using `inngest.send()`. 6. **Agent Execution**: An Inngest function (`run-agent-chat.ts` in our example) is triggered by the event. It sets up the AgentKit network, state, and history adapter. 7. **Running the Network**: The Inngest function calls `network.run(message, { streaming: ... })`. 8. **Streaming Events**: As the network and its agents execute, they generate streaming events (e.g., `run.started`, `part.created`, `text.delta`). The `streaming.publish` function inside the Inngest function forwards these events back to the user’s realtime channel. 9. **Realtime Push**: The events are pushed over the WebSocket connection to the client. 10. **State Update**: `useAgent` receives the raw events, processes them in the correct sequence, handles out-of-order events, and updates its internal state. 11. **UI Re-render**: The component using the hook re-renders with the new messages, agent status, and other UI parts. ### Sequence Diagram [Section titled “Sequence Diagram”](#sequence-diagram) ```mermaid sequenceDiagram participant Client (React UI) participant useAgent Hook participant Backend API participant Inngest participant AgentKit Network Client->>useAgent Hook: Initialize with threadId, userId useAgent Hook->>Backend API: POST /api/realtime/token Backend API-->>useAgent Hook: Subscription Token useAgent Hook->>Inngest: Establishes WebSocket Connection Client->>useAgent Hook: sendMessage("Hello!") useAgent Hook->>Backend API: POST /api/chat (message, history) Backend API->>Inngest: inngest.send("agent/chat.requested") Inngest->>AgentKit Network: Triggers Inngest Function (run-agent-chat.ts) AgentKit Network->>AgentKit Network: network.run(message, { streaming }) loop Event Streaming AgentKit Network-->>Inngest: publish(event) Inngest-->>useAgent Hook: Pushes event via WebSocket useAgent Hook->>useAgent Hook: Processes event, updates state useAgent Hook-->>Client: Re-renders UI with new data end ``` ## Usage Guide [Section titled “Usage Guide”](#usage-guide) ### 1. Backend Setup [Section titled “1. Backend Setup”](#1-backend-setup) First, you need to set up the backend infrastructure to handle chat requests and realtime communication. #### Inngest Function [Section titled “Inngest Function”](#inngest-function) This function is the core of your agent’s execution. It listens for chat requests and runs your AgentKit network, streaming events back to the client. inngest/functions/run-agent-chat.ts ```ts import { inngest } from "../client"; import { createCustomerSupportNetwork } from "../networks/customer-support-network"; import { userChannel } from "../../lib/realtime"; import { createState } from "@inngest/agent-kit"; import type { CustomerSupportState } from "../types/state"; import { PostgresHistoryAdapter } from "../db"; const historyAdapter = new PostgresHistoryAdapter({}); export const runAgentChat = inngest.createFunction( { id: "run-agent-chat" }, { event: "agent/chat.requested" }, async ({ event, step, publish }) => { await step.run("initialize-db-tables", () => historyAdapter.initializeTables() ); const { threadId, message, userId, history, messageId } = event.data; const network = createCustomerSupportNetwork( threadId, createState( { customerId: userId }, { messages: history, threadId } ), historyAdapter ); // Run the network and stream events back to the client const result = await network.run(message, { streaming: { publish: async (chunk) => { const enrichedChunk = { ...chunk, data: { ...chunk.data, threadId, userId }, }; await publish(userChannel(userId).agent_stream(enrichedChunk)); }, }, messageId, }); return { success: true, threadId, result }; } ); ``` #### API Routes [Section titled “API Routes”](#api-routes) You’ll need a few API routes in your Next.js application. app/api/chat/route.ts ```ts import { NextRequest, NextResponse } from "next/server"; import { inngest } from "@/inngest/client"; import { randomUUID } from "crypto"; export async function POST(req: NextRequest) { const { message, threadId: providedThreadId, userId, history, messageId, } = await req.json(); const threadId = providedThreadId || randomUUID(); await inngest.send({ name: "agent/chat.requested", data: { threadId, message, messageId, history, userId }, }); return NextResponse.json({ success: true, threadId }); } ``` app/api/realtime/token/route.ts ```ts import { NextRequest, NextResponse } from "next/server"; import { getSubscriptionToken } from "@inngest/realtime"; import { inngest } from "@/inngest/client"; import { userChannel } from "@/lib/realtime"; export async function POST(req: NextRequest) { const { userId } = await req.json(); // TODO: Add authentication/authorization here const token = await getSubscriptionToken(inngest, { channel: userChannel(userId), topics: ["agent_stream"], }); return NextResponse.json(token); } ``` ### 2. Frontend Setup [Section titled “2. Frontend Setup”](#2-frontend-setup) Now, let’s wire up the `useAgent` hook in a React component. components/ChatComponent.tsx ```tsx "use client"; import { useAgent, type ConversationMessage } from "@/hooks/use-agent"; const USER_ID = "test-user-123"; export function ChatComponent({ threadId }: { threadId: string }) { const { messages, status, sendMessage, isConnected, error, clearError } = useAgent({ threadId: threadId, userId: USER_ID, debug: true, onError: (err) => console.error("Agent error:", err), }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); const formData = new FormData(e.currentTarget); const message = formData.get("message") as string; if (message.trim()) { sendMessage(message); e.currentTarget.reset(); } }; return (
Status: {status}
Connected: {isConnected ? "Yes" : "No"}
{error && (
Error: {error.message}
)}
{messages.map((msg) => ( ))}
); } // A simple component to render a message with all its parts function Message({ message }: { message: ConversationMessage }) { return (
{message.role} {message.parts.map((part, index) => { switch (part.type) { case "text": return

{part.content}

; case "tool-call": return (
Tool Call: {part.toolName}
                  Status: {part.state}
                  
Input: {JSON.stringify(part.input, null, 2)} {part.output && ( <>
Output: {JSON.stringify(part.output, null, 2)} )}
); // TODO: Add cases for other part types like 'data', 'reasoning', etc. default: return null; } })}
); } ``` ## API Reference [Section titled “API Reference”](#api-reference) The `useAgent` hook returns an object with the following properties and methods. ### State Properties [Section titled “State Properties”](#state-properties) `messages` ConversationMessage\[] The array of messages for the **current thread**. Each message contains an array of `parts` that are streamed in real-time. `status` AgentStatus The current status of the agent for the active thread. It can be one of: `"idle"`, `"thinking"`, `"calling-tool"`, `"responding"`, or `"error"`. `error` { message: string; ... } | undefined An object containing details about the last error that occurred in the active thread. `undefined` if there is no error. `threads` Record\ An object containing the full state for all active threads, indexed by `threadId`. This allows you to manage multiple conversations in the background. `currentThreadId` string The ID of the currently active/displayed thread. `isConnected` boolean `true` if the client is currently connected to the Inngest Realtime server. `connectionError` { message: string; ... } | undefined An object containing details about a connection-level error (e.g., failure to get a subscription token). ### Action Methods [Section titled “Action Methods”](#action-methods) `sendMessage` (message: string) => Promise\ Sends a message to the **current thread**. It handles optimistic UI updates and formats the history for the backend. `sendMessageToThread` (threadId: string, message: string) => Promise\ Sends a message to a **specific thread**, which can be different from the currently active one. `setCurrentThread` (threadId: string) => void Switches the active thread. This updates which thread’s state is exposed via the top-level `messages`, `status`, and `error` properties. `createThread` (threadId: string) => void Creates a new, empty thread in the local state. `clearMessages` () => void Clears all messages from the **current thread’s** local state. `replaceMessages` (messages: ConversationMessage\[]) => void Replaces all messages in the **current thread’s** local state. Useful for loading historical messages. `clearError` () => void Clears the error state for the **current thread**. `clearConnectionError` () => void Clears any connection-level error. ## UI Data Models [Section titled “UI Data Models”](#ui-data-models) The `useAgent` hook exposes a set of rich UI data models to make building chat interfaces easier. These types define the structure of messages and their constituent parts. ### ConversationMessage [Section titled “ConversationMessage”](#conversationmessage) Represents a complete message in the conversation, containing one or more parts. ```typescript export interface ConversationMessage { /** Unique identifier for this message */ id: string; /** Whether this message is from the user or the assistant */ role: "user" | "assistant"; /** Array of message parts that make up the complete message */ parts: MessagePart[]; /** ID of the agent that created this message (for assistant messages) */ agentId?: string; /** When this message was created */ timestamp: Date; /** The status of the message, particularly for optimistic user messages */ status?: "sending" | "sent" | "failed"; } ``` ### MessagePart [Section titled “MessagePart”](#messagepart) A union type representing all possible parts of a message. ```typescript export type MessagePart = | TextUIPart | ToolCallUIPart | DataUIPart | FileUIPart | SourceUIPart | ReasoningUIPart | StatusUIPart | ErrorUIPart | HitlUIPart; ``` ### TextUIPart [Section titled “TextUIPart”](#textuipart) Represents a text message part that can be streamed character by character. ```typescript export interface TextUIPart { type: "text"; /** Unique identifier for this text part */ id: string; /** The text content, updated incrementally during streaming */ content: string; /** Whether the text is still being streamed or is complete */ status: "streaming" | "complete"; } ``` ### ToolCallUIPart [Section titled “ToolCallUIPart”](#toolcalluipart) Represents a tool call that the agent is making, with streaming input and output. ```typescript export interface ToolCallUIPart { type: "tool-call"; /** Unique identifier for this tool call */ toolCallId: string; /** Name of the tool being called */ toolName: string; /** Current state of the tool call execution */ state: | "input-streaming" | "input-available" | "awaiting-approval" | "executing" | "output-available"; /** Tool input parameters, streamed incrementally */ input: any; /** Tool output result, if available */ output?: any; /** Error information if the tool call failed */ error?: any; } ``` *(For brevity, other part types like `DataUIPart`, `ReasoningUIPart`, etc., are omitted here but follow a similar structure. You can find their full definitions in the `use-agent.ts` file in the example.)* ### AgentStatus [Section titled “AgentStatus”](#agentstatus) Represents the current activity status of the agent. ```typescript export type AgentStatus = | "idle" | "thinking" | "calling-tool" | "responding" | "error"; ``` # MCP as tools > Provide your Agents with MCP Servers as tools AgentKit supports using [Claude’s Model Context Protocol](https://modelcontextprotocol.io/) as tools. Using MCP as tools allows you to use any MCP server as a tool in your AgentKit network, enabling your Agent to access thousands of pre-built tools to interact with. Our integration with [Smithery](https://smithery.ai/) provides a registry of MCP servers for common use cases, with more than 2,000 servers across multiple use cases. ## Using MCP as tools [Section titled “Using MCP as tools”](#using-mcp-as-tools) AgentKit supports configuring MCP servers via `Streamable HTTP`, `SSE` or `WS` transports: * Self-hosted MCP server ```ts import { createAgent } from "@inngest/agent-kit"; const neonAgent = createAgent({ name: "neon-agent", system: `You are a helpful assistant that help manage a Neon account. `, mcpServers: [ { name: "neon", transport: { type: "ws", url: "ws://localhost:8080", }, }, ], }); ``` * Smithery MCP server ```ts import { createAgent } from "@inngest/agent-kit"; import { createSmitheryUrl } from "@smithery/sdk/config.js"; const smitheryUrl = createSmitheryUrl("https://server.smithery.ai/neon/ws", { neonApiKey: process.env.NEON_API_KEY, }); const neonAgent = createAgent({ name: "neon-agent", system: `You are a helpful assistant that help manage a Neon account. `, mcpServers: [ { name: "neon", transport: { type: "streamable-http", url: neonServerUrl.toString(), }, }, ], }); ``` ## `mcpServers` reference [Section titled “mcpServers reference”](#mcpservers-reference) The `mcpServers` parameter allows you to configure Model Context Protocol servers that provide tools for your agent. AgentKit automatically fetches the list of available tools from these servers and makes them available to your agent. `mcpServers` MCP.Server\[] An array of MCP server configurations. ### MCP.Server [Section titled “MCP.Server”](#mcpserver) `name` string required A short name for the MCP server (e.g., “github”, “neon”). This name is used to namespace tools for each MCP server. Tools from this server will be prefixed with this name (e.g., “neon-createBranch”). `transport` TransportSSE | TransportWebsocket required The transport configuration for connecting to the MCP server. ### TransportSSE [Section titled “TransportSSE”](#transportsse) `type` 'sse' required Specifies that the transport is Server-Sent Events. `url` string required The URL of the SSE endpoint. `eventSourceInit` EventSourceInit Optional configuration for the EventSource. `requestInit` RequestInit Optional request configuration. ### TransportWebsocket [Section titled “TransportWebsocket”](#transportwebsocket) `type` 'ws' required Specifies that the transport is WebSocket. `url` string required The WebSocket URL of the MCP server. ## Examples [Section titled “Examples”](#examples) [Neon Assistant Agent (using MCP) ](https://github.com/inngest/agent-kit/tree/main/examples/mcp-neon-agent/#readme)This example shows how to use the Neon MCP Smithery Server to build a Neon Assistant Agent that can help you manage your Neon databases. # Multi-steps tools > Use multi-steps tools to create more complex Agents. In this guide, we’ll learn how to create a multi-steps tool that can be used in your AgentKit [Tools](/concepts/tools) to reliably perform complex operations. By combining your AgentKit network with Inngest, each step of your tool will be **retried automatically** and you’ll be able to **configure concurrency and throttling**. Prerequisites Your AgentKit network [must be configured with Inngest](/getting-started/local-development#1-install-the-inngest-package). ## Creating a multi-steps tool [Section titled “Creating a multi-steps tool”](#creating-a-multi-steps-tool) Creating a multi-steps tool is done by creating an Inngest Function that will be used as a tool in your AgentKit network. To create an Inngest Function, you’ll need to create an Inngest Client: ```ts import { Inngest } from 'inngest'; const inngest = new Inngest({ id: 'my-agentkit-network', }); ``` Then, we will implement our AgentKit Tool as an Inngest Function with multiple steps. For example, we’ll create a tool that searches for perform a research by crawling the web: ```ts import { inngest } from '../client'; export const researchWebTool = inngest.createFunction({ id: 'research-web-tool', }, { event: "research-web-tool/run" }, async ({ event, step }) => { const { input } = event.data; const searchQueries = await step.ai.infer('generate-search-queries', { model: step.ai.models.openai({ model: "gpt-4o" }), // body is the model request, which is strongly typed depending on the model body: { messages: [{ role: "user", content: `From the given input, generate a list of search queries to perform. \n ${input}`, }], }, }); const searchResults = await Promise.all( searchQueries.map(query => step.run('crawl-web', async (query) => { // perform crawling... }) )); const summary = await step.ai.infer('summarize-search-results', { model: step.ai.models.openai({ model: "gpt-4o" }), body: { messages: [{ role: "user", content: `Summarize the following search results: \n ${searchResults.join('\n')}`, }], }, }); return summary.choices[0].message.content; }); ``` Our `researchWebTool` Inngest defines 3 main steps. * The `step.ai.infer()` call will offload the LLM requests to the Inngest infrastructe which will also handle retries. * The `step.run()` call will run the `crawl-web` step in parallel. All the above steps will be retried automatically in case of failure, resuming the AgentKit network upon completion of the tool. ## Using the multi-steps tool in your AgentKit network [Section titled “Using the multi-steps tool in your AgentKit network”](#using-the-multi-steps-tool-in-your-agentkit-network) We can now add our `researchWebTool` to our AgentKit network: ```ts import { createAgent, createNetwork, openai } from '@inngest/agent-kit'; import { createServer } from '@inngest/agent-kit/server'; import { researchWebTool } from './inngest/tools/research-web'; const deepResearchAgent = createAgent({ name: 'Deep Research Agent', tools: [researchWebTool], }); const network = createNetwork({ name: 'My Network', defaultModel: openai({ model: "gpt-4o" }), agents: [deepResearchAgent], }); const server = createServer({ networks: [network], functions: [researchWebTool], }); server.listen(3010, () => console.log("Agent kit running!")); ``` We first import our `researchWebTool` function and pass it to the `deepResearchAgent` [`tools` array](/reference/create-agent#param-tools). Finally, we also need to pass the `researchWebTool` function to the `createServer()`’s `functions` array. ## Going further [Section titled “Going further”](#going-further) [Configuring Multitenancy ](/advanced-patterns/multitenancy)Learn how to configure user-based capacity for your AgentKit network. [Customizing the retries ](/advanced-patterns/retries)Learn how to customize the retries of your multi-steps tools. # Configuring Multi-tenancy > Configure capacity based on users or organizations. As discussed in the [deployment guide](/concepts/deployment), moving an AgentKit network into users’ hands requires configuring usage limits. To avoid having one user’s usage affect another, you can configure multi-tenancy. Multi-tenancy consists of configuring limits based on users or organizations (*called “tenants”*). It can be easily configured on your AgentKit network using Inngest. Prerequisites Your AgentKit network [must be configured with Inngest](/getting-started/local-development#1-install-the-inngest-package). ## Configuring Multi-tenancy [Section titled “Configuring Multi-tenancy”](#configuring-multi-tenancy) Adding multi-tenancy to your AgentKit network is done by transforming your AgentKit network into an Inngest function. ### Transforming your AgentKit network into an Inngest function [Section titled “Transforming your AgentKit network into an Inngest function”](#transforming-your-agentkit-network-into-an-inngest-function) First, you’ll need to create an Inngest Client: ```ts import { Inngest } from "inngest"; const inngest = new Inngest({ id: "my-agentkit-network", }); ``` Then, transform your AgentKit network into an Inngest function as follows: ```ts import { createAgent, createNetwork, openai } from "@inngest/agent-kit"; import { createServer } from "@inngest/agent-kit/server"; import { inngest } from "./inngest/client"; const deepResearchAgent = createAgent({ name: "Deep Research Agent", tools: [ /* ... */ ], }); const network = createNetwork({ name: "My Network", defaultModel: openai({ model: "gpt-4o" }), agents: [deepResearchAgent], }); const deepResearchNetworkFunction = inngest.createFunction( { id: "deep-research-network", }, { event: "deep-research-network/run", }, async ({ event, step }) => { const { input } = event.data; return network.run(input); } ); const server = createServer({ functions: [deepResearchNetworkFunction], }); server.listen(3010, () => console.log("Agent kit running!")); ``` The `network.run()` is now performed by the Inngest function. Don’t forget to register the function with `createServer`’s `functions` property. ### Configuring a concurrency per user [Section titled “Configuring a concurrency per user”](#configuring-a-concurrency-per-user) We can now configure the capacity by user by adding concurrency and throttling configuration to our Inngest function: ```ts import { createAgent, createNetwork, openai } from '@inngest/agent-kit'; import { createServer } from '@inngest/agent-kit/server'; import { inngest } from './inngest/client'; // network and agent definitions.. const deepResearchNetworkFunction = inngest.createFunction({ id: 'deep-research-network', concurrency: [ { key: "event.data.user_id", limit: 10, }, ], }, { event: "deep-research-network/run" }, async ({ event, step }) => { const { input } = event.data; return network.run(input); }) const server = createServer({ functions: [deepResearchNetworkFunction], }); server.listen(3010, () => console.log("Agent kit running!")); ``` Your AgentKit network will now be limited to 10 concurrent requests per user. The same can be done to add [throttling](https://www.inngest.com/docs/guides/throttling?ref=agentkit-docs-multitenancy), [rate limiting](https://www.inngest.com/docs/guides/rate-limiting?ref=agentkit-docs-multitenancy) or [priority](https://www.inngest.com/docs/guides/priority?ref=agentkit-docs-multitenancy). ## Going further [Section titled “Going further”](#going-further) [Customizing the retries ](/advanced-patterns/retries)Learn how to customize the retries of your multi-steps tools. # Configuring Retries > Configure retries for your AgentKit network Agents and Tool calls. Using AgentKit alongside Inngest enables automatic retries for your AgentKit network Agents and Tools calls. The default retry policy is to retry 4 times with exponential backoff and can be configured by following the steps below. Prerequisites Your AgentKit network [must be configured with Inngest](/getting-started/local-development#1-install-the-inngest-package). ## Configuring Retries [Section titled “Configuring Retries”](#configuring-retries) Configuring a custom retry policy is done by transforming your AgentKit network into an Inngest function. ### Transforming your AgentKit network into an Inngest function [Section titled “Transforming your AgentKit network into an Inngest function”](#transforming-your-agentkit-network-into-an-inngest-function) First, you’ll need to create an Inngest Client: ```ts import { Inngest } from "inngest"; const inngest = new Inngest({ id: "my-agentkit-network", }); ``` Then, transform your AgentKit network into an Inngest function as follows: ```ts import { createAgent, createNetwork, openai } from "@inngest/agent-kit"; import { createServer } from "@inngest/agent-kit/server"; import { inngest } from "./inngest/client"; const deepResearchAgent = createAgent({ name: "Deep Research Agent", tools: [ /* ... */ ], }); const network = createNetwork({ name: "My Network", defaultModel: openai({ model: "gpt-4o" }), agents: [deepResearchAgent], }); const deepResearchNetworkFunction = inngest.createFunction( { id: "deep-research-network", }, { event: "deep-research-network/run", }, async ({ event, step }) => { const { input } = event.data; return network.run(input); } ); const server = createServer({ functions: [deepResearchNetworkFunction], }); server.listen(3010, () => console.log("Agent kit running!")); ``` The `network.run()` is now performed by the Inngest function. Don’t forget to register the function with `createServer`’s `functions` property. ### Configuring a custom retry policy [Section titled “Configuring a custom retry policy”](#configuring-a-custom-retry-policy) We can now configure the capacity by user by adding concurrency and throttling configuration to our Inngest function: ```ts import { createAgent, createNetwork, openai } from '@inngest/agent-kit'; import { createServer } from '@inngest/agent-kit/server'; import { inngest } from './inngest/client'; // network and agent definitions.. const deepResearchNetworkFunction = inngest.createFunction({ id: 'deep-research-network', retries: 1 }, { event: "deep-research-network/run" }, async ({ event, step }) => { const { input } = event.data; return network.run(input); }) const server = createServer({ functions: [deepResearchNetworkFunction], }); server.listen(3010, () => console.log("Agent kit running!")); ``` Your AgentKit network will now retry once on any failure happening during a single execution cycle of your network. ## Going further [Section titled “Going further”](#going-further) [Configuring Multitenancy ](/advanced-patterns/multitenancy)Learn how to configure user-based capacity for your AgentKit network. # Deterministic state routing > State based routing in Agent Networks State based routing is a deterministic approach to managing agent workflows, allowing for more reliable, testable, and maintainable AI agent systems. This documentation covers the core concepts and implementation details based on the Inngest AgentKit framework. ## Core Concepts [Section titled “Core Concepts”](#core-concepts) State based routing models agent workflows as a state machine where: * Each agent has a specific goal within a larger network * The network combines agents to achieve an overall objective, with shared state modified by each agent * The network’s router inspects state and determines which agent should run next * The network runs in a loop, calling the router on each iteration until all goals are met * Agents run with updated conversation history and state on each loop iteration ## Benefits [Section titled “Benefits”](#benefits) Unlike fully autonomous agents that rely on complex prompts to determine their own actions, state based routing: * Makes agent behavior more predictable * Simplifies testing and debugging * Allows for easier identification of failure points * Provides clear separation of concerns between agents ## Implementation Structure [Section titled “Implementation Structure”](#implementation-structure) A state based routing system consists of: 1. State Definition Define structured data that represents the current progress of your workflow: ```typescript export interface AgentState { // files stores all files that currently exist in the repo. files?: string[]; // plan is the plan created by the planning agent. It is optional // as, to begin with, there is no plan. This is set by the planning // agent's tool. plan?: { thoughts: string; plan_details: string; edits: Array<{ filename: string; idea: string; reasoning: string; }>; }, // done indicates whether we're done editing files, and terminates the // network when true. done: boolean; } ``` 2. Network and router implementation Create a router function that inspects state and returns the appropriate agent: ```typescript export const codeWritingNetwork = createNetwork({ name: "Code writing network", agents: [], // We'll add these soon. router: ({ network }): Agent | undefined => { // The router inspects network state to figure out which agent to call next. if (network.state.data.done) { // We're done editing. This is set when the editing agent finishes // implementing the plan. // // At this point, we could hand off to another agent that tests, critiques, // and validates the edits. For now, return undefined to signal that // the network has finished. return; } // By default, there is no plan and we should use the planning agent to read and // understand files. The planning agent's `create_plan` tool modifies state once // it's gathered enough context, which will then cause the router loop to pass // to the editing agent below. if (network.state.data.plan === undefined) { return planningAgent; } // There is a plan, so switch to the editing agent to begin implementing. // // This lets us separate the concerns of planning vs editing, including using differing // prompts and tools at various stages of the editing process. return editingAgent; } } ``` A router has the following definition: ```typescript // T represents the network state's type. type RouterFunction = (args: { input: string; network: NetworkRun; stack: Agent[]; callCount: number; lastResult?: InferenceResult; }) => Promise | undefined>; ``` The router has access to: * `input`: The original input string passed to the network * `network`: The current network run instance with state * `stack`: Array of pending agents to be executed * `callCount`: Number of agent invocations made * `lastResult`: The most recent inference result from the last agent execution 3. Agent Definition Define agents with specific goals and tools. Tools modify the network’s state. For example, a classification agent may have a tool which updates the state’s `classification` property, so that in the next network loop we can determine which new agent to run for the classified request. ```typescript // This agent accepts the network state's type, so that tools are properly typed and can // modify state correctly. export const planningAgent = createAgent({ name: "Planner", description: "Plans the code to write and which files should be edited", tools: [ listFilesTool, createTool({ name: "create_plan", description: "Describe a formal plan for how to fix the issue, including which files to edit and reasoning.", parameters: z.object({ thoughts: z.string(), plan_details: z.string(), edits: z.array( z.object({ filename: z.string(), idea: z.string(), reasoning: z.string(), }) ), }), handler: async (plan, opts: Tool.Options) => { // Store this in the function state for introspection in tracing. await opts.step?.run("plan created", () => plan); if (opts.network) { opts.network.state.data.plan = plan; } }, }), ], // Agent prompts can also inspect network state and conversation history. system: ({ network }) => ` You are an expert Python programmer working on a specific project: ${network?.state.data.repo}. You are given an issue reported within the project. You are planning how to fix the issue by investigating the report, the current code, then devising a "plan" - a spec - to modify code to fix the issue. Your plan will be worked on and implemented after you create it. You MUST create a plan to fix the issue. Be thorough. Think step-by-step using available tools. Techniques you may use to create a plan: - Read entire files - Find specific classes and functions within a file `, }); ``` ## Execution Flow [Section titled “Execution Flow”](#execution-flow) When the network runs: * The network router inspects the current state * It returns an agent to run based on state conditions (or undefined to quit) * The agent executes with access to previous conversation history, current state, and tools * Tools update the state with new information * The router runs again with updated state and conversation history * This continues until the router returns without an agent (workflow complete) ## Best Practices [Section titled “Best Practices”](#best-practices) * **Keep agent goals focused and specific**: Each agent should have a specific goal, and your network should combine agents to solve a larger problem. This makes agents easy to design and test, and it makes routing logic far easier. * **Design state to clearly represent workflow progress**: Moving state out of conversation history and into structured data makes debugging agent workflows simple. * **Use tools to update state in a structured way**: Tools allow you to extract structured data from agents and modify state, making routing easy. * **Implement iteration limits to prevent infinite loops**: The router has a `callCount` parameter allowing you to quit early. ## Error Handling [Section titled “Error Handling”](#error-handling) When deployed to [Inngest](https://www.inngest.com), AgentKit provides built-in error handling: * Automatic retries for failed agent executions * State persistence between retries * Ability to inspect state at any point in the workflow * Tracing capabilities for debugging # Changelog > Recent releases, new features, and fixes. v0.5.0 2025-03-11 * Introducing support for [Grok models](/reference/model-grok) * Adding support for [Gemini latest models](/reference/model-gemini) v0.4.0 2025-03-06 * Add support for model hyper params (ex: temperature, top\_p, etc) * Breaking change: `anthropic()` `max_tokens` options has been moved in `defaultParameters` * Add support OpenAI o3-mini, gpt-4.5, and more * [Integration with Browserbase](/integrations/browserbase) v0.3.0 2025-02-19 * remove `server` export to allow non-Node runtimes * allow tools with no parameters * [Integration with E2B Code Interpreter](/integrations/e2b) v0.2.2 2025-01-29 * Allow specifying [Inngest functions as tools](/advanced-patterns/multi-steps-tools) * Inngest is now an optional dependency v0.2.1 2025-01-16 * Fixed OpenAI adapter to safely parse non-string tool return values for Function calling * Various documentation improvements v0.2.0 2025-01-16 * Added support for Model Context Protocol (MCP) tool calling * Added basic development server * Fixed Anthropic model to ensure proper message handling * Improved code samples and concepts documentation * Added comprehensive quick start guide * Fixed bundling issues * Improved model exports for better discovery * Various cross-platform compatibility improvements v0.1.2 2024-12-19 * Fixed state reference handling in agents * Updated SWEBench example configuration * Various stability improvements v0.1.1 2024-12-19 * Fixed network to agent state propagation in run * Improved git clone handling in SWEBench example * Various minor improvements v0.1.0 2024-12-19 * Initial release of AgentKit * Core framework implementation with lifecycle management * Support for OpenAI and Anthropic models * Network and Agent architecture with state management * ReAct implementation for networks * Tool calling support for agents * Added SWEBench example * Comprehensive documentation structure * Stepless model/network/agent instantiations # Agents > Create agents to accomplish specific tasks with tools inside a network. Agents are the core of AgentKit. Agents are *stateless* entities with a defined goal and an optional set of [Tools](/concepts/tools) that can be used to accomplish a goal. Agents can be called individually or, more powerfully, composed into a [Network](/concepts/networks) with multiple agents that can work together with persisted [State](/concepts/state). At the most basic level, an Agent is a wrapper around a specific provider’s [model](/concepts/models), OpenAI gpt-4 for example, and a set of of [tools](/concepts/tools). ## Creating an Agent [Section titled “Creating an Agent”](#creating-an-agent) To create a simple Agent, all that you need is a `name`, `system` prompt and a `model`. All configuration options are detailed in the `createAgent` [reference](/reference/agent). Here is a simple agent created using the `createAgent` function: ```ts import { createAgent, openai } from '@inngest/agent-kit'; const codeWriterAgent = createAgent({ name: 'Code writer', system: 'You are an expert TypeScript programmer. Given a set of asks, you think step-by-step to plan clean, ' + 'idiomatic TypeScript code, with comments and tests as necessary.' + 'Do not respond with anything else other than the following XML tags:' + '- If you would like to write code, add all code within the following tags (replace $filename and $contents appropriately):' + " $contents", model: openai('gpt-4o-mini'), }); ``` Tip While `system` prompts can be static strings, they are more powerful when they are [dynamic system prompts](#dynamic-system-prompts) defined as callbacks that can add additional context at runtime. Any Agent can be called using `run()` with a user prompt. This performs an inference call to the model with the system prompt as the first message and the input as the user message. ```ts const { output } = codeWriterAgent.run( 'Write a typescript function that removes unnecessary whitespace', ); console.log(output); // [{ role: 'assistant', content: 'function removeUnecessaryWhitespace(...' }] ``` Tip When including your Agent in a Network, a `description` is required. Learn more about [using Agents in Networks here](#using-agents-in-networks). ## Adding tools [Section titled “Adding tools”](#adding-tools) [Tools](/concepts/tools) are functions that extend the capabilities of an Agent. Along with the prompt (see `run()`), Tools are included in calls to the language model through features like OpenAI’s “[function calling](https://platform.openai.com/docs/guides/function-calling)” or Claude’s “[tool use](https://docs.anthropic.com/en/docs/build-with-claude/tool-use).” Tools are defined using the `createTool` function and are passed to agents via the `tools` parameter: ```ts import { createAgent, createTool, openai } from '@inngest/agent-kit'; const listChargesTool = createTool({ name: 'list_charges', description: "Returns all of a user's charges. Call this whenever you need to find one or more charges between a date range.", parameters: z.array( z.object({ userId: z.string(), }), ), handler: async (output, { network, agent, step }) => { // output is strongly typed to match the parameter type. }, }); const supportAgent = createAgent({ name: 'Customer support specialist', system: 'You are an customer support specialist...', model: openai('gpt-3.5-turbo'), tools: [listChargesTool], }); ``` When `run()` is called, any step that the model decides to call is immediately executed before returning the output. Read the “[How agents work](#how-agents-work)” section for additional information. Learn more about Tools in [this guide](/concepts/tools). ## How Agents work [Section titled “How Agents work”](#how-agents-work) Agents themselves are relatively simple. When you call `run()`, there are several steps that happen: 1. **Preparing the prompts** The initial messages are created using the `system` prompt, the `run()` user prompt, and [Network State](/concepts/network-state), if the agent is part of a [Network](/concepts/networks). Note For added control, you can dynamically modify the Agent’s prompts before the next step using the `onStart` [lifecycle hook](#lifecycle-hooks). 2. **Inference call** An inference call is made to the provided [`model`](/concepts/models) using Inngest’s [`step.ai`](https://www.inngest.com/docs/features/inngest-functions/steps-workflows/step-ai-orchestration#step-tools-step-ai). `step.ai` automatically retries on failure and caches the result for durability. The result is parsed into an `InferenceResult` object that contains all messages, tool calls and the raw API response from the model. Note To modify the result prior to calling tools, use the optional `onResponse` [lifecycle hook](#lifecycle-hooks). 3. **Tool calling** If the model decides to call one of the available `tools`, the Tool is automatically called. Note After tool calling is complete, the `onFinish` [lifecycle hook](#lifecycle-hooks) is called with the updated `InferenceResult`. This enables you to modify or inspect the output of the called tools. 4. **Complete** The result is returned to the caller. ### Lifecycle hooks [Section titled “Lifecycle hooks”](#lifecycle-hooks) Agent lifecycle hooks can be used to intercept and modify how an Agent works enabling dynamic control over the system: ```tsx import { createAgent, openai } from '@inngest/agent-kit'; const agent = createAgent({ name: 'Code writer', description: 'An expert TypeScript programmer which can write and debug code.', system: '...', model: openai('gpt-3.5-turbo'), lifecycle: { onStart: async ({ prompt, network: { state }, history }) => { // Dynamically alter prompts using Network state and history. return { prompt, history } }, }, }); ``` As mentioned in the “[How Agents work](#how-agents-work)” section, there are a few lifecycle hooks that can be defined on the Agent’s `lifecycle` options object. * Dynamically alter prompts using Network [State](/concepts/state) or the Network’s history. * Parse output of model after an inference call. Learn more about lifecycle hooks and how to define them in [this reference](/reference/create-agent#lifecycle). ## System prompts [Section titled “System prompts”](#system-prompts) An Agent’s system prompt can be defined as a string or an async callback. When Agents are part of a [Network](/concepts/networks), the Network [State](/concepts/state) is passed as an argument to create dynamic prompts, or instructions, based on history or the outputs of other Agents. ### Dynamic system prompts [Section titled “Dynamic system prompts”](#dynamic-system-prompts) Dynamic system prompts are very useful in agentic workflows, when multiple models are called in a loop, prompts can be adjusted based on network state from other call outputs. ```ts const agent = createAgent({ name: 'Code writer', description: 'An expert TypeScript programmer which can write and debug code.', // The system prompt can be dynamically created at runtime using Network state: system: async ({ network }) => { // A default base prompt to build from: const basePrompt = 'You are an expert TypeScript programmer. ' + 'Given a set of asks, think step-by-step to plan clean, ' + 'idiomatic TypeScript code, with comments and tests as necessary.'; // Inspect the Network state, checking for existing code saved as files: const files: Record | undefined = network.state.data.files; if (!files) { return basePrompt; } // Add the files from Network state as additional context automatically let additionalContext = 'The following code already exists:'; for (const [name, content] of Object.entries(files)) { additionalContext += `${content}`; } return `${basePrompt} ${additionalContext}`; }, }); ``` ### Static system prompts [Section titled “Static system prompts”](#static-system-prompts) Agents may also just have static system prompts which are more useful for simpler use cases. ```ts const codeWriterAgent = createAgent({ name: 'Copy editor', system: `You are an expert copy editor. Given a draft article, you provide ` + `actionable improvements for spelling, grammar, punctuation, and formatting.`, model: openai('gpt-3.5-turbo'), }); ``` ## Using Agents in Networks [Section titled “Using Agents in Networks”](#using-agents-in-networks) Agents are the most powerful when combined into [Networks](/concepts/networks). Networks include [state](/concepts/state) and [routers](/concepts/routers) to create stateful workflows that can enable Agents to work together to accomplish larger goals. ### Agent descriptions [Section titled “Agent descriptions”](#agent-descriptions) Similar to how [Tools](/concepts/tools) have a `description` that enables an LLM to decide when to call it, Agents also have an `description` parameter. This is *required* when using Agents within Networks. Here is an example of an Agent with a description: ```ts const codeWriterAgent = createAgent({ name: 'Code writer', description: 'An expert TypeScript programmer which can write and debug code. Call this when custom code is required to complete a task.', system: `...`, model: openai('gpt-3.5-turbo'), }); ``` # Deployment > Deploy your AgentKit networks to production. Deploying an AgentKit network to production is straightforward but there are a few things to consider: * **Scalability**: Your Network Agents rely on tools which interact with external systems. You’ll need to ensure that your deployment environment can scale to handle the requirements of your network. * **Reliability**: You’ll need to ensure that your AgentKit network can handle failures and recover gracefully. * **Multitenancy**: You’ll need to ensure that your AgentKit network can handle multiple users and requests concurrently without compromising on performance or security. All the above can be easily achieved by using Inngest alongside AgentKit. By installing the Inngest SDK, your AgentKit network will automatically benefit from: * [**Multitenancy support**](/advanced-patterns/multitenancy) with fine grained concurrency and throttling configuration * **Retrieable and [parallel tool calls](/advanced-patterns/retries)** for reliable and performant tool usage * **LLM requests offloading** to improve performance and reliability for Serverless deployments * **Live and detailed observability** with step-by-step traces including the Agents inputs/outputs and token usage You will find below instructions to configure your AgentKit network deployment with Inngest. ## Deploying your AgentKit network with Inngest [Section titled “Deploying your AgentKit network with Inngest”](#deploying-your-agentkit-network-with-inngest) Deploying your AgentKit network with Inngest to benefit from automatic retries, LLM requests offloading and live observability only requires a few steps: ### 1. Install the Inngest SDK [Section titled “1. Install the Inngest SDK”](#1-install-the-inngest-sdk) * npm ```shell npm install inngest ``` * pnpm ```shell pnpm install inngest ``` * yarn ```shell yarn add inngest ``` ### 2. Serve your AgentKit network over HTTP [Section titled “2. Serve your AgentKit network over HTTP”](#2-serve-your-agentkit-network-over-http) Update your AgentKit network to serve over HTTP as follows: ```ts import { createNetwork } from '@inngest/agent-kit'; import { createServer } from '@inngest/agent-kit/server'; const network = createNetwork({ name: 'My Network', agents: [/* ... */], }); const server = createServer({ networks: [network], }); server.listen(3010, () => console.log("Agent kit running!")); ``` ### 3. Deploy your AgentKit network [Section titled “3. Deploy your AgentKit network”](#3-deploy-your-agentkit-network) **Configuring environment variables** [Create an Inngest account](https://www.inngest.com/?ref=agentkit-docs-deployment) and open the top right menu to access your Event Key and Signing Key: ![Inngest Event Key and Signing Key](/graphics/concepts/deployment/inngest-event-and-signing-keys.png) Then configure the following environment variables into your deployment environment (*ex: AWS, Vercel, GCP*): * `INNGEST_API_KEY`: Your Event Key * `INNGEST_SIGNING_KEY`: Your Signing Key **Deploying your AgentKit network** You can now deploy your AgentKit network to your preferred cloud provider. Once deployed, copy the deployment URL for the final configuration step. ### 4. Sync your AgentKit network with the Inngest Platform [Section titled “4. Sync your AgentKit network with the Inngest Platform”](#4-sync-your-agentkit-network-with-the-inngest-platform) On your Inngest dashboard, click on the “Sync new app” button at the top right of the screen. Then, paste the deployment URL into the “App URL” by adding `/api/inngest` to the end of the URL: ![Inngest Event Key and Signing Key](/graphics/concepts/deployment/inngest-sync-app.png) You sync is failing? Read our [troubleshooting guide](https://www.inngest.com/docs/apps/cloud?ref=agentkit-docs-deployment#troubleshooting) for more information. Once the sync succeeds, you can navigate to the *Functions* tabs where you will find your AgentKit network: ![Inngest Event Key and Signing Key](/graphics/concepts/deployment/inngest-functions-tab.png) Your AgentKit network can now be triggered manually from the Inngest Dashboard or [from your app using `network.run()`](/concepts/networks). ## Configuring Multitenancy and Retries [Section titled “Configuring Multitenancy and Retries”](#configuring-multitenancy-and-retries) [Multitenancy ](/advanced-patterns/multitenancy)Configure usage limits based on users or organizations. [Retries ](/advanced-patterns/retries)Learn how to configure retries for your AgentKit Agents and Tools. # History > Learn how to persist conversations for your agents and networks ## Overview [Section titled “Overview”](#overview) AgentKit enables persistent conversations that maintain context across multiple runs. By implementing a **History Adapter**, you can connect your agents and networks to any database or storage solution, allowing conversations to resume exactly where they left off. A History Adapter is a configuration object that bridges AgentKit’s execution lifecycle with your database. It tells AgentKit how to: 1. **Create** new conversation threads 2. **Load** existing conversation history 3. **Save** new messages and results AgentKit is database-agnostic. You can use PostgreSQL, MongoDB, Redis, or any storage solution by implementing the `HistoryConfig` interface. The adapter is passed to `createAgent()` or `createNetwork()` and AgentKit automatically calls your adapter’s methods at the appropriate times during execution. ### HistoryConfig Interface [Section titled “HistoryConfig Interface”](#historyconfig-interface) The `HistoryConfig` interface has four optional methods. Below is an expanded view of the interface showing the context and parameters passed to each method. ```typescript import type { State, NetworkRun, AgentResult, GetStepTools, StateData, } from "@inngest/agent-kit"; interface HistoryConfig { /** * Creates a new conversation thread or ensures it exists. * Invoked at the start of a run to initialize the thread. */ createThread?: (ctx: { state: State; // The current state, including your custom data input: string; // The user's input string network?: NetworkRun; // The network instance (if applicable) step?: GetStepTools; // Inngest step tools for durable execution }) => Promise<{ threadId: string }>; /** * Retrieves conversation history from your database. * Invoked after thread initialization if no history is provided by the client. */ get?: (ctx: { threadId?: string; // The ID of the conversation thread state: State; input: string; network: NetworkRun; step?: GetStepTools; }) => Promise; /** * Saves the user's message at the beginning of a run. * Invoked immediately after thread initialization, before any agents run. */ appendUserMessage?: (ctx: { threadId?: string; userMessage: { id: string; // Canonical, client-generated message ID content: string; role: "user"; timestamp: Date; }; state: State; input: string; network: NetworkRun; step?: GetStepTools; }) => Promise; /** * Saves new agent results to your database after a run. * Invoked at the end of a successful agent or network run. */ appendResults?: (ctx: { threadId?: string; newResults: AgentResult[]; // The new results generated during this run state: State; input: string; network: NetworkRun; step?: GetStepTools; }) => Promise; } ``` #### `createThread` [Section titled “createThread”](#createthread) * Creates a new conversation thread in your database or ensures an existing thread is present * Invoked at the start of a run to initialize the thread * **Important**: If a `threadId` already exists in the state, your adapter should upsert (insert or update) to ensure the thread exists in storage * Returns an object with the `threadId` #### `get` [Section titled “get”](#get) * Retrieves conversation history from your database * Invoked after thread initialization, but **only if**: * A `threadId` is present in the state * The client didn’t provide `results` or `messages` * The thread was not just created in this run (client provided the threadId) * Returns an array of `AgentResult[]` representing the conversation history * **Recommended**: Include both user messages and agent results by converting user messages to `AgentResult` objects with `agentName: "user"` to preserve conversation order #### `appendUserMessage` [Section titled “appendUserMessage”](#appendusermessage) * Saves the user’s message immediately at the beginning of a run * Invoked after thread initialization but before any agents execute * Ensures user intent is captured even if the agent run fails (enables “regenerate” workflows) * Receives the user’s message with a canonical, client-generated ID for idempotency #### `appendResults` [Section titled “appendResults”](#appendresults) * Saves new agent results to your database after a network or agent run * Invoked at the end of a successful agent or network run * Receives only the *new* results generated during this run (AgentKit automatically filters out historical results to prevent duplicates) *** ## Usage [Section titled “Usage”](#usage) Here’s a complete example of creating a network with history persistence: ```typescript import { createNetwork, createAgent, createState, openai, } from "@inngest/agent-kit"; import { db } from "./db"; // Your database client // Define your history adapter with all four methods const conversationHistoryAdapter: HistoryConfig = { // 1. Create new conversation threads (or ensure they exist) createThread: async ({ state, input }) => { // If a threadId already exists, upsert to ensure it's in the database if (state.threadId) { await db.thread.upsert({ where: { id: state.threadId }, update: { updatedAt: new Date() }, create: { id: state.threadId, userId: state.data.userId, title: input.slice(0, 50), createdAt: new Date(), }, }); return { threadId: state.threadId }; } // Otherwise, create a new thread const thread = await db.thread.create({ data: { userId: state.data.userId, title: input.slice(0, 50), // First 50 chars as title createdAt: new Date(), }, }); return { threadId: thread.id }; }, // 2. Load conversation history (including user messages) get: async ({ threadId }) => { if (!threadId) return []; const messages = await db.message.findMany({ where: { threadId }, orderBy: { createdAt: "asc" }, }); // Transform ALL messages (user + agent) to AgentResult format // This preserves the complete conversation order return messages.map((msg) => { if (msg.role === "user") { // Convert user messages to AgentResult with agentName: "user" return new AgentResult( "user", [ { type: "text" as const, role: "user" as const, content: msg.content, stop_reason: "stop", }, ], [], new Date(msg.createdAt) ); } else { // Return agent results return new AgentResult( msg.agentName, [ { type: "text" as const, role: "assistant" as const, content: msg.content, }, ], [], new Date(msg.createdAt) ); } }); }, // 3. Save user message immediately (before agents run) appendUserMessage: async ({ threadId, userMessage }) => { if (!threadId) return; await db.message.create({ data: { messageId: userMessage.id, // Use canonical client-generated ID threadId, role: "user", content: userMessage.content, createdAt: userMessage.timestamp, }, }); }, // 4. Save agent results after the run appendResults: async ({ threadId, newResults }) => { if (!threadId) return; // Save only agent responses (user message already saved) for (const result of newResults) { const content = result.output .filter((msg) => msg.type === "text") .map((msg) => msg.content) .join("\n"); await db.message.create({ data: { messageId: result.id || crypto.randomUUID(), // Use result.id if available threadId, role: "assistant", agentName: result.agentName, content, checksum: result.checksum, // For idempotency createdAt: result.createdAt, }, }); } }, }; ``` *** Once you’ve created your adapter, pass it to the `history` property when creating an agent or network: * Agent ```typescript import { createAgent } from "@inngest/agent-kit"; import { postgresHistoryAdapter } from "./my-postgres-adapter"; const chatAgent = createAgent({ name: "chat-agent", system: "You are a helpful assistant.", history: postgresHistoryAdapter, // Add your adapter here }); // Now the agent will automatically persist conversations await chatAgent.run("Hello!", { state: createState({ userId: "user123" }, { threadId: "thread-456" }), }); ``` * Network ```typescript import { createNetwork, createAgent } from "@inngest/agent-kit"; import { postgresHistoryAdapter } from "./my-postgres-adapter"; const chatAgent = createAgent({ name: "chat-agent", system: "You are a helpful assistant.", }); const chatNetwork = createNetwork({ name: "Chat Network", agents: [chatAgent], history: postgresHistoryAdapter, // Add your adapter here }); // The entire network will use persistent conversations await chatNetwork.run("Hello!"); ``` *** ## Persistence Patterns [Section titled “Persistence Patterns”](#persistence-patterns) AgentKit supports two distint patterns for managing conversation history. ### Server-Authoritative [Section titled “Server-Authoritative”](#server-authoritative) The client sends a message with a `threadId`. AgentKit automatically loads the full conversation context from your database before the network runs. ```typescript // Client sends just the threadId const state = createState( { userId: "user123" }, { threadId: "existing-thread-id" } ); await chatNetwork.run("Continue our conversation", { state }); // AgentKit calls history.get() to load full context for all agents ``` **Use case**: Perfect for restoring conversations after page refresh or when opening the app on a new device. ### Client-Authoritative (Performance Optimized) [Section titled “Client-Authoritative (Performance Optimized)”](#client-authoritative-performance-optimized) The client maintains conversation state locally and sends the complete history with each request. AgentKit detects this and skips the database read for better performance. ```typescript // Client sends the full conversation history const state = createState( { userId: "user123" }, { threadId: "thread-id", results: previousConversationResults, // Full history from client } ); await chatNetwork.run("New message", { state }); // AgentKit skips history.get() call - faster performance! // Still calls appendUserMessage() and appendResults() to save new messages ``` **Use case**: Ideal for interactive chat applications where the frontend maintains conversation state and fetches messages from an existing/separate API **Note**: Providing either `results` or `messages` to `createState` will disable the `history.get()` call, enabling this client-authoritative pattern. ### Server/Client Hybrid Pattern [Section titled “Server/Client Hybrid Pattern”](#serverclient-hybrid-pattern) You can combine the Server-Authoritative and Client-Authoritative patterns for an optimal user experience. This hybrid approach allows for fast initial conversation loading and high-performance interactive chat. 1. **Initial Load (Server-Authoritative):** When a user opens a conversation thread, the client sends only the `threadId`. AgentKit fetches the history from your database using `history.get()`. The application then hydrates the client-side state with this history. 2. **Interactive Session (Client-Authoritative):** For all subsequent requests within the session, the client sends the full, up-to-date history (`results` or `messages`) along with the `threadId`. AgentKit detects the client-provided history and skips the database read, resulting in a faster response. **Use case**: Ideal for interactive chat applications where the frontend maintains conversation state but lets AgentKit fetch messages via their history adapter ## How Thread IDs Are Managed [Section titled “How Thread IDs Are Managed”](#how-thread-ids-are-managed) AgentKit offers a flexible system for managing conversation thread IDs, ensuring that history is handled correctly whether you’re starting a new conversation or continuing an existing one. Here’s how AgentKit determines which `threadId` to use: ### Thread Initialization Flow [Section titled “Thread Initialization Flow”](#thread-initialization-flow) | Scenario | `threadId` provided? | `createThread` exists? | Behavior | | ------------------------------- | -------------------- | ---------------------- | ------------------------------------------------------------ | | **Resume existing thread** | Yes | Yes | Calls `createThread` to upsert/ensure thread exists in DB | | **Resume existing thread** | Yes | No | Uses provided `threadId` directly | | **New conversation** | No | Yes | Calls `createThread` to create new thread and get `threadId` | | **New conversation (fallback)** | No | No (but `get` exists) | Auto-generates UUID as `threadId` | 1. **Explicit `threadId` with `createThread`:** When you provide a `threadId` and your adapter has a `createThread` method, AgentKit calls `createThread` to ensure the thread exists in your database. Your adapter should implement an **upsert** pattern (insert if new, update if exists) to handle both new and existing threads gracefully. ```typescript // Continue a specific, existing conversation const state = createState( { userId: "user-123" }, { threadId: "existing-thread-id-123" } ); await network.run("Let's pick up where we left off.", { state }); // createThread is called to ensure thread exists in DB // Then history.get() loads the conversation history ``` 2. **Automatic Creation via `createThread`:** If you don’t provide a `threadId`, AgentKit checks if your history adapter has a `createThread` method. If so, AgentKit calls it to create a new conversation thread in your database. Your `createThread` function is responsible for generating and returning the new unique `threadId`. This is the recommended approach for starting new conversations, as it ensures a record is created in your backend from the very beginning. ```typescript // Start a new conversation const state = createState({ userId: "user-123" }); await network.run("Hello!", { state }); // createThread is called to create a new thread // state.threadId is set to the new thread ID ``` 3. **Automatic Generation (Fallback):** In cases where you don’t provide a `threadId` and your history adapter does *not* have a `createThread` method but *does* have a `get` method, AgentKit provides a fallback. It will automatically generate a standard UUID and assign it as the `threadId` for the current run. This convenience ensures the conversation can proceed with a unique identifier for saving and loading history, even without an explicit creation step. ```typescript // Fallback: UUID is generated automatically const state = createState({ userId: "user-123" }); await network.run("Hello!", { state }); // state.threadId is set to a new UUID // appendUserMessage and appendResults can use this ID ``` ## Best Practices [Section titled “Best Practices”](#best-practices) Implement Idempotency with Message IDs and Checksums Use unique constraints on `message_id` and `checksum` to prevent duplicate messages during retries or streaming scenarios. ```sql CREATE TABLE messages ( id SERIAL PRIMARY KEY, message_id UUID NOT NULL, thread_id UUID NOT NULL, message_type TEXT NOT NULL, -- 'user' or 'agent' content TEXT, checksum TEXT NOT NULL, created_at TIMESTAMP DEFAULT NOW(), UNIQUE(thread_id, message_id), -- Prevent duplicate message IDs UNIQUE(thread_id, checksum) -- Prevent duplicate content ); ``` ```typescript appendUserMessage: async ({ threadId, userMessage }) => { await db.message.create({ data: { messageId: userMessage.id, // Use canonical client ID threadId, content: userMessage.content, checksum: generateChecksum(userMessage), }, }); }, appendResults: async ({ threadId, newResults }) => { for (const result of newResults) { await db.message.create({ data: { messageId: result.id || crypto.randomUUID(), threadId, checksum: result.checksum, // Built-in checksum // ... other fields }, }); } } ``` Leverage Inngest’s Durable Steps Wrap database operations in `step.run()` for automatic retries and durability. ```typescript appendUserMessage: async ({ threadId, userMessage, step }) => { if (step) { return await step.run("save-user-message", async () => { return await db.saveMessage(threadId, userMessage); }); } return await db.saveMessage(threadId, userMessage); } ``` Handle Missing Threads Gracefully If a thread doesn’t exist, return an empty array rather than throwing an error. ```typescript get: async ({ threadId }) => { if (!threadId) return []; const messages = await db.getMessages(threadId); return messages || []; // Handle null/undefined gracefully } ``` Index Your Database Properly Ensure you have indexes on key columns for fast queries. ```sql CREATE INDEX idx_messages_thread_id ON messages(thread_id); CREATE INDEX idx_messages_created_at ON messages(created_at); CREATE INDEX idx_messages_type ON messages(message_type); CREATE INDEX idx_messages_message_id ON messages(message_id); ``` Return Complete Conversation History Include both user messages and agent results in your `get()` method to preserve conversation order. ```typescript get: async ({ threadId }) => { const messages = await db.message.findMany({ where: { threadId }, orderBy: { createdAt: "asc" }, }); // Convert ALL messages (user + agent) to AgentResult format return messages.map((msg) => { if (msg.role === "user") { return new AgentResult("user", [ { type: "text", role: "user", content: msg.content } ], [], new Date(msg.createdAt)); } else { return new AgentResult(msg.agentName, [ { type: "text", role: "assistant", content: msg.content } ], [], new Date(msg.createdAt)); } }); } ``` Implement Upsert in createThread Handle both new and existing threads gracefully by implementing an upsert pattern. ```typescript createThread: async ({ state }) => { if (state.threadId) { // Upsert: ensure existing thread is in DB await db.thread.upsert({ where: { id: state.threadId }, update: { updatedAt: new Date() }, create: { id: state.threadId, userId: state.data.userId }, }); return { threadId: state.threadId }; } // Create new thread const thread = await db.thread.create({ data: { userId: state.data.userId }, }); return { threadId: thread.id }; } ``` ## Future Enhancements [Section titled “Future Enhancements”](#future-enhancements) The history system provides a foundation for advanced features to be released in the coming future including: * **Database Adapters**: Pre-built adapters for popular databases (coming soon) * **Progressive Summarization**: Automatic conversation compression for long threads * **Search & Retrieval**: Semantic search across conversation history ## Complete Example [Section titled “Complete Example”](#complete-example) Check out the [AgentKit Starter](https://github.com/inngest/agent-kit/tree/main/examples/agentkit-starter) for a complete implementation featuring: * PostgreSQL history adapter * ChatGPT-style UI with thread management * Real-time streaming responses * Both server and client-authoritative patterns The starter includes everything you need to build a conversational AI application with persistent history. # Memory > Learn how to give your agents long-term, reflective memory using Mem0. ## Overview [Section titled “Overview”](#overview) AgentKit allows you to equip your agents with long-term memory, enabling them to recall past interactions, learn user preferences, and maintain context across conversations. By integrating with [Mem0](https://docs.mem0.ai/overview), you can build sophisticated agents that offer personalized and context-aware experiences. A key advantage of combining Mem0 with AgentKit is the power of Inngest for handling memory operations. When an agent needs to create, update, or delete a memory, it can send an event to Inngest for durable background processing. This means: ### Faster Responses [Section titled “Faster Responses”](#faster-responses) Your agent can respond to the user immediately, without waiting for database writes to complete. ### Durable Background Processing [Section titled “Durable Background Processing”](#durable-background-processing) The memory operation runs reliably in the background as a separate, durable Inngest function. If it fails, Inngest automatically retries it. ## Memory Tools [Section titled “Memory Tools”](#memory-tools) To empower your agent with memory, you need to provide it with tools. How you design these tools can significantly impact your agent’s behavior, performance, and reliability. AgentKit supports multiple patterns for memory tools, allowing you to choose the best fit for your use case. The core idea is to abstract memory operations (create, read, update, delete) into tools that an agent can call. These tools can then use Inngest to perform the actual database writes asynchronously, ensuring the agent remains responsive. ```typescript // From examples/mem0-memory/memory-tools.ts const createMemoriesTool = createTool({ name: "create_memories", description: "Save one or more new pieces of information to memory.", parameters: z.object({ statements: z .array(z.string()) .describe("The pieces of information to memorize."), }), handler: async ({ statements }, { step }) => { // 1. Send an event to an Inngest function for background processing await step?.sendEvent("send-create-memories-event", { name: "app/memories.create", data: { statements, }, }); // 2. Return immediately to the user return `I have scheduled the creation of ${statements.length} new memories.`; }, }); // A separate Inngest function handles the event const addMemoriesFn = inngest.createFunction( { id: "add-memories" }, { event: "app/memories.create" }, async ({ event }) => { // 3. Perform the durable memory operation const { statements } = event.data; await mem0.add(statements.map((s) => ({ role: "user", content: s }))); return { status: `Added ${statements.length} memories.` }; } ); ``` Let’s explore two common patterns for designing and integrating these tools into agents. ### Pattern 1: Granular, Single-Purpose Tools [Section titled “Pattern 1: Granular, Single-Purpose Tools”](#pattern-1-granular-single-purpose-tools) This pattern involves creating a distinct tool for each memory operation: * `create_memories`: Adds new information. * `recall_memories`: Retrieves existing information. * `update_memories`: Corrects or changes existing information. * `delete_memories`: Removes information. This gives the agent fine-grained control, but requires it to make more decisions and more tool calls. Here’s how you might define the `recall_memories` and `create_memories` tools: ```typescript // From examples/voice-assistant/tools/memory.ts const recallMemoriesTool = createTool({ name: "recall_memories", description: `Recall memories relevant to one or more queries. Can run multiple queries in parallel.`, parameters: z.object({ queries: z .array(z.string()) .describe( `The questions to ask your memory to find relevant information.` ), }), handler: async ({ queries }, { step, network }) => { // ... implementation to search memories ... }, }); const createMemoriesTool = createTool({ name: "create_memories", description: "Save one or more new pieces of information to memory.", parameters: z.object({ statements: z .array(z.string()) .describe("The pieces of information to memorize."), }), handler: async ({ statements }, { step }) => { await step?.sendEvent("send-create-memories-event", { name: "app/memories.create", data: { statements }, }); return `I have scheduled the creation of ${statements.length} new memories.`; }, }); ``` This approach is used in the **Autonomous Agent** pattern described below, where a single, powerful LLM is prompted to reason about which of the specific tools to use at each turn. ### Pattern 2: Consolidated Tools [Section titled “Pattern 2: Consolidated Tools”](#pattern-2-consolidated-tools) This pattern simplifies the agent’s job by consolidating write operations into a single tool. * `recall_memories`: Same as above, for reading. * `manage_memories`: A single tool that handles creating, updating, *and* deleting memories in one atomic action. This reduces the number of tools the agent needs to know about and can make its behavior more predictable. It’s particularly effective in structured, multi-agent workflows. The `manage_memories` tool can accept lists of creations, updates, and deletions, and then send corresponding events to Inngest. ```typescript // From examples/voice-assistant/tools/memory.ts const manageMemoriesTool = createTool({ name: "manage_memories", description: `Create, update, and/or delete memories in a single atomic operation. This is the preferred way to modify memories.`, parameters: z.object({ creations: z .array(z.string()) .optional() .describe("A list of new statements to save as memories."), updates: z .array( z.object({ id: z.string().describe("The unique ID of the memory to update."), statement: z .string() .describe("The new, corrected information to save."), }) ) .optional() .describe("A list of memories to update."), deletions: z .array( z.object({ id: z.string().describe("The unique ID of the memory to delete."), }) ) .optional() .describe("A list of memories to delete."), }), handler: async ({ creations, updates, deletions }, { step }) => { // Send events to Inngest for background processing if (creations?.length) { await step?.sendEvent("create-memories", { name: "app/memories.create", data: { statements: creations }, }); } if (updates?.length) { await step?.sendEvent("update-memories", { name: "app/memories.update", data: { updates }, }); } if (deletions?.length) { await step?.sendEvent("delete-memories", { name: "app/memories.delete", data: { deletions }, }); } return `Scheduled memory operations.`; }, }); ``` This consolidated `manage_memories` tool is a perfect fit for a **multi-agent network**, where a dedicated “Memory Updater” agent has the single, clear responsibility of calling this tool at the end of a conversation - only running once with a tool that can emit many events / memory operations. *** ## Deterministic vs Non-Deterministic Memory [Section titled “Deterministic vs Non-Deterministic Memory”](#deterministic-vs-non-deterministic-memory) There are two primary patterns for integrating memory into your agents: 1. **Autonmous Agent w/ Tools (Non-Deterministic):** A single, powerful agent is given memory-related tools and decides for itself when and how to use them based on its system prompt and the conversation. This approach offers maximum flexibility and autonomy. 2. **Multi-Agent or Lifecycle-based (Deterministic):** The process is broken down into a structured sequence of specialized agents (e.g., one for retrieval, one for responding, one for updating memory), orchestrated by a code-based router. This approach provides predictability and control. Let’s explore both! ### Pattern 1: Autonomous Agent with Memory Tools [Section titled “Pattern 1: Autonomous Agent with Memory Tools”](#pattern-1-autonomous-agent-with-memory-tools) In this setup, a single agent is responsible for all tasks. Its system prompt instructs it to follow a **recall-reflect-respond** process. The agent uses its own reasoning (powered by the LLM) to decide which memory tool to use, making the flow non-deterministic. #### Example Agent [Section titled “Example Agent”](#example-agent) Here is an agent designed to manage its own memory. Note the detailed system prompt guiding its behavior. ```typescript // From examples/mem0-memory/index.ts const mem0Agent = createAgent({ name: "reflective-mem0-agent", description: "An agent that can reflect on and manage its memories using mem0.", system: ` You are an assistant with a dynamic, reflective memory. You must actively manage your memories to keep them accurate and strategically for search queries to retrieve the most relevant memories related to the user and their query. On every user interaction, you MUST follow this process: 1. **RECALL**: Use the 'recall_memories' tool with a list of queries relevant to the user's input to get context. 2. **ANALYZE & REFLECT**: - Compare the user's new statement with the memories you recalled. - If there are direct contradictions, you MUST use the 'update_memories' tool to correct the old memories. - If old memories are now irrelevant or proven incorrect based on the discussion, you MUST use the 'delete_memories' tool. - If this is brand new information that doesn't conflict, you may use the 'create_memories' tool. 3. **RESPOND**: Never make mention to the user of any memory operations you have executed. `, tools: [ createMemoriesTool, recallMemoriesTool, updateMemoriesTool, deleteMemoriesTool, ], model: openai({ model: "gpt-4o" }), }); ``` #### Execution Flow [Section titled “Execution Flow”](#execution-flow) The agent’s internal monologue drives the process, deciding which tools to call in sequence. ```mermaid sequenceDiagram participant U as User participant AK as AgentKit Server participant A as Autonomous Agent participant T as Memory Tools participant I as Inngest participant M as Mem0 SDK U->>AK: User Input AK->>A: agent.run(input) A->>T: recall_memories(...) Note over T: Agent generates multiple
search queries T->>M: Parallel .search() calls M-->>T: Returns memories T-->>A: Returns unique memories A->>A: ANALYZE & REFLECT A->>T: update_memories(...) or delete_memories(...) etc. T->>I: sendEvent('app/memories.update') Note over I,M: Background Processing I-->>M: Listens for event I-->>M: .update(id, statement) M-->>I: Success T-->>A: "Scheduled" A->>A: FORMULATE RESPONSE A-->>AK: Final response AK-->>U: Streams response ``` Pros: * **Flexibility & Autonomy:** The agent can handle unforeseen scenarios by reasoning about which tools to use. * **Simpler Setup:** Requires only one agent and a comprehensive prompt. Cons: * **Unpredictability:** The agent’s behavior can be inconsistent. It might get stuck in loops, call tools in the wrong order, or fail to answer the user’s question directly. * **Complex Prompting:** The system prompt must be carefully engineered to cover all cases, which can be brittle and hard to maintain. ### Pattern 2: Multi-Agent Network for Memory [Section titled “Pattern 2: Multi-Agent Network for Memory”](#pattern-2-multi-agent-network-for-memory) To address the unpredictability of a single autonomous agent, you can use a deterministic, multi-agent network. The workflow is broken down into a sequence of specialized agents orchestrated by a [code-based router](https://agentkit.inngest.com/concepts/routers#code-based-routers-supervised-routing). #### Example Agents & Router [Section titled “Example Agents & Router”](#example-agents--router) The process is divided into three distinct steps, each handled by a dedicated agent: 1. **Memory Retrieval Agent**: Its sole job is to use the `recall_memories` tool. 2. **Personal Assistant Agent**: Has no tools. Its only job is to synthesize the final answer for the user based on the retrieved memories and history. 3. **Memory Updater Agent**: Reviews the *entire* conversation and uses a `manage_memories` tool to perform all necessary creations, updates, and deletions in one go. ```typescript // From examples/mem0-memory/multi-agent.ts // 1. Retrieval Agent const memoryRetrievalAgent = createAgent({ name: "memory-retrieval-agent", description: "Retrieves relevant memories based on the user query.", system: `Your only job is to use the 'recall_memories' tool. ...`, tools: [recallMemoriesTool], // ... }); // 2. Assistant Agent const personalAssistantAgent = createAgent({ name: "personal-assistant-agent", description: "A helpful personal assistant that answers user questions.", system: `Answer the user's question based on the conversation history...`, // No tools // ... }); // 3. Updater Agent const memoryUpdaterAgent = createAgent({ name: "memory-updater-agent", description: "Reflects on the conversation and updates memories.", system: `Analyze the entire conversation history... you MUST use the 'manage_memories' tool...`, tools: [manageMemoriesTool], // ... }); // The Router enforces the sequence const multiAgentMemoryNetwork = createNetwork({ name: "multi-agent-memory-network", agents: [memoryRetrievalAgent, personalAssistantAgent, memoryUpdaterAgent], router: async ({ callCount }) => { if (callCount === 0) return memoryRetrievalAgent; if (callCount === 1) return personalAssistantAgent; if (callCount === 2) return memoryUpdaterAgent; return undefined; // Stop the network }, // ... }); ``` #### Execution Flow [Section titled “Execution Flow”](#execution-flow-1) The router guarantees a predictable, step-by-step execution path. ```mermaid sequenceDiagram participant U as User participant AK as AgentKit Server participant R as Router participant RA as Retrieval Agent participant PA as Assistant Agent participant UA as Updater Agent participant T as Memory Tools participant I as Inngest participant M as Mem0 SDK U->>AK: User Input AK->>R: network.run(input) R->>RA: (callCount == 0) RA->>T: recall_memories(...) T-->>RA: returns unique memories R->>PA: (callCount == 1) PA->>PA: Synthesizes answer PA-->>R: Final answer R->>UA: (callCount == 2) UA->>T: manage_memories(...) T->>I: sendEvent (create/update/delete) Note over I,M: Background Processing I-->>M: Handles memory ops M-->>I: Success T-->>UA: "Scheduled" R->>AK: Network finished AK-->>U: Streams final answer ``` Pros: * **Predictability & Control:** The workflow is explicit and reliable. Each agent has a single, well-defined responsibility. * **Maintainability:** It’s easier to debug and modify a specific part of the process without affecting the others. Cons: * **More Boilerplate:** Requires defining multiple agents and a router, which can be more verbose for simple use cases. * **Less Flexible:** The rigid structure may not adapt as well to unexpected conversational turns compared to an autonomous agent which can determine on its own - when memories should be retrieved. *** ## Advanced Patterns [Section titled “Advanced Patterns”](#advanced-patterns) ### State-Based Memory Retrieval / Routing [Section titled “State-Based Memory Retrieval / Routing”](#state-based-memory-retrieval--routing) Instead of `callCount`, you can use the network state to create more flexible and explicit routing logic. This is powerful when different agents have different memory needs. ```typescript // Define your network state interface interface NetworkState { memoriesRetrieved?: boolean; assistantResponded?: boolean; } // Use state-based routing const network = createNetwork({ //... router: async ({ network }) => { const state = network.state.data; if (!state.memoriesRetrieved) { // In a real implementation, the agent's tool would set this state // For example: network.state.data.memoriesRetrieved = true; return memoryRetrievalAgent; } if (!state.assistantResponded) { return personalAssistantAgent; } return memoryUpdaterAgent; }, }); ``` ### Lifecycle Integration [Section titled “Lifecycle Integration”](#lifecycle-integration) For a more seamless approach, you can integrate memory operations directly into an agent’s or network’s lifecycle hooks, avoiding the need for explicit memory tools. * **`onStart`**: Fetch memories *before* an agent runs and inject them into the prompt. * **`onFinish`**: Analyze the conversation *after* an agent has run and schedule memory updates. ```typescript const agentWithLifecycleMemory = createAgent({ // ... agent config ... lifecycle: { async onStart({ input, prompt }) { // 1. Fetch memories using a custom utility const memories = await recallMemoriesForAgent(input); // 2. Add memories to the prompt for context const memoryMessages = formatMemoriesAsMessages(memories); prompt.push(...memoryMessages); return { prompt, stop: false }; }, async onFinish({ result, network }) { // 3. Analyze the full conversation to decide on memory operations. await analyzeAndManageMemories(result, network.state.data); }, }, }); ``` ## Complete Example [Section titled “Complete Example”](#complete-example) Check out the [Mem0 Memory Example](https://github.com/inngest/agent-kit/tree/main/examples/mem0-memory) for a complete implementation featuring: * Both single-agent and multi-agent patterns. * Asynchronous memory operations with Inngest. * A local Qdrant vector store setup with Docker. # Models > Leverage different provider's models across Agents. Within AgentKit, models are adapters that wrap a given provider (ex. OpenAI, Anthropic)‘s specific model version (ex. `gpt-3.5`). Each [Agent](/concepts/agents) can each select their own model to use and a [Network](/concepts/networks) can select a default model. ```ts import { openai, anthropic, gemini } from "@inngest/agent-kit"; ``` ## How to use a model [Section titled “How to use a model”](#how-to-use-a-model) ### Create a model instance [Section titled “Create a model instance”](#create-a-model-instance) Note Each model helper will first try to get the API Key from the environment variable. The API Key can also be provided with the `apiKey` option to the model helper. * OpenAI ```ts import { openai, createAgent } from "@inngest/agent-kit"; const model = openai({ model: "gpt-3.5-turbo" }); const modelWithApiKey = openai({ model: "gpt-3.5-turbo", apiKey: "sk-..." }); ``` * Anthropic ```ts import { anthropic, createAgent } from "@inngest/agent-kit"; const model = anthropic({ model: "claude-3-5-haiku-latest" }); const modelWithBetaFlags = anthropic({ model: "claude-3-5-haiku-latest", betaHeaders: ["prompt-caching-2024-07-31"], }); const modelWithApiKey = anthropic({ model: "claude-3-5-haiku-latest", apiKey: "sk-...", // Note: max_tokens is required for Anthropic models defaultParameters: { max_tokens: 4096 }, }); ``` * Gemini ```ts import { gemini, createAgent } from "@inngest/agent-kit"; const model = gemini({ model: "gemini-1.5-flash" }); ``` ### Configure model hyper parameters (temperature, etc.) [Section titled “Configure model hyper parameters (temperature, etc.)”](#configure-model-hyper-parameters-temperature-etc) You can configure the model hyper parameters (temperature, etc.) by passing the `defaultParameters` option: * OpenAI ```ts import { openai, createAgent } from "@inngest/agent-kit"; const model = openai({ model: "gpt-3.5-turbo", defaultParameters: { temperature: 0.5 }, }); ``` * Anthropic ```ts import { anthropic, createAgent } from "@inngest/agent-kit"; const model = anthropic({ model: "claude-3-5-haiku-latest", defaultParameters: { temperature: 0.5, max_tokens: 4096 }, }); ``` * Gemini ```ts import { gemini, createAgent } from "@inngest/agent-kit"; const model = gemini({ model: "gemini-1.5-flash", defaultParameters: { temperature: 0.5 }, }); ``` Note The full list of hyper parameters can be found in the [types definition of each model](https://github.com/inngest/inngest-js/tree/main/packages/ai/src/models). ### Providing a model instance to an Agent [Section titled “Providing a model instance to an Agent”](#providing-a-model-instance-to-an-agent) ```ts import { createAgent } from "@inngest/agent-kit"; const supportAgent = createAgent({ model: openai({ model: "gpt-3.5-turbo" }), name: "Customer support specialist", system: "You are an customer support specialist...", tools: [listChargesTool], }); ``` ### Providing a model instance to a Network [Section titled “Providing a model instance to a Network”](#providing-a-model-instance-to-a-network) Note The provided `defaultModel` will be used for all Agents without a model specified. It will also be used by the “[Default Routing Agent](/concepts/routers#default-routing-agent-autonomous-routing)” if enabled. ```ts import { createNetwork } from "@inngest/agent-kit"; const network = createNetwork({ agents: [supportAgent], defaultModel: openai({ model: "gpt-4o" }), }); ``` ## List of supported models [Section titled “List of supported models”](#list-of-supported-models) For a full list of supported models, you can always check [the models directory here](https://github.com/inngest/inngest-js/tree/main/packages/ai/src/models). * OpenAI ```plaintext "gpt-4.5-preview" "gpt-4o" "chatgpt-4o-latest" "gpt-4o-mini" "gpt-4" "o1" "o1-preview" "o1-mini" "o3-mini" "gpt-4-turbo" "gpt-3.5-turbo" ``` * Anthropic ```plaintext "claude-3-5-haiku-latest" "claude-3-5-haiku-20241022" "claude-3-5-sonnet-latest" "claude-3-5-sonnet-20241022" "claude-3-5-sonnet-20240620" "claude-3-opus-latest" "claude-3-opus-20240229" "claude-3-sonnet-20240229" "claude-3-haiku-20240307" "claude-2.1" "claude-2.0" "claude-instant-1.2"; ``` * Gemini ```plaintext "gemini-1.5-flash" "gemini-1.5-flash-8b" "gemini-1.5-pro" "gemini-1.0-pro" "text-embedding-004" "aqa" ``` * Grok ```plaintext "grok-2-1212" "grok-2" "grok-2-latest" "grok-3" "grok-3-latest" "grok-4" "grok-4-latest" ``` ### Environment variable used for each model provider [Section titled “Environment variable used for each model provider”](#environment-variable-used-for-each-model-provider) * OpenAI: `OPENAI_API_KEY` * Anthropic: `ANTHROPIC_API_KEY` * Gemini: `GEMINI_API_KEY` * Grok: `XAI_API_KEY` ## Contribution [Section titled “Contribution”](#contribution) Is there a model that you’d like to see included in AgentKit? Open an issue, create a pull request, or chat with the team on [Discord in the #ai channel](https://www.inngest.com/community). **[Contribute on GitHub](https://github.com/inngest/agent-kit)** - Fork, clone, and open a pull request. # Networks > Combine one or more agents into a Network. Networks are **Systems of [Agents](/concepts/agents)**. Use Networks to create powerful AI workflows by combining multiple Agents. A network contains three components: * The [Agents](/concepts/agents) that the network can use to achieve a goal * A [State](/concepts/state) including past messages and a key value store, shared between Agents and the Router * A [Router](/concepts/routers), which chooses whether to stop or select the next agent to run in the loop Here’s a simple example: ```tsx import { createNetwork, openai } from '@inngest/agent-kit'; // searchAgent and summaryAgent definitions... // Create a network with two agents. const network = createNetwork({ agents: [searchAgent, summaryAgent], }); // Run the network with a user prompt await network.run('What happened in the 2024 Super Bowl?'); ``` By calling `run()`, the network runs a core loop to call one or more agents to find a suitable answer. ## How Networks work [Section titled “How Networks work”](#how-networks-work) Networks can be thought of as while loops with memory ([State](/concepts/state)) that call Agents and Tools until the Router determines that there is no more work to be done. 1. **Create the Network of Agents** You create a network with a list of available [Agents](/concepts/agents). Each Agent can use a different [model and inference provider](/concepts/models). 2. **Provide the staring prompt** You give the network a user prompt by calling `run()`. 3. **Core execution loop** The network runs its core loop: 1. **Call the Network router** The [Router](/concepts/routers) decides the first Agent to run with your input. 2. **Run the Agent** Call the Agent with your input. This also runs the agent’s [lifecycles](/concepts/agents#lifecycle-hooks), and any [Tools](/concepts/tools) that the model decides to call. 3. **Store the result** Stores the result in the network’s [State](/concepts/state). State can be accessed by the Router or other Agent’s Tools in future loops. 4. **Call the the Router again** Return to the top of the loop and calls the Router with the new State. The Router can decide to quit or run another Agent. ## Model configuration [Section titled “Model configuration”](#model-configuration) A Network must provide a default model which is used for routing between Agents and for Agents that don’t have one: ```tsx import { createNetwork, openai } from '@inngest/agent-kit'; // searchAgent and summaryAgent definitions... const network = createNetwork({ agents: [searchAgent, summaryAgent], defaultModel: openai({ model: 'gpt-4o' }), }); ``` Note A Network not defining a `defaultModel` and composed of Agents without model will throw an error. ### Combination of multiple models [Section titled “Combination of multiple models”](#combination-of-multiple-models) Each Agent can specify it’s own model to use so a Network may end up using multiple models. Here is an example of a Network that defaults to use an OpenAI model, but the `summaryAgent` is configured to use an Anthropic model: ```tsx import { createNetwork, openai, anthropic } from '@inngest/agent-kit'; const searchAgent = createAgent({ name: 'Search', description: 'Search the web for information', }); const summaryAgent = createAgent({ name: 'Summary', description: 'Summarize the information', model: anthropic({ model: 'claude-3-5-sonnet' }), }); // The searchAgent will use gpt-4o, while the summaryAgent will use claude-3-5-sonnet. const network = createNetwork({ agents: [searchAgent, summaryAgent], defaultModel: openai({ model: 'gpt-4o' }), }); ``` ## Routing & maximum iterations [Section titled “Routing & maximum iterations”](#routing--maximum-iterations) ### Routing [Section titled “Routing”](#routing) A Network can specify an optional `defaultRouter` function that will be used to determine the next Agent to run. ```ts import { createNetwork } from '@inngest/agent-kit'; // classifier and writer Agents definition... const network = createNetwork({ agents: [classifier, writer], router: ({ lastResult, callCount }) => { // retrieve the last message from the output const lastMessage = lastResult?.output[lastResult?.output.length - 1]; const content = lastMessage?.type === 'text' ? lastMessage?.content as string : ''; // First call: use the classifier if (callCount === 0) { return classifier; } // Second call: if it's a question, use the writer if (callCount === 1 && content.includes('question')) { return writer; } // Otherwise, we're done! return undefined; }, }); ``` Refer to the [Router](/concepts/routers) documentation for more information about how to create a custom Router. ### Maximum iterations [Section titled “Maximum iterations”](#maximum-iterations) A Network can specify an optional `maxIter` setting to limit the number of iterations. ```tsx import { createNetwork } from '@inngest/agent-kit'; // searchAgent and summaryAgent definitions... const network = createNetwork({ agents: [searchAgent, summaryAgent], defaultModel: openai({ model: 'gpt-4o' }), maxIter: 10, }); ``` Note Specifying a `maxIter` option is useful when using a [Default Routing Agent](/concepts/routers#default-routing-agent-autonomous-routing) or a [Hybrid Router](/concepts/routers#hybrid-code-and-agent-routers-semi-supervised-routing) to avoid infinite loops. A Routing Agent or Hybrid Router rely on LLM calls to make decisions, which means that they can sometimes fail to identify a final condition. ### Combining `maxIter` and `defaultRouter` [Section titled “Combining maxIter and defaultRouter”](#combining-maxiter-and-defaultrouter) You can combine `maxIter` and `defaultRouter` to create a Network that will stop after a certain number of iterations or when a condition is met. However, please note that the `maxIter` option can prevent the `defaultRouter` from being called (For example, if `maxIter` is set to 1, the `defaultRouter` will only be called once). ## Providing a default State [Section titled “Providing a default State”](#providing-a-default-state) A Network can specify an optional `defaultState` setting to provide a default [State](/concepts/state). ```tsx import { createNetwork } from '@inngest/agent-kit'; // searchAgent and summaryAgent definitions... const network = createNetwork({ agents: [searchAgent, summaryAgent], defaultState: new State({ foo: 'bar', }), }); ``` Providing a `defaultState` can be useful to persist the state in database between runs or initialize your network with external data. # Routers > Customize how calls are routed between Agents in a Network. The purpose of a Network’s **Router** is to decide what [Agent](/concepts/agents) to call based off the current Network [State](/concepts/state). ## What is a Router? [Section titled “What is a Router?”](#what-is-a-router) A router is a function that gets called after each agent runs, which decides whether to: 1. Call another agent (by returning an `Agent`) 2. Stop the network’s execution loop (by returning `undefined`) The routing function gets access to everything it needs to make this decision: * The [Network](/concepts/networks) object itself, including it’s [State](/concepts/state). * The stack of [Agents](/concepts/agents) to be called. * The number of times the Network has called Agents (*the number of iterations*). * The result from the previously called Agent in the Network’s execution loop. For more information about the role of a Router in a Network, read about [how Networks work](/concepts/networks#how-networks-work). ## Using a Router [Section titled “Using a Router”](#using-a-router) Tip Providing a custom Router to your Network is optional. If you don’t provide one, the Network will use the “Default Router” Routing Agent. Providing a custom Router to your Network can be achieved using 3 different patterns: * **Writing a custom [Code-based Router](/concepts/routers#code-based-routers-supervised-routing)**: Define a function that makes decisions based on the current [State](/concepts/state). * **Creating a [Routing Agent](/concepts/routers#routing-agent-autonomous-routing)**: Leverages LLM calls to decide which Agents should be called next based on the current [State](/concepts/state). * **Writing a custom [Hybrid Router](/concepts/routers#hybrid-code-and-agent-routers-semi-supervised-routing)**: Mix code and agent-based routing to get the best of both worlds. ## Creating a custom Router [Section titled “Creating a custom Router”](#creating-a-custom-router) Custom Routers can be provided by defining a `defaultRouter` function returning either an instance of an `Agent` object or `undefined`. ```ts import { createNetwork } from "@inngest/agent-kit"; // classifier and writer Agents definition... const network = createNetwork({ agents: [classifier, writer], router: ({ lastResult, callCount }) => { // retrieve the last message from the output const lastMessage = lastResult?.output[lastResult?.output.length - 1]; const content = lastMessage?.type === "text" ? (lastMessage?.content as string) : ""; // First call: use the classifier if (callCount === 0) { return classifier; } // Second call: if it's a question, use the writer if (callCount === 1 && content.includes("question")) { return writer; } // Otherwise, we're done! return undefined; }, }); ``` The `defaultRouter` function receives a number of arguments: ```ts interface RouterArgs { network: Network; // The entire network, including the state and history stack: Agent[]; // Future agents to be called callCount: number; // Number of times the Network has called agents lastResult?: InferenceResult; // The the previously called Agent's result } ``` The available arguments can be used to build the routing patterns described below. ## Routing Patterns [Section titled “Routing Patterns”](#routing-patterns) ### Tips [Section titled “Tips”](#tips) * Start simple with code-based routing for predictable behavior, then add agent-based routing for flexibility. * Remember that routers can access the network’s [state](/concepts/state) * You can return agents that weren’t in the original network * The router runs after each agent call * Returning `undefined` stops the network’s execution loop That’s it! Routing is what makes networks powerful - it lets you build workflows that can be as simple or complex as you need. ### Code-based Routers (supervised routing) [Section titled “Code-based Routers (supervised routing)”](#code-based-routers-supervised-routing) The simplest way to route is to write code that makes decisions. Here’s an example that routes between a classifier and a writer: ```ts import { createNetwork } from "@inngest/agent-kit"; // classifier and writer Agents definition... const network = createNetwork({ agents: [classifier, writer], router: ({ lastResult, callCount }) => { // retrieve the last message from the output const lastMessage = lastResult?.output[lastResult?.output.length - 1]; const content = lastMessage?.type === "text" ? (lastMessage?.content as string) : ""; // First call: use the classifier if (callCount === 0) { return classifier; } // Second call: if it's a question, use the writer if (callCount === 1 && content.includes("question")) { return writer; } // Otherwise, we're done! return undefined; }, }); ``` Code-based routing is great when you want deterministic, predictable behavior. It’s also the fastest option since there’s no LLM calls involved. ### Routing Agent (autonomous routing) [Section titled “Routing Agent (autonomous routing)”](#routing-agent-autonomous-routing) Without a `defaultRouter` defined, the network will use the “Default Routing Agent” to decide which agent to call next. The “Default Routing Agent” is a Routing Agent provided by Agent Kit to handle the default routing logic. You can create your own Routing Agent by using the [`createRoutingAgent`](/reference/network-router#createroutingagent) helper function: ```ts import { createRoutingAgent } from "@inngest/agent-kit"; const routingAgent = createRoutingAgent({ name: "Custom routing agent", description: "Selects agents based on the current state and request", lifecycle: { onRoute: ({ result, network }) => { // custom logic... }, }, }); // classifier and writer Agents definition... const network = createNetwork({ agents: [classifier, writer], router: routingAgent, }); ``` Caution Routing Agents look similar to Agents but are designed to make routing decisions: - Routing Agents cannot have Tools. - Routing Agents provides a single `onRoute` lifecycle method. ### Hybrid code and agent Routers (semi-supervised routing) [Section titled “Hybrid code and agent Routers (semi-supervised routing)”](#hybrid-code-and-agent-routers-semi-supervised-routing) And, of course, you can mix code and agent-based routing. Here’s an example that uses code for the first step, then lets an agent take over: ```tsx import { createNetwork, getDefaultRoutingAgent } from "@inngest/agent-kit"; // classifier and writer Agents definition... const network = createNetwork({ agents: [classifier, writer], router: ({ callCount }) => { // Always start with the classifier if (callCount === 0) { return classifier; } // Then let the routing agent take over return getDefaultRoutingAgent(); }, }); ``` This gives you the best of both worlds: * Predictable first steps when you know what needs to happen * Flexibility when the path forward isn’t clear ### Using state in Routing [Section titled “Using state in Routing”](#using-state-in-routing) The router is the brain of your network - it decides which agent to call next. You can use state to make smart routing decisions: ```tsx import { createNetwork } from '@inngest/agent-kit'; // mathAgent and contextAgent Agents definition... const network = createNetwork({ agents: [mathAgent, contextAgent], router: ({ network, lastResult }): Agent | undefined => { // Check if we've solved the problem const solution = network.state.data.solution; if (solution) { // We're done - return undefined to stop the network return undefined; } // retrieve the last message from the output const lastMessage = lastResult?.output[lastResult?.output.length - 1]; const content = lastMessage?.type === 'text' ? lastMessage?.content as string : ''; // Check the last result to decide what to do next if (content.includes('need more context')) { return contextAgent; } return mathAgent; }; }); ``` ## Related Concepts [Section titled “Related Concepts”](#related-concepts) [Networks ](/concepts/networks)Networks combines the State and Router to execute Agent workflows. [State ](/concepts/state)State is a key-value store that can be used to store data between Agents. # State > Shared memory, history, and key-value state for Agents and Networks. State is shared memory, or context, that is be passed between different [Agents](/concepts/agents) in a [Networks](/concepts/networks). State is used to store message history and build up structured data from tools. State enables agent workflows to execute in a loop and contextually make decisions. Agents continuously build upon and leverage this context to complete complex tasks. AgentKit’s State stores data in two ways: * **History of messages** - A list of prompts, responses, and tool calls. * **Fully typed state data** - Typed state that allows you to build up structured data from agent calls, then implement [deterministic state-based routing](/advanced-patterns/routing) to easily model complex agent workflows. Both history and state data are used automatically by the Network to store and provide context to the next Agent. ## History [Section titled “History”](#history) The history system maintains a chronological record of all Agent interactions in your Network. Each interaction is stored as an `InferenceResult`. Refer to the [InferenceResult reference](/reference/state#inferenceresult) for more information. ## Typed state [Section titled “Typed state”](#typed-state) State contains typed data that can be used to store information between Agent calls, update agent prompts, and manage routing. Networks, agents, and tools use this type in order to set data: ```ts export interface NetworkState { // username is undefined until extracted and set by a tool username?: string; } // You can construct typed state with optional defaults, eg. from memory. const state = createState({ username: "default-username", }); console.log(state.data.username); // 'default-username' state.data.username = "Alice"; console.log(state.data.username); // 'Alice' ``` Common uses for data include: * Storing intermediate results that other Agents might need within lifecycles * Storing user preferences or context * Passing data between Tools and Agents * State based routing Tip The `State`’s data is only retained for a single `Network`’s run. This means that it is only short-term memory and is not persisted across different Network `run()` calls. You can implement memory by inspecting a network’s state after it has finished running. State, which is required by [Networks](/concepts/networks), has many uses across various AgentKit components. Refer to the [State reference](/reference/state#reading-and-modifying-state-states-data) for more information. ## Using state in tools [Section titled “Using state in tools”](#using-state-in-tools) State can be leveraged in a Tool’s `handler` method to get or set data. Here is an example of a Tool that uses `kv` as a temporary store for files and their contents that are being written by the Agent. ```ts const writeFiles = createTool({ name: "write_files", description: "Write code with the given filenames", parameters: z.object({ files: z.array( z.object({ filename: z.string(), content: z.string(), }) ), }), handler: (output, { network }) => { // files is the output from the model's response in the format above. // Here, we store OpenAI's generated files in the response. const files = network.state.data.files || {}; for (const file of output.files) { files[file.filename] = file.content; } network.state.data.files = files; }, }); ``` # Tools > Extending the functionality of Agents for structured output or performing tasks. Tools are functions that extend the capabilities of an [Agent](/concepts/agents). Tools have two core uses: * Calling code, enabling models to interact with systems like your own database or external APIs. * Turning unstructured inputs into structured responses. A list of all available Tools and their configuration is sent in [an Agent’s inference calls](/concepts/agents#how-agents-work) and a model may decide that a certain tool or tools should be called to complete the task. Tools are included in an Agent’s calls to language models through features like OpenAI’s “[function calling](https://platform.openai.com/docs/guides/function-calling)” or Claude’s “[tool use](https://docs.anthropic.com/en/docs/build-with-claude/tool-use).” ## Creating a Tool [Section titled “Creating a Tool”](#creating-a-tool) Each Tool’s `name`, `description`, and `parameters` are part of the function definition that is used by model to learn about the tool’s capabilities and decide when it should be called. The `handler` is the function that is executed by the Agent if the model decides that a particular Tool should be called. Here is a simple tool that lists charges for a given user’s account between a date range: ```ts import { createTool } from '@inngest/agent-kit'; const listChargesTool = createTool({ name: 'list_charges', description: "Returns all of a user's charges. Call this whenever you need to find one or more charges between a date range.", parameters: z.object({ userId: z.string(), created: z.object({ gte: z.string().date(), lte: z.string().date(), }), }), handler: async ({ userId, created }, { network, agent, step }) => { // input is strongly typed to match the parameter type. return [{...}] }, }); ``` Writing quality `name` and `description` parameters help the model determine when the particular Tool should be called. ### Optional parameters [Section titled “Optional parameters”](#optional-parameters) Optional parameters should be defined using `.nullable()` (not `.optional()`): ```ts const listChargesTool = createTool({ name: 'list_charges', description: "Returns all of a user's charges. Call this whenever you need to find one or more charges between a date range.", parameters: z.object({ userId: z.string(), created: z.object({ gte: z.string().date(), lte: z.string().date(), }).nullable(), }), handler: async ({ userId, created }, { network, agent, step }) => { // input is strongly typed to match the parameter type. return [{...}] }, }); ``` ## Examples [Section titled “Examples”](#examples) You can find multiple examples of tools in the below GitHub projects: [Hacker News Agent with Render and Inngest ](https://github.com/inngest/agentkit-render-tutorial)A tutorial showing how to create a Hacker News Agent using AgentKit Code-style routing and Agents with tools. [AgentKit SWE-bench ](https://github.com/inngest/agent-kit/tree/main/examples/swebench#readme)This AgentKit example uses the SWE-bench dataset to train an agent to solve coding problems. It uses advanced tools to interact with files and codebases. # Examples Explore the following examples to see AgentKit Concepts (*Agents, Tools, …*) in action: ## Tutorials [Section titled “Tutorials”](#tutorials) [Build an Agent to chat with code ](/ai-agents-in-practice/ai-workflows)This example shows how to leverages AgentKit's Agent to build an assistant that explain code. [Hacker News Agent with Render and Inngest ](/ai-agents-in-practice/ai-workflows)A tutorial showing how to create a Hacker News Agent using AgentKit Code-style routing and Agents with tools. ## MCP as tools examples [Section titled “MCP as tools examples”](#mcp-as-tools-examples) [Neon Assistant Agent (using MCP) ](https://github.com/inngest/agent-kit/tree/main/examples/mcp-neon-agent/#readme)This examples shows how to use the Neon MCP Smithery Server to build a Neon Assistant Agent that can help you manage your Neon databases. ## Code Examples [Section titled “Code Examples”](#code-examples) [Support Agent with "Human in the loop" ](https://github.com/inngest/agent-kit/tree/main/examples/support-agent-human-in-the-loop#readme)This AgentKit example shows how to build a Support Agent Network with a "Human in the loop" pattern. [AgentKit SWE-bench ](https://github.com/inngest/agent-kit/tree/main/examples/swebench#readme)This AgentKit example uses the SWE-bench dataset to train an agent to solve coding problems. It uses advanced tools to interact with files and codebases. [Coding Agent with E2B sandboxes ](https://github.com/inngest/agent-kit/tree/main/examples/e2b-coding-agent#readme)This AgentKit example uses E2B sandboxes to build a coding agent that can write code in any language. [Coding Agent powered by Daytona infrastructure ](https://github.com/inngest/agent-kit/tree/main/examples/daytona-coding-agent#readme)This AgentKit example uses Daytona to build a fully autonomous coding agent that performs software development tasks. # Installation > How to install AgentKit Install the AgentKit [npm package](https://www.npmjs.com/package/@inngest/agent-kit) and [Inngest](https://www.npmjs.com/package/inngest) using your favorite package manager: * npm ```shell npm install @inngest/agent-kit inngest ``` * pnpm ```shell pnpm install @inngest/agent-kit inngest ``` * yarn ```shell yarn add @inngest/agent-kit inngest ``` Note **Important:** Starting with AgentKit v0.8.0, `inngest` is a required peer dependency. You must install both packages together to ensure proper runtime compatibility and prevent conflicts. ## Beyond installation [Section titled “Beyond installation”](#beyond-installation) [Local development ](/getting-started/local-development)Discover Inngest's Dev Server with live traces and logs. [Deployment to production ](/getting-started/deployment)Add concurrency and throttling to your AgentKit network and deploy it to Inngest. # Local development > Run AgentKit locally with live traces and logs. Developing AgentKit applications locally is a breeze when combined with the [Inngest Dev Server](https://www.inngest.com/docs/dev-server). The Inngest Dev Server is a local development tool that provides live traces and logs for your AgentKit applications, providing a quicker feedback loop and full visibility into your AgentKit’s state and Agent LLM calls: [](https://cdn.inngest.com/agent-kit/agentkit-with-inngest-dev-server.mp4) ## Using AgentKit with the Inngest Dev Server [Section titled “Using AgentKit with the Inngest Dev Server”](#using-agentkit-with-the-inngest-dev-server) ### 1. Install the `inngest` package [Section titled “1. Install the inngest package”](#1-install-the-inngest-package) To use AgentKit with the Inngest Dev Server, you need to install the `inngest` package. * npm ```shell npm install inngest ``` * pnpm ```shell pnpm install inngest ``` * yarn ```shell yarn add inngest ``` ### 2. Expose your AgentKit network over HTTP [Section titled “2. Expose your AgentKit network over HTTP”](#2-expose-your-agentkit-network-over-http) The Inngest Dev Server needs to be able to trigger your AgentKit network over HTTP. If your AgentKit network runs as a CLI, a few lines changes will make it available over HTTP: ```ts import { createNetwork } from '@inngest/agent-kit'; import { createServer } from '@inngest/agent-kit/server'; const network = createNetwork({ name: 'My Network', agents: [/* ... */], }); const server = createServer({ networks: [network], }); server.listen(3010, () => console.log("Agent kit running!")); ``` Now, starting your AgentKit script will make it available over HTTP. Let’s now trigger our AgentKit network from the Inngest Dev Server. ### 3. Trigger your AgentKit network from the Inngest Dev Server [Section titled “3. Trigger your AgentKit network from the Inngest Dev Server”](#3-trigger-your-agentkit-network-from-the-inngest-dev-server) You can start the Inngest Dev Server with the following command: ```shell npx inngest-cli@latest dev ``` And navigate to the Inngest Dev Server by opening in your browser. You can now explore the Inngest Dev Server features: ## Features [Section titled “Features”](#features) ### Triggering your AgentKit network [Section titled “Triggering your AgentKit network”](#triggering-your-agentkit-network) You can trigger your AgentKit network by clicking on the “Trigger” button in the Inngest Dev Server from the “Functions” tab. In the opened, add an `input` property with the input you want to pass to your AgentKit network: ![Inngest Dev Server function list](/graphics/quick-start/dev-server-agent.png) Then, click on the “Run” button to trigger your AgentKit network” ![Inngest Dev Server invoke function modal](/graphics/quick-start/dev-server-invoke.png) ### Inspect AgentKit Agents token usage, input and output [Section titled “Inspect AgentKit Agents token usage, input and output”](#inspect-agentkit-agents-token-usage-input-and-output) In the run view of your AgentKit network run, the Agents step will be highlighted with a ✨ green icon. By expanding the step, you can inspect the Agents: * The **model used**, ex: `gpt-4o` * The **token usage** detailed as prompt tokens, completion tokens, and total tokens * The **input** provided to the Agent * The **output** provided by the Agent ![Inngest Dev Server agent run](/graphics/quick-start/dev-server-agent-step-details.png) Tips You can force line breaks to **make the input and output more readable** using the following button: ![Inngest Dev Server agent run](/graphics/quick-start/dev-server-network-run-linebreak-btn.png) You can **expand the input and output view to show its full content** using the following button: ![Inngest Dev Server agent run](/graphics/quick-start/dev-server-network-run-expand-btn.png) You can **update the input of an AgentKit Agent and trigger a rerun from this step** of the AgentKit network (*see below*) ### Rerun an AgentKit Agent with a different prompt [Section titled “Rerun an AgentKit Agent with a different prompt”](#rerun-an-agentkit-agent-with-a-different-prompt) On a given AgentKit Agent run, you can update the input of the Agent and trigger a rerun from this step of the AgentKit network. First, click on the “Rerun with new prompt” button under the input area. Then, the following modal will open: ![Inngest Dev Server agent run](/graphics/quick-start/dev-server-agent-step-rerun-modal.png) # Quick start > Learn the basics of AgentKit in a few minutes. In this tutorial, you will create an [Agent](/concepts/agents) and run it within a [Network](/concepts/networks) using AgentKit. Note Follow this guide by forking the [quick-start](https://github.com/inngest/agent-kit/tree/main/examples/quick-start) example locally by running: ```shell npx git-ripper https://github.com/inngest/agent-kit/tree/main/examples/quick-start ``` ## Creating a single agent [Section titled “Creating a single agent”](#creating-a-single-agent) 1. **Install AgentKit** Within an existing project, install AgentKit and Inngest from npm: * npm ```shell npm install @inngest/agent-kit inngest ``` * pnpm ```shell pnpm install @inngest/agent-kit inngest ``` * yarn ```shell yarn add @inngest/agent-kit inngest ``` Note **Important:** Starting with AgentKit v0.9.0, `inngest` is a required peer dependency. You must install both packages together to ensure proper runtime compatibility and prevent conflicts. You can always find the latest release version on [npm](https://www.npmjs.com/package/@inngest/agent-kit). Don’t have an existing project? To create a new project, create a new directory and initialize it using your package manager: * npm ```shell mkdir my-agent-kit-project && npm init ``` * pnpm ```shell mkdir my-agent-kit-project && pnpm init ``` * yarn ```shell mkdir my-agent-kit-project && yarn init ``` 2. **Create an agent** To start, we’ll create our first “[Agent](/concepts/agents).” An Agent is an entity that has a specific role to answer questions or perform tasks (see “tools” below). Let’s create a new file, `index.ts`. Using the `createAgent` constructor, give your agent a `name`, a `description`, and its initial `system` prompt. The `name` and `description` properties are used to help the LLM determine which Agent to call. You’ll also specify which `model` you want the agent to use. Here we’ll use Anthropic’s [Claude 3.5 Haiku](https://docs.anthropic.com/en/docs/about-claude/models) model. ([Model reference](/concepts/models)) Your agent can be whatever you want, but in this quick start, we’ll create a PostgreSQL database administrator agent: ```ts import { createAgent, anthropic } from '@inngest/agent-kit'; const dbaAgent = createAgent({ name: 'Database administrator', description: 'Provides expert support for managing PostgreSQL databases', system: 'You are a PostgreSQL expert database administrator. ' + 'You only provide answers to questions related to PostgreSQL database schema, indexes, and extensions.', model: anthropic({ model: 'claude-3-5-haiku-latest', defaultParameters: { max_tokens: 1000, }, }), }); ``` You’ll also need to set your provider API keys as environment variables: ```shell export ANTHROPIC_API_KEY=sk-ant-api03-XXXXXX.... ``` 3. **Run the server** Next, we’ll create an HTTP server to run our agent. In the same file as our Agent definition: ```ts import { createAgent, anthropic } from '@inngest/agent-kit'; import { createServer } from '@inngest/agent-kit/server'; // ... const server = createServer({ agents: [dbaAgent], }); server.listen(3000, () => console.log('AgentKit server running!')); ``` Now we can run our AgentKit server using [`npx`](https://docs.npmjs.com/cli/v8/commands/npx) and [`tsx`](https://tsx.is/) (for easy TypeScript execution): ```shell npx tsx ./index.ts ``` 4. **Test our agent** To test our agent, we’ll use the [Inngest dev server](https://www.inngest.com/docs/local-development) to visually debug our agents. Using `npx`, we’ll start the server and point it to our AgentKit server: ```shell npx inngest-cli@latest dev -u http://localhost:3000/api/inngest ``` Now, open the dev server and select the functions tab (`http://localhost:8288/functions`) and click the “Invoke” button: ![Inngest Dev Server function list](/graphics/quick-start/dev-server-agent.png) In the Invoke function modal, specify the input prompt for your agent and click the “Invoke function” button: ![Inngest Dev Server invoke function modal](/graphics/quick-start/dev-server-invoke.png) ```json { "data": { "input": "How do I aggregate an integer column across a date column by week?" } } ``` You’ll be redirected to watch the agent run and view the output: ![Inngest Dev Server agent run](/graphics/quick-start/dev-server-agent-run.png) A key benefit of AgentKit is the ability to create a system of agents called a “[Network](/concepts/networks).” Networks are used to create AI Agents by combining multiple specialized [Agents](/concepts/agents) to answer more complex questions. Let’s transform our single agent into a network of two agents, capable of helping with both database administration and security questions. ## Creating a multi-agent network [Section titled “Creating a multi-agent network”](#creating-a-multi-agent-network) 1. **Adding a second Agent** Agents collaborate in a Network by sharing a common [State](/concepts/state). Let’s update our Database Administrator Agent to include a tool to save the answer to the question in the database: ```ts const dbaAgent = createAgent({ name: "Database administrator", description: "Provides expert support for managing PostgreSQL databases", system: "You are a PostgreSQL expert database administrator. " + "You only provide answers to questions related to PostgreSQL database schema, indexes, and extensions.", model: anthropic({ model: "claude-3-5-haiku-latest", defaultParameters: { max_tokens: 4096, }, }), tools: [ createTool({ name: "save_answer", description: "Save the answer to the questions", parameters: z.object({ answer: z.string(), }), handler: async ({ answer }, { network }: Tool.Options) => { network.state.data.dba_agent_answer = answer; }, }), ], }); ``` Note [Tools](/concepts/tools) are based on [Tool Calling](https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/overview), enabling your Agent to interact with the [State](/concepts/state) of the Network, store data in external databases, or dynamically fetch data from third-party APIs. Let’s now create a second *Database Security* Agent: ```ts import { createAgent, anthropic } from "@inngest/agent-kit"; // ... const securityAgent = createAgent({ name: "Database Security Expert", description: "Provides expert guidance on PostgreSQL security, access control, audit logging, and compliance best practices", system: "You are a PostgreSQL security expert. " + "You only provide answers to questions related to PostgreSQL security topics such as encryption, access control, audit logging, and compliance best practices.", model: anthropic({ model: "claude-3-5-haiku-latest", defaultParameters: { max_tokens: 1000, }, }), tools: [ createTool({ name: "save_answer", description: "Save the answer to the questions", parameters: z.object({ answer: z.string(), }), handler: async ({ answer }, { network }: Tool.Options) => { network.state.data.security_agent_answer = answer; }, }), ], }); ``` Our second Security Expert Agent is similar to the first, but with a different system prompt specifically for security questions. We can now create a network combining our “Database Administrator” and “Database Security” Agents, which enables us to answer more complex questions. 2. **Creating a Network** Create a network using the `createNetwork` constructor. Define a `name` and include our agents from the previous step in the `agents` array. You must also configure a `router` that the [*Router*](/concepts/routers) will use to determine which agent to call: ```ts import { /*...*/ createNetwork } from "@inngest/agent-kit"; export interface NetworkState { // answer from the Database Administrator Agent dba_agent_answer?: string; // answer from the Security Expert Agent security_agent_answer?: string; } // ... const devOpsNetwork = createNetwork({ name: "DevOps team", agents: [dbaAgent, securityAgent], router: async ({ network }) => { if (!network.state.data.security_agent_answer) { return securityAgent; } else if ( network.state.data.security_agent_answer && network.state.data.dba_agent_answer ) { return; } return dbaAgent; }, }); const server = createServer({ agents: [dbaAgent, securityAgent], networks: [devOpsNetwork], }); ``` The highlighted lines are the key parts of our AI Agent behavior: * The `agents` property defines the agents that are part of the network * The `router` function defines the logic for which agent to call next. In this example, we call the Database Administrator Agent followed by the Security Expert Agent before ending the network (by returning `undefined`). 3. **Test our network** We’ll use the same approach to test our network as we did above. With your Inngest dev server running, open the dev server and select the functions tab (`http://localhost:8288/functions`) and click the “Invoke” button of the *DevOps team* function with the following payload: ```json { "data": { "input": "I am building a Finance application. Help me answer the following 2 questions: \n - How can I scale my application to millions of requests per second? \n - How should I design my schema to ensure the safety of each organization's data?" } } ``` The network will now run through the Agents to answer the questions: ![Inngest Dev Server agent run](/graphics/quick-start/dev-server-network-run.png) You can inspect the answers of each Agent by selecting the *Finalization* step and inspecting the JSON payload in the right panel: ![Inngest Dev Server agent run](/graphics/quick-start/dev-server-network-run-result.png) ## Next steps [Section titled “Next steps”](#next-steps) Congratulations! You’ve now created your first AI Agent with AgentKit. In this guide, you’ve learned that: * [**Agents**](/concepts/agents) are the building blocks of AgentKit. They are used to call a single model to answer specific questions or perform tasks. * [**Networks**](/concepts/networks) are groups of agents that can work together to achieve more complex goals. * [**Routers**](/concepts/routers), combined with [**State**](/concepts/state), enable you to control the flow of your Agents. The following guides will help you build more advanced AI Agents: [Adding Tools to Agents ](/concepts/tools)Let your Agent act and gather data with tools [Implementing reasoning-based routing ](/concepts/routers)Learn how to dynamically route between agents You can also explore the following examples to see how to use AgentKit in more complex scenarios: [Support Agent with "Human in the loop" ](https://github.com/inngest/agent-kit/tree/main/examples/support-agent-human-in-the-loop#readme)This AgentKit example shows how to build a Support Agent Network with a "Human in the loop" pattern. [AgentKit SWE-bench ](https://github.com/inngest/agent-kit/tree/main/examples/swebench#readme)This AgentKit example uses the SWE-bench dataset to train an agent to solve coding problems. It uses advanced tools to interact with files and codebases. # Code Assistant v2: Complex code analysis > Use AgentKit Tools and Custom Router to add agentic capabilities. ## Overview [Section titled “Overview”](#overview) Our [Code Assistant v1](/ai-agents-in-practice/ai-workflows), relying on a RAG workflow, had limited capabilities linked to its lack of reasoning. The second version of our Code Assistant will introduce reasoning capabilities to adapt analysis based on the user’s input: ```typescript const { state: { kv }, } = await network.run( `Analyze the files/example.ts file by suggesting improvements and documentation.` ); console.log("Analysis:", kv.get("summary")); // Analysis: The code analysis suggests several key areas for improvement: // 1. Type Safety and Structure: // - Implement strict TypeScript configurations // - Add explicit return types and interfaces // - Break down complex functions // - Follow Single Responsibility Principle // - Implement proper error handling // 2. Performance Optimization: // - Review and optimize critical operations // ... ``` These agentic (reasoning) capabilities are introduced by the following AgentKit concepts: * **[Tools](/concepts/tools)**: Enables [Agents](/concepts/agents) to interact with their environment (ex: file system or shared State). * **[Router](/concepts/router)**: Powers the flow of the conversation between Agents. * **[Network](/concepts/network)**: Add a shared [State](/concepts/state) to share information between Agents. Let’s learn these concepts in practice. ## Setup [Section titled “Setup”](#setup) Similarly to the [Code Assistant v1](/ai-agents-in-practice/ai-workflows), perform the following steps to setup your project: 1\. Initialize your project * npm ```bash npm init ``` * pnpm ```bash pnpm init ``` * yarn ```bash yarn init ``` 2\. Install the required dependencies * npm ```bash npm install @inngest/agent-kit inngest zod ``` * pnpm ```bash pnpm install @inngest/agent-kit inngest zod ``` * yarn ```bash yarn add @inngest/agent-kit zod ``` 3\. Add TypeScript support * npm ```bash npm install -D tsx @types/node ``` * pnpm ```bash pnpm install -D tsx @types/node ``` * yarn ```bash yarn add -D tsx @types/node ``` And add the following scripts to your `package.json`: ```json "scripts": { "start": "tsx ./index.ts" } ``` 4\. Download the example code file ```bash mkdir files cd files wget https://raw.githubusercontent.com/inngest/agent-kit/main/examples/code-assistant-agentic/files/example.ts cd - ``` You are now set up, let’s implement the v2 of our Code Assistant. ## Implementing our Code Assistant v2 [Section titled “Implementing our Code Assistant v2”](#implementing-our-code-assistant-v2) ### Overview of the agentic workflow [Section titled “Overview of the agentic workflow”](#overview-of-the-agentic-workflow) Our Code Assistant v2 introduces reasoning to perform tailored recommendations based on a given code file: refactoring, documentation, etc. To achieve this behavior, we will need to: * Create a `code_assistant_agent` Agent that will load a given filename from disk and plan a workflow using the following available [Agents](/concepts/agents): * `analysis_agent` that will analyze the code file and suggest improvements * `documentation_agent` that will generate documentation for the code file * Finally, create a `summarization_agent` Agent that will generate a summary of the suggestions made by other agents Compared to our [Code Assistant v1](/ai-agents-in-practice/ai-workflows), this new version does not consist of simple retrieval and generations steps. Instead, it introduces more flexibility by enabling LLM models to plan actions and select tools to use. Let’s see how to implement the Agents. ### A Network of Agents [Section titled “A Network of Agents”](#a-network-of-agents) Our Code Assistant v2 is composed of 4 Agents collaborating together to analyze a given code file. Such collaboration is made possible by using a [Network](/concepts/network) to orchestrate the Agents and share [State](/concepts/state) between them. Unlike the [Code Assistant v1](/ai-agents-in-practice/ai-workflows), the user prompt will be passed to the network instead of an individual Agent: ```typescript await network.run( `Analyze the files/example.ts file by suggesting improvements and documentation.` ); ``` To successfully run, a `Network` relies on: * A Router to **indicate which Agent should be run next** * **A shared State**, updated by the Agents’ LLM responses and **tool calls** Let’s start by implementing our Agents and registering them into the Network. ### Creating Agents with Tools [Section titled “Creating Agents with Tools”](#creating-agents-with-tools) Note Attaching Tools to an Agent helps to: * Enrich dynamically the Agent context with dynamic data * Store the Agent results in the shared State Learn more about [Tools](/concepts/tools). **The Analysis and Documentation Agents** Our first two analysis Agents are straightforward: ```typescript import { createAgent } from "@inngest/agent-kit"; const documentationAgent = createAgent({ name: "documentation_agent", system: "You are an expert at generating documentation for code", }); const analysisAgent = createAgent({ name: "analysis_agent", system: "You are an expert at analyzing code and suggesting improvements", }); ``` Defining task specific LLM calls (Agents) is a great way to make the LLM reasoning more efficient and avoid unnecessary generations. Our `documentation_agent` and `analysis_agent` are currently stateless and need to be *connected* to the Network by saving their suggestions into the shared State. For this, we will create our first Tool using [`createTool`](/reference/create-tool): ```typescript const saveSuggestions = createTool({ name: "save_suggestions", description: "Save the suggestions made by other agents into the state", parameters: z.object({ suggestions: z.array(z.string()), }), handler: async (input, { network }) => { const suggestions = network?.state.kv.get("suggestions") || []; network?.state.kv.set("suggestions", [ ...suggestions, ...input.suggestions, ]); return "Suggestions saved!"; }, }); ``` Tip A Tool is a function that can be called by an Agent. The `name`, `description` and `parameters` are used by the Agent to understand what the Tool does and what it expects as input. The `handler` is the function that will be called when the Tool is used. `save_suggestions`’s handler relies on the [Network’s State `kv` (key-value store)](/reference/state#reading-and-modifying-state-state-kv) API to share information with other Agents. Learn more about the [createTool()](/reference/create-tool) API. The `save_suggestions` Tool is used by both `documentation_agent` and `analysis_agent` to save their suggestions into the shared State: ```typescript import { createAgent } from "@inngest/agent-kit"; // `save_suggestions` definition... const documentationAgent = createAgent({ name: "documentation_agent", system: "You are an expert at generating documentation for code", tools: [saveSuggestions], }); const analysisAgent = createAgent({ name: "analysis_agent", system: "You are an expert at analyzing code and suggesting improvements", tools: [saveSuggestions], }); ``` Our `documentation_agent` and `analysis_agent` are now connected to the Network and will save their suggestions into the shared State. Let’s now create our `code_assistant_agent` that will read the code file from disk and plan the workflow to run. **The Code Assistant Agent** Let’s jump into the action by looking at the full implementation of our `code_assistant_agent`: ```typescript const codeAssistantAgent = createAgent({ name: "code_assistant_agent", system: ({ network }) => { const agents = Array.from(network?.agents.values() || []) .filter( (agent) => !["code_assistant_agent", "summarization_agent"].includes(agent.name) ) .map((agent) => `${agent.name} (${agent.system})`); return `From a given user request, ONLY perform the following tool calls: - read the file content - generate a plan of agents to run from the following list: ${agents.join(", ")} Answer with "done" when you are finished.`; }, tools: [ createTool({ name: "read_file", description: "Read a file from the current directory", parameters: z.object({ filename: z.string(), }), handler: async (input, { network }) => { const filePath = join(process.cwd(), `files/${input.filename}`); const code = readFileSync(filePath, "utf-8"); network?.state.kv.set("code", code); return "File read!"; }, }), createTool({ name: "generate_plan", description: "Generate a plan of agents to run", parameters: z.object({ plan: z.array(z.string()), }), handler: async (input, { network }) => { network?.state.kv.set("plan", input.plan); return "Plan generated!"; }, }), ], }); ``` The highlighted lines emphasize three important parts of the `code_assistant_agent`: * The [`system` property](/reference/create-agent#param-system) can take a function receiving the current Network state as argument, enabling more flexibility in the Agent’s behavior * Here, the `system` function is used to generate a prompt for the LLM based on the available Agents in the Network, enabling the LLM to plan the workflow to run * The `code_assistant_agent` relies on two Tools to achieve its goal: * `read_file` to read the code file from disk and save it into the shared State * `generate_plan` to generate a plan of agents to run and save it into the shared State The pattern of dynamic `system` prompt and tools are also used by the `summarization_agent` to generate a summary of the suggestions made by other agents. **The Summarization Agent** ```typescript const summarizationAgent = createAgent({ name: "summarization_agent", system: ({ network }) => { const suggestions = network?.state.kv.get("suggestions") || []; return `Save a summary of the following suggestions: ${suggestions.join("\n")}`; }, tools: [ createTool({ name: "save_summary", description: "Save a summary of the suggestions made by other agents into the state", parameters: z.object({ summary: z.string(), }), handler: async (input, { network }) => { network?.state.kv.set("summary", input.summary); return "Saved!"; }, }), ], }); ``` Note The `summarization_agent` is a good example on how the State can be used to store intermediate results and pass them to the next Agent: - the `suggestions` are stored in the State by the `documentation_agent` and `analysis_agent` - the `summarization_agent` will read the `suggestions` from the State and generate a summary - the summary is then stored in the State as the `summary` key Our four Agents are now propely defined and connected to the Network’s State. Let’s now configure our Network to run the Agents with a Router. ### Assembling the Network [Section titled “Assembling the Network”](#assembling-the-network) An AgentKit [Network](/concepts/network) is defined by a set of Agents and an optional `defaultModel`: ```typescript import { createNetwork, anthropic } from "@inngest/agent-kit"; // Agent and Tools definitions... const network = createNetwork({ name: "code-assistant-v2", agents: [ codeAssistantAgent, documentationAgent, analysisAgent, summarizationAgent, ], defaultModel: anthropic({ model: "claude-3-5-sonnet-latest", max_tokens: 4096, }), }); ``` Tip The `defaultModel` will be applied to all Agents part of the Network. A model can also be set on an individual Agent by setting the `model` property. Learn more about the [Network Model configuration](/concepts/networks#model-configuration). Our Code Assistant v2 is missing a final piece: the Router. Without a Router, the Network will not know which Agent to run next. **Implementing the Router** As stated in the [workflow overview](#overview-of-the-agentic-workflow), our Code Assistant v2 is an agentic worflow composed of the following steps: 1. The `code_assistant_agent` will read the code file from disk and generate a plan of agents to run 2. Depending on the plan, the Network will run the next Agent in the plan (*ex: `analysis_agent` and `documentation_agent`*) 3. Finally, the `summarization_agent` will generate a summary of the suggestions made by other agents AgentKit’s Router enables us to implement such dynamic workflow with code by providing a `defaultRouter` function: ```typescript const network = createNetwork({ name: "code-assistant-v2", agents: [ codeAssistantAgent, documentationAgent, analysisAgent, summarizationAgent, ], router: ({ network }) => { if (!network?.state.kv.has("code") || !network?.state.kv.has("plan")) { return codeAssistantAgent; } else { const plan = (network?.state.kv.get("plan") || []) as string[]; const nextAgent = plan.pop(); if (nextAgent) { network?.state.kv.set("plan", plan); return network?.agents.get(nextAgent); } else if (!network?.state.kv.has("summary")) { return summarizationAgent; } else { return undefined; } } }, defaultModel: anthropic({ model: "claude-3-5-sonnet-latest", max_tokens: 4096, }), }); ``` Note **How does a Router work?** The Router is a function called by the Network when starting a new run and between each Agent call. The provided Router function (`defaultRouter`) receives a `network` argument granting access to the Network’s state and Agents. Learn more about the [Router](/concepts/router). Let’s have a closer look at the Router implementation: ```typescript const router = ({ network }) => { // the first iteration of the network will have an empty state // also, the first run of `code_assistant_agent` will store the `code`, // requiring a second run to generate the plan if (!network?.state.kv.has("code") || !network?.state.kv.has("plan")) { return codeAssistantAgent; } else { // once the `plan` available in the state, we iterate over the agents to execute const plan = (network?.state.kv.get("plan") || []) as string[]; const nextAgent = plan.pop(); if (nextAgent) { network?.state.kv.set("plan", plan); return network?.agents.get(nextAgent); // we no agents are left to run, we generate a summary } else if (!network?.state.kv.has("summary")) { return summarizationAgent; // if no agent are left to run and a summary is available, we are done } else { return undefined; } } }; ``` Our Code Assistant v2 iteration is now complete. Let’s run it! ## Running the Code Assistant v2 [Section titled “Running the Code Assistant v2”](#running-the-code-assistant-v2) First, go to your Anthropic dashboard and create a new API key. Then, run the following command to execute our Code Assistant: * npm ```bash ANTHROPIC_API_KEY= npm run start ``` * pnpm ```bash ANTHROPIC_API_KEY= pnpm run start ``` * yarn ```bash ANTHROPIC_API_KEY= yarn run start ``` The following output should be displayed in your terminal: ```txt Analysis: The code analysis suggests several key areas for improvement: 1. Type Safety and Structure: - Implement strict TypeScript configurations - Add explicit return types and interfaces - Break down complex functions - Follow Single Responsibility Principle - Implement proper error handling 2. Performance Optimization: - Review and optimize critical operations - Consider caching mechanisms - Improve data processing efficiency 3. Documentation: - Add comprehensive JSDoc comments - Document complex logic and assumptions - Create detailed README - Include setup and usage instructions - Add code examples ``` Note Updating the `files/example.ts` by applying the suggestions and running the Code Assistant again will yield a different planning with a different summary. Try it out! ## What we’ve learned so far [Section titled “What we’ve learned so far”](#what-weve-learned-so-far) Let’s recap what we’ve learned so far: * **Agentic workflows**, compared to RAG workflows, **are more flexible** and can be used to perform more complex tasks * **Combining multiple Agents improves the accuracy** of the LLM reasoning and can save tokens * **AgentKit enables to combine multiple Agents** into a [Network](/concepts/networks), connected by a common [State](/concepts/state) * **AgentKit’s Router enables to implement our workflow with code**, keeping control over our reasoning planning ## Next steps [Section titled “Next steps”](#next-steps) This Code Assistant v2 shines by its analysis capabilities, but cannot be qualified as an AI Agent. In the next version of our Code Assistant, we will transform it into a semi-autonomous AI Agent that can solve bugs and improve code of a small project. [Code Assistant v3: Autonomous Code Assistant ](/ai-agents-in-practice/ai-agents)The final version update of our Code Assistant will transform it into a semi-autonomous AI Agent. # Code Assistant v3: Autonomous Bug Solver > Build a custom Agent Router to autonomously solve bugs. ## Overview [Section titled “Overview”](#overview) Our [Code Assistant v2](/ai-agents-in-practice/agentic-workflows) introduced some limited reasoning capabilities through Tools and a Network of Agents. This third version will transform our Code Assistant into a semi-autonomous AI Agent that can solve bugs and improve code. Our AI Agent will operate over an Express API project containing bugs: ```txt /examples/code-assistant-agent/project ├── package.json ├── tsconfig.json ├── src │ ├── index.ts │ ├── routes │ │ ├── users.ts │ │ └── posts.ts │ ├── models │ │ ├── user.ts │ │ └── post.ts │ └── db.ts └── tests ├── users.test.ts └── posts.test.ts ``` Given a prompt such as: ```txt Can you help me fix the following error? 1. TypeError: Cannot read properties of undefined (reading 'body') at app.post (/project/src/routes/users.ts:10:23) ``` Our Code Assistant v3 will autonomously navigate through the codebase and fix the bug by updating the impacted files. This new version relies on previously covered concepts such as [Tools](/concepts/tools), [Agents](/concepts/agent), and [Networks](/concepts/network) but introduces the creation of a custom [Router Agent](/concepts/routers#routing-agent-autonomous-routing) bringing routing autonomy to the AI Agent. Let’s learn these concepts in practice. ## Setup [Section titled “Setup”](#setup) Similarly to the [Code Assistant v2](/ai-agents-in-practice/agentic-workflows), perform the following steps to setup your project: 1\. Initialize your project * npm ```bash npm init ``` * pnpm ```bash pnpm init ``` * yarn ```bash yarn init ``` 2\. Install the required dependencies * npm ```bash npm install @inngest/agent-kit inngest zod ``` * pnpm ```bash pnpm install @inngest/agent-kit inngest zod ``` * yarn ```bash yarn add @inngest/agent-kit zod ``` 3\. Add TypeScript support * npm ```bash npm install -D tsx @types/node ``` * pnpm ```bash pnpm install -D tsx @types/node ``` * yarn ```bash yarn add -D tsx @types/node ``` And add the following scripts to your `package.json`: ```json "scripts": { "start": "tsx ./index.ts" } ``` You are now set up, let’s implement our autonomous Code Assistant. ## Implementing our Code Assistant v3 [Section titled “Implementing our Code Assistant v3”](#implementing-our-code-assistant-v3) ### Overview of the autonomous workflow [Section titled “Overview of the autonomous workflow”](#overview-of-the-autonomous-workflow) Our Code Assistant v3 introduces autonomy through a specialized Router Agent that orchestrates two task-specific Agents: * `plannerAgent`: Analyzes code and plans fixes using code search capabilities * `editorAgent`: Implements the planned fixes using file system operations The Router Agent acts as the “brain” of our Code Assistant, deciding which Agent to use based on the current context and user request. Let’s implement each component of our autonomous workflow. ### Implementing the Tools [Section titled “Implementing the Tools”](#implementing-the-tools) Our Code Assistant v3 needs to interact with the file system and search through code. Let’s implement these capabilities as Tools: ```typescript import { createTool } from "@inngest/agent-kit"; const writeFile = createTool({ name: "writeFile", description: "Write a file to the filesystem", parameters: z.object({ path: z.string().describe("The path to the file to write"), content: z.string().describe("The content to write to the file"), }), handler: async ({ path, content }) => { try { let relativePath = path.startsWith("/") ? path.slice(1) : path; writeFileSync(relativePath, content); return "File written"; } catch (err) { console.error(`Error writing file ${path}:`, err); throw new Error(`Failed to write file ${path}`); } }, }); const readFile = createTool({ name: "readFile", description: "Read a file from the filesystem", parameters: z.object({ path: z.string().describe("The path to the file to read"), }), handler: async ({ path }) => { try { let relativePath = path.startsWith("/") ? path.slice(1) : path; const content = readFileSync(relativePath, "utf-8"); return content; } catch (err) { console.error(`Error reading file ${path}:`, err); throw new Error(`Failed to read file ${path}`); } }, }); const searchCode = createTool({ name: "searchCode", description: "Search for a given pattern in a project files", parameters: z.object({ query: z.string().describe("The query to search for"), }), handler: async ({ query }) => { const searchFiles = (dir: string, searchQuery: string): string[] => { const results: string[] = []; const walk = (currentPath: string) => { const files = readdirSync(currentPath); for (const file of files) { const filePath = join(currentPath, file); const stat = statSync(filePath); if (stat.isDirectory()) { walk(filePath); } else { try { const content = readFileSync(filePath, "utf-8"); if (content.includes(searchQuery)) { results.push(filePath); } } catch (err) { console.error(`Error reading file ${filePath}:`, err); } } } }; walk(dir); return results; }; const matches = searchFiles(process.cwd(), query); return matches.length === 0 ? "No matches found" : `Found matches in following files:\n${matches.join("\n")}`; }, }); ``` Note Some notes on the highlighted lines: * As noted in the [“Building Effective Agents” article](https://www.anthropic.com/research/building-effective-agents) from Anthropic, Tools based on file system operations are most effective when provided with absolute paths. * Tools performing action such as `writeFile` should always return a value to inform the Agent that the action has been completed. These Tools provide our Agents with the following capabilities: * `writeFile`: Write content to a file * `readFile`: Read content from a file * `searchCode`: Search for patterns in project files Let’s now create our task-specific Agents. ### Creating the Task-Specific Agents [Section titled “Creating the Task-Specific Agents”](#creating-the-task-specific-agents) Our Code Assistant v3 relies on two specialized Agents: ```typescript import { createAgent } from "@inngest/agent-kit"; const plannerAgent = createAgent({ name: "planner", system: "You are an expert in debugging TypeScript projects.", tools: [searchCode], }); const editorAgent = createAgent({ name: "editor", system: "You are an expert in fixing bugs in TypeScript projects.", tools: [writeFile, readFile], }); ``` Each Agent has a specific role: * `plannerAgent` uses the `searchCode` Tool to analyze code and plan fixes * `editorAgent` uses the `readFile` and `writeFile` Tools to implement fixes Separating the Agents into two distinct roles will enable our AI Agent to better *“divide and conquer”* the problem to solve. Let’s now implement the Router Agent that will bring the reasoning capabilities to autonomously orchestrate these Agents. ### Implementing the Router Agent [Section titled “Implementing the Router Agent”](#implementing-the-router-agent) The [Router Agent](/concepts/routers#routing-agent-autonomous-routing) is the “brain” of our Code Assistant, deciding which Agent to use based on the context. The router developed in the [Code Assistant v2](/ai-agents-in-practice/agentic-workflows) was a function that decided which Agent to call next based on the progress of the workflow. Such router made a Agent deterministic, but lacked the reasoning capabilities to autonomously orchestrate the Agents. In this version, we will provide an Agent as a router, called a Router Agent. By doing so, we can leverage the reasoning capabilities of the LLM to autonomously orchestrate the Agents around a given goal (here, fixing the bug). Creating a Router Agent is done by using the [`createRoutingAgent`](/reference/network-router#createroutingagent) helper function: ```typescript import { createRoutingAgent } from "@inngest/agent-kit"; const router = createRoutingAgent({ name: "Code Assistant routing agent", system: async ({ network }): Promise => { if (!network) { throw new Error( "The routing agent can only be used within a network of agents" ); } const agents = await network?.availableAgents(); return `You are the orchestrator between a group of agents. Each agent is suited for a set of specific tasks, and has a name, instructions, and a set of tools. The following agents are available: ${agents .map((a) => { return ` ${a.name} ${a.description} ${JSON.stringify(Array.from(a.tools.values()))} `; }) .join("\n")} Follow the set of instructions: Think about the current history and status. If the user issue has been fixed, call select_agent with "finished" Otherwise, determine which agent to use to handle the user's request, based off of the current agents and their tools. Your aim is to thoroughly complete the request, thinking step by step, choosing the right agent based off of the context. `; }, tools: [ createTool({ name: "select_agent", description: "select an agent to handle the input, based off of the current conversation", parameters: z .object({ name: z .string() .describe("The name of the agent that should handle the request"), }) .strict(), handler: ({ name }, { network }) => { if (!network) { throw new Error( "The routing agent can only be used within a network of agents" ); } if (name === "finished") { return undefined; } const agent = network.agents.get(name); if (agent === undefined) { throw new Error( `The routing agent requested an agent that doesn't exist: ${name}` ); } return agent.name; }, }), ], tool_choice: "select_agent", lifecycle: { onRoute: ({ result }) => { const tool = result.toolCalls[0]; if (!tool) { return; } const agentName = (tool.content as any).data || (tool.content as string); if (agentName === "finished") { return; } else { return [agentName]; } }, }, }); ``` Looking at the highlighted lines, we can see that a Router Agent mixes features from regular Agents and a function Router: 1. A Router Agent is a regular Agent with a `system` function that returns a prompt 2. A Router Agent can use [Tools](/concepts/tools) to interact with the environment 3. Finally, a Router Agent can also define lifecycle callbacks, [like Agents do](/concepts/agents#lifecycle-hooks) Let’s now dissect how this Router Agent works: 1. The `system` function is used to define the prompt dynamically based on the Agents available in the Network * You will notice that the prompt explicitly ask to call a “finished” tool when the user issue has been fixed 2. The `select_agent` Tool is used to validate that the Agent selected is available in the Network * The tool ensures that the “finished” edge case is handled 3. The `onRoute` lifecycle callback is used to determine which Agent to call next * This callback stops the conversation when the user issue has been fixed (when the “finished” Agent is called) This is it! Using this prompt, our Router Agent will orchestrate the Agents until the given bug is fixed. ### Assembling the Network [Section titled “Assembling the Network”](#assembling-the-network) Finally, assemble the Network of Agents and Router Agent: ```typescript const network = createNetwork({ name: "code-assistant-v3", agents: [plannerAgent, editorAgent], defaultModel: anthropic({ model: "claude-3-5-sonnet-latest", max_tokens: 4096, }), router: router, }); ``` Our Code Assistant v3 is now complete and ready to be used! ## Running our Code Assistant v3 [Section titled “Running our Code Assistant v3”](#running-our-code-assistant-v3) First, go to your Anthropic dashboard and create a new API key. Then, run the following command to start the server: * npm ```bash ANTHROPIC_API_KEY= npm run start ``` * pnpm ```bash ANTHROPIC_API_KEY= pnpm run start ``` * yarn ```bash ANTHROPIC_API_KEY= yarn run start ``` Your Code Assistant is now running at `http://localhost:3010` and ready to help fix bugs in your TypeScript projects! ## What we’ve learned so far [Section titled “What we’ve learned so far”](#what-weve-learned-so-far) Let’s recap what we’ve learned so far: * **Autonomous AI Agents** can be built by using [**Router Agents**](/concepts/routers#routing-agent-autonomous-routing), which act as the “brain” of an autonomous system by orchestrating other Agents * **Tools** provide Agents with capabilities to interact with their environment # Code Assistant v1: Explaining a given code file > Leveraging AgentKit's Agent concept to power a RAG workflow. ## Overview [Section titled “Overview”](#overview) As discussed in the [introduction](/ai-agents-in-practice/overview), developing AI applications is a pragmatic approach requiring to start simple and iterate towards complexity. Following this approach, this first version of our Code Assistant will be able to explain a given code file: ```typescript const filePath = join(process.cwd(), `files/example.ts`); const code = readFileSync(filePath, "utf-8"); const { lastMessage } = await codeAssistant.run(`What the following code does? ${code} `); console.log(lastMessage({ type: "text" }).content); // This file (example.ts) is a TypeScript module that provides a collection of type-safe sorting helper functions. It contains five main sorting utility functions: // 1. `sortNumbers(numbers: number[], descending = false)` // - Sorts an array of numbers in ascending (default) or descending order // - Takes an array of numbers and an optional boolean to determine sort direction // 2. `sortStrings(strings: string[], options)` // - Sorts strings alphabetically with customizable options // - Options include: // - caseSensitive (default: false) // - descending (default: false) // ... ``` To implement this capability, we will build a AI workflow leveraging a first important concept of AgentKit: * [Agents](/concepts/agents): Agents act as a wrapper around the LLM (ex: Anthropic), providing a structured way to interact with it. Let’s start our Code Assistant by installing the required dependencies: ## Setup [Section titled “Setup”](#setup) Follow the below steps to setup your project: 1\. Initialize your project * npm ```bash npm init ``` * pnpm ```bash pnpm init ``` * yarn ```bash yarn init ``` 2\. Install the required dependencies * npm ```bash npm install @inngest/agent-kit inngest ``` * pnpm ```bash pnpm install @inngest/agent-kit inngest ``` * yarn ```bash yarn add @inngest/agent-kit inngest ``` 3\. Add TypeScript support * npm ```bash npm install -D tsx @types/node ``` * pnpm ```bash pnpm install -D tsx @types/node ``` * yarn ```bash yarn add -D tsx @types/node ``` And add the following scripts to your `package.json`: ```json "scripts": { "start": "tsx ./index.ts" } ``` 4\. Download the example code file ```bash wget https://raw.githubusercontent.com/inngest/agent-kit/main/examples/code-assistant-rag/files/example.ts ``` You are now set up, let’s implement the first version of our Code Assistant. ## Implementing our Code Assistant v1 [Section titled “Implementing our Code Assistant v1”](#implementing-our-code-assistant-v1) Our first version of our Code Assistant takes the shape of a RAG workflow. A RAG workflow is a specific type of AI workflow that generally consist of two steps: retrieval (fetching relevant information) and generation (creating a response with a LLM). Our Code Assistant will have following two steps: * **A retrieval step** reads the content of a local file specified by the user. * **A generation step** uses Anthropic to analyze the code and provide a detailed explanation of what it does. Let’s start by implementing the retrieval step. ### The retrieval step: loading the code file [Section titled “The retrieval step: loading the code file”](#the-retrieval-step-loading-the-code-file) We earlier downloaded the `example.ts` file locally, let’s load it in our code by creating a `index.ts` file: ```typescript import { readFileSync } from "fs"; import { join } from "path"; async function main() { // First step: Retrieval const filePath = join(process.cwd(), `files/example.ts`); const code = readFileSync(filePath, "utf-8"); } main(); ``` Our example code is now ready to be analyzed. Let’s now implement the generation step. ### The generation step using AgentKit’s Agent [Section titled “The generation step using AgentKit’s Agent”](#the-generation-step-using-agentkits-agent) As covered in the introduction, [AgentKit’s `createAgent()`](/reference/create-agent) is a wrapper around the LLM, providing a structured way to interact with it with 3 main properties: * `name`: A unique identifier for the agent. * `system`: A description of the agent’s purpose. * `model`: The LLM to use. Let’s add configure our Agent with Anthropic’s `claude-3-5-sonnet-latest` model by updating our `index.ts` file: ```typescript import { readFileSync } from "fs"; import { join } from "path"; import { anthropic, createAgent } from "@inngest/agent-kit"; const codeAssistant = createAgent({ name: "code_assistant", system: "An AI assistant that helps answer questions about code by reading and analyzing files", model: anthropic({ model: "claude-3-5-sonnet-latest", max_tokens: 4096, }), }); async function main() { // First step: Retrieval const filePath = join(process.cwd(), `files/example.ts`); const code = readFileSync(filePath, "utf-8"); } main(); ``` Let’s now update our `main()` function to use our `codeAssistant` Agent in the generation step: ```typescript /* eslint-disable */ import { readFileSync } from "fs"; import { join } from "path"; import { anthropic, createAgent } from "@inngest/agent-kit"; // Create the code assistant agent const codeAssistant = createAgent({ name: "code_assistant", system: "An AI assistant that helps answer questions about code by reading and analyzing files", model: anthropic({ model: "claude-3-5-sonnet-latest", max_tokens: 4096, }), }); async function main() { // First step: Retrieval const filePath = join(process.cwd(), `files/example.ts`); const code = readFileSync(filePath, "utf-8"); // Second step: Generation const { output } = await codeAssistant.run(`What the following code does? ${code} `); const lastMessage = output[output.length - 1]; const content = lastMessage?.type === "text" ? (lastMessage?.content as string) : ""; console.log(content); } main(); ``` Let’s review the above code: 1. We load the `example.ts` file in memory. 2. We invoke our Code Assistant using the `codeAssistant.run()` method. 3. We retrieve the last message from the `output` array. 4. We log the content of the last message to the console. Let’s now look at our assistant explanation. ## Running our Code Assistant v1 [Section titled “Running our Code Assistant v1”](#running-our-code-assistant-v1) First, go to your Anthropic dashboard and create a new API key. Then, run the following command to execute our Code Assistant: * npm ```bash ANTHROPIC_API_KEY= npm run start ``` * pnpm ```bash ANTHROPIC_API_KEY= pnpm run start ``` * yarn ```bash ANTHROPIC_API_KEY= yarn run start ``` The following output should be displayed in your terminal: ```plaintext This code is a collection of type-safe sorting utility functions written in TypeScript. Here's a breakdown of each function: 1. `sortNumbers(numbers: number[], descending = false)` - Sorts an array of numbers in ascending (default) or descending order - Returns a new sorted array without modifying the original 2. `sortStrings(strings: string[], options)` - Sorts an array of strings alphabetically - Accepts options for case sensitivity and sort direction - Default behavior is case-insensitive ascending order - Returns a new sorted array 3. `sortByKey(items: T[], key: keyof T, descending = false)` - Sorts an array of objects by a specific key - Handles both number and string values - Generic type T ensures type safety - Returns a new sorted array 4. `sortByMultipleKeys(items: T[], sortKeys: Array<...>)` - Sorts an array of objects by multiple keys in order - Each key can have its own sort configuration (descending, case sensitivity) - Continues to next key if values are equal - Returns a new sorted array ... ``` Congratulations! You’ve just built your first AI workflow using AgentKit. ## What we’ve learned so far [Section titled “What we’ve learned so far”](#what-weve-learned-so-far) Let’s recap what we’ve learned so far: * **A RAG workflow** is a specific type of AI workflow that generally consist of two steps: retrieval (fetching relevant information) and generation (creating a response with a LLM). * *Note that most RAG workflows in production consist of more than two steps and combine multiple sources of information and generation steps. You can see an example in [this blog post](https://www.inngest.com/blog/next-generation-ai-workflows?ref=agentkit-docs).* * **AgentKit’s `createAgent()`** is a wrapper around the LLM, providing a structured way to interact with a LLM model. * *The use of a single Agent is often sufficient to power chatbots or extract structured data from a given text.* ## Next steps [Section titled “Next steps”](#next-steps) Our Code Assistant v1 is a static AI workflow that only works with the `example.ts` file. In the next version of our Code Assistant, we will make it dynamic by allowing the user to specify the file to analyze and also enable our Agent to perform more complete analysis. [Code Assistant v2: Complex code analysis ](/ai-agents-in-practice/agentic-workflows)Our next Code Assistant version will rely on Agentic workflows to perform more complex code analysis. # The three levels of AI apps > A comprehensive guide to building AI Agents with AgentKit AI Agents can be a complex topic to understand and differentiate from RAG, AI workflows, Agentic workflows, and more. This guide will provide a definition of AI Agents with practical examples inspired by the [Building effective agents](https://www.anthropic.com/research/building-effective-agents) manifesto from Anthropic. Developing AI applications leverages multiple patterns from AI workflows with static steps to fully autonomous AI Agents, each fitting specific use cases. The best way to start is to begin simple and iterate towards complexity. This guide features a Code Assistant that will will progressively evolve from a static AI workflow to an autonomous AI Agent. Below are the different versions of our Code Assistant, each progressively adding more autonomy and complexity: [v1 - Explaining a given code file ](/guided-tour/ai-workflows)The first version starts as a AI workflow using a tool to provide a file as context to the LLM (RAG). [v2 - Performing complex code analysis ](/guided-tour/agentic-workflows)Then, we will add Agentic capabilities to our assistant to enable it more complex analysis. [v3 - Autonomously reviewing a pull request ](/guided-tour/ai-agents)Finally, we will add more autonomy to our assistant, transforming it into a semi-autonomous AI Agent. [New - Pushing our Code Assistant to production ](/concepts/deployment)Discover the best practices to deploy your AI Agents to production. Depending on your experience developing AI applications, you can choose to start directly with the second part covering Agentic workflows. Happy coding! # Using AgentKit with Browserbase > Develop AI Agents that can browse the web [Browserbase](https://www.browserbase.com/) provides managed [headless browsers](https://docs.browserbase.com/introduction/what-is-headless-browser) to enable Agents to browse the web autonomously. There are two ways to use Browserbase with AgentKit: * **Create your own Browserbase tools**: useful if you want to build simple actions on webpages with manual browser control. * **Use Browserbase’s [Stagehand](https://www.stagehand.dev/) library as tools**: a better approach for autonomous browsing and resilient scraping. ## Building AgentKit tools using Browserbase [Section titled “Building AgentKit tools using Browserbase”](#building-agentkit-tools-using-browserbase) Creating AgentKit [tools](/concepts/tools) using the Browserbase TypeScript SDK is straightforward. 1. **Install AgentKit** Within an existing project, install AgentKit, Browserbase and Playwright core: * npm ```shell npm install @inngest/agent-kit inngest @browserbasehq/sdk playwright-core ``` * pnpm ```shell pnpm install @inngest/agent-kit inngest @browserbasehq/sdk playwright-core ``` * yarn ```shell yarn add @inngest/agent-kit inngest @browserbasehq/sdk playwright-core ``` Don’t have an existing project? To create a new project, create a new directory then initialize using your package manager: * npm ```shell mkdir my-agent-kit-project && npm init ``` * pnpm ```shell mkdir my-agent-kit-project && pnpm init ``` * yarn ```shell mkdir my-agent-kit-project && yarn init ``` 2. **Setup an AgentKit Network with an Agent** Create a Agent and its associated Network, for example a Reddit Search Agent: ```typescript import { anthropic, createAgent, createNetwork, } from "@inngest/agent-kit"; const searchAgent = createAgent({ name: "reddit_searcher", description: "An agent that searches Reddit for relevant information", system: "You are a helpful assistant that searches Reddit for relevant information.", }); // Create the network const redditSearchNetwork = createNetwork({ name: "reddit_search_network", description: "A network that searches Reddit using Browserbase", agents: [searchAgent], maxIter: 2, defaultModel: anthropic({ model: "claude-3-5-sonnet-latest", max_tokens: 4096, }); ``` 3. **Create a Browserbase tool** Let’s configure the Browserbase SDK and create a tool that can search Reddit: ```typescript import { anthropic, createAgent, createNetwork, createTool, } from "@inngest/agent-kit"; import { z } from "zod"; import { chromium } from "playwright-core"; import Browserbase from "@browserbasehq/sdk"; const bb = new Browserbase({ apiKey: process.env.BROWSERBASE_API_KEY as string, }); // Create a tool to search Reddit using Browserbase const searchReddit = createTool({ name: "search_reddit", description: "Search Reddit posts and comments", parameters: z.object({ query: z.string().describe("The search query for Reddit"), }), handler: async ({ query }, { step }) => { return await step?.run("search-on-reddit", async () => { // Create a new session const session = await bb.sessions.create({ projectId: process.env.BROWSERBASE_PROJECT_ID as string, }); // Connect to the session const browser = await chromium.connectOverCDP(session.connectUrl); try { const page = await browser.newPage(); // Construct the search URL const searchUrl = `https://search-new.pullpush.io/?type=submission&q=${query}`; console.log(searchUrl); await page.goto(searchUrl); // Wait for results to load await page.waitForSelector("div.results", { timeout: 10000 }); // Extract search results const results = await page.evaluate(() => { const posts = document.querySelectorAll("div.results div:has(h1)"); return Array.from(posts).map((post) => ({ title: post.querySelector("h1")?.textContent?.trim(), content: post.querySelector("div")?.textContent?.trim(), })); }); console.log("results", JSON.stringify(results, null, 2)); return results.slice(0, 5); // Return top 5 results } finally { await browser.close(); } }); }, }); ``` Note Configure your `BROWSERBASE_API_KEY` and `BROWSERBASE_PROJECT_ID` in the `.env` file. You can find your API key and project ID from the [Browserbase dashboard](https://docs.browserbase.com/introduction/getting-started#creating-your-account). Tip We recommend building tools using Browserbase using Inngest’s `step.run()` function. This ensures that the tool will only run once across multiple runs. More information about using `step.run()` can be found in the [Multi steps tools](/advanced-patterns/multi-steps-tools) page. ### Example: Reddit Search Agent using Browserbase [Section titled “Example: Reddit Search Agent using Browserbase”](#example-reddit-search-agent-using-browserbase) You will find a complete example of a Reddit search agent using Browserbase in the Reddit Search Agent using Browserbase example: [Reddit Search Agent using Browserbase ](https://github.com/inngest/agent-kit/tree/main/examples/reddit-search-browserbase-tools#readme)This examples shows how to build tools using Browserbase to power a Reddit search agent. ## Enable autonomous browsing with Stagehand [Section titled “Enable autonomous browsing with Stagehand”](#enable-autonomous-browsing-with-stagehand) Building AgentKit tools using [Stagehand](https://www.stagehand.dev/) gives more autonomy to your agents. Stagehand comes with 4 primary API that can be directly used as tools: * `goto()`: navigate to a specific URL * `observe()`: observe the current page * `extract()`: extract data from the current page * `act()`: take action on the current page These methods can be easily directly be used as tools in AgentKit, enabling agents to browse the web autonomously. Below is an example of a simple search agent that uses Stagehand to search the web: ```ts import { createAgent, createTool } from "@inngest/agent-kit"; import { z } from "zod"; import { getStagehand, stringToZodSchema } from "./utils.js"; const webSearchAgent = createAgent({ name: "web_search_agent", description: "I am a web search agent.", system: `You are a web search agent. `, tools: [ createTool({ name: "navigate", description: "Navigate to a given URL", parameters: z.object({ url: z.string().describe("the URL to navigate to"), }), handler: async ({ url }, { step, network }) => { return await step?.run("navigate", async () => { const stagehand = await getStagehand( network?.state.kv.get("browserbaseSessionID")! ); await stagehand.page.goto(url); return `Navigated to ${url}.`; }); }, }), createTool({ name: "extract", description: "Extract data from the page", parameters: z.object({ instruction: z .string() .describe("Instructions for what data to extract from the page"), schema: z .string() .describe( "A string representing the properties and types of data to extract, for example: '{ name: string, age: number }'" ), }), handler: async ({ instruction, schema }, { step, network }) => { return await step?.run("extract", async () => { const stagehand = await getStagehand( network?.state.kv.get("browserbaseSessionID")! ); const zodSchema = stringToZodSchema(schema); return await stagehand.page.extract({ instruction, schema: zodSchema, }); }); }, }), createTool({ name: "act", description: "Perform an action on the page", parameters: z.object({ action: z .string() .describe("The action to perform (e.g. 'click the login button')"), }), handler: async ({ action }, { step, network }) => { return await step?.run("act", async () => { const stagehand = await getStagehand( network?.state.kv.get("browserbaseSessionID")! ); return await stagehand.page.act({ action }); }); }, }), createTool({ name: "observe", description: "Observe the page", parameters: z.object({ instruction: z .string() .describe("Specific instruction for what to observe on the page"), }), handler: async ({ instruction }, { step, network }) => { return await step?.run("observe", async () => { const stagehand = await getStagehand( network?.state.kv.get("browserbaseSessionID")! ); return await stagehand.page.observe({ instruction }); }); }, }), ], }); ``` Note These 4 AgentKit tools using Stagehand enables the Web Search Agent to browse the web autonomously. The `getStagehand()` helper function is used to retrieve the persisted instance created for the network execution (*see full code below*). You will find the complete example on GitHub: [Simple Search Agent using Stagehand ](https://github.com/inngest/agent-kit/tree/main/examples/simple-search-stagehand/#readme)This examples shows how to build tools using Stagehand to power a simple search agent. # Using AgentKit with Daytona > Build Coding Agents with Daytona's secure and elastic infrastructure for executing AI-generated code [Daytona](https://www.daytona.io/) provides secure, high-performance infrastructure for running AI-generated code. It’s designed for any use case requiring secure sandbox environments and lightning-fast execution: running code, spinning up applications, data analysis, automated testing, CI/CD pipelines and more. Daytona is the perfect foundation for building autonomous Coding Agents that can scaffold projects, execute scripts, and deliver production-ready solutions. ## Setup [Section titled “Setup”](#setup) 1. **Install AgentKit and Daytona** Within an existing project, Install AgentKit and Daytona: * npm ```shell npm install @inngest/agent-kit inngest @daytonaio/sdk ``` * pnpm ```shell pnpm install @inngest/agent-kit inngest @daytonaio/sdk ``` * yarn ```shell yarn add @inngest/agent-kit inngest @daytonaio/sdk ``` Don’t have an existing project? To create a new project, create a new directory then initialize using your package manager: * npm ```shell mkdir my-agent-kit-project && npm init ``` * pnpm ```shell mkdir my-agent-kit-project && pnpm init ``` * yarn ```shell mkdir my-agent-kit-project && yarn init ``` 2. **Setup the Coding Agent** Create a Coding Agent with a system prompt that defines its behavior and select a model with appropriate parameters. In this example, we use Anthropic’s Claude model: ```typescript import { createAgent, anthropic, } from "@inngest/agent-kit"; const codingAgent = createAgent({ name: "Coding Agent", description: "An autonomous coding agent for building software in a Daytona sandbox", system: `You are a coding agent designed to help the user achieve software development tasks. You have access to a Daytona sandbox environment. Capabilities: - You can execute code snippets or scripts. - You can run shell commands to install dependencies, manipulate files, and set up environments. - You can create, upload, and organize files and directories to build basic applications and project structures. Workspace Instructions: - You do not need to define, set up, or specify the workspace directory. Assume you are already inside a default workspace directory that is ready for app creation. - All file and folder operations (create, upload, organize) should use paths relative to this default workspace. - Do not attempt to create or configure the workspace itself; focus only on the requested development tasks. Guidelines: - Always analyze the user's request and plan your steps before taking action. - Prefer automation and scripting over manual or interactive steps. - When installing packages or running commands that may prompt for input, use flags (e.g., '-y') to avoid blocking. - If you are developing an app that is served with a development server (e.g. Next.js, React): 1. Return the port information in the form: DEV_SERVER_PORT=$PORT (replace $PORT with the actual port number). 2. Start the development server. 3. After starting the dev server, always check its health in the next iteration. Only mark the task as complete if the health check passes: there must be no stderr output, no errors thrown, and the stdout content must not indicate a problem (such as error messages, stack traces, or failed startup). If any of these are present, diagnose and fix the issue before completing the task. - When you have completed the requested task, set the "TASK_COMPLETED" string in your output to signal that the app is finished. `, model: anthropic({ model: "claude-3-5-haiku-20241022", defaultParameters: { max_tokens: 1024, }, }), }); ``` 3. **Create the Daytona Tools** To fulfill the objectives specified in its system prompt, the Coding Agent needs access to sandbox environments; this is where Daytona steps in. The Daytona sandbox environment is exposed to the Coding Agent via tools. Our goal is to define a set of tools that enable the Coding Agent to achieve virtually any coding task. In this example, we’ll show implementations of `codeRunTool` and `readFileTool`. Note that the code also uses `getSandbox` and `logDebug` utility functions for sandbox management and logging. You can find complete implementations of all coding agent tools and utility functions in the [Daytona Coding Agent example](https://github.com/inngest/agent-kit/tree/main/examples/daytona-coding-agent#readme). ```typescript import { createAgent, anthropic, createTool, } from "@inngest/agent-kit"; import { z } from "zod"; import { CodeRunParams, DaytonaError } from "@daytonaio/sdk"; import { getSandbox, logDebug } from "./utils.js"; const codingAgent = createAgent({ name: "Coding Agent", description: "An autonomous coding agent for building software in a Daytona sandbox", system: `You are a coding agent designed to help the user achieve software development tasks. You have access to a Daytona sandbox environment. Capabilities: - You can execute code snippets or scripts. - You can run shell commands to install dependencies, manipulate files, and set up environments. - You can create, upload, and organize files and directories to build basic applications and project structures. Workspace Instructions: - You do not need to define, set up, or specify the workspace directory. Assume you are already inside a default workspace directory that is ready for app creation. - All file and folder operations (create, upload, organize) should use paths relative to this default workspace. - Do not attempt to create or configure the workspace itself; focus only on the requested development tasks. Guidelines: - Always analyze the user's request and plan your steps before taking action. - Prefer automation and scripting over manual or interactive steps. - When installing packages or running commands that may prompt for input, use flags (e.g., '-y') to avoid blocking. - If you are developing an app that is served with a development server (e.g. Next.js, React): 1. Return the port information in the form: DEV_SERVER_PORT=$PORT (replace $PORT with the actual port number). 2. Start the development server. 3. After starting the dev server, always check its health in the next iteration. Only mark the task as complete if the health check passes: there must be no stderr output, no errors thrown, and the stdout content must not indicate a problem (such as error messages, stack traces, or failed startup). If any of these are present, diagnose and fix the issue before completing the task. - When you have completed the requested task, set the "TASK_COMPLETED" string in your output to signal that the app is finished. `, model: anthropic({ model: "claude-3-5-haiku-20241022", defaultParameters: { max_tokens: 1024, }, }), tools: [ createTool({ name: "codeRunTool", description: `Executes code in the Daytona sandbox. Use this tool to run code snippets, scripts, or application entry points. Parameters: - code: Code to execute. - argv: Command line arguments to pass to the code. - env: Environment variables for the code execution, as key-value pairs.`, parameters: z.object({ code: z.string(), argv: z.array(z.string()).nullable(), env: z.record(z.string(), z.string()).nullable(), }), handler: async ({ code, argv, env }, { network }) => { try { const sandbox = await getSandbox(network); const codeRunParams = new CodeRunParams(); codeRunParams.argv = argv ?? []; codeRunParams.env = env ?? {}; console.log(`[TOOL: codeRunTool]\nParams: ${codeRunParams}\n${code}`); const response = await sandbox.process.codeRun(code, codeRunParams); const responseMessage = `Code run result: ${response.result}${ response.artifacts?.stdout ? `\nStdout: ${response.artifacts.stdout}` : "" }`; logDebug(responseMessage); return responseMessage; } catch (error) { console.error("Error executing code:", error); if (error instanceof DaytonaError) return `Code execution Daytona error: ${error.message}`; else return "Error executing code"; } }, }), createTool({ name: "readFileTool", description: `Reads the contents of a file from the Daytona sandbox. Use this tool to retrieve source code, configuration files, or other assets for analysis or processing.`, parameters: z.object({ filePath: z.string(), }), handler: async ({ filePath }, { network }) => { try { const sandbox = await getSandbox(network); console.log(`[TOOL: readFileTool]\nFile path: ${filePath}`); const fileBuffer = await sandbox.fs.downloadFile(filePath); const fileContent = fileBuffer.toString("utf-8"); const readFileMessage = `Successfully read file: ${filePath}\nContent:\n${fileContent}`; logDebug(readFileMessage); return fileContent; } catch (error) { console.error("Error reading file:", error); if (error instanceof DaytonaError) return `File reading Daytona error: ${error.message}`; else return "Error reading file"; } }, }), ], }); ``` 4. **Create agent network** Create a network with a code-based router that determines when the agent has completed its task and checks if a development server was started to set the data needed for preview generation. ```typescript import { createNetwork, } from "@inngest/agent-kit"; import { extractTextMessageContent, logDebug } from "./utils.js"; const network = createNetwork({ name: "coding-agent-network", agents: [codingAgent], // Using codingAgent from previous step maxIter: 30, defaultRouter: ({ network, callCount }) => { const previousIterationMessageContent = extractTextMessageContent( network.state.results.at(-1) ); if (previousIterationMessageContent) logDebug(`Iteration message:\n${previousIterationMessageContent}\n`); console.log(`\n ===== Iteration #${callCount + 1} =====\n`); if (callCount > 0) { if (previousIterationMessageContent.includes("TASK_COMPLETED")) { const isDevServerAppMessage = network.state.results.map((result) => extractTextMessageContent(result)).find((messageContent) => messageContent.includes("DEV_SERVER_PORT") ); if (isDevServerAppMessage) { const portMatch = isDevServerAppMessage.match( /DEV_SERVER_PORT=([0-9]+)/ ); const port = portMatch && portMatch[1] ? parseInt(portMatch[1], 10) : undefined; if (port) network.state.data.devServerPort = port; } return; } } return codingAgent; }, }); ``` Tip For a more comprehensive guide on using this coding agent, check out the [Daytona documentation](https://www.daytona.io/docs/en/inngest-agentkit-coding-agent/) ## Examples [Section titled “Examples”](#examples) [Coding Agent powered by Daytona infrastructure ](https://github.com/inngest/agent-kit/tree/main/examples/daytona-coding-agent#readme)This AgentKit example uses Daytona to build a fully autonomous coding agent that performs software development tasks # Using AgentKit with E2B > Develop Coding Agents using E2B Sandboxes as tools [E2B](https://e2b.dev) is an open-source runtime for executing AI-generated code in secure cloud sandboxes. Made for agentic & AI use cases. E2B is a perfect fit to build Coding Agents that can write code, fix bugs, and more. ## Setup [Section titled “Setup”](#setup) 1. **Install AgentKit and E2B** Within an existing project, Install AgentKit and E2B from npm: * npm ```shell npm install @inngest/agent-kit inngest @e2b/code-interpreter ``` * pnpm ```shell pnpm install @inngest/agent-kit inngest @e2b/code-interpreter ``` * yarn ```shell yarn add @inngest/agent-kit inngest @e2b/code-interpreter ``` Don’t have an existing project? To create a new project, create a new directory then initialize using your package manager: * npm ```shell mkdir my-agent-kit-project && npm init ``` * pnpm ```shell mkdir my-agent-kit-project && pnpm init ``` * yarn ```shell mkdir my-agent-kit-project && yarn init ``` 2. **Setup your Coding Agent** Create a Agent and its associated Network: ```typescript import { createAgent, createNetwork, anthropic } from "@inngest/agent-kit"; const agent = createAgent({ name: "Coding Agent", description: "An expert coding agent", system: `You are a coding agent help the user to achieve the described task. Once the task completed, you should return the following information: Think step-by-step before you start the task. `, model: anthropic({ model: "claude-3-5-sonnet-latest", max_tokens: 4096, }), }); const network = createNetwork({ name: "Coding Network", agents: [agent], defaultModel: anthropic({ model: "claude-3-5-sonnet-20240620", maxTokens: 1000, }) }); ``` 3. **Create the E2B Tools** To operate, our Coding Agent will need to create files and run commands. Below is an example of how to create the `createOrUpdateFiles` and `terminal` E2B tools: ```typescript import { createAgent, createNetwork, anthropic, createTool } from "@inngest/agent-kit"; const agent = createAgent({ name: "Coding Agent", description: "An expert coding agent", system: `You are a coding agent help the user to achieve the described task. Once the task completed, you should return the following information: Think step-by-step before you start the task. `, model: anthropic({ model: "claude-3-5-sonnet-latest", max_tokens: 4096, }), tools: [ // terminal use createTool({ name: "terminal", description: "Use the terminal to run commands", parameters: z.object({ command: z.string(), }), handler: async ({ command }, { network }) => { const buffers = { stdout: "", stderr: "" }; try { const sandbox = await getSandbox(network); const result = await sandbox.commands.run(command, { onStdout: (data: string) => { buffers.stdout += data; }, onStderr: (data: string) => { buffers.stderr += data; }, }); return result.stdout; } catch (e) { console.error( `Command failed: ${e} \nstdout: ${buffers.stdout}\nstderr: ${buffers.stderr}` ); return `Command failed: ${e} \nstdout: ${buffers.stdout}\nstderr: ${buffers.stderr}`; } }, }), // create or update file createTool({ name: "createOrUpdateFiles", description: "Create or update files in the sandbox", parameters: z.object({ files: z.array( z.object({ path: z.string(), content: z.string(), }) ), }), handler: async ({ files }, { network }) => { try { const sandbox = await getSandbox(network); for (const file of files) { await sandbox.files.write(file.path, file.content); } return `Files created or updated: ${files .map((f) => f.path) .join(", ")}`; } catch (e) { return "Error: " + e; } }, }), ] }); const network = createNetwork({ name: "Coding Network", agents: [agent], defaultModel: anthropic({ model: "claude-3-5-sonnet-20240620", maxTokens: 1000, }) }); ``` You will find the complete example in the [E2B Coding Agent example](https://github.com/inngest/agent-kit/tree/main/examples/e2b-coding-agent#readme). Designing useful tools As covered in Anthropic’s [“Tips for Building AI Agents”](https://www.youtube.com/watch?v=LP5OCa20Zpg), the best Agents Tools are the ones that you will need to accomplish the task by yourself. Do not map tools directly to the underlying API, but rather design tools that are useful for the Agent to accomplish the task. ## Examples [Section titled “Examples”](#examples) [Replicate Cursor's Agent mode ](https://github.com/inngest/agent-kit/tree/main/examples/e2b-coding-agent#readme)This examples shows how to use E2B sandboxes to build a coding agent that can write code and run commands to generate complete project, complete refactoring and fix bugs. [AI-powered CSV contacts importer ](https://github.com/inngest/agent-kit/tree/main/examples/e2b-csv-contacts-importer#readme)Let's reinvent the CSV upload UX with an AgentKit network leveraging E2B sandboxes. # Smithery - MCP Registry > Provide your Agents with hundred of prebuilt tools to interact with [Smithery](https://smithery.ai/) is an MCP ([Model Context Protocol](https://modelcontextprotocol.io/introduction)) servers registry, listing more than 2,000 MCP servers across multiple use cases: * Code related tasks (ex: GitHub, [E2B](/integrations/e2b)) * Web Search Integration (ex: Brave, [Browserbase](/integrations/browserbase)) * Database Integration (ex: Neon, Supabase) * Financial Market Data * Data & App Analysis * And more… ## Adding a Smithery MCP Server to your Agent [Section titled “Adding a Smithery MCP Server to your Agent”](#adding-a-smithery-mcp-server-to-your-agent) 1. **Install AgentKit** Within an existing project, install AgentKit along with the Smithery SDK: * npm ```shell npm install @inngest/agent-kit inngest @smithery/sdk ``` * pnpm ```shell pnpm install @inngest/agent-kit inngest @smithery/sdk ``` * yarn ```shell yarn add @inngest/agent-kit inngest @smithery/sdk ``` Don’t have an existing project? To create a new project, create a new directory then initialize using your package manager: * npm ```shell mkdir my-agent-kit-project && npm init ``` * pnpm ```shell mkdir my-agent-kit-project && pnpm init ``` * yarn ```shell mkdir my-agent-kit-project && yarn init ``` 2. **Setup an AgentKit Network with an Agent** Create an Agent and its associated Network, for example a Neon Assistant Agent: ```typescript import { z } from "zod"; import { anthropic, createAgent, createNetwork, createTool, } from "@inngest/agent-kit"; const neonAgent = createAgent({ name: "neon-agent", system: `You are a helpful assistant that help manage a Neon account. IMPORTANT: Call the 'done' tool when the question is answered. `, tools: [ createTool({ name: "done", description: "Call this tool when you are finished with the task.", parameters: z.object({ answer: z.string().describe("Answer to the user's question."), }), handler: async ({ answer }, { network }) => { network?.state.kv.set("answer", answer); }, }), ], }); const neonAgentNetwork = createNetwork({ name: "neon-agent", agents: [neonAgent], defaultModel: anthropic({ model: "claude-3-5-sonnet-20240620", defaultParameters: { max_tokens: 1000, }, }), router: ({ network }) => { if (!network?.state.kv.get("answer")) { return neonAgent; } return; }, }); ``` 3. **Add the Neon MCP Smithery Server to your Agent** Add the [Neon MCP Smithery Server](https://smithery.ai/server/neon/) to your Agent by using `createSmitheryUrl()` from the `@smithery/sdk/config.js` module and providing it to the Agent via the `mcpServers` option: ```typescript import { anthropic, createAgent, createNetwork, createTool, } from "@inngest/agent-kit"; import { createSmitheryUrl } from "@smithery/sdk/config.js"; import { z } from "zod"; const smitheryUrl = createSmitheryUrl("https://server.smithery.ai/neon/ws", { neonApiKey: process.env.NEON_API_KEY, }); const neonAgent = createAgent({ name: "neon-agent", system: `You are a helpful assistant that help manage a Neon account. IMPORTANT: Call the 'done' tool when the question is answered. `, tools: [ createTool({ name: "done", description: "Call this tool when you are finished with the task.", parameters: z.object({ answer: z.string().describe("Answer to the user's question."), }), handler: async ({ answer }, { network }) => { network?.state.kv.set("answer", answer); }, }), ], mcpServers: [ { name: "neon", transport: { type: "ws", url: smitheryUrl.toString(), }, }, ], }); const neonAgentNetwork = createNetwork({ name: "neon-agent", agents: [neonAgent], defaultModel: anthropic({ model: "claude-3-5-sonnet-20240620", defaultParameters: { max_tokens: 1000, }, }), router: ({ network }) => { if (!network?.state.kv.get("answer")) { return neonAgent; } return; }, }); ``` Caution Integrating Smithery with AgentKit requires using the `createSmitheryUrl()` function to create a valid URL for the MCP server. Most Smithery servers instruct to use the `createTransport()` function which is not supported by AgentKit. To use the `createSmitheryUrl()` function, simply append `/ws` to the end of the Smithery server URL provided by Smithery. You will find the complete example on GitHub: [Neon Assistant Agent (using MCP) ](https://github.com/inngest/agent-kit/tree/main/examples/mcp-neon-agent/#readme)This examples shows how to use the Neon MCP Smithery Server to build a Neon Assistant Agent that can help you manage your Neon databases. # createAgent > Define an agent Agents are defined using the `createAgent` function. ```ts import { createAgent, agenticOpenai as openai } from '@inngest/agent-kit'; const agent = createAgent({ name: 'Code writer', system: 'You are an expert TypeScript programmer. Given a set of asks, you think step-by-step to plan clean, ' + 'idiomatic TypeScript code, with comments and tests as necessary.' + 'Do not respond with anything else other than the following XML tags:' + '- If you would like to write code, add all code within the following tags (replace $filename and $contents appropriately):' + " $contents", model: openai('gpt-4o-mini'), }); ``` ## Options [Section titled “Options”](#options) `name` string required The name of the agent. Displayed in tracing. `description` string Optional description for the agent, used for LLM-based routing to help the network pick which agent to run next. `model` string required The provider model to use for inference calls. `system` string | function required The system prompt, as a string or function. Functions let you change prompts based off of state and memory. `tools` array\ Defined tools that an agent can call. Tools are created via [`createTool`](/reference/createTool). `lifecycle` Lifecycle Lifecycle hooks that can intercept and modify inputs and outputs throughout the stages of execution of `run()`. Learn about each [lifecycle](#lifecycle) hook that can be defined below. ### `lifecycle` [Section titled “lifecycle”](#lifecycle) `onStart` function Called after the initial prompt messages are created and before the inference call request. The `onStart` hook can be used to: * Modify input prompt for the Agent. * Prevent the agent from being called by throwing an error. `onResponse` function Called after the inference call request is completed and before tool calling. The `onResponse` hook can be used to: * Inspect the tools that the model decided to call. * Modify the response prior to tool calling. `onFinish` function Called after tool calling has completed. The `onFinish` hook can be used to: * Modify the `InferenceResult` including the outputs prior to the result being added to [Network state](/concepts/network-state). - onStart ```ts const agent = createAgent({ name: 'Code writer', lifecycles: { onStart: ({ agent, network, input, system, // The system prompt for the agent history, // An array of messages }) => { // Return the system prompt (the first message), and any history added to the // model's conversation. return { system, history }; }, }, }); ``` - onResponse ```ts function onResponse() {} ``` # createNetwork > Define a network Networks are defined using the `createNetwork` function. ```ts import { createNetwork, openai } from '@inngest/agent-kit'; // Create a network with two agents const network = createNetwork({ agents: [searchAgent, summaryAgent], defaultModel: openai({ model: 'gpt-4o', step }), maxIter: 10, }); ``` ## Options [Section titled “Options”](#options) `agents` array\ required Agents that can be called from within the `Network`. `defaultModel` string The provider model to use for routing inference calls. `system` string required The system prompt, as a string or function. Functions let you change prompts based off of state and memory `tools` array\ Defined tools that an agent can call. Tools are created via [`createTool`](/reference/createTool). # createTool > Provide tools to an agent Tools are defined using the `createTool` function. ```ts import { createTool } from '@inngest/agent-kit'; const tool = createTool({ name: 'write-file', description: 'Write a file to disk with the given contents', parameters: { type: 'object', properties: { path: { type: 'string', description: 'The path to write the file to', }, contents: { type: 'string', description: 'The contents to write to the file', }, }, required: ['path', 'contents'], }, handler: async ({ path, contents }, { agent, network }) => { await fs.writeFile(path, contents); return { success: true }; }, }); ``` ## Options [Section titled “Options”](#options) `name` string required The name of the tool. Used by the model to identify which tool to call. `description` string required A clear description of what the tool does. This helps the model understand when and how to use the tool. `parameters` JSONSchema | ZodType required A JSON Schema object or Zod type that defines the parameters the tool accepts. This is used to validate the model’s inputs and provide type safety. `handler` function required The function that executes when the tool is called. It receives the validated parameters as its first argument and a context object as its second argument. `strict` boolean default: true Option to disable strict validation of the tool parameters. `lifecycle` Lifecycle Lifecycle hooks that can intercept and modify inputs and outputs throughout the stages of tool execution. ### Handler Function [Section titled “Handler Function”](#handler-function) The handler function receives two arguments: 1. `input`: The validated parameters matching your schema definition 2. `context`: An object containing: * `agent`: The Agent instance that called the tool * `network`: The network instance, providing access to the [`network.state`](/reference/state). Example handler with full type annotations: ```ts import { createTool } from '@inngest/agent-kit'; const tool = createTool({ name: 'write-file', description: 'Write a file to disk with the given contents', parameters: { type: 'object', properties: { path: { type: 'string' }, contents: { type: 'string' }, }, }, handler: async ({ path, contents }, { agent, network }) => { await fs.writeFile(path, contents); network.state.fileWritten = true; return { success: true }; }, }); ``` ### `lifecycle` [Section titled “lifecycle”](#lifecycle) `onStart` function Called before the tool handler is executed. The `onStart` hook can be used to: * Modify input parameters before they are passed to the handler * Prevent the tool from being called by throwing an error `onFinish` function Called after the tool handler has completed. The `onFinish` hook can be used to: * Modify the result before it is returned to the agent * Perform cleanup operations - onStart ```ts const tool = createTool({ name: 'write-file', lifecycle: { onStart: ({ parameters }) => { // Validate or modify parameters before execution return parameters; }, }, }); ``` - onFinish ```ts const tool = createTool({ name: 'write-file', lifecycle: { onFinish: ({ result }) => { // Modify or enhance the result return result; }, }, }); ``` # Introduction > SDK Reference ## Overview [Section titled “Overview”](#overview) The Inngest Agent Kit is a TypeScript library is divided into two main parts: [Agent APIs ](/reference/create-agent)All the APIs for creating and configuring agents and tools. [Network APIs ](/reference/create-network)All the APIs for creating and configuring networks and routers. # Anthropic Model > Configure Anthropic as your model provider The `anthropic` function configures Anthropic’s Claude as your model provider. ```ts import { createAgent, anthropic } from "@inngest/agent-kit"; const agent = createAgent({ name: "Code writer", system: "You are an expert TypeScript programmer.", model: anthropic({ model: "claude-3-opus", // Note: max_tokens is required for Anthropic models defaultParameters: { max_tokens: 4096 }, }), }); ``` ## Configuration [Section titled “Configuration”](#configuration) The `anthropic` function accepts a model name string or a configuration object: ```ts const agent = createAgent({ model: anthropic({ model: "claude-3-opus", apiKey: process.env.ANTHROPIC_API_KEY, baseUrl: "https://api.anthropic.com/v1/", betaHeaders: ["computer-vision"], defaultParameters: { temperature: 0.5, max_tokens: 4096 }, }), }); ``` Caution **Note: `defaultParameters.max_tokens` is required.** ### Options [Section titled “Options”](#options) `model` string required ID of the model to use. See the [model endpoint compatibility](https://docs.anthropic.com/en/docs/about-claude/models) table for details on which models work with the Anthropic API. `max_tokens` number **This option has been moved to the `defaultParameters` option.** The maximum number of tokens to generate before stopping. `apiKey` string The Anthropic API key to use for authenticating your request. By default we’ll search for and use the `ANTHROPIC_API_KEY` environment variable. `betaHeaders` string\[] The beta headers to enable, eg. for computer use, prompt caching, and so on. `baseUrl` string default: https\://api.anthropic.com/v1/ The base URL for the Anthropic API. `defaultParameters` object required The default parameters to use for the model (ex: `temperature`, `max_tokens`, etc). **Note: `defaultParameters.max_tokens` is required.** ### Available Models [Section titled “Available Models”](#available-models) ```plaintext "claude-3-5-haiku-latest" "claude-3-5-haiku-20241022" "claude-3-5-sonnet-latest" "claude-3-5-sonnet-20241022" "claude-3-5-sonnet-20240620" "claude-3-opus-latest" "claude-3-opus-20240229" "claude-3-sonnet-20240229" "claude-3-haiku-20240307" "claude-2.1" "claude-2.0" "claude-instant-1.2" ``` # Gemini Model > Configure Google Gemini as your model provider The `gemini` function configures Google’s Gemini as your model provider. ```ts import { createAgent, gemini } from "@inngest/agent-kit"; const agent = createAgent({ name: "Code writer", system: "You are an expert TypeScript programmer.", model: gemini({ model: "gemini-pro" }), }); ``` ## Configuration [Section titled “Configuration”](#configuration) The `gemini` function accepts a model name string or a configuration object: ```ts const agent = createAgent({ model: gemini({ model: "gemini-pro", apiKey: process.env.GOOGLE_API_KEY, baseUrl: "https://generativelanguage.googleapis.com/v1/", defaultParameters: { generationConfig: { temperature: 1.5, }, }, }), }); ``` ### Options [Section titled “Options”](#options) `model` string required ID of the model to use. See the [model endpoint compatibility](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/gemini) table for details on which models work with the Gemini API. `apiKey` string The Google API key to use for authenticating your request. By default we’ll search for and use the `GOOGLE_API_KEY` environment variable. `baseUrl` string default: https\://generativelanguage.googleapis.com/v1/ The base URL for the Gemini API. `defaultParameters` object The default parameters to use for the model. See Gemini’s [`models.generateContent` reference](https://ai.google.dev/api/generate-content#method:-models.generatecontent). ### Available Models [Section titled “Available Models”](#available-models) ```plaintext "gemini-1.5-flash" "gemini-1.5-flash-8b" "gemini-1.5-pro" "gemini-1.0-pro" "text-embedding-004" "aqa" ``` For the latest list of available models, see [Google’s Gemini model overview](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/gemini). ## Limitations [Section titled “Limitations”](#limitations) Gemini models do not currently support function without parameters. # Grok Model > Configure Grok as your model provider The `grok` function configures Grok as your model provider. ```ts import { createAgent, grok } from "@inngest/agent-kit"; const agent = createAgent({ name: "Code writer", system: "You are an expert TypeScript programmer.", model: grok({ model: "grok-4-latest" }), }); ``` ## Configuration [Section titled “Configuration”](#configuration) The `grok` function accepts a model name string or a configuration object: ```ts const agent = createAgent({ model: grok({ model: "grok-4-latest", apiKey: process.env.XAI_API_KEY, baseUrl: "https://api.x.ai/v1", defaultParameters: { temperature: 0.5 }, }), }); ``` ### Options [Section titled “Options”](#options) `model` string required ID of the model to use. See the [xAI models list](https://docs.x.ai/docs/models). `apiKey` string The xAI API key to use for authenticating your request. By default we’ll search for and use the `XAI_API_KEY` environment variable. `baseUrl` string default: https\://api.x.ai/v1 The base URL for the xAI API. `defaultParameters` object The default parameters to use for the model (ex: `temperature`, `max_tokens`, etc). ### Available Models [Section titled “Available Models”](#available-models) ```plaintext "grok-2-1212" "grok-2" "grok-2-latest" "grok-3" "grok-3-latest" "grok-4" "grok-4-latest"; ``` For the latest list of available models, see [xAI’s Grok model overview](https://docs.x.ai/docs/models). ## Limitations [Section titled “Limitations”](#limitations) Grok models do not currently support strict function parameters. # OpenAI Model > Configure OpenAI as your model provider The `openai` function configures OpenAI as your model provider. ```ts import { createAgent, openai } from "@inngest/agent-kit"; const agent = createAgent({ name: "Code writer", system: "You are an expert TypeScript programmer.", model: openai({ model: "gpt-4" }), }); ``` ## Configuration [Section titled “Configuration”](#configuration) The `openai` function accepts a model name string or a configuration object: ```ts const agent = createAgent({ model: openai({ model: "gpt-4", apiKey: process.env.OPENAI_API_KEY, baseUrl: "https://api.openai.com/v1/", defaultParameters: { temperature: 0.5 }, }), }); ``` ### Options [Section titled “Options”](#options) `model` string required ID of the model to use. See the [model endpoint compatibility](https://platform.openai.com/docs/models#model-endpoint-compatibility) table for details on which models work with the Chat API. `apiKey` string The OpenAI API key to use for authenticating your request. By default we’ll search for and use the `OPENAI_API_KEY` environment variable. `baseUrl` string default: https\://api.openai.com/v1/ The base URL for the OpenAI API. `defaultParameters` object The default parameters to use for the model (ex: `temperature`, `max_tokens`, etc). ### Available Models [Section titled “Available Models”](#available-models) ```plaintext "gpt-4o" "chatgpt-4o-latest" "gpt-4o-mini" "gpt-4" "o1-preview" "o1-mini" "gpt-3.5-turbo" ``` # Network Router > Controlling the flow of execution between agents in a Network. The `defaultRouter` option in `createNetwork` defines how agents are coordinated within a Network. It can be either a [Function Router](#function-router) or a [Routing Agent](#routing-agent). ## Function Router [Section titled “Function Router”](#function-router) A function router is provided to the `defaultRouter` option in `createNetwork`. ### Example [Section titled “Example”](#example) ```ts const network = createNetwork({ agents: [classifier, writer], router: ({ lastResult, callCount, network, stack, input }) => { // First call: use the classifier if (callCount === 0) { return classifier; } // Get the last message from the output const lastMessage = lastResult?.output[lastResult?.output.length - 1]; const content = lastMessage?.type === "text" ? (lastMessage?.content as string) : ""; // Second call: if it's a question, use the writer if (callCount === 1 && content.includes("question")) { return writer; } // Otherwise, we're done! return undefined; }, }); ``` ### Parameters [Section titled “Parameters”](#parameters) `input` string The original input provided to the network. `network` Network The network instance, including its state and history. See [`Network.State`](/reference/state) for more details. `stack` Agent\[] The list of future agents to be called. (*internal read-only value*) `callCount` number The number of agent calls that have been made. `lastResult` InferenceResult The result from the previously called agent. See [`InferenceResult`](/reference/state#inferenceresult) for more details. ### Return Values [Section titled “Return Values”](#return-values) | Return Type | Description | | -------------- | -------------------------------------------------- | | `Agent` | Single agent to execute next | | `Agent[]` | Multiple agents to execute in sequence | | `RoutingAgent` | Delegate routing decision to another routing agent | | `undefined` | Stop network execution | ## createRoutingAgent() [Section titled “createRoutingAgent()”](#createroutingagent) Creates a new routing agent that can be used as a `defaultRouter` in a network. ### Example [Section titled “Example”](#example-1) ```ts import { createRoutingAgent, createNetwork } from "@inngest/agent-kit"; const routingAgent = createRoutingAgent({ name: "Custom routing agent", description: "Selects agents based on the current state and request", lifecycle: { onRoute: ({ result, network }) => { // Get the agent names from the result const agentNames = result.output .filter((m) => m.type === "text") .map((m) => m.content as string); // Validate that the agents exist return agentNames.filter((name) => network.agents.has(name)); }, }, }); // classifier and writer Agents definition... const network = createNetwork({ agents: [classifier, writer], router: routingAgent, }); ``` ### Parameters [Section titled “Parameters”](#parameters-1) `name` string required The name of the routing agent. `description` string Optional description of the routing agent’s purpose. `lifecycle` object required `onRoute` function required Called after each inference to determine the next agent(s) to call. **Arguments:** ```ts { result: InferenceResult; // The result from the routing agent's inference agent: RoutingAgent; // The routing agent instance network: Network; // The network instance } ``` **Returns:** `string[]` - Array of agent names to call next, or `undefined` to stop execution `model` AiAdapter.Any Optional model to use for routing decisions. If not provided, uses the network’s `defaultModel`. ### Returns [Section titled “Returns”](#returns) Returns a `RoutingAgent` instance that can be used as a network’s `defaultRouter`. ## Related APIs [Section titled “Related APIs”](#related-apis) * [`createNetwork`](/reference/create-network) * [`Network.State`](/reference/state) # AgentProvider API Reference > Complete API documentation for AgentProvider - shared connections and configuration management The `AgentProvider` is a React context provider that enables **shared WebSocket connections** and **centralized configuration** across your entire AgentKit application. It significantly improves performance and simplifies setup. Note Using AgentProvider can reduce WebSocket connections from N (one per hook) to 1 (shared across all components), improving performance by 3-5x and reducing server load substantially. ## Import [Section titled “Import”](#import) ```typescript import { AgentProvider } from "@inngest/use-agent"; ``` ## Basic Usage [Section titled “Basic Usage”](#basic-usage) ```typescript import { AgentProvider } from "@inngest/use-agent"; function App() { return ( ); } function ChatApplication() { // Automatically inherits userId and debug from provider const { messages, sendMessage } = useChat(); return ; } ``` ## Configuration: `AgentProviderProps` [Section titled “Configuration: AgentProviderProps”](#configuration-agentproviderprops) ### User & Channel Configuration [Section titled “User & Channel Configuration”](#user--channel-configuration) `userId` string User identifier for attribution and data ownership. If not provided, automatically generates a persistent anonymous ID. ```typescript // Authenticated user // Anonymous user (auto-generates persistent ID) ``` `channelKey` string Channel key for subscription targeting. Enables collaborative features when multiple users need to share the same conversation stream. ```typescript // Private chat (default - uses userId as channelKey) // Collaborative chat (multiple users share channelKey) ``` `debug` boolean default: true in development Enable comprehensive debug logging for all child hooks and connections. ```typescript ``` ### Transport Configuration [Section titled “Transport Configuration”](#transport-configuration) `transport` AgentTransport | Partial\ Transport configuration for API calls. Can be either a complete transport instance or configuration object to customize the default transport. **Configuration Object** (most common): ```typescript ``` **Transport Instance** (advanced): ```typescript import { DefaultAgentTransport } from "@inngest/use-agent"; const customTransport = new DefaultAgentTransport({ // Custom configuration }); ``` ## Channel Resolution Logic [Section titled “Channel Resolution Logic”](#channel-resolution-logic) The provider uses intelligent logic to determine which WebSocket channel to subscribe to: ```mermaid flowchart TD A[AgentProvider] --> B{channelKey provided?} B -->|Yes| C["Use channelKey"] B -->|No| D{userId provided?} D -->|Yes| E["Use userId as channelKey"] D -->|No| F["Generate anonymous ID"] F --> G["Store in sessionStorage"] G --> H["Use anonymous ID as channelKey"] C --> I[Subscribe to WebSocket channel] E --> I H --> I style C fill:#e8f5e8,color:#000 style E fill:#fff3cd,color:#000 style H fill:#ffe6e6,color:#000 ``` ### Channel Key Examples [Section titled “Channel Key Examples”](#channel-key-examples) ```typescript // 1. Explicit channelKey (collaborative scenarios) // → Subscribes to channel "project-123" // → Multiple users can share this channel // 2. UserId fallback (private chat) // → Subscribes to channel "user-789" // → Private chat for this user // 3. Anonymous fallback (guest users) // → Generates "anon_a1b2c3d4e5f6" // → Stores in sessionStorage // → Persists across page reloads ``` ## Configuration Inheritance [Section titled “Configuration Inheritance”](#configuration-inheritance) ### Hierarchy [Section titled “Hierarchy”](#hierarchy) Child hooks inherit configuration with override capability: 1. **Hook-level options** (highest priority) 2. **Provider options** (inherited when available) 3. **Default values** (fallback) ```typescript {/* Inherits all config */} {/* Override userId, inherit debug + transport */} {/* Override transport, inherit userId + debug */} ``` ### Inheritance Examples [Section titled “Inheritance Examples”](#inheritance-examples) ```typescript // Provider configuration function ChatComponents() { // Full inheritance const chat1 = useChat(); // → userId: "user-123", debug: true, channelKey: "team-chat" // Partial override const chat2 = useChat({ userId: "different-user" }); // → userId: "different-user", debug: true, channelKey: "team-chat" // Full override const chat3 = useChat({ userId: "user-789", channelKey: "private-chat", debug: false }); // → userId: "user-789", debug: false, channelKey: "private-chat" } ``` ## Smart Connection Sharing [Section titled “Smart Connection Sharing”](#smart-connection-sharing) ### Shared Connection Logic [Section titled “Shared Connection Logic”](#shared-connection-logic) The provider determines when to share connections based on resolved channel keys: ```typescript {/* channelKey resolves to "user-123" */} {/* channelKey: "user-123" → shared */} {/* channelKey: "user-123" → shared */} {/* channelKey: "admin" → separate */} ``` **Result**: * Components A & B share a single WebSocket connection * Component C gets its own isolated connection for “admin” channel ### Performance Benefits [Section titled “Performance Benefits”](#performance-benefits) ```typescript // ❌ Without Provider: Multiple connections function App() { return (
{/* useThreads → WebSocket connection 1 */} {/* useAgent → WebSocket connection 2 */} {/* useChat → WebSocket connection 3 */} {/* useAgent → WebSocket connection 4 */}
); } // Result: 4 WebSocket connections, 4x memory usage // ✅ With Provider: Shared connection function App() { return ( {/* Shares connection */} {/* Shares connection */} {/* Shares connection */} {/* Shares connection */} ); } // Result: 1 WebSocket connection, efficient resource usage ``` ## Advanced Usage Patterns [Section titled “Advanced Usage Patterns”](#advanced-usage-patterns) ### Multi-tenant Applications [Section titled “Multi-tenant Applications”](#multi-tenant-applications) ```typescript function TenantApp({ tenantId, userId }) { const tenantTransport = useMemo(() => createDefaultAgentTransport({ headers: { 'X-Tenant-ID': tenantId, 'Authorization': `Bearer ${getTenantToken(tenantId)}` }, baseURL: `https://${tenantId}.api.myapp.com` }), [tenantId] ); return ( ); } ``` ### Environment-based Configuration [Section titled “Environment-based Configuration”](#environment-based-configuration) ```typescript function EnvironmentAwareProvider({ children }) { const config = useMemo(() => { if (process.env.NODE_ENV === 'development') { return { debug: true, transport: { baseURL: 'http://localhost:3000', headers: { 'X-Environment': 'dev' } } }; } if (process.env.NODE_ENV === 'staging') { return { debug: true, transport: { baseURL: 'https://staging-api.myapp.com', headers: { 'X-Environment': 'staging' } } }; } return { debug: false, transport: { baseURL: 'https://api.myapp.com', headers: () => ({ 'Authorization': `Bearer ${getProductionToken()}`, 'X-Environment': 'production' }) } }; }, []); return ( {children} ); } ``` ### Authentication Integration [Section titled “Authentication Integration”](#authentication-integration) ```typescript function AuthenticatedProvider({ children }) { const { user, token, isLoading } = useAuth(); const transport = useMemo(() => createDefaultAgentTransport({ headers: { 'Authorization': `Bearer ${token}`, 'X-User-Role': user?.role || 'guest' } }), [token, user?.role] ); if (isLoading) return ; return user ? ( {children} ) : ( {/* Anonymous mode */} {children} ); } ``` ## Provider Utilities [Section titled “Provider Utilities”](#provider-utilities) ### Optional Access Hooks [Section titled “Optional Access Hooks”](#optional-access-hooks) Access provider values safely, even outside the provider context: ```typescript import { useOptionalGlobalUserId, useOptionalGlobalTransport, useOptionalGlobalAgent, useOptionalGlobalChannelKey, useOptionalGlobalResolvedChannelKey } from "@inngest/use-agent"; function FlexibleComponent() { const globalUserId = useOptionalGlobalUserId(); // string | null const globalTransport = useOptionalGlobalTransport(); // AgentTransport | null const globalAgent = useOptionalGlobalAgent(); // UseAgentReturn | null const globalChannelKey = useOptionalGlobalChannelKey(); // string | null const resolvedChannelKey = useOptionalGlobalResolvedChannelKey(); // string | null // Handle both provider and non-provider scenarios const effectiveUserId = globalUserId || "fallback-user"; const effectiveTransport = globalTransport || createDefaultAgentTransport(); return
Works with or without provider!
; } ``` ### Strict Access Hooks [Section titled “Strict Access Hooks”](#strict-access-hooks) For components that require provider context: ```typescript import { useGlobalAgentStrict, useGlobalTransportStrict, useGlobalUserId, useGlobalChannelKey, } from "@inngest/use-agent"; function ProviderRequiredComponent() { const agent = useGlobalAgentStrict(); // Throws if no provider const transport = useGlobalTransportStrict(); // Throws if no provider const userId = useGlobalUserId(); // Returns null if no provider const channelKey = useGlobalChannelKey(); // Returns null if no provider // These components fail fast if used outside AgentProvider } ``` ## Debugging & Troubleshooting [Section titled “Debugging & Troubleshooting”](#debugging--troubleshooting) ### Debug Output [Section titled “Debug Output”](#debug-output) Enable debug mode to see provider activity: ```typescript // Console output: // 🔍 [AgentProvider] Creating agent connection for channelKey: user-123 // ✅ [AgentProvider] Channel subscription token created // 🔧 [useChat] Inheriting userId from provider: user-123 // 🔄 [useAgent] Using shared connection for channel: user-123 ``` ### Connection Diagnostics [Section titled “Connection Diagnostics”](#connection-diagnostics) ```typescript function ConnectionDiagnostics() { const globalAgent = useOptionalGlobalAgent(); const resolvedChannelKey = useOptionalGlobalResolvedChannelKey(); useEffect(() => { console.log('Provider Diagnostics:', { hasProvider: !!globalAgent, channelKey: resolvedChannelKey, isConnected: globalAgent?.isConnected, activeThreads: globalAgent ? Object.keys(globalAgent.threads).length : 0 }); }, [globalAgent, resolvedChannelKey]); return (
{globalAgent ? (
✅ Provider connection active for {resolvedChannelKey}
) : (
❌ No provider - components using individual connections
)}
); } ``` ### Common Issues [Section titled “Common Issues”](#common-issues) **“Multiple connections created”** * Check for channelKey mismatches between provider and hooks * Ensure transport configuration is stable (use `useMemo`) * Verify provider is properly wrapping all components ```typescript // ❌ Causes multiple connections {/* Separate connection! */} // ✅ Shared connection {/* Inherits channelKey → shared connection */} ``` **“Configuration not inherited”** * Verify hooks are called within provider children * Check for TypeScript errors in configuration * Enable debug mode to trace inheritance **“Transport not working”** * Ensure transport configuration is stable across renders * Check for authentication/network issues * Verify API endpoints are correct ## Real-world Examples [Section titled “Real-world Examples”](#real-world-examples) ### E-commerce Customer Support [Section titled “E-commerce Customer Support”](#e-commerce-customer-support) ```typescript function CustomerSupportApp({ customerId }) { const supportTransport = useMemo(() => createDefaultAgentTransport({ api: { sendMessage: '/api/support/chat', fetchThreads: '/api/support/threads', approveToolCall: '/api/support/approve-action' }, headers: { 'X-Customer-ID': customerId, 'X-Support-Context': 'customer-portal', 'Authorization': `Bearer ${getCustomerToken()}` } }), [customerId] ); return ( ); } ``` ### Team Collaboration Platform [Section titled “Team Collaboration Platform”](#team-collaboration-platform) ```typescript function TeamWorkspace({ teamId, currentUserId }) { return ( {/* Shared team conversations */} {/* Personal AI assistant */} {/* Project-specific chats */} ); } // All components receive events from the same team channel // but can create isolated threads for different discussions ``` ### Development vs Production [Section titled “Development vs Production”](#development-vs-production) ```typescript function EnvironmentProvider({ children }) { const isDev = process.env.NODE_ENV === 'development'; const config = useMemo(() => ({ debug: isDev, transport: { baseURL: isDev ? 'http://localhost:3000' : 'https://api.myapp.com', headers: isDev ? { 'X-Environment': 'development' } : () => ({ 'Authorization': `Bearer ${getProductionToken()}`, 'X-Environment': 'production' }) } }), [isDev]); return ( {children} ); } ``` ## Performance Optimization [Section titled “Performance Optimization”](#performance-optimization) ### Stable Configuration [Section titled “Stable Configuration”](#stable-configuration) ```typescript function OptimizedApp() { // ✅ Stable transport configuration const transport = useMemo(() => createDefaultAgentTransport({ headers: { 'Authorization': `Bearer ${token}` } }), [token] // Only recreate when token changes ); return ( ); } // ❌ Recreated on every render function UnoptimizedApp() { return ( ); } ``` ### Connection Lifecycle [Section titled “Connection Lifecycle”](#connection-lifecycle) The provider manages connection lifecycle efficiently: ```typescript // Provider creates connection on mount useEffect(() => { const connection = establishConnection(resolvedChannelKey); return () => { connection.cleanup(); // Automatic cleanup on unmount }; }, [resolvedChannelKey]); // Only recreate if channel changes // Child components automatically share this connection ``` ### Memory Management [Section titled “Memory Management”](#memory-management) ```typescript function MemoryEfficientProvider({ children }) { // ✅ Stable anonymous ID generation const [anonymousId] = useState(() => { if (typeof window !== 'undefined') { let id = sessionStorage.getItem("agentkit-anonymous-id"); if (!id) { id = `anon_${uuidv4()}`; sessionStorage.setItem("agentkit-anonymous-id", id); } return id; } return `anon_${uuidv4()}`; }); return ( {children} ); } ``` ## Multi-Provider Scenarios [Section titled “Multi-Provider Scenarios”](#multi-provider-scenarios) ### Nested Providers [Section titled “Nested Providers”](#nested-providers) Handle complex applications with multiple user contexts: ```typescript function MultiContextApp() { return ( {/* Main user context */} {/* Nested admin context */} {/* Uses admin context */} {/* Guest support context */} {/* Anonymous user */} {/* Uses anonymous context */} ); } ``` ### Provider Isolation [Section titled “Provider Isolation”](#provider-isolation) Control connection isolation for security or performance: ```typescript function IsolatedFeatures() { return (
{/* Main application */} {/* Isolated admin panel */} {/* Isolated demo area */} {/* Anonymous session */}
); } ``` ## Testing Strategies [Section titled “Testing Strategies”](#testing-strategies) ### Provider Mocking [Section titled “Provider Mocking”](#provider-mocking) ```typescript // Mock provider for testing function MockAgentProvider({ children, mockAgent }) { const mockContext = { agent: mockAgent, transport: mockTransport, userId: "test-user", channelKey: "test-channel", resolvedChannelKey: "test-channel" }; return ( {children} ); } // Test component test('chat component with mock provider', () => { const mockAgent = { messages: [], sendMessage: jest.fn(), status: 'idle', isConnected: true }; render( ); }); ``` ### Integration Testing [Section titled “Integration Testing”](#integration-testing) ```typescript function TestWrapper({ children, config = {} }) { const defaultConfig = { userId: "test-user-123", debug: true, transport: createTestTransport() }; return ( {children} ); } // Use in tests describe('Chat Integration', () => { test('sends message successfully', async () => { render( ); // Test interactions... }); }); ``` ## Security Considerations [Section titled “Security Considerations”](#security-considerations) ### Token Management [Section titled “Token Management”](#token-management) ```typescript function SecureProvider({ children }) { const [token, setToken] = useState(null); const transport = useMemo(() => { if (!token) return undefined; return createDefaultAgentTransport({ headers: () => ({ 'Authorization': `Bearer ${token}`, 'X-Timestamp': Date.now().toString(), 'X-Request-ID': generateRequestId() }) }); }, [token]); // Handle token refresh useEffect(() => { const refreshToken = async () => { try { const newToken = await getValidToken(); setToken(newToken); } catch (error) { console.error('Token refresh failed:', error); // Handle auth failure } }; refreshToken(); const interval = setInterval(refreshToken, 15 * 60 * 1000); // Refresh every 15min return () => clearInterval(interval); }, []); if (!token || !transport) { return ; } return ( {children} ); } ``` ### Channel Security [Section titled “Channel Security”](#channel-security) ```typescript // Ensure channel keys are properly scoped for security function SecureChannelProvider({ userId, tenantId, children }) { // Include tenant context in channel key to prevent cross-tenant access const secureChannelKey = `tenant-${tenantId}-user-${userId}`; return ( {children} ); } ``` ## Best Practices [Section titled “Best Practices”](#best-practices) ### ✅ Do [Section titled “✅ Do”](#-do) * **Wrap early in component tree** (layout.tsx or \_app.tsx) * **Use stable configuration** objects with `useMemo` * **Enable debug mode** during development * **Leverage inheritance** to reduce configuration duplication * **Plan channel keys** for proper connection isolation * **Handle authentication** gracefully with token refresh ### ❌ Don’t [Section titled “❌ Don’t”](#-dont) * **Nest providers unnecessarily** - one provider usually sufficient * **Recreate transport** configuration on every render * **Hardcode sensitive values** - use environment variables * **Ignore connection sharing** - it significantly improves performance * **Mix channelKeys carelessly** - understand security implications ### Performance Tips [Section titled “Performance Tips”](#performance-tips) ```typescript // ✅ Efficient patterns const stableConfig = useMemo(() => ({ transport: createDefaultAgentTransport(config) }), [configDependencies]); // ✅ Proper dependency management const transport = useMemo(() => createDefaultAgentTransport({ headers: { token } }), [token] // Only recreate when token changes ); // ✅ Early provider placement {/* Wrap entire app */} ``` ## Next Steps [Section titled “Next Steps”](#next-steps) [useChat Reference ](/reference/react-hooks/use-chat)Recommended hook that builds on AgentProvider for complete chat apps [Transport Reference ](/reference/react-hooks/transport)Complete transport configuration and customization guide [Provider Pattern Guide ](/streaming/provider-pattern)Best practices and optimization strategies for AgentProvider [Performance Optimization ](/advanced-patterns/production-deployment)Scale AgentKit applications for production workloads AgentProvider is the foundation that makes AgentKit’s React hooks both powerful and efficient. By centralizing configuration and enabling connection sharing, it transforms individual hooks into a cohesive, high-performance system for building production-scale AI applications. # React Hooks API Reference > Complete API documentation for @inngest/use-agent React hooks The `@inngest/use-agent` package provides a comprehensive set of React hooks for building AI chat interfaces with AgentKit networks. This reference section documents every hook, component, and utility in detail. ## Package Overview [Section titled “Package Overview”](#package-overview) ```bash npm install @inngest/use-agent # Peer dependencies npm install react @inngest/realtime uuid ``` [@inngest/use-agent on npm ](https://www.npmjs.com/package/@inngest/use-agent)View package details, versions, and installation stats [GitHub Repository ](https://github.com/inngest/agent-kit/tree/main/packages/use-agent)Source code, issues, and contributions ## Core Hooks [Section titled “Core Hooks”](#core-hooks) These hooks provide the primary functionality for building AI chat applications: [useChat ](/reference/react-hooks/use-chat)Recommended: Unified hook for complete chat applications with streaming, threads, and persistence [useAgent ](/reference/react-hooks/use-agent)Advanced: Low-level hook for real-time streaming and multi-thread management [useThreads ](/reference/react-hooks/use-threads)Specialized: Thread management, pagination, and persistence operations ## Infrastructure Components [Section titled “Infrastructure Components”](#infrastructure-components) Essential components for connection management and configuration: [AgentProvider ](/reference/react-hooks/agent-provider)Context provider for shared connections, configuration, and performance optimization [AgentTransport ](/reference/react-hooks/transport)Configurable API layer for customizing endpoints, authentication, and request handling ## Utility Hooks [Section titled “Utility Hooks”](#utility-hooks) Specialized hooks for specific features and UI patterns: [Utility Hooks Reference ](/reference/react-hooks/utility-hooks)useMessageActions, useEditMessage, useEphemeralThreads, useSidebar, useIsMobile, and more ## Package Architecture [Section titled “Package Architecture”](#package-architecture) ### Hook Categories [Section titled “Hook Categories”](#hook-categories) **Core Streaming Hooks**: * `useAgent`: Multi-thread real-time streaming * `useChat`: Complete chat application functionality * `useThreads`: Thread persistence and management **Specialized Storage**: * `useEphemeralThreads`: Client-side storage for demos * `useConversationBranching`: Message editing workflows **UI Utilities**: * `useMessageActions`: Copy, like, share, read aloud * `useEditMessage`: Message editing state management * `useSidebar`: Responsive sidebar state * `useIsMobile`: Mobile device detection **Infrastructure**: * `AgentProvider`: Shared context and connections * Transport layer: Configurable API communication ### Design Principles [Section titled “Design Principles”](#design-principles) **🎯 Progressive Enhancement**: Start simple with `useChat`, add advanced hooks as needed **🔧 Composable Architecture**: Hooks work independently or together based on your needs **⚡ Performance First**: Intelligent connection sharing and efficient state management **🛡️ Production Ready**: Comprehensive error handling, TypeScript support, and testing **🔄 Backward Compatible**: Smooth migration from local implementations to package ## Type System [Section titled “Type System”](#type-system) ### Core Types [Section titled “Core Types”](#core-types) All hooks use consistent, well-defined TypeScript interfaces: ```typescript import type { // Message and content types ConversationMessage, MessagePart, TextUIPart, ToolCallUIPart, HitlUIPart, // State and status types AgentStatus, Thread, ThreadState, // Event and streaming types NetworkEvent, AgentMessageChunk, // Transport and configuration AgentTransport, UseAgentOptions, UseChatConfig, // Error handling AgentError, ErrorClassification, } from "@inngest/use-agent"; ``` ### Hook Return Types [Section titled “Hook Return Types”](#hook-return-types) Each hook has a comprehensive return type interface: ```typescript import type { UseAgentReturn, UseChatReturn, UseThreadsReturn, } from "@inngest/use-agent"; // Example: Complete typing for useChat const chat: UseChatReturn = useChat({ initialThreadId: "thread-123", }); ``` ## Common Patterns [Section titled “Common Patterns”](#common-patterns) ### Basic Chat Application [Section titled “Basic Chat Application”](#basic-chat-application) ```typescript import { useChat, AgentProvider } from "@inngest/use-agent"; function App() { return ( ); } function ChatInterface() { const { messages, // ConversationMessage[] sendMessage, // (message: string) => Promise status, // AgentStatus isConnected, // boolean threads, // Thread[] switchToThread, // (threadId: string) => Promise createNewThread // () => string } = useChat(); return
/* Your chat UI */
; } ``` ### Custom Transport Configuration [Section titled “Custom Transport Configuration”](#custom-transport-configuration) ```typescript import { AgentProvider, createDefaultAgentTransport, type DefaultAgentTransportConfig } from "@inngest/use-agent"; const transport: DefaultAgentTransportConfig = { api: { sendMessage: '/api/v2/chat', fetchThreads: '/api/v2/conversations' }, headers: { 'Authorization': `Bearer ${token}` } }; ``` ### Multi-thread Management [Section titled “Multi-thread Management”](#multi-thread-management) ```typescript import { useAgent } from "@inngest/use-agent"; function AdvancedChat() { const { threads, // Record currentThreadId, // string setCurrentThread, // (threadId: string) => void sendMessageToThread, // (threadId: string, message: string) => Promise getThread, // (threadId: string) => ThreadState | undefined createThread, // (threadId: string) => void removeThread // (threadId: string) => void } = useAgent({ threadId: 'initial-thread', userId: 'user-123' }); return
/* Custom multi-thread UI */
; } ``` ## Performance Guidelines [Section titled “Performance Guidelines”](#performance-guidelines) ### Memory Management [Section titled “Memory Management”](#memory-management) **✅ Efficient Patterns**: ```typescript // Stable configuration objects const transportConfig = useMemo( () => ({ headers: { Authorization: `Bearer ${token}` }, }), [token] ); // Reasonable state capture state: () => ({ currentTab: activeTab, formData: getCurrentForm(), }); ``` **❌ Memory Leaks**: ```typescript // Don't capture massive objects state: () => ({ entireAppState: store.getState(), // Too large! allUserHistory: history, // Too large! }); // Don't recreate config on every render transport: createDefaultAgentTransport({ // New instance! headers: { Authorization: `Bearer ${token}` }, }); ``` ### Connection Optimization [Section titled “Connection Optimization”](#connection-optimization) **✅ Efficient**: ```typescript // Single provider, shared connections {/* Shares connection */} {/* Shares connection */} ``` **❌ Inefficient**: ```typescript // Multiple separate connections {/* useThreads creates connection */} {/* useAgent creates connection */} ``` ## Error Handling [Section titled “Error Handling”](#error-handling) ### Rich Error Objects [Section titled “Rich Error Objects”](#rich-error-objects) All hooks provide comprehensive error information: ```typescript const { error, connectionError, clearError } = useChat(); if (error) { console.log({ message: error.message, // Human-readable error recoverable: error.recoverable, // Can user retry? timestamp: error.timestamp, // When it occurred suggestion: error.suggestion, // How to fix }); } ``` ### Error Recovery Patterns [Section titled “Error Recovery Patterns”](#error-recovery-patterns) ```typescript // Automatic retry for recoverable errors useEffect(() => { if (error?.recoverable) { const timer = setTimeout(() => { clearError(); // Optionally retry the failed operation }, 3000); return () => clearTimeout(timer); } }, [error]); ``` ## Debug Utilities [Section titled “Debug Utilities”](#debug-utilities) ### Debug Logging [Section titled “Debug Logging”](#debug-logging) Enable comprehensive debug output: ```typescript import { createDebugLogger } from "@inngest/use-agent"; const logger = createDebugLogger("MyComponent", true); logger.log("Component initialized"); logger.warn("Non-critical issue"); logger.error("Critical error"); ``` ### Development vs Production [Section titled “Development vs Production”](#development-vs-production) ```typescript ``` ## Migration Guide [Section titled “Migration Guide”](#migration-guide) ### From Local Implementations [Section titled “From Local Implementations”](#from-local-implementations) ```typescript // Before: Local imports from examples import { useChat } from "@/hooks/use-chat"; import { AgentProvider } from "@/contexts/AgentContext"; // After: Package imports import { useChat, AgentProvider } from "@inngest/use-agent"; ``` **No API changes required** - the package maintains full backward compatibility with local implementations. ### Version Compatibility [Section titled “Version Compatibility”](#version-compatibility) | Package Version | AgentKit Version | React Version | Features | | --------------- | ---------------- | ------------- | ---------------- | | 1.0.0+ | 0.3.0+ | 18.0.0+ | Full feature set | | 0.9.0+ | 0.2.0+ | 18.0.0+ | Beta features | ## Next Steps [Section titled “Next Steps”](#next-steps) [useChat Complete Reference ](/reference/react-hooks/use-chat)Full API documentation for the primary chat hook [AgentProvider Reference ](/reference/react-hooks/agent-provider)Configuration and optimization guide for the provider [Transport Layer Reference ](/reference/react-hooks/transport)Customize API communication and authentication [Streaming Quickstart ](/streaming/react-hooks-quickstart)Get started building with the hooks in 5 minutes This reference section provides the definitive documentation for every aspect of the `@inngest/use-agent` package. Whether you’re just getting started or building advanced custom implementations, you’ll find the complete API documentation and guidance you need here. # useAgent API Reference > Complete API documentation for the useAgent hook - advanced streaming and multi-thread control The `useAgent` hook provides **advanced, low-level control** over AgentKit’s real-time streaming system. It manages WebSocket connections, processes streaming events, and maintains conversation state across multiple threads simultaneously. Caution Use `useAgent` for custom implementations requiring fine control over streaming events. For most applications, [`useChat`](/reference/react-hooks/use-chat) is recommended as it provides the same functionality with automatic coordination. ## Import [Section titled “Import”](#import) ```typescript import { useAgent } from "@inngest/use-agent"; ``` ## Basic Usage [Section titled “Basic Usage”](#basic-usage) ```typescript function CustomChatComponent() { const { messages, status, sendMessage, isConnected, threads, setCurrentThread } = useAgent({ threadId: 'conversation-123', userId: 'user-456', debug: true }); return (
Status: {status}
Connected: {isConnected ? 'Yes' : 'No'}
{/* Manual thread switching */} {Object.keys(threads).map(threadId => ( ))} {messages.map(msg => (
{/* Message rendering */}
))}
); } ``` ## Configuration: `UseAgentOptions` [Section titled “Configuration: UseAgentOptions”](#configuration-useagentoptions) ### Required Options [Section titled “Required Options”](#required-options) `threadId` string required Unique identifier for the conversation thread. This is the primary thread that the hook will manage. ```typescript useAgent({ threadId: "conversation-123" }); ``` ### User & Channel Configuration [Section titled “User & Channel Configuration”](#user--channel-configuration) `userId` string User identifier for attribution and data ownership. If not provided, automatically generates an anonymous ID. ```typescript // Authenticated user useAgent({ threadId: "thread-123", userId: "user-456" }); // Anonymous user (auto-generated ID) useAgent({ threadId: "thread-123" }); ``` `channelKey` string Channel key for subscription targeting. Enables collaborative features and flexible connection management. ```typescript // Private chat (default) useAgent({ threadId: "thread-123", userId: "user-456" }); // Collaborative chat useAgent({ threadId: "thread-123", userId: "user-456", channelKey: "project-789", // Multiple users can share this channel }); ``` ### Advanced Configuration [Section titled “Advanced Configuration”](#advanced-configuration) `debug` boolean default: true in development Enable comprehensive debug logging for event processing, connection management, and state updates. ```typescript useAgent({ threadId: "thread-123", debug: process.env.NODE_ENV === "development", }); ``` `state` () => Record\ Function to capture client-side state with each message for debugging and regeneration workflows. ```typescript useAgent({ threadId: "thread-123", state: () => ({ currentPage: window.location.pathname, formData: getActiveFormData(), uiMode: getCurrentMode(), timestamp: Date.now(), }), }); ``` `transport` AgentTransport Custom transport instance for API calls. If not provided, uses default transport or inherits from AgentProvider. ```typescript import { createDefaultAgentTransport } from "@inngest/use-agent"; const customTransport = createDefaultAgentTransport({ api: { sendMessage: "/api/v2/chat" }, headers: { Authorization: `Bearer ${token}` }, }); useAgent({ threadId: "thread-123", transport: customTransport, }); ``` `onError` (error: Error) => void Callback for handling errors during agent execution. ```typescript useAgent({ threadId: "thread-123", onError: (error) => { console.error("Agent error:", error); showErrorNotification(error.message); analytics.track("agent_error", { error: error.message }); }, }); ``` `__disableSubscription` boolean default: false **Internal**: Disable WebSocket subscription for this instance. Used internally by AgentProvider for connection sharing. ## Return Value: `UseAgentReturn` [Section titled “Return Value: UseAgentReturn”](#return-value-useagentreturn) ### Current Thread State (Backward Compatible) [Section titled “Current Thread State (Backward Compatible)”](#current-thread-state-backward-compatible) `messages` ConversationMessage\[] Messages in the currently active thread, updated in real-time as streaming events arrive. ```typescript messages.forEach((msg) => { console.log(`${msg.role}: ${msg.parts.length} parts`); msg.parts.forEach((part) => { switch (part.type) { case "text": console.log(`Text: ${part.content} (${part.status})`); break; case "tool-call": console.log(`Tool: ${part.toolName} (${part.state})`); break; case "reasoning": console.log(`Reasoning: ${part.content}`); break; } }); }); ``` `status` AgentStatus Current agent execution status for the active thread: `"idle"`, `"thinking"`, `"calling-tool"`, `"responding"`, or `"error"`. ```typescript // UI feedback based on status switch (status) { case 'thinking': return ; case 'calling-tool': return ; case 'responding': return ; case 'error': return ; default: return ; } ``` `currentAgent` string | undefined Name of the agent currently processing requests for the active thread. ```typescript
{currentAgent ? `${currentAgent} is responding...` : 'Assistant'}
``` `error` { message: string; timestamp: Date; recoverable: boolean } | undefined Error information for the active thread, if any. ```typescript {error && ( { clearError(); // Retry logic }} /> )} ``` ### Multi-Thread State [Section titled “Multi-Thread State”](#multi-thread-state) `threads` Record\ Complete state for all active threads, indexed by threadId. Enables background streaming and thread management. ```typescript // Access any thread's state const threadState = threads["thread-789"]; if (threadState) { console.log({ messages: threadState.messages.length, status: threadState.status, hasNewMessages: threadState.hasNewMessages, lastActivity: threadState.lastActivity, }); } // List all active threads Object.keys(threads).forEach((threadId) => { const thread = threads[threadId]; console.log( `Thread ${threadId}: ${thread.messages.length} messages, status: ${thread.status}` ); }); ``` `currentThreadId` string ID of the currently active/displayed thread. ```typescript console.log("Currently viewing thread:", currentThreadId); console.log("Total active threads:", Object.keys(threads).length); ``` ### Connection State [Section titled “Connection State”](#connection-state) `isConnected` boolean WebSocket connection status to the real-time event stream. ```typescript // Show connection indicator
{isConnected ? '🟢 Connected' : '🔴 Disconnected'}
``` `connectionError` { message: string; timestamp: Date; recoverable: boolean } | undefined Connection-level error information (distinct from thread-specific errors). ```typescript {connectionError && ( )} ``` ## Actions [Section titled “Actions”](#actions) ### Message Sending [Section titled “Message Sending”](#message-sending) `sendMessage` (message: string, options?: { messageId?: string }) => Promise\ Send a message to the **current thread** with optimistic updates and error handling. ```typescript // Basic message sending await sendMessage("Hello!"); // With custom message ID await sendMessage("Hello!", { messageId: "custom-msg-123" }); // Automatic optimistic update → backend request → success/failure handling ``` `sendMessageToThread` (threadId: string, message: string, options?: { messageId?: string; state?: Record\ | (() => Record\) }) => Promise\ Send a message to a **specific thread** (can be different from current thread). Advanced use cases like conversation branching. ```typescript // Send to background thread await sendMessageToThread("thread-789", "Background message"); // Send with custom client state (conversation branching) await sendMessageToThread("thread-123", "Edited message", { state: () => ({ mode: "conversation_branching", editFromMessageId: "msg-456", branchHistory: previousMessages, }), }); ``` ### Agent Control [Section titled “Agent Control”](#agent-control) `cancel` () => Promise\ Cancel the current agent run if the transport supports cancellation. ```typescript const handleCancel = async () => { try { await cancel(); console.log("Agent run cancelled successfully"); } catch (error) { console.error("Failed to cancel:", error); } }; ``` `regenerate` () => void Regenerate the last response in the current thread by resending the most recent user message. ```typescript ``` ### Error Management [Section titled “Error Management”](#error-management) `clearError` () => void Clear error state for the active thread. `clearConnectionError` () => void Clear connection-level error state. ## Thread Management [Section titled “Thread Management”](#thread-management) ### Thread Navigation [Section titled “Thread Navigation”](#thread-navigation) `setCurrentThread` (threadId: string) => void Switch the active thread. Updates which thread’s state is exposed via top-level properties (`messages`, `status`, etc.). ```typescript const handleThreadSwitch = (threadId: string) => { setCurrentThread(threadId); // Now `messages` and `status` reflect the new thread }; ``` `getThread` (threadId: string) => ThreadState | undefined Get a specific thread’s complete state without switching to it. ```typescript const threadState = getThread("thread-789"); if (threadState) { console.log({ messageCount: threadState.messages.length, agentStatus: threadState.status, hasUnread: threadState.hasNewMessages, lastActivity: threadState.lastActivity, }); } ``` ### Thread Operations [Section titled “Thread Operations”](#thread-operations) `createThread` (threadId: string) => void Create a new empty thread in local state (does not persist to backend). ```typescript const newThreadId = `thread-${Date.now()}`; createThread(newThreadId); setCurrentThread(newThreadId); ``` `removeThread` (threadId: string) => void Remove a thread completely from local state. ```typescript removeThread("old-thread-123"); // Thread and all its messages removed from memory ``` ### Message Management [Section titled “Message Management”](#message-management) `clearMessages` () => void Clear all messages from the current thread’s local state. ```typescript const handleClearChat = () => { clearMessages(); // Current thread now has empty messages array }; ``` `clearThreadMessages` (threadId: string) => void Clear messages from a specific thread. ```typescript clearThreadMessages("thread-789"); // Specified thread now has empty messages array ``` `replaceMessages` (messages: ConversationMessage\[]) => void Replace all messages in the current thread (used for loading history). ```typescript // Load historical messages const historyMessages = await fetchHistoryFromAPI(currentThreadId); replaceMessages(historyMessages); ``` `replaceThreadMessages` (threadId: string, messages: ConversationMessage\[]) => void Replace messages in a specific thread. ```typescript // Load history for background thread const backgroundHistory = await fetchHistoryFromAPI("thread-789"); replaceThreadMessages("thread-789", backgroundHistory); ``` `markThreadViewed` (threadId: string) => void Mark a thread as viewed (clear `hasNewMessages` flag). ```typescript const handleThreadClick = (threadId: string) => { setCurrentThread(threadId); markThreadViewed(threadId); // Clear unread indicator }; ``` ## Multi-Thread Management [Section titled “Multi-Thread Management”](#multi-thread-management) ### Thread State Interface [Section titled “Thread State Interface”](#thread-state-interface) Each thread in the `threads` object has the following structure: ```typescript interface ThreadState { messages: ConversationMessage[]; // Thread's conversation status: AgentStatus; // Agent execution status currentAgent?: string; // Active agent name hasNewMessages: boolean; // Unread indicator lastActivity: Date; // Last update timestamp error?: { // Thread-specific error message: string; timestamp: Date; recoverable: boolean; }; } ``` ### Background Streaming [Section titled “Background Streaming”](#background-streaming) useAgent processes events for **all threads simultaneously**: ```typescript function MultiThreadChat() { const { threads, currentThreadId, setCurrentThread } = useAgent({ threadId: 'primary-thread', userId: 'user-123' }); // Monitor background thread activity const backgroundThreads = Object.entries(threads).filter( ([threadId, _]) => threadId !== currentThreadId ); const unreadCount = backgroundThreads.reduce( (count, [_, threadState]) => count + (threadState.hasNewMessages ? 1 : 0), 0 ); return (
Active Threads: {Object.keys(threads).length}
Unread: {unreadCount}
{backgroundThreads.map(([threadId, threadState]) => (
setCurrentThread(threadId)} class={threadState.hasNewMessages ? 'unread' : ''} > {threadId}: {threadState.messages.length} messages {threadState.hasNewMessages && ' 🔴'}
))}
); } ``` ### Advanced Thread Operations [Section titled “Advanced Thread Operations”](#advanced-thread-operations) ```typescript function AdvancedThreadManager() { const { threads, sendMessageToThread, replaceThreadMessages, clearThreadMessages, removeThread, } = useAgent({ threadId: "main-thread", userId: "user-123" }); // Send to specific thread without switching const sendToBackground = async (threadId: string, message: string) => { await sendMessageToThread(threadId, message); // Message sent, events processed in background }; // Batch thread operations const cleanupOldThreads = () => { Object.entries(threads).forEach(([threadId, threadState]) => { const daysSinceActivity = (Date.now() - threadState.lastActivity.getTime()) / (1000 * 60 * 60 * 24); if (daysSinceActivity > 30) { removeThread(threadId); // Clean up old threads } }); }; // Archive thread messages const archiveThread = async (threadId: string) => { const threadState = threads[threadId]; if (threadState) { // Save to archive API await saveToArchive(threadId, threadState.messages); // Clear from memory clearThreadMessages(threadId); } }; } ``` ## Event Processing [Section titled “Event Processing”](#event-processing) ### Raw Event Access [Section titled “Raw Event Access”](#raw-event-access) Unlike `useChat`, `useAgent` gives you access to the underlying streaming system: ```typescript function EventDebugger() { const agent = useAgent({ threadId: "debug-thread", debug: true, // Enable event logging onError: (error) => { console.error("Streaming error:", error); }, }); // Debug logging shows: // 🔄 [PROCESS-EVENT] seq:4 type:text.delta threadId:debug-thread // 🔍 [TEXT-DELTA] Applied delta "Hello" // 🔍 [SEQUENCE-DEBUG] Thread debug-thread after processing: 5 events } ``` ### Event Sequence Management [Section titled “Event Sequence Management”](#event-sequence-management) useAgent handles out-of-order events automatically: ```typescript // Events may arrive out of sequence due to network conditions // Incoming: seq 5, 3, 4, 6 // useAgent automatically: // 1. Buffers events 5, 6 (waiting for 3, 4) // 2. Processes 3, then 4 from buffer // 3. Processes 5, 6 in correct order // Result: Perfect chronological message updates ``` ## Advanced Patterns [Section titled “Advanced Patterns”](#advanced-patterns) ### Custom Event Processing [Section titled “Custom Event Processing”](#custom-event-processing) ```typescript function CustomEventHandler() { const agent = useAgent({ threadId: "custom-thread", userId: "user-123", }); // Access low-level state for custom processing useEffect(() => { const currentThread = agent.getThread(agent.currentThreadId); if (currentThread) { // Custom logic based on thread state if (currentThread.status === "error") { handleAgentError(currentThread.error); } if ( currentThread.hasNewMessages && currentThread.id !== agent.currentThreadId ) { showUnreadNotification(currentThread.id); } } }, [agent.threads, agent.currentThreadId]); } ``` ### Multi-Agent Orchestration [Section titled “Multi-Agent Orchestration”](#multi-agent-orchestration) ```typescript function MultiAgentInterface() { const customerSupport = useAgent({ threadId: 'support-thread', channelKey: 'customer-support', userId: 'user-123' }); const technicalSupport = useAgent({ threadId: 'technical-thread', channelKey: 'technical-support', // Different channel userId: 'user-123' }); // Handle escalation between agents const escalateToTechnical = async (message: string) => { // Send context from customer support to technical support const context = customerSupport.messages.map(m => m.parts.filter(p => p.type === 'text').map(p => p.content).join('') ).join('\n'); await technicalSupport.sendMessage( `Escalated from customer support:\n${context}\n\nUser question: ${message}` ); }; return (
); } ``` ### Client State Management [Section titled “Client State Management”](#client-state-management) Advanced client state capture for debugging and message editing: ```typescript function StatefulChat() { const [formData, setFormData] = useState({}); const [activeTab, setActiveTab] = useState("chat"); const agent = useAgent({ threadId: "stateful-thread", // Capture comprehensive client state state: () => ({ formData: formData, activeTab: activeTab, viewport: { width: window.innerWidth, height: window.innerHeight, }, userAgent: navigator.userAgent, timestamp: Date.now(), url: window.location.href, }), }); // Every message sent includes this context for debugging/regeneration } ``` ## Provider Integration [Section titled “Provider Integration”](#provider-integration) ### Automatic Inheritance [Section titled “Automatic Inheritance”](#automatic-inheritance) When used within AgentProvider, useAgent inherits configuration: ```typescript function ChatComponent() { // Inherits userId and debug from provider const agent = useAgent({ threadId: 'thread-456' // userId and debug inherited automatically }); } ``` ### Smart Connection Sharing [Section titled “Smart Connection Sharing”](#smart-connection-sharing) The provider enables intelligent connection sharing: ```typescript {/* Uses shared connection */} {/* Uses shared connection */} {/* Separate connection */} function ComponentA() { // Uses provider's shared connection for "shared-project" const agent = useAgent({ threadId: 'thread-a' }); } function ComponentC() { // Creates separate connection for "isolated" channel const agent = useAgent({ threadId: 'thread-c', channelKey: 'isolated' }); } ``` ## Performance Optimization [Section titled “Performance Optimization”](#performance-optimization) ### Connection Efficiency [Section titled “Connection Efficiency”](#connection-efficiency) ```typescript // ✅ Efficient: Use provider for shared connections {/* Shares connection */} {/* Shares connection */} {/* Shares connection */} // ❌ Inefficient: Multiple separate connections function App() { const agent1 = useAgent({ threadId: 'thread-1', userId: 'user-123' }); // Connection 1 const agent2 = useAgent({ threadId: 'thread-2', userId: 'user-123' }); // Connection 2 const agent3 = useAgent({ threadId: 'thread-3', userId: 'user-123' }); // Connection 3 // 3 separate WebSocket connections! } ``` ### Memory Management [Section titled “Memory Management”](#memory-management) ```typescript // ✅ Efficient: Reasonable state capture state: () => ({ currentForm: getCurrentFormData(), activeTab: getActiveTab(), }); // ❌ Memory leak: Capturing massive objects state: () => ({ entireAppState: store.getState(), // Potentially huge! allUserHistory: getUserHistory(), // Potentially huge! globalCache: getGlobalCache(), // Potentially huge! }); ``` ### Thread Cleanup [Section titled “Thread Cleanup”](#thread-cleanup) ```typescript function ChatWithCleanup() { const agent = useAgent({ threadId: "main-thread", userId: "user-123" }); // Clean up inactive threads periodically useEffect(() => { const cleanup = setInterval( () => { const now = Date.now(); Object.entries(agent.threads).forEach(([threadId, threadState]) => { const inactiveTime = now - threadState.lastActivity.getTime(); const thirtyMinutes = 30 * 60 * 1000; if ( inactiveTime > thirtyMinutes && threadId !== agent.currentThreadId ) { agent.removeThread(threadId); } }); }, 5 * 60 * 1000 ); // Check every 5 minutes return () => clearInterval(cleanup); }, [agent]); } ``` ## Error Handling [Section titled “Error Handling”](#error-handling) ### Error Types [Section titled “Error Types”](#error-types) useAgent provides detailed error information: ```typescript const { error, connectionError, onError } = useAgent({ threadId: "thread-123", onError: (error) => { // Handle errors from agent execution console.error("Agent error:", { message: error.message, stack: error.stack, timestamp: new Date().toISOString(), }); }, }); // Thread-specific error if (error) { console.log("Thread error:", { message: error.message, // "Failed to send message" recoverable: error.recoverable, // true/false timestamp: error.timestamp, // When error occurred }); } // Connection-level error if (connectionError) { console.log("Connection error:", { message: connectionError.message, // "WebSocket connection failed" recoverable: connectionError.recoverable, // true/false timestamp: connectionError.timestamp, // When error occurred }); } ``` ### Error Recovery [Section titled “Error Recovery”](#error-recovery) ```typescript function ChatWithRecovery() { const { error, connectionError, clearError, clearConnectionError, regenerate, } = useAgent({ threadId: "recovery-thread", userId: "user-123", }); // Auto-recovery for recoverable errors useEffect(() => { if (error?.recoverable) { const timer = setTimeout(() => { clearError(); regenerate(); // Retry last message }, 3000); return () => clearTimeout(timer); } }, [error]); // Connection recovery useEffect(() => { if (connectionError?.recoverable) { const timer = setTimeout(() => { clearConnectionError(); // Connection will automatically retry }, 5000); return () => clearTimeout(timer); } }, [connectionError]); } ``` ## Debug and Development [Section titled “Debug and Development”](#debug-and-development) ### Debug Output [Section titled “Debug Output”](#debug-output) Enable debug mode to see detailed event processing: ```typescript useAgent({ threadId: "debug-thread", debug: true, }); // Console output includes: // 🔄 [PROCESS-EVENT] seq:5 type:text.delta threadId:debug-thread // 🔍 [TEXT-DELTA] Applied delta seq:5 "Hello" | before:"" after:"Hello" // 🔍 [THREAD-SWITCH] debug-thread → new-thread (0 → 0 messages) // 🔍 [MESSAGE-SENT] Starting new conversation in thread new-thread ``` ### Event Sequence Debugging [Section titled “Event Sequence Debugging”](#event-sequence-debugging) ```typescript // Monitor event sequence integrity const agent = useAgent({ threadId: "sequence-debug", debug: true, }); // Debug logs show sequence management: // 🔍 [SEQUENCE-DEBUG] Thread filtering events: totalEvents=10, lastProcessed=5 // 🔍 [SEQUENCE-DEBUG] After filtering: unprocessedCount=5, filteredOut=5 // [Thread thread-123] Processing 5/10 new events: text.delta:6,part.created:7... ``` ### Memory Debugging [Section titled “Memory Debugging”](#memory-debugging) ```typescript // Track memory usage across threads function MemoryMonitor() { const agent = useAgent({ threadId: "monitor", userId: "user-123" }); useEffect(() => { const logMemoryStats = () => { console.log("Thread Memory Stats:", { totalThreads: Object.keys(agent.threads).length, totalMessages: Object.values(agent.threads).reduce( (sum, thread) => sum + thread.messages.length, 0 ), currentThread: agent.currentThreadId, threadsWithUnread: Object.values(agent.threads).filter( (t) => t.hasNewMessages ).length, }); }; const interval = setInterval(logMemoryStats, 30000); // Every 30s return () => clearInterval(interval); }, [agent]); } ``` ## Common Patterns [Section titled “Common Patterns”](#common-patterns) ### Custom Chat Implementation [Section titled “Custom Chat Implementation”](#custom-chat-implementation) ```typescript function CustomChat({ initialThreadId }) { const agent = useAgent({ threadId: initialThreadId || `thread-${Date.now()}`, userId: 'user-123', debug: true, state: () => ({ chatMode: 'custom', timestamp: Date.now() }) }); // Custom thread switching with animation const switchThread = useCallback(async (threadId: string) => { setIsTransitioning(true); agent.setCurrentThread(threadId); agent.markThreadViewed(threadId); // Custom history loading try { const history = await fetchCustomHistory(threadId); agent.replaceThreadMessages(threadId, history); } catch (error) { console.warn('Failed to load history:', error); } setIsTransitioning(false); }, [agent]); return (
); } ``` ### Embedded Chat Widget [Section titled “Embedded Chat Widget”](#embedded-chat-widget) ```typescript function EmbeddedChatWidget({ containerId, config }) { const agent = useAgent({ threadId: `embedded-${containerId}`, userId: config.userId, channelKey: config.channelKey, transport: createCustomTransport(config.apiEndpoints), debug: false // Production mode }); // Minimal UI suitable for embedding return (
AI Assistant
{agent.messages.map(msg => ( ))}
); } ``` ### Research/Experimental Interface [Section titled “Research/Experimental Interface”](#researchexperimental-interface) ```typescript function ResearchInterface() { const agent = useAgent({ threadId: "research-thread", userId: "researcher-123", debug: true, state: () => ({ experimentId: getCurrentExperiment(), participantId: getParticipantId(), condition: getExperimentalCondition(), sessionStartTime: getSessionStart(), interactionCount: getInteractionCount(), }), }); // Log all events for research useEffect(() => { // Custom event logging for research const logInteraction = (type: string, data: any) => { analytics.track("research_interaction", { type, threadId: agent.currentThreadId, messageCount: agent.messages.length, agentStatus: agent.status, timestamp: Date.now(), ...data, }); }; // Log state changes logInteraction("thread_switch", { newThreadId: agent.currentThreadId }); }, [agent.currentThreadId]); } ``` ## Migration from useChat [Section titled “Migration from useChat”](#migration-from-usechat) If you need to migrate from useChat to useAgent for more control: ```typescript // Before: useChat (automatic coordination) const { messages, sendMessage, threads, switchToThread } = useChat({ initialThreadId: threadId, }); // After: useAgent (manual coordination) const agent = useAgent({ threadId: threadId, userId: "user-123", }); // Manual thread management (what useChat did automatically) const threads = useThreads({ userId: "user-123" }); useEffect(() => { // Sync thread state manually if (threads.currentThreadId !== agent.currentThreadId) { agent.setCurrentThread(threads.currentThreadId); } }, [threads.currentThreadId, agent.currentThreadId]); const switchToThread = useCallback( async (threadId: string) => { // Manual history loading (what useChat did automatically) threads.setCurrentThreadId(threadId); agent.setCurrentThread(threadId); try { const history = await loadThreadHistory(threadId); agent.replaceThreadMessages(threadId, history); } catch (error) { console.warn("Failed to load thread history:", error); } }, [agent, threads] ); ``` ## Next Steps [Section titled “Next Steps”](#next-steps) [useChat Reference ](/reference/react-hooks/use-chat)Higher-level hook with automatic coordination (recommended for most apps) [useThreads Reference ](/reference/react-hooks/use-threads)Specialized hook for thread management and persistence [Message Types Reference ](/reference/react-hooks/message-types)Complete documentation for ConversationMessage and MessagePart types [Streaming Deep Dive ](/streaming/automatic-streaming)Learn how the underlying streaming system works The `useAgent` hook provides the foundation for all AgentKit React integrations. While `useChat` is recommended for most applications, `useAgent` gives you the granular control needed for advanced implementations, custom UI patterns, and specialized use cases. # useChat API Reference > Complete API documentation for the useChat hook - the recommended way to build chat interfaces The `useChat` hook is the **recommended entry point** for building AI chat applications with AgentKit. It provides a unified API that combines real-time streaming capabilities with thread management, making it perfect for building complete chat applications. Note Use `useChat` for 90% of AgentKit React applications. It handles the complex coordination between real-time events and persistent state automatically. ## Import [Section titled “Import”](#import) ```typescript import { useChat } from "@inngest/use-agent"; ``` ## Basic Usage [Section titled “Basic Usage”](#basic-usage) ```typescript function ChatComponent() { const { messages, sendMessage, status, threads, createNewThread } = useChat({ initialThreadId: 'thread-123', userId: 'user-456' }); return (
); } ``` ## Configuration [Section titled “Configuration”](#configuration) ### Interface: `UseChatConfig` [Section titled “Interface: UseChatConfig”](#interface-usechatconfig) `userId` string required User identifier for attribution and data ownership. **Required** unless provided by AgentProvider. ```typescript useChat({ userId: "user-123" }); ``` `channelKey` string Channel key for subscription targeting. Enables collaborative features when multiple users share the same key. ```typescript // Private chat (default) useChat({ userId: "user-123" }); // Collaborative chat useChat({ channelKey: "project-456", userId: "user-123" }); ``` `initialThreadId` string Thread ID to load on initialization. Perfect for URL-driven chat pages. ```typescript // URL: /chat/[threadId] useChat({ initialThreadId: params.threadId }); ``` `debug` boolean default: false Enable comprehensive debug logging for development. ```typescript useChat({ debug: process.env.NODE_ENV === "development" }); ``` `enableThreadValidation` boolean default: true Validate that initialThreadId exists in the database. Set to `false` for custom persistence layers. ```typescript // Disable for ephemeral/custom storage useChat({ initialThreadId: threadId, enableThreadValidation: false, }); ``` `onThreadNotFound` (threadId: string) => void Custom handler for missing threads. Default behavior redirects to homepage. ```typescript useChat({ initialThreadId: threadId, onThreadNotFound: (missingThreadId) => { showError(`Thread ${missingThreadId} not found`); router.push("/chat"); }, }); ``` ### Advanced Configuration [Section titled “Advanced Configuration”](#advanced-configuration) `state` () => Record\ Function to capture client-side state with each message. Essential for message editing and debugging. ```typescript useChat({ state: () => ({ currentPage: window.location.pathname, formData: getActiveFormData(), userPreferences: getUserSettings(), timestamp: Date.now(), }), }); ``` `onStateRehydrate` (messageState: Record\, messageId: string) => void Callback to restore UI state when editing messages from previous contexts. ```typescript useChat({ onStateRehydrate: (messageState, messageId) => { // Restore form state if (messageState.formData) { restoreFormData(messageState.formData); } // Navigate to original page if (messageState.currentPage) { router.push(messageState.currentPage); } }, }); ``` ### Custom Functions [Section titled “Custom Functions”](#custom-functions) Override default API behavior for custom backends: `fetchThreads` (userId: string, pagination: { limit: number; offset: number }) => Promise<{threads: Thread\[]; hasMore: boolean; total: number}> Custom function for fetching threads list. `fetchHistory` (threadId: string) => Promise\ Custom function for loading thread message history. `createThread` (userId: string) => Promise<{threadId: string; title: string}> Custom function for creating new threads. `deleteThread` (threadId: string) => Promise\ Custom function for deleting threads. `renameThread` (threadId: string, title: string) => Promise\ Custom function for renaming threads. ## Return Value: `UseChatReturn` [Section titled “Return Value: UseChatReturn”](#return-value-usechatreturn) ### Real-time Agent State [Section titled “Real-time Agent State”](#real-time-agent-state) `messages` ConversationMessage\[] Current thread’s messages with real-time streaming updates. Each message contains `parts` that stream incrementally. ```typescript messages.forEach((msg) => { console.log(`${msg.role}: ${msg.parts.length} parts`); msg.parts.forEach((part) => { if (part.type === "text") { console.log(`Text: ${part.content}`); } }); }); ``` `status` AgentStatus Current agent execution status: `"idle"`, `"thinking"`, `"calling-tool"`, `"responding"`, or `"error"`. ```typescript // Show appropriate UI based on status {status === 'thinking' && } {status === 'responding' && } {status === 'error' && } ``` `isConnected` boolean WebSocket connection status to AgentKit networks. ```typescript // Show connection status
Status: {isConnected ? '🟢 Connected' : '🔴 Disconnected'}
``` `currentAgent` string | undefined Name of the currently active agent (if available). ```typescript
Agent: {currentAgent || 'Assistant'}
``` `error` { message: string; timestamp: Date; recoverable: boolean } | undefined Current error state with recovery information. ```typescript {error && ( )} ``` ### Thread Management State [Section titled “Thread Management State”](#thread-management-state) `threads` Thread\[] Array of all conversation threads with metadata and unread indicators. ```typescript threads.forEach((thread) => { console.log({ id: thread.id, title: thread.title, messageCount: thread.messageCount, hasNewMessages: thread.hasNewMessages, lastActivity: thread.lastMessageAt, }); }); ``` `threadsLoading` boolean Loading state for the threads list. `threadsHasMore` boolean Whether more threads are available for pagination. `threadsError` string | null Error state for thread operations. `currentThreadId` string | null ID of the currently active thread. ### Loading States [Section titled “Loading States”](#loading-states) `isLoadingInitialThread` boolean `true` when loading history for a URL-provided `initialThreadId`. ```typescript // Show loading state while fetching initial thread {isLoadingInitialThread ? ( ) : ( )} ``` ## Actions [Section titled “Actions”](#actions) ### Message Sending [Section titled “Message Sending”](#message-sending) `sendMessage` (message: string, options?: { messageId?: string }) => Promise\ Send a message to the current thread with automatic coordination. ```typescript // Basic usage await sendMessage("Hello!"); // With custom message ID await sendMessage("Hello!", { messageId: "custom-id" }); // Handles thread creation, optimistic updates, and streaming automatically ``` `sendMessageToThread` (threadId: string, message: string, options?: { messageId?: string; state?: Record\ | (() => Record\) }) => Promise\ Send a message to a specific thread (advanced use cases like conversation branching). ```typescript // Send to specific thread await sendMessageToThread("thread-789", "Hello from another thread!"); // With custom client state await sendMessageToThread("thread-789", "Edit message", { state: () => ({ mode: "conversation_branching", editFromMessageId: "msg-456", branchHistory: formatBranchHistory(messages.slice(0, editIndex)), }), }); ``` ### Agent Control [Section titled “Agent Control”](#agent-control) `cancel` () => Promise\ Cancel the current agent run. ```typescript ``` `approveToolCall` (toolCallId: string, reason?: string) => Promise\ Approve a tool call in Human-in-the-Loop workflows. ```typescript // In your tool call rendering {part.type === 'tool-call' && part.state === 'awaiting-approval' && (
)} ``` `denyToolCall` (toolCallId: string, reason?: string) => Promise\ Deny a tool call in Human-in-the-Loop workflows. ```typescript ``` ### Thread Navigation [Section titled “Thread Navigation”](#thread-navigation) `switchToThread` (threadId: string) => Promise\ Switch to a thread with automatic history loading and state reconciliation. ```typescript // High-level navigation with history loading const handleThreadClick = async (threadId: string) => { await switchToThread(threadId); // History automatically loaded and merged with any optimistic messages }; ``` `setCurrentThreadId` (threadId: string) => void Immediate thread switch without history loading (escape hatch for ephemeral scenarios). ```typescript // Low-level escape hatch for immediate switching setCurrentThreadId(threadId); // No history loading, immediate switch ``` ### Advanced Thread Operations [Section titled “Advanced Thread Operations”](#advanced-thread-operations) `loadThreadHistory` (threadId: string) => Promise\ Load thread history without switching to it. ```typescript const messages = await loadThreadHistory("thread-789"); console.log(`Thread has ${messages.length} historical messages`); ``` `clearThreadMessages` (threadId: string) => void Clear all messages from a specific thread. ```typescript clearThreadMessages(currentThreadId); // Clear current thread ``` `replaceThreadMessages` (threadId: string, messages: ConversationMessage\[]) => void Replace all messages in a specific thread (used for conversation branching). ```typescript // Restore conversation to a previous state const messagesBeforeEdit = conversation.slice(0, editIndex); replaceThreadMessages(threadId, messagesBeforeEdit); ``` ### Thread CRUD Operations [Section titled “Thread CRUD Operations”](#thread-crud-operations) `createNewThread` () => string Create a new thread and return its ID. Perfect for “New Chat” buttons. ```typescript const handleNewChat = () => { const newThreadId = createNewThread(); router.push(`/chat/${newThreadId}`); }; ``` `deleteThread` (threadId: string) => Promise\ Delete a thread and all its messages permanently. ```typescript const handleDeleteThread = async (threadId: string) => { if (confirm("Delete this conversation?")) { await deleteThread(threadId); // Thread removed from sidebar automatically } }; ``` `loadMoreThreads` () => Promise\ Load the next page of threads for pagination. ```typescript // Infinite scroll implementation const handleScroll = (e) => { const { scrollTop, scrollHeight, clientHeight } = e.target; const isNearBottom = scrollHeight - scrollTop <= clientHeight + 100; if (isNearBottom && threadsHasMore && !threadsLoading) { loadMoreThreads(); } }; ``` `refreshThreads` () => Promise\ Refresh the threads list from the server. ```typescript ``` ### State Management [Section titled “State Management”](#state-management) `rehydrateMessageState` (messageId: string) => void Restore client state for editing messages from previous contexts. ```typescript const handleEditMessage = (messageId: string) => { // Restore UI state from when this message was originally sent rehydrateMessageState(messageId); // Now start editing with proper context restored startEditing(messageId); }; ``` `clearError` () => void Clear the current error state. ```typescript {error && ( )} ``` ## Usage Examples [Section titled “Usage Examples”](#usage-examples) ### URL-driven Chat Pages [Section titled “URL-driven Chat Pages”](#url-driven-chat-pages) Perfect for `/chat/[threadId]` routes: app/chat/\[threadId]/page.tsx ```typescript import { useChat } from "@inngest/use-agent"; export default function ChatPage({ params }) { const { messages, sendMessage, status, threads, switchToThread } = useChat({ initialThreadId: params.threadId, // Auto-loads this thread state: () => ({ currentPage: `/chat/${params.threadId}`, timestamp: Date.now() }) }); return (
); } ``` ### Homepage with New Conversations [Section titled “Homepage with New Conversations”](#homepage-with-new-conversations) Handle new conversations from homepage: app/page.tsx ```typescript export default function HomePage() { const { messages, sendMessage, createNewThread, currentThreadId } = useChat(); // No initialThreadId = fresh conversation const handleSendMessage = async (text: string) => { if (messages.length === 0) { // First message - create thread and navigate const newThreadId = createNewThread(); await sendMessage(text); router.push(`/chat/${newThreadId}`); } else { await sendMessage(text); } }; return messages.length === 0 ? ( ) : ( ); } ``` ### Client State Capture & Rehydration [Section titled “Client State Capture & Rehydration”](#client-state-capture--rehydration) Capture UI context for advanced debugging and message editing: ```typescript function SqlPlaygroundChat() { const [currentSql, setCurrentSql] = useState("SELECT * FROM users;"); const [activeTab, setActiveTab] = useState("query"); const { messages, sendMessage, rehydrateMessageState } = useChat({ // 📸 CAPTURE: Record UI context when messages are sent state: () => ({ sqlQuery: currentSql, activeTab: activeTab, editorConfig: getEditorSettings(), timestamp: Date.now(), }), // 🔄 REHYDRATE: Restore UI context when editing old messages onStateRehydrate: (messageState, messageId) => { // Restore SQL query if (messageState.sqlQuery) { setCurrentSql(messageState.sqlQuery); } // Restore active tab if (messageState.activeTab) { setActiveTab(messageState.activeTab); } console.log(`Restored context for message ${messageId}:`, messageState); }, }); const handleEditMessage = (messageId: string) => { // Restore UI to match when this message was originally sent rehydrateMessageState(messageId); // Now start editing with proper context startMessageEdit(messageId); }; } ``` ### Custom Backend Integration [Section titled “Custom Backend Integration”](#custom-backend-integration) Use custom API functions for non-standard backends: ```typescript const customFetchThreads = async (userId, { limit, offset }) => { const response = await fetch( `/api/v2/conversations?user=${userId}&limit=${limit}&offset=${offset}` ); const data = await response.json(); return { threads: data.conversations.map((conv) => ({ id: conv.id, title: conv.name, messageCount: conv.messageCount, lastMessageAt: new Date(conv.updatedAt), createdAt: new Date(conv.createdAt), updatedAt: new Date(conv.updatedAt), })), hasMore: data.hasNextPage, total: data.totalCount, }; }; const chat = useChat({ fetchThreads: customFetchThreads, createThread: customCreateThread, deleteThread: customDeleteThread, }); ``` ### Multi-Chat Application [Section titled “Multi-Chat Application”](#multi-chat-application) Handle multiple concurrent conversations: ```typescript function MultiChatApp() { const [tabs, setTabs] = useState([ { id: 'tab-1', threadId: 'thread-1', title: 'Chat 1' }, { id: 'tab-2', threadId: 'thread-2', title: 'Chat 2' } ]); const [activeTabId, setActiveTabId] = useState('tab-1'); const activeTab = tabs.find(t => t.id === activeTabId); const chat = useChat({ initialThreadId: activeTab?.threadId, enableThreadValidation: false, // Tabs might not exist in DB yet state: () => ({ activeTabId, tabConfiguration: tabs, multiChatMode: true }) }); // Switch threads when tabs change useEffect(() => { if (activeTab?.threadId && activeTab.threadId !== chat.currentThreadId) { chat.setCurrentThreadId(activeTab.threadId); } }, [activeTab?.threadId, chat.currentThreadId]); return (
); } ``` ## Error Handling [Section titled “Error Handling”](#error-handling) ### Error Types [Section titled “Error Types”](#error-types) ```typescript const { error, threadsError, clearError } = useChat(); // Thread-specific error (agent execution, message sending) if (error) { console.log("Agent error:", { message: error.message, recoverable: error.recoverable, timestamp: error.timestamp, }); } // Threads operation error (loading, creating, deleting threads) if (threadsError) { console.log("Threads error:", threadsError); } ``` ### Error Recovery [Section titled “Error Recovery”](#error-recovery) ```typescript function ChatWithErrorHandling() { const { error, clearError, sendMessage } = useChat(); const handleRetry = () => { clearError(); // Optionally resend the last message sendMessage(lastMessage); }; return (
{error && ( )}
); } ``` ## Provider Integration [Section titled “Provider Integration”](#provider-integration) ### Inheriting Configuration [Section titled “Inheriting Configuration”](#inheriting-configuration) When used within AgentProvider, useChat inherits configuration automatically: ```typescript function ChatComponent() { // Automatically inherits userId="user-123" and debug=true const chat = useChat({ initialThreadId: 'thread-456' // No need to specify userId or debug - inherited from provider }); } ``` ### Overriding Provider Configuration [Section titled “Overriding Provider Configuration”](#overriding-provider-configuration) Hook-level options take precedence over provider values: ```typescript function ChatComponent() { // Override userId while inheriting other provider config const chat = useChat({ userId: "different-user", // Override initialThreadId: 'thread-456' // Other options inherited from provider }); } ``` ## Performance Considerations [Section titled “Performance Considerations”](#performance-considerations) ### Optimistic Updates [Section titled “Optimistic Updates”](#optimistic-updates) useChat automatically handles optimistic UI updates: ```typescript const handleSendMessage = async (message: string) => { // Message appears in UI immediately (optimistic) await sendMessage(message); // If successful, message marked as 'sent' // If failed, message marked as 'failed' with retry option }; ``` ### History Reconciliation [Section titled “History Reconciliation”](#history-reconciliation) Smart merging prevents duplicate messages when loading thread history: ```typescript // When switching threads, useChat automatically: // 1. Loads historical messages from database // 2. Identifies optimistic messages not yet in database // 3. Merges them intelligently to prevent duplicates // 4. Updates UI with combined timeline // This prevents flashing UIs and lost messages during navigation ``` ### Background Thread Updates [Section titled “Background Thread Updates”](#background-thread-updates) Receive updates for inactive threads without UI disruption: ```typescript // Thread A is active, Thread B gets new message // useChat automatically: // 1. Processes Thread B events in background // 2. Updates Thread B's hasNewMessages flag // 3. Shows unread indicator in sidebar // 4. Doesn't disrupt Thread A's current conversation const unreadThreads = threads.filter((t) => t.hasNewMessages); console.log(`${unreadThreads.length} threads have new messages`); ``` ## Integration with Other Hooks [Section titled “Integration with Other Hooks”](#integration-with-other-hooks) ### With Message Actions [Section titled “With Message Actions”](#with-message-actions) ```typescript import { useChat, useMessageActions } from "@inngest/use-agent"; function ChatWithActions() { const { messages, sendMessage } = useChat(); const { copyMessage, likeMessage, shareMessage } = useMessageActions({ showToast: (message, type) => toast[type](message) }); return (
{messages.map(msg => (
copyMessage(msg)} onLike={() => likeMessage(msg.id)} onShare={() => shareMessage(msg)} />
))}
); } ``` ### With Conversation Branching [Section titled “With Conversation Branching”](#with-conversation-branching) ```typescript import { useChat, useConversationBranching } from "@inngest/use-agent"; function ChatWithBranching() { const { messages, sendMessage: originalSendMessage, sendMessageToThread, replaceThreadMessages, currentThreadId, } = useChat(); const branching = useConversationBranching({ userId: "user-123", storageType: "session", }); // Wrap sendMessage to support branching const sendMessage = useCallback( async ( message: string, options?: { editFromMessageId?: string; } ) => { await branching.sendMessage( originalSendMessage, sendMessageToThread, replaceThreadMessages, currentThreadId!, message, messages, options ); }, [ /* dependencies */ ] ); const handleEditMessage = (messageId: string) => { const newContent = prompt("Enter new message:"); if (newContent) { sendMessage(newContent, { editFromMessageId: messageId }); } }; } ``` ## Common Patterns [Section titled “Common Patterns”](#common-patterns) ### Responsive Chat Interface [Section titled “Responsive Chat Interface”](#responsive-chat-interface) ```typescript function ResponsiveChat({ threadId }) { const { messages, sendMessage, status, threads, switchToThread, createNewThread, deleteThread } = useChat({ initialThreadId: threadId, state: () => ({ viewport: getViewportSize(), deviceType: getDeviceType(), timestamp: Date.now() }) }); return (
); } ``` ### Demo/Prototype Mode [Section titled “Demo/Prototype Mode”](#demoprototype-mode) ```typescript import { useChat, useEphemeralThreads } from "@inngest/use-agent"; function DemoChat() { const ephemeralThreads = useEphemeralThreads({ userId: "demo-user", storageType: "session" // Clears when tab closes }); const chat = useChat({ userId: "demo-user", enableThreadValidation: false, // No backend validation ...ephemeralThreads // Provide ephemeral implementations }); return ; } ``` ## Next Steps [Section titled “Next Steps”](#next-steps) [useAgent Reference ](/reference/react-hooks/use-agent)Advanced hook for custom streaming implementations [AgentProvider Reference ](/reference/react-hooks/agent-provider)Provider configuration and optimization guide [Message Types Reference ](/reference/react-hooks/message-types)ConversationMessage, MessagePart, and all content types [Streaming Guide ](/streaming/react-hooks-quickstart)Step-by-step guide to building your first chat app The `useChat` hook is designed to handle the 90% use case of building AI chat applications while providing escape hatches for advanced customization. It combines the power of real-time streaming with the convenience of automatic thread management, making it the perfect foundation for sophisticated AI chat interfaces. # createState > Leverage a Network's State across Routers and Agents. The `State` class provides a way to manage state and history across a network of agents. It includes key-value storage and maintains a stack of all agent interactions. The `State` is accessible to all Agents, Tools and Routers as a `state` or `network.state` property. ## Creating State [Section titled “Creating State”](#creating-state) ```ts import { createState } from '@inngest/agent-kit'; export interface NetworkState { // username is undefined until extracted and set by a tool username?: string; } const state = createState({ username: 'bar', }); console.log(state.data.username); // 'bar' const network = createNetwork({ // ... }); // Pass in state to each run network.run("", { state }) ``` ## Reading and Modifying State’s data (`state.data`) [Section titled “Reading and Modifying State’s data (state.data)”](#reading-and-modifying-states-data-statedata) The `State` class provides typed data accesible via the `data` property. Note Learn more about the State use cases in the [State](/docs/concepts/state) concept guide. `data` object\ A standard, mutable object which can be updated and modified within tools. ## State History [Section titled “State History”](#state-history) The State history is passed as a `history` to the lifecycle hooks and via the `network` argument to the Tools handlers to the Router function. The State history can be retrieved *- as a copy -* using the `state.results` property composed of `InferenceResult` objects: ## InferenceResult [Section titled “InferenceResult”](#inferenceresult) The `InferenceResult` class represents a single agent call as part of the network state. It stores all inputs and outputs for a call. `agent` Agent The agent responsible for this inference call. `input` string The input passed into the agent’s run method. `prompt` Message\[] The input instructions without additional history, including the system prompt, user input, and initial agent assistant message. `history` Message\[] The history sent to the inference call, appended to the prompt to form a complete conversation log. `output` Message\[] The parsed output from the inference call. `toolCalls` ToolResultMessage\[] Output from any tools called by the agent. `raw` string The raw API response from the call in JSON format. ## `Message` Types [Section titled “Message Types”](#message-types) The state system uses several message types to represent different kinds of interactions: ```ts type Message = TextMessage | ToolCallMessage | ToolResultMessage; interface TextMessage { type: "text"; role: "system" | "user" | "assistant"; content: string | Array; stop_reason?: "tool" | "stop"; } interface ToolCallMessage { type: "tool_call"; role: "user" | "assistant"; tools: ToolMessage[]; stop_reason: "tool"; } interface ToolResultMessage { type: "tool_result"; role: "tool_result"; tool: ToolMessage; content: unknown; stop_reason: "tool"; } ``` # useAgent > React hook for building real-time, multi-threaded AI applications The `useAgent` hook is the core of the `@inngest/use-agent` package. It’s a comprehensive, client-side hook for React that manages real-time, multi-threaded conversations with an AgentKit network. It encapsulates the entire lifecycle of agent interactions, including sending messages, receiving streaming events, handling out-of-order event sequences, and managing connection state. ```tsx import { useAgent, AgentProvider } from '@inngest/use-agent'; function App() { return ( ); } function ChatComponent() { const { messages, sendMessage, status, currentThreadId, switchToThread } = useAgent(); // UI to switch threads and display messages... return ; } ``` ## Configuration [Section titled “Configuration”](#configuration) The `useAgent` hook accepts a configuration object with the following properties. ### Identity & Connection [Section titled “Identity & Connection”](#identity--connection) `userId` string A unique identifier for the current user. This is used for personalizing agent interactions and routing real-time events. If not provided, it will be inherited from the `AgentProvider`. `channelKey` string A key for targeting subscriptions, enabling collaborative sessions. If not provided, it defaults to the `userId`. `transport` IClientTransport An optional transport instance to override the default HTTP transport provided by `AgentProvider`. This allows you to customize how the hook communicates with your backend. ### Initial State [Section titled “Initial State”](#initial-state) `initialThreadId` string The ID of the conversation thread to load when the hook is first mounted. `state` () => TState A function that returns the current client-side UI state. This state is captured and sent with each user message, allowing agents to have context about what the user is seeing. It can also be used to restore the UI when revisiting a message. ### Data Fetching & Caching [Section titled “Data Fetching & Caching”](#data-fetching--caching) `fetchThreads` function A function to fetch a paginated list of conversation threads for the user. If not provided, the hook uses the default transport method. `fetchHistory` function A function to fetch the message history for a specific thread. If not provided, the hook uses the default transport method. `threadsPageSize` number default: 20 The number of threads to fetch per page in pagination requests. ### Callbacks [Section titled “Callbacks”](#callbacks) `onEvent` (event, meta) => void A low-level callback invoked for every real-time event processed by the hook. This is useful for building custom UI that reacts to specific agent activities, like showing a “thinking” indicator when a `run.started` event is received. `onStreamEnded` (args) => void A callback fired when a terminal stream event (`stream.ended` or `run.completed`) is received for a thread, indicating the agent has finished its turn. `onToolResult` (result) => void A strongly-typed callback that fires when a tool call completes and returns its final output. This is useful for observing or reacting to the data returned by agents’ tools. `onStateRehydrate` (state, messageId) => void A callback invoked when `rehydrateMessageState` is called. It receives the client state that was captured when the original message was sent, allowing you to restore the UI to its previous state. `onThreadNotFound` (threadId) => void A callback that is triggered if `switchToThread` is called with a `threadId` that cannot be found. ### Behavior [Section titled “Behavior”](#behavior) `debug` boolean default: false Enables detailed logging to the console for debugging the hook’s internal state and event flow. `requireProvider` boolean default: false If `true`, the hook will throw an error if it’s not used within an `AgentProvider`. When `false`, it creates a local fallback instance. `enableThreadValidation` boolean default: true If `true`, the hook will automatically re-fetch a thread’s history if it detects a mismatch between the local message count and the server’s message count, ensuring data consistency. ## Return Values [Section titled “Return Values”](#return-values) The `useAgent` hook returns an object containing state and actions to manage conversations. ### Core Agent State [Section titled “Core Agent State”](#core-agent-state) `messages` ConversationMessage\[] An array of messages for the currently active thread. Each message contains structured parts that are updated in real-time as events are received. `status` AgentStatus The current activity status of the agent for the active thread. Possible values are: `"ready"`, `"submitted"`, `"streaming"`, or `"error"`. `error` AgentError An object containing details about the last error that occurred. It’s `undefined` if there is no error. `clearError` () => void A function to clear the current error state. `isConnected` boolean Returns `true` if the client is currently connected to the real-time event stream. ### Core Actions [Section titled “Core Actions”](#core-actions) `sendMessage` (message, options) => Promise\ Sends a message to the currently active thread. `cancel` () => Promise\ Sends a request to the backend to cancel the current agent run for the active thread. `approveToolCall` (toolCallId, reason) => Promise\ Approves a tool call that is awaiting human-in-the-loop (HITL) confirmation. `denyToolCall` (toolCallId, reason) => Promise\ Denies a tool call that is awaiting human-in-the-loop (HITL) confirmation. ### Thread Management State [Section titled “Thread Management State”](#thread-management-state) `threads` Thread\[] An array of all conversation threads loaded for the user. `currentThreadId` string | null The ID of the currently active thread. `threadsLoading` boolean `true` while the initial list of threads is being fetched. `threadsHasMore` boolean `true` if there are more pages of threads to be loaded. `threadsError` string | null Contains an error message if fetching threads failed. `isLoadingInitialThread` boolean `true` only while the selected thread’s history has not yet been loaded. ### Thread Management Actions [Section titled “Thread Management Actions”](#thread-management-actions) `switchToThread` (threadId) => Promise\ Switches the active conversation to a different thread, loading its history. `setCurrentThreadId` (threadId) => void Immediately changes the `currentThreadId` without fetching history. Useful for optimistic UI updates before `switchToThread` completes. `createNewThread` () => string Creates a new, empty thread locally and returns its generated UUID. `deleteThread` (threadId) => Promise\ Deletes a thread from the backend and removes it from the local state. `loadMoreThreads` () => Promise\ Fetches the next page of threads. `refreshThreads` () => Promise\ Refetches the first page of threads to get the latest list. ### Advanced Actions [Section titled “Advanced Actions”](#advanced-actions) `sendMessageToThread` (threadId, message, options) => Promise\ Sends a message to a specific thread, which may not be the currently active one. `loadThreadHistory` (threadId) => Promise\ Manually fetches the message history for a specific thread. `clearThreadMessages` (threadId) => void Clears all messages from a specific thread’s local state. `replaceThreadMessages` (threadId, messages) => void Replaces all messages in a specific thread’s local state. Useful for manually populating history. `rehydrateMessageState` (messageId) => void Triggers the `onStateRehydrate` callback with the client state associated with a specific message. Useful for UI features like “edit message” where you need to restore the UI to how it was when the message was sent. # Events The `use-agent` hook is built on an event-driven architecture. Real-time events are streamed from the server, processed by a state reducer, and used to build the conversation UI incrementally. This document details the events, their purpose, and how you can use them. ## Understanding the Event Lifecycle [Section titled “Understanding the Event Lifecycle”](#understanding-the-event-lifecycle) 1. **Raw Event Streaming** A raw message is received from the Inngest realtime websocket connection 2. **Mapping** The raw message is passed to an internal `mapToNetworkEvent` function, which transforms it into a standardized, strongly-typed `AgentKitEvent`. Invalid or unrecognized messages are discarded. 3. **Dispatch** The valid `AgentKitEvent` is dispatched to the internal `StreamingEngine`. 4. **Reduction** The `streaming-reducer` processes the event. It uses a sequencing and buffering mechanism to ensure events are applied in the correct order, even if they arrive out of order. 5. **State Update** The reducer applies the event to the state, creating or updating messages and their parts. 6. **UI Render** Your React UI re-renders with the new state. 7. **Callbacks** If configured, `onEvent`, `onStreamEnded`, or `onToolResult` callbacks are fired, allowing you to react to specific events. ## Core Events Reference [Section titled “Core Events Reference”](#core-events-reference) ### Run Lifecycle Events [Section titled “Run Lifecycle Events”](#run-lifecycle-events) These events manage the overall state of an agent or network execution for a given turn. #### `run.started` [Section titled “run.started”](#runstarted) * **Description**: Marks the beginning of an agent or network execution in response to a user message. This is often the first event in a sequence. It sets the agent’s status to `submitted`. * **Payload (`data`)**: * `threadId`: The ID of the thread this run belongs to. * `name`: The name of the agent or network that started. * `scope`: `"network"` or `"agent"`. * `runId`, `parentRunId`, `messageId`. * **State Impact**: Sets `runActive: true`, `agentStatus: 'submitted'`, and may reset the event processing sequence if it’s the start of a new “epoch”. #### `run.completed` [Section titled “run.completed”](#runcompleted) * **Description**: Indicates that an agent or network run has finished its logic. This does *not* mean all streaming is complete. It primarily finalizes any in-flight tool outputs. * **Payload (`data`)**: `threadId`, `scope`, `runId`, `messageId`, `name`. * **State Impact**: Finalizes the state of any tools that were in the `executing` state, moving them to `output-available`. The overall agent status is not yet changed to `ready`. #### `stream.ended` [Section titled “stream.ended”](#streamended) * **Description**: The final event in a sequence. It indicates that all streaming for a turn is complete and the agent is now idle. * **Payload (`data`)**: `threadId`, `scope`, `runId`, `messageId`, `name`. * **State Impact**: Sets `runActive: false` and `agentStatus: 'ready'`. This signals that the system is ready for new user input. ### Content Streaming Events [Section titled “Content Streaming Events”](#content-streaming-events) These events are responsible for building the assistant’s response message part by part. #### `part.created` [Section titled “part.created”](#partcreated) * **Description**: Signals the creation of a new part within an assistant message. * **Payload (`data`)**: * `messageId`: The ID of the message this part belongs to. * `partId`: The unique ID for this new part. * `type`: Either `"text"` or `"tool-call"`. * `metadata`: Optional data, often includes `toolName` for tool calls. * **State Impact**: Adds a new, empty `TextUIPart` or `ToolCallUIPart` to the parts array of the corresponding message. #### `text.delta` [Section titled “text.delta”](#textdelta) * **Description**: Streams a chunk of text for a `TextUIPart`. * **Payload (`data`)**: * `messageId`, `partId`. * `delta`: The string of text to append. * **State Impact**: Appends the `delta` content to the specified `TextUIPart`. #### `part.completed` [Section titled “part.completed”](#partcompleted) * **Description**: Marks a specific message part as complete. The payload varies based on the part type. * **Payload (`data`)**: * `messageId`, `partId`. * `type`: Can be `"text"`, `"tool-call"`, or `"tool-output"`. * `finalContent`: The complete, final content for the part (e.g., the full text or final tool output). * **State Impact**: * For `"text"`: Sets the part’s `status` to `complete`. * For `"tool-call"`: Sets the tool’s `state` to `input-available` and populates the final `input`. * For `"tool-output"`: Sets the tool’s `state` to `output-available` and populates the final `output`. ### Tool Call Events [Section titled “Tool Call Events”](#tool-call-events) These events are specific to the lifecycle of tool calls made by an agent. #### `tool_call.arguments.delta` [Section titled “tool\_call.arguments.delta”](#tool_callargumentsdelta) * **Description**: Streams a chunk of the JSON arguments for a tool call. * **Payload (`data`)**: * `messageId`, `partId`. * `delta`: A string chunk of the JSON arguments object. * **State Impact**: Appends the `delta` to the `input` field of the `ToolCallUIPart`. The reducer attempts to parse the accumulating string as JSON. Sets the tool’s `state` to `input-streaming`. #### `tool_call.output.delta` [Section titled “tool\_call.output.delta”](#tool_calloutputdelta) * **Description**: Streams a chunk of the output from a tool execution. * **Payload (`data`)**: * `messageId`, `partId`. * `delta`: A string chunk of the tool’s output. * **State Impact**: Appends the `delta` to the `output` field of the `ToolCallUIPart`. Sets the tool’s `state` to `executing`. ## Consuming Events in Your App [Section titled “Consuming Events in Your App”](#consuming-events-in-your-app) The `useAgent` hook provides callbacks to tap into this event stream. ### `onEvent` [Section titled “onEvent”](#onevent) This is the lowest-level callback. It fires for every single valid `AgentKitEvent` that the hook processes, giving you full visibility into the streaming process. ```jsx useAgent({ onEvent: (evt, meta) => { console.log('Event received:', evt.event, 'for thread:', meta.threadId); if (evt.event === 'run.started') { // Show a global "thinking" indicator } } }) ``` ### `onToolResult` [Section titled “onToolResult”](#ontoolresult) A convenient, strongly-typed callback that fires only when a tool has finished executing and its final output is available (`part.completed` with `type: "tool-output"`). ```jsx useAgent({ onToolResult: (result) => { if (result.toolName === 'getWeather') { const weatherData = result.data; // strongly typed! // Display a custom weather component } } }) ``` ### `onStreamEnded` [Section titled “onStreamEnded”](#onstreamended) Fires when the entire sequence of events for a turn is complete (`stream.ended`). Useful for triggering actions after the assistant has fully responded. ```jsx useAgent({ onStreamEnded: ({ threadId }) => { console.log(`Agent has finished responding in thread ${threadId}.`); // Maybe run some analytics or enable the input field } }) ``` # Overview > Realtime event streaming with AgentKit + useAgent With a useAgent hook you can seamlessly stream a network of agents, a single agent and durable steps within tools used by your agents. You can think of useAgent as the bridge between durable agents and your user interface. Instead of stitching together events, workflow steps and token streams - your UI receives structured events that describe lifecycles, content parts, tool calls and completions. This hook consumes these events and maintains your UI state for a single conversation or many conversations in parallel. Here’s a simple example of how you would use the hook in your React component: ```tsx import { useAgent } from "@inngest/use-agent"; export function MyAgentUI() { const { messages, sendMessage, status } = useAgent(); const onSubmit = (e) => { e.preventDefault(); const value = new FormData(e.currentTarget).get("input"); sendMessage(value); }; return (
    {messages.map(({ id, role, parts }) => (
  • {role}
    {parts.map(({ id, type, content }) => type === "text" ?
    {content}
    : null )}
  • ))}
); } ``` Let’s take a closer look at what components, endpoints and other files we will need to wire this all up: * **Inngest Client (`/api/inngest/client.ts`)**: Initializes Inngest with the `realtimeMiddleware`. * **Realtime Channel (`/api/inngest/realtime.ts`)**: Defines a typed realtime channel and topic. * **Chat Route: `/api/chat/route.ts`**: This is a standard Next.js API route. Its only job is to receive a request from the frontend and send an event to Inngest to trigger a function. * **Token Route: `/api/realtime/token/route.ts`**: This secure endpoint generates a subscription token that the frontend needs to connect to Inngest realtime. * **Inngest Route: `/api/inngest/route.ts`**: The standard handler that serves all your Inngest functions. Once you’ve configured all the foundational endpoints needed for streaming, you’ll want to create some agents, define types and integrate this into a UI: 1. **Define server-side types** Define your server-side state type, import all your tools and pass them into `createToolManifest` to generate a type that you will use in your UI. ```typescript import { createToolManifest, type StateData } from "@inngest/agent-kit"; import { selectEventsTool } from "./event-matcher"; import { generateSqlTool } from "./query-writer"; // server-side state used by networks, routers and agents export type AgentState = StateData & { userId?: string; eventTypes?: string[]; schemas?: Record; selectedEvents?: { event_name: string; reason: string }[]; currentQuery?: string; sql?: string; }; // a typed manifest of all available tools const manifest = createToolManifest([ generateSqlTool, selectEventsTool, ] as const); export type ToolManifest = typeof manifest; ``` 2. **Define client-side types** Create a `ClientState` type which will type state that your UI will send to your agent backend. This is a great place to pass along important context about the user or what they’re doing in your app. ```typescript import { useAgent, type AgentKitEvent, type UseAgentsConfig, type UseAgentsReturn, } from "@inngest/use-agent"; import type { ToolManifest } from "@/app/api/inngest/functions/agents/types"; export type ClientState = { sqlQuery: string; eventTypes: string[]; schemas: Record | null; currentQuery: string; }; export type AgentConfig = { tools: ToolManifest; state: ClientState }; export type AgentEvent = AgentKitEvent; export function useInsightsAgent( config: UseAgentsConfig ): UseAgentsReturn { return useAgent<{ tools: ToolManifest; state: ClientState }>(config); } ``` 3. **Create agents and an Inngest function to run them** 4. **Integrate a useAgent hook into your UI** For a deeper dive into streaming agents, check out our [Usage Guide](/streaming/usage-guide). # Provider > A deep dive into the provider for streaming agents The `AgentProvider` is a React component that creates a shared context for all `useAgent` hooks in your application. While it’s optional, using it is highly recommended as it improves performance and ensures configuration consistency. ## Why Use the Provider? [Section titled “Why Use the Provider?”](#why-use-the-provider) By wrapping your agent-driven components in `AgentProvider`, you get several key benefits: * **Performance**: A single WebSocket connection is established and shared across all components, reducing network overhead. * **Consistency**: A shared transport configuration, user context, and channel key are used by all hooks, preventing inconsistencies. * **Flexibility**: Individual hooks can still override the shared configuration if needed for specific cases. * **Anonymous Users**: The provider automatically generates and persists a unique ID for anonymous users, allowing them to have a consistent experience across page loads. ## How It Works [Section titled “How It Works”](#how-it-works) `AgentProvider` creates a React Context that provides a shared transport instance, connection, and user information to any `useAgent` hook rendered within it. The hooks will automatically detect and use the context if it’s available. If you don’t use the provider, each `useAgent` hook will create its own transport and connection, which is less efficient. ## Usage Patterns [Section titled “Usage Patterns”](#usage-patterns) Here are some common ways to use the `AgentProvider`. ### Basic Authenticated User [Section titled “Basic Authenticated User”](#basic-authenticated-user) For an application with logged-in users, pass the user’s unique ID to the `userId` prop. This ensures that the agent’s context is tied to the correct user. ```tsx import { AgentProvider } from "@inngest/use-agent"; import { ChatPage } from "./ChatPage"; import { ThreadsSidebar } from "./ThreadsSidebar"; function App({ userId }) { return ( ); } ``` ### Anonymous Users [Section titled “Anonymous Users”](#anonymous-users) If your application supports guest users, you can omit the `userId` prop. The provider will automatically create a unique anonymous ID and store it in `sessionStorage` to maintain a consistent experience for the user during their session. ```tsx import { AgentProvider } from "@inngest/use-agent"; import { GuestChatInterface } from "./GuestChatInterface"; function App() { return ( ); } ``` ### Collaborative Sessions [Section titled “Collaborative Sessions”](#collaborative-sessions) To create shared, collaborative sessions (e.g., a chat where multiple users interact with the same agent in a shared context), you can use the `channelKey` prop. All users who connect with the same `channelKey` will be subscribed to the same real-time channel. ```tsx import { AgentProvider } from "@inngest/use-agent"; import { CollaborativeChat } from "./CollaborativeChat"; function ProjectChat({ projectId }) { return ( ); } ``` ### Custom Transport Configuration [Section titled “Custom Transport Configuration”](#custom-transport-configuration) You can customize the HTTP endpoints and headers used by the transport layer by passing a configuration object to the `transport` prop. This is useful if your API routes don’t follow the default conventions. ```tsx import { AgentProvider } from "@inngest/use-agent"; function App({ userId, getAuthToken }) { return ( ({ 'Authorization': `Bearer ${getAuthToken()}`, }) }} > ); } ``` # Transport > A deep dive into the transport layer for streaming agents AgentKit’s UI streaming is designed to be transport-agnostic, giving you the flexibility to use different real-time communication strategies. At its core, AgentKit doesn’t manage WebSocket connections or push data to clients directly. Instead, it provides a powerful `streaming` configuration hook where you provide a `publish` function. AgentKit calls this function with structured data chunks as your agent network executes, and you decide how to send that data to the client. This decoupled design means you can use any streaming provider that fits your needs. ## Default Transport: Inngest Realtime [Section titled “Default Transport: Inngest Realtime”](#default-transport-inngest-realtime) For most applications, we recommend using **Inngest Realtime** as the transport layer. It’s robust, scalable, and integrates seamlessly with Inngest’s durable function execution, providing a resilient and reliable streaming experience. ### How It Works [Section titled “How It Works”](#how-it-works) The data flow with Inngest Realtime involves a few key components on your backend: 1. **Chat Route (`/api/chat/route.ts`)**: A standard API endpoint that receives a message from your UI and sends an event to Inngest to trigger an agent run. 2. **Token Route (`/api/realtime/token/route.ts`)**: A secure endpoint that generates a short-lived subscription token for the client. The client uses this token to connect to a specific Inngest Realtime channel. 3. **Inngest Function**: The function that runs your agent or network. You pass Inngest’s `publish` function to AgentKit’s `streaming.publish` hook. AgentKit generates the events, and Inngest handles the delivery to the subscribed client. Here’s a look at the end-to-end flow: ```mermaid graph TD subgraph Client UI[React UI with useAgent] end subgraph Your Backend ChatRoute[POST /api/chat] TokenRoute[POST /api/realtime/token] InngestFn[Inngest Function with AgentKit] end subgraph Inngest Cloud Realtime[Inngest Realtime Service] end UI -->|Get subscription token| TokenRoute TokenRoute -->|Returns token| UI UI -->|Connect and subscribe| Realtime UI -->|Send message| ChatRoute ChatRoute -->|Triggers function| InngestFn InngestFn -->|Publishes events| Realtime Realtime -->|Streams events| UI ``` ## Session Transport (In-Memory) [Section titled “Session Transport (In-Memory)”](#session-transport-in-memory) For demos, tutorials, or ephemeral chat experiences where you don’t need to persist conversation history, you can use the session transport. This is a **client-side transport** that manages threads and messages for the current browser tab. It’s important to understand that the session transport still relies on the default HTTP transport to communicate with your backend to initiate agent runs. It doesn’t change how the server-side streaming works; it only affects how the conversation history is stored on the client. This is useful for: * Building live playgrounds of your agents. * Creating temporary chat sessions that are discarded when the browser tab is closed. * Reducing database load for non-essential conversations. ## Other Transports [Section titled “Other Transports”](#other-transports) Because of its decoupled design, you can integrate AgentKit with any real-time provider. To do so, you would create your own `publish` function that sends the event chunks from AgentKit to your provider of choice. ## Overriding Transport Methods [Section titled “Overriding Transport Methods”](#overriding-transport-methods) You can override any of the default transport’s methods or properties by passing a configuration object to the `transport` prop on either the `AgentProvider` or the `useAgent` hook. This is useful for customizing API endpoints, adding authentication headers, or modifying the request body. If you provide a partial configuration, it will be merged with the default transport configuration. ### Example: Customizing API routes, headers, and body [Section titled “Example: Customizing API routes, headers, and body”](#example-customizing-api-routes-headers-and-body) ```tsx import { AgentProvider } from "@inngest/use-agent"; function App({ userId, getAuthToken, getTenantId }) { return ( ({ 'Authorization': `Bearer ${getAuthToken()}`, }), // Add a tenantId to the body of all requests body: () => ({ tenantId: getTenantId(), }) }} > ); } ``` ## Transport API Reference [Section titled “Transport API Reference”](#transport-api-reference) The `IClientTransport` interface defines the methods that a transport must implement. Here is a reference for each method and its associated data structures. ### `sendMessage(params, options?)` [Section titled “sendMessage(params, options?)”](#sendmessageparams-options) Sends a message from the user to the agent. * `params`: `SendMessageParams` * `userMessage`: `object` * `id`: `string` - A unique client-generated ID for the message. * `content`: `string` - The text content of the user’s message. * `role`: `"user"` * `state?`: `Record` - Optional client-side state to persist with the message. * `clientTimestamp?`: `Date` - The timestamp from when the user sent the message. * `systemPrompt?`: `string` - An optional system prompt to override the agent’s default. * `threadId`: `string` - The ID of the conversation thread. * `history`: `unknown[]` - The current conversation history from the client. * `userId?`: `string` - The ID of the user. * `channelKey?`: `string` - The key for a collaborative channel. * `options?`: `RequestOptions` * **Returns**: `Promise<{ success: boolean; threadId: string }>` ### `getRealtimeToken(params, options?)` [Section titled “getRealtimeToken(params, options?)”](#getrealtimetokenparams-options) Fetches a token for connecting to the real-time service. * `params`: `GetRealtimeTokenParams` * `userId?`: `string` * `threadId?`: `string` * `channelKey?`: `string` * `options?`: `RequestOptions` * **Returns**: `Promise` * `token`: `string` - The subscription token. * `expires?`: `number` - Optional expiration timestamp. * `channel?`: `string` - Optional channel information. ### `fetchHistory(params, options?)` [Section titled “fetchHistory(params, options?)”](#fetchhistoryparams-options) Fetches the message history for a specific thread. * `params`: `FetchHistoryParams` * `threadId`: `string` * `options?`: `RequestOptions` * **Returns**: `Promise` - An array of message objects. ## Experimental Methods [Section titled “Experimental Methods”](#experimental-methods) ### `fetchThreads(params, options?)` [Section titled “fetchThreads(params, options?)”](#fetchthreadsparams-options) Fetches a paginated list of conversation threads. * `params`: `FetchThreadsParams` * `userId?`: `string` * `channelKey?`: `string` * `limit?`: `number` * `cursorTimestamp?`: `string` * `cursorId?`: `string` * `offset?`: `number` * `options?`: `RequestOptions` * **Returns**: `Promise` * `threads`: `Thread[]` - An array of thread objects. * `hasMore`: `boolean` - Indicates if more pages are available. * `total`: `number` - The total number of threads. ### `createThread(params, options?)` [Section titled “createThread(params, options?)”](#createthreadparams-options) Creates a new conversation thread. * `params`: `CreateThreadParams` * `userId?`: `string` * `channelKey?`: `string` * `title?`: `string` * `metadata?`: `Record` * `options?`: `RequestOptions` * **Returns**: `Promise<{ threadId: string; title: string }>` ### `deleteThread(params, options?)` [Section titled “deleteThread(params, options?)”](#deletethreadparams-options) Deletes a conversation thread. * `params`: `DeleteThreadParams` * `threadId`: `string` * `options?`: `RequestOptions` * **Returns**: `Promise` # Usage Guide > A deep dive into streaming agents Let’s build a simple SQL generation agent network with realtime streaming. To kick things off, let’s walk through a few endpoints you’ll need to wire this all up: * **Inngest Client (`/api/inngest/client.ts`)**: Initializes Inngest with the `realtimeMiddleware`. * **Realtime Channel (`/api/inngest/realtime.ts`)**: Defines a typed realtime channel and topic. * **Chat Route: `/api/chat/route.ts`**: This is a standard Next.js API route. Its only job is to receive a request from the frontend and send an event to Inngest to trigger a function. * **Token Route: `/api/realtime/token/route.ts`**: This secure endpoint generates a subscription token that the frontend needs to connect to Inngest realtime. * **Inngest Route: `/api/inngest/route.ts`**: The standard handler that serves all your Inngest functions. Let’s take a closer look at each of these endpoints and what they do… *** ## Set up Inngest for streaming [Section titled “Set up Inngest for streaming”](#set-up-inngest-for-streaming) 1. **Inngest Client - /api/inngest/client.ts** This file configures the Inngest client and enables the realtime middleware, which is essential for streaming. app/api/inngest/client.ts ```tsx import { realtimeMiddleware } from "@inngest/realtime/middleware"; import { Inngest } from "inngest"; export const inngest = new Inngest({ id: "agent-app-client", middleware: [realtimeMiddleware()], }); ``` 2. **Realtime Channel - /api/inngest/realtime.ts** Here, we define a strongly-typed channel for our agent’s communications. The `agent_stream` topic is where all message chunks will be published fromAgentKit. app/api/inngest/realtime.ts ```tsx import { type AgentMessageChunk } from "@inngest/agent-kit"; import { channel, topic } from "@inngest/realtime"; export const createChannel = channel( (userId: string) => `user:${userId}` ).addTopic(topic("agent_stream").type()); ``` 3. **Chat API Route - /api/chat/route.ts** This endpoint is the bridge between your frontend and the Inngest backend. It receives the user’s message and dispatches an event to trigger the agent network. app/api/chat/route.ts ```tsx import { NextRequest, NextResponse } from "next/server"; import { auth } from "@clerk/nextjs/server"; // Or your auth provider import { z } from "zod"; import { inngest } from "../inngest/client"; const chatRequestSchema = z.object({ userMessage: z.object({ id: z.string(), content: z.string(), role: z.literal("user"), }), threadId: z.string().optional(), channelKey: z.string(), }); export async function POST(req: NextRequest) { try { const { userId } = auth(); if (!userId) { return NextResponse.json({ error: "Please sign in" }, { status: 401 }); } const validationResult = chatRequestSchema.safeParse(await req.json()); if (!validationResult.success) { return NextResponse.json({ error: "Invalid request" }, { status: 400 }); } const { userMessage, threadId, channelKey } = validationResult.data; await inngest.send({ name: "agent/chat.requested", data: { userMessage, threadId, channelKey, userId, }, }); return NextResponse.json({ success: true }); } catch (error) { return NextResponse.json( { error: error instanceof Error ? error.message : "Failed to start chat", }, { status: 500 } ); } } ``` 4. **Token API Route** **`/api/realtime/token/route.ts`** This secure endpoint generates a subscription token that the frontend needs to connect to Inngest realtime. ```tsx import { NextRequest, NextResponse } from "next/server"; import { auth } from "@clerk/nextjs/server"; // or any auth provider import { getSubscriptionToken } from "@inngest/realtime"; import { inngest } from "../../inngest/client"; import { createChannel } from "../../inngest/realtime"; export type RequestBody = { userId?: string; channelKey?: string; }; export async function POST(req: NextRequest) { const { userId } = auth(); // authenticate the user if (!userId) { return NextResponse.json( { error: "Please sign in to create a token" }, { status: 401 } ); } try { // 1. Get the channel key from the request body and validate it const { channelKey } = (await req.json()) as RequestBody; if (!channelKey) { return NextResponse.json( { error: "channelKey is required" }, { status: 400 } ); } // 2. Create a subscription token for the resolved channel const token = await getSubscriptionToken(inngest, { channel: createChannel(channelKey), topics: ["agent_stream"], }); // 3. Return the token return NextResponse.json(token); } catch (error) { // ... handle error response } } ``` 5. **Inngest Route - /api/inngest/route.ts** This is the standard Next.js route handler for serving all of your Inngest functions. app/api/inngest/route.ts ```tsx import { serve } from "inngest/next"; import { inngest } from "./client"; import { runAgentNetwork } from "./functions/run-network"; export const { GET, POST, PUT } = serve({ client: inngest, functions: [runAgentNetwork], }); ``` Now that we have Inngest and our API routes configured, let’s build out the agents. We are going to create a network of 3 agents orchestrated via a simple code based router. The router will ensure that our network runs the following agents in this exact order: 1. **Event Matcher**: Selects 1-5 event names that we should consider for the query 2. **Query Writer**: Generates a SQL query given a list of events & schemas 3. **Summarizer**: Creates a short summary of the query and adds it to message history Let’s start by creating our event matcher and query writer agents. Each agent will have access to only one tool each which we will ensure is always invoked by defining a static tool\_choice. ```tsx import { createAgent, createTool, openai } from "@inngest/agent-kit"; import { z } from "zod"; import type { AgentState } from "./types"; // Define the tool for generating SQL export const generateSqlTool = createTool({ name: "generate_sql", description: "Provide the final SQL SELECT statement...", parameters: z.object({ sql: z.string().describe("A single valid SELECT statement."), title: z.string().describe("Short 20-30 character title for this query"), reasoning: z.string().describe("Brief explanation..."), }), handler: ({ sql, title, reasoning }) => { return { sql, title, reasoning }; }, }); // Define the agent that uses the tool export const queryWriterAgent = createAgent({ name: "Insights Query Writer", description: "Generates a safe, read-only SQL SELECT statement.", system: async ({ network }) => { /* ... dynamic system prompt ... */ }, model: openai({ model: "gpt-5-nano-2025-08-07" }), tools: [generateSqlTool], tool_choice: "generate_sql", // Force this tool to be called }); // Define the event matcher agent export const selectEventsTool = createTool({ name: "select_events", description: "Select 1-5 event names from the provided list that are most relevant to the user's query.", parameters: z.object({ events: z .array( z.object({ event_name: z.string(), reason: z.string(), }) ) .min(1) .max(6), }), handler: (args, { network }) => { const { events } = args; // Persist selection on network state for downstream agents network.state.data.selectedEvents = events; return { selected: events, reason: "Selected by the LLM based on the user's query.", totalCandidates: network.state.data.eventTypes?.length || 0, }; }, }); export const eventMatcherAgent = createAgent({ name: "Insights Event Matcher", description: "Analyzes available events and selects 1-5 that best match the user's intent.", system: async ({ network }) => { const events = network?.state.data.eventTypes || []; const sample = events.slice(0, 50); // avoid overly long prompts return [ "You are an event selection specialist.", "Your job is to analyze the user's request and the list of available event names, then choose the 1-5 most relevant events.", "", "Instructions:", "- Review the list of available events provided below.", "- Based on the user's query, decide which 1-5 events are the best match.", "- Call the `select_events` tool and pass your final choice in the `events` parameter.", "- Do not guess event names; only use names from the provided list.", "", sample.length ? `Available events (${ events.length } total, showing up to 50):\n${sample.join("\n")}` : "No event list is available. Ask the user to clarify which events they are interested in.", ].join("\n"); }, model: openai({ model: "gpt-5-nano-2025-08-07" }), tools: [selectEventsTool], tool_choice: "select_events", // Force this tool to be called }); ``` Once you have your agent defined, you can define your server-side state type and use `createToolManifest` to create a type which will be used on the client-side to ensure end-to-end type safety. ```tsx import { createToolManifest, type StateData } from "@inngest/agent-kit"; import { selectEventsTool } from "./event-matcher"; import { generateSqlTool } from "./query-writer"; // server-side state used by networks, routers and agents export type AgentState = StateData & { userId?: string; eventTypes?: string[]; schemas?: Record; selectedEvents?: { event_name: string; reason: string }[]; currentQuery?: string; sql?: string; }; // a typed manifest of all available tools const manifest = createToolManifest([ generateSqlTool, selectEventsTool, ] as const); export type ToolManifest = typeof manifest; ``` With server-side state and a ToolManifest now defined, you can strongly type your own agent hook and define client-side state that you may want sent in each message: ```tsx import { useAgent, type AgentKitEvent, type UseAgentsConfig, type UseAgentsReturn, } from "@inngest/use-agent"; import type { ToolManifest } from "@/app/api/inngest/functions/agents/types"; export type ClientState = { sqlQuery: string; eventTypes: string[]; schemas: Record | null; currentQuery: string; }; export type AgentConfig = { tools: ToolManifest; state: ClientState }; export type AgentEvent = AgentKitEvent; export function useInsightsAgent( config: UseAgentsConfig ): UseAgentsReturn { return useAgent<{ tools: ToolManifest; state: ClientState }>(config); } ``` Before we move onto implementing the agent hook into your UI components, let’s create a summarizer agent and an agent network with a code-based router to orchestrate everything: ```tsx // Define the summarizer agent - this agent has no tools and just provides a summary export const summarizerAgent = createAgent({ name: "Insights Summarizer", description: "Writes a concise summary describing what the generated SQL does and why.", system: async ({ network }) => { const events = network?.state.data.selectedEvents?.map((e) => e.event_name) ?? []; const sql = network?.state.data.sql; return [ "You are a helpful assistant summarizing the result of a SQL generation process.", "Write a one sentence short summary that explains:", "- What events were just analyzed (if known).", "- What the query returns and how it helps the user.", "Avoid restating the full SQL. Be clear and non-technical when possible.", events.length ? `Selected events: ${events.join(", ")}` : "", sql ? "A SQL statement has been prepared; summarize its intent, not its exact text." : "", ] .filter(Boolean) .join("\n"); }, model: openai({ model: "gpt-5-nano-2025-08-07" }), }); ``` app/api/inngest/functions/agents/network.ts ```tsx import { createNetwork, openai, type Network } from "@inngest/agent-kit"; import { eventMatcherAgent } from "./event-matcher"; import { queryWriterAgent } from "./query-writer"; import { summarizerAgent } from "./summarizer"; import type { InsightsAgentState } from "./types"; // A simple router that executes agents in a fixed order const sequenceRouter: Network.Router = async ({ callCount, }) => { if (callCount === 0) return eventMatcherAgent; if (callCount === 1) return queryWriterAgent; if (callCount === 2) return summarizerAgent; return undefined; // ends the network run }; // Define the network directly - no factory function needed export const insightsNetwork = createNetwork({ name: "Insights SQL Generation Network", description: "Selects relevant events, proposes a SQL query, and summarizes the result.", agents: [eventMatcherAgent, queryWriterAgent, summarizerAgent], defaultModel: openai({ model: "gpt-5-nano-2025-08-07" }), maxIter: 6, router: sequenceRouter, }); ``` Now let’s create an Inngest function which we’ll use to run our agent network and configure event streaming: app/api/inngest/functions/run-network.ts ```tsx import { createState, type AgentMessageChunk, type Message, } from "@inngest/agent-kit"; import type { ChatRequestEvent } from "@inngest/use-agent"; import { v4 as uuidv4 } from "uuid"; import { inngest } from "../client"; import { createChannel } from "../realtime"; import type { InsightsAgentState } from "./agents/types"; import { insightsNetwork } from "./agents/network"; export const runAgentNetwork = inngest.createFunction( { id: "run-insights-agent", name: "Insights SQL Agent", }, { event: "insights-agent/chat.requested" }, async ({ event, publish, step }) => { const { threadId: providedThreadId, userMessage, // new user message userId, channelKey, // channel to stream on history, // previous messages } = event.data as ChatRequestEvent; // Validate required userId if (!userId) { throw new Error("userId is required for agent chat execution"); } // Generate a threadId const threadId = await step.run("generate-thread-id", async () => { return providedThreadId || uuidv4(); }); // Determine the target channel for publishing (channelKey takes priority) const targetChannel = await step.run( "generate-target-channel", async () => { return channelKey || userId; } ); try { const clientState = userMessage.state || {}; // Create state for the network const networkState = createState( { userId, ...clientState, // passing in client-side managed state into our network }, { messages: history, threadId, } ); // Run the network with streaming enabled await insightsNetwork.run(userMessage, { state: networkState, streaming: { publish: async (chunk: AgentMessageChunk) => { // you can inspect and add metadata to chunks here await publish(createChannel(targetChannel).agent_stream(chunk)); }, }, }); return { success: true, threadId, message: "Agent network completed successfully", }; } catch (error) { // emit an error chunk here } } ); ``` With all that wired up now, you can now render tool calls and messages in your UI like so: ```tsx "use client"; import { useState } from "react"; import { useInsightsAgent, type ClientState } from "@/lib/use-insights-agent"; import type { ToolCallUIPart } from "@inngest/use-agent"; import type { ToolManifest } from "@/app/api/inngest/functions/agents/types"; export default function ChatTestPage() { return (

Minimal example using a single-threaded conversation.

); } function Chat() { const [input, setInput] = useState(""); const { messages, status, sendMessage } = useInsightsAgent({ channelKey: "chat_test", state: (): ClientState => ({ eventTypes: [ "app/user.created", "order.created", "payment.failed", "email.sent", ], schemas: null, currentQuery: "", tabTitle: "Chat Test", mode: "demo", timestamp: Date.now(), }), }); async function onSubmit(e: React.FormEvent) { e.preventDefault(); const value = input.trim(); if (!value || status !== "ready") return; setInput(""); await sendMessage(value); } return (
{messages.map(({ id, role, parts }) => (
{role}
{parts.map((part) => { if (part.type === "text") { return
{part.content}
; } if (part.type === "tool-call") { return ; } return null; })}
))} {status !== "ready" &&

AI is thinking...

}
setInput(e.target.value)} placeholder={status === "ready" ? "Ask me anything" : "Thinking..."} disabled={status !== "ready"} />
); } function ToolCallRenderer({ part }: { part: ToolCallUIPart }) { if (part.state !== "output-available") return null; if (part.toolName === "select_events") { const { data } = part.output; return (
Selected Events:
    {data.selected.map((e) => (
  • {e.event_name}

    {e.reason}

  • ))}
); } if (part.toolName === "generate_sql") { const { data } = part.output; return (
SQL Query:

{data.title}

{data.reasoning}

{data.sql}
); } return null; } ``` With all that done, you should now have a fully functional SQL generation agent network with realtime streaming! By following this guide and using the `useAgent` hook, you now have: 1. **Type Safety**: Tool names, inputs, and outputs are fully typed based on your `ToolManifest` 2. **Real-time Streaming**: See tools execute in real-time with different states (`input-streaming`, `input-available`, `executing`, `output-available`) 3. **Generative UI**: Each tool can have its own custom rendering logic while maintaining type safety 4. **State Management**: The hook automatically manages conversation state, message ordering, and streaming events 5. **Error Handling**: Built-in error states and recovery mechanisms