@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 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();
@@ -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":";;;;;;;;;;;;;;;;;;;;;AA2DA,SAAgB,gBACd,SACmB;CACnB,MAAM,WAAW,QAAQ,WAAW;CAEpC,OAAO,iBAAiB,UAAU,QAAQ;EACxC,MAAM,UAAU,MAAM,eAAe,QAAQ,OAAO;EAEpD,IAAI;EACJ,IAAI;EAEJ,IAAI,UAAU;GAEZ,MAAM,GAAG,oBAAoB,QAAQ,GAAG,EAAE;GAC1C,MAAM,EAAE,eAAe,MAAM,OAAO,WAAW;GAC/C,OAAO;IACL,SAAS,QAAQ;IACjB,QAAQ,QAAQ;IAChB,WAAW;IACX,YAAY,kBAAkB,QAAQ;IACtC,WAAW;IACX,GAAI,OAAO,cAAc,QAAQ,EAAE,YAAY,OAAO,WAAW;GACnE;EACF,OAAO;GAEL,MAAM,QAAQ;GACd,OAAO,oBAAoB,UAAU,MAAM;EAC7C;EAEA,MAAM,WAAW,MAAM,MAAM,KAAK;GAChC,QAAQ;GACR,SAAS;IAAE,gBAAgB;IAAoB,GAAG;GAAQ;GAC1D,MAAM,KAAK,UAAU,IAAI;GACzB,QAAQ,OAAO;EACjB,CAAC;EAED,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,MACR,uBAAuB,SAAS,OAAO,GAAG,SAAS,YACrD;EAGF,OAAO,iBAAiB,QAAQ;CAClC;AACF;AAIA,eAAe,eACb,SAIiC;CACjC,IAAI,CAAC,SAAS,OAAO,CAAC;CACtB,IAAI,OAAO,YAAY,YAAY,OAAO,MAAM,QAAQ;CACxD,OAAO;AACT;;;;;AAMA,SAAS,kBAAkB,UAGzB;CACA,MAAM,QAAwB,CAAC;CAE/B,KAAK,MAAM,OAAO,UAChB,IAAI,IAAI,SAAS,SACf,KAAK,MAAM,QAAQ,eAAe,IAAI,OAAO,GAC3C,MAAM,KAAK,IAAI;MAEZ,IAAI,IAAI,SAAS,QAAQ;EAC9B,IAAI;EACJ,IAAI;GACF,WAAW,KAAK,MAAM,IAAI,OAAO;EACnC,QAAQ;GACN,WAAW,IAAI;EACjB;EACA,MAAM,KAAK,EACT,kBAAkB;GAChB,MAAM,IAAI;GACV,IAAI,IAAI;GACR;EACF,EACF,CAAC;CACH;CAGF,IAAI,MAAM,WAAW,GACnB,MAAM,KAAK,EAAE,MAAM,GAAG,CAAC;CAGzB,OAAO;EAAE,MAAM;EAAQ;CAAM;AAC/B;;;;;AAMA,SAAS,oBACP,UACA,QAKyB;CACzB,MAAM,OAAgC,CAAC;CAEvC,IAAI,OAAO,aAAa,MAAM,KAAK,YAAY,OAAO;CACtD,IAAI,OAAO,gBAAgB,MAAM,KAAK,eAAe,OAAO;CAC5D,IAAI,OAAO,cAAc,MAAM,KAAK,aAAa,OAAO;CAGxD,MAAM,UAAU,SAAS,MAAM,MAAM,EAAE,SAAS,MAAM;CACtD,IAAI,WAAW,QAAQ,SAAS,QAAQ;EAEtC,IAAI,SAAS,SAAS,GAAG;GACvB,KAAK,QAAQ,kBAAkB,QAAQ,CAAC,CAAC;GACzC,OAAO;EACT;EAEA,IAAI;EACJ,IAAI;GACF,SAAS,KAAK,MAAM,QAAQ,OAAO;EACrC,QAAQ;GACN,SAAS,QAAQ;EACnB;EACA,KAAK,OAAO;EACZ,KAAK,aAAa,QAAQ;EAC1B,KAAK,WAAW,QAAQ;EACxB,KAAK,SAAS;EACd,KAAK,UAAU,QAAQ,WAAW;EAClC,OAAO;CACT;CAGA,IAAI,SAAS,WAAW,KAAK,SAAS,EAAE,CAAE,SAAS,SAAS;EAC1D,MAAM,MAAM,SAAS;EACrB,IAAI,OAAO,IAAI,YAAY,UACzB,KAAK,UAAU,IAAI;OAEnB,KAAK,QAAQ,eAAe,IAAI,OAAO;EAEzC,OAAO;CACT;CAGA,KAAK,QAAQ,kBAAkB,QAAQ,CAAC,CAAC;CACzC,OAAO;AACT;AAEA,gBAAgB,iBAAiB,UAA8C;CAC7E,MAAM,SAAS,SAAS,KAAM,UAAU;CACxC,MAAM,UAAU,IAAI,YAAY;CAChC,MAAM,aAAa,IAAI,gBAAgB,EAAE,UAAU,WAAW,CAAC;CAE/D,IAAI;EACF,OAAO,MAAM;GACX,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,KAAK;GAC1C,IAAI,MAAM;IACR,KAAK,MAAM,SAAS,WAAW,KAAK,QAAQ,OAAO,CAAC,GAClD,MAAM,KAAK,MAAM,MAAM,IAAI;IAE7B;GACF;GAEA,KAAK,MAAM,SAAS,WAAW,KAC7B,QAAQ,OAAO,OAAO,EAAE,QAAQ,KAAK,CAAC,CACxC,GACE,MAAM,KAAK,MAAM,MAAM,IAAI;EAE/B;EAEA,MAAM,WAAW,WAAW,MAAM;EAClC,IAAI,aAAa,MAAM,MAAM,KAAK,MAAM,SAAS,IAAI;CACvD,UAAU;EACR,OAAO,YAAY;CACrB;AACF"}
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;cACI,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
+ {"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;;;;;;;;;;;;;;;;;;;;;;iBAuBzC,wBACd,SAAS,2BACR"}
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?.length) return { messages: [] };
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 session.events) messages = accumulator.processEvent(event);
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()).map((a) => a.filename);
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;;;;;;;;;;;;;6BA2DqB,cAAY,QAAU,yBAAoB;;sBA9BhB;0BAUvC"}
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.17",
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.21",
38
- "@assistant-ui/store": "^0.2.20",
37
+ "@assistant-ui/core": "^0.2.22",
38
+ "@assistant-ui/store": "^0.2.21",
39
39
  "assistant-cloud": "*",
