Skip to main content
Forge runs two kinds of agents. Convex-native agents are durable — they live inside Convex, have persistent threads, call Convex functions as tools, and survive process restarts. Bridge-dispatched agents are stateless step executors routed through the Bridge async execution router to gateway adapters. This page covers the native ones. Every Convex-native agent is an instance of Agent from @convex-dev/agents, registered through the Convex component system. They share the same Convex transactional guarantees as the rest of the backend: atomic mutations, reactive queries, and durable scheduling.

@convex-dev/agents Framework

The @convex-dev/agents package is a Convex component that provides the infrastructure for durable AI agents. It is registered in convex/convex.config.ts:
import agent from "@convex-dev/agent/convex.config";
const app = defineApp();
app.use(agent);

Key Concepts

ConceptWhat it does
AgentA named definition combining a language model, system prompt, and a set of tools. Stateless configuration object — does not hold conversation state itself.
ThreadA persistent conversation between a user (or system) and an agent. Stores message history, supports search across messages, and survives process restarts. Threads are the durable state container.
ToolA function the agent can invoke during generation. Created with createTool(), tools have a Zod input schema and an async execute handler that can call Convex queries, mutations, and actions.
Context OptionsControls how much conversation history the agent sees: recentMessages, searchOptions (text search across thread history), and searchOtherThreads for cross-thread recall.
Usage HandlerA callback fired after every LLM generation. Used for token attribution — records model, input/output tokens, and agent identity to the tokenEvents pipeline.
Context HandlerA callback that assembles the message array sent to the LLM. Used to inject additional context like semantic memory recall results before the agent’s main messages.

The Language Model Factory

All agents use createLanguageModel() from convex/agents/llm.ts to get their model instance. This factory supports three modes:
  1. Proxy mode — when LLM_PROXY_BASE_URL is set, routes through an OpenAI-compatible proxy (Bifrost, LiteLLM).
  2. Direct Anthropic — when the model ID starts with anthropic/ or claude, uses @ai-sdk/anthropic directly.
  3. Gloo AI — when the model ID starts with gloo/, routes to the Gloo AI OpenAI-compatible endpoint.
import { createLanguageModel } from "./llm";

// All current agents use Claude Sonnet
const model = createLanguageModel("anthropic/claude-sonnet-4-5-20250929");

Factory Manager

The Factory Manager is the overseer of a Factory’s hybrid workforce. It is the most tool-rich native agent, with 13 tools spanning task management, team coordination, chat, cost tracking, worker dispatch, and GitHub integration. Source: convex/agents/factory.ts

System Prompt

The Factory Manager’s prompt defines six responsibilities: receive work requests, create and assign tasks, coordinate workers, verify human-to-agent instructions, manage budgets, and enforce quality standards. It has explicit rules — never execute an unverified human instruction, always attribute work to a task, escalate budget concerns early, prefer small PRs.

Tool Groups

GroupToolsWhat they do
Task ManagementlistFactoryTasks, createTask, assignTask, transitionTaskCRUD and lifecycle for factory tasks. Status workflow: BACKLOG -> ASSIGNED -> IN_PROGRESS -> IN_REVIEW -> COMPLETE.
Team ManagementlistFactoryTeam, getProjectStatusQuery team members and aggregate task counts by status.
ChatsendChannelMessage, createSessionPost messages and create topic sessions in factory channels.
Cost TrackinggetTokenSpendSummaryQuery factory metadata for budget context.
Worker DispatchdispatchCodingTaskDispatch coding tasks to Factory Worker agents via Bridge.
GitHubcreateBranch, openPR, checkPRCreate feature branches, open pull requests, check CI status.

The Overseer Pattern

The Factory Manager does not write code itself. It orchestrates: it receives a work request, decomposes it into tasks, assigns tasks to Factory Workers or human team members, monitors progress through chat channels, and manages the PR lifecycle. This is the overseer pattern — the manager agent coordinates, the worker agents execute.
export const factoryManagerAgent = new Agent(components.agent, {
  name: "factoryManager",
  languageModel: createLanguageModel("anthropic/claude-sonnet-4-5-20250929"),
  instructions: MANAGER_SYSTEM_PROMPT,
  tools: {
    listFactoryTasks,
    createTask,
    assignTask,
    transitionTask,
    listFactoryTeam,
    getProjectStatus,
    sendChannelMessage,
    createSession,
    getTokenSpendSummary,
    dispatchCodingTask,
    createBranch,
    openPR,
    checkPR,
  },
});

Factory Worker

