@truefoundry/assistant-ui-runtime 0.1.2 → 0.1.3-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truefoundry/assistant-ui-runtime",
3
- "version": "0.1.2",
3
+ "version": "0.1.3-rc.2",
4
4
  "description": "TrueFoundry Gateway agent runtime adapter for assistant-ui",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -50,7 +50,7 @@
50
50
  "peerDependencies": {
51
51
  "@types/react": "*",
52
52
  "react": "^18 || ^19",
53
- "truefoundry-gateway-sdk": "^0.3.1"
53
+ "truefoundry-gateway-sdk": "^0.3.2-rc.1"
54
54
  },
55
55
  "peerDependenciesMeta": {
56
56
  "@types/react": {
@@ -63,8 +63,9 @@
63
63
  "@types/react": "^19.2.17",
64
64
  "jsdom": "^29.1.1",
65
65
  "react": "^19.2.4",
66
+ "truefoundry-gateway-sdk": "0.3.2-rc.1",
66
67
  "tsup": "^8.5.0",
67
68
  "typescript": "^5.9.3",
68
69
  "vitest": "^4.1.9"
69
70
  }
70
- }
71
+ }
@@ -1,7 +1,12 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
 
3
3
  import { mergeAgentSpec } from "./private/agentSpec.js";
4
- import { resolveTrueFoundryAgentConfig } from "./types.js";
4
+ import {
5
+ resolveTrueFoundryAgentConfig,
6
+ resolveTrueFoundryAgentRuntimeOptions,
7
+ } from "./types.js";
8
+ import type { AgentSessionClient } from "truefoundry-gateway-sdk/agents";
9
+ import type { PrivateAgentSessionClient } from "truefoundry-gateway-sdk/agents/private";
5
10
 
