@truefoundry/assistant-ui-runtime 0.1.9 → 0.1.10

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 (33) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/{chunk-CXBZ6WLZ.js → chunk-6ZOGBD7E.js} +99 -17
  3. package/dist/chunk-6ZOGBD7E.js.map +1 -0
  4. package/dist/index.d.ts +26 -5
  5. package/dist/index.js +92 -71
  6. package/dist/index.js.map +1 -1
  7. package/dist/plugins/truefoundry-agent-server-adapter/index.d.ts +1 -1
  8. package/dist/plugins/truefoundry-agent-server-adapter/index.js +1 -1
  9. package/dist/server/index.d.ts +2 -2
  10. package/dist/{types-6yWuWHzK.d.ts → types-Bb8Kaf1h.d.ts} +16 -3
  11. package/package.json +3 -3
  12. package/src/convertTurnMessages.test.ts +23 -1
  13. package/src/convertTurnMessages.ts +9 -3
  14. package/src/draft/truefoundryDraftThreadListAdapter.test.ts +57 -4
  15. package/src/draft/truefoundryDraftThreadListAdapter.ts +18 -22
  16. package/src/hooks.ts +23 -3
  17. package/src/index.ts +1 -0
  18. package/src/messageCustomMetadata.ts +2 -0
  19. package/src/plugins/truefoundry-agent-server-adapter/chatServer.ts +7 -6
  20. package/src/plugins/truefoundry-agent-server-adapter/cp.test.ts +169 -6
  21. package/src/plugins/truefoundry-agent-server-adapter/cp.ts +126 -1
  22. package/src/plugins/truefoundry-agent-server-adapter/normalizeAgentSpec.test.ts +34 -0
  23. package/src/plugins/truefoundry-agent-server-adapter/normalizeAgentSpec.ts +12 -5
  24. package/src/server/types.ts +18 -6
  25. package/src/sessionThreadMetadata.ts +39 -0
  26. package/src/truefoundryExtras.ts +1 -1
  27. package/src/truefoundryOwnedSessionsThreadListAdapter.test.ts +16 -0
  28. package/src/truefoundryOwnedSessionsThreadListAdapter.ts +13 -25
  29. package/src/truefoundryThreadListAdapter.test.ts +17 -9
  30. package/src/truefoundryThreadListAdapter.ts +9 -14
  31. package/src/types.ts +5 -0
  32. package/src/useTrueFoundryAgentRuntime.ts +17 -4
  33. package/dist/chunk-CXBZ6WLZ.js.map +0 -1
