@deuz-sdk/react 2.1.0 → 2.3.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/README.md +71 -10
- package/dist/index.cjs +360 -82
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +84 -8
- package/dist/index.d.ts +84 -8
- package/dist/index.js +361 -83
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
2
|
-
import { filesToImageParts, uiFromMessages,
|
|
2
|
+
import { filesToImageParts, uiFromMessages, applyUIPart, clientToolResultMessage, assistantMessageFromTurn, createAssistantTurn, sealAssistantTurn, userMessageFromInput, dropTrailingAssistant, branchBeforeUserMessage, canonicalFromUI } from '@deuz-sdk/core/chat';
|
|
3
3
|
import { readDeuzStream, connectDeuzStream } from '@deuz-sdk/core/ui';
|
|
4
4
|
import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
|
|
5
5
|
|
|
@@ -16,6 +16,126 @@ function foldPart(turn, part) {
|
|
|
16
16
|
}
|
|
17
17
|
return next;
|
|
18
18
|
}
|
|
19
|
+
function sameTarget(a, b) {
|
|
20
|
+
return a.approvalId === b.approvalId && a.token === b.token;
|
|
21
|
+
}
|
|
22
|
+
function answersRequest(verdict, request) {
|
|
23
|
+
return verdict.approvalId === request.approvalId && (verdict.token === void 0 || verdict.token === request.token);
|
|
24
|
+
}
|
|
25
|
+
function sameRequest(a, b) {
|
|
26
|
+
return sameTarget(a, b) && a.toolCallId === b.toolCallId && (a.agentPath ?? []).join("\0") === (b.agentPath ?? []).join("\0");
|
|
27
|
+
}
|
|
28
|
+
var newLedger = (cursor) => ({
|
|
29
|
+
answeredApprovals: /* @__PURE__ */ new Set(),
|
|
30
|
+
legs: /* @__PURE__ */ new Map(),
|
|
31
|
+
openLeg: /* @__PURE__ */ new Set(),
|
|
32
|
+
cursor
|
|
33
|
+
});
|
|
34
|
+
function recordPart(ledger, turn, part) {
|
|
35
|
+
switch (part.type) {
|
|
36
|
+
case "tool-result":
|
|
37
|
+
turn.approvals.forEach((request, index) => {
|
|
38
|
+
if (request.toolCallId === part.toolCallId) ledger.answeredApprovals.add(index);
|
|
39
|
+
});
|
|
40
|
+
return;
|
|
41
|
+
case "tool-call":
|
|
42
|
+
ledger.openLeg.add(invocationKey(part.toolCallId, part.toolName, part.input));
|
|
43
|
+
return;
|
|
44
|
+
case "finish":
|
|
45
|
+
for (const key of ledger.openLeg) ledger.legs.set(key, (ledger.legs.get(key) ?? 0) + 1);
|
|
46
|
+
ledger.openLeg.clear();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
var deliveries = (ledger, key) => Math.max(1, (ledger.legs.get(key) ?? 0) + (ledger.openLeg.has(key) ? 1 : 0));
|
|
51
|
+
function isLaterId(id, cursor) {
|
|
52
|
+
return cursor !== void 0 && /^\d+$/.test(id) && /^\d+$/.test(cursor) && Number(id) > Number(cursor);
|
|
53
|
+
}
|
|
54
|
+
var NO_INDEXES = /* @__PURE__ */ new Set();
|
|
55
|
+
function openApprovals(turn, answered) {
|
|
56
|
+
const open = [];
|
|
57
|
+
turn.approvals.forEach((request, index) => {
|
|
58
|
+
if (answered.has(index)) return;
|
|
59
|
+
const seen = open.findIndex((entry) => sameRequest(entry, request));
|
|
60
|
+
if (seen === -1) open.push(request);
|
|
61
|
+
else open[seen] = request;
|
|
62
|
+
});
|
|
63
|
+
return open;
|
|
64
|
+
}
|
|
65
|
+
function sortedKeys(_key, value) {
|
|
66
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
|
|
67
|
+
return Object.fromEntries(
|
|
68
|
+
Object.entries(value).sort(
|
|
69
|
+
([a], [b]) => a < b ? -1 : a > b ? 1 : 0
|
|
70
|
+
)
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
function invocationKey(toolCallId, toolName, input) {
|
|
74
|
+
try {
|
|
75
|
+
return JSON.stringify([toolCallId, toolName, input ?? null], sortedKeys);
|
|
76
|
+
} catch {
|
|
77
|
+
return JSON.stringify([toolCallId, toolName]);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
var keyOfCall = (call) => invocationKey(call.toolCallId, call.toolName, call.input);
|
|
81
|
+
function answeredInvocations(history, behind) {
|
|
82
|
+
const answered = /* @__PURE__ */ new Map();
|
|
83
|
+
const results = /* @__PURE__ */ new Set();
|
|
84
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
85
|
+
const message = history[i];
|
|
86
|
+
if (message.role === "user") break;
|
|
87
|
+
if (!Array.isArray(message.content)) continue;
|
|
88
|
+
for (const part of message.content) {
|
|
89
|
+
if (part.type === "tool_result") results.add(part.toolUseId);
|
|
90
|
+
}
|
|
91
|
+
if (message.role !== "assistant") continue;
|
|
92
|
+
for (const part of message.content) {
|
|
93
|
+
if (part.type === "tool_use" && results.delete(part.id) && !behind?.has(message)) {
|
|
94
|
+
const key = invocationKey(part.id, part.name, part.input);
|
|
95
|
+
answered.set(key, (answered.get(key) ?? 0) + 1);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return answered;
|
|
100
|
+
}
|
|
101
|
+
function asClientToolResult(answer) {
|
|
102
|
+
return {
|
|
103
|
+
toolCallId: answer.toolCallId,
|
|
104
|
+
output: answer.result,
|
|
105
|
+
...answer.isError ? { isError: true } : {}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function nativeSuspension(turn) {
|
|
109
|
+
const agent = turn.agent;
|
|
110
|
+
if (agent?.status !== "suspended" || agent.runId === void 0) return void 0;
|
|
111
|
+
const cards = turn.message.toolCalls ?? [];
|
|
112
|
+
return {
|
|
113
|
+
runId: agent.runId,
|
|
114
|
+
approvals: agent.pendingApprovals,
|
|
115
|
+
clientCalls: agent.pendingClientCalls.map(
|
|
116
|
+
(call) => cards.find((card) => card.toolCallId === call.toolCallId && card.input !== void 0) ?? {
|
|
117
|
+
toolCallId: call.toolCallId,
|
|
118
|
+
toolName: call.toolName,
|
|
119
|
+
input: call.input,
|
|
120
|
+
state: "call"
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function unansweredCalls(history) {
|
|
126
|
+
const results = /* @__PURE__ */ new Set();
|
|
127
|
+
const open = /* @__PURE__ */ new Set();
|
|
128
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
129
|
+
const message = history[i];
|
|
130
|
+
if (message.role === "user") break;
|
|
131
|
+
if (!Array.isArray(message.content)) continue;
|
|
132
|
+
for (const part of message.content) {
|
|
133
|
+
if (part.type === "tool_result") results.add(part.toolUseId);
|
|
134
|
+
if (part.type === "tool_use" && !results.has(part.id)) open.add(part.id);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return open;
|
|
138
|
+
}
|
|
19
139
|
var defaultGenerateId = () => typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `deuz-ui-${Math.random().toString(36).slice(2)}`;
|
|
20
140
|
async function partsFromFiles(files) {
|
|
21
141
|
if (!files) return [];
|
|
@@ -61,8 +181,18 @@ function useChat(options) {
|
|
|
61
181
|
);
|
|
62
182
|
const approvalsRef = useRef([]);
|
|
63
183
|
const verdictsRef = useRef([]);
|
|
184
|
+
const nativeGateRef = useRef(void 0);
|
|
185
|
+
const parkedNativeRef = useRef(void 0);
|
|
186
|
+
const gateGenerationRef = useRef(0);
|
|
187
|
+
const priorResultsRef = useRef(
|
|
188
|
+
[]
|
|
189
|
+
);
|
|
190
|
+
const executingRef = useRef(/* @__PURE__ */ new Set());
|
|
64
191
|
const abortRef = useRef(null);
|
|
65
192
|
const lastEventIdRef = useRef(options.resume?.lastEventId);
|
|
193
|
+
const cursorTurnRef = useRef(void 0);
|
|
194
|
+
const cursorLedgerRef = useRef(newLedger());
|
|
195
|
+
const behindCursorRef = useRef([]);
|
|
66
196
|
const placedIdRef = useRef(void 0);
|
|
67
197
|
const throttleMs = options.throttleMs ?? 0;
|
|
68
198
|
const flushTimerRef = useRef(void 0);
|
|
@@ -104,7 +234,15 @@ function useChat(options) {
|
|
|
104
234
|
const previous = snapshotRef.current;
|
|
105
235
|
const placed = placedIdRef.current;
|
|
106
236
|
const ui = previous.history.ui;
|
|
107
|
-
|
|
237
|
+
let index = -1;
|
|
238
|
+
if (placed !== void 0) {
|
|
239
|
+
for (let i = ui.length - 1; i >= 0; i--) {
|
|
240
|
+
if (ui[i].id === placed) {
|
|
241
|
+
index = i;
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
108
246
|
placedIdRef.current = turn.message.id;
|
|
109
247
|
commit({
|
|
110
248
|
...previous,
|
|
@@ -131,7 +269,9 @@ function useChat(options) {
|
|
|
131
269
|
subAgents: turn.subAgents,
|
|
132
270
|
steps: turn.steps,
|
|
133
271
|
usage: turn.usage,
|
|
134
|
-
finishReason: turn.finishReason
|
|
272
|
+
finishReason: turn.finishReason,
|
|
273
|
+
stopDetails: turn.stopDetails,
|
|
274
|
+
agent: turn.agent
|
|
135
275
|
});
|
|
136
276
|
},
|
|
137
277
|
[commit]
|
|
@@ -150,8 +290,11 @@ function useChat(options) {
|
|
|
150
290
|
[commit]
|
|
151
291
|
);
|
|
152
292
|
const clearGates = useCallback(() => {
|
|
293
|
+
gateGenerationRef.current++;
|
|
153
294
|
approvalsRef.current = [];
|
|
154
295
|
verdictsRef.current = [];
|
|
296
|
+
nativeGateRef.current = void 0;
|
|
297
|
+
parkedNativeRef.current = void 0;
|
|
155
298
|
clientToolsRef.current = [];
|
|
156
299
|
toolResultsRef.current = [];
|
|
157
300
|
parkedTurnRef.current = void 0;
|
|
@@ -162,7 +305,8 @@ function useChat(options) {
|
|
|
162
305
|
(turn, part) => {
|
|
163
306
|
const next = foldPart(turn, part);
|
|
164
307
|
const onData = options.onData;
|
|
165
|
-
|
|
308
|
+
const agentFrame = part.type === "data-agent" && next.dataParts === turn.dataParts;
|
|
309
|
+
if (onData && !agentFrame && typeof part.type === "string" && part.type.startsWith("data-")) {
|
|
166
310
|
const data = part;
|
|
167
311
|
try {
|
|
168
312
|
onData({
|
|
@@ -173,6 +317,18 @@ function useChat(options) {
|
|
|
173
317
|
} catch {
|
|
174
318
|
}
|
|
175
319
|
}
|
|
320
|
+
if (part.type === "tool-result" && !(turn.message.toolCalls ?? []).some(
|
|
321
|
+
(call) => call.toolCallId === part.toolCallId && call.input !== void 0
|
|
322
|
+
)) {
|
|
323
|
+
priorResultsRef.current = [
|
|
324
|
+
...priorResultsRef.current,
|
|
325
|
+
{
|
|
326
|
+
toolCallId: part.toolCallId,
|
|
327
|
+
result: part.output,
|
|
328
|
+
...part.isError ? { isError: true } : {}
|
|
329
|
+
}
|
|
330
|
+
];
|
|
331
|
+
}
|
|
176
332
|
return next;
|
|
177
333
|
},
|
|
178
334
|
[options.onData]
|
|
@@ -180,13 +336,123 @@ function useChat(options) {
|
|
|
180
336
|
const stop = useCallback(() => {
|
|
181
337
|
abortRef.current?.abort();
|
|
182
338
|
}, []);
|
|
339
|
+
const settleClientCalls = useCallback(
|
|
340
|
+
async (settled, pending, native) => {
|
|
341
|
+
let turn = settled;
|
|
342
|
+
if (pending.length === 0) {
|
|
343
|
+
return native?.approvalResponses?.length ? { ...native } : void 0;
|
|
344
|
+
}
|
|
345
|
+
if (!options.onToolCall) {
|
|
346
|
+
parkedTurnRef.current = turn;
|
|
347
|
+
parkedNativeRef.current = native;
|
|
348
|
+
clientToolsRef.current = pending;
|
|
349
|
+
toolResultsRef.current = [];
|
|
350
|
+
setPendingToolCalls(pending);
|
|
351
|
+
return void 0;
|
|
352
|
+
}
|
|
353
|
+
const generation = gateGenerationRef.current;
|
|
354
|
+
const running = pending.map(keyOfCall);
|
|
355
|
+
for (const key of running) executingRef.current.add(key);
|
|
356
|
+
try {
|
|
357
|
+
const results = [];
|
|
358
|
+
for (const call of pending) {
|
|
359
|
+
let answer;
|
|
360
|
+
try {
|
|
361
|
+
const out = await options.onToolCall({
|
|
362
|
+
toolCallId: call.toolCallId,
|
|
363
|
+
toolName: call.toolName,
|
|
364
|
+
input: call.input
|
|
365
|
+
});
|
|
366
|
+
answer = { toolCallId: call.toolCallId, result: out };
|
|
367
|
+
} catch (err) {
|
|
368
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
369
|
+
answer = { toolCallId: call.toolCallId, result: message, isError: true };
|
|
370
|
+
}
|
|
371
|
+
if (gateGenerationRef.current !== generation) return void 0;
|
|
372
|
+
results.push(answer);
|
|
373
|
+
turn = applyUIPart(turn, {
|
|
374
|
+
type: "tool-result",
|
|
375
|
+
toolCallId: call.toolCallId,
|
|
376
|
+
toolName: call.toolName,
|
|
377
|
+
output: answer.result,
|
|
378
|
+
...answer.isError ? { isError: true } : {}
|
|
379
|
+
});
|
|
380
|
+
placedIdRef.current = turn.message.id;
|
|
381
|
+
syncTurn(turn);
|
|
382
|
+
}
|
|
383
|
+
appendCanonical(clientToolResultMessage(results));
|
|
384
|
+
return native ? { ...native, clientToolResults: results.map(asClientToolResult) } : {};
|
|
385
|
+
} finally {
|
|
386
|
+
for (const key of running) executingRef.current.delete(key);
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
[options.onToolCall, appendCanonical, syncTurn]
|
|
390
|
+
);
|
|
391
|
+
const settleTurn = useCallback(
|
|
392
|
+
async (settled, replay, cursorRead = false) => {
|
|
393
|
+
const turn = settled;
|
|
394
|
+
const history = snapshotRef.current.history.canonical;
|
|
395
|
+
const behind = replay?.ledger.afterCursor ? new Set(behindCursorRef.current) : void 0;
|
|
396
|
+
const open = unansweredCalls(history);
|
|
397
|
+
const answers = priorResultsRef.current.filter(
|
|
398
|
+
(entry, index, all) => open.has(entry.toolCallId) && all.findIndex((other) => other.toolCallId === entry.toolCallId) === index
|
|
399
|
+
);
|
|
400
|
+
priorResultsRef.current = [];
|
|
401
|
+
if (answers.length > 0) appendCanonical(clientToolResultMessage(answers));
|
|
402
|
+
appendCanonical(assistantMessageFromTurn(turn));
|
|
403
|
+
const ran = (turn.message.toolCalls ?? []).filter(
|
|
404
|
+
(call) => call.input !== void 0 && call.state === "result"
|
|
405
|
+
);
|
|
406
|
+
if (ran.length > 0) {
|
|
407
|
+
appendCanonical(
|
|
408
|
+
clientToolResultMessage(
|
|
409
|
+
ran.map((call) => ({
|
|
410
|
+
toolCallId: call.toolCallId,
|
|
411
|
+
result: call.output,
|
|
412
|
+
...call.isError ? { isError: true } : {}
|
|
413
|
+
}))
|
|
414
|
+
)
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
turnRef.current = void 0;
|
|
418
|
+
if (replay || cursorRead) behindCursorRef.current = snapshotRef.current.history.canonical;
|
|
419
|
+
const native = nativeSuspension(turn);
|
|
420
|
+
if (!native && turn.agent !== void 0) return void 0;
|
|
421
|
+
const answered = replay ? answeredInvocations(history, behind) : void 0;
|
|
422
|
+
const parked = new Set(clientToolsRef.current.map(keyOfCall));
|
|
423
|
+
const candidates = native ? native.clientCalls : (turn.message.toolCalls ?? []).filter(
|
|
424
|
+
// A card without input is a call of an earlier leg, never one to run.
|
|
425
|
+
(call) => call.state === "call" && call.input !== void 0
|
|
426
|
+
);
|
|
427
|
+
const clientPending = candidates.filter((call) => {
|
|
428
|
+
if (!replay || !answered) return true;
|
|
429
|
+
const key = keyOfCall(call);
|
|
430
|
+
return deliveries(replay.ledger, key) > (answered.get(key) ?? 0) && !parked.has(key) && !executingRef.current.has(key);
|
|
431
|
+
});
|
|
432
|
+
const gate = native ? native.approvals : openApprovals(turn, replay?.ledger.answeredApprovals ?? NO_INDEXES);
|
|
433
|
+
if (gate.length > 0) {
|
|
434
|
+
approvalsRef.current = gate;
|
|
435
|
+
verdictsRef.current = [];
|
|
436
|
+
nativeGateRef.current = native ? { runId: native.runId, turn, clientCalls: clientPending } : void 0;
|
|
437
|
+
setPendingApprovals(gate);
|
|
438
|
+
return void 0;
|
|
439
|
+
}
|
|
440
|
+
return settleClientCalls(turn, clientPending, native ? { runId: native.runId } : void 0);
|
|
441
|
+
},
|
|
442
|
+
[appendCanonical, settleClientCalls]
|
|
443
|
+
);
|
|
183
444
|
const run = useCallback(
|
|
184
|
-
async (
|
|
445
|
+
async (initial, gate) => {
|
|
185
446
|
const controller = new AbortController();
|
|
186
447
|
abortRef.current = controller;
|
|
187
448
|
setStatus("streaming");
|
|
188
449
|
setError(void 0);
|
|
450
|
+
let continuation = initial;
|
|
189
451
|
try {
|
|
452
|
+
if (gate) {
|
|
453
|
+
continuation = await settleClientCalls(gate.turn, gate.clientCalls, gate.resume);
|
|
454
|
+
if (!continuation || controller.signal.aborted) return;
|
|
455
|
+
}
|
|
190
456
|
for (; ; ) {
|
|
191
457
|
const doFetch = options.fetch ?? fetch;
|
|
192
458
|
const res = await doFetch(options.api, {
|
|
@@ -195,80 +461,55 @@ function useChat(options) {
|
|
|
195
461
|
body: JSON.stringify({
|
|
196
462
|
messages: snapshotRef.current.history.canonical,
|
|
197
463
|
...options.chatId !== void 0 ? { chatId: options.chatId } : {},
|
|
198
|
-
...approvalResponses?.length ? { approvalResponses } : {},
|
|
464
|
+
...continuation?.approvalResponses?.length ? { approvalResponses: continuation.approvalResponses } : {},
|
|
465
|
+
// A native run's resume (2.3); a chat route never gets these.
|
|
466
|
+
...continuation?.runId !== void 0 ? { runId: continuation.runId } : {},
|
|
467
|
+
...continuation?.clientToolResults?.length ? { clientToolResults: continuation.clientToolResults } : {},
|
|
199
468
|
...options.body
|
|
200
469
|
}),
|
|
201
470
|
signal: controller.signal
|
|
202
471
|
});
|
|
203
|
-
|
|
472
|
+
continuation = void 0;
|
|
204
473
|
let turn = createAssistantTurn(genId());
|
|
205
474
|
turnRef.current = turn;
|
|
475
|
+
priorResultsRef.current = [];
|
|
206
476
|
placedIdRef.current = void 0;
|
|
207
477
|
syncTurn(turn);
|
|
208
478
|
flush();
|
|
479
|
+
const resume = options.resume;
|
|
480
|
+
let cursorRead = false;
|
|
481
|
+
const ledger = newLedger(lastEventIdRef.current);
|
|
482
|
+
ledger.afterCursor = true;
|
|
209
483
|
for await (const part of readDeuzStream(res, {
|
|
210
|
-
...options.onHttpError ? { onHttpError: options.onHttpError } : {}
|
|
484
|
+
...options.onHttpError ? { onHttpError: options.onHttpError } : {},
|
|
485
|
+
...resume ? {
|
|
486
|
+
onEventId: (id) => {
|
|
487
|
+
lastEventIdRef.current = id;
|
|
488
|
+
cursorRead = true;
|
|
489
|
+
}
|
|
490
|
+
} : {}
|
|
211
491
|
})) {
|
|
492
|
+
if (resume) recordPart(ledger, turn, part);
|
|
212
493
|
turn = fold(turn, part);
|
|
213
494
|
turnRef.current = turn;
|
|
495
|
+
if (cursorRead) {
|
|
496
|
+
cursorTurnRef.current = turn;
|
|
497
|
+
cursorLedgerRef.current = ledger;
|
|
498
|
+
}
|
|
214
499
|
syncTurn(turn);
|
|
215
500
|
}
|
|
216
501
|
const sealed = sealAssistantTurn(turn);
|
|
217
502
|
if (sealed !== turn) {
|
|
218
503
|
turn = sealed;
|
|
219
504
|
turnRef.current = turn;
|
|
505
|
+
if (cursorRead) cursorTurnRef.current = turn;
|
|
220
506
|
syncTurn(turn);
|
|
221
507
|
}
|
|
222
508
|
flush();
|
|
223
509
|
if (turn.error !== void 0) throw new Error(turn.error);
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
approvalsRef.current = turn.approvals;
|
|
228
|
-
verdictsRef.current = [];
|
|
229
|
-
setPendingApprovals(turn.approvals);
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
const toolCalls = turn.message.toolCalls ?? [];
|
|
233
|
-
const serverResults = new Set(turn.serverResults);
|
|
234
|
-
const clientPending = toolCalls.filter((t) => !serverResults.has(t.toolCallId));
|
|
235
|
-
if (clientPending.length === 0) return;
|
|
236
|
-
if (!options.onToolCall) {
|
|
237
|
-
parkedTurnRef.current = turn;
|
|
238
|
-
clientToolsRef.current = clientPending;
|
|
239
|
-
toolResultsRef.current = [];
|
|
240
|
-
setPendingToolCalls(clientPending);
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
const results = [];
|
|
244
|
-
for (const call of clientPending) {
|
|
245
|
-
try {
|
|
246
|
-
const out = await options.onToolCall({
|
|
247
|
-
toolCallId: call.toolCallId,
|
|
248
|
-
toolName: call.toolName,
|
|
249
|
-
input: call.input
|
|
250
|
-
});
|
|
251
|
-
results.push({ toolCallId: call.toolCallId, result: out });
|
|
252
|
-
turn = applyUIPart(turn, {
|
|
253
|
-
type: "tool-result",
|
|
254
|
-
toolCallId: call.toolCallId,
|
|
255
|
-
toolName: call.toolName,
|
|
256
|
-
output: out
|
|
257
|
-
});
|
|
258
|
-
} catch (err) {
|
|
259
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
260
|
-
results.push({ toolCallId: call.toolCallId, result: message, isError: true });
|
|
261
|
-
turn = applyUIPart(turn, {
|
|
262
|
-
type: "tool-result",
|
|
263
|
-
toolCallId: call.toolCallId,
|
|
264
|
-
toolName: call.toolName,
|
|
265
|
-
output: message,
|
|
266
|
-
isError: true
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
syncTurn(turn);
|
|
270
|
-
}
|
|
271
|
-
appendCanonical(clientToolResultMessage(results));
|
|
510
|
+
const next = await settleTurn(turn, void 0, cursorRead);
|
|
511
|
+
if (!next) return;
|
|
512
|
+
continuation = next;
|
|
272
513
|
}
|
|
273
514
|
} catch (err) {
|
|
274
515
|
if (controller.signal.aborted) {
|
|
@@ -276,6 +517,7 @@ function useChat(options) {
|
|
|
276
517
|
if (open) {
|
|
277
518
|
const sealed = sealAssistantTurn(open);
|
|
278
519
|
turnRef.current = sealed;
|
|
520
|
+
if (cursorTurnRef.current === open) cursorTurnRef.current = sealed;
|
|
279
521
|
if (sealed !== open) syncTurn(sealed);
|
|
280
522
|
}
|
|
281
523
|
return;
|
|
@@ -286,7 +528,7 @@ function useChat(options) {
|
|
|
286
528
|
options.onError?.(e);
|
|
287
529
|
return;
|
|
288
530
|
} finally {
|
|
289
|
-
setStatus((s) => s === "error" ? s : "idle");
|
|
531
|
+
if (abortRef.current === controller) setStatus((s) => s === "error" ? s : "idle");
|
|
290
532
|
flush();
|
|
291
533
|
}
|
|
292
534
|
},
|
|
@@ -297,12 +539,13 @@ function useChat(options) {
|
|
|
297
539
|
options.body,
|
|
298
540
|
options.chatId,
|
|
299
541
|
options.generateId,
|
|
300
|
-
options.onToolCall,
|
|
301
542
|
options.onError,
|
|
302
543
|
options.onHttpError,
|
|
303
|
-
|
|
544
|
+
options.resume,
|
|
304
545
|
fold,
|
|
305
546
|
flush,
|
|
547
|
+
settleClientCalls,
|
|
548
|
+
settleTurn,
|
|
306
549
|
syncTurn
|
|
307
550
|
]
|
|
308
551
|
);
|
|
@@ -352,6 +595,10 @@ function useChat(options) {
|
|
|
352
595
|
history: { ui: [...next.ui], canonical: [...next.canonical] }
|
|
353
596
|
});
|
|
354
597
|
clearGates();
|
|
598
|
+
lastEventIdRef.current = void 0;
|
|
599
|
+
cursorTurnRef.current = void 0;
|
|
600
|
+
cursorLedgerRef.current = newLedger();
|
|
601
|
+
behindCursorRef.current = [];
|
|
355
602
|
flush();
|
|
356
603
|
},
|
|
357
604
|
[commit, clearGates, flush]
|
|
@@ -392,37 +639,55 @@ function useChat(options) {
|
|
|
392
639
|
...result.isError ? { isError: true } : {}
|
|
393
640
|
});
|
|
394
641
|
parkedTurnRef.current = next;
|
|
642
|
+
placedIdRef.current = next.message.id;
|
|
395
643
|
syncTurn(next);
|
|
396
644
|
flush();
|
|
397
645
|
}
|
|
398
646
|
const answered = toolResultsRef.current;
|
|
399
647
|
if (!parked.every((c) => answered.some((r) => r.toolCallId === c.toolCallId))) return;
|
|
648
|
+
const native = parkedNativeRef.current;
|
|
400
649
|
clientToolsRef.current = [];
|
|
401
650
|
toolResultsRef.current = [];
|
|
402
651
|
parkedTurnRef.current = void 0;
|
|
652
|
+
parkedNativeRef.current = void 0;
|
|
403
653
|
setPendingToolCalls([]);
|
|
404
654
|
appendCanonical(clientToolResultMessage(answered));
|
|
405
|
-
await run(
|
|
655
|
+
await run(
|
|
656
|
+
native ? { ...native, clientToolResults: answered.map(asClientToolResult) } : void 0
|
|
657
|
+
);
|
|
406
658
|
},
|
|
407
659
|
[run, appendCanonical, syncTurn, flush]
|
|
408
660
|
);
|
|
409
661
|
const addToolApprovalResponse = useCallback(
|
|
410
662
|
async (response) => {
|
|
411
|
-
const
|
|
412
|
-
const
|
|
663
|
+
const gate = approvalsRef.current;
|
|
664
|
+
const targets = gate.filter((request) => answersRequest(response, request));
|
|
665
|
+
if (targets.length === 0) return;
|
|
666
|
+
const recorded = [];
|
|
667
|
+
for (const request of targets) {
|
|
668
|
+
const verdict = response.token === void 0 && request.token !== void 0 ? { ...response, token: request.token } : response;
|
|
669
|
+
if (!recorded.some((v) => sameTarget(v, verdict))) recorded.push(verdict);
|
|
670
|
+
}
|
|
413
671
|
verdictsRef.current = [
|
|
414
|
-
...verdictsRef.current.filter((v) =>
|
|
415
|
-
|
|
672
|
+
...verdictsRef.current.filter((v) => !recorded.some((r) => sameTarget(r, v))),
|
|
673
|
+
...recorded
|
|
416
674
|
];
|
|
417
675
|
const verdicts = verdictsRef.current;
|
|
418
|
-
|
|
419
|
-
(p) => verdicts.some((v) => v.approvalId === p.approvalId)
|
|
420
|
-
);
|
|
421
|
-
if (!allSettled) return;
|
|
676
|
+
if (!gate.every((request) => verdicts.some((v) => sameTarget(v, request)))) return;
|
|
422
677
|
approvalsRef.current = [];
|
|
423
678
|
verdictsRef.current = [];
|
|
424
679
|
setPendingApprovals([]);
|
|
425
|
-
|
|
680
|
+
const native = nativeGateRef.current;
|
|
681
|
+
nativeGateRef.current = void 0;
|
|
682
|
+
if (native) {
|
|
683
|
+
await run(void 0, {
|
|
684
|
+
turn: native.turn,
|
|
685
|
+
clientCalls: native.clientCalls,
|
|
686
|
+
resume: { runId: native.runId, approvalResponses: verdicts }
|
|
687
|
+
});
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
await run({ approvalResponses: verdicts });
|
|
426
691
|
},
|
|
427
692
|
[run]
|
|
428
693
|
);
|
|
@@ -438,15 +703,18 @@ function useChat(options) {
|
|
|
438
703
|
try {
|
|
439
704
|
const cursor = lastEventIdRef.current ?? loadCursor(resume);
|
|
440
705
|
const inFlight = turnRef.current;
|
|
441
|
-
|
|
442
|
-
if (
|
|
443
|
-
|
|
444
|
-
turn = createAssistantTurn(genId());
|
|
445
|
-
}
|
|
706
|
+
const continuing = inFlight !== void 0 && cursor !== void 0 && cursorTurnRef.current === inFlight;
|
|
707
|
+
if (inFlight === void 0) placedIdRef.current = void 0;
|
|
708
|
+
let turn = continuing && inFlight ? inFlight : createAssistantTurn(genId());
|
|
446
709
|
turnRef.current = turn;
|
|
710
|
+
cursorTurnRef.current = turn;
|
|
711
|
+
if (!continuing) priorResultsRef.current = [];
|
|
712
|
+
const ledger = continuing ? cursorLedgerRef.current : newLedger(cursor);
|
|
713
|
+
cursorLedgerRef.current = ledger;
|
|
447
714
|
const parts = connectDeuzStream(resume.endpoint, {
|
|
448
715
|
...cursor !== void 0 ? { lastEventId: cursor } : {},
|
|
449
716
|
onCursor: (id) => {
|
|
717
|
+
ledger.afterCursor ??= isLaterId(id, ledger.cursor);
|
|
450
718
|
lastEventIdRef.current = id;
|
|
451
719
|
saveCursor(resume, id);
|
|
452
720
|
},
|
|
@@ -456,12 +724,23 @@ function useChat(options) {
|
|
|
456
724
|
});
|
|
457
725
|
for await (const part of parts) {
|
|
458
726
|
folded++;
|
|
727
|
+
recordPart(ledger, turn, part);
|
|
459
728
|
turn = fold(turn, part);
|
|
460
729
|
turnRef.current = turn;
|
|
730
|
+
cursorTurnRef.current = turn;
|
|
461
731
|
syncTurn(turn);
|
|
462
732
|
}
|
|
463
733
|
if (folded === 0) {
|
|
464
|
-
|
|
734
|
+
const kept = inFlight === void 0 ? void 0 : sealAssistantTurn(inFlight);
|
|
735
|
+
turnRef.current = kept;
|
|
736
|
+
cursorTurnRef.current = continuing ? kept : void 0;
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
if (controller.signal.aborted) {
|
|
740
|
+
const stopped = sealAssistantTurn(turn);
|
|
741
|
+
turnRef.current = stopped;
|
|
742
|
+
cursorTurnRef.current = stopped;
|
|
743
|
+
if (stopped !== turn) syncTurn(stopped);
|
|
465
744
|
return;
|
|
466
745
|
}
|
|
467
746
|
const sealed = sealAssistantTurn(turn);
|
|
@@ -470,14 +749,10 @@ function useChat(options) {
|
|
|
470
749
|
syncTurn(turn);
|
|
471
750
|
}
|
|
472
751
|
turnRef.current = turn;
|
|
752
|
+
cursorTurnRef.current = turn;
|
|
473
753
|
if (turn.error !== void 0) throw new Error(turn.error);
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
if (turn.approvals.length > 0) {
|
|
477
|
-
approvalsRef.current = turn.approvals;
|
|
478
|
-
verdictsRef.current = [];
|
|
479
|
-
setPendingApprovals(turn.approvals);
|
|
480
|
-
}
|
|
754
|
+
const next = await settleTurn(turn, { ledger });
|
|
755
|
+
if (next && !controller.signal.aborted) await run(next);
|
|
481
756
|
} catch (err) {
|
|
482
757
|
if (controller.signal.aborted) return;
|
|
483
758
|
if (silent) return;
|
|
@@ -497,9 +772,10 @@ function useChat(options) {
|
|
|
497
772
|
options.headers,
|
|
498
773
|
options.generateId,
|
|
499
774
|
options.onError,
|
|
500
|
-
appendCanonical,
|
|
501
775
|
fold,
|
|
502
776
|
flush,
|
|
777
|
+
run,
|
|
778
|
+
settleTurn,
|
|
503
779
|
syncTurn
|
|
504
780
|
]
|
|
505
781
|
);
|
|
@@ -530,6 +806,8 @@ function useChat(options) {
|
|
|
530
806
|
...snapshot.steps !== void 0 ? { steps: snapshot.steps } : {},
|
|
531
807
|
...snapshot.usage !== void 0 ? { usage: snapshot.usage } : {},
|
|
532
808
|
...snapshot.finishReason !== void 0 ? { finishReason: snapshot.finishReason } : {},
|
|
809
|
+
...snapshot.stopDetails !== void 0 ? { stopDetails: snapshot.stopDetails } : {},
|
|
810
|
+
...snapshot.agent !== void 0 ? { agent: snapshot.agent } : {},
|
|
533
811
|
sendMessage,
|
|
534
812
|
stop,
|
|
535
813
|
regenerate,
|