@candor-ui/react 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,7 +13,8 @@ export function fromClaudeAgentEvents(events, options) {
13
13
  let usage;
14
14
  let lastCost = 0;
15
15
  let notice;
16
- const current = (id) => (reply ??= { id, parts: [], reasoning: [], tools: new Map(), live: new Map() });
16
+ let permissionMode;
17
+ const current = (id) => (reply ??= { id, parts: [], reasoning: [], tools: new Map(), live: new Map(), sources: new Map(), inputs: new Map() });
17
18
  const close = (last) => {
18
19
  if (reply)
19
20
  messages.push(render(reply, { ...options, model, last }));
@@ -43,11 +44,24 @@ export function fromClaudeAgentEvents(events, options) {
43
44
  const call = approval && reply?.tools.get(approval.toolUseId);
44
45
  if (call && call.status === "waiting") {
45
46
  call.status = e.approved ? "running" : "cancelled";
46
- if (!e.approved)
47
+ if (e.expired)
48
+ call.error = `Expired: ${e.reason ?? "nobody answered in time."}`;
49
+ else if (!e.approved)
47
50
  call.error = e.reason ? `Denied: ${e.reason}` : "Denied.";
51
+ // What ran is what the person allowed, so the card shows it, with what the agent proposed kept beside it.
52
+ if (e.approved && e.input !== undefined) {
53
+ call.approval = { by: "person", originalInput: call.input };
54
+ call.input = json(e.input);
55
+ reply.inputs.set(call.id, e.input);
56
+ }
57
+ else if (e.approved)
58
+ call.approval = { by: "person" };
48
59
  }
49
60
  break;
50
61
  }
62
+ case "candor_mode":
63
+ permissionMode = e.mode;
64
+ break;
51
65
  case "candor_card":
52
66
  current(`card-${messages.length}`).parts.push({ kind: "card", card: e.card, props: e.props, summary: e.summary });
53
67
  break;
@@ -59,6 +73,12 @@ export function fromClaudeAgentEvents(events, options) {
59
73
  if (e.subtype === "init") {
60
74
  sessionId = e.session_id;
61
75
  model = e.model;
76
+ if (e.permissionMode)
77
+ permissionMode = e.permissionMode;
78
+ }
79
+ else if (e.subtype === "status") {
80
+ if (e.permissionMode)
81
+ permissionMode = e.permissionMode;
62
82
  }
63
83
  else if (e.subtype === "permission_denied") {
64
84
  const call = reply?.tools.get(e.tool_use_id);
@@ -117,6 +137,9 @@ export function fromClaudeAgentEvents(events, options) {
117
137
  call.status = block.is_error ? "failed" : "succeeded";
118
138
  if (block.is_error)
119
139
  call.error = firstLine(output) ?? "The tool failed and didn’t say why.";
140
+ else if (options.sourcesFrom)
141
+ for (const found of options.sourcesFrom(call.tool, reply.inputs.get(call.id), output) ?? [])
142
+ addSource(reply, found);
120
143
  }
121
144
  break;
122
145
  }
@@ -146,12 +169,40 @@ export function fromClaudeAgentEvents(events, options) {
146
169
  sessionId,
147
170
  usage,
148
171
  notice: options.running ? notice : undefined,
172
+ permissionMode,
149
173
  };
150
174
  }
175
+ /** Adds a source once (by key) and returns its id. Markers count up in the order sources first appear. */
176
+ function addSource(r, source) {
177
+ const { key: own, ...rest } = source;
178
+ const key = own ?? `${rest.kind}:${rest.label}`;
179
+ const known = r.sources.get(key);
180
+ if (known)
181
+ return known.id;
182
+ const id = `${r.id}-s${r.sources.size + 1}`;
183
+ r.sources.set(key, { ...rest, id, marker: r.sources.size + 1 });
184
+ return id;
185
+ }
186
+ /** One of Claude's citations as a source: a web page by its address, a document or search result by its title. */
187
+ function citationSource(c) {
188
+ const snippet = c.cited_text?.trim() ? clip(c.cited_text.trim(), 280) : undefined;
189
+ if (c.url)
190
+ return { key: `web:${c.url}`, kind: "web", label: c.title || c.url, origin: host(c.url), snippet };
191
+ const title = c.document_title ?? c.title ?? c.source;
192
+ if (title)
193
+ return { key: `document:${title}`, kind: "document", label: title, snippet };
194
+ return undefined;
195
+ }
151
196
  function addBlock(r, block, options) {
152
197
  if (block.type === "text") {
153
- if (block.text.trim())
154
- r.parts.push({ kind: "text", text: block.text });
198
+ if (!block.text.trim())
199
+ return;
200
+ // Claude's citations point at what it read; the [n] comes from them, never from numbers in the text.
201
+ const cites = (block.citations ?? []).flatMap((c) => {
202
+ const source = citationSource(c);
203
+ return source ? [addSource(r, source)] : [];
204
+ });
205
+ r.parts.push({ kind: "text", text: block.text, cites: cites.length ? [...new Set(cites)] : undefined });
155
206
  }
156
207
  else if (block.type === "thinking") {
157
208
  // With the SDK's default display, thinking arrives empty; there's nothing to show then.
@@ -161,6 +212,7 @@ function addBlock(r, block, options) {
161
212
  else if (block.type === "tool_use" || block.type === "server_tool_use" || block.type === "mcp_tool_use") {
162
213
  const call = { id: block.id, tool: block.name, title: options.toolTitle?.(block.name, block.input) ?? block.name, status: "running", input: json(block.input) };
163
214
  r.tools.set(block.id, call);
215
+ r.inputs.set(block.id, block.input);
164
216
  r.parts.push({ kind: "tool", id: block.id });
165
217
  }
166
218
  else if ("tool_use_id" in block) {
@@ -174,6 +226,13 @@ function addBlock(r, block, options) {
174
226
  call.outputLanguage = "json";
175
227
  if (failed)
176
228
  call.error = `The tool failed: ${String(content.error_code)}.`;
229
+ // Web search results are sources the reply can stand on, whether or not the text cites them.
230
+ if (!failed && block.type === "web_search_tool_result" && Array.isArray(content)) {
231
+ for (const item of content) {
232
+ if (item.type === "web_search_result" && item.url)
233
+ addSource(r, { key: `web:${item.url}`, kind: "web", label: item.title || item.url, origin: host(item.url) });
234
+ }
235
+ }
177
236
  }
178
237
  }
179
238
  }
@@ -210,7 +269,7 @@ function render(r, o) {
210
269
  if (p.kind === "card")
211
270
  blocks.push({ kind: "card", card: p.card, props: p.props, summary: p.summary });
212
271
  else
213
- blocks.push(...markdownBlocks(p.text));
272
+ blocks.push(...withCitations(markdownBlocks(p.text), p.cites));
214
273
  }
215
274
  flush();
216
275
  const error = r.failure ?? r.result?.error;
@@ -222,6 +281,7 @@ function render(r, o) {
222
281
  status: open ? "streaming" : r.result?.ok ? "complete" : error ? "failed" : "interrupted",
223
282
  blocks,
224
283
  reasoning: reasoning.length ? reasoning : undefined,
284
+ sources: r.sources.size ? [...r.sources.values()] : undefined,
225
285
  attribution: r.result ? { model: o.model, cost: r.result.cost ? `$${r.result.cost.toFixed(r.result.cost < 0.01 ? 4 : 2)} (estimated)` : undefined } : undefined,
226
286
  };
227
287
  }
@@ -251,6 +311,84 @@ function resultText(content) {
251
311
  return content.map((c) => (c.type === "text" ? (c.text ?? "") : c.type ? `[${c.type}]` : "")).join("\n");
252
312
  }
253
313
  const firstLine = (text) => text.trim().split("\n")[0] || undefined;
314
+ const clip = (text, max) => (text.length > max ? `${text.slice(0, max - 1)}…` : text);
315
+ const host = (url) => {
316
+ try {
317
+ return new URL(url).hostname;
318
+ }
319
+ catch {
320
+ return undefined;
321
+ }
322
+ };
323
+ /** Puts a text block's [n] markers at its end: after the last paragraph's, heading's or list item's words. */
324
+ function withCitations(blocks, cites) {
325
+ if (!cites?.length)
326
+ return blocks;
327
+ const marks = cites.map((sourceId) => ({ kind: "citation", sourceId }));
328
+ const out = [...blocks];
329
+ for (let i = out.length - 1; i >= 0; i--) {
330
+ const b = out[i];
331
+ if (b.kind === "paragraph" || b.kind === "heading") {
332
+ out[i] = { ...b, nodes: [...b.nodes, ...marks] };
333
+ return out;
334
+ }
335
+ if (b.kind === "list" && b.items.length) {
336
+ out[i] = { ...b, items: [...b.items.slice(0, -1), [...b.items.at(-1), ...marks]] };
337
+ return out;
338
+ }
339
+ }
340
+ // Only code or plain text: the markers get a line of their own.
341
+ return [...out, { kind: "paragraph", nodes: marks }];
342
+ }
343
+ /**
344
+ * The questions in an AskUserQuestion call, ready for ClarifyingQuestions; undefined when the input isn't one.
345
+ * Option ids are their labels, which is what the SDK takes back.
346
+ */
347
+ export function claudeQuestions(input) {
348
+ const qs = input?.questions;
349
+ if (!Array.isArray(qs) || !qs.length)
350
+ return undefined;
351
+ const out = [];
352
+ for (const q of qs) {
353
+ if (typeof q?.question !== "string" || !Array.isArray(q.options))
354
+ return undefined;
355
+ out.push({
356
+ id: q.question,
357
+ kind: "choice",
358
+ prompt: q.question,
359
+ tag: typeof q.header === "string" && q.header ? q.header : undefined,
360
+ options: q.options.filter((o) => typeof o?.label === "string").map((o) => ({ id: o.label, label: o.label, detail: o.description || undefined })),
361
+ multiple: q.multiSelect === true,
362
+ other: true,
363
+ });
364
+ }
365
+ return out;
366
+ }
367
+ /**
368
+ * What to hand back to the SDK for a person's answer: the `PermissionResult` canUseTool resolves with. Pure, so it
369
+ * works wherever the answer lands (your own route, a queue, a workflow signal). Questions become AskUserQuestion's
370
+ * `answers`, keyed by question text, several choices joined with ", " and the person's own words used as they are.
371
+ * Only the suggestions the person picked are kept, and none when the request offers no "always allow".
372
+ */
373
+ export function claudePermissionResult(request, answer) {
374
+ if (!answer.approved) {
375
+ return { behavior: "deny", message: answer.reason ? `The user declined: ${answer.reason}` : "The user declined this action." };
376
+ }
377
+ const input = (request.input && typeof request.input === "object" ? request.input : {});
378
+ let updatedInput = answer.updatedInput ?? input;
379
+ if (request.questions) {
380
+ const answers = {};
381
+ for (const q of request.questions) {
382
+ const a = answer.answers?.find((x) => x.questionId === q.id);
383
+ const words = [...(a?.choice ?? []).filter((c) => q.options.some((o) => o.id === c)), ...(a?.text?.trim() ? [a.text.trim()] : [])];
384
+ if (words.length)
385
+ answers[q.id] = words.join(", ");
386
+ }
387
+ updatedInput = { ...input, answers };
388
+ }
389
+ const kept = request.noAlwaysAllow ? [] : [...new Set(answer.alwaysAllow ?? [])].flatMap((i) => (request.suggestions?.[i] ? [request.suggestions[i]] : []));
390
+ return { behavior: "allow", updatedInput, ...(kept.length && { updatedPermissions: kept }) };
391
+ }
254
392
  const json = (v) => (typeof v === "string" ? v : JSON.stringify(v, null, 2));
255
393
  function reasoningEntry(id, text, streaming) {
256
394
  const first = text.trim().split(/\n|(?<=[.!?])\s/)[0] ?? "";
@@ -263,3 +401,23 @@ function reasoningEntry(id, text, streaming) {
263
401
  assumption: "The model’s own reasoning; nothing checked it.",
264
402
  };
265
403
  }
404
+ const scopes = { session: "session", localSettings: "local", projectSettings: "project", userSettings: "user", cliArg: "session" };
405
+ /**
406
+ * The SDK's suggested permission updates as plain parts PermissionPrompt shows, in the same order, so the indexes a
407
+ * person ticks are the ones `alwaysAllow` sends back.
408
+ */
409
+ export function claudeSuggestions(updates) {
410
+ return (updates ?? []).map((u) => {
411
+ const scope = scopes[u.destination] ?? u.destination;
412
+ if (u.type === "setMode")
413
+ return { action: "mode", what: u.mode, scope };
414
+ if (u.type === "addDirectories")
415
+ return { action: "directories", what: u.directories.join(", "), scope };
416
+ if (u.type === "removeDirectories")
417
+ return { action: "other", what: u.directories.join(", "), scope };
418
+ const what = u.rules.map((r) => (r.ruleContent ? `${r.toolName}(${r.ruleContent})` : r.toolName)).join(", ");
419
+ if (u.type === "removeRules")
420
+ return { action: "remove", what, scope };
421
+ return { action: u.behavior, what, scope };
422
+ });
423
+ }
@@ -1,5 +1,123 @@
1
- import type { CanUseTool, Query, SDKMessage } from "@anthropic-ai/claude-agent-sdk";
2
- import type { AgentEvent } from "./claude-agent-sdk.js";
1
+ import type { CanUseTool, PermissionMode, Query, SDKMessage } from "@anthropic-ai/claude-agent-sdk";
2
+ import { type AgentEvent, type ClaudeApproval, type ClaudeApprovalAnswer, type ClaudePermissionMode } from "./claude-agent-sdk.js";
3
+ import type { EntityRef } from "../lib/chat.js";
4
+ /** A permission request as a store keeps it. */
5
+ export type StoredApproval = {
6
+ id: string;
7
+ /** The run it belongs to, when the run has an id. */
8
+ runId?: string;
9
+ /** Who may answer it; checked by answerClaudeApproval. */
10
+ owner?: string;
11
+ request: ClaudeApproval;
12
+ createdAt: string;
13
+ /** Set once answered. Recorded, not necessarily used: a run that stopped at the same moment still denies the tool. */
14
+ answer?: ClaudeApprovalAnswer & {
15
+ at: string;
16
+ };
17
+ /** Set when nobody can answer any more (the run ended or stopped), with why. */
18
+ closed?: {
19
+ at: string;
20
+ reason: string;
21
+ };
22
+ };
23
+ /**
24
+ * Where waiting permission requests live. Answering and closing each happen once: whichever comes first wins, so a
25
+ * late answer to a run that already ended returns false instead of being lost. A store backed by a database does
26
+ * both as one conditional update (`… where answered_at is null and closed_at is null`).
27
+ */
28
+ export interface ApprovalStore {
29
+ /** Record a request that waits on a person. */
30
+ put(approval: StoredApproval): Promise<void>;
31
+ get(id: string): Promise<StoredApproval | undefined>;
32
+ /** Record the answer. False when there's no such request, or it was already answered or closed. */
33
+ answer(id: string, answer: ClaudeApprovalAnswer): Promise<boolean>;
34
+ /** Mark that it can't be answered any more. False when it was answered first. */
35
+ close(id: string, reason: string): Promise<boolean>;
36
+ /**
37
+ * Resolves with the answer once there is one, or undefined once it's closed or `signal` aborts. When `signal`
38
+ * aborts first, close the request in the same step ("The run was stopped."), so an answer arriving just after is
39
+ * refused instead of recorded for a tool that never ran. A store with no way to push can poll with waitByPolling.
40
+ */
41
+ wait(id: string, signal: AbortSignal): Promise<ClaudeApprovalAnswer | undefined>;
42
+ /** Requests still waiting, oldest first, for one run or all: what to show again after a reload. */
43
+ pending(runId?: string): Promise<StoredApproval[]>;
44
+ }
45
+ /**
46
+ * The default ApprovalStore: this process's memory. Answered and closed requests are forgotten after `keepMs`
47
+ * (10 minutes), so it doesn't grow without bound.
48
+ */
49
+ export declare function memoryApprovalStore({ keepMs }?: {
50
+ keepMs?: number;
51
+ }): ApprovalStore;
52
+ /** A `wait` for stores that can't push: reads the request every `intervalMs` (1s) until it's answered or closed. */
53
+ export declare function waitByPolling(get: (id: string) => Promise<StoredApproval | undefined>, id: string, signal: AbortSignal, intervalMs?: number): Promise<ClaudeApprovalAnswer | undefined>;
54
+ export type ClaudeCanUseToolOptions = {
55
+ /** Where requests wait. Defaults to this process's memory. */
56
+ approvals?: ApprovalStore;
57
+ /** Sends each request, and its closing, to whoever shows it (the browser, your event log). */
58
+ emit: (event: AgentEvent) => void;
59
+ /** Who may answer. */
60
+ owner?: string;
61
+ /** The run's id, kept with each request. */
62
+ runId?: string;
63
+ /** The run's permission mode now, recorded on each request. */
64
+ mode?: () => ClaudePermissionMode | undefined;
65
+ /** Called with each request's id as it starts waiting and once it's settled, e.g. to close the rest when the run ends. */
66
+ track?: {
67
+ add(id: string): void;
68
+ delete(id: string): void;
69
+ };
70
+ /**
71
+ * Which tools' input a person may edit before allowing it (`updatedInput`). Every tool by default, as in 1.5.0;
72
+ * an answer that edits a tool this refuses is rejected by answerClaudeApproval, whatever the browser sends.
73
+ */
74
+ editable?: (tool: string, input: unknown) => boolean;
75
+ /** Close a request nobody answered after this long, as expired: the tool is denied, never allowed. No limit by default. */
76
+ approvalTimeoutMs?: number;
77
+ };
78
+ /**
79
+ * A canUseTool that keeps each request in an ApprovalStore and waits for its answer from wherever it's given: your
80
+ * route, a queue, a workflow signal (`approvals.answer(id, { approved: true })`). Use it on its own when your agent
81
+ * runs somewhere other than streamClaudeAgent. When the SDK stops waiting, the request is closed, so a late answer
82
+ * is refused rather than lost.
83
+ */
84
+ export declare function claudeCanUseTool({ approvals, emit, owner, runId, mode, track, editable, approvalTimeoutMs }: ClaudeCanUseToolOptions): CanUseTool;
85
+ /**
86
+ * A run's events, kept so a browser can re-attach after a reload and a finished run can be replayed. `read` returns
87
+ * the events from index `from` on, and whether the run has ended.
88
+ */
89
+ export interface ClaudeRunStore {
90
+ /**
91
+ * Begin a run, with who started it. streamClaudeAgent calls it first. An append, end or subscribe for a run that
92
+ * was never started (or was already forgotten) must do nothing rather than create it, so a forgotten run can't
93
+ * come back without its owner.
94
+ */
95
+ start?(runId: string, owner?: string): Promise<void>;
96
+ append(runId: string, event: AgentEvent): Promise<void>;
97
+ end(runId: string): Promise<void>;
98
+ read(runId: string, from: number): Promise<{
99
+ events: AgentEvent[];
100
+ ended: boolean;
101
+ } | undefined>;
102
+ /** Called when the run has new events or ends. Optional: without it, attaching polls `read`. */
103
+ subscribe?(runId: string, listener: () => void): () => void;
104
+ /** Who started the run, when it was started with an owner; attachClaudeAgentRun checks it. */
105
+ ownerOf?(runId: string): Promise<string | undefined>;
106
+ /** Record the run's owner. Kept from 1.5.0; prefer `start`. */
107
+ setOwner?(runId: string, owner: string): Promise<void>;
108
+ }
109
+ /**
110
+ * The default ClaudeRunStore: this process's memory. It keeps at most `maxRuns` finished runs (100), forgetting the
111
+ * oldest finished one first; a run still going is never forgotten. Each run keeps at most `maxEvents` events
112
+ * (5,000): past that, partial-text events are no longer kept (the finished message still is), so a re-attached
113
+ * browser sees text arrive a message at a time instead of as it streams.
114
+ */
115
+ export declare function memoryRunStore({ maxRuns, maxEvents }?: {
116
+ maxRuns?: number;
117
+ maxEvents?: number;
118
+ }): ClaudeRunStore;
119
+ /** Which permission modes a run may be switched to: a list, or a test. */
120
+ export type ModePolicy = readonly PermissionMode[] | ((mode: PermissionMode) => boolean);
3
121
  export type RunClaudeAgent = (tools: {
4
122
  /** Pass to query()'s `canUseTool`, so permission requests reach the person. */
5
123
  canUseTool: CanUseTool;
@@ -10,11 +128,13 @@ export type RunClaudeAgent = (tools: {
10
128
  * It renders through the app's card registry, in the reply where it was sent.
11
129
  */
12
130
  emit: (event: AgentEvent) => void;
131
+ /** This run's id: the same one the response's `x-candor-run-id` header carries. */
132
+ runId: string;
13
133
  }) => Query;
14
134
  export type StreamClaudeAgentOptions = {
15
- /** The incoming request's signal (`request.signal`): the agent stops when the browser goes away. */
135
+ /** The incoming request's signal (`request.signal`): the agent stops when the browser goes away, unless `detach`. */
16
136
  signal?: AbortSignal;
17
- /** Who may answer this run's permission requests, e.g. the signed-in user's id. Checked by answerClaudeAgentApproval. */
137
+ /** Who may answer this run's permission requests, e.g. the signed-in user's id. Checked when they're answered. */
18
138
  owner?: string;
19
139
  /** The sentence shown when the run fails. Defaults to a generic one, so server errors don't leak to the browser. */
20
140
  describeError?: (error: unknown) => string;
@@ -24,6 +144,36 @@ export type StreamClaudeAgentOptions = {
24
144
  * in hand is still sent, and nothing after it.
25
145
  */
26
146
  onMessage?: (message: SDKMessage) => void | Promise<void>;
147
+ /** Called with every event the browser gets, in order, e.g. to keep the whole conversation for replay. */
148
+ onEvent?: (event: AgentEvent) => void | Promise<void>;
149
+ /** Where permission requests wait. Defaults to this process's memory. */
150
+ approvals?: ApprovalStore;
151
+ /** Keeps the run's events so a browser can re-attach (attachClaudeAgentRun). Without it, a reload loses the run. */
152
+ runs?: ClaudeRunStore;
153
+ /** The run's id; a new one by default. Sent back in the `x-candor-run-id` header. */
154
+ runId?: string;
155
+ /**
156
+ * Keep running when the browser goes away (a reload, a closed tab), so it can re-attach. Stop it with
157
+ * stopClaudeAgentRun. Needs a process that outlives the response: not a serverless function that's frozen when
158
+ * the response ends.
159
+ */
160
+ detach?: boolean;
161
+ /**
162
+ * What the person sent, recorded as the run's first event in `runs`, so a re-attached browser shows it. Pass the
163
+ * `promptId` useClaudeAgent sends as `id`: a browser that re-attaches with `initialEvents` then replaces the run's
164
+ * events instead of adding them again.
165
+ */
166
+ prompt?: {
167
+ id: string;
168
+ text: string;
169
+ references?: EntityRef[];
170
+ };
171
+ /** Which tools' input a person may edit before allowing it. Every tool by default. See claudeCanUseTool. */
172
+ editable?: ClaudeCanUseToolOptions["editable"];
173
+ /** Close a permission request nobody answered after this long, as expired (denied). No limit by default; set one with `detach`. */
174
+ approvalTimeoutMs?: number;
175
+ /** The permission modes setClaudeAgentPermissionMode may switch this run to. Any mode by default. */
176
+ modes?: ModePolicy;
27
177
  };
28
178
  /**
29
179
  * Run an agent and stream it to useClaudeAgent.
@@ -38,16 +188,51 @@ export type StreamClaudeAgentOptions = {
38
188
  * )
39
189
  * }
40
190
  */
41
- export declare function streamClaudeAgent(run: RunClaudeAgent, { signal, owner, describeError, onMessage }?: StreamClaudeAgentOptions): Response;
191
+ export declare function streamClaudeAgent(run: RunClaudeAgent, { signal, owner, describeError, onMessage, onEvent, approvals, runs, runId, detach, prompt, editable, approvalTimeoutMs, modes }?: StreamClaudeAgentOptions): Response;
192
+ /**
193
+ * Stream a run's events again, from index `from`, then live until it ends: for a browser re-attaching after a
194
+ * reload. 404 when the store doesn't know the run, 403 when `owner` isn't who started it.
195
+ *
196
+ * // app/api/agent/[runId]/route.ts
197
+ * export async function GET(request: Request, { params }) {
198
+ * const from = Number(new URL(request.url).searchParams.get("from") ?? 0)
199
+ * return attachClaudeAgentRun(runs, (await params).runId, { from, owner: userId, signal: request.signal })
200
+ * }
201
+ */
202
+ export declare function attachClaudeAgentRun(runs: ClaudeRunStore, runId: string, { from, owner, signal, pollMs }?: {
203
+ from?: number;
204
+ owner?: string;
205
+ signal?: AbortSignal;
206
+ pollMs?: number;
207
+ }): Promise<Response>;
208
+ /** Stop a run this process is running. False when it isn't running here, or `owner` didn't start it. */
209
+ export declare function stopClaudeAgentRun(runId: string, owner?: string): boolean;
42
210
  /**
43
- * Answer a waiting permission request. Returns false when there's no such
44
- * request (already answered, the run ended, or it ran in another process) or
45
- * `owner` doesn't match the run's owner.
211
+ * Change a running agent's permission mode (the SDK's `setPermissionMode`), and tell the browser. Only for a query
212
+ * in streaming input mode, as the SDK requires. False when the run isn't here, `owner` didn't start it, the mode
213
+ * isn't allowed, or the SDK refused. The route is where policy is enforced: a mode PermissionMode shows as locked
214
+ * must be left out of `allowed` (or the run's `modes`) too, since anyone can post to the route directly.
215
+ */
216
+ export declare function setClaudeAgentPermissionMode(runId: string, mode: PermissionMode, owner?: string, { allowed }?: {
217
+ allowed?: ModePolicy;
218
+ }): Promise<boolean>;
219
+ /**
220
+ * Check an answer from the browser against the request it answers, and record it. False when there's no such
221
+ * request, it's already settled, `owner` isn't the run's owner, or the answer doesn't fit the request (an edited
222
+ * input that isn't an object, answers to a request that isn't a question, a suggestion that wasn't offered).
46
223
  *
47
224
  * // app/api/agent/approve/route.ts
48
225
  * export async function POST(request: Request) {
49
- * const { id, approved, reason } = await request.json()
50
- * return Response.json({ ok: answerClaudeAgentApproval(id, approved, reason, userId) })
226
+ * const { id, ...answer } = await request.json()
227
+ * return Response.json({ ok: await answerClaudeApproval(approvals, id, answer, { owner: userId }) })
51
228
  * }
52
229
  */
230
+ export declare function answerClaudeApproval(approvals: ApprovalStore, id: string, answer: ClaudeApprovalAnswer, { owner }?: {
231
+ owner?: string;
232
+ }): Promise<boolean>;
233
+ /**
234
+ * Answer a waiting permission request in the default, in-memory store. Kept from 1.4; answerClaudeApproval takes a
235
+ * store and the full answer (an edited input, answers to questions, "always allow"). Returns false when there's no
236
+ * such request, or `owner` doesn't match the run's owner.
237
+ */
53
238
  export declare function answerClaudeAgentApproval(id: string, approved: boolean, reason?: string, owner?: string): boolean;