@@ -335,9 +335,126 @@ export async function listMcpServers(
335
335
  // ---------------------------------------------------------------------------
336
336
 
337
337
  type RawAgent = {
338
+ id?: string;
338
339
  name?: string;
340
+ latestVersionDetails?: {
341
+ manifest?: unknown;
342
+ };
343
+ /** Defensive: some payloads nest manifest at the top level. */
344
+ manifest?: unknown;
339
345
  };
340
346
 
347
+ function isRecord(value: unknown): value is Record<string, unknown> {
348
+ return value != null && typeof value === "object" && !Array.isArray(value);
349
+ }
350
+
351
+ function snakeToCamelKey(key: string): string {
352
+ return key.replace(/_([a-z])/g, (_, letter: string) => letter.toUpperCase());
353
+ }
354
+
355
+ /** Deep snake_case → camelCase (inverse of {@link toSnakeCaseDeep}). */
356
+ export function toCamelCaseDeep(value: unknown): unknown {
357
+ if (Array.isArray(value)) {
358
+ return value.map(toCamelCaseDeep);
359
+ }
360
+ if (isRecord(value)) {
361
+ const out: Record<string, unknown> = {};
362
+ for (const [k, v] of Object.entries(value)) {
363
+ out[snakeToCamelKey(k)] = toCamelCaseDeep(v);
364
+ }
365
+ return out;
366
+ }
367
+ return value;
368
+ }
369
+
370
+ /**
371
+ * Map a CP AgentManifest (snake_case wire) → FE AgentSpec for Edit seeding.
372
+ * Skills/MCP keep `{ id, name }` for draft pickers plus runtime fields
373
+ * (`enableTools`, `preload`, `config`) so Edit → Save does not widen tool
374
+ * access or wipe settings. Gateway registry shapes are restored on save via
375
+ * normalizeAgentSpecForGateway.
376
+ */
377
+ export function agentSpecFromCpManifest(manifest: unknown): TfyAgentSpec | undefined {
378
+ if (!isRecord(manifest)) return undefined;
379
+ const modelRaw = manifest.model;
380
+ if (!isRecord(modelRaw) || typeof modelRaw.name !== "string" || modelRaw.name === "") {
381
+ return undefined;
382
+ }
383
+ const model = toCamelCaseDeep(modelRaw) as TfyAgentSpec["model"];
384
+
385
+ // Catalog-shaped mounts (`id`/`name` + runtime fields). Not yet gateway
386
+ // registry unions — normalizeAgentSpecForGateway restores those on save.
387
+ const skills: Array<Record<string, unknown>> = [];
388
+ if (Array.isArray(manifest.skills)) {
389
+ for (const row of manifest.skills) {
390
+ if (!isRecord(row)) continue;
391
+ const fqn =
392
+ typeof row.fqn === "string" && row.fqn !== ""
393
+ ? row.fqn
394
+ : typeof row.id === "string" && row.id !== ""
395
+ ? row.id
396
+ : null;
397
+ if (fqn == null) continue;
398
+ const camel = toCamelCaseDeep(row) as Record<string, unknown>;
399
+ const name =
400
+ typeof camel.name === "string" && camel.name !== ""
401
+ ? camel.name
402
+ : fqn;
403
+ skills.push({
404
+ id: fqn,
405
+ name,
406
+ ...(typeof camel.preload === "boolean"
407
+ ? { preload: camel.preload }
408
+ : {}),
409
+ ...(camel.config != null ? { config: camel.config } : {}),
410
+ });
411
+ }
412
+ }
413
+
414
+ const mcpServers: Array<Record<string, unknown>> = [];
415
+ const mcpRaw = Array.isArray(manifest.mcp_servers)
416
+ ? manifest.mcp_servers
417
+ : Array.isArray(manifest.mcpServers)
418
+ ? manifest.mcpServers
419
+ : [];
420
+ for (const row of mcpRaw) {
421
+ if (!isRecord(row)) continue;
422
+ const camel = toCamelCaseDeep(row) as Record<string, unknown>;
423
+ const name =
424
+ typeof camel.name === "string" && camel.name !== ""
425
+ ? camel.name
426
+ : typeof camel.id === "string" && camel.id !== ""
427
+ ? camel.id
428
+ : null;
429
+ if (name == null) continue;
430
+ mcpServers.push({
431
+ id: name,
432
+ name,
433
+ ...(Array.isArray(camel.enableTools)
434
+ ? { enableTools: camel.enableTools }
435
+ : {}),
436
+ ...(typeof camel.preload === "boolean"
437
+ ? { preload: camel.preload }
438
+ : {}),
439
+ ...(camel.config != null ? { config: camel.config } : {}),
440
+ });
441
+ }
442
+
443
+ const configRaw = manifest.config;
444
+ const config =
445
+ configRaw != null ? (toCamelCaseDeep(configRaw) as TfyAgentSpec["config"]) : undefined;
446
+
447
+ return {
448
+ model,
449
+ ...(typeof manifest.instructions === "string"
450
+ ? { instructions: manifest.instructions }
451
+ : {}),
452
+ ...(config != null ? { config } : {}),
453
+ ...(skills.length > 0 ? { skills } : {}),
454
+ ...(mcpServers.length > 0 ? { mcpServers } : {}),
455
+ } as TfyAgentSpec;
456
+ }
457
+
341
458
  export function normalizeAgents(raw: unknown): TfyAgentSelectorEntry[] {
342
459
  const data =
343
460
  raw != null &&
@@ -348,7 +465,15 @@ export function normalizeAgents(raw: unknown): TfyAgentSelectorEntry[] {
348
465
  const out: TfyAgentSelectorEntry[] = [];
349
466
  for (const row of data) {
350
467
  if (row.name == null || row.name === "") continue;
351
- out.push({ name: row.name });
468
+ const manifest = row.latestVersionDetails?.manifest ?? row.manifest;
469
+ const agentSpec = agentSpecFromCpManifest(manifest);
470
+ const agentId =
471
+ typeof row.id === "string" && row.id !== "" ? row.id : row.name;
472
+ out.push({
473
+ name: row.name,
474
+ agentId,
475
+ ...(agentSpec != null ? { agentSpec } : {}),
476
+ });
352
477
  }
353
478
  return out;
354
479
  }
@@ -29,6 +29,24 @@ describe("normalizeMcpMount", () => {
29
29
  name: "deepwiki-mcp",
30
30
  });
31
31
  });
