@candor-ui/react 0.0.0-stage → 1.0.1
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/LICENSE +24 -0
- package/README.md +187 -2
- package/dist/adapters/ag-ui.d.ts +75 -0
- package/dist/adapters/ag-ui.js +441 -0
- package/dist/adapters/ai-sdk.d.ts +22 -0
- package/dist/adapters/ai-sdk.js +129 -0
- package/dist/adapters/claude-agent-sdk.d.ts +80 -0
- package/dist/adapters/claude-agent-sdk.js +265 -0
- package/dist/adapters/claude-agent-server.d.ts +53 -0
- package/dist/adapters/claude-agent-server.js +104 -0
- package/dist/adapters/use-ag-ui.d.ts +55 -0
- package/dist/adapters/use-ag-ui.js +168 -0
- package/dist/adapters/use-candor-chat.d.ts +40 -0
- package/dist/adapters/use-candor-chat.js +56 -0
- package/dist/adapters/use-claude-agent.d.ts +48 -0
- package/dist/adapters/use-claude-agent.js +143 -0
- package/dist/ag-ui.d.ts +2 -0
- package/dist/ag-ui.js +2 -0
- package/dist/ai-sdk.d.ts +2 -0
- package/dist/ai-sdk.js +2 -0
- package/dist/blocks/copy-field.d.ts +13 -0
- package/dist/blocks/copy-field.js +15 -0
- package/dist/blocks/degraded-panel.d.ts +19 -0
- package/dist/blocks/degraded-panel.js +14 -0
- package/dist/blocks/locked.d.ts +26 -0
- package/dist/blocks/locked.js +36 -0
- package/dist/blocks/relative-time.d.ts +13 -0
- package/dist/blocks/relative-time.js +62 -0
- package/dist/blocks/status-dot.d.ts +16 -0
- package/dist/blocks/status-dot.js +25 -0
- package/dist/blocks/step-list.d.ts +23 -0
- package/dist/blocks/step-list.js +37 -0
- package/dist/blocks/streaming-text.d.ts +17 -0
- package/dist/blocks/streaming-text.js +14 -0
- package/dist/blocks/surface.d.ts +9 -0
- package/dist/blocks/surface.js +35 -0
- package/dist/blocks/terms.d.ts +24 -0
- package/dist/blocks/terms.js +17 -0
- package/dist/chat/agent-avatar.d.ts +34 -0
- package/dist/chat/agent-avatar.js +42 -0
- package/dist/chat/card-registry.d.ts +39 -0
- package/dist/chat/card-registry.js +49 -0
- package/dist/chat/citation.d.ts +29 -0
- package/dist/chat/citation.js +44 -0
- package/dist/chat/composer.d.ts +54 -0
- package/dist/chat/composer.js +168 -0
- package/dist/chat/context-meter.d.ts +14 -0
- package/dist/chat/context-meter.js +30 -0
- package/dist/chat/entity-chip.d.ts +37 -0
- package/dist/chat/entity-chip.js +52 -0
- package/dist/chat/message-actions.d.ts +21 -0
- package/dist/chat/message-actions.js +20 -0
- package/dist/chat/message-content.d.ts +25 -0
- package/dist/chat/message-content.js +122 -0
- package/dist/chat/message.d.ts +39 -0
- package/dist/chat/message.js +44 -0
- package/dist/chat/payload.d.ts +16 -0
- package/dist/chat/payload.js +18 -0
- package/dist/chat/prompt-suggestions.d.ts +20 -0
- package/dist/chat/prompt-suggestions.js +18 -0
- package/dist/chat/reasoning.d.ts +18 -0
- package/dist/chat/reasoning.js +31 -0
- package/dist/chat/thread.d.ts +18 -0
- package/dist/chat/thread.js +60 -0
- package/dist/chat/tool-call.d.ts +19 -0
- package/dist/chat/tool-call.js +33 -0
- package/dist/claude-server.d.ts +1 -0
- package/dist/claude-server.js +2 -0
- package/dist/claude.d.ts +2 -0
- package/dist/claude.js +2 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +38 -0
- package/dist/lib/chat.d.ts +241 -0
- package/dist/lib/chat.js +1 -0
- package/dist/lib/know.d.ts +123 -0
- package/dist/lib/know.js +1 -0
- package/dist/lib/render-prop.d.ts +16 -0
- package/dist/lib/render-prop.js +7 -0
- package/dist/lib/safe-markdown.d.ts +12 -0
- package/dist/lib/safe-markdown.js +98 -0
- package/dist/lib/status.d.ts +21 -0
- package/dist/lib/status.js +26 -0
- package/dist/lib/use-copy.d.ts +10 -0
- package/dist/lib/use-copy.js +24 -0
- package/dist/lib/use-shortcut.d.ts +15 -0
- package/dist/lib/use-shortcut.js +62 -0
- package/dist/lib/utils.d.ts +2 -0
- package/dist/lib/utils.js +5 -0
- package/dist/styles.css +460 -0
- package/dist/ui/button.d.ts +12 -0
- package/dist/ui/button.js +41 -0
- package/dist/ui/collapsible.d.ts +6 -0
- package/dist/ui/collapsible.js +15 -0
- package/dist/ui/popover.d.ts +11 -0
- package/dist/ui/popover.js +29 -0
- package/dist/ui/skeleton.d.ts +2 -0
- package/dist/ui/skeleton.js +6 -0
- package/dist/ui/tooltip.d.ts +9 -0
- package/dist/ui/tooltip.js +24 -0
- package/package.json +90 -4
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { EventType } from "@ag-ui/core";
|
|
2
|
+
import { markdownBlocks, plainBlocks } from "../lib/safe-markdown.js";
|
|
3
|
+
/**
|
|
4
|
+
* Turns AG-UI events into Candor messages, waiting approvals and usage.
|
|
5
|
+
* Pure (given `now`), so it can run on every render; useAgUi calls it for you.
|
|
6
|
+
*/
|
|
7
|
+
export function fromAgUiEvents(events, options) {
|
|
8
|
+
const now = options.now ?? new Date();
|
|
9
|
+
let turns = [];
|
|
10
|
+
let threadId;
|
|
11
|
+
let usage;
|
|
12
|
+
/** Interrupts from the latest run that ended on one, until the next run starts. */
|
|
13
|
+
let open = [];
|
|
14
|
+
const answers = new Map();
|
|
15
|
+
// Subagent traffic, by what it opened. Continuations may omit the tag, so ownership is remembered.
|
|
16
|
+
const subMessages = new Set();
|
|
17
|
+
const subTools = new Set();
|
|
18
|
+
const subParents = new Map();
|
|
19
|
+
/** Text messages from roles Candor doesn't show as the agent's (an echoed user, system or developer message). */
|
|
20
|
+
const skipped = new Set();
|
|
21
|
+
const current = (id) => {
|
|
22
|
+
let turn = turns.at(-1);
|
|
23
|
+
if (!turn)
|
|
24
|
+
turns.push((turn = {}));
|
|
25
|
+
return (turn.reply ??= emptyReply(id));
|
|
26
|
+
};
|
|
27
|
+
const findCall = (id) => {
|
|
28
|
+
for (let i = turns.length - 1; i >= 0; i--) {
|
|
29
|
+
const call = turns[i].reply?.tools.get(id);
|
|
30
|
+
if (call)
|
|
31
|
+
return call;
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
};
|
|
35
|
+
const addUsage = (entries, r) => {
|
|
36
|
+
for (const u of entries ?? []) {
|
|
37
|
+
usage ??= {};
|
|
38
|
+
if (u.inputTokens !== undefined)
|
|
39
|
+
usage.inputTokens = (usage.inputTokens ?? 0) + u.inputTokens;
|
|
40
|
+
if (u.outputTokens !== undefined)
|
|
41
|
+
usage.outputTokens = (usage.outputTokens ?? 0) + u.outputTokens;
|
|
42
|
+
if (r && u.model)
|
|
43
|
+
r.model ??= u.model;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
for (const e of events) {
|
|
47
|
+
if (e.type === EventType.SUBAGENT_ERROR) {
|
|
48
|
+
// A subagent that failed is reported where it started: on its parent tool call.
|
|
49
|
+
const parent = subParents.get(e.subagentRunId);
|
|
50
|
+
const call = parent ? findCall(parent) : undefined;
|
|
51
|
+
if (call && call.status === "running") {
|
|
52
|
+
call.status = "failed";
|
|
53
|
+
call.error = `A subagent failed: ${(firstLine(e.message) ?? "it didn’t say why").replace(/[.!]$/, "")}.`;
|
|
54
|
+
}
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (isSubagent(e, subMessages, subTools, subParents))
|
|
58
|
+
continue;
|
|
59
|
+
switch (e.type) {
|
|
60
|
+
case "candor_user":
|
|
61
|
+
turns.push({ user: { id: e.id, author: { role: "user" }, status: "complete", blocks: plainBlocks(e.text) } });
|
|
62
|
+
break;
|
|
63
|
+
case "candor_answer": {
|
|
64
|
+
answers.set(e.interruptId, { approved: e.approved, reason: e.reason });
|
|
65
|
+
const interrupt = open.find((i) => i.id === e.interruptId);
|
|
66
|
+
const call = interrupt?.toolCallId ? findCall(interrupt.toolCallId) : undefined;
|
|
67
|
+
if (call && call.status === "waiting") {
|
|
68
|
+
call.status = e.approved ? "running" : "cancelled";
|
|
69
|
+
if (!e.approved)
|
|
70
|
+
call.error = e.reason ? `Denied: ${e.reason}` : "Denied.";
|
|
71
|
+
}
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
case "candor_error":
|
|
75
|
+
// The first failure is the one that explains what happened.
|
|
76
|
+
current(`error-${turns.length}`).failure ??= e.message;
|
|
77
|
+
break;
|
|
78
|
+
case EventType.RUN_STARTED: {
|
|
79
|
+
threadId = e.threadId;
|
|
80
|
+
open = [];
|
|
81
|
+
const r = current(e.runId);
|
|
82
|
+
r.outcome = undefined;
|
|
83
|
+
r.aborted = false;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
case EventType.RUN_FINISHED: {
|
|
87
|
+
threadId = e.threadId;
|
|
88
|
+
const r = current(e.runId);
|
|
89
|
+
const outcome = e.outcome?.type ?? "success";
|
|
90
|
+
r.outcome = outcome;
|
|
91
|
+
addUsage(e.usage, r);
|
|
92
|
+
if (e.outcome?.type === "interrupt") {
|
|
93
|
+
open = e.outcome.interrupts;
|
|
94
|
+
for (const i of open) {
|
|
95
|
+
const call = i.toolCallId ? findCall(i.toolCallId) : undefined;
|
|
96
|
+
if (call && (call.status === "running" || call.status === "waiting")) {
|
|
97
|
+
call.status = "waiting";
|
|
98
|
+
call.interruptId = i.id;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
case EventType.RUN_ERROR: {
|
|
105
|
+
const r = current(`error-${turns.length}`);
|
|
106
|
+
addUsage(e.usage, r);
|
|
107
|
+
// @ag-ui/client reports a stopped request as a RUN_ERROR with code "abort": that's the person stopping it, not a failure.
|
|
108
|
+
if (e.code === "abort")
|
|
109
|
+
r.aborted = true;
|
|
110
|
+
else
|
|
111
|
+
r.failure ??= runError(e.message);
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
case EventType.TEXT_MESSAGE_START:
|
|
115
|
+
if (e.role && e.role !== "assistant")
|
|
116
|
+
skipped.add(e.messageId);
|
|
117
|
+
else
|
|
118
|
+
addText(current(e.messageId), e.messageId, "");
|
|
119
|
+
break;
|
|
120
|
+
case EventType.TEXT_MESSAGE_CONTENT:
|
|
121
|
+
if (!skipped.has(e.messageId))
|
|
122
|
+
addText(current(e.messageId), e.messageId, e.delta);
|
|
123
|
+
break;
|
|
124
|
+
case EventType.TEXT_MESSAGE_CHUNK: {
|
|
125
|
+
if ((e.role && e.role !== "assistant") || (e.messageId && skipped.has(e.messageId))) {
|
|
126
|
+
if (e.messageId)
|
|
127
|
+
skipped.add(e.messageId);
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
const r = current(e.messageId ?? `text-${turns.length}`);
|
|
131
|
+
// A chunk without an id continues the last text message.
|
|
132
|
+
const id = e.messageId ?? [...r.texts.keys()].at(-1) ?? `${r.id}-text`;
|
|
133
|
+
addText(r, id, e.delta ?? "");
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
case EventType.TOOL_CALL_START:
|
|
137
|
+
addTool(current(e.parentMessageId ?? e.toolCallId), e.toolCallId, e.toolCallName);
|
|
138
|
+
break;
|
|
139
|
+
case EventType.TOOL_CALL_ARGS: {
|
|
140
|
+
const call = findCall(e.toolCallId);
|
|
141
|
+
if (call)
|
|
142
|
+
call.args += e.delta;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
case EventType.TOOL_CALL_CHUNK: {
|
|
146
|
+
if (!e.toolCallId)
|
|
147
|
+
break;
|
|
148
|
+
const call = findCall(e.toolCallId) ?? addTool(current(e.parentMessageId ?? e.toolCallId), e.toolCallId, e.toolCallName ?? "tool");
|
|
149
|
+
call.args += e.delta ?? "";
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
case EventType.TOOL_CALL_RESULT: {
|
|
153
|
+
const call = findCall(e.toolCallId);
|
|
154
|
+
// A denied or expired call stays as the person left it, whatever the agent reports afterwards.
|
|
155
|
+
if (!call || call.status === "cancelled")
|
|
156
|
+
break;
|
|
157
|
+
call.output = resultText(e.content) || undefined;
|
|
158
|
+
call.status = "succeeded";
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
case EventType.REASONING_MESSAGE_START:
|
|
162
|
+
current(e.messageId).reasoning.set(e.messageId, "");
|
|
163
|
+
break;
|
|
164
|
+
case EventType.REASONING_MESSAGE_CONTENT: {
|
|
165
|
+
const r = current(e.messageId);
|
|
166
|
+
r.reasoning.set(e.messageId, (r.reasoning.get(e.messageId) ?? "") + e.delta);
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
case EventType.REASONING_MESSAGE_CHUNK: {
|
|
170
|
+
const r = current(e.messageId ?? `reasoning-${turns.length}`);
|
|
171
|
+
const id = e.messageId ?? [...r.reasoning.keys()].at(-1) ?? `${r.id}-reasoning`;
|
|
172
|
+
r.reasoning.set(id, (r.reasoning.get(id) ?? "") + (e.delta ?? ""));
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
case EventType.MESSAGES_SNAPSHOT:
|
|
176
|
+
turns = applySnapshot(turns, e.messages);
|
|
177
|
+
break;
|
|
178
|
+
case EventType.CUSTOM: {
|
|
179
|
+
if (e.name !== "candor_card")
|
|
180
|
+
break;
|
|
181
|
+
const card = cardValue(e.value);
|
|
182
|
+
if (card)
|
|
183
|
+
current(`card-${turns.length}`).parts.push({ kind: "card", ...card });
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
// Steps, state, activity, raw events and encrypted reasoning are the app's or the agent's own business; nothing to show.
|
|
187
|
+
default:
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const lastReply = turns.findLastIndex((t) => t.reply);
|
|
192
|
+
const isOpen = (i) => !answers.has(i.id) && !expired(i, now);
|
|
193
|
+
const messages = [];
|
|
194
|
+
turns.forEach((t, i) => {
|
|
195
|
+
if (t.user)
|
|
196
|
+
messages.push(t.user);
|
|
197
|
+
if (t.reply)
|
|
198
|
+
messages.push(render(t.reply, { ...options, now, last: i === lastReply, open: open.filter(isOpen) }));
|
|
199
|
+
});
|
|
200
|
+
const waiting = options.running ? [] : open.filter(isOpen);
|
|
201
|
+
const lapsed = options.running ? [] : open.filter((i) => !answers.has(i.id) && expired(i, now));
|
|
202
|
+
return {
|
|
203
|
+
messages,
|
|
204
|
+
approvals: waiting.map((i) => approval(i, findCall, options)),
|
|
205
|
+
threadId,
|
|
206
|
+
usage,
|
|
207
|
+
notice: lapsed.length
|
|
208
|
+
? `${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
|
+
: undefined,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
/* ---------------------------------------------------------------- building a reply */
|
|
213
|
+
const emptyReply = (id) => ({ id, parts: [], texts: new Map(), reasoning: new Map(), tools: new Map() });
|
|
214
|
+
function addText(r, id, delta) {
|
|
215
|
+
if (!r.texts.has(id))
|
|
216
|
+
r.parts.push({ kind: "text", id });
|
|
217
|
+
r.texts.set(id, (r.texts.get(id) ?? "") + delta);
|
|
218
|
+
}
|
|
219
|
+
function addTool(r, id, tool) {
|
|
220
|
+
const existing = r.tools.get(id);
|
|
221
|
+
if (existing)
|
|
222
|
+
return existing;
|
|
223
|
+
const call = { id, tool, args: "", status: "running" };
|
|
224
|
+
r.tools.set(id, call);
|
|
225
|
+
r.parts.push({ kind: "tool", id });
|
|
226
|
+
return call;
|
|
227
|
+
}
|
|
228
|
+
/** True for anything a subagent sent, tagged or continuing something a subagent opened. */
|
|
229
|
+
function isSubagent(e, messages, tools, parents) {
|
|
230
|
+
if (e.type === EventType.SUBAGENT_STARTED) {
|
|
231
|
+
if (e.parentToolCallId)
|
|
232
|
+
parents.set(e.subagentRunId, e.parentToolCallId);
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
if (e.type === EventType.SUBAGENT_FINISHED)
|
|
236
|
+
return true;
|
|
237
|
+
const tagged = "subagentRunId" in e && !!e.subagentRunId;
|
|
238
|
+
const messageId = "messageId" in e ? e.messageId : undefined;
|
|
239
|
+
const toolCallId = "toolCallId" in e ? e.toolCallId : undefined;
|
|
240
|
+
const parent = "parentMessageId" in e ? e.parentMessageId : undefined;
|
|
241
|
+
if (tagged || (messageId && messages.has(messageId)) || (toolCallId && tools.has(toolCallId)) || (parent && messages.has(parent))) {
|
|
242
|
+
if (messageId && e.type !== EventType.TOOL_CALL_RESULT)
|
|
243
|
+
messages.add(messageId);
|
|
244
|
+
if (toolCallId)
|
|
245
|
+
tools.add(toolCallId);
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* A snapshot is the agent's own record of the thread, so it replaces what the
|
|
252
|
+
* stream built: its user messages start turns, everything after each one is
|
|
253
|
+
* that turn's reply. What only Candor knew about a reply (its cards, how its
|
|
254
|
+
* run ended, a denial) is kept when the snapshot still has the same user message.
|
|
255
|
+
*/
|
|
256
|
+
function applySnapshot(previous, snapshot) {
|
|
257
|
+
const byUser = new Map(previous.filter((t) => t.user).map((t) => [t.user.id, t.reply]));
|
|
258
|
+
const turns = previous[0] && !previous[0].user ? [{ reply: previous[0].reply }] : [];
|
|
259
|
+
const seen = new Set();
|
|
260
|
+
for (const m of snapshot) {
|
|
261
|
+
if (m.subagentRunId)
|
|
262
|
+
continue;
|
|
263
|
+
if (m.role === "user") {
|
|
264
|
+
turns.push({ user: { id: m.id, author: { role: "user" }, status: "complete", blocks: plainBlocks(resultText(m.content)) }, reply: byUser.get(m.id) });
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (m.role !== "assistant" && m.role !== "tool" && m.role !== "reasoning")
|
|
268
|
+
continue;
|
|
269
|
+
let t = turns.at(-1);
|
|
270
|
+
if (!t)
|
|
271
|
+
turns.push((t = {}));
|
|
272
|
+
// A reply the stream never saw is history the agent already finished.
|
|
273
|
+
const r = (t.reply ??= { ...emptyReply(m.id), outcome: "success" });
|
|
274
|
+
if (m.role === "assistant") {
|
|
275
|
+
if (m.content !== undefined) {
|
|
276
|
+
if (!r.texts.has(m.id))
|
|
277
|
+
r.parts.push({ kind: "text", id: m.id });
|
|
278
|
+
r.texts.set(m.id, m.content);
|
|
279
|
+
seen.add(m.id);
|
|
280
|
+
}
|
|
281
|
+
for (const tc of m.toolCalls ?? []) {
|
|
282
|
+
const call = addTool(r, tc.id, tc.function.name);
|
|
283
|
+
call.tool = tc.function.name;
|
|
284
|
+
call.args = tc.function.arguments;
|
|
285
|
+
seen.add(tc.id);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
else if (m.role === "tool") {
|
|
289
|
+
const call = r.tools.get(m.toolCallId);
|
|
290
|
+
if (!call || call.status === "cancelled")
|
|
291
|
+
continue;
|
|
292
|
+
const output = resultText(m.content);
|
|
293
|
+
call.output = output || undefined;
|
|
294
|
+
call.status = m.error ? "failed" : "succeeded";
|
|
295
|
+
call.error = m.error ? (firstLine(m.error) ?? "The tool failed and didn’t say why.") : undefined;
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
r.reasoning.set(m.id, m.content);
|
|
299
|
+
seen.add(m.id);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// Drop what the snapshot no longer has; cards are Candor's own and stay.
|
|
303
|
+
for (const { reply: r } of turns) {
|
|
304
|
+
if (!r)
|
|
305
|
+
continue;
|
|
306
|
+
r.parts = r.parts.filter((p) => p.kind === "card" || seen.has(p.id));
|
|
307
|
+
for (const id of r.texts.keys())
|
|
308
|
+
if (!seen.has(id))
|
|
309
|
+
r.texts.delete(id);
|
|
310
|
+
for (const id of r.reasoning.keys())
|
|
311
|
+
if (!seen.has(id))
|
|
312
|
+
r.reasoning.delete(id);
|
|
313
|
+
for (const id of r.tools.keys())
|
|
314
|
+
if (!seen.has(id))
|
|
315
|
+
r.tools.delete(id);
|
|
316
|
+
}
|
|
317
|
+
return turns;
|
|
318
|
+
}
|
|
319
|
+
/* ---------------------------------------------------------------- rendering */
|
|
320
|
+
function render(r, o) {
|
|
321
|
+
const live = o.last && !!o.running;
|
|
322
|
+
// A run that ended can't finish the tool calls it left running.
|
|
323
|
+
const ended = !live;
|
|
324
|
+
const blocks = [];
|
|
325
|
+
let calls = [];
|
|
326
|
+
const flush = () => {
|
|
327
|
+
if (calls.length)
|
|
328
|
+
blocks.push({ kind: "tools", calls });
|
|
329
|
+
calls = [];
|
|
330
|
+
};
|
|
331
|
+
for (const p of r.parts) {
|
|
332
|
+
if (p.kind === "tool") {
|
|
333
|
+
calls.push(toolCall(r.tools.get(p.id), o, ended));
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
flush();
|
|
337
|
+
if (p.kind === "card")
|
|
338
|
+
blocks.push({ kind: "card", card: p.card, props: p.props, summary: p.summary });
|
|
339
|
+
else {
|
|
340
|
+
const text = r.texts.get(p.id) ?? "";
|
|
341
|
+
if (text.trim())
|
|
342
|
+
blocks.push(...markdownBlocks(text));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
flush();
|
|
346
|
+
if (r.failure)
|
|
347
|
+
blocks.push({ kind: "degraded", reason: r.failure });
|
|
348
|
+
const reasoning = [...r.reasoning]
|
|
349
|
+
.filter(([, text]) => text.trim())
|
|
350
|
+
.map(([id, text], i, all) => reasoningEntry(`${r.id}-${id}`, text, live && i === all.length - 1));
|
|
351
|
+
const finished = r.outcome === "success" || r.outcome === "interrupt";
|
|
352
|
+
return {
|
|
353
|
+
id: r.id,
|
|
354
|
+
author: { role: "agent", agent: o.agent },
|
|
355
|
+
status: live ? "streaming" : r.failure ? "failed" : finished && !r.aborted ? "complete" : "interrupted",
|
|
356
|
+
blocks,
|
|
357
|
+
reasoning: reasoning.length ? reasoning : undefined,
|
|
358
|
+
attribution: r.model && finished ? { model: r.model } : undefined,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
function toolCall(c, o, ended) {
|
|
362
|
+
const input = parseArgs(c.args);
|
|
363
|
+
const call = {
|
|
364
|
+
id: c.id,
|
|
365
|
+
tool: c.tool,
|
|
366
|
+
title: o.toolTitle?.(c.tool, input) ?? c.tool,
|
|
367
|
+
status: c.status,
|
|
368
|
+
input: c.args ? (input === undefined ? c.args : json(input)) : undefined,
|
|
369
|
+
output: c.output,
|
|
370
|
+
outputLanguage: c.output === undefined ? undefined : "text",
|
|
371
|
+
error: c.error,
|
|
372
|
+
};
|
|
373
|
+
if (c.status === "waiting") {
|
|
374
|
+
const asking = o.open.some((i) => i.id === c.interruptId);
|
|
375
|
+
if (!asking)
|
|
376
|
+
return { ...call, status: "cancelled", error: "Nobody answered before the request closed." };
|
|
377
|
+
return call;
|
|
378
|
+
}
|
|
379
|
+
if (ended && c.status === "running")
|
|
380
|
+
return { ...call, status: "cancelled", error: "The run ended before this tool returned a result." };
|
|
381
|
+
return call;
|
|
382
|
+
}
|
|
383
|
+
function approval(i, findCall, o) {
|
|
384
|
+
const call = i.toolCallId ? findCall(i.toolCallId) : undefined;
|
|
385
|
+
const input = call ? parseArgs(call.args) : undefined;
|
|
386
|
+
const named = call ? (o.toolTitle?.(call.tool, input) ?? call.tool) : undefined;
|
|
387
|
+
return {
|
|
388
|
+
id: i.id,
|
|
389
|
+
reason: i.reason,
|
|
390
|
+
message: i.message,
|
|
391
|
+
title: i.message ?? (named ? `The agent wants to use ${named}.` : "The agent is waiting for your answer."),
|
|
392
|
+
toolCallId: i.toolCallId,
|
|
393
|
+
tool: call?.tool,
|
|
394
|
+
input,
|
|
395
|
+
expiresAt: i.expiresAt,
|
|
396
|
+
responseSchema: i.responseSchema,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
/* ---------------------------------------------------------------- helpers */
|
|
400
|
+
const expired = (i, now) => i.expiresAt !== undefined && new Date(i.expiresAt).getTime() <= now.getTime();
|
|
401
|
+
function runError(message) {
|
|
402
|
+
const line = firstLine(message);
|
|
403
|
+
return line ? `The agent stopped because of an error: ${line}` : "The agent stopped because of an error.";
|
|
404
|
+
}
|
|
405
|
+
function cardValue(v) {
|
|
406
|
+
if (!v || typeof v !== "object")
|
|
407
|
+
return undefined;
|
|
408
|
+
const { card, props, summary } = v;
|
|
409
|
+
return typeof card === "string" && typeof summary === "string" ? { card, props, summary } : undefined;
|
|
410
|
+
}
|
|
411
|
+
function resultText(content) {
|
|
412
|
+
if (typeof content === "string")
|
|
413
|
+
return content;
|
|
414
|
+
if (!Array.isArray(content))
|
|
415
|
+
return "";
|
|
416
|
+
return content.map((c) => (c.type === "text" ? c.text : `[${c.type}]`)).join("\n");
|
|
417
|
+
}
|
|
418
|
+
function parseArgs(args) {
|
|
419
|
+
if (!args.trim())
|
|
420
|
+
return undefined;
|
|
421
|
+
try {
|
|
422
|
+
return JSON.parse(args);
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
// Still streaming, or not JSON: show it as it came.
|
|
426
|
+
return undefined;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
const firstLine = (text) => text.trim().split("\n")[0] || undefined;
|
|
430
|
+
const json = (v) => (typeof v === "string" ? v : JSON.stringify(v, null, 2));
|
|
431
|
+
function reasoningEntry(id, text, streaming) {
|
|
432
|
+
const first = text.trim().split(/\n|(?<=[.!?])\s/)[0] ?? "";
|
|
433
|
+
return {
|
|
434
|
+
id,
|
|
435
|
+
title: first.length > 90 ? `${first.slice(0, 87)}…` : first,
|
|
436
|
+
body: text.trim(),
|
|
437
|
+
streaming,
|
|
438
|
+
basis: "believed",
|
|
439
|
+
assumption: "The model’s own reasoning; nothing checked it.",
|
|
440
|
+
};
|
|
441
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { UIMessage } from "ai";
|
|
2
|
+
import type { AgentIdentity, Message } from "../lib/chat.js";
|
|
3
|
+
import { inline, markdownBlocks, plainBlocks } from "../lib/safe-markdown.js";
|
|
4
|
+
export { inline, markdownBlocks, plainBlocks };
|
|
5
|
+
export type FromAiSdkOptions = {
|
|
6
|
+
/** Who the assistant is. Candor always names the agent. */
|
|
7
|
+
agent: AgentIdentity;
|
|
8
|
+
/** The chat's status from useChat, so the last reply can show as streaming or failed. */
|
|
9
|
+
chatStatus?: "submitted" | "streaming" | "ready" | "error";
|
|
10
|
+
/** When a message was sent, if your metadata says. Without it, no time is shown. */
|
|
11
|
+
createdAt?: (message: UIMessage) => string | undefined;
|
|
12
|
+
/** A plain-words title for a tool call: "Searched the docs for “region”". */
|
|
13
|
+
toolTitle?: (tool: string, input: unknown) => string | undefined;
|
|
14
|
+
/** The text version of a data part, used when its card can't be shown. */
|
|
15
|
+
summarize?: (name: string, data: unknown) => string;
|
|
16
|
+
};
|
|
17
|
+
/** Convert a whole useChat message list. */
|
|
18
|
+
export declare function fromAiSdkMessages(messages: UIMessage[], options: FromAiSdkOptions): Message[];
|
|
19
|
+
/** Convert one message. `last` marks the reply the chat status applies to. */
|
|
20
|
+
export declare function fromAiSdkMessage(message: UIMessage, options: FromAiSdkOptions & {
|
|
21
|
+
last?: boolean;
|
|
22
|
+
}): Message;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { inline, markdownBlocks, plainBlocks } from "../lib/safe-markdown.js";
|
|
2
|
+
// Re-exported for apps that use them from here. (Imported first: shadcn rewrites import paths, not export-from paths.)
|
|
3
|
+
export { inline, markdownBlocks, plainBlocks };
|
|
4
|
+
/** Convert a whole useChat message list. */
|
|
5
|
+
export function fromAiSdkMessages(messages, options) {
|
|
6
|
+
const lastAssistant = messages.findLastIndex((m) => m.role === "assistant");
|
|
7
|
+
return messages.map((m, i) => fromAiSdkMessage(m, { ...options, last: i === lastAssistant }));
|
|
8
|
+
}
|
|
9
|
+
/** Convert one message. `last` marks the reply the chat status applies to. */
|
|
10
|
+
export function fromAiSdkMessage(message, options) {
|
|
11
|
+
const blocks = [];
|
|
12
|
+
const reasoning = [];
|
|
13
|
+
const sources = [];
|
|
14
|
+
let tools = null;
|
|
15
|
+
const flushTools = () => {
|
|
16
|
+
if (tools?.length)
|
|
17
|
+
blocks.push({ kind: "tools", calls: tools });
|
|
18
|
+
tools = null;
|
|
19
|
+
};
|
|
20
|
+
for (const part of message.parts) {
|
|
21
|
+
if (part.type === "dynamic-tool" || part.type.startsWith("tool-")) {
|
|
22
|
+
tools ??= [];
|
|
23
|
+
tools.push(toolCall(part, options));
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
flushTools();
|
|
27
|
+
switch (part.type) {
|
|
28
|
+
case "text":
|
|
29
|
+
// A person's own words are kept as typed; only the assistant's text is read as markdown.
|
|
30
|
+
blocks.push(...(message.role === "user" ? plainBlocks(part.text) : markdownBlocks(part.text)));
|
|
31
|
+
break;
|
|
32
|
+
case "reasoning":
|
|
33
|
+
if (part.text.trim())
|
|
34
|
+
reasoning.push(reasoningEntry(part.id ?? `r${reasoning.length}`, part.text, part.state === "streaming"));
|
|
35
|
+
break;
|
|
36
|
+
case "source-url":
|
|
37
|
+
sources.push({ id: part.sourceId, marker: sources.length + 1, kind: "web", label: part.title ?? part.url, origin: host(part.url) });
|
|
38
|
+
break;
|
|
39
|
+
case "source-document":
|
|
40
|
+
sources.push({ id: part.sourceId, marker: sources.length + 1, kind: "document", label: part.title, origin: part.filename });
|
|
41
|
+
break;
|
|
42
|
+
case "file":
|
|
43
|
+
blocks.push({ kind: "paragraph", nodes: [{ kind: "text", text: "Attached " }, { kind: "code", text: part.filename ?? part.mediaType }] });
|
|
44
|
+
break;
|
|
45
|
+
case "step-start":
|
|
46
|
+
case "custom":
|
|
47
|
+
case "reasoning-file":
|
|
48
|
+
break;
|
|
49
|
+
default:
|
|
50
|
+
if (part.type.startsWith("data-")) {
|
|
51
|
+
const name = part.type.slice(5);
|
|
52
|
+
const data = part.data;
|
|
53
|
+
blocks.push({ kind: "card", card: name, props: data, summary: options.summarize?.(name, data) ?? `A “${name}” card.` });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
flushTools();
|
|
58
|
+
const author = message.role === "assistant" ? { role: "agent", agent: options.agent } : message.role === "user" ? { role: "user" } : { role: "system" };
|
|
59
|
+
return {
|
|
60
|
+
id: message.id,
|
|
61
|
+
author,
|
|
62
|
+
createdAt: options.createdAt?.(message),
|
|
63
|
+
status: statusOf(options),
|
|
64
|
+
blocks,
|
|
65
|
+
reasoning: reasoning.length ? reasoning : undefined,
|
|
66
|
+
sources: sources.length ? sources : undefined,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function statusOf({ last, chatStatus }) {
|
|
70
|
+
if (!last)
|
|
71
|
+
return "complete";
|
|
72
|
+
if (chatStatus === "submitted" || chatStatus === "streaming")
|
|
73
|
+
return "streaming";
|
|
74
|
+
if (chatStatus === "error")
|
|
75
|
+
return "failed";
|
|
76
|
+
return "complete";
|
|
77
|
+
}
|
|
78
|
+
const toolStatus = (p) => {
|
|
79
|
+
switch (p.state) {
|
|
80
|
+
case "input-streaming":
|
|
81
|
+
case "input-available":
|
|
82
|
+
return "running";
|
|
83
|
+
case "approval-requested":
|
|
84
|
+
return "waiting";
|
|
85
|
+
case "approval-responded":
|
|
86
|
+
return p.approval?.approved ? "running" : "cancelled";
|
|
87
|
+
case "output-available":
|
|
88
|
+
return p.preliminary ? "running" : "succeeded";
|
|
89
|
+
case "output-error":
|
|
90
|
+
return "failed";
|
|
91
|
+
case "output-denied":
|
|
92
|
+
return "cancelled";
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
function toolCall(p, options) {
|
|
96
|
+
const tool = p.toolName ?? p.type.replace(/^tool-/, "");
|
|
97
|
+
const status = toolStatus(p);
|
|
98
|
+
return {
|
|
99
|
+
id: p.toolCallId,
|
|
100
|
+
tool,
|
|
101
|
+
title: options.toolTitle?.(tool, p.input) ?? p.title ?? tool,
|
|
102
|
+
status,
|
|
103
|
+
input: p.input === undefined ? undefined : json(p.input),
|
|
104
|
+
output: p.output === undefined ? undefined : json(p.output),
|
|
105
|
+
outputLanguage: p.output === undefined || typeof p.output === "string" ? "text" : "json",
|
|
106
|
+
error: status === "failed" ? (p.errorText ?? "The tool failed and didn’t say why.") : status === "cancelled" && p.approval?.reason ? `Denied: ${p.approval.reason}` : undefined,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const json = (v) => (typeof v === "string" ? v : JSON.stringify(v, null, 2));
|
|
110
|
+
/* ---------------------------------------------------------------- reasoning */
|
|
111
|
+
function reasoningEntry(id, text, streaming) {
|
|
112
|
+
const first = text.trim().split(/\n|(?<=[.!?])\s/)[0] ?? "";
|
|
113
|
+
return {
|
|
114
|
+
id,
|
|
115
|
+
title: first.length > 90 ? `${first.slice(0, 87)}…` : first,
|
|
116
|
+
body: text.trim(),
|
|
117
|
+
streaming,
|
|
118
|
+
basis: "believed",
|
|
119
|
+
assumption: "The model’s own reasoning; nothing checked it.",
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const host = (url) => {
|
|
123
|
+
try {
|
|
124
|
+
return new URL(url).hostname;
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
import type { AgentIdentity, EntityRef, Message } from "../lib/chat.js";
|
|
3
|
+
/** Events of Candor's own, mixed into the SDK's stream. */
|
|
4
|
+
export type CandorAgentEvent =
|
|
5
|
+
/** What the user sent. Added in the browser; the SDK doesn't echo a plain prompt back. */
|
|
6
|
+
{
|
|
7
|
+
type: "candor_user";
|
|
8
|
+
id: string;
|
|
9
|
+
text: string;
|
|
10
|
+
references?: EntityRef[];
|
|
11
|
+
}
|
|
12
|
+
/** A tool call is waiting for a person to allow or deny it. */
|
|
13
|
+
| {
|
|
14
|
+
type: "candor_approval";
|
|
15
|
+
id: string;
|
|
16
|
+
toolUseId: string;
|
|
17
|
+
tool: string;
|
|
18
|
+
input: unknown;
|
|
19
|
+
/** The SDK's own sentence for the request, e.g. "Claude wants to edit notes.md". */
|
|
20
|
+
title?: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
/** Why the SDK asked, when it says. */
|
|
23
|
+
reason?: string;
|
|
24
|
+
blockedPath?: string;
|
|
25
|
+
/** Open on "Deny" and offer no one-key approve: the request grants more than it looks. */
|
|
26
|
+
defaultToNo?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/** The request was answered, or the run ended before anyone did. */
|
|
29
|
+
| {
|
|
30
|
+
type: "candor_approval_closed";
|
|
31
|
+
id: string;
|
|
32
|
+
approved: boolean;
|
|
33
|
+
reason?: string;
|
|
34
|
+
}
|
|
35
|
+
/** 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
|
+
| {
|
|
37
|
+
type: "candor_card";
|
|
38
|
+
card: string;
|
|
39
|
+
props: unknown;
|
|
40
|
+
summary: string;
|
|
41
|
+
}
|
|
42
|
+
/** The run failed outside the SDK's own messages (the request, the stream, the server). */
|
|
43
|
+
| {
|
|
44
|
+
type: "candor_error";
|
|
45
|
+
message: string;
|
|
46
|
+
};
|
|
47
|
+
export type AgentEvent = SDKMessage | CandorAgentEvent;
|
|
48
|
+
export type ClaudeApproval = Omit<Extract<CandorAgentEvent, {
|
|
49
|
+
type: "candor_approval";
|
|
50
|
+
}>, "type">;
|
|
51
|
+
/** What the SDK measured for the session so far. Cost is its estimate, not a bill. */
|
|
52
|
+
export type ClaudeAgentUsage = {
|
|
53
|
+
costUsd: number;
|
|
54
|
+
inputTokens: number;
|
|
55
|
+
outputTokens: number;
|
|
56
|
+
turns: number;
|
|
57
|
+
};
|
|
58
|
+
export type FromClaudeAgentOptions = {
|
|
59
|
+
/** Who the agent is. Candor always names the agent. */
|
|
60
|
+
agent: AgentIdentity;
|
|
61
|
+
/** A request is open now: the last reply is still streaming and its approvals can be answered. */
|
|
62
|
+
running?: boolean;
|
|
63
|
+
/** A plain-words title for a tool call: "Read notes.md". */
|
|
64
|
+
toolTitle?: (tool: string, input: unknown) => string | undefined;
|
|
65
|
+
};
|
|
66
|
+
export type ClaudeAgentState = {
|
|
67
|
+
messages: Message[];
|
|
68
|
+
/** Tool calls waiting on a person, oldest first. Empty once the request has ended. */
|
|
69
|
+
approvals: ClaudeApproval[];
|
|
70
|
+
/** Send it back with the next message to continue the same session (the SDK's `resume`). */
|
|
71
|
+
sessionId?: string;
|
|
72
|
+
usage?: ClaudeAgentUsage;
|
|
73
|
+
/** A passing problem the agent is handling itself, such as a retried API call. */
|
|
74
|
+
notice?: string;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Turns Claude Agent SDK events into Candor messages, waiting approvals and usage.
|
|
78
|
+
* Pure, so it can run on every render; useClaudeAgent calls it for you.
|
|
79
|
+
*/
|
|
80
|
+
export declare function fromClaudeAgentEvents(events: AgentEvent[], options: FromClaudeAgentOptions): ClaudeAgentState;
|