diff --git a/sdks/community/dart/.gitignore b/sdks/community/dart/.gitignore new file mode 100644 index 0000000000..43e466e3e0 --- /dev/null +++ b/sdks/community/dart/.gitignore @@ -0,0 +1,36 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# Visual Studio Code related +.classpath +.project +.settings/ +.vscode/* +!.vscode/settings.json + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins-dependencies +**/generated_plugin_registrant.dart +.packages +.pub-preload-cache/ +.pub-cache/ +.pub/ +build/ + +# Coverage +coverage/ diff --git a/sdks/community/dart/lib/src/events/event_type.dart b/sdks/community/dart/lib/src/events/event_type.dart index 2edb8e2072..b67303480d 100644 --- a/sdks/community/dart/lib/src/events/event_type.dart +++ b/sdks/community/dart/lib/src/events/event_type.dart @@ -21,6 +21,7 @@ enum EventType { stateSnapshot('STATE_SNAPSHOT'), stateDelta('STATE_DELTA'), messagesSnapshot('MESSAGES_SNAPSHOT'), + activitySnapshot('ACTIVITY_SNAPSHOT'), raw('RAW'), custom('CUSTOM'), runStarted('RUN_STARTED'), diff --git a/sdks/community/dart/lib/src/events/events.dart b/sdks/community/dart/lib/src/events/events.dart index 7562b6c39e..a6e55e7576 100644 --- a/sdks/community/dart/lib/src/events/events.dart +++ b/sdks/community/dart/lib/src/events/events.dart @@ -75,6 +75,8 @@ sealed class BaseEvent extends AGUIModel with TypeDiscriminator { return StateDeltaEvent.fromJson(json); case EventType.messagesSnapshot: return MessagesSnapshotEvent.fromJson(json); + case EventType.activitySnapshot: + return ActivitySnapshotEvent.fromJson(json); case EventType.raw: return RawEvent.fromJson(json); case EventType.custom: @@ -898,6 +900,62 @@ final class MessagesSnapshotEvent extends BaseEvent { } } +/// Event containing a snapshot of activity state. +final class ActivitySnapshotEvent extends BaseEvent { + const ActivitySnapshotEvent({ + required this.messageId, + required this.activityType, + required this.content, + this.replace = true, + super.timestamp, + super.rawEvent, + }) : super(eventType: EventType.activitySnapshot); + + factory ActivitySnapshotEvent.fromJson(Map json) { + return ActivitySnapshotEvent( + messageId: JsonDecoder.requireField(json, 'messageId'), + activityType: JsonDecoder.requireField(json, 'activityType'), + content: JsonDecoder.requireField>(json, 'content'), + replace: json['replace'] as bool? ?? true, + timestamp: JsonDecoder.optionalField(json, 'timestamp'), + rawEvent: json['rawEvent'], + ); + } + + final String messageId; + final String activityType; + final Map content; + final bool replace; + + @override + Map toJson() => { + ...super.toJson(), + 'messageId': messageId, + 'activityType': activityType, + 'content': content, + 'replace': replace, + }; + + @override + ActivitySnapshotEvent copyWith({ + String? messageId, + String? activityType, + Map? content, + bool? replace, + int? timestamp, + dynamic rawEvent, + }) { + return ActivitySnapshotEvent( + messageId: messageId ?? this.messageId, + activityType: activityType ?? this.activityType, + content: content ?? this.content, + replace: replace ?? this.replace, + timestamp: timestamp ?? this.timestamp, + rawEvent: rawEvent ?? this.rawEvent, + ); + } +} + /// Event containing a raw event final class RawEvent extends BaseEvent { final dynamic event; diff --git a/sdks/community/dart/test/events/event_test.dart b/sdks/community/dart/test/events/event_test.dart index c1246cc467..feae49d9ea 100644 --- a/sdks/community/dart/test/events/event_test.dart +++ b/sdks/community/dart/test/events/event_test.dart @@ -340,5 +340,163 @@ void main() { expect(decoded.value, customValue); }); }); + + group('ActivityEvents', () { + test('ActivitySnapshotEvent serialization with spec fields', () { + final content = {'skill': 'rag', 'tool_name': 'search'}; + final event = ActivitySnapshotEvent( + messageId: 'rag:abc123', + activityType: 'skill_tool_call', + content: content, + ); + + final json = event.toJson(); + expect(json['type'], 'ACTIVITY_SNAPSHOT'); + expect(json['messageId'], 'rag:abc123'); + expect(json['activityType'], 'skill_tool_call'); + expect(json['content'], content); + expect(json['replace'], true); + + final decoded = ActivitySnapshotEvent.fromJson(json); + expect(decoded.messageId, 'rag:abc123'); + expect(decoded.activityType, 'skill_tool_call'); + expect(decoded.content, content); + expect(decoded.replace, true); + }); + + test('ActivitySnapshotEvent replace can be set to false', () { + const event = ActivitySnapshotEvent( + messageId: 'msg-1', + activityType: 'test', + content: {}, + replace: false, + ); + expect(event.replace, false); + + final json = event.toJson(); + expect(json['replace'], false); + + final decoded = ActivitySnapshotEvent.fromJson(json); + expect(decoded.replace, false); + }); + + test('ActivitySnapshotEvent fromJson with missing optional replace', () { + final json = { + 'type': 'ACTIVITY_SNAPSHOT', + 'messageId': 'msg-1', + 'activityType': 'test', + 'content': {'key': 'value'}, + }; + + final event = ActivitySnapshotEvent.fromJson(json); + expect(event.messageId, 'msg-1'); + expect(event.activityType, 'test'); + expect(event.content, {'key': 'value'}); + expect(event.replace, true); + }); + + test('ActivitySnapshotEvent fromJson throws on missing messageId', () { + final json = { + 'type': 'ACTIVITY_SNAPSHOT', + 'activityType': 'test', + 'content': {'key': 'value'}, + }; + + expect( + () => ActivitySnapshotEvent.fromJson(json), + throwsA(isA()), + ); + }); + + test('ActivitySnapshotEvent fromJson throws on missing activityType', () { + final json = { + 'type': 'ACTIVITY_SNAPSHOT', + 'messageId': 'msg-1', + 'content': {'key': 'value'}, + }; + + expect( + () => ActivitySnapshotEvent.fromJson(json), + throwsA(isA()), + ); + }); + + test('ActivitySnapshotEvent fromJson throws on missing content', () { + final json = { + 'type': 'ACTIVITY_SNAPSHOT', + 'messageId': 'msg-1', + 'activityType': 'test', + }; + + expect( + () => ActivitySnapshotEvent.fromJson(json), + throwsA(isA()), + ); + }); + + test('ActivitySnapshotEvent copyWith preserves unchanged fields', () { + const event = ActivitySnapshotEvent( + messageId: 'msg-1', + activityType: 'test', + content: {'a': 1}, + replace: false, + timestamp: 1000, + ); + + final withMessageId = event.copyWith(messageId: 'msg-2'); + expect(withMessageId.messageId, 'msg-2'); + expect(withMessageId.activityType, 'test'); + expect(withMessageId.content, {'a': 1}); + expect(withMessageId.replace, false); + expect(withMessageId.timestamp, 1000); + + final withActivityType = event.copyWith(activityType: 'updated'); + expect(withActivityType.messageId, 'msg-1'); + expect(withActivityType.activityType, 'updated'); + + final withContent = event.copyWith(content: {'b': 2}); + expect(withContent.content, {'b': 2}); + expect(withContent.messageId, 'msg-1'); + + final withReplace = event.copyWith(replace: true); + expect(withReplace.replace, true); + expect(withReplace.messageId, 'msg-1'); + + final withTimestamp = event.copyWith(timestamp: 2000); + expect(withTimestamp.timestamp, 2000); + expect(withTimestamp.messageId, 'msg-1'); + }); + + test('ActivitySnapshotEvent timestamp survives serialization', () { + const event = ActivitySnapshotEvent( + messageId: 'msg-1', + activityType: 'test', + content: {'key': 'value'}, + timestamp: 1710000000, + ); + + final json = event.toJson(); + expect(json['timestamp'], 1710000000); + + final decoded = ActivitySnapshotEvent.fromJson(json); + expect(decoded.timestamp, 1710000000); + }); + + test('ActivitySnapshotEvent via BaseEvent.fromJson factory', () { + final json = { + 'type': 'ACTIVITY_SNAPSHOT', + 'messageId': 'rag:abc123', + 'activityType': 'skill_tool_call', + 'content': {'skill': 'rag'}, + 'replace': true, + }; + + final event = BaseEvent.fromJson(json); + expect(event, isA()); + final activity = event as ActivitySnapshotEvent; + expect(activity.messageId, 'rag:abc123'); + expect(activity.activityType, 'skill_tool_call'); + }); + }); }); } \ No newline at end of file diff --git a/sdks/community/dart/test/events/event_type_test.dart b/sdks/community/dart/test/events/event_type_test.dart index b12feaf47b..7e34c0ff8b 100644 --- a/sdks/community/dart/test/events/event_type_test.dart +++ b/sdks/community/dart/test/events/event_type_test.dart @@ -91,7 +91,7 @@ void main() { }); test('values list contains all event types', () { - expect(EventType.values.length, equals(25)); + expect(EventType.values.length, equals(26)); // Verify specific important event types are included expect(EventType.values, contains(EventType.textMessageStart)); diff --git a/sdks/community/dart/test/integration/fixtures_integration_test.dart b/sdks/community/dart/test/integration/fixtures_integration_test.dart index 881ee3ea03..0262bcbdd8 100644 --- a/sdks/community/dart/test/integration/fixtures_integration_test.dart +++ b/sdks/community/dart/test/integration/fixtures_integration_test.dart @@ -410,6 +410,11 @@ void main() { StateDeltaEvent(delta: [ {'op': 'replace', 'path': '/count', 'value': 43}, ]), + const ActivitySnapshotEvent( + messageId: 'rag:abc123', + activityType: 'skill_tool_call', + content: {'skill': 'rag', 'tool_name': 'search'}, + ), RunFinishedEvent(threadId: 'thread_01', runId: 'run_01'), ]; @@ -439,6 +444,12 @@ void main() { final decodedSnapshot = decodedEvents[7] as StateSnapshotEvent; expect(decodedSnapshot.snapshot['count'], equals(42)); expect(decodedSnapshot.snapshot['items'], equals(['a', 'b', 'c'])); + + final decodedActivity = decodedEvents[9] as ActivitySnapshotEvent; + expect(decodedActivity.messageId, equals('rag:abc123')); + expect(decodedActivity.activityType, equals('skill_tool_call')); + expect(decodedActivity.content, equals({'skill': 'rag', 'tool_name': 'search'})); + expect(decodedActivity.replace, isTrue); }); test('handles protobuf content type negotiation', () {