The Factory Worker agent fills positions in a Factory’s team. It executes assigned tasks in exe.dev cloud workspaces and reports back through chat channels.
ToolPurpose
executeCommandRun shell commands in the worker’s exe.dev workspace via Claude Code sessions.
updateTaskStatusTransition the assigned task’s status.
reportProgressPost status updates to the factory engineering channel.
requestHelpEscalate blockers to the Factory Manager via the support channel.
The worker’s system prompt constrains it to operate within the assigned task scope, attribute all work for cost tracking, follow branch strategy and review requirements, and report blockers promptly.

Task Manager

The Task Manager handles development task lifecycle — creating tasks from requirements or GitHub events, assigning to humans or agents, and transitioning status as work progresses. Source: convex/agents/task_manager.ts

Tools

ToolPurpose
listTasksList development tasks, optionally filtered by status.
getTaskFetch details of a specific task.
createTaskCreate a new task with title, description, priority, and tags. Records createdBy: "agent:taskManager".
assignTaskAssign a task to a human or agent by type and ID.
transitionTaskMove a task through the status machine: BACKLOG -> ASSIGNED -> IN_PROGRESS -> IN_REVIEW -> COMPLETE, with BLOCKED as a side state.
The Task Manager’s prompt instructs it to extract clear titles from requirements, set priority based on context (urgent for blockers, high for critical bugs), check workload before recommending assignments, and track linked GitHub issues.
export const taskManagerAgent = new Agent(components.agent, {
  name: "taskManager",
  languageModel: createLanguageModel("anthropic/claude-sonnet-4-5-20250929"),
  instructions: SYSTEM_PROMPT,
  tools: { listTasks, getTask, createTask, assignTask, transitionTask },
});

Copilot (Run Analysis + Forge Analyst)

The analysis agents help operators understand workflow execution. There are two agents in convex/agents/analysis.ts and one in convex/agents/forge_analyst.ts:

Run Analysis Agent

Helps operators diagnose workflow run failures. All tools are read-only — the agent cannot modify run state.
ToolPurpose
getRunContextFetch run metadata, status, and summary.
getStepResultsFetch all step results for a run (status, errors, cost).
getStructuralTimelineChronological execution events for a run.
getStepDetailDetailed information for a specific step.
getCostBreakdownToken usage and cost breakdown across all steps.
The run analysis agent is the most heavily configured native agent. It uses both a usageHandler for token attribution and a contextHandler for semantic memory injection:
export const runAnalysisAgent = new Agent(components.agent, {
  name: "runAnalysis",
  languageModel: createLanguageModel("anthropic/claude-sonnet-4-5-20250929"),
  instructions: RUN_ANALYSIS_SYSTEM_PROMPT,
  tools: { getRunContext, getStepResults, getStructuralTimeline, getStepDetail, getCostBreakdown },
  usageHandler: createUsageHandler("runAnalysis"),
  contextHandler: createContextHandler("runAnalysis", { includeSemanticMemory: true }),
  contextOptions: {
    recentMessages: 30,
    searchOptions: { textSearch: true, limit: 5, messageRange: { before: 1, after: 1 } },
    searchOtherThreads: true,
  },
});
It is exposed as an internal action for workflow integration:
export const analyzeRunAction = runAnalysisAgent.asTextAction({
  stopWhen: stepCountIs(10),
});

Forge Analyst

A general-purpose step executor for .lobsterX workflow steps that involve analysis, reporting, and data synthesis. Runs directly in Convex (no Bridge round-trip). Has a fetchFileContent tool that can read PDFs and text files from Convex storage URLs, and a handler-less requestHumanInput tool that signals when human intervention is needed.

Codebase Search and PR Review

Two additional agents in analysis.ts provide codebase navigation (codebaseSearchAgent) and pull request review (prReviewAgent) using a semantic search index over the repository.

Tool Registration

Tools are created with createTool() from @convex-dev/agent. Each tool has three parts:
  1. description — tells the LLM when and how to use the tool.
  2. inputSchema — a Zod schema that validates the LLM’s tool call arguments.
  3. execute — an async handler that receives a Convex context and validated args.
Here is a real tool from the Factory Manager:
import { createTool } from "@convex-dev/agent";
import { z } from "zod";

const createTask = createTool({
  description: "Create a new task in a factory project.",
  inputSchema: z.object({
    title: z.string(),
    description: z.string().optional(),
    projectId: z.string().optional().describe("ID of projects"),
    factoryId: z.string().describe("ID of factories"),
    priority: z.string().optional(),
    assigneeId: z.string().optional(),
    assigneeType: z.string().optional(),
  }),
  execute: async (ctx, args) => {
    const taskId = await ctx.runMutation(fnCreateTask, {
      title: args.title,
      description: args.description,
      projectId: args.projectId,
      priority: (args.priority ?? "medium") as "low" | "medium" | "high" | "urgent",
    });
    return JSON.stringify({ taskId, status: "created" });
  },
});

