@statelyai/agent 1.1.5 → 2.0.0-next.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 (54) hide show
  1. package/.changeset/light-hats-drive.md +9 -0
  2. package/.changeset/pre.json +10 -0
  3. package/.vscode/launch.json +6 -0
  4. package/CHANGELOG.md +17 -0
  5. package/dist/index.d.mts +262 -165
  6. package/dist/index.d.ts +262 -165
  7. package/dist/index.js +368 -258
  8. package/dist/index.mjs +371 -256
  9. package/examples/chatbot-alt.ts +57 -0
  10. package/examples/chatbot.ts +11 -16
  11. package/examples/cot.ts +25 -22
  12. package/examples/customer-service-sim.ts +107 -0
  13. package/examples/email.ts +14 -14
  14. package/examples/example.ts +5 -5
  15. package/examples/executor.ts +66 -0
  16. package/examples/goal.ts +11 -11
  17. package/examples/helpers/helpers.ts +26 -14
  18. package/examples/joke.ts +78 -75
  19. package/examples/jugs.ts +125 -0
  20. package/examples/multi.ts +4 -4
  21. package/examples/newspaper.ts +98 -104
  22. package/examples/number.ts +5 -4
  23. package/examples/raffle.ts +10 -11
  24. package/examples/river-crossing.ts +140 -0
  25. package/examples/sandbox.ts +1 -1
  26. package/examples/simple.ts +4 -2
  27. package/examples/summary.ts +121 -0
  28. package/examples/support.ts +5 -5
  29. package/examples/ticTacToe.ts +86 -45
  30. package/examples/todo.ts +6 -6
  31. package/examples/tutor.ts +13 -13
  32. package/examples/verify.ts +2 -2
  33. package/examples/weather.ts +5 -8
  34. package/examples/wiki.ts +26 -7
  35. package/examples/word.ts +15 -10
  36. package/package.json +15 -12
  37. package/readme.md +1 -1
  38. package/src/agent-experimental.ts +1 -1
  39. package/src/agent.test.ts +117 -228
  40. package/src/agent.ts +469 -81
  41. package/src/{decision.test.ts → decide.test.ts} +26 -50
  42. package/src/decide.ts +153 -0
  43. package/src/index.ts +1 -1
  44. package/src/middleware.ts +103 -0
  45. package/src/mockModel.ts +47 -0
  46. package/src/planners/shortestPathPlanner.ts +151 -13
  47. package/src/planners/simplePlanner.ts +57 -85
  48. package/src/strategies/chain-of-note.ts +6 -55
  49. package/src/text.ts +51 -139
  50. package/src/types.ts +172 -204
  51. package/src/utils.ts +37 -4
  52. package/src/adapters/vercel.ts +0 -7
  53. package/src/decision.ts +0 -84
  54. package/src/memory.ts +0 -25
package/src/agent.test.ts CHANGED
@@ -1,24 +1,18 @@
1
1
  import { test, expect, vi } from 'vitest';
2
- import {
3
- AgentGenerateTextResult,
4
- AgentMessage,
5
- createAgent,
6
- type AIAdapter,
7
- } from './';
2
+ import { createAgent } from './';
8
3
  import { createActor, createMachine } from 'xstate';
9
- import { GenerateTextResult } from 'ai';
4
+ import { LanguageModelV1CallOptions } from 'ai';
10
5
  import { z } from 'zod';
6
+ import { dummyResponseValues, MockLanguageModelV1 } from './mockModel';
11
7
 
