مفتوح المصدر · ROSH™ Company Labs

cut-at-k ماذا يقول عميلك عن بثّ انقطع قبل نهايته

تعيد cut-at-k تشغيل البثّ كاملًا ثم عند كل نقطة قطع، وتتحقّق هل يعترف عميلك بأنه قُطع. رخصة MIT، بدون اعتماديات، 21 اختبارًا. ROSH™ Company Labs.

البثّ الذي ينقطع باكرًا ليس حالة نادرة

يسقط الاتصال. تنفد مهلة الوسيط. يموت المُنتِج في منتصف الجملة، أو يضغط القارئ زرّ الإيقاف. ينتهي البثّ حيث ينتهي، وعلى مكتبة العميل أن تقول لمُستدعيها شيئًا عمّا جرى للتوّ. واختبار هذا المسار يعني اختيار نقطة قطع.

يزيل cut-at-k هذا الاختيار. يأخذ بثًّا واحدًا، ويمرّره عبر عميلك مرّة كاملًا ومرّة عند كل بادئة منه، ثم يسأل عند كل قطع سؤالين:

السؤال الثاني هو موضع الخلل في العادة، وهو خلل صامت حين يقع: الوعد يُحَلّ، وكائن النتيجة يبدو عاديًا، ولا شيء فيه يقول إن هذا الجواب نصف جواب.

لا تعرف شيئًا عن بروتوكولك

أنت تعطيها الأحداث ودالّة إعادة تشغيل، وهي تعطيك الحلقة والمقارنة. أحداث SSE، أو أُطر WebSocket، أو عيّنة JSON سطرية، أو مصفوفة بنيتها بيدك — لا تنظر داخل أي حدث قط.

npm install cut-at-k
import { severAtEveryPoint } from 'cut-at-k';

await severAtEveryPoint({
  events,                                   // the stream, as an array
  label: 'checkout-run',
  replay: (prefix) => runMyClient(prefix)   // called whole, then per prefix
});

ثلاث نقاط دخول تحمل العمل: severAtEveryPoint تشغّل الإعادات، وsummarise تختصرها إلى تقرير، وformat تطبعه. بدون أي اعتماديات، وعشرة ملفات، ورخصة MIT.

ترفض أن تصرخ «ذئب»

ثلاثة أشياء تبدو متطابقة في الأرقام الخام، وواحد منها فقط هو النتيجة التي تستحقّ الإبلاغ.

لذلك لا تبلّغ summarise بأي عيب إطلاقًا ما لم تمرّر لها مُسنِدًا باسم lostContent. أنت من يقرّر ما يُعدّ محتوى في بروتوكولك، والأداة لن تخمّن نيابةً عنك. والبثوث المستبعَدة لكونها غير صالحة عن قصد تعود في صورة عدد لا أن تختفي بهدوء، فيبقى في وسعك دائمًا أن ترى كم استُبعد من المجموعة.

وقد تعلّمنا هذا بالخطأ. النسخة الأولى من summarise لم تميّز بين أيّ من هذه الحالات، فعدّت 154 قطعًا مشكلةً في مجموعة كان فيها 54. ويحتفظ README بالجملة التي خرجت من ذلك: «A tool that cries wolf is worse than no tool» — أي أن أداةً تصرخ «ذئب» أسوأ من لا أداة.

تشغيلها على AG-UI: 227 قطعًا، و54 أفقدت شيئًا بصمت

يشحن AG-UI 68 عيّنة مطابقة كتبها مؤلّفو البروتوكول أنفسهم. اثنتان منها بطول حدث واحد فلا نقطة قطع فيهما، فأُعيد تشغيل 66 — كلّ واحدة عبر HttpAgent حقيقي على HTTP وSSE حقيقيَّين مع @ag-ui/client 1.0.0، كاملةً ثم مقطوعةً عند كل حدّ بين حدثين.

48 streams, 227 cuts, 18 streams excluded as invalid on purpose
4 cuts threw during replay

reported the same as the whole run    : 154
  of which something was actually lost : 54
  of those, no terminal event either   : 52
spread across                          : 34 streams

أصغر الحالات أوضحها. خذ العيّنة المسمّاة conformant-run-is-quiet واقطعها بعد حدثها الرابع. الاستدعاء المقطوع والاستدعاء المكتمل كلاهما يُحَلّ، ولا أحد منهما يُرفَض. وRunAgentResult لا يحمل أي حقل يقول أيّهما قُطع. وعدد الرسائل 1 في الحالتين. فالمُستدعي الذي يتحقّق هل نجح التشغيل، أو كم رسالة عادت، يُقال له الشيء نفسه تمامًا من تشغيل اكتمل ومن تشغيل توقّف في منتصف الجملة.

