@truefoundry/assistant-ui-runtime 0.1.7 → 0.1.9

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 (36) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +19 -15
  3. package/dist/chunk-CXBZ6WLZ.js +636 -0
  4. package/dist/chunk-CXBZ6WLZ.js.map +1 -0
  5. package/dist/index.d.ts +3 -3
  6. package/dist/index.js +14 -5
  7. package/dist/index.js.map +1 -1
  8. package/dist/plugins/truefoundry-agent-server-adapter/index.d.ts +81 -35
  9. package/dist/plugins/truefoundry-agent-server-adapter/index.js +3 -1
  10. package/dist/server/index.d.ts +2 -2
  11. package/dist/{types-DbNsU075.d.ts → types-6yWuWHzK.d.ts} +109 -28
  12. package/package.json +1 -1
  13. package/src/convertTurnMessages.ts +4 -0
  14. package/src/draft/truefoundryDraftThreadListAdapter.ts +4 -1
  15. package/src/harness.temp.ts +85 -0
  16. package/src/index.ts +27 -0
  17. package/src/plugins/truefoundry-agent-server-adapter/README.md +83 -44
  18. package/src/plugins/truefoundry-agent-server-adapter/chatServer.ts +365 -0
  19. package/src/plugins/truefoundry-agent-server-adapter/cp.test.ts +444 -0
  20. package/src/plugins/truefoundry-agent-server-adapter/cp.ts +482 -0
  21. package/src/plugins/truefoundry-agent-server-adapter/createTrueFoundryAgentUIServer.ts +94 -0
  22. package/src/plugins/truefoundry-agent-server-adapter/guards.ts +1 -1
  23. package/src/plugins/truefoundry-agent-server-adapter/index.ts +20 -391
  24. package/src/plugins/truefoundry-agent-server-adapter/normalizeAgentSpec.test.ts +85 -0
  25. package/src/plugins/truefoundry-agent-server-adapter/normalizeAgentSpec.ts +84 -0
  26. package/src/plugins/truefoundry-agent-server-adapter/types.ts +1 -1
  27. package/src/server/index.ts +20 -0
  28. package/src/server/types.ts +125 -28
  29. package/src/streamTurn.test.ts +27 -27
  30. package/src/streamTurn.ts +2 -2
  31. package/src/truefoundryOwnedSessionsThreadListAdapter.ts +4 -1
  32. package/src/truefoundryThreadListAdapter.test.ts +22 -0
  33. package/src/truefoundryThreadListAdapter.ts +4 -1
  34. package/src/useTrueFoundryAgentMessages.test.tsx +1 -1
  35. package/dist/chunk-SQDOTGP2.js +0 -292
  36. package/dist/chunk-SQDOTGP2.js.map +0 -1
