@livekit/agents 1.0.15 → 1.0.17

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 (98) hide show
  1. package/dist/cli.cjs +12 -12
  2. package/dist/cli.cjs.map +1 -1
  3. package/dist/cli.d.cts +3 -3
  4. package/dist/cli.d.ts +3 -3
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +13 -13
  7. package/dist/cli.js.map +1 -1
  8. package/dist/inference/stt.cjs.map +1 -1
  9. package/dist/inference/stt.d.ts.map +1 -1
  10. package/dist/inference/stt.js +1 -1
  11. package/dist/inference/stt.js.map +1 -1
  12. package/dist/inference/tts.cjs.map +1 -1
  13. package/dist/inference/tts.d.cts +2 -1
  14. package/dist/inference/tts.d.ts +2 -1
  15. package/dist/inference/tts.d.ts.map +1 -1
  16. package/dist/inference/tts.js +1 -5
  17. package/dist/inference/tts.js.map +1 -1
  18. package/dist/llm/chat_context.cjs +78 -0
  19. package/dist/llm/chat_context.cjs.map +1 -1
  20. package/dist/llm/chat_context.d.cts +16 -0
  21. package/dist/llm/chat_context.d.ts +16 -0
  22. package/dist/llm/chat_context.d.ts.map +1 -1
  23. package/dist/llm/chat_context.js +78 -0
  24. package/dist/llm/chat_context.js.map +1 -1
  25. package/dist/llm/chat_context.test.cjs +531 -0
  26. package/dist/llm/chat_context.test.cjs.map +1 -1
  27. package/dist/llm/chat_context.test.js +531 -0
  28. package/dist/llm/chat_context.test.js.map +1 -1
  29. package/dist/llm/tool_context.cjs +40 -0
  30. package/dist/llm/tool_context.cjs.map +1 -1
  31. package/dist/llm/tool_context.d.cts +2 -0
  32. package/dist/llm/tool_context.d.ts +2 -0
  33. package/dist/llm/tool_context.d.ts.map +1 -1
  34. package/dist/llm/tool_context.js +38 -0
  35. package/dist/llm/tool_context.js.map +1 -1
  36. package/dist/metrics/base.cjs.map +1 -1
  37. package/dist/metrics/base.d.cts +7 -0
  38. package/dist/metrics/base.d.ts +7 -0
  39. package/dist/metrics/base.d.ts.map +1 -1
  40. package/dist/stt/stt.cjs +1 -1
  41. package/dist/stt/stt.cjs.map +1 -1
  42. package/dist/stt/stt.d.cts +7 -1
  43. package/dist/stt/stt.d.ts +7 -1
  44. package/dist/stt/stt.d.ts.map +1 -1
  45. package/dist/stt/stt.js +1 -1
  46. package/dist/stt/stt.js.map +1 -1
  47. package/dist/tts/tts.cjs +2 -4
  48. package/dist/tts/tts.cjs.map +1 -1
  49. package/dist/tts/tts.d.ts.map +1 -1
  50. package/dist/tts/tts.js +3 -5
  51. package/dist/tts/tts.js.map +1 -1
  52. package/dist/voice/agent_activity.cjs +83 -8
  53. package/dist/voice/agent_activity.cjs.map +1 -1
  54. package/dist/voice/agent_activity.d.cts +6 -2
  55. package/dist/voice/agent_activity.d.ts +6 -2
  56. package/dist/voice/agent_activity.d.ts.map +1 -1
  57. package/dist/voice/agent_activity.js +83 -8
  58. package/dist/voice/agent_activity.js.map +1 -1
  59. package/dist/voice/agent_session.cjs +3 -2
  60. package/dist/voice/agent_session.cjs.map +1 -1
  61. package/dist/voice/agent_session.d.cts +2 -1
  62. package/dist/voice/agent_session.d.ts +2 -1
  63. package/dist/voice/agent_session.d.ts.map +1 -1
  64. package/dist/voice/agent_session.js +3 -2
  65. package/dist/voice/agent_session.js.map +1 -1
  66. package/dist/voice/audio_recognition.cjs +138 -16
  67. package/dist/voice/audio_recognition.cjs.map +1 -1
  68. package/dist/voice/audio_recognition.d.cts +11 -0
  69. package/dist/voice/audio_recognition.d.ts +11 -0
  70. package/dist/voice/audio_recognition.d.ts.map +1 -1
  71. package/dist/voice/audio_recognition.js +138 -16
  72. package/dist/voice/audio_recognition.js.map +1 -1
  73. package/dist/voice/room_io/_input.cjs.map +1 -1
  74. package/dist/voice/room_io/_input.d.ts.map +1 -1
  75. package/dist/voice/room_io/_input.js +0 -1
  76. package/dist/voice/room_io/_input.js.map +1 -1
  77. package/dist/worker.cjs +17 -11
  78. package/dist/worker.cjs.map +1 -1
  79. package/dist/worker.d.cts +16 -9
  80. package/dist/worker.d.ts +16 -9
  81. package/dist/worker.d.ts.map +1 -1
  82. package/dist/worker.js +16 -12
  83. package/dist/worker.js.map +1 -1
  84. package/package.json +1 -1
  85. package/src/cli.ts +17 -17
  86. package/src/inference/stt.ts +2 -1
  87. package/src/inference/tts.ts +2 -5
  88. package/src/llm/chat_context.test.ts +607 -0
  89. package/src/llm/chat_context.ts +106 -0
  90. package/src/llm/tool_context.ts +44 -0
  91. package/src/metrics/base.ts +7 -0
  92. package/src/stt/stt.ts +8 -1
  93. package/src/tts/tts.ts +7 -5
  94. package/src/voice/agent_activity.ts +119 -9
  95. package/src/voice/agent_session.ts +3 -1
  96. package/src/voice/audio_recognition.ts +235 -57
  97. package/src/voice/room_io/_input.ts +1 -1
  98. package/src/worker.ts +29 -18
