Skip to content

Core Mental Model

If you want to customize RED, you only need one product mental model:

A user sends a message.
An agent decides what to do.
Tools read or change your product data.
Long work becomes a task.
Task events update the UI.
When a task finishes, the result can re-enter the conversation.

The Brief AI Assistant included in RED is the reference implementation of that loop.

In the demo, each thread is bound to one brief. The assistant can:

  • Read all sections with getBriefSections.
  • Edit a section with patchBrief.
  • Add a section with addBriefSection.
  • Start a Markdown export with exportBrief.

The assistant does not ask the model to invent a briefId. The agent resolves it every turn:

threadId
-> assistantAgent.resolveContext()
-> looks up the brief attached to the thread
-> returns { briefId }
-> every tool receives it as ToolContext.extras.briefId

That pattern is important. Product-specific context flows through resolveContext and ToolContext.extras, not through the engine.

Agent
Instructions, model choice, tools, and optional per-turn context. Example: assistantAgent.

Tool
A function the model can call. Tools are where your agent reads data, edits data, starts tasks, or calls services.

Thread
A conversation. Agent choice is locked when the thread is created because the stored checkpoint contains tool names from that agent.

Boundary
A shared workspace around threads. It can hold memory, members, and a default agent key.

Task node
A backend unit of work registered with the task engine. A node can be internal or external.

Task event
A progress, success, error, heartbeat, cancelled, or custom event attached to a task.

Callback handle
A random capability token for external tasks. Remote services post events to POST /api/tasks/{handle}/event.

RED has four practical tool modes.

Use defineSyncTool() for quick work that can finish inside the model step and does not need a task row.

Good for:

  • Reading a small record.
  • Formatting a quick answer.
  • Running short deterministic logic.

The tool body receives the AI SDK options object. RED’s engine context is inside options.experimental_context:

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

Use defineSyncInternalTaskTool() when the work is still fast, but you want task history and task events.

Good for:

  • Short mutations that should be visible in the task system.
  • Work that should share logic with a task node.
  • Audit-friendly internal operations.

The node must be an internal node with inlineExecution: "sync".

Use defineAsyncTool() with an internal task node when the work runs inside Convex but may take time or needs progress events.

The Brief export uses this pattern. It is an internal node:

exportBrief tool
-> startTaskForAi(nodeTypeKey: "tasks.briefs.export")
-> briefExportNodeDefinition.kind = "internal"
-> runBriefExport schedules progress events
-> final action writes the Markdown artifact

The tool intentionally returns blocking: false, so the chat remains usable while the export finishes. This is a product choice, not a requirement of internal tasks.

Use defineAsyncTool() with an external node when the real work happens outside Convex, such as a Python worker, AWS Lambda, Render job, or third-party API.

External nodes implement trigger(ctx, task, { handle, handleUrl }). The trigger sends handleUrl to the remote service. The remote service posts events back:

POST /api/tasks/{handle}/event
Content-Type: application/json
{
"type": "progress",
"payload": {
"percent": 50,
"message": "Rendering"
}
}

The HTTP endpoint stores events. The engine polls stored task events and forwards new progress to the UI.

sendMessage
-> writes the user message
-> creates or wakes an aiRun
-> schedules driveRun
driveRun
-> loads the thread checkpoint
-> resolves the agent and its context
-> streams model text and tool calls
-> writes message parts for the UI
-> suspends if async tasks are pending
pollWaitingTasks
-> reads new taskEvents
-> writes progress status parts
-> settles finished tasks
-> requeues the run when the agent should continue

Users see updates through Convex reactive queries. There is no separate WebSocket server to maintain for chat streaming.

Async tools return an async sentinel with a blocking flag.

Blocking means: the agent needs the result before it can answer. The run waits in waiting_tasks. When the task succeeds, the checkpoint is rewritten with the real tool result and the agent continues.

Non-blocking means: the task can finish later. The run moves to awaiting_background, the user can keep chatting, and task completion becomes a task_event message in a later turn.

The Brief export is non-blocking because the user can continue editing while the Markdown file is generated.

A task event stream makes long work visible:

started
progress: Preparing brief
progress: Serializing sections
progress: Rendering markdown
progress: Finalizing artifact
success: artifact metadata

The task engine stores those events in taskEvents. The AI engine polls for new events and writes UI status parts into the originating assistant message. For non-blocking completions, it also writes a task_event message so the agent can naturally say something like “Your export is ready.”

RED stores the run, messages, message parts, checkpoint parts, tasks, and task events in Convex. If a user closes the tab mid-run, the data is still there. When they return, reactive queries load the current state.

This is the main difference from a simple HTTP streaming endpoint. RED treats agent work as persisted application state, not as a browser connection.

To build your own AI feature, you usually add:

  1. An agent file in packages/backend/src/convex/modules/ai/agents/.
  2. Tool files in packages/backend/src/convex/modules/ai/tools/.
  3. Task nodes in packages/backend/src/convex/modules/tasks/node_types/ when work should be tracked or backgrounded.
  4. A frontend route or workspace UI that creates threads with your agent.

The core engine remains agent-agnostic. It knows about runs, threads, checkpoints, tasks, and boundaries; your product-specific IDs live in agent context and product tables.

Next: Building AI Features gives copy-paste recipes for each extension point.