@statelyai/agent 1.1.6 → 2.0.0-next.1

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 (62) hide show
  1. package/.changeset/cyan-carpets-perform.md +5 -0
  2. package/.changeset/fast-donkeys-argue.md +5 -0
  3. package/.changeset/light-hats-drive.md +9 -0
  4. package/.changeset/old-jobs-check.md +5 -0
  5. package/.changeset/pre.json +13 -0
  6. package/.vscode/launch.json +6 -0
  7. package/CHANGELOG.md +22 -0
  8. package/dist/index.d.mts +262 -171
  9. package/dist/index.d.ts +262 -171
  10. package/dist/index.js +383 -274
  11. package/dist/index.mjs +386 -272
  12. package/examples/chatbot-alt.ts +57 -0
  13. package/examples/chatbot.ts +12 -17
  14. package/examples/cot.ts +26 -23
  15. package/examples/customer-service-sim.ts +107 -0
  16. package/examples/email.ts +37 -41
  17. package/examples/example.ts +6 -6
  18. package/examples/executor.ts +66 -0
  19. package/examples/goal.ts +12 -12
  20. package/examples/helpers/helpers.ts +26 -14
  21. package/examples/joke.ts +79 -76
  22. package/examples/jugs.ts +125 -0
  23. package/examples/multi.ts +5 -5
  24. package/examples/newspaper.ts +98 -104
  25. package/examples/number.ts +6 -5
  26. package/examples/raffle.ts +11 -12
  27. package/examples/river-crossing.ts +140 -0
  28. package/examples/sandbox.ts +1 -1
  29. package/examples/simple.ts +5 -3
  30. package/examples/summary.ts +121 -0
  31. package/examples/support.ts +6 -6
  32. package/examples/ticTacToe.ts +86 -45
  33. package/examples/todo.ts +7 -7
  34. package/examples/tutor.ts +15 -15
  35. package/examples/verify.ts +3 -3
  36. package/examples/weather.ts +6 -9
  37. package/examples/wiki.ts +27 -8
  38. package/examples/word.ts +16 -11
  39. package/package.json +16 -11
  40. package/readme.md +1 -1
  41. package/src/agent-experimental.ts +1 -1
  42. package/src/agent.test.ts +243 -214
  43. package/src/agent.ts +286 -95
  44. package/src/decide.test.ts +276 -0
  45. package/src/decide.ts +163 -0
  46. package/src/index.ts +1 -1
  47. package/src/middleware.ts +91 -0
  48. package/src/mockModel.ts +47 -0
  49. package/src/planners/shortestPath.test.ts +94 -0
  50. package/src/planners/shortestPath.ts +177 -0
  51. package/src/planners/simple.ts +105 -0
  52. package/src/strategies/chain-of-note.ts +6 -55
  53. package/src/text.ts +51 -144
  54. package/src/types.ts +187 -212
  55. package/src/utils.ts +48 -4
  56. package/vitest.config.ts +9 -3
  57. package/src/adapters/vercel.ts +0 -7
  58. package/src/decision.test.ts +0 -179
  59. package/src/decision.ts +0 -84
  60. package/src/memory.ts +0 -25
  61. package/src/planners/shortestPathPlanner.ts +0 -22
  62. package/src/planners/simplePlanner.ts +0 -139
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
- name: 'test',
10
+ id: '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
- name: 'test',
34
+ id: '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
  );