40
- "assistant-stream": "^0.3.26",
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.7",
59
+ "react": "^19.2.8",
60
60
  "vitest": "^4.1.10",
61
- "@assistant-ui/x-buildutils": "0.0.18"
61
+ "@assistant-ui/x-buildutils": "0.0.19"
62
62
  },
63
63
  "publishConfig": {
64
64
  "access": "public",
@@ -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(new Response(sseBody(""), { status: 200 }));
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(new Response(sseBody(""), { status: 200 }));
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(new Response(sseBody(""), { status: 200 }));
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(new Response(sseBody(""), { status: 200 }));
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(new Response(sseBody(""), { status: 200 }));
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(new Response(sseBody(""), { status: 200 }));
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(new Response(sseBody(""), { status: 200 }));
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(new Response(sseBody(""), { status: 200 }));
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(new Response(sseBody(""), { status: 200 }));
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(new Response(sseBody(""), { status: 200 }));
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(new Response(sseBody(""), { status: 200 }));
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(new Response(sseBody(""), { status: 200 }));
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("skips :ok SSE comments", async () => {
417
- const text = `:ok\n\ndata: ${JSON.stringify({ id: "e1" })}\n\n`;
450
+ it("accepts parameterized event-stream content types", async () => {
451
+ const event: AdkEvent = { id: "e1" };
418
452
  mockFetch.mockResolvedValueOnce(
419
- new Response(sseBody(text), { status: 200 }),
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(new Response(body, { status: 200 }));
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(new Response(body, { status: 200 }));
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(new Response(body, { status: 200 }));
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(new Response(sseBody(""), { status: 200 }));
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(new Response(sseBody(""), { status: 200 }));
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(new Response(sseBody(""), { status: 200 }));
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(new Response(sseBody(""), { status: 200 }));
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(new Response(sseBody(""), { status: 200 }));
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(new Response(sseBody(""), { status: 200 }));
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(new Response(sseBody(""), { status: 200 }));
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(new Response(sseBody(""), { status: 200 }));
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(new Response(sseBody(""), { status: 200 }));
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(new Response(sseBody(""), { status: 200 }));
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(new Response(sseBody(""), { status: 200 }));
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", () => {
@@ -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()) as Array<{
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()) as { id: string };
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()) as { id: string };
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()) as {
195
- id: string;
196
- events?: AdkEvent[];
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 (!session.events?.length) {
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 session.events) {
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
- const data = (await res.json()) as Array<{ filename: string }>;
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()) as AdkArtifactData;
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()) as number[];
362
+ return parseAdkArtifactVersionsResponse(await res.json());
240
363
  },
241
364
 
242
365
  async delete(sessionId, artifactName) {