# Durable Agents Durable agent execution - Package: @bijectionhq/durable-agents - Category: AI & Agents - Status: Reference - License: Apache-2.0 - Bijection source (repository access required): https://github.com/bijectionhq/durable-agents/tree/6322213b09dc69d3c9c82ab465ca30fbcb7d2070 - Upstream: https://github.com/ziegfried/convex-durable-agents/tree/85959879a9d61a5b85b1eb543531606277e071b4 - Documentation: https://docs.bijection.com/components/using - Directory: https://bijection.com/components/durable-agents ## Get the source Bijection repository access is required. Request access at https://bijection.com/contact. ```sh git clone https://github.com/bijectionhq/durable-agents.git cd durable-agents git checkout 6322213b09dc69d3c9c82ab465ca30fbcb7d2070 ``` ## Before using this component Read the package README at the recorded revision for setup, dependencies, and API examples. Upstream documentation describes the original package and may differ from the Bijection adaptation. This is a source candidate for evaluation. Catalog inclusion does not establish npm availability or runtime qualification. Check compatibility with your SDK and deployment before adoption. External providers run their own compute. ## Upstream package documentation Recorded 2026-10-23. This describes convex-durable-agents; check the Bijection adaptation separately. This component provides durable AI agents that can run indefinitely and survive server restarts or failures. Built on AI SDK v6, it executes agent tool loops asynchronously to avoid Convex action time limits while maintaining persistent streaming and automatic retries. Agents can call external APIs through Convex actions and optionally route execution through workpools for parallelism control. ### Use cases - **Long-running AI workflows** that need to survive dev server restarts and production failures without losing conversation state - **AI agents with external API calls** that require automatic retry logic and durable execution when tools fail or timeout - **Multi-step agent tasks** that exceed Convex action time limits by running tool loops asynchronously with persistent state - **Production chatbots** that need real-time streaming with message persistence and the ability to resume interrupted conversations - **Background AI processing** where agents need to continue working even when the user disconnects or the browser closes ### How it works The component wraps AI SDK's `streamText` API with Convex's durable execution model. You define agents using `streamHandlerAction` with your language model and tools, then export a complete API using `defineAgentApi` for public access or `defineInternalAgentApi` for server-only access. Tools are implemented as Convex actions using `createActionTool` for synchronous results or `createAsyncTool` for background processing. The component handles automatic retries with configurable backoff strategies and can route execution through `@convex-dev/workpool` for parallelism control. Real-time streaming works through `streamUpdates` queries that deliver message deltas to React components via the `useAgentChat` hook. Thread state persists across failures, allowing agents to resume exactly where they left off using `resumeThread`. Authorization callbacks protect thread access, and the component maintains full conversation history with support for both user messages and tool execution results. ### Upstream README # Durable Agents Component for Convex A Convex component for building durable AI agents with an async tool loop. The goal of this component is to provide a way to build AI agents that can run indefinitely and survive failures and restarts. It provides some of the functionality of the [Convex Agents Component](https://www.convex.dev/components/agent) (such as persistent streaming), while deliberately leaving out some of the more advanced features (context management, RAG, rate limiting, etc.). The component is built on top of the [AI SDK v6](https://ai-sdk.dev/) and aims to expose its full `streamText` API with persistence and durable execution. **Note:** This component is still in early development and is not yet ready for production use. The API will very likely change before a first stable release. ## Features - **Async Execution**: Agent tool loop is executed asynchronously to avoid time limits of convex actions - **Tool Execution**: via convex actions - support for both sync and async tools - **Automatic Retries**: Failed tool calls are automatically retried (opt-in) - **Durable Execution**: Agent tool loops survive crashes and dev server restarts - **Workpool Support**: Optionally route agent and tool execution through `@convex-dev/workpool` for parallelism control ## Installation ```bash npm install convex-durable-agents ai zod ``` ## Quick Start ### 1. Configure the Component Create a `convex.config.ts` file in your app's `convex/` folder: ```ts // convex/convex.config.ts import { defineApp } from "convex/server"; import durableAgents from "convex-durable-agents/convex.config.js"; const app = defineApp(); app.use(durableAgents); export default app; ``` ### 2. Define Your Agent Create a chat handler with your AI model and tools: ```ts // convex/chat.ts import { z } from "zod"; import { components, internal } from "./_generated/api"; import { createActionTool, defineAgentApi, streamHandlerAction } from "convex-durable-agents"; // Define the stream handler with your model and tools export const chatAgentHandler = streamHandlerAction(components.durableAgents, { model: "anthropic/claude-haiku-4.5", system: "You are a helpful AI assistant.", tools: { get_weather: createActionTool({ description: "Get weather for a location", args: z.object({ location: z.string() }), handler: internal.tools.weather.getWeather, retry: { enabled: true, maxAttempts: 5, }, }), }, saveStreamDeltas: true, // Enable real-time streaming }); // Export the agent API (public - callable from clients) export const { createThread, sendMessage, getThread, listMessages, streamUpdates, listThreads, deleteThread, resumeThread, stopThread, addToolResult, addToolError, } = defineAgentApi(components.durableAgents, internal.chat.chatAgentHandler, { // Optional: Add authorization to protect thread access authorizationCallback: async (ctx, threadId) => { // Example: verify the user owns this thread // const identity = await ctx.auth.getUserIdentity(); // if (!identity) throw new Error("Unauthorized"); }, }); ``` #### Using Internal API If you want to restrict the agent API to only be callable from other Convex functions (not directly from clients), use `defineInternalAgentApi` instead: ```ts // convex/chat.ts import { defineInternalAgentApi } from "convex-durable-agents"; // Export internal agent API (only callable from other Convex functions) export const { createThread, sendMessage, getThread, // ... other functions } = defineInternalAgentApi(components.durableAgents, internal.chat.chatAgentHandler); ``` This is useful when you want to: - Add authentication/authorization checks before calling agent functions - Wrap agent functions with additional business logic - Prevent direct client access to the agent API - Run agents in the background You can then create your own public functions that call the internal API: ```ts // convex/myChat.ts import { mutation, query } from "./_generated/server"; import { internal } from "./_generated/api"; import { v } from "convex/values"; // Public wrapper with auth check export const sendMessage = mutation({ args: { threadId: v.string(), prompt: v.string() }, handler: async (ctx, args) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) throw new Error("Not authenticated"); // Call the internal agent API return ctx.runMutation(internal.chat.sendMessage, args); }, }); ``` ### 3. Create Tool Handlers Tools are defined as Convex actions: ```ts // convex/tools/weather.ts import { v } from "convex/values"; import { internalAction } from "../_generated/server"; export const getWeather = internalAction({ args: { location: v.string() }, returns: v.object({ weather: v.string(), temperature: v.number() }), handler: async (_ctx, args) => { // Call your weather API here return { weather: "sunny", temperature: 72 }; }, }); ``` ### 4. Build Your UI Use the React hooks to build your chat interface: ```tsx import { useAgentChat } from "convex-durable-agents/react"; import { api } from "../convex/_generated/api"; function ChatView({ threadId }: { threadId: string }) { const { messages, status, isRunning, sendMessage, stop } = useAgentChat({ listMessages: api.chat.listMessages, streamUpdates: api.chat.streamUpdates, getThread: api.chat.getThread, sendMessage: api.chat.sendMessage, stopThread: api.chat.stopThread, resumeThread: api.chat.resumeThread, threadId, }); return (
{messages.map((msg) => (
{msg.role}: {msg.parts.map((p) => (p.type === "text" ? p.text : null))}
))} {isRunning && } { if (e.key === "Enter" && !isRunning) { sendMessage(e.currentTarget.value); e.currentTarget.value = ""; } }} disabled={isRunning} />
); } ``` ## API Reference ### Client API #### `defineAgentApi(component, streamHandler, options?)` Creates the full agent API with **public** functions that can be called directly from clients: - `createThread({ prompt?, messages?, autoStart? })` - Create a new conversation thread - `sendMessage({ threadId, prompt })` - Send a message to a thread - `addMessage({ threadId, msg })` - Add a message to a thread without triggering the agent - `resumeThread({ threadId, prompt? })` - Resume a stopped/failed thread - `stopThread({ threadId })` - Stop a running thread - `getThread({ threadId })` - Get thread details - `listMessages({ threadId })` - List messages in a thread - `streamUpdates({ threadId, fromSeq? })` - Get streaming message updates for real-time UI - `listThreads({ limit? })` - List all threads - `deleteThread({ threadId })` - Delete a thread - `addToolResult({ threadId, toolCallId, result })` - Add result for async tool - `addToolError({ threadId, toolCallId, error })` - Add error for async tool **Options:** ```ts type AgentApiOptions = { authorizationCallback?: (ctx: QueryCtx | MutationCtx | ActionCtx, threadId: string) => Promise | void; workpoolEnqueueAction?: FunctionReference<"mutation", "internal">; toolExecutionWorkpoolEnqueueAction?: FunctionReference<"mutation", "internal">; onStatusChange?: FunctionReference<"mutation", "internal">; excludeSystemMessages?: boolean; }; ``` - `authorizationCallback` - Called before any operation that accesses an existing thread. Use it to verify the user has permission to access the thread. Throw an error to deny access. - `workpoolEnqueueAction` - Route agent and tool execution through a workpool for parallelism control - `toolExecutionWorkpoolEnqueueAction` - Override workpool for tool execution only (falls back to `workpoolEnqueueAction` if not set) - `onStatusChange` - Mutation callback invoked when thread status changes - `excludeSystemMessages` - Whether to exclude system messages from message lists (defaults to `true`) **Protected endpoints:** `sendMessage`, `resumeThread`, `stopThread`, `getThread`, `listMessages`, `deleteThread` **Example with ownership check:** ```ts defineAgentApi(components.durableAgents, internal.chat.chatAgentHandler, { authorizationCallback: async (ctx, threadId) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) throw new Error("Unauthorized"); // Query your threads table to verify ownership const thread = await ctx.runQuery(api.threads.getOwner, { threadId }); if (thread?.userId !== identity.subject) { throw new Error("Access denied"); } }, }); ``` #### `defineInternalAgentApi(component, streamHandler, options?)` Same as `defineAgentApi` but creates internal functions that can only be called from other Convex functions. Use this when you want to add authentication, authorization, or other business logic before calling agent functions. #### `streamHandlerAction(component, options)` Creates the stream handler action: ```ts streamHandlerAction(component, { model: languageModel, // AI SDK language model system?: string, // System prompt tools?: Record, saveStreamDeltas?: boolean | StreamingOptions, transformMessages?: (messages) => messages, // ... other AI SDK streamText options }); ``` #### `createActionTool(options)` Creates a sync tool that returns results directly: ```ts createActionTool({ description: string, args: ZodSchema, handler: FunctionReference<"action">, retry?: true | { enabled: true, maxAttempts?: number, // default 3 backoff?: { strategy?: "fixed", delayMs: number, jitter?: boolean, } | { strategy: "exponential", initialDelayMs: number, multiplier?: number, // default 2 maxDelayMs?: number, jitter?: boolean, }, shouldRetryError?: FunctionReference<"action">, }, }); ``` Pass `retry: true` for defaults (3 attempts, exponential backoff) or provide a full config object. The optional `shouldRetryError` action receives `{ threadId, toolCallId, toolName, args, error, attempt, maxAttempts }` and should return a truthy value to retry or falsy to fail immediately. #### `createAsyncTool(options)` Creates an async tool where results are provided later: ```ts createAsyncTool({ description: string, args: ZodSchema, callback: FunctionReference<"action">, }); ``` ### React Hooks #### `useAgentChat(options)` All-in-one hook for chat functionality that combines thread state with mutations: ```ts const { messages, // UIMessageWithConvexMetadata[] thread, // ThreadDoc | null status, // ThreadStatus isLoading, // boolean isRunning, // boolean isComplete, // boolean isFailed, // boolean isStopped, // boolean sendMessage, // (prompt: string) => Promise stop, // () => Promise resume, // (prompt?: string) => Promise } = useAgentChat({ listMessages: api.chat.listMessages, streamUpdates: api.chat.streamUpdates, getThread: api.chat.getThread, sendMessage: api.chat.sendMessage, stopThread: api.chat.stopThread, resumeThread: api.chat.resumeThread, threadId, }); // Send a message (threadId is automatically included) await sendMessage("Hello!"); // Stop the agent await stop(); // Resume after stopping or failure await resume(); ``` #### `useThreadMessages(options)` Lower-level hook for thread messages with streaming support (use `useAgentChat` for most cases): ```ts const { messages, // UIMessageWithConvexMetadata[] thread, // ThreadDoc | null status, // ThreadStatus isLoading, // boolean isRunning, // boolean isComplete, // boolean isFailed, // boolean isStopped, // boolean } = useThreadMessages({ messagesQuery: api.chat.listMessages, streamingMessageUpdatesQuery: api.chat.streamUpdates [README truncated for prompt length]