Diagnose interrupted agent SSE streams

This commit is contained in:
Hamza Ayed
2026-10-07 18:40:31 +03:00
parent 52142ec053
commit d36957a1ca
3 changed files with 55 additions and 27 deletions
@@ -568,10 +568,16 @@ class ApiRepository {
.send(request)
.timeout(const Duration(minutes: 10));
final auditId = streamed.headers['x-agent-audit-id'];
final streamStopwatch = Stopwatch()..start();
var heartbeatCount = 0;
var lastEventName = 'بدء الاتصال';
String withAuditId(String message) =>
auditId == null || auditId.isEmpty
? message
: '$message\nمعرّف تتبع الطلب: $auditId';
String streamDiagnostics() =>
'بعد ${streamStopwatch.elapsed.inSeconds} ثانية؛ آخر حدث: '
'$lastEventName؛ عدد النبضات: $heartbeatCount.';
if (streamed.statusCode < 200 || streamed.statusCode >= 300) {
final response = await http.Response.fromStream(streamed);
_checkStatus(response);
@@ -579,37 +585,50 @@ class ApiRepository {
String? eventName;
Map<String, dynamic>? resultData;
String? streamError;
await for (final line in streamed.stream
.transform(utf8.decoder)
.transform(const LineSplitter())
.timeout(const Duration(minutes: 10))) {
if (line.startsWith('event: ')) {
eventName = line.substring(7).trim();
} else if (line.startsWith('data: ')) {
final data = jsonDecode(line.substring(6)) as Map<String, dynamic>;
switch (eventName) {
case 'progress':
final message = data['message']?.toString();
if (message != null) onProgress?.call(message);
break;
case 'heartbeat':
onProgress?.call('الوكيل ما زال يعمل، أنتظر النموذج المحلي...');
break;
case 'done':
resultData = data;
break;
case 'error':
streamError =
data['message']?.toString() ?? 'تعذر تنفيذ مهمة الوكيل.';
break;
try {
await for (final line in streamed.stream
.transform(utf8.decoder)
.transform(const LineSplitter())
.timeout(const Duration(minutes: 10))) {
if (line.startsWith('event: ')) {
eventName = line.substring(7).trim();
lastEventName = eventName;
} else if (line.startsWith('data: ')) {
final data = jsonDecode(line.substring(6)) as Map<String, dynamic>;
switch (eventName) {
case 'progress':
final message = data['message']?.toString();
if (message != null) onProgress?.call(message);
break;
case 'heartbeat':
heartbeatCount++;
onProgress?.call('الوكيل ما زال يعمل، أنتظر النموذج المحلي...');
break;
case 'done':
resultData = data;
break;
case 'error':
streamError =
data['message']?.toString() ?? 'تعذر تنفيذ مهمة الوكيل.';
break;
}
eventName = null;
}
eventName = null;
}
} on Exception catch (error) {
throw Exception(
withAuditId(
'انقطع اتصال بث الوكيل ${streamDiagnostics()} نوع الخطأ: '
'${error.runtimeType}.',
),
);
}
if (streamError != null) throw Exception(withAuditId(streamError));
final data = resultData;
if (data == null) {
throw Exception(withAuditId('انقطع اتصال البث قبل اكتمال الوكيل.'));
throw Exception(
withAuditId('انتهى البث قبل اكتمال الوكيل ${streamDiagnostics()}'),
);
}
return AgentRunResult.fromApiData(data);
} finally {
@@ -45,8 +45,16 @@ void main() {
await expectLater(
repository.runAgent('اختبار انقطاع البث'),
throwsA(
predicate(
(error) => error.toString().contains('trace-closed-42'),
allOf(
predicate(
(error) => error.toString().contains('trace-closed-42'),
),
predicate(
(error) => error.toString().contains('آخر حدث: progress'),
),
predicate(
(error) => error.toString().contains('عدد النبضات: 0'),
),
),
),
);