@truefoundry/assistant-ui-runtime 0.1.6 → 0.1.8

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 (46) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +22 -17
  3. package/dist/chunk-CXBZ6WLZ.js +636 -0
  4. package/dist/chunk-CXBZ6WLZ.js.map +1 -0
  5. package/dist/index.d.ts +20 -24
  6. package/dist/index.js +269 -166
  7. package/dist/index.js.map +1 -1
  8. package/dist/plugins/truefoundry-agent-server-adapter/index.d.ts +82 -39
  9. package/dist/plugins/truefoundry-agent-server-adapter/index.js +3 -1
  10. package/dist/server/index.d.ts +2 -2
  11. package/dist/{types-BfiFf8O1.d.ts → types-B_z-FsDS.d.ts} +208 -10
  12. package/package.json +1 -1
  13. package/src/convertTurnMessages.ts +4 -0
  14. package/src/{private → draft}/agentSpec.ts +14 -17
  15. package/src/{private → draft}/draftSessionBridge.ts +1 -2
  16. package/src/{private → draft}/truefoundryDraftThreadListAdapter.test.ts +1 -1
  17. package/src/{private → draft}/truefoundryDraftThreadListAdapter.ts +6 -2
  18. package/src/{private → draft}/useDraftAgentSpec.ts +16 -5
  19. package/src/draftAgentConfig.test.ts +2 -1
  20. package/src/harness.temp.ts +85 -0
  21. package/src/index.ts +43 -7
  22. package/src/plugins/truefoundry-agent-server-adapter/README.md +83 -44
  23. package/src/plugins/truefoundry-agent-server-adapter/chatServer.ts +365 -0
  24. package/src/plugins/truefoundry-agent-server-adapter/cp.test.ts +444 -0
  25. package/src/plugins/truefoundry-agent-server-adapter/cp.ts +482 -0
  26. package/src/plugins/truefoundry-agent-server-adapter/createTrueFoundryAgentUIServer.ts +94 -0
  27. package/src/plugins/truefoundry-agent-server-adapter/guards.ts +1 -1
  28. package/src/plugins/truefoundry-agent-server-adapter/index.ts +20 -351
  29. package/src/plugins/truefoundry-agent-server-adapter/normalizeAgentSpec.test.ts +85 -0
  30. package/src/plugins/truefoundry-agent-server-adapter/normalizeAgentSpec.ts +84 -0
  31. package/src/plugins/truefoundry-agent-server-adapter/types.ts +7 -5
  32. package/src/server/index.ts +29 -0
  33. package/src/server/types.ts +264 -12
  34. package/src/streamTurn.test.ts +27 -27
  35. package/src/streamTurn.ts +2 -2
  36. package/src/truefoundryExtras.ts +4 -1
  37. package/src/truefoundryOwnedSessionsThreadListAdapter.ts +5 -2
  38. package/src/truefoundryThreadListAdapter.test.ts +22 -0
  39. package/src/truefoundryThreadListAdapter.ts +4 -1
  40. package/src/types.ts +1 -2
  41. package/src/useTrueFoundryAgentMessages.test.tsx +262 -2
  42. package/src/useTrueFoundryAgentMessages.ts +284 -176
  43. package/src/useTrueFoundryAgentRuntime.ts +31 -21
  44. package/dist/chunk-Q2SHKMLM.js +0 -270
  45. package/dist/chunk-Q2SHKMLM.js.map +0 -1
  46. /package/src/{private → draft}/useDraftAgentSpec.test.tsx +0 -0
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  mergeEventDelta
4
4
  } from "./chunk-3A2EPLQG.js";
5
5
  import {
6
+ createTrueFoundryAgentUIServer,
6
7
  createTrueFoundryChatServer,
7
8
  getTfyMcpInitServers,
8
9
  getTfyThreadState,
@@ -10,7 +11,7 @@ import {
10
11
  isTfyMcpToolInfo,
11
12
  isTfySystemToolInfo,
12
13
  isTfyToolInfo
13
- } from "./chunk-Q2SHKMLM.js";
14
+ } from "./chunk-CXBZ6WLZ.js";
14
15
 
15
16
  // src/useTrueFoundryAgentRuntime.ts
