@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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { useState, useRef, useCallback, useEffect } from 'react';
2
- import { filesToImageParts, uiFromMessages, createAssistantTurn, sealAssistantTurn, assistantMessageFromTurn, applyUIPart, clientToolResultMessage, userMessageFromInput, dropTrailingAssistant, branchBeforeUserMessage, canonicalFromUI } from '@deuz-sdk/core/chat';
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
- const index = placed === void 0 ? -1 : ui.findIndex((m) => m.id === placed);
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
- if (onData && typeof part.type === "string" && part.type.startsWith("data-")) {
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 (approvalResponses) => {
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
- approvalResponses = void 0;
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
- appendCanonical(assistantMessageFromTurn(turn));
225
- turnRef.current = void 0;
226
- if (turn.approvals.length > 0) {
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
- appendCanonical,
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 request = approvalsRef.current.find((p) => p.approvalId === response.approvalId);
412
- const verdict = response.token === void 0 && request?.token !== void 0 ? { ...response, token: request.token } : response;
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) => v.approvalId !== verdict.approvalId),
415
- verdict
672
+ ...verdictsRef.current.filter((v) => !recorded.some((r) => sameTarget(r, v))),
673
+ ...recorded
416
674
  ];
417
675
  const verdicts = verdictsRef.current;
418
- const allSettled = approvalsRef.current.every(
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
- await run(verdicts);
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
- let turn = inFlight;
442
- if (turn === void 0 || cursor === void 0) {
443
- if (inFlight === void 0) placedIdRef.current = void 0;
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
- turnRef.current = inFlight === void 0 ? void 0 : sealAssistantTurn(inFlight);
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
- appendCanonical(assistantMessageFromTurn(turn));
475
- turnRef.current = void 0;
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,