Claude Agent SDK server

Free · MIT

The default ApprovalStore: this process's memory. Answered and closed requests are forgotten after keepMs (10 minutes), so it doesn't grow without bound.

Markdown

Preview

It's types and helpers, listed below.

Install

With the Candor CLI

First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/claude-agent-server

Brings in: chat, claude-agent-sdk · npm: @anthropic-ai/claude-agent-sdk@^0.3.318

Functions

memoryApprovalStore({ keepMs = 10 * 60_000 }: { keepMs?: number | undefined } = {})

The default ApprovalStore: this process's memory. Answered and closed requests are forgotten after keepMs (10 minutes), so it doesn't grow without bound.

waitByPolling(get: (id: string) => Promise<StoredApproval | undefined>, id: string, signal: AbortSignal, intervalMs = 1000)

A wait for stores that can't push: reads the request every intervalMs (1s) until it's answered or closed.

claudeCanUseTool({ approvals = defaultApprovals, emit, owner, runId, mode, track, editable, approvalTimeoutMs }: ClaudeCanUseToolOptions)

A canUseTool that keeps each request in an ApprovalStore and waits for its answer from wherever it's given: your route, a queue, a workflow signal (approvals.answer(id, { approved: true })). Use it on its own when your agent runs somewhere other than streamClaudeAgent. When the SDK stops waiting, the request is closed, so a late answer is refused rather than lost.

memoryRunStore({ maxRuns = 100, maxEvents = 5000 }: { maxRuns?: number | undefined; maxEvents?: number | undefined } = {})

The default ClaudeRunStore: this process's memory. It keeps at most maxRuns finished runs (100), forgetting the oldest finished one first; a run still going is never forgotten. Each run keeps at most maxEvents events (5,000): past that, partial-text events are no longer kept (the finished message still is), so a re-attached browser sees text arrive a message at a time instead of as it streams.

streamClaudeAgent(run: RunClaudeAgent, { signal, owner, describeError, onMessage, onEvent, approvals = defaultApprovals, runs, runId = crypto.randomUUID(), detach = false, prompt, editable, approvalTimeoutMs, modes }: StreamClaudeAgentOptions = {})

Run an agent and stream it to useClaudeAgent.

// app/api/agent/route.ts export async function POST(request: Request) { const { prompt, sessionId } = await request.json() return streamClaudeAgent( ({ canUseTool, abortController }) => query({ prompt, options: { canUseTool, abortController, resume: sessionId, includePartialMessages: true } }), { signal: request.signal, owner: userId, onMessage: (m) => m.type === "result" && recordCost(m.total_cost_usd) } ) }

attachClaudeAgentRun(runs: ClaudeRunStore, runId: string, { from = 0, owner, signal, pollMs = 500 }: { from?: number | undefined; owner?: string | undefined; signal?: AbortSignal | undefined; pollMs?: number | undefined } = {})

Stream a run's events again, from index from, then live until it ends: for a browser re-attaching after a reload. 404 when the store doesn't know the run, 403 when owner isn't who started it.

// app/api/agent/[runId]/route.ts export async function GET(request: Request, { params }) { const from = Number(new URL(request.url).searchParams.get("from") ?? 0) return attachClaudeAgentRun(runs, (await params).runId, { from, owner: userId, signal: request.signal }) }

stopClaudeAgentRun(runId: string, owner?: string)

Stop a run this process is running. False when it isn't running here, or owner didn't start it.

setClaudeAgentPermissionMode(runId: string, mode: PermissionMode, owner?: string, { allowed }: { allowed?: ModePolicy | undefined } = {})

Change a running agent's permission mode (the SDK's setPermissionMode), and tell the browser. Only for a query in streaming input mode, as the SDK requires. False when the run isn't here, owner didn't start it, the mode isn't allowed, or the SDK refused. The route is where policy is enforced: a mode PermissionMode shows as locked must be left out of allowed (or the run's modes) too, since anyone can post to the route directly.

answerClaudeApproval(approvals: ApprovalStore, id: string, answer: ClaudeApprovalAnswer, { owner }: { owner?: string | undefined } = {})

Check an answer from the browser against the request it answers, and record it. False when there's no such request, it's already settled, owner isn't the run's owner, or the answer doesn't fit the request (an edited input that isn't an object, answers to a request that isn't a question, a suggestion that wasn't offered).

// app/api/agent/approve/route.ts export async function POST(request: Request) { const { id, ...answer } = await request.json() return Response.json({ ok: await answerClaudeApproval(approvals, id, answer, { owner: userId }) }) }

answerClaudeAgentApproval(id: string, approved: boolean, reason?: string, owner?: string)

Answer a waiting permission request in the default, in-memory store. Kept from 1.4; answerClaudeApproval takes a store and the full answer (an edited input, answers to questions, "always allow"). Returns false when there's no such request, or owner doesn't match the run's owner.

Types

StoredApproval
A permission request as a store keeps it.
{ id: string /** The run it belongs to, when the run has an id. */ runId?: string | undefined /** Who may answer it; checked by answerClaudeApproval. */ owner?: string | undefined request: ClaudeApproval createdAt: string /** Set once answered. Recorded, not necessarily used: a run that stopped at the same moment still denies the tool. */ answer?: ClaudeApprovalAnswer & { at: string } | undefined
ClaudeCanUseToolOptions
{ /** Where requests wait. Defaults to this process's memory. */ approvals?: ApprovalStore | undefined /** Sends each request, and its closing, to whoever shows it (the browser, your event log). */ emit: (event: AgentEvent) => void /** Who may answer. */ owner?: string | undefined /** The run's id, kept with each request. */ runId?: string | undefined /** The run's permission mode now, recorded on
ModePolicy
Which permission modes a run may be switched to: a list, or a test.
readonly PermissionMode[] | ((mode: PermissionMode) => boolean)
RunClaudeAgent
(tools: { /** Pass to query()'s `canUseTool`, so permission requests reach the person. */ canUseTool: CanUseTool /** Pass to query()'s `abortController`, so stopping in the browser stops the agent. */ abortController: AbortController /** * Send a typed card to the browser, e.g. from a tool handler: `emit({ type: "candor_card", card: "diff", props, summary })`. * It renders through the app's card r
StreamClaudeAgentOptions
{ /** The incoming request's signal (`request.signal`): the agent stops when the browser goes away, unless `detach`. */ signal?: AbortSignal | undefined /** Who may answer this run's permission requests, e.g. the signed-in user's id. Checked when they're answered. */ owner?: string | undefined /** The sentence shown when the run fails. Defaults to a generic one, so server errors don't leak to the