# Claude Agent SDK server

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

- Registry name: `claude-agent-server` (`@candor/claude-agent-server`)
- Tier: Free (MIT). No licence key needed.
- Docs: https://candor.themethodic.com/docs/claude-agent-server

## Install

```bash
npx candor-ui add claude-agent-server
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/claude-agent-server`.
It brings in: chat, claude-agent-sdk.
npm packages: @anthropic-ai/claude-agent-sdk@^0.3.318.

## Functions

### memoryApprovalStore

```ts
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

```ts
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

```ts
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

```ts
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

```ts
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

```ts
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

```ts
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

```ts
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

```ts
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

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

```ts
{ 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

```ts
{ /** 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.

```ts
readonly PermissionMode[] | ((mode: PermissionMode) => boolean)
```

### RunClaudeAgent

```ts
(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

```ts
{ /** 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 
```

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider` (fixed strings) and `TermsProvider` (nouns); entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
