@statelyai/agent 2.0.0-next.4 → 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.
package/src/agent.test.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  import { test, expect, vi } from 'vitest';
2
- import { AgentDecision, 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';
@@ -76,12 +81,13 @@ test('agent.addFeedback() adds to feedback', () => {
76
81
 
77
82
  const decision: AgentDecision<typeof agent> = {
78
83
  goal: 'Win the game',
84
+ decisionId: null,
79
85
  episodeId: agent.episodeId,
80
86
  goalState: { value: 'won' },
81
87
  id: 'decision-1',
82
88
  nextEvent: { type: 'play', position: 3 },
83
89
  paths: [],
84
- strategy: 'simple',
90
+ policy: 'simple',
85
91
  timestamp: Date.now(),
86
92
  };
87
93
 
@@ -93,16 +99,16 @@ test('agent.addFeedback() adds to feedback', () => {
93
99
  });
94
100
 
95
101
  const feedback = agent.addFeedback({
96
- score: 0,
97
- observationId: obs.id,
102
+ reward: 0,
103
+ decisionId: decision.id,
98
104
  });
99
105
 
100
106
  expect(feedback.episodeId).toEqual(agent.episodeId);
101
107
 
102
108
  expect(agent.getFeedback()).toContainEqual(
103
109
  expect.objectContaining({
104
- score: 0,
105
- observationId: obs.id,
110
+ reward: 0,
111
+ decisionId: decision.id,
106
112
  episodeId: expect.any(String),
107
113
  timestamp: expect.any(Number),
108
114
  })
@@ -159,7 +165,7 @@ test('agent.addObservation() adds to observations (initial state)', () => {
159
165
  );
160
166
  });
161
167
 
162
- test('agent.addObservation() adds to observations with machine hash', () => {
168
+ test.skip('agent.addObservation() adds to observations with machine hash', () => {
163
169
  const agent = createAgent({
164
170
  id: 'test',
165
171
  events: {},
@@ -198,7 +204,7 @@ test('agent.addObservation() adds to observations with machine hash', () => {
198
204
  );
199
205
  });
200
206
 
201
- test('agent.addFeedback() adds to feedback (with observation)', () => {
207
+ test('agent.addInsight() adds to insights (with observation)', () => {
202
208
  const agent = createAgent({
203
209
  id: 'test',
204
210
  events: {},
@@ -212,24 +218,26 @@ test('agent.addFeedback() adds to feedback (with observation)', () => {
212
218
  goal: 'Win the game',
213
219
  });
214
220
 
215
- const feedback = agent.addFeedback({
216
- score: 0,
221
+ const insight = agent.addInsight({
217
222
  observationId: observation.id,
223
+ attributes: {
224
+ advantage: 50,
225
+ },
218
226
  });
219
227
 
220
- expect(feedback.episodeId).toEqual(agent.episodeId);
228
+ expect(insight.episodeId).toEqual(agent.episodeId);
221
229
 
222
- expect(agent.getFeedback()).toContainEqual(
230
+ expect(agent.getInsights()).toContainEqual(
223
231
  expect.objectContaining({
224
- score: 0,
232
+ attributes: { advantage: 50 },
225
233
  observationId: observation.id,
226
234
  episodeId: expect.any(String),
227
235
  timestamp: expect.any(Number),
228
236
  })
229
237
  );
230
- expect(agent.getFeedback()).toContainEqual(
238
+ expect(agent.getInsights()).toContainEqual(
231
239
  expect.objectContaining({
232
- score: 0,
240
+ attributes: { advantage: 50 },
233
241
  observationId: observation.id,
234
242
  episodeId: expect.any(String),
235
243
  timestamp: expect.any(Number),
@@ -292,14 +300,25 @@ test('You can listen for feedback events', () => {
292
300
  model: {} as any,
293
301
  });
294
302
 
295
- agent.on('feedback', fn);
303
+ agent.onFeedback(fn);
296
304
 
297
305
  agent.addFeedback({
298
- score: -1,
299
- observationId: 'obs-1',
306
+ reward: 1,
307
+ decisionId: 'dec-1',
308
+ comment: 'Good move',
309
+ attributes: { confidence: 'high' },
300
310
  });
301
311
 
302
- 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
+ );
303
322
  });
304
323
 
305
324
  test('You can listen for decision events', async () => {
@@ -438,20 +457,21 @@ test('agent.getDecisions() returns decisions from context', () => {
438
457
  id: 'test',
439
458
  events: {},
440
459
  model: {} as any,
441
- strategy: async (agent) => {
460
+ policy: async (agent) => {
442
461
  return {
443
462
  id: Date.now().toString(),
463
+ decisionId: null,
444
464
  episodeId: agent.episodeId,
445
- strategy: 'test-strategy',
465
+ policy: 'test-policy',
446
466
  goal: '',
447
- goalState: undefined,
467
+ goalState: null,
448
468
  paths: [
449
469
  {
450
- state: undefined,
470
+ state: null,
451
471
  steps: [],
452
472
  },
453
473
  ],
454
- nextEvent: undefined,
474
+ nextEvent: null,
455
475
  timestamp: Date.now(),
456
476
  };
457
477
  },
@@ -561,8 +581,8 @@ test('agent.addFeedback() accepts custom episodeId', () => {
561
581
 
562
582
  const customEpisodeId = 'custom-episode-123';
563
583
  const feedback = agent.addFeedback({
564
- score: 1,
565
- observationId: 'obs-1',
584
+ reward: 1,
585
+ decisionId: 'dec-1',
566
586
  episodeId: customEpisodeId,
567
587
  });
568
588
 
@@ -605,7 +625,7 @@ test('agent.addFeedback() accepts decisionId', () => {
605
625
 
606
626
  const decisionId = 'decision-123';
607
627
  const feedback = agent.addFeedback({
608
- score: 1,
628
+ reward: 1,
609
629
  decisionId,
610
630
  });
611
631
 
@@ -616,3 +636,242 @@ test('agent.addFeedback() accepts decisionId', () => {
616
636
  })
617
637
  );
618
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
+ });