وأربعة من هذه الـ227 قطعًا رمت استثناءً أثناء إعادة التشغيل. وهي تبقى ضمن المجموع وتُنحّى جانبًا قبل المقارنة، لأن قطعًا يُعطِّل العميل اكتشافٌ مختلف عن قطعٍ أبلغ بشيء خاطئ.

ما لا تدّعيه هذه الصفحة

يستطيع المشترك أن يكشف هذا اليوم. فـonRunFinishedEvent يقع على التشغيل المكتمل لا على المقطوع، بينما يقع onRunFinalized على كليهما — فالفرق ملحوظ إن كنت مُنصتًا على تلك القناة. والادّعاء هنا أضيق من ذلك: الاستدعاء المنتظَر لا يُظهره. وتصادف أن تلك هي القناة التي يستعملها أكثر المُستدعين، ولهذا يستحقّ الأمر التدوين، لكنه ليس قولًا بأن المعلومة غير متاحة.

وليس في هذا كلّه اكتشاف. فقد أُبلغ عن السلوك في 3 أغسطس 2026 في ag-ui-protocol/ag-ui#2300، وطلب الدمج PR #2354 مفتوح منذ 7 أغسطس 2026 ويحمل الإصلاح. وكلاهما كان ما يزال مفتوحًا حين كُتبت هذه الصفحة. والحجّة لصالح الأداة ليست الجِدّة بل الاتّساع: وُجد العطل مرّة واحدة، يدويًا، على بثّ واحد، والحلقة تُظهر 54 قطعًا من الشكل نفسه عبر 34 بثًّا دون أن يضطرّ أحد إلى تخمين موضع النظر.

ستّ مكتبات، والسؤال نفسه، وأربعة أجوبة سليمة

يحمل المستودع مسبارات تسأل خمس قواعد شيفرة أخرى: ماذا تقول لمُستدعيها حين يُقطع البثّ؟ أربع منها تجيب إجابة صحيحة:

أما MCP TypeScript SDK فلم يفعل. فحين يموت شقّ الاستجابة من طلب POST، ينتظر الطلب مهلته كاملة قبل أن يُخبَر المُستدعي — عند كل نقطة قطع، بما فيها نقطة كان نصف الردّ قد وصل عندها.

ستٌّ سُئلت في المجموع، بحساب AG-UI نفسه — وأربع منها سليمة. والمكتبتان اللتان تجيبان إجابة خاطئة، وهما AG-UI وMCP SDK، وجدهما غيرُنا أولًا كلتاهما. هذا هو الحساب الأمين: هذه طريقة لطرح سؤال قديم في كل مكان دفعة واحدة، لا مصدرٌ لاكتشافات جديدة.

أرقام

كيف تستشهد بها

للمكتبة DOI خاصّ بها: 10.5281/zenodo.23002965. ويُشحن ملف CITATION.cff داخل المستودع، فيُملأ منه زرّ Cite this repository في GitHub نفسه.

المشرف

يشرف عليها Redouane — ROSH™ Company Labs. تُرسل الأخطاء وطلبات الميزات إلى مسائل GitHub. وأنفع بلاغ يمكن أن يصل هذا المشروع هو مجموعة عيّنات عدَّ فيها التقريرُ شيئًا اكتشافًا وهو ليس كذلك.

FAQ

أسئلة يطرحها المطوّرون

ما الذي تفعله cut-at-k بالضبط؟

تأخذ بثًّا واحدًا وتمرّره عبر عميلك مرّتين: مرّة كاملًا، ومرّة عند كل بادئة منه. وعند كل قطع تسأل أمرين. هل تغيّر الرأس — فالقطع قد يُفقد الذيل، لكنه يجب ألّا يغيّر أبدًا ما جاء قبل موضع القطع. وهل يقول المستهلك ذلك — فالتشغيل المقطوع يجب ألّا يبلّغ بما يبلّغ به التشغيل نفسه حين يكتمل. والثاني هو موضع المشاكل عادةً، وهو صامت حين يقع الخطأ فيه.

\n
لماذا القطع عند كل نقطة بدل اختبار انقطاع واحد في النهاية؟

