@truefoundry/assistant-ui-runtime 0.1.13 → 0.1.15

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.
@@ -1,6 +1,6 @@
1
1
  import { AgentSessionClient } from 'truefoundry-gateway-sdk/agents';
2
2
  import { PrivateAgentSessionClient } from 'truefoundry-gateway-sdk/agents/private';
3
- import { A as AgentSpec, K as CreateSessionRequest, W as ListSessionsParams, S as Session, d as Turn, az as TurnState, a as AgentChatServer, aH as UpdateSessionRequest, i as AgentSelectorEntry, E as ConnectorSelectorEntry, a6 as ModelSelectorEntry, au as SkillSelectorEntry, g as AgentBuilderServer } from '../../types-BE-F3vCD.js';
3
+ import { A as AgentSpec, N as CreateSessionRequest, Y as ListSessionsParams, S as Session, d as Turn, aC as TurnState, a as AgentChatServer, aK as UpdateSessionRequest, k as AgentSelectorEntry, G as ConnectorSelectorEntry, a8 as ModelSelectorEntry, ax as SkillSelectorEntry, g as AgentBuilderServer } from '../../types-VU3Fc17W.js';
4
4
  import { TruefoundryGatewayApi } from 'truefoundry-gateway-sdk';
5
5
 
6
6
  /**
@@ -29,8 +29,7 @@ type RuntimeSkillMount = NonNullable<NonNullable<AgentSpec["skills"]>[number]>;
29
29
  type RuntimeMcpServerMount = NonNullable<NonNullable<AgentSpec["mcpServers"]>[number]>;
30
30
  type TfySkillMount = RuntimeSkillMount & TruefoundryGatewayApi.SkillMount;
31
31
  type TfyMcpServerMount = RuntimeMcpServerMount & TruefoundryGatewayApi.McpServer;
32
- interface TfyAgentSpec extends AgentSpec<TruefoundryGatewayApi.Model, TfySkillMount, TfyMcpServerMount> {
33
- config?: TruefoundryGatewayApi.RuntimeConfig;
32
+ interface TfyAgentSpec extends AgentSpec<TruefoundryGatewayApi.Model, TfySkillMount, TfyMcpServerMount, TruefoundryGatewayApi.RuntimeConfig> {
34
33
  responseFormat?: TruefoundryGatewayApi.ResponseFormat;
35
34
  messages?: TruefoundryGatewayApi.AgentSpecUserMessage[];
36
35
  }
@@ -7,7 +7,7 @@ import {
7
7
  isTfyMcpToolInfo,
8
8
  isTfySystemToolInfo,
9
9
  isTfyToolInfo
10
- } from "../../chunk-XYGEKRA2.js";
10
+ } from "../../chunk-S4SH2KT2.js";
11
11
  export {
12
12
  createTrueFoundryAgentUIServer,
13
13
  createTrueFoundryChatServer,
@@ -1,5 +1,5 @@
1
- import { aC as TurnStreamingEvent, aJ as DeltaEvents, T as TurnEvent } from '../types-BE-F3vCD.js';
2
- export { aK as ActionRequiredEvent, f as AgentBuilderCapabilitiesResponse, g as AgentBuilderServer, a as AgentChatServer, aL as AgentInfo, h as AgentLibraryEntry, aM as AgentParent, i as AgentSelectorEntry, j as AgentSkill, A as AgentSpec, k as AgentUIServer, l as AgentUIServerPort, m as ApprovalDecision, n as AuthenticateConnectorRequest, C as CatalogServer, aN as ChunkDeltaToolCall, o as ConnectorAuth, p as ConnectorAuthApiKey, q as ConnectorAuthNone, r as ConnectorAuthOAuth, s as ConnectorAuthPublic, t as ConnectorAuthPublicApiKey, u as ConnectorAuthPublicNone, v as ConnectorAuthPublicOAuth, w as ConnectorAuthType, x as ConnectorAuthenticationResult, y as ConnectorBase, z as ConnectorCatalogEntry, B as ConnectorCatalogServer, D as ConnectorConfigBase, E as ConnectorSelectorEntry, F as ConnectorState, G as CreateConnectorRequest, H as CreateModelProviderRequest, I as CreateSandboxProviderRequest, J as CreateSandboxRequest, K as CreateSessionRequest, L as CreateSkillRequest, N as CreateSkillRequestBase, O as DefinedSkill, P as GithubSkill, Q as ImportGithubSkillRequest, R as ListResult, V as ListSessionsOrder, W as ListSessionsParams, M as McpAuthRequiredEvent, aO as McpInitializeEvent, aP as McpServerAuthInfo, X as McpServerMount, Y as Model, Z as ModelCatalogServer, _ as ModelEntry, aQ as ModelMessageContentPart, aR as ModelMessageDeltaEvent, $ as ModelMessageEvent, a0 as ModelParams, a1 as ModelProperties, a2 as ModelProviderBase, a3 as ModelProviderCatalogEntry, a4 as ModelProviderConfigBase, a5 as ModelSelection, a6 as ModelSelectorEntry, a7 as PageParams, a8 as PreviousTurnIdInput, a9 as ProviderEntry, aa as ProviderType, ab as RegistrySkill, ac as SandboxBase, ad as SandboxCatalogEntry, ae as SandboxCatalogServer, af as SandboxConfig, ag as SandboxCreatedEvent, ah as SandboxProviderBase, ai as SandboxProviderCatalogEntry, aj as SandboxProviderConfig, ak as SaveAgentRequest, al as SearchAgentSelectorParams, am as SearchAgentsParams, an as SelectRegistrySkillRequest, S as Session, ao as SessionEventItem, ap as SkillBase, aq as SkillCatalogEntry, ar as SkillCatalogServer, as as SkillConfigBase, at as SkillMount, au as SkillSelectorEntry, b as ThreadCreatedEvent, aS as ThreadDoneEvent, av as ToolApprovalRequiredEvent, aw as ToolBase, ax as ToolCall, aT as ToolCallFunction, aU as ToolCallRef, aV as ToolInfo, aW as ToolResponseEvent, ay as ToolResponseRequiredEvent, d as Turn, aX as TurnCreatedEvent, aY as TurnDoneEvent, e as TurnInputItem, az as TurnState, aZ as TurnStateCancelled, aA as TurnStateDone, a_ as TurnStateError, a$ as TurnStateRunning, aB as TurnStreamData, aD as UpdateConnectorRequest, aE as UpdateModelProviderRequest, aF as UpdateSandboxProviderRequest, aG as UpdateSandboxRequest, aH as UpdateSessionRequest, aI as UserMessage, b0 as UserMessageContent, c as UserToolApprovalEvent, U as UserToolResponseEvent } from '../types-BE-F3vCD.js';
1
+ import { aF as TurnStreamingEvent, aM as DeltaEvents, T as TurnEvent } from '../types-VU3Fc17W.js';
2
+ export { aN as ActionRequiredEvent, f as AgentBuilderCapabilitiesResponse, g as AgentBuilderServer, h as AgentCapabilityConfig, a as AgentChatServer, aO as AgentInfo, i as AgentLibraryEntry, aP as AgentParent, j as AgentRuntimeConfig, k as AgentSelectorEntry, l as AgentSkill, A as AgentSpec, m as AgentUIServer, n as AgentUIServerPort, o as ApprovalDecision, p as AuthenticateConnectorRequest, C as CatalogServer, aQ as ChunkDeltaToolCall, q as ConnectorAuth, r as ConnectorAuthApiKey, s as ConnectorAuthNone, t as ConnectorAuthOAuth, u as ConnectorAuthPublic, v as ConnectorAuthPublicApiKey, w as ConnectorAuthPublicNone, x as ConnectorAuthPublicOAuth, y as ConnectorAuthType, z as ConnectorAuthenticationResult, B as ConnectorBase, D as ConnectorCatalogEntry, E as ConnectorCatalogServer, F as ConnectorConfigBase, G as ConnectorSelectorEntry, H as ConnectorState, I as CreateConnectorRequest, J as CreateModelProviderRequest, K as CreateSandboxProviderRequest, L as CreateSandboxRequest, N as CreateSessionRequest, O as CreateSkillRequest, P as CreateSkillRequestBase, Q as DefinedSkill, R as GithubSkill, V as ImportGithubSkillRequest, W as ListResult, X as ListSessionsOrder, Y as ListSessionsParams, M as McpAuthRequiredEvent, aR as McpInitializeEvent, aS as McpServerAuthInfo, Z as McpServerMount, _ as Model, $ as ModelCatalogServer, a0 as ModelEntry, aT as ModelMessageContentPart, aU as ModelMessageDeltaEvent, a1 as ModelMessageEvent, a2 as ModelParams, a3 as ModelProperties, a4 as ModelProviderBase, a5 as ModelProviderCatalogEntry, a6 as ModelProviderConfigBase, a7 as ModelSelection, a8 as ModelSelectorEntry, a9 as PageParams, aa as PreviousTurnIdInput, ab as ProviderEntry, ac as ProviderType, ad as RegistrySkill, ae as SandboxBase, af as SandboxCatalogEntry, ag as SandboxCatalogServer, ah as SandboxConfig, ai as SandboxCreatedEvent, aj as SandboxProviderBase, ak as SandboxProviderCatalogEntry, al as SandboxProviderConfig, am as SaveAgentRequest, an as SaveAgentResult, ao as SearchAgentSelectorParams, ap as SearchAgentsParams, aq as SelectRegistrySkillRequest, S as Session, ar as SessionEventItem, as as SkillBase, at as SkillCatalogEntry, au as SkillCatalogServer, av as SkillConfigBase, aw as SkillMount, ax as SkillSelectorEntry, b as ThreadCreatedEvent, aV as ThreadDoneEvent, ay as ToolApprovalRequiredEvent, az as ToolBase, aA as ToolCall, aW as ToolCallFunction, aX as ToolCallRef, aY as ToolInfo, aZ as ToolResponseEvent, aB as ToolResponseRequiredEvent, d as Turn, a_ as TurnCreatedEvent, a$ as TurnDoneEvent, e as TurnInputItem, aC as TurnState, b0 as TurnStateCancelled, aD as TurnStateDone, b1 as TurnStateError, b2 as TurnStateRunning, aE as TurnStreamData, aG as UpdateConnectorRequest, aH as UpdateModelProviderRequest, aI as UpdateSandboxProviderRequest, aJ as UpdateSandboxRequest, aK as UpdateSessionRequest, aL as UserMessage, b3 as UserMessageContent, c as UserToolApprovalEvent, U as UserToolResponseEvent } from '../types-VU3Fc17W.js';
3
3
 
4
4
  /**
5
5
  * Local implementations of streaming delta helpers.
@@ -294,15 +294,24 @@ interface Model {
294
294
  name: string;
295
295
  params?: ModelParams;
296
296
  }
297
+ interface AgentCapabilityConfig {
298
+ enabled?: boolean;
299
+ }
300
+ interface AgentRuntimeConfig {
301
+ generativeUi?: AgentCapabilityConfig;
302
+ dynamicSubAgents?: AgentCapabilityConfig;
303
+ askUserQuestions?: AgentCapabilityConfig;
304
+ }
297
305
  /**
298
306
  * SDK-owned agent definition — fields the FE reads/writes.
299
- * Host widens `model` / `skills` / `mcpServers` via type params, and adds
300
- * extra fields via `TSpec extends AgentSpec<...>`.
307
+ * Host widens `model` / `skills` / `mcpServers` / `config` via type params,
308
+ * and adds extra fields via `TSpec extends AgentSpec<...>`.
301
309
  */
