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