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

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 (50) 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.map +1 -1
  5. package/dist/AdkEventAccumulator.js +11 -1
  6. package/dist/AdkEventAccumulator.js.map +1 -1
  7. package/dist/AdkSessionAdapter.d.ts.map +1 -1
  8. package/dist/AdkSessionAdapter.js +2 -1
  9. package/dist/AdkSessionAdapter.js.map +1 -1
  10. package/dist/adkToolApproval.d.ts +40 -0
  11. package/dist/adkToolApproval.d.ts.map +1 -0
  12. package/dist/adkToolApproval.js +166 -0
  13. package/dist/adkToolApproval.js.map +1 -0
  14. package/dist/convertAdkMessages.d.ts +3 -1
  15. package/dist/convertAdkMessages.d.ts.map +1 -1
  16. package/dist/convertAdkMessages.js +27 -16
  17. package/dist/convertAdkMessages.js.map +1 -1
  18. package/dist/hooks.d.ts.map +1 -1
  19. package/dist/hooks.js +2 -11
  20. package/dist/hooks.js.map +1 -1
  21. package/dist/parseAdkEvent.d.ts +6 -0
  22. package/dist/parseAdkEvent.d.ts.map +1 -0
  23. package/dist/parseAdkEvent.js +16 -0
  24. package/dist/parseAdkEvent.js.map +1 -0
  25. package/dist/server/parseAdkRequest.d.ts.map +1 -1
  26. package/dist/server/parseAdkRequest.js +79 -13
  27. package/dist/server/parseAdkRequest.js.map +1 -1
  28. package/dist/useAdkMessages.js +104 -64
  29. package/dist/useAdkMessages.js.map +1 -1
  30. package/dist/useAdkRuntime.d.ts.map +1 -1
  31. package/dist/useAdkRuntime.js +11 -3
  32. package/dist/useAdkRuntime.js.map +1 -1
  33. package/package.json +5 -5
  34. package/src/AdkClient.test.ts +137 -0
  35. package/src/AdkClient.ts +15 -3
  36. package/src/AdkEventAccumulator.test.ts +77 -0
  37. package/src/AdkEventAccumulator.ts +24 -1
  38. package/src/AdkSessionAdapter.test.ts +145 -1
  39. package/src/AdkSessionAdapter.ts +7 -2
  40. package/src/adkToolApproval.test.ts +345 -0
  41. package/src/adkToolApproval.ts +215 -0
  42. package/src/convertAdkMessages.ts +72 -43
  43. package/src/hooks.ts +4 -16
  44. package/src/parseAdkEvent.ts +40 -0
  45. package/src/server/parseAdkRequest.test.ts +141 -0
  46. package/src/server/parseAdkRequest.ts +145 -13
  47. package/src/useAdkMessages.test.ts +313 -0
  48. package/src/useAdkMessages.ts +62 -4
  49. package/src/useAdkRuntime.ts +29 -3
  50. package/src/useAdkRuntimeApproval.test.tsx +344 -0
package/src/hooks.ts CHANGED
@@ -2,6 +2,7 @@ import { useAui } from "@assistant-ui/store";
2
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. */
@@ -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
  };