@@ -0,0 +1,444 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+
3
+ import {
4
+ buildSaveAgentManifest,
5
+ normalizeAgents,
6
+ normalizeAgentSkills,
7
+ normalizeEnabledModels,
8
+ normalizeMcpServers,
9
+ resolveGatewayURL,
10
+ saveAgent,
11
+ SAVE_AGENT_COLLABORATORS,
12
+ SAVE_AGENT_METADATA_TAGS,
13
+ toSnakeCaseDeep,
14
+ } from "./cp.js";
15
+
16
+ afterEach(() => {
17
+ vi.unstubAllGlobals();
18
+ vi.restoreAllMocks();
19
+ });
20
+
21
+ describe("resolveGatewayURL", () => {
22
+ it("short-circuits when gatewayURL is set (no fetch)", async () => {
23
+ const fetchMock = vi.fn();
24
+ vi.stubGlobal("fetch", fetchMock);
25
+
26
+ const url = await resolveGatewayURL({
27
+ apiKey: "key",
28
+ cpURL: "https://cp.example",
29
+ gatewayURL: "https://gateway.truefoundry.ai/acme",
30
+ });
31
+
32
+ expect(url).toBe("https://gateway.truefoundry.ai/acme");
33
+ expect(fetchMock).not.toHaveBeenCalled();
34
+ });
35
+
36
+ it("resolves via /session → {cpURL}{LLM_GATEWAY_URL}/{tenantName}", async () => {
37
+ vi.stubGlobal(
38
+ "fetch",
39
+ vi.fn(async () =>
40
+ Response.json({
41
+ user: { tenantName: "truefoundry" },
42
+ env: {
43
+ TENANT_NAME: "truefoundry",
44
+ LLM_GATEWAY_URL: "/api/llm",
45
+ },
46
+ }),
47
+ ),
48
+ );
49
+
50
+ const url = await resolveGatewayURL({
51
+ apiKey: "key",
52
+ cpURL: "https://cp.example/",
53
+ });
54
+
55
+ expect(url).toBe("https://cp.example/api/llm/truefoundry");
56
+ expect(fetch).toHaveBeenCalledWith(
57
+ "https://cp.example/api/svc/v1/session",
58
+ expect.objectContaining({
59
+ headers: expect.objectContaining({
60
+ Authorization: "Bearer key",
61
+ }),
62
+ }),
63
+ );
64
+ });
65
+
66
+ it("defaults llm prefix to /api/llm when env.LLM_GATEWAY_URL is absent", async () => {
67
+ vi.stubGlobal(
68
+ "fetch",
69
+ vi.fn(async () =>
70
+ Response.json({
71
+ user: { tenantName: "acme" },
72
+ }),
73
+ ),
74
+ );
75
+
76
+ const url = await resolveGatewayURL({
77
+ apiKey: "key",
78
+ cpURL: "https://cp.example",
79
+ });
80
+
81
+ expect(url).toBe("https://cp.example/api/llm/acme");
82
+ });
83
+
84
+ it("throws on session HTTP failure (no public-gateway fallback)", async () => {
85
+ vi.stubGlobal(
86
+ "fetch",
87
+ vi.fn(async () => new Response("nope", { status: 401 })),
88
+ );
89
+
90
+ await expect(
91
+ resolveGatewayURL({
92
+ apiKey: "key",
93
+ cpURL: "https://cp.example",
94
+ }),
95
+ ).rejects.toThrow(/CP 401/);
96
+ });
97
+
98
+ it("throws when tenantName is missing", async () => {
99
+ vi.stubGlobal(
100
+ "fetch",
101
+ vi.fn(async () => Response.json({ user: {}, env: {} })),
102
+ );
103
+
104
+ await expect(
105
+ resolveGatewayURL({
106
+ apiKey: "key",
107
+ cpURL: "https://cp.example",
108
+ }),
109
+ ).rejects.toThrow(/tenantName/);
110
+ });
111
+ });
112
+
113
+ describe("normalizeEnabledModels", () => {
114
+ it("flattens nested provider → account → models", () => {
115
+ const rows = normalizeEnabledModels({
116
+ openai: {
117
+ "openai-main": [
118
+ {
119
+ name: "gpt-4.1",
120
+ provider: "openai",
121
+ provider_account_name: "openai-main",
122
+ model_id: "gpt-4.1",
123
+ model_fqn: "openai-main/gpt-4.1",
124
+ types: ["chat"],
125
+ },
126
+ ],
127
+ },
128
+ anthropic: {
129
+ "anthropic-prod": [
130
+ {
131
+ name: "claude-sonnet-4-6",
132
+ provider: "anthropic",
133
+ provider_account_name: "anthropic-prod",
134
+ model_id: "claude-sonnet-4-6",
135
+ model_fqn: "anthropic-prod/claude-sonnet-4-6",
136
+ types: ["chat"],
137
+ },
138
+ {
139
+ name: "embed-only",
140
+ provider: "anthropic",
141
+ model_fqn: "anthropic-prod/embed",
142
+ types: ["embedding"],
143
+ },
144
+ ],
145
+ },
146
+ });
147
+
148
+ expect(rows).toEqual([
149
+ {
150
+ name: "gpt-4.1",
151
+ provider: "openai",
152
+ apiModel: "openai-main/gpt-4.1",
153
+ modelId: "gpt-4.1",
154
+ providerAccount: "openai-main",
155
+ id: "openai-main/gpt-4.1",
156
+ },
157
+ {
158
+ name: "claude-sonnet-4-6",
159
+ provider: "anthropic",
160
+ apiModel: "anthropic-prod/claude-sonnet-4-6",
161
+ modelId: "claude-sonnet-4-6",
162
+ providerAccount: "anthropic-prod",
163
+ id: "anthropic-prod/claude-sonnet-4-6",
164
+ },
165
+ ]);
166
+ });
167
+
168
+ it("flattens virtual-model top-level account → models[]", () => {
169
+ const rows = normalizeEnabledModels({
170
+ "virtual-main": [
171
+ {
172
+ name: "router",
173
+ provider: "virtual-model",
174
+ provider_account_name: "virtual-main",
175
+ model_id: "router",
176
+ model_fqn: "virtual-main/router",
177
+ types: ["chat"],
178
+ },
179
+ ],
180
+ });
181
+
182
+ expect(rows).toEqual([
183
+ {
184
+ name: "router",
185
+ provider: "virtual-model",
186
+ apiModel: "virtual-main/router",
187
+ modelId: "router",
188
+ providerAccount: "virtual-main",
189
+ id: "virtual-main/router",
190
+ },
191
+ ]);
192
+ });
193
+
194
+ it("returns [] for empty object", () => {
195
+ expect(normalizeEnabledModels({})).toEqual([]);
196
+ });
197
+ });
198
+
199
+ describe("normalizeAgentSkills", () => {
200
+ it("maps latest_version.fqn to id/fqn", () => {
201
+ const rows = normalizeAgentSkills({
202
+ data: [
203
+ {
204
+ id: "sk_abc",
205
+ name: "web-search",
206
+ latest_version: {
207
+ id: "sv_1",
208
+ fqn: "agent-skill:truefoundry/skills/web-search:1",
209
+ manifest: {
210
+ source: { description: "Search the web" },
211
+ },
212
+ },
213
+ },
214
+ {
215
+ id: "sk_drop",
216
+ name: "no-version",
217
+ },
218
+ ],
219
+ });
220
+
221
+ expect(rows).toEqual([
222
+ {
223
+ id: "agent-skill:truefoundry/skills/web-search:1",
224
+ name: "web-search",
225
+ fqn: "agent-skill:truefoundry/skills/web-search:1",
226
+ description: "Search the web",
227
+ },
228
+ ]);
229
+ });
230
+ });
231
+
232
+ describe("normalizeMcpServers", () => {
233
+ it("uses server name as id/mcpName and dedupes", () => {
234
+ const rows = normalizeMcpServers({
235
+ data: [
236
+ {
237
+ id: "mcp_01",
238
+ name: "github",
239
+ manifest: { description: "GitHub MCP" },
240
+ authStatus: { status: "unauthenticated" },
241
+ },
242
+ {
243
+ id: "mcp_dup",
244
+ name: "github",
245
+ manifest: { description: "dup" },
246
+ },
247
+ {
248
+ id: "mcp_02",
249
+ name: "slack",
250
+ authStatus: { status: "authenticated" },
251
+ },
252
+ ],
253
+ });
254
+
255
+ expect(rows).toEqual([
256
+ {
257
+ id: "github",
258
+ name: "github",
259
+ mcpName: "github",
260
+ description: "GitHub MCP",
261
+ serverId: "mcp_01",
262
+ authenticated: false,
263
+ },
264
+ {
265
+ id: "slack",
266
+ name: "slack",
267
+ mcpName: "slack",
268
+ serverId: "mcp_02",
269
+ authenticated: true,
270
+ },
271
+ ]);
272
+ });
273
+ });
274
+
275
+ describe("normalizeAgents", () => {
276
+ it("maps name only", () => {
277
+ expect(
278
+ normalizeAgents({
279
+ data: [
280
+ { name: "ask-ai-agent", id: "ag_1" },
281
+ { name: "" },
282
+ {},
283
+ ],
284
+ }),
285
+ ).toEqual([{ name: "ask-ai-agent" }]);
286
+ });
287
+ });
288
+
289
+ describe("toSnakeCaseDeep", () => {
290
+ it("converts nested camelCase keys", () => {
291
+ expect(
292
+ toSnakeCaseDeep({
293
+ maxTokens: 8192,
294
+ reasoningEffort: "medium",
295
+ nested: { iterationLimit: 50 },
296
+ }),
297
+ ).toEqual({
298
+ max_tokens: 8192,
299
+ reasoning_effort: "medium",
300
+ nested: { iteration_limit: 50 },
301
+ });
302
+ });
303
+ });
304
+
305
+ describe("buildSaveAgentManifest", () => {
306
+ it("hardcodes type / metadata_tags / collaborators and snake_cases spec fields", () => {
307
+ const manifest = buildSaveAgentManifest("my-agent", {
308
+ model: {
309
+ name: "ai-foundry/claude-sonnet-4-6",
310
+ params: { maxTokens: 8192, reasoningEffort: "medium" },
311
+ },
312
+ instructions: "Be helpful",
313
+ config: {
314
+ iterationLimit: 50,
315
+ askUserQuestions: { enabled: true },
316
+ } as never,
317
+ mcpServers: [
318
+ {
319
+ type: "truefoundry-mcp-registry",
320
+ name: "gmail",
321
+ enableTools: ["@read-only"],
322
+ },
323
+ ] as never[],
324
+ skills: [
325
+ {
326
+ type: "truefoundry-skills-registry",
327
+ fqn: "agent-skill:truefoundry/skills/web:1",
328
+ preload: true,
329
+ },
330
+ ] as never[],
331
+ });
332
+
333
+ expect(manifest).toEqual({
334
+ type: "truefoundry-agent",
335
+ name: "my-agent",
336
+ description: "",
337
+ model: {
338
+ name: "ai-foundry/claude-sonnet-4-6",
339
+ params: { max_tokens: 8192, reasoning_effort: "medium" },
340
+ },
341
+ metadata_tags: { ...SAVE_AGENT_METADATA_TAGS },
342
+ collaborators: [...SAVE_AGENT_COLLABORATORS],
343
+ instructions: "Be helpful",
344
+ config: {
345
+ iteration_limit: 50,
346
+ ask_user_questions: { enabled: true },
347
+ },
348
+ mcp_servers: [
349
+ {
350
+ type: "truefoundry-mcp-registry",
351
+ name: "gmail",
352
+ enable_tools: ["@read-only"],
353
+ preload: false,
354
+ },
355
+ ],
356
+ skills: [
357
+ {
358
+ type: "truefoundry-skills-registry",
359
+ fqn: "agent-skill:truefoundry/skills/web:1",
360
+ preload: true,
361
+ },
362
+ ],
363
+ });
364
+ });
365
+
366
+ it("normalizes UI catalog mounts and defaults enable_tools / preload", () => {
367
+ const manifest = buildSaveAgentManifest("draft-save", {
368
+ model: { name: "openai-main/gpt-4.1" },
369
+ mcpServers: [{ id: "gmail", name: "gmail" }] as never[],
370
+ skills: [
371
+ {
372
+ id: "agent-skill:truefoundry/skills/web:1",
373
+ name: "web",
374
+ },
375
+ ] as never[],
376
+ });
377
+
378
+ expect(manifest.mcp_servers).toEqual([
379
+ {
380
+ type: "truefoundry-mcp-registry",
381
+ name: "gmail",
382
+ enable_tools: ["@all"],
383
+ preload: false,
384
+ },
385
+ ]);
386
+ expect(manifest.skills).toEqual([
387
+ {
388
+ type: "truefoundry-skills-registry",
389
+ fqn: "agent-skill:truefoundry/skills/web:1",
390
+ preload: false,
391
+ },
392
+ ]);
393
+ expect(manifest.description).toBe("");
394
+ expect(manifest).not.toHaveProperty("instructions");
395
+ expect(manifest).not.toHaveProperty("config");
396
+ });
397
+
398
+ it("passes through description when present on the spec", () => {
399
+ const manifest = buildSaveAgentManifest("named", {
400
+ model: { name: "openai-main/gpt-4.1" },
401
+ description: "My agent",
402
+ } as never);
403
+
404
+ expect(manifest.description).toBe("My agent");
405
+ });
406
+ });
407
+
408
+ describe("saveAgent", () => {
409
+ it("PUTs { manifest } to /api/svc/v1/agents", async () => {
410
+ const fetchMock = vi.fn(async () =>
411
+ Response.json({ id: "ag_1", name: "my-agent" }),
412
+ );
413
+ vi.stubGlobal("fetch", fetchMock);
414
+
415
+ const result = await saveAgent(
416
+ { apiKey: "key", cpURL: "https://cp.example/" },
417
+ {
418
+ agentName: "my-agent",
419
+ agentSpec: { model: { name: "openai-main/gpt-4.1" } },
420
+ },
421
+ );
422
+
423
+ expect(result).toEqual({ id: "ag_1", name: "my-agent" });
424
+ expect(fetchMock).toHaveBeenCalledWith(
425
+ "https://cp.example/api/svc/v1/agents",
426
+ expect.objectContaining({
427
+ method: "PUT",
428
+ headers: expect.objectContaining({
429
+ Authorization: "Bearer key",
430
+ Accept: "application/json",
431
+ "Content-Type": "application/json",
432
+ }),
433
+ }),
434
+ );
435
+ const body = JSON.parse(
436
+ (fetchMock.mock.calls[0]?.[1] as RequestInit).body as string,
437
+ );
438
+ expect(body.manifest.type).toBe("truefoundry-agent");
439
+ expect(body.manifest.name).toBe("my-agent");
440
+ expect(body.manifest.model).toEqual({ name: "openai-main/gpt-4.1" });
441
+ expect(body.manifest.metadata_tags).toEqual(SAVE_AGENT_METADATA_TAGS);
442
+ expect(body.manifest.collaborators).toEqual([...SAVE_AGENT_COLLABORATORS]);
443
+ });
444
+ });