@assistant-ui/react-google-adk 0.0.31 → 0.0.32
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.d.ts +24 -26
- package/dist/AdkClient.d.ts.map +1 -1
- package/dist/AdkClient.js +5 -3
- package/dist/AdkClient.js.map +1 -1
- package/dist/AdkEventAccumulator.d.ts +39 -41
- package/dist/AdkEventAccumulator.d.ts.map +1 -1
- package/dist/AdkSessionAdapter.d.ts +38 -39
- package/dist/AdkSessionAdapter.d.ts.map +1 -1
- package/dist/AdkSessionAdapter.js +4 -3
- package/dist/AdkSessionAdapter.js.map +1 -1
- package/dist/adkAuthRequest.d.ts +17 -0
- package/dist/adkAuthRequest.d.ts.map +1 -0
- package/dist/adkAuthRequest.js +45 -0
- package/dist/adkAuthRequest.js.map +1 -0
- package/dist/adkExtras.d.ts +1 -3
- package/dist/adkExtras.d.ts.map +1 -1
- package/dist/adkToolApproval.d.ts +12 -14
- package/dist/adkToolApproval.d.ts.map +1 -1
- package/dist/contentToParts.d.ts +1 -3
- package/dist/contentToParts.d.ts.map +1 -1
- package/dist/convertAdkMessages.d.ts +3 -5
- package/dist/convertAdkMessages.d.ts.map +1 -1
- package/dist/convertAdkMessages.js +4 -3
- package/dist/convertAdkMessages.js.map +1 -1
- package/dist/convertToAdkMessages.d.ts +32 -34
- package/dist/convertToAdkMessages.d.ts.map +1 -1
- package/dist/hooks.d.ts +10 -7
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +11 -9
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +11 -10
- package/dist/index.d.ts.map +1 -0
- package/dist/normalizeAdkPart.d.ts +0 -2
- package/dist/normalizeAdkPart.d.ts.map +1 -1
- package/dist/parseAdkEvent.d.ts +1 -3
- package/dist/parseAdkEvent.d.ts.map +1 -1
- package/dist/raceWithAbortSignal.d.ts +2 -0
- package/dist/raceWithAbortSignal.d.ts.map +1 -0
- package/dist/raceWithAbortSignal.js +45 -0
- package/dist/raceWithAbortSignal.js.map +1 -0
- package/dist/sdkIdentity.d.ts +1 -3
- package/dist/sdkIdentity.d.ts.map +1 -1
- package/dist/sdkIdentity.js +1 -1
- package/dist/server/adkEventStream.d.ts +23 -24
- package/dist/server/adkEventStream.d.ts.map +1 -1
- package/dist/server/adkEventStream.js +23 -15
- package/dist/server/adkEventStream.js.map +1 -1
- package/dist/server/createAdkApiRoute.d.ts +21 -22
- package/dist/server/createAdkApiRoute.d.ts.map +1 -1
- package/dist/server/index.d.ts +4 -4
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/parseAdkRequest.d.ts +16 -17
- package/dist/server/parseAdkRequest.d.ts.map +1 -1
- package/dist/structuredEvents.d.ts +1 -3
- package/dist/structuredEvents.d.ts.map +1 -1
- package/dist/toAdkFunctionResponse.d.ts +0 -2
- package/dist/toAdkFunctionResponse.d.ts.map +1 -1
- package/dist/trimTrailingSlashes.d.ts +0 -2
- package/dist/trimTrailingSlashes.d.ts.map +1 -1
- package/dist/types.d.ts +232 -234
- package/dist/types.d.ts.map +1 -1
- package/dist/useAdkMessages.d.ts +25 -27
- package/dist/useAdkMessages.d.ts.map +1 -1
- package/dist/useAdkRuntime.d.ts +48 -49
- package/dist/useAdkRuntime.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/AdkClient.test.ts +50 -0
- package/src/AdkClient.ts +10 -3
- package/src/AdkSessionAdapter.test.ts +17 -0
- package/src/AdkSessionAdapter.ts +8 -3
- package/src/adkAuthRequest.test.ts +101 -0
- package/src/adkAuthRequest.ts +50 -0
- package/src/convertAdkMessages.test.ts +41 -0
- package/src/convertAdkMessages.ts +19 -7
- package/src/hooks.render.test.tsx +95 -2
- package/src/hooks.ts +12 -13
- package/src/raceWithAbortSignal.test.ts +73 -0
- package/src/raceWithAbortSignal.ts +48 -0
- package/src/server/adkEventStream.test.ts +147 -0
- package/src/server/adkEventStream.ts +27 -20
package/src/hooks.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { generateId } from "@assistant-ui/core";
|
|
|
2
2
|
import { useAui } from "@assistant-ui/store";
|
|
3
3
|
import { useShallowSelector } from "@assistant-ui/store/internal";
|
|
4
4
|
import type { ReadonlyJSONValue } from "assistant-stream/utils";
|
|
5
|
+
import { toAdkAuthReply } from "./adkAuthRequest";
|
|
5
6
|
import { adkExtras } from "./adkExtras";
|
|
6
7
|
import { toAdkConfirmationReply } from "./adkToolApproval";
|
|
7
8
|
import type {
|
|
@@ -74,23 +75,21 @@ export const useAdkConfirmTool = () => {
|
|
|
74
75
|
.send([toAdkConfirmationReply(toolCallId, confirmed, payload)], {});
|
|
75
76
|
};
|
|
76
77
|
|
|
77
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* Returns a function to submit auth credentials for a pending auth request.
|
|
80
|
+
* The reply carries the request's auth config with the credential as its
|
|
81
|
+
* `exchangedAuthCredential`, which is the shape ADK resumes the tool on, so the
|
|
82
|
+
* id must name a request `useAdkAuthRequests` currently lists.
|
|
83
|
+
*/
|
|
78
84
|
export const useAdkSubmitAuth = () => {
|
|
79
85
|
const aui = useAui();
|
|
80
|
-
return (toolCallId: string, credential: AdkAuthCredential) =>
|
|
81
|
-
adkExtras.get(aui)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
id: generateId(),
|
|
85
|
-
type: "tool",
|
|
86
|
-
tool_call_id: toolCallId,
|
|
87
|
-
name: "adk_request_credential",
|
|
88
|
-
content: JSON.stringify(credential),
|
|
89
|
-
status: "success",
|
|
90
|
-
},
|
|
91
|
-
],
|
|
86
|
+
return (toolCallId: string, credential: AdkAuthCredential) => {
|
|
87
|
+
const extras = adkExtras.get(aui);
|
|
88
|
+
return extras.send(
|
|
89
|
+
[toAdkAuthReply(toolCallId, credential, extras.authRequests)],
|
|
92
90
|
{},
|
|
93
91
|
);
|
|
92
|
+
};
|
|
94
93
|
};
|
|
95
94
|
|
|
96
95
|
/** Returns a function to submit the user's answer for a pending `adk_request_input` HITL interrupt. */
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { raceWithAbortSignal } from "./raceWithAbortSignal";
|
|
3
|
+
|
|
4
|
+
describe("raceWithAbortSignal", () => {
|
|
5
|
+
it("invokes the operation synchronously without a signal", async () => {
|
|
6
|
+
const order: string[] = [];
|
|
7
|
+
|
|
8
|
+
const result = raceWithAbortSignal(undefined, () => {
|
|
9
|
+
order.push("operation");
|
|
10
|
+
return "done";
|
|
11
|
+
});
|
|
12
|
+
order.push("after");
|
|
13
|
+
|
|
14
|
+
expect(order).toEqual(["operation", "after"]);
|
|
15
|
+
await expect(result).resolves.toBe("done");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("converts a synchronous operation error to a rejection", async () => {
|
|
19
|
+
const error = new Error("failed");
|
|
20
|
+
|
|
21
|
+
const result = raceWithAbortSignal(undefined, () => {
|
|
22
|
+
throw error;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
await expect(result).rejects.toBe(error);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("rejects a pending operation with the abort reason", async () => {
|
|
29
|
+
const controller = new AbortController();
|
|
30
|
+
const reason = new Error("cancelled");
|
|
31
|
+
let resolveOperation!: (value: string) => void;
|
|
32
|
+
const operation = new Promise<string>((resolve) => {
|
|
33
|
+
resolveOperation = resolve;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const result = raceWithAbortSignal(controller.signal, () => operation);
|
|
37
|
+
controller.abort(reason);
|
|
38
|
+
|
|
39
|
+
await expect(result).rejects.toBe(reason);
|
|
40
|
+
resolveOperation("late result");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("rejects before invoking an operation for an already aborted signal", async () => {
|
|
44
|
+
const controller = new AbortController();
|
|
45
|
+
const reason = new Error("already cancelled");
|
|
46
|
+
const operation = vi.fn(() => "done");
|
|
47
|
+
controller.abort(reason);
|
|
48
|
+
|
|
49
|
+
const result = raceWithAbortSignal(controller.signal, operation);
|
|
50
|
+
|
|
51
|
+
await expect(result).rejects.toBe(reason);
|
|
52
|
+
expect(operation).not.toHaveBeenCalled();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("removes the abort listener after the operation settles", async () => {
|
|
56
|
+
const controller = new AbortController();
|
|
57
|
+
const removeEventListener = vi.spyOn(
|
|
58
|
+
controller.signal,
|
|
59
|
+
"removeEventListener",
|
|
60
|
+
);
|
|
61
|
+
const result = raceWithAbortSignal(controller.signal, () => "done");
|
|
62
|
+
|
|
63
|
+
await expect(result).resolves.toBe("done");
|
|
64
|
+
expect(removeEventListener).toHaveBeenCalledOnce();
|
|
65
|
+
expect(removeEventListener).toHaveBeenCalledWith(
|
|
66
|
+
"abort",
|
|
67
|
+
expect.any(Function),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
controller.abort(new Error("late abort"));
|
|
71
|
+
await expect(result).resolves.toBe("done");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const getAbortReason = (signal: AbortSignal): unknown => {
|
|
2
|
+
if (signal.reason !== undefined) return signal.reason;
|
|
3
|
+
const error = new Error("The operation was aborted");
|
|
4
|
+
error.name = "AbortError";
|
|
5
|
+
return error;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const raceWithAbortSignal = <T>(
|
|
9
|
+
signal: AbortSignal | undefined,
|
|
10
|
+
operation: () => T | PromiseLike<T>,
|
|
11
|
+
): Promise<T> => {
|
|
12
|
+
if (!signal) {
|
|
13
|
+
try {
|
|
14
|
+
return Promise.resolve(operation());
|
|
15
|
+
} catch (error) {
|
|
16
|
+
return Promise.reject(error);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (signal.aborted) return Promise.reject(getAbortReason(signal));
|
|
20
|
+
|
|
21
|
+
return new Promise<T>((resolve, reject) => {
|
|
22
|
+
let settled = false;
|
|
23
|
+
const cleanup = () => signal.removeEventListener("abort", handleAbort);
|
|
24
|
+
const resolveOnce = (value: T) => {
|
|
25
|
+
if (settled) return;
|
|
26
|
+
settled = true;
|
|
27
|
+
cleanup();
|
|
28
|
+
resolve(value);
|
|
29
|
+
};
|
|
30
|
+
const rejectOnce = (error: unknown) => {
|
|
31
|
+
if (settled) return;
|
|
32
|
+
settled = true;
|
|
33
|
+
cleanup();
|
|
34
|
+
reject(error);
|
|
35
|
+
};
|
|
36
|
+
const handleAbort = () => rejectOnce(getAbortReason(signal));
|
|
37
|
+
|
|
38
|
+
signal.addEventListener("abort", handleAbort, { once: true });
|
|
39
|
+
let result: T | PromiseLike<T>;
|
|
40
|
+
try {
|
|
41
|
+
result = operation();
|
|
42
|
+
} catch (error) {
|
|
43
|
+
rejectOnce(error);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
Promise.resolve(result).then(resolveOnce, rejectOnce);
|
|
47
|
+
});
|
|
48
|
+
};
|
|
@@ -20,6 +20,153 @@ async function readSSE(response: Response): Promise<string> {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
describe("adkEventStream", () => {
|
|
23
|
+
it("does not advance the generator before the response is read", async () => {
|
|
24
|
+
let produced = 0;
|
|
25
|
+
async function* events() {
|
|
26
|
+
for (let index = 0; index < 1000; index++) {
|
|
27
|
+
produced++;
|
|
28
|
+
yield { id: String(index) };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const response = adkEventStream(events());
|
|
32
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
33
|
+
try {
|
|
34
|
+
expect(produced).toBe(0);
|
|
35
|
+
} finally {
|
|
36
|
+
await response.body!.cancel();
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("buffers at most one event ahead of a slow reader", async () => {
|
|
41
|
+
let produced = 0;
|
|
42
|
+
async function* events() {
|
|
43
|
+
for (let index = 0; index < 1000; index++) {
|
|
44
|
+
produced++;
|
|
45
|
+
yield { id: String(index) };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const reader = adkEventStream(events()).body!.getReader();
|
|
49
|
+
try {
|
|
50
|
+
expect(new TextDecoder().decode((await reader.read()).value)).toBe(
|
|
51
|
+
":ok\n\n",
|
|
52
|
+
);
|
|
53
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
54
|
+
expect(produced).toBe(1);
|
|
55
|
+
expect(new TextDecoder().decode((await reader.read()).value)).toContain(
|
|
56
|
+
'"id":"0"',
|
|
57
|
+
);
|
|
58
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
59
|
+
expect(produced).toBe(2);
|
|
60
|
+
} finally {
|
|
61
|
+
await reader.cancel();
|
|
62
|
+
reader.releaseLock();
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("finalizes the generator when the reader cancels between events", async () => {
|
|
67
|
+
const finalized = vi.fn();
|
|
68
|
+
const onError = vi.fn();
|
|
69
|
+
async function* events() {
|
|
70
|
+
try {
|
|
71
|
+
while (true) yield { id: "event" };
|
|
72
|
+
} finally {
|
|
73
|
+
finalized();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const reader = adkEventStream(events(), { onError }).body!.getReader();
|
|
77
|
+
await reader.read();
|
|
78
|
+
await reader.read();
|
|
79
|
+
await reader.cancel();
|
|
80
|
+
expect(finalized).toHaveBeenCalledTimes(1);
|
|
81
|
+
expect(onError).not.toHaveBeenCalled();
|
|
82
|
+
expect(await reader.read()).toEqual({ done: true, value: undefined });
|
|
83
|
+
reader.releaseLock();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("delivers every event in order when the response is fully consumed", async () => {
|
|
87
|
+
async function* events() {
|
|
88
|
+
for (let index = 0; index < 1000; index++) yield { id: String(index) };
|
|
89
|
+
}
|
|
90
|
+
const text = await readSSE(adkEventStream(events()));
|
|
91
|
+
const ids = text
|
|
92
|
+
.split("\n\n")
|
|
93
|
+
.filter((line) => line.startsWith("data: "))
|
|
94
|
+
.map((line) => JSON.parse(line.slice(6)).id);
|
|
95
|
+
expect(ids).toEqual(
|
|
96
|
+
Array.from({ length: 1000 }, (_, index) => String(index)),
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("allows onError to cancel the reader without writing to the closed stream", async () => {
|
|
101
|
+
let reader: ReadableStreamDefaultReader<Uint8Array>;
|
|
102
|
+
let cancelled: Promise<void> | undefined;
|
|
103
|
+
async function* events() {
|
|
104
|
+
throw new Error("stream failed");
|
|
105
|
+
}
|
|
106
|
+
const response = adkEventStream(events(), {
|
|
107
|
+
onError: () => {
|
|
108
|
+
cancelled = reader.cancel();
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
reader = response.body!.getReader();
|
|
112
|
+
await reader.read();
|
|
113
|
+
expect(await reader.read()).toEqual({ done: true, value: undefined });
|
|
114
|
+
await cancelled;
|
|
115
|
+
reader.releaseLock();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it.each(["yield", "finish", "reject"])(
|
|
119
|
+
"does not write after cancellation while next() later %s",
|
|
120
|
+
async (outcome) => {
|
|
121
|
+
let release!: () => void;
|
|
122
|
+
const waiting = new Promise<void>((resolve) => {
|
|
123
|
+
release = resolve;
|
|
124
|
+
});
|
|
125
|
+
let started!: () => void;
|
|
126
|
+
const entered = new Promise<void>((resolve) => {
|
|
127
|
+
started = resolve;
|
|
128
|
+
});
|
|
129
|
+
const finalized = vi.fn();
|
|
130
|
+
const onError = vi.fn();
|
|
131
|
+
async function* events() {
|
|
132
|
+
try {
|
|
133
|
+
started();
|
|
134
|
+
await waiting;
|
|
135
|
+
if (outcome === "reject") throw new Error("late error");
|
|
136
|
+
if (outcome === "yield") yield { id: "late" };
|
|
137
|
+
} finally {
|
|
138
|
+
finalized();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const reader = adkEventStream(events(), { onError }).body!.getReader();
|
|
142
|
+
await reader.read();
|
|
143
|
+
await entered;
|
|
144
|
+
const cancelled = reader.cancel();
|
|
145
|
+
release();
|
|
146
|
+
await cancelled;
|
|
147
|
+
expect(finalized).toHaveBeenCalledTimes(1);
|
|
148
|
+
expect(onError).not.toHaveBeenCalled();
|
|
149
|
+
expect(await reader.read()).toEqual({ done: true, value: undefined });
|
|
150
|
+
reader.releaseLock();
|
|
151
|
+
},
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
it("finalizes the source and emits an error frame if event serialization fails", async () => {
|
|
155
|
+
const finalized = vi.fn();
|
|
156
|
+
const onError = vi.fn();
|
|
157
|
+
async function* events() {
|
|
158
|
+
try {
|
|
159
|
+
yield { id: "invalid", customMetadata: { unsupported: 1n } };
|
|
160
|
+
} finally {
|
|
161
|
+
finalized();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const text = await readSSE(adkEventStream(events(), { onError }));
|
|
165
|
+
expect(text).toContain('"errorCode":"STREAM_ERROR"');
|
|
166
|
+
expect(finalized).toHaveBeenCalledTimes(1);
|
|
167
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
168
|
+
});
|
|
169
|
+
|
|
23
170
|
it("returns a Response with text/event-stream content-type", () => {
|
|
24
171
|
const response = adkEventStream(yieldEvents([]));
|
|
25
172
|
expect(response.headers.get("Content-Type")).toBe("text/event-stream");
|
|
@@ -133,31 +133,38 @@ export const adkEventStream = (
|
|
|
133
133
|
const encoder = new TextEncoder();
|
|
134
134
|
let cancelled = false;
|
|
135
135
|
const stream = new ReadableStream({
|
|
136
|
-
|
|
136
|
+
start(controller) {
|
|
137
137
|
// Initial SSE comment to keep connection alive through proxies
|
|
138
138
|
controller.enqueue(encoder.encode(":ok\n\n"));
|
|
139
|
+
},
|
|
140
|
+
async pull(controller) {
|
|
139
141
|
try {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
controller.
|
|
144
|
-
|
|
145
|
-
);
|
|
142
|
+
const { done, value } = await events.next();
|
|
143
|
+
if (cancelled) return;
|
|
144
|
+
if (done) {
|
|
145
|
+
controller.close();
|
|
146
|
+
return;
|
|
146
147
|
}
|
|
148
|
+
const wireEvent = convertSdkEvent(value);
|
|
149
|
+
controller.enqueue(
|
|
150
|
+
encoder.encode(`data: ${JSON.stringify(wireEvent)}\n\n`),
|
|
151
|
+
);
|
|
147
152
|
} catch (e) {
|
|
148
|
-
if (
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
|
|
153
|
+
if (cancelled) return;
|
|
154
|
+
try {
|
|
155
|
+
await events.return?.(undefined as any);
|
|
156
|
+
} catch {}
|
|
157
|
+
if (cancelled) return;
|
|
158
|
+
notifyError(options?.onError, e);
|
|
159
|
+
if (cancelled) return;
|
|
160
|
+
const errorEvent: AdkEvent = {
|
|
161
|
+
id: "",
|
|
162
|
+
errorCode: "STREAM_ERROR",
|
|
163
|
+
errorMessage: e instanceof Error ? e.message : "Unknown stream error",
|
|
164
|
+
};
|
|
165
|
+
controller.enqueue(
|
|
166
|
+
encoder.encode(`data: ${JSON.stringify(errorEvent)}\n\n`),
|
|
167
|
+
);
|
|
161
168
|
controller.close();
|
|
162
169
|
}
|
|
163
170
|
},
|