AI Agent Runs
Run Claude Code agents against durable run records, an orchestration layer with exactly-once finalization, epoch-fenced worker leases, and a KV-backed resumable stream so in-flight agent runs survive client reconnects.
The @questpie/ai module is a run-orchestration layer for executing Claude Code agents inside a QUESTPIE app. It does not try to be a chat SDK, it owns the hard parts of running long, streaming, resumable agent turns reliably: a worker fleet with epoch-fenced leases, an exactly-once run finalizer, and a KV-backed resumable UI-message stream so a run keeps streaming even if the browser reconnects mid-turn.
Headless-first, and partly in-flight
This package is server/worker-first: everything is driven by module + worker code, and an admin UI can sit on top. It is also younger than the core framework, so some of its surface is still stubbed. This page documents what actually works today and calls out the parked pieces inline. Concretely, as of @questpie/ai@3.1.0:
- The real execution path is the in-process embedded worker (
startAIWorker). The HTTP worker fleet (enrollment + poll routes) exists but finalize-over-HTTP is parked (HITL follow-up), so a decoupled HTTP fleet is not complete. aiConfig()/aiPlugin/ aconfig/ai.tsfile and theonBeforeRun/onAfterCompletehooks are defined but not wired to anything yet, treat them as a placeholder.@questpie/ai/clientcurrently exports nothing (the relay-based streaming components were removed in the chat-v7 cutover). Client streaming is the server-side resumable sink plus an app-owned SSE tail.- Only the
claude-coderuntime is supported.
None of the parked surface is documented below as if it works.
What it does
- A worker fleet with leases. Register
aiModuleand you get theai_workerscollection, worker enrollment/register/poll/heartbeat/deregister routes, a serviceworkerManager, and a cron jobai-worker-timeout(every 5 min) that reaps stale leases and marks dead workers offline. - Exactly-once finalization. A run is finalized once and only once via an epoch-fenced compare-and-set (
finalizedAt IS NULL∧ matching lease epoch ∧ non-terminal status). The finalizer seals the stream, writes the terminal status/summary/tokens, and fires side effects a single time even if two workers race. - Resumable streaming. Agent output is written into a KV-backed
ResumableUIMessageStore(keysrs:{id}:*, 1h TTL). Your app serves an SSE tail off it; a browser that drops reconnects withLast-Event-ID/?offsetand keeps reading, with agap→expiredfallback to the persisted transcript. - A Harness execution core.
createHarnessAgentwraps@ai-sdk/harness+createClaudeCode()in a local-host sandbox;toUIMessagesbridges the harness result stream into AI-SDKUIMessagechunks (surfacing real error text, not masking it).
You own the run record
@questpie/ai does not ship the execution record. Your app defines a run_links collection (the single row per run, with fields like kind, runtime, status, instructions, activeStreamId, producerLease, harnessResumeState, uiMessages, finalizedAt, retryPolicy). The worker, finalizer, and reaper all operate on the collections.run_links injected through the app context. See the tanstack-autopilot app for a complete run_links definition.
Quick start
1. Register the module
import { adminModule } from "@questpie/admin/modules/admin";
import { aiModule } from "@questpie/ai/modules/ai";
export default [adminModule, aiModule] as const;Then run codegen:
bun questpie generateThe module contributes the ai_workers collection (admin-hidden), the workerManager service, the enrollment/register/poll/heartbeat/deregister routes, and the ai-worker-timeout cron.
2. Define your run_links collection
This is the durable execution record your worker claims and finalizes. At minimum it needs status, runtime, instructions, activeStreamId, producerLease, finalizedAt, and retryPolicy. Model it as a normal collection under collections/.
3. Run an embedded worker
The worker is a separate process with a resolved system context (it reads ctx.services.workerManager, ctx.collections, ctx.kv). Point it at your Claude Code runtime:
import { createContext } from "#questpie";
import { startAIWorker } from "@questpie/ai/worker";
const ctx = await createContext({ accessMode: "system" });
await startAIWorker(ctx, {
runtimes: [{ runtime: "claude-code" }],
maxConcurrentRuns: 1,
pollIntervalMs: 1000,
// Personal-machine auth: let the harness bridge read ~/.claude
sandbox: { passthroughHomeForAuth: true },
mcpServers: [
{
name: "questpie",
command: process.execPath,
args: ["--bun", "run", "./src/mcp-entry.ts"],
env: {},
},
],
});The worker registers itself (ai_workers row), then poll-loops heartbeat → claimRun (an id-scoped CAS pending → claimed that bumps producerLease.epoch) → execute (streaming into the KV sink) → the one finalizeRun.
A decoupled worker needs shared KV
The HTTP SSE tail reads the same resumable stream the worker writes. If the worker runs in a different process from the web server, they must share a Redis KV, an in-process MemoryKV sink is invisible to the other process. Configure a Redis KV adapter before splitting the worker out.
4. Serve the resumable stream tail
On the server, drive an SSE response off the resumable store and finalize on failure:
import {
createQuestpieResumableStreamStore,
finalizeRun,
type FinalizeRunDeps,
} from "@questpie/ai/harness-core";
const store = createQuestpieResumableStreamStore({ kv });
// Read from an offset to drive the SSE tail; resume via Last-Event-ID / ?offset.
for await (const chunk of store.readFrom(activeStreamId, offset)) {
// write chunk to the SSE response
}
// Fail a run whose worker lease expired (idempotent, epoch-fenced):
await finalizeRun(
{ collections, streamStore: store, workflows } satisfies FinalizeRunDeps,
{ runId, kind, terminal: "failed", epoch, error: "worker lease expired", activeStreamId },
);Public API
| Import | Key exports |
|---|---|
@questpie/ai/modules/ai | aiModule |
@questpie/ai/worker | startAIWorker, EmbeddedWorkerConfig, HarnessRuntime |
@questpie/ai/harness-core | createHarnessAgent, resumeOrCreateSession, streamTurn, toUIMessages, ResumableUIMessageStore, createQuestpieResumableStreamStore, finalizeRun, reapExpiredRunLinks |
@questpie/ai | aiConfig, aiPlugin, contract types (AiRunStatus, AgentRuntimeRunRequest, …), see the warning about wiring |
startAIWorker(ctx, config) returns { stop(), workerId }. EmbeddedWorkerConfig takes runtimes, maxConcurrentRuns?, pollIntervalMs?, sandbox?, mcpServers?, resolveSkills?(run), workerDir?, name?.
createHarnessAgent({ runtime, workRoot, instructions?, skills?, tools?, permissionMode?, mcpServers?, sandbox? }) throws for any runtime other than claude-code.
How a run flows
- Your app inserts a
run_linksrow withstatus: "pending". - A worker's poll loop
claimRuns it (CASpending→claimed, epoch bumped). executeRun→createHarnessAgentstreamstoUIMessageschunks into the resumable KV sink underactiveStreamId, heartbeating under the lease epoch and pollingrun_links.statusfor cancellation.- The single
finalizeRunlatch seals the stream, writes terminalstatus/summary/tokens/uiMessages, and (forkind: "task"/"chat") writes knowledge artifacts + assistant messages, exactly once. reapExpiredRunLinks(the cron, plus inline on each tail read) requeues expired leases whenretryPolicy: "auto"(bump epoch →pending) or fails them otherwise, and marks stale workers offline.
Limitations
claude-codeonly. No other runtime is implemented.- The bundled sandbox is not isolation.
@questpie/airuns the agent through its own local-host sandbox (createLocalHostSandbox, a hostbash -lcspawner that isolates HOME/XDG and filters secret-looking env vars). WithpassthroughHomeForAuth: true(needed to reuse~/.claudecredentials) that HOME isolation is deliberately relaxed, and the worker runspermissionMode: "allow-all". This is a different thing from@questpie/sandbox(the Deno code-execution engine), do not confuse them. - No live cross-turn attach. Resume is replay/rerun against the persisted per-session HOME;
harnessResumeStateis written once at end-of-turn because the bridge session is destroyed when the turn's job ends. - HTTP worker fleet is parked. Real usage is the in-process embedded worker with in-process
finalizeRun. - Postgres-coupled epoch fence. The exactly-once CAS uses a raw JSONB predicate over the double-encoded
producerLeasecolumn.
Overview
External-facing modules that expose your QUESTPIE app to tools and API consumers through MCP and OpenAPI.
MCP
Add the mcpModule and your QUESTPIE app exposes a Model Context Protocol server, collections, globals, and annotated routes become tools an AI client can call, gated by your existing access rules.