32
+
33
+ it("forwards enableTools / preload / config from catalog rows", () => {
34
+ expect(
35
+ normalizeMcpMount({
36
+ id: "gmail",
37
+ name: "gmail",
38
+ enableTools: ["@read-only"],
39
+ preload: true,
40
+ config: { locale: "en" },
41
+ }),
42
+ ).toEqual({
43
+ type: "truefoundry-mcp-registry",
44
+ name: "gmail",
45
+ enableTools: ["@read-only"],
46
+ preload: true,
47
+ config: { locale: "en" },
48
+ });
49
+ });
32
50
  });
33
51
 
34
52
  describe("normalizeSkillMount", () => {
@@ -57,6 +75,22 @@ describe("normalizeSkillMount", () => {
57
75
  fqn: "agent-skill:truefoundry/skills/web:1",
58
76
  });
59
77
  });
78
+
79
+ it("forwards preload / config from catalog rows, including preload false", () => {
80
+ expect(
81
+ normalizeSkillMount({
82
+ id: "agent-skill:truefoundry/skills/web:1",
83
+ name: "web",
84
+ preload: false,
85
+ config: { timeoutMs: 1000 },
86
+ }),
87
+ ).toEqual({
88
+ type: "truefoundry-skills-registry",
89
+ fqn: "agent-skill:truefoundry/skills/web:1",
90
+ preload: false,
91
+ config: { timeoutMs: 1000 },
92
+ });
93
+ });
60
94
  });
61
95
 
62
96
  describe("normalizeAgentSpecForGateway", () => {
@@ -20,7 +20,7 @@ export function normalizeMcpMount(
20
20
  throw new Error("mcpServers entry must be an object");
21
21
  }
22
22
  if (raw.type === "truefoundry-mcp-registry" || raw.type === "inline") {
23
- return raw as TruefoundryGatewayApi.McpServer;
23
+ return raw as unknown as TruefoundryGatewayApi.McpServer;
24
24
  }
25
25
  const name =
26
26
  (typeof raw.name === "string" && raw.name !== "" ? raw.name : null) ??
@@ -33,7 +33,13 @@ export function normalizeMcpMount(
33
33
  "mcpServers entry needs name, mcpName, or id to mount as registry MCP",
34
34
  );
35
35
  }
36
- return { type: "truefoundry-mcp-registry", name };
36
+ return {
37
+ type: "truefoundry-mcp-registry",
38
+ name,
39
+ ...(Array.isArray(raw.enableTools) ? { enableTools: raw.enableTools } : {}),
40
+ ...(typeof raw.preload === "boolean" ? { preload: raw.preload } : {}),
41
+ ...(raw.config != null ? { config: raw.config } : {}),
42
+ } as unknown as TruefoundryGatewayApi.McpServer;
37
43
  }
