@ohos-ports/genkit 1.42.0-beta.0

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 (202) hide show
  1. package/LICENSE +203 -0
  2. package/README.md +434 -0
  3. package/lib/beta.d.mts +18 -0
  4. package/lib/beta.d.ts +18 -0
  5. package/lib/beta.js +88 -0
  6. package/lib/beta.js.map +1 -0
  7. package/lib/beta.mjs +64 -0
  8. package/lib/beta.mjs.map +1 -0
  9. package/lib/client/agent.d.mts +52 -0
  10. package/lib/client/agent.d.ts +52 -0
  11. package/lib/client/agent.js +90 -0
  12. package/lib/client/agent.js.map +1 -0
  13. package/lib/client/agent.mjs +68 -0
  14. package/lib/client/agent.mjs.map +1 -0
  15. package/lib/client/client.d.mts +80 -0
  16. package/lib/client/client.d.ts +80 -0
  17. package/lib/client/client.js +164 -0
  18. package/lib/client/client.js.map +1 -0
  19. package/lib/client/client.mjs +139 -0
  20. package/lib/client/client.mjs.map +1 -0
  21. package/lib/client/index.d.mts +6 -0
  22. package/lib/client/index.d.ts +6 -0
  23. package/lib/client/index.js +55 -0
  24. package/lib/client/index.js.map +1 -0
  25. package/lib/client/index.mjs +31 -0
  26. package/lib/client/index.mjs.map +1 -0
  27. package/lib/client/types.d.mts +17 -0
  28. package/lib/client/types.d.ts +17 -0
  29. package/lib/client/types.js +17 -0
  30. package/lib/client/types.js.map +1 -0
  31. package/lib/client/types.mjs +1 -0
  32. package/lib/client/types.mjs.map +1 -0
  33. package/lib/common.d.mts +3 -0
  34. package/lib/common.d.ts +3 -0
  35. package/lib/common.js +151 -0
  36. package/lib/common.js.map +1 -0
  37. package/lib/common.mjs +128 -0
  38. package/lib/common.mjs.map +1 -0
  39. package/lib/context.d.mts +1 -0
  40. package/lib/context.d.ts +1 -0
  41. package/lib/context.js +50 -0
  42. package/lib/context.js.map +1 -0
  43. package/lib/context.mjs +28 -0
  44. package/lib/context.mjs.map +1 -0
  45. package/lib/embedder.d.mts +1 -0
  46. package/lib/embedder.d.ts +1 -0
  47. package/lib/embedder.js +48 -0
  48. package/lib/embedder.js.map +1 -0
  49. package/lib/embedder.mjs +26 -0
  50. package/lib/embedder.mjs.map +1 -0
  51. package/lib/evaluator.d.mts +1 -0
  52. package/lib/evaluator.d.ts +1 -0
  53. package/lib/evaluator.js +60 -0
  54. package/lib/evaluator.js.map +1 -0
  55. package/lib/evaluator.mjs +38 -0
  56. package/lib/evaluator.mjs.map +1 -0
  57. package/lib/extract.d.mts +1 -0
  58. package/lib/extract.d.ts +1 -0
  59. package/lib/extract.js +48 -0
  60. package/lib/extract.js.map +1 -0
  61. package/lib/extract.mjs +23 -0
  62. package/lib/extract.mjs.map +1 -0
  63. package/lib/formats.d.mts +1 -0
  64. package/lib/formats.d.ts +1 -0
  65. package/lib/formats.js +34 -0
  66. package/lib/formats.js.map +1 -0
  67. package/lib/formats.mjs +18 -0
  68. package/lib/formats.mjs.map +1 -0
  69. package/lib/genkit-beta.d.mts +183 -0
  70. package/lib/genkit-beta.d.ts +183 -0
  71. package/lib/genkit-beta.js +178 -0
  72. package/lib/genkit-beta.js.map +1 -0
  73. package/lib/genkit-beta.mjs +159 -0
  74. package/lib/genkit-beta.mjs.map +1 -0
  75. package/lib/genkit.d.mts +10 -0
  76. package/lib/genkit.d.ts +10 -0
  77. package/lib/genkit.js +471 -0
  78. package/lib/genkit.js.map +1 -0
  79. package/lib/genkit.mjs +489 -0
  80. package/lib/genkit.mjs.map +1 -0
  81. package/lib/index-BSkuxdwl.d.mts +311 -0
  82. package/lib/index-BSkuxdwl.d.ts +311 -0
  83. package/lib/index.d.mts +10 -0
  84. package/lib/index.d.ts +10 -0
  85. package/lib/index.js +51 -0
  86. package/lib/index.js.map +1 -0
  87. package/lib/index.mjs +24 -0
  88. package/lib/index.mjs.map +1 -0
  89. package/lib/logging.d.mts +1 -0
  90. package/lib/logging.d.ts +1 -0
  91. package/lib/logging.js +46 -0
  92. package/lib/logging.js.map +1 -0
  93. package/lib/logging.mjs +22 -0
  94. package/lib/logging.mjs.map +1 -0
  95. package/lib/middleware.d.mts +1 -0
  96. package/lib/middleware.d.ts +1 -0
  97. package/lib/middleware.js +56 -0
  98. package/lib/middleware.js.map +1 -0
  99. package/lib/middleware.mjs +34 -0
  100. package/lib/middleware.mjs.map +1 -0
  101. package/lib/model.d.mts +1 -0
  102. package/lib/model.d.ts +1 -0
  103. package/lib/model.js +92 -0
  104. package/lib/model.js.map +1 -0
  105. package/lib/model.mjs +70 -0
  106. package/lib/model.mjs.map +1 -0
  107. package/lib/plugin.d.mts +10 -0
  108. package/lib/plugin.d.ts +10 -0
  109. package/lib/plugin.js +143 -0
  110. package/lib/plugin.js.map +1 -0
  111. package/lib/plugin.mjs +113 -0
  112. package/lib/plugin.mjs.map +1 -0
  113. package/lib/registry.d.mts +1 -0
  114. package/lib/registry.d.ts +1 -0
  115. package/lib/registry.js +46 -0
  116. package/lib/registry.js.map +1 -0
  117. package/lib/registry.mjs +24 -0
  118. package/lib/registry.mjs.map +1 -0
  119. package/lib/reranker.d.mts +1 -0
  120. package/lib/reranker.d.ts +1 -0
  121. package/lib/reranker.js +54 -0
  122. package/lib/reranker.js.map +1 -0
  123. package/lib/reranker.mjs +32 -0
  124. package/lib/reranker.mjs.map +1 -0
  125. package/lib/retriever.d.mts +1 -0
  126. package/lib/retriever.d.ts +1 -0
  127. package/lib/retriever.js +58 -0
  128. package/lib/retriever.js.map +1 -0
  129. package/lib/retriever.mjs +36 -0
  130. package/lib/retriever.mjs.map +1 -0
  131. package/lib/schema.d.mts +1 -0
  132. package/lib/schema.d.ts +1 -0
  133. package/lib/schema.js +54 -0
  134. package/lib/schema.js.map +1 -0
  135. package/lib/schema.mjs +32 -0
  136. package/lib/schema.mjs.map +1 -0
  137. package/lib/testing.d.mts +1 -0
  138. package/lib/testing.d.ts +1 -0
  139. package/lib/testing.js +50 -0
  140. package/lib/testing.js.map +1 -0
  141. package/lib/testing.mjs +28 -0
  142. package/lib/testing.mjs.map +1 -0
  143. package/lib/tool.d.mts +1 -0
  144. package/lib/tool.d.ts +1 -0
  145. package/lib/tool.js +54 -0
  146. package/lib/tool.js.map +1 -0
  147. package/lib/tool.mjs +32 -0
  148. package/lib/tool.mjs.map +1 -0
  149. package/lib/tracing.d.mts +1 -0
  150. package/lib/tracing.d.ts +1 -0
  151. package/lib/tracing.js +82 -0
  152. package/lib/tracing.js.map +1 -0
  153. package/lib/tracing.mjs +60 -0
  154. package/lib/tracing.mjs.map +1 -0
  155. package/package.json +233 -0
  156. package/src/beta.ts +85 -0
  157. package/src/client/agent.ts +160 -0
  158. package/src/client/client.ts +237 -0
  159. package/src/client/index.ts +72 -0
  160. package/src/client/types.ts +41 -0
  161. package/src/common.ts +175 -0
  162. package/src/context.ts +37 -0
  163. package/src/embedder.ts +41 -0
  164. package/src/evaluator.ts +50 -0
  165. package/src/extract.ts +30 -0
  166. package/src/formats.ts +30 -0
  167. package/src/genkit-beta.ts +275 -0
  168. package/src/genkit.ts +818 -0
  169. package/src/index.ts +29 -0
  170. package/src/logging.ts +30 -0
  171. package/src/middleware.ts +38 -0
  172. package/src/model.ts +87 -0
  173. package/src/plugin.ts +172 -0
  174. package/src/registry.ts +36 -0
  175. package/src/reranker.ts +42 -0
  176. package/src/retriever.ts +55 -0
  177. package/src/schema.ts +40 -0
  178. package/src/testing.ts +47 -0
  179. package/src/tool.ts +40 -0
  180. package/src/tracing.ts +55 -0
  181. package/tests/agent_client_test.ts +497 -0
  182. package/tests/embed_test.ts +142 -0
  183. package/tests/evaluate_test.ts +52 -0
  184. package/tests/flow_test.ts +94 -0
  185. package/tests/formats_test.ts +168 -0
  186. package/tests/generate_test.ts +1631 -0
  187. package/tests/genkit_test.ts +33 -0
  188. package/tests/helpers.ts +176 -0
  189. package/tests/mock-model_test.ts +490 -0
  190. package/tests/plugins_test.ts +259 -0
  191. package/tests/prompts/badSchemaRef.prompt +9 -0
  192. package/tests/prompts/chat_preamble.prompt +5 -0
  193. package/tests/prompts/kitchensink.prompt +25 -0
  194. package/tests/prompts/output.prompt +11 -0
  195. package/tests/prompts/schemaRef.prompt +9 -0
  196. package/tests/prompts/sub/test.prompt +5 -0
  197. package/tests/prompts/test.prompt +5 -0
  198. package/tests/prompts/test.variant.prompt +8 -0
  199. package/tests/prompts/toolPrompt.prompt +6 -0
  200. package/tests/prompts_test.ts +1442 -0
  201. package/tests/session_test.ts +17 -0
  202. package/typedoc.json +22 -0
