@truefoundry/assistant-ui-runtime 0.1.6-rc.0 → 0.1.7
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 +193 -579
- package/dist/chunk-3A2EPLQG.js +93 -0
- package/dist/chunk-3A2EPLQG.js.map +1 -0
- package/dist/chunk-SQDOTGP2.js +292 -0
- package/dist/chunk-SQDOTGP2.js.map +1 -0
- package/dist/index.d.ts +24 -36
- package/dist/index.js +276 -249
- package/dist/index.js.map +1 -1
- package/dist/plugins/truefoundry-agent-server-adapter/index.d.ts +134 -5
- package/dist/plugins/truefoundry-agent-server-adapter/index.js +16 -195
- package/dist/plugins/truefoundry-agent-server-adapter/index.js.map +1 -1
- package/dist/server/index.d.ts +17 -0
- package/dist/server/index.js +9 -0
- package/dist/server/index.js.map +1 -0
- package/dist/{types-VUBzoJT2.d.ts → types-DbNsU075.d.ts} +212 -19
- package/package.json +10 -5
- package/src/{private → draft}/agentSpec.ts +14 -17
- package/src/{private → draft}/draftSessionBridge.ts +1 -2
- package/src/{private → draft}/truefoundryDraftThreadListAdapter.test.ts +1 -1
- package/src/{private → draft}/truefoundryDraftThreadListAdapter.ts +2 -1
- package/src/{private → draft}/useDraftAgentSpec.ts +16 -5
- package/src/draftAgentConfig.test.ts +2 -1
- package/src/index.ts +71 -7
- package/src/plugins/truefoundry-agent-server-adapter/README.md +178 -0
- package/src/plugins/truefoundry-agent-server-adapter/guards.test.ts +113 -0
- package/src/plugins/truefoundry-agent-server-adapter/guards.ts +130 -0
- package/src/plugins/truefoundry-agent-server-adapter/index.ts +154 -40
- package/src/plugins/truefoundry-agent-server-adapter/types.ts +137 -0
- package/src/plugins/truefoundry-agent-server-adapter/types.typecheck.ts +164 -0
- package/src/server/index.ts +23 -0
- package/src/server/types.ts +272 -21
- package/src/truefoundryExtras.ts +4 -1
- package/src/truefoundryOwnedSessionsThreadListAdapter.ts +1 -1
- package/src/types.ts +1 -2
- package/src/useTrueFoundryAgentMessages.test.tsx +261 -1
- package/src/useTrueFoundryAgentMessages.ts +284 -176
- package/src/useTrueFoundryAgentRuntime.ts +31 -21
- /package/src/{private → draft}/useDraftAgentSpec.test.tsx +0 -0
|
@@ -58,6 +58,8 @@ export type UseTrueFoundryAgentMessagesOptions = {
|
|
|
58
58
|
sessionId: string | undefined;
|
|
59
59
|
/** When true the thread is the currently selected (main) thread. */
|
|
60
60
|
isMain?: boolean | undefined;
|
|
61
|
+
/** URL-selected session may load before the thread list marks it as main. */
|
|
62
|
+
isInitialSession?: boolean | undefined;
|
|
61
63
|
listEventsConcurrency?: number | undefined;
|
|
62
64
|
onError?: ((error: unknown) => void) | undefined;
|
|
63
65
|
initializeSession?: () => Promise<{
|
|
@@ -77,12 +79,16 @@ export type SendTurnOptions =
|
|
|
77
79
|
| {
|
|
78
80
|
userMessage: UserMessageContent;
|
|
79
81
|
previousTurnId?: string | null;
|
|
82
|
+
/** Invoked only when the user turn fails before the gateway registers it. */
|
|
83
|
+
onPreTurnFailure?: () => void;
|
|
80
84
|
/**
|
|
81
85
|
* When branching (edit/reset), the already-rewound history to send from.
|
|
82
86
|
* Applied atomically with `pendingUser` so a stale React snapshot cannot
|
|
83
87
|
* keep pre-branch turns while the new user message is appended.
|
|
84
88
|
*/
|
|
85
89
|
branchFromSnapshot?: SessionSnapshot;
|
|
90
|
+
/** Original history restored when a branch fails before turn.created. */
|
|
91
|
+
branchRollbackSnapshot?: SessionSnapshot;
|
|
86
92
|
}
|
|
87
93
|
| { inputs: RequiredActionInput[] }
|
|
88
94
|
| { resumeMcpAuth: true };
|
|
@@ -286,6 +292,7 @@ export function useTrueFoundryAgentMessages({
|
|
|
286
292
|
server,
|
|
287
293
|
sessionId,
|
|
288
294
|
isMain,
|
|
295
|
+
isInitialSession,
|
|
289
296
|
listEventsConcurrency,
|
|
290
297
|
onError,
|
|
291
298
|
initializeSession,
|
|
@@ -294,7 +301,12 @@ export function useTrueFoundryAgentMessages({
|
|
|
294
301
|
}: UseTrueFoundryAgentMessagesOptions) {
|
|
295
302
|
const [snapshot, setSnapshot] = useState<SessionSnapshot>(createEmptySessionSnapshot);
|
|
296
303
|
const [isRunning, setIsRunning] = useState(false);
|
|
297
|
-
|
|
304
|
+
// Existing sessions have history pending from the first render. Starting at
|
|
305
|
+
// false causes consumers to briefly render an empty thread before the load
|
|
306
|
+
// effect runs and flips this flag to true.
|
|
307
|
+
const [isLoading, setIsLoading] = useState(
|
|
308
|
+
sessionId != null && (isMain !== false || isInitialSession === true),
|
|
309
|
+
);
|
|
298
310
|
const [isLoadingOlderHistory, setIsLoadingOlderHistory] = useState(false);
|
|
299
311
|
const [loadRetryTrigger, setLoadRetryTrigger] = useState(0);
|
|
300
312
|
|
|
@@ -318,6 +330,8 @@ export function useTrueFoundryAgentMessages({
|
|
|
318
330
|
const loadGenerationRef = useRef(0);
|
|
319
331
|
const streamGenerationRef = useRef(0);
|
|
320
332
|
const lazilyCreatedSessionIdRef = useRef<string | undefined>(undefined);
|
|
333
|
+
const initialLoadStartedForRef = useRef<string | undefined>(undefined);
|
|
334
|
+
const skipInitialPromotionLoadForRef = useRef<string | undefined>(undefined);
|
|
321
335
|
|
|
322
336
|
const projectOptions = useMemo(
|
|
323
337
|
() => ({
|
|
@@ -459,17 +473,34 @@ export function useTrueFoundryAgentMessages({
|
|
|
459
473
|
);
|
|
460
474
|
|
|
461
475
|
const load = useCallback(async () => {
|
|
476
|
+
// Reading the retry counter intentionally makes retryLoad recreate this callback.
|
|
477
|
+
void loadRetryTrigger;
|
|
462
478
|
if (sessionId == null) {
|
|
463
479
|
createdAtByMessageIdRef.current = new Map();
|
|
464
480
|
setSnapshot(createEmptySessionSnapshot());
|
|
465
481
|
return;
|
|
466
482
|
}
|
|
467
483
|
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
484
|
+
// Allow the URL-selected session one early load before assistant-ui marks
|
|
485
|
+
// it main. Suppress only that first promotion; later selections still reload.
|
|
486
|
+
const isEarlyInitialLoad =
|
|
487
|
+
isMain === false &&
|
|
488
|
+
isInitialSession === true &&
|
|
489
|
+
initialLoadStartedForRef.current !== sessionId;
|
|
490
|
+
if (isMain === false) {
|
|
491
|
+
if (!isEarlyInitialLoad) return;
|
|
492
|
+
initialLoadStartedForRef.current = sessionId;
|
|
493
|
+
skipInitialPromotionLoadForRef.current = sessionId;
|
|
494
|
+
} else if (
|
|
495
|
+
isMain === true &&
|
|
496
|
+
skipInitialPromotionLoadForRef.current === sessionId
|
|
497
|
+
) {
|
|
498
|
+
skipInitialPromotionLoadForRef.current = undefined;
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (isInitialSession === true) {
|
|
502
|
+
initialLoadStartedForRef.current = sessionId;
|
|
503
|
+
}
|
|
473
504
|
|
|
474
505
|
// When we are loading a *different* session the user has navigated away
|
|
475
506
|
// from the lazily-created one — clear the guard so navigating back to it
|
|
@@ -484,6 +515,7 @@ export function useTrueFoundryAgentMessages({
|
|
|
484
515
|
|
|
485
516
|
const generation = ++loadGenerationRef.current;
|
|
486
517
|
++streamGenerationRef.current;
|
|
518
|
+
setIsRunning(false);
|
|
487
519
|
abortControllerRef.current?.abort();
|
|
488
520
|
loadOlderInflightRef.current = null;
|
|
489
521
|
createdAtByMessageIdRef.current = new Map();
|
|
@@ -544,6 +576,11 @@ export function useTrueFoundryAgentMessages({
|
|
|
544
576
|
}
|
|
545
577
|
} catch (error) {
|
|
546
578
|
if (generation === loadGenerationRef.current) {
|
|
579
|
+
if (isEarlyInitialLoad) {
|
|
580
|
+
// Allow retryLoad while still backgrounded (before isMain promotion).
|
|
581
|
+
initialLoadStartedForRef.current = undefined;
|
|
582
|
+
skipInitialPromotionLoadForRef.current = undefined;
|
|
583
|
+
}
|
|
547
584
|
onErrorRef.current?.(error);
|
|
548
585
|
}
|
|
549
586
|
throw error;
|
|
@@ -552,7 +589,14 @@ export function useTrueFoundryAgentMessages({
|
|
|
552
589
|
setIsLoading(false);
|
|
553
590
|
}
|
|
554
591
|
}
|
|
555
|
-
}, [
|
|
592
|
+
}, [
|
|
593
|
+
server,
|
|
594
|
+
runStream,
|
|
595
|
+
sessionId,
|
|
596
|
+
loadRetryTrigger,
|
|
597
|
+
isMain,
|
|
598
|
+
isInitialSession,
|
|
599
|
+
]);
|
|
556
600
|
|
|
557
601
|
useEffect(() => {
|
|
558
602
|
void load().catch(() => undefined);
|
|
@@ -560,163 +604,211 @@ export function useTrueFoundryAgentMessages({
|
|
|
560
604
|
|
|
561
605
|
const sendTurn = useCallback(
|
|
562
606
|
async (options: SendTurnOptions) => {
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
607
|
+
// A turn.created event means the gateway registered the user message.
|
|
608
|
+
// Errors after that point must keep the message in chat.
|
|
609
|
+
let gatewayTurnAccepted = false;
|
|
610
|
+
let pendingUserWasSet = false;
|
|
611
|
+
let runStreamStarted = false;
|
|
612
|
+
let pendingUserTurnId: string | undefined;
|
|
613
|
+
|
|
614
|
+
try {
|
|
615
|
+
let activeSessionId = sessionId;
|
|
616
|
+
if (activeSessionId == null) {
|
|
617
|
+
if (initializeSessionRef.current == null) {
|
|
618
|
+
throw new Error("Cannot send a turn without an active session.");
|
|
619
|
+
}
|
|
620
|
+
const { remoteId } = await initializeSessionRef.current();
|
|
621
|
+
activeSessionId = remoteId;
|
|
622
|
+
lazilyCreatedSessionIdRef.current = remoteId;
|
|
567
623
|
}
|
|
568
|
-
const { remoteId } = await initializeSessionRef.current();
|
|
569
|
-
activeSessionId = remoteId;
|
|
570
|
-
lazilyCreatedSessionIdRef.current = remoteId;
|
|
571
|
-
}
|
|
572
624
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
);
|
|
577
|
-
const turnHeaders = await getTurnHeadersRef.current?.();
|
|
578
|
-
const streamHeaders =
|
|
579
|
-
turnHeaders != null ? { headers: turnHeaders } : {};
|
|
580
|
-
const isContinuation =
|
|
581
|
-
"inputs" in options ||
|
|
582
|
-
("resumeMcpAuth" in options && options.resumeMcpAuth === true);
|
|
583
|
-
const continuationTurnId = snapshotRef.current.activeStream?.turnId;
|
|
584
|
-
const turnId =
|
|
585
|
-
isContinuation && continuationTurnId != null
|
|
586
|
-
? continuationTurnId
|
|
587
|
-
: generateId();
|
|
588
|
-
// First turns must send previousTurnId: "none".
|
|
589
|
-
const isFirstTurnInSession =
|
|
590
|
-
"userMessage" in options &&
|
|
591
|
-
options.previousTurnId === undefined &&
|
|
592
|
-
snapshotRef.current.turns.length === 0 &&
|
|
593
|
-
snapshotRef.current.pendingUser == null &&
|
|
594
|
-
snapshotRef.current.activeStream == null;
|
|
595
|
-
|
|
596
|
-
// Mutable ref so runStream always reads the latest ID. For new
|
|
597
|
-
// user-message turns the local `generateId()` value is replaced
|
|
598
|
-
// with the gateway-assigned ID once the first SSE event arrives.
|
|
599
|
-
const turnIdRef = { current: turnId };
|
|
600
|
-
|
|
601
|
-
if ("inputs" in options) {
|
|
602
|
-
applyUserToolResponsesToFold(
|
|
603
|
-
snapshotRef.current.fold,
|
|
604
|
-
options.inputs,
|
|
625
|
+
const conversationSessionId = await resolveActiveSessionId(
|
|
626
|
+
activeSessionId,
|
|
627
|
+
resolveConversationSessionIdRef.current,
|
|
605
628
|
);
|
|
606
|
-
|
|
629
|
+
const turnHeaders = await getTurnHeadersRef.current?.();
|
|
630
|
+
const streamHeaders =
|
|
631
|
+
turnHeaders != null ? { headers: turnHeaders } : {};
|
|
632
|
+
const isContinuation =
|
|
633
|
+
"inputs" in options ||
|
|
634
|
+
("resumeMcpAuth" in options && options.resumeMcpAuth === true);
|
|
635
|
+
const continuationTurnId = snapshotRef.current.activeStream?.turnId;
|
|
636
|
+
const turnId =
|
|
637
|
+
isContinuation && continuationTurnId != null
|
|
638
|
+
? continuationTurnId
|
|
639
|
+
: generateId();
|
|
640
|
+
// First turns must send previousTurnId: "none".
|
|
641
|
+
const isFirstTurnInSession =
|
|
642
|
+
"userMessage" in options &&
|
|
643
|
+
options.previousTurnId === undefined &&
|
|
644
|
+
snapshotRef.current.turns.length === 0 &&
|
|
645
|
+
snapshotRef.current.pendingUser == null &&
|
|
646
|
+
snapshotRef.current.activeStream == null;
|
|
647
|
+
|
|
648
|
+
// Mutable ref so runStream always reads the latest ID. For new
|
|
649
|
+
// user-message turns the local `generateId()` value is replaced
|
|
650
|
+
// with the gateway-assigned ID once the first SSE event arrives.
|
|
651
|
+
const turnIdRef = { current: turnId };
|
|
652
|
+
|
|
653
|
+
if ("inputs" in options) {
|
|
654
|
+
applyUserToolResponsesToFold(
|
|
655
|
+
snapshotRef.current.fold,
|
|
656
|
+
options.inputs,
|
|
657
|
+
);
|
|
658
|
+
}
|
|
607
659
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
if (branchBase != null && "userMessage" in options) {
|
|
614
|
-
// Atomic apply: never merge pendingUser onto a stale React `prev`
|
|
615
|
-
// that still holds pre-branch turns (edit would show old + new).
|
|
616
|
-
const rootBucket = branchBase.fold.threads.get(ROOT_THREAD_ID);
|
|
617
|
-
groupRootBaseline = [...(rootBucket?.modelMessageIds ?? [])];
|
|
618
|
-
const nextSnapshot = replaceSessionSnapshot(branchBase, {
|
|
619
|
-
pendingUser: {
|
|
620
|
-
turnId,
|
|
621
|
-
content: options.userMessage,
|
|
622
|
-
createdAt: new Date(),
|
|
623
|
-
},
|
|
624
|
-
activeStream: undefined,
|
|
625
|
-
groupRootBaseline,
|
|
626
|
-
});
|
|
627
|
-
snapshotRef.current = nextSnapshot;
|
|
628
|
-
setSnapshot(nextSnapshot);
|
|
629
|
-
} else {
|
|
630
|
-
setSnapshot((prev) =>
|
|
631
|
-
commitActiveStream(
|
|
632
|
-
prev,
|
|
633
|
-
"inputs" in options ? options.inputs : undefined,
|
|
634
|
-
),
|
|
635
|
-
);
|
|
660
|
+
const branchBase =
|
|
661
|
+
"userMessage" in options ? options.branchFromSnapshot : undefined;
|
|
662
|
+
|
|
663
|
+
let groupRootBaseline: readonly string[] | undefined;
|
|
636
664
|
|
|
637
|
-
if ("userMessage" in options) {
|
|
638
|
-
|
|
639
|
-
|
|
665
|
+
if (branchBase != null && "userMessage" in options) {
|
|
666
|
+
// Atomic apply: never merge pendingUser onto a stale React `prev`
|
|
667
|
+
// that still holds pre-branch turns (edit would show old + new).
|
|
668
|
+
const rootBucket = branchBase.fold.threads.get(ROOT_THREAD_ID);
|
|
640
669
|
groupRootBaseline = [...(rootBucket?.modelMessageIds ?? [])];
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
groupRootBaseline,
|
|
650
|
-
});
|
|
651
|
-
snapshotRef.current = next;
|
|
652
|
-
return next;
|
|
670
|
+
const nextSnapshot = replaceSessionSnapshot(branchBase, {
|
|
671
|
+
pendingUser: {
|
|
672
|
+
turnId,
|
|
673
|
+
content: options.userMessage,
|
|
674
|
+
createdAt: new Date(),
|
|
675
|
+
},
|
|
676
|
+
activeStream: undefined,
|
|
677
|
+
groupRootBaseline,
|
|
653
678
|
});
|
|
679
|
+
snapshotRef.current = nextSnapshot;
|
|
680
|
+
setSnapshot(nextSnapshot);
|
|
681
|
+
pendingUserWasSet = true;
|
|
682
|
+
pendingUserTurnId = turnId;
|
|
654
683
|
} else {
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
684
|
+
setSnapshot((prev) =>
|
|
685
|
+
commitActiveStream(
|
|
686
|
+
prev,
|
|
687
|
+
"inputs" in options ? options.inputs : undefined,
|
|
688
|
+
),
|
|
689
|
+
);
|
|
660
690
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
691
|
+
if ("userMessage" in options) {
|
|
692
|
+
const rootBucket =
|
|
693
|
+
snapshotRef.current.fold.threads.get(ROOT_THREAD_ID);
|
|
694
|
+
groupRootBaseline = [...(rootBucket?.modelMessageIds ?? [])];
|
|
695
|
+
setSnapshot((prev) => {
|
|
696
|
+
const next = replaceSessionSnapshot(prev, {
|
|
697
|
+
pendingUser: {
|
|
698
|
+
turnId,
|
|
699
|
+
content: options.userMessage,
|
|
700
|
+
createdAt: new Date(),
|
|
701
|
+
},
|
|
702
|
+
activeStream: undefined,
|
|
703
|
+
groupRootBaseline,
|
|
704
|
+
});
|
|
705
|
+
snapshotRef.current = next;
|
|
706
|
+
return next;
|
|
707
|
+
});
|
|
708
|
+
pendingUserWasSet = true;
|
|
709
|
+
pendingUserTurnId = turnId;
|
|
710
|
+
} else {
|
|
711
|
+
groupRootBaseline =
|
|
712
|
+
snapshotRef.current.groupRootBaseline ??
|
|
713
|
+
computeGroupRootBaseline(snapshotRef.current.turns);
|
|
672
714
|
}
|
|
673
|
-
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
runStreamStarted = true;
|
|
718
|
+
await runStream(
|
|
719
|
+
(signal) => {
|
|
720
|
+
if ("inputs" in options) {
|
|
721
|
+
return streamTurnContent(
|
|
722
|
+
server,
|
|
723
|
+
conversationSessionId,
|
|
724
|
+
snapshotRef.current.fold,
|
|
725
|
+
{ inputs: options.inputs, ...streamHeaders },
|
|
726
|
+
signal,
|
|
727
|
+
groupRootBaseline,
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
if ("resumeMcpAuth" in options) {
|
|
731
|
+
return streamTurnContent(
|
|
732
|
+
server,
|
|
733
|
+
conversationSessionId,
|
|
734
|
+
snapshotRef.current.fold,
|
|
735
|
+
{ resumeMcpAuth: true, ...streamHeaders },
|
|
736
|
+
signal,
|
|
737
|
+
groupRootBaseline,
|
|
738
|
+
);
|
|
739
|
+
}
|
|
674
740
|
return streamTurnContent(
|
|
675
741
|
server,
|
|
676
742
|
conversationSessionId,
|
|
677
743
|
snapshotRef.current.fold,
|
|
678
|
-
{
|
|
744
|
+
{
|
|
745
|
+
userMessage: options.userMessage,
|
|
746
|
+
...(options.previousTurnId !== undefined
|
|
747
|
+
? { previousTurnId: options.previousTurnId ?? "none" }
|
|
748
|
+
: isFirstTurnInSession
|
|
749
|
+
? { previousTurnId: "none" }
|
|
750
|
+
: {}),
|
|
751
|
+
...streamHeaders,
|
|
752
|
+
},
|
|
679
753
|
signal,
|
|
680
754
|
groupRootBaseline,
|
|
755
|
+
// Rename the optimistic local ID to the gateway turn ID
|
|
756
|
+
// so that edit/retry can resolve the turn via the gateway.
|
|
757
|
+
(gatewayTurnId) => {
|
|
758
|
+
const oldId = turnIdRef.current;
|
|
759
|
+
// turn.created proves the gateway registered the message.
|
|
760
|
+
gatewayTurnAccepted = true;
|
|
761
|
+
if (gatewayTurnId === oldId) return;
|
|
762
|
+
turnIdRef.current = gatewayTurnId;
|
|
763
|
+
// Rename in the ref immediately so any synchronous read
|
|
764
|
+
// (e.g. commitActiveStream) sees the correct ID.
|
|
765
|
+
const renamePendingUser = (prev: SessionSnapshot): SessionSnapshot => {
|
|
766
|
+
if (prev.pendingUser?.turnId !== oldId) return prev;
|
|
767
|
+
return replaceSessionSnapshot(prev, {
|
|
768
|
+
pendingUser: { ...prev.pendingUser, turnId: gatewayTurnId },
|
|
769
|
+
});
|
|
770
|
+
};
|
|
771
|
+
snapshotRef.current = renamePendingUser(snapshotRef.current);
|
|
772
|
+
setSnapshot(renamePendingUser);
|
|
773
|
+
},
|
|
681
774
|
);
|
|
775
|
+
},
|
|
776
|
+
turnIdRef,
|
|
777
|
+
isContinuation,
|
|
778
|
+
);
|
|
779
|
+
} catch (error) {
|
|
780
|
+
if ("userMessage" in options && !gatewayTurnAccepted) {
|
|
781
|
+
const branchRollbackSnapshot =
|
|
782
|
+
options.branchRollbackSnapshot;
|
|
783
|
+
const canRestoreBranch =
|
|
784
|
+
branchRollbackSnapshot != null &&
|
|
785
|
+
(snapshotRef.current === options.branchFromSnapshot ||
|
|
786
|
+
snapshotRef.current.pendingUser?.turnId ===
|
|
787
|
+
pendingUserTurnId);
|
|
788
|
+
if (canRestoreBranch) {
|
|
789
|
+
snapshotRef.current = branchRollbackSnapshot;
|
|
790
|
+
setSnapshot(branchRollbackSnapshot);
|
|
791
|
+
} else if (pendingUserWasSet) {
|
|
792
|
+
const clearPendingUser = (
|
|
793
|
+
previous: SessionSnapshot,
|
|
794
|
+
): SessionSnapshot => {
|
|
795
|
+
if (previous.pendingUser?.turnId !== pendingUserTurnId) {
|
|
796
|
+
return previous;
|
|
797
|
+
}
|
|
798
|
+
return replaceSessionSnapshot(previous, {
|
|
799
|
+
pendingUser: undefined,
|
|
800
|
+
});
|
|
801
|
+
};
|
|
802
|
+
snapshotRef.current = clearPendingUser(snapshotRef.current);
|
|
803
|
+
setSnapshot(clearPendingUser);
|
|
682
804
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
? { previousTurnId: options.previousTurnId ?? "none" }
|
|
691
|
-
: isFirstTurnInSession
|
|
692
|
-
? { previousTurnId: "none" }
|
|
693
|
-
: {}),
|
|
694
|
-
...streamHeaders,
|
|
695
|
-
},
|
|
696
|
-
signal,
|
|
697
|
-
groupRootBaseline,
|
|
698
|
-
// Rename the optimistic local ID to the gateway turn ID
|
|
699
|
-
// so that edit/retry can resolve the turn via the gateway.
|
|
700
|
-
(gatewayTurnId) => {
|
|
701
|
-
const oldId = turnIdRef.current;
|
|
702
|
-
if (gatewayTurnId === oldId) return;
|
|
703
|
-
turnIdRef.current = gatewayTurnId;
|
|
704
|
-
// Rename in the ref immediately so any synchronous read
|
|
705
|
-
// (e.g. commitActiveStream) sees the correct ID.
|
|
706
|
-
const renamePendingUser = (prev: SessionSnapshot): SessionSnapshot => {
|
|
707
|
-
if (prev.pendingUser?.turnId !== oldId) return prev;
|
|
708
|
-
return replaceSessionSnapshot(prev, {
|
|
709
|
-
pendingUser: { ...prev.pendingUser, turnId: gatewayTurnId },
|
|
710
|
-
});
|
|
711
|
-
};
|
|
712
|
-
snapshotRef.current = renamePendingUser(snapshotRef.current);
|
|
713
|
-
setSnapshot(renamePendingUser);
|
|
714
|
-
},
|
|
715
|
-
);
|
|
716
|
-
},
|
|
717
|
-
turnIdRef,
|
|
718
|
-
isContinuation,
|
|
719
|
-
);
|
|
805
|
+
options.onPreTurnFailure?.();
|
|
806
|
+
}
|
|
807
|
+
if (!runStreamStarted) {
|
|
808
|
+
onErrorRef.current?.(error);
|
|
809
|
+
}
|
|
810
|
+
throw error;
|
|
811
|
+
}
|
|
720
812
|
},
|
|
721
813
|
[server, runStream, sessionId],
|
|
722
814
|
);
|
|
@@ -756,7 +848,8 @@ export function useTrueFoundryAgentMessages({
|
|
|
756
848
|
}
|
|
757
849
|
const inputs = collectRequiredActionInputs(paused);
|
|
758
850
|
if (inputs.length > 0) {
|
|
759
|
-
|
|
851
|
+
// sendTurn/runStream already report via onError; swallow to avoid duplicates.
|
|
852
|
+
void sendTurn({ inputs }).catch(() => undefined);
|
|
760
853
|
}
|
|
761
854
|
},
|
|
762
855
|
[projectOptions, sendTurn],
|
|
@@ -823,41 +916,52 @@ export function useTrueFoundryAgentMessages({
|
|
|
823
916
|
|
|
824
917
|
const branchFromTurn = useCallback(
|
|
825
918
|
async (turnId: string, userMessage: UserMessageContent) => {
|
|
826
|
-
let
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
919
|
+
let committed: SessionSnapshot;
|
|
920
|
+
let previousTurnId: string;
|
|
921
|
+
let rewound: SessionSnapshot;
|
|
922
|
+
try {
|
|
923
|
+
let activeSessionId = sessionId;
|
|
924
|
+
if (activeSessionId == null) {
|
|
925
|
+
throw new Error("Cannot branch from a turn without an active session.");
|
|
926
|
+
}
|
|
830
927
|
|
|
831
|
-
|
|
832
|
-
|
|
928
|
+
committed = commitActiveStream(snapshotRef.current);
|
|
929
|
+
setSnapshot(committed);
|
|
833
930
|
|
|
834
|
-
|
|
931
|
+
await cancel();
|
|
835
932
|
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
933
|
+
const conversationSessionId = await resolveActiveSessionId(
|
|
934
|
+
activeSessionId,
|
|
935
|
+
resolveConversationSessionIdRef.current,
|
|
936
|
+
);
|
|
937
|
+
previousTurnId = await resolveGatewayBranchPreviousTurnIdForTurn(
|
|
938
|
+
server,
|
|
939
|
+
conversationSessionId,
|
|
940
|
+
turnId,
|
|
941
|
+
);
|
|
942
|
+
rewound = await buildSnapshotBeforeTurn(
|
|
943
|
+
server,
|
|
944
|
+
conversationSessionId,
|
|
945
|
+
turnId,
|
|
946
|
+
listEventsConcurrency,
|
|
947
|
+
);
|
|
948
|
+
createdAtByMessageIdRef.current = new Map();
|
|
949
|
+
// Keep the ref aligned before awaiting sendTurn so any intermediate
|
|
950
|
+
// reads (and the atomic pendingUser apply) see the rewound history.
|
|
951
|
+
snapshotRef.current = rewound;
|
|
952
|
+
setSnapshot(rewound);
|
|
953
|
+
} catch (error) {
|
|
954
|
+
// Setup failures never reach sendTurn/runStream reporting.
|
|
955
|
+
onErrorRef.current?.(error);
|
|
956
|
+
throw error;
|
|
957
|
+
}
|
|
856
958
|
|
|
959
|
+
// sendTurn/runStream own error reporting for the turn itself.
|
|
857
960
|
await sendTurn({
|
|
858
961
|
userMessage,
|
|
859
962
|
previousTurnId,
|
|
860
963
|
branchFromSnapshot: rewound,
|
|
964
|
+
branchRollbackSnapshot: committed,
|
|
861
965
|
});
|
|
862
966
|
},
|
|
863
967
|
[
|
|
@@ -874,7 +978,9 @@ export function useTrueFoundryAgentMessages({
|
|
|
874
978
|
const committed = commitActiveStream(snapshotRef.current);
|
|
875
979
|
const originalInput = resolveTurnInput(committed, turnId);
|
|
876
980
|
if (originalInput == null) {
|
|
877
|
-
|
|
981
|
+
const error = new Error(`Turn ${turnId} not found in session snapshot`);
|
|
982
|
+
onErrorRef.current?.(error);
|
|
983
|
+
throw error;
|
|
878
984
|
}
|
|
879
985
|
const userMessage = extractTurnUserMessageContent(originalInput);
|
|
880
986
|
await branchFromTurn(turnId, userMessage);
|
|
@@ -887,7 +993,9 @@ export function useTrueFoundryAgentMessages({
|
|
|
887
993
|
const committed = commitActiveStream(snapshotRef.current);
|
|
888
994
|
const originalInput = resolveTurnInput(committed, turnId);
|
|
889
995
|
if (originalInput == null) {
|
|
890
|
-
|
|
996
|
+
const error = new Error(`Turn ${turnId} not found in session snapshot`);
|
|
997
|
+
onErrorRef.current?.(error);
|
|
998
|
+
throw error;
|
|
891
999
|
}
|
|
892
1000
|
const userMessage = buildEditedUserMessageContent(
|
|
893
1001
|
editedText,
|