@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/dist/useAdkRuntime.d.ts
CHANGED
|
@@ -1,53 +1,52 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
1
|
+
import { type AttachmentAdapter, type DictationAdapter, type ExternalStoreSharedOptions, type FeedbackAdapter, type RealtimeVoiceAdapter, type SpeechSynthesisAdapter } from "@assistant-ui/core";
|
|
2
|
+
import type { AssistantCloud } from "assistant-cloud";
|
|
3
|
+
import type { RemoteThreadListAdapter } from "@assistant-ui/core";
|
|
4
|
+
import type { AdkMessage, AdkThreadSnapshot, AdkStreamCallback, OnAdkErrorCallback, OnAdkCustomEventCallback, OnAdkAgentTransferCallback } from "./types.js";
|
|
5
5
|
export type UseAdkRuntimeOptions = ExternalStoreSharedOptions & {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
6
|
+
stream: AdkStreamCallback;
|
|
7
|
+
/**
|
|
8
|
+
* Called whenever the active thread's canonical (remote) ID changes, so the
|
|
9
|
+
* value can be treated as a managed/controlled variable (e.g. synced to a URL
|
|
10
|
+
* query param). Only the settled remote ID is emitted: while a freshly created
|
|
11
|
+
* thread is still optimistic the value is `undefined`, and the real ID is
|
|
12
|
+
* emitted once the thread is initialized; the transient local ID is never
|
|
13
|
+
* surfaced.
|
|
14
|
+
*/
|
|
15
|
+
onThreadIdChange?: ((threadId: string | undefined) => void) | undefined;
|
|
16
|
+
autoCancelPendingToolCalls?: boolean | undefined;
|
|
17
|
+
unstable_allowCancellation?: boolean | undefined;
|
|
18
|
+
getCheckpointId?: (threadId: string, parentMessages: AdkMessage[]) => Promise<string | null>;
|
|
19
|
+
/**
|
|
20
|
+
* Loads a thread's stored state. Called when the thread opens, and again for
|
|
21
|
+
* `threads.reloadMainThread()`, which refetches in place rather than
|
|
22
|
+
* remounting the runtime; the signal aborts a load the runtime no longer
|
|
23
|
+
* needs.
|
|
24
|
+
*/
|
|
25
|
+
load?: (threadId: string, options?: {
|
|
26
|
+
signal?: AbortSignal | undefined;
|
|
27
|
+
}) => Promise<AdkThreadSnapshot>;
|
|
28
|
+
create?: () => Promise<{
|
|
29
|
+
externalId: string;
|
|
30
|
+
}>;
|
|
31
|
+
delete?: (threadId: string) => Promise<void>;
|
|
32
|
+
adapters?: {
|
|
33
|
+
attachments?: AttachmentAdapter;
|
|
34
|
+
speech?: SpeechSynthesisAdapter;
|
|
35
|
+
dictation?: DictationAdapter;
|
|
36
|
+
voice?: RealtimeVoiceAdapter;
|
|
37
|
+
feedback?: FeedbackAdapter;
|
|
38
|
+
} | undefined;
|
|
39
|
+
eventHandlers?: {
|
|
40
|
+
onError?: OnAdkErrorCallback;
|
|
41
|
+
onCustomEvent?: OnAdkCustomEventCallback;
|
|
42
|
+
onAgentTransfer?: OnAdkAgentTransferCallback;
|
|
43
|
+
} | undefined;
|
|
44
|
+
cloud?: AssistantCloud | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* A `RemoteThreadListAdapter` to use instead of the cloud adapter.
|
|
47
|
+
* Use with `createAdkSessionAdapter` for ADK session-backed persistence.
|
|
48
|
+
*/
|
|
49
|
+
sessionAdapter?: RemoteThreadListAdapter | undefined;
|
|
50
50
|
};
|
|
51
51
|
export declare const useAdkRuntime: ({ cloud, sessionAdapter, create, delete: deleteFn, onThreadIdChange, ...options }: UseAdkRuntimeOptions) => import("@assistant-ui/core").AssistantRuntime;
|
|
52
|
-
//#endregion
|
|
53
52
|
//# sourceMappingURL=useAdkRuntime.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAdkRuntime.d.ts","
|
|
1
|
+
{"version":3,"file":"useAdkRuntime.d.ts","sourceRoot":"","sources":["../src/useAdkRuntime.ts"],"names":[],"mappings":"AAQA,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,0BAA0B,EAC/B,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAK5B,MAAM,oBAAoB,CAAC;AAY5B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAClE,OAAO,KAAK,EACV,UAAU,EACV,iBAAiB,EAEjB,iBAAiB,EACjB,kBAAkB,EAClB,wBAAwB,EACxB,0BAA0B,EAC3B,mBAAgB;AAmBjB,MAAM,MAAM,oBAAoB,GAAG,0BAA0B,GAAG;IAC9D,MAAM,EAAE,iBAAiB,CAAC;IAC1B;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;IACxE,0BAA0B,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACjD,0BAA0B,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACjD,eAAe,CAAC,EAAE,CAChB,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,UAAU,EAAE,KACzB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC5B;;;;;OAKG;IACH,IAAI,CAAC,EAAE,CACL,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAA;KAAE,KAC3C,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAChC,MAAM,CAAC,EAAE,MAAM,OAAO,CAAC;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/C,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,QAAQ,CAAC,EACL;QACE,WAAW,CAAC,EAAE,iBAAiB,CAAC;QAChC,MAAM,CAAC,EAAE,sBAAsB,CAAC;QAChC,SAAS,CAAC,EAAE,gBAAgB,CAAC;QAC7B,KAAK,CAAC,EAAE,oBAAoB,CAAC;QAC7B,QAAQ,CAAC,EAAE,eAAe,CAAC;KAC5B,GACD,SAAS,CAAC;IACd,aAAa,CAAC,EACV;QACE,OAAO,CAAC,EAAE,kBAAkB,CAAC;QAC7B,aAAa,CAAC,EAAE,wBAAwB,CAAC;QACzC,eAAe,CAAC,EAAE,0BAA0B,CAAC;KAC9C,GACD,SAAS,CAAC;IACd,KAAK,CAAC,EAAE,cAAc,GAAG,SAAS,CAAC;IACnC;;;OAGG;IACH,cAAc,CAAC,EAAE,uBAAuB,GAAG,SAAS,CAAC;CACtD,CAAC;AAiYF,eAAO,MAAM,aAAa,GAAI,mFAO3B,oBAAoB,kDAsBtB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assistant-ui/react-google-adk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.32",
|
|
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.3.
|
|
38
|
-
"@assistant-ui/store": "^0.3.
|
|
37
|
+
"@assistant-ui/core": "^0.3.21",
|
|
38
|
+
"@assistant-ui/store": "^0.3.15",
|
|
39
39
|
"assistant-cloud": "*",
|
|
40
|
-
"assistant-stream": "^0.3.
|
|
40
|
+
"assistant-stream": "^0.3.45"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"@google/adk": ">=0.5.0",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
}
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"@assistant-ui/x-buildutils": "0.0.
|
|
56
|
+
"@assistant-ui/x-buildutils": "0.0.29",
|
|
57
57
|
"@testing-library/dom": "^10.4.2",
|
|
58
58
|
"@testing-library/react": "^16.3.3",
|
|
59
59
|
"@types/react": "^19.3.0",
|
package/src/AdkClient.test.ts
CHANGED
|
@@ -124,6 +124,24 @@ describe.each(["direct", "proxy", "proxy batch"] as const)(
|
|
|
124
124
|
// ── Proxy mode ──
|
|
125
125
|
|
|
126
126
|
describe("createAdkStream - proxy mode", () => {
|
|
127
|
+
it("aborts while dynamic headers are pending", async () => {
|
|
128
|
+
const controller = new AbortController();
|
|
129
|
+
const stream = createAdkStream({
|
|
130
|
+
api: "/api/adk",
|
|
131
|
+
headers: () => new Promise<Record<string, string>>(() => {}),
|
|
132
|
+
});
|
|
133
|
+
const events = await stream(
|
|
134
|
+
[{ id: "m1", type: "human", content: "Hello" }],
|
|
135
|
+
makeConfig({ abortSignal: controller.signal }),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const next = events.next();
|
|
139
|
+
controller.abort();
|
|
140
|
+
|
|
141
|
+
await expect(next).rejects.toBe(controller.signal.reason);
|
|
142
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
143
|
+
});
|
|
144
|
+
|
|
127
145
|
it("accumulates snake_case image and file parts from SSE", async () => {
|
|
128
146
|
const event = {
|
|
129
147
|
id: "media",
|
|
@@ -1077,6 +1095,38 @@ describe("createAdkStream - error handling", () => {
|
|
|
1077
1095
|
'Expected ADK stream response Content-Type "text/event-stream", received no Content-Type header',
|
|
1078
1096
|
);
|
|
1079
1097
|
});
|
|
1098
|
+
|
|
1099
|
+
it("rejects event-stream responses without a body", async () => {
|
|
1100
|
+
mockFetch.mockResolvedValueOnce(sseResponse(null));
|
|
1101
|
+
|
|
1102
|
+
const stream = createAdkStream({ api: "/api/adk" });
|
|
1103
|
+
await expect(async () => {
|
|
1104
|
+
const gen = await stream(
|
|
1105
|
+
[{ id: "m1", type: "human", content: "Hi" }],
|
|
1106
|
+
makeConfig(),
|
|
1107
|
+
);
|
|
1108
|
+
for await (const _ of gen) {
|
|
1109
|
+
/* noop */
|
|
1110
|
+
}
|
|
1111
|
+
}).rejects.toThrow("Expected ADK stream response body, received no body");
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
it("rejects non-standard responses with an undefined body", async () => {
|
|
1115
|
+
const response = sseResponse(null);
|
|
1116
|
+
Object.defineProperty(response, "body", { value: undefined });
|
|
1117
|
+
mockFetch.mockResolvedValueOnce(response);
|
|
1118
|
+
|
|
1119
|
+
const stream = createAdkStream({ api: "/api/adk" });
|
|
1120
|
+
await expect(async () => {
|
|
1121
|
+
const gen = await stream(
|
|
1122
|
+
[{ id: "m1", type: "human", content: "Hi" }],
|
|
1123
|
+
makeConfig(),
|
|
1124
|
+
);
|
|
1125
|
+
for await (const _ of gen) {
|
|
1126
|
+
/* noop */
|
|
1127
|
+
}
|
|
1128
|
+
}).rejects.toThrow("Expected ADK stream response body, received no body");
|
|
1129
|
+
});
|
|
1080
1130
|
});
|
|
1081
1131
|
|
|
1082
1132
|
// ── Headers ──
|
package/src/AdkClient.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SSEEventDecoder } from "assistant-stream/utils";
|
|
2
2
|
import { contentToParts } from "./contentToParts";
|
|
3
3
|
import { parseAdkEventValue } from "./parseAdkEvent";
|
|
4
|
+
import { raceWithAbortSignal } from "./raceWithAbortSignal";
|
|
4
5
|
import { toAdkFunctionResponse } from "./toAdkFunctionResponse";
|
|
5
6
|
import { trimTrailingSlashes } from "./trimTrailingSlashes";
|
|
6
7
|
import type {
|
|
@@ -76,7 +77,7 @@ export function createAdkStream(
|
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
return async function* (messages, config) {
|
|
79
|
-
const headers = await resolveHeaders(options.headers);
|
|
80
|
+
const headers = await resolveHeaders(options.headers, config.abortSignal);
|
|
80
81
|
|
|
81
82
|
let url: string;
|
|
82
83
|
let body: unknown;
|
|
@@ -149,9 +150,12 @@ async function resolveHeaders(
|
|
|
149
150
|
| Record<string, string>
|
|
150
151
|
| (() => Record<string, string> | Promise<Record<string, string>>)
|
|
151
152
|
| undefined,
|
|
153
|
+
signal?: AbortSignal,
|
|
152
154
|
): Promise<Record<string, string>> {
|
|
153
155
|
if (!headers) return {};
|
|
154
|
-
if (typeof headers === "function")
|
|
156
|
+
if (typeof headers === "function") {
|
|
157
|
+
return await raceWithAbortSignal(signal, headers);
|
|
158
|
+
}
|
|
155
159
|
return headers;
|
|
156
160
|
}
|
|
157
161
|
|
|
@@ -252,7 +256,10 @@ function messagesToProxyBody(
|
|
|
252
256
|
}
|
|
253
257
|
|
|
254
258
|
async function* parseSSEResponse(response: Response): AsyncGenerator<AdkEvent> {
|
|
255
|
-
|
|
259
|
+
if (!response.body) {
|
|
260
|
+
throw new Error("Expected ADK stream response body, received no body");
|
|
261
|
+
}
|
|
262
|
+
const reader = response.body.getReader();
|
|
256
263
|
const decoder = new TextDecoder();
|
|
257
264
|
const sseDecoder = new SSEEventDecoder({ trailing: "dispatch" });
|
|
258
265
|
|
|
@@ -22,6 +22,23 @@ const baseOptions = {
|
|
|
22
22
|
const expectedBaseUrl =
|
|
23
23
|
"http://localhost:8000/apps/my-app/users/user-1/sessions";
|
|
24
24
|
|
|
25
|
+
describe("createAdkSessionAdapter - load cancellation", () => {
|
|
26
|
+
it("aborts while dynamic headers are pending", async () => {
|
|
27
|
+
const controller = new AbortController();
|
|
28
|
+
const reason = new Error("cancelled");
|
|
29
|
+
const { load } = createAdkSessionAdapter({
|
|
30
|
+
...baseOptions,
|
|
31
|
+
headers: () => new Promise<Record<string, string>>(() => {}),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const result = load("session-1", { signal: controller.signal });
|
|
35
|
+
controller.abort(reason);
|
|
36
|
+
|
|
37
|
+
await expect(result).rejects.toBe(reason);
|
|
38
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
25
42
|
// ── adapter.list() ──
|
|
26
43
|
|
|
27
44
|
describe("createAdkSessionAdapter - list", () => {
|
package/src/AdkSessionAdapter.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { normalizeAdkPart } from "./normalizeAdkPart";
|
|
|
10
10
|
import { parseAdkEventValue } from "./parseAdkEvent";
|
|
11
11
|
import type { AdkMessage, AdkThreadSnapshot } from "./types";
|
|
12
12
|
import { trimTrailingSlashes } from "./trimTrailingSlashes";
|
|
13
|
+
import { raceWithAbortSignal } from "./raceWithAbortSignal";
|
|
13
14
|
|
|
14
15
|
export type AdkSessionAdapterOptions = {
|
|
15
16
|
/**
|
|
@@ -230,9 +231,13 @@ export function createAdkSessionAdapter(
|
|
|
230
231
|
const normalizedApiUrl = trimTrailingSlashes(apiUrl);
|
|
231
232
|
const baseUrl = `${normalizedApiUrl}/apps/${encodeURIComponent(appName)}/users/${encodeURIComponent(userId)}/sessions`;
|
|
232
233
|
|
|
233
|
-
const getHeaders = async (
|
|
234
|
+
const getHeaders = async (
|
|
235
|
+
signal?: AbortSignal,
|
|
236
|
+
): Promise<Record<string, string>> => {
|
|
234
237
|
if (!options.headers) return {};
|
|
235
|
-
if (typeof options.headers === "function")
|
|
238
|
+
if (typeof options.headers === "function") {
|
|
239
|
+
return await raceWithAbortSignal(signal, options.headers);
|
|
240
|
+
}
|
|
236
241
|
return options.headers;
|
|
237
242
|
};
|
|
238
243
|
|
|
@@ -327,7 +332,7 @@ export function createAdkSessionAdapter(
|
|
|
327
332
|
sessionId: string,
|
|
328
333
|
options?: { signal?: AbortSignal | undefined },
|
|
329
334
|
): Promise<AdkThreadSnapshot> => {
|
|
330
|
-
const headers = await getHeaders();
|
|
335
|
+
const headers = await getHeaders(options?.signal);
|
|
331
336
|
const res = await fetch(`${baseUrl}/${encodeURIComponent(sessionId)}`, {
|
|
332
337
|
headers,
|
|
333
338
|
...(options?.signal ? { signal: options.signal } : {}),
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { toAdkAuthReply } from "./adkAuthRequest";
|
|
3
|
+
import type { AdkAuthRequest } from "./types";
|
|
4
|
+
|
|
5
|
+
const CREDENTIAL_CALL = "adk-credential-1";
|
|
6
|
+
|
|
7
|
+
const apiKeyScheme = { type: "apiKey", in: "header", name: "x-api-key" };
|
|
8
|
+
|
|
9
|
+
const request = (authConfig: unknown): AdkAuthRequest => ({
|
|
10
|
+
toolCallId: CREDENTIAL_CALL,
|
|
11
|
+
authConfig,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const wire = (content: string) => JSON.parse(content);
|
|
15
|
+
|
|
16
|
+
describe("toAdkAuthReply", () => {
|
|
17
|
+
it("answers the credential call with the request's config carrying the credential", () => {
|
|
18
|
+
const reply = toAdkAuthReply(
|
|
19
|
+
CREDENTIAL_CALL,
|
|
20
|
+
{ authType: "apiKey", apiKey: "secret" },
|
|
21
|
+
[request({ authScheme: apiKeyScheme, credentialKey: "weather" })],
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
expect(reply).toMatchObject({
|
|
25
|
+
type: "tool",
|
|
26
|
+
tool_call_id: CREDENTIAL_CALL,
|
|
27
|
+
name: "adk_request_credential",
|
|
28
|
+
status: "success",
|
|
29
|
+
});
|
|
30
|
+
expect(wire(reply.content)).toEqual({
|
|
31
|
+
authScheme: apiKeyScheme,
|
|
32
|
+
credentialKey: "weather",
|
|
33
|
+
exchangedAuthCredential: { authType: "apiKey", apiKey: "secret" },
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("lays the OAuth2 answer over the exchanged credential the request issued", () => {
|
|
38
|
+
const oauth2Scheme = { type: "oauth2", flows: {} };
|
|
39
|
+
const reply = toAdkAuthReply(
|
|
40
|
+
CREDENTIAL_CALL,
|
|
41
|
+
{
|
|
42
|
+
authType: "oauth2",
|
|
43
|
+
oauth2: { authResponseUri: "https://app/cb?code=c&state=s" },
|
|
44
|
+
},
|
|
45
|
+
[
|
|
46
|
+
request({
|
|
47
|
+
authScheme: oauth2Scheme,
|
|
48
|
+
credentialKey: "calendar",
|
|
49
|
+
rawAuthCredential: { authType: "oauth2", oauth2: { clientId: "id" } },
|
|
50
|
+
exchangedAuthCredential: {
|
|
51
|
+
authType: "oauth2",
|
|
52
|
+
oauth2: {
|
|
53
|
+
clientId: "id",
|
|
54
|
+
authUri: "https://idp/authorize?state=s",
|
|
55
|
+
state: "s",
|
|
56
|
+
redirectUri: "https://app/cb",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
}),
|
|
60
|
+
],
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
expect(wire(reply.content)).toEqual({
|
|
64
|
+
authScheme: oauth2Scheme,
|
|
65
|
+
credentialKey: "calendar",
|
|
66
|
+
rawAuthCredential: { authType: "oauth2", oauth2: { clientId: "id" } },
|
|
67
|
+
exchangedAuthCredential: {
|
|
68
|
+
authType: "oauth2",
|
|
69
|
+
oauth2: {
|
|
70
|
+
clientId: "id",
|
|
71
|
+
authUri: "https://idp/authorize?state=s",
|
|
72
|
+
state: "s",
|
|
73
|
+
redirectUri: "https://app/cb",
|
|
74
|
+
authResponseUri: "https://app/cb?code=c&state=s",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("sends the credential alone when the request carried no config", () => {
|
|
81
|
+
const reply = toAdkAuthReply(
|
|
82
|
+
CREDENTIAL_CALL,
|
|
83
|
+
{ authType: "apiKey", apiKey: "secret" },
|
|
84
|
+
[request(undefined)],
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
expect(wire(reply.content)).toEqual({
|
|
88
|
+
exchangedAuthCredential: { authType: "apiKey", apiKey: "secret" },
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("throws for an id that is not a pending request", () => {
|
|
93
|
+
expect(() =>
|
|
94
|
+
toAdkAuthReply("adk-credential-2", { authType: "apiKey", apiKey: "x" }, [
|
|
95
|
+
request({ authScheme: apiKeyScheme, credentialKey: "weather" }),
|
|
96
|
+
]),
|
|
97
|
+
).toThrow(
|
|
98
|
+
'No pending ADK auth request for tool call id "adk-credential-2"',
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { generateId } from "@assistant-ui/core";
|
|
2
|
+
import { isRecord } from "@assistant-ui/core/internal";
|
|
3
|
+
import type { AdkAuthCredential, AdkAuthRequest, AdkMessage } from "./types";
|
|
4
|
+
|
|
5
|
+
export const ADK_REQUEST_CREDENTIAL = "adk_request_credential";
|
|
6
|
+
|
|
7
|
+
const recordOf = (value: unknown): Record<string, unknown> =>
|
|
8
|
+
isRecord(value) ? value : {};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* ADK binds a credential reply to the `adk_request_credential` call it issued
|
|
12
|
+
* and takes only `exchangedAuthCredential` off the response, so the reply is
|
|
13
|
+
* the request's own auth config with the credential filled in, as ADK's
|
|
14
|
+
* reference client sends it: adk-python validates the response as a whole auth
|
|
15
|
+
* config, and it exchanges an OAuth2 code with the client id, redirect uri and
|
|
16
|
+
* state the request carried, so the caller's fields land over the request's
|
|
17
|
+
* exchanged credential rather than replacing it. An id that is not a pending
|
|
18
|
+
* request would send a reply adk-js ignores and adk-python answers by
|
|
19
|
+
* requesting again, so it throws instead.
|
|
20
|
+
*/
|
|
21
|
+
export const toAdkAuthReply = (
|
|
22
|
+
toolCallId: string,
|
|
23
|
+
credential: AdkAuthCredential,
|
|
24
|
+
requests: readonly AdkAuthRequest[],
|
|
25
|
+
): AdkMessage & { type: "tool" } => {
|
|
26
|
+
const request = requests.find((r) => r.toolCallId === toolCallId);
|
|
27
|
+
if (request === undefined)
|
|
28
|
+
throw new Error(
|
|
29
|
+
`No pending ADK auth request for tool call id "${toolCallId}"`,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const authConfig = recordOf(request.authConfig);
|
|
33
|
+
const requested = recordOf(authConfig.exchangedAuthCredential);
|
|
34
|
+
const oauth2 = { ...recordOf(requested.oauth2), ...credential.oauth2 };
|
|
35
|
+
return {
|
|
36
|
+
id: generateId(),
|
|
37
|
+
type: "tool",
|
|
38
|
+
tool_call_id: toolCallId,
|
|
39
|
+
name: ADK_REQUEST_CREDENTIAL,
|
|
40
|
+
content: JSON.stringify({
|
|
41
|
+
...authConfig,
|
|
42
|
+
exchangedAuthCredential: {
|
|
43
|
+
...requested,
|
|
44
|
+
...credential,
|
|
45
|
+
...(Object.keys(oauth2).length > 0 && { oauth2 }),
|
|
46
|
+
},
|
|
47
|
+
}),
|
|
48
|
+
status: "success",
|
|
49
|
+
};
|
|
50
|
+
};
|
|
@@ -13,6 +13,20 @@ describe("convertAdkMessage - human messages", () => {
|
|
|
13
13
|
});
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
+
it("returns empty content when content is neither a string nor an array", () => {
|
|
17
|
+
for (const content of [null, undefined, { text: "Hello" }]) {
|
|
18
|
+
const msg = {
|
|
19
|
+
id: "m1",
|
|
20
|
+
type: "human",
|
|
21
|
+
content,
|
|
22
|
+
} as unknown as AdkMessage;
|
|
23
|
+
expect(convertAdkMessage(msg, {})).toMatchObject({
|
|
24
|
+
role: "user",
|
|
25
|
+
content: [],
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
16
30
|
it("converts a human message with text content parts", () => {
|
|
17
31
|
const msg: AdkMessage = {
|
|
18
32
|
id: "m1",
|
|
@@ -128,6 +142,33 @@ describe("convertAdkMessage - human messages", () => {
|
|
|
128
142
|
});
|
|
129
143
|
|
|
130
144
|
describe("convertAdkMessage - ai messages", () => {
|
|
145
|
+
it("coerces missing text on text and reasoning parts to an empty string", () => {
|
|
146
|
+
const msg = {
|
|
147
|
+
id: "m1",
|
|
148
|
+
type: "ai",
|
|
149
|
+
content: [{ type: "text" }, { type: "reasoning" }],
|
|
150
|
+
} as AdkMessage;
|
|
151
|
+
expect(convertAdkMessage(msg, {})).toMatchObject({
|
|
152
|
+
role: "assistant",
|
|
153
|
+
content: [
|
|
154
|
+
{ type: "text", text: "" },
|
|
155
|
+
{ type: "reasoning", text: "" },
|
|
156
|
+
],
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("skips non-object entries in content arrays", () => {
|
|
161
|
+
const msg = {
|
|
162
|
+
id: "m1",
|
|
163
|
+
type: "ai",
|
|
164
|
+
content: [null, "Hello", { type: "text", text: "Hi" }],
|
|
165
|
+
} as unknown as AdkMessage;
|
|
166
|
+
expect(convertAdkMessage(msg, {})).toMatchObject({
|
|
167
|
+
role: "assistant",
|
|
168
|
+
content: [{ type: "text", text: "Hi" }],
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
131
172
|
it("converts an ai message with text content", () => {
|
|
132
173
|
const msg: AdkMessage = {
|
|
133
174
|
id: "m1",
|
|
@@ -27,14 +27,25 @@ const contentToParts = (
|
|
|
27
27
|
): ContentPart[] => {
|
|
28
28
|
if (typeof content === "string")
|
|
29
29
|
return [{ type: "text" as const, text: content }];
|
|
30
|
+
if (!Array.isArray(content)) return [];
|
|
30
31
|
|
|
31
|
-
return
|
|
32
|
+
return content
|
|
33
|
+
.filter(
|
|
34
|
+
(part): part is AdkMessageContentPart =>
|
|
35
|
+
typeof part === "object" && part !== null,
|
|
36
|
+
)
|
|
32
37
|
.map((part): ContentPart | null => {
|
|
33
38
|
switch (part.type) {
|
|
34
39
|
case "text":
|
|
35
|
-
return {
|
|
40
|
+
return {
|
|
41
|
+
type: "text",
|
|
42
|
+
text: typeof part.text === "string" ? part.text : "",
|
|
43
|
+
};
|
|
36
44
|
case "reasoning":
|
|
37
|
-
return {
|
|
45
|
+
return {
|
|
46
|
+
type: "reasoning",
|
|
47
|
+
text: typeof part.text === "string" ? part.text : "",
|
|
48
|
+
};
|
|
38
49
|
case "image":
|
|
39
50
|
return {
|
|
40
51
|
type: "image",
|
|
@@ -132,10 +143,11 @@ export const createAdkMessageConverter =
|
|
|
132
143
|
|
|
133
144
|
case "tool": {
|
|
134
145
|
// A confirmation reply ADK could not read leaves its gate undecided.
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
// reply to the confirmation itself is dropped: the gated call
|
|
138
|
-
// the same approval, and its own result is the agent's real
|
|
146
|
+
// The reply is not the agent's output, so it is dropped rather than
|
|
147
|
+
// shown as the call's result while the gate waits to be answered again.
|
|
148
|
+
// Only a reply to the confirmation itself is dropped: the gated call
|
|
149
|
+
// carries the same approval, and its own result is the agent's real
|
|
150
|
+
// output.
|
|
139
151
|
const approval = approvals.get(message.tool_call_id);
|
|
140
152
|
if (
|
|
141
153
|
message.name === ADK_REQUEST_CONFIRMATION &&
|
|
@@ -4,8 +4,8 @@ import { act, render, waitFor } from "@testing-library/react";
|
|
|
4
4
|
import type { AssistantRuntime } from "@assistant-ui/core";
|
|
5
5
|
import { AssistantRuntimeProvider } from "@assistant-ui/core/react";
|
|
6
6
|
import { describe, expect, it, vi } from "vitest";
|
|
7
|
-
import { useAdkAppState } from "./hooks";
|
|
8
|
-
import type { AdkEvent } from "./types";
|
|
7
|
+
import { useAdkAppState, useAdkAuthRequests, useAdkSubmitAuth } from "./hooks";
|
|
8
|
+
import type { AdkAuthRequest, AdkEvent, AdkMessage } from "./types";
|
|
9
9
|
import { useAdkRuntime } from "./useAdkRuntime";
|
|
10
10
|
|
|
11
11
|
describe("ADK state hook rendering", () => {
|
|
@@ -72,3 +72,96 @@ describe("ADK state hook rendering", () => {
|
|
|
72
72
|
expect(appState).not.toBe(initial);
|
|
73
73
|
});
|
|
74
74
|
});
|
|
75
|
+
|
|
76
|
+
describe("useAdkSubmitAuth", () => {
|
|
77
|
+
it("answers the pending credential request with its config and the credential", async () => {
|
|
78
|
+
const authConfig = {
|
|
79
|
+
authScheme: { type: "apiKey", in: "header", name: "x-api-key" },
|
|
80
|
+
credentialKey: "weather",
|
|
81
|
+
};
|
|
82
|
+
const stream = vi.fn(async function* () {
|
|
83
|
+
if (stream.mock.calls.length === 1) {
|
|
84
|
+
yield {
|
|
85
|
+
id: "event-1",
|
|
86
|
+
author: "agent",
|
|
87
|
+
content: {
|
|
88
|
+
role: "model",
|
|
89
|
+
parts: [
|
|
90
|
+
{
|
|
91
|
+
functionCall: {
|
|
92
|
+
id: "cred-1",
|
|
93
|
+
name: "adk_request_credential",
|
|
94
|
+
args: { function_call_id: "tc-1", auth_config: authConfig },
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
longRunningToolIds: ["cred-1"],
|
|
100
|
+
turnComplete: true,
|
|
101
|
+
} satisfies AdkEvent;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
yield {
|
|
105
|
+
id: "event-2",
|
|
106
|
+
author: "agent",
|
|
107
|
+
content: { role: "model", parts: [{ text: "Sunny" }] },
|
|
108
|
+
turnComplete: true,
|
|
109
|
+
} satisfies AdkEvent;
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
let runtime: AssistantRuntime | undefined;
|
|
113
|
+
let authRequests: AdkAuthRequest[] | undefined;
|
|
114
|
+
let submitAuth: ReturnType<typeof useAdkSubmitAuth> | undefined;
|
|
115
|
+
|
|
116
|
+
const Probe = () => {
|
|
117
|
+
authRequests = useAdkAuthRequests();
|
|
118
|
+
submitAuth = useAdkSubmitAuth();
|
|
119
|
+
return null;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const App = () => {
|
|
123
|
+
runtime = useAdkRuntime({
|
|
124
|
+
stream,
|
|
125
|
+
create: async () => ({ externalId: "thread-1" }),
|
|
126
|
+
});
|
|
127
|
+
return (
|
|
128
|
+
<AssistantRuntimeProvider runtime={runtime}>
|
|
129
|
+
<Probe />
|
|
130
|
+
</AssistantRuntimeProvider>
|
|
131
|
+
);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
render(<App />);
|
|
135
|
+
await act(async () => {
|
|
136
|
+
await runtime!.thread.append({
|
|
137
|
+
role: "user",
|
|
138
|
+
content: [{ type: "text", text: "weather?" }],
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
await waitFor(() =>
|
|
142
|
+
expect(authRequests).toEqual([{ toolCallId: "cred-1", authConfig }]),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
await act(async () => {
|
|
146
|
+
await submitAuth!("cred-1", { authType: "apiKey", apiKey: "secret" });
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const [replied] = stream.mock.calls[1] as unknown as [AdkMessage[]];
|
|
150
|
+
expect(replied).toHaveLength(1);
|
|
151
|
+
expect(replied[0]).toMatchObject({
|
|
152
|
+
type: "tool",
|
|
153
|
+
tool_call_id: "cred-1",
|
|
154
|
+
name: "adk_request_credential",
|
|
155
|
+
});
|
|
156
|
+
expect(JSON.parse((replied[0] as { content: string }).content)).toEqual({
|
|
157
|
+
...authConfig,
|
|
158
|
+
exchangedAuthCredential: { authType: "apiKey", apiKey: "secret" },
|
|
159
|
+
});
|
|
160
|
+
await waitFor(() => expect(authRequests).toEqual([]));
|
|
161
|
+
|
|
162
|
+
expect(() =>
|
|
163
|
+
submitAuth!("cred-1", { authType: "apiKey", apiKey: "again" }),
|
|
164
|
+
).toThrow('No pending ADK auth request for tool call id "cred-1"');
|
|
165
|
+
expect(stream).toHaveBeenCalledTimes(2);
|
|
166
|
+
});
|
|
167
|
+
});
|