@candor-ui/react 1.3.0 → 1.7.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.
Files changed (74) hide show
  1. package/dist/adapters/ag-ui.d.ts +81 -16
  2. package/dist/adapters/ag-ui.js +308 -1
  3. package/dist/adapters/ai-sdk.d.ts +11 -6
  4. package/dist/adapters/ai-sdk.js +18 -1
  5. package/dist/adapters/claude-agent-sdk.d.ts +123 -14
  6. package/dist/adapters/claude-agent-sdk.js +163 -5
  7. package/dist/adapters/claude-agent-server.d.ts +199 -14
  8. package/dist/adapters/claude-agent-server.js +448 -49
  9. package/dist/adapters/use-ag-ui.d.ts +12 -6
  10. package/dist/adapters/use-ag-ui.js +23 -5
  11. package/dist/adapters/use-candor-chat.d.ts +1 -1
  12. package/dist/adapters/use-candor-chat.js +3 -3
  13. package/dist/adapters/use-claude-agent.d.ts +38 -9
  14. package/dist/adapters/use-claude-agent.js +96 -20
  15. package/dist/blocks/copy-field.d.ts +1 -1
  16. package/dist/blocks/copy-field.js +1 -1
  17. package/dist/blocks/degraded-panel.d.ts +5 -5
  18. package/dist/blocks/locale.d.ts +6 -6
  19. package/dist/blocks/locked.d.ts +8 -8
  20. package/dist/blocks/relative-time.d.ts +6 -6
  21. package/dist/blocks/status-dot.d.ts +3 -3
  22. package/dist/blocks/step-list.d.ts +3 -3
  23. package/dist/blocks/streaming-text.d.ts +3 -3
  24. package/dist/chat/agent-avatar.d.ts +4 -4
  25. package/dist/chat/ai-disclosure.d.ts +37 -0
  26. package/dist/chat/ai-disclosure.js +48 -0
  27. package/dist/chat/attachment-preview.d.ts +6 -6
  28. package/dist/chat/attachment-preview.js +2 -2
  29. package/dist/chat/card-registry.js +1 -1
  30. package/dist/chat/citation.d.ts +3 -3
  31. package/dist/chat/citation.js +6 -4
  32. package/dist/chat/composer.d.ts +31 -20
  33. package/dist/chat/composer.js +10 -6
  34. package/dist/chat/context-meter.d.ts +1 -1
  35. package/dist/chat/context-meter.js +1 -1
  36. package/dist/chat/entity-chip.d.ts +3 -3
  37. package/dist/chat/entity-chip.js +1 -1
  38. package/dist/chat/folded-turns.d.ts +11 -6
  39. package/dist/chat/folded-turns.js +2 -2
  40. package/dist/chat/message-actions.d.ts +6 -6
  41. package/dist/chat/message-actions.js +1 -1
  42. package/dist/chat/message-content.d.ts +13 -7
  43. package/dist/chat/message-content.js +10 -5
  44. package/dist/chat/message.d.ts +64 -37
  45. package/dist/chat/message.js +17 -11
  46. package/dist/chat/payload.d.ts +4 -4
  47. package/dist/chat/payload.js +5 -2
  48. package/dist/chat/permission-prompt.d.ts +129 -0
  49. package/dist/chat/permission-prompt.js +144 -0
  50. package/dist/chat/prompt-suggestions.d.ts +4 -4
  51. package/dist/chat/prompt-suggestions.js +1 -1
  52. package/dist/chat/reasoning.d.ts +3 -3
  53. package/dist/chat/reasoning.js +1 -1
  54. package/dist/chat/thread-divider.d.ts +3 -3
  55. package/dist/chat/thread-event.d.ts +10 -5
  56. package/dist/chat/thread-event.js +3 -2
  57. package/dist/chat/thread.d.ts +23 -7
  58. package/dist/chat/thread.js +29 -6
  59. package/dist/chat/tool-call.d.ts +7 -2
  60. package/dist/chat/tool-call.js +8 -3
  61. package/dist/index.d.ts +2 -0
  62. package/dist/index.js +2 -0
  63. package/dist/lib/chat.d.ts +53 -38
  64. package/dist/lib/know.d.ts +14 -12
  65. package/dist/lib/render-prop.d.ts +4 -4
  66. package/dist/lib/safe-markdown.d.ts +1 -1
  67. package/dist/lib/status.d.ts +21 -0
  68. package/dist/lib/status.js +17 -0
  69. package/dist/lib/use-shortcut.d.ts +1 -1
  70. package/dist/lib/use-virtual-list.d.ts +6 -6
  71. package/dist/styles.css +4 -1
  72. package/dist/ui/button.d.ts +1 -1
  73. package/dist/ui/button.js +1 -1
  74. package/package.json +1 -1
@@ -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. */
@@ -7,14 +8,14 @@ export type CandorAgUiEvent =
7
8
  type: "candor_user";
8
9
  id: string;
9
10
  text: string;
10
- references?: EntityRef[];
11
+ references?: EntityRef[] | undefined;
11
12
  }
12
13
  /** A person answered an interrupt; the next run resumes with it. */
