@truefoundry/assistant-ui-runtime 0.1.5 → 0.1.6-rc.0
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/README.md +374 -190
- package/dist/index.d.ts +32 -29
- package/dist/index.js +330 -236
- package/dist/index.js.map +1 -1
- package/dist/plugins/truefoundry-agent-server-adapter/index.d.ts +30 -0
- package/dist/plugins/truefoundry-agent-server-adapter/index.js +198 -0
- package/dist/plugins/truefoundry-agent-server-adapter/index.js.map +1 -0
- package/dist/types-VUBzoJT2.d.ts +462 -0
- package/package.json +10 -2
- package/src/askUserQuestion.ts +3 -3
- package/src/buildEditedUserMessageContent.test.ts +2 -2
- package/src/collectPending.ts +1 -1
- package/src/convertTurnMessages.test.ts +141 -196
- package/src/convertTurnMessages.ts +130 -76
- package/src/createSubAgent.ts +1 -1
- package/src/draftAgentConfig.test.ts +26 -29
- package/src/extractTurnUserText.ts +1 -1
- package/src/foldPeerThreads.test.ts +1 -1
- package/src/foldPeerThreads.ts +3 -2
- package/src/index.ts +39 -4
- package/src/listPages.ts +21 -0
- package/src/loadSessionSnapshot.test.ts +9 -8
- package/src/loadSessionSnapshot.ts +9 -14
- package/src/mcpAuth.ts +6 -3
- package/src/messageCustomMetadata.ts +1 -1
- package/src/modelMessageContent.ts +1 -1
- package/src/modelMessageImageContent.test.ts +1 -1
- package/src/modelMessageImageContent.ts +7 -6
- package/src/plugins/truefoundry-agent-server-adapter/index.ts +285 -0
- package/src/private/agentSpec.ts +8 -3
- package/src/private/draftSessionBridge.ts +14 -13
- package/src/private/truefoundryDraftThreadListAdapter.test.ts +44 -49
- package/src/private/truefoundryDraftThreadListAdapter.ts +22 -16
- package/src/requiredActionInputs.ts +1 -1
- package/src/requiredActionsFromActiveUpdate.test.ts +1 -1
- package/src/server/eventUtils.ts +120 -0
- package/src/server/events.ts +246 -0
- package/src/server/index.ts +66 -0
- package/src/server/types.ts +313 -0
- package/src/sessionSnapshot.ts +1 -1
- package/src/sessions.ts +5 -21
- package/src/streamTurn.test.ts +172 -155
- package/src/streamTurn.ts +51 -48
- package/src/toolApproval.ts +4 -4
- package/src/toolResponse.ts +4 -4
- package/src/truefoundryExtras.ts +1 -1
- package/src/truefoundryOwnedSessionsThreadListAdapter.test.ts +26 -29
- package/src/truefoundryOwnedSessionsThreadListAdapter.ts +18 -23
- package/src/truefoundryThreadListAdapter.test.ts +16 -18
- package/src/truefoundryThreadListAdapter.ts +7 -7
- package/src/turnEventHelpers.ts +1 -1
- package/src/types.ts +2 -16
- package/src/useTrueFoundryAgentMessages.test.tsx +38 -70
- package/src/useTrueFoundryAgentMessages.ts +32 -44
- package/src/useTrueFoundryAgentRuntime.ts +11 -28
- package/src/private/bindDraftAgentSession.test.ts +0 -54
- package/src/private/bindDraftAgentSession.ts +0 -28
- package/src/private/getGatewayFromPrivateClient.ts +0 -13
package/src/streamTurn.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
AgentChatServer,
|
|
3
|
+
PreviousTurnIdInput,
|
|
4
4
|
TurnInputItem,
|
|
5
|
-
} from "
|
|
6
|
-
import type {
|
|
5
|
+
} from "./server/types.js";
|
|
6
|
+
import type { TurnStreamData } from "./server/events.js";
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
streamTurnEvents,
|
|
@@ -18,11 +18,11 @@ export type StreamTurnOptions = {
|
|
|
18
18
|
resumeMcpAuth?: boolean;
|
|
19
19
|
inputs?: RequiredActionInput[];
|
|
20
20
|
/**
|
|
21
|
-
* Branch anchor for
|
|
22
|
-
* root turn
|
|
21
|
+
* Branch anchor for prepareAndExecuteTurn. Omit for `"auto"`. Pass `"none"` for a fresh
|
|
22
|
+
* root turn.
|
|
23
23
|
*/
|
|
24
|
-
previousTurnId?:
|
|
25
|
-
/** Extra headers for the
|
|
24
|
+
previousTurnId?: PreviousTurnIdInput;
|
|
25
|
+
/** Extra headers for the turn request. */
|
|
26
26
|
headers?: Record<string, string>;
|
|
27
27
|
};
|
|
28
28
|
|
|
@@ -36,9 +36,13 @@ function buildTurnInput(options: StreamTurnOptions): TurnInputItem[] {
|
|
|
36
36
|
return [{ type: "user.message", content: options.userMessage ?? "" }];
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
function bindAbort(
|
|
39
|
+
function bindAbort(
|
|
40
|
+
server: AgentChatServer,
|
|
41
|
+
sessionId: string,
|
|
42
|
+
abortSignal: AbortSignal,
|
|
43
|
+
): () => void {
|
|
40
44
|
const onAbort = () => {
|
|
41
|
-
void
|
|
45
|
+
void server.cancelSession({ sessionId }).catch(() => undefined);
|
|
42
46
|
};
|
|
43
47
|
if (abortSignal.aborted) {
|
|
44
48
|
onAbort();
|
|
@@ -49,63 +53,50 @@ function bindAbort(session: AgentSession, abortSignal: AbortSignal): () => void
|
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
export async function* streamTurnContent(
|
|
52
|
-
|
|
56
|
+
server: AgentChatServer,
|
|
57
|
+
sessionId: string,
|
|
53
58
|
foldState: PeerThreadFoldState,
|
|
54
59
|
options: StreamTurnOptions,
|
|
55
60
|
abortSignal: AbortSignal,
|
|
56
61
|
groupRootBaseline?: readonly string[],
|
|
57
62
|
/**
|
|
58
|
-
* Called once with the
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* so that edit/retry can find the turn in `buildSnapshotBeforeTurn`.
|
|
63
|
+
* Called once with the turn ID as soon as it becomes available (first
|
|
64
|
+
* `turn.created` SSE event). Use this to reconcile the locally-generated
|
|
65
|
+
* optimistic ID with the real turn ID.
|
|
62
66
|
*/
|
|
63
67
|
onTurnIdAvailable?: (turnId: string) => void,
|
|
64
68
|
): AsyncGenerator<TurnStreamUpdate> {
|
|
65
|
-
const
|
|
66
|
-
input: buildTurnInput(options),
|
|
67
|
-
previousTurnId: options.previousTurnId ?? "auto",
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
const onAbort = bindAbort(session, abortSignal);
|
|
69
|
+
const onAbort = bindAbort(server, sessionId, abortSignal);
|
|
71
70
|
if (abortSignal.aborted) {
|
|
72
71
|
return;
|
|
73
72
|
}
|
|
74
73
|
|
|
75
74
|
let turnIdNotified = false;
|
|
76
|
-
const
|
|
77
|
-
if (!turnIdNotified
|
|
78
|
-
onTurnIdAvailable?.(
|
|
75
|
+
const notifyTurnId = (turnId: string) => {
|
|
76
|
+
if (!turnIdNotified) {
|
|
77
|
+
onTurnIdAvailable?.(turnId);
|
|
79
78
|
turnIdNotified = true;
|
|
80
79
|
}
|
|
81
80
|
};
|
|
82
81
|
|
|
82
|
+
const stream: AsyncIterable<TurnStreamData> = server.prepareAndExecuteTurn({
|
|
83
|
+
sessionId,
|
|
84
|
+
input: buildTurnInput(options),
|
|
85
|
+
previousTurnId: options.previousTurnId ?? "auto",
|
|
86
|
+
abortSignal,
|
|
87
|
+
...(options.headers != null ? { headers: options.headers } : {}),
|
|
88
|
+
});
|
|
89
|
+
|
|
83
90
|
try {
|
|
84
91
|
for await (const update of streamTurnEvents(
|
|
85
|
-
|
|
86
|
-
{ stream: true },
|
|
87
|
-
{
|
|
88
|
-
abortSignal,
|
|
89
|
-
...(options.headers != null ? { headers: options.headers } : {}),
|
|
90
|
-
},
|
|
91
|
-
),
|
|
92
|
+
stream,
|
|
92
93
|
foldState,
|
|
93
94
|
groupRootBaseline,
|
|
95
|
+
notifyTurnId,
|
|
94
96
|
)) {
|
|
95
|
-
// After the first `turn.created` event, `turn.id` is set.
|
|
96
|
-
// Notify BEFORE yielding so the caller can update its tracking
|
|
97
|
-
// before the snapshot is written with the stream update.
|
|
98
|
-
notifyTurnIdIfAvailable();
|
|
99
97
|
yield update;
|
|
100
98
|
}
|
|
101
|
-
// Handle streams that complete without yielding any content.
|
|
102
|
-
notifyTurnIdIfAvailable();
|
|
103
99
|
} catch (error) {
|
|
104
|
-
// Error streams often throw on `turn.done` (status=error) without ever
|
|
105
|
-
// yielding content (e.g. model not servable). `turn.id` is still set
|
|
106
|
-
// after `turn.created` — notify so edit/retry can resolve the turn.
|
|
107
|
-
// Same for AbortError after create: keep local ids aligned with gateway.
|
|
108
|
-
notifyTurnIdIfAvailable();
|
|
109
100
|
if (error instanceof Error && error.name === "AbortError") {
|
|
110
101
|
return;
|
|
111
102
|
}
|
|
@@ -117,14 +108,22 @@ export async function* streamTurnContent(
|
|
|
117
108
|
|
|
118
109
|
/** TODO: wire `afterSequenceNumber` from the last ingested stream event to skip replay on reconnect. */
|
|
119
110
|
export async function* resumeTurnStream(
|
|
120
|
-
|
|
111
|
+
server: AgentChatServer,
|
|
112
|
+
sessionId: string,
|
|
113
|
+
turnId: string,
|
|
121
114
|
foldState: PeerThreadFoldState,
|
|
122
115
|
abortSignal: AbortSignal,
|
|
123
116
|
afterSequenceNumber?: number,
|
|
124
117
|
groupRootBaseline?: readonly string[],
|
|
125
118
|
): AsyncGenerator<TurnStreamUpdate> {
|
|
119
|
+
if (server.subscribeToTurn == null) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
"resumeTurnStream requires AgentChatServer.subscribeToTurn",
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
126
125
|
const onAbort = () => {
|
|
127
|
-
void
|
|
126
|
+
void server.cancelSession({ sessionId }).catch(() => undefined);
|
|
128
127
|
};
|
|
129
128
|
if (abortSignal.aborted) {
|
|
130
129
|
onAbort();
|
|
@@ -134,10 +133,14 @@ export async function* resumeTurnStream(
|
|
|
134
133
|
|
|
135
134
|
try {
|
|
136
135
|
yield* streamTurnEvents(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
136
|
+
server.subscribeToTurn({
|
|
137
|
+
sessionId,
|
|
138
|
+
turnId,
|
|
139
|
+
...(afterSequenceNumber != null
|
|
140
|
+
? { afterSequenceNumber }
|
|
141
|
+
: {}),
|
|
142
|
+
abortSignal,
|
|
143
|
+
}),
|
|
141
144
|
foldState,
|
|
142
145
|
groupRootBaseline,
|
|
143
146
|
);
|
package/src/toolApproval.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type {
|
|
|
8
8
|
ToolApprovalRequiredEvent,
|
|
9
9
|
Turn,
|
|
10
10
|
UserToolApprovalEvent,
|
|
11
|
-
} from "
|
|
11
|
+
} from "./server/index.js";
|
|
12
12
|
|
|
13
13
|
import { ROOT_THREAD_ID } from "./constants.js";
|
|
14
14
|
import type { ToolApprovalMessageCustomMetadata } from "./messageCustomMetadata.js";
|
|
@@ -148,10 +148,10 @@ export function findApprovalRequiredInTurn(
|
|
|
148
148
|
if (turn.state.status !== "done") {
|
|
149
149
|
return undefined;
|
|
150
150
|
}
|
|
151
|
-
|
|
152
|
-
(action)
|
|
153
|
-
action.type === "tool.approval_required",
|
|
151
|
+
const found = turn.state.requiredActions?.find(
|
|
152
|
+
(action) => action.type === "tool.approval_required",
|
|
154
153
|
);
|
|
154
|
+
return found?.type === "tool.approval_required" ? found : undefined;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
function toolCallPartHasPendingApproval(part: ToolCallPart): boolean {
|
package/src/toolResponse.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
Turn,
|
|
10
10
|
TurnInputItem,
|
|
11
11
|
UserToolResponseEvent,
|
|
12
|
-
} from "
|
|
12
|
+
} from "./server/index.js";
|
|
13
13
|
|
|
14
14
|
import { ROOT_THREAD_ID } from "./constants.js";
|
|
15
15
|
import {
|
|
@@ -85,10 +85,10 @@ export function findResponseRequiredInTurn(
|
|
|
85
85
|
if (turn.state.status !== "done") {
|
|
86
86
|
return undefined;
|
|
87
87
|
}
|
|
88
|
-
|
|
89
|
-
(action)
|
|
90
|
-
action.type === "tool.response_required",
|
|
88
|
+
const found = turn.state.requiredActions?.find(
|
|
89
|
+
(action) => action.type === "tool.response_required",
|
|
91
90
|
);
|
|
91
|
+
return found?.type === "tool.response_required" ? found : undefined;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
function applyToolResponseToToolCall(
|
package/src/truefoundryExtras.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRuntimeExtras } from "@assistant-ui/core/internal";
|
|
2
|
-
import type { McpAuthRequiredEvent } from "
|
|
2
|
+
import type { McpAuthRequiredEvent } from "./server/index.js";
|
|
3
3
|
|
|
4
4
|
import type { AgentSpec, AgentSpecUpdate } from "./private/agentSpec.js";
|
|
5
5
|
import type { PendingApproval, PendingToolResponse } from "./collectPending.js";
|
|
@@ -1,53 +1,54 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { AgentChatServer, Session } from "./server/index.js";
|
|
4
4
|
|
|
5
5
|
import { createTrueFoundryOwnedSessionsThreadListAdapter } from "./truefoundryOwnedSessionsThreadListAdapter.js";
|
|
6
6
|
|
|
7
|
-
function mockNamedSession(id: string, title: string, updatedAt: string) {
|
|
7
|
+
function mockNamedSession(id: string, title: string, updatedAt: string): Session {
|
|
8
8
|
return {
|
|
9
|
-
type: "session" as const,
|
|
10
9
|
id,
|
|
11
10
|
agentName: "my-agent",
|
|
12
11
|
title,
|
|
13
|
-
createdBySubject: { type: "user" as const, id: "u1" },
|
|
14
12
|
createdAt: updatedAt,
|
|
15
13
|
updatedAt,
|
|
14
|
+
isMutable: false,
|
|
16
15
|
};
|
|
17
16
|
}
|
|
18
17
|
|
|
19
|
-
function mockDraftSession(
|
|
18
|
+
function mockDraftSession(
|
|
19
|
+
id: string,
|
|
20
|
+
title: string | undefined,
|
|
21
|
+
updatedAt: string,
|
|
22
|
+
): Session {
|
|
20
23
|
return {
|
|
21
|
-
type: "session/draft" as const,
|
|
22
24
|
id,
|
|
23
25
|
agentSpec: { model: { name: "anthropic/claude-sonnet-4-6" } },
|
|
24
26
|
title,
|
|
25
|
-
createdBySubject: { type: "user" as const, id: "u1" },
|
|
26
27
|
createdAt: updatedAt,
|
|
27
28
|
updatedAt,
|
|
29
|
+
isMutable: true,
|
|
28
30
|
};
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
function mockServer(partial: Partial<AgentChatServer>): AgentChatServer {
|
|
34
|
+
return partial as AgentChatServer;
|
|
35
|
+
}
|
|
36
|
+
|
|
31
37
|
describe("createTrueFoundryOwnedSessionsThreadListAdapter", () => {
|
|
32
38
|
it("lists owned sessions (named + draft) with pagination cursor", async () => {
|
|
33
|
-
const
|
|
39
|
+
const listSessions = vi.fn().mockResolvedValue({
|
|
34
40
|
data: [
|
|
35
41
|
mockNamedSession("s1", "Named chat", "2026-06-30T12:00:00.000Z"),
|
|
36
42
|
mockDraftSession("d1", "Draft chat", "2026-06-30T11:00:00.000Z"),
|
|
37
43
|
],
|
|
38
|
-
|
|
39
|
-
pagination: { nextPageToken: "page-2", limit: 20 },
|
|
40
|
-
},
|
|
44
|
+
nextPageToken: "page-2",
|
|
41
45
|
});
|
|
42
|
-
const
|
|
43
|
-
listOwnedSessions,
|
|
44
|
-
getDraftSession: vi.fn(),
|
|
45
|
-
} as unknown as PrivateAgentSessionClient;
|
|
46
|
+
const server = mockServer({ listSessions, getSession: vi.fn() });
|
|
46
47
|
|
|
47
|
-
const adapter = createTrueFoundryOwnedSessionsThreadListAdapter({
|
|
48
|
+
const adapter = createTrueFoundryOwnedSessionsThreadListAdapter({ server });
|
|
48
49
|
const result = await adapter.list();
|
|
49
50
|
|
|
50
|
-
expect(
|
|
51
|
+
expect(listSessions).toHaveBeenCalledWith(
|
|
51
52
|
expect.objectContaining({
|
|
52
53
|
limit: 20,
|
|
53
54
|
pageToken: undefined,
|
|
@@ -72,28 +73,24 @@ describe("createTrueFoundryOwnedSessionsThreadListAdapter", () => {
|
|
|
72
73
|
});
|
|
73
74
|
|
|
74
75
|
it("falls back to model name for untitled drafts", async () => {
|
|
75
|
-
const
|
|
76
|
+
const listSessions = vi.fn().mockResolvedValue({
|
|
76
77
|
data: [mockDraftSession("d1", undefined, "2026-06-30T11:00:00.000Z")],
|
|
77
|
-
response: { pagination: { limit: 20 } },
|
|
78
78
|
});
|
|
79
|
-
const
|
|
80
|
-
listOwnedSessions,
|
|
81
|
-
getDraftSession: vi.fn(),
|
|
82
|
-
} as unknown as PrivateAgentSessionClient;
|
|
79
|
+
const server = mockServer({ listSessions, getSession: vi.fn() });
|
|
83
80
|
|
|
84
|
-
const adapter = createTrueFoundryOwnedSessionsThreadListAdapter({
|
|
81
|
+
const adapter = createTrueFoundryOwnedSessionsThreadListAdapter({ server });
|
|
85
82
|
const result = await adapter.list();
|
|
86
83
|
|
|
87
84
|
expect(result.threads[0]?.title).toBe("anthropic/claude-sonnet-4-6");
|
|
88
85
|
});
|
|
89
86
|
|
|
90
87
|
it("throws on initialize because the adapter is read-only", async () => {
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
88
|
+
const server = mockServer({
|
|
89
|
+
listSessions: vi.fn(),
|
|
90
|
+
getSession: vi.fn(),
|
|
91
|
+
});
|
|
95
92
|
|
|
96
|
-
const adapter = createTrueFoundryOwnedSessionsThreadListAdapter({
|
|
93
|
+
const adapter = createTrueFoundryOwnedSessionsThreadListAdapter({ server });
|
|
97
94
|
|
|
98
95
|
await expect(adapter.initialize("local")).rejects.toThrow(/read-only/);
|
|
99
96
|
});
|
|
@@ -1,34 +1,33 @@
|
|
|
1
1
|
import type { RemoteThreadListAdapter } from "@assistant-ui/core";
|
|
2
|
-
import type { AgentSession } from "truefoundry-gateway-sdk/agents";
|
|
3
|
-
import type {
|
|
4
|
-
AgentDraftSession,
|
|
5
|
-
PrivateAgentSessionClient,
|
|
6
|
-
} from "truefoundry-gateway-sdk/agents/private";
|
|
7
2
|
|
|
3
|
+
import type { AgentChatServer, Session } from "./server/types.js";
|
|
8
4
|
import { draftSessionTitle } from "./private/agentSpec.js";
|
|
9
5
|
import { sessionListStartTimestamp } from "./sessionListStartTimestamp.js";
|
|
10
6
|
|
|
11
7
|
const THREAD_LIST_PAGE_SIZE = 20;
|
|
12
8
|
|
|
13
|
-
function ownedSessionTitle(session:
|
|
14
|
-
if (session.
|
|
15
|
-
return draftSessionTitle(
|
|
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
|
+
});
|
|
16
15
|
}
|
|
17
|
-
return session.title ?? session.agentName;
|
|
16
|
+
return session.title ?? session.agentName ?? session.id;
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
/**
|
|
21
|
-
* Read-only thread-list adapter backed by `
|
|
22
|
-
*
|
|
20
|
+
* Read-only thread-list adapter backed by `AgentChatServer.listSessions`.
|
|
21
|
+
* Hosts that previously used listOwnedSessions should filter in their server impl.
|
|
23
22
|
*/
|
|
24
23
|
export function createTrueFoundryOwnedSessionsThreadListAdapter(options: {
|
|
25
|
-
|
|
24
|
+
server: AgentChatServer;
|
|
26
25
|
}): RemoteThreadListAdapter {
|
|
27
|
-
const {
|
|
26
|
+
const { server } = options;
|
|
28
27
|
|
|
29
28
|
return {
|
|
30
29
|
async list({ after } = {}) {
|
|
31
|
-
const page = await
|
|
30
|
+
const page = await server.listSessions({
|
|
32
31
|
limit: THREAD_LIST_PAGE_SIZE,
|
|
33
32
|
pageToken: after,
|
|
34
33
|
startTimestamp: sessionListStartTimestamp(),
|
|
@@ -41,7 +40,7 @@ export function createTrueFoundryOwnedSessionsThreadListAdapter(options: {
|
|
|
41
40
|
}));
|
|
42
41
|
return {
|
|
43
42
|
threads,
|
|
44
|
-
nextCursor: page.
|
|
43
|
+
nextCursor: page.nextPageToken ?? undefined,
|
|
45
44
|
};
|
|
46
45
|
},
|
|
47
46
|
|
|
@@ -52,16 +51,12 @@ export function createTrueFoundryOwnedSessionsThreadListAdapter(options: {
|
|
|
52
51
|
},
|
|
53
52
|
|
|
54
53
|
async fetch(remoteId) {
|
|
55
|
-
|
|
56
|
-
// surface through listOwnedSessions; fetch falls back to draft get.
|
|
57
|
-
const draft = await privateClient.getDraftSession({
|
|
58
|
-
draftSessionId: remoteId,
|
|
59
|
-
});
|
|
54
|
+
const session = await server.getSession({ sessionId: remoteId });
|
|
60
55
|
return {
|
|
61
56
|
status: "regular" as const,
|
|
62
|
-
remoteId:
|
|
63
|
-
title:
|
|
64
|
-
lastMessageAt: new Date(
|
|
57
|
+
remoteId: session.id,
|
|
58
|
+
title: ownedSessionTitle(session),
|
|
59
|
+
lastMessageAt: new Date(session.updatedAt),
|
|
65
60
|
};
|
|
66
61
|
},
|
|
67
62
|
|
|
@@ -1,32 +1,30 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { AgentChatServer, Session } from "./server/index.js";
|
|
4
4
|
|
|
5
5
|
import { createTrueFoundryThreadListAdapter } from "./truefoundryThreadListAdapter.js";
|
|
6
6
|
|
|
7
|
-
function mockSession(id: string, title: string, updatedAt: string) {
|
|
7
|
+
function mockSession(id: string, title: string, updatedAt: string): Session {
|
|
8
8
|
return {
|
|
9
9
|
id,
|
|
10
10
|
title,
|
|
11
11
|
updatedAt,
|
|
12
|
+
createdAt: updatedAt,
|
|
13
|
+
isMutable: false,
|
|
12
14
|
};
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
function mockListSessionsPage(
|
|
16
|
-
sessions: ReturnType<typeof mockSession>[],
|
|
17
|
-
nextPageToken?: string,
|
|
18
|
-
) {
|
|
17
|
+
function mockListSessionsPage(sessions: Session[], nextPageToken?: string) {
|
|
19
18
|
return {
|
|
20
19
|
data: sessions,
|
|
21
|
-
|
|
22
|
-
pagination: {
|
|
23
|
-
nextPageToken,
|
|
24
|
-
limit: 20,
|
|
25
|
-
},
|
|
26
|
-
},
|
|
20
|
+
...(nextPageToken != null ? { nextPageToken } : {}),
|
|
27
21
|
};
|
|
28
22
|
}
|
|
29
23
|
|
|
24
|
+
function mockServer(partial: Partial<AgentChatServer>): AgentChatServer {
|
|
25
|
+
return partial as AgentChatServer;
|
|
26
|
+
}
|
|
27
|
+
|
|
30
28
|
describe("createTrueFoundryThreadListAdapter", () => {
|
|
31
29
|
it("lists the first page with limit and returns nextCursor", async () => {
|
|
32
30
|
const listSessions = vi.fn().mockResolvedValue(
|
|
@@ -35,9 +33,9 @@ describe("createTrueFoundryThreadListAdapter", () => {
|
|
|
35
33
|
"page-2",
|
|
36
34
|
),
|
|
37
35
|
);
|
|
38
|
-
const
|
|
36
|
+
const server = mockServer({ listSessions });
|
|
39
37
|
const adapter = createTrueFoundryThreadListAdapter({
|
|
40
|
-
|
|
38
|
+
server,
|
|
41
39
|
agentName: "my-agent",
|
|
42
40
|
});
|
|
43
41
|
|
|
@@ -68,9 +66,9 @@ describe("createTrueFoundryThreadListAdapter", () => {
|
|
|
68
66
|
[mockSession("s2", "Second", "2026-06-29T10:00:00.000Z")],
|
|
69
67
|
),
|
|
70
68
|
);
|
|
71
|
-
const
|
|
69
|
+
const server = mockServer({ listSessions });
|
|
72
70
|
const adapter = createTrueFoundryThreadListAdapter({
|
|
73
|
-
|
|
71
|
+
server,
|
|
74
72
|
agentName: "my-agent",
|
|
75
73
|
});
|
|
76
74
|
|
|
@@ -90,9 +88,9 @@ describe("createTrueFoundryThreadListAdapter", () => {
|
|
|
90
88
|
const listSessions = vi.fn().mockResolvedValue(
|
|
91
89
|
mockListSessionsPage([mockSession("s1", "Only", "2026-06-30T10:00:00.000Z")]),
|
|
92
90
|
);
|
|
93
|
-
const
|
|
91
|
+
const server = mockServer({ listSessions });
|
|
94
92
|
const adapter = createTrueFoundryThreadListAdapter({
|
|
95
|
-
|
|
93
|
+
server,
|
|
96
94
|
agentName: "my-agent",
|
|
97
95
|
});
|
|
98
96
|
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import type { RemoteThreadListAdapter } from "@assistant-ui/core";
|
|
2
|
-
import type { AgentSessionClient } from "truefoundry-gateway-sdk/agents";
|
|
3
2
|
|
|
3
|
+
import type { AgentChatServer } from "./server/types.js";
|
|
4
4
|
import { getSession } from "./sessions.js";
|
|
5
5
|
import { sessionListStartTimestamp } from "./sessionListStartTimestamp.js";
|
|
6
6
|
|
|
7
7
|
const THREAD_LIST_PAGE_SIZE = 20;
|
|
8
8
|
|
|
9
9
|
export function createTrueFoundryThreadListAdapter(options: {
|
|
10
|
-
|
|
10
|
+
server: AgentChatServer;
|
|
11
11
|
agentName: string;
|
|
12
12
|
}): RemoteThreadListAdapter {
|
|
13
|
-
const {
|
|
13
|
+
const { server, agentName } = options;
|
|
14
14
|
|
|
15
15
|
return {
|
|
16
16
|
async list({ after } = {}) {
|
|
17
|
-
const page = await
|
|
17
|
+
const page = await server.listSessions({
|
|
18
18
|
agentName,
|
|
19
19
|
limit: THREAD_LIST_PAGE_SIZE,
|
|
20
20
|
pageToken: after,
|
|
@@ -28,17 +28,17 @@ export function createTrueFoundryThreadListAdapter(options: {
|
|
|
28
28
|
}));
|
|
29
29
|
return {
|
|
30
30
|
threads,
|
|
31
|
-
nextCursor: page.
|
|
31
|
+
nextCursor: page.nextPageToken ?? undefined,
|
|
32
32
|
};
|
|
33
33
|
},
|
|
34
34
|
|
|
35
35
|
async initialize(_threadId: string) {
|
|
36
|
-
const session = await
|
|
36
|
+
const session = await server.createSession({ agentName });
|
|
37
37
|
return { remoteId: session.id, externalId: undefined };
|
|
38
38
|
},
|
|
39
39
|
|
|
40
40
|
async fetch(remoteId) {
|
|
41
|
-
const session = await getSession(
|
|
41
|
+
const session = await getSession(server, remoteId);
|
|
42
42
|
return {
|
|
43
43
|
status: "regular" as const,
|
|
44
44
|
remoteId: session.id,
|
package/src/turnEventHelpers.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -6,10 +6,9 @@ import type {
|
|
|
6
6
|
RealtimeVoiceAdapter,
|
|
7
7
|
SpeechSynthesisAdapter,
|
|
8
8
|
} from "@assistant-ui/core";
|
|
9
|
-
import type { AgentSessionClient } from "truefoundry-gateway-sdk/agents";
|
|
10
|
-
import type { PrivateAgentSessionClient } from "truefoundry-gateway-sdk/agents/private";
|
|
11
9
|
|
|
12
10
|
import type { AgentSpec } from "./private/agentSpec.js";
|
|
11
|
+
import type { AgentChatServer } from "./server/types.js";
|
|
13
12
|
|
|
14
13
|
export type NamedAgentConfig = {
|
|
15
14
|
mode: "named";
|
|
@@ -25,7 +24,7 @@ export type DraftAgentConfig = {
|
|
|
25
24
|
export type TrueFoundryAgentConfig = NamedAgentConfig | DraftAgentConfig;
|
|
26
25
|
|
|
27
26
|
type TrueFoundryAgentRuntimeBaseOptions = ExternalStoreSharedOptions & {
|
|
28
|
-
|
|
27
|
+
server: AgentChatServer;
|
|
29
28
|
initialSessionId?: string | undefined;
|
|
30
29
|
threadId?: string | undefined;
|
|
31
30
|
onThreadIdChange?: ((threadId: string | undefined) => void) | undefined;
|
|
@@ -47,16 +46,10 @@ export type UseTrueFoundryAgentRuntimeOptions = TrueFoundryAgentRuntimeBaseOptio
|
|
|
47
46
|
agent?: TrueFoundryAgentConfig | undefined;
|
|
48
47
|
/** Legacy named-agent shorthand. Prefer `agent: { mode: "named", agentName }`. */
|
|
49
48
|
agentName?: string | undefined;
|
|
50
|
-
/**
|
|
51
|
-
* Required when `agent.mode === "draft"`. Also used for sandbox file downloads
|
|
52
|
-
* (named or draft).
|
|
53
|
-
*/
|
|
54
|
-
privateClient?: PrivateAgentSessionClient | undefined;
|
|
55
49
|
};
|
|
56
50
|
|
|
57
51
|
export type ResolvedTrueFoundryAgentRuntimeOptions = TrueFoundryAgentRuntimeBaseOptions & {
|
|
58
52
|
agent: TrueFoundryAgentConfig;
|
|
59
|
-
privateClient?: PrivateAgentSessionClient | undefined;
|
|
60
53
|
};
|
|
61
54
|
|
|
62
55
|
export function resolveTrueFoundryAgentConfig(
|
|
@@ -81,15 +74,8 @@ export function resolveTrueFoundryAgentRuntimeOptions(
|
|
|
81
74
|
): ResolvedTrueFoundryAgentRuntimeOptions {
|
|
82
75
|
const agent = resolveTrueFoundryAgentConfig(options);
|
|
83
76
|
|
|
84
|
-
if (agent.mode === "draft" && options.privateClient == null) {
|
|
85
|
-
throw new Error(
|
|
86
|
-
"Draft agent mode requires a `privateClient` PrivateAgentSessionClient.",
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
77
|
return {
|
|
91
78
|
...options,
|
|
92
79
|
agent,
|
|
93
|
-
privateClient: options.privateClient,
|
|
94
80
|
};
|
|
95
81
|
}
|