@truefoundry/assistant-ui-runtime 0.1.6 → 0.1.8
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 +23 -0
- package/README.md +22 -17
- package/dist/chunk-CXBZ6WLZ.js +636 -0
- package/dist/chunk-CXBZ6WLZ.js.map +1 -0
- package/dist/index.d.ts +20 -24
- package/dist/index.js +269 -166
- package/dist/index.js.map +1 -1
- package/dist/plugins/truefoundry-agent-server-adapter/index.d.ts +82 -39
- package/dist/plugins/truefoundry-agent-server-adapter/index.js +3 -1
- package/dist/server/index.d.ts +2 -2
- package/dist/{types-BfiFf8O1.d.ts → types-B_z-FsDS.d.ts} +208 -10
- package/package.json +1 -1
- package/src/convertTurnMessages.ts +4 -0
- package/src/{private → draft}/agentSpec.ts +14 -17
- package/src/{private → draft}/draftSessionBridge.ts +1 -2
- package/src/{private → draft}/truefoundryDraftThreadListAdapter.test.ts +1 -1
- package/src/{private → draft}/truefoundryDraftThreadListAdapter.ts +6 -2
- package/src/{private → draft}/useDraftAgentSpec.ts +16 -5
- package/src/draftAgentConfig.test.ts +2 -1
- package/src/harness.temp.ts +85 -0
- package/src/index.ts +43 -7
- package/src/plugins/truefoundry-agent-server-adapter/README.md +83 -44
- package/src/plugins/truefoundry-agent-server-adapter/chatServer.ts +365 -0
- package/src/plugins/truefoundry-agent-server-adapter/cp.test.ts +444 -0
- package/src/plugins/truefoundry-agent-server-adapter/cp.ts +482 -0
- package/src/plugins/truefoundry-agent-server-adapter/createTrueFoundryAgentUIServer.ts +94 -0
- package/src/plugins/truefoundry-agent-server-adapter/guards.ts +1 -1
- package/src/plugins/truefoundry-agent-server-adapter/index.ts +20 -351
- package/src/plugins/truefoundry-agent-server-adapter/normalizeAgentSpec.test.ts +85 -0
- package/src/plugins/truefoundry-agent-server-adapter/normalizeAgentSpec.ts +84 -0
- package/src/plugins/truefoundry-agent-server-adapter/types.ts +7 -5
- package/src/server/index.ts +29 -0
- package/src/server/types.ts +264 -12
- package/src/streamTurn.test.ts +27 -27
- package/src/streamTurn.ts +2 -2
- package/src/truefoundryExtras.ts +4 -1
- package/src/truefoundryOwnedSessionsThreadListAdapter.ts +5 -2
- package/src/truefoundryThreadListAdapter.test.ts +22 -0
- package/src/truefoundryThreadListAdapter.ts +4 -1
- package/src/types.ts +1 -2
- package/src/useTrueFoundryAgentMessages.test.tsx +262 -2
- package/src/useTrueFoundryAgentMessages.ts +284 -176
- package/src/useTrueFoundryAgentRuntime.ts +31 -21
- package/dist/chunk-Q2SHKMLM.js +0 -270
- package/dist/chunk-Q2SHKMLM.js.map +0 -1
- /package/src/{private → draft}/useDraftAgentSpec.test.tsx +0 -0
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Control Plane HTTP for builder catalog lists + gateway URL resolution.
|
|
3
|
+
*
|
|
4
|
+
* Wire shapes are host/CP contracts (not Fern / gateway SDK). Paths match
|
|
5
|
+
* ai.tf cpApi usage — expect drift; keep normalizers defensive.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
AgentSelectorEntry,
|
|
10
|
+
ConnectorSelectorEntry,
|
|
11
|
+
ModelSelectorEntry,
|
|
12
|
+
SearchAgentSelectorParams,
|
|
13
|
+
SkillSelectorEntry,
|
|
14
|
+
} from "../../server/types.js";
|
|
15
|
+
import { normalizeAgentSpecForGateway } from "./normalizeAgentSpec.js";
|
|
16
|
+
import type { TfyAgentSpec } from "./types.js";
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Selector rows — FE base + TFY mount fields
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
export interface TfyModelSelectorEntry extends ModelSelectorEntry {
|
|
23
|
+
/** Write into AgentSpec.model.name (model_fqn). */
|
|
24
|
+
apiModel: string;
|
|
25
|
+
modelId: string;
|
|
26
|
+
providerAccount?: string;
|
|
27
|
+
id?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface TfySkillSelectorEntry extends SkillSelectorEntry {
|
|
31
|
+
/** Version FQN — mount as RegisteredSkillMount.fqn. */
|
|
32
|
+
fqn: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface TfyConnectorSelectorEntry extends ConnectorSelectorEntry {
|
|
36
|
+
/** Mount as RegisteredMcpServer.name. */
|
|
37
|
+
mcpName: string;
|
|
38
|
+
serverId?: string | null;
|
|
39
|
+
authenticated?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type TfyAgentSelectorEntry = AgentSelectorEntry;
|
|
43
|
+
|
|
44
|
+
export type CpCredentials = {
|
|
45
|
+
apiKey: string;
|
|
46
|
+
cpURL: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// HTTP
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
function trimSlash(url: string): string {
|
|
54
|
+
return url.replace(/\/+$/, "");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function cpFetch<T>(
|
|
58
|
+
opts: CpCredentials,
|
|
59
|
+
path: string,
|
|
60
|
+
init?: RequestInit,
|
|
61
|
+
): Promise<T> {
|
|
62
|
+
const url = `${trimSlash(opts.cpURL)}${path.startsWith("/") ? path : `/${path}`}`;
|
|
63
|
+
const res = await fetch(url, {
|
|
64
|
+
...init,
|
|
65
|
+
headers: {
|
|
66
|
+
Authorization: `Bearer ${opts.apiKey}`,
|
|
67
|
+
Accept: "application/json",
|
|
68
|
+
...(init?.headers ?? {}),
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
if (!res.ok) {
|
|
72
|
+
throw new Error(`CP ${res.status} ${res.statusText}: ${path}`);
|
|
73
|
+
}
|
|
74
|
+
return (await res.json()) as T;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Gateway URL resolve
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Real CP `/api/svc/v1/session` is flat `{ user, env, ... }` (no `data` wrapper).
|
|
83
|
+
* `env.LLM_GATEWAY_URL` is a prefix like `/api/llm`; tenant is appended:
|
|
84
|
+
* `{cpURL}{LLM_GATEWAY_URL}/{tenantName}` → e.g. `…/api/llm/truefoundry`.
|
|
85
|
+
*/
|
|
86
|
+
type SessionResponse = {
|
|
87
|
+
user?: {
|
|
88
|
+
tenantName?: string;
|
|
89
|
+
};
|
|
90
|
+
env?: {
|
|
91
|
+
TENANT_NAME?: string;
|
|
92
|
+
LLM_GATEWAY_URL?: string;
|
|
93
|
+
};
|
|
94
|
+
/** Legacy / alternate wrap — keep for safety. */
|
|
95
|
+
data?: {
|
|
96
|
+
user?: {
|
|
97
|
+
tenantName?: string;
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
function joinCpPath(cpURL: string, path: string): string {
|
|
103
|
+
const base = trimSlash(cpURL);
|
|
104
|
+
const p = path.startsWith("/") ? path : `/${path}`;
|
|
105
|
+
return `${base}${p}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Resolve gateway base URL.
|
|
110
|
+
* - `gatewayURL` set → use it (no /session).
|
|
111
|
+
* - else GET /api/svc/v1/session →
|
|
112
|
+
* `{cpURL}{env.LLM_GATEWAY_URL ?? "/api/llm"}/{tenantName}`.
|
|
113
|
+
* - session HTTP failure or missing tenantName → throw (no silent public fallback).
|
|
114
|
+
*/
|
|
115
|
+
export async function resolveGatewayURL(opts: {
|
|
116
|
+
apiKey: string;
|
|
117
|
+
cpURL: string;
|
|
118
|
+
gatewayURL?: string;
|
|
119
|
+
}): Promise<string> {
|
|
120
|
+
if (opts.gatewayURL != null && opts.gatewayURL !== "") {
|
|
121
|
+
return opts.gatewayURL;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const session = await cpFetch<SessionResponse>(opts, "/api/svc/v1/session");
|
|
125
|
+
const tenantName =
|
|
126
|
+
session.user?.tenantName ??
|
|
127
|
+
session.env?.TENANT_NAME ??
|
|
128
|
+
session.data?.user?.tenantName;
|
|
129
|
+
if (tenantName == null || tenantName === "") {
|
|
130
|
+
throw new Error(
|
|
131
|
+
"resolveGatewayURL: /api/svc/v1/session did not return user.tenantName",
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const llmPrefix = trimSlash(session.env?.LLM_GATEWAY_URL ?? "/api/llm");
|
|
136
|
+
return joinCpPath(opts.cpURL, `${llmPrefix}/${tenantName}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// Models
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
type RawEnabledModel = {
|
|
144
|
+
id?: string;
|
|
145
|
+
name?: string;
|
|
146
|
+
provider?: string;
|
|
147
|
+
provider_account_name?: string;
|
|
148
|
+
model_id?: string;
|
|
149
|
+
model_fqn?: string;
|
|
150
|
+
types?: string[];
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
function isChatModel(row: RawEnabledModel): boolean {
|
|
154
|
+
const types = row.types;
|
|
155
|
+
if (types == null || types.length === 0) return true;
|
|
156
|
+
return types.includes("chat");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function toModelEntry(row: RawEnabledModel): TfyModelSelectorEntry | null {
|
|
160
|
+
if (!isChatModel(row)) return null;
|
|
161
|
+
const apiModel = row.model_fqn ?? row.id;
|
|
162
|
+
const name = row.name;
|
|
163
|
+
if (apiModel == null || apiModel === "" || name == null || name === "") {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
const provider = row.provider ?? "unknown";
|
|
167
|
+
const modelId = row.model_id ?? name;
|
|
168
|
+
return {
|
|
169
|
+
name,
|
|
170
|
+
provider,
|
|
171
|
+
apiModel,
|
|
172
|
+
modelId,
|
|
173
|
+
...(row.provider_account_name != null
|
|
174
|
+
? { providerAccount: row.provider_account_name }
|
|
175
|
+
: {}),
|
|
176
|
+
id: apiModel,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Flatten enabled-models payload.
|
|
182
|
+
* Nested: provider → account → models[].
|
|
183
|
+
* Virtual: top-level account → models[] (array values that look like model rows).
|
|
184
|
+
*/
|
|
185
|
+
export function normalizeEnabledModels(raw: unknown): TfyModelSelectorEntry[] {
|
|
186
|
+
if (raw == null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
const out: TfyModelSelectorEntry[] = [];
|
|
190
|
+
const seen = new Set<string>();
|
|
191
|
+
|
|
192
|
+
function pushRow(row: RawEnabledModel): void {
|
|
193
|
+
const entry = toModelEntry(row);
|
|
194
|
+
if (entry == null || seen.has(entry.apiModel)) return;
|
|
195
|
+
seen.add(entry.apiModel);
|
|
196
|
+
out.push(entry);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
for (const level1 of Object.values(raw as Record<string, unknown>)) {
|
|
200
|
+
if (Array.isArray(level1)) {
|
|
201
|
+
// virtual-model style: account → models[]
|
|
202
|
+
for (const row of level1) {
|
|
203
|
+
if (row != null && typeof row === "object") {
|
|
204
|
+
pushRow(row as RawEnabledModel);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (level1 == null || typeof level1 !== "object") continue;
|
|
210
|
+
for (const level2 of Object.values(level1 as Record<string, unknown>)) {
|
|
211
|
+
if (!Array.isArray(level2)) continue;
|
|
212
|
+
for (const row of level2) {
|
|
213
|
+
if (row != null && typeof row === "object") {
|
|
214
|
+
pushRow(row as RawEnabledModel);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return out;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export async function listEnabledModels(
|
|
223
|
+
opts: CpCredentials,
|
|
224
|
+
): Promise<TfyModelSelectorEntry[]> {
|
|
225
|
+
const raw = await cpFetch<unknown>(
|
|
226
|
+
opts,
|
|
227
|
+
"/api/svc/v1/llm-gateway/model/enabled",
|
|
228
|
+
);
|
|
229
|
+
return normalizeEnabledModels(raw);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
// Skills
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
type RawAgentSkill = {
|
|
237
|
+
id?: string;
|
|
238
|
+
name?: string;
|
|
239
|
+
fqn?: string;
|
|
240
|
+
latest_version?: {
|
|
241
|
+
id?: string;
|
|
242
|
+
fqn?: string;
|
|
243
|
+
manifest?: {
|
|
244
|
+
ml_repo?: string;
|
|
245
|
+
source?: { description?: string };
|
|
246
|
+
};
|
|
247
|
+
};
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
export function normalizeAgentSkills(raw: unknown): TfySkillSelectorEntry[] {
|
|
251
|
+
const data =
|
|
252
|
+
raw != null &&
|
|
253
|
+
typeof raw === "object" &&
|
|
254
|
+
Array.isArray((raw as { data?: unknown }).data)
|
|
255
|
+
? ((raw as { data: RawAgentSkill[] }).data)
|
|
256
|
+
: [];
|
|
257
|
+
const out: TfySkillSelectorEntry[] = [];
|
|
258
|
+
for (const row of data) {
|
|
259
|
+
const fqn = row.latest_version?.fqn;
|
|
260
|
+
const name = row.name;
|
|
261
|
+
if (fqn == null || fqn === "" || name == null || name === "") continue;
|
|
262
|
+
const description = row.latest_version?.manifest?.source?.description;
|
|
263
|
+
out.push({
|
|
264
|
+
id: fqn,
|
|
265
|
+
name,
|
|
266
|
+
fqn,
|
|
267
|
+
...(description != null ? { description } : {}),
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export async function listAgentSkills(
|
|
274
|
+
opts: CpCredentials,
|
|
275
|
+
): Promise<TfySkillSelectorEntry[]> {
|
|
276
|
+
const raw = await cpFetch<unknown>(
|
|
277
|
+
opts,
|
|
278
|
+
"/api/ml/v1/agent-skills?include_empty_agent_skills=false",
|
|
279
|
+
);
|
|
280
|
+
return normalizeAgentSkills(raw);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
// MCP
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
|
|
287
|
+
type RawMcpServer = {
|
|
288
|
+
id?: string;
|
|
289
|
+
name?: string;
|
|
290
|
+
fqn?: string;
|
|
291
|
+
manifest?: {
|
|
292
|
+
description?: string;
|
|
293
|
+
url?: string;
|
|
294
|
+
auth_data?: { type?: string; auth_level?: string };
|
|
295
|
+
};
|
|
296
|
+
authStatus?: { status?: string };
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
export function normalizeMcpServers(raw: unknown): TfyConnectorSelectorEntry[] {
|
|
300
|
+
const data =
|
|
301
|
+
raw != null &&
|
|
302
|
+
typeof raw === "object" &&
|
|
303
|
+
Array.isArray((raw as { data?: unknown }).data)
|
|
304
|
+
? ((raw as { data: RawMcpServer[] }).data)
|
|
305
|
+
: [];
|
|
306
|
+
const out: TfyConnectorSelectorEntry[] = [];
|
|
307
|
+
const seen = new Set<string>();
|
|
308
|
+
for (const row of data) {
|
|
309
|
+
const mcpName = row.name;
|
|
310
|
+
if (mcpName == null || mcpName === "" || seen.has(mcpName)) continue;
|
|
311
|
+
seen.add(mcpName);
|
|
312
|
+
const description = row.manifest?.description;
|
|
313
|
+
const authenticated = row.authStatus?.status === "authenticated";
|
|
314
|
+
out.push({
|
|
315
|
+
id: mcpName,
|
|
316
|
+
name: mcpName,
|
|
317
|
+
mcpName,
|
|
318
|
+
...(description != null ? { description } : {}),
|
|
319
|
+
...(row.id != null ? { serverId: row.id } : { serverId: null }),
|
|
320
|
+
authenticated,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
return out;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export async function listMcpServers(
|
|
327
|
+
opts: CpCredentials,
|
|
328
|
+
): Promise<TfyConnectorSelectorEntry[]> {
|
|
329
|
+
const raw = await cpFetch<unknown>(opts, "/api/svc/v1/mcp-servers");
|
|
330
|
+
return normalizeMcpServers(raw);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
// Agents
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
|
|
337
|
+
type RawAgent = {
|
|
338
|
+
name?: string;
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
export function normalizeAgents(raw: unknown): TfyAgentSelectorEntry[] {
|
|
342
|
+
const data =
|
|
343
|
+
raw != null &&
|
|
344
|
+
typeof raw === "object" &&
|
|
345
|
+
Array.isArray((raw as { data?: unknown }).data)
|
|
346
|
+
? ((raw as { data: RawAgent[] }).data)
|
|
347
|
+
: [];
|
|
348
|
+
const out: TfyAgentSelectorEntry[] = [];
|
|
349
|
+
for (const row of data) {
|
|
350
|
+
if (row.name == null || row.name === "") continue;
|
|
351
|
+
out.push({ name: row.name });
|
|
352
|
+
}
|
|
353
|
+
return out;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export async function listAgents(
|
|
357
|
+
opts: CpCredentials,
|
|
358
|
+
req?: SearchAgentSelectorParams,
|
|
359
|
+
): Promise<TfyAgentSelectorEntry[]> {
|
|
360
|
+
const limit = req?.limit ?? 50;
|
|
361
|
+
const offset = req?.offset ?? 0;
|
|
362
|
+
const namePrefix = req?.query ?? "";
|
|
363
|
+
const qs = new URLSearchParams({
|
|
364
|
+
type: "truefoundry-agent",
|
|
365
|
+
limit: String(limit),
|
|
366
|
+
offset: String(offset),
|
|
367
|
+
namePrefix,
|
|
368
|
+
});
|
|
369
|
+
const raw = await cpFetch<unknown>(
|
|
370
|
+
opts,
|
|
371
|
+
`/api/svc/v1/agents?${qs.toString()}`,
|
|
372
|
+
);
|
|
373
|
+
return normalizeAgents(raw);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ---------------------------------------------------------------------------
|
|
377
|
+
// Save agent (PUT upsert by name)
|
|
378
|
+
// ---------------------------------------------------------------------------
|
|
379
|
+
|
|
380
|
+
/** Platform feature flags baked into every saved agent manifest. */
|
|
381
|
+
export const SAVE_AGENT_METADATA_TAGS = {
|
|
382
|
+
agent: "tfy-ai-gateway-agent",
|
|
383
|
+
TFY_ALPHA_ENABLE_OPENUI: "true",
|
|
384
|
+
TFY_ALPHA_ENABLE_ASK_USER: "true",
|
|
385
|
+
TFY_ALPHA_ENABLE_ASK_SECRET: "true",
|
|
386
|
+
TFY_ALPHA_CONTEXT_MANAGEMENT:
|
|
387
|
+
'{"large_tool_response":{"individual_tool_response_token_threshold":8000}}',
|
|
388
|
+
TFY_ALPHA_ENABLE_FILE_DOWNLOAD: "true",
|
|
389
|
+
} as const;
|
|
390
|
+
|
|
391
|
+
/** Default ACL on create/update — everyone on the tenant gets agent-access. */
|
|
392
|
+
export const SAVE_AGENT_COLLABORATORS = [
|
|
393
|
+
{ subject: "team:everyone", role_id: "agent-access" },
|
|
394
|
+
] as const;
|
|
395
|
+
|
|
396
|
+
function camelToSnakeKey(key: string): string {
|
|
397
|
+
return key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Deep camelCase → snake_case for CP wire (model.params, config, mounts). */
|
|
401
|
+
export function toSnakeCaseDeep(value: unknown): unknown {
|
|
402
|
+
if (Array.isArray(value)) {
|
|
403
|
+
return value.map(toSnakeCaseDeep);
|
|
404
|
+
}
|
|
405
|
+
if (value != null && typeof value === "object") {
|
|
406
|
+
const out: Record<string, unknown> = {};
|
|
407
|
+
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
408
|
+
out[camelToSnakeKey(k)] = toSnakeCaseDeep(v);
|
|
409
|
+
}
|
|
410
|
+
return out;
|
|
411
|
+
}
|
|
412
|
+
return value;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function mcpMountForCp(mount: unknown): Record<string, unknown> {
|
|
416
|
+
const snake = toSnakeCaseDeep(mount) as Record<string, unknown>;
|
|
417
|
+
if (snake.type === "truefoundry-mcp-registry") {
|
|
418
|
+
return {
|
|
419
|
+
...snake,
|
|
420
|
+
enable_tools: snake.enable_tools ?? ["@all"],
|
|
421
|
+
preload: snake.preload ?? false,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
return snake;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function skillMountForCp(mount: unknown): Record<string, unknown> {
|
|
428
|
+
const snake = toSnakeCaseDeep(mount) as Record<string, unknown>;
|
|
429
|
+
if (snake.type === "truefoundry-skills-registry") {
|
|
430
|
+
return {
|
|
431
|
+
...snake,
|
|
432
|
+
preload: snake.preload ?? false,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
return snake;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Build CP `manifest` for `PUT /api/svc/v1/agents`.
|
|
440
|
+
* Normalizes UI catalog mounts, snake_cases gateway fields, hardcodes type /
|
|
441
|
+
* metadata_tags / collaborators.
|
|
442
|
+
*/
|
|
443
|
+
export function buildSaveAgentManifest(
|
|
444
|
+
agentName: string,
|
|
445
|
+
agentSpec: TfyAgentSpec,
|
|
446
|
+
): Record<string, unknown> {
|
|
447
|
+
const spec = normalizeAgentSpecForGateway(agentSpec);
|
|
448
|
+
const mcpServers = (spec.mcpServers ?? []).map(mcpMountForCp);
|
|
449
|
+
const skills = (spec.skills ?? []).map(skillMountForCp);
|
|
450
|
+
// CP AgentManifest requires description; not on FE AgentSpec yet.
|
|
451
|
+
const rawDescription = (agentSpec as { description?: unknown }).description;
|
|
452
|
+
const description = typeof rawDescription === "string" ? rawDescription : "";
|
|
453
|
+
|
|
454
|
+
return {
|
|
455
|
+
type: "truefoundry-agent",
|
|
456
|
+
name: agentName,
|
|
457
|
+
description,
|
|
458
|
+
model: toSnakeCaseDeep(spec.model),
|
|
459
|
+
metadata_tags: { ...SAVE_AGENT_METADATA_TAGS },
|
|
460
|
+
collaborators: [...SAVE_AGENT_COLLABORATORS],
|
|
461
|
+
...(spec.instructions != null ? { instructions: spec.instructions } : {}),
|
|
462
|
+
...(spec.config != null ? { config: toSnakeCaseDeep(spec.config) } : {}),
|
|
463
|
+
...(mcpServers.length > 0 ? { mcp_servers: mcpServers } : {}),
|
|
464
|
+
...(skills.length > 0 ? { skills } : {}),
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Upsert a named agent on the Control Plane.
|
|
470
|
+
* `PUT /api/svc/v1/agents` with `{ manifest }` — name is the upsert key.
|
|
471
|
+
*/
|
|
472
|
+
export async function saveAgent(
|
|
473
|
+
opts: CpCredentials,
|
|
474
|
+
req: { agentName: string; agentSpec: TfyAgentSpec },
|
|
475
|
+
): Promise<unknown> {
|
|
476
|
+
const manifest = buildSaveAgentManifest(req.agentName, req.agentSpec);
|
|
477
|
+
return cpFetch(opts, "/api/svc/v1/agents", {
|
|
478
|
+
method: "PUT",
|
|
479
|
+
headers: { "Content-Type": "application/json" },
|
|
480
|
+
body: JSON.stringify({ manifest }),
|
|
481
|
+
});
|
|
482
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { AgentBuilderServer } from "../../server/types.js";
|
|
2
|
+
import {
|
|
3
|
+
listAgents,
|
|
4
|
+
listAgentSkills,
|
|
5
|
+
listEnabledModels,
|
|
6
|
+
listMcpServers,
|
|
7
|
+
resolveGatewayURL,
|
|
8
|
+
saveAgent,
|
|
9
|
+
type TfyAgentSelectorEntry,
|
|
10
|
+
type TfyConnectorSelectorEntry,
|
|
11
|
+
type TfyModelSelectorEntry,
|
|
12
|
+
type TfySkillSelectorEntry,
|
|
13
|
+
} from "./cp.js";
|
|
14
|
+
import {
|
|
15
|
+
createTrueFoundryChatServer,
|
|
16
|
+
type TrueFoundryChatServer,
|
|
17
|
+
} from "./chatServer.js";
|
|
18
|
+
import type { TfyAgentSpec } from "./types.js";
|
|
19
|
+
|
|
20
|
+
export type CreateTrueFoundryAgentUIServerOptions = {
|
|
21
|
+
apiKey: string;
|
|
22
|
+
/** Control Plane base URL (builder lists + optional /session for gateway resolve). */
|
|
23
|
+
cpURL: string;
|
|
24
|
+
/**
|
|
25
|
+
* Gateway base URL. When omitted, resolved via
|
|
26
|
+
* `GET {cpURL}/api/svc/v1/session` → `{cpURL}{env.LLM_GATEWAY_URL ?? "/api/llm"}/{tenantName}`.
|
|
27
|
+
* Session failure throws (no silent public-gateway fallback).
|
|
28
|
+
*/
|
|
29
|
+
gatewayURL?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type TrueFoundryAgentUIServer<TSpec extends TfyAgentSpec = TfyAgentSpec> =
|
|
33
|
+
TrueFoundryChatServer<TSpec> &
|
|
34
|
+
AgentBuilderServer<
|
|
35
|
+
TSpec,
|
|
36
|
+
TfyModelSelectorEntry,
|
|
37
|
+
TfySkillSelectorEntry,
|
|
38
|
+
TfyConnectorSelectorEntry,
|
|
39
|
+
TfyAgentSelectorEntry,
|
|
40
|
+
unknown
|
|
41
|
+
>;
|
|
42
|
+
|
|
43
|
+
function credentialsKey(opts: CreateTrueFoundryAgentUIServerOptions): string {
|
|
44
|
+
return `${opts.apiKey}\0${opts.cpURL}\0${opts.gatewayURL ?? ""}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const inflight = new Map<
|
|
48
|
+
string,
|
|
49
|
+
Promise<TrueFoundryAgentUIServer<TfyAgentSpec>>
|
|
50
|
+
>();
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Full pack: gateway chat + Control Plane builder lists.
|
|
54
|
+
*
|
|
55
|
+
* Same `apiKey` bearer is used for CP and gateway. Concurrent calls with the
|
|
56
|
+
* same credentials share one in-flight promise (React Strict Mode safe).
|
|
57
|
+
*/
|
|
58
|
+
export async function createTrueFoundryAgentUIServer<
|
|
59
|
+
TSpec extends TfyAgentSpec = TfyAgentSpec,
|
|
60
|
+
>(
|
|
61
|
+
opts: CreateTrueFoundryAgentUIServerOptions,
|
|
62
|
+
): Promise<TrueFoundryAgentUIServer<TSpec>> {
|
|
63
|
+
const key = credentialsKey(opts);
|
|
64
|
+
const existing = inflight.get(key);
|
|
65
|
+
if (existing != null) {
|
|
66
|
+
return existing as Promise<TrueFoundryAgentUIServer<TSpec>>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const promise = (async () => {
|
|
70
|
+
const baseUrl = await resolveGatewayURL(opts);
|
|
71
|
+
const chat = createTrueFoundryChatServer<TSpec>({
|
|
72
|
+
apiKey: opts.apiKey,
|
|
73
|
+
baseUrl,
|
|
74
|
+
});
|
|
75
|
+
const cp = { apiKey: opts.apiKey, cpURL: opts.cpURL };
|
|
76
|
+
|
|
77
|
+
const server: TrueFoundryAgentUIServer<TSpec> = {
|
|
78
|
+
...chat,
|
|
79
|
+
getModels: () => listEnabledModels(cp),
|
|
80
|
+
getSkills: () => listAgentSkills(cp),
|
|
81
|
+
getMcp: () => listMcpServers(cp),
|
|
82
|
+
searchAgents: (req) => listAgents(cp, req),
|
|
83
|
+
saveAgent: (req) => saveAgent(cp, req),
|
|
84
|
+
};
|
|
85
|
+
return server as TrueFoundryAgentUIServer<TfyAgentSpec>;
|
|
86
|
+
})();
|
|
87
|
+
|
|
88
|
+
inflight.set(key, promise);
|
|
89
|
+
try {
|
|
90
|
+
return (await promise) as TrueFoundryAgentUIServer<TSpec>;
|
|
91
|
+
} finally {
|
|
92
|
+
inflight.delete(key);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Point-of-use narrowing for the event half of the gateway protocol.
|
|
3
3
|
*
|
|
4
4
|
* `AgentChatServer` hardcodes the runtime's event types on listEvents,
|
|
5
|
-
* listTurnEvents, subscribeToTurn and
|
|
5
|
+
* listTurnEvents, subscribeToTurn and createTurn — there is no
|
|
6
6
|
* generic to override them from here. So instead of typing those channels,
|
|
7
7
|
* hosts call these guards on the values they receive.
|
|
8
8
|
*
|