@truefoundry/assistant-ui-runtime 0.1.0-rc.1 → 0.1.1
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 +14 -77
- package/dist/index.d.ts +4 -1
- package/dist/index.js +314 -122
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/agentSessionModule.d.ts +14 -2
- package/src/convertTurnMessages.test.ts +362 -0
- package/src/convertTurnMessages.ts +193 -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 +6 -0
- package/src/index.ts +6 -5
- package/src/loadSessionSnapshot.test.ts +21 -6
- package/src/loadSessionSnapshot.ts +9 -5
- package/src/{truefoundryDraftThreadListAdapter.ts → private/truefoundryDraftThreadListAdapter.ts} +1 -1
- package/src/sessionSnapshot.ts +27 -1
- package/src/sessions.ts +1 -1
- package/src/truefoundryExtras.ts +2 -1
- package/src/types.ts +1 -1
- package/src/useTrueFoundryAgentMessages.test.tsx +225 -1
- package/src/useTrueFoundryAgentMessages.ts +178 -60
- package/src/useTrueFoundryAgentRuntime.ts +27 -13
- /package/src/{agentSpec.ts → private/agentSpec.ts} +0 -0
- /package/src/{bindDraftAgentSession.test.ts → private/bindDraftAgentSession.test.ts} +0 -0
- /package/src/{bindDraftAgentSession.ts → private/bindDraftAgentSession.ts} +0 -0
- /package/src/{draftSessionBridge.ts → private/draftSessionBridge.ts} +0 -0
- /package/src/{truefoundryDraftThreadListAdapter.test.ts → private/truefoundryDraftThreadListAdapter.test.ts} +0 -0
- /package/src/{useDraftAgentSpec.ts → private/useDraftAgentSpec.ts} +0 -0
|
@@ -57,6 +57,8 @@ import type { TurnStreamUpdate } from "./turnStreamUpdate.js";
|
|
|
57
57
|
export type UseTrueFoundryAgentMessagesOptions = {
|
|
58
58
|
client: AgentSessionClient;
|
|
59
59
|
sessionId: string | undefined;
|
|
60
|
+
/** When true the thread is the currently selected (main) thread. */
|
|
61
|
+
isMain?: boolean | undefined;
|
|
60
62
|
listEventsConcurrency?: number | undefined;
|
|
61
63
|
onError?: ((error: unknown) => void) | undefined;
|
|
62
64
|
initializeSession?: () => Promise<{
|
|
@@ -70,7 +72,16 @@ export type UseTrueFoundryAgentMessagesOptions = {
|
|
|
70
72
|
};
|
|
71
73
|
|
|
72
74
|
export type SendTurnOptions =
|
|
73
|
-
| {
|
|
75
|
+
| {
|
|
76
|
+
userMessage: UserMessageContent;
|
|
77
|
+
previousTurnId?: string | null;
|
|
78
|
+
/**
|
|
79
|
+
* When branching (edit/reset), the already-rewound history to send from.
|
|
80
|
+
* Applied atomically with `pendingUser` so a stale React snapshot cannot
|
|
81
|
+
* keep pre-branch turns while the new user message is appended.
|
|
82
|
+
*/
|
|
83
|
+
branchFromSnapshot?: SessionSnapshot;
|
|
84
|
+
}
|
|
74
85
|
| { inputs: RequiredActionInput[] }
|
|
75
86
|
| { resumeMcpAuth: true };
|
|
76
87
|
|
|
@@ -272,6 +283,7 @@ function resolveTurnInput(
|
|
|
272
283
|
export function useTrueFoundryAgentMessages({
|
|
273
284
|
client,
|
|
274
285
|
sessionId,
|
|
286
|
+
isMain,
|
|
275
287
|
listEventsConcurrency,
|
|
276
288
|
onError,
|
|
277
289
|
initializeSession,
|
|
@@ -287,15 +299,24 @@ export function useTrueFoundryAgentMessages({
|
|
|
287
299
|
const [snapshot, setSnapshot] = useState<SessionSnapshot>(createEmptySessionSnapshot);
|
|
288
300
|
const [isRunning, setIsRunning] = useState(false);
|
|
289
301
|
const [isLoading, setIsLoading] = useState(false);
|
|
302
|
+
const [loadRetryTrigger, setLoadRetryTrigger] = useState(0);
|
|
290
303
|
|
|
291
304
|
const snapshotRef = useRef(snapshot);
|
|
292
305
|
snapshotRef.current = snapshot;
|
|
293
306
|
|
|
307
|
+
const onErrorRef = useRef(onError);
|
|
308
|
+
onErrorRef.current = onError;
|
|
309
|
+
const resolveConversationSessionIdRef = useRef(resolveConversationSessionId);
|
|
310
|
+
resolveConversationSessionIdRef.current = resolveConversationSessionId;
|
|
311
|
+
const initializeSessionRef = useRef(initializeSession);
|
|
312
|
+
initializeSessionRef.current = initializeSession;
|
|
313
|
+
|
|
294
314
|
const createdAtByMessageIdRef = useRef(new Map<string, Date>());
|
|
295
315
|
const abortControllerRef = useRef<AbortController | null>(null);
|
|
296
316
|
const activeRunRef = useRef<Promise<void> | null>(null);
|
|
297
317
|
const runningTurnRef = useRef<Turn | undefined>(undefined);
|
|
298
318
|
const loadGenerationRef = useRef(0);
|
|
319
|
+
const streamGenerationRef = useRef(0);
|
|
299
320
|
const lazilyCreatedSessionIdRef = useRef<string | undefined>(undefined);
|
|
300
321
|
|
|
301
322
|
const projectOptions = useMemo(
|
|
@@ -318,47 +339,81 @@ export function useTrueFoundryAgentMessages({
|
|
|
318
339
|
[snapshot, projectOptions],
|
|
319
340
|
);
|
|
320
341
|
|
|
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
342
|
const runStream = useCallback(
|
|
337
343
|
(
|
|
338
344
|
createStream: (signal: AbortSignal) => AsyncGenerator<TurnStreamUpdate>,
|
|
339
345
|
turnId: string,
|
|
340
346
|
isContinuation: boolean,
|
|
341
347
|
): Promise<void> => {
|
|
348
|
+
const streamGeneration = ++streamGenerationRef.current;
|
|
342
349
|
abortControllerRef.current?.abort();
|
|
343
350
|
const abortController = new AbortController();
|
|
344
351
|
abortControllerRef.current = abortController;
|
|
345
352
|
setIsRunning(true);
|
|
346
353
|
|
|
347
354
|
const run = (async () => {
|
|
355
|
+
// Sub-agent turns can emit 100+ stream events per frame. Coalesce to one
|
|
356
|
+
// setSnapshot per animation frame so assistant-ui does not remount the whole
|
|
357
|
+
// message tree (UI hang). The buffer belongs to this stream only.
|
|
358
|
+
let pendingStreamUpdate: {
|
|
359
|
+
update: TurnStreamUpdate;
|
|
360
|
+
turnId: string;
|
|
361
|
+
isContinuation: boolean;
|
|
362
|
+
} | null = null;
|
|
363
|
+
let streamUpdateRaf: number | null = null;
|
|
364
|
+
|
|
365
|
+
const flushPendingStreamUpdate = () => {
|
|
366
|
+
streamUpdateRaf = null;
|
|
367
|
+
const pending = pendingStreamUpdate;
|
|
368
|
+
pendingStreamUpdate = null;
|
|
369
|
+
if (
|
|
370
|
+
pending == null ||
|
|
371
|
+
streamGeneration !== streamGenerationRef.current
|
|
372
|
+
) {
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const { update, turnId: pendingTurnId, isContinuation: pendingIsContinuation } =
|
|
376
|
+
pending;
|
|
377
|
+
setSnapshot((prev) =>
|
|
378
|
+
replaceSessionSnapshot(prev, {
|
|
379
|
+
activeStream: {
|
|
380
|
+
turnId: pendingTurnId,
|
|
381
|
+
update,
|
|
382
|
+
isContinuation: pendingIsContinuation,
|
|
383
|
+
},
|
|
384
|
+
}),
|
|
385
|
+
);
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const applyStreamUpdate = (update: TurnStreamUpdate) => {
|
|
389
|
+
pendingStreamUpdate = { update, turnId, isContinuation };
|
|
390
|
+
if (streamUpdateRaf == null) {
|
|
391
|
+
streamUpdateRaf = requestAnimationFrame(flushPendingStreamUpdate);
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
|
|
348
395
|
try {
|
|
349
396
|
for await (const update of createStream(abortController.signal)) {
|
|
350
397
|
if (abortController.signal.aborted) {
|
|
351
398
|
return;
|
|
352
399
|
}
|
|
353
|
-
applyStreamUpdate(update
|
|
400
|
+
applyStreamUpdate(update);
|
|
354
401
|
}
|
|
355
402
|
} catch (error) {
|
|
356
403
|
if (error instanceof Error && error.name === "AbortError") {
|
|
357
404
|
return;
|
|
358
405
|
}
|
|
359
|
-
|
|
406
|
+
onErrorRef.current?.(error);
|
|
360
407
|
throw error;
|
|
361
408
|
} finally {
|
|
409
|
+
if (streamUpdateRaf != null) {
|
|
410
|
+
cancelAnimationFrame(streamUpdateRaf);
|
|
411
|
+
streamUpdateRaf = null;
|
|
412
|
+
}
|
|
413
|
+
if (streamGeneration !== streamGenerationRef.current) {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
flushPendingStreamUpdate();
|
|
362
417
|
if (abortControllerRef.current === abortController) {
|
|
363
418
|
abortControllerRef.current = null;
|
|
364
419
|
}
|
|
@@ -392,7 +447,7 @@ export function useTrueFoundryAgentMessages({
|
|
|
392
447
|
});
|
|
393
448
|
return run;
|
|
394
449
|
},
|
|
395
|
-
[
|
|
450
|
+
[onError],
|
|
396
451
|
);
|
|
397
452
|
|
|
398
453
|
const load = useCallback(async () => {
|
|
@@ -401,24 +456,45 @@ export function useTrueFoundryAgentMessages({
|
|
|
401
456
|
setSnapshot(createEmptySessionSnapshot());
|
|
402
457
|
return;
|
|
403
458
|
}
|
|
459
|
+
|
|
460
|
+
// Thread components are never unmounted on navigation — isMain going
|
|
461
|
+
// false→true is the only reliable signal that the user has clicked on
|
|
462
|
+
// this thread. Skip the load when this thread is not the active one
|
|
463
|
+
// so that isMain being a dep triggers a fresh load on every selection.
|
|
464
|
+
if (isMain === false) return;
|
|
465
|
+
|
|
466
|
+
// When we are loading a *different* session the user has navigated away
|
|
467
|
+
// from the lazily-created one — clear the guard so navigating back to it
|
|
468
|
+
// later triggers a proper reload instead of silently skipping.
|
|
469
|
+
if (lazilyCreatedSessionIdRef.current != null && sessionId !== lazilyCreatedSessionIdRef.current) {
|
|
470
|
+
lazilyCreatedSessionIdRef.current = undefined;
|
|
471
|
+
}
|
|
472
|
+
|
|
404
473
|
if (sessionId === lazilyCreatedSessionIdRef.current) {
|
|
405
474
|
return;
|
|
406
475
|
}
|
|
407
476
|
|
|
408
477
|
const generation = ++loadGenerationRef.current;
|
|
478
|
+
++streamGenerationRef.current;
|
|
409
479
|
abortControllerRef.current?.abort();
|
|
480
|
+
createdAtByMessageIdRef.current = new Map();
|
|
481
|
+
setSnapshot(createEmptySessionSnapshot());
|
|
410
482
|
setIsLoading(true);
|
|
411
483
|
|
|
412
484
|
try {
|
|
413
485
|
const conversationSessionId = await resolveActiveSessionId(
|
|
414
486
|
sessionId,
|
|
415
|
-
|
|
487
|
+
resolveConversationSessionIdRef.current,
|
|
416
488
|
);
|
|
417
489
|
const loadedSnapshot = await loadSessionSnapshot(
|
|
418
490
|
client,
|
|
419
491
|
conversationSessionId,
|
|
420
|
-
listEventsConcurrency,
|
|
421
492
|
sessionOptions,
|
|
493
|
+
(snap) => {
|
|
494
|
+
if (generation === loadGenerationRef.current) {
|
|
495
|
+
setSnapshot(snap);
|
|
496
|
+
}
|
|
497
|
+
},
|
|
422
498
|
);
|
|
423
499
|
if (generation !== loadGenerationRef.current) {
|
|
424
500
|
return;
|
|
@@ -428,32 +504,44 @@ export function useTrueFoundryAgentMessages({
|
|
|
428
504
|
setSnapshot(loadedSnapshot);
|
|
429
505
|
runningTurnRef.current = loadedSnapshot.runningTurn;
|
|
430
506
|
|
|
507
|
+
// History (and any seeded pendingUser for the in-flight turn) is
|
|
508
|
+
// ready — clear loading before resuming. Awaiting the subscribe
|
|
509
|
+
// stream here previously kept isLoading true for the entire
|
|
510
|
+
// backend run, so reconnect UIs stayed on shimmers even though
|
|
511
|
+
// subscribe was already live.
|
|
512
|
+
setIsLoading(false);
|
|
513
|
+
|
|
431
514
|
if (loadedSnapshot.runningTurn != null) {
|
|
432
515
|
const turn = loadedSnapshot.runningTurn;
|
|
433
516
|
const isContinuation = !extractTurnUserText(turn.input);
|
|
434
517
|
// TODO: pass afterSequenceNumber once stream ingestion tracks sequence numbers.
|
|
435
|
-
|
|
518
|
+
// Use loadedSnapshot directly — snapshotRef.current still points at
|
|
519
|
+
// the empty snapshot cleared above until the setSnapshot(loadedSnapshot)
|
|
520
|
+
// call re-renders.
|
|
521
|
+
void runStream(
|
|
436
522
|
(signal) =>
|
|
437
523
|
resumeTurnStream(
|
|
438
524
|
turn,
|
|
439
|
-
|
|
525
|
+
loadedSnapshot.fold,
|
|
440
526
|
signal,
|
|
441
527
|
undefined,
|
|
442
|
-
|
|
528
|
+
loadedSnapshot.groupRootBaseline,
|
|
443
529
|
),
|
|
444
530
|
turn.id,
|
|
445
531
|
isContinuation,
|
|
446
|
-
);
|
|
532
|
+
).catch(() => undefined);
|
|
447
533
|
}
|
|
448
534
|
} catch (error) {
|
|
449
|
-
|
|
535
|
+
if (generation === loadGenerationRef.current) {
|
|
536
|
+
onErrorRef.current?.(error);
|
|
537
|
+
}
|
|
450
538
|
throw error;
|
|
451
539
|
} finally {
|
|
452
540
|
if (generation === loadGenerationRef.current) {
|
|
453
541
|
setIsLoading(false);
|
|
454
542
|
}
|
|
455
543
|
}
|
|
456
|
-
}, [client,
|
|
544
|
+
}, [client, runStream, sessionId, sessionOptions, loadRetryTrigger, isMain]);
|
|
457
545
|
|
|
458
546
|
useEffect(() => {
|
|
459
547
|
void load().catch(() => undefined);
|
|
@@ -463,17 +551,17 @@ export function useTrueFoundryAgentMessages({
|
|
|
463
551
|
async (options: SendTurnOptions) => {
|
|
464
552
|
let activeSessionId = sessionId;
|
|
465
553
|
if (activeSessionId == null) {
|
|
466
|
-
if (
|
|
554
|
+
if (initializeSessionRef.current == null) {
|
|
467
555
|
throw new Error("Cannot send a turn without an active session.");
|
|
468
556
|
}
|
|
469
|
-
const { remoteId } = await
|
|
557
|
+
const { remoteId } = await initializeSessionRef.current();
|
|
470
558
|
activeSessionId = remoteId;
|
|
471
559
|
lazilyCreatedSessionIdRef.current = remoteId;
|
|
472
560
|
}
|
|
473
561
|
|
|
474
562
|
const conversationSessionId = await resolveActiveSessionId(
|
|
475
563
|
activeSessionId,
|
|
476
|
-
|
|
564
|
+
resolveConversationSessionIdRef.current,
|
|
477
565
|
);
|
|
478
566
|
const session = await getSession(client, conversationSessionId, sessionOptions);
|
|
479
567
|
const isContinuation =
|
|
@@ -492,34 +580,57 @@ export function useTrueFoundryAgentMessages({
|
|
|
492
580
|
);
|
|
493
581
|
}
|
|
494
582
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
prev,
|
|
498
|
-
"inputs" in options ? options.inputs : undefined,
|
|
499
|
-
),
|
|
500
|
-
);
|
|
583
|
+
const branchBase =
|
|
584
|
+
"userMessage" in options ? options.branchFromSnapshot : undefined;
|
|
501
585
|
|
|
502
586
|
let groupRootBaseline: readonly string[] | undefined;
|
|
503
587
|
|
|
504
|
-
if ("userMessage" in options) {
|
|
505
|
-
|
|
506
|
-
|
|
588
|
+
if (branchBase != null && "userMessage" in options) {
|
|
589
|
+
// Atomic apply: never merge pendingUser onto a stale React `prev`
|
|
590
|
+
// that still holds pre-branch turns (edit would show old + new).
|
|
591
|
+
const rootBucket = branchBase.fold.threads.get(ROOT_THREAD_ID);
|
|
507
592
|
groupRootBaseline = [...(rootBucket?.modelMessageIds ?? [])];
|
|
593
|
+
const nextSnapshot = replaceSessionSnapshot(branchBase, {
|
|
594
|
+
pendingUser: {
|
|
595
|
+
turnId,
|
|
596
|
+
content: options.userMessage,
|
|
597
|
+
createdAt: new Date(),
|
|
598
|
+
},
|
|
599
|
+
activeStream: undefined,
|
|
600
|
+
groupRootBaseline,
|
|
601
|
+
});
|
|
602
|
+
snapshotRef.current = nextSnapshot;
|
|
603
|
+
setSnapshot(nextSnapshot);
|
|
604
|
+
} else {
|
|
508
605
|
setSnapshot((prev) =>
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
createdAt: new Date(),
|
|
514
|
-
},
|
|
515
|
-
activeStream: undefined,
|
|
516
|
-
groupRootBaseline,
|
|
517
|
-
}),
|
|
606
|
+
commitActiveStream(
|
|
607
|
+
prev,
|
|
608
|
+
"inputs" in options ? options.inputs : undefined,
|
|
609
|
+
),
|
|
518
610
|
);
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
611
|
+
|
|
612
|
+
if ("userMessage" in options) {
|
|
613
|
+
const rootBucket =
|
|
614
|
+
snapshotRef.current.fold.threads.get(ROOT_THREAD_ID);
|
|
615
|
+
groupRootBaseline = [...(rootBucket?.modelMessageIds ?? [])];
|
|
616
|
+
setSnapshot((prev) => {
|
|
617
|
+
const next = replaceSessionSnapshot(prev, {
|
|
618
|
+
pendingUser: {
|
|
619
|
+
turnId,
|
|
620
|
+
content: options.userMessage,
|
|
621
|
+
createdAt: new Date(),
|
|
622
|
+
},
|
|
623
|
+
activeStream: undefined,
|
|
624
|
+
groupRootBaseline,
|
|
625
|
+
});
|
|
626
|
+
snapshotRef.current = next;
|
|
627
|
+
return next;
|
|
628
|
+
});
|
|
629
|
+
} else {
|
|
630
|
+
groupRootBaseline =
|
|
631
|
+
snapshotRef.current.groupRootBaseline ??
|
|
632
|
+
computeGroupRootBaseline(snapshotRef.current.turns);
|
|
633
|
+
}
|
|
523
634
|
}
|
|
524
635
|
|
|
525
636
|
await runStream(
|
|
@@ -559,7 +670,7 @@ export function useTrueFoundryAgentMessages({
|
|
|
559
670
|
isContinuation,
|
|
560
671
|
);
|
|
561
672
|
},
|
|
562
|
-
[client,
|
|
673
|
+
[client, runStream, sessionId, sessionOptions],
|
|
563
674
|
);
|
|
564
675
|
|
|
565
676
|
const cancel = useCallback(async () => {
|
|
@@ -569,7 +680,7 @@ export function useTrueFoundryAgentMessages({
|
|
|
569
680
|
}
|
|
570
681
|
const conversationSessionId = await resolveActiveSessionId(
|
|
571
682
|
sessionId,
|
|
572
|
-
|
|
683
|
+
resolveConversationSessionIdRef.current,
|
|
573
684
|
);
|
|
574
685
|
const session = await getSession(client, conversationSessionId, sessionOptions);
|
|
575
686
|
// Request cancellation but keep consuming the stream. After cancel(),
|
|
@@ -581,7 +692,7 @@ export function useTrueFoundryAgentMessages({
|
|
|
581
692
|
// reconcile is needed here — the cancelled turn is terminal and local
|
|
582
693
|
// state reconciles against the event log on the next session load.
|
|
583
694
|
await activeRunRef.current?.catch(() => undefined);
|
|
584
|
-
}, [client,
|
|
695
|
+
}, [client, sessionId, sessionOptions]);
|
|
585
696
|
|
|
586
697
|
const isRunningRef = useRef(isRunning);
|
|
587
698
|
isRunningRef.current = isRunning;
|
|
@@ -598,10 +709,10 @@ export function useTrueFoundryAgentMessages({
|
|
|
598
709
|
}
|
|
599
710
|
const inputs = collectRequiredActionInputs(paused);
|
|
600
711
|
if (inputs.length > 0) {
|
|
601
|
-
void sendTurn({ inputs }).catch((error) =>
|
|
712
|
+
void sendTurn({ inputs }).catch((error) => onErrorRef.current?.(error));
|
|
602
713
|
}
|
|
603
714
|
},
|
|
604
|
-
[
|
|
715
|
+
[projectOptions, sendTurn],
|
|
605
716
|
);
|
|
606
717
|
|
|
607
718
|
const respondToToolApproval = useCallback(
|
|
@@ -677,7 +788,7 @@ export function useTrueFoundryAgentMessages({
|
|
|
677
788
|
|
|
678
789
|
const conversationSessionId = await resolveActiveSessionId(
|
|
679
790
|
activeSessionId,
|
|
680
|
-
|
|
791
|
+
resolveConversationSessionIdRef.current,
|
|
681
792
|
);
|
|
682
793
|
const session = await getSession(client, conversationSessionId, sessionOptions);
|
|
683
794
|
const previousTurnId = await resolveGatewayBranchPreviousTurnId(
|
|
@@ -690,18 +801,21 @@ export function useTrueFoundryAgentMessages({
|
|
|
690
801
|
listEventsConcurrency,
|
|
691
802
|
);
|
|
692
803
|
createdAtByMessageIdRef.current = new Map();
|
|
804
|
+
// Keep the ref aligned before awaiting sendTurn so any intermediate
|
|
805
|
+
// reads (and the atomic pendingUser apply) see the rewound history.
|
|
806
|
+
snapshotRef.current = rewound;
|
|
693
807
|
setSnapshot(rewound);
|
|
694
808
|
|
|
695
809
|
await sendTurn({
|
|
696
810
|
userMessage,
|
|
697
811
|
previousTurnId,
|
|
812
|
+
branchFromSnapshot: rewound,
|
|
698
813
|
});
|
|
699
814
|
},
|
|
700
815
|
[
|
|
701
816
|
cancel,
|
|
702
817
|
client,
|
|
703
818
|
listEventsConcurrency,
|
|
704
|
-
resolveConversationSessionId,
|
|
705
819
|
sendTurn,
|
|
706
820
|
sessionId,
|
|
707
821
|
sessionOptions,
|
|
@@ -737,11 +851,15 @@ export function useTrueFoundryAgentMessages({
|
|
|
737
851
|
[branchFromTurn],
|
|
738
852
|
);
|
|
739
853
|
|
|
854
|
+
const retryLoad = useCallback(() => {
|
|
855
|
+
setLoadRetryTrigger((n) => n + 1);
|
|
856
|
+
}, []);
|
|
857
|
+
|
|
740
858
|
return {
|
|
741
859
|
messages,
|
|
742
860
|
isRunning,
|
|
743
861
|
isLoading,
|
|
744
|
-
|
|
862
|
+
retryLoad,
|
|
745
863
|
sendTurn,
|
|
746
864
|
cancel,
|
|
747
865
|
respondToToolApproval,
|
|
@@ -14,7 +14,7 @@ import { useAui, useAuiState } from "@assistant-ui/store";
|
|
|
14
14
|
import type { MutableRefObject } from "react";
|
|
15
15
|
import { useCallback, useMemo, useRef, useState } from "react";
|
|
16
16
|
|
|
17
|
-
import type { AgentSpec } from "./agentSpec.js";
|
|
17
|
+
import type { AgentSpec } from "./private/agentSpec.js";
|
|
18
18
|
import {
|
|
19
19
|
collectPendingApprovals,
|
|
20
20
|
collectPendingToolResponses,
|
|
@@ -26,14 +26,14 @@ import {
|
|
|
26
26
|
extractEditedText,
|
|
27
27
|
parseTurnIdFromMessageId,
|
|
28
28
|
} from "./convertTurnMessages.js";
|
|
29
|
-
import { createDraftSessionBridge } from "./draftSessionBridge.js";
|
|
29
|
+
import { createDraftSessionBridge } from "./private/draftSessionBridge.js";
|
|
30
30
|
import { MCP_AUTH_RESUME_RUN_CUSTOM_KEY } from "./mcpAuth.js";
|
|
31
|
-
import { createTrueFoundryDraftThreadListAdapter } from "./truefoundryDraftThreadListAdapter.js";
|
|
31
|
+
import { createTrueFoundryDraftThreadListAdapter } from "./private/truefoundryDraftThreadListAdapter.js";
|
|
32
32
|
import { trueFoundryExtras } from "./truefoundryExtras.js";
|
|
33
33
|
import { createTrueFoundryThreadListAdapter } from "./truefoundryThreadListAdapter.js";
|
|
34
34
|
import type { UseTrueFoundryAgentRuntimeOptions } from "./types.js";
|
|
35
35
|
import { resolveTrueFoundryAgentRuntimeOptions } from "./types.js";
|
|
36
|
-
import { useDraftAgentSpec } from "./useDraftAgentSpec.js";
|
|
36
|
+
import { useDraftAgentSpec } from "./private/useDraftAgentSpec.js";
|
|
37
37
|
import { useTrueFoundryAgentMessages } from "./useTrueFoundryAgentMessages.js";
|
|
38
38
|
|
|
39
39
|
function useTrueFoundryAgentRuntimeImpl(
|
|
@@ -63,6 +63,9 @@ function useTrueFoundryAgentRuntimeImpl(
|
|
|
63
63
|
: undefined,
|
|
64
64
|
);
|
|
65
65
|
const sessionId = useAuiState((state) => state.threadListItem.remoteId ?? undefined);
|
|
66
|
+
const isMain = useAuiState(
|
|
67
|
+
(state) => state.threads.mainThreadId === state.threadListItem.id,
|
|
68
|
+
);
|
|
66
69
|
|
|
67
70
|
const draftSpec = useDraftAgentSpec({
|
|
68
71
|
draftSessionId,
|
|
@@ -94,9 +97,11 @@ function useTrueFoundryAgentRuntimeImpl(
|
|
|
94
97
|
resumeRun,
|
|
95
98
|
editFromTurn,
|
|
96
99
|
resetFromTurn,
|
|
100
|
+
retryLoad,
|
|
97
101
|
} = useTrueFoundryAgentMessages({
|
|
98
102
|
client,
|
|
99
103
|
sessionId,
|
|
104
|
+
isMain,
|
|
100
105
|
listEventsConcurrency,
|
|
101
106
|
onError,
|
|
102
107
|
initializeSession,
|
|
@@ -175,6 +180,7 @@ function useTrueFoundryAgentRuntimeImpl(
|
|
|
175
180
|
resetFromTurn(turnId).catch((error) => {
|
|
176
181
|
onError?.(error);
|
|
177
182
|
}),
|
|
183
|
+
reload: retryLoad,
|
|
178
184
|
draft: draftExtras,
|
|
179
185
|
}),
|
|
180
186
|
unstable_enableToolInvocations: true,
|
|
@@ -228,34 +234,42 @@ function useTrueFoundryAgentRuntimeImpl(
|
|
|
228
234
|
}
|
|
229
235
|
|
|
230
236
|
export function useTrueFoundryAgentRuntime(options: UseTrueFoundryAgentRuntimeOptions) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
237
|
+
// resolveTrueFoundryAgentRuntimeOptions is a cheap pure function; memoizing on the
|
|
238
|
+
// whole `options` object (which callers typically pass as an inline literal) means
|
|
239
|
+
// `resolved` — and everything derived from it — would be recreated on every render,
|
|
240
|
+
// causing `threadListAdapter` to be a new object each render. We compute `resolved`
|
|
241
|
+
// unconditionally and stabilize individual downstream values with primitives/refs.
|
|
242
|
+
const resolved = resolveTrueFoundryAgentRuntimeOptions(options);
|
|
235
243
|
const { client, agent, gateway } = resolved;
|
|
236
244
|
|
|
237
245
|
const pendingAgentSpecRef = useRef<AgentSpec | undefined>(
|
|
238
246
|
agent.mode === "draft" ? agent.defaultAgentSpec : undefined,
|
|
239
247
|
);
|
|
240
248
|
|
|
249
|
+
// Use stable primitive deps so that threadListAdapter is not recreated every render
|
|
250
|
+
// when options is an inline object literal (new reference on every parent render).
|
|
251
|
+
const agentMode = agent.mode;
|
|
252
|
+
const namedAgentName = agent.mode === "named" ? agent.agentName : undefined;
|
|
241
253
|
const threadListAdapter = useMemo(() => {
|
|
242
|
-
if (
|
|
254
|
+
if (agentMode === "draft") {
|
|
243
255
|
if (gateway == null) {
|
|
244
256
|
throw new Error(
|
|
245
257
|
"Draft agent mode requires a `gateway` TrueFoundryGateway client.",
|
|
246
258
|
);
|
|
247
259
|
}
|
|
260
|
+
const draftAgent = agent as Extract<typeof agent, { mode: "draft" }>;
|
|
248
261
|
return createTrueFoundryDraftThreadListAdapter({
|
|
249
262
|
gateway,
|
|
250
|
-
defaultAgentSpec:
|
|
251
|
-
getAgentSpec: () => pendingAgentSpecRef.current ??
|
|
263
|
+
defaultAgentSpec: draftAgent.defaultAgentSpec,
|
|
264
|
+
getAgentSpec: () => pendingAgentSpecRef.current ?? draftAgent.defaultAgentSpec,
|
|
252
265
|
});
|
|
253
266
|
}
|
|
254
267
|
return createTrueFoundryThreadListAdapter({
|
|
255
268
|
client,
|
|
256
|
-
agentName:
|
|
269
|
+
agentName: namedAgentName!,
|
|
257
270
|
});
|
|
258
|
-
|
|
271
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
272
|
+
}, [agentMode, namedAgentName, client, gateway]);
|
|
259
273
|
|
|
260
274
|
return useRemoteThreadListRuntime({
|
|
261
275
|
allowNesting: true,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|