@assistant-ui/react-google-adk 0.0.17 → 0.0.18
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/AdkClient.js +11 -0
- package/dist/AdkClient.js.map +1 -1
- package/dist/AdkEventAccumulator.d.ts.map +1 -1
- package/dist/AdkSessionAdapter.d.ts.map +1 -1
- package/dist/AdkSessionAdapter.js +54 -9
- package/dist/AdkSessionAdapter.js.map +1 -1
- package/dist/useAdkMessages.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/AdkClient.test.ts +134 -44
- package/src/AdkClient.ts +26 -0
- package/src/AdkSessionAdapter.test.ts +199 -0
- package/src/AdkSessionAdapter.ts +141 -18
package/dist/AdkClient.js
CHANGED
|
@@ -20,7 +20,9 @@ import { SSEEventDecoder } from "assistant-stream/utils";
|
|
|
20
20
|
* ```
|
|
21
21
|
*/
|
|
22
22
|
function createAdkStream(options) {
|
|
23
|
+
if (options.appName === "") throw new Error("createAdkStream direct mode requires a non-empty \"appName\".");
|
|
23
24
|
const isDirect = options.appName != null;
|
|
25
|
+
if (isDirect && (options.userId == null || options.userId === "")) throw new Error("createAdkStream direct mode requires \"userId\" when \"appName\" is provided.");
|
|
24
26
|
return async function* (messages, config) {
|
|
25
27
|
const headers = await resolveHeaders(options.headers);
|
|
26
28
|
let url;
|
|
@@ -50,9 +52,18 @@ function createAdkStream(options) {
|
|
|
50
52
|
signal: config.abortSignal
|
|
51
53
|
});
|
|
52
54
|
if (!response.ok) throw new Error(`ADK request failed: ${response.status} ${response.statusText}`);
|
|
55
|
+
validateEventStreamContentType(response);
|
|
53
56
|
yield* parseSSEResponse(response);
|
|
54
57
|
};
|
|
55
58
|
}
|
|
59
|
+
function validateEventStreamContentType(response) {
|
|
60
|
+
const contentType = response.headers.get("Content-Type");
|
|
61
|
+
if (contentType?.split(";", 1)[0]?.trim().toLowerCase() !== "text/event-stream") {
|
|
62
|
+
const received = contentType ? `"${contentType}"` : "no Content-Type header";
|
|
63
|
+
response.body?.cancel().catch(() => void 0);
|
|
64
|
+
throw new Error(`Expected ADK stream response Content-Type "text/event-stream", received ${received}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
56
67
|
async function resolveHeaders(headers) {
|
|
57
68
|
if (!headers) return {};
|
|
58
69
|
if (typeof headers === "function") return await headers();
|
package/dist/AdkClient.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdkClient.js","names":[],"sources":["../src/AdkClient.ts"],"sourcesContent":["import { SSEEventDecoder } from \"assistant-stream/utils\";\nimport { contentToParts } from \"./contentToParts\";\nimport { trimTrailingSlashes } from \"./trimTrailingSlashes\";\nimport type {\n AdkEvent,\n AdkEventPart,\n AdkMessage,\n AdkStreamCallback,\n} from \"./types\";\n\nexport type CreateAdkStreamOptions = {\n /**\n * URL to POST to. Either a proxy route (e.g. \"/api/adk\") or\n * an ADK server base URL (e.g. \"http://localhost:8000\").\n *\n * When `appName` and `userId` are provided, POSTs to `${api}/run_sse`\n * in ADK-native format. Otherwise POSTs directly to `api` in proxy format\n * (compatible with `parseAdkRequest`).\n */\n api: string;\n\n /**\n * ADK application name. When provided along with `userId`,\n * enables direct connection to an ADK server.\n */\n appName?: string | undefined;\n\n /**\n * ADK user ID. Required when `appName` is provided.\n */\n userId?: string | undefined;\n\n /**\n * Extra headers to send with every request.\n * Can be a static object or an async function for dynamic auth tokens.\n */\n headers?:\n | Record<string, string>\n | (() => Record<string, string> | Promise<Record<string, string>>)\n | undefined;\n};\n\n/**\n * Creates an `AdkStreamCallback` that connects to an ADK endpoint.\n *\n * @example Proxy mode (with a Next.js API route)\n * ```ts\n * const stream = createAdkStream({ api: \"/api/adk\" });\n * ```\n *\n * @example Direct mode (connecting to ADK server)\n * ```ts\n * const stream = createAdkStream({\n * api: \"http://localhost:8000\",\n * appName: \"my-app\",\n * userId: \"user-1\",\n * });\n * ```\n */\nexport function createAdkStream(\n options: CreateAdkStreamOptions,\n): AdkStreamCallback {\n const isDirect = options.appName != null;\n\n return async function* (messages, config) {\n const headers = await resolveHeaders(options.headers);\n\n let url: string;\n let body: unknown;\n\n if (isDirect) {\n // Direct mode: POST to ADK server's /run_sse\n url = `${trimTrailingSlashes(options.api)}/run_sse`;\n const { externalId } = await config.initialize();\n body = {\n appName: options.appName,\n userId: options.userId,\n sessionId: externalId,\n newMessage: messagesToContent(messages),\n streaming: true,\n ...(config.stateDelta != null && { stateDelta: config.stateDelta }),\n };\n } else {\n // Proxy mode: POST in parseAdkRequest-compatible format\n url = options.api;\n body = messagesToProxyBody(messages, config);\n }\n\n const response = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...headers },\n body: JSON.stringify(body),\n signal: config.abortSignal,\n });\n\n if (!response.ok) {\n throw new Error(\n `ADK request failed: ${response.status} ${response.statusText}`,\n );\n }\n\n yield* parseSSEResponse(response);\n };\n}\n\n// ── Internal helpers ──\n\nasync function resolveHeaders(\n headers:\n | Record<string, string>\n | (() => Record<string, string> | Promise<Record<string, string>>)\n | undefined,\n): Promise<Record<string, string>> {\n if (!headers) return {};\n if (typeof headers === \"function\") return await headers();\n return headers;\n}\n\n/**\n * Converts AdkMessage[] (new messages) into ADK Content format\n * for the direct `/run_sse` endpoint.\n */\nfunction messagesToContent(messages: AdkMessage[]): {\n role: string;\n parts: AdkEventPart[];\n} {\n const parts: AdkEventPart[] = [];\n\n for (const msg of messages) {\n if (msg.type === \"human\") {\n for (const part of contentToParts(msg.content)) {\n parts.push(part);\n }\n } else if (msg.type === \"tool\") {\n let response: unknown;\n try {\n response = JSON.parse(msg.content);\n } catch {\n response = msg.content;\n }\n parts.push({\n functionResponse: {\n name: msg.name,\n id: msg.tool_call_id,\n response,\n },\n });\n }\n }\n\n if (parts.length === 0) {\n parts.push({ text: \"\" });\n }\n\n return { role: \"user\", parts };\n}\n\n/**\n * Converts AdkMessage[] into the proxy request body format\n * (compatible with `parseAdkRequest`).\n */\nfunction messagesToProxyBody(\n messages: AdkMessage[],\n config: {\n runConfig?: unknown;\n checkpointId?: string | undefined;\n stateDelta?: Record<string, unknown> | undefined;\n },\n): Record<string, unknown> {\n const body: Record<string, unknown> = {};\n\n if (config.runConfig != null) body.runConfig = config.runConfig;\n if (config.checkpointId != null) body.checkpointId = config.checkpointId;\n if (config.stateDelta != null) body.stateDelta = config.stateDelta;\n\n // Check if there's a tool result\n const toolMsg = messages.find((m) => m.type === \"tool\");\n if (toolMsg && toolMsg.type === \"tool\") {\n // If there are also other messages (e.g. cancellations), send as parts\n if (messages.length > 1) {\n body.parts = messagesToContent(messages).parts;\n return body;\n }\n\n let result: unknown;\n try {\n result = JSON.parse(toolMsg.content);\n } catch {\n result = toolMsg.content;\n }\n body.type = \"tool-result\";\n body.toolCallId = toolMsg.tool_call_id;\n body.toolName = toolMsg.name;\n body.result = result;\n body.isError = toolMsg.status === \"error\";\n return body;\n }\n\n // Human message(s) - possibly with cancellation tool results prepended\n if (messages.length === 1 && messages[0]!.type === \"human\") {\n const msg = messages[0]!;\n if (typeof msg.content === \"string\") {\n body.message = msg.content;\n } else {\n body.parts = contentToParts(msg.content);\n }\n return body;\n }\n\n // Multiple messages (e.g. cancellations + human): send as parts array\n body.parts = messagesToContent(messages).parts;\n return body;\n}\n\nasync function* parseSSEResponse(response: Response): AsyncGenerator<AdkEvent> {\n const reader = response.body!.getReader();\n const decoder = new TextDecoder();\n const sseDecoder = new SSEEventDecoder({ trailing: \"dispatch\" });\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) {\n for (const event of sseDecoder.push(decoder.decode())) {\n yield JSON.parse(event.data) as AdkEvent;\n }\n break;\n }\n\n for (const event of sseDecoder.push(\n decoder.decode(value, { stream: true }),\n )) {\n yield JSON.parse(event.data) as AdkEvent;\n }\n }\n\n const trailing = sseDecoder.flush();\n if (trailing !== null) yield JSON.parse(trailing.data) as AdkEvent;\n } finally {\n reader.releaseLock();\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AdkClient.js","names":[],"sources":["../src/AdkClient.ts"],"sourcesContent":["import { SSEEventDecoder } from \"assistant-stream/utils\";\nimport { contentToParts } from \"./contentToParts\";\nimport { trimTrailingSlashes } from \"./trimTrailingSlashes\";\nimport type {\n AdkEvent,\n AdkEventPart,\n AdkMessage,\n AdkStreamCallback,\n} from \"./types\";\n\nexport type CreateAdkStreamOptions = {\n /**\n * URL to POST to. Either a proxy route (e.g. \"/api/adk\") or\n * an ADK server base URL (e.g. \"http://localhost:8000\").\n *\n * When `appName` and `userId` are provided, POSTs to `${api}/run_sse`\n * in ADK-native format. Otherwise POSTs directly to `api` in proxy format\n * (compatible with `parseAdkRequest`).\n */\n api: string;\n\n /**\n * ADK application name. When provided along with `userId`,\n * enables direct connection to an ADK server.\n */\n appName?: string | undefined;\n\n /**\n * ADK user ID. Required when `appName` is provided.\n */\n userId?: string | undefined;\n\n /**\n * Extra headers to send with every request.\n * Can be a static object or an async function for dynamic auth tokens.\n */\n headers?:\n | Record<string, string>\n | (() => Record<string, string> | Promise<Record<string, string>>)\n | undefined;\n};\n\n/**\n * Creates an `AdkStreamCallback` that connects to an ADK endpoint.\n *\n * @example Proxy mode (with a Next.js API route)\n * ```ts\n * const stream = createAdkStream({ api: \"/api/adk\" });\n * ```\n *\n * @example Direct mode (connecting to ADK server)\n * ```ts\n * const stream = createAdkStream({\n * api: \"http://localhost:8000\",\n * appName: \"my-app\",\n * userId: \"user-1\",\n * });\n * ```\n */\nexport function createAdkStream(\n options: CreateAdkStreamOptions,\n): AdkStreamCallback {\n if (options.appName === \"\") {\n throw new Error(\n 'createAdkStream direct mode requires a non-empty \"appName\".',\n );\n }\n\n const isDirect = options.appName != null;\n if (isDirect && (options.userId == null || options.userId === \"\")) {\n throw new Error(\n 'createAdkStream direct mode requires \"userId\" when \"appName\" is provided.',\n );\n }\n\n return async function* (messages, config) {\n const headers = await resolveHeaders(options.headers);\n\n let url: string;\n let body: unknown;\n\n if (isDirect) {\n // Direct mode: POST to ADK server's /run_sse\n url = `${trimTrailingSlashes(options.api)}/run_sse`;\n const { externalId } = await config.initialize();\n body = {\n appName: options.appName,\n userId: options.userId,\n sessionId: externalId,\n newMessage: messagesToContent(messages),\n streaming: true,\n ...(config.stateDelta != null && { stateDelta: config.stateDelta }),\n };\n } else {\n // Proxy mode: POST in parseAdkRequest-compatible format\n url = options.api;\n body = messagesToProxyBody(messages, config);\n }\n\n const response = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...headers },\n body: JSON.stringify(body),\n signal: config.abortSignal,\n });\n\n if (!response.ok) {\n throw new Error(\n `ADK request failed: ${response.status} ${response.statusText}`,\n );\n }\n\n validateEventStreamContentType(response);\n yield* parseSSEResponse(response);\n };\n}\n\n// ── Internal helpers ──\n\nfunction validateEventStreamContentType(response: Response): void {\n const contentType = response.headers.get(\"Content-Type\");\n const mediaType = contentType?.split(\";\", 1)[0]?.trim().toLowerCase();\n if (mediaType !== \"text/event-stream\") {\n const received = contentType\n ? `\"${contentType}\"`\n : \"no Content-Type header\";\n void response.body?.cancel().catch(() => undefined);\n throw new Error(\n `Expected ADK stream response Content-Type \"text/event-stream\", received ${received}`,\n );\n }\n}\n\nasync function resolveHeaders(\n headers:\n | Record<string, string>\n | (() => Record<string, string> | Promise<Record<string, string>>)\n | undefined,\n): Promise<Record<string, string>> {\n if (!headers) return {};\n if (typeof headers === \"function\") return await headers();\n return headers;\n}\n\n/**\n * Converts AdkMessage[] (new messages) into ADK Content format\n * for the direct `/run_sse` endpoint.\n */\nfunction messagesToContent(messages: AdkMessage[]): {\n role: string;\n parts: AdkEventPart[];\n} {\n const parts: AdkEventPart[] = [];\n\n for (const msg of messages) {\n if (msg.type === \"human\") {\n for (const part of contentToParts(msg.content)) {\n parts.push(part);\n }\n } else if (msg.type === \"tool\") {\n let response: unknown;\n try {\n response = JSON.parse(msg.content);\n } catch {\n response = msg.content;\n }\n parts.push({\n functionResponse: {\n name: msg.name,\n id: msg.tool_call_id,\n response,\n },\n });\n }\n }\n\n if (parts.length === 0) {\n parts.push({ text: \"\" });\n }\n\n return { role: \"user\", parts };\n}\n\n/**\n * Converts AdkMessage[] into the proxy request body format\n * (compatible with `parseAdkRequest`).\n */\nfunction messagesToProxyBody(\n messages: AdkMessage[],\n config: {\n runConfig?: unknown;\n checkpointId?: string | undefined;\n stateDelta?: Record<string, unknown> | undefined;\n },\n): Record<string, unknown> {\n const body: Record<string, unknown> = {};\n\n if (config.runConfig != null) body.runConfig = config.runConfig;\n if (config.checkpointId != null) body.checkpointId = config.checkpointId;\n if (config.stateDelta != null) body.stateDelta = config.stateDelta;\n\n // Check if there's a tool result\n const toolMsg = messages.find((m) => m.type === \"tool\");\n if (toolMsg && toolMsg.type === \"tool\") {\n // If there are also other messages (e.g. cancellations), send as parts\n if (messages.length > 1) {\n body.parts = messagesToContent(messages).parts;\n return body;\n }\n\n let result: unknown;\n try {\n result = JSON.parse(toolMsg.content);\n } catch {\n result = toolMsg.content;\n }\n body.type = \"tool-result\";\n body.toolCallId = toolMsg.tool_call_id;\n body.toolName = toolMsg.name;\n body.result = result;\n body.isError = toolMsg.status === \"error\";\n return body;\n }\n\n // Human message(s) - possibly with cancellation tool results prepended\n if (messages.length === 1 && messages[0]!.type === \"human\") {\n const msg = messages[0]!;\n if (typeof msg.content === \"string\") {\n body.message = msg.content;\n } else {\n body.parts = contentToParts(msg.content);\n }\n return body;\n }\n\n // Multiple messages (e.g. cancellations + human): send as parts array\n body.parts = messagesToContent(messages).parts;\n return body;\n}\n\nasync function* parseSSEResponse(response: Response): AsyncGenerator<AdkEvent> {\n const reader = response.body!.getReader();\n const decoder = new TextDecoder();\n const sseDecoder = new SSEEventDecoder({ trailing: \"dispatch\" });\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) {\n for (const event of sseDecoder.push(decoder.decode())) {\n yield JSON.parse(event.data) as AdkEvent;\n }\n break;\n }\n\n for (const event of sseDecoder.push(\n decoder.decode(value, { stream: true }),\n )) {\n yield JSON.parse(event.data) as AdkEvent;\n }\n }\n\n const trailing = sseDecoder.flush();\n if (trailing !== null) yield JSON.parse(trailing.data) as AdkEvent;\n } finally {\n reader.releaseLock();\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdkEventAccumulator.d.ts","names":[],"sources":["../src/AdkEventAccumulator.ts"],"mappings":";;;;;;cAuBa,kBAAmB,OAAO;cAqJ1B;UACH;UACA;UACA;UACA;UACA;UACA;UACA;UAIA;UACA;UACA;UACA;UACA;UACA;
|
|
1
|
+
{"version":3,"file":"AdkEventAccumulator.d.ts","names":[],"sources":["../src/AdkEventAccumulator.ts"],"mappings":";;;;;;cAuBa,kBAAmB,OAAO;cAqJ1B;UACH;UACA;UACA;UACA;UACA;UACA;UACA;UAIA;UACA;UACA;UACA;UACA;UACA;EACI,YAAA,kBAAkB;EAQ9B,aAAa,UAAU,WAAW;UAwL1B;UAmJA;UAmBA;UAMA;UAuBA;UASA;UAYA;UAeA;EAMR,eAAe;EAIf,iBAAiB;EAIjB,oBAAoB;EAIpB;IAAkB;IAA2B;;EAI7C;EAIA;EAIA,wBAAwB;EAIxB,mBAAmB;EAInB;EAIA,sBAAsB,YAAY"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdkSessionAdapter.d.ts","names":[],"sources":["../src/AdkSessionAdapter.ts"],"mappings":";;;KAWY;;;;EAIV;;;;EAKA;;;;EAKA;;;;EAKA,UACI,gCACO,yBAAyB,QAAQ;;KAIlC;EACV;IAAe;IAAkB;;EACjC;;KAGG;EACH,SAAS;EACT,OAAO,sBAAsB;IAAU,UAAU;;EACjD;IACE,OAAO,sBAAsB;IAC7B,OACE,mBACA,sBACA,qBACG,QAAQ;IACb,eACE,mBACA,yBACG;IACL,SAAS,mBAAmB,yBAAyB;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"AdkSessionAdapter.d.ts","names":[],"sources":["../src/AdkSessionAdapter.ts"],"mappings":";;;KAWY;;;;EAIV;;;;EAKA;;;;EAKA;;;;EAKA,UACI,gCACO,yBAAyB,QAAQ;;KAIlC;EACV;IAAe;IAAkB;;EACjC;;KAGG;EACH,SAAS;EACT,OAAO,sBAAsB;IAAU,UAAU;;EACjD;IACE,OAAO,sBAAsB;IAC7B,OACE,mBACA,sBACA,qBACG,QAAQ;IACb,eACE,mBACA,yBACG;IACL,SAAS,mBAAmB,yBAAyB;;;;;;;;;;;;;;;;;;;;;;iBAmJzC,wBACd,SAAS,2BACR"}
|
|
@@ -1,6 +1,49 @@
|
|
|
1
1
|
import { trimTrailingSlashes } from "./trimTrailingSlashes.js";
|
|
2
2
|
import { AdkEventAccumulator } from "./AdkEventAccumulator.js";
|
|
3
3
|
//#region src/AdkSessionAdapter.ts
|
|
4
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
|
+
const isAdkSessionResponse = (value) => {
|
|
6
|
+
if (!isRecord(value)) return false;
|
|
7
|
+
const id = value.id;
|
|
8
|
+
return typeof id === "string" && id.length > 0;
|
|
9
|
+
};
|
|
10
|
+
const parseAdkSessionResponse = (value, operation) => {
|
|
11
|
+
if (!isAdkSessionResponse(value)) throw new Error(`Invalid ADK session ${operation} response: expected an object with a non-empty string "id".`);
|
|
12
|
+
return value;
|
|
13
|
+
};
|
|
14
|
+
const parseAdkSessionListResponse = (value) => {
|
|
15
|
+
if (!Array.isArray(value)) throw new Error("Invalid ADK session list response: expected an array of sessions.");
|
|
16
|
+
return value.map((session, index) => {
|
|
17
|
+
if (!isAdkSessionResponse(session)) throw new Error(`Invalid ADK session list response: session at index ${index} must have a non-empty string "id".`);
|
|
18
|
+
return session;
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
const parseAdkArtifactListResponse = (value) => {
|
|
22
|
+
if (!Array.isArray(value)) throw new Error("Invalid ADK artifact list response: expected an array of artifact names.");
|
|
23
|
+
return value.map((artifact, index) => {
|
|
24
|
+
if (typeof artifact === "string" && artifact.length > 0) return artifact;
|
|
25
|
+
if (isRecord(artifact)) {
|
|
26
|
+
const filename = artifact.filename;
|
|
27
|
+
if (typeof filename === "string" && filename.length > 0) return filename;
|
|
28
|
+
}
|
|
29
|
+
throw new Error(`Invalid ADK artifact list response: artifact at index ${index} must be a non-empty string or an object with a non-empty string "filename".`);
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
const parseAdkArtifactResponse = (value) => {
|
|
33
|
+
if (!isRecord(value)) throw new Error("Invalid ADK artifact load response: expected an object containing \"text\" or \"inlineData\".");
|
|
34
|
+
const { text, inlineData } = value;
|
|
35
|
+
if (text === void 0 && inlineData === void 0) throw new Error("Invalid ADK artifact load response: expected an object containing \"text\" or \"inlineData\".");
|
|
36
|
+
if (text !== void 0 && typeof text !== "string") throw new Error("Invalid ADK artifact load response: \"text\" must be a string when present.");
|
|
37
|
+
if (inlineData !== void 0 && (!isRecord(inlineData) || typeof inlineData.mimeType !== "string" || typeof inlineData.data !== "string")) throw new Error("Invalid ADK artifact load response: \"inlineData\" must contain string \"mimeType\" and \"data\" fields.");
|
|
38
|
+
return value;
|
|
39
|
+
};
|
|
40
|
+
const parseAdkArtifactVersionsResponse = (value) => {
|
|
41
|
+
if (!Array.isArray(value)) throw new Error("Invalid ADK artifact versions response: expected an array of version numbers.");
|
|
42
|
+
return value.map((version, index) => {
|
|
43
|
+
if (typeof version !== "number" || !Number.isInteger(version) || version < 0) throw new Error(`Invalid ADK artifact versions response: version at index ${index} must be a non-negative integer.`);
|
|
44
|
+
return version;
|
|
45
|
+
});
|
|
46
|
+
};
|
|
4
47
|
/**
|
|
5
48
|
* Creates a `RemoteThreadListAdapter` backed by ADK's session REST API,
|
|
6
49
|
* plus a `load` function that reconstructs messages from session events.
|
|
@@ -33,7 +76,7 @@ function createAdkSessionAdapter(options) {
|
|
|
33
76
|
const headers = await getHeaders();
|
|
34
77
|
const res = await fetch(baseUrl, { headers });
|
|
35
78
|
if (!res.ok) throw new Error(`Failed to list sessions: ${res.status}`);
|
|
36
|
-
return { threads: (await res.json()).map((session) => ({
|
|
79
|
+
return { threads: parseAdkSessionListResponse(await res.json()).map((session) => ({
|
|
37
80
|
status: "regular",
|
|
38
81
|
remoteId: session.id,
|
|
39
82
|
externalId: session.id,
|
|
@@ -51,7 +94,7 @@ function createAdkSessionAdapter(options) {
|
|
|
51
94
|
body: JSON.stringify({})
|
|
52
95
|
});
|
|
53
96
|
if (!res.ok) throw new Error(`Failed to create session: ${res.status}`);
|
|
54
|
-
const session = await res.json();
|
|
97
|
+
const session = parseAdkSessionResponse(await res.json(), "create");
|
|
55
98
|
return {
|
|
56
99
|
remoteId: session.id,
|
|
57
100
|
externalId: session.id
|
|
@@ -77,7 +120,7 @@ function createAdkSessionAdapter(options) {
|
|
|
77
120
|
const headers = await getHeaders();
|
|
78
121
|
const res = await fetch(`${baseUrl}/${encodeURIComponent(threadId)}`, { headers });
|
|
79
122
|
if (!res.ok) throw new Error(`Session not found: ${res.status}`);
|
|
80
|
-
const session = await res.json();
|
|
123
|
+
const session = parseAdkSessionResponse(await res.json(), "fetch");
|
|
81
124
|
return {
|
|
82
125
|
status: "regular",
|
|
83
126
|
remoteId: session.id,
|
|
@@ -90,11 +133,13 @@ function createAdkSessionAdapter(options) {
|
|
|
90
133
|
const headers = await getHeaders();
|
|
91
134
|
const res = await fetch(`${baseUrl}/${encodeURIComponent(sessionId)}`, { headers });
|
|
92
135
|
if (!res.ok) throw new Error(`Failed to load session: ${res.status}`);
|
|
93
|
-
const session = await res.json();
|
|
94
|
-
if (!session.events
|
|
136
|
+
const session = parseAdkSessionResponse(await res.json(), "load");
|
|
137
|
+
if (session.events !== void 0 && !Array.isArray(session.events)) throw new Error("Invalid ADK session load response: expected \"events\" to be an array when present.");
|
|
138
|
+
const events = session.events;
|
|
139
|
+
if (!events?.length) return { messages: [] };
|
|
95
140
|
const accumulator = new AdkEventAccumulator();
|
|
96
141
|
let messages = [];
|
|
97
|
-
for (const event of
|
|
142
|
+
for (const event of events) messages = accumulator.processEvent(event);
|
|
98
143
|
return { messages };
|
|
99
144
|
};
|
|
100
145
|
const artifactBaseUrl = (sessionId) => `${baseUrl}/${encodeURIComponent(sessionId)}/artifacts`;
|
|
@@ -106,7 +151,7 @@ function createAdkSessionAdapter(options) {
|
|
|
106
151
|
const headers = await getHeaders();
|
|
107
152
|
const res = await fetch(artifactBaseUrl(sessionId), { headers });
|
|
108
153
|
if (!res.ok) throw new Error(`Failed to list artifacts: ${res.status}`);
|
|
109
|
-
return (await res.json())
|
|
154
|
+
return parseAdkArtifactListResponse(await res.json());
|
|
110
155
|
},
|
|
111
156
|
async load(sessionId, artifactName, version) {
|
|
112
157
|
const headers = await getHeaders();
|
|
@@ -114,14 +159,14 @@ function createAdkSessionAdapter(options) {
|
|
|
114
159
|
if (version != null) url += `/versions/${version}`;
|
|
115
160
|
const res = await fetch(url, { headers });
|
|
116
161
|
if (!res.ok) throw new Error(`Failed to load artifact: ${res.status}`);
|
|
117
|
-
return await res.json();
|
|
162
|
+
return parseAdkArtifactResponse(await res.json());
|
|
118
163
|
},
|
|
119
164
|
async listVersions(sessionId, artifactName) {
|
|
120
165
|
const headers = await getHeaders();
|
|
121
166
|
const url = `${artifactBaseUrl(sessionId)}/${encodeURIComponent(artifactName)}/versions`;
|
|
122
167
|
const res = await fetch(url, { headers });
|
|
123
168
|
if (!res.ok) throw new Error(`Failed to list artifact versions: ${res.status}`);
|
|
124
|
-
return await res.json();
|
|
169
|
+
return parseAdkArtifactVersionsResponse(await res.json());
|
|
125
170
|
},
|
|
126
171
|
async delete(sessionId, artifactName) {
|
|
127
172
|
const headers = await getHeaders();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdkSessionAdapter.js","names":[],"sources":["../src/AdkSessionAdapter.ts"],"sourcesContent":["import type { AssistantStream, AssistantStreamChunk } from \"assistant-stream\";\nimport type {\n RemoteThreadInitializeResponse,\n RemoteThreadListAdapter,\n RemoteThreadListResponse,\n RemoteThreadMetadata,\n} from \"@assistant-ui/core\";\nimport { AdkEventAccumulator } from \"./AdkEventAccumulator\";\nimport type { AdkEvent, AdkMessage } from \"./types\";\nimport { trimTrailingSlashes } from \"./trimTrailingSlashes\";\n\nexport type AdkSessionAdapterOptions = {\n /**\n * ADK server base URL (e.g. \"http://localhost:8000\").\n */\n apiUrl: string;\n\n /**\n * ADK application name.\n */\n appName: string;\n\n /**\n * ADK user ID.\n */\n userId: string;\n\n /**\n * Extra headers for API requests.\n */\n headers?:\n | Record<string, string>\n | (() => Record<string, string> | Promise<Record<string, string>>)\n | undefined;\n};\n\nexport type AdkArtifactData = {\n inlineData?: { mimeType: string; data: string } | undefined;\n text?: string | undefined;\n};\n\ntype AdkSessionAdapterResult = {\n adapter: RemoteThreadListAdapter;\n load: (sessionId: string) => Promise<{ messages: AdkMessage[] }>;\n artifacts: {\n list: (sessionId: string) => Promise<string[]>;\n load: (\n sessionId: string,\n artifactName: string,\n version?: number,\n ) => Promise<AdkArtifactData>;\n listVersions: (\n sessionId: string,\n artifactName: string,\n ) => Promise<number[]>;\n delete: (sessionId: string, artifactName: string) => Promise<void>;\n };\n};\n\n/**\n * Creates a `RemoteThreadListAdapter` backed by ADK's session REST API,\n * plus a `load` function that reconstructs messages from session events.\n *\n * @example\n * ```ts\n * const { adapter, load } = createAdkSessionAdapter({\n * apiUrl: \"http://localhost:8000\",\n * appName: \"my-app\",\n * userId: \"user-1\",\n * });\n *\n * const runtime = useAdkRuntime({\n * stream: createAdkStream({ ... }),\n * sessionAdapter: adapter,\n * load,\n * });\n * ```\n */\nexport function createAdkSessionAdapter(\n options: AdkSessionAdapterOptions,\n): AdkSessionAdapterResult {\n const { apiUrl, appName, userId } = options;\n const normalizedApiUrl = trimTrailingSlashes(apiUrl);\n const baseUrl = `${normalizedApiUrl}/apps/${encodeURIComponent(appName)}/users/${encodeURIComponent(userId)}/sessions`;\n\n const getHeaders = async (): Promise<Record<string, string>> => {\n if (!options.headers) return {};\n if (typeof options.headers === \"function\") return await options.headers();\n return options.headers;\n };\n\n const adapter: RemoteThreadListAdapter = {\n async list(): Promise<RemoteThreadListResponse> {\n const headers = await getHeaders();\n const res = await fetch(baseUrl, { headers });\n if (!res.ok) {\n throw new Error(`Failed to list sessions: ${res.status}`);\n }\n const data = (await res.json()) as Array<{\n id: string;\n app_name?: string;\n user_id?: string;\n last_update_time?: number;\n }>;\n\n const threads: RemoteThreadMetadata[] = data.map((session) => ({\n status: \"regular\" as const,\n remoteId: session.id,\n externalId: session.id,\n title: undefined,\n }));\n\n return { threads };\n },\n\n async initialize(\n _threadId: string,\n ): Promise<RemoteThreadInitializeResponse> {\n const headers = await getHeaders();\n const res = await fetch(baseUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...headers },\n body: JSON.stringify({}),\n });\n if (!res.ok) {\n throw new Error(`Failed to create session: ${res.status}`);\n }\n const session = (await res.json()) as { id: string };\n return { remoteId: session.id, externalId: session.id };\n },\n\n async delete(remoteId: string): Promise<void> {\n const headers = await getHeaders();\n const res = await fetch(`${baseUrl}/${encodeURIComponent(remoteId)}`, {\n method: \"DELETE\",\n headers,\n });\n if (!res.ok && res.status !== 404) {\n throw new Error(`Failed to delete session: ${res.status}`);\n }\n },\n\n async rename(): Promise<void> {\n // ADK sessions don't support titles\n },\n\n async archive(): Promise<void> {\n // ADK sessions don't support archiving\n },\n\n async unarchive(): Promise<void> {\n // ADK sessions don't support archiving\n },\n\n generateTitle(): Promise<AssistantStream> {\n // Title generation not supported without assistant-cloud\n return Promise.resolve(\n new ReadableStream<AssistantStreamChunk>({\n start(controller) {\n controller.close();\n },\n }),\n );\n },\n\n async fetch(threadId: string): Promise<RemoteThreadMetadata> {\n const headers = await getHeaders();\n const res = await fetch(`${baseUrl}/${encodeURIComponent(threadId)}`, {\n headers,\n });\n if (!res.ok) {\n throw new Error(`Session not found: ${res.status}`);\n }\n const session = (await res.json()) as { id: string };\n return {\n status: \"regular\",\n remoteId: session.id,\n externalId: session.id,\n title: undefined,\n };\n },\n };\n\n const load = async (\n sessionId: string,\n ): Promise<{ messages: AdkMessage[] }> => {\n const headers = await getHeaders();\n const res = await fetch(`${baseUrl}/${encodeURIComponent(sessionId)}`, {\n headers,\n });\n if (!res.ok) {\n throw new Error(`Failed to load session: ${res.status}`);\n }\n const session = (await res.json()) as {\n id: string;\n events?: AdkEvent[];\n };\n\n if (!session.events?.length) {\n return { messages: [] };\n }\n\n const accumulator = new AdkEventAccumulator();\n let messages: AdkMessage[] = [];\n for (const event of session.events) {\n messages = accumulator.processEvent(event);\n }\n return { messages };\n };\n\n const artifactBaseUrl = (sessionId: string) =>\n `${baseUrl}/${encodeURIComponent(sessionId)}/artifacts`;\n\n const artifacts: AdkSessionAdapterResult[\"artifacts\"] = {\n async list(sessionId) {\n const headers = await getHeaders();\n const res = await fetch(artifactBaseUrl(sessionId), { headers });\n if (!res.ok) throw new Error(`Failed to list artifacts: ${res.status}`);\n const data = (await res.json()) as Array<{ filename: string }>;\n return data.map((a) => a.filename);\n },\n\n async load(sessionId, artifactName, version?) {\n const headers = await getHeaders();\n let url = `${artifactBaseUrl(sessionId)}/${encodeURIComponent(artifactName)}`;\n if (version != null) url += `/versions/${version}`;\n const res = await fetch(url, { headers });\n if (!res.ok) throw new Error(`Failed to load artifact: ${res.status}`);\n return (await res.json()) as AdkArtifactData;\n },\n\n async listVersions(sessionId, artifactName) {\n const headers = await getHeaders();\n const url = `${artifactBaseUrl(sessionId)}/${encodeURIComponent(artifactName)}/versions`;\n const res = await fetch(url, { headers });\n if (!res.ok) {\n throw new Error(`Failed to list artifact versions: ${res.status}`);\n }\n return (await res.json()) as number[];\n },\n\n async delete(sessionId, artifactName) {\n const headers = await getHeaders();\n const url = `${artifactBaseUrl(sessionId)}/${encodeURIComponent(artifactName)}`;\n const res = await fetch(url, { method: \"DELETE\", headers });\n if (!res.ok && res.status !== 404) {\n throw new Error(`Failed to delete artifact: ${res.status}`);\n }\n },\n };\n\n return { adapter, load, artifacts };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AdkSessionAdapter.js","names":[],"sources":["../src/AdkSessionAdapter.ts"],"sourcesContent":["import type { AssistantStream, AssistantStreamChunk } from \"assistant-stream\";\nimport type {\n RemoteThreadInitializeResponse,\n RemoteThreadListAdapter,\n RemoteThreadListResponse,\n RemoteThreadMetadata,\n} from \"@assistant-ui/core\";\nimport { AdkEventAccumulator } from \"./AdkEventAccumulator\";\nimport type { AdkEvent, AdkMessage } from \"./types\";\nimport { trimTrailingSlashes } from \"./trimTrailingSlashes\";\n\nexport type AdkSessionAdapterOptions = {\n /**\n * ADK server base URL (e.g. \"http://localhost:8000\").\n */\n apiUrl: string;\n\n /**\n * ADK application name.\n */\n appName: string;\n\n /**\n * ADK user ID.\n */\n userId: string;\n\n /**\n * Extra headers for API requests.\n */\n headers?:\n | Record<string, string>\n | (() => Record<string, string> | Promise<Record<string, string>>)\n | undefined;\n};\n\nexport type AdkArtifactData = {\n inlineData?: { mimeType: string; data: string } | undefined;\n text?: string | undefined;\n};\n\ntype AdkSessionAdapterResult = {\n adapter: RemoteThreadListAdapter;\n load: (sessionId: string) => Promise<{ messages: AdkMessage[] }>;\n artifacts: {\n list: (sessionId: string) => Promise<string[]>;\n load: (\n sessionId: string,\n artifactName: string,\n version?: number,\n ) => Promise<AdkArtifactData>;\n listVersions: (\n sessionId: string,\n artifactName: string,\n ) => Promise<number[]>;\n delete: (sessionId: string, artifactName: string) => Promise<void>;\n };\n};\n\ntype AdkSessionResponse = {\n id: string;\n events?: unknown;\n};\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nconst isAdkSessionResponse = (value: unknown): value is AdkSessionResponse => {\n if (!isRecord(value)) return false;\n\n const id = value.id;\n return typeof id === \"string\" && id.length > 0;\n};\n\nconst parseAdkSessionResponse = (\n value: unknown,\n operation: \"create\" | \"fetch\" | \"load\",\n): AdkSessionResponse => {\n if (!isAdkSessionResponse(value)) {\n throw new Error(\n `Invalid ADK session ${operation} response: expected an object with a non-empty string \"id\".`,\n );\n }\n return value;\n};\n\nconst parseAdkSessionListResponse = (value: unknown): AdkSessionResponse[] => {\n if (!Array.isArray(value)) {\n throw new Error(\n \"Invalid ADK session list response: expected an array of sessions.\",\n );\n }\n\n return value.map((session, index) => {\n if (!isAdkSessionResponse(session)) {\n throw new Error(\n `Invalid ADK session list response: session at index ${index} must have a non-empty string \"id\".`,\n );\n }\n return session;\n });\n};\n\nconst parseAdkArtifactListResponse = (value: unknown): string[] => {\n if (!Array.isArray(value)) {\n throw new Error(\n \"Invalid ADK artifact list response: expected an array of artifact names.\",\n );\n }\n\n return value.map((artifact, index) => {\n if (typeof artifact === \"string\" && artifact.length > 0) {\n return artifact;\n }\n\n if (isRecord(artifact)) {\n const filename = artifact.filename;\n if (typeof filename === \"string\" && filename.length > 0) {\n return filename;\n }\n }\n\n throw new Error(\n `Invalid ADK artifact list response: artifact at index ${index} must be a non-empty string or an object with a non-empty string \"filename\".`,\n );\n });\n};\n\nconst parseAdkArtifactResponse = (value: unknown): AdkArtifactData => {\n if (!isRecord(value)) {\n throw new Error(\n 'Invalid ADK artifact load response: expected an object containing \"text\" or \"inlineData\".',\n );\n }\n\n const { text, inlineData } = value;\n if (text === undefined && inlineData === undefined) {\n throw new Error(\n 'Invalid ADK artifact load response: expected an object containing \"text\" or \"inlineData\".',\n );\n }\n\n if (text !== undefined && typeof text !== \"string\") {\n throw new Error(\n 'Invalid ADK artifact load response: \"text\" must be a string when present.',\n );\n }\n\n if (\n inlineData !== undefined &&\n (!isRecord(inlineData) ||\n typeof inlineData.mimeType !== \"string\" ||\n typeof inlineData.data !== \"string\")\n ) {\n throw new Error(\n 'Invalid ADK artifact load response: \"inlineData\" must contain string \"mimeType\" and \"data\" fields.',\n );\n }\n\n return value as AdkArtifactData;\n};\n\nconst parseAdkArtifactVersionsResponse = (value: unknown): number[] => {\n if (!Array.isArray(value)) {\n throw new Error(\n \"Invalid ADK artifact versions response: expected an array of version numbers.\",\n );\n }\n\n return value.map((version, index) => {\n if (\n typeof version !== \"number\" ||\n !Number.isInteger(version) ||\n version < 0\n ) {\n throw new Error(\n `Invalid ADK artifact versions response: version at index ${index} must be a non-negative integer.`,\n );\n }\n return version;\n });\n};\n\n/**\n * Creates a `RemoteThreadListAdapter` backed by ADK's session REST API,\n * plus a `load` function that reconstructs messages from session events.\n *\n * @example\n * ```ts\n * const { adapter, load } = createAdkSessionAdapter({\n * apiUrl: \"http://localhost:8000\",\n * appName: \"my-app\",\n * userId: \"user-1\",\n * });\n *\n * const runtime = useAdkRuntime({\n * stream: createAdkStream({ ... }),\n * sessionAdapter: adapter,\n * load,\n * });\n * ```\n */\nexport function createAdkSessionAdapter(\n options: AdkSessionAdapterOptions,\n): AdkSessionAdapterResult {\n const { apiUrl, appName, userId } = options;\n const normalizedApiUrl = trimTrailingSlashes(apiUrl);\n const baseUrl = `${normalizedApiUrl}/apps/${encodeURIComponent(appName)}/users/${encodeURIComponent(userId)}/sessions`;\n\n const getHeaders = async (): Promise<Record<string, string>> => {\n if (!options.headers) return {};\n if (typeof options.headers === \"function\") return await options.headers();\n return options.headers;\n };\n\n const adapter: RemoteThreadListAdapter = {\n async list(): Promise<RemoteThreadListResponse> {\n const headers = await getHeaders();\n const res = await fetch(baseUrl, { headers });\n if (!res.ok) {\n throw new Error(`Failed to list sessions: ${res.status}`);\n }\n const data = parseAdkSessionListResponse(await res.json());\n\n const threads: RemoteThreadMetadata[] = data.map((session) => ({\n status: \"regular\" as const,\n remoteId: session.id,\n externalId: session.id,\n title: undefined,\n }));\n\n return { threads };\n },\n\n async initialize(\n _threadId: string,\n ): Promise<RemoteThreadInitializeResponse> {\n const headers = await getHeaders();\n const res = await fetch(baseUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...headers },\n body: JSON.stringify({}),\n });\n if (!res.ok) {\n throw new Error(`Failed to create session: ${res.status}`);\n }\n const session = parseAdkSessionResponse(await res.json(), \"create\");\n return { remoteId: session.id, externalId: session.id };\n },\n\n async delete(remoteId: string): Promise<void> {\n const headers = await getHeaders();\n const res = await fetch(`${baseUrl}/${encodeURIComponent(remoteId)}`, {\n method: \"DELETE\",\n headers,\n });\n if (!res.ok && res.status !== 404) {\n throw new Error(`Failed to delete session: ${res.status}`);\n }\n },\n\n async rename(): Promise<void> {\n // ADK sessions don't support titles\n },\n\n async archive(): Promise<void> {\n // ADK sessions don't support archiving\n },\n\n async unarchive(): Promise<void> {\n // ADK sessions don't support archiving\n },\n\n generateTitle(): Promise<AssistantStream> {\n // Title generation not supported without assistant-cloud\n return Promise.resolve(\n new ReadableStream<AssistantStreamChunk>({\n start(controller) {\n controller.close();\n },\n }),\n );\n },\n\n async fetch(threadId: string): Promise<RemoteThreadMetadata> {\n const headers = await getHeaders();\n const res = await fetch(`${baseUrl}/${encodeURIComponent(threadId)}`, {\n headers,\n });\n if (!res.ok) {\n throw new Error(`Session not found: ${res.status}`);\n }\n const session = parseAdkSessionResponse(await res.json(), \"fetch\");\n return {\n status: \"regular\",\n remoteId: session.id,\n externalId: session.id,\n title: undefined,\n };\n },\n };\n\n const load = async (\n sessionId: string,\n ): Promise<{ messages: AdkMessage[] }> => {\n const headers = await getHeaders();\n const res = await fetch(`${baseUrl}/${encodeURIComponent(sessionId)}`, {\n headers,\n });\n if (!res.ok) {\n throw new Error(`Failed to load session: ${res.status}`);\n }\n const session = parseAdkSessionResponse(await res.json(), \"load\");\n\n if (session.events !== undefined && !Array.isArray(session.events)) {\n throw new Error(\n 'Invalid ADK session load response: expected \"events\" to be an array when present.',\n );\n }\n\n const events = session.events as AdkEvent[] | undefined;\n\n if (!events?.length) {\n return { messages: [] };\n }\n\n const accumulator = new AdkEventAccumulator();\n let messages: AdkMessage[] = [];\n for (const event of events) {\n messages = accumulator.processEvent(event);\n }\n return { messages };\n };\n\n const artifactBaseUrl = (sessionId: string) =>\n `${baseUrl}/${encodeURIComponent(sessionId)}/artifacts`;\n\n const artifacts: AdkSessionAdapterResult[\"artifacts\"] = {\n async list(sessionId) {\n const headers = await getHeaders();\n const res = await fetch(artifactBaseUrl(sessionId), { headers });\n if (!res.ok) throw new Error(`Failed to list artifacts: ${res.status}`);\n return parseAdkArtifactListResponse(await res.json());\n },\n\n async load(sessionId, artifactName, version?) {\n const headers = await getHeaders();\n let url = `${artifactBaseUrl(sessionId)}/${encodeURIComponent(artifactName)}`;\n if (version != null) url += `/versions/${version}`;\n const res = await fetch(url, { headers });\n if (!res.ok) throw new Error(`Failed to load artifact: ${res.status}`);\n return parseAdkArtifactResponse(await res.json());\n },\n\n async listVersions(sessionId, artifactName) {\n const headers = await getHeaders();\n const url = `${artifactBaseUrl(sessionId)}/${encodeURIComponent(artifactName)}/versions`;\n const res = await fetch(url, { headers });\n if (!res.ok) {\n throw new Error(`Failed to list artifact versions: ${res.status}`);\n }\n return parseAdkArtifactVersionsResponse(await res.json());\n },\n\n async delete(sessionId, artifactName) {\n const headers = await getHeaders();\n const url = `${artifactBaseUrl(sessionId)}/${encodeURIComponent(artifactName)}`;\n const res = await fetch(url, { method: \"DELETE\", headers });\n if (!res.ok && res.status !== 404) {\n throw new Error(`Failed to delete artifact: ${res.status}`);\n }\n },\n };\n\n return { adapter, load, artifacts };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAdkMessages.d.ts","names":[],"sources":["../src/useAdkMessages.ts"],"mappings":";;KAkBY;EACV,QAAQ;EACR;IACE,UAAU;IACV,gBAAgB;IAChB,kBAAkB;;;cAIT,mBAAkB,QAAA,iBAG5B
|
|
1
|
+
{"version":3,"file":"useAdkMessages.d.ts","names":[],"sources":["../src/useAdkMessages.ts"],"mappings":";;KAkBY;EACV,QAAQ;EACR;IACE,UAAU;IACV,gBAAgB;IAChB,kBAAkB;;;cAIT,mBAAkB,QAAA,iBAG5B;;;;IAIQ;IACE;;;;;;;;EAsDW,cAAA,aAAA,cAAY,QAAU,yBAAoB;;EA9BhB,cAAA,MAAA;EAUvC,kBAAA,MAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assistant-ui/react-google-adk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.18",
|
|
4
4
|
"description": "Google ADK adapter for assistant-ui",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"google-adk",
|
|
@@ -34,10 +34,10 @@
|
|
|
34
34
|
],
|
|
35
35
|
"sideEffects": false,
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@assistant-ui/core": "^0.2.
|
|
38
|
-
"@assistant-ui/store": "^0.2.
|
|
37
|
+
"@assistant-ui/core": "^0.2.22",
|
|
38
|
+
"@assistant-ui/store": "^0.2.21",
|
|
39
39
|
"assistant-cloud": "*",
|
|
40
|
-
"assistant-stream": "^0.3.
|
|
40
|
+
"assistant-stream": "^0.3.27",
|
|
41
41
|
"uuid": "^14.0.1"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
@@ -56,9 +56,9 @@
|
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@types/react": "^19.2.17",
|
|
58
58
|
"@types/uuid": "^11.0.0",
|
|
59
|
-
"react": "^19.2.
|
|
59
|
+
"react": "^19.2.8",
|
|
60
60
|
"vitest": "^4.1.10",
|
|
61
|
-
"@assistant-ui/x-buildutils": "0.0.
|
|
61
|
+
"@assistant-ui/x-buildutils": "0.0.19"
|
|
62
62
|
},
|
|
63
63
|
"publishConfig": {
|
|
64
64
|
"access": "public",
|
package/src/AdkClient.test.ts
CHANGED
|
@@ -34,6 +34,17 @@ const sseBody = (text: string): ReadableStream<Uint8Array> => {
|
|
|
34
34
|
});
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
+
const sseResponse = (
|
|
38
|
+
body: BodyInit | null,
|
|
39
|
+
contentType: string | null = "text/event-stream",
|
|
40
|
+
): Response =>
|
|
41
|
+
new Response(body, {
|
|
42
|
+
status: 200,
|
|
43
|
+
...(contentType === null
|
|
44
|
+
? {}
|
|
45
|
+
: { headers: { "Content-Type": contentType } }),
|
|
46
|
+
});
|
|
47
|
+
|
|
37
48
|
const nextWithTimeout = async <T>(
|
|
38
49
|
promise: Promise<IteratorResult<T>>,
|
|
39
50
|
): Promise<IteratorResult<T> | "timeout"> => {
|
|
@@ -57,7 +68,7 @@ beforeEach(() => {
|
|
|
57
68
|
|
|
58
69
|
describe("createAdkStream - proxy mode", () => {
|
|
59
70
|
it("POSTs to the api URL directly", async () => {
|
|
60
|
-
mockFetch.mockResolvedValueOnce(
|
|
71
|
+
mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
|
|
61
72
|
|
|
62
73
|
const stream = createAdkStream({ api: "/api/adk" });
|
|
63
74
|
const messages: AdkMessage[] = [
|
|
@@ -78,7 +89,7 @@ describe("createAdkStream - proxy mode", () => {
|
|
|
78
89
|
});
|
|
79
90
|
|
|
80
91
|
it("sends runConfig and checkpointId in proxy body", async () => {
|
|
81
|
-
mockFetch.mockResolvedValueOnce(
|
|
92
|
+
mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
|
|
82
93
|
|
|
83
94
|
const stream = createAdkStream({ api: "/api/adk" });
|
|
84
95
|
const gen = await stream(
|
|
@@ -95,7 +106,7 @@ describe("createAdkStream - proxy mode", () => {
|
|
|
95
106
|
});
|
|
96
107
|
|
|
97
108
|
it("sends a tool-result body when message type is tool", async () => {
|
|
98
|
-
mockFetch.mockResolvedValueOnce(
|
|
109
|
+
mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
|
|
99
110
|
|
|
100
111
|
const stream = createAdkStream({ api: "/api/adk" });
|
|
101
112
|
const messages: AdkMessage[] = [
|
|
@@ -122,7 +133,7 @@ describe("createAdkStream - proxy mode", () => {
|
|
|
122
133
|
});
|
|
123
134
|
|
|
124
135
|
it("sends parts when message has multimodal content", async () => {
|
|
125
|
-
mockFetch.mockResolvedValueOnce(
|
|
136
|
+
mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
|
|
126
137
|
|
|
127
138
|
const stream = createAdkStream({ api: "/api/adk" });
|
|
128
139
|
const messages: AdkMessage[] = [
|
|
@@ -149,7 +160,7 @@ describe("createAdkStream - proxy mode", () => {
|
|
|
149
160
|
});
|
|
150
161
|
|
|
151
162
|
it("sends file parts as inlineData in proxy mode", async () => {
|
|
152
|
-
mockFetch.mockResolvedValueOnce(
|
|
163
|
+
mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
|
|
153
164
|
|
|
154
165
|
const stream = createAdkStream({ api: "/api/adk" });
|
|
155
166
|
const messages: AdkMessage[] = [
|
|
@@ -180,7 +191,7 @@ describe("createAdkStream - proxy mode", () => {
|
|
|
180
191
|
});
|
|
181
192
|
|
|
182
193
|
it("sends parts array when multiple messages are provided", async () => {
|
|
183
|
-
mockFetch.mockResolvedValueOnce(
|
|
194
|
+
mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
|
|
184
195
|
|
|
185
196
|
const stream = createAdkStream({ api: "/api/adk" });
|
|
186
197
|
const messages: AdkMessage[] = [
|
|
@@ -205,7 +216,7 @@ describe("createAdkStream - proxy mode", () => {
|
|
|
205
216
|
});
|
|
206
217
|
|
|
207
218
|
it("marks isError=true when tool status is error", async () => {
|
|
208
|
-
mockFetch.mockResolvedValueOnce(
|
|
219
|
+
mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
|
|
209
220
|
|
|
210
221
|
const stream = createAdkStream({ api: "/api/adk" });
|
|
211
222
|
const messages: AdkMessage[] = [
|
|
@@ -231,8 +242,35 @@ describe("createAdkStream - proxy mode", () => {
|
|
|
231
242
|
// ── Direct mode ──
|
|
232
243
|
|
|
233
244
|
describe("createAdkStream - direct mode", () => {
|
|
245
|
+
it("rejects direct mode with an empty appName", () => {
|
|
246
|
+
expect(() =>
|
|
247
|
+
createAdkStream({
|
|
248
|
+
api: "http://localhost:8000",
|
|
249
|
+
appName: "",
|
|
250
|
+
userId: "user-1",
|
|
251
|
+
}),
|
|
252
|
+
).toThrow('createAdkStream direct mode requires a non-empty "appName".');
|
|
253
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it.each([
|
|
257
|
+
["missing", undefined],
|
|
258
|
+
["empty", ""],
|
|
259
|
+
])("rejects direct mode with a %s userId", (_label, userId) => {
|
|
260
|
+
expect(() =>
|
|
261
|
+
createAdkStream({
|
|
262
|
+
api: "http://localhost:8000",
|
|
263
|
+
appName: "my-app",
|
|
264
|
+
userId,
|
|
265
|
+
}),
|
|
266
|
+
).toThrow(
|
|
267
|
+
'createAdkStream direct mode requires "userId" when "appName" is provided.',
|
|
268
|
+
);
|
|
269
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
270
|
+
});
|
|
271
|
+
|
|
234
272
|
it("POSTs to /run_sse with ADK-native body", async () => {
|
|
235
|
-
mockFetch.mockResolvedValueOnce(
|
|
273
|
+
mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
|
|
236
274
|
|
|
237
275
|
const stream = createAdkStream({
|
|
238
276
|
api: "http://localhost:8000",
|
|
@@ -264,9 +302,7 @@ describe("createAdkStream - direct mode", () => {
|
|
|
264
302
|
it.each(["http://localhost:8000/", "http://localhost:8000//"])(
|
|
265
303
|
"normalizes trailing slashes in the api URL: %s",
|
|
266
304
|
async (api) => {
|
|
267
|
-
mockFetch.mockResolvedValueOnce(
|
|
268
|
-
new Response(sseBody(""), { status: 200 }),
|
|
269
|
-
);
|
|
305
|
+
mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
|
|
270
306
|
|
|
271
307
|
const stream = createAdkStream({
|
|
272
308
|
api,
|
|
@@ -289,7 +325,7 @@ describe("createAdkStream - direct mode", () => {
|
|
|
289
325
|
const initialize = vi
|
|
290
326
|
.fn()
|
|
291
327
|
.mockResolvedValue({ remoteId: "r1", externalId: "s-42" });
|
|
292
|
-
mockFetch.mockResolvedValueOnce(
|
|
328
|
+
mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
|
|
293
329
|
|
|
294
330
|
const stream = createAdkStream({
|
|
295
331
|
api: "http://localhost:8000",
|
|
@@ -310,7 +346,7 @@ describe("createAdkStream - direct mode", () => {
|
|
|
310
346
|
});
|
|
311
347
|
|
|
312
348
|
it("converts tool messages to functionResponse parts", async () => {
|
|
313
|
-
mockFetch.mockResolvedValueOnce(
|
|
349
|
+
mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
|
|
314
350
|
|
|
315
351
|
const stream = createAdkStream({
|
|
316
352
|
api: "http://localhost:8000",
|
|
@@ -342,7 +378,7 @@ describe("createAdkStream - direct mode", () => {
|
|
|
342
378
|
});
|
|
343
379
|
|
|
344
380
|
it("falls back to raw string when tool content is not valid JSON", async () => {
|
|
345
|
-
mockFetch.mockResolvedValueOnce(
|
|
381
|
+
mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
|
|
346
382
|
|
|
347
383
|
const stream = createAdkStream({
|
|
348
384
|
api: "http://localhost:8000",
|
|
@@ -368,7 +404,7 @@ describe("createAdkStream - direct mode", () => {
|
|
|
368
404
|
});
|
|
369
405
|
|
|
370
406
|
it("sends empty text part when no messages provided", async () => {
|
|
371
|
-
mockFetch.mockResolvedValueOnce(
|
|
407
|
+
mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
|
|
372
408
|
|
|
373
409
|
const stream = createAdkStream({
|
|
374
410
|
api: "http://localhost:8000",
|
|
@@ -394,9 +430,7 @@ describe("createAdkStream - SSE parsing", () => {
|
|
|
394
430
|
{ id: "e2", content: { parts: [{ text: "world" }] } },
|
|
395
431
|
];
|
|
396
432
|
const text = events.map((e) => `data: ${JSON.stringify(e)}\n\n`).join("");
|
|
397
|
-
mockFetch.mockResolvedValueOnce(
|
|
398
|
-
new Response(sseBody(text), { status: 200 }),
|
|
399
|
-
);
|
|
433
|
+
mockFetch.mockResolvedValueOnce(sseResponse(sseBody(text)));
|
|
400
434
|
|
|
401
435
|
const stream = createAdkStream({ api: "/api/adk" });
|
|
402
436
|
const gen = await stream(
|
|
@@ -413,12 +447,30 @@ describe("createAdkStream - SSE parsing", () => {
|
|
|
413
447
|
expect(collected[1]!.id).toBe("e2");
|
|
414
448
|
});
|
|
415
449
|
|
|
416
|
-
it("
|
|
417
|
-
const
|
|
450
|
+
it("accepts parameterized event-stream content types", async () => {
|
|
451
|
+
const event: AdkEvent = { id: "e1" };
|
|
418
452
|
mockFetch.mockResolvedValueOnce(
|
|
419
|
-
|
|
453
|
+
sseResponse(
|
|
454
|
+
sseBody(`data: ${JSON.stringify(event)}\n\n`),
|
|
455
|
+
"Text/Event-Stream; charset=utf-8",
|
|
456
|
+
),
|
|
420
457
|
);
|
|
421
458
|
|
|
459
|
+
const stream = createAdkStream({ api: "/api/adk" });
|
|
460
|
+
const gen = await stream(
|
|
461
|
+
[{ id: "m1", type: "human", content: "Hi" }],
|
|
462
|
+
makeConfig(),
|
|
463
|
+
);
|
|
464
|
+
const collected: AdkEvent[] = [];
|
|
465
|
+
for await (const value of gen) collected.push(value);
|
|
466
|
+
|
|
467
|
+
expect(collected).toEqual([event]);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it("skips :ok SSE comments", async () => {
|
|
471
|
+
const text = `:ok\n\ndata: ${JSON.stringify({ id: "e1" })}\n\n`;
|
|
472
|
+
mockFetch.mockResolvedValueOnce(sseResponse(sseBody(text)));
|
|
473
|
+
|
|
422
474
|
const stream = createAdkStream({ api: "/api/adk" });
|
|
423
475
|
const gen = await stream(
|
|
424
476
|
[{ id: "m1", type: "human", content: "Hi" }],
|
|
@@ -435,9 +487,7 @@ describe("createAdkStream - SSE parsing", () => {
|
|
|
435
487
|
|
|
436
488
|
it("parses data fields without a space after the colon", async () => {
|
|
437
489
|
const text = `data:${JSON.stringify({ id: "e1" })}\n\n`;
|
|
438
|
-
mockFetch.mockResolvedValueOnce(
|
|
439
|
-
new Response(sseBody(text), { status: 200 }),
|
|
440
|
-
);
|
|
490
|
+
mockFetch.mockResolvedValueOnce(sseResponse(sseBody(text)));
|
|
441
491
|
|
|
442
492
|
const stream = createAdkStream({ api: "/api/adk" });
|
|
443
493
|
const gen = await stream(
|
|
@@ -456,9 +506,7 @@ describe("createAdkStream - SSE parsing", () => {
|
|
|
456
506
|
it("parses CR-delimited SSE events", async () => {
|
|
457
507
|
const events: AdkEvent[] = [{ id: "e1" }, { id: "e2" }];
|
|
458
508
|
const text = events.map((e) => `data: ${JSON.stringify(e)}\r\r`).join("");
|
|
459
|
-
mockFetch.mockResolvedValueOnce(
|
|
460
|
-
new Response(sseBody(text), { status: 200 }),
|
|
461
|
-
);
|
|
509
|
+
mockFetch.mockResolvedValueOnce(sseResponse(sseBody(text)));
|
|
462
510
|
|
|
463
511
|
const stream = createAdkStream({ api: "/api/adk" });
|
|
464
512
|
const gen = await stream(
|
|
@@ -483,7 +531,7 @@ describe("createAdkStream - SSE parsing", () => {
|
|
|
483
531
|
controller = nextController;
|
|
484
532
|
},
|
|
485
533
|
});
|
|
486
|
-
mockFetch.mockResolvedValueOnce(
|
|
534
|
+
mockFetch.mockResolvedValueOnce(sseResponse(body));
|
|
487
535
|
|
|
488
536
|
const stream = createAdkStream({ api: "/api/adk" });
|
|
489
537
|
const gen = await stream(
|
|
@@ -520,7 +568,7 @@ describe("createAdkStream - SSE parsing", () => {
|
|
|
520
568
|
controller.close();
|
|
521
569
|
},
|
|
522
570
|
});
|
|
523
|
-
mockFetch.mockResolvedValueOnce(
|
|
571
|
+
mockFetch.mockResolvedValueOnce(sseResponse(body));
|
|
524
572
|
|
|
525
573
|
const stream = createAdkStream({ api: "/api/adk" });
|
|
526
574
|
const gen = await stream(
|
|
@@ -547,7 +595,7 @@ describe("createAdkStream - SSE parsing", () => {
|
|
|
547
595
|
controller.close();
|
|
548
596
|
},
|
|
549
597
|
});
|
|
550
|
-
mockFetch.mockResolvedValueOnce(
|
|
598
|
+
mockFetch.mockResolvedValueOnce(sseResponse(body));
|
|
551
599
|
|
|
552
600
|
const stream = createAdkStream({ api: "/api/adk" });
|
|
553
601
|
const gen = await stream(
|
|
@@ -567,9 +615,7 @@ describe("createAdkStream - SSE parsing", () => {
|
|
|
567
615
|
// No trailing \n\n
|
|
568
616
|
const event = { id: "e1" };
|
|
569
617
|
const text = `data: ${JSON.stringify(event)}\n`;
|
|
570
|
-
mockFetch.mockResolvedValueOnce(
|
|
571
|
-
new Response(sseBody(text), { status: 200 }),
|
|
572
|
-
);
|
|
618
|
+
mockFetch.mockResolvedValueOnce(sseResponse(sseBody(text)));
|
|
573
619
|
|
|
574
620
|
const stream = createAdkStream({ api: "/api/adk" });
|
|
575
621
|
const gen = await stream(
|
|
@@ -605,13 +651,57 @@ describe("createAdkStream - error handling", () => {
|
|
|
605
651
|
}
|
|
606
652
|
}).rejects.toThrow("ADK request failed: 404 Not Found");
|
|
607
653
|
});
|
|
654
|
+
|
|
655
|
+
it("rejects successful HTML responses and cancels their body", async () => {
|
|
656
|
+
const cancelBody = vi.fn();
|
|
657
|
+
const body = new ReadableStream<Uint8Array>({
|
|
658
|
+
start(controller) {
|
|
659
|
+
controller.enqueue(new TextEncoder().encode("<html>Sign in</html>"));
|
|
660
|
+
},
|
|
661
|
+
cancel: cancelBody,
|
|
662
|
+
});
|
|
663
|
+
mockFetch.mockResolvedValueOnce(
|
|
664
|
+
sseResponse(body, "text/html; charset=utf-8"),
|
|
665
|
+
);
|
|
666
|
+
|
|
667
|
+
const stream = createAdkStream({ api: "/api/adk" });
|
|
668
|
+
await expect(async () => {
|
|
669
|
+
const gen = await stream(
|
|
670
|
+
[{ id: "m1", type: "human", content: "Hi" }],
|
|
671
|
+
makeConfig(),
|
|
672
|
+
);
|
|
673
|
+
for await (const _ of gen) {
|
|
674
|
+
/* noop */
|
|
675
|
+
}
|
|
676
|
+
}).rejects.toThrow(
|
|
677
|
+
'Expected ADK stream response Content-Type "text/event-stream", received "text/html; charset=utf-8"',
|
|
678
|
+
);
|
|
679
|
+
expect(cancelBody).toHaveBeenCalledOnce();
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
it("rejects successful responses without a content type", async () => {
|
|
683
|
+
mockFetch.mockResolvedValueOnce(sseResponse(sseBody(""), null));
|
|
684
|
+
|
|
685
|
+
const stream = createAdkStream({ api: "/api/adk" });
|
|
686
|
+
await expect(async () => {
|
|
687
|
+
const gen = await stream(
|
|
688
|
+
[{ id: "m1", type: "human", content: "Hi" }],
|
|
689
|
+
makeConfig(),
|
|
690
|
+
);
|
|
691
|
+
for await (const _ of gen) {
|
|
692
|
+
/* noop */
|
|
693
|
+
}
|
|
694
|
+
}).rejects.toThrow(
|
|
695
|
+
'Expected ADK stream response Content-Type "text/event-stream", received no Content-Type header',
|
|
696
|
+
);
|
|
697
|
+
});
|
|
608
698
|
});
|
|
609
699
|
|
|
610
700
|
// ── Headers ──
|
|
611
701
|
|
|
612
702
|
describe("createAdkStream - headers", () => {
|
|
613
703
|
it("sends static headers", async () => {
|
|
614
|
-
mockFetch.mockResolvedValueOnce(
|
|
704
|
+
mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
|
|
615
705
|
|
|
616
706
|
const stream = createAdkStream({
|
|
617
707
|
api: "/api/adk",
|
|
@@ -634,7 +724,7 @@ describe("createAdkStream - headers", () => {
|
|
|
634
724
|
});
|
|
635
725
|
|
|
636
726
|
it("resolves dynamic headers from a function", async () => {
|
|
637
|
-
mockFetch.mockResolvedValueOnce(
|
|
727
|
+
mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
|
|
638
728
|
|
|
639
729
|
const stream = createAdkStream({
|
|
640
730
|
api: "/api/adk",
|
|
@@ -656,7 +746,7 @@ describe("createAdkStream - headers", () => {
|
|
|
656
746
|
});
|
|
657
747
|
|
|
658
748
|
it("resolves async dynamic headers", async () => {
|
|
659
|
-
mockFetch.mockResolvedValueOnce(
|
|
749
|
+
mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
|
|
660
750
|
|
|
661
751
|
const stream = createAdkStream({
|
|
662
752
|
api: "/api/adk",
|
|
@@ -678,7 +768,7 @@ describe("createAdkStream - headers", () => {
|
|
|
678
768
|
});
|
|
679
769
|
|
|
680
770
|
it("sends no extra headers when headers option is undefined", async () => {
|
|
681
|
-
mockFetch.mockResolvedValueOnce(
|
|
771
|
+
mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
|
|
682
772
|
|
|
683
773
|
const stream = createAdkStream({ api: "/api/adk" });
|
|
684
774
|
const gen = await stream(
|
|
@@ -701,7 +791,7 @@ describe("createAdkStream - headers", () => {
|
|
|
701
791
|
|
|
702
792
|
describe("createAdkStream - AbortSignal", () => {
|
|
703
793
|
it("forwards the AbortSignal to fetch", async () => {
|
|
704
|
-
mockFetch.mockResolvedValueOnce(
|
|
794
|
+
mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
|
|
705
795
|
|
|
706
796
|
const controller = new AbortController();
|
|
707
797
|
const stream = createAdkStream({ api: "/api/adk" });
|
|
@@ -721,7 +811,7 @@ describe("createAdkStream - AbortSignal", () => {
|
|
|
721
811
|
|
|
722
812
|
describe("createAdkStream - content conversion", () => {
|
|
723
813
|
it("converts reasoning content parts to thought parts in direct mode", async () => {
|
|
724
|
-
mockFetch.mockResolvedValueOnce(
|
|
814
|
+
mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
|
|
725
815
|
|
|
726
816
|
const stream = createAdkStream({
|
|
727
817
|
api: "http://localhost:8000",
|
|
@@ -748,7 +838,7 @@ describe("createAdkStream - content conversion", () => {
|
|
|
748
838
|
});
|
|
749
839
|
|
|
750
840
|
it("converts image_url content parts to fileData in direct mode", async () => {
|
|
751
|
-
mockFetch.mockResolvedValueOnce(
|
|
841
|
+
mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
|
|
752
842
|
|
|
753
843
|
const stream = createAdkStream({
|
|
754
844
|
api: "http://localhost:8000",
|
|
@@ -774,7 +864,7 @@ describe("createAdkStream - content conversion", () => {
|
|
|
774
864
|
});
|
|
775
865
|
|
|
776
866
|
it("converts file content parts to inlineData in direct mode", async () => {
|
|
777
|
-
mockFetch.mockResolvedValueOnce(
|
|
867
|
+
mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
|
|
778
868
|
|
|
779
869
|
const stream = createAdkStream({
|
|
780
870
|
api: "http://localhost:8000",
|
|
@@ -807,7 +897,7 @@ describe("createAdkStream - content conversion", () => {
|
|
|
807
897
|
});
|
|
808
898
|
|
|
809
899
|
it("converts file_url content parts to fileData with mimeType in direct mode", async () => {
|
|
810
|
-
mockFetch.mockResolvedValueOnce(
|
|
900
|
+
mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
|
|
811
901
|
|
|
812
902
|
const stream = createAdkStream({
|
|
813
903
|
api: "http://localhost:8000",
|
|
@@ -842,7 +932,7 @@ describe("createAdkStream - content conversion", () => {
|
|
|
842
932
|
});
|
|
843
933
|
|
|
844
934
|
it("converts code content parts to executableCode", async () => {
|
|
845
|
-
mockFetch.mockResolvedValueOnce(
|
|
935
|
+
mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
|
|
846
936
|
|
|
847
937
|
const stream = createAdkStream({
|
|
848
938
|
api: "http://localhost:8000",
|
|
@@ -868,7 +958,7 @@ describe("createAdkStream - content conversion", () => {
|
|
|
868
958
|
});
|
|
869
959
|
|
|
870
960
|
it("converts code_result content parts to codeExecutionResult", async () => {
|
|
871
|
-
mockFetch.mockResolvedValueOnce(
|
|
961
|
+
mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
|
|
872
962
|
|
|
873
963
|
const stream = createAdkStream({
|
|
874
964
|
api: "http://localhost:8000",
|
package/src/AdkClient.ts
CHANGED
|
@@ -60,7 +60,18 @@ export type CreateAdkStreamOptions = {
|
|
|
60
60
|
export function createAdkStream(
|
|
61
61
|
options: CreateAdkStreamOptions,
|
|
62
62
|
): AdkStreamCallback {
|
|
63
|
+
if (options.appName === "") {
|
|
64
|
+
throw new Error(
|
|
65
|
+
'createAdkStream direct mode requires a non-empty "appName".',
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
63
69
|
const isDirect = options.appName != null;
|
|
70
|
+
if (isDirect && (options.userId == null || options.userId === "")) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
'createAdkStream direct mode requires "userId" when "appName" is provided.',
|
|
73
|
+
);
|
|
74
|
+
}
|
|
64
75
|
|
|
65
76
|
return async function* (messages, config) {
|
|
66
77
|
const headers = await resolveHeaders(options.headers);
|
|
@@ -99,12 +110,27 @@ export function createAdkStream(
|
|
|
99
110
|
);
|
|
100
111
|
}
|
|
101
112
|
|
|
113
|
+
validateEventStreamContentType(response);
|
|
102
114
|
yield* parseSSEResponse(response);
|
|
103
115
|
};
|
|
104
116
|
}
|
|
105
117
|
|
|
106
118
|
// ── Internal helpers ──
|
|
107
119
|
|
|
120
|
+
function validateEventStreamContentType(response: Response): void {
|
|
121
|
+
const contentType = response.headers.get("Content-Type");
|
|
122
|
+
const mediaType = contentType?.split(";", 1)[0]?.trim().toLowerCase();
|
|
123
|
+
if (mediaType !== "text/event-stream") {
|
|
124
|
+
const received = contentType
|
|
125
|
+
? `"${contentType}"`
|
|
126
|
+
: "no Content-Type header";
|
|
127
|
+
void response.body?.cancel().catch(() => undefined);
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Expected ADK stream response Content-Type "text/event-stream", received ${received}`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
108
134
|
async function resolveHeaders(
|
|
109
135
|
headers:
|
|
110
136
|
| Record<string, string>
|
|
@@ -62,6 +62,30 @@ describe("createAdkSessionAdapter - list", () => {
|
|
|
62
62
|
expect(result.threads).toHaveLength(0);
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
+
it("rejects a successful response that is not a session array", async () => {
|
|
66
|
+
mockFetch.mockResolvedValueOnce(
|
|
67
|
+
new Response(JSON.stringify({}), { status: 200 }),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const { adapter } = createAdkSessionAdapter(baseOptions);
|
|
71
|
+
|
|
72
|
+
await expect(adapter.list()).rejects.toThrow(
|
|
73
|
+
"Invalid ADK session list response: expected an array of sessions.",
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("rejects session list entries without a valid id", async () => {
|
|
78
|
+
mockFetch.mockResolvedValueOnce(
|
|
79
|
+
new Response(JSON.stringify([{ id: "s1" }, {}]), { status: 200 }),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const { adapter } = createAdkSessionAdapter(baseOptions);
|
|
83
|
+
|
|
84
|
+
await expect(adapter.list()).rejects.toThrow(
|
|
85
|
+
'Invalid ADK session list response: session at index 1 must have a non-empty string "id".',
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
65
89
|
it("throws when response is not ok", async () => {
|
|
66
90
|
mockFetch.mockResolvedValueOnce(
|
|
67
91
|
new Response("Server error", { status: 500 }),
|
|
@@ -139,6 +163,18 @@ describe("createAdkSessionAdapter - initialize", () => {
|
|
|
139
163
|
"Failed to create session: 403",
|
|
140
164
|
);
|
|
141
165
|
});
|
|
166
|
+
|
|
167
|
+
it("rejects a successful response without a session id", async () => {
|
|
168
|
+
mockFetch.mockResolvedValueOnce(
|
|
169
|
+
new Response(JSON.stringify({}), { status: 200 }),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const { adapter } = createAdkSessionAdapter(baseOptions);
|
|
173
|
+
|
|
174
|
+
await expect(adapter.initialize("thread-1")).rejects.toThrow(
|
|
175
|
+
'Invalid ADK session create response: expected an object with a non-empty string "id".',
|
|
176
|
+
);
|
|
177
|
+
});
|
|
142
178
|
});
|
|
143
179
|
|
|
144
180
|
// ── adapter.delete() ──
|
|
@@ -250,6 +286,18 @@ describe("createAdkSessionAdapter - fetch", () => {
|
|
|
250
286
|
"Session not found: 404",
|
|
251
287
|
);
|
|
252
288
|
});
|
|
289
|
+
|
|
290
|
+
it("rejects a successful response without a session id", async () => {
|
|
291
|
+
mockFetch.mockResolvedValueOnce(
|
|
292
|
+
new Response(JSON.stringify({}), { status: 200 }),
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
const { adapter } = createAdkSessionAdapter(baseOptions);
|
|
296
|
+
|
|
297
|
+
await expect(adapter.fetch("s1")).rejects.toThrow(
|
|
298
|
+
'Invalid ADK session fetch response: expected an object with a non-empty string "id".',
|
|
299
|
+
);
|
|
300
|
+
});
|
|
253
301
|
});
|
|
254
302
|
|
|
255
303
|
// ── load() ──
|
|
@@ -305,6 +353,30 @@ describe("createAdkSessionAdapter - load", () => {
|
|
|
305
353
|
expect(result.messages).toEqual([]);
|
|
306
354
|
});
|
|
307
355
|
|
|
356
|
+
it("rejects a successful response without a session id", async () => {
|
|
357
|
+
mockFetch.mockResolvedValueOnce(
|
|
358
|
+
new Response(JSON.stringify({}), { status: 200 }),
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
const { load } = createAdkSessionAdapter(baseOptions);
|
|
362
|
+
|
|
363
|
+
await expect(load("s1")).rejects.toThrow(
|
|
364
|
+
'Invalid ADK session load response: expected an object with a non-empty string "id".',
|
|
365
|
+
);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it("rejects a session with a malformed events field", async () => {
|
|
369
|
+
mockFetch.mockResolvedValueOnce(
|
|
370
|
+
new Response(JSON.stringify({ id: "s1", events: {} }), { status: 200 }),
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
const { load } = createAdkSessionAdapter(baseOptions);
|
|
374
|
+
|
|
375
|
+
await expect(load("s1")).rejects.toThrow(
|
|
376
|
+
'Invalid ADK session load response: expected "events" to be an array when present.',
|
|
377
|
+
);
|
|
378
|
+
});
|
|
379
|
+
|
|
308
380
|
it("throws when session fetch fails", async () => {
|
|
309
381
|
mockFetch.mockResolvedValueOnce(
|
|
310
382
|
new Response("Server error", { status: 500 }),
|
|
@@ -343,6 +415,133 @@ describe("createAdkSessionAdapter - load", () => {
|
|
|
343
415
|
});
|
|
344
416
|
});
|
|
345
417
|
|
|
418
|
+
describe("createAdkSessionAdapter - artifacts", () => {
|
|
419
|
+
it("lists names from current and legacy artifact responses", async () => {
|
|
420
|
+
mockFetch.mockResolvedValueOnce(
|
|
421
|
+
new Response(JSON.stringify(["report.pdf", { filename: "chart.png" }]), {
|
|
422
|
+
status: 200,
|
|
423
|
+
}),
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
const { artifacts } = createAdkSessionAdapter(baseOptions);
|
|
427
|
+
|
|
428
|
+
await expect(artifacts.list("s1")).resolves.toEqual([
|
|
429
|
+
"report.pdf",
|
|
430
|
+
"chart.png",
|
|
431
|
+
]);
|
|
432
|
+
expect(mockFetch.mock.calls[0]![0]).toBe(`${expectedBaseUrl}/s1/artifacts`);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it("rejects an artifact list that is not an array", async () => {
|
|
436
|
+
mockFetch.mockResolvedValueOnce(
|
|
437
|
+
new Response(JSON.stringify({}), { status: 200 }),
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
const { artifacts } = createAdkSessionAdapter(baseOptions);
|
|
441
|
+
|
|
442
|
+
await expect(artifacts.list("s1")).rejects.toThrow(
|
|
443
|
+
"Invalid ADK artifact list response: expected an array of artifact names.",
|
|
444
|
+
);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it("rejects malformed artifact list entries", async () => {
|
|
448
|
+
mockFetch.mockResolvedValueOnce(
|
|
449
|
+
new Response(JSON.stringify(["report.pdf", {}]), { status: 200 }),
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
const { artifacts } = createAdkSessionAdapter(baseOptions);
|
|
453
|
+
|
|
454
|
+
await expect(artifacts.list("s1")).rejects.toThrow(
|
|
455
|
+
'Invalid ADK artifact list response: artifact at index 1 must be a non-empty string or an object with a non-empty string "filename".',
|
|
456
|
+
);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it.each([
|
|
460
|
+
["text", { text: "artifact contents" }],
|
|
461
|
+
[
|
|
462
|
+
"inline data",
|
|
463
|
+
{ inlineData: { mimeType: "image/png", data: "aGVsbG8=" } },
|
|
464
|
+
],
|
|
465
|
+
])("loads valid %s artifacts", async (_label, artifact) => {
|
|
466
|
+
mockFetch.mockResolvedValueOnce(
|
|
467
|
+
new Response(JSON.stringify(artifact), { status: 200 }),
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
const { artifacts } = createAdkSessionAdapter(baseOptions);
|
|
471
|
+
|
|
472
|
+
await expect(artifacts.load("s1", "report.pdf")).resolves.toEqual(artifact);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it("rejects an artifact without supported content", async () => {
|
|
476
|
+
mockFetch.mockResolvedValueOnce(
|
|
477
|
+
new Response(JSON.stringify({}), { status: 200 }),
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
const { artifacts } = createAdkSessionAdapter(baseOptions);
|
|
481
|
+
|
|
482
|
+
await expect(artifacts.load("s1", "report.pdf")).rejects.toThrow(
|
|
483
|
+
'Invalid ADK artifact load response: expected an object containing "text" or "inlineData".',
|
|
484
|
+
);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it.each([
|
|
488
|
+
[
|
|
489
|
+
"text",
|
|
490
|
+
{ text: 42 },
|
|
491
|
+
'Invalid ADK artifact load response: "text" must be a string when present.',
|
|
492
|
+
],
|
|
493
|
+
[
|
|
494
|
+
"inline data",
|
|
495
|
+
{ inlineData: { mimeType: "image/png" } },
|
|
496
|
+
'Invalid ADK artifact load response: "inlineData" must contain string "mimeType" and "data" fields.',
|
|
497
|
+
],
|
|
498
|
+
])("rejects malformed %s artifact content", async (_label, value, error) => {
|
|
499
|
+
mockFetch.mockResolvedValueOnce(
|
|
500
|
+
new Response(JSON.stringify(value), { status: 200 }),
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
const { artifacts } = createAdkSessionAdapter(baseOptions);
|
|
504
|
+
|
|
505
|
+
await expect(artifacts.load("s1", "report.pdf")).rejects.toThrow(error);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it("lists artifact versions", async () => {
|
|
509
|
+
mockFetch.mockResolvedValueOnce(
|
|
510
|
+
new Response(JSON.stringify([0, 1, 2]), { status: 200 }),
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
const { artifacts } = createAdkSessionAdapter(baseOptions);
|
|
514
|
+
|
|
515
|
+
await expect(artifacts.listVersions("s1", "report.pdf")).resolves.toEqual([
|
|
516
|
+
0, 1, 2,
|
|
517
|
+
]);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it("rejects an artifact version list that is not an array", async () => {
|
|
521
|
+
mockFetch.mockResolvedValueOnce(
|
|
522
|
+
new Response(JSON.stringify({}), { status: 200 }),
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
const { artifacts } = createAdkSessionAdapter(baseOptions);
|
|
526
|
+
|
|
527
|
+
await expect(artifacts.listVersions("s1", "report.pdf")).rejects.toThrow(
|
|
528
|
+
"Invalid ADK artifact versions response: expected an array of version numbers.",
|
|
529
|
+
);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it("rejects malformed artifact version entries", async () => {
|
|
533
|
+
mockFetch.mockResolvedValueOnce(
|
|
534
|
+
new Response(JSON.stringify([0, "1"]), { status: 200 }),
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
const { artifacts } = createAdkSessionAdapter(baseOptions);
|
|
538
|
+
|
|
539
|
+
await expect(artifacts.listVersions("s1", "report.pdf")).rejects.toThrow(
|
|
540
|
+
"Invalid ADK artifact versions response: version at index 1 must be a non-negative integer.",
|
|
541
|
+
);
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
|
|
346
545
|
// ── URL construction ──
|
|
347
546
|
|
|
348
547
|
describe("createAdkSessionAdapter - URL construction", () => {
|
package/src/AdkSessionAdapter.ts
CHANGED
|
@@ -57,6 +57,130 @@ type AdkSessionAdapterResult = {
|
|
|
57
57
|
};
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
+
type AdkSessionResponse = {
|
|
61
|
+
id: string;
|
|
62
|
+
events?: unknown;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
66
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
67
|
+
|
|
68
|
+
const isAdkSessionResponse = (value: unknown): value is AdkSessionResponse => {
|
|
69
|
+
if (!isRecord(value)) return false;
|
|
70
|
+
|
|
71
|
+
const id = value.id;
|
|
72
|
+
return typeof id === "string" && id.length > 0;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const parseAdkSessionResponse = (
|
|
76
|
+
value: unknown,
|
|
77
|
+
operation: "create" | "fetch" | "load",
|
|
78
|
+
): AdkSessionResponse => {
|
|
79
|
+
if (!isAdkSessionResponse(value)) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Invalid ADK session ${operation} response: expected an object with a non-empty string "id".`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return value;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const parseAdkSessionListResponse = (value: unknown): AdkSessionResponse[] => {
|
|
88
|
+
if (!Array.isArray(value)) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
"Invalid ADK session list response: expected an array of sessions.",
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return value.map((session, index) => {
|
|
95
|
+
if (!isAdkSessionResponse(session)) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`Invalid ADK session list response: session at index ${index} must have a non-empty string "id".`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return session;
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const parseAdkArtifactListResponse = (value: unknown): string[] => {
|
|
105
|
+
if (!Array.isArray(value)) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
"Invalid ADK artifact list response: expected an array of artifact names.",
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return value.map((artifact, index) => {
|
|
112
|
+
if (typeof artifact === "string" && artifact.length > 0) {
|
|
113
|
+
return artifact;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (isRecord(artifact)) {
|
|
117
|
+
const filename = artifact.filename;
|
|
118
|
+
if (typeof filename === "string" && filename.length > 0) {
|
|
119
|
+
return filename;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
throw new Error(
|
|
124
|
+
`Invalid ADK artifact list response: artifact at index ${index} must be a non-empty string or an object with a non-empty string "filename".`,
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const parseAdkArtifactResponse = (value: unknown): AdkArtifactData => {
|
|
130
|
+
if (!isRecord(value)) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
'Invalid ADK artifact load response: expected an object containing "text" or "inlineData".',
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const { text, inlineData } = value;
|
|
137
|
+
if (text === undefined && inlineData === undefined) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
'Invalid ADK artifact load response: expected an object containing "text" or "inlineData".',
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (text !== undefined && typeof text !== "string") {
|
|
144
|
+
throw new Error(
|
|
145
|
+
'Invalid ADK artifact load response: "text" must be a string when present.',
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (
|
|
150
|
+
inlineData !== undefined &&
|
|
151
|
+
(!isRecord(inlineData) ||
|
|
152
|
+
typeof inlineData.mimeType !== "string" ||
|
|
153
|
+
typeof inlineData.data !== "string")
|
|
154
|
+
) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
'Invalid ADK artifact load response: "inlineData" must contain string "mimeType" and "data" fields.',
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return value as AdkArtifactData;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const parseAdkArtifactVersionsResponse = (value: unknown): number[] => {
|
|
164
|
+
if (!Array.isArray(value)) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
"Invalid ADK artifact versions response: expected an array of version numbers.",
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return value.map((version, index) => {
|
|
171
|
+
if (
|
|
172
|
+
typeof version !== "number" ||
|
|
173
|
+
!Number.isInteger(version) ||
|
|
174
|
+
version < 0
|
|
175
|
+
) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`Invalid ADK artifact versions response: version at index ${index} must be a non-negative integer.`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
return version;
|
|
181
|
+
});
|
|
182
|
+
};
|
|
183
|
+
|
|
60
184
|
/**
|
|
61
185
|
* Creates a `RemoteThreadListAdapter` backed by ADK's session REST API,
|
|
62
186
|
* plus a `load` function that reconstructs messages from session events.
|
|
@@ -96,12 +220,7 @@ export function createAdkSessionAdapter(
|
|
|
96
220
|
if (!res.ok) {
|
|
97
221
|
throw new Error(`Failed to list sessions: ${res.status}`);
|
|
98
222
|
}
|
|
99
|
-
const data = (await res.json())
|
|
100
|
-
id: string;
|
|
101
|
-
app_name?: string;
|
|
102
|
-
user_id?: string;
|
|
103
|
-
last_update_time?: number;
|
|
104
|
-
}>;
|
|
223
|
+
const data = parseAdkSessionListResponse(await res.json());
|
|
105
224
|
|
|
106
225
|
const threads: RemoteThreadMetadata[] = data.map((session) => ({
|
|
107
226
|
status: "regular" as const,
|
|
@@ -125,7 +244,7 @@ export function createAdkSessionAdapter(
|
|
|
125
244
|
if (!res.ok) {
|
|
126
245
|
throw new Error(`Failed to create session: ${res.status}`);
|
|
127
246
|
}
|
|
128
|
-
const session = (await res.json())
|
|
247
|
+
const session = parseAdkSessionResponse(await res.json(), "create");
|
|
129
248
|
return { remoteId: session.id, externalId: session.id };
|
|
130
249
|
},
|
|
131
250
|
|
|
@@ -171,7 +290,7 @@ export function createAdkSessionAdapter(
|
|
|
171
290
|
if (!res.ok) {
|
|
172
291
|
throw new Error(`Session not found: ${res.status}`);
|
|
173
292
|
}
|
|
174
|
-
const session = (await res.json())
|
|
293
|
+
const session = parseAdkSessionResponse(await res.json(), "fetch");
|
|
175
294
|
return {
|
|
176
295
|
status: "regular",
|
|
177
296
|
remoteId: session.id,
|
|
@@ -191,18 +310,23 @@ export function createAdkSessionAdapter(
|
|
|
191
310
|
if (!res.ok) {
|
|
192
311
|
throw new Error(`Failed to load session: ${res.status}`);
|
|
193
312
|
}
|
|
194
|
-
const session = (await res.json())
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
313
|
+
const session = parseAdkSessionResponse(await res.json(), "load");
|
|
314
|
+
|
|
315
|
+
if (session.events !== undefined && !Array.isArray(session.events)) {
|
|
316
|
+
throw new Error(
|
|
317
|
+
'Invalid ADK session load response: expected "events" to be an array when present.',
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const events = session.events as AdkEvent[] | undefined;
|
|
198
322
|
|
|
199
|
-
if (!
|
|
323
|
+
if (!events?.length) {
|
|
200
324
|
return { messages: [] };
|
|
201
325
|
}
|
|
202
326
|
|
|
203
327
|
const accumulator = new AdkEventAccumulator();
|
|
204
328
|
let messages: AdkMessage[] = [];
|
|
205
|
-
for (const event of
|
|
329
|
+
for (const event of events) {
|
|
206
330
|
messages = accumulator.processEvent(event);
|
|
207
331
|
}
|
|
208
332
|
return { messages };
|
|
@@ -216,8 +340,7 @@ export function createAdkSessionAdapter(
|
|
|
216
340
|
const headers = await getHeaders();
|
|
217
341
|
const res = await fetch(artifactBaseUrl(sessionId), { headers });
|
|
218
342
|
if (!res.ok) throw new Error(`Failed to list artifacts: ${res.status}`);
|
|
219
|
-
|
|
220
|
-
return data.map((a) => a.filename);
|
|
343
|
+
return parseAdkArtifactListResponse(await res.json());
|
|
221
344
|
},
|
|
222
345
|
|
|
223
346
|
async load(sessionId, artifactName, version?) {
|
|
@@ -226,7 +349,7 @@ export function createAdkSessionAdapter(
|
|
|
226
349
|
if (version != null) url += `/versions/${version}`;
|
|
227
350
|
const res = await fetch(url, { headers });
|
|
228
351
|
if (!res.ok) throw new Error(`Failed to load artifact: ${res.status}`);
|
|
229
|
-
return (await res.json())
|
|
352
|
+
return parseAdkArtifactResponse(await res.json());
|
|
230
353
|
},
|
|
231
354
|
|
|
232
355
|
async listVersions(sessionId, artifactName) {
|
|
@@ -236,7 +359,7 @@ export function createAdkSessionAdapter(
|
|
|
236
359
|
if (!res.ok) {
|
|
237
360
|
throw new Error(`Failed to list artifact versions: ${res.status}`);
|
|
238
361
|
}
|
|
239
|
-
return (await res.json())
|
|
362
|
+
return parseAdkArtifactVersionsResponse(await res.json());
|
|
240
363
|
},
|
|
241
364
|
|
|
242
365
|
async delete(sessionId, artifactName) {
|