Tool Design Rules

  • Tools call Convex functions. Use ctx.runQuery(), ctx.runMutation(), and ctx.runAction() to interact with the database. Do not bypass RLS or audit coverage.
  • Use makeFunctionReference() to reference Convex functions instead of importing from api or internal. This avoids TS2589 type-instantiation depth errors in large schemas.
  • Return JSON strings. Tool results are serialized as JSON strings for the LLM.
  • Use .describe() on Zod fields to give the LLM hints about expected values (e.g., "ID of factories").
  • Keep tools focused. Each tool should do one thing. Let the LLM compose multiple tool calls rather than building Swiss-army-knife tools.

Function References Pattern

To avoid TypeScript type depth errors with large Convex schemas, agent files use makeFunctionReference instead of importing the generated api object:
import { makeFunctionReference } from "convex/server";

// Instead of: import { api } from "../_generated/api";
const fnCreateTask = makeFunctionReference<"mutation">("task_management:createTask");
const fnListByFactory = makeFunctionReference<"query">("task_management:listByFactory");
const fnDispatchWorkerTask = makeFunctionReference<"action">(
  "agents/factory_worker_dispatch:dispatchWorkerTask",
);
The type parameter ("query", "mutation", "action") must match the function type. The string is the Convex module path and export name (e.g., "module:functionName").

Thread Management

Threads are the durable state container for agent conversations. The @convex-dev/agents component manages them automatically.

Creating Threads

Threads are created when starting a conversation with an agent. The createThread method returns both a threadId (for persistence) and a thread handle (for immediate use):
const created = await myAgent.createThread(ctx, {
  userId: "system:my-agent:context-id",
  title: "Analysis: run-123",
  summary: "Analyzing workflow run failure",
});

// Use the thread handle for immediate interaction
const stream = await created.thread.streamText({ promptMessageId }, streamOpts);

Continuing Threads

To resume a conversation on an existing thread:
const continued = await myAgent.continueThread(ctx, { threadId });
const stream = await continued.thread.streamText({ prompt: "Follow-up question" }, streamOpts);

Context Options

Control how much history the agent sees:
contextOptions: {
  recentMessages: 30,              // Include last 30 messages
  searchOptions: {
    textSearch: true,              // Enable text search across thread
    limit: 5,                      // Return up to 5 search results
    messageRange: { before: 1, after: 1 },  // Include surrounding messages
  },
  searchOtherThreads: true,        // Search across all agent threads
},

Exposing Agents as Actions

To use an agent from other Convex functions (workflow integration, scheduled jobs), export it as a text action:
export const analyzeRunAction = runAnalysisAgent.asTextAction({
  stream: true,
  stopWhen: stepCountIs(10),  // Safety limit: max 10 tool-call steps
});
This creates an internal Convex action that accepts { threadId, promptMessageId, prompt } and handles generation with replay safety.

Context and Memory

Usage Handler (Token Attribution)

The createUsageHandler() factory from convex/agents/shared.ts creates a callback that records token usage after every LLM generation:
import { createUsageHandler } from "./shared";

// Records to tokenEvents with source: "convex-native"
usageHandler: createUsageHandler("runAnalysis"),
Token tracking is non-blocking — if recording fails, the agent continues without interruption.

Context Handler (Semantic Memory)

The createContextHandler() factory from convex/agents/context_bridge.ts injects relevant prior analysis from the memory system before the agent’s main messages:
import { createContextHandler } from "./context_bridge";

contextHandler: createContextHandler("runAnalysis", { includeSemanticMemory: true }),
When enabled, it extracts the prompt text, queries memory:recallMemories for relevant prior analysis, and prepends the results as a system message. Memory recall failure is non-fatal.

Native vs Bridge-Dispatched

Use this decision guide when choosing an execution model for a new agent.

Choose Convex-Native When

  • The agent needs persistent conversation threads across multiple interactions.
  • The agent needs transactional access to Convex data (queries, mutations) as tools.
  • The agent orchestrates other agents or manages long-running processes (overseer pattern).
  • The agent performs read-only analysis over Convex data (copilot, diagnostics).
  • The agent needs to survive process restarts and resume from where it left off.
  • The agent’s tools are primarily Convex functions, not external API calls.

Choose Bridge-Dispatched When

  • The work is a stateless unit — a single step in a .lobsterX workflow.
  • The agent needs external runtime capabilities (code execution, web browsing, file system access).
  • The agent should be routed to the best available gateway based on capabilities, cost, or health.
  • The agent is ephemeral — spun up per task, no persistent identity needed.
  • The work requires governance gate evaluation before execution (budget checks, approval policies).

