@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.
- package/dist/adapters/ag-ui.d.ts +66 -1
- package/dist/adapters/ag-ui.js +308 -1
- package/dist/adapters/ai-sdk.d.ts +6 -1
- package/dist/adapters/ai-sdk.js +18 -1
- package/dist/adapters/claude-agent-sdk.d.ts +111 -2
- package/dist/adapters/claude-agent-sdk.js +163 -5
- package/dist/adapters/claude-agent-server.d.ts +195 -10
- package/dist/adapters/claude-agent-server.js +448 -49
- package/dist/adapters/use-ag-ui.d.ts +6 -0
- package/dist/adapters/use-ag-ui.js +10 -4
- package/dist/adapters/use-candor-chat.js +2 -2
- package/dist/adapters/use-claude-agent.d.ts +35 -6
- package/dist/adapters/use-claude-agent.js +96 -20
- package/dist/chat/ai-disclosure.d.ts +37 -0
- package/dist/chat/ai-disclosure.js +48 -0
- package/dist/chat/attachment-preview.js +1 -1
- package/dist/chat/card-registry.js +1 -1
- package/dist/chat/citation.js +5 -3
- package/dist/chat/entity-chip.js +1 -1
- package/dist/chat/folded-turns.js +1 -1
- package/dist/chat/message.js +1 -1
- package/dist/chat/payload.js +5 -2
- package/dist/chat/permission-prompt.d.ts +129 -0
- package/dist/chat/permission-prompt.js +144 -0
- package/dist/chat/reasoning.js +1 -1
- package/dist/chat/thread.js +1 -1
- package/dist/chat/tool-call.d.ts +5 -0
- package/dist/chat/tool-call.js +7 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/lib/chat.d.ts +15 -0
- package/package.json +1 -1
|
@@ -13,7 +13,8 @@ export function fromClaudeAgentEvents(events, options) {
|
|
|
13
13
|
let usage;
|
|
14
14
|
let lastCost = 0;
|
|
15
15
|
let notice;
|
|
16
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
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,
|
|
50
|
-
* return Response.json({ ok:
|
|
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;
|