@assistant-ui/react-google-adk 0.0.25 → 0.0.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/AdkClient.d.ts.map +1 -1
  2. package/dist/AdkClient.js +13 -3
  3. package/dist/AdkClient.js.map +1 -1
  4. package/dist/AdkEventAccumulator.d.ts +2 -0
  5. package/dist/AdkEventAccumulator.d.ts.map +1 -1
  6. package/dist/AdkEventAccumulator.js +32 -10
  7. package/dist/AdkEventAccumulator.js.map +1 -1
  8. package/dist/AdkSessionAdapter.d.ts.map +1 -1
  9. package/dist/AdkSessionAdapter.js +2 -1
  10. package/dist/AdkSessionAdapter.js.map +1 -1
  11. package/dist/adkToolApproval.d.ts +40 -0
  12. package/dist/adkToolApproval.d.ts.map +1 -0
  13. package/dist/adkToolApproval.js +166 -0
  14. package/dist/adkToolApproval.js.map +1 -0
  15. package/dist/convertAdkMessages.d.ts +3 -1
  16. package/dist/convertAdkMessages.d.ts.map +1 -1
  17. package/dist/convertAdkMessages.js +27 -16
  18. package/dist/convertAdkMessages.js.map +1 -1
  19. package/dist/convertToAdkMessages.d.ts +49 -0
  20. package/dist/convertToAdkMessages.d.ts.map +1 -0
  21. package/dist/convertToAdkMessages.js +86 -0
  22. package/dist/convertToAdkMessages.js.map +1 -0
  23. package/dist/hooks.d.ts.map +1 -1
  24. package/dist/hooks.js +5 -14
  25. package/dist/hooks.js.map +1 -1
  26. package/dist/parseAdkEvent.d.ts +6 -0
  27. package/dist/parseAdkEvent.d.ts.map +1 -0
  28. package/dist/parseAdkEvent.js +16 -0
  29. package/dist/parseAdkEvent.js.map +1 -0
  30. package/dist/server/parseAdkRequest.d.ts.map +1 -1
  31. package/dist/server/parseAdkRequest.js +79 -13
  32. package/dist/server/parseAdkRequest.js.map +1 -1
  33. package/dist/useAdkMessages.d.ts.map +1 -1
  34. package/dist/useAdkMessages.js +110 -79
  35. package/dist/useAdkMessages.js.map +1 -1
  36. package/dist/useAdkRuntime.d.ts.map +1 -1
  37. package/dist/useAdkRuntime.js +44 -129
  38. package/dist/useAdkRuntime.js.map +1 -1
  39. package/package.json +6 -7
  40. package/src/AdkClient.test.ts +137 -0
  41. package/src/AdkClient.ts +15 -3
  42. package/src/AdkEventAccumulator.test.ts +161 -1
  43. package/src/AdkEventAccumulator.ts +44 -8
  44. package/src/AdkSessionAdapter.test.ts +145 -1
  45. package/src/AdkSessionAdapter.ts +7 -2
  46. package/src/adkToolApproval.test.ts +345 -0
  47. package/src/adkToolApproval.ts +215 -0
  48. package/src/convertAdkMessages.ts +72 -43
  49. package/src/{useAdkRuntime.test.ts → convertToAdkMessages.test.ts} +1 -1
  50. package/src/convertToAdkMessages.ts +139 -0
  51. package/src/hooks.ts +7 -19
  52. package/src/parseAdkEvent.ts +40 -0
  53. package/src/server/parseAdkRequest.test.ts +141 -0
  54. package/src/server/parseAdkRequest.ts +145 -13
  55. package/src/useAdkMessages.test.ts +313 -0
  56. package/src/useAdkMessages.ts +72 -25
  57. package/src/useAdkRuntime.ts +66 -188
  58. package/src/useAdkRuntimeApproval.test.tsx +344 -0