@@ -0,0 +1,497 @@
1
+ /**
2
+ * Copyright 2026 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import * as assert from 'assert';
18
+ import { afterEach, beforeEach, describe, it } from 'node:test';
19
+ import { AgentError, remoteAgent } from '../src/client/agent';
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Test transport: a fake `fetch` that drives the streamFlow/runFlow protocol.
23
+ // ---------------------------------------------------------------------------
24
+
25
+ interface RecordedRequest {
26
+ url: string;
27
+ body: any;
28
+ headers: Record<string, string>;
29
+ }
30
+
31
+ /** Builds a streaming (SSE) Response from a list of chunk objects. */
32
+ function sseResponse(
33
+ chunks: Array<{ message?: any } | { result?: any } | { error?: any }>
34
+ ): Response {
35
+ const encoder = new TextEncoder();
36
+ const lines = chunks.map((c) => `data: ${JSON.stringify(c)}\n\n`).join('');
37
+ const stream = new ReadableStream<Uint8Array>({
38
+ start(controller) {
39
+ controller.enqueue(encoder.encode(lines));
40
+ controller.close();
41
+ },
42
+ });
43
+ return new Response(stream, {
44
+ status: 200,
45
+ headers: { 'Content-Type': 'text/event-stream' },
46
+ });
47
+ }
48
+
49
+ /** Builds a JSON Response (runFlow protocol). */
50
+ function jsonResponse(obj: any): Response {
51
+ return new Response(JSON.stringify(obj), {
52
+ status: 200,
53
+ headers: { 'Content-Type': 'application/json' },
54
+ });
55
+ }
56
+
57
+ /** A scriptable fetch mock. */
58
+ class FetchMock {
59
+ requests: RecordedRequest[] = [];
60
+ private handlers: Array<(req: RecordedRequest) => Response> = [];
61
+
62
+ /** Queues a response for the next matching request. */
63
+ onNext(handler: (req: RecordedRequest) => Response): void {
64
+ this.handlers.push(handler);
65
+ }
66
+
67
+ get fetch() {
68
+ return async (url: any, init: any): Promise<Response> => {
69
+ const req: RecordedRequest = {
70
+ url: String(url),
71
+ body: init?.body ? JSON.parse(init.body) : undefined,
72
+ headers: init?.headers ?? {},
73
+ };
74
+ this.requests.push(req);
75
+ const handler = this.handlers.shift();
76
+ if (!handler) {
77
+ throw new Error(`Unexpected fetch to ${req.url}`);
78
+ }
79
+ return handler(req);
80
+ };
81
+ }
82
+ }
83
+
84
+ describe('remoteAgent', () => {
85
+ let mock: FetchMock;
86
+ let originalFetch: typeof globalThis.fetch;
87
+
88
+ beforeEach(() => {
89
+ mock = new FetchMock();
90
+ originalFetch = globalThis.fetch;
91
+ globalThis.fetch = mock.fetch as any;
92
+ });
93
+
94
+ afterEach(() => {
95
+ globalThis.fetch = originalFetch;
96
+ });
97
+
98
+ function modelChunk(text: string) {
99
+ return {
100
+ message: {
101
+ modelChunk: { role: 'model', content: [{ text }] },
102
+ },
103
+ };
104
+ }
105
+
106
+ function customChunk(customPatch: any) {
107
+ return { message: { customPatch } };
108
+ }
109
+
110
+ function turnEndResult(out: any) {
111
+ return { result: out };
112
+ }
113
+
114
+ it('streams text and resolves a response', async () => {
115
+ mock.onNext(() =>
116
+ sseResponse([
117
+ modelChunk('Hello '),
118
+ modelChunk('world'),
119
+ turnEndResult({
120
+ snapshotId: 'snap-1',
121
+ message: {
122
+ role: 'model',
123
+ content: [{ text: 'Hello world' }],
124
+ },
125
+ finishReason: 'stop',
126
+ }),
127
+ ])
128
+ );
129
+
130
+ const agent = remoteAgent({ url: '/api/weatherAgent' });
131
+ const chat = agent.chat();
132
+ const turn = chat.sendStream('Weather in Tokyo?');
133
+
134
+ const seen: string[] = [];
135
+ for await (const chunk of turn.stream) {
136
+ if (chunk.text) seen.push(chunk.accumulatedText);
137
+ }
138
+ assert.deepEqual(seen, ['Hello ', 'Hello world']);
139
+
140
+ const res = await turn.response;
141
+ assert.equal(res.text, 'Hello world');
142
+ assert.equal(res.finishReason, 'stop');
143
+ assert.equal(res.snapshotId, 'snap-1');
144
+ assert.equal(chat.snapshotId, 'snap-1');
145
+
146
+ // Request body shape.
147
+ assert.equal(mock.requests.length, 1);
148
+ assert.equal(mock.requests[0].url, '/api/weatherAgent');
149
+ assert.deepEqual(mock.requests[0].body.data, {
150
+ message: { role: 'user', content: [{ text: 'Weather in Tokyo?' }] },
151
+ });
152
+ });
153
+
154
+ it('surfaces streamed custom-state updates as chunk.custom (post-patch)', async () => {
155
+ mock.onNext(() =>
156
+ sseResponse([
157
+ // First patch of a turn is a whole-document replace re-basing onto the
158
+ // server baseline.
159
+ customChunk([
160
+ { op: 'replace', path: '', value: { status: 'Decomposing…' } },
161
+ ]),
162
+ modelChunk('Answer '),
163
+ // Subsequent patches are incremental, bare-rooted diffs.
164
+ customChunk([{ op: 'replace', path: '/status', value: 'Done' }]),
165
+ turnEndResult({
166
+ snapshotId: 'snap-1',
167
+ state: { custom: { status: 'Done' }, messages: [] },
168
+ message: { role: 'model', content: [{ text: 'Answer ' }] },
169
+ finishReason: 'stop',
170
+ }),
171
+ ])
172
+ );
173
+
174
+ const agent = remoteAgent<{ status: string }>({
175
+ url: '/api/researchAgent',
176
+ });
177
+ const chat = agent.chat();
178
+ const turn = chat.sendStream('Research X');
179
+
180
+ const customs: Array<{ status: string } | undefined> = [];
181
+ const texts: string[] = [];
182
+ for await (const chunk of turn.stream) {
183
+ if (chunk.custom) {
184
+ customs.push(chunk.custom);
185
+ // chunk.custom mirrors the live chat.state at yield time.
186
+ assert.deepEqual(chunk.custom, chat.state);
187
+ } else if (chunk.text) {
188
+ texts.push(chunk.text);
189
+ }
190
+ }
191
+
192
+ assert.deepEqual(customs, [{ status: 'Decomposing…' }, { status: 'Done' }]);
193
+ assert.deepEqual(texts, ['Answer ']);
194
+ // Distinct, fresh object references for `===` change detection.
195
+ assert.notStrictEqual(customs[0], customs[1]);
196
+
197
+ const res = await turn.response;
198
+ assert.deepEqual(res.state, { status: 'Done' });
199
+ assert.deepEqual(chat.state, { status: 'Done' });
200
+ });
201
+
202
+ it('carries snapshotId across multi-turn (server-managed)', async () => {
203
+ mock.onNext(() =>
204
+ sseResponse([
205
+ turnEndResult({ snapshotId: 'snap-1', finishReason: 'stop' }),
206
+ ])
207
+ );
208
+ mock.onNext((req) => {
209
+ // Second turn must send init.snapshotId.
210
+ assert.deepEqual(req.body.init, { snapshotId: 'snap-1' });
211
+ return sseResponse([
212
+ turnEndResult({ snapshotId: 'snap-2', finishReason: 'stop' }),
213
+ ]);
214
+ });
215
+
216
+ const agent = remoteAgent({ url: '/api/a' });
217
+ const chat = agent.chat();
218
+ await chat.send('one');
219
+ assert.equal(chat.snapshotId, 'snap-1');
220
+ await chat.send('two');
221
+ assert.equal(chat.snapshotId, 'snap-2');
222
+ });
223
+
224
+ it('keeps chat.state live across a non-streaming send() for server-managed agents and res.state mirrors it', async () => {
225
+ // Server-managed agents return only a snapshotId on the wire (no `state`);
226
+ // custom state arrives as streamed customPatch chunks. send() must drain the
227
+ // stream so chat.state stays live and res.state falls back to it.
228
+ mock.onNext(() =>
229
+ sseResponse([
230
+ customChunk([{ op: 'replace', path: '', value: { count: 1 } }]),
231
+ turnEndResult({ snapshotId: 'snap-1', finishReason: 'stop' }),
232
+ ])
233
+ );
234
+ mock.onNext(() =>
235
+ sseResponse([
236
+ customChunk([{ op: 'replace', path: '/count', value: 2 }]),
237
+ turnEndResult({ snapshotId: 'snap-2', finishReason: 'stop' }),
238
+ ])
239
+ );
240
+
241
+ const agent = remoteAgent<{ count: number }>({ url: '/api/counter' });
242
+ const chat = agent.chat();
243
+
244
+ const res1 = await chat.send('inc');
245
+ assert.deepEqual(chat.state, { count: 1 });
246
+ // res.state mirrors chat.state even though the wire output had no `state`.
247
+ assert.deepEqual(res1.state, { count: 1 });
248
+ assert.equal(res1.snapshotId, 'snap-1');
249
+
250
+ // A second non-streaming send() must refresh chat.state (not go stale).
251
+ const res2 = await chat.send('inc');
252
+ assert.deepEqual(chat.state, { count: 2 });
253
+ assert.deepEqual(res2.state, { count: 2 });
254
+ });
255
+
256
+ it('carries state across multi-turn (client-managed)', async () => {
257
+ mock.onNext(() =>
258
+ sseResponse([
259
+ turnEndResult({
260
+ state: { custom: { unit: 'celsius' }, messages: [] },
261
+ finishReason: 'stop',
262
+ }),
263
+ ])
264
+ );
265
+ mock.onNext((req) => {
266
+ assert.deepEqual(req.body.init, {
267
+ state: { custom: { unit: 'celsius' }, messages: [] },
268
+ });
269
+ return sseResponse([
270
+ turnEndResult({
271
+ state: { custom: { unit: 'fahrenheit' }, messages: [] },
272
+ finishReason: 'stop',
273
+ }),
274
+ ]);
275
+ });
276
+
277
+ const agent = remoteAgent<{ unit: string }>({ url: '/api/a' });
278
+ const chat = agent.chat();
279
+ const res1 = await chat.send('one');
280
+ assert.deepEqual(res1.state, { unit: 'celsius' });
281
+ assert.deepEqual(chat.state, { unit: 'celsius' });
282
+ const res2 = await chat.send('two');
283
+ assert.deepEqual(res2.state, { unit: 'fahrenheit' });
284
+ assert.deepEqual(chat.state, { unit: 'fahrenheit' });
285
+ });
286
+
287
+ it('exposes interrupts and builds resume parts', async () => {
288
+ mock.onNext(() =>
289
+ sseResponse([
290
+ turnEndResult({
291
+ snapshotId: 'snap-1',
292
+ message: {
293
+ role: 'model',
294
+ content: [
295
+ {
296
+ toolRequest: {
297
+ name: 'userApproval',
298
+ ref: 'r1',
299
+ input: { action: 'transfer' },
300
+ },
301
+ metadata: { interrupt: true },
302
+ },
303
+ ],
304
+ },
305
+ finishReason: 'interrupted',
306
+ }),
307
+ ])
308
+ );
309
+
310
+ const agent = remoteAgent({ url: '/api/bank' });
311
+ const chat = agent.chat();
312
+ const res = await chat.send('Transfer $500');
313
+
314
+ assert.equal(res.finishReason, 'interrupted');
315
+ assert.equal(res.interrupts.length, 1);
316
+ const approval = res.interrupts[0];
317
+ assert.equal(approval.name, 'userApproval');
318
+ assert.deepEqual(approval.input, { action: 'transfer' });
319
+
320
+ const respondPart = approval.respond({ approved: true });
321
+ assert.deepEqual(respondPart, {
322
+ toolResponse: {
323
+ name: 'userApproval',
324
+ ref: 'r1',
325
+ output: { approved: true },
326
+ },
327
+ });
328
+
329
+ const restartPart = approval.restart();
330
+ assert.deepEqual(restartPart, {
331
+ toolRequest: {
332
+ name: 'userApproval',
333
+ ref: 'r1',
334
+ input: { action: 'transfer' },
335
+ },
336
+ });
337
+
338
+ // chat.resume is sugar for send({ resume }).
339
+ mock.onNext((req) => {
340
+ assert.deepEqual(req.body.data.resume, { respond: [respondPart] });
341
+ return sseResponse([
342
+ turnEndResult({ snapshotId: 'snap-2', finishReason: 'stop' }),
343
+ ]);
344
+ });
345
+ await chat.resume({ respond: [respondPart] });
346
+ assert.equal(chat.snapshotId, 'snap-2');
347
+ });
348
+
349
+ it('throws AgentError on a failed turn and keeps last-good state', async () => {
350
+ // First successful turn establishes last-good snapshot.
351
+ mock.onNext(() =>
352
+ sseResponse([
353
+ turnEndResult({ snapshotId: 'snap-1', finishReason: 'stop' }),
354
+ ])
355
+ );
356
+ // Second turn fails.
357
+ mock.onNext(() =>
358
+ sseResponse([
359
+ turnEndResult({
360
+ snapshotId: 'snap-1',
361
+ finishReason: 'failed',
362
+ error: { status: 'INTERNAL', message: 'boom' },
363
+ }),
364
+ ])
365
+ );
366
+
367
+ const agent = remoteAgent({ url: '/api/a' });
368
+ const chat = agent.chat();
369
+ await chat.send('one');
370
+ assert.equal(chat.snapshotId, 'snap-1');
371
+
372
+ await assert.rejects(
373
+ () => chat.send('two'),
374
+
375
+ (err: AgentError) => {
376
+ assert.ok(err instanceof AgentError);
377
+ assert.equal(err.status, 'INTERNAL');
378
+ assert.equal(err.message, 'boom');
379
+ assert.equal(err.snapshotId, 'snap-1');
380
+ return true;
381
+ }
382
+ );
383
+ // Chat recovered to last-good snapshot.
384
+ assert.equal(chat.snapshotId, 'snap-1');
385
+ });
386
+
387
+ it('throws AgentError when the stream is iterated on a failed turn', async () => {
388
+ mock.onNext(() =>
389
+ sseResponse([
390
+ turnEndResult({
391
+ finishReason: 'failed',
392
+ error: { status: 'UNAVAILABLE', message: 'down' },
393
+ }),
394
+ ])
395
+ );
396
+ const agent = remoteAgent({ url: '/api/a' });
397
+ const chat = agent.chat();
398
+ const turn = chat.sendStream('hi');
399
+ await assert.rejects(async () => {
400
+ for await (const _ of turn.stream) {
401
+ // drain
402
+ }
403
+ }, AgentError);
404
+ });
405
+
406
+ it('loadChat() loads a snapshot and restores history', async () => {
407
+ mock.onNext((req) => {
408
+ assert.equal(req.url, '/api/a/getSnapshot');
409
+ assert.deepEqual(req.body.data, { snapshotId: 'snap-9' });
410
+ return jsonResponse({
411
+ result: {
412
+ snapshotId: 'snap-9',
413
+ createdAt: '2026-01-01',
414
+ state: {
415
+ messages: [{ role: 'user', content: [{ text: 'earlier' }] }],
416
+ },
417
+ status: 'completed',
418
+ },
419
+ });
420
+ });
421
+
422
+ const agent = remoteAgent({ url: '/api/a' });
423
+ const chat = await agent.loadChat({ snapshotId: 'snap-9' });
424
+ assert.equal(chat.snapshotId, 'snap-9');
425
+ assert.equal(chat.messages.length, 1);
426
+ assert.equal(chat.messages[0].content[0].text, 'earlier');
427
+ });
428
+
429
+ it('getSnapshot reads via the getSnapshot endpoint', async () => {
430
+ mock.onNext((req) => {
431
+ assert.equal(req.url, '/api/a/getSnapshot');
432
+ return jsonResponse({
433
+ result: {
434
+ snapshotId: 'snap-1',
435
+ createdAt: '2026',
436
+ state: {},
437
+ status: 'completed',
438
+ },
439
+ });
440
+ });
441
+ const agent = remoteAgent({ url: '/api/a' });
442
+ const snap = await agent.getSnapshot('snap-1');
443
+ assert.equal(snap?.snapshotId, 'snap-1');
444
+ });
445
+
446
+ it('abort posts to the abort endpoint', async () => {
447
+ mock.onNext((req) => {
448
+ assert.equal(req.url, '/api/a/abort');
449
+ assert.deepEqual(req.body.data, { snapshotId: 'snap-1' });
450
+ return jsonResponse({
451
+ result: { snapshotId: 'snap-1', status: 'pending' },
452
+ });
453
+ });
454
+ const agent = remoteAgent({ url: '/api/a' });
455
+ const status = await agent.abort('snap-1');
456
+ assert.equal(status, 'pending');
457
+ });
458
+
459
+ it('detach submits a background task and reports detached', async () => {
460
+ mock.onNext((req) => {
461
+ assert.equal(req.body.data.detach, true);
462
+ return sseResponse([
463
+ turnEndResult({ snapshotId: 'bg-1', finishReason: 'detached' }),
464
+ ]);
465
+ });
466
+ const agent = remoteAgent({ url: '/api/a' });
467
+ const chat = agent.chat();
468
+ const task = await chat.detach('long job');
469
+ assert.equal(task.snapshotId, 'bg-1');
470
+
471
+ // wait() polls /getSnapshot until terminal.
472
+ mock.onNext(() =>
473
+ jsonResponse({
474
+ result: {
475
+ snapshotId: 'bg-1',
476
+ createdAt: '2026',
477
+ state: {},
478
+ status: 'completed',
479
+ },
480
+ })
481
+ );
482
+ const snap = await task.wait({ intervalMs: 1 });
483
+ assert.equal(snap.status, 'completed');
484
+ });
485
+
486
+ it('applies static and async headers', async () => {
487
+ mock.onNext((req) => {
488
+ assert.equal(req.headers['Authorization'], 'Bearer xyz');
489
+ return sseResponse([turnEndResult({ finishReason: 'stop' })]);
490
+ });
491
+ const agent = remoteAgent({
492
+ url: '/api/a',
493
+ headers: async () => ({ Authorization: 'Bearer xyz' }),
494
+ });
495
+ await agent.chat().send('hi');
496
+ });
497
+ });
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Copyright 2024 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import { Document, embedderRef, type EmbedderAction } from '@genkit-ai/ai';
18
+ import * as assert from 'assert';
19
+ import { beforeEach, describe, it } from 'node:test';
20
+ import { genkit, type Genkit } from '../src/index.js';
21
+
22
+ describe('embed', () => {
23
+ describe('default model', () => {
24
+ let ai: Genkit;
25
+ let embedder: EmbedderAction;
26
+
27
+ beforeEach(() => {
28
+ ai = genkit({});
29
+ embedder = defineTestEmbedder(ai);
30
+ });
31
+
32
+ it('passes string content as docs', async () => {
33
+ const response = (
34
+ await ai.embed({
35
+ embedder: 'echoEmbedder',
36
+ content: 'hi',
37
+ })
38
+ )[0].embedding;
39
+ assert.deepStrictEqual((embedder as any).lastRequest, [
40
+ [Document.fromText('hi')],
41
+ {
42
+ version: undefined,
43
+ },
44
+ ]);
45
+ assert.deepStrictEqual(response, [1, 2, 3, 4]);
46
+ });
47
+
48
+ it('passes docs content as docs', async () => {
49
+ const response = await ai.embed({
50
+ embedder: 'echoEmbedder',
51
+ content: Document.fromText('hi'),
52
+ });
53
+ assert.deepStrictEqual((embedder as any).lastRequest, [
54
+ [Document.fromText('hi')],
55
+ {
56
+ version: undefined,
57
+ },
58
+ ]);
59
+ assert.deepStrictEqual(response, [{ embedding: [1, 2, 3, 4] }]);
60
+ });
61
+ });
62
+
63
+ describe('config', () => {
64
+ let ai: Genkit;
65
+ let embedder: EmbedderAction;
66
+
67
+ beforeEach(() => {
68
+ ai = genkit({});
69
+ embedder = defineTestEmbedder(ai);
70
+ });
71
+
72
+ it('takes config passed to generate', async () => {
73
+ const response = await ai.embed({
74
+ embedder: 'echoEmbedder',
75
+ content: 'hi',
76
+ options: {
77
+ temperature: 11,
78
+ },
79
+ });
80
+ assert.deepStrictEqual(response, [{ embedding: [1, 2, 3, 4] }]);
81
+ assert.deepStrictEqual((embedder as any).lastRequest[1], {
82
+ temperature: 11,
83
+ version: undefined,
84
+ });
85
+ });
86
+
87
+ it('merges config from the ref', async () => {
88
+ const response = await ai.embed({
89
+ embedder: embedderRef({
90
+ name: 'echoEmbedder',
91
+ config: {
92
+ version: 'abc',
93
+ },
94
+ }),
95
+ content: 'hi',
96
+ options: {
97
+ temperature: 11,
98
+ },
99
+ });
100
+ assert.deepStrictEqual(response, [{ embedding: [1, 2, 3, 4] }]);
101
+ assert.deepStrictEqual((embedder as any).lastRequest[1], {
102
+ temperature: 11,
103
+ version: 'abc',
104
+ });
105
+ });
106
+
107
+ it('picks up the top-level version from the ref', async () => {
108
+ const response = await ai.embed({
109
+ embedder: embedderRef({
110
+ name: 'echoEmbedder',
111
+ version: 'abc',
112
+ }),
113
+ content: 'hi',
114
+ options: {
115
+ temperature: 11,
116
+ },
117
+ });
118
+ assert.deepStrictEqual(response, [{ embedding: [1, 2, 3, 4] }]);
119
+ assert.deepStrictEqual((embedder as any).lastRequest[1], {
120
+ temperature: 11,
121
+ version: 'abc',
122
+ });
123
+ });
124
+ });
125
+ });
126
+
127
+ function defineTestEmbedder(ai: Genkit) {
128
+ const embedder = ai.defineEmbedder(
129
+ { name: 'echoEmbedder' },
130
+ async (input, config) => {
131
+ (embedder as any).lastRequest = [input, config];
132
+ return {
133
+ embeddings: [
134
+ {
135
+ embedding: [1, 2, 3, 4],
136
+ },
137
+ ],
138
+ };
139
+ }
140
+ );
141
+ return embedder;
142
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Copyright 2024 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import * as assert from 'assert';
18
+ import { beforeEach, describe, it } from 'node:test';
19
+ import { genkit, type Genkit } from '../src/index.js';
20
+ import { bonknessEvaluator } from './helpers';
21
+
22
+ describe('evaluate', () => {
23
+ describe('default model', () => {
24
+ let ai: Genkit;
25
+
26
+ beforeEach(() => {
27
+ ai = genkit({});
28
+ bonknessEvaluator(ai);
29
+ });
30
+
31
+ it('calls evaluator', async () => {
32
+ const response = await ai.evaluate({
33
+ evaluator: 'bonkness',
34
+ dataset: [
35
+ {
36
+ testCaseId: 'test-case-1',
37
+ input: 'Why did the chicken cross the road?',
38
+ output: 'To get to the other side',
39
+ },
40
+ ],
41
+ evalRunId: 'my-dog-eval',
42
+ });
43
+
44
+ assert.strictEqual(response.length, 1);
45
+ assert.strictEqual(response[0].evaluation.score, 'Much bonk');
46
+ assert.strictEqual(
47
+ response[0].evaluation.details?.reasoning,
48
+ 'Because I said so!'
49
+ );
50
+ });
51
+ });
52
+ });