@statelyai/agent 2.0.0-next.3 → 2.0.0-next.5

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 (37) hide show
  1. package/.changeset/calm-beans-talk.md +5 -0
  2. package/.changeset/long-guests-explode.md +5 -0
  3. package/.changeset/nice-pants-rule.md +10 -0
  4. package/.changeset/odd-kiwis-compare.md +5 -0
  5. package/.changeset/pre.json +4 -0
  6. package/CHANGELOG.md +23 -0
  7. package/architecture.tldr +797 -0
  8. package/dist/index.d.mts +198 -140
  9. package/dist/index.d.ts +198 -140
  10. package/dist/index.js +4397 -148
  11. package/dist/index.mjs +4397 -149
  12. package/examples/chatbot.ts +9 -5
  13. package/examples/cot.ts +2 -4
  14. package/examples/jugs.ts +2 -2
  15. package/examples/learn-from-feedback.ts +7 -7
  16. package/examples/newspaper.ts +1 -1
  17. package/examples/rewoo.ts +62 -0
  18. package/examples/river-crossing.ts +2 -2
  19. package/examples/serverless.ts +71 -0
  20. package/examples/simple.ts +1 -1
  21. package/examples/ticTacToe.ts +6 -2
  22. package/examples/wiki.ts +2 -2
  23. package/package.json +14 -12
  24. package/readme.md +57 -0
  25. package/src/agent.test.ts +387 -30
  26. package/src/agent.ts +177 -64
  27. package/src/decide.test.ts +24 -2
  28. package/src/decide.ts +34 -78
  29. package/src/index.ts +1 -0
  30. package/src/{strategies/chainOfThought.ts → policies/chainOfThoughtPolicy.ts} +7 -9
  31. package/src/policies/index.ts +3 -0
  32. package/src/{strategies/shortestPath.test.ts → policies/shortestPathPolicy.test.ts} +2 -2
  33. package/src/{strategies/shortestPath.ts → policies/shortestPathPolicy.ts} +8 -8
  34. package/src/{strategies/simple.ts → policies/toolPolicy.ts} +27 -26
  35. package/src/text.ts +17 -22
  36. package/src/types.ts +162 -166
  37. package/src/agent-experimental.ts +0 -221
package/src/agent.test.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  import { test, expect, vi } from 'vitest';
2
- import { createAgent, TypesFromAgent } from './';
2
+ import {
3
+ AgentDecision,
4
+ AgentFeedbackInput,
5
+ createAgent,
6
+ TypesFromAgent,
7
+ } from './';
3
8
  import { createActor, createMachine } from 'xstate';
4
9
  import { LanguageModelV1CallOptions } from 'ai';
5
10
  import { z } from 'zod';