@@ -0,0 +1,139 @@
1
+ import {
2
+ generateId,
3
+ getExternalStoreMessages,
4
+ type AppendMessage,
5
+ type ThreadMessage,
6
+ } from "@assistant-ui/core";
7
+ import {
8
+ createToolCallCancellationStub,
9
+ parseDataUrl,
10
+ resolveFilePartSource,
11
+ scanPendingToolCalls,
12
+ } from "@assistant-ui/core/internal";
13
+ import type { AdkMessage } from "./types";
14
+
15
+ /** Exported for unit tests. */
16
+ export const getMessageContent = (msg: AppendMessage) => {
17
+ const allContent = [
18
+ ...msg.content,
19
+ ...(msg.attachments?.flatMap((a) => a.content) ?? []),
20
+ ];
21
+ const content = allContent.flatMap((part) => {
22
+ const type = part.type;
23
+ switch (type) {
24
+ case "text":
25
+ return { type: "text" as const, text: part.text };
26
+ case "image":
27
+ return { type: "image_url" as const, url: part.image };
28
+ case "file": {
29
+ const source = resolveFilePartSource(part);
30
+ if (source.kind === "url") {
31
+ return {
32
+ type: "file_url" as const,
33
+ url: source.url,
34
+ mimeType: part.mimeType,
35
+ };
36
+ }
37
+ return {
38
+ type: "file" as const,
39
+ mimeType: part.mimeType,
40
+ // Lands in Gemini `inlineData.data`, which takes bare base64, so a
41
+ // data URL envelope is stripped rather than forwarded.
42
+ data: source.data,
43
+ ...(part.filename != null && { filename: part.filename }),
44
+ };
45
+ }
46
+ case "audio": {
47
+ const parsed = parseDataUrl(part.audio.data);
48
+ return {
49
+ type: "file" as const,
50
+ mimeType: `audio/${part.audio.format}`,
51
+ data: parsed?.data ?? part.audio.data,
52
+ };
53
+ }
54
+ case "data":
55
+ return [];
56
+
57
+ case "tool-call":
58
+ throw new Error("Tool call appends are not supported.");
59
+
60
+ default: {
61
+ const _exhaustiveCheck: "reasoning" | "source" | "generative-ui" = type;
62
+ throw new Error(
63
+ `Unsupported append message part type: ${_exhaustiveCheck}`,
64
+ );
65
+ }
66
+ }
67
+ });
68
+
69
+ if (content.length === 1 && content[0]?.type === "text") {
70
+ return content[0].text ?? "";
71
+ }
72
+
73
+ return content;
74
+ };
75
+
76
+ /** Exported for unit tests. */
77
+ export const getPendingToolCalls = (messages: AdkMessage[]) => {
78
+ return scanPendingToolCalls(
79
+ messages,
80
+ (message) => {
81
+ if (message.type === "ai") {
82
+ return { toolCalls: message.tool_calls ?? [] };
83
+ }
84
+ if (message.type === "tool") {
85
+ return { toolCallId: message.tool_call_id };
86
+ }
87
+ return undefined;
88
+ },
89
+ (toolCall) => toolCall.id,
90
+ );
91
+ };
92
+
93
+ /**
94
+ * Exported for unit tests.
95
+ *
96
+ * Returns `{cancelled: true}` tool responses for pending tool calls when the
97
+ * user sends a new turn, EXCEPT for HITL interrupts marked via
98
+ * `long_running_tool_ids` (`adk_request_input`, `adk_request_confirmation`,
99
+ * `adk_request_credential`). Those must be answered through a dedicated tool
100
+ * UI + submit helper, not auto-cancelled.
101
+ */
102
+ export const getPendingCancellations = (
103
+ messages: AdkMessage[],
104
+ longRunningToolIds: readonly string[],
105
+ ): Array<AdkMessage & { type: "tool" }> => {
106
+ const longRunningSet = new Set(longRunningToolIds);
107
+ return getPendingToolCalls(messages)
108
+ .filter((t) => !longRunningSet.has(t.id))
109
+ .map(
110
+ (t) =>
111
+ ({
112
+ id: generateId(),
113
+ ...createToolCallCancellationStub(t),
114
+ }) satisfies AdkMessage & { type: "tool" },
115
+ );
116
+ };
117
+
118
+ export const truncateAdkMessages = (
119
+ threadMessages: readonly ThreadMessage[],
120
+ parentId: string | null,
121
+ ): AdkMessage[] => {
122
+ if (parentId === null) return [];
123
+ const parentIndex = threadMessages.findIndex((m) => m.id === parentId);
124
+ if (parentIndex === -1) return [];
125
+ const truncated: AdkMessage[] = [];
126
+ for (let i = 0; i <= parentIndex && i < threadMessages.length; i++) {
127
+ truncated.push(...getExternalStoreMessages<AdkMessage>(threadMessages[i]!));
128
+ }
129
+ return truncated;
130
+ };
131
+
132
+ export const toAdkUserMessage = (
133
+ msg: AppendMessage,
134
+ id = generateId(),
135
+ ): AdkMessage & { type: "human"; id: string } => ({
136
+ id,
137
+ type: "human",
138
+ content: getMessageContent(msg),
139
+ });
package/src/hooks.ts CHANGED
@@ -1,7 +1,8 @@
1
+ import { generateId } from "@assistant-ui/core";
1
2
  import { useAui } from "@assistant-ui/store";
