feat: AG-UI 协议对齐与路由导航功能

- 前端: 添加 SSE 流式支持、stateSnapshot 事件、路由导航工具
- 前端: 实现工具调用审批流程,支持 pending 状态展示
- 后端: Agent 状态管理与会话持久化相关重构
- 文档: 新增 agent-agui-full-alignance 设计文档
- 测试: 补充相关单元测试和集成测试
This commit is contained in:
zl-q
2026-03-07 17:30:20 +08:00
parent ec33bb0cee
commit 120df903d2
52 changed files with 4305 additions and 1672 deletions
@@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:json_annotation/json_annotation.dart';
import 'tool_result.dart';
@@ -15,6 +17,7 @@ class AgUiEventTypeWire {
static const toolCallEnd = 'TOOL_CALL_END';
static const toolCallResult = 'TOOL_CALL_RESULT';
static const toolCallError = 'TOOL_CALL_ERROR';
static const stateSnapshot = 'STATE_SNAPSHOT';
static const messagesSnapshot = 'MESSAGES_SNAPSHOT';
}
@@ -30,6 +33,7 @@ enum AgUiEventType {
toolCallEnd,
toolCallResult,
toolCallError,
stateSnapshot,
messagesSnapshot,
unknown,
}
@@ -47,6 +51,7 @@ const _wireToTypeMap = {
AgUiEventTypeWire.toolCallEnd: AgUiEventType.toolCallEnd,
AgUiEventTypeWire.toolCallResult: AgUiEventType.toolCallResult,
AgUiEventTypeWire.toolCallError: AgUiEventType.toolCallError,
AgUiEventTypeWire.stateSnapshot: AgUiEventType.stateSnapshot,
AgUiEventTypeWire.messagesSnapshot: AgUiEventType.messagesSnapshot,
};
@@ -63,6 +68,7 @@ const _typeToWireMap = {
AgUiEventType.toolCallEnd: AgUiEventTypeWire.toolCallEnd,
AgUiEventType.toolCallResult: AgUiEventTypeWire.toolCallResult,
AgUiEventType.toolCallError: AgUiEventTypeWire.toolCallError,
AgUiEventType.stateSnapshot: AgUiEventTypeWire.stateSnapshot,
AgUiEventType.messagesSnapshot: AgUiEventTypeWire.messagesSnapshot,
AgUiEventType.unknown: '',
};
@@ -85,6 +91,7 @@ final _typeToFactory = {
AgUiEventType.toolCallEnd: ToolCallEndEvent.fromJson,
AgUiEventType.toolCallResult: ToolCallResultEvent.fromJson,
AgUiEventType.toolCallError: ToolCallErrorEvent.fromJson,
AgUiEventType.stateSnapshot: StateSnapshotEvent.fromJson,
AgUiEventType.messagesSnapshot: MessagesSnapshotEvent.fromJson,
AgUiEventType.unknown: UnknownAgUiEvent.fromJson,
};
@@ -255,25 +262,61 @@ class ToolCallEndEvent extends AgUiEvent {
Map<String, dynamic> toJson() => _$ToolCallEndEventToJson(this);
}
@JsonSerializable()
@JsonSerializable(createFactory: false, createToJson: false)
class ToolCallResultEvent extends AgUiEvent {
final String messageId;
final String toolCallId;
final Map<String, dynamic> result;
final UiCard? ui;
final String content;
ToolCallResultEvent({
required this.messageId,
required this.toolCallId,
required this.result,
this.ui,
required this.content,
}) : super(type: AgUiEventType.toolCallResult);
factory ToolCallResultEvent.fromJson(Map<String, dynamic> json) =>
_$ToolCallResultEventFromJson(json);
Map<String, dynamic> get payload {
try {
final decoded = jsonDecode(content);
if (decoded is Map<String, dynamic>) {
return decoded;
}
} catch (_) {}
return {'content': content};
}
Map<String, dynamic> get result {
final rawResult = payload['result'];
if (rawResult is Map<String, dynamic>) {
return rawResult;
}
return payload;
}
UiCard? get ui {
final rawUi = payload['ui'];
if (rawUi is Map<String, dynamic>) {
return UiCard.fromJson(rawUi);
}
return null;
}
factory ToolCallResultEvent.fromJson(Map<String, dynamic> json) {
final rawContent = json['content'];
final content = rawContent is String ? rawContent : '';
return ToolCallResultEvent(
messageId: json['messageId'] as String,
toolCallId: json['toolCallId'] as String,
content: content,
);
}
@override
Map<String, dynamic> toJson() => _$ToolCallResultEventToJson(this);
Map<String, dynamic> toJson() => {
'type': agUiEventTypeToWire(type),
'messageId': messageId,
'toolCallId': toolCallId,
'content': content,
};
}
@JsonSerializable()
@@ -292,6 +335,29 @@ class ToolCallErrorEvent extends AgUiEvent {
Map<String, dynamic> toJson() => _$ToolCallErrorEventToJson(this);
}
@JsonSerializable(createFactory: false, createToJson: false)
class StateSnapshotEvent extends AgUiEvent {
final Map<String, dynamic> snapshot;
StateSnapshotEvent({required this.snapshot})
: super(type: AgUiEventType.stateSnapshot);
factory StateSnapshotEvent.fromJson(Map<String, dynamic> json) {
final rawSnapshot = json['snapshot'];
return StateSnapshotEvent(
snapshot: rawSnapshot is Map<String, dynamic>
? rawSnapshot
: <String, dynamic>{},
);
}
@override
Map<String, dynamic> toJson() => {
'type': agUiEventTypeToWire(type),
'snapshot': snapshot,
};
}
@JsonSerializable()
class MessagesSnapshotEvent extends AgUiEvent {
final List<SnapshotMessage> messages;
@@ -121,10 +121,7 @@ ToolCallResultEvent _$ToolCallResultEventFromJson(Map<String, dynamic> json) =>
ToolCallResultEvent(
messageId: json['messageId'] as String,
toolCallId: json['toolCallId'] as String,
result: json['result'] as Map<String, dynamic>,
ui: json['ui'] == null
? null
: UiCard.fromJson(json['ui'] as Map<String, dynamic>),
content: json['content'] as String,
);
Map<String, dynamic> _$ToolCallResultEventToJson(
@@ -132,8 +129,7 @@ Map<String, dynamic> _$ToolCallResultEventToJson(
) => <String, dynamic>{
'messageId': instance.messageId,
'toolCallId': instance.toolCallId,
'result': instance.result,
'ui': instance.ui,
'content': instance.content,
};
ToolCallErrorEvent _$ToolCallErrorEventFromJson(Map<String, dynamic> json) =>