AI jobs
Streaming replies, background builds, model sizes and notes.
Every piece of AI work in a space is an AI job: a chat reply, a bot's thought, a request an outside AI hands over through MCP. Jobs are general, so anything that needs Claude with tools runs on the same engine (code: convex/aiJobs.ts, convex/lib/ai/).
What people see #
- Replies stream in as the AI writes them. The server writes the growing text a few times a second (every 250 ms by default), not per word, so phones redraw little and stay cool.
- A progress line under the reply says what the AI is doing ("Checking the change…", "Searching the web…", or the model's own short note between tool calls).
- Stop replaces Send while the AI works. It stops between words; proposals already made stay.
- Messages wait in line. A second message sent while the AI works shows "Waiting for the AI…" and runs next.
- A push when a long build finishes (over 30 seconds) while the person is away, unless
/ai/notifyis false. It opens the AI chat.
How a job runs #
start → queued | running → one model turn per background step → done | failed | cancelled
- Each model turn is its own scheduled action, so a long build never hits the action time limit, and Stop works between turns as well as between words.
- The job keeps its conversation append-only (thinking blocks included) and snapshots the blueprint and notes at its first turn, so every turn sends the same prefix and the stable build guide stays cached.
- Jobs in one lane run one at a time: the space's chat is one lane, each bot is its own.
- A cron ends jobs whose step stopped answering for 12 minutes (a crash or deploy mid-turn).
- Tokens per response are counted by the AI limits kit (
_addTokens), and limits are checked before a job starts (takeAiTurn). See AI-LIMITS.md.
Model sizes #
Spaces never name a model; they pick a size at /ai/model:
| Setting | What runs |
|---|---|
auto (default) | Small edits and short questions go to the fast model; building, anything new, web research and long asks go to the careful one. |
small | Always the fast model (claude-haiku-5-5), low effort. |
large | Always the careful model (claude-opus-5-5), medium effort. |
If the fast model gets stuck (it declines, runs out of room, or keeps making invalid changes), the job starts over once on the careful model. The chat block can also ask for a size per message (ai.chat prop model).
/ai settings #
All optional, changed like any setting (HQ, propose_change, MCP):
{ "ai": { "model": "auto", "stream": true, "remember": false, "notify": true, "maxTurns": 10 } }
stream: false shows the reply only when it's done (fewer redraws; the progress line still shows).remember: the AI keeps notes (below). The AI helper kit turns it on.maxTurns: 1 to 20 model turns per chat job.limits: belongs to the AI limits kit.
AI notes #
Short notes on how each person likes things built ("Prefers dark looks", "Calls members crew"). One list per person per space, at most 30, each under 200 characters. They go into the AI's instructions for that person's jobs. Notes are about style: anything that looks like an email, a phone number, a key or a password is refused, and in kids spaces the AI is told never to note anything about a child. People read, add and delete their notes on the ai.notes block.
Blocks #
| Block | What it is |
|---|---|
ai.chat | The builder chat itself (the dashboard's AI pane is this block). Props: placeholder, model, steps. |
ai.model | Model size, streaming and finish pushes (writes /ai). |
ai.notes | What the AI remembers about the viewer; add, delete, notes on or off. |
ai.jobs | The space's latest AI work with Stop for running jobs. Props: title, limit. |
Kit: AI helper (ai-helper, group Build) adds an owner-only "AI" page with ai.model, ai.notes and ai.jobs, and sets /ai/remember to true.
Tools (MCP and the in-app AI) #
| Tool | Scope | What it does |
|---|---|---|
list_notes | read | The caller's notes in this space. |
remember_note | propose | Keep a note (refused when notes are off). |
forget_note | propose | Remove a note by id or exact text. |
list_ai_jobs | read | Latest jobs: status, step, size, turns, tokens. MCP only. |
ask_space_ai | propose | Hand a request to the space's own AI, as if typed in the chat. MCP only. |
cancel_ai_job | propose | Stop a queued or running job. MCP only. |
The in-app AI gets the note tools only where notes are on.
For engines (bots, automations) #
const turn = await ctx.runMutation(internal.kits.ailimits.limits.take, { workspaceId, userId: null, source: "bot" });
if (!turn.ok) return; // turn.limit.message says why
const jobId = await ctx.runMutation(internal.aiJobs.start, {
workspaceId, kind: "bot", bot: "greeter", ref: runId,
prompt: "A new member joined: say hi in #lobby",
system: "You greet new members warmly, in one line.",
tools: ["post_message"], // narrows the tool list; approve tools are never given
tier: "small", maxTurns: 6, key: turn.key,
callback: "kits/bots/engine:_thought", // called with { ref, jobId, text, status, proposalIds }
});
internal.aiJobs.cancel({ jobId }) stops it; internal.aiJobs._get({ jobId }) reads it. Tools run through runTool with caller.for (the person) and caller.bot set.