لأن اختبار انقطاع واحد يغطّي نقطة قطع واحدة. فالقطع بعد آخر حدث محتوى لا يُفقد شيئًا ولا بأس به، بينما القطع قبله ببضعة أحداث قد يُسقط رسالة كاملة والاستدعاء يُحَلّ حلًّا عاديًا — وفي مجموعة AG-UI هذه هي العيّنة conformant-run-is-quiet مقطوعةً بعد حدثها الرابع. وعلى تلك العيّنات أنتجت 227 قطعًا منها 54 أفقدت شيئًا ولم تقل عنه شيئًا، موزّعة على 34 بثًّا. والعطل الذي وراء هذه الحالات وُجد مرّة واحدة، يدويًا، على بثّ واحد. وتغطية هذا الاتّساع باليد تعني كتابة 227 اختبار انقطاع، وتخمين نقطة القطع الصحيحة في كلٍّ منها.

\n
هل تحتاج إلى فهم بروتوكولي — SSE أو WebSocket أو شيء خاصّ بي؟

لا. أنت تمرّر مصفوفة أحداث ودالّة إعادة تشغيل، وهي لا تنظر داخل أي حدث. فأحداث SSE وأُطر WebSocket وأسطر JSON ومصفوفة جمّعتها بيدك تعمل كلّها بالطريقة نفسها. وتحمل العملَ ثلاثُ نقاط دخول: severAtEveryPoint({events, label, replay}) تشغّل الإعادات، وsummarise() تختصرها، وformat() تطبع التقرير. بدون أي اعتماديات، وعشرة ملفات، ورخصة MIT.

\n
ألن يُنتج قطع البثّ بـ227 طريقة 227 إنذارًا كاذبًا؟

هذا بالضبط ما فعلته النسخة الأولى — عدّت 154 قطعًا مشكلةً في مجموعة كان فيها 54. ثلاث حالات تبدو متطابقة في الأرقام الخام: قطعٌ لم يُفقد شيئًا، وبثٌّ غير صالح عن قصد، وقطعٌ أفقد محتوى ثم أُبلغ عنه كأنه مكتمل. والثالثة وحدها هي الاكتشاف. وsummarise() اليوم تفصل بينها، ولا تبلّغ بأي عيب حتى تخبرها أنت بما يعنيه المحتوى عندك.

\n
ما هو مُسنِد lostContent ولماذا عليّ كتابته بنفسي؟

هو الدالّة التي تقرّر هل أسقط القطعُ فعلًا شيئًا يهمّ المستهلك. وأنت وحدك من يعرف ذلك في بروتوكولك: فحدث المسك الختامي ليس محتوى، ومتن الرسالة محتوى. وبدون هذا المُسنِد لا تبلّغ summarise() بأي عيب إطلاقًا، وهذا مقصود. فأداةٌ تخمّن ما يُعدّ محتوى ستخمّن خطأً، وتقريرٌ لا يثق به أحد أسوأ من لا تقرير.

\n
ماذا تعني عبارة «مستبعَد لكونه غير صالح عن قصد» في المخرجات؟

مجموعات المطابقة تتضمّن عمدًا بثوثًا مشوّهة ليُختبر رفض العميل لها. والقطع قبل الخطأ المتعمَّد يجعل التشغيل المقطوع يسلّم أكثر ممّا يسلّمه الكامل، فتنقلب المقارنة ولا تعني شيئًا. فتُنحّى تلك البثوث جانبًا — 18 من أصل 66 في تشغيل AG-UI — ويُبلَّغ عنها في صورة عدد لا أن تُسقط بصمت، فترى كم من المجموعة لم يُمتحَن فعلًا.

\n
ماذا وجدت في AG-UI؟

أُعيد تشغيل 66 من أصل 68 عيّنة مطابقة عبر HttpAgent حقيقي على HTTP وSSE مع @ag-ui/client 1.0.0. فنتج عن ذلك 48 بثًّا و227 قطعًا. أبلغ 154 قطعًا بما يبلّغ به التشغيل الكامل، ومنها 54 أفقدت شيئًا فعلًا، موزّعة على 34 بثًّا؛ و52 من هذه الـ54 كانت بلا حدث ختامي أيضًا. وأصغر الحالات: اقطع conformant-run-is-quiet بعد حدثها الرابع، فيُحَلّ الاستدعاءان كلاهما برسالة واحدة لكلٍّ منهما، وبلا أي حقل يقول أيّهما قُطع.

\n
هل هذا عطل في AG-UI لم يعرفه أحد؟

