@candor-ui/react 1.1.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/composer.d.ts +5 -2
- package/dist/chat/composer.js +4 -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.d.ts +7 -1
- package/dist/chat/thread.js +24 -3
- package/dist/chat/tool-call.d.ts +5 -0
- package/dist/chat/tool-call.js +7 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/lib/chat.d.ts +15 -0
- package/dist/lib/use-virtual-list.d.ts +39 -0
- package/dist/lib/use-virtual-list.js +124 -0
- package/package.json +1 -1
package/dist/adapters/ag-ui.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { type Event } from "@ag-ui/core";
|
|
1
|
+
import { type Event, type JsonPatch } from "@ag-ui/core";
|
|
2
2
|
import type { AgentIdentity, EntityRef, Message } from "../lib/chat.js";
|
|
3
|
+
import type { RunStatus } from "../lib/status.js";
|
|
3
4
|
/** Events of Candor's own, mixed into the AG-UI stream by useAgUi. */
|
|
4
5
|
export type CandorAgUiEvent =
|
|
5
6
|
/** What the user sent. Added in the browser, with the same id as the AG-UI user message. */
|
|
@@ -57,6 +58,49 @@ export type FromAgUiOptions = {
|
|
|
57
58
|
toolTitle?: (tool: string, input: unknown) => string | undefined;
|
|
58
59
|
/** The time to judge interrupt expiry by. Defaults to now. */
|
|
59
60
|
now?: Date;
|
|
61
|
+
/** The state the first run started with; the first STATE_DELTA patches it. Defaults to `{}`, as AG-UI clients send. */
|
|
62
|
+
initialState?: unknown;
|
|
63
|
+
/** Other agents the app knows, by id. An activity naming an agent id not here (or `agent`) isn't mapped to an agent entry. */
|
|
64
|
+
agents?: AgentIdentity[];
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* A plan step read from an activity whose content has `steps: [{ title, status }]`.
|
|
68
|
+
* The same shape as PlanStep, so it can go straight to a plan view.
|
|
69
|
+
*/
|
|
70
|
+
export type AgUiPlanStep = {
|
|
71
|
+
id: string;
|
|
72
|
+
title: string;
|
|
73
|
+
status: RunStatus;
|
|
74
|
+
/** The step's own summary, when it sent one as text. */
|
|
75
|
+
summary?: string;
|
|
76
|
+
/** Why it failed: the agent's words, or a plain sentence saying it didn't say. Set only when status is "failed". */
|
|
77
|
+
failure?: string;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* What an agent is doing, read from an activity whose content has `status`
|
|
81
|
+
* (and optionally `doing`, `since`, `agentId`). The same shape as AgentActivityEntry.
|
|
82
|
+
*/
|
|
83
|
+
export type AgUiAgentActivityEntry = {
|
|
84
|
+
/** The app's own identity for the agent, never one taken from the content. */
|
|
85
|
+
agent: AgentIdentity;
|
|
86
|
+
doing?: string;
|
|
87
|
+
status: RunStatus;
|
|
88
|
+
since?: string;
|
|
89
|
+
};
|
|
90
|
+
/** An activity the agent reported: structured progress that isn't part of a reply. */
|
|
91
|
+
export type AgUiActivity = {
|
|
92
|
+
/** The activity message's id. */
|
|
93
|
+
id: string;
|
|
94
|
+
/** The agent's own kind for it, e.g. "plan" or "search". An open string. */
|
|
95
|
+
type: string;
|
|
96
|
+
/** Its content as the agent sent it, after any patches. Data, never markup. */
|
|
97
|
+
content: Record<string, unknown>;
|
|
98
|
+
/** Set when an update to it couldn't be applied: the content is the last good one, and may be behind. */
|
|
99
|
+
outOfDate?: string;
|
|
100
|
+
/** Its `steps` as plan steps, when every step has a title and a status Candor knows. */
|
|
101
|
+
plan?: AgUiPlanStep[];
|
|
102
|
+
/** Its `status`/`doing` as an agent entry, when the status is one Candor knows and the agent is one the app named. */
|
|
103
|
+
agentActivity?: AgUiAgentActivityEntry;
|
|
60
104
|
};
|
|
61
105
|
export type AgUiState = {
|
|
62
106
|
messages: Message[];
|
|
@@ -67,9 +111,30 @@ export type AgUiState = {
|
|
|
67
111
|
usage?: AgUiUsage;
|
|
68
112
|
/** Something the person should know that isn't part of a reply, such as an expired request. */
|
|
69
113
|
notice?: string;
|
|
114
|
+
/** The agent's shared state from STATE_SNAPSHOT and STATE_DELTA; undefined until the agent sends one. */
|
|
115
|
+
state?: unknown;
|
|
116
|
+
/** Set when a state update couldn't be applied: `state` is the last good value and may be behind. Cleared by the next snapshot. */
|
|
117
|
+
stateOutOfDate?: string;
|
|
118
|
+
/** The activities the agent reported, in the order they first arrived. */
|
|
119
|
+
activities: AgUiActivity[];
|
|
70
120
|
};
|
|
71
121
|
/**
|
|
72
122
|
* Turns AG-UI events into Candor messages, waiting approvals and usage.
|
|
73
123
|
* Pure (given `now`), so it can run on every render; useAgUi calls it for you.
|
|
74
124
|
*/
|
|
75
125
|
export declare function fromAgUiEvents(events: AgUiEvent[], options: FromAgUiOptions): AgUiState;
|
|
126
|
+
/** What applying a JSON Patch gave: the new document, or why it didn't apply (and then nothing changed). */
|
|
127
|
+
export type JsonPatchResult = {
|
|
128
|
+
ok: true;
|
|
129
|
+
value: unknown;
|
|
130
|
+
} | {
|
|
131
|
+
ok: false;
|
|
132
|
+
reason: string;
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* Applies an RFC 6902 JSON Patch to a JSON document, all or nothing. Pure: the
|
|
136
|
+
* document passed in is never changed, and the result shares nothing with the
|
|
137
|
+
* patch's values. A patch that fails (a path that doesn't resolve, a failed
|
|
138
|
+
* `test`, a malformed operation) returns `ok: false` with a plain reason.
|
|
139
|
+
*/
|
|
140
|
+
export declare function applyJsonPatch(document: unknown, patch: JsonPatch | unknown): JsonPatchResult;
|
package/dist/adapters/ag-ui.js
CHANGED
|
@@ -18,6 +18,10 @@ export function fromAgUiEvents(events, options) {
|
|
|
18
18
|
const subParents = new Map();
|
|
19
19
|
/** Text messages from roles Candor doesn't show as the agent's (an echoed user, system or developer message). */
|
|
20
20
|
const skipped = new Set();
|
|
21
|
+
let state;
|
|
22
|
+
let stateKnown = false;
|
|
23
|
+
let stateOutOfDate;
|
|
24
|
+
const activities = new Map();
|
|
21
25
|
const current = (id) => {
|
|
22
26
|
let turn = turns.at(-1);
|
|
23
27
|
if (!turn)
|
|
@@ -174,7 +178,56 @@ export function fromAgUiEvents(events, options) {
|
|
|
174
178
|
}
|
|
175
179
|
case EventType.MESSAGES_SNAPSHOT:
|
|
176
180
|
turns = applySnapshot(turns, e.messages);
|
|
181
|
+
// Activities are part of the agent's record too; a snapshot's version of one replaces ours.
|
|
182
|
+
for (const m of e.messages) {
|
|
183
|
+
if (m.role === "activity" && !m.subagentRunId && isRecord(m.content))
|
|
184
|
+
activities.set(m.id, { type: m.activityType, content: m.content });
|
|
185
|
+
}
|
|
186
|
+
break;
|
|
187
|
+
case EventType.STATE_SNAPSHOT:
|
|
188
|
+
state = e.snapshot;
|
|
189
|
+
stateKnown = true;
|
|
190
|
+
stateOutOfDate = undefined;
|
|
177
191
|
break;
|
|
192
|
+
case EventType.STATE_DELTA: {
|
|
193
|
+
// After a patch that failed, later ones would patch the wrong base; wait for a snapshot.
|
|
194
|
+
if (stateOutOfDate)
|
|
195
|
+
break;
|
|
196
|
+
const patched = applyJsonPatch(stateKnown ? state : (options.initialState ?? {}), e.delta);
|
|
197
|
+
if (patched.ok) {
|
|
198
|
+
state = patched.value;
|
|
199
|
+
stateKnown = true;
|
|
200
|
+
}
|
|
201
|
+
else
|
|
202
|
+
stateOutOfDate = `An update to the agent’s state couldn’t be applied (${patched.reason}), so it may be out of date.`;
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
case EventType.ACTIVITY_SNAPSHOT: {
|
|
206
|
+
const existing = activities.get(e.messageId);
|
|
207
|
+
// `replace: false` asks to keep what's there; it isn't a merge.
|
|
208
|
+
if (existing && e.replace === false)
|
|
209
|
+
break;
|
|
210
|
+
if (isRecord(e.content))
|
|
211
|
+
activities.set(e.messageId, { type: e.activityType, content: e.content });
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
case EventType.ACTIVITY_DELTA: {
|
|
215
|
+
const existing = activities.get(e.messageId);
|
|
216
|
+
if (!existing) {
|
|
217
|
+
activities.set(e.messageId, { type: e.activityType, content: {}, outOfDate: "An update arrived for an activity the agent never described." });
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
if (existing.outOfDate)
|
|
221
|
+
break;
|
|
222
|
+
const patched = applyJsonPatch(existing.content, e.patch);
|
|
223
|
+
if (patched.ok && isRecord(patched.value))
|
|
224
|
+
activities.set(e.messageId, { ...existing, content: patched.value });
|
|
225
|
+
else {
|
|
226
|
+
const reason = patched.ok ? "the content stopped being an object" : patched.reason;
|
|
227
|
+
activities.set(e.messageId, { ...existing, outOfDate: `An update to this activity couldn’t be applied (${reason}), so it may be out of date.` });
|
|
228
|
+
}
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
178
231
|
case EventType.CUSTOM: {
|
|
179
232
|
if (e.name !== "candor_card")
|
|
180
233
|
break;
|
|
@@ -183,7 +236,7 @@ export function fromAgUiEvents(events, options) {
|
|
|
183
236
|
current(`card-${turns.length}`).parts.push({ kind: "card", ...card });
|
|
184
237
|
break;
|
|
185
238
|
}
|
|
186
|
-
// Steps,
|
|
239
|
+
// Steps, raw events and encrypted reasoning are the app's or the agent's own business; nothing to show.
|
|
187
240
|
default:
|
|
188
241
|
break;
|
|
189
242
|
}
|
|
@@ -207,6 +260,9 @@ export function fromAgUiEvents(events, options) {
|
|
|
207
260
|
notice: lapsed.length
|
|
208
261
|
? `${lapsed.length === 1 ? "A request" : `${lapsed.length} requests`} for your answer expired before anyone answered. Send a message to carry on; the agent will be told ${lapsed.length === 1 ? "it was" : "they were"} cancelled.`
|
|
209
262
|
: undefined,
|
|
263
|
+
state,
|
|
264
|
+
stateOutOfDate,
|
|
265
|
+
activities: [...activities].map(([id, a]) => activity(id, a, options)),
|
|
210
266
|
};
|
|
211
267
|
}
|
|
212
268
|
/* ---------------------------------------------------------------- building a reply */
|
|
@@ -396,6 +452,257 @@ function approval(i, findCall, o) {
|
|
|
396
452
|
responseSchema: i.responseSchema,
|
|
397
453
|
};
|
|
398
454
|
}
|
|
455
|
+
/* ---------------------------------------------------------------- activities */
|
|
456
|
+
function activity(id, a, o) {
|
|
457
|
+
const out = { id, type: a.type, content: a.content };
|
|
458
|
+
if (a.outOfDate)
|
|
459
|
+
out.outOfDate = a.outOfDate;
|
|
460
|
+
const plan = planSteps(id, a.content);
|
|
461
|
+
if (plan)
|
|
462
|
+
out.plan = plan;
|
|
463
|
+
const entry = agentEntry(a.content, o);
|
|
464
|
+
if (entry)
|
|
465
|
+
out.agentActivity = entry;
|
|
466
|
+
return out;
|
|
467
|
+
}
|
|
468
|
+
/** Status words agents commonly send, as Candor's. Anything else is unknown and isn't mapped. */
|
|
469
|
+
const statusWords = {
|
|
470
|
+
idle: "idle",
|
|
471
|
+
queued: "queued",
|
|
472
|
+
running: "running",
|
|
473
|
+
waiting: "waiting",
|
|
474
|
+
blocked: "blocked",
|
|
475
|
+
succeeded: "succeeded",
|
|
476
|
+
failed: "failed",
|
|
477
|
+
cancelled: "cancelled",
|
|
478
|
+
skipped: "skipped",
|
|
479
|
+
pending: "idle",
|
|
480
|
+
in_progress: "running",
|
|
481
|
+
"in-progress": "running",
|
|
482
|
+
completed: "succeeded",
|
|
483
|
+
complete: "succeeded",
|
|
484
|
+
done: "succeeded",
|
|
485
|
+
error: "failed",
|
|
486
|
+
canceled: "cancelled",
|
|
487
|
+
};
|
|
488
|
+
const runStatus = (v) => typeof v === "string" && Object.prototype.hasOwnProperty.call(statusWords, v.trim().toLowerCase()) ? statusWords[v.trim().toLowerCase()] : undefined;
|
|
489
|
+
const textOf = (v) => (typeof v === "string" && v.trim() ? v : undefined);
|
|
490
|
+
/** Plan steps, only when every step has a title and a known status: a plan with a step dropped would mislead. */
|
|
491
|
+
function planSteps(id, content) {
|
|
492
|
+
const steps = content.steps;
|
|
493
|
+
if (!Array.isArray(steps) || !steps.length)
|
|
494
|
+
return undefined;
|
|
495
|
+
const out = [];
|
|
496
|
+
for (const [i, s] of steps.entries()) {
|
|
497
|
+
if (!isRecord(s))
|
|
498
|
+
return undefined;
|
|
499
|
+
const title = textOf(s.title);
|
|
500
|
+
const status = runStatus(s.status);
|
|
501
|
+
if (!title || !status)
|
|
502
|
+
return undefined;
|
|
503
|
+
const step = { id: textOf(s.id) ?? `${id}-${i}`, title, status };
|
|
504
|
+
const summary = textOf(s.summary);
|
|
505
|
+
if (summary)
|
|
506
|
+
step.summary = summary;
|
|
507
|
+
if (status === "failed")
|
|
508
|
+
step.failure = textOf(s.failure) ?? textOf(s.error) ?? "The agent didn’t say why this step failed.";
|
|
509
|
+
out.push(step);
|
|
510
|
+
}
|
|
511
|
+
return out;
|
|
512
|
+
}
|
|
513
|
+
/** An agent entry, only for a known status and an agent the app named. */
|
|
514
|
+
function agentEntry(content, o) {
|
|
515
|
+
if (Array.isArray(content.steps))
|
|
516
|
+
return undefined;
|
|
517
|
+
const status = runStatus(content.status);
|
|
518
|
+
if (!status)
|
|
519
|
+
return undefined;
|
|
520
|
+
let agent = o.agent;
|
|
521
|
+
if (content.agentId !== undefined) {
|
|
522
|
+
agent = typeof content.agentId === "string" ? [o.agent, ...(o.agents ?? [])].find((a) => a.id === content.agentId) : undefined;
|
|
523
|
+
if (!agent)
|
|
524
|
+
return undefined;
|
|
525
|
+
}
|
|
526
|
+
const entry = { agent, status };
|
|
527
|
+
const doing = textOf(content.doing);
|
|
528
|
+
if (doing)
|
|
529
|
+
entry.doing = doing;
|
|
530
|
+
const since = textOf(content.since);
|
|
531
|
+
if (since && !Number.isNaN(Date.parse(since)))
|
|
532
|
+
entry.since = since;
|
|
533
|
+
return entry;
|
|
534
|
+
}
|
|
535
|
+
class PatchError extends Error {
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Applies an RFC 6902 JSON Patch to a JSON document, all or nothing. Pure: the
|
|
539
|
+
* document passed in is never changed, and the result shares nothing with the
|
|
540
|
+
* patch's values. A patch that fails (a path that doesn't resolve, a failed
|
|
541
|
+
* `test`, a malformed operation) returns `ok: false` with a plain reason.
|
|
542
|
+
*/
|
|
543
|
+
export function applyJsonPatch(document, patch) {
|
|
544
|
+
if (!Array.isArray(patch))
|
|
545
|
+
return { ok: false, reason: "the patch isn’t a list of operations" };
|
|
546
|
+
let doc;
|
|
547
|
+
try {
|
|
548
|
+
doc = clone(document);
|
|
549
|
+
for (const [i, op] of patch.entries())
|
|
550
|
+
doc = applyOp(doc, op, i);
|
|
551
|
+
}
|
|
552
|
+
catch (err) {
|
|
553
|
+
if (err instanceof PatchError)
|
|
554
|
+
return { ok: false, reason: err.message };
|
|
555
|
+
// A document or value nested too deep to copy: refused like any other patch that can't apply.
|
|
556
|
+
if (err instanceof RangeError)
|
|
557
|
+
return { ok: false, reason: "the document is nested too deeply to patch" };
|
|
558
|
+
throw err;
|
|
559
|
+
}
|
|
560
|
+
return { ok: true, value: doc };
|
|
561
|
+
}
|
|
562
|
+
function applyOp(doc, op, i) {
|
|
563
|
+
const fail = (why) => {
|
|
564
|
+
throw new PatchError(`operation ${i + 1}: ${why}`);
|
|
565
|
+
};
|
|
566
|
+
if (!isRecord(op) || typeof op.op !== "string" || typeof op.path !== "string")
|
|
567
|
+
return fail("not an operation");
|
|
568
|
+
const path = pointer(op.path, fail);
|
|
569
|
+
const needsValue = op.op === "add" || op.op === "replace" || op.op === "test";
|
|
570
|
+
if (needsValue && !("value" in op))
|
|
571
|
+
return fail(`${op.op} has no value`);
|
|
572
|
+
switch (op.op) {
|
|
573
|
+
case "add":
|
|
574
|
+
return add(doc, path, clone(op.value), fail);
|
|
575
|
+
case "remove":
|
|
576
|
+
return remove(doc, path, fail).doc;
|
|
577
|
+
case "replace":
|
|
578
|
+
return replace(doc, path, clone(op.value), fail);
|
|
579
|
+
case "test":
|
|
580
|
+
if (!equal(get(doc, path, fail), op.value))
|
|
581
|
+
fail(`test failed at "${op.path}"`);
|
|
582
|
+
return doc;
|
|
583
|
+
case "move":
|
|
584
|
+
case "copy": {
|
|
585
|
+
if (typeof op.from !== "string")
|
|
586
|
+
return fail(`${op.op} has no from`);
|
|
587
|
+
const from = pointer(op.from, fail);
|
|
588
|
+
if (op.op === "move") {
|
|
589
|
+
if (op.from === op.path) {
|
|
590
|
+
get(doc, from, fail);
|
|
591
|
+
return doc;
|
|
592
|
+
}
|
|
593
|
+
if (op.path.startsWith(`${op.from}/`))
|
|
594
|
+
fail("can’t move a value into itself");
|
|
595
|
+
const taken = remove(doc, from, fail);
|
|
596
|
+
return add(taken.doc, path, taken.value, fail);
|
|
597
|
+
}
|
|
598
|
+
return add(doc, path, clone(get(doc, from, fail)), fail);
|
|
599
|
+
}
|
|
600
|
+
default:
|
|
601
|
+
return fail(`unknown op "${op.op}"`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
/** RFC 6901: "" is the whole document; otherwise slash-separated tokens with ~1 for "/" and ~0 for "~". */
|
|
605
|
+
function pointer(p, fail) {
|
|
606
|
+
if (p === "")
|
|
607
|
+
return [];
|
|
608
|
+
if (!p.startsWith("/"))
|
|
609
|
+
fail(`"${p}" isn’t a JSON Pointer`);
|
|
610
|
+
return p
|
|
611
|
+
.slice(1)
|
|
612
|
+
.split("/")
|
|
613
|
+
.map((t) => {
|
|
614
|
+
if (/~[^01]|~$/.test(t))
|
|
615
|
+
fail(`"${p}" has a bad escape`);
|
|
616
|
+
return t.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
const own = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
|
|
620
|
+
/** Sets an own property even for keys like "__proto__", so a patch can't reach a prototype. */
|
|
621
|
+
const put = (o, k, v) => Object.defineProperty(o, k, { value: v, writable: true, enumerable: true, configurable: true });
|
|
622
|
+
function arrayIndex(a, token, adding, fail) {
|
|
623
|
+
if (adding && token === "-")
|
|
624
|
+
return a.length;
|
|
625
|
+
if (!/^(0|[1-9]\d*)$/.test(token))
|
|
626
|
+
fail(`"${token}" isn’t an array index`);
|
|
627
|
+
const n = Number(token);
|
|
628
|
+
if (n > a.length || (!adding && n === a.length))
|
|
629
|
+
fail(`index ${n} is past the end of the list`);
|
|
630
|
+
return n;
|
|
631
|
+
}
|
|
632
|
+
/** The container holding the last token, and that token. */
|
|
633
|
+
function parentOf(doc, path, fail) {
|
|
634
|
+
const container = get(doc, path.slice(0, -1), fail);
|
|
635
|
+
if (!isRecord(container) && !Array.isArray(container))
|
|
636
|
+
fail(`"/${path.slice(0, -1).join("/")}" isn’t an object or a list`);
|
|
637
|
+
return { container: container, key: path.at(-1) };
|
|
638
|
+
}
|
|
639
|
+
function get(doc, path, fail) {
|
|
640
|
+
let at = doc;
|
|
641
|
+
for (const token of path) {
|
|
642
|
+
if (Array.isArray(at))
|
|
643
|
+
at = at[arrayIndex(at, token, false, fail)];
|
|
644
|
+
else if (isRecord(at) && own(at, token))
|
|
645
|
+
at = at[token];
|
|
646
|
+
else
|
|
647
|
+
fail(`nothing at "/${path.join("/")}"`);
|
|
648
|
+
}
|
|
649
|
+
return at;
|
|
650
|
+
}
|
|
651
|
+
function add(doc, path, value, fail) {
|
|
652
|
+
if (!path.length)
|
|
653
|
+
return value;
|
|
654
|
+
const { container, key } = parentOf(doc, path, fail);
|
|
655
|
+
if (Array.isArray(container))
|
|
656
|
+
container.splice(arrayIndex(container, key, true, fail), 0, value);
|
|
657
|
+
else
|
|
658
|
+
put(container, key, value);
|
|
659
|
+
return doc;
|
|
660
|
+
}
|
|
661
|
+
function replace(doc, path, value, fail) {
|
|
662
|
+
if (!path.length)
|
|
663
|
+
return value;
|
|
664
|
+
get(doc, path, fail);
|
|
665
|
+
const { container, key } = parentOf(doc, path, fail);
|
|
666
|
+
if (Array.isArray(container))
|
|
667
|
+
container[arrayIndex(container, key, false, fail)] = value;
|
|
668
|
+
else
|
|
669
|
+
put(container, key, value);
|
|
670
|
+
return doc;
|
|
671
|
+
}
|
|
672
|
+
function remove(doc, path, fail) {
|
|
673
|
+
if (!path.length)
|
|
674
|
+
return fail("can’t remove the whole document");
|
|
675
|
+
const { container, key } = parentOf(doc, path, fail);
|
|
676
|
+
if (Array.isArray(container))
|
|
677
|
+
return { doc, value: container.splice(arrayIndex(container, key, false, fail), 1)[0] };
|
|
678
|
+
if (!own(container, key))
|
|
679
|
+
fail(`nothing at "/${path.join("/")}"`);
|
|
680
|
+
const value = container[key];
|
|
681
|
+
delete container[key];
|
|
682
|
+
return { doc, value };
|
|
683
|
+
}
|
|
684
|
+
function clone(v) {
|
|
685
|
+
if (Array.isArray(v))
|
|
686
|
+
return v.map(clone);
|
|
687
|
+
if (isRecord(v)) {
|
|
688
|
+
const out = {};
|
|
689
|
+
for (const k of Object.keys(v))
|
|
690
|
+
put(out, k, clone(v[k]));
|
|
691
|
+
return out;
|
|
692
|
+
}
|
|
693
|
+
return v;
|
|
694
|
+
}
|
|
695
|
+
function equal(a, b) {
|
|
696
|
+
if (a === b)
|
|
697
|
+
return true;
|
|
698
|
+
if (Array.isArray(a) || Array.isArray(b))
|
|
699
|
+
return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((x, i) => equal(x, b[i]));
|
|
700
|
+
if (!isRecord(a) || !isRecord(b))
|
|
701
|
+
return false;
|
|
702
|
+
const keys = Object.keys(a);
|
|
703
|
+
return keys.length === Object.keys(b).length && keys.every((k) => own(b, k) && equal(a[k], b[k]));
|
|
704
|
+
}
|
|
705
|
+
const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
399
706
|
/* ---------------------------------------------------------------- helpers */
|
|
400
707
|
const expired = (i, now) => i.expiresAt !== undefined && new Date(i.expiresAt).getTime() <= now.getTime();
|
|
401
708
|
function runError(message) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { UIMessage } from "ai";
|
|
2
|
-
import type { AgentIdentity, Message } from "../lib/chat.js";
|
|
2
|
+
import type { AgentIdentity, Message, ToolSource } from "../lib/chat.js";
|
|
3
3
|
import { inline, markdownBlocks, plainBlocks } from "../lib/safe-markdown.js";
|
|
4
4
|
export { inline, markdownBlocks, plainBlocks };
|
|
5
5
|
export type FromAiSdkOptions = {
|
|
@@ -13,6 +13,11 @@ export type FromAiSdkOptions = {
|
|
|
13
13
|
toolTitle?: (tool: string, input: unknown) => string | undefined;
|
|
14
14
|
/** The text version of a data part, used when its card can't be shown. */
|
|
15
15
|
summarize?: (name: string, data: unknown) => string;
|
|
16
|
+
/**
|
|
17
|
+
* The sources a tool's result supports, added to the reply's sources after the SDK's own source parts:
|
|
18
|
+
* `(tool, input, output) => [{ kind: "document", label: "Refund policy v2" }]`. Read only from the tool's output.
|
|
19
|
+
*/
|
|
20
|
+
sourcesFrom?: (tool: string, input: unknown, output: unknown) => ToolSource[] | undefined;
|
|
16
21
|
};
|
|
17
22
|
/** Convert a whole useChat message list. */
|
|
18
23
|
export declare function fromAiSdkMessages(messages: UIMessage[], options: FromAiSdkOptions): Message[];
|
package/dist/adapters/ai-sdk.js
CHANGED
|
@@ -11,6 +11,8 @@ export function fromAiSdkMessage(message, options) {
|
|
|
11
11
|
const blocks = [];
|
|
12
12
|
const reasoning = [];
|
|
13
13
|
const sources = [];
|
|
14
|
+
const toolSources = [];
|
|
15
|
+
const toolSourceKeys = new Set();
|
|
14
16
|
let tools = null;
|
|
15
17
|
const flushTools = () => {
|
|
16
18
|
if (tools?.length)
|
|
@@ -20,7 +22,18 @@ export function fromAiSdkMessage(message, options) {
|
|
|
20
22
|
for (const part of message.parts) {
|
|
21
23
|
if (part.type === "dynamic-tool" || part.type.startsWith("tool-")) {
|
|
22
24
|
tools ??= [];
|
|
23
|
-
|
|
25
|
+
const call = toolCall(part, options);
|
|
26
|
+
tools.push(call);
|
|
27
|
+
const p = part;
|
|
28
|
+
if (call.status === "succeeded" && options.sourcesFrom) {
|
|
29
|
+
for (const { key, ...found } of options.sourcesFrom(call.tool, p.input, p.output) ?? []) {
|
|
30
|
+
const k = key ?? `${found.kind}:${found.label}`;
|
|
31
|
+
if (toolSourceKeys.has(k))
|
|
32
|
+
continue;
|
|
33
|
+
toolSourceKeys.add(k);
|
|
34
|
+
toolSources.push(found);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
24
37
|
continue;
|
|
25
38
|
}
|
|
26
39
|
flushTools();
|
|
@@ -55,6 +68,8 @@ export function fromAiSdkMessage(message, options) {
|
|
|
55
68
|
}
|
|
56
69
|
}
|
|
57
70
|
flushTools();
|
|
71
|
+
for (const [i, found] of toolSources.entries())
|
|
72
|
+
sources.push({ ...found, id: `${message.id}-tool-s${i + 1}`, marker: sources.length + 1 });
|
|
58
73
|
const author = message.role === "assistant" ? { role: "agent", agent: options.agent } : message.role === "user" ? { role: "user" } : { role: "system" };
|
|
59
74
|
return {
|
|
60
75
|
id: message.id,
|
|
@@ -104,6 +119,8 @@ function toolCall(p, options) {
|
|
|
104
119
|
output: p.output === undefined ? undefined : json(p.output),
|
|
105
120
|
outputLanguage: p.output === undefined || typeof p.output === "string" ? "text" : "json",
|
|
106
121
|
error: status === "failed" ? (p.errorText ?? "The tool failed and didn’t say why.") : status === "cancelled" && p.approval?.reason ? `Denied: ${p.approval.reason}` : undefined,
|
|
122
|
+
// Only a decided approval says who decided; one still waiting is the person's to make.
|
|
123
|
+
approval: p.approval?.approved === true ? { by: p.approval.isAutomatic ? "automatic" : "person", note: p.approval.reason || undefined } : undefined,
|
|
107
124
|
};
|
|
108
125
|
}
|
|
109
126
|
const json = (v) => (typeof v === "string" ? v : JSON.stringify(v, null, 2));
|
|
@@ -1,5 +1,46 @@
|
|
|
1
|
-
import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
-
import type { AgentIdentity, EntityRef, Message } from "../lib/chat.js";
|
|
1
|
+
import type { PermissionResult, PermissionUpdate, SDKMessage } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
import type { AgentIdentity, EntityRef, Message, ToolSource } from "../lib/chat.js";
|
|
3
|
+
/**
|
|
4
|
+
* How the agent's tool calls are approved, as the SDK names it: "default" (ask for anything not allowed already),
|
|
5
|
+
* "acceptEdits", "plan", "auto" (a classifier decides), "dontAsk" (deny what isn't allowed already) and
|
|
6
|
+
* "bypassPermissions". Kept open: a newer SDK's mode passes through as its name.
|
|
7
|
+
*/
|
|
8
|
+
export type ClaudePermissionMode = "default" | "acceptEdits" | "plan" | "auto" | "dontAsk" | "bypassPermissions" | (string & {});
|
|
9
|
+
/** One of the agent's questions (the SDK's AskUserQuestion), shaped for ClarifyingQuestions. Options' ids are their labels. */
|
|
10
|
+
export type ClaudeQuestion = {
|
|
11
|
+
/** The question's text, which is also how its answer is keyed back to the SDK. */
|
|
12
|
+
id: string;
|
|
13
|
+
kind: "choice";
|
|
14
|
+
prompt: string;
|
|
15
|
+
/** The SDK's short header ("Library", "Approach"). */
|
|
16
|
+
tag?: string;
|
|
17
|
+
options: {
|
|
18
|
+
id: string;
|
|
19
|
+
label: string;
|
|
20
|
+
detail?: string;
|
|
21
|
+
}[];
|
|
22
|
+
multiple: boolean;
|
|
23
|
+
/** The SDK always lets the person answer in their own words. */
|
|
24
|
+
other: true;
|
|
25
|
+
};
|
|
26
|
+
/** A person's answer to one ClaudeQuestion: the chosen labels, their own words, or both. Same shape as ClarifyingAnswer. */
|
|
27
|
+
export type ClaudeQuestionAnswer = {
|
|
28
|
+
questionId: string;
|
|
29
|
+
choice?: string[];
|
|
30
|
+
text?: string;
|
|
31
|
+
};
|
|
32
|
+
/** How a person answered a permission request. */
|
|
33
|
+
export type ClaudeApprovalAnswer = {
|
|
34
|
+
approved: boolean;
|
|
35
|
+
/** Why it was denied, sent to the agent. */
|
|
36
|
+
reason?: string;
|
|
37
|
+
/** The tool's input as the person edited it before allowing it. The tool runs with exactly this. */
|
|
38
|
+
updatedInput?: Record<string, unknown>;
|
|
39
|
+
/** Answers to the agent's questions, when the request is one (`questions`). */
|
|
40
|
+
answers?: ClaudeQuestionAnswer[];
|
|
41
|
+
/** Which of the request's `suggestions` the person chose to keep ("always allow"), by index. None unless they choose. */
|
|
42
|
+
alwaysAllow?: number[];
|
|
43
|
+
};
|
|
3
44
|
/** Events of Candor's own, mixed into the SDK's stream. */
|
|
4
45
|
export type CandorAgentEvent =
|
|
5
46
|
/** What the user sent. Added in the browser; the SDK doesn't echo a plain prompt back. */
|
|
@@ -24,6 +65,28 @@ export type CandorAgentEvent =
|
|
|
24
65
|
blockedPath?: string;
|
|
25
66
|
/** Open on "Deny" and offer no one-key approve: the request grants more than it looks. */
|
|
26
67
|
defaultToNo?: boolean;
|
|
68
|
+
/** A short name for the action: "Edit file". */
|
|
69
|
+
displayName?: string;
|
|
70
|
+
/**
|
|
71
|
+
* Rules the SDK suggests so this isn't asked again ("always allow"), as the SDK sends them. Offer them as a
|
|
72
|
+
* choice the person makes; never keep one unless they pick it.
|
|
73
|
+
*/
|
|
74
|
+
suggestions?: PermissionUpdate[];
|
|
75
|
+
/** Offer no "always allow" for this request: its rule would grant more than this one action. */
|
|
76
|
+
noAlwaysAllow?: boolean;
|
|
77
|
+
/** The permission mode the run was in when it asked. */
|
|
78
|
+
mode?: ClaudePermissionMode;
|
|
79
|
+
/** The agent's questions, when the tool is AskUserQuestion: answer them instead of allowing a tool. */
|
|
80
|
+
questions?: ClaudeQuestion[];
|
|
81
|
+
/** Set when a subagent asked, with its id. */
|
|
82
|
+
agentId?: string;
|
|
83
|
+
/** For an MCP tool: its server's name and where that server was configured, as the SDK reports them. */
|
|
84
|
+
mcpServer?: {
|
|
85
|
+
name: string;
|
|
86
|
+
source: string;
|
|
87
|
+
};
|
|
88
|
+
/** Whether the server lets a person edit this call's input before allowing it. False means no; left out means yes (1.5.0). */
|
|
89
|
+
editable?: boolean;
|
|
27
90
|
}
|
|
28
91
|
/** The request was answered, or the run ended before anyone did. */
|
|
29
92
|
| {
|
|
@@ -31,6 +94,19 @@ export type CandorAgentEvent =
|
|
|
31
94
|
id: string;
|
|
32
95
|
approved: boolean;
|
|
33
96
|
reason?: string;
|
|
97
|
+
/** The person changed the input before allowing it. */
|
|
98
|
+
edited?: boolean;
|
|
99
|
+
/** The input the tool ran with, when the person edited it. */
|
|
100
|
+
input?: unknown;
|
|
101
|
+
/** Nobody answered in time (the server's `approvalTimeoutMs`). */
|
|
102
|
+
expired?: boolean;
|
|
103
|
+
/** How many "always allow" rules the person kept. */
|
|
104
|
+
kept?: number;
|
|
105
|
+
}
|
|
106
|
+
/** The run's permission mode changed, by the app (`setPermissionMode`). The SDK's own mode reports also count. */
|
|
107
|
+
| {
|
|
108
|
+
type: "candor_mode";
|
|
109
|
+
mode: ClaudePermissionMode;
|
|
34
110
|
}
|
|
35
111
|
/** A typed card for the app's card registry, sent by your server (a tool's result as a diff, a plan, a table). */
|
|
36
112
|
| {
|
|
@@ -62,6 +138,12 @@ export type FromClaudeAgentOptions = {
|
|
|
62
138
|
running?: boolean;
|
|
63
139
|
/** A plain-words title for a tool call: "Read notes.md". */
|
|
64
140
|
toolTitle?: (tool: string, input: unknown) => string | undefined;
|
|
141
|
+
/**
|
|
142
|
+
* The sources a tool's result supports, for the reply's source list: `(tool, input, output) => [{ kind: "document",
|
|
143
|
+
* label: "Pricing policy v3" }]`. Read only from what the tool returned, never from numbers in the agent's text.
|
|
144
|
+
* Claude's own citations and web search results are read without it.
|
|
145
|
+
*/
|
|
146
|
+
sourcesFrom?: (tool: string, input: unknown, output: string) => ToolSource[] | undefined;
|
|
65
147
|
};
|
|
66
148
|
export type ClaudeAgentState = {
|
|
67
149
|
messages: Message[];
|
|
@@ -72,9 +154,36 @@ export type ClaudeAgentState = {
|
|
|
72
154
|
usage?: ClaudeAgentUsage;
|
|
73
155
|
/** A passing problem the agent is handling itself, such as a retried API call. */
|
|
74
156
|
notice?: string;
|
|
157
|
+
/** The permission mode last reported, by the SDK or the app. Absent until one is. */
|
|
158
|
+
permissionMode?: ClaudePermissionMode;
|
|
75
159
|
};
|
|
76
160
|
/**
|
|
77
161
|
* Turns Claude Agent SDK events into Candor messages, waiting approvals and usage.
|
|
78
162
|
* Pure, so it can run on every render; useClaudeAgent calls it for you.
|
|
79
163
|
*/
|
|
80
164
|
export declare function fromClaudeAgentEvents(events: AgentEvent[], options: FromClaudeAgentOptions): ClaudeAgentState;
|
|
165
|
+
/**
|
|
166
|
+
* The questions in an AskUserQuestion call, ready for ClarifyingQuestions; undefined when the input isn't one.
|
|
167
|
+
* Option ids are their labels, which is what the SDK takes back.
|
|
168
|
+
*/
|
|
169
|
+
export declare function claudeQuestions(input: unknown): ClaudeQuestion[] | undefined;
|
|
170
|
+
type Answerable = Pick<ClaudeApproval, "input" | "suggestions" | "noAlwaysAllow" | "questions">;
|
|
171
|
+
/**
|
|
172
|
+
* What to hand back to the SDK for a person's answer: the `PermissionResult` canUseTool resolves with. Pure, so it
|
|
173
|
+
* works wherever the answer lands (your own route, a queue, a workflow signal). Questions become AskUserQuestion's
|
|
174
|
+
* `answers`, keyed by question text, several choices joined with ", " and the person's own words used as they are.
|
|
175
|
+
* Only the suggestions the person picked are kept, and none when the request offers no "always allow".
|
|
176
|
+
*/
|
|
177
|
+
export declare function claudePermissionResult(request: Answerable, answer: ClaudeApprovalAnswer): PermissionResult;
|
|
178
|
+
/** A "don't ask again" rule in plain parts, for PermissionPrompt's `suggestions`. */
|
|
179
|
+
export type ClaudeSuggestion = {
|
|
180
|
+
action: "allow" | "deny" | "ask" | "remove" | "mode" | "directories" | "other";
|
|
181
|
+
what: string;
|
|
182
|
+
scope?: string;
|
|
183
|
+
};
|
|
184
|
+
/**
|
|
185
|
+
* The SDK's suggested permission updates as plain parts PermissionPrompt shows, in the same order, so the indexes a
|
|
186
|
+
* person ticks are the ones `alwaysAllow` sends back.
|
|
187
|
+
*/
|
|
188
|
+
export declare function claudeSuggestions(updates: PermissionUpdate[] | undefined): ClaudeSuggestion[];
|
|
189
|
+
export {};
|