@@ -448,3 +448,610 @@ describe('ReadonlyChatContext with immutable array', () => {
448
448
  expect(ids).toEqual(['msg_1', 'msg_2']);
449
449
  });
450
450
  });
451
+
452
+ describe('ChatContext.isEquivalent', () => {
453
+ it('should return true for same reference', () => {
454
+ const ctx = new ChatContext();
455
+ ctx.addMessage({
456
+ id: 'msg_1',
457
+ role: 'user',
458
+ content: 'Hello',
459
+ });
460
+
461
+ expect(ctx.isEquivalent(ctx)).toBe(true);
462
+ });
463
+
464
+ it('should return true for identical empty contexts', () => {
465
+ const ctx1 = new ChatContext();
466
+ const ctx2 = new ChatContext();
467
+
468
+ expect(ctx1.isEquivalent(ctx2)).toBe(true);
469
+ });
470
+
471
+ it('should return false for contexts with different lengths', () => {
472
+ const ctx1 = new ChatContext();
473
+ ctx1.addMessage({
474
+ id: 'msg_1',
475
+ role: 'user',
476
+ content: 'Hello',
477
+ });
478
+
479
+ const ctx2 = new ChatContext();
480
+ ctx2.addMessage({
481
+ id: 'msg_1',
482
+ role: 'user',
483
+ content: 'Hello',
484
+ });
485
+ ctx2.addMessage({
486
+ id: 'msg_2',
487
+ role: 'assistant',
488
+ content: 'Hi',
489
+ });
490
+
491
+ expect(ctx1.isEquivalent(ctx2)).toBe(false);
492
+ });
493
+
494
+ it('should return false for contexts with different item IDs', () => {
495
+ const ctx1 = new ChatContext();
496
+ ctx1.addMessage({
497
+ id: 'msg_1',
498
+ role: 'user',
499
+ content: 'Hello',
500
+ });
501
+
502
+ const ctx2 = new ChatContext();
503
+ ctx2.addMessage({
504
+ id: 'msg_2',
505
+ role: 'user',
506
+ content: 'Hello',
507
+ });
508
+
509
+ expect(ctx1.isEquivalent(ctx2)).toBe(false);
510
+ });
511
+
512
+ it('should return false for contexts with different item types', () => {
513
+ const ctx1 = new ChatContext();
514
+ ctx1.addMessage({
515
+ id: 'msg_1',
516
+ role: 'user',
517
+ content: 'Hello',
518
+ });
519
+
520
+ const ctx2 = new ChatContext();
521
+ ctx2.insert(
522
+ new FunctionCall({
523
+ id: 'msg_1',
524
+ callId: 'call_1',
525
+ name: 'test',
526
+ args: '{}',
527
+ }),
528
+ );
529
+
530
+ expect(ctx1.isEquivalent(ctx2)).toBe(false);
531
+ });
532
+
533
+ describe('message comparison', () => {
534
+ it('should return true for identical messages', () => {
535
+ const ctx1 = new ChatContext();
536
+ ctx1.addMessage({
537
+ id: 'msg_1',
538
+ role: 'user',
539
+ content: 'Hello',
540
+ interrupted: false,
541
+ });
542
+
543
+ const ctx2 = new ChatContext();
544
+ ctx2.addMessage({
545
+ id: 'msg_1',
546
+ role: 'user',
547
+ content: 'Hello',
548
+ interrupted: false,
549
+ });
550
+
551
+ expect(ctx1.isEquivalent(ctx2)).toBe(true);
552
+ });
553
+
554
+ it('should return false for messages with different roles', () => {
555
+ const ctx1 = new ChatContext();
556
+ ctx1.addMessage({
557
+ id: 'msg_1',
558
+ role: 'user',
559
+ content: 'Hello',
560
+ });
561
+
562
+ const ctx2 = new ChatContext();
563
+ ctx2.addMessage({
564
+ id: 'msg_1',
565
+ role: 'assistant',
566
+ content: 'Hello',
567
+ });
568
+
569
+ expect(ctx1.isEquivalent(ctx2)).toBe(false);
570
+ });
571
+
572
+ it('should return false for messages with different interrupted flags', () => {
573
+ const ctx1 = new ChatContext();
574
+ ctx1.addMessage({
575
+ id: 'msg_1',
576
+ role: 'user',
577
+ content: 'Hello',
578
+ interrupted: false,
579
+ });
580
+
581
+ const ctx2 = new ChatContext();
582
+ ctx2.addMessage({
583
+ id: 'msg_1',
584
+ role: 'user',
585
+ content: 'Hello',
586
+ interrupted: true,
587
+ });
588
+
589
+ expect(ctx1.isEquivalent(ctx2)).toBe(false);
590
+ });
591
+
592
+ it('should return false for messages with different content', () => {
593
+ const ctx1 = new ChatContext();
594
+ ctx1.addMessage({
595
+ id: 'msg_1',
596
+ role: 'user',
597
+ content: 'Hello',
598
+ });
599
+
600
+ const ctx2 = new ChatContext();
601
+ ctx2.addMessage({
602
+ id: 'msg_1',
603
+ role: 'user',
604
+ content: 'World',
605
+ });
606
+
607
+ expect(ctx1.isEquivalent(ctx2)).toBe(false);
608
+ });
609
+
610
+ it('should return true for messages with identical array content', () => {
611
+ const ctx1 = new ChatContext();
612
+ ctx1.addMessage({
613
+ id: 'msg_1',
614
+ role: 'user',
615
+ content: ['Hello', 'World'],
616
+ });
617
+
618
+ const ctx2 = new ChatContext();
619
+ ctx2.addMessage({
620
+ id: 'msg_1',
621
+ role: 'user',
622
+ content: ['Hello', 'World'],
623
+ });
624
+
625
+ expect(ctx1.isEquivalent(ctx2)).toBe(true);
626
+ });
627
+
628
+ it('should return false for messages with different array content', () => {
629
+ const ctx1 = new ChatContext();
630
+ ctx1.addMessage({
631
+ id: 'msg_1',
632
+ role: 'user',
633
+ content: ['Hello', 'World'],
634
+ });
635
+
636
+ const ctx2 = new ChatContext();
637
+ ctx2.addMessage({
638
+ id: 'msg_1',
639
+ role: 'user',
640
+ content: ['Hello'],
641
+ });
642
+
643
+ expect(ctx1.isEquivalent(ctx2)).toBe(false);
644
+ });
645
+
646
+ it('should return true for messages with identical image content', () => {
647
+ const imageContent: ImageContent = {
648
+ id: 'img_1',
649
+ type: 'image_content',
650
+ image: 'https://example.com/image.jpg',
651
+ inferenceDetail: 'high',
652
+ inferenceWidth: 1024,
653
+ inferenceHeight: 768,
654
+ mimeType: 'image/jpeg',
655
+ _cache: {},
656
+ };
657
+
658
+ const ctx1 = new ChatContext();
659
+ ctx1.addMessage({
660
+ id: 'msg_1',
661
+ role: 'user',
662
+ content: ['Check this:', imageContent],
663
+ });
664
+
665
+ const ctx2 = new ChatContext();
666
+ ctx2.addMessage({
667
+ id: 'msg_1',
668
+ role: 'user',
669
+ content: ['Check this:', { ...imageContent }],
670
+ });
671
+
672
+ expect(ctx1.isEquivalent(ctx2)).toBe(true);
673
+ });
674
+
675
+ it('should return false for messages with different image content', () => {
676
+ const imageContent1: ImageContent = {
677
+ id: 'img_1',
678
+ type: 'image_content',
679
+ image: 'https://example.com/image1.jpg',
680
+ inferenceDetail: 'high',
681
+ _cache: {},
682
+ };
683
+
684
+ const imageContent2: ImageContent = {
685
+ id: 'img_2',
686
+ type: 'image_content',
687
+ image: 'https://example.com/image2.jpg',
688
+ inferenceDetail: 'high',
689
+ _cache: {},
690
+ };
691
+
692
+ const ctx1 = new ChatContext();
693
+ ctx1.addMessage({
694
+ id: 'msg_1',
695
+ role: 'user',
696
+ content: ['Check this:', imageContent1],
697
+ });
698
+
699
+ const ctx2 = new ChatContext();
700
+ ctx2.addMessage({
701
+ id: 'msg_1',
702
+ role: 'user',
703
+ content: ['Check this:', imageContent2],
704
+ });
705
+
706
+ expect(ctx1.isEquivalent(ctx2)).toBe(false);
707
+ });
708
+ });
709
+
710
+ describe('function call comparison', () => {
711
+ it('should return true for identical function calls', () => {
712
+ const ctx1 = new ChatContext();
713
+ ctx1.insert(
714
+ new FunctionCall({
715
+ id: 'func_1',
716
+ callId: 'call_1',
717
+ name: 'get_weather',
718
+ args: '{"location": "Paris"}',
719
+ }),
720
+ );
721
+
722
+ const ctx2 = new ChatContext();
723
+ ctx2.insert(
724
+ new FunctionCall({
725
+ id: 'func_1',
726
+ callId: 'call_1',
727
+ name: 'get_weather',
728
+ args: '{"location": "Paris"}',
729
+ }),
730
+ );
731
+
732
+ expect(ctx1.isEquivalent(ctx2)).toBe(true);
733
+ });
734
+
735
+ it('should return false for function calls with different names', () => {
736
+ const ctx1 = new ChatContext();
737
+ ctx1.insert(
738
+ new FunctionCall({
739
+ id: 'func_1',
740
+ callId: 'call_1',
741
+ name: 'get_weather',
742
+ args: '{}',
743
+ }),
744
+ );
745
+
746
+ const ctx2 = new ChatContext();
747
+ ctx2.insert(
748
+ new FunctionCall({
749
+ id: 'func_1',
750
+ callId: 'call_1',
751
+ name: 'get_time',
752
+ args: '{}',
753
+ }),
754
+ );
755
+
756
+ expect(ctx1.isEquivalent(ctx2)).toBe(false);
757
+ });
758
+
759
+ it('should return false for function calls with different call IDs', () => {
760
+ const ctx1 = new ChatContext();
761
+ ctx1.insert(
762
+ new FunctionCall({
763
+ id: 'func_1',
764
+ callId: 'call_1',
765
+ name: 'get_weather',
766
+ args: '{}',
767
+ }),
768
+ );
769
+
770
+ const ctx2 = new ChatContext();
771
+ ctx2.insert(
772
+ new FunctionCall({
773
+ id: 'func_1',
774
+ callId: 'call_2',
775
+ name: 'get_weather',
776
+ args: '{}',
777
+ }),
778
+ );
779
+
780
+ expect(ctx1.isEquivalent(ctx2)).toBe(false);
781
+ });
782
+
783
+ it('should return false for function calls with different arguments', () => {
784
+ const ctx1 = new ChatContext();
785
+ ctx1.insert(
786
+ new FunctionCall({
787
+ id: 'func_1',
788
+ callId: 'call_1',
789
+ name: 'get_weather',
790
+ args: '{"location": "Paris"}',
791
+ }),
792
+ );
793
+
794
+ const ctx2 = new ChatContext();
795
+ ctx2.insert(
796
+ new FunctionCall({
797
+ id: 'func_1',
798
+ callId: 'call_1',
799
+ name: 'get_weather',
800
+ args: '{"location": "London"}',
801
+ }),
802
+ );
803
+
804
+ expect(ctx1.isEquivalent(ctx2)).toBe(false);
805
+ });
806
+
807
+ it('should ignore timestamps', () => {
808
+ const ctx1 = new ChatContext();
809
+ ctx1.insert(
810
+ new FunctionCall({
811
+ id: 'func_1',
812
+ callId: 'call_1',
813
+ name: 'get_weather',
814
+ args: '{}',
815
+ createdAt: 1000,
816
+ }),
817
+ );
818
+
819
+ const ctx2 = new ChatContext();
820
+ ctx2.insert(
821
+ new FunctionCall({
822
+ id: 'func_1',
823
+ callId: 'call_1',
824
+ name: 'get_weather',
825
+ args: '{}',
826
+ createdAt: 2000,
827
+ }),
828
+ );
829
+
830
+ expect(ctx1.isEquivalent(ctx2)).toBe(true);
831
+ });
832
+ });
833
+
834
+ describe('function call output comparison', () => {
835
+ it('should return true for identical function call outputs', () => {
836
+ const ctx1 = new ChatContext();
837
+ ctx1.insert(
838
+ new FunctionCallOutput({
839
+ id: 'output_1',
840
+ callId: 'call_1',
841
+ name: 'get_weather',
842
+ output: '{"temperature": 22}',
843
+ isError: false,
844
+ }),
845
+ );
846
+
847
+ const ctx2 = new ChatContext();
848
+ ctx2.insert(
849
+ new FunctionCallOutput({
850
+ id: 'output_1',
851
+ callId: 'call_1',
852
+ name: 'get_weather',
853
+ output: '{"temperature": 22}',
854
+ isError: false,
855
+ }),
856
+ );
857
+
858
+ expect(ctx1.isEquivalent(ctx2)).toBe(true);
859
+ });
860
+
861
+ it('should return false for function call outputs with different names', () => {
862
+ const ctx1 = new ChatContext();
863
+ ctx1.insert(
864
+ new FunctionCallOutput({
865
+ id: 'output_1',
866
+ callId: 'call_1',
867
+ name: 'get_weather',
868
+ output: '{}',
869
+ isError: false,
870
+ }),
871
+ );
872
+
873
+ const ctx2 = new ChatContext();
874
+ ctx2.insert(
875
+ new FunctionCallOutput({
876
+ id: 'output_1',
877
+ callId: 'call_1',
878
+ name: 'get_time',
879
+ output: '{}',
880
+ isError: false,
881
+ }),
882
+ );
883
+
884
+ expect(ctx1.isEquivalent(ctx2)).toBe(false);
885
+ });
886
+
887
+ it('should return false for function call outputs with different call IDs', () => {
888
+ const ctx1 = new ChatContext();
889
+ ctx1.insert(
890
+ new FunctionCallOutput({
891
+ id: 'output_1',
892
+ callId: 'call_1',
893
+ name: 'get_weather',
894
+ output: '{}',
895
+ isError: false,
896
+ }),
897
+ );
898
+
899
+ const ctx2 = new ChatContext();
900
+ ctx2.insert(
901
+ new FunctionCallOutput({
902
+ id: 'output_1',
903
+ callId: 'call_2',
904
+ name: 'get_weather',
905
+ output: '{}',
906
+ isError: false,
907
+ }),
908
+ );
909
+
910
+ expect(ctx1.isEquivalent(ctx2)).toBe(false);
911
+ });
912
+
913
+ it('should return false for function call outputs with different output values', () => {
914
+ const ctx1 = new ChatContext();
915
+ ctx1.insert(
916
+ new FunctionCallOutput({
917
+ id: 'output_1',
918
+ callId: 'call_1',
919
+ name: 'get_weather',
920
+ output: '{"temperature": 22}',
921
+ isError: false,
922
+ }),
923
+ );
924
+
925
+ const ctx2 = new ChatContext();
926
+ ctx2.insert(
927
+ new FunctionCallOutput({
928
+ id: 'output_1',
929
+ callId: 'call_1',
930
+ name: 'get_weather',
931
+ output: '{"temperature": 25}',
932
+ isError: false,
933
+ }),
934
+ );
935
+
936
+ expect(ctx1.isEquivalent(ctx2)).toBe(false);
937
+ });
938
+
939
+ it('should return false for function call outputs with different error flags', () => {
940
+ const ctx1 = new ChatContext();
941
+ ctx1.insert(
942
+ new FunctionCallOutput({
943
+ id: 'output_1',
944
+ callId: 'call_1',
945
+ name: 'get_weather',
946
+ output: 'Error occurred',
947
+ isError: false,
948
+ }),
949
+ );
950
+
951
+ const ctx2 = new ChatContext();
952
+ ctx2.insert(
953
+ new FunctionCallOutput({
954
+ id: 'output_1',
955
+ callId: 'call_1',
956
+ name: 'get_weather',
957
+ output: 'Error occurred',
958
+ isError: true,
959
+ }),
960
+ );
961
+
962
+ expect(ctx1.isEquivalent(ctx2)).toBe(false);
963
+ });
964
+
965
+ it('should ignore timestamps', () => {
966
+ const ctx1 = new ChatContext();
967
+ ctx1.insert(
968
+ new FunctionCallOutput({
969
+ id: 'output_1',
970
+ callId: 'call_1',
971
+ name: 'get_weather',
972
+ output: '{}',
973
+ isError: false,
974
+ createdAt: 1000,
975
+ }),
976
+ );
977
+
978
+ const ctx2 = new ChatContext();
979
+ ctx2.insert(
980
+ new FunctionCallOutput({
981
+ id: 'output_1',
982
+ callId: 'call_1',
983
+ name: 'get_weather',
984
+ output: '{}',
985
+ isError: false,
986
+ createdAt: 2000,
987
+ }),
988
+ );
989
+
990
+ expect(ctx1.isEquivalent(ctx2)).toBe(true);
991
+ });
992
+ });
993
+
994
+ describe('complex context comparison', () => {
995
+ it('should return true for identical complex contexts', () => {
996
+ const ctx1 = new ChatContext();
997
+ ctx1.addMessage({
998
+ id: 'msg_1',
999
+ role: 'user',
1000
+ content: 'What is the weather?',
1001
+ });
1002
+ ctx1.insert(
1003
+ new FunctionCall({
1004
+ id: 'func_1',
1005
+ callId: 'call_1',
1006
+ name: 'get_weather',
1007
+ args: '{"location": "Paris"}',
1008
+ }),
1009
+ );
1010
+ ctx1.insert(
1011
+ new FunctionCallOutput({
1012
+ id: 'output_1',
1013
+ callId: 'call_1',
1014
+ name: 'get_weather',
1015
+ output: '{"temperature": 22}',
1016
+ isError: false,
1017
+ }),
1018
+ );
1019
+ ctx1.addMessage({
1020
+ id: 'msg_2',
1021
+ role: 'assistant',
1022
+ content: 'The weather is 22°C',
1023
+ });
1024
+
1025
+ const ctx2 = new ChatContext();
1026
+ ctx2.addMessage({
1027
+ id: 'msg_1',
1028
+ role: 'user',
1029
+ content: 'What is the weather?',
1030
+ });
1031
+ ctx2.insert(
1032
+ new FunctionCall({
1033
+ id: 'func_1',
1034
+ callId: 'call_1',
1035
+ name: 'get_weather',
1036
+ args: '{"location": "Paris"}',
1037
+ }),
1038
+ );
1039
+ ctx2.insert(
1040
+ new FunctionCallOutput({
1041
+ id: 'output_1',
1042
+ callId: 'call_1',
1043
+ name: 'get_weather',
1044
+ output: '{"temperature": 22}',
1045
+ isError: false,
1046
+ }),
1047
+ );
1048
+ ctx2.addMessage({
1049
+ id: 'msg_2',
1050
+ role: 'assistant',
1051
+ content: 'The weather is 22°C',
1052
+ });
1053
+
1054
+ expect(ctx1.isEquivalent(ctx2)).toBe(true);
1055
+ });
1056
+ });
1057
+ });
@@ -513,6 +513,112 @@ export class ChatContext {
513
513
  return 0;
514
514
  }
