@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.
- package/CHANGELOG.md +66 -0
- package/dist/{chunk-BRPTG6VA.js → chunk-2NLIAJJX.js} +100 -12
- package/dist/chunk-2NLIAJJX.js.map +1 -0
- package/dist/index.d.ts +13 -2
- package/dist/index.js +70 -66
- package/dist/index.js.map +1 -1
- package/dist/plugins/truefoundry-agent-server-adapter/index.d.ts +3 -3
- package/dist/plugins/truefoundry-agent-server-adapter/index.js +1 -1
- package/dist/server/index.d.ts +2 -2
- package/dist/{types-CgZUnILu.d.ts → types-CSRQSnHu.d.ts} +148 -80
- package/package.json +3 -3
- package/src/convertTurnMessages.test.ts +123 -0
- package/src/convertTurnMessages.ts +8 -2
- package/src/draft/truefoundryDraftThreadListAdapter.test.ts +58 -4
- package/src/draft/truefoundryDraftThreadListAdapter.ts +18 -22
- package/src/index.ts +27 -0
- package/src/plugins/truefoundry-agent-server-adapter/chatServer.ts +1 -1
- package/src/plugins/truefoundry-agent-server-adapter/cp.test.ts +169 -6
- package/src/plugins/truefoundry-agent-server-adapter/cp.ts +126 -1
- package/src/plugins/truefoundry-agent-server-adapter/createTrueFoundryAgentUIServer.ts +7 -0
- package/src/plugins/truefoundry-agent-server-adapter/normalizeAgentSpec.test.ts +34 -0
- package/src/plugins/truefoundry-agent-server-adapter/normalizeAgentSpec.ts +12 -5
- package/src/plugins/truefoundry-agent-server-adapter/types.ts +2 -2
- package/src/server/index.ts +15 -0
- package/src/server/types.ts +448 -381
- package/src/sessionThreadMetadata.ts +42 -0
- package/src/truefoundryOwnedSessionsThreadListAdapter.test.ts +17 -0
- package/src/truefoundryOwnedSessionsThreadListAdapter.ts +13 -25
- package/src/truefoundryThreadListAdapter.test.ts +17 -9
- package/src/truefoundryThreadListAdapter.ts +9 -14
- package/src/types.ts +5 -0
- package/src/useTrueFoundryAgentRuntime.ts +4 -1
- 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
|
-
...(
|
|
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: {
|
|
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(
|
|
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
|
|
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
|
-
[
|
|
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({
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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?.
|
|
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
|
|
278
|
+
it("maps name, agentId, and agentSpec from latestVersionDetails.manifest", () => {
|
|
277
279
|
expect(
|
|
278
280
|
normalizeAgents({
|
|
279
281
|
data: [
|
|
280
|
-
{
|
|
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([
|
|
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
|
|
436
|
-
|
|
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
|
-
|
|
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),
|