@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.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
- const index = placed === void 0 ? -1 : ui.findIndex((m) => m.id === placed);
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
- if (onData && typeof part.type === "string" && part.type.startsWith("data-")) {
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 (approvalResponses) => {
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
- approvalResponses = void 0;
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
- appendCanonical(chat.assistantMessageFromTurn(turn));
227
- turnRef.current = void 0;
228
- if (turn.approvals.length > 0) {
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
- appendCanonical,
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 request = approvalsRef.current.find((p) => p.approvalId === response.approvalId);
414
- const verdict = response.token === void 0 && request?.token !== void 0 ? { ...response, token: request.token } : response;
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) => v.approvalId !== verdict.approvalId),
417
- verdict
674
+ ...verdictsRef.current.filter((v) => !recorded.some((r) => sameTarget(r, v))),
675
+ ...recorded
418
676
  ];
419
677
  const verdicts = verdictsRef.current;
420
- const allSettled = approvalsRef.current.every(
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
- await run(verdicts);
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
- let turn = inFlight;
444
- if (turn === void 0 || cursor === void 0) {
445
- if (inFlight === void 0) placedIdRef.current = void 0;
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
- turnRef.current = inFlight === void 0 ? void 0 : chat.sealAssistantTurn(inFlight);
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
- appendCanonical(chat.assistantMessageFromTurn(turn));
477
- turnRef.current = void 0;
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,