12
8
  test('an agent has the expected interface', () => {
13
9
  const agent = createAgent({
14
10
  name: 'test',
15
11
  events: {},
16
- model: {} as any,
12
+ model: new MockLanguageModelV1(),
17
13
  });
18
14
 
19
15
  expect(agent.decide).toBeDefined();
20
- expect(agent.generateText).toBeDefined();
21
- expect(agent.streamText).toBeDefined();
22
16
 
23
17
  expect(agent.addMessage).toBeDefined();
24
18
  expect(agent.addObservation).toBeDefined();
@@ -34,46 +28,36 @@ test('an agent has the expected interface', () => {
34
28
  });
35
29
 
36
30
  test('agent.addMessage() adds to message history', () => {
31
+ const model = new MockLanguageModelV1();
32
+
37
33
  const agent = createAgent({
38
34
  name: 'test',
39
35
  events: {},
40
- model: {} as any,
36
+ model,
41
37
  });
42
38
 
43
39
  agent.addMessage({
44
- content: 'msg 1',
45
40
  role: 'user',
41
+ content: [{ type: 'text', text: 'msg 1' }],
46
42
  });
47
43
 
48
44
  const messageHistory = agent.addMessage({
49
- content: 'response 1',
50
45
  role: 'assistant',
46
+ content: [{ type: 'text', text: 'response 1' }],
51
47
  });
52
48
 
53
- expect(messageHistory.sessionId).toEqual(agent.sessionId);
49
+ expect(messageHistory.episodeId).toEqual(agent.episodeId);
54
50
 
55
- expect(agent.select((c) => c.messages)).toContainEqual(
56
- expect.objectContaining({
57
- content: 'msg 1',
58
- })
59
- );
60
51
  expect(agent.getMessages()).toContainEqual(
61
52
  expect.objectContaining({
62
- content: 'msg 1',
53
+ content: [expect.objectContaining({ text: 'msg 1' })],
63
54
  })
64
55
  );
65
56
 
66
- expect(agent.select((c) => c.messages)).toContainEqual(
67
- expect.objectContaining({
68
- content: 'response 1',
69
- sessionId: expect.any(String),
70
- timestamp: expect.any(Number),
71
- })
72
- );
73
57
  expect(agent.getMessages()).toContainEqual(
74
58
  expect.objectContaining({
75
- content: 'response 1',
76
- sessionId: expect.any(String),
59
+ content: [expect.objectContaining({ text: 'response 1' })],
60
+ episodeId: expect.any(String),
77
61
  timestamp: expect.any(Number),
78
62
  })
79
63
  );
@@ -94,16 +78,16 @@ test('agent.addFeedback() adds to feedback', () => {
94
78
  observationId: 'obs-1',
95
79
  });
96
80
 
97
- expect(feedback.sessionId).toEqual(agent.sessionId);
81
+ expect(feedback.episodeId).toEqual(agent.episodeId);
98
82
 
99
- expect(agent.select((c) => c.feedback)).toContainEqual(
83
+ expect(agent.getFeedback()).toContainEqual(
100
84
  expect.objectContaining({
101
85
  attributes: {
102
86
  score: -1,
103
87
  },
104
88
  goal: 'Win the game',
105
89
  observationId: 'obs-1',
106
- sessionId: expect.any(String),
90
+ episodeId: expect.any(String),
107
91
  timestamp: expect.any(Number),
108
92
  })
109
93
  );
@@ -114,7 +98,7 @@ test('agent.addFeedback() adds to feedback', () => {
114
98
  },
115
99
  goal: 'Win the game',
116
100
  observationId: 'obs-1',
117
- sessionId: expect.any(String),
101
+ episodeId: expect.any(String),
118
102
  timestamp: expect.any(Number),
119
103
  })
120
104
  );
@@ -133,14 +117,36 @@ test('agent.addObservation() adds to observations', () => {
133
117
  state: { value: 'lost', context: {} },
134
118
  });
135
119
 
136
- expect(observation.sessionId).toEqual(agent.sessionId);
120
+ expect(observation.episodeId).toEqual(agent.episodeId);
137
121
 
138
- expect(agent.select((c) => c.observations)).toContainEqual(
122
+ expect(agent.getObservations()).toContainEqual(
139
123
  expect.objectContaining({
140
124
  prevState: { value: 'playing', context: {} },
141
125
  event: { type: 'play', position: 3 },
142
126
  state: { value: 'lost', context: {} },
143
- sessionId: expect.any(String),
127
+ episodeId: expect.any(String),
128
+ timestamp: expect.any(Number),
129
+ })
130
+ );
131
+ });
132
+
133
+ test('agent.addObservation() adds to observations (initial state)', () => {
134
+ const agent = createAgent({
135
+ name: 'test',
136
+ events: {},
137
+ model: {} as any,
138
+ });
139
+
140
+ const observation = agent.addObservation({
141
+ state: { value: 'lost' },
142
+ });
143
+
144
+ expect(observation.episodeId).toEqual(agent.episodeId);
145
+
146
+ expect(agent.getObservations()).toContainEqual(
147
+ expect.objectContaining({
148
+ state: { value: 'lost', context: undefined },
149
+ episodeId: expect.any(String),
144
150
  timestamp: expect.any(Number),
145
151
  })
146
152
  );
@@ -172,15 +178,62 @@ test('agent.addObservation() adds to observations with machine hash', () => {
172
178
  machine,
173
179
  });
174
180
 
175
- expect(observation.sessionId).toEqual(agent.sessionId);
181
+ expect(observation.episodeId).toEqual(agent.episodeId);
176
182
 
177
- expect(agent.select((c) => c.observations)).toContainEqual(
183
+ expect(agent.getObservations()).toContainEqual(
178
184
  expect.objectContaining({
179
185
  prevState: { value: 'playing', context: {} },
180
186
  event: { type: 'play', position: 3 },
181
187
  state: { value: 'lost', context: {} },
182
188
  machineHash: expect.any(String),
183
- sessionId: expect.any(String),
189
+ episodeId: expect.any(String),
190
+ timestamp: expect.any(Number),
191
+ })
192
+ );
193
+ });
194
+
195
+ test('agent.addFeedback() adds to feedback (with observation)', () => {
196
+ const agent = createAgent({
197
+ name: 'test',
198
+ events: {},
199
+ model: {} as any,
200
+ });
201
+
202
+ const observation = agent.addObservation({
203
+ state: {
204
+ value: 'playing',
205
+ },
206
+ });
207
+
208
+ const feedback = agent.addFeedback({
209
+ attributes: {
210
+ score: -1,
211
+ },
212
+ goal: 'Win the game',
213
+ observationId: observation.id,
214
+ });
215
+
216
+ expect(feedback.episodeId).toEqual(agent.episodeId);
217
+
218
+ expect(agent.getFeedback()).toContainEqual(
219
+ expect.objectContaining({
220
+ attributes: {
221
+ score: -1,
222
+ },
223
+ goal: 'Win the game',
224
+ observationId: observation.id,
225
+ episodeId: expect.any(String),
226
+ timestamp: expect.any(Number),
227
+ })
228
+ );
229
+ expect(agent.getFeedback()).toContainEqual(
230
+ expect.objectContaining({
231
+ attributes: {
232
+ score: -1,
233
+ },
234
+ goal: 'Win the game',
235
+ observationId: observation.id,
236
+ episodeId: expect.any(String),
184
237
  timestamp: expect.any(Number),
185
238
  })
186
239
  );
@@ -209,7 +262,7 @@ test('agent.interact() observes machine actors (no 2nd arg)', () => {
209
262
 
210
263
  actor.start();
211
264
 
212
- expect(agent.select((c) => c.observations)).toContainEqual(
265
+ expect(agent.getObservations()).toContainEqual(
213
266
  expect.objectContaining({
214
267
  prevState: undefined,
215
268
  state: expect.objectContaining({ value: 'a' }),
@@ -224,7 +277,7 @@ test('agent.interact() observes machine actors (no 2nd arg)', () => {
224
277
 
225
278
  actor.send({ type: 'NEXT' });
226
279
 
227
- expect(agent.select((c) => c.observations)).toContainEqual(
280
+ expect(agent.getObservations()).toContainEqual(
228
281
  expect.objectContaining({
229
282
  prevState: expect.objectContaining({ value: 'a' }),
230
283
  event: { type: 'NEXT' },
@@ -233,35 +286,11 @@ test('agent.interact() observes machine actors (no 2nd arg)', () => {
233
286
  );
234
287
  });
235
288
 
236
- test('Agents can use a custom adapter', async () => {
237
- const adapter = {
238
- generateText: async () => {
239
- return {
240
- text: 'Response',
241
- } as any;
242
- },
243
- } as unknown as AIAdapter;
244
-
245
- const agent = createAgent({
246
- name: 'test',
247
- events: {},
248
- adapter,
249
- model: {} as any,
250
- });
251
-
252
- const res = await agent.generateText({
253
- prompt: 'Question?',
254
- });
255
-
256
- expect(res.text).toEqual('Response');
257
- });
258
-
259
289
  test('You can listen for feedback events', () => {
260
290
  const fn = vi.fn();
261
291
  const agent = createAgent({
262
292
  name: 'test',
263
293
  events: {},
264
- adapter: {} as any,
265
294
  model: {} as any,
266
295
  });
267
296
 
@@ -280,32 +309,34 @@ test('You can listen for feedback events', () => {
280
309
 
281
310
  test('You can listen for plan events', async () => {
282
311
  const fn = vi.fn();
312
+ const model = new MockLanguageModelV1({
313
+ doGenerate: async (params: LanguageModelV1CallOptions) => {
314
+ const keys =
315
+ params.mode.type === 'regular'
316
+ ? params.mode.tools?.map((tool) => tool.name)
317
+ : [];
318
+
319
+ return {
320
+ ...dummyResponseValues,
321
+ finishReason: 'tool-calls',
322
+ toolCalls: [
323
+ {
324
+ toolCallType: 'function',
325
+ toolCallId: 'call-1',
326
+ toolName: keys![0],
327
+ args: `{ "type": "${keys?.[0]}" }`,
328
+ },
329
+ ],
330
+ } as any;
331
+ },
332
+ });
333
+
283
334
  const agent = createAgent({
284
335
  name: 'test',
285
- model: {} as any,
336
+ model,
286
337
  events: {
287
338
  WIN: z.object({}),
288
339
  },
289
- adapter: {
290
- generateText: async (arg) => {
291
- const keys = Object.keys(arg.tools!);
292
-
293
- if (keys.length !== 1) {
294
- throw new Error('Expected only 1 choice');
295
- }
296
-
297
- return {
298
- toolResults: [
299
- {
300
- result: {
301
- type: keys[0],
302
- },
303
- },
304
- ],
305
- } as any as AgentGenerateTextResult;
306
- },
307
- streamText: {} as any,
308
- },
309
340
  });
310
341
 
311
342
  agent.on('plan', fn);
@@ -362,145 +393,3 @@ test('agent.types provides context and event types', () => {
362
393
  // @ts-expect-error
363
394
  agent.types.context satisfies { score: string };
364
395
  });
365
-
366
- test.each(['generateText', 'streamText'] as const)(
367
- 'can provide a correlation ID (%s)',
368
- async (method) => {
369
- const agent = createAgent({
370
- model: {} as any,
371
- events: {},
372
- adapter: {
373
- [method]: async (opts: any) => {
374
- const res = {
375
- text: 'response',
376
- };
377
-
378
- opts.onFinish?.(res);
379
-
380
- return res as AgentGenerateTextResult;
381
- },
382
- } as any as AIAdapter,
383
- });
384
-
385
- const promise = new Promise<AgentMessage>((res) => {
386
- agent.onMessage((msg) => {
387
- if (msg.role === 'assistant') {
388
- res(msg);
389
- }
390
- });
391
- });
392
-
393
- await agent[method]({
394
- prompt: 'hi',
395
- correlationId: 'c-1',
396
- });
397
-
398
- const msg = await promise;
399
-
400
- expect(msg.correlationId).toBe('c-1');
401
- expect(msg.parentCorrelationId).toBe(undefined);
402
- }
403
- );
404
-
405
- test.each(['generateText', 'streamText'] as const)(
406
- 'correlation IDs are automatically generated if not provided (%s)',
407
- async (method) => {
408
- const agent = createAgent({
409
- model: {} as any,
410
- events: {},
411
- adapter: {
412
- [method]: async (opts: any) => {
413
- const res = {
414
- text: 'response',
415
- };
416
-
417
- opts.onFinish?.(res);
418
-
419
- return res as AgentGenerateTextResult;
420
- },
421
- } as any as AIAdapter,
422
- });
423
-
424
- await agent[method]({
425
- prompt: 'hi',
426
- });
427
-
428
- const messages = agent.getMessages();
429
-
430
- expect(messages[0]?.correlationId).toEqual(expect.stringMatching(/.+/));
431
- expect(messages[0]?.role).toBe('user');
432
- expect(messages[1]?.correlationId).toEqual(expect.stringMatching(/.+/));
433
- expect(messages[1]?.role).toBe('assistant');
434
-
435
- expect(messages[0]!.correlationId).toEqual(messages[1]!.correlationId);
436
- }
437
- );
438
-
439
- test.each(['generateText', 'streamText'] as const)(
440
- 'can provide a parent correlation ID (%s)',
441
- async (method) => {
442
- const agent = createAgent({
443
- model: {} as any,
444
- events: {},
445
- adapter: {
446
- [method]: async (opts: any) => {
447
- const res = {
448
- text: 'response',
449
- };
450
-
451
- opts.onFinish?.(res);
452
-
453
- return res as AgentGenerateTextResult;
454
- },
455
- } as any as AIAdapter,
456
- });
457
-
458
- await agent[method]({
459
- prompt: 'hi',
460
- correlationId: 'c-1',
461
- parentCorrelationId: 'c-0',
462
- });
463
-
464
- const msg = agent.getMessages().find((msg) => msg.role === 'assistant')!;
465
-
466
- expect(msg.correlationId).toBe('c-1');
467
- expect(msg.parentCorrelationId).toBe('c-0');
468
- }
469
- );
470
-
471
- test.each(['generateText', 'streamText'] as const)(
472
- 'can add feedback to a correlation (%s)',
473
- async (method) => {
474
- const agent = createAgent({
475
- name: 'test',
476
- model: {} as any,
477
- events: {},
478
- adapter: {
479
- [method]: async (opts: any) => {
480
- const res = {
481
- text: 'response',
482
- };
483
-
484
- opts.onFinish?.(res);
485
-
486
- return res as AgentGenerateTextResult;
487
- },
488
- } as any as AIAdapter,
489
- });
490
-
491
- const res = await agent[method]({
492
- prompt: 'test',
493
- });
494
-
495
- agent.addFeedback({
496
- correlationId: res.correlationId,
497
- reward: -1,
498
- });
499
-
500
- const message = agent.getMessages()[0]!;
501
- const feedback = agent.getFeedback()[0]!;
502
-
503
- expect(message.correlationId).toBeDefined();
504
- expect(feedback.correlationId).toEqual(message.correlationId);
505
- }
506
- );