@@ -66,28 +71,44 @@ test('agent.addMessage() adds to message history', () => {
66
71
  test('agent.addFeedback() adds to feedback', () => {
67
72
  const agent = createAgent({
68
73
  id: 'test',
69
- events: {},
74
+ events: {
75
+ play: z.object({
76
+ position: z.number(),
77
+ }),
78
+ },
70
79
  model: {} as any,
71
80
  });
72
81
 
82
+ const decision: AgentDecision<typeof agent> = {
83
+ goal: 'Win the game',
84
+ decisionId: null,
85
+ episodeId: agent.episodeId,
86
+ goalState: { value: 'won' },
87
+ id: 'decision-1',
88
+ nextEvent: { type: 'play', position: 3 },
89
+ paths: [],
90
+ policy: 'simple',
91
+ timestamp: Date.now(),
92
+ };
93
+
73
94
  const obs = agent.addObservation({
95
+ decisionId: decision.id,
74
96
  prevState: { value: 'playing' },
75
- state: { value: 'lost' },
76
97
  event: { type: 'play', position: 3 },
77
- goal: 'Win the game',
98
+ state: { value: 'lost' },
78
99
  });
79
100
 
80
101
  const feedback = agent.addFeedback({
81
- score: 0,
82
- observationId: obs.id,
102
+ reward: 0,
103
+ decisionId: decision.id,
83
104
  });
84
105
 
85
106
  expect(feedback.episodeId).toEqual(agent.episodeId);
86
107
 
87
108
  expect(agent.getFeedback()).toContainEqual(
88
109
  expect.objectContaining({
89
- score: 0,
90
- observationId: obs.id,
110
+ reward: 0,
111
+ decisionId: decision.id,
91
112
  episodeId: expect.any(String),
92
113
  timestamp: expect.any(Number),
93
114
  })
@@ -144,7 +165,7 @@ test('agent.addObservation() adds to observations (initial state)', () => {
144
165
  );
145
166
  });
146
167
 
147
- test('agent.addObservation() adds to observations with machine hash', () => {
168
+ test.skip('agent.addObservation() adds to observations with machine hash', () => {
148
169
  const agent = createAgent({
149
170
  id: 'test',
150
171
  events: {},
@@ -167,7 +188,6 @@ test('agent.addObservation() adds to observations with machine hash', () => {
167
188
  prevState: { value: 'playing', context: {} },
168
189
  event: { type: 'play', position: 3 },
169
190
  state: { value: 'lost', context: {} },
170
- machine,
171
191
  goal: 'Win the game',
172
192
  });
173
193
 
@@ -178,14 +198,13 @@ test('agent.addObservation() adds to observations with machine hash', () => {
178
198
  prevState: { value: 'playing', context: {} },
179
199
  event: { type: 'play', position: 3 },
180
200
  state: { value: 'lost', context: {} },
181
- machineHash: expect.any(String),
182
201
  episodeId: expect.any(String),
183
202
  timestamp: expect.any(Number),
184
203
  })
185
204
  );
186
205
  });
187
206
 
188
- test('agent.addFeedback() adds to feedback (with observation)', () => {
207
+ test('agent.addInsight() adds to insights (with observation)', () => {
189
208
  const agent = createAgent({
190
209
  id: 'test',
191
210
  events: {},
@@ -199,24 +218,26 @@ test('agent.addFeedback() adds to feedback (with observation)', () => {
199
218
  goal: 'Win the game',
200
219
  });
201
220
 
202
- const feedback = agent.addFeedback({
203
- score: 0,
221
+ const insight = agent.addInsight({
204
222
  observationId: observation.id,
223
+ attributes: {
224
+ advantage: 50,
225
+ },
205
226
  });
206
227
 
207
- expect(feedback.episodeId).toEqual(agent.episodeId);
228
+ expect(insight.episodeId).toEqual(agent.episodeId);
208
229
 
209
- expect(agent.getFeedback()).toContainEqual(
230
+ expect(agent.getInsights()).toContainEqual(
210
231
  expect.objectContaining({
211
- score: 0,
232
+ attributes: { advantage: 50 },
212
233
  observationId: observation.id,
213
234
  episodeId: expect.any(String),
214
235
  timestamp: expect.any(Number),
215
236
  })
216
237
  );
217
- expect(agent.getFeedback()).toContainEqual(
238
+ expect(agent.getInsights()).toContainEqual(
218
239
  expect.objectContaining({
219
- score: 0,
240
+ attributes: { advantage: 50 },
220
241
  observationId: observation.id,
221
242
  episodeId: expect.any(String),
222
243
  timestamp: expect.any(Number),
@@ -279,14 +300,25 @@ test('You can listen for feedback events', () => {
279
300
  model: {} as any,
280
301
  });
281
302
 
282
- agent.on('feedback', fn);
303
+ agent.onFeedback(fn);
283
304
 
284
305
  agent.addFeedback({
285
- score: -1,
286
- observationId: 'obs-1',
306
+ reward: 1,
307
+ decisionId: 'dec-1',
308
+ comment: 'Good move',
309
+ attributes: { confidence: 'high' },
287
310
  });
288
311
 
289
- expect(fn).toHaveBeenCalled();
312
+ expect(fn).toHaveBeenCalledWith(
313
+ expect.objectContaining({
314
+ reward: 1,
315
+ decisionId: 'dec-1',
316
+ comment: 'Good move',
317
+ attributes: { confidence: 'high' },
318
+ episodeId: expect.any(String),
319
+ timestamp: expect.any(Number),
320
+ })
321
+ );
290
322
  });
291
323
 
292
324
  test('You can listen for decision events', async () => {
@@ -425,20 +457,21 @@ test('agent.getDecisions() returns decisions from context', () => {
425
457
  id: 'test',
426
458
  events: {},
427
459
  model: {} as any,
428
- strategy: async (agent) => {
460
+ policy: async (agent) => {
429
461
  return {
430
462
  id: Date.now().toString(),
463
+ decisionId: null,
431
464
  episodeId: agent.episodeId,
432
- strategy: 'test-strategy',
465
+ policy: 'test-policy',
433
466
  goal: '',
434
- goalState: undefined,
467
+ goalState: null,
435
468
  paths: [
436
469
  {
437
- state: undefined,
470
+ state: null,
438
471
  steps: [],
439
472
  },
440
473
  ],
441
- nextEvent: undefined,
474
+ nextEvent: null,
442
475
  timestamp: Date.now(),
443
476
  };
444
477
  },
@@ -503,7 +536,6 @@ test('agent.observe() adds observations from actor snapshots', () => {
503
536
  expect(agent.getObservations()).toContainEqual(
504
537
  expect.objectContaining({
505
538
  state: expect.objectContaining({ value: 'idle' }),
506
- machineHash: expect.any(String),
507
539
  })
508
540
  );
509
541
 
@@ -512,9 +544,334 @@ test('agent.observe() adds observations from actor snapshots', () => {
512
544
  prevState: expect.objectContaining({ value: 'idle' }),
513
545
  event: { type: 'START' },
514
546
  state: expect.objectContaining({ value: 'running' }),
515
- machineHash: expect.any(String),
516
547
  })
517
548
  );
518
549
 
519
550
  subscription.unsubscribe();
520
551
  });
552
+
553
+ test('agent.addObservation() accepts custom episodeId', () => {
554
+ const agent = createAgent({
555
+ id: 'test',
556
+ events: {},
557
+ model: {} as any,
558
+ });
559
+
560
+ const customEpisodeId = 'custom-episode-123';
561
+ const observation = agent.addObservation({
562
+ state: { value: 'playing' },
563
+ goal: 'Win the game',
564
+ episodeId: customEpisodeId,
565
+ });
566
+
567
+ expect(observation.episodeId).toEqual(customEpisodeId);
568
+ expect(agent.getObservations()).toContainEqual(
569
+ expect.objectContaining({
570
+ episodeId: customEpisodeId,
571
+ })
572
+ );
573
+ });
574
+
575
+ test('agent.addFeedback() accepts custom episodeId', () => {
576
+ const agent = createAgent({
577
+ id: 'test',
578
+ events: {},
579
+ model: {} as any,
580
+ });
581
+
582
+ const customEpisodeId = 'custom-episode-123';
583
+ const feedback = agent.addFeedback({
584
+ reward: 1,
585
+ decisionId: 'dec-1',
586
+ episodeId: customEpisodeId,
587
+ });
588
+
589
+ expect(feedback.episodeId).toEqual(customEpisodeId);
590
+ expect(agent.getFeedback()).toContainEqual(
591
+ expect.objectContaining({
592
+ episodeId: customEpisodeId,
593
+ })
594
+ );
595
+ });
596
+
597
+ test('agent.addObservation() accepts decisionId', () => {
598
+ const agent = createAgent({
599
+ id: 'test',
600
+ events: {},
601
+ model: {} as any,
602
+ });
603
+
604
+ const decisionId = 'decision-123';
605
+ const observation = agent.addObservation({
606
+ state: { value: 'playing' },
607
+ goal: 'Win the game',
608
+ decisionId,
609
+ });
610
+
611
+ expect(observation.decisionId).toEqual(decisionId);
612
+ expect(agent.getObservations()).toContainEqual(
613
+ expect.objectContaining({
614
+ decisionId,
615
+ })
616
+ );
617
+ });
618
+
619
+ test('agent.addFeedback() accepts decisionId', () => {
620
+ const agent = createAgent({
621
+ id: 'test',
622
+ events: {},
623
+ model: {} as any,
624
+ });
625
+
626
+ const decisionId = 'decision-123';
627
+ const feedback = agent.addFeedback({
628
+ reward: 1,
629
+ decisionId,
630
+ });
631
+
632
+ expect(feedback.decisionId).toEqual(decisionId);
633
+ expect(agent.getFeedback()).toContainEqual(
634
+ expect.objectContaining({
635
+ decisionId,
636
+ })
637
+ );
638
+ });
639
+
640
+ test('You can listen for observation events', () => {
641
+ const fn = vi.fn();
642
+ const agent = createAgent({
643
+ id: 'test',
644
+ events: {},
645
+ model: {} as any,
646
+ });
647
+
648
+ agent.onObservation(fn);
649
+
650
+ agent.addObservation({
651
+ state: { value: 'playing' },
652
+ goal: 'Win the game',
653
+ });
654
+
655
+ expect(fn).toHaveBeenCalledWith(
656
+ expect.objectContaining({
657
+ state: { value: 'playing' },
658
+ episodeId: expect.any(String),
659
+ timestamp: expect.any(Number),
660
+ })
661
+ );
662
+ });
663
+
664
+ test('You can listen for decision events', () => {
665
+ const fn = vi.fn();
666
+ const agent = createAgent({
667
+ id: 'test',
668
+ events: {
669
+ MOVE: z.object({}),
670
+ },
671
+ model: {} as any,
672
+ });
673
+
674
+ agent.onDecision(fn);
675
+
676
+ const decision = {
677
+ id: 'decision-1',
678
+ decisionId: null,
679
+ episodeId: agent.episodeId,
680
+ policy: 'test-policy',
681
+ goal: 'Win the game',
682
+ goalState: { value: 'won' },
683
+ paths: [],
684
+ nextEvent: { type: 'MOVE' },
685
+ timestamp: Date.now(),
686
+ } satisfies AgentDecision<typeof agent>;
687
+
688
+ agent.addDecision(decision);
689
+
690
+ expect(fn).toHaveBeenCalledWith(
691
+ expect.objectContaining({
692
+ id: 'decision-1',
693
+ episodeId: agent.episodeId,
694
+ policy: 'test-policy',
695
+ goal: 'Win the game',
696
+ nextEvent: { type: 'MOVE' },
697
+ })
698
+ );
699
+ });
700
+
701
+ test('Event listeners can be unsubscribed (onObservation)', () => {
702
+ const fn = vi.fn();
703
+ const agent = createAgent({
704
+ id: 'test',
705
+ events: {},
706
+ model: {} as any,
707
+ });
708
+
709
+ const subscription = agent.onObservation(fn);
710
+
711
+ agent.addObservation({
712
+ state: { value: 'playing' },
713
+ goal: 'Win the game',
714
+ });
715
+
716
+ expect(fn).toHaveBeenCalledTimes(1);
717
+
718
+ subscription.unsubscribe();
719
+
720
+ agent.addObservation({
721
+ state: { value: 'playing' },
722
+ goal: 'Win the game again',
723
+ });
724
+
725
+ expect(fn).toHaveBeenCalledTimes(1); // Still only called once
726
+ });
727
+
728
+ test('Event listeners can be unsubscribed (onDecision)', () => {
729
+ const fn = vi.fn();
730
+ const agent = createAgent({
731
+ id: 'test',
732
+ events: {
733
+ MOVE: z.object({}),
734
+ },
735
+ model: {} as any,
736
+ });
737
+
738
+ const subscription = agent.onDecision(fn);
739
+
740
+ const decision = {
741
+ id: 'decision-1',
742
+ decisionId: null,
743
+ episodeId: agent.episodeId,
744
+ policy: 'test-policy',
745
+ goal: 'Win the game',
746
+ goalState: { value: 'won' },
747
+ paths: [],
748
+ nextEvent: { type: 'MOVE' },
749
+ timestamp: Date.now(),
750
+ } satisfies AgentDecision<typeof agent>;
751
+
752
+ agent.addDecision(decision);
753
+
754
+ expect(fn).toHaveBeenCalledTimes(1);
755
+
756
+ subscription.unsubscribe();
757
+
758
+ agent.addDecision({
759
+ ...decision,
760
+ id: 'decision-2',
761
+ });
762
+
763
+ expect(fn).toHaveBeenCalledTimes(1); // Still only called once
764
+ });
765
+
766
+ test('Event listeners can be unsubscribed (onFeedback)', () => {
767
+ const fn = vi.fn();
768
+ const agent = createAgent({
769
+ id: 'test',
770
+ events: {},
771
+ model: {} as any,
772
+ });
773
+
774
+ const subscription = agent.onFeedback(fn);
775
+
776
+ agent.addFeedback({
777
+ reward: 1,
778
+ decisionId: 'dec-1',
779
+ });
780
+
781
+ expect(fn).toHaveBeenCalledTimes(1);
782
+
783
+ subscription.unsubscribe();
784
+
785
+ agent.addFeedback({
786
+ reward: 0,
787
+ decisionId: 'dec-2',
788
+ });
789
+
790
+ expect(fn).toHaveBeenCalledTimes(1); // Still only called once
791
+ });
792
+
793
+ test('Feedback events include optional fields', () => {
794
+ const fn = vi.fn();
795
+ const agent = createAgent({
796
+ id: 'test',
797
+ events: {},
798
+ model: {} as any,
799
+ });
800
+
801
+ agent.onFeedback(fn);
802
+
803
+ // Test with minimal feedback
804
+ agent.addFeedback({
805
+ reward: 1,
806
+ decisionId: 'dec-1',
807
+ });
808
+
809
+ expect(fn).toHaveBeenCalledWith(
810
+ expect.objectContaining({
811
+ reward: 1,
812
+ decisionId: 'dec-1',
813
+ comment: undefined,
814
+ attributes: {},
815
+ episodeId: expect.any(String),
816
+ timestamp: expect.any(Number),
817
+ })
818
+ );
819
+
820
+ // Test with all optional fields
821
+ agent.addFeedback({
822
+ reward: 0,
823
+ decisionId: 'dec-2',
824
+ comment: 'Could be better',
825
+ attributes: { reason: 'suboptimal' },
826
+ } satisfies AgentFeedbackInput);
827
+
828
+ expect(fn).toHaveBeenLastCalledWith(
829
+ expect.objectContaining({
830
+ reward: 0,
831
+ decisionId: 'dec-2',
832
+ comment: 'Could be better',
833
+ attributes: { reason: 'suboptimal' },
834
+ episodeId: expect.any(String),
835
+ timestamp: expect.any(Number),
836
+ })
837
+ );
838
+ });
839
+
840
+ test('Feedback events maintain episodeId consistency', () => {
841
+ const fn = vi.fn();
842
+ const customEpisodeId = 'custom-episode-123';
843
+
844
+ const agent = createAgent({
845
+ id: 'test',
846
+ events: {},
847
+ model: {} as any,
848
+ episodeId: customEpisodeId,
849
+ });
850
+
851
+ agent.onFeedback(fn);
852
+
853
+ agent.addFeedback({
854
+ reward: 1,
855
+ decisionId: 'dec-1',
856
+ });
857
+
858
+ expect(fn).toHaveBeenCalledWith(
859
+ expect.objectContaining({
860
+ episodeId: customEpisodeId,
861
+ })
862
+ );
863
+
864
+ // Test with explicit different episodeId
865
+ const differentEpisodeId = 'different-episode-456';
866
+ agent.addFeedback({
867
+ reward: 0,
868
+ decisionId: 'dec-2',
869
+ episodeId: differentEpisodeId,
870
+ });
871
+
872
+ expect(fn).toHaveBeenLastCalledWith(
873
+ expect.objectContaining({
874
+ episodeId: differentEpisodeId,
875
+ })
876
+ );
877
+ });