@@ -81,7 +65,7 @@ test('agent.addMessage() adds to message history', () => {
81
65
 
82
66
  test('agent.addFeedback() adds to feedback', () => {
83
67
  const agent = createAgent({
84
- name: 'test',
68
+ id: 'test',
85
69
  events: {},
86
70
  model: {} as any,
87
71
  });
@@ -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
  );
@@ -122,7 +106,7 @@ test('agent.addFeedback() adds to feedback', () => {
122
106
 
123
107
  test('agent.addObservation() adds to observations', () => {
124
108
  const agent = createAgent({
125
- name: 'test',
109
+ id: 'test',
126
110
  events: {},
127
111
  model: {} as any,
128
112
  });
@@ -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
+ id: '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
  );
@@ -148,7 +154,7 @@ test('agent.addObservation() adds to observations', () => {
148
154
 
149
155
  test('agent.addObservation() adds to observations with machine hash', () => {
150
156
  const agent = createAgent({
151
- name: 'test',
157
+ id: 'test',
152
158
  events: {},
153
159
  model: {} as any,
154
160
  });
@@ -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
+ id: '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
  );
@@ -198,7 +251,7 @@ test('agent.interact() observes machine actors (no 2nd arg)', () => {
198
251
  });
199
252
 
200
253
  const agent = createAgent({
201
- name: 'test',
254
+ id: 'test',
202
255
  events: {},
203
256
  model: {} as any,
204
257
  });
@@ -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
- name: 'test',
292
+ id: '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
- name: 'test',
285
- model: {} as any,
335
+ id: 'test',
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);
@@ -363,144 +394,142 @@ test('agent.types provides context and event types', () => {
363
394
  agent.types.context satisfies { score: string };
364
395
  });
365
396
 
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
- });
397
+ test('It allows unrecognized events', () => {
398
+ const agent = createAgent({
399
+ model: {} as any,
400
+ events: {},
401
+ context: {},
402
+ });
384
403
 
385
- const promise = new Promise<AgentMessage>((res) => {
386
- agent.onMessage((msg) => {
387
- if (msg.role === 'assistant') {
388
- res(msg);
389
- }
390
- });
404
+ expect(() => {
405
+ agent.send({
406
+ // @ts-expect-error
407
+ type: 'unrecognized',
391
408
  });
409
+ }).not.toThrow();
410
+ });
392
411
 
393
- await agent[method]({
394
- prompt: 'hi',
395
- correlationId: 'c-1',
396
- });
412
+ test('You can listen for message events', () => {
413
+ const fn = vi.fn();
414
+ const agent = createAgent({
415
+ id: 'test',
416
+ events: {},
417
+ model: {} as any,
418
+ });
397
419
 
398
- const msg = await promise;
420
+ agent.onMessage(fn);
399
421
 
400
- expect(msg.correlationId).toBe('c-1');
401
- expect(msg.parentCorrelationId).toBe(undefined);
402
- }
403
- );
422
+ const message = {
423
+ role: 'user' as const,
424
+ content: [{ type: 'text' as const, text: 'test message' }],
425
+ };
404
426
 
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
- };
427
+ agent.addMessage(message);
416
428
 
417
- opts.onFinish?.(res);
429
+ expect(fn).toHaveBeenCalledWith(
430
+ expect.objectContaining({
431
+ role: 'user',
432
+ content: [{ type: 'text', text: 'test message' }],
433
+ episodeId: expect.any(String),
434
+ timestamp: expect.any(Number),
435
+ })
436
+ );
437
+ });
418
438
 
419
- return res as AgentGenerateTextResult;
420
- },
421
- } as any as AIAdapter,
422
- });
439
+ test('agent.getPlans() returns plans from context', () => {
440
+ const agent = createAgent({
441
+ id: 'test',
442
+ events: {},
443
+ model: {} as any,
444
+ planner: async (agent) => {
445
+ return {
446
+ episodeId: agent.episodeId,
447
+ planner: 'test-planner',
448
+ goal: '',
449
+ goalState: undefined,
450
+ paths: [
451
+ {
452
+ state: undefined,
453
+ steps: [],
454
+ },
455
+ ],
456
+ nextEvent: undefined,
457
+ timestamp: Date.now(),
458
+ };
459
+ },
460
+ });
423
461
 
424
- await agent[method]({
425
- prompt: 'hi',
426
- });
462
+ const plans = agent.getPlans();
463
+
464
+ expect(plans).toBeDefined();
465
+ expect(Array.isArray(plans)).toBe(true);
466
+ });
467
+
468
+ test('Event listeners can be unsubscribed', () => {
469
+ const fn = vi.fn();
470
+ const agent = createAgent({
471
+ id: 'test',
472
+ events: {},
473
+ model: {} as any,
474
+ });
427
475
 
428
- const messages = agent.getMessages();
476
+ const subscription = agent.on('message', fn);
429
477
 
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');
478
+ agent.addMessage({
479
+ role: 'user',
480
+ content: [{ type: 'text', text: 'first message' }],
481
+ });
434
482
 
435
- expect(messages[0]!.correlationId).toEqual(messages[1]!.correlationId);
436
- }
437
- );
483
+ expect(fn).toHaveBeenCalledTimes(1);
438
484
 
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
- };
485
+ subscription.unsubscribe();
450
486
 
451
- opts.onFinish?.(res);
487
+ agent.addMessage({
488
+ role: 'user',
489
+ content: [{ type: 'text', text: 'second message' }],
490
+ });
452
491
 
453
- return res as AgentGenerateTextResult;
454
- },
455
- } as any as AIAdapter,
456
- });
492
+ expect(fn).toHaveBeenCalledTimes(1); // Still only called once
493
+ });
457
494
 
458
- await agent[method]({
459
- prompt: 'hi',
460
- correlationId: 'c-1',
461
- parentCorrelationId: 'c-0',
462
- });
495
+ test('agent.observe() adds observations from actor snapshots', () => {
496
+ const machine = createMachine({
497
+ initial: 'idle',
498
+ states: {
499
+ idle: {
500
+ on: { START: 'running' },
501
+ },
502
+ running: {},
503
+ },
504
+ });
463
505
 
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
- });
506
+ const agent = createAgent({
507
+ id: 'test',
508
+ events: {},
509
+ model: {} as any,
510
+ });
490
511
 
491
- const res = await agent[method]({
492
- prompt: 'test',
493
- });
512
+ const actor = createActor(machine);
513
+ const subscription = agent.observe(actor);
494
514
 
495
- agent.addFeedback({
496
- correlationId: res.correlationId,
497
- reward: -1,
498
- });
515
+ actor.start();
516
+ actor.send({ type: 'START' });
499
517
 
500
- const message = agent.getMessages()[0]!;
501
- const feedback = agent.getFeedback()[0]!;
518
+ expect(agent.getObservations()).toContainEqual(
519
+ expect.objectContaining({
520
+ state: expect.objectContaining({ value: 'idle' }),
521
+ machineHash: expect.any(String),
522
+ })
523
+ );
524
+
525
+ expect(agent.getObservations()).toContainEqual(
526
+ expect.objectContaining({
527
+ prevState: expect.objectContaining({ value: 'idle' }),
528
+ event: { type: 'START' },
529
+ state: expect.objectContaining({ value: 'running' }),
530
+ machineHash: expect.any(String),
531
+ })
532
+ );
502
533
 
503
- expect(message.correlationId).toBeDefined();
504
- expect(feedback.correlationId).toEqual(message.correlationId);
505
- }
506
- );
534
+ subscription.unsubscribe();
535
+ });