2
- import { v4 as uuidv4 } from "uuid";
3
3
  import type { ReadonlyJSONValue } from "assistant-stream/utils";
4
4
  import { adkExtras } from "./adkExtras";
5
+ import { toAdkConfirmationReply } from "./adkToolApproval";
5
6
  import type {
6
7
  AdkMessage,
7
8
  AdkSendMessageConfig,
@@ -66,22 +67,9 @@ export const useAdkConfirmTool = () => {
66
67
  confirmed: boolean,
67
68
  payload?: ReadonlyJSONValue,
68
69
  ) =>
69
- adkExtras.get(aui).send(
70
- [
71
- {
72
- id: uuidv4(),
73
- type: "tool",
74
- tool_call_id: toolCallId,
75
- name: "adk_request_confirmation",
76
- content: JSON.stringify({
77
- confirmed,
78
- ...(payload != null && { payload }),
79
- }),
80
- status: "success",
81
- },
82
- ],
83
- {},
84
- );
70
+ adkExtras
71
+ .get(aui)
72
+ .send([toAdkConfirmationReply(toolCallId, confirmed, payload)], {});
85
73
  };
86
74
 
87
75
  /** Returns a function to submit auth credentials for a pending auth request. */
@@ -91,7 +79,7 @@ export const useAdkSubmitAuth = () => {
91
79
  adkExtras.get(aui).send(
92
80
  [
93
81
  {
94
- id: uuidv4(),
82
+ id: generateId(),
95
83
  type: "tool",
96
84
  tool_call_id: toolCallId,
97
85
  name: "adk_request_credential",
@@ -110,7 +98,7 @@ export const useAdkSubmitInput = () => {
110
98
  adkExtras.get(aui).send(
111
99
  [
112
100
  {
113
- id: uuidv4(),
101
+ id: generateId(),
114
102
  type: "tool",
115
103
  tool_call_id: toolCallId,
116
104
  name: "adk_request_input",
@@ -0,0 +1,40 @@
1
+ import type { AdkEvent } from "./types";
2
+
3
+ export function parseAdkEventValue(
4
+ value: unknown,
5
+ errorPrefix: string,
6
+ ): AdkEvent {
7
+ if (
8
+ typeof value !== "object" ||
9
+ value === null ||
10
+ Array.isArray(value) ||
11
+ Object.keys(value).length === 0
12
+ ) {
13
+ throw new Error(`${errorPrefix}: expected a non-empty object.`);
14
+ }
15
+
16
+ const { id: rawId, ...event } = value as Record<string, unknown>;
17
+ if (
18
+ rawId != null &&
19
+ typeof rawId !== "string" &&
20
+ (typeof rawId !== "number" || !Number.isFinite(rawId))
21
+ ) {
22
+ throw new Error(
23
+ `${errorPrefix}: expected "id" to be a string or finite number when present.`,
24
+ );
25
+ }
26
+
27
+ const errorMessage =
28
+ "error" in event && typeof event.error === "string"
29
+ ? event.error
30
+ : undefined;
31
+ return {
32
+ ...event,
33
+ ...(rawId != null && { id: String(rawId) }),
34
+ ...(errorMessage !== undefined &&
35
+ !("errorMessage" in event) &&
36
+ !("error_message" in event) && {
37
+ errorMessage,
38
+ }),
39
+ } as AdkEvent;
40
+ }
@@ -49,6 +49,33 @@ describe("parseAdkRequest", () => {
49
49
  expect(result.parts).toHaveLength(2);
50
50
  });
51
51
 
52
+ it("parses each supported ADK part shape", async () => {
53
+ const parts = [
54
+ { text: "Hello", thought: true },
55
+ { functionCall: { name: "search", id: "call-1", args: {} } },
56
+ {
57
+ functionResponse: {
58
+ name: "search",
59
+ id: "call-1",
60
+ response: null,
61
+ },
62
+ },
63
+ { executableCode: { code: "print('hello')", language: "python" } },
64
+ { codeExecutionResult: { output: "hello", outcome: "ok" } },
65
+ { inlineData: { mimeType: "image/png", data: "abc" } },
66
+ {
67
+ fileData: {
68
+ fileUri: "https://example.com/file",
69
+ mimeType: "text/plain",
70
+ },
71
+ },
72
+ ];
73
+
74
+ await expect(
75
+ parseAdkRequest(makeRequest({ parts })),
76
+ ).resolves.toMatchObject({ parts });
77
+ });
78
+
52
79
  it("parses a tool-result request", async () => {
53
80
  const result = await parseAdkRequest(
54
81
  makeRequest({
@@ -73,6 +100,7 @@ describe("parseAdkRequest", () => {
73
100
  makeRequest({
74
101
  type: "tool-result",
75
102
  toolCallId: "tc-1",
103
+ toolName: "search",
76
104
  result: {},
77
105
  }),
78
106
  );
@@ -95,6 +123,119 @@ describe("parseAdkRequest", () => {
95
123
  "Google ADK proxy request body must be a JSON object",
96
124
  );
97
125
  });
126
+
127
+ it.each([
128
+ [{ message: 42 }, 'field "message"'],
129
+ [{ parts: {} }, 'field "parts"'],
130
+ [{ parts: [null] }, 'field "parts"'],
131
+ [{ type: "unknown", message: "hello" }, 'field "type"'],
132
+ [{ message: "hello", checkpointId: 42 }, 'field "checkpointId"'],
133
+ [{ message: "hello", stateDelta: [] }, 'field "stateDelta"'],
134
+ ])("rejects malformed message requests %#", async (body, error) => {
135
+ await expect(parseAdkRequest(makeRequest(body))).rejects.toThrow(error);
136
+ });
137
+
138
+ it.each([
139
+ [{ parts: [{}] }, 'field "parts[0]"'],
140
+ [{ parts: [{ text: 42 }] }, 'field "parts[0].text"'],
141
+ [
142
+ { parts: [{ inlineData: { mimeType: "image/png", data: 42 } }] },
143
+ 'field "parts[0].inlineData.data"',
144
+ ],
145
+ [
146
+ { parts: [{ functionCall: { name: "search", args: [] } }] },
147
+ 'field "parts[0].functionCall.args"',
148
+ ],
149
+ [
150
+ { parts: [{ functionResponse: { name: 42 } }] },
151
+ 'field "parts[0].functionResponse.name"',
152
+ ],
153
+ ])("rejects malformed nested ADK parts %#", async (body, error) => {
154
+ await expect(parseAdkRequest(makeRequest(body))).rejects.toThrow(error);
155
+ });
156
+
157
+ it("accepts exclude_none and unknown part shapes", async () => {
158
+ const parts = [
159
+ { functionCall: { name: "get_time", id: "call-1" } },
160
+ { functionResponse: { name: "search", id: "call-1" } },
161
+ { text: "hi", inlineData: { mimeType: "image/png", data: "abc" } },
162
+ { videoMetadata: { fps: 1 } },
163
+ { text: "hello", thoughtSignature: "sig" },
164
+ ];
165
+
166
+ await expect(
167
+ parseAdkRequest(makeRequest({ parts })),
168
+ ).resolves.toMatchObject({ parts });
169
+ });
170
+
171
+ it("requires message content", async () => {
172
+ await expect(parseAdkRequest(makeRequest({}))).rejects.toThrow(
173
+ 'expected a "message" string or a "parts" array',
174
+ );
175
+ });
176
+
177
+ it.each([
178
+ [
179
+ { type: "message", message: "hello" },
180
+ { type: "message", text: "hello", config: {} },
181
+ ],
182
+ [{ parts: [] }, { type: "message", text: "", parts: [], config: {} }],
183
+ ])(
184
+ "preserves supported empty and explicit message shapes %#",
185
+ async (body, expected) => {
186
+ await expect(parseAdkRequest(makeRequest(body))).resolves.toEqual(
187
+ expected,
188
+ );
189
+ },
190
+ );
191
+
192
+ it("accepts empty tool identifiers emitted by id-less ADK calls", async () => {
193
+ await expect(
194
+ parseAdkRequest(
195
+ makeRequest({
196
+ type: "tool-result",
197
+ toolCallId: "",
198
+ toolName: "",
199
+ result: {},
200
+ }),
201
+ ),
202
+ ).resolves.toMatchObject({
203
+ type: "tool-result",
204
+ toolCallId: "",
205
+ toolName: "",
206
+ });
207
+ });
208
+
209
+ it.each([
210
+ [
211
+ { type: "tool-result", toolName: "search", result: {} },
212
+ 'field "toolCallId"',
213
+ ],
214
+ [
215
+ { type: "tool-result", toolCallId: "tc-1", result: {} },
216
+ 'field "toolName"',
217
+ ],
218
+ [
219
+ { type: "tool-result", toolCallId: 42, toolName: "search", result: {} },
220
+ 'field "toolCallId"',
221
+ ],
222
+ [
223
+ {
224
+ type: "tool-result",
225
+ toolCallId: "tc-1",
226
+ toolName: "search",
227
+ result: {},
228
+ isError: "false",
229
+ },
230
+ 'field "isError"',
231
+ ],
232
+ [
233
+ { type: "tool-result", toolCallId: "tc-1", toolName: "search" },
234
+ 'field "result"',
235
+ ],
236
+ ])("rejects malformed tool-result requests %#", async (body, error) => {
237
+ await expect(parseAdkRequest(makeRequest(body))).rejects.toThrow(error);
238
+ });
98
239
  });
99
240
 
100
241
  describe("toAdkContent", () => {
@@ -1,3 +1,4 @@
1
+ import { isRecord } from "@assistant-ui/core/internal";
1
2
  import type { AdkSendMessageConfig } from "../types";
2
3
 
3
4
  type ParsedAdkRequest =
@@ -18,6 +19,107 @@ type ParsedAdkRequest =
18
19
  stateDelta?: Record<string, unknown> | undefined;
19
20
  };
20
21
 
22
+ const invalidField = (field: string, expectation: string): Error =>
23
+ new Error(
24
+ `Invalid Google ADK proxy request field "${field}": expected ${expectation}.`,
25
+ );
26
+
27
+ const readString = (body: Record<string, unknown>, field: string): string => {
28
+ const value = body[field];
29
+ if (typeof value !== "string") throw invalidField(field, "a string");
30
+ return value;
31
+ };
32
+
33
+ const readOptionalString = (
34
+ body: Record<string, unknown>,
35
+ field: string,
36
+ ): string | undefined => {
37
+ const value = body[field];
38
+ if (value === undefined) return undefined;
39
+ if (typeof value !== "string") throw invalidField(field, "a string");
40
+ return value;
41
+ };
42
+
43
+ const validateOptionalPartString = (
44
+ part: Record<string, unknown>,
45
+ key: string,
46
+ field: string,
47
+ ) => {
48
+ if (part[key] !== undefined && typeof part[key] !== "string") {
49
+ throw invalidField(field, "a string");
50
+ }
51
+ };
52
+
53
+ // Nested fields are validated only when present: ADK serializes with
54
+ // exclude_none semantics and the Part vocabulary grows upstream, so absent
55
+ // fields and unknown part kinds pass through to the runner untouched.
56
+ const validatePart = (part: Record<string, unknown>, index: number) => {
57
+ const field = (name: string) => `parts[${index}].${name}`;
58
+ if (Object.keys(part).length === 0) {
59
+ throw invalidField(`parts[${index}]`, "a non-empty part object");
60
+ }
61
+ if (part.thought !== undefined && typeof part.thought !== "boolean") {
62
+ throw invalidField(field("thought"), "a boolean");
63
+ }
64
+ if (part.text !== undefined && typeof part.text !== "string") {
65
+ throw invalidField(field("text"), "a string");
66
+ }
67
+
68
+ const validateRecordField = (
69
+ key: string,
70
+ validateContent: (content: Record<string, unknown>) => void,
71
+ ) => {
72
+ const value = part[key];
73
+ if (value === undefined) return;
74
+ if (!isRecord(value)) throw invalidField(field(key), "an object");
75
+ validateContent(value);
76
+ };
77
+
78
+ validateRecordField("functionCall", (content) => {
79
+ validateOptionalPartString(content, "name", field("functionCall.name"));
80
+ validateOptionalPartString(content, "id", field("functionCall.id"));
81
+ if (content.args !== undefined && !isRecord(content.args)) {
82
+ throw invalidField(field("functionCall.args"), "an object");
83
+ }
84
+ });
85
+ validateRecordField("functionResponse", (content) => {
86
+ validateOptionalPartString(content, "name", field("functionResponse.name"));
87
+ validateOptionalPartString(content, "id", field("functionResponse.id"));
88
+ });
89
+ validateRecordField("executableCode", (content) => {
90
+ validateOptionalPartString(content, "code", field("executableCode.code"));
91
+ validateOptionalPartString(
92
+ content,
93
+ "language",
94
+ field("executableCode.language"),
95
+ );
96
+ });
97
+ validateRecordField("codeExecutionResult", (content) => {
98
+ validateOptionalPartString(
99
+ content,
100
+ "output",
101
+ field("codeExecutionResult.output"),
102
+ );
103
+ validateOptionalPartString(
104
+ content,
105
+ "outcome",
106
+ field("codeExecutionResult.outcome"),
107
+ );
108
+ });
109
+ validateRecordField("inlineData", (content) => {
110
+ validateOptionalPartString(
111
+ content,
112
+ "mimeType",
113
+ field("inlineData.mimeType"),
114
+ );
115
+ validateOptionalPartString(content, "data", field("inlineData.data"));
116
+ });
117
+ validateRecordField("fileData", (content) => {
118
+ validateOptionalPartString(content, "fileUri", field("fileData.fileUri"));
119
+ validateOptionalPartString(content, "mimeType", field("fileData.mimeType"));
120
+ });
121
+ };
122
+
21
123
  /**
22
124
  * Parses an incoming HTTP request into a structured ADK request.
23
125
  *
@@ -42,44 +144,74 @@ type ParsedAdkRequest =
42
144
  export const parseAdkRequest = async (
43
145
  request: Request,
44
146
  ): Promise<ParsedAdkRequest> => {
45
- let body: Record<string, unknown>;
147
+ let body: unknown;
46
148
  try {
47
- body = (await request.json()) as Record<string, unknown>;
149
+ body = await request.json();
48
150
  } catch {
49
151
  throw new Error(
50
152
  'Invalid JSON in Google ADK proxy request body. Expected a JSON object like {"message":"Hello"} or {"type":"tool-result",...}.',
51
153
  );
52
154
  }
53
155
 
54
- if (!body || typeof body !== "object" || Array.isArray(body)) {
156
+ if (!isRecord(body)) {
55
157
  throw new Error("Google ADK proxy request body must be a JSON object");
56
158
  }
57
159
 
58
160
  const config: AdkSendMessageConfig = {};
59
161
  if (body.runConfig !== undefined) config.runConfig = body.runConfig;
60
- if (body.checkpointId !== undefined)
61
- config.checkpointId = body.checkpointId as string;
162
+ const checkpointId = readOptionalString(body, "checkpointId");
163
+ if (checkpointId !== undefined) config.checkpointId = checkpointId;
62
164
 
63
- const stateDelta = body.stateDelta as Record<string, unknown> | undefined;
165
+ const stateDelta = body.stateDelta;
166
+ if (stateDelta !== undefined && !isRecord(stateDelta)) {
167
+ throw invalidField("stateDelta", "an object");
168
+ }
64
169
 
65
170
  if (body.type === "tool-result") {
171
+ if (!("result" in body)) {
172
+ throw invalidField("result", "a value");
173
+ }
174
+ if (body.isError !== undefined && typeof body.isError !== "boolean") {
175
+ throw invalidField("isError", "a boolean");
176
+ }
66
177
  return {
67
178
  type: "tool-result",
68
- toolCallId: (body.toolCallId as string) ?? "",
69
- toolName: (body.toolName as string) ?? "",
179
+ toolCallId: readString(body, "toolCallId"),
180
+ toolName: readString(body, "toolName"),
70
181
  result: body.result,
71
- isError: (body.isError as boolean) ?? false,
182
+ isError: body.isError ?? false,
72
183
  config,
73
184
  ...(stateDelta != null && { stateDelta }),
74
185
  };
75
186
  }
76
187
 
188
+ if (body.type !== undefined && body.type !== "message") {
189
+ throw invalidField("type", '"message", "tool-result", or omitted');
190
+ }
191
+
192
+ const text = body.message;
193
+ if (text !== undefined && typeof text !== "string") {
194
+ throw invalidField("message", "a string");
195
+ }
196
+
197
+ const parts = body.parts;
198
+ if (
199
+ parts !== undefined &&
200
+ (!Array.isArray(parts) || !parts.every(isRecord))
201
+ ) {
202
+ throw invalidField("parts", "an array of objects");
203
+ }
204
+ parts?.forEach(validatePart);
205
+ if (!("message" in body) && !("parts" in body)) {
206
+ throw new Error(
207
+ 'Invalid Google ADK proxy request: expected a "message" string or a "parts" array.',
208
+ );
209
+ }
210
+
77
211
  return {
78
212
  type: "message",
79
- text: (body.message as string) ?? "",
80
- ...(body.parts != null && {
81
- parts: body.parts as Array<Record<string, unknown>>,
82
- }),
213
+ text: text ?? "",
214
+ ...(parts !== undefined && { parts }),
83
215
  config,
84
216
  ...(stateDelta != null && { stateDelta }),
85
217
  };