# Human in the Loop Source: https://agentkit.inngest.com/advanced-patterns/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 "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 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, }); ``` 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 First, you'll need to create an Inngest Client: ```ts src/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 src/inngest/agent-network.ts {21-54} 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 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 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. # MCP as tools Source: https://agentkit.inngest.com/advanced-patterns/mcp 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 AgentKit supports configuring MCP servers via `Streamable HTTP`, `SSE` or `WS` transports: ```ts Self-hosted MCP server 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", }, }, ], }); ``` ```ts Smithery MCP server 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 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. An array of MCP server configurations. ### MCP.Server 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"). The transport configuration for connecting to the MCP server. ### TransportSSE Specifies that the transport is Server-Sent Events. The URL of the SSE endpoint. Optional configuration for the EventSource. Optional request configuration. ### TransportWebsocket Specifies that the transport is WebSocket. The WebSocket URL of the MCP server. ## Examples This examples shows how to use the [Neon MCP Smithery Server](https://smithery.ai/server/neon/) to build a Neon Assistant Agent that can help you manage your Neon databases. {" "}
{" "} Agents Tools Network Integrations
Code-based Router
# Multi-steps tools Source: https://agentkit.inngest.com/advanced-patterns/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 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 src/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 src/inngest/tools/research-web.ts {10, 22, 27} 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 We can now add our `researchWebTool` to our AgentKit network: ```ts src/inngest/agent-network.ts {2, 7, 18} 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 Learn how to configure user-based capacity for your AgentKit network. Learn how to customize the retries of your multi-steps tools. # Configuring Multi-tenancy Source: https://agentkit.inngest.com/advanced-patterns/multitenancy 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 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 First, you'll need to create an Inngest Client: ```ts src/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 src/inngest/agent-network.ts {19-30, 33} 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 We can now configure the capacity by user by adding concurrency and throttling configuration to our Inngest function: ```ts src/inngest/agent-network.ts {8-13} 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 Learn how to customize the retries of your multi-steps tools. # Configuring Retries Source: https://agentkit.inngest.com/advanced-patterns/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 Configuring a custom retry policy is done by 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 src/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 src/inngest/agent-network.ts {19-30, 33} 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 We can now configure the capacity by user by adding concurrency and throttling configuration to our Inngest function: ```ts src/inngest/agent-network.ts {8} 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 Learn how to configure user-based capacity for your AgentKit network. # Deterministic state routing Source: https://agentkit.inngest.com/advanced-patterns/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 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 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 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 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 * **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 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 # UI Streaming Source: https://agentkit.inngest.com/advanced-patterns/ui-streaming Enable your Agents to stream updates to your UI. AgentKit integrates with Inngest's [Realtime API](https://www.inngest.com/docs/features/realtime), enabling you to stream updates to your AI Agent's UI. This guide will show you how to stream updates to an example Next.js app. Find the complete source code on GitHub. Dig into the Inngest Realtime API documentation. ## Streaming updates to a Next.js app Let's add a simple UI with streamed updates to our [Quickstart Database AI Agent](/getting-started/quick-start) composed of two specialized [Agents](/concepts/agents): a Database Administrator and a Security Expert. ![UI of the Database AI Agent](https://mintlify.s3.us-west-1.amazonaws.com/inngest/graphics/advanced-patterns/ui-streaming/database-agent-ui.png) To enable our Agents to stream updates to the UI, we'll need to: 1. Update our Inngest client configuration 2. Create a channel for our Agents to publish updates to 3. Update our Agents to publish updates to the UI 4. Set up the frontend to subscribe to the updates ### 1. Updating the Inngest client configuration Create or update your Inngest client as follows: ```ts lib/inngest/client.ts {1, 6} import { realtimeMiddleware } from "@inngest/realtime"; import { Inngest } from "inngest"; export const inngest = new Inngest({ id: "realtime-ui-agent-kit-nextjs", middleware: [realtimeMiddleware()], }); ``` This will enable the Realtime API to be used in your Inngest functions. ### 2. Create a channel for our Agents to publish updates to In a dedicated file or above your existing Inngest function, create a Realtime channel as follows: ```ts lib/inngest/functions.ts import { channel, topic } from "@inngest/realtime"; // create a channel for each discussion, given a thread ID. A channel is a namespace for one or more topics of streams. export const databaseAgentChannel = channel( (threadId: string) => `thread:${threadId}` ) // Add a specific topic, eg. "ai" for all AI data within the user's channel .addTopic( topic("messages").schema( z.object({ message: z.string(), id: z.string(), }) ) ) .addTopic( topic("status").schema( z.object({ status: z.enum(["running", "completed", "error"]), }) ) ); ``` Our `databaseAgentChannel` takes a unique `threadId` as an argument, ensuring that each discussion has its own channel. We also added two topics to the channel: * `messages`: For all messages sent by the Agents * `status`: For global status updates ### 3. Enabling our Agents to publish updates to the UI To enable our Agents to stream updates to the UI, we need to move our Agents definition inside an Inngest function. By doing so, our Agents' tools will get access to the `publish()` function, which we'll use to publish updates to the UI: ```ts lib/inngest/functions.ts {8, 9, 12, 38-43} export const databaseAgentFunction = inngest.createFunction( { id: "database-agent", }, { event: "database-agent/run", }, async ({ event, publish }) => { const { query, threadId } = event.data; await publish(databaseAgentChannel(threadId).status({ status: "running" })); 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 linked to Postgres database schema, indexes, extensions.", model: anthropic({ model: "claude-3-5-haiku-latest", defaultParameters: { max_tokens: 4096, }, }), tools: [ createTool({ name: "provide_answer", description: "Provide the answer to the questions", parameters: z.object({ answer: z.string(), }), handler: async ( { answer }, { network }: Tool.Options ) => { network.state.data.dba_agent_answer = answer; await publish( databaseAgentChannel(threadId).messages({ message: `The Database administrator Agent has the following recommendation: ${network.state.data.dba_agent_answer}`, id: crypto.randomUUID(), }) ); }, }), ], }); // securityAgent and network definitions... await network.run(query); await publish( databaseAgentChannel(threadId).status({ status: "completed" }) ); } ); ``` `publish()` takes a channel topic as an argument, ensuring end-to-end type safety when writing your publish calls. All messages sent using `publish()` are guaranteed to be delivered at most once with the lowest latency possible. Your Inngest Function needs to be served via a Next.js API route: [see the example for more details](https://github.com/inngest/agent-kit/tree/main/examples/api/inngest/route.ts). ### 4. Build the frontend to subscribe to the updates Our Database AI Agent is now ready to stream updates to the UI. **Triggering the Agent** First, we'll need to trigger our Agent with a unique `threadId` as follows. In a Next.js application, triggering Inngest functions can be achieved using a Server Action: ```tsx app/actions.ts "use server"; import { randomUUID } from "crypto"; export async function runDatabaseAgent(query: string) { const threadId = randomUUID(); await inngest.send({ name: "database-agent/run", data: { threadId, query }, }); return threadId; } ``` **Subscribing to the updates** Now, we'll need to subscribe to the updates in our Next.js app using Inngest Realtime's `useInngestSubscription` hook: ```tsx app/page.tsx {11-15, 17-19, 22, 25} "use client"; import { useInngestSubscription } from "@inngest/realtime/hooks"; import { useCallback, useState } from "react"; import { fetchSubscriptionToken, runDatabaseAgent } from "./actions"; import { databaseAgentChannel } from "@/lib/inngest/functions"; import { Realtime } from "@inngest/realtime"; export default function Home() { const [query, setQuery] = useState(""); const [inputValue, setInputValue] = useState(""); const [threadId, setThreadId] = useState(undefined); const [subscriptionToken, setSubscriptionToken] = useState< | Realtime.Token | undefined >(undefined); const { data } = useInngestSubscription({ token: subscriptionToken, }); const startChat = useCallback(async () => { setInputValue(""); const threadId = await runDatabaseAgent(inputValue); setThreadId(threadId); setQuery(inputValue); setSubscriptionToken(await fetchSubscriptionToken(threadId)); }, [inputValue]); const onKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === "Enter") { startChat(); } }, [startChat] ); return ( // UI ... ) } ``` Looking at the highlighted lines, we can see that the flow is as follows: 1. The `startChat()` callback is called when the user clicks the "Run" button or presses Enter. 2. The `startChat()` callback calls the `runDatabaseAgent()` server action to trigger the Agent. 3. The `runDatabaseAgent()` server action generates a unique `threadId` and sends it to the Agent. 4. The `fetchSubscriptionToken()` server action fetches a subscription token for the `threadId`. 5. The `useInngestSubscription()` hook subscribes to the `messages` and `status` topics and updates the UI in realtime. Then, the rendering part of the component gets access to a fully typed `data` object, which contains the latest updates from the Agent: ```tsx JSX example using the fully typed data object { data.map((message, idx) => message.topic === "messages" ? (
{message.data.message}
) : (
{message.data.status === "completed" ? "Here are my recommendations, feel free to ask me anything else!" : message.data.status === "error" ? "I faced an error, please try again." : "Interesting question, I'm thinking..."}
) ); } ``` For more details on how to use the `useInngestSubscription()` hook, please refer to the [Inngest Realtime API documentation](https://www.inngest.com/docs/features/realtime/react-hooks). # Changelog Source: https://agentkit.inngest.com/changelog/overview Recent releases, new features, and fixes. * Introducing support for [Grok models](/reference/model-grok) * Adding support for [Gemini latest models](/reference/model-gemini) * 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) * remove `server` export to allow non-Node runtimes * allow tools with no parameters * [Integration with E2B Code Interpreter](/integrations/e2b) * Allow specifying [Inngest functions as tools](/advanced-patterns/multi-steps-tools) * Inngest is now an optional dependency * Fixed OpenAI adapter to safely parse non-string tool return values for Function calling * Various documentation improvements * 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 * Fixed state reference handling in agents * Updated SWEBench example configuration * Various stability improvements * Fixed network to agent state propagation in run * Improved git clone handling in SWEBench example * Various minor improvements * 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 Source: https://agentkit.inngest.com/concepts/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 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'), }); ``` 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(...' }] ``` When including your Agent in a Network, a `description` is required. Learn more about [using Agents in Networks here](#using-agents-in-networks). {/* TODO - Compare to OpenAI sdk call */} {/* TODO - When combined with Inngest's step.ai...expand */} ## 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 Agents themselves are relatively simple. When you call `run()`, there are several steps that happen: 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). For added control, you can dynamically modify the Agent's prompts before the next step using the `onStart` [lifecycle hook](#lifecycle-hooks). {/* TODO - Update this when Inngest isn't a requirement */} 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. To modify the result prior to calling tools, use the optional `onResponse` [lifecycle hook](#lifecycle-hooks). If the model decides to call one of the available `tools`, the Tool is automatically called. 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. The result is returned to the caller. ### 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 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 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 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 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 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 Source: https://agentkit.inngest.com/concepts/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 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 ```shell npm npm install inngest ``` ```shell pnpm pnpm install inngest ``` ```shell yarn yarn add inngest ``` ### 2. Serve your AgentKit network over HTTP Update your AgentKit network to serve over HTTP as follows: ```ts {1, 8-13} 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 **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](https://mintlify.s3.us-west-1.amazonaws.com/inngest/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 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](https://mintlify.s3.us-west-1.amazonaws.com/inngest/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](https://mintlify.s3.us-west-1.amazonaws.com/inngest/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 Parallel tool calls, Multitenancy and Retries */} ## Configuring Multitenancy and Retries {/* Learn how to run multiple tools in parallel. */} Configure usage limits based on users or organizations. Learn how to configure retries for your AgentKit Agents and Tools. # History Source: https://agentkit.inngest.com/concepts/history Learn how to persist conversations for your agents and networks ## 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 The `HistoryConfig` interface has three 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. * Invoked at the start of a run if no `threadId` exists in the state. */ 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 new messages 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 userMessage?: { content: string; role: "user"; timestamp: Date }; // The user's message state: State; input: string; network: NetworkRun; step?: GetStepTools; }) => Promise; } ``` #### `createThread` * Creates a new conversation thread in your database * Invoked at the start of a run if no `threadId` exists in the state * Returns an object with the new `threadId` #### `get` * Retrieves conversation history from your database * Invoked after thread initialization, but only if the client didn't provide `results` or `messages` * Returns an array of `AgentResult[]` representing the conversation history #### `appendResults` * Saves new messages 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 (prevents duplicates) *** ## 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 three methods const conversationHistoryAdapter: HistoryConfig = { // 1. Create new conversation threads createThread: async ({ state, input }) => { 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 get: async ({ threadId }) => { if (!threadId) return []; const messages = await db.message.findMany({ where: { threadId }, orderBy: { createdAt: "asc" }, }); // Transform database records to AgentResult format return messages .filter((msg) => msg.role === "assistant") .map((msg) => ({ agentName: msg.agentName, output: [ { type: "text" as const, role: "assistant" as const, content: msg.content, }, ], toolCalls: [], createdAt: new Date(msg.createdAt), })); }, // 3. Save new messages appendResults: async ({ threadId, newResults, userMessage }) => { if (!threadId) return; // Save user message if (userMessage) { await db.message.create({ data: { threadId, role: "user", content: userMessage.content, createdAt: userMessage.timestamp, }, }); } // Save agent responses 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: { threadId, role: "assistant", agentName: result.agentName, content, createdAt: result.createdAt, }, }); } }, }; // Create agents const researcher = createAgent({ name: "researcher", description: "Searches for information", model: openai({ model: "gpt-4" }), }); const writer = createAgent({ name: "writer", description: "Writes comprehensive responses", model: openai({ model: "gpt-4" }), }); // Create network with history configuration const assistantNetwork = createNetwork({ name: "Research Assistant", agents: [researcher, writer], defaultModel: openai({ model: "gpt-4" }), history: conversationHistoryAdapter, // Add history adapter here }); // Use the network - conversations will be automatically persisted const state = createState( { userId: "user-123" }, { threadId: "existing-thread-id" } // Optional: continue existing conversation ); await assistantNetwork.run("Tell me about quantum computing", { state }); ``` *** Once you've created your adapter, pass it to the `history` property when creating an agent or network: ```typescript Agent 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" }), }); ``` ```typescript Network 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 AgentKit supports two distint patterns for managing conversation history. ### 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) 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 history.appendResults() to save new messages ``` **Use case**: Ideal for interactive chat applications where the frontend maintains conversation state and fetches messages from an existing/seperate API ### Server/Client 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 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, in order of precedence: 1. **Explicit `threadId` (Highest Priority):** The most direct method is to provide a `threadId` when you create your state. This is the standard way to resume a specific, existing conversation. AgentKit will use this ID to load the relevant history via the `history.get()` method. ```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 }); ``` 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. 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. ## Best Practices Wrap database operations in `step.run()` for automatic retries and durability. ```typescript await step.run("database-operation", async () => { return await db.someOperation(); }); ``` 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 } ``` Ensure you have indexes on `thread_id` and `created_at` 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); ``` ## 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 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 Source: https://agentkit.inngest.com/concepts/memory Learn how to give your agents long-term, reflective memory using Mem0. ## 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: Your agent can respond to the user immediately, without waiting for database writes to complete. The memory operation runs reliably in the background as a separate, durable Inngest function. If it fails, Inngest automatically retries it. ## 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 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 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 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 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 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 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 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 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 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 ### 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 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 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 Source: https://agentkit.inngest.com/concepts/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 ### Create a model instance 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. ```ts OpenAI 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-..." }); ``` ```ts Anthropic 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 }, }); ``` ```ts Gemini import { gemini, createAgent } from "@inngest/agent-kit"; const model = gemini({ model: "gemini-1.5-flash" }); ``` ### Configure model hyper parameters (temperature, etc.) You can configure the model hyper parameters (temperature, etc.) by passing the `defaultParameters` option: ```ts OpenAI import { openai, createAgent } from "@inngest/agent-kit"; const model = openai({ model: "gpt-3.5-turbo", defaultParameters: { temperature: 0.5 }, }); ``` ```ts Anthropic import { anthropic, createAgent } from "@inngest/agent-kit"; const model = anthropic({ model: "claude-3-5-haiku-latest", defaultParameters: { temperature: 0.5, max_tokens: 4096 }, }); ``` ```ts Gemini import { gemini, createAgent } from "@inngest/agent-kit"; const model = gemini({ model: "gemini-1.5-flash", defaultParameters: { temperature: 0.5 }, }); ``` 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 ```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 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 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). ```plaintext OpenAI "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" ``` ```plaintext Anthropic "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"; ``` ```plaintext Gemini "gemini-1.5-flash" "gemini-1.5-flash-8b" "gemini-1.5-pro" "gemini-1.0-pro" "text-embedding-004" "aqa" ``` ```plaintext Grok "grok-2-1212" "grok-2" "grok-2-latest" "grok-3" "grok-3-latest" ``` ### Environment variable used for each model provider * OpenAI: `OPENAI_API_KEY` * Anthropic: `ANTHROPIC_API_KEY` * Gemini: `GEMINI_API_KEY` * Grok: `XAI_API_KEY` ## 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). Fork, clone, and open a pull request. # Networks Source: https://agentkit.inngest.com/concepts/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 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. You create a network with a list of available [Agents](/concepts/agents). Each Agent can use a different [model and inference provider](/concepts/models). You give the network a user prompt by calling `run()`. The network runs its core loop: The [Router](/concepts/routers) decides the first Agent to run with your input. 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. 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. 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 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' }), }); ``` A Network not defining a `defaultModel` and composed of Agents without model will throw an error. ### 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 ### 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 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, }); ``` 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` 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 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 Source: https://agentkit.inngest.com/concepts/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? 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). {/* TODO - The "stack" of agents isn't clear how this stack is created and when they are executed in relation to the router */} * 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 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 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 @inngest/agent-kit 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 ### 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) 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) 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, }); ``` 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) 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 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 Networks combines the State and Router to execute Agent workflows. State is a key-value store that can be used to store data between Agents. # State Source: https://agentkit.inngest.com/concepts/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 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 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 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 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; }, }); ``` {// TODO // - Using state in routers (why, how, example) // - Using state in agent prompts (why, how, example) } # Tools Source: https://agentkit.inngest.com/concepts/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 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 Optional parameters should be defined using `.nullable()` (not `.optional()`): ```ts {7-10} 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 You can find multiple examples of tools in the below GitHub projects: A tutorial showing how to create a Hacker News Agent using AgentKit Code-style routing and Agents with tools. 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. {/* TODO - Talk about the handler arguments and what you can do */} {/* TODO - Typing with zod */} {/* TODO - Showing how tools can be used for structured output */} {/* TODO - Leveraging state within tools */} {/* TODO - Using tool output from agent.run */} {/* TODO - Using Inngest steps with tools, human in the middle, etc. */} # Examples Source: https://agentkit.inngest.com/examples/overview Explore the following examples to see AgentKit Concepts (*Agents, Tools, ...*) in action: ## Tutorials This example shows how to leverages AgentKit's Agent to build an assistant that explain code. Agents A tutorial showing how to create a Hacker News Agent using AgentKit Code-style routing and Agents with tools. Agents Tools Network State
Code-based Router

## MCP as tools examples This examples shows how to use the [Neon MCP Smithery Server](https://smithery.ai/server/neon/) to build a Neon Assistant Agent that can help you manage your Neon databases.
Agents Tools Network Integrations
Code-based Router

## Code Examples This AgentKit example shows how to build a Support Agent Network with a "Human in the loop" pattern. Agents Tools Network
Agent Router
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. Agents Tools Network
Code-based Router
This AgentKit example uses E2B sandboxes to build a coding agent that can write code in any language. Agents Tools Network Integrations
Code-based Router
# Installation Source: https://agentkit.inngest.com/getting-started/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: ```shell npm npm install @inngest/agent-kit inngest ``` ```shell pnpm pnpm install @inngest/agent-kit inngest ``` ```shell yarn yarn add @inngest/agent-kit inngest ``` **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 Discover Inngest's Dev Server with live traces and logs. Add concurrency and throttling to your AgentKit network and deploy it to Inngest. # Local development Source: https://agentkit.inngest.com/getting-started/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: