feat(agent): migrate to native CrewAI tool loop and async resume enqueue
This commit is contained in:
@@ -228,6 +228,113 @@ void main() {
|
||||
});
|
||||
|
||||
group('AgUiService real api-path mock', () {
|
||||
test('sendMessage posts only current user message to run API', () async {
|
||||
final client = MockApiClient();
|
||||
final service = AgUiService(onEvent: (_) {}, apiClient: client);
|
||||
client.clearMocks();
|
||||
|
||||
Map<String, dynamic>? postedRunInput;
|
||||
client.registerHandler('/api/v1/agent/runs', 'POST', (request) {
|
||||
postedRunInput = request.data as Map<String, dynamic>;
|
||||
return {
|
||||
'taskId': 'task-1',
|
||||
'threadId': 'thread-1',
|
||||
'runId': 'run-1',
|
||||
'created': false,
|
||||
};
|
||||
});
|
||||
client.registerHandler('/api/v1/agent/runs/thread-1/events', 'SSE', (_) {
|
||||
return <String>[
|
||||
'event: RUN_STARTED',
|
||||
'data: {"type":"RUN_STARTED","threadId":"thread-1","runId":"run-1"}',
|
||||
'',
|
||||
'event: RUN_FINISHED',
|
||||
'data: {"type":"RUN_FINISHED","threadId":"thread-1","runId":"run-1"}',
|
||||
'',
|
||||
];
|
||||
});
|
||||
|
||||
await service.sendMessage('只发送当前输入');
|
||||
|
||||
expect(postedRunInput, isNotNull);
|
||||
final messages = postedRunInput!['messages'] as List<dynamic>;
|
||||
expect(messages.length, 1);
|
||||
final first = messages.first as Map<String, dynamic>;
|
||||
expect(first['role'], 'user');
|
||||
expect(first['content'], '只发送当前输入');
|
||||
});
|
||||
|
||||
test('approveToolCall posts only tool message to resume API', () async {
|
||||
final client = MockApiClient();
|
||||
final service = AgUiService(onEvent: (_) {}, apiClient: client);
|
||||
RouteNavigationTool.instance.bindNavigator((_, {replace = false}) {
|
||||
final _ = replace;
|
||||
});
|
||||
client.clearMocks();
|
||||
|
||||
client.registerHandler('/api/v1/agent/runs', 'POST', (_) {
|
||||
return {
|
||||
'taskId': 'task-1',
|
||||
'threadId': 'thread-1',
|
||||
'runId': 'run-1',
|
||||
'created': false,
|
||||
};
|
||||
});
|
||||
var eventCallCount = 0;
|
||||
client.registerHandler('/api/v1/agent/runs/thread-1/events', 'SSE', (_) {
|
||||
eventCallCount += 1;
|
||||
if (eventCallCount == 1) {
|
||||
return <String>[
|
||||
'event: RUN_STARTED',
|
||||
'data: {"type":"RUN_STARTED","threadId":"thread-1","runId":"run-1"}',
|
||||
'',
|
||||
'event: RUN_FINISHED',
|
||||
'data: {"type":"RUN_FINISHED","threadId":"thread-1","runId":"run-1"}',
|
||||
'',
|
||||
];
|
||||
}
|
||||
return <String>[
|
||||
'event: RUN_STARTED',
|
||||
'data: {"type":"RUN_STARTED","threadId":"thread-1","runId":"run-2"}',
|
||||
'',
|
||||
'event: RUN_FINISHED',
|
||||
'data: {"type":"RUN_FINISHED","threadId":"thread-1","runId":"run-2"}',
|
||||
'',
|
||||
];
|
||||
});
|
||||
|
||||
Map<String, dynamic>? postedResumeInput;
|
||||
client.registerHandler('/api/v1/agent/runs/thread-1/resume', 'POST', (
|
||||
request,
|
||||
) {
|
||||
postedResumeInput = request.data as Map<String, dynamic>;
|
||||
return {
|
||||
'taskId': 'task-2',
|
||||
'threadId': 'thread-1',
|
||||
'runId': 'run-2',
|
||||
'created': false,
|
||||
};
|
||||
});
|
||||
|
||||
await service.sendMessage('初始化会话');
|
||||
await service.approveToolCall(
|
||||
toolCallId: 'call-1',
|
||||
toolName: 'navigate_to_route',
|
||||
args: {
|
||||
'target': '/calendar/dayweek',
|
||||
'replace': false,
|
||||
'__nonce': 'nonce-1',
|
||||
},
|
||||
);
|
||||
|
||||
expect(postedResumeInput, isNotNull);
|
||||
final messages = postedResumeInput!['messages'] as List<dynamic>;
|
||||
expect(messages.length, 1);
|
||||
final first = messages.first as Map<String, dynamic>;
|
||||
expect(first['role'], 'tool');
|
||||
expect(first.containsKey('toolCallId'), true);
|
||||
});
|
||||
|
||||
test('approveToolCall resumes and emits TOOL_CALL_RESULT', () async {
|
||||
final events = <AgUiEvent>[];
|
||||
final realService = AgUiService(onEvent: events.add);
|
||||
@@ -238,13 +345,16 @@ void main() {
|
||||
await realService.sendMessage('打开日历页面');
|
||||
|
||||
final toolStart = events.whereType<ToolCallStartEvent>().first;
|
||||
final toolArgsEvent = events
|
||||
.whereType<ToolCallArgsEvent>()
|
||||
.firstWhere((e) => e.toolCallId == toolStart.toolCallId);
|
||||
final toolArgsEvent = events.whereType<ToolCallArgsEvent>().firstWhere(
|
||||
(e) => e.toolCallId == toolStart.toolCallId,
|
||||
);
|
||||
final toolArgs = jsonDecode(toolArgsEvent.delta) as Map<String, dynamic>;
|
||||
expect(toolStart.toolCallName, 'navigate_to_route');
|
||||
expect(
|
||||
events.whereType<ToolCallResultEvent>().where((e) => e.toolCallId == toolStart.toolCallId).isEmpty,
|
||||
events
|
||||
.whereType<ToolCallResultEvent>()
|
||||
.where((e) => e.toolCallId == toolStart.toolCallId)
|
||||
.isEmpty,
|
||||
true,
|
||||
);
|
||||
|
||||
@@ -267,9 +377,9 @@ void main() {
|
||||
|
||||
await realService.sendMessage('打开日历页面');
|
||||
final toolStart = events.whereType<ToolCallStartEvent>().first;
|
||||
final toolArgsEvent = events
|
||||
.whereType<ToolCallArgsEvent>()
|
||||
.firstWhere((e) => e.toolCallId == toolStart.toolCallId);
|
||||
final toolArgsEvent = events.whereType<ToolCallArgsEvent>().firstWhere(
|
||||
(e) => e.toolCallId == toolStart.toolCallId,
|
||||
);
|
||||
final toolArgs = jsonDecode(toolArgsEvent.delta) as Map<String, dynamic>;
|
||||
|
||||
// replace navigator -> true 会失败,因为未绑定 navigator。
|
||||
@@ -287,10 +397,7 @@ void main() {
|
||||
test('stream ignores malformed SSE payload and continues', () async {
|
||||
final events = <AgUiEvent>[];
|
||||
final client = MockApiClient();
|
||||
final service = AgUiService(
|
||||
onEvent: events.add,
|
||||
apiClient: client,
|
||||
);
|
||||
final service = AgUiService(onEvent: events.add, apiClient: client);
|
||||
client.clearMocks();
|
||||
client.registerHandler('/api/v1/agent/runs', 'POST', (_) {
|
||||
return {
|
||||
@@ -326,10 +433,7 @@ void main() {
|
||||
|
||||
test('subsequent SSE requests carry Last-Event-ID header', () async {
|
||||
final client = MockApiClient();
|
||||
final service = AgUiService(
|
||||
onEvent: (_) {},
|
||||
apiClient: client,
|
||||
);
|
||||
final service = AgUiService(onEvent: (_) {}, apiClient: client);
|
||||
client.clearMocks();
|
||||
var runCount = 0;
|
||||
final seenLastEventIds = <String?>[];
|
||||
@@ -342,7 +446,9 @@ void main() {
|
||||
'created': false,
|
||||
};
|
||||
});
|
||||
client.registerHandler('/api/v1/agent/runs/thread-1/events', 'SSE', (request) {
|
||||
client.registerHandler('/api/v1/agent/runs/thread-1/events', 'SSE', (
|
||||
request,
|
||||
) {
|
||||
seenLastEventIds.add(request.headers?['Last-Event-ID']);
|
||||
if (runCount == 1) {
|
||||
return <String>[
|
||||
|
||||
Reference in New Issue
Block a user