لا، والصفحة تقول ذلك. فقد أُبلغ عنه في 3 أغسطس 2026 في ag-ui-protocol/ag-ui#2300، وطلب الدمج #2354 مفتوح منذ 7 أغسطس 2026 ويحمل الإصلاح، وكلاهما كان مفتوحًا حين كُتبت هذه الصفحة. وما يُعرض هنا ليس الجِدّة. وُجد العطل مرّة واحدة، يدويًا، على بثّ واحد — والحلقة تُظهر 54 قطعًا من الشكل نفسه عبر 34 بثًّا دون أن يخمّن أحد موضع النظر.

\n
ألا يستطيع مشترك في AG-UI أن يعرف أصلًا أن التشغيل قُطع؟

بلى، وهذا الحدّ جزء من أي خلاصة أمينة. فـonRunFinishedEvent يقع على التشغيل المكتمل لا على المقطوع، بينما يقع onRunFinalized على كليهما، فيستطيع المشترك أن يرى الفرق اليوم. والادّعاء الأضيق هو المطروح هنا: الاستدعاء المنتظَر لا يُظهره. فـRunAgentResult لا حقل فيه لذلك، وكلا الاستدعاءين يُحَلّ، وعدد الرسائل متطابق. وتلك هي القناة التي يستعملها أكثر المُستدعين فعلًا.

\n
ما المكتبات الأخرى التي فُحصت، وكيف كانت نتيجتها؟

خمس مكتبات، في مجلّد probes: Vercel AI SDK وLangGraph JS وMastra وOpenAI Node SDK وMCP TypeScript SDK. أربع منها عادت سليمة. يبلّغ Vercel AI SDK بسبب إنهاء قيمته other على تشغيل مقطوع، أو يرفع خطأً إن لم يمرّ أي نصّ؛ وتتوقّف نقطة حفظ LangGraph حيث توقّف المستهلك المُجهَض وتقول من أين تُستأنف؛ وذاكرة Mastra ما تزال تحتفظ بما عُرض على المستهلك؛ والردّ النهائي في OpenAI يحتفظ باستدعاء الأداة الذي بثّه ولا يدّعي الاكتمال قطّ.

\n
ما الخطأ في MCP TypeScript SDK؟

حين يموت شقّ الاستجابة من طلب POST، ينتظر الطلب مهلته كاملة قبل أن يُخبَر المُستدعي — عند كل نقطة قطع مُختبَرة، بما فيها نقطة كان نصف الردّ قد وصل عندها. فالمُستدعي لا يُعطى جوابًا خاطئًا بقدر ما يُترك بلا جواب زمنًا طويلًا. وقد وجده غيرُنا أيضًا قبل أن تنظر هذه الأداة: سُئلت ستّ مكتبات، وكانت أربع سليمة، والمكتبتان اللتان أجابتا خطأً كان لكلٍّ منهما بلاغ سابق.

\n
كيف أشغّلها على عميلي أنا؟

اجمع بثًّا واحدًا في صورة مصفوفة أحداث — عيّنة مسجّلة تكفي. ثم اكتب دالّة إعادة تشغيل تُطعم بادئةً لعميلك كما يفعل الإنتاج، عبر ناقل حقيقي إن استطعت، وتعيد ما يصل مُستدعيك. مرّر الاثنين إلى severAtEveryPoint مع تسمية، وأضف مُسنِد lostContent الخاص ببروتوكولك، ثم summarise() وformat(). وابدأ ببثّ تثق به أصلًا؛ فالنتيجة المثيرة هي القطع الذي ما كنت لتفكّر في كتابته.

\n
ما مدى نضجها، بصراحة؟

عمرها ثلاثة أيام وقت كتابة هذا. نُشرت أول مرّة في 25 سبتمبر 2026، وهي الآن 0.1.3، ولها 0 نجمة على GitHub. فيها 21 اختبارًا، كلّها الـ21 ناجحة، وعشرة ملفات بلا اعتماديات، فقراءتها من أوّلها إلى آخرها تكلّف ظهيرة واحدة على الأكثر. اقرأها قبل أن تثق بتقريرها. وهذه ليست نصيحة تواضع، بل هي الأساس الوحيد لاستعمال أداة عمرها ثلاثة أيام.

\n
هل أستطيع الاستشهاد بها؟

نعم. لها DOI خاصّ بها، 10.5281/zenodo.23002965، وتُشحن معها CITATION.cff، فيُملأ منه زرّ Cite this repository في GitHub. وهي برخصة MIT ومنشورة مع إثبات مصدر موثّق، فيمكن تتبّع الحزمة المنشورة على npm رجوعًا إلى مسار العمل الذي بناها.