302
- interface AgentSpec<TModel extends Model = Model, TSkill extends SkillMount = SkillMount, TMcp extends McpServerMount = McpServerMount> {
310
+ interface AgentSpec<TModel extends Model = Model, TSkill extends SkillMount = SkillMount, TMcp extends McpServerMount = McpServerMount, TConfig extends AgentRuntimeConfig = AgentRuntimeConfig> {
303
311
  model: TModel;
304
312
  skills?: TSkill[];
305
313
  mcpServers?: TMcp[];
314
+ config?: TConfig;
306
315
  instructions?: string;
307
316
  variables?: Record<string, string>;
308
317
  }
@@ -475,6 +484,15 @@ interface AgentChatServer<TSpec extends AgentSpec = AgentSpec, TSession extends
475
484
  interface SaveAgentRequest<TSpec extends AgentSpec = AgentSpec> {
476
485
  agentName: string;
477
486
  agentSpec: TSpec;
487
+ intent: "create" | "update";
488
+ /** Current mutable session to update atomically with the named agent. */
489
+ sessionId?: string;
490
+ }
491
+ interface SaveAgentResult {
492
+ /** Immutable id allocated by the host registry. */
493
+ agentId?: string;
494
+ /** Timestamp returned when the active mutable session was updated. */
495
+ sessionUpdatedAt?: string;
478
496
  }
479
497
  /**
480
498
  * Builder feature flags — atoms gate sandbox / skill / settings surfaces on these.
@@ -497,7 +515,7 @@ interface AgentBuilderCapabilitiesResponse {
497
515
  * Builder catalog + persist port — atoms call these.
498
516
  * Passed separately from the runtime's chat server.
499
517
  */
500
- interface AgentBuilderServer<TSpec extends AgentSpec = AgentSpec, TModel extends ModelSelectorEntry = ModelSelectorEntry, TSkill extends SkillSelectorEntry = SkillSelectorEntry, TMcp extends ConnectorSelectorEntry = ConnectorSelectorEntry, TAgent extends AgentSelectorEntry = AgentSelectorEntry, TSave = unknown, TCapabilities extends AgentBuilderCapabilitiesResponse = AgentBuilderCapabilitiesResponse> {
518
+ interface AgentBuilderServer<TSpec extends AgentSpec = AgentSpec, TModel extends ModelSelectorEntry = ModelSelectorEntry, TSkill extends SkillSelectorEntry = SkillSelectorEntry, TMcp extends ConnectorSelectorEntry = ConnectorSelectorEntry, TAgent extends AgentSelectorEntry = AgentSelectorEntry, TSave = SaveAgentResult, TCapabilities extends AgentBuilderCapabilitiesResponse = AgentBuilderCapabilitiesResponse> {
501
519
  getCapabilities(): Promise<TCapabilities>;
502
520
  getModels(): Promise<TModel[]>;
503
521
  getSkills(): Promise<TSkill[]>;
@@ -815,4 +833,4 @@ type AgentUIServerPort<TChat extends AgentChatServer = AgentChatServer, TBuilder
815
833
  /** Host-facing alias used by trueforge-ui. */
816
834
  type AgentUIServer = AgentUIServerPort;
817
835
 
818
- export type { ModelMessageEvent as $, AgentSpec as A, ConnectorCatalogServer as B, CatalogServer as C, ConnectorConfigBase as D, ConnectorSelectorEntry as E, ConnectorState as F, CreateConnectorRequest as G, CreateModelProviderRequest as H, CreateSandboxProviderRequest as I, CreateSandboxRequest as J, CreateSessionRequest as K, CreateSkillRequest as L, McpAuthRequiredEvent as M, CreateSkillRequestBase as N, DefinedSkill as O, GithubSkill as P, ImportGithubSkillRequest as Q, ListResult as R, Session as S, TurnEvent as T, UserToolResponseEvent as U, ListSessionsOrder as V, ListSessionsParams as W, McpServerMount as X, Model as Y, ModelCatalogServer as Z, ModelEntry as _, AgentChatServer as a, TurnStateRunning as a$, ModelParams as a0, ModelProperties as a1, ModelProviderBase as a2, ModelProviderCatalogEntry as a3, ModelProviderConfigBase as a4, ModelSelection as a5, ModelSelectorEntry as a6, PageParams as a7, PreviousTurnIdInput as a8, ProviderEntry as a9, TurnStateDone as aA, TurnStreamData as aB, TurnStreamingEvent as aC, UpdateConnectorRequest as aD, UpdateModelProviderRequest as aE, UpdateSandboxProviderRequest as aF, UpdateSandboxRequest as aG, UpdateSessionRequest as aH, UserMessage as aI, DeltaEvents as aJ, ActionRequiredEvent as aK, AgentInfo as aL, AgentParent as aM, ChunkDeltaToolCall as aN, McpInitializeEvent as aO, McpServerAuthInfo as aP, ModelMessageContentPart as aQ, ModelMessageDeltaEvent as aR, ThreadDoneEvent as aS, ToolCallFunction as aT, ToolCallRef as aU, ToolInfo as aV, ToolResponseEvent as aW, TurnCreatedEvent as aX, TurnDoneEvent as aY, TurnStateCancelled as aZ, TurnStateError as a_, ProviderType as aa, RegistrySkill as ab, SandboxBase as ac, SandboxCatalogEntry as ad, SandboxCatalogServer as ae, SandboxConfig as af, SandboxCreatedEvent as ag, SandboxProviderBase as ah, SandboxProviderCatalogEntry as ai, SandboxProviderConfig as aj, SaveAgentRequest as ak, SearchAgentSelectorParams as al, SearchAgentsParams as am, SelectRegistrySkillRequest as an, SessionEventItem as ao, SkillBase as ap, SkillCatalogEntry as aq, SkillCatalogServer as ar, SkillConfigBase as as, SkillMount as at, SkillSelectorEntry as au, ToolApprovalRequiredEvent as av, ToolBase as aw, ToolCall as ax, ToolResponseRequiredEvent as ay, TurnState as az, ThreadCreatedEvent as b, UserMessageContent as b0, UserToolApprovalEvent as c, Turn as d, TurnInputItem as e, AgentBuilderCapabilitiesResponse as f, AgentBuilderServer as g, AgentLibraryEntry as h, AgentSelectorEntry as i, AgentSkill as j, AgentUIServer as k, AgentUIServerPort as l, ApprovalDecision as m, AuthenticateConnectorRequest as n, ConnectorAuth as o, ConnectorAuthApiKey as p, ConnectorAuthNone as q, ConnectorAuthOAuth as r, ConnectorAuthPublic as s, ConnectorAuthPublicApiKey as t, ConnectorAuthPublicNone as u, ConnectorAuthPublicOAuth as v, ConnectorAuthType as w, ConnectorAuthenticationResult as x, ConnectorBase as y, ConnectorCatalogEntry as z };
836
+ export type { ModelCatalogServer as $, AgentSpec as A, ConnectorBase as B, CatalogServer as C, ConnectorCatalogEntry as D, ConnectorCatalogServer as E, ConnectorConfigBase as F, ConnectorSelectorEntry as G, ConnectorState as H, CreateConnectorRequest as I, CreateModelProviderRequest as J, CreateSandboxProviderRequest as K, CreateSandboxRequest as L, McpAuthRequiredEvent as M, CreateSessionRequest as N, CreateSkillRequest as O, CreateSkillRequestBase as P, DefinedSkill as Q, GithubSkill as R, Session as S, TurnEvent as T, UserToolResponseEvent as U, ImportGithubSkillRequest as V, ListResult as W, ListSessionsOrder as X, ListSessionsParams as Y, McpServerMount as Z, Model as _, AgentChatServer as a, TurnDoneEvent as a$, ModelEntry as a0, ModelMessageEvent as a1, ModelParams as a2, ModelProperties as a3, ModelProviderBase as a4, ModelProviderCatalogEntry as a5, ModelProviderConfigBase as a6, ModelSelection as a7, ModelSelectorEntry as a8, PageParams as a9, ToolCall as aA, ToolResponseRequiredEvent as aB, TurnState as aC, TurnStateDone as aD, TurnStreamData as aE, TurnStreamingEvent as aF, UpdateConnectorRequest as aG, UpdateModelProviderRequest as aH, UpdateSandboxProviderRequest as aI, UpdateSandboxRequest as aJ, UpdateSessionRequest as aK, UserMessage as aL, DeltaEvents as aM, ActionRequiredEvent as aN, AgentInfo as aO, AgentParent as aP, ChunkDeltaToolCall as aQ, McpInitializeEvent as aR, McpServerAuthInfo as aS, ModelMessageContentPart as aT, ModelMessageDeltaEvent as aU, ThreadDoneEvent as aV, ToolCallFunction as aW, ToolCallRef as aX, ToolInfo as aY, ToolResponseEvent as aZ, TurnCreatedEvent as a_, PreviousTurnIdInput as aa, ProviderEntry as ab, ProviderType as ac, RegistrySkill as ad, SandboxBase as ae, SandboxCatalogEntry as af, SandboxCatalogServer as ag, SandboxConfig as ah, SandboxCreatedEvent as ai, SandboxProviderBase as aj, SandboxProviderCatalogEntry as ak, SandboxProviderConfig as al, SaveAgentRequest as am, SaveAgentResult as an, SearchAgentSelectorParams as ao, SearchAgentsParams as ap, SelectRegistrySkillRequest as aq, SessionEventItem as ar, SkillBase as as, SkillCatalogEntry as at, SkillCatalogServer as au, SkillConfigBase as av, SkillMount as aw, SkillSelectorEntry as ax, ToolApprovalRequiredEvent as ay, ToolBase as az, ThreadCreatedEvent as b, TurnStateCancelled as b0, TurnStateError as b1, TurnStateRunning as b2, UserMessageContent as b3, UserToolApprovalEvent as c, Turn as d, TurnInputItem as e, AgentBuilderCapabilitiesResponse as f, AgentBuilderServer as g, AgentCapabilityConfig as h, AgentLibraryEntry as i, AgentRuntimeConfig as j, AgentSelectorEntry as k, AgentSkill as l, AgentUIServer as m, AgentUIServerPort as n, ApprovalDecision as o, AuthenticateConnectorRequest as p, ConnectorAuth as q, ConnectorAuthApiKey as r, ConnectorAuthNone as s, ConnectorAuthOAuth as t, ConnectorAuthPublic as u, ConnectorAuthPublicApiKey as v, ConnectorAuthPublicNone as w, ConnectorAuthPublicOAuth as x, ConnectorAuthType as y, ConnectorAuthenticationResult as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truefoundry/assistant-ui-runtime",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "TrueFoundry Gateway agent runtime adapter for assistant-ui",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -2434,7 +2434,7 @@ describe("buildSnapshotFromSessionEvents", () => {
2434
2434
  expect(progressSnapshots).toEqual([1, 2]);
2435
2435
  });
2436
2436
 
2437
- it("only inspects the newest listTurns page for a running turn", async () => {
2437
+ it("falls back to listTurns({ limit: 1 }) when listEvents omits the running tip", async () => {
2438
2438
  const listTurns = vi.fn(async () => ({
2439
2439
  data: [
2440
2440
  {
@@ -2460,6 +2460,300 @@ describe("buildSnapshotFromSessionEvents", () => {
2460
2460
  expect(snapshot.pendingUser?.content).toBe("now");
2461
2461
  });
2462
2462
 
2463
+ it("attaches an open tip from events when listTurns is oldest-first and clears answered ask-user", async () => {
2464
+ // Trueforge listTurns is ASC: limit:1 returns the oldest done turn even
2465
+ // while a later tip is still running. Tip detection must use the open
2466
+ // turn.created in listEvents (which includes running turns).
2467
+ const oldestDoneTurn = {
2468
+ id: "t-pause",
2469
+ sessionId: SESSION_ID,
2470
+ state: {
2471
+ status: "done",
2472
+ requiredActions: [
2473
+ {
2474
+ id: "resp-req-1",
2475
+ type: "tool.response_required",
2476
+ threadId: ROOT_THREAD_ID,
2477
+ createdAt,
2478
+ toolCalls: [{ id: "question-1", sourceEventId: "model-1" }],
2479
+ },
2480
+ ],
2481
+ completedAt: createdAt,
2482
+ },
2483
+ input: [{ type: "user.message", content: "please research" }],
2484
+ createdAt,
2485
+ } as unknown as Turn;
2486
+
2487
+ const runningTip = {
2488
+ id: "t-running",
2489
+ sessionId: SESSION_ID,
2490
+ state: { status: "running" },
2491
+ input: [
2492
+ {
2493
+ type: "user.tool_response",
2494
+ threadId: ROOT_THREAD_ID,
2495
+ toolCallId: "question-1",
2496
+ content: "AI-native / LLM-powered SaaS",
2497
+ },
2498
+ ],
2499
+ createdAt,
2500
+ } as unknown as Turn;
2501
+
2502
+ const items: SessionEventItem[] = [
2503
+ {
2504
+ turnId: "t-pause",
2505
+ event: {
2506
+ type: "turn.created",
2507
+ id: "evt-c-pause",
2508
+ turnId: "t-pause",
2509
+ input: oldestDoneTurn.input,
2510
+ state: { status: "running" },
2511
+ createdAt,
2512
+ },
2513
+ },
2514
+ {
2515
+ turnId: "t-pause",
2516
+ event: modelMessage({
2517
+ id: "model-1",
2518
+ threadId: ROOT_THREAD_ID,
2519
+ content: "One calibration question:",
2520
+ toolCalls: [
2521
+ {
2522
+ id: "question-1",
2523
+ type: "function",
2524
+ function: {
2525
+ name: "ask_user_question",
2526
+ arguments: JSON.stringify({
2527
+ question: "Which direction should I focus on?",
2528
+ options: [
2529
+ "General B2B SaaS",
2530
+ "AI-native / LLM-powered SaaS",
2531
+ ],
2532
+ }),
2533
+ },
2534
+ toolInfo: {
2535
+ type: "truefoundry-system",
2536
+ name: "ask_user_question",
2537
+ },
2538
+ },
2539
+ ],
2540
+ }),
2541
+ },
2542
+ {
2543
+ turnId: "t-pause",
2544
+ event: responseRequired({
2545
+ id: "resp-req-1",
2546
+ threadId: ROOT_THREAD_ID,
2547
+ toolCalls: [{ id: "question-1", sourceEventId: "model-1" }],
2548
+ }),
2549
+ },
2550
+ {
2551
+ turnId: "t-pause",
2552
+ event: {
2553
+ type: "turn.done",
2554
+ id: "evt-d-pause",
2555
+ state: oldestDoneTurn.state as TurnDoneEvent["state"],
2556
+ createdAt,
2557
+ } as TurnDoneEvent,
2558
+ },
2559
+ {
2560
+ turnId: "t-running",
2561
+ event: {
2562
+ type: "turn.created",
2563
+ id: "evt-c-running",
2564
+ turnId: "t-running",
2565
+ previousTurnId: "t-pause",
2566
+ input: runningTip.input,
2567
+ state: { status: "running" },
2568
+ createdAt,
2569
+ },
2570
+ },
2571
+ {
2572
+ turnId: "t-running",
2573
+ event: modelMessage({
2574
+ id: "model-2",
2575
+ threadId: ROOT_THREAD_ID,
2576
+ content: "Locked in — starting deep research.",
2577
+ }),
2578
+ },
2579
+ ];
2580
+
2581
+ const listTurns = vi.fn(async () => ({
2582
+ data: [oldestDoneTurn],
2583
+ nextPageToken: "eyJvZmZzZXQiOjF9",
2584
+ }));
2585
+ const getTurn = vi.fn(async () => runningTip);
2586
+ const server = {
2587
+ listTurns,
2588
+ getTurn,
2589
+ listEvents: sessionEventsPage(items),
2590
+ listTurnEvents: async () => ({ data: [] }),
2591
+ } as unknown as AgentChatServer;
2592
+
2593
+ const snapshot = await buildSnapshotFromSessionEvents(server, SESSION_ID);
2594
+
2595
+ expect(getTurn).toHaveBeenCalledWith({
2596
+ sessionId: SESSION_ID,
2597
+ turnId: "t-running",
2598
+ });
2599
+ expect(listTurns).not.toHaveBeenCalled();
2600
+ expect(snapshot.runningTurn?.id).toBe("t-running");
2601
+ expect(snapshot.unstable_resume).toBe(true);
2602
+
2603
+ const messages = projectSessionMessages(snapshot);
2604
+ expect(collectPendingToolResponses(messages)).toHaveLength(0);
2605
+
2606
+ const assistant = messages.find((message) => message.role === "assistant");
2607
+ expect(assistant).toBeDefined();
2608
+ const toolCall = assistant?.content.find(
2609
+ (part) => part.type === "tool-call" && part.toolCallId === "question-1",
2610
+ );
2611
+ expect(toolCall).toMatchObject({
2612
+ type: "tool-call",
2613
+ toolCallId: "question-1",
2614
+ result: "AI-native / LLM-powered SaaS",
2615
+ });
2616
+ if (toolCall?.type === "tool-call") {
2617
+ expect(toolCall.interrupt).toBeUndefined();
2618
+ }
2619
+ });
2620
+
2621
+ it("keeps an answered ask-user answered when the open tip finished before getTurn", async () => {
2622
+ // The tip can complete between listEvents and getTurn. There is nothing
2623
+ // left to resume, but its user.tool_response input never reached the
2624
+ // fold via ingestion (no turn.done in the window), so it must still be
2625
+ // applied — otherwise the answered prompt resurfaces as pending.
2626
+ const finishedTip = {
2627
+ id: "t-tip",
2628
+ sessionId: SESSION_ID,
2629
+ state: { status: "done", requiredActions: [], completedAt: createdAt },
2630
+ input: [
2631
+ {
2632
+ type: "user.tool_response",
2633
+ threadId: ROOT_THREAD_ID,
2634
+ toolCallId: "question-1",
2635
+ content: "AI-native / LLM-powered SaaS",
2636
+ },
2637
+ ],
2638
+ createdAt,
2639
+ } as unknown as Turn;
2640
+
2641
+ const items: SessionEventItem[] = [
2642
+ {
2643
+ turnId: "t-pause",
2644
+ event: {
2645
+ type: "turn.created",
2646
+ id: "evt-c-pause",
2647
+ turnId: "t-pause",
2648
+ input: [{ type: "user.message", content: "please research" }],
2649
+ state: { status: "running" },
2650
+ createdAt,
2651
+ },
2652
+ },
2653
+ {
2654
+ turnId: "t-pause",
2655
+ event: modelMessage({
2656
+ id: "model-1",
2657
+ threadId: ROOT_THREAD_ID,
2658
+ content: "One calibration question:",
2659
+ toolCalls: [
2660
+ {
2661
+ id: "question-1",
2662
+ type: "function",
2663
+ function: {
2664
+ name: "ask_user_question",
2665
+ arguments: JSON.stringify({
2666
+ question: "Which direction should I focus on?",
2667
+ options: [
2668
+ "General B2B SaaS",
2669
+ "AI-native / LLM-powered SaaS",
2670
+ ],
2671
+ }),
2672
+ },
2673
+ toolInfo: {
2674
+ type: "truefoundry-system",
2675
+ name: "ask_user_question",
2676
+ },
2677
+ },
2678
+ ],
2679
+ }),
2680
+ },
2681
+ {
2682
+ turnId: "t-pause",
2683
+ event: responseRequired({
2684
+ id: "resp-req-1",
2685
+ threadId: ROOT_THREAD_ID,
2686
+ toolCalls: [{ id: "question-1", sourceEventId: "model-1" }],
2687
+ }),
2688
+ },
2689
+ {
2690
+ turnId: "t-pause",
2691
+ event: {
2692
+ type: "turn.done",
2693
+ id: "evt-d-pause",
2694
+ state: {
2695
+ status: "done",
2696
+ requiredActions: [
2697
+ {
2698
+ id: "resp-req-1",
2699
+ type: "tool.response_required",
2700
+ threadId: ROOT_THREAD_ID,
2701
+ createdAt,
2702
+ toolCalls: [
2703
+ { id: "question-1", sourceEventId: "model-1" },
2704
+ ],
2705
+ },
2706
+ ],
2707
+ completedAt: createdAt,
2708
+ },
2709
+ createdAt,
2710
+ } as unknown as TurnDoneEvent,
2711
+ },
2712
+ {
2713
+ turnId: "t-tip",
2714
+ event: {
2715
+ type: "turn.created",
2716
+ id: "evt-c-tip",
2717
+ turnId: "t-tip",
2718
+ previousTurnId: "t-pause",
2719
+ input: finishedTip.input,
2720
+ state: { status: "running" },
2721
+ createdAt,
2722
+ },
2723
+ },
2724
+ ];
2725
+
2726
+ const listTurns = vi.fn(async () => ({ data: [], nextPageToken: undefined }));
2727
+ const getTurn = vi.fn(async () => finishedTip);
2728
+ const server = {
2729
+ listTurns,
2730
+ getTurn,
2731
+ listEvents: sessionEventsPage(items),
2732
+ listTurnEvents: async () => ({ data: [] }),
2733
+ } as unknown as AgentChatServer;
2734
+
2735
+ const snapshot = await buildSnapshotFromSessionEvents(server, SESSION_ID);
2736
+
2737
+ expect(snapshot.runningTurn).toBeUndefined();
2738
+ expect(snapshot.unstable_resume).toBeFalsy();
2739
+
2740
+ const messages = projectSessionMessages(snapshot);
2741
+ expect(collectPendingToolResponses(messages)).toHaveLength(0);
2742
+
2743
+ const assistant = messages.find((message) => message.role === "assistant");
2744
+ const toolCall = assistant?.content.find(
2745
+ (part) => part.type === "tool-call" && part.toolCallId === "question-1",
2746
+ );
2747
+ expect(toolCall).toMatchObject({
2748
+ type: "tool-call",
2749
+ toolCallId: "question-1",
2750
+ result: "AI-native / LLM-powered SaaS",
2751
+ });
2752
+ if (toolCall?.type === "tool-call") {
2753
+ expect(toolCall.interrupt).toBeUndefined();
2754
+ }
2755
+ });
2756
+
2463
2757
  it("loads only the newest event page and exposes an older-history cursor", async () => {
2464
2758
  const makeTurnItems = (id: string, text: string): SessionEventItem[] => [
2465
2759
  {
@@ -516,10 +516,6 @@ function attachRunningTurn(
516
516
  if (runningTurn == null) {
517
517
  return snapshot;
518
518
  }
519
- // Session-level history excludes the running turn. Apply continuation inputs
520
- // from the turn listing so answered approvals / ask-user prompts are not
521
- // restored as pending while reconnecting to that turn after a refresh.
522
- applyUserToolResponsesToFold(snapshot.fold, runningTurn.input ?? []);
523
519
  const pendingUserText = extractTurnUserText(runningTurn.input);
524
520
  return replaceSessionSnapshot(snapshot, {
525
521
  runningTurn,
@@ -537,13 +533,117 @@ function attachRunningTurn(
537
533
  });
538
534
  }
539
535
 
536
+ /**
537
+ * Last `turn.created` in ASC event order with no following `turn.done` — the
538
+ * open tip of the active branch in this window.
539
+ */
540
+ function findOpenTurnCreated(
541
+ itemsAsc: readonly GatewaySessionEventItem[],
542
+ ): { turnId: string; event: TurnCreatedEvent } | undefined {
543
+ let open: { turnId: string; event: TurnCreatedEvent } | undefined;
544
+ for (const item of itemsAsc) {
545
+ if (item.event.type === "turn.created") {
546
+ open = { turnId: item.turnId, event: item.event };
547
+ } else if (item.event.type === "turn.done") {
548
+ open = undefined;
549
+ }
550
+ }
551
+ return open;
552
+ }
553
+
554
+ function turnFromCreatedEvent(options: {
555
+ sessionId: string;
556
+ turnId: string;
557
+ event: TurnCreatedEvent;
558
+ }): Turn {
559
+ const { sessionId, turnId, event } = options;
560
+ return {
561
+ id: turnId,
562
+ sessionId,
563
+ state: { status: "running" },
564
+ createdAt: event.createdAt,
565
+ ...(event.input != null ? { input: event.input } : {}),
566
+ ...(event.previousTurnId === undefined
567
+ ? {}
568
+ : { previousTurnId: event.previousTurnId }),
569
+ };
570
+ }
571
+
572
+ type SessionTip = {
573
+ /** Turn to resume and subscribe to; absent once the tip has finished. */
574
+ runningTurn?: Turn;
575
+ /**
576
+ * Tip input that event ingestion could not apply, because it only folds a
577
+ * turn's input once that turn's `turn.done` arrives. Answered ask-user
578
+ * prompts and approvals live here, so this must be folded even when the tip
579
+ * is no longer running.
580
+ */
581
+ continuationInput: readonly TurnInputItem[];
582
+ };
583
+
584
+ /**
585
+ * Resolves the tip turn of the active branch for resume/subscribe.
586
+ *
587
+ * Prefer an open tip from the events window (works when listTurns is
588
+ * oldest-first and when listEvents includes the running turn). Fall back to
589
+ * `listTurns({ limit: 1 })` for hosts that omit the running turn from
590
+ * listEvents and put the tip first.
591
+ */
592
+ async function resolveSessionTip(options: {
593
+ server: AgentChatServer;
594
+ sessionId: string;
595
+ itemsAsc: readonly GatewaySessionEventItem[];
596
+ }): Promise<SessionTip> {
597
+ const { server, sessionId, itemsAsc } = options;
598
+ const open = findOpenTurnCreated(itemsAsc);
599
+ if (open != null) {
600
+ const continuationInput = open.event.input ?? [];
601
+ if (typeof server.getTurn === "function") {
602
+ try {
603
+ const turn = await server.getTurn({
604
+ sessionId,
605
+ turnId: open.turnId,
606
+ });
607
+ if (turn.state.status === "running") {
608
+ return {
609
+ runningTurn: turn,
610
+ continuationInput: turn.input ?? continuationInput,
611
+ };
612
+ }
613
+ // Tip finished between listEvents and getTurn: nothing to
614
+ // resume, but its answers still belong in the fold.
615
+ return { continuationInput: turn.input ?? continuationInput };
616
+ } catch {
617
+ // getTurn failed; synthesize from the open turn.created below.
618
+ }
619
+ }
620
+ return {
621
+ runningTurn: turnFromCreatedEvent({
622
+ sessionId,
623
+ turnId: open.turnId,
624
+ event: open.event,
625
+ }),
626
+ continuationInput,
627
+ };
628
+ }
629
+
630
+ // Hosts that exclude the running turn from listEvents still surface it as
631
+ // the first row of listTurns when that API is tip-first.
632
+ const turnsPage = await server.listTurns({ sessionId, limit: 1 });
633
+ const tip = turnsPage.data[0] as Turn | undefined;
634
+ return tip?.state?.status === "running"
635
+ ? { runningTurn: tip, continuationInput: tip.input ?? [] }
636
+ : { continuationInput: [] };
637
+ }
638
+
540
639
  /**
541
640
  * Builds a session snapshot using the session-level `listEvents` API.
542
641
  *
543
642
  * Loads the newest event page (extending only when a page boundary splits a
544
643
  * turn group), then leaves older pages for `prependOlderSessionHistory`.
545
- * Only `listTurns({ limit: 1 })` is called first to detect a currently-running
546
- * turn (the session-level API does not return events for the running turn).
644
+ * Detects a currently-running tip from an open `turn.created` in that window
645
+ * when present; otherwise falls back to `listTurns({ limit: 1 })` for hosts
646
+ * that omit the running turn from listEvents.
547
647
  *
548
648
  * `onProgress` is called after each complete turn is ingested so callers can
549
649
  * update the UI progressively while the processing loop runs.
@@ -553,12 +653,6 @@ export async function buildSnapshotFromSessionEvents(
553
653
  sessionId: string,
554
654
  onProgress?: (snap: SessionSnapshot) => void,
555
655
  ): Promise<SessionSnapshot> {
556
- // Detect a running turn with a single listTurns page — do not drain pagination.
557
- const turnsPage = await server.listTurns({ sessionId, limit: 1 });
558
- const newestTurn = turnsPage.data[0] as Turn | undefined;
559
- const runningTurn =
560
- newestTurn?.state?.status === "running" ? newestTurn : undefined;
561
-
562
656
  const window = await fetchSessionEventsWindow(server, sessionId);
563
657
  const historyPagination: SessionHistoryPagination = {
564
658
  hasOlder: window.hasOlder,
@@ -575,7 +669,16 @@ export async function buildSnapshotFromSessionEvents(
575
669
  historyPagination,
576
670
  });
577
671
 
578
- return attachRunningTurn(withHistory, runningTurn);
672
+ const tip = await resolveSessionTip({
673
+ server,
674
+ sessionId,
675
+ itemsAsc: window.itemsAsc,
676
+ });
677
+ // The tip has no turn.done in this window, so ingestion never folded its
678
+ // input. Apply it here so answered approvals / ask-user prompts are not
679
+ // restored as pending after a refresh.
680
+ applyUserToolResponsesToFold(withHistory.fold, tip.continuationInput);
681
+ return attachRunningTurn(withHistory, tip.runningTurn);
579
682
  }
580
683
 
581
684
  /**