@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,1631 @@
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 type { GenerateResponseChunkData, MessageData } from '@genkit-ai/ai';
18
+ import { ModelAction } from '@genkit-ai/ai/model';
19
+ import { Operation, z, type JSONSchema7 } from '@genkit-ai/core';
20
+ import * as assert from 'assert';
21
+ import { beforeEach, describe, it } from 'node:test';
22
+ import { generateMiddleware } from '../../ai/src/generate/middleware';
23
+ import { modelRef } from '../../ai/src/model';
24
+ import { interrupt } from '../../ai/src/tool';
25
+ import { dynamicResource, genkit, tool, type GenkitBeta } from '../src/beta';
26
+ import {
27
+ defineEchoModel,
28
+ defineProgrammableModel,
29
+ runAsync,
30
+ type ProgrammableModel,
31
+ } from './helpers';
32
+
33
+ describe('generate', () => {
34
+ describe('default model', () => {
35
+ let ai: GenkitBeta;
36
+
37
+ beforeEach(() => {
38
+ ai = genkit({
39
+ model: 'echoModel',
40
+ });
41
+ defineEchoModel(ai);
42
+ });
43
+
44
+ it('calls the default model', async () => {
45
+ const response = await ai.generate({
46
+ prompt: 'hi',
47
+ });
48
+ assert.strictEqual(response.text, 'Echo: hi; config: {}');
49
+ });
50
+
51
+ it('calls the default model with just a string prompt', async () => {
52
+ const response = await ai.generate('hi');
53
+ assert.strictEqual(response.text, 'Echo: hi; config: {}');
54
+ });
55
+
56
+ it('calls the default model with just parts prompt', async () => {
57
+ const response = await ai.generate([{ text: 'hi' }]);
58
+ assert.strictEqual(response.text, 'Echo: hi; config: {}');
59
+ });
60
+
61
+ it('calls the default model system', async () => {
62
+ const response = await ai.generate({
63
+ prompt: 'hi',
64
+ system: 'talk like a pirate',
65
+ });
66
+ assert.strictEqual(
67
+ response.text,
68
+ 'Echo: system: talk like a pirate,hi; config: {}'
69
+ );
70
+ assert.deepStrictEqual(response.request, {
71
+ config: {
72
+ version: undefined,
73
+ },
74
+ docs: undefined,
75
+ messages: [
76
+ {
77
+ role: 'system',
78
+ content: [{ text: 'talk like a pirate' }],
79
+ },
80
+ {
81
+ role: 'user',
82
+ content: [{ text: 'hi' }],
83
+ },
84
+ ],
85
+ output: {},
86
+ tools: [],
87
+ });
88
+ });
89
+
90
+ it('calls the default model with tool choice', async () => {
91
+ const response = await ai.generate({
92
+ prompt: 'hi',
93
+ toolChoice: 'required',
94
+ });
95
+ assert.strictEqual(response.text, 'Echo: hi; config: {}');
96
+ assert.deepStrictEqual(response.request, {
97
+ config: {
98
+ version: undefined,
99
+ },
100
+ docs: undefined,
101
+ messages: [
102
+ {
103
+ role: 'user',
104
+ content: [{ text: 'hi' }],
105
+ },
106
+ ],
107
+ output: {},
108
+ tools: [],
109
+ toolChoice: 'required',
110
+ });
111
+ });
112
+
113
+ it('works with a middleware plugin', async () => {
114
+ let middlewareExecuted = false;
115
+ const myMiddleware = generateMiddleware(
116
+ { name: 'myMiddleware', configSchema: z.string() },
117
+ ({ config }) => {
118
+ return {
119
+ model: async (req, ctx, next) => {
120
+ middlewareExecuted = true;
121
+ return {
122
+ request: req,
123
+ finishReason: 'stop',
124
+ message: {
125
+ role: 'model',
126
+ content: [{ text: `${config}: hi` }],
127
+ },
128
+ };
129
+ },
130
+ };
131
+ }
132
+ );
133
+
134
+ const aiWithPlugin = genkit({
135
+ model: 'echoModel',
136
+ plugins: [myMiddleware.plugin()],
137
+ });
138
+ defineEchoModel(aiWithPlugin);
139
+
140
+ const response = await aiWithPlugin.generate({
141
+ prompt: 'hi',
142
+ use: [myMiddleware('z-prefix')],
143
+ });
144
+
145
+ assert.strictEqual(response.text, 'z-prefix: hi');
146
+ assert.strictEqual(middlewareExecuted, true);
147
+ });
148
+
149
+ it('streams the default model', async () => {
150
+ const { response, stream } = await ai.generateStream('hi');
151
+
152
+ const chunks: string[] = [];
153
+ for await (const chunk of stream) {
154
+ chunks.push(chunk.text);
155
+ }
156
+ assert.strictEqual((await response).text, 'Echo: hi; config: {}');
157
+ assert.deepStrictEqual(chunks, ['3', '2', '1']);
158
+ });
159
+ });
160
+
161
+ describe('explicit model', () => {
162
+ let ai: GenkitBeta;
163
+
164
+ beforeEach(() => {
165
+ ai = genkit({});
166
+ defineEchoModel(ai);
167
+ });
168
+
169
+ it('calls the explicitly passed in model', async () => {
170
+ const response = await ai.generate({
171
+ model: 'echoModel',
172
+ prompt: 'hi',
173
+ });
174
+ assert.strictEqual(response.text, 'Echo: hi; config: {}');
175
+ });
176
+
177
+ it('rejects on invalid model', async () => {
178
+ const response = ai.generate({
179
+ model: 'modelThatDoesNotExist',
180
+ prompt: 'hi',
181
+ });
182
+ await assert.rejects(response, 'Model modelThatDoesNotExist not found');
183
+ });
184
+ });
185
+
186
+ describe('streaming', () => {
187
+ let ai: GenkitBeta;
188
+
189
+ beforeEach(() => {
190
+ ai = genkit({});
191
+ });
192
+
193
+ it('rethrows response errors', async () => {
194
+ ai.defineModel(
195
+ {
196
+ apiVersion: 'v2',
197
+ name: 'blockingModel',
198
+ },
199
+ async (request, { sendChunk, streamingRequested }) => {
200
+ if (streamingRequested) {
201
+ await runAsync(() => {
202
+ sendChunk({
203
+ content: [
204
+ {
205
+ text: '3',
206
+ },
207
+ ],
208
+ });
209
+ });
210
+ await runAsync(() => {
211
+ sendChunk({
212
+ content: [
213
+ {
214
+ text: '2',
215
+ },
216
+ ],
217
+ });
218
+ });
219
+ await runAsync(() => {
220
+ sendChunk({
221
+ content: [
222
+ {
223
+ text: '1',
224
+ },
225
+ ],
226
+ });
227
+ });
228
+ }
229
+ return await runAsync(() => ({
230
+ message: {
231
+ role: 'model',
232
+ content: [],
233
+ },
234
+ finishReason: 'blocked',
235
+ }));
236
+ }
237
+ );
238
+
239
+ await assert.rejects(async () => {
240
+ const { response, stream } = ai.generateStream({
241
+ prompt: 'hi',
242
+ model: 'blockingModel',
243
+ });
244
+ for await (const chunk of stream) {
245
+ // nothing
246
+ }
247
+ await response;
248
+ });
249
+ });
250
+
251
+ it('rethrows initialization errors', async () => {
252
+ await assert.rejects(
253
+ async () => {
254
+ const { stream } = ai.generateStream({
255
+ prompt: 'hi',
256
+ model: 'modelNotFound',
257
+ });
258
+ for await (const chunk of stream) {
259
+ // nothing
260
+ }
261
+ },
262
+ { status: 'NOT_FOUND' }
263
+ );
264
+ });
265
+
266
+ it('passes the streaming callback to the model', async () => {
267
+ const model = defineEchoModel(ai);
268
+ const flow = ai.defineFlow('wrapper', async (_, streamingCallback) => {
269
+ const response = await ai.generate({
270
+ model: model,
271
+ prompt: 'hi',
272
+ onChunk: console.log,
273
+ });
274
+ return response.text;
275
+ });
276
+ const text = await flow();
277
+ assert.ok((model as any).__test__lastStreamingCallback);
278
+ });
279
+
280
+ it('strips out the noop streaming callback', async () => {
281
+ const model = defineEchoModel(ai);
282
+ const flow = ai.defineFlow('wrapper', async (_, streamingCallback) => {
283
+ const response = await ai.generate({
284
+ model: model,
285
+ prompt: 'hi',
286
+ onChunk: streamingCallback,
287
+ });
288
+ return response.text;
289
+ });
290
+ const text = await flow();
291
+ assert.ok(!(model as any).__test__lastStreamingCallback);
292
+ });
293
+ });
294
+
295
+ describe('config', () => {
296
+ let ai: GenkitBeta;
297
+
298
+ beforeEach(() => {
299
+ ai = genkit({});
300
+ defineEchoModel(ai);
301
+ });
302
+
303
+ it('takes config passed to generate', async () => {
304
+ const response = await ai.generate({
305
+ prompt: 'hi',
306
+ model: 'echoModel',
307
+ config: {
308
+ temperature: 11,
309
+ },
310
+ });
311
+ assert.strictEqual(response.text, 'Echo: hi; config: {"temperature":11}');
312
+ });
313
+
314
+ it('merges config from the ref', async () => {
315
+ const response = await ai.generate({
316
+ prompt: 'hi',
317
+ model: modelRef({ name: 'echoModel' }).withConfig({
318
+ version: 'abc',
319
+ }),
320
+ config: {
321
+ temperature: 11,
322
+ },
323
+ });
324
+ assert.strictEqual(
325
+ response.text,
326
+ 'Echo: hi; config: {"version":"abc","temperature":11}'
327
+ );
328
+ });
329
+
330
+ it('picks up the top-level version from the ref', async () => {
331
+ const response = await ai.generate({
332
+ prompt: 'hi',
333
+ model: modelRef({ name: 'echoModel' }).withVersion('bcd'),
334
+ config: {
335
+ temperature: 11,
336
+ },
337
+ });
338
+ assert.strictEqual(
339
+ response.text,
340
+ 'Echo: hi; config: {"version":"bcd","temperature":11}'
341
+ );
342
+ });
343
+ });
344
+
345
+ describe('tools', () => {
346
+ let ai: GenkitBeta;
347
+ let pm: ProgrammableModel;
348
+ let echo: ModelAction;
349
+
350
+ beforeEach(() => {
351
+ class Extra {
352
+ toJSON() {
353
+ return 'extra';
354
+ }
355
+ }
356
+ ai = genkit({
357
+ model: 'programmableModel',
358
+ // testing with a non-serializable data in the context
359
+ context: { something: new Extra() },
360
+ });
361
+ pm = defineProgrammableModel(ai);
362
+ echo = defineEchoModel(ai);
363
+ });
364
+
365
+ it('call the tool', async () => {
366
+ ai.defineTool(
367
+ { name: 'testTool', description: 'description' },
368
+ async () => 'tool called'
369
+ );
370
+
371
+ // first response is a tool call, the subsequent responses are just text response from agent b.
372
+ let reqCounter = 0;
373
+ pm.handleResponse = async (req, sc) => {
374
+ return {
375
+ message: {
376
+ role: 'model',
377
+ content: [
378
+ reqCounter++ === 0
379
+ ? {
380
+ toolRequest: {
381
+ name: 'testTool',
382
+ input: {},
383
+ ref: 'ref123',
384
+ },
385
+ }
386
+ : { text: 'done' },
387
+ ],
388
+ },
389
+ };
390
+ };
391
+
392
+ const { text } = await ai.generate({
393
+ prompt: 'call the tool',
394
+ tools: ['testTool'],
395
+ });
396
+
397
+ assert.strictEqual(text, 'done');
398
+ assert.deepStrictEqual(
399
+ pm.lastRequest,
400
+
401
+ {
402
+ config: {},
403
+ messages: [
404
+ {
405
+ role: 'user',
406
+ content: [{ text: 'call the tool' }],
407
+ },
408
+ {
409
+ role: 'model',
410
+ content: [
411
+ {
412
+ toolRequest: {
413
+ input: {},
414
+ name: 'testTool',
415
+ ref: 'ref123',
416
+ },
417
+ },
418
+ ],
419
+ },
420
+ {
421
+ role: 'tool',
422
+ content: [
423
+ {
424
+ toolResponse: {
425
+ name: 'testTool',
426
+ output: 'tool called',
427
+ ref: 'ref123',
428
+ },
429
+ },
430
+ ],
431
+ },
432
+ ],
433
+ output: {},
434
+ tools: [
435
+ {
436
+ description: 'description',
437
+ inputSchema: {
438
+ $schema: 'http://json-schema.org/draft-07/schema#',
439
+ },
440
+ name: 'testTool',
441
+ outputSchema: {
442
+ $schema: 'http://json-schema.org/draft-07/schema#',
443
+ },
444
+ key: '/tool/testTool',
445
+ },
446
+ ],
447
+ }
448
+ );
449
+ });
450
+
451
+ it('call the tool with context', async () => {
452
+ ai.defineTool(
453
+ { name: 'testTool', description: 'description' },
454
+ async (_, { context }) => JSON.stringify(context)
455
+ );
456
+
457
+ // first response is a tool call, the subsequent responses are just text response from agent b.
458
+ let reqCounter = 0;
459
+ pm.handleResponse = async (req, sc) => {
460
+ return {
461
+ message: {
462
+ role: 'model',
463
+ content: [
464
+ reqCounter++ === 0
465
+ ? {
466
+ toolRequest: {
467
+ name: 'testTool',
468
+ input: {},
469
+ ref: 'ref123',
470
+ },
471
+ }
472
+ : { text: 'done' },
473
+ ],
474
+ },
475
+ };
476
+ };
477
+
478
+ const { messages } = await ai.generate({
479
+ prompt: 'call the tool',
480
+ tools: ['testTool'],
481
+ });
482
+
483
+ assert.deepStrictEqual(messages[2], {
484
+ role: 'tool',
485
+ content: [
486
+ {
487
+ toolResponse: {
488
+ name: 'testTool',
489
+ output: '{"something":"extra"}',
490
+ ref: 'ref123',
491
+ },
492
+ },
493
+ ],
494
+ });
495
+ });
496
+
497
+ it('calls the dynamic tool', async () => {
498
+ const schema = {
499
+ properties: {
500
+ foo: { type: 'string' },
501
+ },
502
+ } as JSONSchema7;
503
+ const dynamicTestTool1 = tool(
504
+ {
505
+ name: 'dynamicTestTool1',
506
+ inputJsonSchema: schema,
507
+ description: 'description',
508
+ },
509
+ async () => 'tool called 1'
510
+ );
511
+ const dynamicTestTool2 = ai.dynamicTool(
512
+ {
513
+ name: 'dynamicTestTool2',
514
+ inputJsonSchema: schema,
515
+ description: 'description 2',
516
+ },
517
+ async () => 'tool called 2'
518
+ );
519
+ const dynamicMultipartTool = tool(
520
+ {
521
+ multipart: true,
522
+ name: 'dynamicMultipartTool',
523
+ inputJsonSchema: schema,
524
+ description: 'description',
525
+ },
526
+ async () => {
527
+ return {
528
+ output: 'main output',
529
+ content: [{ text: 'part 1' }],
530
+ };
531
+ }
532
+ );
533
+
534
+ // first response is a tool call, the subsequent responses are just text response from agent b.
535
+ let reqCounter = 0;
536
+ pm.handleResponse = async (req, sc) => {
537
+ return {
538
+ message: {
539
+ role: 'model',
540
+ content:
541
+ reqCounter++ === 0
542
+ ? [
543
+ {
544
+ toolRequest: {
545
+ name: 'dynamicTestTool1',
546
+ input: { foo: 'bar' },
547
+ ref: 'ref123',
548
+ },
549
+ },
550
+ {
551
+ toolRequest: {
552
+ name: 'dynamicTestTool2',
553
+ input: { foo: 'baz' },
554
+ ref: 'ref234',
555
+ },
556
+ },
557
+ {
558
+ toolRequest: {
559
+ name: 'dynamicMultipartTool',
560
+ input: { foo: 'baz' },
561
+ ref: 'ref234',
562
+ },
563
+ },
564
+ ]
565
+ : [{ text: 'done' }],
566
+ },
567
+ };
568
+ };
569
+
570
+ const { text } = await ai.generate({
571
+ prompt: 'call the tool',
572
+ tools: [dynamicTestTool1, dynamicTestTool2, dynamicMultipartTool],
573
+ });
574
+
575
+ assert.strictEqual(text, 'done');
576
+ // remove properties from outputSchema -- too verbose, not important for the test
577
+ delete pm.lastRequest?.tools?.[2]?.outputSchema?.properties;
578
+ assert.deepStrictEqual(pm.lastRequest, {
579
+ config: {},
580
+ messages: [
581
+ {
582
+ role: 'user',
583
+ content: [{ text: 'call the tool' }],
584
+ },
585
+ {
586
+ role: 'model',
587
+ content: [
588
+ {
589
+ toolRequest: {
590
+ input: { foo: 'bar' },
591
+ name: 'dynamicTestTool1',
592
+ ref: 'ref123',
593
+ },
594
+ },
595
+ {
596
+ toolRequest: {
597
+ input: { foo: 'baz' },
598
+ name: 'dynamicTestTool2',
599
+ ref: 'ref234',
600
+ },
601
+ },
602
+ {
603
+ toolRequest: {
604
+ input: {
605
+ foo: 'baz',
606
+ },
607
+ name: 'dynamicMultipartTool',
608
+ ref: 'ref234',
609
+ },
610
+ },
611
+ ],
612
+ },
613
+ {
614
+ role: 'tool',
615
+ content: [
616
+ {
617
+ toolResponse: {
618
+ name: 'dynamicTestTool1',
619
+ output: 'tool called 1',
620
+ ref: 'ref123',
621
+ },
622
+ },
623
+ {
624
+ toolResponse: {
625
+ name: 'dynamicTestTool2',
626
+ output: 'tool called 2',
627
+ ref: 'ref234',
628
+ },
629
+ },
630
+ {
631
+ toolResponse: {
632
+ content: [
633
+ {
634
+ text: 'part 1',
635
+ },
636
+ ],
637
+ name: 'dynamicMultipartTool',
638
+ output: 'main output',
639
+ ref: 'ref234',
640
+ },
641
+ },
642
+ ],
643
+ },
644
+ ],
645
+ output: {},
646
+ tools: [
647
+ {
648
+ description: 'description',
649
+ inputSchema: schema,
650
+ name: 'dynamicTestTool1',
651
+ outputSchema: {
652
+ $schema: 'http://json-schema.org/draft-07/schema#',
653
+ },
654
+ key: '/tool/dynamicTestTool1',
655
+ },
656
+ {
657
+ description: 'description 2',
658
+ inputSchema: schema,
659
+ name: 'dynamicTestTool2',
660
+ outputSchema: {
661
+ $schema: 'http://json-schema.org/draft-07/schema#',
662
+ },
663
+ key: '/tool/dynamicTestTool2',
664
+ },
665
+ {
666
+ description: 'description',
667
+ inputSchema: {
668
+ properties: {
669
+ foo: {
670
+ type: 'string',
671
+ },
672
+ },
673
+ },
674
+ name: 'dynamicMultipartTool',
675
+ outputSchema: {
676
+ $schema: 'http://json-schema.org/draft-07/schema#',
677
+ additionalProperties: true,
678
+ type: 'object',
679
+ },
680
+ key: '/tool.v2/dynamicMultipartTool',
681
+ },
682
+ ],
683
+ });
684
+ });
685
+
686
+ it('calls the dynamic resource', async () => {
687
+ const dynamicTestResource = dynamicResource(
688
+ {
689
+ name: 'dynamicTestTool',
690
+ uri: 'foo://foo',
691
+ description: 'description',
692
+ },
693
+ async () => ({ content: [{ text: 'dynamic text' }] })
694
+ );
695
+ ai.defineResource(
696
+ {
697
+ name: 'regularResource',
698
+ template: 'bar://{value}',
699
+ description: 'description 2',
700
+ },
701
+ async () => ({ content: [{ text: 'regular text' }] })
702
+ );
703
+
704
+ const { text } = await ai.generate({
705
+ model: 'echoModel',
706
+ prompt: [
707
+ { text: 'some text' },
708
+ { resource: { uri: 'foo://foo' } },
709
+ { resource: { uri: 'bar://bar' } },
710
+ ],
711
+ resources: [dynamicTestResource],
712
+ });
713
+ assert.strictEqual(
714
+ text,
715
+ 'Echo: some text,dynamic text,regular text; config: {}'
716
+ );
717
+ assert.deepStrictEqual((echo as any).__test__lastRequest.messages, [
718
+ {
719
+ role: 'user',
720
+ content: [
721
+ { text: 'some text' },
722
+ {
723
+ metadata: {
724
+ resource: {
725
+ uri: 'foo://foo',
726
+ },
727
+ },
728
+ text: 'dynamic text',
729
+ },
730
+ {
731
+ metadata: {
732
+ resource: {
733
+ template: 'bar://{value}',
734
+ uri: 'bar://bar',
735
+ },
736
+ },
737
+ text: 'regular text',
738
+ },
739
+ ],
740
+ },
741
+ ]);
742
+ });
743
+
744
+ it('interrupts the dynamic tool with no impl', async () => {
745
+ const schema = {
746
+ properties: {
747
+ foo: { type: 'string' },
748
+ },
749
+ } as JSONSchema7;
750
+ const dynamicTestTool = ai.dynamicTool({
751
+ name: 'dynamicTestTool',
752
+ inputJsonSchema: schema,
753
+ description: 'description',
754
+ });
755
+
756
+ // first response is a tool call, the subsequent responses are just text response from agent b.
757
+ let reqCounter = 0;
758
+ pm.handleResponse = async (req, sc) => {
759
+ return {
760
+ message: {
761
+ role: 'model',
762
+ content: [
763
+ reqCounter++ === 0
764
+ ? {
765
+ toolRequest: {
766
+ name: 'dynamicTestTool',
767
+ input: { foo: 'bar' },
768
+ ref: 'ref123',
769
+ },
770
+ }
771
+ : { text: 'done' },
772
+ ],
773
+ },
774
+ };
775
+ };
776
+
777
+ const response = await ai.generate({
778
+ prompt: 'call the tool',
779
+ tools: [dynamicTestTool],
780
+ });
781
+
782
+ assert.deepStrictEqual(response.interrupts, [
783
+ {
784
+ metadata: {
785
+ interrupt: true,
786
+ },
787
+ toolRequest: {
788
+ input: {
789
+ foo: 'bar',
790
+ },
791
+ name: 'dynamicTestTool',
792
+ ref: 'ref123',
793
+ },
794
+ },
795
+ ]);
796
+ });
797
+
798
+ it('call the tool with output schema', async () => {
799
+ const schema = z.object({
800
+ foo: z.string(),
801
+ });
802
+
803
+ ai.defineTool(
804
+ {
805
+ name: 'testTool',
806
+ description: 'description',
807
+ inputSchema: schema,
808
+ outputSchema: schema,
809
+ },
810
+ async () => {
811
+ return {
812
+ foo: 'bar',
813
+ };
814
+ }
815
+ );
816
+
817
+ // first response is a tool call, the subsequent responses are just text response from agent b.
818
+ let reqCounter = 0;
819
+ pm.handleResponse = async (req, sc) => {
820
+ return {
821
+ message: {
822
+ role: 'model',
823
+ content: [
824
+ reqCounter++ === 0
825
+ ? {
826
+ toolRequest: {
827
+ name: 'testTool',
828
+ input: { foo: 'fromTool' },
829
+ ref: 'ref123',
830
+ },
831
+ }
832
+ : {
833
+ text: "```\n{foo: 'fromModel'}\n```",
834
+ },
835
+ ],
836
+ },
837
+ };
838
+ };
839
+ const { text, output } = await ai.generate({
840
+ output: { schema },
841
+ prompt: 'call the tool',
842
+ tools: ['testTool'],
843
+ });
844
+ assert.strictEqual(text, "```\n{foo: 'fromModel'}\n```");
845
+ assert.deepStrictEqual(output, {
846
+ foo: 'fromModel',
847
+ });
848
+ });
849
+
850
+ it('should propagate context to the tool', async () => {
851
+ const schema = z.object({
852
+ foo: z.string(),
853
+ });
854
+
855
+ ai.defineTool(
856
+ {
857
+ name: 'testTool',
858
+ description: 'description',
859
+ inputSchema: schema,
860
+ outputSchema: schema,
861
+ },
862
+ async (_, { context }) => {
863
+ return {
864
+ foo: `bar ${context.auth?.email}`,
865
+ };
866
+ }
867
+ );
868
+
869
+ // first response is a tool call, the subsequent responses are just text response from agent b.
870
+ let reqCounter = 0;
871
+ pm.handleResponse = async (req, sc) => {
872
+ return {
873
+ message: {
874
+ role: 'model',
875
+ content: [
876
+ reqCounter++ === 0
877
+ ? {
878
+ toolRequest: {
879
+ name: 'testTool',
880
+ input: { foo: 'fromTool' },
881
+ ref: 'ref123',
882
+ },
883
+ }
884
+ : {
885
+ text: req.messages
886
+ .splice(-1)
887
+ .map((m) =>
888
+ m.content
889
+ .map(
890
+ (c) =>
891
+ c.text || JSON.stringify(c.toolResponse?.output)
892
+ )
893
+ .join()
894
+ )
895
+ .join(),
896
+ },
897
+ ],
898
+ },
899
+ };
900
+ };
901
+ const { text } = await ai.generate({
902
+ prompt: 'call the tool',
903
+ tools: ['testTool'],
904
+ context: { auth: { email: 'a@b.c' } },
905
+ });
906
+ assert.strictEqual(text, '{"foo":"bar a@b.c"}');
907
+ });
908
+
909
+ it('calls the multipart tool', async () => {
910
+ const t = ai.defineTool(
911
+ { name: 'testTool', description: 'description', multipart: true },
912
+ async () => ({
913
+ output: 'tool called',
914
+ content: [{ text: 'part 1' }],
915
+ })
916
+ );
917
+
918
+ // first response is a tool call, the subsequent responses are just text response from agent b.
919
+ let reqCounter = 0;
920
+ pm.handleResponse = async (req, sc) => {
921
+ return {
922
+ message: {
923
+ role: 'model',
924
+ content: [
925
+ reqCounter++ === 0
926
+ ? {
927
+ toolRequest: {
928
+ name: 'testTool',
929
+ input: {},
930
+ ref: 'ref123',
931
+ },
932
+ }
933
+ : { text: 'done' },
934
+ ],
935
+ },
936
+ };
937
+ };
938
+
939
+ const { text, messages } = await ai.generate({
940
+ prompt: 'call the tool',
941
+ tools: [t],
942
+ });
943
+
944
+ assert.strictEqual(text, 'done');
945
+ assert.strictEqual(messages.length, 4);
946
+ const toolMessage = messages[2];
947
+ assert.strictEqual(toolMessage.role, 'tool');
948
+ assert.deepStrictEqual(toolMessage.content, [
949
+ {
950
+ toolResponse: {
951
+ name: 'testTool',
952
+ ref: 'ref123',
953
+ output: 'tool called',
954
+ content: [{ text: 'part 1' }],
955
+ },
956
+ },
957
+ ]);
958
+ });
959
+
960
+ it('streams the tool responses', async () => {
961
+ ai.defineTool(
962
+ { name: 'testTool', description: 'description' },
963
+ async () => 'tool called'
964
+ );
965
+
966
+ // first response is a tool call, the subsequent responses are just text response from agent b.
967
+ let reqCounter = 0;
968
+ pm.handleResponse = async (req, sc) => {
969
+ if (sc) {
970
+ sc({
971
+ content: [
972
+ reqCounter === 0
973
+ ? {
974
+ toolRequest: {
975
+ name: 'testTool',
976
+ input: {},
977
+ ref: 'ref123',
978
+ },
979
+ }
980
+ : { text: 'done' },
981
+ ],
982
+ });
983
+ }
984
+ return {
985
+ message: {
986
+ role: 'model',
987
+ content: [
988
+ reqCounter++ === 0
989
+ ? {
990
+ toolRequest: {
991
+ name: 'testTool',
992
+ input: {},
993
+ ref: 'ref123',
994
+ },
995
+ }
996
+ : { text: 'done' },
997
+ ],
998
+ },
999
+ };
1000
+ };
1001
+
1002
+ const { stream, response } = await ai.generateStream({
1003
+ prompt: 'call the tool',
1004
+ tools: ['testTool'],
1005
+ });
1006
+
1007
+ const chunks: any[] = [];
1008
+ for await (const chunk of stream) {
1009
+ chunks.push(chunk.toJSON());
1010
+ }
1011
+
1012
+ assert.strictEqual((await response).text, 'done');
1013
+ assert.deepStrictEqual(chunks, [
1014
+ {
1015
+ content: [
1016
+ {
1017
+ toolRequest: {
1018
+ input: {},
1019
+ name: 'testTool',
1020
+ ref: 'ref123',
1021
+ },
1022
+ },
1023
+ ],
1024
+ index: 0,
1025
+ role: 'model',
1026
+ },
1027
+ {
1028
+ content: [
1029
+ {
1030
+ toolResponse: {
1031
+ name: 'testTool',
1032
+ output: 'tool called',
1033
+ ref: 'ref123',
1034
+ },
1035
+ },
1036
+ ],
1037
+ index: 1,
1038
+ role: 'tool',
1039
+ },
1040
+ {
1041
+ content: [{ text: 'done' }],
1042
+ index: 2,
1043
+ role: 'model',
1044
+ },
1045
+ ]);
1046
+ });
1047
+
1048
+ it('throws when exceeding max tool call iterations', async () => {
1049
+ ai.defineTool(
1050
+ { name: 'testTool', description: 'description' },
1051
+ async () => 'tool called'
1052
+ );
1053
+
1054
+ // this will result in the tool getting called infinitely in a loop.
1055
+ pm.handleResponse = async () => {
1056
+ return {
1057
+ message: {
1058
+ role: 'model',
1059
+ content: [
1060
+ {
1061
+ toolRequest: {
1062
+ name: 'testTool',
1063
+ input: {},
1064
+ ref: 'ref123',
1065
+ },
1066
+ },
1067
+ ],
1068
+ },
1069
+ };
1070
+ };
1071
+
1072
+ await assert.rejects(
1073
+ ai.generate({
1074
+ prompt: 'call the tool',
1075
+ tools: ['testTool'],
1076
+ maxTurns: 17,
1077
+ }),
1078
+ (err: Error) => {
1079
+ return err.message.includes(
1080
+ 'Exceeded maximum tool call iterations (17)'
1081
+ );
1082
+ }
1083
+ );
1084
+ });
1085
+
1086
+ it('interrupts tool execution', async () => {
1087
+ ai.defineTool(
1088
+ { name: 'simpleTool', description: 'description' },
1089
+ async (input) => `response: ${input.name}`
1090
+ );
1091
+ ai.defineTool(
1092
+ { name: 'interruptingTool', description: 'description' },
1093
+ async (input, { interrupt }) =>
1094
+ interrupt({ confirm: 'is it a banana?' })
1095
+ );
1096
+ ai.defineTool(
1097
+ { name: 'resumableTool', description: 'description' },
1098
+ async (input, { interrupt, resumed }) => {
1099
+ if ((resumed as any)?.status === 'ok') return true;
1100
+ return interrupt();
1101
+ }
1102
+ );
1103
+ const dynamicInterrupt = interrupt({
1104
+ name: 'dynamicInterrupt',
1105
+ description: 'description',
1106
+ });
1107
+
1108
+ // first response is a tool call, the subsequent responses are just text response from agent b.
1109
+ let reqCounter = 0;
1110
+ pm.handleResponse = async (req, sc) => {
1111
+ return {
1112
+ message: {
1113
+ role: 'model',
1114
+ content:
1115
+ reqCounter++ === 0
1116
+ ? [
1117
+ {
1118
+ text: 'reasoning',
1119
+ },
1120
+ {
1121
+ toolRequest: {
1122
+ name: 'interruptingTool',
1123
+ input: {},
1124
+ ref: 'ref123',
1125
+ },
1126
+ },
1127
+ {
1128
+ toolRequest: {
1129
+ name: 'simpleTool',
1130
+ input: { name: 'foo' },
1131
+ ref: 'ref456',
1132
+ },
1133
+ },
1134
+ {
1135
+ toolRequest: {
1136
+ name: 'resumableTool',
1137
+ input: { doIt: true },
1138
+ ref: 'ref789',
1139
+ },
1140
+ },
1141
+ {
1142
+ toolRequest: {
1143
+ name: 'dynamicInterrupt',
1144
+ input: { doIt: true },
1145
+ ref: 'ref890',
1146
+ },
1147
+ },
1148
+ ]
1149
+ : [{ text: 'done' }],
1150
+ },
1151
+ };
1152
+ };
1153
+
1154
+ const response = await ai.generate({
1155
+ prompt: 'call the tool',
1156
+ tools: [
1157
+ 'interruptingTool',
1158
+ 'simpleTool',
1159
+ 'resumableTool',
1160
+ dynamicInterrupt,
1161
+ ],
1162
+ });
1163
+
1164
+ assert.strictEqual(reqCounter, 1);
1165
+ assert.deepStrictEqual(response.toolRequests, [
1166
+ {
1167
+ toolRequest: {
1168
+ input: {},
1169
+ name: 'interruptingTool',
1170
+ ref: 'ref123',
1171
+ },
1172
+ metadata: {
1173
+ interrupt: {
1174
+ confirm: 'is it a banana?',
1175
+ },
1176
+ },
1177
+ },
1178
+ {
1179
+ toolRequest: {
1180
+ input: {
1181
+ name: 'foo',
1182
+ },
1183
+ name: 'simpleTool',
1184
+ ref: 'ref456',
1185
+ },
1186
+ metadata: {
1187
+ pendingOutput: 'response: foo',
1188
+ },
1189
+ },
1190
+ {
1191
+ metadata: {
1192
+ interrupt: true,
1193
+ },
1194
+ toolRequest: {
1195
+ name: 'resumableTool',
1196
+ ref: 'ref789',
1197
+ input: {
1198
+ doIt: true,
1199
+ },
1200
+ },
1201
+ },
1202
+ {
1203
+ metadata: { interrupt: true },
1204
+ toolRequest: {
1205
+ input: {
1206
+ doIt: true,
1207
+ },
1208
+ name: 'dynamicInterrupt',
1209
+ ref: 'ref890',
1210
+ },
1211
+ },
1212
+ ]);
1213
+ assert.deepStrictEqual(response.message?.toJSON(), {
1214
+ role: 'model',
1215
+ content: [
1216
+ {
1217
+ text: 'reasoning',
1218
+ },
1219
+ {
1220
+ metadata: {
1221
+ interrupt: {
1222
+ confirm: 'is it a banana?',
1223
+ },
1224
+ },
1225
+ toolRequest: {
1226
+ input: {},
1227
+ name: 'interruptingTool',
1228
+ ref: 'ref123',
1229
+ },
1230
+ },
1231
+ {
1232
+ toolRequest: {
1233
+ input: {
1234
+ name: 'foo',
1235
+ },
1236
+ name: 'simpleTool',
1237
+ ref: 'ref456',
1238
+ },
1239
+ metadata: {
1240
+ pendingOutput: 'response: foo',
1241
+ },
1242
+ },
1243
+ {
1244
+ metadata: {
1245
+ interrupt: true,
1246
+ },
1247
+ toolRequest: {
1248
+ name: 'resumableTool',
1249
+ ref: 'ref789',
1250
+ input: {
1251
+ doIt: true,
1252
+ },
1253
+ },
1254
+ },
1255
+ {
1256
+ metadata: { interrupt: true },
1257
+ toolRequest: {
1258
+ input: {
1259
+ doIt: true,
1260
+ },
1261
+ name: 'dynamicInterrupt',
1262
+ ref: 'ref890',
1263
+ },
1264
+ },
1265
+ ],
1266
+ });
1267
+ assert.deepStrictEqual(pm.lastRequest, {
1268
+ config: {},
1269
+ messages: [
1270
+ {
1271
+ role: 'user',
1272
+ content: [{ text: 'call the tool' }],
1273
+ },
1274
+ ],
1275
+ output: {},
1276
+ tools: [
1277
+ {
1278
+ description: 'description',
1279
+ inputSchema: {
1280
+ $schema: 'http://json-schema.org/draft-07/schema#',
1281
+ },
1282
+ name: 'interruptingTool',
1283
+ outputSchema: {
1284
+ $schema: 'http://json-schema.org/draft-07/schema#',
1285
+ },
1286
+ key: '/tool/interruptingTool',
1287
+ },
1288
+ {
1289
+ description: 'description',
1290
+ inputSchema: {
1291
+ $schema: 'http://json-schema.org/draft-07/schema#',
1292
+ },
1293
+ name: 'simpleTool',
1294
+ outputSchema: {
1295
+ $schema: 'http://json-schema.org/draft-07/schema#',
1296
+ },
1297
+ key: '/tool/simpleTool',
1298
+ },
1299
+ {
1300
+ description: 'description',
1301
+ inputSchema: {
1302
+ $schema: 'http://json-schema.org/draft-07/schema#',
1303
+ },
1304
+ name: 'resumableTool',
1305
+ outputSchema: {
1306
+ $schema: 'http://json-schema.org/draft-07/schema#',
1307
+ },
1308
+ key: '/tool/resumableTool',
1309
+ },
1310
+ {
1311
+ description: 'description',
1312
+ inputSchema: {
1313
+ $schema: 'http://json-schema.org/draft-07/schema#',
1314
+ },
1315
+ name: 'dynamicInterrupt',
1316
+ outputSchema: {
1317
+ $schema: 'http://json-schema.org/draft-07/schema#',
1318
+ },
1319
+ key: '/tool/dynamicInterrupt',
1320
+ },
1321
+ ],
1322
+ });
1323
+ });
1324
+
1325
+ it('can resume generation', async () => {
1326
+ const interrupter = ai.defineInterrupt({
1327
+ name: 'interrupter',
1328
+ description: 'always interrupts',
1329
+ });
1330
+ const truth = ai.defineTool(
1331
+ { name: 'truth', description: 'always returns true' },
1332
+ async () => true
1333
+ );
1334
+ const resumable = ai.defineTool(
1335
+ {
1336
+ name: 'resumable',
1337
+ description: 'interrupts unless resumed with {status: "ok"}',
1338
+ },
1339
+ async (_input, { interrupt, resumed }) => {
1340
+ console.log('RESUMABLE TOOL CALLED WITH:', resumed);
1341
+ if ((resumed as any)?.status === 'ok') return true;
1342
+ return interrupt();
1343
+ }
1344
+ );
1345
+
1346
+ const messages: MessageData[] = [
1347
+ { role: 'user', content: [{ text: 'hello' }] },
1348
+ {
1349
+ role: 'model',
1350
+ content: [
1351
+ {
1352
+ toolRequest: { name: 'interrupter', input: {} },
1353
+ metadata: { interrupt: true },
1354
+ },
1355
+ {
1356
+ toolRequest: { name: 'truth', input: {} },
1357
+ metadata: { pendingOutput: true },
1358
+ },
1359
+ {
1360
+ toolRequest: { name: 'resumable', input: {} },
1361
+ metadata: { interrupt: true },
1362
+ },
1363
+ ],
1364
+ },
1365
+ ];
1366
+
1367
+ const response = await ai.generate({
1368
+ model: 'echoModel',
1369
+ messages,
1370
+ tools: [interrupter, resumable, truth],
1371
+ resume: {
1372
+ respond: interrupter.respond(
1373
+ {
1374
+ toolRequest: { name: 'interrupter', input: {} },
1375
+ metadata: { interrupt: true },
1376
+ },
1377
+ 23
1378
+ ),
1379
+ restart: resumable.restart(
1380
+ {
1381
+ toolRequest: { name: 'resumable', input: {} },
1382
+ metadata: { interrupt: true },
1383
+ },
1384
+ { status: 'ok' }
1385
+ ),
1386
+ },
1387
+ });
1388
+
1389
+ const revisedModelMessage = response.messages.at(-3);
1390
+ const toolMessage = response.messages.at(-2);
1391
+
1392
+ assert.deepStrictEqual(
1393
+ revisedModelMessage?.content,
1394
+ [
1395
+ {
1396
+ metadata: {
1397
+ resolvedInterrupt: true,
1398
+ },
1399
+ toolRequest: {
1400
+ input: {},
1401
+ name: 'interrupter',
1402
+ },
1403
+ },
1404
+ {
1405
+ metadata: {},
1406
+ toolRequest: {
1407
+ input: {},
1408
+ name: 'truth',
1409
+ },
1410
+ },
1411
+ {
1412
+ metadata: {
1413
+ resolvedInterrupt: true,
1414
+ },
1415
+ toolRequest: {
1416
+ input: {},
1417
+ name: 'resumable',
1418
+ },
1419
+ },
1420
+ ],
1421
+ 'resuming amends the model message to resolve interrupts'
1422
+ );
1423
+ assert.deepStrictEqual(
1424
+ toolMessage?.content,
1425
+ [
1426
+ {
1427
+ metadata: {
1428
+ interruptResponse: true,
1429
+ },
1430
+ toolResponse: {
1431
+ name: 'interrupter',
1432
+ output: 23,
1433
+ },
1434
+ },
1435
+ {
1436
+ metadata: {
1437
+ source: 'pending',
1438
+ },
1439
+ toolResponse: {
1440
+ name: 'truth',
1441
+ output: true,
1442
+ },
1443
+ },
1444
+ {
1445
+ toolResponse: {
1446
+ name: 'resumable',
1447
+ output: true,
1448
+ },
1449
+ },
1450
+ ],
1451
+ 'resuming generates a tool message containing all expected responses'
1452
+ );
1453
+ });
1454
+
1455
+ it('streams a generated tool message when resumed', async () => {
1456
+ pm.handleResponse = async (request, sendChunk) => {
1457
+ sendChunk?.({
1458
+ role: 'model',
1459
+ index: 0,
1460
+ content: [{ text: 'final response' }],
1461
+ });
1462
+ return {
1463
+ message: { role: 'model', content: [{ text: 'final response' }] },
1464
+ };
1465
+ };
1466
+
1467
+ const chunks: GenerateResponseChunkData[] = [];
1468
+ await ai.generate({
1469
+ onChunk: (chunk) => chunks.push(chunk.toJSON()),
1470
+ messages: [
1471
+ { role: 'user', content: [{ text: 'use the doThing tool' }] },
1472
+ {
1473
+ role: 'model',
1474
+ content: [
1475
+ {
1476
+ toolRequest: { name: 'doThing', input: {} },
1477
+ metadata: { interrupt: true },
1478
+ },
1479
+ ],
1480
+ },
1481
+ ],
1482
+ resume: {
1483
+ respond: { toolResponse: { name: 'doThing', output: 'did thing' } },
1484
+ },
1485
+ });
1486
+
1487
+ assert.deepStrictEqual(chunks, [
1488
+ {
1489
+ content: [
1490
+ {
1491
+ toolResponse: {
1492
+ name: 'doThing',
1493
+ output: 'did thing',
1494
+ },
1495
+ },
1496
+ ],
1497
+ index: 0,
1498
+ role: 'tool',
1499
+ },
1500
+ {
1501
+ content: [
1502
+ {
1503
+ text: 'final response',
1504
+ },
1505
+ ],
1506
+ index: 1,
1507
+ role: 'model',
1508
+ },
1509
+ ]);
1510
+ });
1511
+ });
1512
+
1513
+ describe('long running', () => {
1514
+ let ai: GenkitBeta;
1515
+
1516
+ beforeEach(() => {
1517
+ ai = genkit({
1518
+ model: 'programmableModel',
1519
+ });
1520
+ defineProgrammableModel(ai);
1521
+ });
1522
+
1523
+ it('starts the operation', async () => {
1524
+ ai.defineTool(
1525
+ { name: 'testTool', description: 'description' },
1526
+ async () => 'tool called'
1527
+ );
1528
+
1529
+ ai.defineBackgroundModel({
1530
+ name: 'bkg-model',
1531
+ async start(_) {
1532
+ return {
1533
+ id: '123',
1534
+ };
1535
+ },
1536
+ async check(operation) {
1537
+ return {
1538
+ id: '123',
1539
+ };
1540
+ },
1541
+ });
1542
+
1543
+ const { operation } = await ai.generate({
1544
+ model: 'bkg-model',
1545
+ prompt: 'call the tool',
1546
+ tools: ['testTool'],
1547
+ });
1548
+
1549
+ delete (operation as any).latencyMs;
1550
+ assert.deepStrictEqual(operation, {
1551
+ action: '/background-model/bkg-model',
1552
+ id: '123',
1553
+ });
1554
+ });
1555
+
1556
+ it('checks operation status', async () => {
1557
+ const newOp = {
1558
+ id: '123',
1559
+ done: true,
1560
+ output: {
1561
+ finishReason: 'stop',
1562
+ message: {
1563
+ role: 'model',
1564
+ content: [{ text: 'done' }],
1565
+ },
1566
+ },
1567
+ } as Operation;
1568
+
1569
+ ai.defineBackgroundModel({
1570
+ name: 'bkg-model',
1571
+ async start(_) {
1572
+ return {
1573
+ id: '123',
1574
+ };
1575
+ },
1576
+ async check(operation) {
1577
+ return { ...newOp };
1578
+ },
1579
+ });
1580
+
1581
+ const operation = await ai.checkOperation({
1582
+ action: '/background-model/bkg-model',
1583
+ id: '123',
1584
+ });
1585
+
1586
+ assert.deepStrictEqual(operation, {
1587
+ ...newOp,
1588
+ action: '/background-model/bkg-model',
1589
+ });
1590
+ });
1591
+
1592
+ it('cancels operation', async () => {
1593
+ const newOp = {
1594
+ id: '123',
1595
+ done: true,
1596
+ output: {
1597
+ finishReason: 'stop',
1598
+ message: {
1599
+ role: 'model',
1600
+ content: [{ text: 'cancelled' }],
1601
+ },
1602
+ },
1603
+ } as Operation;
1604
+
1605
+ ai.defineBackgroundModel({
1606
+ name: 'bkg-model',
1607
+ async start(_) {
1608
+ return {
1609
+ id: '123',
1610
+ };
1611
+ },
1612
+ async check(operation) {
1613
+ return operation;
1614
+ },
1615
+ async cancel(operation) {
1616
+ return { ...newOp };
1617
+ },
1618
+ });
1619
+
1620
+ const operation = await ai.cancelOperation({
1621
+ action: '/background-model/bkg-model',
1622
+ id: '123',
1623
+ });
1624
+
1625
+ assert.deepStrictEqual(operation, {
1626
+ ...newOp,
1627
+ action: '/background-model/bkg-model',
1628
+ });
1629
+ });
1630
+ });
1631
+ });