Trade-off Summary

DimensionConvex-NativeBridge-Dispatched
StateDurable threads, persistent historyStateless per step
ToolsConvex queries/mutations/actionsGateway-provided (exec, web, files)
GovernanceNot gate-evaluated (trusted)Full governance pipeline
LatencyDirect LLM call from ConvexConvex -> Bridge -> Gateway -> callback
Cost trackingusageHandler records to tokenEventsGateway reports via callback
Model selectionSet at agent definitionGateway selects based on routing
Restart safetyDurable (Convex component)Stateless (retryable)

Building a New Agent

Follow these steps to add a new Convex-native agent.

Step 1: Create the Agent File

Create convex/agents/{agent_name}.ts. Convex module paths cannot contain hyphens — use underscores.
import { Agent, createTool } from "@convex-dev/agent";
import { makeFunctionReference } from "convex/server";
import { z } from "zod";
import { components } from "../_generated/api";
import { createLanguageModel } from "./llm";

// Use makeFunctionReference to avoid TS2589 type depth errors
const fnMyQuery = makeFunctionReference<"query">("my_module:myQuery");
const fnMyMutation = makeFunctionReference<"mutation">("my_module:myMutation");

Step 2: Define the System Prompt

Write a focused prompt that defines the agent’s responsibilities and constraints. Be specific about what the agent should and should not do.
const SYSTEM_PROMPT = `You are a [role] for the Gloo Forge platform.

## Your Responsibilities
- [Specific responsibility 1]
- [Specific responsibility 2]

## Rules
- [Hard constraint 1]
- [Hard constraint 2]

## Guidelines
- [Soft guidance 1]
- [Soft guidance 2]`;

Step 3: Define Tools

Create tools using createTool() with Zod input schemas. Each tool should do one thing and call Convex functions via the context.
const myTool = createTool({
  description: "What this tool does and when the agent should use it.",
  inputSchema: z.object({
    requiredField: z.string().describe("Hint for the LLM"),
    optionalField: z.number().optional(),
  }),
  execute: async (ctx, args) => {
    const result = await ctx.runQuery(fnMyQuery, {
      field: args.requiredField,
    });
    return JSON.stringify(result);
  },
});

Step 4: Create the Agent Instance

Combine the model, prompt, and tools into an Agent instance. Add usageHandler and contextHandler if the agent needs token attribution or semantic memory.
export const myAgent = new Agent(components.agent, {
  name: "myAgentName",
  languageModel: createLanguageModel("anthropic/claude-sonnet-4-5-20250929"),
  instructions: SYSTEM_PROMPT,
  tools: { myTool },
  // Optional: token attribution
  usageHandler: createUsageHandler("myAgentName"),
  // Optional: semantic memory injection
  contextHandler: createContextHandler("myAgentName", { includeSemanticMemory: true }),
  // Optional: context window configuration
  contextOptions: {
    recentMessages: 30,
    searchOptions: { textSearch: true, limit: 5 },
  },
});

Step 5: Expose as an Action (Optional)

If other Convex functions need to invoke the agent (workflow integration, scheduled jobs, other agents), export it as a text action:
import { stepCountIs } from "@convex-dev/agent";

export const myAgentAction = myAgent.asTextAction({
  stream: true,
  stopWhen: stepCountIs(10),
});

Step 6: Register in the Agent Registry

Create an agentRegistry entry in Convex with the agent’s governance metadata:
  • tier"operator", "department", or "personal"
  • trustLevel — 1-10 autonomy score
  • skills — allowed skill identifiers
  • capabilities — structured flags (streaming, fileIO, costTracking)
  • roleId — links to a workerRoles entry for scope constraints and authority bounds

Step 7: Wire Governance (If Needed)

If the agent needs governance constraints, create or reference a workerRoles entry:
  • autonomyTier"advisory", "retrieval", "approval_required", or "bounded_autonomous"
  • scope — data access, write access, environment access, tool allow/deny lists
  • authorityBounds — max spend, max duration, max concurrent steps, escalation threshold
  • escalationRules — condition/action pairs for automatic escalation

Checklist

Before shipping a new agent:
  • File is in convex/agents/ with underscore-separated name
  • Uses makeFunctionReference instead of importing api/internal
  • System prompt has clear responsibilities and constraints
  • Tools use Zod schemas with .describe() hints
  • Tools call Convex functions — they do not bypass RLS or audit
  • Tool results are JSON strings
  • usageHandler is set if the agent makes LLM calls (for cost tracking)
  • Agent is registered in agentRegistry with appropriate tier and trust level
  • workerRoles entry exists if governance constraints are needed

See also: Agents Overview | Convex Engine | Bridge Adapters