@assistant-ui/react-google-adk 0.0.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.
Files changed (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +156 -0
  3. package/dist/AdkClient.d.ts +45 -0
  4. package/dist/AdkClient.d.ts.map +1 -0
  5. package/dist/AdkClient.js +204 -0
  6. package/dist/AdkClient.js.map +1 -0
  7. package/dist/AdkEventAccumulator.d.ts +45 -0
  8. package/dist/AdkEventAccumulator.d.ts.map +1 -0
  9. package/dist/AdkEventAccumulator.js +508 -0
  10. package/dist/AdkEventAccumulator.js.map +1 -0
  11. package/dist/AdkSessionAdapter.d.ts +61 -0
  12. package/dist/AdkSessionAdapter.d.ts.map +1 -0
  13. package/dist/AdkSessionAdapter.js +159 -0
  14. package/dist/AdkSessionAdapter.js.map +1 -0
  15. package/dist/convertAdkMessages.d.ts +4 -0
  16. package/dist/convertAdkMessages.d.ts.map +1 -0
  17. package/dist/convertAdkMessages.js +75 -0
  18. package/dist/convertAdkMessages.js.map +1 -0
  19. package/dist/hooks.d.ts +50 -0
  20. package/dist/hooks.d.ts.map +1 -0
  21. package/dist/hooks.js +173 -0
  22. package/dist/hooks.js.map +1 -0
  23. package/dist/index.d.ts +11 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +10 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/server/adkEventStream.d.ts +42 -0
  28. package/dist/server/adkEventStream.d.ts.map +1 -0
  29. package/dist/server/adkEventStream.js +135 -0
  30. package/dist/server/adkEventStream.js.map +1 -0
  31. package/dist/server/createAdkApiRoute.d.ts +47 -0
  32. package/dist/server/createAdkApiRoute.d.ts.map +1 -0
  33. package/dist/server/createAdkApiRoute.js +41 -0
  34. package/dist/server/createAdkApiRoute.js.map +1 -0
  35. package/dist/server/index.d.ts +4 -0
  36. package/dist/server/index.d.ts.map +1 -0
  37. package/dist/server/index.js +4 -0
  38. package/dist/server/index.js.map +1 -0
  39. package/dist/server/parseAdkRequest.d.ts +56 -0
  40. package/dist/server/parseAdkRequest.d.ts.map +1 -0
  41. package/dist/server/parseAdkRequest.js +93 -0
  42. package/dist/server/parseAdkRequest.js.map +1 -0
  43. package/dist/structuredEvents.d.ts +7 -0
  44. package/dist/structuredEvents.d.ts.map +1 -0
  45. package/dist/structuredEvents.js +79 -0
  46. package/dist/structuredEvents.js.map +1 -0
  47. package/dist/types.d.ts +253 -0
  48. package/dist/types.d.ts.map +1 -0
  49. package/dist/types.js +14 -0
  50. package/dist/types.js.map +1 -0
  51. package/dist/useAdkMessages.d.ts +28 -0
  52. package/dist/useAdkMessages.d.ts.map +1 -0
  53. package/dist/useAdkMessages.js +198 -0
  54. package/dist/useAdkMessages.js.map +1 -0
  55. package/dist/useAdkRuntime.d.ts +36 -0
  56. package/dist/useAdkRuntime.d.ts.map +1 -0
  57. package/dist/useAdkRuntime.js +252 -0
  58. package/dist/useAdkRuntime.js.map +1 -0
  59. package/package.json +83 -0
  60. package/server/package.json +4 -0
  61. package/src/AdkClient.test.ts +662 -0
  62. package/src/AdkClient.ts +274 -0
  63. package/src/AdkEventAccumulator.test.ts +591 -0
  64. package/src/AdkEventAccumulator.ts +602 -0
  65. package/src/AdkSessionAdapter.test.ts +362 -0
  66. package/src/AdkSessionAdapter.ts +245 -0
  67. package/src/convertAdkMessages.test.ts +209 -0
  68. package/src/convertAdkMessages.ts +93 -0
  69. package/src/hooks.ts +217 -0
  70. package/src/index.ts +66 -0
  71. package/src/server/adkEventStream.test.ts +78 -0
  72. package/src/server/adkEventStream.ts +161 -0
  73. package/src/server/createAdkApiRoute.test.ts +370 -0
  74. package/src/server/createAdkApiRoute.ts +86 -0
  75. package/src/server/index.ts +6 -0
  76. package/src/server/parseAdkRequest.test.ts +152 -0
  77. package/src/server/parseAdkRequest.ts +122 -0
  78. package/src/structuredEvents.ts +81 -0
  79. package/src/types.ts +265 -0
  80. package/src/useAdkMessages.ts +259 -0
  81. package/src/useAdkRuntime.ts +398 -0
@@ -0,0 +1,362 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { createAdkSessionAdapter } from "./AdkSessionAdapter";
3
+
4
+ // ── Helpers ──
5
+
6
+ const mockFetch =
7
+ vi.fn<(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>>();
8
+
9
+ beforeEach(() => {
10
+ vi.stubGlobal("fetch", mockFetch);
11
+ mockFetch.mockReset();
12
+ });
13
+
14
+ const baseOptions = {
15
+ apiUrl: "http://localhost:8000",
16
+ appName: "my-app",
17
+ userId: "user-1",
18
+ };
19
+
20
+ const expectedBaseUrl =
21
+ "http://localhost:8000/apps/my-app/users/user-1/sessions";
22
+
23
+ // ── adapter.list() ──
24
+
25
+ describe("createAdkSessionAdapter - list", () => {
26
+ it("fetches sessions and maps them to RemoteThreadMetadata", async () => {
27
+ const sessions = [
28
+ { id: "s1", app_name: "my-app", user_id: "user-1" },
29
+ { id: "s2", app_name: "my-app", user_id: "user-1" },
30
+ ];
31
+ mockFetch.mockResolvedValueOnce(
32
+ new Response(JSON.stringify(sessions), { status: 200 }),
33
+ );
34
+
35
+ const { adapter } = createAdkSessionAdapter(baseOptions);
36
+ const result = await adapter.list();
37
+
38
+ expect(mockFetch).toHaveBeenCalledOnce();
39
+ expect(mockFetch.mock.calls[0]![0]).toBe(expectedBaseUrl);
40
+ expect(result.threads).toHaveLength(2);
41
+ expect(result.threads[0]).toMatchObject({
42
+ status: "regular",
43
+ remoteId: "s1",
44
+ externalId: "s1",
45
+ title: undefined,
46
+ });
47
+ expect(result.threads[1]).toMatchObject({
48
+ status: "regular",
49
+ remoteId: "s2",
50
+ externalId: "s2",
51
+ });
52
+ });
53
+
54
+ it("returns empty array when no sessions exist", async () => {
55
+ mockFetch.mockResolvedValueOnce(
56
+ new Response(JSON.stringify([]), { status: 200 }),
57
+ );
58
+
59
+ const { adapter } = createAdkSessionAdapter(baseOptions);
60
+ const result = await adapter.list();
61
+
62
+ expect(result.threads).toHaveLength(0);
63
+ });
64
+
65
+ it("throws when response is not ok", async () => {
66
+ mockFetch.mockResolvedValueOnce(
67
+ new Response("Server error", { status: 500 }),
68
+ );
69
+
70
+ const { adapter } = createAdkSessionAdapter(baseOptions);
71
+ await expect(adapter.list()).rejects.toThrow(
72
+ "Failed to list sessions: 500",
73
+ );
74
+ });
75
+
76
+ it("passes custom headers", async () => {
77
+ mockFetch.mockResolvedValueOnce(
78
+ new Response(JSON.stringify([]), { status: 200 }),
79
+ );
80
+
81
+ const { adapter } = createAdkSessionAdapter({
82
+ ...baseOptions,
83
+ headers: { Authorization: "Bearer tok" },
84
+ });
85
+ await adapter.list();
86
+
87
+ const init = mockFetch.mock.calls[0]![1];
88
+ expect((init?.headers as Record<string, string>).Authorization).toBe(
89
+ "Bearer tok",
90
+ );
91
+ });
92
+
93
+ it("resolves dynamic headers from a function", async () => {
94
+ mockFetch.mockResolvedValueOnce(
95
+ new Response(JSON.stringify([]), { status: 200 }),
96
+ );
97
+
98
+ const { adapter } = createAdkSessionAdapter({
99
+ ...baseOptions,
100
+ headers: async () => ({ "X-Dynamic": "val" }),
101
+ });
102
+ await adapter.list();
103
+
104
+ const init = mockFetch.mock.calls[0]![1];
105
+ expect((init?.headers as Record<string, string>)["X-Dynamic"]).toBe("val");
106
+ });
107
+ });
108
+
109
+ // ── adapter.initialize() ──
110
+
111
+ describe("createAdkSessionAdapter - initialize", () => {
112
+ it("creates a new session via POST and returns remoteId/externalId", async () => {
113
+ mockFetch.mockResolvedValueOnce(
114
+ new Response(JSON.stringify({ id: "new-session-1" }), { status: 200 }),
115
+ );
116
+
117
+ const { adapter } = createAdkSessionAdapter(baseOptions);
118
+ const result = await adapter.initialize("thread-1");
119
+
120
+ expect(mockFetch).toHaveBeenCalledOnce();
121
+ const [url, init] = mockFetch.mock.calls[0]!;
122
+ expect(url).toBe(expectedBaseUrl);
123
+ expect(init?.method).toBe("POST");
124
+ expect(init?.headers).toMatchObject({
125
+ "Content-Type": "application/json",
126
+ });
127
+ expect(JSON.parse(init?.body as string)).toEqual({});
128
+ expect(result).toMatchObject({
129
+ remoteId: "thread-1",
130
+ externalId: "new-session-1",
131
+ });
132
+ });
133
+
134
+ it("throws when POST fails", async () => {
135
+ mockFetch.mockResolvedValueOnce(new Response("Forbidden", { status: 403 }));
136
+
137
+ const { adapter } = createAdkSessionAdapter(baseOptions);
138
+ await expect(adapter.initialize("thread-1")).rejects.toThrow(
139
+ "Failed to create session: 403",
140
+ );
141
+ });
142
+ });
143
+
144
+ // ── adapter.delete() ──
145
+
146
+ describe("createAdkSessionAdapter - delete", () => {
147
+ it("sends DELETE to the session URL", async () => {
148
+ mockFetch.mockResolvedValueOnce(new Response("", { status: 200 }));
149
+
150
+ const { adapter } = createAdkSessionAdapter(baseOptions);
151
+ await adapter.delete("session-1");
152
+
153
+ expect(mockFetch).toHaveBeenCalledOnce();
154
+ const [url, init] = mockFetch.mock.calls[0]!;
155
+ expect(url).toBe(`${expectedBaseUrl}/session-1`);
156
+ expect(init?.method).toBe("DELETE");
157
+ });
158
+
159
+ it("ignores 404 responses", async () => {
160
+ mockFetch.mockResolvedValueOnce(new Response("Not found", { status: 404 }));
161
+
162
+ const { adapter } = createAdkSessionAdapter(baseOptions);
163
+ await expect(adapter.delete("missing-session")).resolves.toBeUndefined();
164
+ });
165
+
166
+ it("throws on other error responses", async () => {
167
+ mockFetch.mockResolvedValueOnce(
168
+ new Response("Server error", { status: 500 }),
169
+ );
170
+
171
+ const { adapter } = createAdkSessionAdapter(baseOptions);
172
+ await expect(adapter.delete("session-1")).rejects.toThrow(
173
+ "Failed to delete session: 500",
174
+ );
175
+ });
176
+
177
+ it("URL-encodes the remoteId", async () => {
178
+ mockFetch.mockResolvedValueOnce(new Response("", { status: 200 }));
179
+
180
+ const { adapter } = createAdkSessionAdapter(baseOptions);
181
+ await adapter.delete("session/with spaces");
182
+
183
+ const url = mockFetch.mock.calls[0]![0] as string;
184
+ expect(url).toBe(`${expectedBaseUrl}/session%2Fwith%20spaces`);
185
+ });
186
+ });
187
+
188
+ // ── adapter.rename / archive / unarchive ──
189
+
190
+ describe("createAdkSessionAdapter - no-op methods", () => {
191
+ it("rename resolves without error", async () => {
192
+ const { adapter } = createAdkSessionAdapter(baseOptions);
193
+ await expect(adapter.rename("s1", "New Title")).resolves.toBeUndefined();
194
+ expect(mockFetch).not.toHaveBeenCalled();
195
+ });
196
+
197
+ it("archive resolves without error", async () => {
198
+ const { adapter } = createAdkSessionAdapter(baseOptions);
199
+ await expect(adapter.archive("s1")).resolves.toBeUndefined();
200
+ expect(mockFetch).not.toHaveBeenCalled();
201
+ });
202
+
203
+ it("unarchive resolves without error", async () => {
204
+ const { adapter } = createAdkSessionAdapter(baseOptions);
205
+ await expect(adapter.unarchive("s1")).resolves.toBeUndefined();
206
+ expect(mockFetch).not.toHaveBeenCalled();
207
+ });
208
+ });
209
+
210
+ // ── adapter.generateTitle ──
211
+
212
+ describe("createAdkSessionAdapter - generateTitle", () => {
213
+ it("returns an empty ReadableStream", async () => {
214
+ const { adapter } = createAdkSessionAdapter(baseOptions);
215
+ const result = await adapter.generateTitle("s1");
216
+ expect(result).toBeInstanceOf(ReadableStream);
217
+ });
218
+ });
219
+
220
+ // ── adapter.fetch() ──
221
+
222
+ describe("createAdkSessionAdapter - fetch", () => {
223
+ it("fetches a session and returns metadata", async () => {
224
+ mockFetch.mockResolvedValueOnce(
225
+ new Response(JSON.stringify({ id: "s1" }), { status: 200 }),
226
+ );
227
+
228
+ const { adapter } = createAdkSessionAdapter(baseOptions);
229
+ const result = await adapter.fetch("s1");
230
+
231
+ expect(mockFetch).toHaveBeenCalledOnce();
232
+ expect(mockFetch.mock.calls[0]![0]).toBe(`${expectedBaseUrl}/s1`);
233
+ expect(result).toMatchObject({
234
+ status: "regular",
235
+ remoteId: "s1",
236
+ externalId: "s1",
237
+ title: undefined,
238
+ });
239
+ });
240
+
241
+ it("throws when session is not found", async () => {
242
+ mockFetch.mockResolvedValueOnce(new Response("Not found", { status: 404 }));
243
+
244
+ const { adapter } = createAdkSessionAdapter(baseOptions);
245
+ await expect(adapter.fetch("missing")).rejects.toThrow(
246
+ "Session not found: 404",
247
+ );
248
+ });
249
+ });
250
+
251
+ // ── load() ──
252
+
253
+ describe("createAdkSessionAdapter - load", () => {
254
+ it("reconstructs messages from session events", async () => {
255
+ const session = {
256
+ id: "s1",
257
+ events: [
258
+ {
259
+ id: "e1",
260
+ author: "user",
261
+ content: { role: "user", parts: [{ text: "Hello" }] },
262
+ },
263
+ {
264
+ id: "e2",
265
+ author: "agent",
266
+ content: { role: "model", parts: [{ text: "Hi there!" }] },
267
+ },
268
+ ],
269
+ };
270
+ mockFetch.mockResolvedValueOnce(
271
+ new Response(JSON.stringify(session), { status: 200 }),
272
+ );
273
+
274
+ const { load } = createAdkSessionAdapter(baseOptions);
275
+ const result = await load("s1");
276
+
277
+ expect(mockFetch).toHaveBeenCalledOnce();
278
+ expect(mockFetch.mock.calls[0]![0]).toBe(`${expectedBaseUrl}/s1`);
279
+ expect(result.messages.length).toBeGreaterThanOrEqual(1);
280
+ });
281
+
282
+ it("returns empty messages when session has no events", async () => {
283
+ mockFetch.mockResolvedValueOnce(
284
+ new Response(JSON.stringify({ id: "s1", events: [] }), { status: 200 }),
285
+ );
286
+
287
+ const { load } = createAdkSessionAdapter(baseOptions);
288
+ const result = await load("s1");
289
+
290
+ expect(result.messages).toEqual([]);
291
+ });
292
+
293
+ it("returns empty messages when events field is missing", async () => {
294
+ mockFetch.mockResolvedValueOnce(
295
+ new Response(JSON.stringify({ id: "s1" }), { status: 200 }),
296
+ );
297
+
298
+ const { load } = createAdkSessionAdapter(baseOptions);
299
+ const result = await load("s1");
300
+
301
+ expect(result.messages).toEqual([]);
302
+ });
303
+
304
+ it("throws when session fetch fails", async () => {
305
+ mockFetch.mockResolvedValueOnce(
306
+ new Response("Server error", { status: 500 }),
307
+ );
308
+
309
+ const { load } = createAdkSessionAdapter(baseOptions);
310
+ await expect(load("s1")).rejects.toThrow("Failed to load session: 500");
311
+ });
312
+
313
+ it("URL-encodes the sessionId", async () => {
314
+ mockFetch.mockResolvedValueOnce(
315
+ new Response(JSON.stringify({ id: "s/1", events: [] }), { status: 200 }),
316
+ );
317
+
318
+ const { load } = createAdkSessionAdapter(baseOptions);
319
+ await load("s/1");
320
+
321
+ expect(mockFetch.mock.calls[0]![0]).toBe(`${expectedBaseUrl}/s%2F1`);
322
+ });
323
+
324
+ it("passes custom headers to the load request", async () => {
325
+ mockFetch.mockResolvedValueOnce(
326
+ new Response(JSON.stringify({ id: "s1", events: [] }), { status: 200 }),
327
+ );
328
+
329
+ const { load } = createAdkSessionAdapter({
330
+ ...baseOptions,
331
+ headers: { Authorization: "Bearer tok" },
332
+ });
333
+ await load("s1");
334
+
335
+ const init = mockFetch.mock.calls[0]![1];
336
+ expect((init?.headers as Record<string, string>).Authorization).toBe(
337
+ "Bearer tok",
338
+ );
339
+ });
340
+ });
341
+
342
+ // ── URL encoding ──
343
+
344
+ describe("createAdkSessionAdapter - URL encoding", () => {
345
+ it("encodes special characters in appName and userId", async () => {
346
+ mockFetch.mockResolvedValueOnce(
347
+ new Response(JSON.stringify([]), { status: 200 }),
348
+ );
349
+
350
+ const { adapter } = createAdkSessionAdapter({
351
+ apiUrl: "http://localhost:8000",
352
+ appName: "my app/special",
353
+ userId: "user@1",
354
+ });
355
+ await adapter.list();
356
+
357
+ const url = mockFetch.mock.calls[0]![0] as string;
358
+ expect(url).toBe(
359
+ "http://localhost:8000/apps/my%20app%2Fspecial/users/user%401/sessions",
360
+ );
361
+ });
362
+ });
@@ -0,0 +1,245 @@
1
+ import { AssistantStream, AssistantStreamChunk } from "assistant-stream";
2
+ import type {
3
+ RemoteThreadInitializeResponse,
4
+ RemoteThreadListAdapter,
5
+ RemoteThreadListResponse,
6
+ RemoteThreadMetadata,
7
+ } from "@assistant-ui/core";
8
+ import { AdkEventAccumulator } from "./AdkEventAccumulator";
9
+ import type { AdkEvent, AdkMessage } from "./types";
10
+
11
+ export type AdkSessionAdapterOptions = {
12
+ /**
13
+ * ADK server base URL (e.g. "http://localhost:8000").
14
+ */
15
+ apiUrl: string;
16
+
17
+ /**
18
+ * ADK application name.
19
+ */
20
+ appName: string;
21
+
22
+ /**
23
+ * ADK user ID.
24
+ */
25
+ userId: string;
26
+
27
+ /**
28
+ * Extra headers for API requests.
29
+ */
30
+ headers?:
31
+ | Record<string, string>
32
+ | (() => Record<string, string> | Promise<Record<string, string>>)
33
+ | undefined;
34
+ };
35
+
36
+ export type AdkArtifactData = {
37
+ inlineData?: { mimeType: string; data: string } | undefined;
38
+ text?: string | undefined;
39
+ };
40
+
41
+ type AdkSessionAdapterResult = {
42
+ adapter: RemoteThreadListAdapter;
43
+ load: (sessionId: string) => Promise<{ messages: AdkMessage[] }>;
44
+ artifacts: {
45
+ list: (sessionId: string) => Promise<string[]>;
46
+ load: (
47
+ sessionId: string,
48
+ artifactName: string,
49
+ version?: number,
50
+ ) => Promise<AdkArtifactData>;
51
+ listVersions: (
52
+ sessionId: string,
53
+ artifactName: string,
54
+ ) => Promise<number[]>;
55
+ delete: (sessionId: string, artifactName: string) => Promise<void>;
56
+ };
57
+ };
58
+
59
+ /**
60
+ * Creates a `RemoteThreadListAdapter` backed by ADK's session REST API,
61
+ * plus a `load` function that reconstructs messages from session events.
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * const { adapter, load } = createAdkSessionAdapter({
66
+ * apiUrl: "http://localhost:8000",
67
+ * appName: "my-app",
68
+ * userId: "user-1",
69
+ * });
70
+ *
71
+ * const runtime = useAdkRuntime({
72
+ * stream: createAdkStream({ ... }),
73
+ * sessionAdapter: adapter,
74
+ * load,
75
+ * });
76
+ * ```
77
+ */
78
+ export function createAdkSessionAdapter(
79
+ options: AdkSessionAdapterOptions,
80
+ ): AdkSessionAdapterResult {
81
+ const { apiUrl, appName, userId } = options;
82
+ const baseUrl = `${apiUrl}/apps/${encodeURIComponent(appName)}/users/${encodeURIComponent(userId)}/sessions`;
83
+
84
+ const getHeaders = async (): Promise<Record<string, string>> => {
85
+ if (!options.headers) return {};
86
+ if (typeof options.headers === "function") return await options.headers();
87
+ return options.headers;
88
+ };
89
+
90
+ const adapter: RemoteThreadListAdapter = {
91
+ async list(): Promise<RemoteThreadListResponse> {
92
+ const headers = await getHeaders();
93
+ const res = await fetch(baseUrl, { headers });
94
+ if (!res.ok) {
95
+ throw new Error(`Failed to list sessions: ${res.status}`);
96
+ }
97
+ const data = (await res.json()) as Array<{
98
+ id: string;
99
+ app_name?: string;
100
+ user_id?: string;
101
+ last_update_time?: number;
102
+ }>;
103
+
104
+ const threads: RemoteThreadMetadata[] = data.map((session) => ({
105
+ status: "regular" as const,
106
+ remoteId: session.id,
107
+ externalId: session.id,
108
+ title: undefined,
109
+ }));
110
+
111
+ return { threads };
112
+ },
113
+
114
+ async initialize(
115
+ threadId: string,
116
+ ): Promise<RemoteThreadInitializeResponse> {
117
+ const headers = await getHeaders();
118
+ const res = await fetch(baseUrl, {
119
+ method: "POST",
120
+ headers: { "Content-Type": "application/json", ...headers },
121
+ body: JSON.stringify({}),
122
+ });
123
+ if (!res.ok) {
124
+ throw new Error(`Failed to create session: ${res.status}`);
125
+ }
126
+ const session = (await res.json()) as { id: string };
127
+ return { remoteId: threadId, externalId: session.id };
128
+ },
129
+
130
+ async delete(remoteId: string): Promise<void> {
131
+ const headers = await getHeaders();
132
+ const res = await fetch(`${baseUrl}/${encodeURIComponent(remoteId)}`, {
133
+ method: "DELETE",
134
+ headers,
135
+ });
136
+ if (!res.ok && res.status !== 404) {
137
+ throw new Error(`Failed to delete session: ${res.status}`);
138
+ }
139
+ },
140
+
141
+ async rename(): Promise<void> {
142
+ // ADK sessions don't support titles
143
+ },
144
+
145
+ async archive(): Promise<void> {
146
+ // ADK sessions don't support archiving
147
+ },
148
+
149
+ async unarchive(): Promise<void> {
150
+ // ADK sessions don't support archiving
151
+ },
152
+
153
+ generateTitle(): Promise<AssistantStream> {
154
+ // Title generation not supported without assistant-cloud
155
+ return Promise.resolve(new ReadableStream<AssistantStreamChunk>());
156
+ },
157
+
158
+ async fetch(threadId: string): Promise<RemoteThreadMetadata> {
159
+ const headers = await getHeaders();
160
+ const res = await fetch(`${baseUrl}/${encodeURIComponent(threadId)}`, {
161
+ headers,
162
+ });
163
+ if (!res.ok) {
164
+ throw new Error(`Session not found: ${res.status}`);
165
+ }
166
+ const session = (await res.json()) as { id: string };
167
+ return {
168
+ status: "regular",
169
+ remoteId: session.id,
170
+ externalId: session.id,
171
+ title: undefined,
172
+ };
173
+ },
174
+ };
175
+
176
+ const load = async (
177
+ sessionId: string,
178
+ ): Promise<{ messages: AdkMessage[] }> => {
179
+ const headers = await getHeaders();
180
+ const res = await fetch(`${baseUrl}/${encodeURIComponent(sessionId)}`, {
181
+ headers,
182
+ });
183
+ if (!res.ok) {
184
+ throw new Error(`Failed to load session: ${res.status}`);
185
+ }
186
+ const session = (await res.json()) as {
187
+ id: string;
188
+ events?: AdkEvent[];
189
+ };
190
+
191
+ if (!session.events?.length) {
192
+ return { messages: [] };
193
+ }
194
+
195
+ const accumulator = new AdkEventAccumulator();
196
+ let messages: AdkMessage[] = [];
197
+ for (const event of session.events) {
198
+ messages = accumulator.processEvent(event);
199
+ }
200
+ return { messages };
201
+ };
202
+
203
+ const artifactBaseUrl = (sessionId: string) =>
204
+ `${baseUrl}/${encodeURIComponent(sessionId)}/artifacts`;
205
+
206
+ const artifacts: AdkSessionAdapterResult["artifacts"] = {
207
+ async list(sessionId) {
208
+ const headers = await getHeaders();
209
+ const res = await fetch(artifactBaseUrl(sessionId), { headers });
210
+ if (!res.ok) throw new Error(`Failed to list artifacts: ${res.status}`);
211
+ const data = (await res.json()) as Array<{ filename: string }>;
212
+ return data.map((a) => a.filename);
213
+ },
214
+
215
+ async load(sessionId, artifactName, version?) {
216
+ const headers = await getHeaders();
217
+ let url = `${artifactBaseUrl(sessionId)}/${encodeURIComponent(artifactName)}`;
218
+ if (version != null) url += `/versions/${version}`;
219
+ const res = await fetch(url, { headers });
220
+ if (!res.ok) throw new Error(`Failed to load artifact: ${res.status}`);
221
+ return (await res.json()) as AdkArtifactData;
222
+ },
223
+
224
+ async listVersions(sessionId, artifactName) {
225
+ const headers = await getHeaders();
226
+ const url = `${artifactBaseUrl(sessionId)}/${encodeURIComponent(artifactName)}/versions`;
227
+ const res = await fetch(url, { headers });
228
+ if (!res.ok) {
229
+ throw new Error(`Failed to list artifact versions: ${res.status}`);
230
+ }
231
+ return (await res.json()) as number[];
232
+ },
233
+
234
+ async delete(sessionId, artifactName) {
235
+ const headers = await getHeaders();
236
+ const url = `${artifactBaseUrl(sessionId)}/${encodeURIComponent(artifactName)}`;
237
+ const res = await fetch(url, { method: "DELETE", headers });
238
+ if (!res.ok && res.status !== 404) {
239
+ throw new Error(`Failed to delete artifact: ${res.status}`);
240
+ }
241
+ },
242
+ };
243
+
244
+ return { adapter, load, artifacts };
245
+ }