@truefoundry/assistant-ui-runtime 0.1.1 → 0.1.3-rc.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.
@@ -11,16 +11,17 @@ import type {
11
11
  TurnInputItem,
12
12
  TurnStateDone,
13
13
  } from "truefoundry-gateway-sdk/agents";
14
- import type { TrueFoundryGateway } from "truefoundry-gateway-sdk";
14
+ import type { PrivateAgentSessionClient } from "truefoundry-gateway-sdk/agents/private";
15
15
 
16
16
  import { ROOT_THREAD_ID } from "./constants.js";
17
17
  import {
18
18
  buildEditedUserMessageContent,
19
- buildSnapshotBeforeTurnIndex,
19
+ buildSnapshotBeforeTurn,
20
20
  computeGroupRootBaseline,
21
21
  extractTurnUserMessageContent,
22
+ prependOlderSessionHistory,
22
23
  projectSessionMessages,
23
- resolveGatewayBranchPreviousTurnId,
24
+ resolveGatewayBranchPreviousTurnIdForTurn,
24
25
  rootModelMessageIdsSinceBaseline,
25
26
  userMessageContentToText,
26
27
  type UserMessageContent,
@@ -67,8 +68,13 @@ export type UseTrueFoundryAgentMessagesOptions = {
67
68
  }>;
68
69
  /** Maps a thread `remoteId` to the gateway session id used for turns. */
69
70
  resolveConversationSessionId?: (remoteId: string) => Promise<string>;
70
- /** When set, turns bind to `/agents/sessions/{draftSessionId}/turns` after draft validation. */
71
- draftGateway?: TrueFoundryGateway;
71
+ /** When set, turns bind via PrivateAgentSessionClient.getDraftSession. */
72
+ privateClient?: PrivateAgentSessionClient;
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>;
72
78
  };
73
79
 
74
80
  export type SendTurnOptions =
