AI Module Deep Dive
Read Core Mental Model first if you are new to RED’s AI module. This page is the deeper reference for engineers who need to understand how the engine behaves.
What the Module Provides
Section titled “What the Module Provides”The AI module turns chat into persisted application state:
aiThreadsstore conversations.aiMessagesandaiMessagePartsstore the UI transcript.aiRunsstore active run state.aiCheckpointPartsstore provider-shaped model history.tasksandtaskEventsstore tool-backed work.- Boundaries provide shared workspace memory, members, and default agent selection.
The browser subscribes through Convex reactive queries. The engine writes rows as tokens, tool calls, tool results, task progress, and errors arrive.
Engine Flow
Section titled “Engine Flow”sendMessage -> append user message -> create or wake aiRun -> schedule driveRun
driveRun -> prepareRun: queued -> running -> resolve agent by agentKey -> resolve agent context through resolveContext -> load checkpoint messages -> stream the ToolLoopAgent -> drain stream into aiMessageParts -> write new checkpoint parts -> complete, requeue, or suspend for async tasks
pollWaitingTasks -> read taskEvents since the last cursor -> publish progress as status parts -> settle terminal tasks -> requeue driveRun when neededThe core engine is intentionally agent-agnostic. It knows about threads, runs, checkpoints, boundaries, tasks, and messages. Product-specific IDs flow through agent context.
Two Storage Planes
Section titled “Two Storage Planes”RED keeps UI transcript and model checkpoint separate.
UI transcript
aiMessagesaiMessageParts
This is what the frontend renders. It can include token chunks, tool calls, tool results, status parts, and errors.
Provider checkpoint
aiCheckpointPartsaiRuns.checkpointIndex
This is what the next driveRun rehydrates into model messages. Async settlements rewrite sentinel rows in aiCheckpointParts, not in aiRuns.
That separation lets RED display live progress while keeping the model’s next turn clean.
Agent Definitions
Section titled “Agent Definitions”Agents live in packages/backend/src/convex/modules/ai/agents/.
An agent declares:
keyinstructionsmodelDefaulttools- optional
resolveContext - optional
stopWhen - optional
composeUserAttachments
The default assistant is assistantAgent. It resolves the brief bound to the current thread and exposes it to tools as ToolContext.extras.briefId.
Tool Context
Section titled “Tool Context”Tool bodies receive the AI SDK execution options. RED’s context is in options.experimental_context:
const toolContext = options.experimental_context as ToolContext<MyContext> | undefinedif (!toolContext) throw new Error("Missing tool context")ToolContext includes:
ctx— Convex action context.runIdthreadIdorganizationIdstartedByagentKeyattachmentsattachmentsByUserMessageIdextras— the agent-specific payload fromresolveContext.
Feature-specific values belong in extras. Do not add product fields to the core engine.
Tool Modes
Section titled “Tool Modes”RED has three helper APIs and four common modes.
Direct Sync
Section titled “Direct Sync”defineSyncTool() wraps the AI SDK tool helper and adds a per-tool timeout. Use it for fast work with no task row.
Sync Internal Task
Section titled “Sync Internal Task”defineSyncInternalTaskTool() runs an internal task node inline and optionally persists task history. The node must be internal and set inlineExecution: "sync".
Async Internal Task
Section titled “Async Internal Task”defineAsyncTool() starts a task with startTaskForAi. If the node is internal, dispatchTask calls the node’s run(ctx, input, { task }).
The Brief export is an async internal task:
exportBrief -> startTaskForAi("tasks.briefs.export") -> briefExportNodeDefinition.kind = "internal" -> runBriefExport schedules progress events -> finalizeBriefExport writes the Markdown artifactIt returns blocking: false, so completion arrives later as a task_event.
Async External Task
Section titled “Async External Task”External nodes implement trigger(ctx, task, { handle, handleUrl }). The trigger calls a remote service and gives it handleUrl.
The remote service posts events to:
POST /api/tasks/{handle}/eventThe HTTP route validates the handle, appends the task event, and terminal events finalize the task.
Blocking and Non-Blocking Async Tools
Section titled “Blocking and Non-Blocking Async Tools”Async tools return a sentinel with:
taskIdssummarypollIntervalMsblocking
blocking: true means the agent needs the result before it can continue. The run waits in waiting_tasks. When the task settles, the checkpoint is rewritten with the real tool result and the run is queued again.
blocking: false means the agent can keep talking while work continues. The run moves to awaiting_background. When the task settles, RED creates a task_event message and queues the run so the agent can react.
This policy is independent of node kind. Internal tasks can be non-blocking, as the Brief export demonstrates.
Task Nodes
Section titled “Task Nodes”Task node definitions live under packages/backend/src/convex/modules/tasks/node_types/.
Internal nodes use run:
export const myNodeDefinition = { key: "tasks.example.my_node", name: "Example Node", description: "Does internal work.", kind: "internal" as const, scope: "member" as const, inputSchema, outputSchema, run,}External nodes use trigger:
export const myExternalNodeDefinition = { key: "tasks.example.remote", name: "Remote Node", description: "Delegates work to a remote service.", kind: "external" as const, scope: "member" as const, inputSchema, outputSchema, trigger,}Register built-in product nodes in packages/backend/src/convex/modules/tasks/node_types/registry.ts. The engine uses tasks_engine.ts to look up and dispatch those definitions.
Task Events
Section titled “Task Events”Supported event types:
startedprogressheartbeatsuccesserrorcancelledcustom
Progress events are rendered as status parts in the originating assistant bubble. Success and error events settle the async sentinel. For non-blocking tasks, terminal settlement also creates a task_event message.
Brief Assistant Reference
Section titled “Brief Assistant Reference”The included assistant uses these real files:
modules/ai/agents/assistant.agent.ts— instructions, tools,resolveContext, and attachment composition.modules/ai/tools/getBriefSections.tool.ts— reads current sections.modules/ai/tools/patchBrief.tool.ts— edits one section.modules/ai/tools/addBriefSection.tool.ts— creates a section.modules/ai/tools/exportBrief.tool.ts— starts the non-blocking Markdown export task.modules/tasks/node_types/brief_export_node.ts— internal node that emits progress and writes the artifact.modules/tasks/node_types/registry.ts— registers the Brief export node.http.ts— exposes the external task callback route for external nodes.
Add an Agent
Section titled “Add an Agent”- Add a file in
modules/ai/agents/. - Add the key to
packages/backend-contract/src/ai/agents.ts. - Register the agent in
modules/ai/agents/agents.registry.ts. - Create threads with that
agentKey.
Agent selection is locked at thread creation because the checkpoint stores tool names from that agent’s tool map.
Add a Tool
Section titled “Add a Tool”- Pick the right helper:
defineSyncTool,defineSyncInternalTaskTool, ordefineAsyncTool. - Read RED context from
options.experimental_context. - Keep org-scoped checks close to document reads and writes.
- For async work, start a task with
tasks_internal.startTaskForAi.
Add a Task Node
Section titled “Add a Task Node”- Add a node file under
modules/tasks/node_types/. - Export input and output schemas.
- Export an internal
runor externaltrigger. - Register it in
node_types/registry.ts. - Run
bunx convex dev --onceafter backend changes.
Operational Notes
Section titled “Operational Notes”aiRuns.pendingAsyncis the poll state for async work.sentinelResultstracks checkpoint tool-call IDs that need settlement.aiRuns.checkpointIndexis the commit marker for visible checkpoint parts.activeAssistantMessageIdis a progress anchor, not a lock.task_eventmessages re-enter the model as user messages on the next turn.
For implementation recipes, use Building AI Features.