@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.
Files changed (38) hide show
  1. package/README.md +193 -579
  2. package/dist/chunk-3A2EPLQG.js +93 -0
  3. package/dist/chunk-3A2EPLQG.js.map +1 -0
  4. package/dist/chunk-SQDOTGP2.js +292 -0
  5. package/dist/chunk-SQDOTGP2.js.map +1 -0
  6. package/dist/index.d.ts +24 -36
  7. package/dist/index.js +276 -249
  8. package/dist/index.js.map +1 -1
  9. package/dist/plugins/truefoundry-agent-server-adapter/index.d.ts +134 -5
  10. package/dist/plugins/truefoundry-agent-server-adapter/index.js +16 -195
  11. package/dist/plugins/truefoundry-agent-server-adapter/index.js.map +1 -1
  12. package/dist/server/index.d.ts +17 -0
  13. package/dist/server/index.js +9 -0
  14. package/dist/server/index.js.map +1 -0
  15. package/dist/{types-VUBzoJT2.d.ts → types-DbNsU075.d.ts} +212 -19
  16. package/package.json +10 -5
  17. package/src/{private → draft}/agentSpec.ts +14 -17
  18. package/src/{private → draft}/draftSessionBridge.ts +1 -2
  19. package/src/{private → draft}/truefoundryDraftThreadListAdapter.test.ts +1 -1
  20. package/src/{private → draft}/truefoundryDraftThreadListAdapter.ts +2 -1
  21. package/src/{private → draft}/useDraftAgentSpec.ts +16 -5
  22. package/src/draftAgentConfig.test.ts +2 -1
  23. package/src/index.ts +71 -7
  24. package/src/plugins/truefoundry-agent-server-adapter/README.md +178 -0
  25. package/src/plugins/truefoundry-agent-server-adapter/guards.test.ts +113 -0
  26. package/src/plugins/truefoundry-agent-server-adapter/guards.ts +130 -0
  27. package/src/plugins/truefoundry-agent-server-adapter/index.ts +154 -40
  28. package/src/plugins/truefoundry-agent-server-adapter/types.ts +137 -0
  29. package/src/plugins/truefoundry-agent-server-adapter/types.typecheck.ts +164 -0
  30. package/src/server/index.ts +23 -0
  31. package/src/server/types.ts +272 -21
  32. package/src/truefoundryExtras.ts +4 -1
  33. package/src/truefoundryOwnedSessionsThreadListAdapter.ts +1 -1
  34. package/src/types.ts +1 -2
  35. package/src/useTrueFoundryAgentMessages.test.tsx +261 -1
  36. package/src/useTrueFoundryAgentMessages.ts +284 -176
  37. package/src/useTrueFoundryAgentRuntime.ts +31 -21
  38. /package/src/{private → draft}/useDraftAgentSpec.test.tsx +0 -0
package/dist/index.js CHANGED
@@ -1,3 +1,17 @@
1
+ import {
2
+ isEventDelta,
3
+ mergeEventDelta
4
+ } from "./chunk-3A2EPLQG.js";
5
+ import {
6
+ createTrueFoundryChatServer,
7
+ getTfyMcpInitServers,
8
+ getTfyThreadState,
9
+ getTfyUsage,
10
+ isTfyMcpToolInfo,
11
+ isTfySystemToolInfo,
12
+ isTfyToolInfo
13
+ } from "./chunk-SQDOTGP2.js";
14
+
1
15
  // src/useTrueFoundryAgentRuntime.ts
