Durable Agents
Durable agent execution
git clone https://github.com/bijectionhq/durable-agents.git
cd durable-agents
git checkout 6322213b09dc69d3c9c82ab465ca30fbcb7d2070Bijection repository access is required. Request access.
Description
Durable agent execution
Explore the package README for API concepts, configuration, and examples. Check the Bijection source for adaptation-specific setup.
Review the source and its dependencies before adopting this package. Source availability does not establish npm publication or runtime qualification.
Getting started
- Get the source at the revision above and read its package documentation.
- Check the required Bijection SDK version, dependencies, and external service credentials.
- Follow the package setup instructions and the guide to using components.
- Exercise the component in your deployment according to your project’s verification rules.
Open source
Maintained source: bijectionhq/durable-agents. Based on ziegfried/convex-durable-agents. License: Apache-2.0. Consult the repository for license terms, notices, and contributor attribution.
From the upstream publisher
A durable AI agents component for Convex with automatic retries, crash recovery, and tool execution.
Explore the demo ↗Upstream overview
From the upstream package documentation.
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
From the upstream package documentation.
- 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
From the upstream package documentation.
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 package FAQ
Is this component ready for production use?
The Durable Agents Component for Convex is currently in early development and not recommended for production use. The API is likely to change significantly before the first stable release, so expect breaking changes during development.
How does this differ from the official Convex Agents Component?
The Durable Agents Component focuses specifically on persistence and durability while deliberately omitting advanced features like context management, RAG, and rate limiting found in the official component. It exposes the full AI SDK v6 streamText API with added persistence and fault tolerance.
What AI SDK version does this component support?
The Durable Agents Component for Convex is built on top of AI SDK v6 and aims to expose its complete streamText API. This ensures compatibility with the latest AI SDK features while adding persistence and durable execution capabilities.
Can I control parallelism for multiple agents?
Yes, the Durable Agents Component supports optional integration with @convex-dev/workpool for parallelism control. This allows you to route agent and tool execution through a workpool to manage concurrent agent workflows effectively.
Use with agents and CLI
Give your coding agent the component’s source identity and setup guidance.
Help me integrate Durable Agents with Bijection.
Read: https://bijection.com/components/durable-agents/SKILL.md
Check SDK compatibility and dependency availability.
Follow this project's execution and verification rules.Upstream README.md
View repository ↗Documentation from ziegfried/convex-durable-agents, recorded October 23, 2026. APIs and setup may differ in the Bijection adaptation.
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 (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 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/workpoolfor parallelism control
Installation
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:
// 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:
// 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:
// 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:
// 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:
// 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:
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 (
<div>
{messages.map((msg) => (
<div key={msg.metadata?.key}>
<strong>{msg.role}:</strong> {msg.parts.map((p) => (p.type === "text" ? p.text : null))}
</div>
))}
{isRunning && <button onClick={() => stop()}>Stop</button>}
<input
onKeyPress={(e) => {
if (e.key === "Enter" && !isRunning) {
sendMessage(e.currentTarget.value);
e.currentTarget.value = "";
}
}}
disabled={isRunning}
/>
</div>
);
}
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 threadsendMessage({ threadId, prompt })- Send a message to a threadaddMessage({ threadId, msg })- Add a message to a thread without triggering the agentresumeThread({ threadId, prompt? })- Resume a stopped/failed threadstopThread({ threadId })- Stop a running threadgetThread({ threadId })- Get thread detailslistMessages({ threadId })- List messages in a threadstreamUpdates({ threadId, fromSeq? })- Get streaming message updates for real-time UIlistThreads({ limit? })- List all threadsdeleteThread({ threadId })- Delete a threadaddToolResult({ threadId, toolCallId, result })- Add result for async tooladdToolError({ threadId, toolCallId, error })- Add error for async tool
Options:
type AgentApiOptions = {
authorizationCallback?: (ctx: QueryCtx | MutationCtx | ActionCtx, threadId: string) => Promise<void> | 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 controltoolExecutionWorkpoolEnqueueAction- Override workpool for tool execution only (falls back toworkpoolEnqueueActionif not set)onStatusChange- Mutation callback invoked when thread status changesexcludeSystemMessages- Whether to exclude system messages from message lists (defaults totrue)
Protected endpoints: sendMessage, resumeThread, stopThread, getThread, listMessages, deleteThread
Example with ownership check:
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:
streamHandlerAction(component, {
model: languageModel, // AI SDK language model
system?: string, // System prompt
tools?: Record<string, DurableTool>,
saveStreamDeltas?: boolean | StreamingOptions,
transformMessages?: (messages) => messages,
// ... other AI SDK streamText options
});
createActionTool(options)
Creates a sync tool that returns results directly:
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:
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:
const {
messages, // UIMessageWithConvexMetadata[]
thread, // ThreadDoc | null
status, // ThreadStatus
isLoading, // boolean
isRunning, // boolean
isComplete, // boolean
isFailed, // boolean
isStopped, // boolean
sendMessage, // (prompt: string) => Promise<null>
stop, // () => Promise<null>
resume, // (prompt?: string) => Promise<null>
} = 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):
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]