@truefoundry/assistant-ui-runtime 0.1.9 → 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-CXBZ6WLZ.js → chunk-6ZOGBD7E.js} +99 -17
- package/dist/chunk-6ZOGBD7E.js.map +1 -0
- package/dist/index.d.ts +26 -5
- package/dist/index.js +92 -71
- 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-6yWuWHzK.d.ts → types-Bb8Kaf1h.d.ts} +16 -3
- package/package.json +3 -3
- package/src/convertTurnMessages.test.ts +23 -1
- package/src/convertTurnMessages.ts +9 -3
- package/src/draft/truefoundryDraftThreadListAdapter.test.ts +57 -4
- package/src/draft/truefoundryDraftThreadListAdapter.ts +18 -22
- package/src/hooks.ts +23 -3
- package/src/index.ts +1 -0
- package/src/messageCustomMetadata.ts +2 -0
- package/src/plugins/truefoundry-agent-server-adapter/chatServer.ts +7 -6
- 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 +18 -6
- package/src/sessionThreadMetadata.ts +39 -0
- package/src/truefoundryExtras.ts +1 -1
- 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 +17 -4
- package/dist/chunk-CXBZ6WLZ.js.map +0 -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", () => {
|
|
@@ -20,7 +20,7 @@ export function normalizeMcpMount(
|
|
|
20
20
|
throw new Error("mcpServers entry must be an object");
|
|
21
21
|
}
|
|
22
22
|
if (raw.type === "truefoundry-mcp-registry" || raw.type === "inline") {
|
|
23
|
-
return raw as TruefoundryGatewayApi.McpServer;
|
|
23
|
+
return raw as unknown as TruefoundryGatewayApi.McpServer;
|
|
24
24
|
}
|
|
25
25
|
const name =
|
|
26
26
|
(typeof raw.name === "string" && raw.name !== "" ? raw.name : null) ??
|
|
@@ -33,7 +33,13 @@ export function normalizeMcpMount(
|
|
|
33
33
|
"mcpServers entry needs name, mcpName, or id to mount as registry MCP",
|
|
34
34
|
);
|
|
35
35
|
}
|
|
36
|
-
return {
|
|
36
|
+
return {
|
|
37
|
+
type: "truefoundry-mcp-registry",
|
|
38
|
+
name,
|
|
39
|
+
...(Array.isArray(raw.enableTools) ? { enableTools: raw.enableTools } : {}),
|
|
40
|
+
...(typeof raw.preload === "boolean" ? { preload: raw.preload } : {}),
|
|
41
|
+
...(raw.config != null ? { config: raw.config } : {}),
|
|
42
|
+
} as unknown as TruefoundryGatewayApi.McpServer;
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
export function normalizeSkillMount(
|
|
@@ -43,7 +49,7 @@ export function normalizeSkillMount(
|
|
|
43
49
|
throw new Error("skills entry must be an object");
|
|
44
50
|
}
|
|
45
51
|
if (raw.type === "truefoundry-skills-registry" || raw.type === "git") {
|
|
46
|
-
return raw as TruefoundryGatewayApi.SkillMount;
|
|
52
|
+
return raw as unknown as TruefoundryGatewayApi.SkillMount;
|
|
47
53
|
}
|
|
48
54
|
const fqn =
|
|
49
55
|
(typeof raw.fqn === "string" && raw.fqn !== "" ? raw.fqn : null) ??
|
|
@@ -56,8 +62,9 @@ export function normalizeSkillMount(
|
|
|
56
62
|
return {
|
|
57
63
|
type: "truefoundry-skills-registry",
|
|
58
64
|
fqn,
|
|
59
|
-
...(raw.preload ===
|
|
60
|
-
|
|
65
|
+
...(typeof raw.preload === "boolean" ? { preload: raw.preload } : {}),
|
|
66
|
+
...(raw.config != null ? { config: raw.config } : {}),
|
|
67
|
+
} as unknown as TruefoundryGatewayApi.SkillMount;
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
/** Rewrite UI `{id,name}` mounts so create/update draft session can serialize. */
|
package/src/server/types.ts
CHANGED
|
@@ -38,9 +38,13 @@ export interface ConnectorSelectorEntry {
|
|
|
38
38
|
description?: string;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
/** Agent selector row
|
|
41
|
+
/** Agent selector row. Host extends for metadata; `agentSpec` enables Edit. */
|
|
42
42
|
export interface AgentSelectorEntry {
|
|
43
43
|
name: string;
|
|
44
|
+
/** Stable id when distinct from display `name`. Falls back to `name` when omitted. */
|
|
45
|
+
agentId?: string;
|
|
46
|
+
/** Published agent spec — required for Edit; optional for Try-only hosts. */
|
|
47
|
+
agentSpec?: AgentSpec;
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
export type SearchAgentSelectorParams = {
|
|
@@ -147,7 +151,8 @@ export type PageParams = {
|
|
|
147
151
|
};
|
|
148
152
|
|
|
149
153
|
export interface ListSessionsParams extends PageParams {
|
|
150
|
-
|
|
154
|
+
/** Host-owned agent identity filter. Hosts that key agents by name pass that name here. */
|
|
155
|
+
agentId?: string;
|
|
151
156
|
/** Host-specific filter (e.g. TFY startTimestamp). */
|
|
152
157
|
startTimestamp?: string;
|
|
153
158
|
}
|
|
@@ -291,10 +296,17 @@ export interface AgentChatServer<
|
|
|
291
296
|
abortSignal?: AbortSignal;
|
|
292
297
|
}): AsyncIterable<TurnStreamData>;
|
|
293
298
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
299
|
+
/**
|
|
300
|
+
* Reads a file the agent wrote inside its sandbox. Hosts whose download route is scoped to a
|
|
301
|
+
* turn resolve the sandbox from `turnId` and ignore `sandboxId`; hosts addressing sandboxes
|
|
302
|
+
* directly use `sandboxId`.
|
|
303
|
+
*/
|
|
304
|
+
downloadSandboxFile?(req: {
|
|
305
|
+
sessionId: string;
|
|
306
|
+
turnId: string;
|
|
307
|
+
sandboxId: string;
|
|
308
|
+
path: string;
|
|
309
|
+
}): Promise<Blob>;
|
|
298
310
|
}
|
|
299
311
|
|
|
300
312
|
/**
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { RemoteThreadMetadata } from "@assistant-ui/core";
|
|
2
|
+
|
|
3
|
+
import { draftSessionTitle } from "./draft/agentSpec.js";
|
|
4
|
+
import type { AgentSpec, Session } from "./server/types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Title for a session row in a mixed (draft + named) thread list.
|
|
8
|
+
* Mutable sessions use draftSessionTitle (optionally falling back to
|
|
9
|
+
* `defaultAgentSpec`); named sessions use title → agentName → id.
|
|
10
|
+
*/
|
|
11
|
+
export function sessionDisplayTitle(
|
|
12
|
+
session: Session,
|
|
13
|
+
defaultAgentSpec?: AgentSpec,
|
|
14
|
+
): string {
|
|
15
|
+
if (session.isMutable) {
|
|
16
|
+
const agentSpec = session.agentSpec ?? defaultAgentSpec;
|
|
17
|
+
if (agentSpec != null) {
|
|
18
|
+
return draftSessionTitle({
|
|
19
|
+
title: session.title,
|
|
20
|
+
agentSpec,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return session.title ?? session.agentName ?? session.id;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Map a Session DTO onto RemoteThreadMetadata, including display agentName in custom. */
|
|
28
|
+
export function sessionToThreadMetadata(
|
|
29
|
+
session: Session,
|
|
30
|
+
title: string | undefined,
|
|
31
|
+
): RemoteThreadMetadata {
|
|
32
|
+
return {
|
|
33
|
+
status: "regular",
|
|
34
|
+
remoteId: session.id,
|
|
35
|
+
title,
|
|
36
|
+
lastMessageAt: new Date(session.updatedAt),
|
|
37
|
+
...(session.agentName != null ? { custom: { agentName: session.agentName } } : {}),
|
|
38
|
+
};
|
|
39
|
+
}
|
package/src/truefoundryExtras.ts
CHANGED
|
@@ -26,7 +26,7 @@ export type TrueFoundryRuntimeExtras = {
|
|
|
26
26
|
respondToToolApproval: (response: RespondToToolApprovalOptions) => void;
|
|
27
27
|
respondToToolResponse: (response: RespondToToolResponseOptions) => void;
|
|
28
28
|
resumeMcpAuth: () => Promise<void>;
|
|
29
|
-
downloadSandboxFile: (path: string) => Promise<Blob>;
|
|
29
|
+
downloadSandboxFile: (req: { turnId: string; path: string }) => Promise<Blob>;
|
|
30
30
|
cancel: () => Promise<void>;
|
|
31
31
|
resetFromTurn: (turnId: string) => Promise<void>;
|
|
32
32
|
reload: () => void;
|
|
@@ -61,6 +61,7 @@ describe("createTrueFoundryOwnedSessionsThreadListAdapter", () => {
|
|
|
61
61
|
remoteId: "s1",
|
|
62
62
|
title: "Named chat",
|
|
63
63
|
lastMessageAt: new Date("2026-06-30T12:00:00.000Z"),
|
|
64
|
+
custom: { agentName: "my-agent" },
|
|
64
65
|
},
|
|
65
66
|
{
|
|
66
67
|
status: "regular",
|
|
@@ -72,6 +73,21 @@ describe("createTrueFoundryOwnedSessionsThreadListAdapter", () => {
|
|
|
72
73
|
expect(result.nextCursor).toBe("page-2");
|
|
73
74
|
});
|
|
74
75
|
|
|
76
|
+
it("forwards listSessionsAgentId as agentId", async () => {
|
|
77
|
+
const listSessions = vi.fn().mockResolvedValue({ data: [] });
|
|
78
|
+
const server = mockServer({ listSessions, getSession: vi.fn() });
|
|
79
|
+
|
|
80
|
+
const adapter = createTrueFoundryOwnedSessionsThreadListAdapter({
|
|
81
|
+
server,
|
|
82
|
+
listSessionsAgentId: "agent-x",
|
|
83
|
+
});
|
|
84
|
+
await adapter.list();
|
|
85
|
+
|
|
86
|
+
expect(listSessions).toHaveBeenCalledWith(
|
|
87
|
+
expect.objectContaining({ agentId: "agent-x" }),
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
75
91
|
it("falls back to model name for untitled drafts", async () => {
|
|
76
92
|
const listSessions = vi.fn().mockResolvedValue({
|
|
77
93
|
data: [mockDraftSession("d1", undefined, "2026-06-30T11:00:00.000Z")],
|
|
@@ -1,43 +1,36 @@
|
|
|
1
1
|
import type { RemoteThreadListAdapter } from "@assistant-ui/core";
|
|
2
2
|
|
|
3
|
-
import type { AgentChatServer
|
|
4
|
-
import { draftSessionTitle } from "./draft/agentSpec.js";
|
|
3
|
+
import type { AgentChatServer } from "./server/types.js";
|
|
5
4
|
import { sessionListStartTimestamp } from "./sessionListStartTimestamp.js";
|
|
5
|
+
import {
|
|
6
|
+
sessionDisplayTitle,
|
|
7
|
+
sessionToThreadMetadata,
|
|
8
|
+
} from "./sessionThreadMetadata.js";
|
|
6
9
|
|
|
7
10
|
const THREAD_LIST_PAGE_SIZE = 20;
|
|
8
11
|
|
|
9
|
-
function ownedSessionTitle(session: Session): string {
|
|
10
|
-
if (session.isMutable && session.agentSpec != null) {
|
|
11
|
-
return draftSessionTitle({
|
|
12
|
-
title: session.title,
|
|
13
|
-
agentSpec: session.agentSpec,
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
return session.title ?? session.agentName ?? session.id;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
12
|
/**
|
|
20
13
|
* Read-only thread-list adapter backed by `AgentChatServer.listSessions`.
|
|
21
14
|
* Hosts that previously used listOwnedSessions should filter in their server impl.
|
|
22
15
|
*/
|
|
23
16
|
export function createTrueFoundryOwnedSessionsThreadListAdapter(options: {
|
|
24
17
|
server: AgentChatServer;
|
|
18
|
+
/** When set, filters `listSessions` by this agent id. Omit for all chats. */
|
|
19
|
+
listSessionsAgentId?: string;
|
|
25
20
|
}): RemoteThreadListAdapter {
|
|
26
|
-
const { server } = options;
|
|
21
|
+
const { server, listSessionsAgentId } = options;
|
|
27
22
|
|
|
28
23
|
return {
|
|
29
24
|
async list({ after } = {}) {
|
|
30
25
|
const page = await server.listSessions({
|
|
26
|
+
...(listSessionsAgentId != null ? { agentId: listSessionsAgentId } : {}),
|
|
31
27
|
limit: THREAD_LIST_PAGE_SIZE,
|
|
32
28
|
pageToken: after,
|
|
33
29
|
startTimestamp: sessionListStartTimestamp(),
|
|
34
30
|
});
|
|
35
|
-
const threads = page.data.map((session) =>
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
title: ownedSessionTitle(session),
|
|
39
|
-
lastMessageAt: new Date(session.updatedAt),
|
|
40
|
-
}));
|
|
31
|
+
const threads = page.data.map((session) =>
|
|
32
|
+
sessionToThreadMetadata(session, sessionDisplayTitle(session)),
|
|
33
|
+
);
|
|
41
34
|
return {
|
|
42
35
|
threads,
|
|
43
36
|
nextCursor: page.nextPageToken ?? undefined,
|
|
@@ -52,12 +45,7 @@ export function createTrueFoundryOwnedSessionsThreadListAdapter(options: {
|
|
|
52
45
|
|
|
53
46
|
async fetch(remoteId) {
|
|
54
47
|
const session = await server.getSession({ sessionId: remoteId });
|
|
55
|
-
return
|
|
56
|
-
status: "regular" as const,
|
|
57
|
-
remoteId: session.id,
|
|
58
|
-
title: ownedSessionTitle(session),
|
|
59
|
-
lastMessageAt: new Date(session.updatedAt),
|
|
60
|
-
};
|
|
48
|
+
return sessionToThreadMetadata(session, sessionDisplayTitle(session));
|
|
61
49
|
},
|
|
62
50
|
|
|
63
51
|
async rename() {},
|
|
@@ -4,13 +4,19 @@ import type { AgentChatServer, Session } from "./server/index.js";
|
|
|
4
4
|
|
|
5
5
|
import { createTrueFoundryThreadListAdapter } from "./truefoundryThreadListAdapter.js";
|
|
6
6
|
|
|
7
|
-
function mockSession(
|
|
7
|
+
function mockSession(
|
|
8
|
+
id: string,
|
|
9
|
+
title: string,
|
|
10
|
+
updatedAt: string,
|
|
11
|
+
agentName?: string,
|
|
12
|
+
): Session {
|
|
8
13
|
return {
|
|
9
14
|
id,
|
|
10
15
|
title,
|
|
11
16
|
updatedAt,
|
|
12
17
|
createdAt: updatedAt,
|
|
13
18
|
isMutable: false,
|
|
19
|
+
...(agentName != null ? { agentName } : {}),
|
|
14
20
|
};
|
|
15
21
|
}
|
|
16
22
|
|
|
@@ -26,10 +32,10 @@ function mockServer(partial: Partial<AgentChatServer>): AgentChatServer {
|
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
describe("createTrueFoundryThreadListAdapter", () => {
|
|
29
|
-
it("lists the first page
|
|
35
|
+
it("lists the first page without agentId when filter is omitted", async () => {
|
|
30
36
|
const listSessions = vi.fn().mockResolvedValue(
|
|
31
37
|
mockListSessionsPage(
|
|
32
|
-
[mockSession("s1", "First", "2026-06-30T10:00:00.000Z")],
|
|
38
|
+
[mockSession("s1", "First", "2026-06-30T10:00:00.000Z", "my-agent")],
|
|
33
39
|
"page-2",
|
|
34
40
|
),
|
|
35
41
|
);
|
|
@@ -43,40 +49,42 @@ describe("createTrueFoundryThreadListAdapter", () => {
|
|
|
43
49
|
|
|
44
50
|
expect(listSessions).toHaveBeenCalledWith(
|
|
45
51
|
expect.objectContaining({
|
|
46
|
-
agentName: "my-agent",
|
|
47
52
|
limit: 20,
|
|
48
53
|
pageToken: undefined,
|
|
49
54
|
startTimestamp: expect.any(String),
|
|
50
55
|
}),
|
|
51
56
|
);
|
|
57
|
+
expect(listSessions).toHaveBeenCalledWith(
|
|
58
|
+
expect.not.objectContaining({ agentId: expect.anything() }),
|
|
59
|
+
);
|
|
52
60
|
expect(result.threads).toEqual([
|
|
53
61
|
{
|
|
54
62
|
status: "regular",
|
|
55
63
|
remoteId: "s1",
|
|
56
64
|
title: "First",
|
|
57
65
|
lastMessageAt: new Date("2026-06-30T10:00:00.000Z"),
|
|
66
|
+
custom: { agentName: "my-agent" },
|
|
58
67
|
},
|
|
59
68
|
]);
|
|
60
69
|
expect(result.nextCursor).toBe("page-2");
|
|
61
70
|
});
|
|
62
71
|
|
|
63
|
-
it("forwards
|
|
72
|
+
it("forwards listSessionsAgentId as agentId", async () => {
|
|
64
73
|
const listSessions = vi.fn().mockResolvedValue(
|
|
65
|
-
mockListSessionsPage(
|
|
66
|
-
[mockSession("s2", "Second", "2026-06-29T10:00:00.000Z")],
|
|
67
|
-
),
|
|
74
|
+
mockListSessionsPage([mockSession("s2", "Second", "2026-06-29T10:00:00.000Z")]),
|
|
68
75
|
);
|
|
69
76
|
const server = mockServer({ listSessions });
|
|
70
77
|
const adapter = createTrueFoundryThreadListAdapter({
|
|
71
78
|
server,
|
|
72
79
|
agentName: "my-agent",
|
|
80
|
+
listSessionsAgentId: "filter-agent",
|
|
73
81
|
});
|
|
74
82
|
|
|
75
83
|
const result = await adapter.list({ after: "page-2" });
|
|
76
84
|
|
|
77
85
|
expect(listSessions).toHaveBeenCalledWith(
|
|
78
86
|
expect.objectContaining({
|
|
79
|
-
|
|
87
|
+
agentId: "filter-agent",
|
|
80
88
|
limit: 20,
|
|
81
89
|
pageToken: "page-2",
|
|
82
90
|
}),
|
|
@@ -3,29 +3,29 @@ import type { RemoteThreadListAdapter } from "@assistant-ui/core";
|
|
|
3
3
|
import type { AgentChatServer } from "./server/types.js";
|
|
4
4
|
import { getSession } from "./sessions.js";
|
|
5
5
|
import { sessionListStartTimestamp } from "./sessionListStartTimestamp.js";
|
|
6
|
+
import { sessionToThreadMetadata } from "./sessionThreadMetadata.js";
|
|
6
7
|
|
|
7
8
|
const THREAD_LIST_PAGE_SIZE = 20;
|
|
8
9
|
|
|
9
10
|
export function createTrueFoundryThreadListAdapter(options: {
|
|
10
11
|
server: AgentChatServer;
|
|
11
12
|
agentName: string;
|
|
13
|
+
/** When set, filters `listSessions` by this agent id. Omit for all chats. */
|
|
14
|
+
listSessionsAgentId?: string;
|
|
12
15
|
}): RemoteThreadListAdapter {
|
|
13
|
-
const { server, agentName } = options;
|
|
16
|
+
const { server, agentName, listSessionsAgentId } = options;
|
|
14
17
|
|
|
15
18
|
return {
|
|
16
19
|
async list({ after } = {}) {
|
|
17
20
|
const page = await server.listSessions({
|
|
18
|
-
|
|
21
|
+
...(listSessionsAgentId != null ? { agentId: listSessionsAgentId } : {}),
|
|
19
22
|
limit: THREAD_LIST_PAGE_SIZE,
|
|
20
23
|
pageToken: after,
|
|
21
24
|
startTimestamp: sessionListStartTimestamp(),
|
|
22
25
|
});
|
|
23
|
-
const threads = page.data.map((session) =>
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
title: session.title ?? undefined,
|
|
27
|
-
lastMessageAt: new Date(session.updatedAt),
|
|
28
|
-
}));
|
|
26
|
+
const threads = page.data.map((session) =>
|
|
27
|
+
sessionToThreadMetadata(session, session.title ?? undefined),
|
|
28
|
+
);
|
|
29
29
|
return {
|
|
30
30
|
threads,
|
|
31
31
|
nextCursor: page.nextPageToken ?? undefined,
|
|
@@ -39,12 +39,7 @@ export function createTrueFoundryThreadListAdapter(options: {
|
|
|
39
39
|
|
|
40
40
|
async fetch(remoteId) {
|
|
41
41
|
const session = await getSession(server, remoteId);
|
|
42
|
-
return
|
|
43
|
-
status: "regular" as const,
|
|
44
|
-
remoteId: session.id,
|
|
45
|
-
title: session.title ?? undefined,
|
|
46
|
-
lastMessageAt: new Date(session.updatedAt),
|
|
47
|
-
};
|
|
42
|
+
return sessionToThreadMetadata(session, session.title ?? undefined);
|
|
48
43
|
},
|
|
49
44
|
|
|
50
45
|
async rename() {},
|
package/src/types.ts
CHANGED
|
@@ -29,6 +29,11 @@ type TrueFoundryAgentRuntimeBaseOptions = ExternalStoreSharedOptions & {
|
|
|
29
29
|
onThreadIdChange?: ((threadId: string | undefined) => void) | undefined;
|
|
30
30
|
onError?: ((error: unknown) => void) | undefined;
|
|
31
31
|
listEventsConcurrency?: number | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Optional filter forwarded to `listSessions({ agentId })`.
|
|
34
|
+
* Omit for all chats; hosts that key agents by name pass that name as the id.
|
|
35
|
+
*/
|
|
36
|
+
listSessionsAgentId?: string | undefined;
|
|
32
37
|
adapters?:
|
|
33
38
|
| {
|
|
34
39
|
attachments?: AttachmentAdapter | undefined;
|
|
@@ -152,18 +152,28 @@ function useTrueFoundryAgentRuntimeImpl(
|
|
|
152
152
|
);
|
|
153
153
|
|
|
154
154
|
const downloadSandboxFile = useCallback(
|
|
155
|
-
async (path: string) => {
|
|
155
|
+
async ({ turnId, path }: { turnId: string; path: string }) => {
|
|
156
156
|
if (server.downloadSandboxFile == null) {
|
|
157
157
|
throw new Error(
|
|
158
158
|
"Downloading a sandbox file requires AgentChatServer.downloadSandboxFile.",
|
|
159
159
|
);
|
|
160
160
|
}
|
|
161
|
+
if (sessionId == null) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
"This session has not been saved yet, so its files cannot be downloaded.",
|
|
164
|
+
);
|
|
165
|
+
}
|
|
161
166
|
if (sandboxId == null) {
|
|
162
167
|
throw new Error("No sandbox is available yet for this session.");
|
|
163
168
|
}
|
|
164
|
-
return await server.downloadSandboxFile(
|
|
169
|
+
return await server.downloadSandboxFile({
|
|
170
|
+
sessionId,
|
|
171
|
+
turnId,
|
|
172
|
+
sandboxId,
|
|
173
|
+
path,
|
|
174
|
+
});
|
|
165
175
|
},
|
|
166
|
-
[server, sandboxId],
|
|
176
|
+
[server, sessionId, sandboxId],
|
|
167
177
|
);
|
|
168
178
|
|
|
169
179
|
const draftExtras = useMemo(() => {
|
|
@@ -272,6 +282,7 @@ export function useTrueFoundryAgentRuntime(options: UseTrueFoundryAgentRuntimeOp
|
|
|
272
282
|
|
|
273
283
|
const agentMode = agent.mode;
|
|
274
284
|
const namedAgentName = agent.mode === "named" ? agent.agentName : undefined;
|
|
285
|
+
const listSessionsAgentId = resolved.listSessionsAgentId;
|
|
275
286
|
const threadListAdapter = useMemo(() => {
|
|
276
287
|
if (agentMode === "draft") {
|
|
277
288
|
const draftAgent = agent as Extract<typeof agent, { mode: "draft" }>;
|
|
@@ -279,14 +290,16 @@ export function useTrueFoundryAgentRuntime(options: UseTrueFoundryAgentRuntimeOp
|
|
|
279
290
|
server,
|
|
280
291
|
defaultAgentSpec: draftAgent.defaultAgentSpec,
|
|
281
292
|
getAgentSpec: () => pendingAgentSpecRef.current ?? draftAgent.defaultAgentSpec,
|
|
293
|
+
listSessionsAgentId,
|
|
282
294
|
});
|
|
283
295
|
}
|
|
284
296
|
return createTrueFoundryThreadListAdapter({
|
|
285
297
|
server,
|
|
286
298
|
agentName: namedAgentName!,
|
|
299
|
+
listSessionsAgentId,
|
|
287
300
|
});
|
|
288
301
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
289
|
-
}, [agentMode, namedAgentName, server]);
|
|
302
|
+
}, [agentMode, namedAgentName, listSessionsAgentId, server]);
|
|
290
303
|
|
|
291
304
|
return useRemoteThreadListRuntime({
|
|
292
305
|
allowNesting: true,
|