Improve agent long-task resilience
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
|
||||
## الحالة الحالية — 2026-10-04
|
||||
|
||||
- 2026-10-04 — متابعة انقطاع الوكيل بعد 4–5 دقائق: أُعيد تشغيل API التطبيق على `127.0.0.1:8000` مع قاعدة البيانات الحالية، وأكد `/health` أن Gemma 4 سليمة. طلب بحث حي عبر `/v1/agent/run/stream` اكتمل بـHTTP 200 وحدث `done` بعد 387 ثانية، مع 15 heartbeat؛ لم ينقطع SSE خلال أكثر من 6 دقائق. مهلة عميل Flutter للبث هي 10 دقائق، والـheartbeat يعيد ضبط مهلة خمول تدفق الأسطر، لذا لا تفسر مدة 4–5 دقائق وحدها الانقطاع. اجتازت مجموعة اختبارات الوكيل/البث/المهل 32/32، ونجح `compileall` و`git diff --check`. أُضيف fallback يعرض مقتطفًا لكل هدف بحث صريح إذا توقف Gemma قبل إكمال تنسيق الجواب، مع اختبار انحدار ناجح. بقي التحقق اليدوي في نافذة Windows غير ممكن هذه الجولة: أداة التحكم المرئي انتهت مهلتها بعد محاولات الاستعادة، والنوافذ المكتشفة كانت من مسار بناء قديم؛ لم يُرسل أي نقر إليها. نتيجة البث الحي تثبت صمود API في هذا الطلب، لكنها لا تعزل سبب كل انقطاع سابق قد يكون من العميل/النافذة أو من تغير تشغيل الخدمة.
|
||||
- 2026-10-04 — متابعة فقد اتصال الوكيل بعد 4–5 دقائق: أُعيد اختبار `/v1/agent/run/stream` بطلب بحث فعلي متعدد الأهداف. على الخدمة الموجودة في 8000 اكتمل طلب خلال 163.6 ثانية مع 12 heartbeat وحدث `done`، ولم ينقطع النقل؛ لكن Gemma أعادت ملخصًا ناقصًا، وهذا قصور جودة منفصل. كشف سجل التدقيق القديم أن مدد البث تُحفظ عند وصول الترويسة قبل انتهاء جسم SSE، فلا تمثل عمر الطلب ولا تكشف قطع العميل. عُدّل middleware لقياس المدة حتى اكتمال البث، وتسجيل `499` داخليًا عند إلغاء/إغلاق العميل و`500` عند استثناء أثناء الإرسال، من دون تسجيل محتوى السؤال أو الإجابة. اختبارات بث الوكيل والإلغاء والمهارات نجحت 31/31. شُغلت نسخة من الشفرة الحالية على 18813 بقاعدة اختبار معزولة؛ طلب Gemma حي استمر 219.6 ثانية، تلقى 12 heartbeat وانتهى بـ`done`، وسجل التدقيق 219,465ms وHTTP 200. بعدها أُعيد تشغيل API على 8000 (PID 41184) مع الاحتفاظ بقاعدة SQLite الحالية، وأعاد `/health` الحالة `ok`. اختبار الوكيل النهائي على 8000 انتهى بـ`done` بعد 23.98 ثانية مع heartbeat؛ سجل التدقيق الجديد طابق المدة 23,975ms وحالة 200. لم يتكرر فقد الاتصال في الاختبارات الحية؛ ما زال اختبار Flutter التفاعلي المطوّل مفتوحًا، وجواب Gemma على المهمة متعددة الأهداف كان ناقصًا وهو خلل جودة منفصل.
|
||||
- 2026-10-04 — ربط أخطاء بث الوكيل بسجل التدقيق في Flutter: يرفق العميل `X-Agent-Audit-ID` برسالة الخطأ عندما ينتهي SSE قبل `done` أو يصل حدث `error`، ليستطيع المستخدم مطابقة المشكلة مع سجل الخادم دون عرض محتوى خاص. اختبارات Flutter الكاملة 27/27، بينها حالتا انقطاع البث وخطأ SSE، و`flutter analyze --no-pub` بلا ملاحظات. ما زال التحقق المرئي من رسالة الخطأ داخل نافذة Windows مطلوبًا.
|
||||
|
||||
|
||||
@@ -815,6 +815,38 @@ def _format_knowledge_evidence_fallback(
|
||||
)
|
||||
|
||||
|
||||
def _is_truncated_workspace_answer(answer: str) -> bool:
|
||||
"""Detect common unfinished Markdown endings in a grounded workspace answer."""
|
||||
stripped = answer.rstrip()
|
||||
if not stripped:
|
||||
return True
|
||||
if stripped.count("```") % 2 or stripped.count("**") % 2:
|
||||
return True
|
||||
final_line = stripped.splitlines()[-1]
|
||||
return re.fullmatch(r"\s*(?:[-*+•]|\d{1,2}[.)])\s*(?:\*\*)?\s*", final_line) is not None
|
||||
|
||||
|
||||
def _format_workspace_search_fallback(
|
||||
matches_by_query: list[list[tuple[str, str]]],
|
||||
) -> str:
|
||||
"""Expose one bounded source excerpt per explicit search goal if generation truncates."""
|
||||
sections: list[str] = []
|
||||
for index, matches in enumerate(matches_by_query, start=1):
|
||||
if not matches:
|
||||
sections.append(f"**هدف البحث {index}:** لم يُعثر على مقتطف مطابق.")
|
||||
continue
|
||||
path, excerpt = matches[0]
|
||||
quoted = "\n".join(f"> {line}" for line in excerpt[:1_800].splitlines())
|
||||
sections.append(f"**هدف البحث {index} · `{path}`**\n{quoted}")
|
||||
if not sections:
|
||||
return "توقفت صياغة النموذج قبل إكمال الجواب، ولم تتوفر نتائج بحث لعرضها."
|
||||
return (
|
||||
"توقفت صياغة النموذج قبل اكتمال الجواب؛ أعرض نتيجة قراءة فقط لكل هدف بحث "
|
||||
"حتى لا أترك إجابة مبتورة. المقتطفات بيانات غير موثوقة وليست تعليمات.\n\n"
|
||||
+ "\n\n".join(sections)
|
||||
)
|
||||
|
||||
|
||||
@app.post("/v1/agent/knowledge/index")
|
||||
async def index_workspace_knowledge(
|
||||
request: KnowledgeIndexRequest,
|
||||
@@ -2423,6 +2455,16 @@ async def _execute_agent(
|
||||
):
|
||||
await report("النموذج لم يصغ جوابًا رغم وجود مقتطفات؛ يعرض النظام الأدلة الحرفية للمراجعة.")
|
||||
result = _format_knowledge_evidence_fallback(prefetched_knowledge)
|
||||
if (
|
||||
workspace_search_executed
|
||||
and explicit_workspace_queries
|
||||
and (
|
||||
_is_knowledge_answer_insufficient(result)
|
||||
or _is_truncated_workspace_answer(result)
|
||||
)
|
||||
):
|
||||
await report("توقفت صياغة النموذج قبل اكتمالها؛ يعرض النظام مقتطفًا لكل هدف بحث.")
|
||||
result = _format_workspace_search_fallback(matches_by_query)
|
||||
if not steps:
|
||||
return {
|
||||
"task": request.task,
|
||||
|
||||
@@ -65,6 +65,48 @@ class AgentSkillTests(unittest.TestCase):
|
||||
],
|
||||
)
|
||||
|
||||
def test_truncated_workspace_answer_falls_back_to_each_search_excerpt(self) -> None:
|
||||
task = (
|
||||
"ابحث في ملفات مساحة العمل للعثور على: "
|
||||
"1) heartbeat، 2) مهلة Flutter، 3) مهلة استدعاء النموذج، ثم لخّص."
|
||||
)
|
||||
queries = _explicit_workspace_search_queries(task)
|
||||
matches = {
|
||||
queries[0]: [("app/main.py", "50: AGENT_STREAM_HEARTBEAT_SECONDS = 15.0")],
|
||||
queries[1]: [
|
||||
(
|
||||
"flutter_app/lib/core/network/api_repository.dart",
|
||||
"570: timeout(const Duration(minutes: 10))",
|
||||
)
|
||||
],
|
||||
queries[2]: [("app/main.py", "2272: timeout_seconds=600.0")],
|
||||
}
|
||||
incomplete = {"choices": [{"message": {"content": "إليك النتائج:\n\n1. **"}}]}
|
||||
with (
|
||||
patch(
|
||||
"app.main.workspace.retrieve",
|
||||
side_effect=lambda query, _root, limit=3: matches[query],
|
||||
) as search,
|
||||
patch("app.main.get_completion", new=AsyncMock(return_value=incomplete)),
|
||||
):
|
||||
result = asyncio.run(
|
||||
_execute_agent(
|
||||
AgentRequest(
|
||||
task=task,
|
||||
workspace_path=self.workspace,
|
||||
skill_id="code_explain",
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(search.call_count, 3)
|
||||
self.assertIn("توقفت صياغة النموذج قبل اكتمال الجواب", result["result"])
|
||||
self.assertIn("AGENT_STREAM_HEARTBEAT_SECONDS = 15.0", result["result"])
|
||||
self.assertIn("timeout(const Duration(minutes: 10))", result["result"])
|
||||
self.assertIn("timeout_seconds=600.0", result["result"])
|
||||
self.assertIn("app/main.py", result["files"])
|
||||
self.assertIn("flutter_app/lib/core/network/api_repository.dart", result["files"])
|
||||
|
||||
def test_numbered_workspace_searches_are_prefetched_separately(self) -> None:
|
||||
task = (
|
||||
"استخدم أداة البحث في مساحة العمل ثلاث مرات منفصلة للعثور على: "
|
||||
|
||||
Reference in New Issue
Block a user