@lumerahq/ui 0.7.6 → 0.8.0-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/RecordSheet-CI_6QC-a.js +38151 -0
- package/dist/{api-BrhR_Jjl.js → api-Da1IIWDG.js} +1 -1
- package/dist/{automations-XN4WPV_g.js → automations-BEBG7FqJ.js} +160 -26
- package/dist/components/agent-chat/AgentChat.d.ts +3 -0
- package/dist/components/agent-chat/AgentChat.d.ts.map +1 -0
- package/dist/components/agent-chat/index.d.ts +5 -0
- package/dist/components/agent-chat/index.d.ts.map +1 -0
- package/dist/components/agent-chat/lumera-agent-transport.d.ts +20 -0
- package/dist/components/agent-chat/lumera-agent-transport.d.ts.map +1 -0
- package/dist/components/agent-chat/types.d.ts +376 -0
- package/dist/components/agent-chat/types.d.ts.map +1 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +8 -6
- package/dist/components/ui/button.d.ts +1 -1
- package/dist/{formatters-Baj7FkeG.js → formatters-D4T821Dv.js} +2 -1
- package/dist/highlighted-body-OFNGDK62-38mi922Z.js +19 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +6 -5
- package/dist/hooks/use-agent-chat.d.ts +47 -0
- package/dist/hooks/use-agent-chat.d.ts.map +1 -0
- package/dist/index.js +51 -45
- package/dist/lib/bridge.d.ts +71 -5
- package/dist/lib/bridge.d.ts.map +1 -1
- package/dist/lib/index.d.ts +1 -1
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +29 -26
- package/dist/logo.svg +15 -0
- package/dist/mermaid-GHXKKRXX-CdpO_nzu.js +4 -0
- package/dist/ui.css +254 -2
- package/dist/use-automation-run-rhYZZhj7.js +746 -0
- package/dist/{use-sql-table-B7C06_An.js → use-sql-table-1EiZ9qCa.js} +2 -2
- package/package.json +2 -1
- package/dist/RecordSheet-BvHHP8P4.js +0 -12241
- package/dist/logo.png +0 -0
- package/dist/lumera-logo.png +0 -0
- package/dist/use-automation-run-Cecvf4Aq.js +0 -134
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
import { useState, useRef, useCallback, useEffect, useMemo } from "react";
|
|
2
|
+
import { useQueryClient, useMutation, useQuery } from "@tanstack/react-query";
|
|
3
|
+
import { m as ensureAutomationRun, k as createAutomationRun, f as cancelAutomationRun, e as automationStatuses, r as getAutomationRun } from "./automations-BEBG7FqJ.js";
|
|
4
|
+
const DEFAULT_SESSION_ID = "default";
|
|
5
|
+
const DEFAULT_HISTORY_LIMIT = 10;
|
|
6
|
+
const DEFAULT_MAX_BUFFERED_MESSAGES = 100;
|
|
7
|
+
const DEFAULT_MAX_QUEUED_MESSAGES = 1;
|
|
8
|
+
function useAgentChat({
|
|
9
|
+
agentId,
|
|
10
|
+
sessionId = DEFAULT_SESSION_ID,
|
|
11
|
+
transport,
|
|
12
|
+
initialMessages = [],
|
|
13
|
+
initialPage,
|
|
14
|
+
historyLimit = DEFAULT_HISTORY_LIMIT,
|
|
15
|
+
maxBufferedMessages = DEFAULT_MAX_BUFFERED_MESSAGES,
|
|
16
|
+
maxQueuedMessages = DEFAULT_MAX_QUEUED_MESSAGES,
|
|
17
|
+
disabled = false,
|
|
18
|
+
autoLoadHistory = true,
|
|
19
|
+
autoSubscribe = true,
|
|
20
|
+
idFactory = defaultIDFactory,
|
|
21
|
+
onMessagesChange,
|
|
22
|
+
onTurnComplete,
|
|
23
|
+
onError
|
|
24
|
+
}) {
|
|
25
|
+
const [messages, setMessagesState] = useState(
|
|
26
|
+
() => trimMessages(initialMessages, maxBufferedMessages)
|
|
27
|
+
);
|
|
28
|
+
const [input, setInput] = useState("");
|
|
29
|
+
const [pendingFiles, setPendingFiles] = useState([]);
|
|
30
|
+
const [page, setPage] = useState(initialPage ?? null);
|
|
31
|
+
const [tokenUsage, setTokenUsage] = useState(null);
|
|
32
|
+
const [error, setError] = useState(null);
|
|
33
|
+
const [isLoadingHistory, setIsLoadingHistory] = useState(false);
|
|
34
|
+
const [isLoadingEarlier, setIsLoadingEarlier] = useState(false);
|
|
35
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
36
|
+
const [isStreaming, setIsStreaming] = useState(false);
|
|
37
|
+
const [isAborting, setIsAborting] = useState(false);
|
|
38
|
+
const messagesRef = useRef(messages);
|
|
39
|
+
const subscriptionRef = useRef(null);
|
|
40
|
+
const activeRequestIdRef = useRef(void 0);
|
|
41
|
+
const mountedRef = useRef(true);
|
|
42
|
+
const setMessages = useCallback(
|
|
43
|
+
(updater) => {
|
|
44
|
+
setMessagesState((current) => {
|
|
45
|
+
const nextRaw = typeof updater === "function" ? updater(current) : updater;
|
|
46
|
+
const next = trimMessages(nextRaw, maxBufferedMessages);
|
|
47
|
+
messagesRef.current = next;
|
|
48
|
+
onMessagesChange?.(next);
|
|
49
|
+
return next;
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
[maxBufferedMessages, onMessagesChange]
|
|
53
|
+
);
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
mountedRef.current = true;
|
|
56
|
+
return () => {
|
|
57
|
+
mountedRef.current = false;
|
|
58
|
+
subscriptionRef.current?.close();
|
|
59
|
+
subscriptionRef.current = null;
|
|
60
|
+
};
|
|
61
|
+
}, []);
|
|
62
|
+
const handleError = useCallback(
|
|
63
|
+
(err) => {
|
|
64
|
+
if (!mountedRef.current) return;
|
|
65
|
+
setError(err);
|
|
66
|
+
onError?.(err);
|
|
67
|
+
},
|
|
68
|
+
[onError]
|
|
69
|
+
);
|
|
70
|
+
const applyHistoryResult = useCallback(
|
|
71
|
+
(result) => {
|
|
72
|
+
const projected = mergePendingMessages(result.messages, result.pending ?? []);
|
|
73
|
+
setMessages((current) => mergeUniqueMessages([...projected, ...missingActiveUserMessages(current, projected)]));
|
|
74
|
+
setPage(result.page ?? null);
|
|
75
|
+
},
|
|
76
|
+
[setMessages]
|
|
77
|
+
);
|
|
78
|
+
const loadHistory = useCallback(async () => {
|
|
79
|
+
if (disabled) return;
|
|
80
|
+
setIsLoadingHistory(true);
|
|
81
|
+
setError(null);
|
|
82
|
+
try {
|
|
83
|
+
const result = await transport.loadHistory({ agentId, sessionId, limit: historyLimit });
|
|
84
|
+
if (!mountedRef.current) return;
|
|
85
|
+
applyHistoryResult(result);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
handleError(err);
|
|
88
|
+
} finally {
|
|
89
|
+
if (mountedRef.current) setIsLoadingHistory(false);
|
|
90
|
+
}
|
|
91
|
+
}, [agentId, applyHistoryResult, disabled, handleError, historyLimit, sessionId, transport]);
|
|
92
|
+
const loadEarlier = useCallback(async () => {
|
|
93
|
+
if (disabled || isLoadingEarlier || !page?.hasMoreBefore || !page.beforeCursor) return;
|
|
94
|
+
const loadEarlierImpl = transport.loadEarlier ?? transport.loadHistory;
|
|
95
|
+
setIsLoadingEarlier(true);
|
|
96
|
+
setError(null);
|
|
97
|
+
try {
|
|
98
|
+
const result = await loadEarlierImpl({
|
|
99
|
+
agentId,
|
|
100
|
+
sessionId,
|
|
101
|
+
beforeCursor: page.beforeCursor,
|
|
102
|
+
limit: historyLimit
|
|
103
|
+
});
|
|
104
|
+
if (!mountedRef.current) return;
|
|
105
|
+
const earlier = mergePendingMessages(result.messages, result.pending ?? []);
|
|
106
|
+
setMessages((current) => mergeUniqueMessages([...earlier, ...current]));
|
|
107
|
+
setPage(result.page ?? page);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
handleError(err);
|
|
110
|
+
} finally {
|
|
111
|
+
if (mountedRef.current) setIsLoadingEarlier(false);
|
|
112
|
+
}
|
|
113
|
+
}, [agentId, disabled, handleError, historyLimit, isLoadingEarlier, page, sessionId, setMessages, transport]);
|
|
114
|
+
const settleRequest = useCallback(
|
|
115
|
+
(requestId, status, errorMessage) => {
|
|
116
|
+
if (!requestId) return;
|
|
117
|
+
setMessages(
|
|
118
|
+
(current) => current.map(
|
|
119
|
+
(messageItem) => messageItem.role === "user" && messageItem.requestId === requestId ? { ...messageItem, status, error: errorMessage ?? messageItem.error } : messageItem
|
|
120
|
+
)
|
|
121
|
+
);
|
|
122
|
+
setIsStreaming(false);
|
|
123
|
+
const turn = buildAgentChatTurns(messagesRef.current).find((item) => item.requestId === requestId);
|
|
124
|
+
if (turn) onTurnComplete?.(turn);
|
|
125
|
+
if (!transport.subscribe) return;
|
|
126
|
+
void loadHistory();
|
|
127
|
+
},
|
|
128
|
+
[loadHistory, onTurnComplete, setMessages, transport.subscribe]
|
|
129
|
+
);
|
|
130
|
+
const applyStreamEvent = useCallback(
|
|
131
|
+
(event) => {
|
|
132
|
+
setError(null);
|
|
133
|
+
switch (event.type) {
|
|
134
|
+
case "text_delta":
|
|
135
|
+
setMessages((current) => applyAssistantDelta(current, event.requestId, "text", event.delta));
|
|
136
|
+
markUserTurnRunning(setMessages, event.requestId);
|
|
137
|
+
return;
|
|
138
|
+
case "thinking_delta":
|
|
139
|
+
setMessages((current) => applyAssistantDelta(current, event.requestId, "thinking", event.delta));
|
|
140
|
+
markUserTurnRunning(setMessages, event.requestId);
|
|
141
|
+
return;
|
|
142
|
+
case "message":
|
|
143
|
+
setMessages((current) => mergeUniqueMessages([...current, event.message]));
|
|
144
|
+
if (event.message.requestId) markUserTurnRunning(setMessages, event.message.requestId);
|
|
145
|
+
return;
|
|
146
|
+
case "tool_start":
|
|
147
|
+
case "tool_update":
|
|
148
|
+
case "tool_end":
|
|
149
|
+
setMessages((current) => upsertAssistantToolBlock(current, event.requestId, event.toolCall));
|
|
150
|
+
markUserTurnRunning(setMessages, event.requestId);
|
|
151
|
+
return;
|
|
152
|
+
case "action":
|
|
153
|
+
setMessages(
|
|
154
|
+
(current) => appendAssistantBlock(current, event.requestId, { type: "action", action: event.action })
|
|
155
|
+
);
|
|
156
|
+
markUserTurnRunning(setMessages, event.requestId);
|
|
157
|
+
return;
|
|
158
|
+
case "file":
|
|
159
|
+
setMessages((current) => appendAssistantBlock(current, event.requestId, { type: "file", file: event.file }));
|
|
160
|
+
markUserTurnRunning(setMessages, event.requestId);
|
|
161
|
+
return;
|
|
162
|
+
case "info":
|
|
163
|
+
setMessages(
|
|
164
|
+
(current) => appendAssistantBlock(current, event.requestId, {
|
|
165
|
+
type: "info",
|
|
166
|
+
content: event.content,
|
|
167
|
+
tone: event.tone
|
|
168
|
+
})
|
|
169
|
+
);
|
|
170
|
+
markUserTurnRunning(setMessages, event.requestId);
|
|
171
|
+
return;
|
|
172
|
+
case "usage":
|
|
173
|
+
setTokenUsage((current) => accumulateTokenUsage(current, event.usage));
|
|
174
|
+
return;
|
|
175
|
+
case "terminal":
|
|
176
|
+
settleRequest(event.requestId, event.status ?? "completed", event.error);
|
|
177
|
+
return;
|
|
178
|
+
case "error":
|
|
179
|
+
settleRequest(event.requestId, "failed", event.error);
|
|
180
|
+
handleError(new Error(event.error));
|
|
181
|
+
return;
|
|
182
|
+
case "custom":
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
[handleError, setMessages, settleRequest]
|
|
187
|
+
);
|
|
188
|
+
const startSubscription = useCallback(
|
|
189
|
+
(requestId) => {
|
|
190
|
+
if (!autoSubscribe || !transport.subscribe || !requestId) return;
|
|
191
|
+
subscriptionRef.current?.close();
|
|
192
|
+
activeRequestIdRef.current = requestId;
|
|
193
|
+
setIsStreaming(true);
|
|
194
|
+
subscriptionRef.current = transport.subscribe(
|
|
195
|
+
{ agentId, sessionId, requestId },
|
|
196
|
+
{
|
|
197
|
+
onEvent: applyStreamEvent,
|
|
198
|
+
onError: (err) => {
|
|
199
|
+
handleError(err);
|
|
200
|
+
if (mountedRef.current) setIsStreaming(false);
|
|
201
|
+
},
|
|
202
|
+
onClose: () => {
|
|
203
|
+
if (mountedRef.current && activeRequestIdRef.current === requestId) {
|
|
204
|
+
setIsStreaming(false);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
);
|
|
209
|
+
},
|
|
210
|
+
[agentId, applyStreamEvent, autoSubscribe, handleError, sessionId, transport]
|
|
211
|
+
);
|
|
212
|
+
const sendMessage = useCallback(
|
|
213
|
+
async (message) => {
|
|
214
|
+
const content = (message ?? input).trim();
|
|
215
|
+
if (disabled || isSubmitting || !content && pendingFiles.length === 0) {
|
|
216
|
+
return { accepted: false, error: "Nothing to send" };
|
|
217
|
+
}
|
|
218
|
+
const active = activeUserMessages(messagesRef.current);
|
|
219
|
+
const queued = active.filter((item) => item.status === "queued").length;
|
|
220
|
+
if (active.length > 0 && queued >= maxQueuedMessages) {
|
|
221
|
+
return { accepted: false, error: "Queue is full" };
|
|
222
|
+
}
|
|
223
|
+
const requestId = idFactory();
|
|
224
|
+
setIsSubmitting(true);
|
|
225
|
+
setError(null);
|
|
226
|
+
try {
|
|
227
|
+
const result = await transport.sendMessage({
|
|
228
|
+
message: content,
|
|
229
|
+
requestId,
|
|
230
|
+
sessionId,
|
|
231
|
+
files: pendingFiles
|
|
232
|
+
});
|
|
233
|
+
if (!mountedRef.current) return result;
|
|
234
|
+
if (result.accepted) {
|
|
235
|
+
setInput("");
|
|
236
|
+
setPendingFiles([]);
|
|
237
|
+
if (result.pending) {
|
|
238
|
+
setMessages((current) => mergeUniqueMessages([...current, result.pending]));
|
|
239
|
+
} else if (!transport.subscribe) {
|
|
240
|
+
await loadHistory();
|
|
241
|
+
}
|
|
242
|
+
startSubscription(result.requestId ?? requestId);
|
|
243
|
+
}
|
|
244
|
+
return result;
|
|
245
|
+
} catch (err) {
|
|
246
|
+
handleError(err);
|
|
247
|
+
return { accepted: false, requestId, error: err instanceof Error ? err.message : String(err) };
|
|
248
|
+
} finally {
|
|
249
|
+
if (mountedRef.current) setIsSubmitting(false);
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
[
|
|
253
|
+
disabled,
|
|
254
|
+
handleError,
|
|
255
|
+
idFactory,
|
|
256
|
+
input,
|
|
257
|
+
isSubmitting,
|
|
258
|
+
loadHistory,
|
|
259
|
+
maxQueuedMessages,
|
|
260
|
+
pendingFiles,
|
|
261
|
+
sessionId,
|
|
262
|
+
setMessages,
|
|
263
|
+
startSubscription,
|
|
264
|
+
transport
|
|
265
|
+
]
|
|
266
|
+
);
|
|
267
|
+
const abort = useCallback(async () => {
|
|
268
|
+
if (disabled || isAborting) return;
|
|
269
|
+
const requestId = latestActiveRequestId(messagesRef.current);
|
|
270
|
+
setIsAborting(true);
|
|
271
|
+
try {
|
|
272
|
+
await transport.abort?.({ agentId, sessionId, requestId });
|
|
273
|
+
subscriptionRef.current?.close();
|
|
274
|
+
subscriptionRef.current = null;
|
|
275
|
+
setIsStreaming(false);
|
|
276
|
+
setMessages(
|
|
277
|
+
(current) => current.map(
|
|
278
|
+
(messageItem) => messageItem.role === "user" && isActiveStatus(messageItem.status) ? { ...messageItem, status: "interrupted" } : messageItem
|
|
279
|
+
)
|
|
280
|
+
);
|
|
281
|
+
} catch (err) {
|
|
282
|
+
handleError(err);
|
|
283
|
+
} finally {
|
|
284
|
+
if (mountedRef.current) setIsAborting(false);
|
|
285
|
+
}
|
|
286
|
+
}, [agentId, disabled, handleError, isAborting, sessionId, setMessages, transport]);
|
|
287
|
+
const attachFiles = useCallback(
|
|
288
|
+
async (files) => {
|
|
289
|
+
if (files.length === 0) return;
|
|
290
|
+
const pending = files.map((file) => ({
|
|
291
|
+
id: idFactory(),
|
|
292
|
+
file,
|
|
293
|
+
name: file.name,
|
|
294
|
+
mimeType: file.type,
|
|
295
|
+
size: file.size,
|
|
296
|
+
status: transport.uploadFile ? "uploading" : "pending"
|
|
297
|
+
}));
|
|
298
|
+
setPendingFiles((current) => [...current, ...pending]);
|
|
299
|
+
if (!transport.uploadFile) return;
|
|
300
|
+
await Promise.all(
|
|
301
|
+
pending.map(async (pendingFile) => {
|
|
302
|
+
if (!pendingFile.file) return;
|
|
303
|
+
try {
|
|
304
|
+
const upload = await transport.uploadFile?.(pendingFile.file);
|
|
305
|
+
if (!mountedRef.current) return;
|
|
306
|
+
setPendingFiles(
|
|
307
|
+
(current) => current.map((item) => item.id === pendingFile.id ? { ...item, upload, status: "uploaded" } : item)
|
|
308
|
+
);
|
|
309
|
+
} catch (err) {
|
|
310
|
+
if (!mountedRef.current) return;
|
|
311
|
+
const messageText = err instanceof Error ? err.message : String(err);
|
|
312
|
+
setPendingFiles(
|
|
313
|
+
(current) => current.map(
|
|
314
|
+
(item) => item.id === pendingFile.id ? { ...item, status: "failed", error: messageText } : item
|
|
315
|
+
)
|
|
316
|
+
);
|
|
317
|
+
handleError(err);
|
|
318
|
+
}
|
|
319
|
+
})
|
|
320
|
+
);
|
|
321
|
+
},
|
|
322
|
+
[handleError, idFactory, transport]
|
|
323
|
+
);
|
|
324
|
+
const removePendingFile = useCallback((id) => {
|
|
325
|
+
setPendingFiles((current) => current.filter((file) => file.id !== id));
|
|
326
|
+
}, []);
|
|
327
|
+
const clearPendingFiles = useCallback(() => {
|
|
328
|
+
setPendingFiles([]);
|
|
329
|
+
}, []);
|
|
330
|
+
useEffect(() => {
|
|
331
|
+
if (!autoLoadHistory) return;
|
|
332
|
+
void loadHistory();
|
|
333
|
+
}, [autoLoadHistory, loadHistory]);
|
|
334
|
+
const turns = useMemo(() => buildAgentChatTurns(messages), [messages]);
|
|
335
|
+
const queueLength = useMemo(
|
|
336
|
+
() => messages.filter((message) => message.role === "user" && message.status === "queued").length,
|
|
337
|
+
[messages]
|
|
338
|
+
);
|
|
339
|
+
const hasActive = useMemo(() => activeUserMessages(messages).length > 0, [messages]);
|
|
340
|
+
const hasFailedUploads = pendingFiles.some((file) => file.status === "failed");
|
|
341
|
+
const hasUploadingFiles = pendingFiles.some((file) => file.status === "uploading");
|
|
342
|
+
const canSubmit = !disabled && !isSubmitting && !hasUploadingFiles && !hasFailedUploads && (input.trim().length > 0 || pendingFiles.length > 0) && (!hasActive || queueLength < maxQueuedMessages);
|
|
343
|
+
const canAbort = !disabled && !isAborting && hasActive;
|
|
344
|
+
const phase = derivePhase({ isLoadingHistory, isLoadingEarlier, isSubmitting, isStreaming, isAborting, error });
|
|
345
|
+
return {
|
|
346
|
+
messages,
|
|
347
|
+
turns,
|
|
348
|
+
input,
|
|
349
|
+
setInput,
|
|
350
|
+
pendingFiles,
|
|
351
|
+
attachFiles,
|
|
352
|
+
removePendingFile,
|
|
353
|
+
clearPendingFiles,
|
|
354
|
+
phase,
|
|
355
|
+
error,
|
|
356
|
+
page,
|
|
357
|
+
tokenUsage,
|
|
358
|
+
isLoadingHistory,
|
|
359
|
+
isLoadingEarlier,
|
|
360
|
+
isSubmitting,
|
|
361
|
+
isStreaming,
|
|
362
|
+
isAborting,
|
|
363
|
+
canLoadEarlier: Boolean(page?.hasMoreBefore && page.beforeCursor),
|
|
364
|
+
queueLength,
|
|
365
|
+
canSubmit,
|
|
366
|
+
canAbort,
|
|
367
|
+
loadHistory,
|
|
368
|
+
loadEarlier,
|
|
369
|
+
sendMessage,
|
|
370
|
+
abort
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
function derivePhase(input) {
|
|
374
|
+
if (input.isLoadingHistory) return "loading-history";
|
|
375
|
+
if (input.isLoadingEarlier) return "loading-earlier";
|
|
376
|
+
if (input.isSubmitting) return "submitting";
|
|
377
|
+
if (input.isAborting) return "aborting";
|
|
378
|
+
if (input.isStreaming) return "streaming";
|
|
379
|
+
if (input.error) return "error";
|
|
380
|
+
return "idle";
|
|
381
|
+
}
|
|
382
|
+
function defaultIDFactory() {
|
|
383
|
+
if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
|
|
384
|
+
return crypto.randomUUID();
|
|
385
|
+
}
|
|
386
|
+
return `chat_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;
|
|
387
|
+
}
|
|
388
|
+
function isActiveStatus(status) {
|
|
389
|
+
return status === "queued" || status === "claimed" || status === "preparing" || status === "submitting" || status === "running";
|
|
390
|
+
}
|
|
391
|
+
function activeUserMessages(messages) {
|
|
392
|
+
return messages.filter((message) => message.role === "user" && isActiveStatus(message.status));
|
|
393
|
+
}
|
|
394
|
+
function latestActiveRequestId(messages) {
|
|
395
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
396
|
+
const message = messages[index];
|
|
397
|
+
if (message?.role === "user" && isActiveStatus(message.status) && message.requestId) {
|
|
398
|
+
return message.requestId;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return void 0;
|
|
402
|
+
}
|
|
403
|
+
function messageKeys(message) {
|
|
404
|
+
const keys = [`${message.role}:id:${message.id}`];
|
|
405
|
+
if (message.role === "user" && message.requestId) {
|
|
406
|
+
keys.push(`user:request:${message.requestId}`);
|
|
407
|
+
}
|
|
408
|
+
return keys;
|
|
409
|
+
}
|
|
410
|
+
function mergeUniqueMessages(messages) {
|
|
411
|
+
const seen = /* @__PURE__ */ new Set();
|
|
412
|
+
const merged = [];
|
|
413
|
+
for (const message of messages) {
|
|
414
|
+
const keys = messageKeys(message);
|
|
415
|
+
if (keys.some((key) => seen.has(key))) continue;
|
|
416
|
+
keys.forEach((key) => seen.add(key));
|
|
417
|
+
merged.push(message);
|
|
418
|
+
}
|
|
419
|
+
return merged;
|
|
420
|
+
}
|
|
421
|
+
function trimMessages(messages, max) {
|
|
422
|
+
return max > 0 && messages.length > max ? messages.slice(-max) : messages;
|
|
423
|
+
}
|
|
424
|
+
function mergePendingMessages(messages, pending) {
|
|
425
|
+
const next = [...messages];
|
|
426
|
+
for (const pendingMessage of pending) {
|
|
427
|
+
if (!pendingMessage.requestId) {
|
|
428
|
+
insertChronologically(next, pendingMessage);
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
const matchIndex = next.findIndex(
|
|
432
|
+
(message) => message.role === "user" && message.requestId === pendingMessage.requestId
|
|
433
|
+
);
|
|
434
|
+
if (matchIndex >= 0) {
|
|
435
|
+
const matched = next[matchIndex];
|
|
436
|
+
next[matchIndex] = {
|
|
437
|
+
...matched,
|
|
438
|
+
content: matched?.content || pendingMessage.content,
|
|
439
|
+
timestamp: matched?.timestamp || pendingMessage.timestamp,
|
|
440
|
+
status: matched?.status && !isActiveStatus(matched.status) ? matched.status : pendingMessage.status,
|
|
441
|
+
error: matched?.error ?? pendingMessage.error,
|
|
442
|
+
errorCategory: matched?.errorCategory ?? pendingMessage.errorCategory
|
|
443
|
+
};
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
insertChronologically(next, pendingMessage);
|
|
447
|
+
}
|
|
448
|
+
return next;
|
|
449
|
+
}
|
|
450
|
+
function missingActiveUserMessages(current, projected) {
|
|
451
|
+
const projectedRequestIds = new Set(projected.map((message) => message.requestId).filter(Boolean));
|
|
452
|
+
return current.filter(
|
|
453
|
+
(message) => message.role === "user" && isActiveStatus(message.status) && message.requestId && !projectedRequestIds.has(message.requestId)
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
function timestampMs(timestamp) {
|
|
457
|
+
if (!timestamp) return null;
|
|
458
|
+
const parsed = Date.parse(timestamp);
|
|
459
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
460
|
+
}
|
|
461
|
+
function insertChronologically(messages, message) {
|
|
462
|
+
const ts = timestampMs(message.timestamp);
|
|
463
|
+
if (ts == null) {
|
|
464
|
+
messages.push(message);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
let insertAt = messages.length;
|
|
468
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
469
|
+
const currentTs = timestampMs(messages[index]?.timestamp);
|
|
470
|
+
if (currentTs == null) continue;
|
|
471
|
+
if (currentTs <= ts) {
|
|
472
|
+
insertAt = index + 1;
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
insertAt = index;
|
|
476
|
+
}
|
|
477
|
+
messages.splice(insertAt, 0, message);
|
|
478
|
+
}
|
|
479
|
+
function upsertAssistantToolBlock(messages, requestId, toolCall) {
|
|
480
|
+
const block = { type: "tool_call", toolCall };
|
|
481
|
+
const next = [...messages];
|
|
482
|
+
const index = latestAssistantIndex(next, requestId);
|
|
483
|
+
if (index < 0) {
|
|
484
|
+
return appendAssistantBlock(messages, requestId, block);
|
|
485
|
+
}
|
|
486
|
+
const message = next[index];
|
|
487
|
+
const blocks = [...message?.blocks ?? []];
|
|
488
|
+
const blockIndex = blocks.findIndex((item) => item.type === "tool_call" && item.toolCall.id === toolCall.id);
|
|
489
|
+
if (blockIndex >= 0) {
|
|
490
|
+
blocks[blockIndex] = block;
|
|
491
|
+
} else {
|
|
492
|
+
blocks.push(block);
|
|
493
|
+
}
|
|
494
|
+
next[index] = {
|
|
495
|
+
...message,
|
|
496
|
+
blocks,
|
|
497
|
+
toolCalls: blocks.flatMap((item) => item.type === "tool_call" ? [item.toolCall] : [])
|
|
498
|
+
};
|
|
499
|
+
return next;
|
|
500
|
+
}
|
|
501
|
+
function appendAssistantBlock(messages, requestId, block) {
|
|
502
|
+
const next = [...messages];
|
|
503
|
+
const index = latestAssistantIndex(next, requestId);
|
|
504
|
+
if (index >= 0) {
|
|
505
|
+
const message = next[index];
|
|
506
|
+
next[index] = {
|
|
507
|
+
...message,
|
|
508
|
+
blocks: [...message?.blocks ?? [], block]
|
|
509
|
+
};
|
|
510
|
+
return next;
|
|
511
|
+
}
|
|
512
|
+
next.push({
|
|
513
|
+
id: `assistant:${requestId ?? defaultIDFactory()}`,
|
|
514
|
+
role: "assistant",
|
|
515
|
+
content: "",
|
|
516
|
+
requestId,
|
|
517
|
+
blocks: [block]
|
|
518
|
+
});
|
|
519
|
+
return next;
|
|
520
|
+
}
|
|
521
|
+
function applyAssistantDelta(messages, requestId, blockType, delta) {
|
|
522
|
+
if (!delta) return messages;
|
|
523
|
+
const next = [...messages];
|
|
524
|
+
const index = latestAssistantIndex(next, requestId);
|
|
525
|
+
if (index < 0) {
|
|
526
|
+
const block = blockType === "text" ? { type: "text", content: delta } : { type: "thinking", content: delta };
|
|
527
|
+
next.push({
|
|
528
|
+
id: `assistant:${requestId ?? defaultIDFactory()}`,
|
|
529
|
+
role: "assistant",
|
|
530
|
+
content: blockType === "text" ? delta : "",
|
|
531
|
+
requestId,
|
|
532
|
+
blocks: [block]
|
|
533
|
+
});
|
|
534
|
+
return next;
|
|
535
|
+
}
|
|
536
|
+
const message = next[index];
|
|
537
|
+
const blocks = [...message?.blocks ?? []];
|
|
538
|
+
const last = blocks[blocks.length - 1];
|
|
539
|
+
if (last?.type === blockType) {
|
|
540
|
+
blocks[blocks.length - 1] = { ...last, content: last.content + delta };
|
|
541
|
+
} else {
|
|
542
|
+
blocks.push(blockType === "text" ? { type: "text", content: delta } : { type: "thinking", content: delta });
|
|
543
|
+
}
|
|
544
|
+
next[index] = {
|
|
545
|
+
...message,
|
|
546
|
+
content: blockType === "text" ? `${message?.content ?? ""}${delta}` : message?.content ?? "",
|
|
547
|
+
blocks
|
|
548
|
+
};
|
|
549
|
+
return next;
|
|
550
|
+
}
|
|
551
|
+
function latestAssistantIndex(messages, requestId) {
|
|
552
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
553
|
+
const message = messages[index];
|
|
554
|
+
if (message?.role !== "assistant") continue;
|
|
555
|
+
if (!requestId || message.requestId === requestId) return index;
|
|
556
|
+
}
|
|
557
|
+
return -1;
|
|
558
|
+
}
|
|
559
|
+
function markUserTurnRunning(setMessages, requestId) {
|
|
560
|
+
if (!requestId) return;
|
|
561
|
+
setMessages(
|
|
562
|
+
(current) => current.map(
|
|
563
|
+
(message) => message.role === "user" && message.requestId === requestId && isActiveStatus(message.status) && message.status !== "running" ? { ...message, status: "running" } : message
|
|
564
|
+
)
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
function buildAgentChatTurns(messages) {
|
|
568
|
+
const turns = [];
|
|
569
|
+
const turnByRequest = /* @__PURE__ */ new Map();
|
|
570
|
+
let latestUserTurn;
|
|
571
|
+
for (const message of messages) {
|
|
572
|
+
if (message.role === "system") {
|
|
573
|
+
turns.push({ id: message.id, systems: [message], assistants: [] });
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
if (message.role === "user") {
|
|
577
|
+
const turn2 = {
|
|
578
|
+
id: message.requestId ?? message.id,
|
|
579
|
+
requestId: message.requestId,
|
|
580
|
+
user: message,
|
|
581
|
+
assistants: [],
|
|
582
|
+
status: message.status
|
|
583
|
+
};
|
|
584
|
+
turns.push(turn2);
|
|
585
|
+
latestUserTurn = turn2;
|
|
586
|
+
if (message.requestId) turnByRequest.set(message.requestId, turn2);
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
const turn = message.requestId ? turnByRequest.get(message.requestId) : latestUserTurn;
|
|
590
|
+
if (turn) {
|
|
591
|
+
turn.assistants.push(message);
|
|
592
|
+
} else {
|
|
593
|
+
turns.push({ id: message.requestId ?? message.id, requestId: message.requestId, assistants: [message] });
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return turns;
|
|
597
|
+
}
|
|
598
|
+
function accumulateTokenUsage(current, incoming) {
|
|
599
|
+
if (!current) return incoming;
|
|
600
|
+
return {
|
|
601
|
+
input: current.input + incoming.input,
|
|
602
|
+
output: current.output + incoming.output,
|
|
603
|
+
cacheRead: (current.cacheRead ?? 0) + (incoming.cacheRead ?? 0),
|
|
604
|
+
cacheWrite: (current.cacheWrite ?? 0) + (incoming.cacheWrite ?? 0),
|
|
605
|
+
total: (current.total ?? 0) + (incoming.total ?? 0),
|
|
606
|
+
cost: incoming.cost ? {
|
|
607
|
+
input: (current.cost?.input ?? 0) + (incoming.cost.input ?? 0),
|
|
608
|
+
output: (current.cost?.output ?? 0) + (incoming.cost.output ?? 0),
|
|
609
|
+
cacheRead: (current.cost?.cacheRead ?? 0) + (incoming.cost.cacheRead ?? 0),
|
|
610
|
+
cacheWrite: (current.cost?.cacheWrite ?? 0) + (incoming.cost.cacheWrite ?? 0),
|
|
611
|
+
total: (current.cost?.total ?? 0) + (incoming.cost.total ?? 0)
|
|
612
|
+
} : current.cost
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
function useAutomationRun(options) {
|
|
616
|
+
const {
|
|
617
|
+
agentId,
|
|
618
|
+
inputs,
|
|
619
|
+
externalId,
|
|
620
|
+
autoStart = true,
|
|
621
|
+
pollIntervalMs = 2e3,
|
|
622
|
+
dedupeWithExternalId = true,
|
|
623
|
+
onComplete,
|
|
624
|
+
onCreated,
|
|
625
|
+
onUpdate
|
|
626
|
+
} = options;
|
|
627
|
+
const queryClient = useQueryClient();
|
|
628
|
+
const [currentRunId, setCurrentRunId] = useState(null);
|
|
629
|
+
const [errorV, setErrorV] = useState(null);
|
|
630
|
+
const startMutation = useMutation({
|
|
631
|
+
mutationFn: async () => {
|
|
632
|
+
setErrorV(null);
|
|
633
|
+
if (!agentId) throw new Error("agentId is required");
|
|
634
|
+
if (externalId && dedupeWithExternalId) {
|
|
635
|
+
return ensureAutomationRun({ agentId, inputs, externalId });
|
|
636
|
+
}
|
|
637
|
+
return createAutomationRun({ agentId, inputs, externalId });
|
|
638
|
+
},
|
|
639
|
+
onSuccess: (data) => {
|
|
640
|
+
setCurrentRunId(data.id);
|
|
641
|
+
queryClient.setQueryData(["automation-run", data.id], data);
|
|
642
|
+
onCreated?.(data);
|
|
643
|
+
onUpdate?.(data);
|
|
644
|
+
},
|
|
645
|
+
onError: (err) => {
|
|
646
|
+
setErrorV(err instanceof Error ? err.message : "Unable to start automation");
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
const cancelMutation = useMutation({
|
|
650
|
+
mutationFn: async (runId) => {
|
|
651
|
+
return cancelAutomationRun(runId);
|
|
652
|
+
},
|
|
653
|
+
onSuccess: (data) => {
|
|
654
|
+
queryClient.setQueryData(["automation-run", data.id], data);
|
|
655
|
+
onUpdate?.(data);
|
|
656
|
+
},
|
|
657
|
+
onError: (err) => {
|
|
658
|
+
setErrorV(err instanceof Error ? err.message : "Unable to cancel automation");
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
const {
|
|
662
|
+
data: run,
|
|
663
|
+
isError: isQueryError,
|
|
664
|
+
error: queryError,
|
|
665
|
+
refetch
|
|
666
|
+
} = useQuery({
|
|
667
|
+
queryKey: ["automation-run", currentRunId],
|
|
668
|
+
queryFn: async () => {
|
|
669
|
+
if (!currentRunId) return null;
|
|
670
|
+
return getAutomationRun(currentRunId);
|
|
671
|
+
},
|
|
672
|
+
enabled: !!currentRunId,
|
|
673
|
+
// Poll while active, stop when terminal
|
|
674
|
+
refetchInterval: (query) => {
|
|
675
|
+
const status = query.state.data?.status;
|
|
676
|
+
if (status && automationStatuses.TERMINAL_STATUSES.includes(status)) {
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
return pollIntervalMs;
|
|
680
|
+
},
|
|
681
|
+
// Immediately show data from setQueryData
|
|
682
|
+
initialData: void 0
|
|
683
|
+
});
|
|
684
|
+
const completedRunRef = useRef(null);
|
|
685
|
+
useEffect(() => {
|
|
686
|
+
if (run) {
|
|
687
|
+
onUpdate?.(run);
|
|
688
|
+
if (automationStatuses.TERMINAL_STATUSES.includes(run.status)) {
|
|
689
|
+
if (completedRunRef.current !== run.id) {
|
|
690
|
+
completedRunRef.current = run.id;
|
|
691
|
+
onComplete?.(run);
|
|
692
|
+
}
|
|
693
|
+
} else {
|
|
694
|
+
if (completedRunRef.current === run.id) {
|
|
695
|
+
completedRunRef.current = null;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}, [run, onComplete, onUpdate]);
|
|
700
|
+
useEffect(() => {
|
|
701
|
+
if (isQueryError && queryError) {
|
|
702
|
+
setErrorV(queryError instanceof Error ? queryError.message : "Failed to poll automation");
|
|
703
|
+
}
|
|
704
|
+
}, [isQueryError, queryError]);
|
|
705
|
+
const start = useCallback(async () => {
|
|
706
|
+
try {
|
|
707
|
+
const result = await startMutation.mutateAsync();
|
|
708
|
+
return result;
|
|
709
|
+
} catch {
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
}, [startMutation]);
|
|
713
|
+
const cancel = useCallback(async () => {
|
|
714
|
+
if (!currentRunId) return null;
|
|
715
|
+
try {
|
|
716
|
+
return await cancelMutation.mutateAsync(currentRunId);
|
|
717
|
+
} catch {
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
}, [cancelMutation, currentRunId]);
|
|
721
|
+
const refresh = useCallback(async () => {
|
|
722
|
+
if (!currentRunId) return start();
|
|
723
|
+
const result = await refetch();
|
|
724
|
+
return result.data ?? null;
|
|
725
|
+
}, [currentRunId, refetch, start]);
|
|
726
|
+
const autoStartedRef = useRef(false);
|
|
727
|
+
useEffect(() => {
|
|
728
|
+
if (autoStart && !autoStartedRef.current && !currentRunId) {
|
|
729
|
+
autoStartedRef.current = true;
|
|
730
|
+
void start();
|
|
731
|
+
}
|
|
732
|
+
}, [autoStart, currentRunId, start]);
|
|
733
|
+
return {
|
|
734
|
+
run: run ?? null,
|
|
735
|
+
status: run?.status ?? "idle",
|
|
736
|
+
isLoading: startMutation.isPending || cancelMutation.isPending,
|
|
737
|
+
error: errorV,
|
|
738
|
+
start,
|
|
739
|
+
refresh,
|
|
740
|
+
cancel
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
export {
|
|
744
|
+
useAutomationRun as a,
|
|
745
|
+
useAgentChat as u
|
|
746
|
+
};
|