@truefoundry/assistant-ui-runtime 0.1.10-rc.1 → 0.1.12

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 +66 -0
  2. package/dist/{chunk-BRPTG6VA.js → chunk-2NLIAJJX.js} +100 -12
  3. package/dist/chunk-2NLIAJJX.js.map +1 -0
  4. package/dist/index.d.ts +13 -2
  5. package/dist/index.js +70 -66
  6. package/dist/index.js.map +1 -1
  7. package/dist/plugins/truefoundry-agent-server-adapter/index.d.ts +3 -3
  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-CgZUnILu.d.ts → types-CSRQSnHu.d.ts} +148 -80
  11. package/package.json +3 -3
  12. package/src/convertTurnMessages.test.ts +123 -0
  13. package/src/convertTurnMessages.ts +8 -2
  14. package/src/draft/truefoundryDraftThreadListAdapter.test.ts +58 -4
  15. package/src/draft/truefoundryDraftThreadListAdapter.ts +18 -22
  16. package/src/index.ts +27 -0
  17. package/src/plugins/truefoundry-agent-server-adapter/chatServer.ts +1 -1
  18. package/src/plugins/truefoundry-agent-server-adapter/cp.test.ts +169 -6
  19. package/src/plugins/truefoundry-agent-server-adapter/cp.ts +126 -1
  20. package/src/plugins/truefoundry-agent-server-adapter/createTrueFoundryAgentUIServer.ts +7 -0
  21. package/src/plugins/truefoundry-agent-server-adapter/normalizeAgentSpec.test.ts +34 -0
  22. package/src/plugins/truefoundry-agent-server-adapter/normalizeAgentSpec.ts +12 -5
  23. package/src/plugins/truefoundry-agent-server-adapter/types.ts +2 -2
  24. package/src/server/index.ts +15 -0
  25. package/src/server/types.ts +448 -381
  26. package/src/sessionThreadMetadata.ts +42 -0
  27. package/src/truefoundryOwnedSessionsThreadListAdapter.test.ts +17 -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 +4 -1
  33. package/dist/chunk-BRPTG6VA.js.map +0 -1