38
44
 
39
45
  export function normalizeSkillMount(
@@ -43,7 +49,7 @@ export function normalizeSkillMount(
43
49
  throw new Error("skills entry must be an object");
44
50
  }
45
51
  if (raw.type === "truefoundry-skills-registry" || raw.type === "git") {
46
- return raw as TruefoundryGatewayApi.SkillMount;
52
+ return raw as unknown as TruefoundryGatewayApi.SkillMount;
47
53
  }
48
54
  const fqn =
49
55
  (typeof raw.fqn === "string" && raw.fqn !== "" ? raw.fqn : null) ??
@@ -56,8 +62,9 @@ export function normalizeSkillMount(
56
62
  return {
57
63
  type: "truefoundry-skills-registry",
58
64
  fqn,
59
- ...(raw.preload === true ? { preload: true } : {}),
60
- };
65
+ ...(typeof raw.preload === "boolean" ? { preload: raw.preload } : {}),
66
+ ...(raw.config != null ? { config: raw.config } : {}),
67
+ } as unknown as TruefoundryGatewayApi.SkillMount;
61
68
  }
62
69
 
63
70
  /** Rewrite UI `{id,name}` mounts so create/update draft session can serialize. */
@@ -38,9 +38,13 @@ export interface ConnectorSelectorEntry {
38
38
  description?: string;
39
39
  }
40
40
 
41
- /** Agent selector row — UI shows name only. Host extends for metadata. */
41
+ /** Agent selector row. Host extends for metadata; `agentSpec` enables Edit. */
42
42
  export interface AgentSelectorEntry {
43
43
  name: string;
44
+ /** Stable id when distinct from display `name`. Falls back to `name` when omitted. */
45
+ agentId?: string;
46
+ /** Published agent spec — required for Edit; optional for Try-only hosts. */
47
+ agentSpec?: AgentSpec;
44
48
  }
45
49
 
46
50
  export type SearchAgentSelectorParams = {
@@ -147,7 +151,8 @@ export type PageParams = {
147
151
  };
148
152
 
149
153
  export interface ListSessionsParams extends PageParams {
150
- agentName?: string;
154
+ /** Host-owned agent identity filter. Hosts that key agents by name pass that name here. */
155
+ agentId?: string;
151
156
  /** Host-specific filter (e.g. TFY startTimestamp). */
152
157
  startTimestamp?: string;
153
158
  }
@@ -291,10 +296,17 @@ export interface AgentChatServer<
291
296
  abortSignal?: AbortSignal;
292
297
  }): AsyncIterable<TurnStreamData>;
293
298
 
294
- downloadSandboxFile?(
295
- sandboxId: string,
296
- req: { path: string },
297
- ): Promise<Blob>;
299
+ /**
300
+ * Reads a file the agent wrote inside its sandbox. Hosts whose download route is scoped to a
301
+ * turn resolve the sandbox from `turnId` and ignore `sandboxId`; hosts addressing sandboxes
302
+ * directly use `sandboxId`.
303
+ */
304
+ downloadSandboxFile?(req: {
305
+ sessionId: string;
306
+ turnId: string;
307
+ sandboxId: string;
308
+ path: string;
309
+ }): Promise<Blob>;
298
310
  }
299
311
 
