@truefoundry/assistant-ui-runtime 0.1.4 → 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 +334 -241
- 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 +12 -4
- package/src/askUserQuestion.ts +3 -3
- package/src/collectPending.ts +1 -1
- package/src/convertTurnMessages.test.ts +141 -196
- package/src/convertTurnMessages.ts +131 -77
- 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 +175 -158
- package/src/streamTurn.ts +51 -57
- 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 +9 -9
- package/src/turnEventHelpers.ts +1 -1
- package/src/types.ts +2 -16
- package/src/useTrueFoundryAgentMessages.test.tsx +38 -70
- package/src/useTrueFoundryAgentMessages.ts +33 -45
- 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,72 +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
|
-
|
|
66
|
-
// SDK serializer emits `previous_turn_id: null` on the wire (first turn in session).
|
|
67
|
-
// The SDK type doesn't admit null, but the Fern serializer handles it correctly.
|
|
68
|
-
const previousTurnId: TruefoundryGatewayApi.PreviousTurnIdInput | null | undefined =
|
|
69
|
-
options.previousTurnId === null
|
|
70
|
-
? null
|
|
71
|
-
: (options.previousTurnId ?? "auto");
|
|
72
|
-
const turn = session.prepareTurn({
|
|
73
|
-
input: buildTurnInput(options),
|
|
74
|
-
...(previousTurnId !== undefined
|
|
75
|
-
? { previousTurnId: previousTurnId as TruefoundryGatewayApi.PreviousTurnIdInput }
|
|
76
|
-
: {}),
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
const onAbort = bindAbort(session, abortSignal);
|
|
69
|
+
const onAbort = bindAbort(server, sessionId, abortSignal);
|
|
80
70
|
if (abortSignal.aborted) {
|
|
81
71
|
return;
|
|
82
72
|
}
|
|
83
73
|
|
|
84
74
|
let turnIdNotified = false;
|
|
85
|
-
const
|
|
86
|
-
if (!turnIdNotified
|
|
87
|
-
onTurnIdAvailable?.(
|
|
75
|
+
const notifyTurnId = (turnId: string) => {
|
|
76
|
+
if (!turnIdNotified) {
|
|
77
|
+
onTurnIdAvailable?.(turnId);
|
|
88
78
|
turnIdNotified = true;
|
|
89
79
|
}
|
|
90
80
|
};
|
|
91
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
|
+
|
|
92
90
|
try {
|
|
93
91
|
for await (const update of streamTurnEvents(
|
|
94
|
-
|
|
95
|
-
{ stream: true },
|
|
96
|
-
{
|
|
97
|
-
abortSignal,
|
|
98
|
-
...(options.headers != null ? { headers: options.headers } : {}),
|
|
99
|
-
},
|
|
100
|
-
),
|
|
92
|
+
stream,
|
|
101
93
|
foldState,
|
|
102
94
|
groupRootBaseline,
|
|
95
|
+
notifyTurnId,
|
|
103
96
|
)) {
|
|
104
|
-
// After the first `turn.created` event, `turn.id` is set.
|
|
105
|
-
// Notify BEFORE yielding so the caller can update its tracking
|
|
106
|
-
// before the snapshot is written with the stream update.
|
|
107
|
-
notifyTurnIdIfAvailable();
|
|
108
97
|
yield update;
|
|
109
98
|
}
|
|
110
|
-
// Handle streams that complete without yielding any content.
|
|
111
|
-
notifyTurnIdIfAvailable();
|
|
112
99
|
} catch (error) {
|
|
113
|
-
// Error streams often throw on `turn.done` (status=error) without ever
|
|
114
|
-
// yielding content (e.g. model not servable). `turn.id` is still set
|
|
115
|
-
// after `turn.created` — notify so edit/retry can resolve the turn.
|
|
116
|
-
// Same for AbortError after create: keep local ids aligned with gateway.
|
|
117
|
-
notifyTurnIdIfAvailable();
|
|
118
100
|
if (error instanceof Error && error.name === "AbortError") {
|
|
119
101
|
return;
|
|
120
102
|
}
|
|
@@ -126,14 +108,22 @@ export async function* streamTurnContent(
|
|
|
126
108
|
|
|
127
109
|
/** TODO: wire `afterSequenceNumber` from the last ingested stream event to skip replay on reconnect. */
|
|
128
110
|
export async function* resumeTurnStream(
|
|
129
|
-
|
|
111
|
+
server: AgentChatServer,
|
|
112
|
+
sessionId: string,
|
|
113
|
+
turnId: string,
|
|
130
114
|
foldState: PeerThreadFoldState,
|
|
131
115
|
abortSignal: AbortSignal,
|
|
132
116
|
afterSequenceNumber?: number,
|
|
133
117
|
groupRootBaseline?: readonly string[],
|
|
134
118
|
): AsyncGenerator<TurnStreamUpdate> {
|
|
119
|
+
if (server.subscribeToTurn == null) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
"resumeTurnStream requires AgentChatServer.subscribeToTurn",
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
135
125
|
const onAbort = () => {
|
|
136
|
-
void
|
|
126
|
+
void server.cancelSession({ sessionId }).catch(() => undefined);
|
|
137
127
|
};
|
|
138
128
|
if (abortSignal.aborted) {
|
|
139
129
|
onAbort();
|
|
@@ -143,10 +133,14 @@ export async function* resumeTurnStream(
|
|
|
143
133
|
|
|
144
134
|
try {
|
|
145
135
|
yield* streamTurnEvents(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
136
|
+
server.subscribeToTurn({
|
|
137
|
+
sessionId,
|
|
138
|
+
turnId,
|
|
139
|
+
...(afterSequenceNumber != null
|
|
140
|
+
? { afterSequenceNumber }
|
|
141
|
+
: {}),
|
|
142
|
+
abortSignal,
|
|
143
|
+
}),
|
|
150
144
|
foldState,
|
|
151
145
|
groupRootBaseline,
|
|
152
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,
|
|
@@ -23,26 +23,26 @@ export function createTrueFoundryThreadListAdapter(options: {
|
|
|
23
23
|
const threads = page.data.map((session) => ({
|
|
24
24
|
status: "regular" as const,
|
|
25
25
|
remoteId: session.id,
|
|
26
|
-
title: session.title,
|
|
26
|
+
title: session.title ?? undefined,
|
|
27
27
|
lastMessageAt: new Date(session.updatedAt),
|
|
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,
|
|
45
|
-
title: session.title,
|
|
45
|
+
title: session.title ?? undefined,
|
|
46
46
|
lastMessageAt: new Date(session.updatedAt),
|
|
47
47
|
};
|
|
48
48
|
},
|
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
|
}
|