6
11
  describe("resolveTrueFoundryAgentConfig", () => {
7
12
  it("supports legacy agentName", () => {
@@ -36,6 +41,30 @@ describe("resolveTrueFoundryAgentConfig", () => {
36
41
  });
37
42
  });
38
43
 
44
+ describe("resolveTrueFoundryAgentRuntimeOptions", () => {
45
+ const client = {} as AgentSessionClient;
46
+
47
+ it("requires privateClient for draft mode", () => {
48
+ expect(() =>
49
+ resolveTrueFoundryAgentRuntimeOptions({
50
+ client,
51
+ agent: { mode: "draft", defaultAgentSpec: { model: { name: "x" } } },
52
+ }),
53
+ ).toThrow(/privateClient/);
54
+ });
55
+
56
+ it("accepts privateClient for draft mode", () => {
57
+ const privateClient = {} as PrivateAgentSessionClient;
58
+ const resolved = resolveTrueFoundryAgentRuntimeOptions({
59
+ client,
60
+ privateClient,
61
+ agent: { mode: "draft", defaultAgentSpec: { model: { name: "x" } } },
62
+ });
63
+ expect(resolved.privateClient).toBe(privateClient);
64
+ expect(resolved.agent.mode).toBe("draft");
65
+ });
66
+ });
67
+
39
68
  describe("mergeAgentSpec", () => {
40
69
  it("deep-merges model params", () => {
41
70
  const base = {
package/src/index.ts CHANGED
@@ -16,8 +16,11 @@ export type { UseTrueFoundryAgentRuntimeOptions, NamedAgentConfig, DraftAgentCon
16
16
  export type { AgentSpec, AgentSpecUpdate, DraftSession } from "./private/agentSpec.js";
17
17
  export { mergeAgentSpec, draftSessionTitle } from "./private/agentSpec.js";
18
18
  export { createTrueFoundryDraftThreadListAdapter } from "./private/truefoundryDraftThreadListAdapter.js";
19
+ export { createTrueFoundryOwnedSessionsThreadListAdapter } from "./truefoundryOwnedSessionsThreadListAdapter.js";
19
20
  export { createDraftSessionBridge } from "./private/draftSessionBridge.js";
20
21
  export type { DraftSessionBridge } from "./private/draftSessionBridge.js";
22
+ export { PrivateAgentSessionClient } from "truefoundry-gateway-sdk/agents/private";
23
+ export type { AgentDraftSession } from "truefoundry-gateway-sdk/agents/private";
21
24
  export {
22
25
  useTrueFoundryAgentSpec,
23
26
  useTrueFoundryUpdateAgentSpec,
@@ -21,7 +21,7 @@ export function loadSessionSnapshot(
21
21
  onProgress?: (snap: SessionSnapshot) => void,
22
22
  ): Promise<SessionSnapshot> {
23
23
  const cacheKey =
24
- sessionOptions?.draftGateway != null ? `draft:${sessionId}` : sessionId;
24
+ sessionOptions?.privateClient != null ? `draft:${sessionId}` : sessionId;
25
25
  let inflight = inflightBySessionId.get(cacheKey);
26
26
  if (inflight == null) {
27
27
  inflight = getSession(client, sessionId, sessionOptions)
@@ -1,11 +1,11 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
2
 
3
- import type { TrueFoundryGateway } from "truefoundry-gateway-sdk";
4
- import type { AgentSessionClient } from "truefoundry-gateway-sdk/agents";
3
+ import type { PrivateAgentSessionClient } from "truefoundry-gateway-sdk/agents/private";
5
4
 
6
5
  import { bindDraftAgentSession } from "./bindDraftAgentSession.js";
7
6
 
8
7
  const draft = {
8
+ type: "session/draft" as const,
9
9
  id: "draft-1",
10
10
  agentSpec: { model: { name: "anthropic/claude-sonnet-4-6" } },
11
11
  createdBySubject: { type: "user" as const, id: "u1" },
@@ -15,17 +15,14 @@ const draft = {
15
15
 
16
16
  describe("bindDraftAgentSession", () => {
17
17
  it("validates the draft and binds turns to the draft session id", async () => {
18
- const get = vi.fn().mockResolvedValue({ data: draft });
19
- const gateway = {
20
- agents: { private: { draftSessions: { get } } },
21
- } as unknown as TrueFoundryGateway;
22
- const client = {
23
- client: gateway,
24
- } as unknown as AgentSessionClient;
18
+ const getDraftSession = vi.fn().mockResolvedValue(draft);
19
+ const privateClient = {
20
+ getDraftSession,
21
+ } as unknown as PrivateAgentSessionClient;
25
22
 
26
- const session = await bindDraftAgentSession(client, gateway, "draft-1");
23
+ const session = await bindDraftAgentSession(privateClient, "draft-1");
27
24
 
28
- expect(get).toHaveBeenCalledWith("draft-1");
25
+ expect(getDraftSession).toHaveBeenCalledWith({ draftSessionId: "draft-1" });
29
26
  expect(session.id).toBe("draft-1");
30
27
  });
31
28
  });
@@ -34,18 +31,19 @@ describe("createDraftSessionBridge", () => {
34
31
  it("syncs agent spec via draftSessions.update and returns updatedAt", async () => {
35
32
  const { createDraftSessionBridge } = await import("./draftSessionBridge.js");
36
33
  const update = vi.fn().mockResolvedValue({ data: draft });
37
- const gateway = {
38
- agents: {
39
- private: {
40
- draftSessions: {
41
- get: vi.fn().mockResolvedValue({ data: draft }),
42
- update,
34
+ const getDraftSession = vi.fn().mockResolvedValue(draft);
35
+ const privateClient = {
36
+ getDraftSession,
37
+ client: {
38
+ agents: {
39
+ private: {
40
+ draftSessions: { update },
43
41
  },
44
42
  },
45
43
  },
46
- } as unknown as TrueFoundryGateway;
44
+ } as unknown as PrivateAgentSessionClient;
47
45
 
48
- const bridge = createDraftSessionBridge(gateway);
46
+ const bridge = createDraftSessionBridge(privateClient);
49
47
  const updatedAt = await bridge.syncAgentSpec("draft-1", draft.agentSpec);
50
48
 
51
49
  expect(update).toHaveBeenCalledWith("draft-1", {
@@ -1,65 +1,27 @@
1
- import type { TrueFoundryGateway } from "truefoundry-gateway-sdk";
2
- import type { AgentSession, AgentSessionClient } from "truefoundry-gateway-sdk/agents";
3
- // SDK does not publicly export the AgentSession class; bind draft ids for turn routes.
4
- import { AgentSession as AgentSessionClass } from "truefoundry-gateway-sdk/dist/esm/agents/AgentSession.mjs";
5
-
6
- import type { DraftSession } from "./agentSpec.js";
7
-
8
- type SessionRecord = {
9
- id: string;
10
- agentName: string;
11
- title?: string;
12
- createdBySubject: DraftSession["createdBySubject"];
13
- createdAt: string;
14
- updatedAt: string;
15
- };
1
+ import type { AgentSession } from "truefoundry-gateway-sdk/agents";
2
+ import type { PrivateAgentSessionClient } from "truefoundry-gateway-sdk/agents/private";
16
3
 
17
4
  const inflightByDraftId = new Map<string, Promise<AgentSession>>();
18
5
 
19
- export function getGatewayFromSessionClient(
20
- client: AgentSessionClient,
21
- ): TrueFoundryGateway {
22
- const internal = client as unknown as { client: TrueFoundryGateway };
23
- if (internal.client == null) {
24
- throw new Error("AgentSessionClient is missing an internal gateway client.");
25
- }
26
- return internal.client;
27
- }
28
-
29
- function draftToSessionRecord(draft: DraftSession): SessionRecord {
30
- return {
31
- id: draft.id,
32
- agentName: draft.agentName ?? "",
33
- title: draft.title,
34
- createdBySubject: draft.createdBySubject,
35
- createdAt: draft.createdAt,
36
- updatedAt: draft.updatedAt,
37
- };
38
- }
39
-
40
6
  /**
41
- * Binds an `AgentSession` for turn APIs at `/agents/sessions/{draftSessionId}/turns`
42
- * after validating the draft via `GET draft-sessions/{draftSessionId}`.
43
- * Does not call `GET` or `POST` on `/agents/sessions` root.
7
+ * Binds turn APIs for a draft session via `PrivateAgentSessionClient.getDraftSession`.
8
+ * Returns an `AgentDraftSession` (identical turn surface to `AgentSession`).
44
9
  */
45
10
  export async function bindDraftAgentSession(
46
- client: AgentSessionClient,
47
- gateway: TrueFoundryGateway,
11
+ privateClient: PrivateAgentSessionClient,
48
12
  draftSessionId: string,
49
13
  ): Promise<AgentSession> {
50
14
  let inflight = inflightByDraftId.get(draftSessionId);
51
15
  if (inflight == null) {
52
- inflight = (async () => {
53
- const response = await gateway.agents.private.draftSessions.get(draftSessionId);
54
- return new AgentSessionClass(
55
- draftToSessionRecord(response.data),
56
- getGatewayFromSessionClient(client),
57
- ) as AgentSession;
58
- })().finally(() => {
59
- if (inflightByDraftId.get(draftSessionId) === inflight) {
60
- inflightByDraftId.delete(draftSessionId);
61
- }
62
- });
16
+ inflight = privateClient
17
+ .getDraftSession({ draftSessionId })
18
+ // AgentDraftSession shares the turn API with AgentSession.
19
+ .then((draft) => draft as unknown as AgentSession)
20
+ .finally(() => {
21
+ if (inflightByDraftId.get(draftSessionId) === inflight) {
22
+ inflightByDraftId.delete(draftSessionId);
23
+ }
24
+ });
63
25
  inflightByDraftId.set(draftSessionId, inflight);
64
26
  }
65
27
  return inflight;
@@ -1,6 +1,7 @@
1
- import type { TrueFoundryGateway } from "truefoundry-gateway-sdk";
1
+ import type { PrivateAgentSessionClient } from "truefoundry-gateway-sdk/agents/private";
2
2
 
3
3
  import type { AgentSpec } from "./agentSpec.js";
4
+ import { getGatewayFromPrivateClient } from "./getGatewayFromPrivateClient.js";
4
5
 
5
6
  export const DRAFT_SESSION_LAST_UPDATED_AT_HEADER = "x-tfy-session-last-updated-at";
6
7
 
@@ -10,20 +11,17 @@ export type DraftSessionBridge = {
10
11
  };
11
12
 
12
13
  export function createDraftSessionBridge(
13
- gateway: TrueFoundryGateway,
14
+ privateClient: PrivateAgentSessionClient,
14
15
  ): DraftSessionBridge {
15
- async function getDraft(draftSessionId: string) {
16
- const response = await gateway.agents.private.draftSessions.get(draftSessionId);
17
- return response.data;
18
- }
19
-
20
16
  return {
21
17
  async getDraftAgentSpec(draftSessionId) {
22
- const draft = await getDraft(draftSessionId);
18
+ const draft = await privateClient.getDraftSession({ draftSessionId });
23
19
  return draft.agentSpec;
24
20
  },
25
21
 
26
22
  async syncAgentSpec(draftSessionId, agentSpec) {
23
+ // PrivateAgentSessionClient does not wrap update yet — use the low-level client.
24
+ const gateway = getGatewayFromPrivateClient(privateClient);
27
25
  const response = await gateway.agents.private.draftSessions.update(
28
26
  draftSessionId,
29
27
  { agentSpec },
@@ -0,0 +1,13 @@
1
+ import type { TrueFoundryGateway } from "truefoundry-gateway-sdk";
2
+ import type { PrivateAgentSessionClient } from "truefoundry-gateway-sdk/agents/private";
3
+
4
+ /** Reach the underlying gateway for APIs not yet wrapped on PrivateAgentSessionClient (update, sandbox download). */
5
+ export function getGatewayFromPrivateClient(
6
+ privateClient: PrivateAgentSessionClient,
7
+ ): TrueFoundryGateway {
8
+ const internal = privateClient as unknown as { client: TrueFoundryGateway };
9
+ if (internal.client == null) {
10
+ throw new Error("PrivateAgentSessionClient is missing an internal gateway client.");
11
+ }
12
+ return internal.client;
13
+ }
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
2
 
3
- import type { TrueFoundryGateway } from "truefoundry-gateway-sdk";
3
+ import type { PrivateAgentSessionClient } from "truefoundry-gateway-sdk/agents/private";
4
4
 
5
5
  import { createTrueFoundryDraftThreadListAdapter } from "./truefoundryDraftThreadListAdapter.js";
6
6
  import type { AgentSpec } from "./agentSpec.js";
@@ -12,6 +12,7 @@ const defaultAgentSpec: AgentSpec = {
12
12
 
13
13
  function mockDraft(id: string, title: string | undefined, updatedAt: string) {
14
14
  return {
15
+ type: "session/draft" as const,
15
16
  id,
16
17
  agentSpec: defaultAgentSpec,
17
18
  title,
@@ -38,31 +39,33 @@ function mockDraftListPage(
38
39
 
39
40
  describe("createTrueFoundryDraftThreadListAdapter", () => {
40
41
  it("lists draft sessions with pagination cursor", async () => {
41
- const list = vi.fn().mockResolvedValue(
42
+ const listDraftSessions = vi.fn().mockResolvedValue(
42
43
  mockDraftListPage(
43
44
  [mockDraft("d1", "My draft", "2026-06-30T10:00:00.000Z")],
44
45
  "page-2",
45
46
  ),
46
47
  );
47
- const gateway = {
48
- agents: { private: { draftSessions: { list, create: vi.fn(), get: vi.fn() } } },
49
- } as unknown as TrueFoundryGateway;
48
+ const privateClient = {
49
+ listDraftSessions,
50
+ createDraftSession: vi.fn(),
51
+ getDraftSession: vi.fn(),
52
+ } as unknown as PrivateAgentSessionClient;
50
53
 
51
54
  const adapter = createTrueFoundryDraftThreadListAdapter({
52
- gateway,
55
+ privateClient,
53
56
  defaultAgentSpec,
54
57
  });
55
58
 
56
59
  const result = await adapter.list();
57
60
 
58
- expect(list).toHaveBeenCalledWith(
61
+ expect(listDraftSessions).toHaveBeenCalledWith(
59
62
  expect.objectContaining({
60
63
  limit: 20,
61
64
  pageToken: undefined,
62
65
  startTimestamp: expect.any(String),
63
66
  }),
64
67
  );
65
- expect(list).toHaveBeenCalledWith(
68
+ expect(listDraftSessions).toHaveBeenCalledWith(
66
69
  expect.not.objectContaining({ agentName: expect.anything() }),
67
70
  );
68
71
  expect(result.threads).toEqual([
@@ -77,21 +80,23 @@ describe("createTrueFoundryDraftThreadListAdapter", () => {
77
80
  });
78
81
 
79
82
  it("creates a draft session on initialize", async () => {
80
- const create = vi.fn().mockResolvedValue({
81
- data: mockDraft("d-new", undefined, "2026-06-30T12:00:00.000Z"),
82
- });
83
- const gateway = {
84
- agents: { private: { draftSessions: { list: vi.fn(), create, get: vi.fn() } } },
85
- } as unknown as TrueFoundryGateway;
83
+ const createDraftSession = vi.fn().mockResolvedValue(
84
+ mockDraft("d-new", undefined, "2026-06-30T12:00:00.000Z"),
85
+ );
86
+ const privateClient = {
87
+ listDraftSessions: vi.fn(),
88
+ createDraftSession,
89
+ getDraftSession: vi.fn(),
90
+ } as unknown as PrivateAgentSessionClient;
86
91
 
87
92
  const adapter = createTrueFoundryDraftThreadListAdapter({
88
- gateway,
93
+ privateClient,
89
94
  defaultAgentSpec,
90
95
  });
91
96
 
92
97
  const result = await adapter.initialize("local-thread-id");
93
98
 
94
- expect(create).toHaveBeenCalledWith({ agentSpec: defaultAgentSpec });
99
+ expect(createDraftSession).toHaveBeenCalledWith({ agentSpec: defaultAgentSpec });
95
100
  expect(result).toEqual({ remoteId: "d-new", externalId: undefined });
96
101
  });
97
102
 
@@ -102,39 +107,44 @@ describe("createTrueFoundryDraftThreadListAdapter", () => {
102
107
  mcpServers: [{ name: "github", enableTools: ["@all"] }],
103
108
  skills: [{ fqn: "acme/skill-a:1", preload: false }],
104
109
  };
105
- const create = vi.fn().mockResolvedValue({
106
- data: mockDraft("d-new", undefined, "2026-06-30T12:00:00.000Z"),
107
- });
108
- const gateway = {
109
- agents: { private: { draftSessions: { list: vi.fn(), create, get: vi.fn() } } },
110
- } as unknown as TrueFoundryGateway;
110
+ const createDraftSession = vi.fn().mockResolvedValue(
111
+ mockDraft("d-new", undefined, "2026-06-30T12:00:00.000Z"),
112
+ );
113
+ const privateClient = {
114
+ listDraftSessions: vi.fn(),
115
+ createDraftSession,
116
+ getDraftSession: vi.fn(),
117
+ } as unknown as PrivateAgentSessionClient;
111
118
 
112
119
  const adapter = createTrueFoundryDraftThreadListAdapter({
113
- gateway,
120
+ privateClient,
114
121
  defaultAgentSpec,
115
122
  getAgentSpec: () => liveAgentSpec,
116
123
  });
117
124
 
118
125
  await adapter.initialize("local-thread-id");
119
126
 
120
- expect(create).toHaveBeenCalledWith({ agentSpec: liveAgentSpec });
127
+ expect(createDraftSession).toHaveBeenCalledWith({ agentSpec: liveAgentSpec });
121
128
  });
122
129
 
123
130
  it("falls back to model name for title when draft has no title", async () => {
124
- const get = vi.fn().mockResolvedValue({
125
- data: mockDraft("d1", undefined, "2026-06-30T10:00:00.000Z"),
126
- });
127
- const gateway = {
128
- agents: { private: { draftSessions: { list: vi.fn(), create: vi.fn(), get } } },
129
- } as unknown as TrueFoundryGateway;
131
+ const getDraftSession = vi.fn().mockResolvedValue(
132
+ mockDraft("d1", undefined, "2026-06-30T10:00:00.000Z"),
133
+ );
134
+ const privateClient = {
135
+ listDraftSessions: vi.fn(),
136
+ createDraftSession: vi.fn(),
137
+ getDraftSession,
138
+ } as unknown as PrivateAgentSessionClient;
130
139
 
131
140
  const adapter = createTrueFoundryDraftThreadListAdapter({
132
- gateway,
141
+ privateClient,
133
142
  defaultAgentSpec,
134
143
  });
135
144
 
136
145
  const result = await adapter.fetch("d1");
137
146
 
147
+ expect(getDraftSession).toHaveBeenCalledWith({ draftSessionId: "d1" });
138
148
  expect(result.title).toBe("anthropic/claude-sonnet-4-6");
139
149
  });
140
150
  });
@@ -1,5 +1,5 @@
1
1
  import type { RemoteThreadListAdapter } from "@assistant-ui/core";
2
- import type { TrueFoundryGateway } from "truefoundry-gateway-sdk";
2
+ import type { PrivateAgentSessionClient } from "truefoundry-gateway-sdk/agents/private";
3
3
 
4
4
  import { draftSessionTitle, type AgentSpec } from "./agentSpec.js";
5
5
  import { sessionListStartTimestamp } from "../sessionListStartTimestamp.js";
@@ -7,15 +7,15 @@ import { sessionListStartTimestamp } from "../sessionListStartTimestamp.js";
7
7
  const THREAD_LIST_PAGE_SIZE = 20;
8
8
 
9
9
  export function createTrueFoundryDraftThreadListAdapter(options: {
10
- gateway: TrueFoundryGateway;
10
+ privateClient: PrivateAgentSessionClient;
11
11
  defaultAgentSpec: AgentSpec;
12
12
  getAgentSpec?: () => AgentSpec;
13
13
  }): RemoteThreadListAdapter {
14
- const { gateway, defaultAgentSpec, getAgentSpec } = options;
14
+ const { privateClient, defaultAgentSpec, getAgentSpec } = options;
15
15
 
16
16
  return {
17
17
  async list({ after } = {}) {
18
- const page = await gateway.agents.private.draftSessions.list({
18
+ const page = await privateClient.listDraftSessions({
19
19
  limit: THREAD_LIST_PAGE_SIZE,
20
20
  pageToken: after,
21
21
  startTimestamp: sessionListStartTimestamp(),
@@ -33,16 +33,16 @@ export function createTrueFoundryDraftThreadListAdapter(options: {
33
33
  },
34
34
 
35
35
  async initialize(_threadId: string) {
36
- const response = await gateway.agents.private.draftSessions.create({
36
+ const draft = await privateClient.createDraftSession({
37
37
  agentSpec: getAgentSpec?.() ?? defaultAgentSpec,
38
38
  });
39
- const draft = response.data;
40
39
  return { remoteId: draft.id, externalId: undefined };
41
40
  },
42
41
 
43
42
  async fetch(remoteId) {
44
- const response = await gateway.agents.private.draftSessions.get(remoteId);
45
- const draft = response.data;
43
+ const draft = await privateClient.getDraftSession({
44
+ draftSessionId: remoteId,
45
+ });
46
46
  return {
47
47
  status: "regular" as const,
48
48
  remoteId: draft.id,
package/src/sessions.ts CHANGED
@@ -2,15 +2,15 @@ import type {
2
2
  AgentSession,
3
3
  AgentSessionClient,
4
4
  } from "truefoundry-gateway-sdk/agents";
5
- import type { TrueFoundryGateway } from "truefoundry-gateway-sdk";
5
+ import type { PrivateAgentSessionClient } from "truefoundry-gateway-sdk/agents/private";
6
6
 
7
7
  import { bindDraftAgentSession } from "./private/bindDraftAgentSession.js";
8
8
 
9
9
  const inflightBySessionId = new Map<string, Promise<AgentSession>>();
10
10
 
11
11
  export type GetSessionOptions = {
12
- /** When set, validates the draft and binds turns to `/agents/sessions/{id}/turns`. */
13
- draftGateway?: TrueFoundryGateway;
12
+ /** When set, validates the draft and binds turns via PrivateAgentSessionClient. */
13
+ privateClient?: PrivateAgentSessionClient;
14
14
  };
15
15
 
16
16
  /** `sessionId` is the assistant-ui thread `remoteId` from `RemoteThreadListAdapter.initialize`. */
@@ -19,8 +19,8 @@ export function getSession(
19
19
  sessionId: string,
20
20
  options?: GetSessionOptions,
21
21
  ): Promise<AgentSession> {
22
- if (options?.draftGateway != null) {
23
- return bindDraftAgentSession(client, options.draftGateway, sessionId);
22
+ if (options?.privateClient != null) {
23
+ return bindDraftAgentSession(options.privateClient, sessionId);
24
24
  }
25
25
 
26
26
  let inflight = inflightBySessionId.get(sessionId);
package/src/streamTurn.ts CHANGED
@@ -53,6 +53,13 @@ export async function* streamTurnContent(
53
53
  options: StreamTurnOptions,
54
54
  abortSignal: AbortSignal,
55
55
  groupRootBaseline?: readonly string[],
56
+ /**
57
+ * Called once with the gateway-assigned turn ID as soon as it becomes
58
+ * available (after the first `turn.created` SSE event). Use this to
59
+ * reconcile the locally-generated optimistic ID with the real gateway ID
60
+ * so that edit/retry can find the turn in `buildSnapshotBeforeTurn`.
61
+ */
62
+ onTurnIdAvailable?: (turnId: string) => void,
56
63
  ): AsyncGenerator<TurnStreamUpdate> {
57
64
  const previousTurnId =
58
65
  options.previousTurnId === null
@@ -69,7 +76,8 @@ export async function* streamTurnContent(
69
76
  }
70
77
 
71
78
  try {
72
- yield* streamTurnEvents(
79
+ let turnIdNotified = false;
80
+ for await (const update of streamTurnEvents(
73
81
  turn.execute(
74
82
  { stream: true },
75
83
  {
@@ -79,7 +87,20 @@ export async function* streamTurnContent(
79
87
  ),
80
88
  foldState,
81
89
  groupRootBaseline,
82
- );
90
+ )) {
91
+ // After the first `turn.created` event, `turn.id` is set.
92
+ // Notify BEFORE yielding so the caller can update its tracking
93
+ // before the snapshot is written with the stream update.
94
+ if (!turnIdNotified && turn.id != null) {
95
+ onTurnIdAvailable?.(turn.id);
96
+ turnIdNotified = true;
97
+ }
98
+ yield update;
99
+ }
100
+ // Handle streams that complete without yielding any content.
101
+ if (!turnIdNotified && turn.id != null) {
102
+ onTurnIdAvailable?.(turn.id);
103
+ }
83
104
  } catch (error) {
84
105
  if (error instanceof Error && error.name === "AbortError") {
85
106
  return;
@@ -0,0 +1,100 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+
3
+ import type { PrivateAgentSessionClient } from "truefoundry-gateway-sdk/agents/private";
4
+
5
+ import { createTrueFoundryOwnedSessionsThreadListAdapter } from "./truefoundryOwnedSessionsThreadListAdapter.js";
6
+
7
+ function mockNamedSession(id: string, title: string, updatedAt: string) {
8
+ return {
9
+ type: "session" as const,
10
+ id,
11
+ agentName: "my-agent",
12
+ title,
13
+ createdBySubject: { type: "user" as const, id: "u1" },
14
+ createdAt: updatedAt,
15
+ updatedAt,
16
+ };
17
+ }
18
+
19
+ function mockDraftSession(id: string, title: string | undefined, updatedAt: string) {
20
+ return {
21
+ type: "session/draft" as const,
22
+ id,
23
+ agentSpec: { model: { name: "anthropic/claude-sonnet-4-6" } },
24
+ title,
25
+ createdBySubject: { type: "user" as const, id: "u1" },
26
+ createdAt: updatedAt,
27
+ updatedAt,
28
+ };
29
+ }
30
+
31
+ describe("createTrueFoundryOwnedSessionsThreadListAdapter", () => {
32
+ it("lists owned sessions (named + draft) with pagination cursor", async () => {
33
+ const listOwnedSessions = vi.fn().mockResolvedValue({
34
+ data: [
35
+ mockNamedSession("s1", "Named chat", "2026-06-30T12:00:00.000Z"),
36
+ mockDraftSession("d1", "Draft chat", "2026-06-30T11:00:00.000Z"),
37
+ ],
38
+ response: {
39
+ pagination: { nextPageToken: "page-2", limit: 20 },
40
+ },
41
+ });
42
+ const privateClient = {
43
+ listOwnedSessions,
44
+ getDraftSession: vi.fn(),
45
+ } as unknown as PrivateAgentSessionClient;
46
+
47
+ const adapter = createTrueFoundryOwnedSessionsThreadListAdapter({ privateClient });
48
+ const result = await adapter.list();
49
+
50
+ expect(listOwnedSessions).toHaveBeenCalledWith(
51
+ expect.objectContaining({
52
+ limit: 20,
53
+ pageToken: undefined,
54
+ startTimestamp: expect.any(String),
55
+ }),
56
+ );
57
+ expect(result.threads).toEqual([
58
+ {
59
+ status: "regular",
60
+ remoteId: "s1",
61
+ title: "Named chat",
62
+ lastMessageAt: new Date("2026-06-30T12:00:00.000Z"),
63
+ },
64
+ {
65
+ status: "regular",
66
+ remoteId: "d1",
67
+ title: "Draft chat",
68
+ lastMessageAt: new Date("2026-06-30T11:00:00.000Z"),
69
+ },
70
+ ]);
71
+ expect(result.nextCursor).toBe("page-2");
72
+ });
73
+
74
+ it("falls back to model name for untitled drafts", async () => {
75
+ const listOwnedSessions = vi.fn().mockResolvedValue({
76
+ data: [mockDraftSession("d1", undefined, "2026-06-30T11:00:00.000Z")],
77
+ response: { pagination: { limit: 20 } },
78
+ });
79
+ const privateClient = {
80
+ listOwnedSessions,
81
+ getDraftSession: vi.fn(),
82
+ } as unknown as PrivateAgentSessionClient;
83
+
84
+ const adapter = createTrueFoundryOwnedSessionsThreadListAdapter({ privateClient });
85
+ const result = await adapter.list();
86
+
87
+ expect(result.threads[0]?.title).toBe("anthropic/claude-sonnet-4-6");
88
+ });
89
+
90
+ it("throws on initialize because the adapter is read-only", async () => {
91
+ const privateClient = {
92
+ listOwnedSessions: vi.fn(),
93
+ getDraftSession: vi.fn(),
94
+ } as unknown as PrivateAgentSessionClient;
95
+
96
+ const adapter = createTrueFoundryOwnedSessionsThreadListAdapter({ privateClient });
97
+
98
+ await expect(adapter.initialize("local")).rejects.toThrow(/read-only/);
99
+ });
100
+ });