300
312
  /**
@@ -0,0 +1,39 @@
1
+ import type { RemoteThreadMetadata } from "@assistant-ui/core";
2
+
3
+ import { draftSessionTitle } from "./draft/agentSpec.js";
4
+ import type { AgentSpec, Session } from "./server/types.js";
5
+
6
+ /**
7
+ * Title for a session row in a mixed (draft + named) thread list.
8
+ * Mutable sessions use draftSessionTitle (optionally falling back to
9
+ * `defaultAgentSpec`); named sessions use title → agentName → id.
10
+ */
11
+ export function sessionDisplayTitle(
12
+ session: Session,
13
+ defaultAgentSpec?: AgentSpec,
14
+ ): string {
15
+ if (session.isMutable) {
16
+ const agentSpec = session.agentSpec ?? defaultAgentSpec;
17
+ if (agentSpec != null) {
18
+ return draftSessionTitle({
19
+ title: session.title,
20
+ agentSpec,
21
+ });
22
+ }
23
+ }
24
+ return session.title ?? session.agentName ?? session.id;
25
+ }
26
+
27
+ /** Map a Session DTO onto RemoteThreadMetadata, including display agentName in custom. */
28
+ export function sessionToThreadMetadata(
29
+ session: Session,
30
+ title: string | undefined,
31
+ ): RemoteThreadMetadata {
32
+ return {
33
+ status: "regular",
34
+ remoteId: session.id,
35
+ title,
36
+ lastMessageAt: new Date(session.updatedAt),
37
+ ...(session.agentName != null ? { custom: { agentName: session.agentName } } : {}),
38
+ };
39
+ }
@@ -26,7 +26,7 @@ export type TrueFoundryRuntimeExtras = {
26
26
  respondToToolApproval: (response: RespondToToolApprovalOptions) => void;
27
27
  respondToToolResponse: (response: RespondToToolResponseOptions) => void;
28
28
  resumeMcpAuth: () => Promise<void>;
29
- downloadSandboxFile: (path: string) => Promise<Blob>;
29
+ downloadSandboxFile: (req: { turnId: string; path: string }) => Promise<Blob>;
30
30
  cancel: () => Promise<void>;
31
31
  resetFromTurn: (turnId: string) => Promise<void>;
32
32
  reload: () => void;
@@ -61,6 +61,7 @@ describe("createTrueFoundryOwnedSessionsThreadListAdapter", () => {
61
61
  remoteId: "s1",
62
62
  title: "Named chat",
63
63
  lastMessageAt: new Date("2026-06-30T12:00:00.000Z"),
64
+ custom: { agentName: "my-agent" },
64
65
  },
65
66
  {
66
67
  status: "regular",
@@ -72,6 +73,21 @@ describe("createTrueFoundryOwnedSessionsThreadListAdapter", () => {
72
73
  expect(result.nextCursor).toBe("page-2");
73
74
  });
74
75
 
76
+ it("forwards listSessionsAgentId as agentId", async () => {
77
+ const listSessions = vi.fn().mockResolvedValue({ data: [] });
78
+ const server = mockServer({ listSessions, getSession: vi.fn() });
79
+
80
+ const adapter = createTrueFoundryOwnedSessionsThreadListAdapter({
81
+ server,
82
+ listSessionsAgentId: "agent-x",
83
+ });
84
+ await adapter.list();
85
+
86
+ expect(listSessions).toHaveBeenCalledWith(
87
+ expect.objectContaining({ agentId: "agent-x" }),
88
+ );
89
+ });
90
+
75
91
  it("falls back to model name for untitled drafts", async () => {
76
92
  const listSessions = vi.fn().mockResolvedValue({
77
93
  data: [mockDraftSession("d1", undefined, "2026-06-30T11:00:00.000Z")],
@@ -1,43 +1,36 @@
1
1
  import type { RemoteThreadListAdapter } from "@assistant-ui/core";
2
2
 
3
- import type { AgentChatServer, Session } from "./server/types.js";
4
- import { draftSessionTitle } from "./draft/agentSpec.js";
3
+ import type { AgentChatServer } from "./server/types.js";
5
4
  import { sessionListStartTimestamp } from "./sessionListStartTimestamp.js";
5
+ import {
6
+ sessionDisplayTitle,
7
+ sessionToThreadMetadata,
8
+ } from "./sessionThreadMetadata.js";
6
9
 
7
10
  const THREAD_LIST_PAGE_SIZE = 20;
8
11
 
9
- function ownedSessionTitle(session: Session): string {
10
- if (session.isMutable && session.agentSpec != null) {
11
- return draftSessionTitle({
12
- title: session.title,
13
- agentSpec: session.agentSpec,
14
- });
15
- }
16
- return session.title ?? session.agentName ?? session.id;
17
- }
18
-
19
12
  /**
20
13
  * Read-only thread-list adapter backed by `AgentChatServer.listSessions`.
21
14
  * Hosts that previously used listOwnedSessions should filter in their server impl.
22
15
  */
23
16
  export function createTrueFoundryOwnedSessionsThreadListAdapter(options: {
24
17
  server: AgentChatServer;
18
+ /** When set, filters `listSessions` by this agent id. Omit for all chats. */
19
+ listSessionsAgentId?: string;
25
20
  }): RemoteThreadListAdapter {
26
- const { server } = options;
21
+ const { server, listSessionsAgentId } = options;
27
22
 
28
23
  return {
29
24
  async list({ after } = {}) {
30
25
  const page = await server.listSessions({
26
+ ...(listSessionsAgentId != null ? { agentId: listSessionsAgentId } : {}),
31
27
  limit: THREAD_LIST_PAGE_SIZE,
32
28
  pageToken: after,
33
29
  startTimestamp: sessionListStartTimestamp(),
34
30
  });
35
- const threads = page.data.map((session) => ({
36
- status: "regular" as const,
37
- remoteId: session.id,
38
- title: ownedSessionTitle(session),
39
- lastMessageAt: new Date(session.updatedAt),
40
- }));
31
+ const threads = page.data.map((session) =>
32
+ sessionToThreadMetadata(session, sessionDisplayTitle(session)),
33
+ );
41
34
  return {
42
35
  threads,
43
36
  nextCursor: page.nextPageToken ?? undefined,
@@ -52,12 +45,7 @@ export function createTrueFoundryOwnedSessionsThreadListAdapter(options: {
52
45
 
53
46
  async fetch(remoteId) {
54
47
  const session = await server.getSession({ sessionId: remoteId });
55
- return {
56
- status: "regular" as const,
57
- remoteId: session.id,
58
- title: ownedSessionTitle(session),
59
- lastMessageAt: new Date(session.updatedAt),
60
- };
48
+ return sessionToThreadMetadata(session, sessionDisplayTitle(session));
61
49
  },
62
50
 
63
51
  async rename() {},
@@ -4,13 +4,19 @@ import type { AgentChatServer, Session } from "./server/index.js";
4
4
 
5
5
  import { createTrueFoundryThreadListAdapter } from "./truefoundryThreadListAdapter.js";
6
6
 
7
- function mockSession(id: string, title: string, updatedAt: string): Session {
7
+ function mockSession(
8
+ id: string,
9
+ title: string,
10
+ updatedAt: string,
11
+ agentName?: string,
12
+ ): Session {
8
13
  return {
9
14
  id,
10
15
  title,
11
16
  updatedAt,
12
17
  createdAt: updatedAt,
13
18
  isMutable: false,
19
+ ...(agentName != null ? { agentName } : {}),
14
20
  };
15
21
  }
16
22
 
@@ -26,10 +32,10 @@ function mockServer(partial: Partial<AgentChatServer>): AgentChatServer {
26
32
  }
27
33
 
28
34
  describe("createTrueFoundryThreadListAdapter", () => {
29
- it("lists the first page with limit and returns nextCursor", async () => {
35
+ it("lists the first page without agentId when filter is omitted", async () => {
30
36
  const listSessions = vi.fn().mockResolvedValue(
31
37
  mockListSessionsPage(
32
- [mockSession("s1", "First", "2026-06-30T10:00:00.000Z")],
38
+ [mockSession("s1", "First", "2026-06-30T10:00:00.000Z", "my-agent")],
33
39
  "page-2",
34
40
  ),
35
41
  );
@@ -43,40 +49,42 @@ describe("createTrueFoundryThreadListAdapter", () => {
43
49
 
44
50
  expect(listSessions).toHaveBeenCalledWith(
45
51
  expect.objectContaining({
46
- agentName: "my-agent",
47
52
  limit: 20,
48
53
  pageToken: undefined,
49
54
  startTimestamp: expect.any(String),
50
55
  }),
51
56
  );
57
+ expect(listSessions).toHaveBeenCalledWith(
58
+ expect.not.objectContaining({ agentId: expect.anything() }),
59
+ );
52
60
  expect(result.threads).toEqual([
53
61
  {
54
62
  status: "regular",
55
63
  remoteId: "s1",
56
64
  title: "First",
57
65
  lastMessageAt: new Date("2026-06-30T10:00:00.000Z"),
66
+ custom: { agentName: "my-agent" },
58
67
  },
59
68
  ]);
60
69
  expect(result.nextCursor).toBe("page-2");
61
70
  });
62
71
 
63
- it("forwards after as pageToken for subsequent pages", async () => {
72
+ it("forwards listSessionsAgentId as agentId", async () => {
64
73
  const listSessions = vi.fn().mockResolvedValue(
65
- mockListSessionsPage(
66
- [mockSession("s2", "Second", "2026-06-29T10:00:00.000Z")],
67
- ),
74
+ mockListSessionsPage([mockSession("s2", "Second", "2026-06-29T10:00:00.000Z")]),
68
75
  );
69
76
  const server = mockServer({ listSessions });
70
77
  const adapter = createTrueFoundryThreadListAdapter({
71
78
  server,
72
79
  agentName: "my-agent",
80
+ listSessionsAgentId: "filter-agent",
73
81
  });
74
82
 
75
83
  const result = await adapter.list({ after: "page-2" });
76
84
 
77
85
  expect(listSessions).toHaveBeenCalledWith(
78
86
  expect.objectContaining({
79
- agentName: "my-agent",
87
+ agentId: "filter-agent",
80
88
  limit: 20,
81
89
  pageToken: "page-2",
82
90
  }),
@@ -3,29 +3,29 @@ import type { RemoteThreadListAdapter } from "@assistant-ui/core";
3
3
  import type { AgentChatServer } from "./server/types.js";
4
4
  import { getSession } from "./sessions.js";
5
5
  import { sessionListStartTimestamp } from "./sessionListStartTimestamp.js";
6
+ import { sessionToThreadMetadata } from "./sessionThreadMetadata.js";
6
7
 
7
8
  const THREAD_LIST_PAGE_SIZE = 20;
8
9
 
9
10
  export function createTrueFoundryThreadListAdapter(options: {
10
11
  server: AgentChatServer;
11
12
  agentName: string;
13
+ /** When set, filters `listSessions` by this agent id. Omit for all chats. */
14
+ listSessionsAgentId?: string;
12
15
  }): RemoteThreadListAdapter {
13
- const { server, agentName } = options;
16
+ const { server, agentName, listSessionsAgentId } = options;
14
17
 
15
18
  return {
16
19
  async list({ after } = {}) {
17
20
  const page = await server.listSessions({
18
- agentName,
21
+ ...(listSessionsAgentId != null ? { agentId: listSessionsAgentId } : {}),
19
22
  limit: THREAD_LIST_PAGE_SIZE,
20
23
  pageToken: after,
21
24
  startTimestamp: sessionListStartTimestamp(),
22
25
  });
23
- const threads = page.data.map((session) => ({
24
- status: "regular" as const,
25
- remoteId: session.id,
26
- title: session.title ?? undefined,
27
- lastMessageAt: new Date(session.updatedAt),
28
- }));
26
+ const threads = page.data.map((session) =>
27
+ sessionToThreadMetadata(session, session.title ?? undefined),
28
+ );
29
29
  return {
30
30
  threads,
31
31
  nextCursor: page.nextPageToken ?? undefined,
@@ -39,12 +39,7 @@ export function createTrueFoundryThreadListAdapter(options: {
39
39
 
40
40
  async fetch(remoteId) {
41
41
  const session = await getSession(server, remoteId);
42
- return {
43
- status: "regular" as const,
44
- remoteId: session.id,
45
- title: session.title ?? undefined,
46
- lastMessageAt: new Date(session.updatedAt),
47
- };
42
+ return sessionToThreadMetadata(session, session.title ?? undefined);
48
43
  },
49
44
 
50
45
  async rename() {},
package/src/types.ts CHANGED
@@ -29,6 +29,11 @@ type TrueFoundryAgentRuntimeBaseOptions = ExternalStoreSharedOptions & {
29
29
  onThreadIdChange?: ((threadId: string | undefined) => void) | undefined;
30
30
  onError?: ((error: unknown) => void) | undefined;
31
31
  listEventsConcurrency?: number | undefined;
32
+ /**
33
+ * Optional filter forwarded to `listSessions({ agentId })`.
34
+ * Omit for all chats; hosts that key agents by name pass that name as the id.
35
+ */
36
+ listSessionsAgentId?: string | undefined;
32
37
  adapters?:
33
38
  | {
34
39
  attachments?: AttachmentAdapter | undefined;
@@ -152,18 +152,28 @@ function useTrueFoundryAgentRuntimeImpl(
152
152
  );
153
153
 
154
154
  const downloadSandboxFile = useCallback(
155
- async (path: string) => {
155
+ async ({ turnId, path }: { turnId: string; path: string }) => {
156
156
  if (server.downloadSandboxFile == null) {
157
157
  throw new Error(
158
158
  "Downloading a sandbox file requires AgentChatServer.downloadSandboxFile.",
159
159
  );
160
160
  }
161
+ if (sessionId == null) {
162
+ throw new Error(
163
+ "This session has not been saved yet, so its files cannot be downloaded.",
164
+ );
165
+ }
161
166
  if (sandboxId == null) {
162
167
  throw new Error("No sandbox is available yet for this session.");
163
168
  }
164
- return await server.downloadSandboxFile(sandboxId, { path });
169
+ return await server.downloadSandboxFile({
170
+ sessionId,
171
+ turnId,
172
+ sandboxId,
173
+ path,
174
+ });
165
175
  },
166
- [server, sandboxId],
176
+ [server, sessionId, sandboxId],
167
177
  );
168
178
 
169
179
  const draftExtras = useMemo(() => {
@@ -272,6 +282,7 @@ export function useTrueFoundryAgentRuntime(options: UseTrueFoundryAgentRuntimeOp
272
282
 
273
283
  const agentMode = agent.mode;
274
284
  const namedAgentName = agent.mode === "named" ? agent.agentName : undefined;
285
+ const listSessionsAgentId = resolved.listSessionsAgentId;
275
286
  const threadListAdapter = useMemo(() => {
276
287
  if (agentMode === "draft") {
277
288
  const draftAgent = agent as Extract<typeof agent, { mode: "draft" }>;
@@ -279,14 +290,16 @@ export function useTrueFoundryAgentRuntime(options: UseTrueFoundryAgentRuntimeOp
279
290
  server,
280
291
  defaultAgentSpec: draftAgent.defaultAgentSpec,
281
292
  getAgentSpec: () => pendingAgentSpecRef.current ?? draftAgent.defaultAgentSpec,
293
+ listSessionsAgentId,
282
294
  });
283
295
  }
284
296
  return createTrueFoundryThreadListAdapter({
285
297
  server,
286
298
  agentName: namedAgentName!,
299
+ listSessionsAgentId,
287
300
  });
288
301
  // eslint-disable-next-line react-hooks/exhaustive-deps
289
- }, [agentMode, namedAgentName, server]);
302
+ }, [agentMode, namedAgentName, listSessionsAgentId, server]);
290
303
 
291
304
  return useRemoteThreadListRuntime({
292
305
  allowNesting: true,