@@ -1029,10 +1029,12 @@ function projectHistoryTurns(
1029
1029
  const messages: ThreadMessage[] = [];
1030
1030
  let lastAssistantIndex: number | undefined;
1031
1031
  let groupRootIds: string[] = [];
1032
+ let sandboxId: string | undefined;
1032
1033
 
1033
1034
  for (let turnIndex = 0; turnIndex < snapshot.turns.length; turnIndex++) {
1034
1035
  const record = snapshot.turns[turnIndex]!;
1035
1036
  const turnRootIds = record.rootModelMessageIds ?? [];
1037
+ sandboxId = record.sandboxId ?? sandboxId;
1036
1038
 
1037
1039
  if (record.userText) {
1038
1040
  groupRootIds = [...turnRootIds];
@@ -1080,7 +1082,7 @@ function projectHistoryTurns(
1080
1082
  const custom = {
1081
1083
  ...baseCustom,
1082
1084
  turnId: record.id,
1083
- ...(record.sandboxId != null ? { sandboxId: record.sandboxId } : {}),
1085
+ ...(sandboxId != null ? { sandboxId } : {}),
1084
1086
  };
1085
1087
 
1086
1088
  if (record.userText) {
@@ -1738,7 +1740,11 @@ export function turnStreamUpdateToAssistantMessage(
1738
1740
  unstable_annotations: [],
1739
1741
  unstable_data: [],
1740
1742
  steps: [],
1741
- custom: { ...update.metadata?.custom, turnId },
1743
+ custom: {
1744
+ ...(existing?.role === "assistant" ? existing.metadata.custom : {}),
1745
+ ...update.metadata?.custom,
1746
+ turnId,
1747
+ },
1742
1748
  },
1743
1749
  };
1744
1750
  }
@@ -10,7 +10,12 @@ const defaultAgentSpec: AgentSpec = {
10
10
  instructions: "You are helpful.",
11
11
  };
12
12
 
13
- function mockDraft(id: string, title: string | undefined, updatedAt: string): Session {
13
+ function mockDraft(
14
+ id: string,
15
+ title: string | undefined,
16
+ updatedAt: string,
17
+ extras?: Partial<Session>,
18
+ ): Session {
14
19
  return {
15
20
  id,
16
21
  agentSpec: defaultAgentSpec,
@@ -18,6 +23,7 @@ function mockDraft(id: string, title: string | undefined, updatedAt: string): Se
18
23
  createdAt: updatedAt,
19
24
  updatedAt,
20
25
  isMutable: true,
26
+ ...extras,
21
27
  };
22
28
  }
23
29
 
@@ -33,10 +39,22 @@ function mockServer(partial: Partial<AgentChatServer>): AgentChatServer {
33
39
  }
34
40
 
35
41
  describe("createTrueFoundryDraftThreadListAdapter", () => {
36
- it("lists draft sessions with pagination cursor", async () => {
42
+ it("lists sessions with pagination cursor and does not filter isMutable client-side", async () => {
37
43
  const listSessions = vi.fn().mockResolvedValue(
38
44
  mockDraftListPage(
39
- [mockDraft("d1", "My draft", "2026-06-30T10:00:00.000Z")],
45
+ [
46
+ mockDraft("d1", "My draft", "2026-06-30T10:00:00.000Z"),
47
+ mockDraft("s1", "Named", "2026-06-30T09:00:00.000Z", {
48
+ isMutable: false,
49
+ agentName: "from-sdk",
50
+ agentSpec: undefined,
51
+ }),
52
+ mockDraft("s2", undefined, "2026-06-30T08:00:00.000Z", {
53
+ isMutable: false,
54
+ agentName: "untitled-agent",
55
+ agentSpec: undefined,
56
+ }),
57
+ ],
40
58
  "page-2",
41
59
  ),
42
60
  );
@@ -61,7 +79,7 @@ describe("createTrueFoundryDraftThreadListAdapter", () => {
61
79
  }),
62
80
  );
63
81
  expect(listSessions).toHaveBeenCalledWith(
64
- expect.not.objectContaining({ agentName: expect.anything() }),
82
+ expect.not.objectContaining({ agentId: expect.anything() }),
65
83
  );
66
84
  expect(result.threads).toEqual([
67
85
  {
@@ -69,11 +87,47 @@ describe("createTrueFoundryDraftThreadListAdapter", () => {
69
87
  remoteId: "d1",
70
88
  title: "My draft",
71
89
  lastMessageAt: new Date("2026-06-30T10:00:00.000Z"),
90
+ custom: { isMutable: true },
91
+ },
92
+ {
93
+ status: "regular",
94
+ remoteId: "s1",
95
+ title: "Named",
96
+ lastMessageAt: new Date("2026-06-30T09:00:00.000Z"),
97
+ custom: { isMutable: false, agentName: "from-sdk" },
98
+ },
99
+ {
100
+ status: "regular",
101
+ remoteId: "s2",
102
+ title: "untitled-agent",
103
+ lastMessageAt: new Date("2026-06-30T08:00:00.000Z"),
104
+ custom: { isMutable: false, agentName: "untitled-agent" },
72
105
  },
73
106
  ]);
74
107
  expect(result.nextCursor).toBe("page-2");
75
108
  });
76
109
 
110
+ it("forwards listSessionsAgentId as agentId", async () => {
111
+ const listSessions = vi.fn().mockResolvedValue(mockDraftListPage([]));
112
+ const server = mockServer({
113
+ listSessions,
114
+ createSession: vi.fn(),
115
+ getSession: vi.fn(),
116
+ });
117
+
118
+ const adapter = createTrueFoundryDraftThreadListAdapter({
119
+ server,
120
+ defaultAgentSpec,
121
+ listSessionsAgentId: "agent-x",
122
+ });
123
+
124
+ await adapter.list();
125
+
126
+ expect(listSessions).toHaveBeenCalledWith(
127
+ expect.objectContaining({ agentId: "agent-x" }),
128
+ );
129
+ });
130
+
77
131
  it("creates a draft session on initialize", async () => {
78
132
  const createSession = vi.fn().mockResolvedValue(
79
133
  mockDraft("d-new", undefined, "2026-06-30T12:00:00.000Z"),
@@ -2,8 +2,11 @@ import type { RemoteThreadListAdapter } from "@assistant-ui/core";
2
2
 
3
3
  import type { AgentChatServer } from "../server/types.js";
4
4
  import type { AgentSpec } from "../server/types.js";
5
- import { draftSessionTitle } from "./agentSpec.js";
6
5
  import { sessionListStartTimestamp } from "../sessionListStartTimestamp.js";
6
+ import {
7
+ sessionDisplayTitle,
8
+ sessionToThreadMetadata,
9
+ } from "../sessionThreadMetadata.js";
7
10
 
8
11
  const THREAD_LIST_PAGE_SIZE = 20;
9
12
 
@@ -11,27 +14,25 @@ export function createTrueFoundryDraftThreadListAdapter(options: {
11
14
  server: AgentChatServer;
12
15
  defaultAgentSpec: AgentSpec;
13
16
  getAgentSpec?: () => AgentSpec;
17
+ /** When set, filters `listSessions` by this agent id. Omit for all chats. */
18
+ listSessionsAgentId?: string;
14
19
  }): RemoteThreadListAdapter {
15
- const { server, defaultAgentSpec, getAgentSpec } = options;
20
+ const { server, defaultAgentSpec, getAgentSpec, listSessionsAgentId } = options;
16
21
 
17
22
  return {
18
23
  async list({ after } = {}) {
19
24
  const page = await server.listSessions({
25
+ ...(listSessionsAgentId != null ? { agentId: listSessionsAgentId } : {}),
20
26
  limit: THREAD_LIST_PAGE_SIZE,
21
27
  pageToken: after,
22
28
  startTimestamp: sessionListStartTimestamp(),
23
29
  });
24
- const threads = page.data
25
- .filter((session) => session.isMutable)
26
- .map((draft) => ({
27
- status: "regular" as const,
28
- remoteId: draft.id,
29
- title: draftSessionTitle({
30
- title: draft.title,
31
- agentSpec: draft.agentSpec ?? defaultAgentSpec,
32
- }),
33
- lastMessageAt: new Date(draft.updatedAt),
34
- }));
30
+ const threads = page.data.map((session) =>
31
+ sessionToThreadMetadata(
32
+ session,
33
+ sessionDisplayTitle(session, defaultAgentSpec),
34
+ ),
35
+ );
35
36
  return {
36
37
  threads,
37
38
  nextCursor: page.nextPageToken ?? undefined,
@@ -47,15 +48,10 @@ export function createTrueFoundryDraftThreadListAdapter(options: {
47
48
 
48
49
  async fetch(remoteId) {
49
50
  const draft = await server.getSession({ sessionId: remoteId });
50
- return {
51
- status: "regular" as const,
52
- remoteId: draft.id,
53
- title: draftSessionTitle({
54
- title: draft.title,
55
- agentSpec: draft.agentSpec ?? defaultAgentSpec,
56
- }),
57
- lastMessageAt: new Date(draft.updatedAt),
58
- };
51
+ return sessionToThreadMetadata(
52
+ draft,
53
+ sessionDisplayTitle(draft, defaultAgentSpec),
54
+ );
59
55
  },
60
56
 
61
57
  async rename() {},
package/src/index.ts CHANGED
@@ -77,7 +77,9 @@ export { trueFoundryAttachmentAdapter } from "./attachmentAdapter.js";
77
77
 
78
78
  export type {
79
79
  AgentChatServer,
80
+ AgentBuilderCapabilitiesResponse,
80
81
  AgentBuilderServer,
82
+ SaveAgentRequest,
81
83
  Session,
82
84
  Turn,
83
85
  TurnState,
@@ -91,8 +93,11 @@ export type {
91
93
  UserMessage,
92
94
  UserToolApprovalEvent,
93
95
  UserToolResponseEvent,
96
+ ApprovalDecision,
94
97
  PreviousTurnIdInput,
95
98
  ProviderType,
99
+ PageParams,
100
+ ListSessionsOrder,
96
101
  ModelEntry,
97
102
  ModelProviderConfigBase,
98
103
  ModelProviderBase,
@@ -115,6 +120,8 @@ export type {
115
120
  ConnectorCatalogEntry,
116
121
  CreateConnectorRequest,
117
122
  UpdateConnectorRequest,
123
+ AuthenticateConnectorRequest,
124
+ ConnectorAuthenticationResult,
118
125
  ConnectorCatalogServer,
119
126
  SkillBase,
120
127
  RegistrySkill,
@@ -132,9 +139,29 @@ export type {
132
139
  SandboxBase,
133
140
  CreateSandboxRequest,
134
141
  UpdateSandboxRequest,
142
+ SandboxProviderConfig,
143
+ SandboxProviderCatalogEntry,
144
+ SandboxProviderBase,
145
+ CreateSandboxProviderRequest,
146
+ UpdateSandboxProviderRequest,
135
147
  SandboxCatalogServer,
136
148
  CatalogServer,
137
149
  AgentUIServerPort,
150
+ AgentUIServer,
151
+ ModelSelectorEntry,
152
+ SkillSelectorEntry,
153
+ ConnectorSelectorEntry,
154
+ AgentSelectorEntry,
155
+ SearchAgentSelectorParams,
156
+ ModelSelection,
157
+ AgentSkill,
158
+ ConnectorState,
159
+ AgentLibraryEntry,
160
+ SearchAgentsParams,
161
+ SkillMount,
162
+ McpServerMount,
163
+ ModelParams,
164
+ Model,
138
165
  } from "./server/index.js";
139
166
  export type {
140
167
  SandboxCreatedEvent,
@@ -227,7 +227,7 @@ export function createTrueFoundryChatServer<
227
227
  pageToken: req?.pageToken,
228
228
  startTimestamp: req?.startTimestamp,
229
229
  endTimestamp: req?.endTimestamp,
230
- ...(req?.agentName != null ? { agentName: req.agentName } : {}),
230
+ ...(req?.agentId != null ? { agentName: req.agentId } : {}),
231
231
  });
232
232
  const result = await toListResult(page, (s) => toSession<TSpec>(s));
233
233
  for (const session of result.data) {
@@ -1,6 +1,7 @@
1
1
  import { afterEach, describe, expect, it, vi } from "vitest";
2
2
 
3
3
  import {
4
+ agentSpecFromCpManifest,
4
5
  buildSaveAgentManifest,
5
6
  normalizeAgents,
6
7
  normalizeAgentSkills,
@@ -10,6 +11,7 @@ import {
10
11
  saveAgent,
11
12
  SAVE_AGENT_COLLABORATORS,
12
13
  SAVE_AGENT_METADATA_TAGS,
14
+ toCamelCaseDeep,
13
15
  toSnakeCaseDeep,
14
16
  } from "./cp.js";
15
17
 
@@ -273,16 +275,175 @@ describe("normalizeMcpServers", () => {
273
275
  });
274
276
 
275
277
  describe("normalizeAgents", () => {
276
- it("maps name only", () => {
278
+ it("maps name, agentId, and agentSpec from latestVersionDetails.manifest", () => {
277
279
  expect(
278
280
  normalizeAgents({
279
281
  data: [
280
- { name: "ask-ai-agent", id: "ag_1" },
282
+ {
283
+ id: "ag_1",
284
+ name: "ask-ai-agent",
285
+ latestVersionDetails: {
286
+ manifest: {
287
+ type: "truefoundry-agent",
288
+ name: "ask-ai-agent",
289
+ model: {
290
+ name: "openai-main/gpt-4.1",
291
+ params: {
292
+ max_tokens: 8192,
293
+ reasoning_effort: "medium",
294
+ },
295
+ },
296
+ instructions: "Be helpful",
297
+ config: {
298
+ iteration_limit: 50,
299
+ ask_user_questions: { enabled: true },
300
+ },
301
+ skills: [
302
+ {
303
+ type: "truefoundry-skills-registry",
304
+ fqn: "agent-skill:tfy/skills/web:1",
305
+ preload: true,
306
+ config: { timeout_ms: 1000 },
307
+ },
308
+ ],
309
+ mcp_servers: [
310
+ {
311
+ name: "gmail",
312
+ enable_tools: ["@read-only"],
313
+ preload: true,
314
+ config: { locale: "en" },
315
+ },
316
+ ],
317
+ },
318
+ },
319
+ },
281
320
  { name: "" },
282
321
  {},
322
+ {
323
+ name: "try-only",
324
+ // no manifest → Try still works; Edit hidden
325
+ },
283
326
  ],
284
327
  }),
285
- ).toEqual([{ name: "ask-ai-agent" }]);
328
+ ).toEqual([
329
+ {
330
+ name: "ask-ai-agent",
331
+ agentId: "ag_1",
332
+ agentSpec: {
333
+ model: {
334
+ name: "openai-main/gpt-4.1",
335
+ params: { maxTokens: 8192, reasoningEffort: "medium" },
336
+ },
337
+ instructions: "Be helpful",
338
+ config: {
339
+ iterationLimit: 50,
340
+ askUserQuestions: { enabled: true },
341
+ },
342
+ skills: [
343
+ {
344
+ id: "agent-skill:tfy/skills/web:1",
345
+ name: "agent-skill:tfy/skills/web:1",
346
+ preload: true,
347
+ config: { timeoutMs: 1000 },
348
+ },
349
+ ],
350
+ mcpServers: [
351
+ {
352
+ id: "gmail",
353
+ name: "gmail",
354
+ enableTools: ["@read-only"],
355
+ preload: true,
356
+ config: { locale: "en" },
357
+ },
358
+ ],
359
+ },
360
+ },
361
+ { name: "try-only", agentId: "try-only" },
362
+ ]);
363
+ });
364
+ });
365
+
366
+ describe("toCamelCaseDeep", () => {
367
+ it("converts nested snake_case keys", () => {
368
+ expect(
369
+ toCamelCaseDeep({
370
+ max_tokens: 8192,
371
+ reasoning_effort: "medium",
372
+ nested: { iteration_limit: 50 },
373
+ }),
374
+ ).toEqual({
375
+ maxTokens: 8192,
376
+ reasoningEffort: "medium",
377
+ nested: { iterationLimit: 50 },
378
+ });
379
+ });
380
+ });
381
+
382
+ describe("agentSpecFromCpManifest", () => {
383
+ it("returns undefined when model.name is missing", () => {
384
+ expect(agentSpecFromCpManifest({ type: "truefoundry-agent" })).toBeUndefined();
385
+ expect(agentSpecFromCpManifest(null)).toBeUndefined();
386
+ });
387
+
388
+ it("preserves mount runtime fields and agent config for Edit → Save", () => {
389
+ const spec = agentSpecFromCpManifest({
390
+ type: "truefoundry-agent",
391
+ name: "saved",
392
+ model: { name: "openai-main/gpt-4.1" },
393
+ config: { iteration_limit: 25 },
394
+ skills: [
395
+ {
396
+ type: "truefoundry-skills-registry",
397
+ fqn: "agent-skill:tfy/skills/web:1",
398
+ preload: false,
399
+ },
400
+ ],
401
+ mcp_servers: [
402
+ {
403
+ name: "gmail",
404
+ enable_tools: ["@read-only"],
405
+ preload: true,
406
+ },
407
+ ],
408
+ });
409
+
410
+ expect(spec).toEqual({
411
+ model: { name: "openai-main/gpt-4.1" },
412
+ config: { iterationLimit: 25 },
413
+ skills: [
414
+ {
415
+ id: "agent-skill:tfy/skills/web:1",
416
+ name: "agent-skill:tfy/skills/web:1",
417
+ preload: false,
418
+ },
419
+ ],
420
+ mcpServers: [
421
+ {
422
+ id: "gmail",
423
+ name: "gmail",
424
+ enableTools: ["@read-only"],
425
+ preload: true,
426
+ },
427
+ ],
428
+ });
429
+
430
+ const saved = buildSaveAgentManifest("saved", spec!);
431
+ expect(saved.config).toEqual({ iteration_limit: 25 });
432
+ expect(saved.mcp_servers).toEqual([
433
+ {
434
+ type: "truefoundry-mcp-registry",
435
+ name: "gmail",
436
+ enable_tools: ["@read-only"],
437
+ preload: true,
438
+ },
439
+ ]);
440
+ expect(saved.skills).toEqual([
441
+ {
442
+ type: "truefoundry-skills-registry",
443
+ fqn: "agent-skill:tfy/skills/web:1",
444
+ preload: false,
445
+ },
446
+ ]);
286
447
  });
287
448
  });
288
449
 
@@ -432,9 +593,11 @@ describe("saveAgent", () => {
432
593
  }),
433
594
  }),
434
595
  );
435
- const body = JSON.parse(
436
- (fetchMock.mock.calls[0]?.[1] as RequestInit).body as string,
437
- );
596
+ const [, init] = fetchMock.mock.calls[0] as unknown as [
597
+ string,
598
+ RequestInit,
599
+ ];
600
+ const body = JSON.parse(String(init.body));
438
601
  expect(body.manifest.type).toBe("truefoundry-agent");
439
602
  expect(body.manifest.name).toBe("my-agent");
440
603
  expect(body.manifest.model).toEqual({ name: "openai-main/gpt-4.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
  }
@@ -76,6 +76,13 @@ export async function createTrueFoundryAgentUIServer<
76
76
 
77
77
  const server: TrueFoundryAgentUIServer<TSpec> = {
78
78
  ...chat,
79
+ getCapabilities: async () => ({
80
+ data: {
81
+ sandbox: { enabled: true },
82
+ skill: { enabled: true },
83
+ settings: { enabled: true },
84
+ },
85
+ }),
79
86
  getModels: () => listEnabledModels(cp),
80
87
  getSkills: () => listAgentSkills(cp),
81
88
  getMcp: () => listMcpServers(cp),