@truefoundry/assistant-ui-runtime 0.1.2 → 0.1.3-rc.1
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/dist/index.d.ts +21 -8
- package/dist/index.js +104 -1057
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/src/draftAgentConfig.test.ts +30 -1
- package/src/index.ts +3 -0
- package/src/loadSessionSnapshot.ts +1 -1
- package/src/private/bindDraftAgentSession.test.ts +17 -19
- package/src/private/bindDraftAgentSession.ts +14 -52
- package/src/private/draftSessionBridge.ts +6 -8
- package/src/private/getGatewayFromPrivateClient.ts +13 -0
- package/src/private/truefoundryDraftThreadListAdapter.test.ts +41 -31
- package/src/private/truefoundryDraftThreadListAdapter.ts +8 -8
- package/src/sessions.ts +5 -5
- package/src/truefoundryOwnedSessionsThreadListAdapter.test.ts +100 -0
- package/src/truefoundryOwnedSessionsThreadListAdapter.ts +77 -0
- package/src/types.ts +10 -7
- package/src/useTrueFoundryAgentMessages.test.tsx +5 -5
- package/src/useTrueFoundryAgentMessages.ts +6 -6
- package/src/useTrueFoundryAgentRuntime.ts +15 -12
- package/src/agentSessionModule.d.ts +0 -37
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@truefoundry/assistant-ui-runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3-rc.1",
|
|
4
4
|
"description": "TrueFoundry Gateway agent runtime adapter for assistant-ui",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"@types/react": "*",
|
|
52
52
|
"react": "^18 || ^19",
|
|
53
|
-
"truefoundry-gateway-sdk": "^0.3.1"
|
|
53
|
+
"truefoundry-gateway-sdk": "^0.3.2-rc.1"
|
|
54
54
|
},
|
|
55
55
|
"peerDependenciesMeta": {
|
|
56
56
|
"@types/react": {
|
|
@@ -63,8 +63,9 @@
|
|
|
63
63
|
"@types/react": "^19.2.17",
|
|
64
64
|
"jsdom": "^29.1.1",
|
|
65
65
|
"react": "^19.2.4",
|
|
66
|
+
"truefoundry-gateway-sdk": "0.3.2-rc.1",
|
|
66
67
|
"tsup": "^8.5.0",
|
|
67
68
|
"typescript": "^5.9.3",
|
|
68
69
|
"vitest": "^4.1.9"
|
|
69
70
|
}
|
|
70
|
-
}
|
|
71
|
+
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
|
|
3
3
|
import { mergeAgentSpec } from "./private/agentSpec.js";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
resolveTrueFoundryAgentConfig,
|
|
6
|
+
resolveTrueFoundryAgentRuntimeOptions,
|
|
7
|
+
} from "./types.js";
|
|
8
|
+
import type { AgentSessionClient } from "truefoundry-gateway-sdk/agents";
|
|
9
|
+
import type { PrivateAgentSessionClient } from "truefoundry-gateway-sdk/agents/private";
|
|
5
10
|
|
|
6
11
|
describe("resolveTrueFoundryAgentConfig", () => {
|
|
7
12
|
it("supports legacy agentName", () => {
|
|
@@ -36,6 +41,30 @@ describe("resolveTrueFoundryAgentConfig", () => {
|
|
|
36
41
|
});
|
|
37
42
|
});
|
|
38
43
|
|
|
44
|
+
describe("resolveTrueFoundryAgentRuntimeOptions", () => {
|
|
45
|
+
const client = {} as AgentSessionClient;
|
|
46
|
+
|
|
47
|
+
it("requires privateClient for draft mode", () => {
|
|
48
|
+
expect(() =>
|
|
49
|
+
resolveTrueFoundryAgentRuntimeOptions({
|
|
50
|
+
client,
|
|
51
|
+
agent: { mode: "draft", defaultAgentSpec: { model: { name: "x" } } },
|
|
52
|
+
}),
|
|
53
|
+
).toThrow(/privateClient/);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("accepts privateClient for draft mode", () => {
|
|
57
|
+
const privateClient = {} as PrivateAgentSessionClient;
|
|
58
|
+
const resolved = resolveTrueFoundryAgentRuntimeOptions({
|
|
59
|
+
client,
|
|
60
|
+
privateClient,
|
|
61
|
+
agent: { mode: "draft", defaultAgentSpec: { model: { name: "x" } } },
|
|
62
|
+
});
|
|
63
|
+
expect(resolved.privateClient).toBe(privateClient);
|
|
64
|
+
expect(resolved.agent.mode).toBe("draft");
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
39
68
|
describe("mergeAgentSpec", () => {
|
|
40
69
|
it("deep-merges model params", () => {
|
|
41
70
|
const base = {
|
package/src/index.ts
CHANGED
|
@@ -16,8 +16,11 @@ export type { UseTrueFoundryAgentRuntimeOptions, NamedAgentConfig, DraftAgentCon
|
|
|
16
16
|
export type { AgentSpec, AgentSpecUpdate, DraftSession } from "./private/agentSpec.js";
|
|
17
17
|
export { mergeAgentSpec, draftSessionTitle } from "./private/agentSpec.js";
|
|
18
18
|
export { createTrueFoundryDraftThreadListAdapter } from "./private/truefoundryDraftThreadListAdapter.js";
|
|
19
|
+
export { createTrueFoundryOwnedSessionsThreadListAdapter } from "./truefoundryOwnedSessionsThreadListAdapter.js";
|
|
19
20
|
export { createDraftSessionBridge } from "./private/draftSessionBridge.js";
|
|
20
21
|
export type { DraftSessionBridge } from "./private/draftSessionBridge.js";
|
|
22
|
+
export { PrivateAgentSessionClient } from "truefoundry-gateway-sdk/agents/private";
|
|
23
|
+
export type { AgentDraftSession } from "truefoundry-gateway-sdk/agents/private";
|
|
21
24
|
export {
|
|
22
25
|
useTrueFoundryAgentSpec,
|
|
23
26
|
useTrueFoundryUpdateAgentSpec,
|
|
@@ -21,7 +21,7 @@ export function loadSessionSnapshot(
|
|
|
21
21
|
onProgress?: (snap: SessionSnapshot) => void,
|
|
22
22
|
): Promise<SessionSnapshot> {
|
|
23
23
|
const cacheKey =
|
|
24
|
-
sessionOptions?.
|
|
24
|
+
sessionOptions?.privateClient != null ? `draft:${sessionId}` : sessionId;
|
|
25
25
|
let inflight = inflightBySessionId.get(cacheKey);
|
|
26
26
|
if (inflight == null) {
|
|
27
27
|
inflight = getSession(client, sessionId, sessionOptions)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
2
|
|
|
3
|
-
import type {
|
|
4
|
-
import type { AgentSessionClient } from "truefoundry-gateway-sdk/agents";
|
|
3
|
+
import type { PrivateAgentSessionClient } from "truefoundry-gateway-sdk/agents/private";
|
|
5
4
|
|
|
6
5
|
import { bindDraftAgentSession } from "./bindDraftAgentSession.js";
|
|
7
6
|
|
|
8
7
|
const draft = {
|
|
8
|
+
type: "session/draft" as const,
|
|
9
9
|
id: "draft-1",
|
|
10
10
|
agentSpec: { model: { name: "anthropic/claude-sonnet-4-6" } },
|
|
11
11
|
createdBySubject: { type: "user" as const, id: "u1" },
|
|
@@ -15,17 +15,14 @@ const draft = {
|
|
|
15
15
|
|
|
16
16
|
describe("bindDraftAgentSession", () => {
|
|
17
17
|
it("validates the draft and binds turns to the draft session id", async () => {
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
} as unknown as
|
|
22
|
-
const client = {
|
|
23
|
-
client: gateway,
|
|
24
|
-
} as unknown as AgentSessionClient;
|
|
18
|
+
const getDraftSession = vi.fn().mockResolvedValue(draft);
|
|
19
|
+
const privateClient = {
|
|
20
|
+
getDraftSession,
|
|
21
|
+
} as unknown as PrivateAgentSessionClient;
|
|
25
22
|
|
|
26
|
-
const session = await bindDraftAgentSession(
|
|
23
|
+
const session = await bindDraftAgentSession(privateClient, "draft-1");
|
|
27
24
|
|
|
28
|
-
expect(
|
|
25
|
+
expect(getDraftSession).toHaveBeenCalledWith({ draftSessionId: "draft-1" });
|
|
29
26
|
expect(session.id).toBe("draft-1");
|
|
30
27
|
});
|
|
31
28
|
});
|
|
@@ -34,18 +31,19 @@ describe("createDraftSessionBridge", () => {
|
|
|
34
31
|
it("syncs agent spec via draftSessions.update and returns updatedAt", async () => {
|
|
35
32
|
const { createDraftSessionBridge } = await import("./draftSessionBridge.js");
|
|
36
33
|
const update = vi.fn().mockResolvedValue({ data: draft });
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
34
|
+
const getDraftSession = vi.fn().mockResolvedValue(draft);
|
|
35
|
+
const privateClient = {
|
|
36
|
+
getDraftSession,
|
|
37
|
+
client: {
|
|
38
|
+
agents: {
|
|
39
|
+
private: {
|
|
40
|
+
draftSessions: { update },
|
|
43
41
|
},
|
|
44
42
|
},
|
|
45
43
|
},
|
|
46
|
-
} as unknown as
|
|
44
|
+
} as unknown as PrivateAgentSessionClient;
|
|
47
45
|
|
|
48
|
-
const bridge = createDraftSessionBridge(
|
|
46
|
+
const bridge = createDraftSessionBridge(privateClient);
|
|
49
47
|
const updatedAt = await bridge.syncAgentSpec("draft-1", draft.agentSpec);
|
|
50
48
|
|
|
51
49
|
expect(update).toHaveBeenCalledWith("draft-1", {
|
|
@@ -1,65 +1,27 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
3
|
-
// SDK does not publicly export the AgentSession class; bind draft ids for turn routes.
|
|
4
|
-
import { AgentSession as AgentSessionClass } from "truefoundry-gateway-sdk/dist/esm/agents/AgentSession.mjs";
|
|
5
|
-
|
|
6
|
-
import type { DraftSession } from "./agentSpec.js";
|
|
7
|
-
|
|
8
|
-
type SessionRecord = {
|
|
9
|
-
id: string;
|
|
10
|
-
agentName: string;
|
|
11
|
-
title?: string;
|
|
12
|
-
createdBySubject: DraftSession["createdBySubject"];
|
|
13
|
-
createdAt: string;
|
|
14
|
-
updatedAt: string;
|
|
15
|
-
};
|
|
1
|
+
import type { AgentSession } from "truefoundry-gateway-sdk/agents";
|
|
2
|
+
import type { PrivateAgentSessionClient } from "truefoundry-gateway-sdk/agents/private";
|
|
16
3
|
|
|
17
4
|
const inflightByDraftId = new Map<string, Promise<AgentSession>>();
|
|
18
5
|
|
|
19
|
-
export function getGatewayFromSessionClient(
|
|
20
|
-
client: AgentSessionClient,
|
|
21
|
-
): TrueFoundryGateway {
|
|
22
|
-
const internal = client as unknown as { client: TrueFoundryGateway };
|
|
23
|
-
if (internal.client == null) {
|
|
24
|
-
throw new Error("AgentSessionClient is missing an internal gateway client.");
|
|
25
|
-
}
|
|
26
|
-
return internal.client;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function draftToSessionRecord(draft: DraftSession): SessionRecord {
|
|
30
|
-
return {
|
|
31
|
-
id: draft.id,
|
|
32
|
-
agentName: draft.agentName ?? "",
|
|
33
|
-
title: draft.title,
|
|
34
|
-
createdBySubject: draft.createdBySubject,
|
|
35
|
-
createdAt: draft.createdAt,
|
|
36
|
-
updatedAt: draft.updatedAt,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
6
|
/**
|
|
41
|
-
* Binds
|
|
42
|
-
*
|
|
43
|
-
* Does not call `GET` or `POST` on `/agents/sessions` root.
|
|
7
|
+
* Binds turn APIs for a draft session via `PrivateAgentSessionClient.getDraftSession`.
|
|
8
|
+
* Returns an `AgentDraftSession` (identical turn surface to `AgentSession`).
|
|
44
9
|
*/
|
|
45
10
|
export async function bindDraftAgentSession(
|
|
46
|
-
|
|
47
|
-
gateway: TrueFoundryGateway,
|
|
11
|
+
privateClient: PrivateAgentSessionClient,
|
|
48
12
|
draftSessionId: string,
|
|
49
13
|
): Promise<AgentSession> {
|
|
50
14
|
let inflight = inflightByDraftId.get(draftSessionId);
|
|
51
15
|
if (inflight == null) {
|
|
52
|
-
inflight =
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
});
|
|
16
|
+
inflight = privateClient
|
|
17
|
+
.getDraftSession({ draftSessionId })
|
|
18
|
+
// AgentDraftSession shares the turn API with AgentSession.
|
|
19
|
+
.then((draft) => draft as unknown as AgentSession)
|
|
20
|
+
.finally(() => {
|
|
21
|
+
if (inflightByDraftId.get(draftSessionId) === inflight) {
|
|
22
|
+
inflightByDraftId.delete(draftSessionId);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
63
25
|
inflightByDraftId.set(draftSessionId, inflight);
|
|
64
26
|
}
|
|
65
27
|
return inflight;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { PrivateAgentSessionClient } from "truefoundry-gateway-sdk/agents/private";
|
|
2
2
|
|
|
3
3
|
import type { AgentSpec } from "./agentSpec.js";
|
|
4
|
+
import { getGatewayFromPrivateClient } from "./getGatewayFromPrivateClient.js";
|
|
4
5
|
|
|
5
6
|
export const DRAFT_SESSION_LAST_UPDATED_AT_HEADER = "x-tfy-session-last-updated-at";
|
|
6
7
|
|
|
@@ -10,20 +11,17 @@ export type DraftSessionBridge = {
|
|
|
10
11
|
};
|
|
11
12
|
|
|
12
13
|
export function createDraftSessionBridge(
|
|
13
|
-
|
|
14
|
+
privateClient: PrivateAgentSessionClient,
|
|
14
15
|
): DraftSessionBridge {
|
|
15
|
-
async function getDraft(draftSessionId: string) {
|
|
16
|
-
const response = await gateway.agents.private.draftSessions.get(draftSessionId);
|
|
17
|
-
return response.data;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
16
|
return {
|
|
21
17
|
async getDraftAgentSpec(draftSessionId) {
|
|
22
|
-
const draft = await
|
|
18
|
+
const draft = await privateClient.getDraftSession({ draftSessionId });
|
|
23
19
|
return draft.agentSpec;
|
|
24
20
|
},
|
|
25
21
|
|
|
26
22
|
async syncAgentSpec(draftSessionId, agentSpec) {
|
|
23
|
+
// PrivateAgentSessionClient does not wrap update yet — use the low-level client.
|
|
24
|
+
const gateway = getGatewayFromPrivateClient(privateClient);
|
|
27
25
|
const response = await gateway.agents.private.draftSessions.update(
|
|
28
26
|
draftSessionId,
|
|
29
27
|
{ agentSpec },
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { TrueFoundryGateway } from "truefoundry-gateway-sdk";
|
|
2
|
+
import type { PrivateAgentSessionClient } from "truefoundry-gateway-sdk/agents/private";
|
|
3
|
+
|
|
4
|
+
/** Reach the underlying gateway for APIs not yet wrapped on PrivateAgentSessionClient (update, sandbox download). */
|
|
5
|
+
export function getGatewayFromPrivateClient(
|
|
6
|
+
privateClient: PrivateAgentSessionClient,
|
|
7
|
+
): TrueFoundryGateway {
|
|
8
|
+
const internal = privateClient as unknown as { client: TrueFoundryGateway };
|
|
9
|
+
if (internal.client == null) {
|
|
10
|
+
throw new Error("PrivateAgentSessionClient is missing an internal gateway client.");
|
|
11
|
+
}
|
|
12
|
+
return internal.client;
|
|
13
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { PrivateAgentSessionClient } from "truefoundry-gateway-sdk/agents/private";
|
|
4
4
|
|
|
5
5
|
import { createTrueFoundryDraftThreadListAdapter } from "./truefoundryDraftThreadListAdapter.js";
|
|
6
6
|
import type { AgentSpec } from "./agentSpec.js";
|
|
@@ -12,6 +12,7 @@ const defaultAgentSpec: AgentSpec = {
|
|
|
12
12
|
|
|
13
13
|
function mockDraft(id: string, title: string | undefined, updatedAt: string) {
|
|
14
14
|
return {
|
|
15
|
+
type: "session/draft" as const,
|
|
15
16
|
id,
|
|
16
17
|
agentSpec: defaultAgentSpec,
|
|
17
18
|
title,
|
|
@@ -38,31 +39,33 @@ function mockDraftListPage(
|
|
|
38
39
|
|
|
39
40
|
describe("createTrueFoundryDraftThreadListAdapter", () => {
|
|
40
41
|
it("lists draft sessions with pagination cursor", async () => {
|
|
41
|
-
const
|
|
42
|
+
const listDraftSessions = vi.fn().mockResolvedValue(
|
|
42
43
|
mockDraftListPage(
|
|
43
44
|
[mockDraft("d1", "My draft", "2026-06-30T10:00:00.000Z")],
|
|
44
45
|
"page-2",
|
|
45
46
|
),
|
|
46
47
|
);
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
const privateClient = {
|
|
49
|
+
listDraftSessions,
|
|
50
|
+
createDraftSession: vi.fn(),
|
|
51
|
+
getDraftSession: vi.fn(),
|
|
52
|
+
} as unknown as PrivateAgentSessionClient;
|
|
50
53
|
|
|
51
54
|
const adapter = createTrueFoundryDraftThreadListAdapter({
|
|
52
|
-
|
|
55
|
+
privateClient,
|
|
53
56
|
defaultAgentSpec,
|
|
54
57
|
});
|
|
55
58
|
|
|
56
59
|
const result = await adapter.list();
|
|
57
60
|
|
|
58
|
-
expect(
|
|
61
|
+
expect(listDraftSessions).toHaveBeenCalledWith(
|
|
59
62
|
expect.objectContaining({
|
|
60
63
|
limit: 20,
|
|
61
64
|
pageToken: undefined,
|
|
62
65
|
startTimestamp: expect.any(String),
|
|
63
66
|
}),
|
|
64
67
|
);
|
|
65
|
-
expect(
|
|
68
|
+
expect(listDraftSessions).toHaveBeenCalledWith(
|
|
66
69
|
expect.not.objectContaining({ agentName: expect.anything() }),
|
|
67
70
|
);
|
|
68
71
|
expect(result.threads).toEqual([
|
|
@@ -77,21 +80,23 @@ describe("createTrueFoundryDraftThreadListAdapter", () => {
|
|
|
77
80
|
});
|
|
78
81
|
|
|
79
82
|
it("creates a draft session on initialize", async () => {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
const createDraftSession = vi.fn().mockResolvedValue(
|
|
84
|
+
mockDraft("d-new", undefined, "2026-06-30T12:00:00.000Z"),
|
|
85
|
+
);
|
|
86
|
+
const privateClient = {
|
|
87
|
+
listDraftSessions: vi.fn(),
|
|
88
|
+
createDraftSession,
|
|
89
|
+
getDraftSession: vi.fn(),
|
|
90
|
+
} as unknown as PrivateAgentSessionClient;
|
|
86
91
|
|
|
87
92
|
const adapter = createTrueFoundryDraftThreadListAdapter({
|
|
88
|
-
|
|
93
|
+
privateClient,
|
|
89
94
|
defaultAgentSpec,
|
|
90
95
|
});
|
|
91
96
|
|
|
92
97
|
const result = await adapter.initialize("local-thread-id");
|
|
93
98
|
|
|
94
|
-
expect(
|
|
99
|
+
expect(createDraftSession).toHaveBeenCalledWith({ agentSpec: defaultAgentSpec });
|
|
95
100
|
expect(result).toEqual({ remoteId: "d-new", externalId: undefined });
|
|
96
101
|
});
|
|
97
102
|
|
|
@@ -102,39 +107,44 @@ describe("createTrueFoundryDraftThreadListAdapter", () => {
|
|
|
102
107
|
mcpServers: [{ name: "github", enableTools: ["@all"] }],
|
|
103
108
|
skills: [{ fqn: "acme/skill-a:1", preload: false }],
|
|
104
109
|
};
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
const createDraftSession = vi.fn().mockResolvedValue(
|
|
111
|
+
mockDraft("d-new", undefined, "2026-06-30T12:00:00.000Z"),
|
|
112
|
+
);
|
|
113
|
+
const privateClient = {
|
|
114
|
+
listDraftSessions: vi.fn(),
|
|
115
|
+
createDraftSession,
|
|
116
|
+
getDraftSession: vi.fn(),
|
|
117
|
+
} as unknown as PrivateAgentSessionClient;
|
|
111
118
|
|
|
112
119
|
const adapter = createTrueFoundryDraftThreadListAdapter({
|
|
113
|
-
|
|
120
|
+
privateClient,
|
|
114
121
|
defaultAgentSpec,
|
|
115
122
|
getAgentSpec: () => liveAgentSpec,
|
|
116
123
|
});
|
|
117
124
|
|
|
118
125
|
await adapter.initialize("local-thread-id");
|
|
119
126
|
|
|
120
|
-
expect(
|
|
127
|
+
expect(createDraftSession).toHaveBeenCalledWith({ agentSpec: liveAgentSpec });
|
|
121
128
|
});
|
|
122
129
|
|
|
123
130
|
it("falls back to model name for title when draft has no title", async () => {
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
131
|
+
const getDraftSession = vi.fn().mockResolvedValue(
|
|
132
|
+
mockDraft("d1", undefined, "2026-06-30T10:00:00.000Z"),
|
|
133
|
+
);
|
|
134
|
+
const privateClient = {
|
|
135
|
+
listDraftSessions: vi.fn(),
|
|
136
|
+
createDraftSession: vi.fn(),
|
|
137
|
+
getDraftSession,
|
|
138
|
+
} as unknown as PrivateAgentSessionClient;
|
|
130
139
|
|
|
131
140
|
const adapter = createTrueFoundryDraftThreadListAdapter({
|
|
132
|
-
|
|
141
|
+
privateClient,
|
|
133
142
|
defaultAgentSpec,
|
|
134
143
|
});
|
|
135
144
|
|
|
136
145
|
const result = await adapter.fetch("d1");
|
|
137
146
|
|
|
147
|
+
expect(getDraftSession).toHaveBeenCalledWith({ draftSessionId: "d1" });
|
|
138
148
|
expect(result.title).toBe("anthropic/claude-sonnet-4-6");
|
|
139
149
|
});
|
|
140
150
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RemoteThreadListAdapter } from "@assistant-ui/core";
|
|
2
|
-
import type {
|
|
2
|
+
import type { PrivateAgentSessionClient } from "truefoundry-gateway-sdk/agents/private";
|
|
3
3
|
|
|
4
4
|
import { draftSessionTitle, type AgentSpec } from "./agentSpec.js";
|
|
5
5
|
import { sessionListStartTimestamp } from "../sessionListStartTimestamp.js";
|
|
@@ -7,15 +7,15 @@ import { sessionListStartTimestamp } from "../sessionListStartTimestamp.js";
|
|
|
7
7
|
const THREAD_LIST_PAGE_SIZE = 20;
|
|
8
8
|
|
|
9
9
|
export function createTrueFoundryDraftThreadListAdapter(options: {
|
|
10
|
-
|
|
10
|
+
privateClient: PrivateAgentSessionClient;
|
|
11
11
|
defaultAgentSpec: AgentSpec;
|
|
12
12
|
getAgentSpec?: () => AgentSpec;
|
|
13
13
|
}): RemoteThreadListAdapter {
|
|
14
|
-
const {
|
|
14
|
+
const { privateClient, defaultAgentSpec, getAgentSpec } = options;
|
|
15
15
|
|
|
16
16
|
return {
|
|
17
17
|
async list({ after } = {}) {
|
|
18
|
-
const page = await
|
|
18
|
+
const page = await privateClient.listDraftSessions({
|
|
19
19
|
limit: THREAD_LIST_PAGE_SIZE,
|
|
20
20
|
pageToken: after,
|
|
21
21
|
startTimestamp: sessionListStartTimestamp(),
|
|
@@ -33,16 +33,16 @@ export function createTrueFoundryDraftThreadListAdapter(options: {
|
|
|
33
33
|
},
|
|
34
34
|
|
|
35
35
|
async initialize(_threadId: string) {
|
|
36
|
-
const
|
|
36
|
+
const draft = await privateClient.createDraftSession({
|
|
37
37
|
agentSpec: getAgentSpec?.() ?? defaultAgentSpec,
|
|
38
38
|
});
|
|
39
|
-
const draft = response.data;
|
|
40
39
|
return { remoteId: draft.id, externalId: undefined };
|
|
41
40
|
},
|
|
42
41
|
|
|
43
42
|
async fetch(remoteId) {
|
|
44
|
-
const
|
|
45
|
-
|
|
43
|
+
const draft = await privateClient.getDraftSession({
|
|
44
|
+
draftSessionId: remoteId,
|
|
45
|
+
});
|
|
46
46
|
return {
|
|
47
47
|
status: "regular" as const,
|
|
48
48
|
remoteId: draft.id,
|
package/src/sessions.ts
CHANGED
|
@@ -2,15 +2,15 @@ import type {
|
|
|
2
2
|
AgentSession,
|
|
3
3
|
AgentSessionClient,
|
|
4
4
|
} from "truefoundry-gateway-sdk/agents";
|
|
5
|
-
import type {
|
|
5
|
+
import type { PrivateAgentSessionClient } from "truefoundry-gateway-sdk/agents/private";
|
|
6
6
|
|
|
7
7
|
import { bindDraftAgentSession } from "./private/bindDraftAgentSession.js";
|
|
8
8
|
|
|
9
9
|
const inflightBySessionId = new Map<string, Promise<AgentSession>>();
|
|
10
10
|
|
|
11
11
|
export type GetSessionOptions = {
|
|
12
|
-
/** When set, validates the draft and binds turns
|
|
13
|
-
|
|
12
|
+
/** When set, validates the draft and binds turns via PrivateAgentSessionClient. */
|
|
13
|
+
privateClient?: PrivateAgentSessionClient;
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
/** `sessionId` is the assistant-ui thread `remoteId` from `RemoteThreadListAdapter.initialize`. */
|
|
@@ -19,8 +19,8 @@ export function getSession(
|
|
|
19
19
|
sessionId: string,
|
|
20
20
|
options?: GetSessionOptions,
|
|
21
21
|
): Promise<AgentSession> {
|
|
22
|
-
if (options?.
|
|
23
|
-
return bindDraftAgentSession(
|
|
22
|
+
if (options?.privateClient != null) {
|
|
23
|
+
return bindDraftAgentSession(options.privateClient, sessionId);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
let inflight = inflightBySessionId.get(sessionId);
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import type { PrivateAgentSessionClient } from "truefoundry-gateway-sdk/agents/private";
|
|
4
|
+
|
|
5
|
+
import { createTrueFoundryOwnedSessionsThreadListAdapter } from "./truefoundryOwnedSessionsThreadListAdapter.js";
|
|
6
|
+
|
|
7
|
+
function mockNamedSession(id: string, title: string, updatedAt: string) {
|
|
8
|
+
return {
|
|
9
|
+
type: "session" as const,
|
|
10
|
+
id,
|
|
11
|
+
agentName: "my-agent",
|
|
12
|
+
title,
|
|
13
|
+
createdBySubject: { type: "user" as const, id: "u1" },
|
|
14
|
+
createdAt: updatedAt,
|
|
15
|
+
updatedAt,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function mockDraftSession(id: string, title: string | undefined, updatedAt: string) {
|
|
20
|
+
return {
|
|
21
|
+
type: "session/draft" as const,
|
|
22
|
+
id,
|
|
23
|
+
agentSpec: { model: { name: "anthropic/claude-sonnet-4-6" } },
|
|
24
|
+
title,
|
|
25
|
+
createdBySubject: { type: "user" as const, id: "u1" },
|
|
26
|
+
createdAt: updatedAt,
|
|
27
|
+
updatedAt,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("createTrueFoundryOwnedSessionsThreadListAdapter", () => {
|
|
32
|
+
it("lists owned sessions (named + draft) with pagination cursor", async () => {
|
|
33
|
+
const listOwnedSessions = vi.fn().mockResolvedValue({
|
|
34
|
+
data: [
|
|
35
|
+
mockNamedSession("s1", "Named chat", "2026-06-30T12:00:00.000Z"),
|
|
36
|
+
mockDraftSession("d1", "Draft chat", "2026-06-30T11:00:00.000Z"),
|
|
37
|
+
],
|
|
38
|
+
response: {
|
|
39
|
+
pagination: { nextPageToken: "page-2", limit: 20 },
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
const privateClient = {
|
|
43
|
+
listOwnedSessions,
|
|
44
|
+
getDraftSession: vi.fn(),
|
|
45
|
+
} as unknown as PrivateAgentSessionClient;
|
|
46
|
+
|
|
47
|
+
const adapter = createTrueFoundryOwnedSessionsThreadListAdapter({ privateClient });
|
|
48
|
+
const result = await adapter.list();
|
|
49
|
+
|
|
50
|
+
expect(listOwnedSessions).toHaveBeenCalledWith(
|
|
51
|
+
expect.objectContaining({
|
|
52
|
+
limit: 20,
|
|
53
|
+
pageToken: undefined,
|
|
54
|
+
startTimestamp: expect.any(String),
|
|
55
|
+
}),
|
|
56
|
+
);
|
|
57
|
+
expect(result.threads).toEqual([
|
|
58
|
+
{
|
|
59
|
+
status: "regular",
|
|
60
|
+
remoteId: "s1",
|
|
61
|
+
title: "Named chat",
|
|
62
|
+
lastMessageAt: new Date("2026-06-30T12:00:00.000Z"),
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
status: "regular",
|
|
66
|
+
remoteId: "d1",
|
|
67
|
+
title: "Draft chat",
|
|
68
|
+
lastMessageAt: new Date("2026-06-30T11:00:00.000Z"),
|
|
69
|
+
},
|
|
70
|
+
]);
|
|
71
|
+
expect(result.nextCursor).toBe("page-2");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("falls back to model name for untitled drafts", async () => {
|
|
75
|
+
const listOwnedSessions = vi.fn().mockResolvedValue({
|
|
76
|
+
data: [mockDraftSession("d1", undefined, "2026-06-30T11:00:00.000Z")],
|
|
77
|
+
response: { pagination: { limit: 20 } },
|
|
78
|
+
});
|
|
79
|
+
const privateClient = {
|
|
80
|
+
listOwnedSessions,
|
|
81
|
+
getDraftSession: vi.fn(),
|
|
82
|
+
} as unknown as PrivateAgentSessionClient;
|
|
83
|
+
|
|
84
|
+
const adapter = createTrueFoundryOwnedSessionsThreadListAdapter({ privateClient });
|
|
85
|
+
const result = await adapter.list();
|
|
86
|
+
|
|
87
|
+
expect(result.threads[0]?.title).toBe("anthropic/claude-sonnet-4-6");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("throws on initialize because the adapter is read-only", async () => {
|
|
91
|
+
const privateClient = {
|
|
92
|
+
listOwnedSessions: vi.fn(),
|
|
93
|
+
getDraftSession: vi.fn(),
|
|
94
|
+
} as unknown as PrivateAgentSessionClient;
|
|
95
|
+
|
|
96
|
+
const adapter = createTrueFoundryOwnedSessionsThreadListAdapter({ privateClient });
|
|
97
|
+
|
|
98
|
+
await expect(adapter.initialize("local")).rejects.toThrow(/read-only/);
|
|
99
|
+
});
|
|
100
|
+
});
|