2
16
  import {
3
17
  pickExternalStoreSharedOptions
@@ -251,94 +265,6 @@ function toTrueFoundryApprovalInputs(message, response, defaultThreadId = ROOT_T
251
265
  return collectApprovalInputs(updated, defaultThreadId);
252
266
  }
253
267
 
254
- // src/server/eventUtils.ts
255
- function isEventDelta(event) {
256
- return typeof event.type === "string" && event.type.endsWith(".delta");
257
- }
258
- function mergeEventDelta(base, delta) {
259
- if (base.id !== delta.id) {
260
- throw new Error(
261
- `Cannot merge delta into a different event: base id "${base.id}" != delta id "${delta.id}".`
262
- );
263
- }
264
- if (delta.type === "model.message.delta" && base.type === "model.message") {
265
- mergeModelMessageDelta(base, delta);
266
- }
267
- }
268
- function asToolInfo(value) {
269
- if (value == null || typeof value !== "object") {
270
- return void 0;
271
- }
272
- return value;
273
- }
274
- function mergeModelMessageDelta(base, delta) {
275
- if (delta.content) {
276
- if (base.content === void 0 || base.content === null || typeof base.content === "string") {
277
- base.content = (base.content ?? "") + delta.content;
278
- } else {
279
- const last = base.content[base.content.length - 1];
280
- if (last && last.type === "text") {
281
- last.text += delta.content;
282
- } else {
283
- base.content.push({ type: "text", text: delta.content });
284
- }
285
- }
286
- }
287
- if (delta.refusal) {
288
- base.refusal = (base.refusal ?? "") + delta.refusal;
289
- }
290
- if (delta.toolCalls) {
291
- base.toolCalls ??= [];
292
- for (const d of delta.toolCalls) {
293
- let tc = base.toolCalls[d.index];
294
- if (tc === void 0) {
295
- const toolInfo2 = asToolInfo(d.toolInfo);
296
- tc = {
297
- id: d.id ?? "",
298
- type: d.type ?? "function",
299
- function: {
300
- name: d.function?.name ?? "",
301
- arguments: ""
302
- },
303
- ...toolInfo2 != null ? { toolInfo: toolInfo2 } : {}
304
- };
305
- base.toolCalls[d.index] = tc;
306
- }
307
- if (d.id) {
308
- tc.id = d.id;
309
- }
310
- if (d.type) {
311
- tc.type = d.type;
312
- }
313
- if (d.function?.name) {
314
- tc.function.name = d.function.name;
315
- }
316
- if (d.function?.arguments) {
317
- tc.function.arguments += d.function.arguments;
318
- }
319
- const toolInfo = asToolInfo(d.toolInfo);
320
- if (toolInfo != null) {
321
- tc.toolInfo = toolInfo;
322
- }
323
- if (d.providerSpecificFields) {
324
- tc.providerSpecificFields = {
325
- ...tc.providerSpecificFields ?? {},
326
- ...d.providerSpecificFields
327
- };
328
- }
329
- }
330
- }
331
- if (delta.finishReason) {
332
- base.finishReason = delta.finishReason;
333
- }
334
- if (delta.reasoningContent) {
335
- base.reasoningContent = (base.reasoningContent ?? "") + delta.reasoningContent;
336
- }
337
- if (delta.usage) {
338
- base.usage = delta.usage;
339
- }
340
- }
341
-
342
268
  // src/askUserQuestion.ts
343
269
  function parseAskUserQuestionArgs(argsText) {
344
270
  if (!argsText) {
@@ -2460,7 +2386,7 @@ function repositoryItemsFromMessages(messages) {
2460
2386
  return items;
2461
2387
  }
2462
2388
 
2463
- // src/private/draftSessionBridge.ts
2389
+ // src/draft/draftSessionBridge.ts
2464
2390
  var DRAFT_SESSION_LAST_UPDATED_AT_HEADER = "x-tfy-session-last-updated-at";
2465
2391
  function createDraftSessionBridge(server) {
2466
2392
  return {
@@ -2483,7 +2409,7 @@ function createDraftSessionBridge(server) {
2483
2409
  };
2484
2410
  }
2485
2411
 
2486
- // src/private/agentSpec.ts
2412
+ // src/draft/agentSpec.ts
2487
2413
  function mergeAgentSpec(base, update) {
2488
2414
  const { model: modelUpdate, ...rest } = update;
2489
2415
  const next = {
@@ -2511,7 +2437,7 @@ function sessionListStartTimestamp() {
2511
2437
  return start.toISOString();
2512
2438
  }
2513
2439
 
2514
- // src/private/truefoundryDraftThreadListAdapter.ts
2440
+ // src/draft/truefoundryDraftThreadListAdapter.ts
2515
2441
  var THREAD_LIST_PAGE_SIZE = 20;
2516
2442
  function createTrueFoundryDraftThreadListAdapter(options) {
2517
2443
  const { server, defaultAgentSpec, getAgentSpec } = options;
@@ -2576,6 +2502,7 @@ var trueFoundryExtras = createRuntimeExtras(
2576
2502
  var EMPTY_DRAFT_EXTRAS = {
2577
2503
  agentSpec: null,
2578
2504
  draftSessionId: void 0,
2505
+ isSpecLoading: false,
2579
2506
  isSpecSyncing: false,
2580
2507
  specError: null,
2581
2508
  updateAgentSpec: () => {
@@ -2671,7 +2598,7 @@ function resolveTrueFoundryAgentRuntimeOptions(options) {
2671
2598
  };
2672
2599
  }
2673
2600
 
2674
- // src/private/useDraftAgentSpec.ts
2601
+ // src/draft/useDraftAgentSpec.ts
2675
2602
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2676
2603
  var SPEC_SYNC_DEBOUNCE_MS = 400;
2677
2604
  function useDraftAgentSpec({
@@ -2683,6 +2610,7 @@ function useDraftAgentSpec({
2683
2610
  }) {
2684
2611
  const enabled = draftBridge != null;
2685
2612
  const [agentSpec, setAgentSpec] = useState(defaultAgentSpec);
2613
+ const [isSpecLoading, setIsSpecLoading] = useState(false);
2686
2614
  const [isSpecSyncing, setIsSpecSyncing] = useState(false);
2687
2615
  const [specError, setSpecError] = useState(null);
2688
2616
  const agentSpecRef = useRef(agentSpec);
@@ -2723,12 +2651,14 @@ function useDraftAgentSpec({
2723
2651
  setAgentSpec(defaultAgentSpec);
2724
2652
  localDirtyRef.current = false;
2725
2653
  setSpecError(null);
2654
+ setIsSpecLoading(false);
2726
2655
  return;
2727
2656
  }
2728
2657
  if (loadedDraftIdRef.current === draftSessionId) {
2729
2658
  return;
2730
2659
  }
2731
2660
  let cancelled = false;
2661
+ setIsSpecLoading(true);
2732
2662
  void (async () => {
2733
2663
  try {
2734
2664
  const loaded = await draftBridge.getDraftAgentSpec(draftSessionId);
@@ -2740,19 +2670,23 @@ function useDraftAgentSpec({
2740
2670
  scheduleSpecSyncRef.current?.(draftSessionId, agentSpecRef.current);
2741
2671
  localDirtyRef.current = false;
2742
2672
  setSpecError(null);
2673
+ setIsSpecLoading(false);
2743
2674
  return;
2744
2675
  }
2745
2676
  setAgentSpec(loaded);
2746
2677
  setSpecError(null);
2678
+ setIsSpecLoading(false);
2747
2679
  } catch (error) {
2748
2680
  if (!cancelled) {
2749
2681
  onError?.(error);
2750
2682
  setSpecError(error);
2683
+ setIsSpecLoading(false);
2751
2684
  }
2752
2685
  }
2753
2686
  })();
2754
2687
  return () => {
2755
2688
  cancelled = true;
2689
+ setIsSpecLoading(false);
2756
2690
  };
2757
2691
  }, [defaultAgentSpec, draftBridge, draftSessionId, enabled, onError]);
2758
2692
  const flushSpecSync = useCallback(
@@ -2855,6 +2789,7 @@ function useDraftAgentSpec({
2855
2789
  () => ({
2856
2790
  agentSpec: enabled ? agentSpec : null,
2857
2791
  draftSessionId: enabled ? draftSessionId : void 0,
2792
+ isSpecLoading: enabled ? isSpecLoading : false,
2858
2793
  isSpecSyncing: enabled ? isSpecSyncing : false,
2859
2794
  specError: enabled ? specError : null,
2860
2795
  updateAgentSpec,
@@ -2864,6 +2799,7 @@ function useDraftAgentSpec({
2864
2799
  agentSpec,
2865
2800
  draftSessionId,
2866
2801
  enabled,
2802
+ isSpecLoading,
2867
2803
  isSpecSyncing,
2868
2804
  specError,
2869
2805
  takeTurnHeaderTimestamp,
@@ -3132,6 +3068,7 @@ function useTrueFoundryAgentMessages({
3132
3068
  server,
3133
3069
  sessionId,
3134
3070
  isMain,
3071
+ isInitialSession,
3135
3072
  listEventsConcurrency,
3136
3073
  onError,
3137
3074
  initializeSession,
@@ -3140,7 +3077,9 @@ function useTrueFoundryAgentMessages({
3140
3077
  }) {
3141
3078
  const [snapshot, setSnapshot] = useState2(createEmptySessionSnapshot);
3142
3079
  const [isRunning, setIsRunning] = useState2(false);
3143
- const [isLoading, setIsLoading] = useState2(false);
3080
+ const [isLoading, setIsLoading] = useState2(
3081
+ sessionId != null && (isMain !== false || isInitialSession === true)
3082
+ );
3144
3083
  const [isLoadingOlderHistory, setIsLoadingOlderHistory] = useState2(false);
3145
3084
  const [loadRetryTrigger, setLoadRetryTrigger] = useState2(0);
3146
3085
  const snapshotRef = useRef2(snapshot);
@@ -3161,6 +3100,8 @@ function useTrueFoundryAgentMessages({
3161
3100
  const loadGenerationRef = useRef2(0);
3162
3101
  const streamGenerationRef = useRef2(0);
3163
3102
  const lazilyCreatedSessionIdRef = useRef2(void 0);
3103
+ const initialLoadStartedForRef = useRef2(void 0);
3104
+ const skipInitialPromotionLoadForRef = useRef2(void 0);
3164
3105
  const projectOptions = useMemo2(
3165
3106
  () => ({
3166
3107
  getCreatedAt: (messageId, fallback) => {
@@ -3271,12 +3212,24 @@ function useTrueFoundryAgentMessages({
3271
3212
  [onError]
3272
3213
  );
3273
3214
  const load = useCallback2(async () => {
3215
+ void loadRetryTrigger;
3274
3216
  if (sessionId == null) {
3275
3217
  createdAtByMessageIdRef.current = /* @__PURE__ */ new Map();
3276
3218
  setSnapshot(createEmptySessionSnapshot());
3277
3219
  return;
3278
3220
  }
3279
- if (isMain === false) return;
3221
+ const isEarlyInitialLoad = isMain === false && isInitialSession === true && initialLoadStartedForRef.current !== sessionId;
3222
+ if (isMain === false) {
3223
+ if (!isEarlyInitialLoad) return;
3224
+ initialLoadStartedForRef.current = sessionId;
3225
+ skipInitialPromotionLoadForRef.current = sessionId;
3226
+ } else if (isMain === true && skipInitialPromotionLoadForRef.current === sessionId) {
3227
+ skipInitialPromotionLoadForRef.current = void 0;
3228
+ return;
3229
+ }
3230
+ if (isInitialSession === true) {
3231
+ initialLoadStartedForRef.current = sessionId;
3232
+ }
3280
3233
  if (lazilyCreatedSessionIdRef.current != null && sessionId !== lazilyCreatedSessionIdRef.current) {
3281
3234
  lazilyCreatedSessionIdRef.current = void 0;
3282
3235
  }
@@ -3285,6 +3238,7 @@ function useTrueFoundryAgentMessages({
3285
3238
  }
3286
3239
  const generation = ++loadGenerationRef.current;
3287
3240
  ++streamGenerationRef.current;
3241
+ setIsRunning(false);
3288
3242
  abortControllerRef.current?.abort();
3289
3243
  loadOlderInflightRef.current = null;
3290
3244
  createdAtByMessageIdRef.current = /* @__PURE__ */ new Map();
@@ -3331,6 +3285,10 @@ function useTrueFoundryAgentMessages({
3331
3285
  }
3332
3286
  } catch (error) {
3333
3287
  if (generation === loadGenerationRef.current) {
3288
+ if (isEarlyInitialLoad) {
3289
+ initialLoadStartedForRef.current = void 0;
3290
+ skipInitialPromotionLoadForRef.current = void 0;
3291
+ }
3334
3292
  onErrorRef.current?.(error);
3335
3293
  }
3336
3294
  throw error;
@@ -3339,134 +3297,178 @@ function useTrueFoundryAgentMessages({
3339
3297
  setIsLoading(false);
3340
3298
  }
3341
3299
  }
3342
- }, [server, runStream, sessionId, loadRetryTrigger, isMain]);
3300
+ }, [
3301
+ server,
3302
+ runStream,
3303
+ sessionId,
3304
+ loadRetryTrigger,
3305
+ isMain,
3306
+ isInitialSession
3307
+ ]);
3343
3308
  useEffect2(() => {
3344
3309
  void load().catch(() => void 0);
3345
3310
  }, [load]);
3346
3311
  const sendTurn = useCallback2(
3347
3312
  async (options) => {
3348
- let activeSessionId = sessionId;
3349
- if (activeSessionId == null) {
3350
- if (initializeSessionRef.current == null) {
3351
- throw new Error("Cannot send a turn without an active session.");
3313
+ let gatewayTurnAccepted = false;
3314
+ let pendingUserWasSet = false;
3315
+ let runStreamStarted = false;
3316
+ let pendingUserTurnId;
3317
+ try {
3318
+ let activeSessionId = sessionId;
3319
+ if (activeSessionId == null) {
3320
+ if (initializeSessionRef.current == null) {
3321
+ throw new Error("Cannot send a turn without an active session.");
3322
+ }
3323
+ const { remoteId } = await initializeSessionRef.current();
3324
+ activeSessionId = remoteId;
3325
+ lazilyCreatedSessionIdRef.current = remoteId;
3352
3326
  }
3353
- const { remoteId } = await initializeSessionRef.current();
3354
- activeSessionId = remoteId;
3355
- lazilyCreatedSessionIdRef.current = remoteId;
3356
- }
3357
- const conversationSessionId = await resolveActiveSessionId(
3358
- activeSessionId,
3359
- resolveConversationSessionIdRef.current
3360
- );
3361
- const turnHeaders = await getTurnHeadersRef.current?.();
3362
- const streamHeaders = turnHeaders != null ? { headers: turnHeaders } : {};
3363
- const isContinuation = "inputs" in options || "resumeMcpAuth" in options && options.resumeMcpAuth === true;
3364
- const continuationTurnId = snapshotRef.current.activeStream?.turnId;
3365
- const turnId = isContinuation && continuationTurnId != null ? continuationTurnId : generateId();
3366
- const isFirstTurnInSession = "userMessage" in options && options.previousTurnId === void 0 && snapshotRef.current.turns.length === 0 && snapshotRef.current.pendingUser == null && snapshotRef.current.activeStream == null;
3367
- const turnIdRef = { current: turnId };
3368
- if ("inputs" in options) {
3369
- applyUserToolResponsesToFold(
3370
- snapshotRef.current.fold,
3371
- options.inputs
3372
- );
3373
- }
3374
- const branchBase = "userMessage" in options ? options.branchFromSnapshot : void 0;
3375
- let groupRootBaseline;
3376
- if (branchBase != null && "userMessage" in options) {
3377
- const rootBucket = branchBase.fold.threads.get(ROOT_THREAD_ID);
3378
- groupRootBaseline = [...rootBucket?.modelMessageIds ?? []];
3379
- const nextSnapshot = replaceSessionSnapshot(branchBase, {
3380
- pendingUser: {
3381
- turnId,
3382
- content: options.userMessage,
3383
- createdAt: /* @__PURE__ */ new Date()
3384
- },
3385
- activeStream: void 0,
3386
- groupRootBaseline
3387
- });
3388
- snapshotRef.current = nextSnapshot;
3389
- setSnapshot(nextSnapshot);
3390
- } else {
3391
- setSnapshot(
3392
- (prev) => commitActiveStream(
3393
- prev,
3394
- "inputs" in options ? options.inputs : void 0
3395
- )
3327
+ const conversationSessionId = await resolveActiveSessionId(
3328
+ activeSessionId,
3329
+ resolveConversationSessionIdRef.current
3396
3330
  );
3397
- if ("userMessage" in options) {
3398
- const rootBucket = snapshotRef.current.fold.threads.get(ROOT_THREAD_ID);
3331
+ const turnHeaders = await getTurnHeadersRef.current?.();
3332
+ const streamHeaders = turnHeaders != null ? { headers: turnHeaders } : {};
3333
+ const isContinuation = "inputs" in options || "resumeMcpAuth" in options && options.resumeMcpAuth === true;
3334
+ const continuationTurnId = snapshotRef.current.activeStream?.turnId;
3335
+ const turnId = isContinuation && continuationTurnId != null ? continuationTurnId : generateId();
3336
+ const isFirstTurnInSession = "userMessage" in options && options.previousTurnId === void 0 && snapshotRef.current.turns.length === 0 && snapshotRef.current.pendingUser == null && snapshotRef.current.activeStream == null;
3337
+ const turnIdRef = { current: turnId };
3338
+ if ("inputs" in options) {
3339
+ applyUserToolResponsesToFold(
3340
+ snapshotRef.current.fold,
3341
+ options.inputs
3342
+ );
3343
+ }
3344
+ const branchBase = "userMessage" in options ? options.branchFromSnapshot : void 0;
3345
+ let groupRootBaseline;
3346
+ if (branchBase != null && "userMessage" in options) {
3347
+ const rootBucket = branchBase.fold.threads.get(ROOT_THREAD_ID);
3399
3348
  groupRootBaseline = [...rootBucket?.modelMessageIds ?? []];
3400
- setSnapshot((prev) => {
3401
- const next = replaceSessionSnapshot(prev, {
3402
- pendingUser: {
3403
- turnId,
3404
- content: options.userMessage,
3405
- createdAt: /* @__PURE__ */ new Date()
3406
- },
3407
- activeStream: void 0,
3408
- groupRootBaseline
3409
- });
3410
- snapshotRef.current = next;
3411
- return next;
3349
+ const nextSnapshot = replaceSessionSnapshot(branchBase, {
3350
+ pendingUser: {
3351
+ turnId,
3352
+ content: options.userMessage,
3353
+ createdAt: /* @__PURE__ */ new Date()
3354
+ },
3355
+ activeStream: void 0,
3356
+ groupRootBaseline
3412
3357
  });
3358
+ snapshotRef.current = nextSnapshot;
3359
+ setSnapshot(nextSnapshot);
3360
+ pendingUserWasSet = true;
3361
+ pendingUserTurnId = turnId;
3413
3362
  } else {
3414
- groupRootBaseline = snapshotRef.current.groupRootBaseline ?? computeGroupRootBaseline(snapshotRef.current.turns);
3415
- }
3416
- }
3417
- await runStream(
3418
- (signal) => {
3419
- if ("inputs" in options) {
3420
- return streamTurnContent(
3421
- server,
3422
- conversationSessionId,
3423
- snapshotRef.current.fold,
3424
- { inputs: options.inputs, ...streamHeaders },
3425
- signal,
3426
- groupRootBaseline
3427
- );
3363
+ setSnapshot(
3364
+ (prev) => commitActiveStream(
3365
+ prev,
3366
+ "inputs" in options ? options.inputs : void 0
3367
+ )
3368
+ );
3369
+ if ("userMessage" in options) {
3370
+ const rootBucket = snapshotRef.current.fold.threads.get(ROOT_THREAD_ID);
3371
+ groupRootBaseline = [...rootBucket?.modelMessageIds ?? []];
3372
+ setSnapshot((prev) => {
3373
+ const next = replaceSessionSnapshot(prev, {
3374
+ pendingUser: {
3375
+ turnId,
3376
+ content: options.userMessage,
3377
+ createdAt: /* @__PURE__ */ new Date()
3378
+ },
3379
+ activeStream: void 0,
3380
+ groupRootBaseline
3381
+ });
3382
+ snapshotRef.current = next;
3383
+ return next;
3384
+ });
3385
+ pendingUserWasSet = true;
3386
+ pendingUserTurnId = turnId;
3387
+ } else {
3388
+ groupRootBaseline = snapshotRef.current.groupRootBaseline ?? computeGroupRootBaseline(snapshotRef.current.turns);
3428
3389
  }
3429
- if ("resumeMcpAuth" in options) {
3390
+ }
3391
+ runStreamStarted = true;
3392
+ await runStream(
3393
+ (signal) => {
3394
+ if ("inputs" in options) {
3395
+ return streamTurnContent(
3396
+ server,
3397
+ conversationSessionId,
3398
+ snapshotRef.current.fold,
3399
+ { inputs: options.inputs, ...streamHeaders },
3400
+ signal,
3401
+ groupRootBaseline
3402
+ );
3403
+ }
3404
+ if ("resumeMcpAuth" in options) {
3405
+ return streamTurnContent(
3406
+ server,
3407
+ conversationSessionId,
3408
+ snapshotRef.current.fold,
3409
+ { resumeMcpAuth: true, ...streamHeaders },
3410
+ signal,
3411
+ groupRootBaseline
3412
+ );
3413
+ }
3430
3414
  return streamTurnContent(
3431
3415
  server,
3432
3416
  conversationSessionId,
3433
3417
  snapshotRef.current.fold,
3434
- { resumeMcpAuth: true, ...streamHeaders },
3418
+ {
3419
+ userMessage: options.userMessage,
3420
+ ...options.previousTurnId !== void 0 ? { previousTurnId: options.previousTurnId ?? "none" } : isFirstTurnInSession ? { previousTurnId: "none" } : {},
3421
+ ...streamHeaders
3422
+ },
3435
3423
  signal,
3436
- groupRootBaseline
3424
+ groupRootBaseline,
3425
+ // Rename the optimistic local ID to the gateway turn ID
3426
+ // so that edit/retry can resolve the turn via the gateway.
3427
+ (gatewayTurnId) => {
3428
+ const oldId = turnIdRef.current;
3429
+ gatewayTurnAccepted = true;
3430
+ if (gatewayTurnId === oldId) return;
3431
+ turnIdRef.current = gatewayTurnId;
3432
+ const renamePendingUser = (prev) => {
3433
+ if (prev.pendingUser?.turnId !== oldId) return prev;
3434
+ return replaceSessionSnapshot(prev, {
3435
+ pendingUser: { ...prev.pendingUser, turnId: gatewayTurnId }
3436
+ });
3437
+ };
3438
+ snapshotRef.current = renamePendingUser(snapshotRef.current);
3439
+ setSnapshot(renamePendingUser);
3440
+ }
3437
3441
  );
3442
+ },
3443
+ turnIdRef,
3444
+ isContinuation
3445
+ );
3446
+ } catch (error) {
3447
+ if ("userMessage" in options && !gatewayTurnAccepted) {
3448
+ const branchRollbackSnapshot = options.branchRollbackSnapshot;
3449
+ const canRestoreBranch = branchRollbackSnapshot != null && (snapshotRef.current === options.branchFromSnapshot || snapshotRef.current.pendingUser?.turnId === pendingUserTurnId);
3450
+ if (canRestoreBranch) {
3451
+ snapshotRef.current = branchRollbackSnapshot;
3452
+ setSnapshot(branchRollbackSnapshot);
3453
+ } else if (pendingUserWasSet) {
3454
+ const clearPendingUser = (previous) => {
3455
+ if (previous.pendingUser?.turnId !== pendingUserTurnId) {
3456
+ return previous;
3457
+ }
3458
+ return replaceSessionSnapshot(previous, {
3459
+ pendingUser: void 0
3460
+ });
3461
+ };
3462
+ snapshotRef.current = clearPendingUser(snapshotRef.current);
3463
+ setSnapshot(clearPendingUser);
3438
3464
  }
3439
- return streamTurnContent(
3440
- server,
3441
- conversationSessionId,
3442
- snapshotRef.current.fold,
3443
- {
3444
- userMessage: options.userMessage,
3445
- ...options.previousTurnId !== void 0 ? { previousTurnId: options.previousTurnId ?? "none" } : isFirstTurnInSession ? { previousTurnId: "none" } : {},
3446
- ...streamHeaders
3447
- },
3448
- signal,
3449
- groupRootBaseline,
3450
- // Rename the optimistic local ID to the gateway turn ID
3451
- // so that edit/retry can resolve the turn via the gateway.
3452
- (gatewayTurnId) => {
3453
- const oldId = turnIdRef.current;
3454
- if (gatewayTurnId === oldId) return;
3455
- turnIdRef.current = gatewayTurnId;
3456
- const renamePendingUser = (prev) => {
3457
- if (prev.pendingUser?.turnId !== oldId) return prev;
3458
- return replaceSessionSnapshot(prev, {
3459
- pendingUser: { ...prev.pendingUser, turnId: gatewayTurnId }
3460
- });
3461
- };
3462
- snapshotRef.current = renamePendingUser(snapshotRef.current);
3463
- setSnapshot(renamePendingUser);
3464
- }
3465
- );
3466
- },
3467
- turnIdRef,
3468
- isContinuation
3469
- );
3465
+ options.onPreTurnFailure?.();
3466
+ }
3467
+ if (!runStreamStarted) {
3468
+ onErrorRef.current?.(error);
3469
+ }
3470
+ throw error;
3471
+ }
3470
3472
  },
3471
3473
  [server, runStream, sessionId]
3472
3474
  );
@@ -3496,7 +3498,7 @@ function useTrueFoundryAgentMessages({
3496
3498
  }
3497
3499
  const inputs = collectRequiredActionInputs(paused);
3498
3500
  if (inputs.length > 0) {
3499
- void sendTurn({ inputs }).catch((error) => onErrorRef.current?.(error));
3501
+ void sendTurn({ inputs }).catch(() => void 0);
3500
3502
  }
3501
3503
  },
3502
3504
  [projectOptions, sendTurn]
@@ -3557,35 +3559,44 @@ function useTrueFoundryAgentMessages({
3557
3559
  }, [runStream, server]);
3558
3560
  const branchFromTurn = useCallback2(
3559
3561
  async (turnId, userMessage) => {
3560
- let activeSessionId = sessionId;
3561
- if (activeSessionId == null) {
3562
- throw new Error("Cannot branch from a turn without an active session.");
3562
+ let committed;
3563
+ let previousTurnId;
3564
+ let rewound;
3565
+ try {
3566
+ let activeSessionId = sessionId;
3567
+ if (activeSessionId == null) {
3568
+ throw new Error("Cannot branch from a turn without an active session.");
3569
+ }
3570
+ committed = commitActiveStream(snapshotRef.current);
3571
+ setSnapshot(committed);
3572
+ await cancel();
3573
+ const conversationSessionId = await resolveActiveSessionId(
3574
+ activeSessionId,
3575
+ resolveConversationSessionIdRef.current
3576
+ );
3577
+ previousTurnId = await resolveGatewayBranchPreviousTurnIdForTurn(
3578
+ server,
3579
+ conversationSessionId,
3580
+ turnId
3581
+ );
3582
+ rewound = await buildSnapshotBeforeTurn(
3583
+ server,
3584
+ conversationSessionId,
3585
+ turnId,
3586
+ listEventsConcurrency
3587
+ );
3588
+ createdAtByMessageIdRef.current = /* @__PURE__ */ new Map();
3589
+ snapshotRef.current = rewound;
3590
+ setSnapshot(rewound);
3591
+ } catch (error) {
3592
+ onErrorRef.current?.(error);
3593
+ throw error;
3563
3594
  }
3564
- const committed = commitActiveStream(snapshotRef.current);
3565
- setSnapshot(committed);
3566
- await cancel();
3567
- const conversationSessionId = await resolveActiveSessionId(
3568
- activeSessionId,
3569
- resolveConversationSessionIdRef.current
3570
- );
3571
- const previousTurnId = await resolveGatewayBranchPreviousTurnIdForTurn(
3572
- server,
3573
- conversationSessionId,
3574
- turnId
3575
- );
3576
- const rewound = await buildSnapshotBeforeTurn(
3577
- server,
3578
- conversationSessionId,
3579
- turnId,
3580
- listEventsConcurrency
3581
- );
3582
- createdAtByMessageIdRef.current = /* @__PURE__ */ new Map();
3583
- snapshotRef.current = rewound;
3584
- setSnapshot(rewound);
3585
3595
  await sendTurn({
3586
3596
  userMessage,
3587
3597
  previousTurnId,
3588
- branchFromSnapshot: rewound
3598
+ branchFromSnapshot: rewound,
3599
+ branchRollbackSnapshot: committed
3589
3600
  });
3590
3601
  },
3591
3602
  [
@@ -3601,7 +3612,9 @@ function useTrueFoundryAgentMessages({
3601
3612
  const committed = commitActiveStream(snapshotRef.current);
3602
3613
  const originalInput = resolveTurnInput(committed, turnId);
3603
3614
  if (originalInput == null) {
3604
- throw new Error(`Turn ${turnId} not found in session snapshot`);
3615
+ const error = new Error(`Turn ${turnId} not found in session snapshot`);
3616
+ onErrorRef.current?.(error);
3617
+ throw error;
3605
3618
  }
3606
3619
  const userMessage = extractTurnUserMessageContent(originalInput);
3607
3620
  await branchFromTurn(turnId, userMessage);
@@ -3613,7 +3626,9 @@ function useTrueFoundryAgentMessages({
3613
3626
  const committed = commitActiveStream(snapshotRef.current);
3614
3627
  const originalInput = resolveTurnInput(committed, turnId);
3615
3628
  if (originalInput == null) {
3616
- throw new Error(`Turn ${turnId} not found in session snapshot`);
3629
+ const error = new Error(`Turn ${turnId} not found in session snapshot`);
3630
+ onErrorRef.current?.(error);
3631
+ throw error;
3617
3632
  }
3618
3633
  const userMessage = buildEditedUserMessageContent(
3619
3634
  editedText,
@@ -3712,6 +3727,7 @@ function useTrueFoundryAgentRuntimeImpl(options, pendingAgentSpecRef) {
3712
3727
  const isMain = useAuiState(
3713
3728
  (state) => state.threads.mainThreadId === state.threadListItem.id
3714
3729
  );
3730
+ const isInitialSession = sessionId != null && sessionId === options.initialSessionId;
3715
3731
  const draftSpec = useDraftAgentSpec({
3716
3732
  draftSessionId,
3717
3733
  draftBridge: draftBridgeRef.current,
@@ -3757,6 +3773,7 @@ function useTrueFoundryAgentRuntimeImpl(options, pendingAgentSpecRef) {
3757
3773
  server,
3758
3774
  sessionId,
3759
3775
  isMain,
3776
+ isInitialSession,
3760
3777
  listEventsConcurrency,
3761
3778
  onError,
3762
3779
  initializeSession,
@@ -3782,20 +3799,16 @@ function useTrueFoundryAgentRuntimeImpl(options, pendingAgentSpecRef) {
3782
3799
  const downloadSandboxFile = useCallback3(
3783
3800
  async (path) => {
3784
3801
  if (server.downloadSandboxFile == null) {
3785
- const error = new Error(
3802
+ throw new Error(
3786
3803
  "Downloading a sandbox file requires AgentChatServer.downloadSandboxFile."
3787
3804
  );
3788
- onError?.(error);
3789
- throw error;
3790
3805
  }
3791
3806
  if (sandboxId == null) {
3792
- const error = new Error("No sandbox is available yet for this session.");
3793
- onError?.(error);
3794
- throw error;
3807
+ throw new Error("No sandbox is available yet for this session.");
3795
3808
  }
3796
3809
  return await server.downloadSandboxFile(sandboxId, { path });
3797
3810
  },
3798
- [server, sandboxId, onError]
3811
+ [server, sandboxId]
3799
3812
  );
3800
3813
  const draftExtras = useMemo3(() => {
3801
3814
  if (agent.mode !== "draft") {
@@ -3804,6 +3817,7 @@ function useTrueFoundryAgentRuntimeImpl(options, pendingAgentSpecRef) {
3804
3817
  return {
3805
3818
  agentSpec: draftSpec.agentSpec,
3806
3819
  draftSessionId: draftSpec.draftSessionId,
3820
+ isSpecLoading: draftSpec.isSpecLoading,
3807
3821
  isSpecSyncing: draftSpec.isSpecSyncing,
3808
3822
  specError: draftSpec.specError,
3809
3823
  updateAgentSpec: draftSpec.updateAgentSpec
@@ -3824,9 +3838,8 @@ function useTrueFoundryAgentRuntimeImpl(options, pendingAgentSpecRef) {
3824
3838
  resumeMcpAuth,
3825
3839
  downloadSandboxFile,
3826
3840
  cancel,
3827
- resetFromTurn: (turnId) => resetFromTurn(turnId).catch((error) => {
3828
- onError?.(error);
3829
- }),
3841
+ // resetFromTurn/branchFromTurn/sendTurn already report via onError.
3842
+ resetFromTurn: (turnId) => resetFromTurn(turnId).catch(() => void 0),
3830
3843
  reload: retryLoad,
3831
3844
  hasOlderHistory,
3832
3845
  isLoadingOlderHistory,
@@ -3851,7 +3864,19 @@ function useTrueFoundryAgentRuntimeImpl(options, pendingAgentSpecRef) {
3851
3864
  await sendTurn({ resumeMcpAuth: true });
3852
3865
  return;
3853
3866
  }
3854
- await sendTurn({ userMessage: buildUserMessageContent(message) });
3867
+ const userMessage = buildUserMessageContent(message);
3868
+ await sendTurn({
3869
+ userMessage,
3870
+ // The composer clears before onNew runs. Restore its text only when
3871
+ // the turn failed before turn.created registered it in the backend.
3872
+ onPreTurnFailure: () => {
3873
+ const text = userMessageContentToText(userMessage);
3874
+ const composer = aui.thread().composer();
3875
+ if (text && !composer.getState().text.trim()) {
3876
+ composer.setText(text);
3877
+ }
3878
+ }
3879
+ });
3855
3880
  },
3856
3881
  onCancel: async () => {
3857
3882
  await cancel();
@@ -3869,12 +3894,7 @@ function useTrueFoundryAgentRuntimeImpl(options, pendingAgentSpecRef) {
3869
3894
  }
3870
3895
  const turnId = parseTurnIdFromMessageId(sourceId);
3871
3896
  const editedText = extractEditedText(message);
3872
- try {
3873
- await editFromTurn(turnId, editedText);
3874
- } catch (error) {
3875
- onError?.(error);
3876
- throw error;
3877
- }
3897
+ await editFromTurn(turnId, editedText);
3878
3898
  }
3879
3899
  });
3880
3900
  }
@@ -4123,14 +4143,21 @@ export {
4123
4143
  collectResponseInputs,
4124
4144
  convertTurnsToThreadMessages,
4125
4145
  createDraftSessionBridge,
4146
+ createTrueFoundryChatServer,
4126
4147
  createTrueFoundryDraftThreadListAdapter,
4127
4148
  createTrueFoundryOwnedSessionsThreadListAdapter,
4128
4149
  createTrueFoundryThreadListAdapter,
4129
4150
  draftSessionTitle,
4130
4151
  findPausedAssistantMessage,
4131
4152
  getSession,
4153
+ getTfyMcpInitServers,
4154
+ getTfyThreadState,
4155
+ getTfyUsage,
4132
4156
  getTurnMessageContent,
4133
4157
  isEventDelta,
4158
+ isTfyMcpToolInfo,
4159
+ isTfySystemToolInfo,
4160
+ isTfyToolInfo,
4134
4161
  mergeAgentSpec,
4135
4162
  mergeEventDelta,
4136
4163
  messageHasPendingApprovals,