@truefoundry/assistant-ui-runtime 0.1.0-rc.1 → 0.1.2
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 +45 -77
- package/dist/index.d.ts +14 -2
- package/dist/index.js +697 -147
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/agentSessionModule.d.ts +14 -2
- package/src/convertTurnMessages.test.ts +502 -1
- package/src/convertTurnMessages.ts +471 -28
- package/src/createSubAgent.ts +13 -5
- package/src/draftAgentConfig.test.ts +1 -1
- package/src/foldPeerThreads.test.ts +56 -0
- package/src/foldPeerThreads.ts +7 -1
- package/src/hooks.ts +24 -0
- package/src/index.ts +7 -5
- package/src/loadSessionSnapshot.test.ts +21 -6
- package/src/loadSessionSnapshot.ts +9 -5
- package/src/{bindDraftAgentSession.test.ts → private/bindDraftAgentSession.test.ts} +3 -2
- package/src/{draftSessionBridge.ts → private/draftSessionBridge.ts} +8 -2
- package/src/{truefoundryDraftThreadListAdapter.ts → private/truefoundryDraftThreadListAdapter.ts} +1 -1
- package/src/private/useDraftAgentSpec.test.tsx +153 -0
- package/src/{useDraftAgentSpec.ts → private/useDraftAgentSpec.ts} +80 -3
- package/src/sessionSnapshot.ts +48 -2
- package/src/sessions.ts +1 -1
- package/src/streamTurn.test.ts +34 -0
- package/src/streamTurn.ts +9 -1
- package/src/truefoundryExtras.ts +5 -1
- package/src/types.ts +1 -1
- package/src/useTrueFoundryAgentMessages.test.tsx +275 -1
- package/src/useTrueFoundryAgentMessages.ts +263 -70
- package/src/useTrueFoundryAgentRuntime.ts +51 -13
- /package/src/{agentSpec.ts → private/agentSpec.ts} +0 -0
- /package/src/{bindDraftAgentSession.ts → private/bindDraftAgentSession.ts} +0 -0
- /package/src/{truefoundryDraftThreadListAdapter.test.ts → private/truefoundryDraftThreadListAdapter.test.ts} +0 -0
|
@@ -16,11 +16,12 @@ import type { TrueFoundryGateway } from "truefoundry-gateway-sdk";
|
|
|
16
16
|
import { ROOT_THREAD_ID } from "./constants.js";
|
|
17
17
|
import {
|
|
18
18
|
buildEditedUserMessageContent,
|
|
19
|
-
|
|
19
|
+
buildSnapshotBeforeTurn,
|
|
20
20
|
computeGroupRootBaseline,
|
|
21
21
|
extractTurnUserMessageContent,
|
|
22
|
+
prependOlderSessionHistory,
|
|
22
23
|
projectSessionMessages,
|
|
23
|
-
|
|
24
|
+
resolveGatewayBranchPreviousTurnIdForTurn,
|
|
24
25
|
rootModelMessageIdsSinceBaseline,
|
|
25
26
|
userMessageContentToText,
|
|
26
27
|
type UserMessageContent,
|
|
@@ -57,6 +58,8 @@ import type { TurnStreamUpdate } from "./turnStreamUpdate.js";
|
|
|
57
58
|
export type UseTrueFoundryAgentMessagesOptions = {
|
|
58
59
|
client: AgentSessionClient;
|
|
59
60
|
sessionId: string | undefined;
|
|
61
|
+
/** When true the thread is the currently selected (main) thread. */
|
|
62
|
+
isMain?: boolean | undefined;
|
|
60
63
|
listEventsConcurrency?: number | undefined;
|
|
61
64
|
onError?: ((error: unknown) => void) | undefined;
|
|
62
65
|
initializeSession?: () => Promise<{
|
|
@@ -67,10 +70,24 @@ export type UseTrueFoundryAgentMessagesOptions = {
|
|
|
67
70
|
resolveConversationSessionId?: (remoteId: string) => Promise<string>;
|
|
68
71
|
/** When set, turns bind to `/agents/sessions/{draftSessionId}/turns` after draft validation. */
|
|
69
72
|
draftGateway?: TrueFoundryGateway;
|
|
73
|
+
/**
|
|
74
|
+
* Optional per-turn headers for createTurn. Invoked once per `sendTurn` after
|
|
75
|
+
* the session is resolved; return value is forwarded to `turn.execute`.
|
|
76
|
+
*/
|
|
77
|
+
getTurnHeaders?: () => Promise<Record<string, string> | undefined>;
|
|
70
78
|
};
|
|
71
79
|
|
|
72
80
|
export type SendTurnOptions =
|
|
73
|
-
| {
|
|
81
|
+
| {
|
|
82
|
+
userMessage: UserMessageContent;
|
|
83
|
+
previousTurnId?: string | null;
|
|
84
|
+
/**
|
|
85
|
+
* When branching (edit/reset), the already-rewound history to send from.
|
|
86
|
+
* Applied atomically with `pendingUser` so a stale React snapshot cannot
|
|
87
|
+
* keep pre-branch turns while the new user message is appended.
|
|
88
|
+
*/
|
|
89
|
+
branchFromSnapshot?: SessionSnapshot;
|
|
90
|
+
}
|
|
74
91
|
| { inputs: RequiredActionInput[] }
|
|
75
92
|
| { resumeMcpAuth: true };
|
|
76
93
|
|
|
@@ -272,11 +289,13 @@ function resolveTurnInput(
|
|
|
272
289
|
export function useTrueFoundryAgentMessages({
|
|
273
290
|
client,
|
|
274
291
|
sessionId,
|
|
292
|
+
isMain,
|
|
275
293
|
listEventsConcurrency,
|
|
276
294
|
onError,
|
|
277
295
|
initializeSession,
|
|
278
296
|
resolveConversationSessionId,
|
|
279
297
|
draftGateway,
|
|
298
|
+
getTurnHeaders,
|
|
280
299
|
}: UseTrueFoundryAgentMessagesOptions) {
|
|
281
300
|
const sessionOptions = useMemo<GetSessionOptions | undefined>(
|
|
282
301
|
() =>
|
|
@@ -287,15 +306,28 @@ export function useTrueFoundryAgentMessages({
|
|
|
287
306
|
const [snapshot, setSnapshot] = useState<SessionSnapshot>(createEmptySessionSnapshot);
|
|
288
307
|
const [isRunning, setIsRunning] = useState(false);
|
|
289
308
|
const [isLoading, setIsLoading] = useState(false);
|
|
309
|
+
const [isLoadingOlderHistory, setIsLoadingOlderHistory] = useState(false);
|
|
310
|
+
const [loadRetryTrigger, setLoadRetryTrigger] = useState(0);
|
|
290
311
|
|
|
291
312
|
const snapshotRef = useRef(snapshot);
|
|
292
313
|
snapshotRef.current = snapshot;
|
|
314
|
+
const loadOlderInflightRef = useRef<Promise<void> | null>(null);
|
|
315
|
+
|
|
316
|
+
const onErrorRef = useRef(onError);
|
|
317
|
+
onErrorRef.current = onError;
|
|
318
|
+
const resolveConversationSessionIdRef = useRef(resolveConversationSessionId);
|
|
319
|
+
resolveConversationSessionIdRef.current = resolveConversationSessionId;
|
|
320
|
+
const initializeSessionRef = useRef(initializeSession);
|
|
321
|
+
initializeSessionRef.current = initializeSession;
|
|
322
|
+
const getTurnHeadersRef = useRef(getTurnHeaders);
|
|
323
|
+
getTurnHeadersRef.current = getTurnHeaders;
|
|
293
324
|
|
|
294
325
|
const createdAtByMessageIdRef = useRef(new Map<string, Date>());
|
|
295
326
|
const abortControllerRef = useRef<AbortController | null>(null);
|
|
296
327
|
const activeRunRef = useRef<Promise<void> | null>(null);
|
|
297
328
|
const runningTurnRef = useRef<Turn | undefined>(undefined);
|
|
298
329
|
const loadGenerationRef = useRef(0);
|
|
330
|
+
const streamGenerationRef = useRef(0);
|
|
299
331
|
const lazilyCreatedSessionIdRef = useRef<string | undefined>(undefined);
|
|
300
332
|
|
|
301
333
|
const projectOptions = useMemo(
|
|
@@ -318,47 +350,81 @@ export function useTrueFoundryAgentMessages({
|
|
|
318
350
|
[snapshot, projectOptions],
|
|
319
351
|
);
|
|
320
352
|
|
|
321
|
-
const applyStreamUpdate = useCallback(
|
|
322
|
-
(update: TurnStreamUpdate, turnId: string, isContinuation: boolean) => {
|
|
323
|
-
setSnapshot((prev) =>
|
|
324
|
-
replaceSessionSnapshot(prev, {
|
|
325
|
-
activeStream: {
|
|
326
|
-
turnId,
|
|
327
|
-
update,
|
|
328
|
-
isContinuation,
|
|
329
|
-
},
|
|
330
|
-
}),
|
|
331
|
-
);
|
|
332
|
-
},
|
|
333
|
-
[],
|
|
334
|
-
);
|
|
335
|
-
|
|
336
353
|
const runStream = useCallback(
|
|
337
354
|
(
|
|
338
355
|
createStream: (signal: AbortSignal) => AsyncGenerator<TurnStreamUpdate>,
|
|
339
356
|
turnId: string,
|
|
340
357
|
isContinuation: boolean,
|
|
341
358
|
): Promise<void> => {
|
|
359
|
+
const streamGeneration = ++streamGenerationRef.current;
|
|
342
360
|
abortControllerRef.current?.abort();
|
|
343
361
|
const abortController = new AbortController();
|
|
344
362
|
abortControllerRef.current = abortController;
|
|
345
363
|
setIsRunning(true);
|
|
346
364
|
|
|
347
365
|
const run = (async () => {
|
|
366
|
+
// Sub-agent turns can emit 100+ stream events per frame. Coalesce to one
|
|
367
|
+
// setSnapshot per animation frame so assistant-ui does not remount the whole
|
|
368
|
+
// message tree (UI hang). The buffer belongs to this stream only.
|
|
369
|
+
let pendingStreamUpdate: {
|
|
370
|
+
update: TurnStreamUpdate;
|
|
371
|
+
turnId: string;
|
|
372
|
+
isContinuation: boolean;
|
|
373
|
+
} | null = null;
|
|
374
|
+
let streamUpdateRaf: number | null = null;
|
|
375
|
+
|
|
376
|
+
const flushPendingStreamUpdate = () => {
|
|
377
|
+
streamUpdateRaf = null;
|
|
378
|
+
const pending = pendingStreamUpdate;
|
|
379
|
+
pendingStreamUpdate = null;
|
|
380
|
+
if (
|
|
381
|
+
pending == null ||
|
|
382
|
+
streamGeneration !== streamGenerationRef.current
|
|
383
|
+
) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const { update, turnId: pendingTurnId, isContinuation: pendingIsContinuation } =
|
|
387
|
+
pending;
|
|
388
|
+
setSnapshot((prev) =>
|
|
389
|
+
replaceSessionSnapshot(prev, {
|
|
390
|
+
activeStream: {
|
|
391
|
+
turnId: pendingTurnId,
|
|
392
|
+
update,
|
|
393
|
+
isContinuation: pendingIsContinuation,
|
|
394
|
+
},
|
|
395
|
+
}),
|
|
396
|
+
);
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const applyStreamUpdate = (update: TurnStreamUpdate) => {
|
|
400
|
+
pendingStreamUpdate = { update, turnId, isContinuation };
|
|
401
|
+
if (streamUpdateRaf == null) {
|
|
402
|
+
streamUpdateRaf = requestAnimationFrame(flushPendingStreamUpdate);
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
|
|
348
406
|
try {
|
|
349
407
|
for await (const update of createStream(abortController.signal)) {
|
|
350
408
|
if (abortController.signal.aborted) {
|
|
351
409
|
return;
|
|
352
410
|
}
|
|
353
|
-
applyStreamUpdate(update
|
|
411
|
+
applyStreamUpdate(update);
|
|
354
412
|
}
|
|
355
413
|
} catch (error) {
|
|
356
414
|
if (error instanceof Error && error.name === "AbortError") {
|
|
357
415
|
return;
|
|
358
416
|
}
|
|
359
|
-
|
|
417
|
+
onErrorRef.current?.(error);
|
|
360
418
|
throw error;
|
|
361
419
|
} finally {
|
|
420
|
+
if (streamUpdateRaf != null) {
|
|
421
|
+
cancelAnimationFrame(streamUpdateRaf);
|
|
422
|
+
streamUpdateRaf = null;
|
|
423
|
+
}
|
|
424
|
+
if (streamGeneration !== streamGenerationRef.current) {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
flushPendingStreamUpdate();
|
|
362
428
|
if (abortControllerRef.current === abortController) {
|
|
363
429
|
abortControllerRef.current = null;
|
|
364
430
|
}
|
|
@@ -392,7 +458,7 @@ export function useTrueFoundryAgentMessages({
|
|
|
392
458
|
});
|
|
393
459
|
return run;
|
|
394
460
|
},
|
|
395
|
-
[
|
|
461
|
+
[onError],
|
|
396
462
|
);
|
|
397
463
|
|
|
398
464
|
const load = useCallback(async () => {
|
|
@@ -401,24 +467,47 @@ export function useTrueFoundryAgentMessages({
|
|
|
401
467
|
setSnapshot(createEmptySessionSnapshot());
|
|
402
468
|
return;
|
|
403
469
|
}
|
|
470
|
+
|
|
471
|
+
// Thread components are never unmounted on navigation — isMain going
|
|
472
|
+
// false→true is the only reliable signal that the user has clicked on
|
|
473
|
+
// this thread. Skip the load when this thread is not the active one
|
|
474
|
+
// so that isMain being a dep triggers a fresh load on every selection.
|
|
475
|
+
if (isMain === false) return;
|
|
476
|
+
|
|
477
|
+
// When we are loading a *different* session the user has navigated away
|
|
478
|
+
// from the lazily-created one — clear the guard so navigating back to it
|
|
479
|
+
// later triggers a proper reload instead of silently skipping.
|
|
480
|
+
if (lazilyCreatedSessionIdRef.current != null && sessionId !== lazilyCreatedSessionIdRef.current) {
|
|
481
|
+
lazilyCreatedSessionIdRef.current = undefined;
|
|
482
|
+
}
|
|
483
|
+
|
|
404
484
|
if (sessionId === lazilyCreatedSessionIdRef.current) {
|
|
405
485
|
return;
|
|
406
486
|
}
|
|
407
487
|
|
|
408
488
|
const generation = ++loadGenerationRef.current;
|
|
489
|
+
++streamGenerationRef.current;
|
|
409
490
|
abortControllerRef.current?.abort();
|
|
491
|
+
loadOlderInflightRef.current = null;
|
|
492
|
+
createdAtByMessageIdRef.current = new Map();
|
|
493
|
+
setSnapshot(createEmptySessionSnapshot());
|
|
410
494
|
setIsLoading(true);
|
|
495
|
+
setIsLoadingOlderHistory(false);
|
|
411
496
|
|
|
412
497
|
try {
|
|
413
498
|
const conversationSessionId = await resolveActiveSessionId(
|
|
414
499
|
sessionId,
|
|
415
|
-
|
|
500
|
+
resolveConversationSessionIdRef.current,
|
|
416
501
|
);
|
|
417
502
|
const loadedSnapshot = await loadSessionSnapshot(
|
|
418
503
|
client,
|
|
419
504
|
conversationSessionId,
|
|
420
|
-
listEventsConcurrency,
|
|
421
505
|
sessionOptions,
|
|
506
|
+
(snap) => {
|
|
507
|
+
if (generation === loadGenerationRef.current) {
|
|
508
|
+
setSnapshot(snap);
|
|
509
|
+
}
|
|
510
|
+
},
|
|
422
511
|
);
|
|
423
512
|
if (generation !== loadGenerationRef.current) {
|
|
424
513
|
return;
|
|
@@ -428,32 +517,44 @@ export function useTrueFoundryAgentMessages({
|
|
|
428
517
|
setSnapshot(loadedSnapshot);
|
|
429
518
|
runningTurnRef.current = loadedSnapshot.runningTurn;
|
|
430
519
|
|
|
520
|
+
// History (and any seeded pendingUser for the in-flight turn) is
|
|
521
|
+
// ready — clear loading before resuming. Awaiting the subscribe
|
|
522
|
+
// stream here previously kept isLoading true for the entire
|
|
523
|
+
// backend run, so reconnect UIs stayed on shimmers even though
|
|
524
|
+
// subscribe was already live.
|
|
525
|
+
setIsLoading(false);
|
|
526
|
+
|
|
431
527
|
if (loadedSnapshot.runningTurn != null) {
|
|
432
528
|
const turn = loadedSnapshot.runningTurn;
|
|
433
529
|
const isContinuation = !extractTurnUserText(turn.input);
|
|
434
530
|
// TODO: pass afterSequenceNumber once stream ingestion tracks sequence numbers.
|
|
435
|
-
|
|
531
|
+
// Use loadedSnapshot directly — snapshotRef.current still points at
|
|
532
|
+
// the empty snapshot cleared above until the setSnapshot(loadedSnapshot)
|
|
533
|
+
// call re-renders.
|
|
534
|
+
void runStream(
|
|
436
535
|
(signal) =>
|
|
437
536
|
resumeTurnStream(
|
|
438
537
|
turn,
|
|
439
|
-
|
|
538
|
+
loadedSnapshot.fold,
|
|
440
539
|
signal,
|
|
441
540
|
undefined,
|
|
442
|
-
|
|
541
|
+
loadedSnapshot.groupRootBaseline,
|
|
443
542
|
),
|
|
444
543
|
turn.id,
|
|
445
544
|
isContinuation,
|
|
446
|
-
);
|
|
545
|
+
).catch(() => undefined);
|
|
447
546
|
}
|
|
448
547
|
} catch (error) {
|
|
449
|
-
|
|
548
|
+
if (generation === loadGenerationRef.current) {
|
|
549
|
+
onErrorRef.current?.(error);
|
|
550
|
+
}
|
|
450
551
|
throw error;
|
|
451
552
|
} finally {
|
|
452
553
|
if (generation === loadGenerationRef.current) {
|
|
453
554
|
setIsLoading(false);
|
|
454
555
|
}
|
|
455
556
|
}
|
|
456
|
-
}, [client,
|
|
557
|
+
}, [client, runStream, sessionId, sessionOptions, loadRetryTrigger, isMain]);
|
|
457
558
|
|
|
458
559
|
useEffect(() => {
|
|
459
560
|
void load().catch(() => undefined);
|
|
@@ -463,19 +564,22 @@ export function useTrueFoundryAgentMessages({
|
|
|
463
564
|
async (options: SendTurnOptions) => {
|
|
464
565
|
let activeSessionId = sessionId;
|
|
465
566
|
if (activeSessionId == null) {
|
|
466
|
-
if (
|
|
567
|
+
if (initializeSessionRef.current == null) {
|
|
467
568
|
throw new Error("Cannot send a turn without an active session.");
|
|
468
569
|
}
|
|
469
|
-
const { remoteId } = await
|
|
570
|
+
const { remoteId } = await initializeSessionRef.current();
|
|
470
571
|
activeSessionId = remoteId;
|
|
471
572
|
lazilyCreatedSessionIdRef.current = remoteId;
|
|
472
573
|
}
|
|
473
574
|
|
|
474
575
|
const conversationSessionId = await resolveActiveSessionId(
|
|
475
576
|
activeSessionId,
|
|
476
|
-
|
|
577
|
+
resolveConversationSessionIdRef.current,
|
|
477
578
|
);
|
|
478
579
|
const session = await getSession(client, conversationSessionId, sessionOptions);
|
|
580
|
+
const turnHeaders = await getTurnHeadersRef.current?.();
|
|
581
|
+
const streamHeaders =
|
|
582
|
+
turnHeaders != null ? { headers: turnHeaders } : {};
|
|
479
583
|
const isContinuation =
|
|
480
584
|
"inputs" in options ||
|
|
481
585
|
("resumeMcpAuth" in options && options.resumeMcpAuth === true);
|
|
@@ -492,34 +596,57 @@ export function useTrueFoundryAgentMessages({
|
|
|
492
596
|
);
|
|
493
597
|
}
|
|
494
598
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
prev,
|
|
498
|
-
"inputs" in options ? options.inputs : undefined,
|
|
499
|
-
),
|
|
500
|
-
);
|
|
599
|
+
const branchBase =
|
|
600
|
+
"userMessage" in options ? options.branchFromSnapshot : undefined;
|
|
501
601
|
|
|
502
602
|
let groupRootBaseline: readonly string[] | undefined;
|
|
503
603
|
|
|
504
|
-
if ("userMessage" in options) {
|
|
505
|
-
|
|
506
|
-
|
|
604
|
+
if (branchBase != null && "userMessage" in options) {
|
|
605
|
+
// Atomic apply: never merge pendingUser onto a stale React `prev`
|
|
606
|
+
// that still holds pre-branch turns (edit would show old + new).
|
|
607
|
+
const rootBucket = branchBase.fold.threads.get(ROOT_THREAD_ID);
|
|
507
608
|
groupRootBaseline = [...(rootBucket?.modelMessageIds ?? [])];
|
|
609
|
+
const nextSnapshot = replaceSessionSnapshot(branchBase, {
|
|
610
|
+
pendingUser: {
|
|
611
|
+
turnId,
|
|
612
|
+
content: options.userMessage,
|
|
613
|
+
createdAt: new Date(),
|
|
614
|
+
},
|
|
615
|
+
activeStream: undefined,
|
|
616
|
+
groupRootBaseline,
|
|
617
|
+
});
|
|
618
|
+
snapshotRef.current = nextSnapshot;
|
|
619
|
+
setSnapshot(nextSnapshot);
|
|
620
|
+
} else {
|
|
508
621
|
setSnapshot((prev) =>
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
createdAt: new Date(),
|
|
514
|
-
},
|
|
515
|
-
activeStream: undefined,
|
|
516
|
-
groupRootBaseline,
|
|
517
|
-
}),
|
|
622
|
+
commitActiveStream(
|
|
623
|
+
prev,
|
|
624
|
+
"inputs" in options ? options.inputs : undefined,
|
|
625
|
+
),
|
|
518
626
|
);
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
627
|
+
|
|
628
|
+
if ("userMessage" in options) {
|
|
629
|
+
const rootBucket =
|
|
630
|
+
snapshotRef.current.fold.threads.get(ROOT_THREAD_ID);
|
|
631
|
+
groupRootBaseline = [...(rootBucket?.modelMessageIds ?? [])];
|
|
632
|
+
setSnapshot((prev) => {
|
|
633
|
+
const next = replaceSessionSnapshot(prev, {
|
|
634
|
+
pendingUser: {
|
|
635
|
+
turnId,
|
|
636
|
+
content: options.userMessage,
|
|
637
|
+
createdAt: new Date(),
|
|
638
|
+
},
|
|
639
|
+
activeStream: undefined,
|
|
640
|
+
groupRootBaseline,
|
|
641
|
+
});
|
|
642
|
+
snapshotRef.current = next;
|
|
643
|
+
return next;
|
|
644
|
+
});
|
|
645
|
+
} else {
|
|
646
|
+
groupRootBaseline =
|
|
647
|
+
snapshotRef.current.groupRootBaseline ??
|
|
648
|
+
computeGroupRootBaseline(snapshotRef.current.turns);
|
|
649
|
+
}
|
|
523
650
|
}
|
|
524
651
|
|
|
525
652
|
await runStream(
|
|
@@ -528,7 +655,7 @@ export function useTrueFoundryAgentMessages({
|
|
|
528
655
|
return streamTurnContent(
|
|
529
656
|
session,
|
|
530
657
|
snapshotRef.current.fold,
|
|
531
|
-
{ inputs: options.inputs },
|
|
658
|
+
{ inputs: options.inputs, ...streamHeaders },
|
|
532
659
|
signal,
|
|
533
660
|
groupRootBaseline,
|
|
534
661
|
);
|
|
@@ -537,7 +664,7 @@ export function useTrueFoundryAgentMessages({
|
|
|
537
664
|
return streamTurnContent(
|
|
538
665
|
session,
|
|
539
666
|
snapshotRef.current.fold,
|
|
540
|
-
{ resumeMcpAuth: true },
|
|
667
|
+
{ resumeMcpAuth: true, ...streamHeaders },
|
|
541
668
|
signal,
|
|
542
669
|
groupRootBaseline,
|
|
543
670
|
);
|
|
@@ -550,6 +677,7 @@ export function useTrueFoundryAgentMessages({
|
|
|
550
677
|
...(options.previousTurnId !== undefined
|
|
551
678
|
? { previousTurnId: options.previousTurnId }
|
|
552
679
|
: {}),
|
|
680
|
+
...streamHeaders,
|
|
553
681
|
},
|
|
554
682
|
signal,
|
|
555
683
|
groupRootBaseline,
|
|
@@ -559,7 +687,7 @@ export function useTrueFoundryAgentMessages({
|
|
|
559
687
|
isContinuation,
|
|
560
688
|
);
|
|
561
689
|
},
|
|
562
|
-
[client,
|
|
690
|
+
[client, runStream, sessionId, sessionOptions],
|
|
563
691
|
);
|
|
564
692
|
|
|
565
693
|
const cancel = useCallback(async () => {
|
|
@@ -569,7 +697,7 @@ export function useTrueFoundryAgentMessages({
|
|
|
569
697
|
}
|
|
570
698
|
const conversationSessionId = await resolveActiveSessionId(
|
|
571
699
|
sessionId,
|
|
572
|
-
|
|
700
|
+
resolveConversationSessionIdRef.current,
|
|
573
701
|
);
|
|
574
702
|
const session = await getSession(client, conversationSessionId, sessionOptions);
|
|
575
703
|
// Request cancellation but keep consuming the stream. After cancel(),
|
|
@@ -581,7 +709,7 @@ export function useTrueFoundryAgentMessages({
|
|
|
581
709
|
// reconcile is needed here — the cancelled turn is terminal and local
|
|
582
710
|
// state reconciles against the event log on the next session load.
|
|
583
711
|
await activeRunRef.current?.catch(() => undefined);
|
|
584
|
-
}, [client,
|
|
712
|
+
}, [client, sessionId, sessionOptions]);
|
|
585
713
|
|
|
586
714
|
const isRunningRef = useRef(isRunning);
|
|
587
715
|
isRunningRef.current = isRunning;
|
|
@@ -598,10 +726,10 @@ export function useTrueFoundryAgentMessages({
|
|
|
598
726
|
}
|
|
599
727
|
const inputs = collectRequiredActionInputs(paused);
|
|
600
728
|
if (inputs.length > 0) {
|
|
601
|
-
void sendTurn({ inputs }).catch((error) =>
|
|
729
|
+
void sendTurn({ inputs }).catch((error) => onErrorRef.current?.(error));
|
|
602
730
|
}
|
|
603
731
|
},
|
|
604
|
-
[
|
|
732
|
+
[projectOptions, sendTurn],
|
|
605
733
|
);
|
|
606
734
|
|
|
607
735
|
const respondToToolApproval = useCallback(
|
|
@@ -671,37 +799,38 @@ export function useTrueFoundryAgentMessages({
|
|
|
671
799
|
const committed = commitActiveStream(snapshotRef.current);
|
|
672
800
|
setSnapshot(committed);
|
|
673
801
|
|
|
674
|
-
const turnIndex = findTurnIndex(committed, turnId);
|
|
675
|
-
|
|
676
802
|
await cancel();
|
|
677
803
|
|
|
678
804
|
const conversationSessionId = await resolveActiveSessionId(
|
|
679
805
|
activeSessionId,
|
|
680
|
-
|
|
806
|
+
resolveConversationSessionIdRef.current,
|
|
681
807
|
);
|
|
682
808
|
const session = await getSession(client, conversationSessionId, sessionOptions);
|
|
683
|
-
const previousTurnId = await
|
|
809
|
+
const previousTurnId = await resolveGatewayBranchPreviousTurnIdForTurn(
|
|
684
810
|
session,
|
|
685
|
-
|
|
811
|
+
turnId,
|
|
686
812
|
);
|
|
687
|
-
const rewound = await
|
|
813
|
+
const rewound = await buildSnapshotBeforeTurn(
|
|
688
814
|
session,
|
|
689
|
-
|
|
815
|
+
turnId,
|
|
690
816
|
listEventsConcurrency,
|
|
691
817
|
);
|
|
692
818
|
createdAtByMessageIdRef.current = new Map();
|
|
819
|
+
// Keep the ref aligned before awaiting sendTurn so any intermediate
|
|
820
|
+
// reads (and the atomic pendingUser apply) see the rewound history.
|
|
821
|
+
snapshotRef.current = rewound;
|
|
693
822
|
setSnapshot(rewound);
|
|
694
823
|
|
|
695
824
|
await sendTurn({
|
|
696
825
|
userMessage,
|
|
697
826
|
previousTurnId,
|
|
827
|
+
branchFromSnapshot: rewound,
|
|
698
828
|
});
|
|
699
829
|
},
|
|
700
830
|
[
|
|
701
831
|
cancel,
|
|
702
832
|
client,
|
|
703
833
|
listEventsConcurrency,
|
|
704
|
-
resolveConversationSessionId,
|
|
705
834
|
sendTurn,
|
|
706
835
|
sessionId,
|
|
707
836
|
sessionOptions,
|
|
@@ -737,11 +866,75 @@ export function useTrueFoundryAgentMessages({
|
|
|
737
866
|
[branchFromTurn],
|
|
738
867
|
);
|
|
739
868
|
|
|
869
|
+
const retryLoad = useCallback(() => {
|
|
870
|
+
setLoadRetryTrigger((n) => n + 1);
|
|
871
|
+
}, []);
|
|
872
|
+
|
|
873
|
+
const hasOlderHistory = snapshot.historyPagination?.hasOlder === true;
|
|
874
|
+
|
|
875
|
+
const loadOlderHistory = useCallback(async () => {
|
|
876
|
+
if (sessionId == null || isMain === false) {
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
if (loadOlderInflightRef.current != null) {
|
|
880
|
+
return loadOlderInflightRef.current;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const current = snapshotRef.current;
|
|
884
|
+
if (current.historyPagination?.hasOlder !== true) {
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
if (current.historyPagination.olderPageToken == null) {
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
const generation = loadGenerationRef.current;
|
|
892
|
+
setIsLoadingOlderHistory(true);
|
|
893
|
+
|
|
894
|
+
const run = (async () => {
|
|
895
|
+
try {
|
|
896
|
+
const conversationSessionId = await resolveActiveSessionId(
|
|
897
|
+
sessionId,
|
|
898
|
+
resolveConversationSessionIdRef.current,
|
|
899
|
+
);
|
|
900
|
+
const session = await getSession(
|
|
901
|
+
client,
|
|
902
|
+
conversationSessionId,
|
|
903
|
+
sessionOptions,
|
|
904
|
+
);
|
|
905
|
+
const next = await prependOlderSessionHistory(
|
|
906
|
+
session,
|
|
907
|
+
snapshotRef.current,
|
|
908
|
+
);
|
|
909
|
+
if (generation !== loadGenerationRef.current) {
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
setSnapshot(next);
|
|
913
|
+
} catch (error) {
|
|
914
|
+
if (generation === loadGenerationRef.current) {
|
|
915
|
+
onErrorRef.current?.(error);
|
|
916
|
+
}
|
|
917
|
+
throw error;
|
|
918
|
+
} finally {
|
|
919
|
+
if (generation === loadGenerationRef.current) {
|
|
920
|
+
setIsLoadingOlderHistory(false);
|
|
921
|
+
}
|
|
922
|
+
loadOlderInflightRef.current = null;
|
|
923
|
+
}
|
|
924
|
+
})();
|
|
925
|
+
|
|
926
|
+
loadOlderInflightRef.current = run;
|
|
927
|
+
return run;
|
|
928
|
+
}, [client, isMain, sessionId, sessionOptions]);
|
|
929
|
+
|
|
740
930
|
return {
|
|
741
931
|
messages,
|
|
742
932
|
isRunning,
|
|
743
933
|
isLoading,
|
|
744
|
-
|
|
934
|
+
isLoadingOlderHistory,
|
|
935
|
+
hasOlderHistory,
|
|
936
|
+
loadOlderHistory,
|
|
937
|
+
retryLoad,
|
|
745
938
|
sendTurn,
|
|
746
939
|
cancel,
|
|
747
940
|
respondToToolApproval,
|