13
14
  | {
14
15
  type: "candor_answer";
15
16
  interruptId: string;
16
17
  approved: boolean;
17
- reason?: string;
18
+ reason?: string | undefined;
18
19
  }
19
20
  /** The run failed outside the protocol's own events (the request, the stream). */
20
21
  | {
@@ -29,47 +30,111 @@ export type AgUiApproval = {
29
30
  /** The agent's machine-readable reason, e.g. "tool_approval". */
30
31
  reason: string;
31
32
  /** The agent's own sentence for the request, when it sent one. */
32
- message?: string;
33
+ message?: string | undefined;
33
34
  /** A sentence to show: the agent's message, or a plain one naming the tool. */
34
35
  title: string;
35
36
  /** Set when the interrupt asks to run a tool call. */
36
- toolCallId?: string;
37
+ toolCallId?: string | undefined;
37
38
  /** The tool's name, when the tool call is in the thread. */
38
- tool?: string;
39
+ tool?: string | undefined;
39
40
  /** The tool call's arguments, when they arrived and parse. */
40
41
  input?: unknown;
41
42
  /** When the agent stops waiting (ISO 8601). */
42
- expiresAt?: string;
43
+ expiresAt?: string | undefined;
43
44
  /** The shape the agent expects an answer in, when it says. */
44
- responseSchema?: Record<string, unknown>;
45
+ responseSchema?: Record<string, unknown> | undefined;
45
46
  };
46
47
  /** Tokens the runs reported, summed. A count no run reported stays unknown. AG-UI reports no cost. */
47
48
  export type AgUiUsage = {
48
- inputTokens?: number;
49
- outputTokens?: number;
49
+ inputTokens?: number | undefined;
50
+ outputTokens?: number | undefined;
50
51
  };
51
52
  export type FromAgUiOptions = {
52
53
  /** Who the agent is. Candor always names the agent. */
53
54
  agent: AgentIdentity;
54
55
  /** A run is open now: the last reply is still streaming, and no interrupt can be answered yet. */
55
- running?: boolean;
56
+ running?: boolean | undefined;
56
57
  /** A plain-words title for a tool call: "Read notes.md". `input` is undefined until the arguments arrive. */
57
- toolTitle?: (tool: string, input: unknown) => string | undefined;
58
+ toolTitle?: ((tool: string, input: unknown) => string | undefined) | undefined;
58
59
  /** The time to judge interrupt expiry by. Defaults to now. */
59
- now?: Date;
60
+ now?: Date | undefined;
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[] | undefined;
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 | undefined;
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 | undefined;
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 | undefined;
87
+ status: RunStatus;
88
+ since?: string | undefined;
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 | undefined;
100
+ /** Its `steps` as plan steps, when every step has a title and a status Candor knows. */
101
+ plan?: AgUiPlanStep[] | undefined;
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 | undefined;
60
104
  };
61
105
  export type AgUiState = {
62
106
  messages: Message[];
63
107
  /** Interrupts waiting on a person, oldest first. Empty while a run is open; expired ones are left out. */
64
108
  approvals: AgUiApproval[];
65
109
  /** The thread the runs belong to. Send it back to continue the conversation. */
66
- threadId?: string;
67
- usage?: AgUiUsage;
110
+ threadId?: string | undefined;
111
+ usage?: AgUiUsage | undefined;
68
112
  /** Something the person should know that isn't part of a reply, such as an expired request. */
69
- notice?: string;
113
+ notice?: string | undefined;
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 | undefined;
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;
@@ -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, state, activity, raw events and encrypted reasoning are the app's or the agent's own business; nothing to show.
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,22 +1,27 @@
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 = {
6
6
  /** Who the assistant is. Candor always names the agent. */
7
7
  agent: AgentIdentity;
8
8
  /** The chat's status from useChat, so the last reply can show as streaming or failed. */
9
- chatStatus?: "submitted" | "streaming" | "ready" | "error";
9
+ chatStatus?: "submitted" | "streaming" | "ready" | "error" | undefined;
10
10
  /** When a message was sent, if your metadata says. Without it, no time is shown. */
11
- createdAt?: (message: UIMessage) => string | undefined;
11
+ createdAt?: ((message: UIMessage) => string | undefined) | undefined;
12
12
  /** A plain-words title for a tool call: "Searched the docs for “region”". */
13
- toolTitle?: (tool: string, input: unknown) => string | undefined;
13
+ toolTitle?: ((tool: string, input: unknown) => string | undefined) | undefined;
14
14
  /** The text version of a data part, used when its card can't be shown. */
15
- summarize?: (name: string, data: unknown) => string;
15
+ summarize?: ((name: string, data: unknown) => string) | undefined;
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) | undefined;
16
21
  };
17
22
  /** Convert a whole useChat message list. */
18
23
  export declare function fromAiSdkMessages(messages: UIMessage[], options: FromAiSdkOptions): Message[];
19
24
  /** Convert one message. `last` marks the reply the chat status applies to. */
20
25
  export declare function fromAiSdkMessage(message: UIMessage, options: FromAiSdkOptions & {
21
- last?: boolean;
26
+ last?: boolean | undefined;
22
27
  }): Message;
@@ -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
- tools.push(toolCall(part, options));
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));