515
515
 
516
+ /**
517
+ * Return true if `other` has the same sequence of items with matching
518
+ * essential fields (IDs, types, and payload) as this context.
519
+ *
520
+ * Comparison rules:
521
+ * - Messages: compares the full `content` list, `role` and `interrupted`.
522
+ * - Function calls: compares `name`, `callId`, and `args`.
523
+ * - Function call outputs: compares `name`, `callId`, `output`, and `isError`.
524
+ *
525
+ * Does not consider timestamps or other metadata.
526
+ */
527
+ isEquivalent(other: ChatContext): boolean {
528
+ if (this === other) {
529
+ return true;
530
+ }
531
+
532
+ if (this.items.length !== other.items.length) {
533
+ return false;
534
+ }
535
+
536
+ for (let i = 0; i < this.items.length; i++) {
537
+ const a = this.items[i]!;
538
+ const b = other.items[i]!;
539
+
540
+ if (a.id !== b.id || a.type !== b.type) {
541
+ return false;
542
+ }
543
+
544
+ if (a.type === 'message' && b.type === 'message') {
545
+ if (
546
+ a.role !== b.role ||
547
+ a.interrupted !== b.interrupted ||
548
+ !this.compareContent(a.content, b.content)
549
+ ) {
550
+ return false;
551
+ }
552
+ } else if (a.type === 'function_call' && b.type === 'function_call') {
553
+ if (a.name !== b.name || a.callId !== b.callId || a.args !== b.args) {
554
+ return false;
555
+ }
556
+ } else if (a.type === 'function_call_output' && b.type === 'function_call_output') {
557
+ if (
558
+ a.name !== b.name ||
559
+ a.callId !== b.callId ||
560
+ a.output !== b.output ||
561
+ a.isError !== b.isError
562
+ ) {
563
+ return false;
564
+ }
565
+ }
566
+ }
567
+
568
+ return true;
569
+ }
570
+
571
+ /**
572
+ * Compare two content arrays for equality.
573
+ */
574
+ private compareContent(a: ChatContent[], b: ChatContent[]): boolean {
575
+ if (a.length !== b.length) {
576
+ return false;
577
+ }
578
+
579
+ for (let i = 0; i < a.length; i++) {
580
+ const contentA = a[i]!;
581
+ const contentB = b[i]!;
582
+
583
+ if (typeof contentA === 'string' && typeof contentB === 'string') {
584
+ if (contentA !== contentB) {
585
+ return false;
586
+ }
587
+ continue;
588
+ }
589
+
590
+ if (typeof contentA !== typeof contentB) {
591
+ return false;
592
+ }
593
+
594
+ if (typeof contentA === 'object' && typeof contentB === 'object') {
595
+ if (contentA.type === 'image_content' && contentB.type === 'image_content') {
596
+ if (
597
+ contentA.id !== contentB.id ||
598
+ contentA.image !== contentB.image ||
599
+ contentA.inferenceDetail !== contentB.inferenceDetail ||
600
+ contentA.inferenceWidth !== contentB.inferenceWidth ||
601
+ contentA.inferenceHeight !== contentB.inferenceHeight ||
602
+ contentA.mimeType !== contentB.mimeType
603
+ ) {
604
+ return false;
605
+ }
606
+ } else if (contentA.type === 'audio_content' && contentB.type === 'audio_content') {
607
+ if (contentA.frame.length !== contentB.frame.length) {
608
+ return false;
609
+ }
610
+ if (contentA.transcript !== contentB.transcript) {
611
+ return false;
612
+ }
613
+ } else {
614
+ return false;
615
+ }
616
+ }
617
+ }
618
+
619
+ return true;
620
+ }
621
+
516
622
  /**
517
623
  * Indicates whether the context is read-only
518
624
  */