@@ -288,21 +294,24 @@ export function useTrueFoundryAgentMessages({
288
294
  onError,
289
295
  initializeSession,
290
296
  resolveConversationSessionId,
291
- draftGateway,
297
+ privateClient,
298
+ getTurnHeaders,
292
299
  }: UseTrueFoundryAgentMessagesOptions) {
293
300
  const sessionOptions = useMemo<GetSessionOptions | undefined>(
294
301
  () =>
295
- draftGateway != null ? { draftGateway } : undefined,
296
- [draftGateway],
302
+ privateClient != null ? { privateClient } : undefined,
303
+ [privateClient],
297
304
  );
298
305
 
299
306
  const [snapshot, setSnapshot] = useState<SessionSnapshot>(createEmptySessionSnapshot);
300
307
  const [isRunning, setIsRunning] = useState(false);
301
308
  const [isLoading, setIsLoading] = useState(false);
309
+ const [isLoadingOlderHistory, setIsLoadingOlderHistory] = useState(false);
302
310
  const [loadRetryTrigger, setLoadRetryTrigger] = useState(0);
303
311
 
304
312
  const snapshotRef = useRef(snapshot);
305
313
  snapshotRef.current = snapshot;
314
+ const loadOlderInflightRef = useRef<Promise<void> | null>(null);
306
315
 
307
316
  const onErrorRef = useRef(onError);
308
317
  onErrorRef.current = onError;
@@ -310,6 +319,8 @@ export function useTrueFoundryAgentMessages({
310
319
  resolveConversationSessionIdRef.current = resolveConversationSessionId;
311
320
  const initializeSessionRef = useRef(initializeSession);
312
321
  initializeSessionRef.current = initializeSession;
322
+ const getTurnHeadersRef = useRef(getTurnHeaders);
323
+ getTurnHeadersRef.current = getTurnHeaders;
313
324
 
314
325
  const createdAtByMessageIdRef = useRef(new Map<string, Date>());
315
326
  const abortControllerRef = useRef<AbortController | null>(null);
@@ -477,9 +488,11 @@ export function useTrueFoundryAgentMessages({
477
488
  const generation = ++loadGenerationRef.current;
478
489
  ++streamGenerationRef.current;
479
490
  abortControllerRef.current?.abort();
491
+ loadOlderInflightRef.current = null;
480
492
  createdAtByMessageIdRef.current = new Map();
481
493
  setSnapshot(createEmptySessionSnapshot());
482
494
  setIsLoading(true);
495
+ setIsLoadingOlderHistory(false);
483
496
 
484
497
  try {
485
498
  const conversationSessionId = await resolveActiveSessionId(
@@ -564,6 +577,9 @@ export function useTrueFoundryAgentMessages({
564
577
  resolveConversationSessionIdRef.current,
565
578
  );
566
579
  const session = await getSession(client, conversationSessionId, sessionOptions);
580
+ const turnHeaders = await getTurnHeadersRef.current?.();
581
+ const streamHeaders =
582
+ turnHeaders != null ? { headers: turnHeaders } : {};
567
583
  const isContinuation =
568
584
  "inputs" in options ||
569
585
  ("resumeMcpAuth" in options && options.resumeMcpAuth === true);
@@ -639,7 +655,7 @@ export function useTrueFoundryAgentMessages({
639
655
  return streamTurnContent(
640
656
  session,
641
657
  snapshotRef.current.fold,
642
- { inputs: options.inputs },
658
+ { inputs: options.inputs, ...streamHeaders },
643
659
  signal,
644
660
  groupRootBaseline,
645
661
  );
@@ -648,7 +664,7 @@ export function useTrueFoundryAgentMessages({
648
664
  return streamTurnContent(
649
665
  session,
650
666
  snapshotRef.current.fold,
651
- { resumeMcpAuth: true },
667
+ { resumeMcpAuth: true, ...streamHeaders },
652
668
  signal,
653
669
  groupRootBaseline,
654
670
  );
@@ -661,6 +677,7 @@ export function useTrueFoundryAgentMessages({
661
677
  ...(options.previousTurnId !== undefined
662
678
  ? { previousTurnId: options.previousTurnId }
663
679
  : {}),
680
+ ...streamHeaders,
664
681
  },
665
682
  signal,
666
683
  groupRootBaseline,
@@ -782,8 +799,6 @@ export function useTrueFoundryAgentMessages({
782
799
  const committed = commitActiveStream(snapshotRef.current);
783
800
  setSnapshot(committed);
784
801
 
785
- const turnIndex = findTurnIndex(committed, turnId);
786
-
787
802
  await cancel();
788
803
 
789
804
  const conversationSessionId = await resolveActiveSessionId(
@@ -791,13 +806,13 @@ export function useTrueFoundryAgentMessages({
791
806
  resolveConversationSessionIdRef.current,
792
807
  );
793
808
  const session = await getSession(client, conversationSessionId, sessionOptions);
794
- const previousTurnId = await resolveGatewayBranchPreviousTurnId(
809
+ const previousTurnId = await resolveGatewayBranchPreviousTurnIdForTurn(
795
810
  session,
796
- turnIndex,
811
+ turnId,
797
812
  );
798
- const rewound = await buildSnapshotBeforeTurnIndex(
813
+ const rewound = await buildSnapshotBeforeTurn(
799
814
  session,
800
- turnIndex,
815
+ turnId,
801
816
  listEventsConcurrency,
802
817
  );
803
818
  createdAtByMessageIdRef.current = new Map();
@@ -855,10 +870,70 @@ export function useTrueFoundryAgentMessages({
855
870
  setLoadRetryTrigger((n) => n + 1);
856
871
  }, []);
857
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
+
858
930
  return {
859
931
  messages,
860
932
  isRunning,
861
933
  isLoading,
934
+ isLoadingOlderHistory,
935
+ hasOlderHistory,
936
+ loadOlderHistory,
862
937
  retryLoad,
863
938
  sendTurn,
864
939
  cancel,
@@ -26,7 +26,11 @@ import {
26
26
  extractEditedText,
27
27
  parseTurnIdFromMessageId,
28
28
  } from "./convertTurnMessages.js";
29
- import { createDraftSessionBridge } from "./private/draftSessionBridge.js";
29
+ import {
30
+ createDraftSessionBridge,
31
+ DRAFT_SESSION_LAST_UPDATED_AT_HEADER,
32
+ } from "./private/draftSessionBridge.js";
33
+ import { getGatewayFromPrivateClient } from "./private/getGatewayFromPrivateClient.js";
30
34
  import { MCP_AUTH_RESUME_RUN_CUSTOM_KEY } from "./mcpAuth.js";
31
35
  import { createTrueFoundryDraftThreadListAdapter } from "./private/truefoundryDraftThreadListAdapter.js";
32
36
  import { trueFoundryExtras } from "./truefoundryExtras.js";
@@ -43,7 +47,7 @@ function useTrueFoundryAgentRuntimeImpl(
43
47
  const {
44
48
  client,
45
49
  agent,
46
- gateway,
50
+ privateClient,
47
51
  adapters,
48
52
  onError,
49
53
  listEventsConcurrency,
@@ -51,8 +55,8 @@ function useTrueFoundryAgentRuntimeImpl(
51
55
  } = options;
52
56
 
53
57
  const draftBridgeRef = useRef(
54
- agent.mode === "draft" && gateway != null
55
- ? createDraftSessionBridge(gateway)
58
+ agent.mode === "draft" && privateClient != null
59
+ ? createDraftSessionBridge(privateClient)
56
60
  : null,
57
61
  );
58
62
 
@@ -76,6 +80,20 @@ function useTrueFoundryAgentRuntimeImpl(
76
80
  onError,
77
81
  });
78
82
 
83
+ const takeTurnHeaderTimestampRef = useRef(draftSpec.takeTurnHeaderTimestamp);
84
+ takeTurnHeaderTimestampRef.current = draftSpec.takeTurnHeaderTimestamp;
85
+
86
+ const getTurnHeaders = useCallback(async () => {
87
+ if (agent.mode !== "draft") {
88
+ return undefined;
89
+ }
90
+ const updatedAt = await takeTurnHeaderTimestampRef.current();
91
+ if (updatedAt == null) {
92
+ return undefined;
93
+ }
94
+ return { [DRAFT_SESSION_LAST_UPDATED_AT_HEADER]: updatedAt };
95
+ }, [agent.mode]);
96
+
79
97
  const aui = useAui();
80
98
  const initializeSession = useCallback(
81
99
  () => aui.threadListItem().initialize(),
@@ -90,6 +108,9 @@ function useTrueFoundryAgentRuntimeImpl(
90
108
  messages,
91
109
  isRunning,
92
110
  isLoading,
111
+ isLoadingOlderHistory,
112
+ hasOlderHistory,
113
+ loadOlderHistory,
93
114
  sendTurn,
94
115
  cancel,
95
116
  respondToToolApproval,
@@ -105,7 +126,8 @@ function useTrueFoundryAgentRuntimeImpl(
105
126
  listEventsConcurrency,
106
127
  onError,
107
128
  initializeSession,
108
- draftGateway: agent.mode === "draft" ? gateway : undefined,
129
+ privateClient: agent.mode === "draft" ? privateClient : undefined,
130
+ getTurnHeaders: agent.mode === "draft" ? getTurnHeaders : undefined,
109
131
  });
110
132
 
111
133
  if (agent.mode === "draft" && draftSpec.agentSpec != null) {
@@ -130,9 +152,9 @@ function useTrueFoundryAgentRuntimeImpl(
130
152
 
131
153
  const downloadSandboxFile = useCallback(
132
154
  async (path: string) => {
133
- if (gateway == null) {
155
+ if (privateClient == null) {
134
156
  const error = new Error(
135
- "Downloading a sandbox file requires a `gateway` TrueFoundryGateway client.",
157
+ "Downloading a sandbox file requires a `privateClient` PrivateAgentSessionClient.",
136
158
  );
137
159
  onError?.(error);
138
160
  throw error;
@@ -142,10 +164,12 @@ function useTrueFoundryAgentRuntimeImpl(
142
164
  onError?.(error);
143
165
  throw error;
144
166
  }
167
+ // Same gateway.agents.downloadSandboxFile path as before, via the wrapped client.
168
+ const gateway = getGatewayFromPrivateClient(privateClient);
145
169
  const response = await gateway.agents.downloadSandboxFile(sandboxId, { path });
146
170
  return await response.blob();
147
171
  },
148
- [gateway, sandboxId, onError],
172
+ [privateClient, sandboxId, onError],
149
173
  );
150
174
 
151
175
  const draftExtras = useMemo(() => {
@@ -181,6 +205,9 @@ function useTrueFoundryAgentRuntimeImpl(
181
205
  onError?.(error);
182
206
  }),
183
207
  reload: retryLoad,
208
+ hasOlderHistory,
209
+ isLoadingOlderHistory,
210
+ loadOlderHistory,
184
211
  draft: draftExtras,
185
212
  }),
186
213
  unstable_enableToolInvocations: true,
@@ -240,7 +267,7 @@ export function useTrueFoundryAgentRuntime(options: UseTrueFoundryAgentRuntimeOp
240
267
  // causing `threadListAdapter` to be a new object each render. We compute `resolved`
241
268
  // unconditionally and stabilize individual downstream values with primitives/refs.
242
269
  const resolved = resolveTrueFoundryAgentRuntimeOptions(options);
243
- const { client, agent, gateway } = resolved;
270
+ const { client, agent, privateClient } = resolved;
244
271
 
245
272
  const pendingAgentSpecRef = useRef<AgentSpec | undefined>(
246
273
  agent.mode === "draft" ? agent.defaultAgentSpec : undefined,
@@ -252,14 +279,14 @@ export function useTrueFoundryAgentRuntime(options: UseTrueFoundryAgentRuntimeOp
252
279
  const namedAgentName = agent.mode === "named" ? agent.agentName : undefined;
253
280
  const threadListAdapter = useMemo(() => {
254
281
  if (agentMode === "draft") {
255
- if (gateway == null) {
282
+ if (privateClient == null) {
256
283
  throw new Error(
257
- "Draft agent mode requires a `gateway` TrueFoundryGateway client.",
284
+ "Draft agent mode requires a `privateClient` PrivateAgentSessionClient.",
258
285
  );
259
286
  }
260
287
  const draftAgent = agent as Extract<typeof agent, { mode: "draft" }>;
261
288
  return createTrueFoundryDraftThreadListAdapter({
262
- gateway,
289
+ privateClient,
263
290
  defaultAgentSpec: draftAgent.defaultAgentSpec,
264
291
  getAgentSpec: () => pendingAgentSpecRef.current ?? draftAgent.defaultAgentSpec,
265
292
  });
@@ -269,7 +296,7 @@ export function useTrueFoundryAgentRuntime(options: UseTrueFoundryAgentRuntimeOp
269
296
  agentName: namedAgentName!,
270
297
  });
271
298
  // eslint-disable-next-line react-hooks/exhaustive-deps
272
- }, [agentMode, namedAgentName, client, gateway]);
299
+ }, [agentMode, namedAgentName, client, privateClient]);
273
300
 
274
301
  return useRemoteThreadListRuntime({
275
302
  allowNesting: true,
@@ -1,37 +0,0 @@
1
- declare module "truefoundry-gateway-sdk/dist/esm/agents/AgentSession.mjs" {
2
- import type { TrueFoundryGateway } from "truefoundry-gateway-sdk";
3
- import type * as TrueFoundryGatewayApi from "truefoundry-gateway-sdk";
4
- import type { PreparedTurn } from "truefoundry-gateway-sdk/agents";
5
- import type * as core from "truefoundry-gateway-sdk/core";
6
-
7
- export class AgentSession implements TrueFoundryGatewayApi.Session {
8
- readonly id: string;
9
- readonly agentName: string;
10
- readonly title?: string;
11
- readonly createdBySubject: TrueFoundryGatewayApi.Subject;
12
- readonly createdAt: string;
13
- readonly updatedAt: string;
14
- constructor(session: TrueFoundryGatewayApi.Session, client: TrueFoundryGateway);
15
- prepareTurn(opts?: {
16
- input?: TrueFoundryGatewayApi.TurnInputItem[];
17
- previousTurnId?: TrueFoundryGatewayApi.PreviousTurnIdInput;
18
- }): PreparedTurn;
19
- listTurns(
20
- opts?: TrueFoundryGatewayApi.agents.SessionsListTurnsRequest,
21
- requestOptions?: unknown,
22
- ): Promise<core.Page<unknown, TrueFoundryGatewayApi.ListTurnsResponse>>;
23
- /**
24
- * Paginated session events across turns (newest first); subscribe to a
25
- * running turn for live events.
26
- *
27
- * @param opts.lastTurnId - Newest turn in the listing window (initial load only).
28
- * Omit to use the session last turn. Running-turn events are excluded.
29
- */
30
- listEvents(
31
- opts?: TrueFoundryGatewayApi.agents.SessionsListEventsRequest,
32
- requestOptions?: unknown,
33
- ): Promise<core.Page<TrueFoundryGatewayApi.SessionEventItem, TrueFoundryGatewayApi.ListSessionEventsResponse>>;
34
- getTurn(opts: { turnId: string }, requestOptions?: unknown): Promise<unknown>;
35
- cancel(requestOptions?: unknown): Promise<void>;
36
- }
37
- }