@cubos/agent-sdk-react 0.0.1136563
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/README.md +304 -0
- package/dist/blocks.d.ts +47 -0
- package/dist/context.d.ts +23 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +876 -0
- package/dist/index.js.map +15 -0
- package/dist/use-conversation-events.d.ts +47 -0
- package/dist/use-conversation-list.d.ts +32 -0
- package/dist/use-conversation.d.ts +358 -0
- package/dist/use-identity.d.ts +9 -0
- package/package.json +51 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,876 @@
|
|
|
1
|
+
// src/blocks.ts
|
|
2
|
+
import { createElement, Fragment } from "react";
|
|
3
|
+
function renderBlocks(message, options = {}) {
|
|
4
|
+
if (message.blocks === undefined)
|
|
5
|
+
return null;
|
|
6
|
+
const out = [];
|
|
7
|
+
for (const [index, block] of message.blocks.entries()) {
|
|
8
|
+
const node = renderBlock(block, `${message.id}:${index}`, options);
|
|
9
|
+
if (node !== null)
|
|
10
|
+
out.push(node);
|
|
11
|
+
}
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
14
|
+
function renderBlock(block, key, options) {
|
|
15
|
+
if (block.type === "markdown") {
|
|
16
|
+
if (block.text.trim() === "")
|
|
17
|
+
return null;
|
|
18
|
+
const node = options.renderMarkdown?.(block.text) ?? block.text;
|
|
19
|
+
return createElement(Fragment, { key }, options.wrapBlock?.(node, block) ?? node);
|
|
20
|
+
}
|
|
21
|
+
const render = options.components?.[block.tag];
|
|
22
|
+
if (!render)
|
|
23
|
+
return null;
|
|
24
|
+
if (options.wrapBlock === undefined)
|
|
25
|
+
return createElement(render, { ...block.props, key });
|
|
26
|
+
return createElement(Fragment, { key }, options.wrapBlock(createElement(render, block.props), block));
|
|
27
|
+
}
|
|
28
|
+
// src/context.tsx
|
|
29
|
+
import { createUserClient } from "@cubos/agent-sdk";
|
|
30
|
+
import { createContext, createElement as createElement2, useContext, useMemo, useRef } from "react";
|
|
31
|
+
var AgentContext = createContext(null);
|
|
32
|
+
function AgentProvider({ children, ...options }) {
|
|
33
|
+
const credentialRef = useRef(options);
|
|
34
|
+
credentialRef.current = options;
|
|
35
|
+
const client = useMemo(() => createUserClient({
|
|
36
|
+
baseUrl: options.baseUrl,
|
|
37
|
+
tenant: options.tenant,
|
|
38
|
+
fetch: options.fetch,
|
|
39
|
+
timeoutMs: options.timeoutMs,
|
|
40
|
+
getToken: (opts) => {
|
|
41
|
+
const current = credentialRef.current;
|
|
42
|
+
return "getToken" in current ? current.getToken(opts) : current.token;
|
|
43
|
+
}
|
|
44
|
+
}), [options.baseUrl, options.tenant, options.fetch, options.timeoutMs]);
|
|
45
|
+
return createElement2(AgentContext.Provider, { value: client }, children);
|
|
46
|
+
}
|
|
47
|
+
function useAgentClient() {
|
|
48
|
+
const client = useContext(AgentContext);
|
|
49
|
+
if (!client) {
|
|
50
|
+
throw new Error("No AgentProvider found. Wrap your app in <AgentProvider baseUrl=… getToken=…>.");
|
|
51
|
+
}
|
|
52
|
+
return client;
|
|
53
|
+
}
|
|
54
|
+
// src/use-conversation.ts
|
|
55
|
+
import {
|
|
56
|
+
mergeToolActivities,
|
|
57
|
+
mergeVoiceMessages
|
|
58
|
+
} from "@cubos/agent-sdk";
|
|
59
|
+
import { useCallback, useEffect, useMemo as useMemo2, useRef as useRef2, useState } from "react";
|
|
60
|
+
var IDLE = { isProcessing: false, hasPendingTurn: false };
|
|
61
|
+
function mergeMessages(existing, incoming) {
|
|
62
|
+
const byId = new Map(existing.map((m) => [m.id, m]));
|
|
63
|
+
for (const m of incoming)
|
|
64
|
+
byId.set(m.id, m);
|
|
65
|
+
const held = [...byId.values()];
|
|
66
|
+
const arrived = new Set(held.filter((m) => !isOptimistic(m)).map((m) => optimisticIdFor(m)));
|
|
67
|
+
const ordered = held.filter((m) => !(isOptimistic(m) && arrived.has(m.id))).sort((a, b) => a.seq - b.seq);
|
|
68
|
+
return mergeVoiceMessages(ordered);
|
|
69
|
+
}
|
|
70
|
+
function isOptimistic(message) {
|
|
71
|
+
return message.id.startsWith("optimistic:");
|
|
72
|
+
}
|
|
73
|
+
function trailOf(message, messages, activity) {
|
|
74
|
+
if (message.role !== "agent" || activity.length === 0)
|
|
75
|
+
return [];
|
|
76
|
+
let previousSeq = -1;
|
|
77
|
+
for (const m of messages) {
|
|
78
|
+
if (m.seq >= message.seq)
|
|
79
|
+
break;
|
|
80
|
+
previousSeq = m.seq;
|
|
81
|
+
}
|
|
82
|
+
return activity.filter((a) => a.seq > previousSeq && a.seq < message.seq);
|
|
83
|
+
}
|
|
84
|
+
function trailAfterLast(messages, activity) {
|
|
85
|
+
if (activity.length === 0)
|
|
86
|
+
return [];
|
|
87
|
+
let lastSeq = -1;
|
|
88
|
+
for (const m of messages) {
|
|
89
|
+
if (isOptimistic(m))
|
|
90
|
+
continue;
|
|
91
|
+
if (m.seq > lastSeq)
|
|
92
|
+
lastSeq = m.seq;
|
|
93
|
+
}
|
|
94
|
+
return activity.filter((a) => a.seq > lastSeq);
|
|
95
|
+
}
|
|
96
|
+
function turnIsRunning(messages, activity) {
|
|
97
|
+
if (messages.some(isOptimistic))
|
|
98
|
+
return true;
|
|
99
|
+
return activity.isProcessing || activity.hasPendingTurn;
|
|
100
|
+
}
|
|
101
|
+
function mergePlans(existing, incoming) {
|
|
102
|
+
const bySeq = new Map;
|
|
103
|
+
for (const plan of existing)
|
|
104
|
+
bySeq.set(plan.seq, plan);
|
|
105
|
+
for (const plan of incoming)
|
|
106
|
+
bySeq.set(plan.seq, plan);
|
|
107
|
+
return [...bySeq.values()].sort((a, b) => a.seq - b.seq);
|
|
108
|
+
}
|
|
109
|
+
function planOf(message, messages, plans) {
|
|
110
|
+
if (message.role !== "agent" || plans.length === 0)
|
|
111
|
+
return null;
|
|
112
|
+
let previousSeq = -1;
|
|
113
|
+
for (const m of messages) {
|
|
114
|
+
if (m.seq >= message.seq)
|
|
115
|
+
break;
|
|
116
|
+
previousSeq = m.seq;
|
|
117
|
+
}
|
|
118
|
+
let found = null;
|
|
119
|
+
for (const plan of plans) {
|
|
120
|
+
if (plan.seq > previousSeq && plan.seq < message.seq)
|
|
121
|
+
found = plan.todos;
|
|
122
|
+
}
|
|
123
|
+
return found;
|
|
124
|
+
}
|
|
125
|
+
function planAfterLast(messages, plans) {
|
|
126
|
+
if (plans.length === 0)
|
|
127
|
+
return null;
|
|
128
|
+
let lastSeq = -1;
|
|
129
|
+
for (const m of messages) {
|
|
130
|
+
if (isOptimistic(m))
|
|
131
|
+
continue;
|
|
132
|
+
if (m.seq > lastSeq)
|
|
133
|
+
lastSeq = m.seq;
|
|
134
|
+
}
|
|
135
|
+
let found = null;
|
|
136
|
+
for (const plan of plans) {
|
|
137
|
+
if (plan.seq > lastSeq)
|
|
138
|
+
found = plan.todos;
|
|
139
|
+
}
|
|
140
|
+
return found;
|
|
141
|
+
}
|
|
142
|
+
function useConversation(conversationId, options = {}) {
|
|
143
|
+
const pageSize = options.pageSize ?? 50;
|
|
144
|
+
const { agentSlug } = options;
|
|
145
|
+
const declareTools = options.declareClientTools ?? true;
|
|
146
|
+
const client = useAgentClient();
|
|
147
|
+
const [conversation, setConversation] = useState(null);
|
|
148
|
+
const [messages, setMessages] = useState([]);
|
|
149
|
+
const [toolActivity, setToolActivity] = useState([]);
|
|
150
|
+
const [plans, setPlans] = useState([]);
|
|
151
|
+
const [todos, setTodos] = useState([]);
|
|
152
|
+
const [activity, setActivity] = useState(IDLE);
|
|
153
|
+
const [isLoading, setIsLoading] = useState(conversationId !== null);
|
|
154
|
+
const [error, setError] = useState(null);
|
|
155
|
+
const [isSending, setIsSending] = useState(false);
|
|
156
|
+
const [hasOlder, setHasOlder] = useState(false);
|
|
157
|
+
const [isLoadingOlder, setIsLoadingOlder] = useState(false);
|
|
158
|
+
const [workspaceRevision, setWorkspaceRevision] = useState(0);
|
|
159
|
+
const onCreated = useRef2(options.onCreated);
|
|
160
|
+
onCreated.current = options.onCreated;
|
|
161
|
+
const adopting = useRef2(null);
|
|
162
|
+
const oldestSeq = useRef2(null);
|
|
163
|
+
const loadingOlder = useRef2(false);
|
|
164
|
+
const cursor = useRef2(null);
|
|
165
|
+
const hasOlderRef = useRef2(false);
|
|
166
|
+
const pendingIds = useRef2(new Set);
|
|
167
|
+
const clientTools = useRef2(options.clientTools);
|
|
168
|
+
clientTools.current = options.clientTools;
|
|
169
|
+
const components = useRef2(options.components);
|
|
170
|
+
components.current = options.components;
|
|
171
|
+
const onClientToolError = useRef2(options.onClientToolError);
|
|
172
|
+
onClientToolError.current = options.onClientToolError;
|
|
173
|
+
const heldContext = useRef2(undefined);
|
|
174
|
+
const sentContext = useRef2(null);
|
|
175
|
+
const isTurnRunningRef = useRef2(false);
|
|
176
|
+
const flushContext = useCallback(async (id) => {
|
|
177
|
+
const held = heldContext.current;
|
|
178
|
+
if (held === undefined)
|
|
179
|
+
return;
|
|
180
|
+
const sent = sentContext.current;
|
|
181
|
+
if (sent?.id === id && sent.value === held)
|
|
182
|
+
return;
|
|
183
|
+
sentContext.current = { id, value: held };
|
|
184
|
+
try {
|
|
185
|
+
await client.setContext(id, held);
|
|
186
|
+
} catch {
|
|
187
|
+
sentContext.current = null;
|
|
188
|
+
}
|
|
189
|
+
}, [client]);
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
heldContext.current = options.context;
|
|
192
|
+
if (conversationId !== null && isTurnRunningRef.current)
|
|
193
|
+
flushContext(conversationId);
|
|
194
|
+
}, [options.context, conversationId, flushContext]);
|
|
195
|
+
const toolsKey = useMemo2(() => stableKey(options.clientTools), [options.clientTools]);
|
|
196
|
+
const handlers = useRef2({});
|
|
197
|
+
const declaredKey = useRef2(null);
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
Object.assign(handlers.current, options.clientTools ?? {});
|
|
200
|
+
}, [options.clientTools]);
|
|
201
|
+
const flushClientTools = useCallback(async (id) => {
|
|
202
|
+
if (!declareTools || toolsKey === null)
|
|
203
|
+
return;
|
|
204
|
+
const last = declaredKey.current;
|
|
205
|
+
if (last?.id === id && last.key === toolsKey)
|
|
206
|
+
return;
|
|
207
|
+
declaredKey.current = { id, key: toolsKey };
|
|
208
|
+
try {
|
|
209
|
+
await client.setClientTools(id, clientTools.current ?? {});
|
|
210
|
+
} catch (err) {
|
|
211
|
+
declaredKey.current = null;
|
|
212
|
+
onClientToolError.current?.(err);
|
|
213
|
+
}
|
|
214
|
+
}, [client, declareTools, toolsKey]);
|
|
215
|
+
const syncClientTools = useCallback(async () => {
|
|
216
|
+
if (conversationId !== null)
|
|
217
|
+
await flushClientTools(conversationId);
|
|
218
|
+
}, [conversationId, flushClientTools]);
|
|
219
|
+
const libraries = useRef2(options.componentLibraries);
|
|
220
|
+
libraries.current = options.componentLibraries;
|
|
221
|
+
const librariesKey = options.componentLibraries === undefined ? null : JSON.stringify(options.componentLibraries);
|
|
222
|
+
const toolSession = useRef2(null);
|
|
223
|
+
const registered = useRef2(null);
|
|
224
|
+
useEffect(() => {
|
|
225
|
+
const adopted = conversationId !== null && conversationId === adopting.current;
|
|
226
|
+
adopting.current = null;
|
|
227
|
+
if (!adopted) {
|
|
228
|
+
setConversation(null);
|
|
229
|
+
setMessages([]);
|
|
230
|
+
setToolActivity([]);
|
|
231
|
+
setTodos([]);
|
|
232
|
+
setPlans([]);
|
|
233
|
+
setActivity(IDLE);
|
|
234
|
+
pendingIds.current.clear();
|
|
235
|
+
}
|
|
236
|
+
setError(null);
|
|
237
|
+
setHasOlder(false);
|
|
238
|
+
setIsLoadingOlder(false);
|
|
239
|
+
oldestSeq.current = null;
|
|
240
|
+
loadingOlder.current = false;
|
|
241
|
+
cursor.current = null;
|
|
242
|
+
hasOlderRef.current = false;
|
|
243
|
+
if (conversationId === null) {
|
|
244
|
+
setIsLoading(false);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
setIsLoading(true);
|
|
248
|
+
const controller = new AbortController;
|
|
249
|
+
let subscription = null;
|
|
250
|
+
let closed = false;
|
|
251
|
+
(async () => {
|
|
252
|
+
try {
|
|
253
|
+
const page = await client.loadHistory(conversationId, {
|
|
254
|
+
pageSize,
|
|
255
|
+
signal: controller.signal
|
|
256
|
+
});
|
|
257
|
+
if (controller.signal.aborted)
|
|
258
|
+
return;
|
|
259
|
+
setMessages((prev) => mergeMessages(prev, page.messages));
|
|
260
|
+
setToolActivity((prev) => mergeToolActivities([...prev, ...page.toolActivity]));
|
|
261
|
+
setPlans((prev) => mergePlans(prev, page.plans));
|
|
262
|
+
oldestSeq.current = page.oldestSeq;
|
|
263
|
+
cursor.current = page.latestChangeSeq;
|
|
264
|
+
hasOlderRef.current = page.hasOlder;
|
|
265
|
+
setHasOlder(page.hasOlder);
|
|
266
|
+
setIsLoading(false);
|
|
267
|
+
if (closed)
|
|
268
|
+
return;
|
|
269
|
+
subscription = client.subscribe(conversationId, {
|
|
270
|
+
onOpen: () => toolSession.current?.poke(),
|
|
271
|
+
onClientToolCall: () => toolSession.current?.poke(),
|
|
272
|
+
onEvent: (event) => {
|
|
273
|
+
if (event.type === "workspace_root")
|
|
274
|
+
setWorkspaceRevision((n) => n + 1);
|
|
275
|
+
},
|
|
276
|
+
onToolActivity: (activity2) => {
|
|
277
|
+
setToolActivity((prev) => mergeToolActivities([...prev, activity2]));
|
|
278
|
+
},
|
|
279
|
+
onMessage: (message) => {
|
|
280
|
+
pendingIds.current.delete(optimisticIdFor(message));
|
|
281
|
+
setMessages((prev) => mergeMessages(dropOptimisticEcho(prev, message), [message]));
|
|
282
|
+
setIsLoading(false);
|
|
283
|
+
},
|
|
284
|
+
onTodos: (todos2, seq) => {
|
|
285
|
+
setTodos(todos2);
|
|
286
|
+
setPlans((prev) => mergePlans(prev, [{ todos: todos2, seq }]));
|
|
287
|
+
},
|
|
288
|
+
onActivity: setActivity,
|
|
289
|
+
onConversation: setConversation,
|
|
290
|
+
onCursor: (changeSeq) => {
|
|
291
|
+
if (cursor.current === null || changeSeq > cursor.current) {
|
|
292
|
+
cursor.current = changeSeq;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}, { since: page.latestChangeSeq ?? undefined });
|
|
296
|
+
} catch (err) {
|
|
297
|
+
if (controller.signal.aborted)
|
|
298
|
+
return;
|
|
299
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
300
|
+
setIsLoading(false);
|
|
301
|
+
}
|
|
302
|
+
})();
|
|
303
|
+
return () => {
|
|
304
|
+
closed = true;
|
|
305
|
+
controller.abort();
|
|
306
|
+
subscription?.close();
|
|
307
|
+
};
|
|
308
|
+
}, [client, conversationId, pageSize]);
|
|
309
|
+
const snapshot = useRef2({
|
|
310
|
+
messages,
|
|
311
|
+
toolActivity,
|
|
312
|
+
plans,
|
|
313
|
+
oldestSeq,
|
|
314
|
+
cursor,
|
|
315
|
+
hasOlderRef
|
|
316
|
+
});
|
|
317
|
+
snapshot.current.messages = messages;
|
|
318
|
+
snapshot.current.toolActivity = toolActivity;
|
|
319
|
+
snapshot.current.plans = plans;
|
|
320
|
+
const persist = useCallback((id) => {
|
|
321
|
+
const { messages: held, toolActivity: trail, plans: planned } = snapshot.current;
|
|
322
|
+
client.saveHistory(id, {
|
|
323
|
+
toolActivity: trail,
|
|
324
|
+
plans: planned,
|
|
325
|
+
messages: held.filter((m) => !m.id.startsWith("optimistic:")),
|
|
326
|
+
oldestSeq: oldestSeq.current,
|
|
327
|
+
latestChangeSeq: cursor.current,
|
|
328
|
+
hasOlder: hasOlderRef.current
|
|
329
|
+
});
|
|
330
|
+
}, [client]);
|
|
331
|
+
useEffect(() => {
|
|
332
|
+
if (conversationId === null || messages.length === 0)
|
|
333
|
+
return;
|
|
334
|
+
const timer = setTimeout(() => persist(conversationId), 400);
|
|
335
|
+
return () => clearTimeout(timer);
|
|
336
|
+
}, [conversationId, messages, persist]);
|
|
337
|
+
useEffect(() => {
|
|
338
|
+
if (conversationId === null)
|
|
339
|
+
return;
|
|
340
|
+
return () => persist(conversationId);
|
|
341
|
+
}, [conversationId, persist]);
|
|
342
|
+
useEffect(() => {
|
|
343
|
+
const current = libraries.current;
|
|
344
|
+
if (conversationId === null || librariesKey === null || current === undefined)
|
|
345
|
+
return;
|
|
346
|
+
const stamp = `${conversationId}\x00${librariesKey}`;
|
|
347
|
+
if (registered.current === stamp)
|
|
348
|
+
return;
|
|
349
|
+
const controller = new AbortController;
|
|
350
|
+
client.setComponentLibraries(conversationId, current, controller.signal).then((enabled) => {
|
|
351
|
+
registered.current = stamp;
|
|
352
|
+
warnMissingRenderers(enabled.tags, components.current);
|
|
353
|
+
}).catch((err) => {
|
|
354
|
+
if (controller.signal.aborted)
|
|
355
|
+
return;
|
|
356
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
357
|
+
});
|
|
358
|
+
return () => controller.abort();
|
|
359
|
+
}, [client, conversationId, librariesKey]);
|
|
360
|
+
const servesTools = options.clientTools !== undefined;
|
|
361
|
+
const flushToolsRef = useRef2(flushClientTools);
|
|
362
|
+
flushToolsRef.current = flushClientTools;
|
|
363
|
+
useEffect(() => {
|
|
364
|
+
if (conversationId === null || !servesTools)
|
|
365
|
+
return;
|
|
366
|
+
let session = null;
|
|
367
|
+
let cancelled = false;
|
|
368
|
+
(async () => {
|
|
369
|
+
try {
|
|
370
|
+
const started = await client.serveClientTools(conversationId, {
|
|
371
|
+
tools: handlers.current,
|
|
372
|
+
watch: false,
|
|
373
|
+
declare: false,
|
|
374
|
+
onError: (err) => onClientToolError.current?.(err)
|
|
375
|
+
});
|
|
376
|
+
flushToolsRef.current(conversationId);
|
|
377
|
+
if (cancelled) {
|
|
378
|
+
started.stop();
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
session = started;
|
|
382
|
+
toolSession.current = started;
|
|
383
|
+
started.poke();
|
|
384
|
+
} catch (err) {
|
|
385
|
+
if (!cancelled)
|
|
386
|
+
onClientToolError.current?.(err);
|
|
387
|
+
}
|
|
388
|
+
})();
|
|
389
|
+
return () => {
|
|
390
|
+
cancelled = true;
|
|
391
|
+
toolSession.current = null;
|
|
392
|
+
session?.stop();
|
|
393
|
+
};
|
|
394
|
+
}, [client, conversationId, servesTools]);
|
|
395
|
+
const loadOlder = useCallback(async () => {
|
|
396
|
+
if (conversationId === null || loadingOlder.current)
|
|
397
|
+
return 0;
|
|
398
|
+
const before = oldestSeq.current;
|
|
399
|
+
if (before === null)
|
|
400
|
+
return 0;
|
|
401
|
+
loadingOlder.current = true;
|
|
402
|
+
setIsLoadingOlder(true);
|
|
403
|
+
try {
|
|
404
|
+
const page = await client.listMessagesPage(conversationId, {
|
|
405
|
+
before,
|
|
406
|
+
limit: pageSize
|
|
407
|
+
});
|
|
408
|
+
setMessages((prev) => mergeMessages(prev, page.messages));
|
|
409
|
+
setToolActivity((prev) => mergeToolActivities([...prev, ...page.toolActivity]));
|
|
410
|
+
setPlans((prev) => mergePlans(prev, page.plans));
|
|
411
|
+
if (page.oldestSeq !== null)
|
|
412
|
+
oldestSeq.current = page.oldestSeq;
|
|
413
|
+
hasOlderRef.current = page.hasOlder;
|
|
414
|
+
setHasOlder(page.hasOlder);
|
|
415
|
+
return page.messages.length;
|
|
416
|
+
} catch (err) {
|
|
417
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
418
|
+
return 0;
|
|
419
|
+
} finally {
|
|
420
|
+
loadingOlder.current = false;
|
|
421
|
+
setIsLoadingOlder(false);
|
|
422
|
+
}
|
|
423
|
+
}, [client, conversationId, pageSize]);
|
|
424
|
+
const target = useCallback(async () => {
|
|
425
|
+
if (conversationId !== null)
|
|
426
|
+
return conversationId;
|
|
427
|
+
const created = await client.createConversation({
|
|
428
|
+
agentSlug,
|
|
429
|
+
...libraries.current === undefined ? {} : { componentLibraries: libraries.current }
|
|
430
|
+
});
|
|
431
|
+
if (libraries.current !== undefined && librariesKey !== null) {
|
|
432
|
+
registered.current = `${created.id}\x00${librariesKey}`;
|
|
433
|
+
client.listComponentLibraries(created.id).then((enabled) => warnMissingRenderers(enabled.tags, components.current)).catch(() => {});
|
|
434
|
+
}
|
|
435
|
+
if (clientTools.current !== undefined && declareTools) {
|
|
436
|
+
await client.setClientTools(created.id, clientTools.current);
|
|
437
|
+
}
|
|
438
|
+
adopting.current = created.id;
|
|
439
|
+
setConversation(created);
|
|
440
|
+
await onCreated.current?.(created);
|
|
441
|
+
return created.id;
|
|
442
|
+
}, [client, conversationId, agentSlug, librariesKey, declareTools]);
|
|
443
|
+
const send = useCallback(async (content) => {
|
|
444
|
+
const text = content.trim();
|
|
445
|
+
if (!text)
|
|
446
|
+
return;
|
|
447
|
+
const optimistic = {
|
|
448
|
+
id: `optimistic:${text}`,
|
|
449
|
+
role: "user",
|
|
450
|
+
content: text,
|
|
451
|
+
attachments: [],
|
|
452
|
+
seq: Number.MAX_SAFE_INTEGER,
|
|
453
|
+
at: new Date().toISOString()
|
|
454
|
+
};
|
|
455
|
+
pendingIds.current.add(optimistic.id);
|
|
456
|
+
setMessages((prev) => mergeMessages(prev, [optimistic]));
|
|
457
|
+
setIsSending(true);
|
|
458
|
+
setError(null);
|
|
459
|
+
setActivity((prev) => ({ ...prev, hasPendingTurn: true }));
|
|
460
|
+
try {
|
|
461
|
+
const id = await target();
|
|
462
|
+
await flushContext(id);
|
|
463
|
+
await flushClientTools(id);
|
|
464
|
+
await client.sendMessage(id, text);
|
|
465
|
+
} catch (err) {
|
|
466
|
+
pendingIds.current.delete(optimistic.id);
|
|
467
|
+
setMessages((prev) => prev.filter((m) => m.id !== optimistic.id));
|
|
468
|
+
setActivity((prev) => ({ ...prev, hasPendingTurn: false }));
|
|
469
|
+
const wrapped = err instanceof Error ? err : new Error(String(err));
|
|
470
|
+
setError(wrapped);
|
|
471
|
+
throw wrapped;
|
|
472
|
+
} finally {
|
|
473
|
+
setIsSending(false);
|
|
474
|
+
}
|
|
475
|
+
}, [client, target, flushContext, flushClientTools]);
|
|
476
|
+
const sendAudio = useCallback(async (audio) => {
|
|
477
|
+
if (audio.size === 0)
|
|
478
|
+
return;
|
|
479
|
+
setIsSending(true);
|
|
480
|
+
setError(null);
|
|
481
|
+
setActivity((prev) => ({ ...prev, hasPendingTurn: true }));
|
|
482
|
+
try {
|
|
483
|
+
const id = await target();
|
|
484
|
+
await flushContext(id);
|
|
485
|
+
await flushClientTools(id);
|
|
486
|
+
await client.sendAudio(id, audio);
|
|
487
|
+
} catch (err) {
|
|
488
|
+
setActivity((prev) => ({ ...prev, hasPendingTurn: false }));
|
|
489
|
+
const wrapped = err instanceof Error ? err : new Error(String(err));
|
|
490
|
+
setError(wrapped);
|
|
491
|
+
throw wrapped;
|
|
492
|
+
} finally {
|
|
493
|
+
setIsSending(false);
|
|
494
|
+
}
|
|
495
|
+
}, [client, target, flushContext, flushClientTools]);
|
|
496
|
+
const sendImages = useCallback(async (images, caption) => {
|
|
497
|
+
if (images.length === 0)
|
|
498
|
+
return;
|
|
499
|
+
setIsSending(true);
|
|
500
|
+
setError(null);
|
|
501
|
+
setActivity((prev) => ({ ...prev, hasPendingTurn: true }));
|
|
502
|
+
try {
|
|
503
|
+
const id = await target();
|
|
504
|
+
await flushContext(id);
|
|
505
|
+
await flushClientTools(id);
|
|
506
|
+
await client.sendImages(id, images, { caption });
|
|
507
|
+
} catch (err) {
|
|
508
|
+
setActivity((prev) => ({ ...prev, hasPendingTurn: false }));
|
|
509
|
+
const wrapped = err instanceof Error ? err : new Error(String(err));
|
|
510
|
+
setError(wrapped);
|
|
511
|
+
throw wrapped;
|
|
512
|
+
} finally {
|
|
513
|
+
setIsSending(false);
|
|
514
|
+
}
|
|
515
|
+
}, [client, target, flushContext, flushClientTools]);
|
|
516
|
+
const listFiles = useCallback(async (path) => {
|
|
517
|
+
if (conversationId === null)
|
|
518
|
+
return [];
|
|
519
|
+
const dir = await client.listFiles(conversationId, { path });
|
|
520
|
+
return dir.entries;
|
|
521
|
+
}, [client, conversationId]);
|
|
522
|
+
const readFile = useCallback(async (path) => {
|
|
523
|
+
if (conversationId === null) {
|
|
524
|
+
throw new Error("There is no conversation yet, so there are no files to read.");
|
|
525
|
+
}
|
|
526
|
+
return await client.readFile(conversationId, path);
|
|
527
|
+
}, [client, conversationId]);
|
|
528
|
+
const writeFiles = useCallback(async (files) => {
|
|
529
|
+
if (files.length === 0)
|
|
530
|
+
return;
|
|
531
|
+
const id = await target();
|
|
532
|
+
await client.writeFiles(id, files);
|
|
533
|
+
setWorkspaceRevision((n) => n + 1);
|
|
534
|
+
}, [client, target]);
|
|
535
|
+
const deleteFile = useCallback(async (path) => {
|
|
536
|
+
if (conversationId === null)
|
|
537
|
+
return;
|
|
538
|
+
await client.deleteFile(conversationId, path);
|
|
539
|
+
setWorkspaceRevision((n) => n + 1);
|
|
540
|
+
}, [client, conversationId]);
|
|
541
|
+
const moveFile = useCallback(async (from, to) => {
|
|
542
|
+
if (conversationId === null)
|
|
543
|
+
return;
|
|
544
|
+
await client.moveFile(conversationId, from, to);
|
|
545
|
+
setWorkspaceRevision((n) => n + 1);
|
|
546
|
+
}, [client, conversationId]);
|
|
547
|
+
const steer = useCallback(async (content) => {
|
|
548
|
+
const text = content.trim();
|
|
549
|
+
if (!text || conversationId === null)
|
|
550
|
+
return;
|
|
551
|
+
await client.steer(conversationId, text);
|
|
552
|
+
}, [client, conversationId]);
|
|
553
|
+
const trailFor = useCallback((message) => trailOf(message, messages, toolActivity), [messages, toolActivity]);
|
|
554
|
+
const pendingTrail = useMemo2(() => trailAfterLast(messages, toolActivity), [messages, toolActivity]);
|
|
555
|
+
const planFor = useCallback((message) => planOf(message, messages, plans), [messages, plans]);
|
|
556
|
+
const pendingPlan = useMemo2(() => planAfterLast(messages, plans), [messages, plans]);
|
|
557
|
+
const isTurnRunning = useMemo2(() => turnIsRunning(messages, activity), [messages, activity]);
|
|
558
|
+
isTurnRunningRef.current = isTurnRunning;
|
|
559
|
+
const renderMessage = useCallback((message) => renderBlocks(message, {
|
|
560
|
+
components: components.current,
|
|
561
|
+
renderMarkdown: options.renderMarkdown,
|
|
562
|
+
wrapBlock: options.wrapBlock
|
|
563
|
+
}), [options.renderMarkdown, options.wrapBlock]);
|
|
564
|
+
return useMemo2(() => ({
|
|
565
|
+
conversation,
|
|
566
|
+
messages,
|
|
567
|
+
toolActivity,
|
|
568
|
+
trailFor,
|
|
569
|
+
pendingTrail,
|
|
570
|
+
planFor,
|
|
571
|
+
pendingPlan,
|
|
572
|
+
todos,
|
|
573
|
+
activity,
|
|
574
|
+
isTurnRunning,
|
|
575
|
+
isLoading,
|
|
576
|
+
hasOlder,
|
|
577
|
+
isLoadingOlder,
|
|
578
|
+
loadOlder,
|
|
579
|
+
error,
|
|
580
|
+
send,
|
|
581
|
+
sendAudio,
|
|
582
|
+
sendImages,
|
|
583
|
+
steer,
|
|
584
|
+
isSending,
|
|
585
|
+
renderMessage,
|
|
586
|
+
syncClientTools,
|
|
587
|
+
workspaceRevision,
|
|
588
|
+
listFiles,
|
|
589
|
+
readFile,
|
|
590
|
+
writeFiles,
|
|
591
|
+
deleteFile,
|
|
592
|
+
moveFile
|
|
593
|
+
}), [
|
|
594
|
+
conversation,
|
|
595
|
+
messages,
|
|
596
|
+
toolActivity,
|
|
597
|
+
trailFor,
|
|
598
|
+
pendingTrail,
|
|
599
|
+
planFor,
|
|
600
|
+
pendingPlan,
|
|
601
|
+
todos,
|
|
602
|
+
activity,
|
|
603
|
+
isTurnRunning,
|
|
604
|
+
isLoading,
|
|
605
|
+
hasOlder,
|
|
606
|
+
isLoadingOlder,
|
|
607
|
+
loadOlder,
|
|
608
|
+
error,
|
|
609
|
+
send,
|
|
610
|
+
sendAudio,
|
|
611
|
+
sendImages,
|
|
612
|
+
steer,
|
|
613
|
+
isSending,
|
|
614
|
+
renderMessage,
|
|
615
|
+
syncClientTools,
|
|
616
|
+
workspaceRevision,
|
|
617
|
+
listFiles,
|
|
618
|
+
readFile,
|
|
619
|
+
writeFiles,
|
|
620
|
+
deleteFile,
|
|
621
|
+
moveFile
|
|
622
|
+
]);
|
|
623
|
+
}
|
|
624
|
+
function optimisticIdFor(message) {
|
|
625
|
+
return `optimistic:${message.content.trim()}`;
|
|
626
|
+
}
|
|
627
|
+
function dropOptimisticEcho(messages, real) {
|
|
628
|
+
if (real.role !== "user")
|
|
629
|
+
return messages;
|
|
630
|
+
const echoId = optimisticIdFor(real);
|
|
631
|
+
return messages.filter((m) => m.id !== echoId);
|
|
632
|
+
}
|
|
633
|
+
function warnMissingRenderers(tags, components) {
|
|
634
|
+
const missing = tags.filter((tag) => components?.[tag] === undefined);
|
|
635
|
+
if (missing.length === 0)
|
|
636
|
+
return;
|
|
637
|
+
console.warn(`[cubos-agent] The enabled component libraries offer ${missing.map((t) => `<${t}>`).join(", ")}, which this app has no renderer for. The agent may use them ` + "and nothing will be drawn. Add them to `components`, or drop the library " + "that declares them from `componentLibraries`.");
|
|
638
|
+
}
|
|
639
|
+
function stableKey(tools) {
|
|
640
|
+
if (tools === undefined)
|
|
641
|
+
return null;
|
|
642
|
+
return JSON.stringify(Object.entries(tools).map(([name, tool]) => [
|
|
643
|
+
name,
|
|
644
|
+
tool.description,
|
|
645
|
+
tool.inputSchema,
|
|
646
|
+
tool.outputSchema,
|
|
647
|
+
tool.readOnlyHint,
|
|
648
|
+
tool.destructiveHint,
|
|
649
|
+
tool.idempotentHint,
|
|
650
|
+
tool.timeoutSeconds
|
|
651
|
+
]));
|
|
652
|
+
}
|
|
653
|
+
// src/use-conversation-events.ts
|
|
654
|
+
import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef3, useState as useState2 } from "react";
|
|
655
|
+
function useConversationEvents(conversationId, options = {}) {
|
|
656
|
+
const client = useAgentClient();
|
|
657
|
+
const pageSize = options.pageSize ?? 50;
|
|
658
|
+
const [events, setEvents] = useState2([]);
|
|
659
|
+
const [initialLoaded, setInitialLoaded] = useState2(false);
|
|
660
|
+
const [hasOlder, setHasOlder] = useState2(false);
|
|
661
|
+
const [isLoadingOlder, setIsLoadingOlder] = useState2(false);
|
|
662
|
+
const [activity, setActivity] = useState2(IDLE2);
|
|
663
|
+
const oldestSeq = useRef3(null);
|
|
664
|
+
const loadingOlder = useRef3(false);
|
|
665
|
+
const onEvent = useRef3(options.onEvent);
|
|
666
|
+
onEvent.current = options.onEvent;
|
|
667
|
+
useEffect2(() => {
|
|
668
|
+
setEvents([]);
|
|
669
|
+
setInitialLoaded(false);
|
|
670
|
+
setHasOlder(false);
|
|
671
|
+
setIsLoadingOlder(false);
|
|
672
|
+
setActivity(IDLE2);
|
|
673
|
+
oldestSeq.current = null;
|
|
674
|
+
loadingOlder.current = false;
|
|
675
|
+
if (conversationId === null)
|
|
676
|
+
return;
|
|
677
|
+
const controller = new AbortController;
|
|
678
|
+
let subscription = null;
|
|
679
|
+
let closed = false;
|
|
680
|
+
(async () => {
|
|
681
|
+
try {
|
|
682
|
+
const page = await client.listEventsPage(conversationId, {
|
|
683
|
+
limit: pageSize,
|
|
684
|
+
signal: controller.signal
|
|
685
|
+
});
|
|
686
|
+
if (controller.signal.aborted)
|
|
687
|
+
return;
|
|
688
|
+
setEvents(page.events);
|
|
689
|
+
oldestSeq.current = page.oldestSeq;
|
|
690
|
+
setHasOlder(page.hasOlder);
|
|
691
|
+
setInitialLoaded(true);
|
|
692
|
+
if (closed)
|
|
693
|
+
return;
|
|
694
|
+
subscription = client.subscribe(conversationId, {
|
|
695
|
+
onEvent: (event) => {
|
|
696
|
+
setEvents((prev) => upsert(prev, event));
|
|
697
|
+
onEvent.current?.(event);
|
|
698
|
+
},
|
|
699
|
+
onActivity: setActivity
|
|
700
|
+
}, { since: page.latestChangeSeq ?? undefined });
|
|
701
|
+
} catch {
|
|
702
|
+
if (!controller.signal.aborted)
|
|
703
|
+
setInitialLoaded(true);
|
|
704
|
+
}
|
|
705
|
+
})();
|
|
706
|
+
return () => {
|
|
707
|
+
closed = true;
|
|
708
|
+
controller.abort();
|
|
709
|
+
subscription?.close();
|
|
710
|
+
};
|
|
711
|
+
}, [client, conversationId, pageSize]);
|
|
712
|
+
const loadOlder = useCallback2(async () => {
|
|
713
|
+
const before = oldestSeq.current;
|
|
714
|
+
if (conversationId === null || before === null)
|
|
715
|
+
return 0;
|
|
716
|
+
if (loadingOlder.current || !hasOlder)
|
|
717
|
+
return 0;
|
|
718
|
+
loadingOlder.current = true;
|
|
719
|
+
setIsLoadingOlder(true);
|
|
720
|
+
try {
|
|
721
|
+
const page = await client.listEventsPage(conversationId, { before, limit: pageSize });
|
|
722
|
+
if (page.events.length === 0) {
|
|
723
|
+
setHasOlder(false);
|
|
724
|
+
return 0;
|
|
725
|
+
}
|
|
726
|
+
let added = 0;
|
|
727
|
+
setEvents((prev) => {
|
|
728
|
+
const held = new Set(prev.map((e) => e.id));
|
|
729
|
+
const fresh = page.events.filter((e) => !held.has(e.id));
|
|
730
|
+
added = fresh.length;
|
|
731
|
+
return [...fresh, ...prev];
|
|
732
|
+
});
|
|
733
|
+
oldestSeq.current = page.oldestSeq ?? before;
|
|
734
|
+
setHasOlder(page.hasOlder);
|
|
735
|
+
return added;
|
|
736
|
+
} catch {
|
|
737
|
+
return 0;
|
|
738
|
+
} finally {
|
|
739
|
+
loadingOlder.current = false;
|
|
740
|
+
setIsLoadingOlder(false);
|
|
741
|
+
}
|
|
742
|
+
}, [client, conversationId, hasOlder, pageSize]);
|
|
743
|
+
return { events, initialLoaded, hasOlder, isLoadingOlder, loadOlder, activity };
|
|
744
|
+
}
|
|
745
|
+
var IDLE2 = { isProcessing: false, hasPendingTurn: false };
|
|
746
|
+
function upsert(held, incoming) {
|
|
747
|
+
const at = held.findIndex((e) => e.id === incoming.id);
|
|
748
|
+
if (at >= 0) {
|
|
749
|
+
const next2 = held.slice();
|
|
750
|
+
next2[at] = incoming;
|
|
751
|
+
return next2;
|
|
752
|
+
}
|
|
753
|
+
const next = [...held, incoming];
|
|
754
|
+
next.sort((a, b) => a.seq - b.seq);
|
|
755
|
+
return next;
|
|
756
|
+
}
|
|
757
|
+
// src/use-conversation-list.ts
|
|
758
|
+
import { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef4, useState as useState3 } from "react";
|
|
759
|
+
function upsert2(list, incoming) {
|
|
760
|
+
const next = list.filter((c) => c.id !== incoming.id);
|
|
761
|
+
next.push(incoming);
|
|
762
|
+
return next.sort((a, b) => b.lastActivityAt.localeCompare(a.lastActivityAt));
|
|
763
|
+
}
|
|
764
|
+
function useConversationList(opts = {}) {
|
|
765
|
+
const client = useAgentClient();
|
|
766
|
+
const pageSize = opts.pageSize ?? 30;
|
|
767
|
+
const [conversations, setConversations] = useState3([]);
|
|
768
|
+
const [isLoading, setIsLoading] = useState3(true);
|
|
769
|
+
const [isLoadingMore, setIsLoadingMore] = useState3(false);
|
|
770
|
+
const [error, setError] = useState3(null);
|
|
771
|
+
const cursor = useRef4(null);
|
|
772
|
+
const [hasMore, setHasMore] = useState3(false);
|
|
773
|
+
useEffect3(() => {
|
|
774
|
+
const controller = new AbortController;
|
|
775
|
+
setIsLoading(true);
|
|
776
|
+
client.listConversations({ limit: pageSize, signal: controller.signal }).then((page) => {
|
|
777
|
+
if (controller.signal.aborted)
|
|
778
|
+
return;
|
|
779
|
+
setConversations(page.items);
|
|
780
|
+
cursor.current = page.nextCursor;
|
|
781
|
+
setHasMore(page.nextCursor !== null);
|
|
782
|
+
setIsLoading(false);
|
|
783
|
+
}).catch((err) => {
|
|
784
|
+
if (controller.signal.aborted)
|
|
785
|
+
return;
|
|
786
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
787
|
+
setIsLoading(false);
|
|
788
|
+
});
|
|
789
|
+
const subscription = client.subscribeToConversations({
|
|
790
|
+
onConversation: (conversation) => {
|
|
791
|
+
setConversations((prev) => conversation.archived ? prev.filter((c) => c.id !== conversation.id) : upsert2(prev, conversation));
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
return () => {
|
|
795
|
+
controller.abort();
|
|
796
|
+
subscription.close();
|
|
797
|
+
};
|
|
798
|
+
}, [client, pageSize]);
|
|
799
|
+
const loadMore = useCallback3(async () => {
|
|
800
|
+
if (cursor.current === null)
|
|
801
|
+
return;
|
|
802
|
+
setIsLoadingMore(true);
|
|
803
|
+
try {
|
|
804
|
+
const page = await client.listConversations({ limit: pageSize, before: cursor.current });
|
|
805
|
+
setConversations((prev) => {
|
|
806
|
+
const seen = new Set(prev.map((c) => c.id));
|
|
807
|
+
return [...prev, ...page.items.filter((c) => !seen.has(c.id))];
|
|
808
|
+
});
|
|
809
|
+
cursor.current = page.nextCursor;
|
|
810
|
+
setHasMore(page.nextCursor !== null);
|
|
811
|
+
} catch (err) {
|
|
812
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
813
|
+
} finally {
|
|
814
|
+
setIsLoadingMore(false);
|
|
815
|
+
}
|
|
816
|
+
}, [client, pageSize]);
|
|
817
|
+
const create = useCallback3(async (createOpts = {}) => {
|
|
818
|
+
const conversation = await client.createConversation(createOpts);
|
|
819
|
+
setConversations((prev) => upsert2(prev, conversation));
|
|
820
|
+
return conversation;
|
|
821
|
+
}, [client]);
|
|
822
|
+
const archive = useCallback3(async (id) => {
|
|
823
|
+
await client.archiveConversation(id);
|
|
824
|
+
setConversations((prev) => prev.filter((c) => c.id !== id));
|
|
825
|
+
}, [client]);
|
|
826
|
+
return {
|
|
827
|
+
conversations,
|
|
828
|
+
isLoading,
|
|
829
|
+
isLoadingMore,
|
|
830
|
+
error,
|
|
831
|
+
hasMore,
|
|
832
|
+
loadMore,
|
|
833
|
+
create,
|
|
834
|
+
archive
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
// src/use-identity.ts
|
|
838
|
+
import { useEffect as useEffect4, useState as useState4 } from "react";
|
|
839
|
+
function useIdentity() {
|
|
840
|
+
const client = useAgentClient();
|
|
841
|
+
const [identity, setIdentity] = useState4(null);
|
|
842
|
+
const [isLoading, setIsLoading] = useState4(true);
|
|
843
|
+
const [error, setError] = useState4(null);
|
|
844
|
+
useEffect4(() => {
|
|
845
|
+
let active = true;
|
|
846
|
+
setIsLoading(true);
|
|
847
|
+
setError(null);
|
|
848
|
+
client.me().then((next) => {
|
|
849
|
+
if (!active)
|
|
850
|
+
return;
|
|
851
|
+
setIdentity(next);
|
|
852
|
+
setIsLoading(false);
|
|
853
|
+
}).catch((err) => {
|
|
854
|
+
if (!active)
|
|
855
|
+
return;
|
|
856
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
857
|
+
setIsLoading(false);
|
|
858
|
+
});
|
|
859
|
+
return () => {
|
|
860
|
+
active = false;
|
|
861
|
+
};
|
|
862
|
+
}, [client]);
|
|
863
|
+
return { identity, isLoading, error };
|
|
864
|
+
}
|
|
865
|
+
export {
|
|
866
|
+
useIdentity,
|
|
867
|
+
useConversationList,
|
|
868
|
+
useConversationEvents,
|
|
869
|
+
useConversation,
|
|
870
|
+
useAgentClient,
|
|
871
|
+
renderBlocks,
|
|
872
|
+
AgentProvider
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
//# debugId=54F834B7C1AC034364756E2164756E21
|
|
876
|
+
//# sourceMappingURL=index.js.map
|