@truefoundry/assistant-ui-runtime 0.1.10-rc.1 → 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.
- package/CHANGELOG.md +26 -0
- package/dist/{chunk-BRPTG6VA.js → chunk-6ZOGBD7E.js} +93 -12
- package/dist/chunk-6ZOGBD7E.js.map +1 -0
- package/dist/index.d.ts +13 -2
- package/dist/index.js +59 -64
- package/dist/index.js.map +1 -1
- package/dist/plugins/truefoundry-agent-server-adapter/index.d.ts +1 -1
- 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-Bb8Kaf1h.d.ts} +7 -2
- package/package.json +3 -3
- package/src/draft/truefoundryDraftThreadListAdapter.test.ts +57 -4
- package/src/draft/truefoundryDraftThreadListAdapter.ts +18 -22
- 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/normalizeAgentSpec.test.ts +34 -0
- package/src/plugins/truefoundry-agent-server-adapter/normalizeAgentSpec.ts +12 -5
- package/src/server/types.ts +7 -2
- package/src/sessionThreadMetadata.ts +39 -0
- package/src/truefoundryOwnedSessionsThreadListAdapter.test.ts +16 -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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AgentSessionClient } from 'truefoundry-gateway-sdk/agents';
|
|
2
2
|
import { PrivateAgentSessionClient } from 'truefoundry-gateway-sdk/agents/private';
|
|
3
|
-
import { A as AgentSpec, x as CreateSessionRequest, B as ListSessionsParams, S as Session, d as Turn, a7 as TurnState, a as AgentChatServer, ae as UpdateSessionRequest, ag as AgentSelectorEntry, ah as ConnectorSelectorEntry, ai as ModelSelectorEntry, aj as SkillSelectorEntry, f as AgentBuilderServer } from '../../types-
|
|
3
|
+
import { A as AgentSpec, x as CreateSessionRequest, B as ListSessionsParams, S as Session, d as Turn, a7 as TurnState, a as AgentChatServer, ae as UpdateSessionRequest, ag as AgentSelectorEntry, ah as ConnectorSelectorEntry, ai as ModelSelectorEntry, aj as SkillSelectorEntry, f as AgentBuilderServer } from '../../types-Bb8Kaf1h.js';
|
|
4
4
|
import { TruefoundryGatewayApi } from 'truefoundry-gateway-sdk';
|
|
5
5
|
|
|
6
6
|
/**
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { aa as TurnStreamingEvent, ak as DeltaEvents, T as TurnEvent } from '../types-
|
|
2
|
-
export { al as ActionRequiredEvent, f as AgentBuilderServer, a as AgentChatServer, am as AgentInfo, an as AgentParent, ag as AgentSelectorEntry, A as AgentSpec, g as AgentUIServerPort, ao as ApprovalDecision, C as CatalogServer, ap as ChunkDeltaToolCall, h as ConnectorAuth, i as ConnectorAuthApiKey, j as ConnectorAuthNone, k as ConnectorAuthOAuth, l as ConnectorAuthPublic, m as ConnectorAuthPublicApiKey, n as ConnectorAuthPublicNone, o as ConnectorAuthPublicOAuth, p as ConnectorAuthType, q as ConnectorBase, r as ConnectorCatalogEntry, s as ConnectorCatalogServer, t as ConnectorConfigBase, ah as ConnectorSelectorEntry, u as CreateConnectorRequest, v as CreateModelProviderRequest, w as CreateSandboxRequest, x as CreateSessionRequest, y as CreateSkillRequest, z as CreateSkillRequestBase, D as DefinedSkill, G as GithubSkill, I as ImportGithubSkillRequest, L as ListResult, aq as ListSessionsOrder, B as ListSessionsParams, M as McpAuthRequiredEvent, ar as McpInitializeEvent, as as McpServerAuthInfo, at as McpServerMount, au as Model, E as ModelCatalogServer, F as ModelEntry, av as ModelMessageContentPart, aw as ModelMessageDeltaEvent, H as ModelMessageEvent, ax as ModelParams, J as ModelProviderBase, K as ModelProviderCatalogEntry, N as ModelProviderConfigBase, ai as ModelSelectorEntry, ay as PageParams, P as PreviousTurnIdInput, O as ProviderType, R as RegistrySkill, Q as SandboxBase, V as SandboxCatalogEntry, W as SandboxCatalogServer, X as SandboxConfig, Y as SandboxCreatedEvent, az as SearchAgentSelectorParams, Z as SelectRegistrySkillRequest, S as Session, _ as SessionEventItem, $ as SkillBase, a0 as SkillCatalogEntry, a1 as SkillCatalogServer, a2 as SkillConfigBase, aA as SkillMount, aj as SkillSelectorEntry, b as ThreadCreatedEvent, aB as ThreadDoneEvent, a3 as ToolApprovalRequiredEvent, a4 as ToolBase, a5 as ToolCall, aC as ToolCallFunction, aD as ToolCallRef, aE as ToolInfo, aF as ToolResponseEvent, a6 as ToolResponseRequiredEvent, d as Turn, aG as TurnCreatedEvent, aH as TurnDoneEvent, e as TurnInputItem, a7 as TurnState, aI as TurnStateCancelled, a8 as TurnStateDone, aJ as TurnStateError, aK as TurnStateRunning, a9 as TurnStreamData, ab as UpdateConnectorRequest, ac as UpdateModelProviderRequest, ad as UpdateSandboxRequest, ae as UpdateSessionRequest, af as UserMessage, aL as UserMessageContent, c as UserToolApprovalEvent, U as UserToolResponseEvent } from '../types-
|
|
1
|
+
import { aa as TurnStreamingEvent, ak as DeltaEvents, T as TurnEvent } from '../types-Bb8Kaf1h.js';
|
|
2
|
+
export { al as ActionRequiredEvent, f as AgentBuilderServer, a as AgentChatServer, am as AgentInfo, an as AgentParent, ag as AgentSelectorEntry, A as AgentSpec, g as AgentUIServerPort, ao as ApprovalDecision, C as CatalogServer, ap as ChunkDeltaToolCall, h as ConnectorAuth, i as ConnectorAuthApiKey, j as ConnectorAuthNone, k as ConnectorAuthOAuth, l as ConnectorAuthPublic, m as ConnectorAuthPublicApiKey, n as ConnectorAuthPublicNone, o as ConnectorAuthPublicOAuth, p as ConnectorAuthType, q as ConnectorBase, r as ConnectorCatalogEntry, s as ConnectorCatalogServer, t as ConnectorConfigBase, ah as ConnectorSelectorEntry, u as CreateConnectorRequest, v as CreateModelProviderRequest, w as CreateSandboxRequest, x as CreateSessionRequest, y as CreateSkillRequest, z as CreateSkillRequestBase, D as DefinedSkill, G as GithubSkill, I as ImportGithubSkillRequest, L as ListResult, aq as ListSessionsOrder, B as ListSessionsParams, M as McpAuthRequiredEvent, ar as McpInitializeEvent, as as McpServerAuthInfo, at as McpServerMount, au as Model, E as ModelCatalogServer, F as ModelEntry, av as ModelMessageContentPart, aw as ModelMessageDeltaEvent, H as ModelMessageEvent, ax as ModelParams, J as ModelProviderBase, K as ModelProviderCatalogEntry, N as ModelProviderConfigBase, ai as ModelSelectorEntry, ay as PageParams, P as PreviousTurnIdInput, O as ProviderType, R as RegistrySkill, Q as SandboxBase, V as SandboxCatalogEntry, W as SandboxCatalogServer, X as SandboxConfig, Y as SandboxCreatedEvent, az as SearchAgentSelectorParams, Z as SelectRegistrySkillRequest, S as Session, _ as SessionEventItem, $ as SkillBase, a0 as SkillCatalogEntry, a1 as SkillCatalogServer, a2 as SkillConfigBase, aA as SkillMount, aj as SkillSelectorEntry, b as ThreadCreatedEvent, aB as ThreadDoneEvent, a3 as ToolApprovalRequiredEvent, a4 as ToolBase, a5 as ToolCall, aC as ToolCallFunction, aD as ToolCallRef, aE as ToolInfo, aF as ToolResponseEvent, a6 as ToolResponseRequiredEvent, d as Turn, aG as TurnCreatedEvent, aH as TurnDoneEvent, e as TurnInputItem, a7 as TurnState, aI as TurnStateCancelled, a8 as TurnStateDone, aJ as TurnStateError, aK as TurnStateRunning, a9 as TurnStreamData, ab as UpdateConnectorRequest, ac as UpdateModelProviderRequest, ad as UpdateSandboxRequest, ae as UpdateSessionRequest, af as UserMessage, aL as UserMessageContent, c as UserToolApprovalEvent, U as UserToolResponseEvent } from '../types-Bb8Kaf1h.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Local implementations of streaming delta helpers.
|
|
@@ -245,9 +245,13 @@ interface ConnectorSelectorEntry {
|
|
|
245
245
|
name: string;
|
|
246
246
|
description?: string;
|
|
247
247
|
}
|
|
248
|
-
/** Agent selector row
|
|
248
|
+
/** Agent selector row. Host extends for metadata; `agentSpec` enables Edit. */
|
|
249
249
|
interface AgentSelectorEntry {
|
|
250
250
|
name: string;
|
|
251
|
+
/** Stable id when distinct from display `name`. Falls back to `name` when omitted. */
|
|
252
|
+
agentId?: string;
|
|
253
|
+
/** Published agent spec — required for Edit; optional for Try-only hosts. */
|
|
254
|
+
agentSpec?: AgentSpec;
|
|
251
255
|
}
|
|
252
256
|
type SearchAgentSelectorParams = {
|
|
253
257
|
query?: string;
|
|
@@ -321,7 +325,8 @@ type PageParams = {
|
|
|
321
325
|
pageToken?: string;
|
|
322
326
|
};
|
|
323
327
|
interface ListSessionsParams extends PageParams {
|
|
324
|
-
|
|
328
|
+
/** Host-owned agent identity filter. Hosts that key agents by name pass that name here. */
|
|
329
|
+
agentId?: string;
|
|
325
330
|
/** Host-specific filter (e.g. TFY startTimestamp). */
|
|
326
331
|
startTimestamp?: string;
|
|
327
332
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@truefoundry/assistant-ui-runtime",
|
|
3
|
-
"version": "0.1.10
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "TrueFoundry Gateway agent runtime adapter for assistant-ui",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
"typecheck": "tsc --noEmit"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@assistant-ui/core": "^0.2.
|
|
58
|
-
"@assistant-ui/store": "^0.2.
|
|
57
|
+
"@assistant-ui/core": "^0.2.22",
|
|
58
|
+
"@assistant-ui/store": "^0.2.21"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"@types/react": "*",
|
|
@@ -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
|
{
|
|
@@ -70,10 +88,45 @@ describe("createTrueFoundryDraftThreadListAdapter", () => {
|
|
|
70
88
|
title: "My draft",
|
|
71
89
|
lastMessageAt: new Date("2026-06-30T10:00:00.000Z"),
|
|
72
90
|
},
|
|
91
|
+
{
|
|
92
|
+
status: "regular",
|
|
93
|
+
remoteId: "s1",
|
|
94
|
+
title: "Named",
|
|
95
|
+
lastMessageAt: new Date("2026-06-30T09:00:00.000Z"),
|
|
96
|
+
custom: { agentName: "from-sdk" },
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
status: "regular",
|
|
100
|
+
remoteId: "s2",
|
|
101
|
+
title: "untitled-agent",
|
|
102
|
+
lastMessageAt: new Date("2026-06-30T08:00:00.000Z"),
|
|
103
|
+
custom: { agentName: "untitled-agent" },
|
|
104
|
+
},
|
|
73
105
|
]);
|
|
74
106
|
expect(result.nextCursor).toBe("page-2");
|
|
75
107
|
});
|
|
76
108
|
|
|
109
|
+
it("forwards listSessionsAgentId as agentId", async () => {
|
|
110
|
+
const listSessions = vi.fn().mockResolvedValue(mockDraftListPage([]));
|
|
111
|
+
const server = mockServer({
|
|
112
|
+
listSessions,
|
|
113
|
+
createSession: vi.fn(),
|
|
114
|
+
getSession: vi.fn(),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const adapter = createTrueFoundryDraftThreadListAdapter({
|
|
118
|
+
server,
|
|
119
|
+
defaultAgentSpec,
|
|
120
|
+
listSessionsAgentId: "agent-x",
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await adapter.list();
|
|
124
|
+
|
|
125
|
+
expect(listSessions).toHaveBeenCalledWith(
|
|
126
|
+
expect.objectContaining({ agentId: "agent-x" }),
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
77
130
|
it("creates a draft session on initialize", async () => {
|
|
78
131
|
const createSession = vi.fn().mockResolvedValue(
|
|
79
132
|
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() {},
|
|
@@ -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
|
}
|
|
@@ -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", () => {
|