Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions sdks/community/dart/.gitignore
Original file line number Diff line number Diff line change
@@ -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/
1 change: 1 addition & 0 deletions sdks/community/dart/lib/src/events/event_type.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
58 changes: 58 additions & 0 deletions sdks/community/dart/lib/src/events/events.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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<String, dynamic> json) {
return ActivitySnapshotEvent(
messageId: JsonDecoder.requireField<String>(json, 'messageId'),
activityType: JsonDecoder.requireField<String>(json, 'activityType'),
content: JsonDecoder.requireField<Map<String, dynamic>>(json, 'content'),
replace: json['replace'] as bool? ?? true,
timestamp: JsonDecoder.optionalField<int>(json, 'timestamp'),
rawEvent: json['rawEvent'],
);
}

final String messageId;
final String activityType;
final Map<String, dynamic> content;
final bool replace;

@override
Map<String, dynamic> toJson() => {
...super.toJson(),
'messageId': messageId,
'activityType': activityType,
'content': content,
'replace': replace,
};

@override
ActivitySnapshotEvent copyWith({
String? messageId,
String? activityType,
Map<String, dynamic>? 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;
Expand Down
158 changes: 158 additions & 0 deletions sdks/community/dart/test/events/event_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<AGUIValidationError>()),
);
});

test('ActivitySnapshotEvent fromJson throws on missing activityType', () {
final json = {
'type': 'ACTIVITY_SNAPSHOT',
'messageId': 'msg-1',
'content': {'key': 'value'},
};

expect(
() => ActivitySnapshotEvent.fromJson(json),
throwsA(isA<AGUIValidationError>()),
);
});

test('ActivitySnapshotEvent fromJson throws on missing content', () {
final json = {
'type': 'ACTIVITY_SNAPSHOT',
'messageId': 'msg-1',
'activityType': 'test',
};

expect(
() => ActivitySnapshotEvent.fromJson(json),
throwsA(isA<AGUIValidationError>()),
);
});

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<ActivitySnapshotEvent>());
final activity = event as ActivitySnapshotEvent;
expect(activity.messageId, 'rag:abc123');
expect(activity.activityType, 'skill_tool_call');
});
});
});
}
2 changes: 1 addition & 1 deletion sdks/community/dart/test/events/event_type_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
];

Expand Down Expand Up @@ -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', () {
Expand Down