16
17
  import {
@@ -1533,6 +1534,7 @@ function attachRunningTurn(snapshot, runningTurn) {
1533
1534
  if (runningTurn == null) {
1534
1535
  return snapshot;
1535
1536
  }
1537
+ applyUserToolResponsesToFold(snapshot.fold, runningTurn.input ?? []);
1536
1538
  const pendingUserText = extractTurnUserText(runningTurn.input);
1537
1539
  return replaceSessionSnapshot(snapshot, {
1538
1540
  runningTurn,
@@ -2386,7 +2388,7 @@ function repositoryItemsFromMessages(messages) {
2386
2388
  return items;
2387
2389
  }
2388
2390
 
2389
- // src/private/draftSessionBridge.ts
2391
+ // src/draft/draftSessionBridge.ts
2390
2392
  var DRAFT_SESSION_LAST_UPDATED_AT_HEADER = "x-tfy-session-last-updated-at";
2391
2393
  function createDraftSessionBridge(server) {
2392
2394
  return {
@@ -2409,7 +2411,7 @@ function createDraftSessionBridge(server) {
2409
2411
  };
2410
2412
  }
2411
2413
 
2412
- // src/private/agentSpec.ts
2414
+ // src/draft/agentSpec.ts
2413
2415
  function mergeAgentSpec(base, update) {
2414
2416
  const { model: modelUpdate, ...rest } = update;
2415
2417
  const next = {
@@ -2437,7 +2439,7 @@ function sessionListStartTimestamp() {
2437
2439
  return start.toISOString();
2438
2440
  }
2439
2441
 
2440
- // src/private/truefoundryDraftThreadListAdapter.ts
2442
+ // src/draft/truefoundryDraftThreadListAdapter.ts
2441
2443
  var THREAD_LIST_PAGE_SIZE = 20;
2442
2444
  function createTrueFoundryDraftThreadListAdapter(options) {
2443
2445
  const { server, defaultAgentSpec, getAgentSpec } = options;
@@ -2486,7 +2488,9 @@ function createTrueFoundryDraftThreadListAdapter(options) {
2486
2488
  },
2487
2489
  async unarchive() {
2488
2490
  },
2489
- async delete() {
2491
+ async delete(remoteId) {
2492
+ if (typeof server.deleteSession !== "function") return;
2493
+ await server.deleteSession({ sessionId: remoteId });
2490
2494
  },
2491
2495
  async generateTitle() {
2492
2496
  return new ReadableStream();
@@ -2502,6 +2506,7 @@ var trueFoundryExtras = createRuntimeExtras(
2502
2506
  var EMPTY_DRAFT_EXTRAS = {
2503
2507
  agentSpec: null,
2504
2508
  draftSessionId: void 0,
2509
+ isSpecLoading: false,
2505
2510
  isSpecSyncing: false,
2506
2511
  specError: null,
2507
2512
  updateAgentSpec: () => {
@@ -2566,7 +2571,9 @@ function createTrueFoundryThreadListAdapter(options) {
2566
2571
  },
2567
2572
  async unarchive() {
2568
2573
  },
2569
- async delete() {
2574
+ async delete(remoteId) {
2575
+ if (typeof server.deleteSession !== "function") return;
2576
+ await server.deleteSession({ sessionId: remoteId });
2570
2577
  },
2571
2578
  async generateTitle() {
2572
2579
  return new ReadableStream();
@@ -2597,7 +2604,7 @@ function resolveTrueFoundryAgentRuntimeOptions(options) {
2597
2604
  };
2598
2605
  }
2599
2606
 
2600
- // src/private/useDraftAgentSpec.ts
2607
+ // src/draft/useDraftAgentSpec.ts
2601
2608
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2602
2609
  var SPEC_SYNC_DEBOUNCE_MS = 400;
2603
2610
  function useDraftAgentSpec({
@@ -2609,6 +2616,7 @@ function useDraftAgentSpec({
2609
2616
  }) {
2610
2617
  const enabled = draftBridge != null;
2611
2618
  const [agentSpec, setAgentSpec] = useState(defaultAgentSpec);
2619
+ const [isSpecLoading, setIsSpecLoading] = useState(false);
2612
2620
  const [isSpecSyncing, setIsSpecSyncing] = useState(false);
2613
2621
  const [specError, setSpecError] = useState(null);
2614
2622
  const agentSpecRef = useRef(agentSpec);
@@ -2649,12 +2657,14 @@ function useDraftAgentSpec({
2649
2657
  setAgentSpec(defaultAgentSpec);
2650
2658
  localDirtyRef.current = false;
2651
2659
  setSpecError(null);
2660
+ setIsSpecLoading(false);
2652
2661
  return;
2653
2662
  }
2654
2663
  if (loadedDraftIdRef.current === draftSessionId) {
2655
2664
  return;
2656
2665
  }
2657
2666
  let cancelled = false;
2667
+ setIsSpecLoading(true);
2658
2668
  void (async () => {
2659
2669
  try {
2660
2670
  const loaded = await draftBridge.getDraftAgentSpec(draftSessionId);
@@ -2666,19 +2676,23 @@ function useDraftAgentSpec({
2666
2676
  scheduleSpecSyncRef.current?.(draftSessionId, agentSpecRef.current);
2667
2677
  localDirtyRef.current = false;
2668
2678
  setSpecError(null);
2679
+ setIsSpecLoading(false);
2669
2680
  return;
2670
2681
  }
2671
2682
  setAgentSpec(loaded);
2672
2683
  setSpecError(null);
2684
+ setIsSpecLoading(false);
2673
2685
  } catch (error) {
2674
2686
  if (!cancelled) {
2675
2687
  onError?.(error);
2676
2688
  setSpecError(error);
2689
+ setIsSpecLoading(false);
2677
2690
  }
2678
2691
  }
2679
2692
  })();
2680
2693
  return () => {
2681
2694
  cancelled = true;
2695
+ setIsSpecLoading(false);
2682
2696
  };
2683
2697
  }, [defaultAgentSpec, draftBridge, draftSessionId, enabled, onError]);
2684
2698
  const flushSpecSync = useCallback(
@@ -2781,6 +2795,7 @@ function useDraftAgentSpec({
2781
2795
  () => ({
2782
2796
  agentSpec: enabled ? agentSpec : null,
2783
2797
  draftSessionId: enabled ? draftSessionId : void 0,
2798
+ isSpecLoading: enabled ? isSpecLoading : false,
2784
2799
  isSpecSyncing: enabled ? isSpecSyncing : false,
2785
2800
  specError: enabled ? specError : null,
2786
2801
  updateAgentSpec,
@@ -2790,6 +2805,7 @@ function useDraftAgentSpec({
2790
2805
  agentSpec,
2791
2806
  draftSessionId,
2792
2807
  enabled,
2808
+ isSpecLoading,
2793
2809
  isSpecSyncing,
2794
2810
  specError,
2795
2811
  takeTurnHeaderTimestamp,
@@ -2873,7 +2889,7 @@ async function* streamTurnContent(server, sessionId, foldState, options, abortSi
2873
2889
  turnIdNotified = true;
2874
2890
  }
2875
2891
  };
2876
- const stream = server.prepareAndExecuteTurn({
2892
+ const stream = server.createTurn({
2877
2893
  sessionId,
2878
2894
  input: buildTurnInput(options),
2879
2895
  previousTurnId: options.previousTurnId ?? "auto",
@@ -3058,6 +3074,7 @@ function useTrueFoundryAgentMessages({
3058
3074
  server,
3059
3075
  sessionId,
3060
3076
  isMain,
3077
+ isInitialSession,
3061
3078
  listEventsConcurrency,
3062
3079
  onError,
3063
3080
  initializeSession,
@@ -3066,7 +3083,9 @@ function useTrueFoundryAgentMessages({
3066
3083
  }) {
3067
3084
  const [snapshot, setSnapshot] = useState2(createEmptySessionSnapshot);
3068
3085
  const [isRunning, setIsRunning] = useState2(false);
3069
- const [isLoading, setIsLoading] = useState2(false);
3086
+ const [isLoading, setIsLoading] = useState2(
3087
+ sessionId != null && (isMain !== false || isInitialSession === true)
3088
+ );
3070
3089
  const [isLoadingOlderHistory, setIsLoadingOlderHistory] = useState2(false);
3071
3090
  const [loadRetryTrigger, setLoadRetryTrigger] = useState2(0);
3072
3091
  const snapshotRef = useRef2(snapshot);
@@ -3087,6 +3106,8 @@ function useTrueFoundryAgentMessages({
3087
3106
  const loadGenerationRef = useRef2(0);
3088
3107
  const streamGenerationRef = useRef2(0);
3089
3108
  const lazilyCreatedSessionIdRef = useRef2(void 0);
3109
+ const initialLoadStartedForRef = useRef2(void 0);
3110
+ const skipInitialPromotionLoadForRef = useRef2(void 0);
3090
3111
  const projectOptions = useMemo2(
3091
3112
  () => ({
3092
3113
  getCreatedAt: (messageId, fallback) => {
@@ -3197,12 +3218,24 @@ function useTrueFoundryAgentMessages({
3197
3218
  [onError]
3198
3219
  );
3199
3220
  const load = useCallback2(async () => {
3221
+ void loadRetryTrigger;
3200
3222
  if (sessionId == null) {
3201
3223
  createdAtByMessageIdRef.current = /* @__PURE__ */ new Map();
3202
3224
  setSnapshot(createEmptySessionSnapshot());
3203
3225
  return;
3204
3226
  }
3205
- if (isMain === false) return;
3227
+ const isEarlyInitialLoad = isMain === false && isInitialSession === true && initialLoadStartedForRef.current !== sessionId;
3228
+ if (isMain === false) {
3229
+ if (!isEarlyInitialLoad) return;
3230
+ initialLoadStartedForRef.current = sessionId;
3231
+ skipInitialPromotionLoadForRef.current = sessionId;
3232
+ } else if (isMain === true && skipInitialPromotionLoadForRef.current === sessionId) {
3233
+ skipInitialPromotionLoadForRef.current = void 0;
3234
+ return;
3235
+ }
3236
+ if (isInitialSession === true) {
3237
+ initialLoadStartedForRef.current = sessionId;
3238
+ }
3206
3239
  if (lazilyCreatedSessionIdRef.current != null && sessionId !== lazilyCreatedSessionIdRef.current) {
3207
3240
  lazilyCreatedSessionIdRef.current = void 0;
3208
3241
  }
@@ -3211,6 +3244,7 @@ function useTrueFoundryAgentMessages({
3211
3244
  }
3212
3245
  const generation = ++loadGenerationRef.current;
3213
3246
  ++streamGenerationRef.current;
3247
+ setIsRunning(false);
3214
3248
  abortControllerRef.current?.abort();
3215
3249
  loadOlderInflightRef.current = null;
3216
3250
  createdAtByMessageIdRef.current = /* @__PURE__ */ new Map();
@@ -3257,6 +3291,10 @@ function useTrueFoundryAgentMessages({
3257
3291
  }
3258
3292
  } catch (error) {
3259
3293
  if (generation === loadGenerationRef.current) {
3294
+ if (isEarlyInitialLoad) {
3295
+ initialLoadStartedForRef.current = void 0;
3296
+ skipInitialPromotionLoadForRef.current = void 0;
3297
+ }
3260
3298
  onErrorRef.current?.(error);
3261
3299
  }
3262
3300
  throw error;
@@ -3265,134 +3303,178 @@ function useTrueFoundryAgentMessages({
3265
3303
  setIsLoading(false);
3266
3304
  }
3267
3305
  }
3268
- }, [server, runStream, sessionId, loadRetryTrigger, isMain]);
3306
+ }, [
3307
+ server,
3308
+ runStream,
3309
+ sessionId,
3310
+ loadRetryTrigger,
3311
+ isMain,
3312
+ isInitialSession
3313
+ ]);
3269
3314
  useEffect2(() => {
3270
3315
  void load().catch(() => void 0);
3271
3316
  }, [load]);
3272
3317
  const sendTurn = useCallback2(
3273
3318
  async (options) => {
3274
- let activeSessionId = sessionId;
3275
- if (activeSessionId == null) {
3276
- if (initializeSessionRef.current == null) {
3277
- throw new Error("Cannot send a turn without an active session.");
3319
+ let gatewayTurnAccepted = false;
3320
+ let pendingUserWasSet = false;
3321
+ let runStreamStarted = false;
3322
+ let pendingUserTurnId;
3323
+ try {
3324
+ let activeSessionId = sessionId;
3325
+ if (activeSessionId == null) {
3326
+ if (initializeSessionRef.current == null) {
3327
+ throw new Error("Cannot send a turn without an active session.");
3328
+ }
3329
+ const { remoteId } = await initializeSessionRef.current();
3330
+ activeSessionId = remoteId;
3331
+ lazilyCreatedSessionIdRef.current = remoteId;
3278
3332
  }
3279
- const { remoteId } = await initializeSessionRef.current();
3280
- activeSessionId = remoteId;
3281
- lazilyCreatedSessionIdRef.current = remoteId;
3282
- }
3283
- const conversationSessionId = await resolveActiveSessionId(
3284
- activeSessionId,
3285
- resolveConversationSessionIdRef.current
3286
- );
3287
- const turnHeaders = await getTurnHeadersRef.current?.();
3288
- const streamHeaders = turnHeaders != null ? { headers: turnHeaders } : {};
3289
- const isContinuation = "inputs" in options || "resumeMcpAuth" in options && options.resumeMcpAuth === true;
3290
- const continuationTurnId = snapshotRef.current.activeStream?.turnId;
3291
- const turnId = isContinuation && continuationTurnId != null ? continuationTurnId : generateId();
3292
- const isFirstTurnInSession = "userMessage" in options && options.previousTurnId === void 0 && snapshotRef.current.turns.length === 0 && snapshotRef.current.pendingUser == null && snapshotRef.current.activeStream == null;
3293
- const turnIdRef = { current: turnId };
3294
- if ("inputs" in options) {
3295
- applyUserToolResponsesToFold(
3296
- snapshotRef.current.fold,
3297
- options.inputs
3298
- );
3299
- }
3300
- const branchBase = "userMessage" in options ? options.branchFromSnapshot : void 0;
3301
- let groupRootBaseline;
3302
- if (branchBase != null && "userMessage" in options) {
3303
- const rootBucket = branchBase.fold.threads.get(ROOT_THREAD_ID);
3304
- groupRootBaseline = [...rootBucket?.modelMessageIds ?? []];
3305
- const nextSnapshot = replaceSessionSnapshot(branchBase, {
3306
- pendingUser: {
3307
- turnId,
3308
- content: options.userMessage,
3309
- createdAt: /* @__PURE__ */ new Date()
3310
- },
3311
- activeStream: void 0,
3312
- groupRootBaseline
3313
- });
3314
- snapshotRef.current = nextSnapshot;
3315
- setSnapshot(nextSnapshot);
3316
- } else {
3317
- setSnapshot(
3318
- (prev) => commitActiveStream(
3319
- prev,
3320
- "inputs" in options ? options.inputs : void 0
3321
- )
3333
+ const conversationSessionId = await resolveActiveSessionId(
3334
+ activeSessionId,
3335
+ resolveConversationSessionIdRef.current
3322
3336
  );
3323
- if ("userMessage" in options) {
3324
- const rootBucket = snapshotRef.current.fold.threads.get(ROOT_THREAD_ID);
3337
+ const turnHeaders = await getTurnHeadersRef.current?.();
3338
+ const streamHeaders = turnHeaders != null ? { headers: turnHeaders } : {};
3339
+ const isContinuation = "inputs" in options || "resumeMcpAuth" in options && options.resumeMcpAuth === true;
3340
+ const continuationTurnId = snapshotRef.current.activeStream?.turnId;
3341
+ const turnId = isContinuation && continuationTurnId != null ? continuationTurnId : generateId();
3342
+ const isFirstTurnInSession = "userMessage" in options && options.previousTurnId === void 0 && snapshotRef.current.turns.length === 0 && snapshotRef.current.pendingUser == null && snapshotRef.current.activeStream == null;
3343
+ const turnIdRef = { current: turnId };
3344
+ if ("inputs" in options) {
3345
+ applyUserToolResponsesToFold(
3346
+ snapshotRef.current.fold,
3347
+ options.inputs
3348
+ );
3349
+ }
3350
+ const branchBase = "userMessage" in options ? options.branchFromSnapshot : void 0;
3351
+ let groupRootBaseline;
3352
+ if (branchBase != null && "userMessage" in options) {
3353
+ const rootBucket = branchBase.fold.threads.get(ROOT_THREAD_ID);
3325
3354
  groupRootBaseline = [...rootBucket?.modelMessageIds ?? []];
3326
- setSnapshot((prev) => {
3327
- const next = replaceSessionSnapshot(prev, {
3328
- pendingUser: {
3329
- turnId,
3330
- content: options.userMessage,
3331
- createdAt: /* @__PURE__ */ new Date()
3332
- },
3333
- activeStream: void 0,
3334
- groupRootBaseline
3335
- });
3336
- snapshotRef.current = next;
3337
- return next;
3355
+ const nextSnapshot = replaceSessionSnapshot(branchBase, {
3356
+ pendingUser: {
3357
+ turnId,
3358
+ content: options.userMessage,
3359
+ createdAt: /* @__PURE__ */ new Date()
3360
+ },
3361
+ activeStream: void 0,
3362
+ groupRootBaseline
3338
3363
  });
3364
+ snapshotRef.current = nextSnapshot;
3365
+ setSnapshot(nextSnapshot);
3366
+ pendingUserWasSet = true;
3367
+ pendingUserTurnId = turnId;
3339
3368
  } else {
3340
- groupRootBaseline = snapshotRef.current.groupRootBaseline ?? computeGroupRootBaseline(snapshotRef.current.turns);
3341
- }
3342
- }
3343
- await runStream(
3344
- (signal) => {
3345
- if ("inputs" in options) {
3346
- return streamTurnContent(
3347
- server,
3348
- conversationSessionId,
3349
- snapshotRef.current.fold,
3350
- { inputs: options.inputs, ...streamHeaders },
3351
- signal,
3352
- groupRootBaseline
3353
- );
3369
+ setSnapshot(
3370
+ (prev) => commitActiveStream(
3371
+ prev,
3372
+ "inputs" in options ? options.inputs : void 0
3373
+ )
3374
+ );
3375
+ if ("userMessage" in options) {
3376
+ const rootBucket = snapshotRef.current.fold.threads.get(ROOT_THREAD_ID);
3377
+ groupRootBaseline = [...rootBucket?.modelMessageIds ?? []];
3378
+ setSnapshot((prev) => {
3379
+ const next = replaceSessionSnapshot(prev, {
3380
+ pendingUser: {
3381
+ turnId,
3382
+ content: options.userMessage,
3383
+ createdAt: /* @__PURE__ */ new Date()
3384
+ },
3385
+ activeStream: void 0,
3386
+ groupRootBaseline
3387
+ });
3388
+ snapshotRef.current = next;
3389
+ return next;
3390
+ });
3391
+ pendingUserWasSet = true;
3392
+ pendingUserTurnId = turnId;
3393
+ } else {
3394
+ groupRootBaseline = snapshotRef.current.groupRootBaseline ?? computeGroupRootBaseline(snapshotRef.current.turns);
3354
3395
  }
3355
- if ("resumeMcpAuth" in options) {
3396
+ }
3397
+ runStreamStarted = true;
3398
+ await runStream(
3399
+ (signal) => {
3400
+ if ("inputs" in options) {
3401
+ return streamTurnContent(
3402
+ server,
3403
+ conversationSessionId,
3404
+ snapshotRef.current.fold,
3405
+ { inputs: options.inputs, ...streamHeaders },
3406
+ signal,
3407
+ groupRootBaseline
3408
+ );
3409
+ }
3410
+ if ("resumeMcpAuth" in options) {
3411
+ return streamTurnContent(
3412
+ server,
3413
+ conversationSessionId,
3414
+ snapshotRef.current.fold,
3415
+ { resumeMcpAuth: true, ...streamHeaders },
3416
+ signal,
3417
+ groupRootBaseline
3418
+ );
3419
+ }
3356
3420
  return streamTurnContent(
3357
3421
  server,
3358
3422
  conversationSessionId,
3359
3423
  snapshotRef.current.fold,
3360
- { resumeMcpAuth: true, ...streamHeaders },
3424
+ {
3425
+ userMessage: options.userMessage,
3426
+ ...options.previousTurnId !== void 0 ? { previousTurnId: options.previousTurnId ?? "none" } : isFirstTurnInSession ? { previousTurnId: "none" } : {},
3427
+ ...streamHeaders
3428
+ },
3361
3429
  signal,
3362
- groupRootBaseline
3430
+ groupRootBaseline,
3431
+ // Rename the optimistic local ID to the gateway turn ID
3432
+ // so that edit/retry can resolve the turn via the gateway.
3433
+ (gatewayTurnId) => {
3434
+ const oldId = turnIdRef.current;
3435
+ gatewayTurnAccepted = true;
3436
+ if (gatewayTurnId === oldId) return;
3437
+ turnIdRef.current = gatewayTurnId;
3438
+ const renamePendingUser = (prev) => {
3439
+ if (prev.pendingUser?.turnId !== oldId) return prev;
3440
+ return replaceSessionSnapshot(prev, {
3441
+ pendingUser: { ...prev.pendingUser, turnId: gatewayTurnId }
3442
+ });
3443
+ };
3444
+ snapshotRef.current = renamePendingUser(snapshotRef.current);
3445
+ setSnapshot(renamePendingUser);
3446
+ }
3363
3447
  );
3448
+ },
3449
+ turnIdRef,
3450
+ isContinuation
3451
+ );
3452
+ } catch (error) {
3453
+ if ("userMessage" in options && !gatewayTurnAccepted) {
3454
+ const branchRollbackSnapshot = options.branchRollbackSnapshot;
3455
+ const canRestoreBranch = branchRollbackSnapshot != null && (snapshotRef.current === options.branchFromSnapshot || snapshotRef.current.pendingUser?.turnId === pendingUserTurnId);
3456
+ if (canRestoreBranch) {
3457
+ snapshotRef.current = branchRollbackSnapshot;
3458
+ setSnapshot(branchRollbackSnapshot);
3459
+ } else if (pendingUserWasSet) {
3460
+ const clearPendingUser = (previous) => {
3461
+ if (previous.pendingUser?.turnId !== pendingUserTurnId) {
3462
+ return previous;
3463
+ }
3464
+ return replaceSessionSnapshot(previous, {
3465
+ pendingUser: void 0
3466
+ });
3467
+ };
3468
+ snapshotRef.current = clearPendingUser(snapshotRef.current);
3469
+ setSnapshot(clearPendingUser);
3364
3470
  }
3365
- return streamTurnContent(
3366
- server,
3367
- conversationSessionId,
3368
- snapshotRef.current.fold,
3369
- {
3370
- userMessage: options.userMessage,
3371
- ...options.previousTurnId !== void 0 ? { previousTurnId: options.previousTurnId ?? "none" } : isFirstTurnInSession ? { previousTurnId: "none" } : {},
3372
- ...streamHeaders
3373
- },
3374
- signal,
3375
- groupRootBaseline,
3376
- // Rename the optimistic local ID to the gateway turn ID
3377
- // so that edit/retry can resolve the turn via the gateway.
3378
- (gatewayTurnId) => {
3379
- const oldId = turnIdRef.current;
3380
- if (gatewayTurnId === oldId) return;
3381
- turnIdRef.current = gatewayTurnId;
3382
- const renamePendingUser = (prev) => {
3383
- if (prev.pendingUser?.turnId !== oldId) return prev;
3384
- return replaceSessionSnapshot(prev, {
3385
- pendingUser: { ...prev.pendingUser, turnId: gatewayTurnId }
3386
- });
3387
- };
3388
- snapshotRef.current = renamePendingUser(snapshotRef.current);
3389
- setSnapshot(renamePendingUser);
3390
- }
3391
- );
3392
- },
3393
- turnIdRef,
3394
- isContinuation
3395
- );
3471
+ options.onPreTurnFailure?.();
3472
+ }
3473
+ if (!runStreamStarted) {
3474
+ onErrorRef.current?.(error);
3475
+ }
3476
+ throw error;
3477
+ }
3396
3478
  },
3397
3479
  [server, runStream, sessionId]
3398
3480
  );
@@ -3422,7 +3504,7 @@ function useTrueFoundryAgentMessages({
3422
3504
  }
3423
3505
  const inputs = collectRequiredActionInputs(paused);
3424
3506
  if (inputs.length > 0) {
3425
- void sendTurn({ inputs }).catch((error) => onErrorRef.current?.(error));
3507
+ void sendTurn({ inputs }).catch(() => void 0);
3426
3508
  }
3427
3509
  },
3428
3510
  [projectOptions, sendTurn]
@@ -3483,35 +3565,44 @@ function useTrueFoundryAgentMessages({
3483
3565
  }, [runStream, server]);
3484
3566
  const branchFromTurn = useCallback2(
3485
3567
  async (turnId, userMessage) => {
3486
- let activeSessionId = sessionId;
3487
- if (activeSessionId == null) {
3488
- throw new Error("Cannot branch from a turn without an active session.");
3568
+ let committed;
3569
+ let previousTurnId;
3570
+ let rewound;
3571
+ try {
3572
+ let activeSessionId = sessionId;
3573
+ if (activeSessionId == null) {
3574
+ throw new Error("Cannot branch from a turn without an active session.");
3575
+ }
3576
+ committed = commitActiveStream(snapshotRef.current);
3577
+ setSnapshot(committed);
3578
+ await cancel();
3579
+ const conversationSessionId = await resolveActiveSessionId(
3580
+ activeSessionId,
3581
+ resolveConversationSessionIdRef.current
3582
+ );
3583
+ previousTurnId = await resolveGatewayBranchPreviousTurnIdForTurn(
3584
+ server,
3585
+ conversationSessionId,
3586
+ turnId
3587
+ );
3588
+ rewound = await buildSnapshotBeforeTurn(
3589
+ server,
3590
+ conversationSessionId,
3591
+ turnId,
3592
+ listEventsConcurrency
3593
+ );
3594
+ createdAtByMessageIdRef.current = /* @__PURE__ */ new Map();
3595
+ snapshotRef.current = rewound;
3596
+ setSnapshot(rewound);
3597
+ } catch (error) {
3598
+ onErrorRef.current?.(error);
3599
+ throw error;
3489
3600
  }
3490
- const committed = commitActiveStream(snapshotRef.current);
3491
- setSnapshot(committed);
3492
- await cancel();
3493
- const conversationSessionId = await resolveActiveSessionId(
3494
- activeSessionId,
3495
- resolveConversationSessionIdRef.current
3496
- );
3497
- const previousTurnId = await resolveGatewayBranchPreviousTurnIdForTurn(
3498
- server,
3499
- conversationSessionId,
3500
- turnId
3501
- );
3502
- const rewound = await buildSnapshotBeforeTurn(
3503
- server,
3504
- conversationSessionId,
3505
- turnId,
3506
- listEventsConcurrency
3507
- );
3508
- createdAtByMessageIdRef.current = /* @__PURE__ */ new Map();
3509
- snapshotRef.current = rewound;
3510
- setSnapshot(rewound);
3511
3601
  await sendTurn({
3512
3602
  userMessage,
3513
3603
  previousTurnId,
3514
- branchFromSnapshot: rewound
3604
+ branchFromSnapshot: rewound,
3605
+ branchRollbackSnapshot: committed
3515
3606
  });
3516
3607
  },
3517
3608
  [
@@ -3527,7 +3618,9 @@ function useTrueFoundryAgentMessages({
3527
3618
  const committed = commitActiveStream(snapshotRef.current);
3528
3619
  const originalInput = resolveTurnInput(committed, turnId);
3529
3620
  if (originalInput == null) {
3530
- throw new Error(`Turn ${turnId} not found in session snapshot`);
3621
+ const error = new Error(`Turn ${turnId} not found in session snapshot`);
3622
+ onErrorRef.current?.(error);
3623
+ throw error;
3531
3624
  }
3532
3625
  const userMessage = extractTurnUserMessageContent(originalInput);
3533
3626
  await branchFromTurn(turnId, userMessage);
@@ -3539,7 +3632,9 @@ function useTrueFoundryAgentMessages({
3539
3632
  const committed = commitActiveStream(snapshotRef.current);
3540
3633
  const originalInput = resolveTurnInput(committed, turnId);
3541
3634
  if (originalInput == null) {
3542
- throw new Error(`Turn ${turnId} not found in session snapshot`);
3635
+ const error = new Error(`Turn ${turnId} not found in session snapshot`);
3636
+ onErrorRef.current?.(error);
3637
+ throw error;
3543
3638
  }
3544
3639
  const userMessage = buildEditedUserMessageContent(
3545
3640
  editedText,
@@ -3638,6 +3733,7 @@ function useTrueFoundryAgentRuntimeImpl(options, pendingAgentSpecRef) {
3638
3733
  const isMain = useAuiState(
3639
3734
  (state) => state.threads.mainThreadId === state.threadListItem.id
3640
3735
  );
3736
+ const isInitialSession = sessionId != null && sessionId === options.initialSessionId;
3641
3737
  const draftSpec = useDraftAgentSpec({
3642
3738
  draftSessionId,
3643
3739
  draftBridge: draftBridgeRef.current,
@@ -3683,6 +3779,7 @@ function useTrueFoundryAgentRuntimeImpl(options, pendingAgentSpecRef) {
3683
3779
  server,
3684
3780
  sessionId,
3685
3781
  isMain,
3782
+ isInitialSession,
3686
3783
  listEventsConcurrency,
3687
3784
  onError,
3688
3785
  initializeSession,
@@ -3708,20 +3805,16 @@ function useTrueFoundryAgentRuntimeImpl(options, pendingAgentSpecRef) {
3708
3805
  const downloadSandboxFile = useCallback3(
3709
3806
  async (path) => {
3710
3807
  if (server.downloadSandboxFile == null) {
3711
- const error = new Error(
3808
+ throw new Error(
3712
3809
  "Downloading a sandbox file requires AgentChatServer.downloadSandboxFile."
3713
3810
  );
3714
- onError?.(error);
3715
- throw error;
3716
3811
  }
3717
3812
  if (sandboxId == null) {
3718
- const error = new Error("No sandbox is available yet for this session.");
3719
- onError?.(error);
3720
- throw error;
3813
+ throw new Error("No sandbox is available yet for this session.");
3721
3814
  }
3722
3815
  return await server.downloadSandboxFile(sandboxId, { path });
3723
3816
  },
3724
- [server, sandboxId, onError]
3817
+ [server, sandboxId]
3725
3818
  );
3726
3819
  const draftExtras = useMemo3(() => {
3727
3820
  if (agent.mode !== "draft") {
@@ -3730,6 +3823,7 @@ function useTrueFoundryAgentRuntimeImpl(options, pendingAgentSpecRef) {
3730
3823
  return {
3731
3824
  agentSpec: draftSpec.agentSpec,
3732
3825
  draftSessionId: draftSpec.draftSessionId,
3826
+ isSpecLoading: draftSpec.isSpecLoading,
3733
3827
  isSpecSyncing: draftSpec.isSpecSyncing,
3734
3828
  specError: draftSpec.specError,
3735
3829
  updateAgentSpec: draftSpec.updateAgentSpec
@@ -3750,9 +3844,8 @@ function useTrueFoundryAgentRuntimeImpl(options, pendingAgentSpecRef) {
3750
3844
  resumeMcpAuth,
3751
3845
  downloadSandboxFile,
3752
3846
  cancel,
3753
- resetFromTurn: (turnId) => resetFromTurn(turnId).catch((error) => {
3754
- onError?.(error);
3755
- }),
3847
+ // resetFromTurn/branchFromTurn/sendTurn already report via onError.
3848
+ resetFromTurn: (turnId) => resetFromTurn(turnId).catch(() => void 0),
3756
3849
  reload: retryLoad,
3757
3850
  hasOlderHistory,
3758
3851
  isLoadingOlderHistory,
@@ -3777,7 +3870,19 @@ function useTrueFoundryAgentRuntimeImpl(options, pendingAgentSpecRef) {
3777
3870
  await sendTurn({ resumeMcpAuth: true });
3778
3871
  return;
3779
3872
  }
3780
- await sendTurn({ userMessage: buildUserMessageContent(message) });
3873
+ const userMessage = buildUserMessageContent(message);
3874
+ await sendTurn({
3875
+ userMessage,
3876
+ // The composer clears before onNew runs. Restore its text only when
3877
+ // the turn failed before turn.created registered it in the backend.
3878
+ onPreTurnFailure: () => {
3879
+ const text = userMessageContentToText(userMessage);
3880
+ const composer = aui.thread().composer();
3881
+ if (text && !composer.getState().text.trim()) {
3882
+ composer.setText(text);
3883
+ }
3884
+ }
3885
+ });
3781
3886
  },
3782
3887
  onCancel: async () => {
3783
3888
  await cancel();
@@ -3795,12 +3900,7 @@ function useTrueFoundryAgentRuntimeImpl(options, pendingAgentSpecRef) {
3795
3900
  }
3796
3901
  const turnId = parseTurnIdFromMessageId(sourceId);
3797
3902
  const editedText = extractEditedText(message);
3798
- try {
3799
- await editFromTurn(turnId, editedText);
3800
- } catch (error) {
3801
- onError?.(error);
3802
- throw error;
3803
- }
3903
+ await editFromTurn(turnId, editedText);
3804
3904
  }
3805
3905
  });
3806
3906
  }
@@ -3887,7 +3987,9 @@ function createTrueFoundryOwnedSessionsThreadListAdapter(options) {
3887
3987
  },
3888
3988
  async unarchive() {
3889
3989
  },
3890
- async delete() {
3990
+ async delete(remoteId) {
3991
+ if (typeof server.deleteSession !== "function") return;
3992
+ await server.deleteSession({ sessionId: remoteId });
3891
3993
  },
3892
3994
  async generateTitle() {
3893
3995
  return new ReadableStream();
@@ -4049,6 +4151,7 @@ export {
4049
4151
  collectResponseInputs,
4050
4152
  convertTurnsToThreadMessages,
4051
4153
  createDraftSessionBridge,
4154
+ createTrueFoundryAgentUIServer,
4052
4155
  createTrueFoundryChatServer,
4053
4156
  createTrueFoundryDraftThreadListAdapter,
4054
4157
  createTrueFoundryOwnedSessionsThreadListAdapter,