Skip to content

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.

The AI module turns chat into persisted application state:

  • aiThreads store conversations.
  • aiMessages and aiMessageParts store the UI transcript.
  • aiRuns store active run state.
  • aiCheckpointParts store provider-shaped model history.
  • tasks and taskEvents store 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.

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 needed

The core engine is intentionally agent-agnostic. It knows about threads, runs, checkpoints, boundaries, tasks, and messages. Product-specific IDs flow through agent context.

RED keeps UI transcript and model checkpoint separate.

UI transcript

  • aiMessages
  • aiMessageParts

This is what the frontend renders. It can include token chunks, tool calls, tool results, status parts, and errors.

Provider checkpoint

  • aiCheckpointParts
  • aiRuns.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.

Agents live in packages/backend/src/convex/modules/ai/agents/.

An agent declares:

  • key
  • instructions
  • modelDefault
  • tools
  • 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 bodies receive the AI SDK execution options. RED’s context is in options.experimental_context:

const toolContext = options.experimental_context as ToolContext<MyContext> | undefined
if (!toolContext) throw new Error("Missing tool context")

ToolContext includes:

  • ctx — Convex action context.
  • runId
  • threadId
  • organizationId
  • startedBy
  • agentKey
  • attachments
  • attachmentsByUserMessageId
  • extras — the agent-specific payload from resolveContext.

Feature-specific values belong in extras. Do not add product fields to the core engine.

RED has three helper APIs and four common modes.

defineSyncTool() wraps the AI SDK tool helper and adds a per-tool timeout. Use it for fast work with no task row.

defineSyncInternalTaskTool() runs an internal task node inline and optionally persists task history. The node must be internal and set inlineExecution: "sync".

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 artifact

It returns blocking: false, so completion arrives later as a task_event.

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}/event

The HTTP route validates the handle, appends the task event, and terminal events finalize the task.

Async tools return a sentinel with:

  • taskIds
  • summary
  • pollIntervalMs
  • blocking

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 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.

Supported event types:

  • started
  • progress
  • heartbeat
  • success
  • error
  • cancelled
  • custom

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.

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.
  1. Add a file in modules/ai/agents/.
  2. Add the key to packages/backend-contract/src/ai/agents.ts.
  3. Register the agent in modules/ai/agents/agents.registry.ts.
  4. Create threads with that agentKey.

Agent selection is locked at thread creation because the checkpoint stores tool names from that agent’s tool map.

  1. Pick the right helper: defineSyncTool, defineSyncInternalTaskTool, or defineAsyncTool.
  2. Read RED context from options.experimental_context.
  3. Keep org-scoped checks close to document reads and writes.
  4. For async work, start a task with tasks_internal.startTaskForAi.
  1. Add a node file under modules/tasks/node_types/.
  2. Export input and output schemas.
  3. Export an internal run or external trigger.
  4. Register it in node_types/registry.ts.
  5. Run bunx convex dev --once after backend changes.
  • aiRuns.pendingAsync is the poll state for async work.
  • sentinelResults tracks checkpoint tool-call IDs that need settlement.
  • aiRuns.checkpointIndex is the commit marker for visible checkpoint parts.
  • activeAssistantMessageId is a progress anchor, not a lock.
  • task_event messages re-enter the model as user messages on the next turn.

For implementation recipes, use Building AI Features.