agent.stream() الوكيل تمامًا كما تفعل agent.send()، وتُبلغ عن التشغيل
في صورة بثّ من الأحداث محدَّدة الأنواع: نص النموذج فور وصوله، وكل استدعاء أداة،
وحدود الخطوات، وطلبات الموافقة، ثم الحدث الختامي run.done. كل حدث كائن JSON
عادي، فيمكنك تمريره إلى المتصفح عبر Server-Sent Events أو WebSocket دون أي
تحويل.
AgentExecutor.stream(options)
الخيارات نفسها التي تقبلها AgentExecutor.execute() (مخزن الموافقات، نقاط الحفظ،
الخطّافات، التتبّع، toolConcurrency، …).
هذه الأحداث، أي الاتحاد AgentEvent، هي نظام الأحداث الوحيد في الـ SDK: البث،
وخيارات المستمع، وsession.on()، وخطّافات واجهة
المستخدم، ومسارات الخادم، و ACP والقنوات كلها تنقل هذه الأحداث نفسها.
المقبض AgentRun
تُرجع stream() كائنًا من النوع AgentRun:
- يبدأ التشغيل فورًا. لست مضطرًا إلى المرور على الأحداث: يكفي
await run.resultوحده ليمضي التشغيل حتى نهايته. resultوعد يُنجَز (resolve) بالقيمةExecutionResultنفسها التي تُرجعهاsend()، ويُرفَض (reject) بالخطأ نفسه الذي كانتsend()سترفض به. التشغيل المُلغى يُنجَز بـfinishReason: 'aborted'؛ والتشغيل المتوقف مؤقتًا بانتظار موافقة يُنجَز بـfinishReason: 'awaiting-approval'معapprovalId. وتركresultدونawaitلا يسبّب أبدًا رفضًا غير معالَج (unhandled rejection).- الخروج المبكر من حلقة
for awaitيُلغي التشغيل (عبر المسار نفسه الذي يسلكهsignal، انظر الإلغاء). وعندها يُنجَزresultبـfinishReason: 'aborted'. - لا ضغط عكسي (backpressure)، ولا يُسقَط شيء. التشغيل لا ينتظر المستهلك أبدًا. تُخزَّن الأحداث مؤقتًا إلى أن تقرأها، فالمستهلك البطيء يرى كل حدث وبالترتيب. والمرور على الأحداث بعد انتهاء التشغيل يعطيك أحداثه كلها أيضًا.
enqueue(input)تضيف مدخلات المستخدم إلى التشغيل وهو يعمل: تنضم إلى سجل المحادثة قبل استدعاء النموذج التالي. انظر المدخلات في قائمة الانتظار.steer(input)تعيد توجيه التشغيل: استدعاء النموذج الذي لم يُنتج شيئًا بعدُ يُلغى ويُعاد مع المدخلات الجديدة. انظر التوجيه.- مستهلك واحد. لا يمكن المرور على
AgentRunإلا مرة واحدة؛ وحلقةfor awaitثانية ترمي خطأً. لتوزيع الأحداث على أكثر من جهة، اجمعها بنفسك. - الخيارات غير الصالحة (غياب
providerأوagentأوinput، أو قيمةtoolConcurrencyخاطئة) تجعلstream()ترمي الخطأ بشكل متزامن.
الاستماع دون المرور على الأحداث
لمراقبة كل تشغيل دون المرور على بثّه، أعطِ الوكيل مستمعًا:createAgent({ onEvent })، أو onAgentEvent ضمن خيارات
AgentExecutor.execute() / stream() / resumeAfterApproval(). يُستدعى
المستمع بشكل متزامن مع كل AgentEvent لحظة وقوعه، في send() كما في
stream()، وفي دورات الجلسات وفي التشغيلات المستأنَفة بعد موافقة. ويتلقى
الأحداث نفسها وبالترتيب نفسه كما لو مررت على البث، مع فارق واحد: send() /
execute() تولّدان كل خطوة نموذج دفعة واحدة، فيأتي نص الخطوة في text.delta
واحد (كما يحدث في stream() مع مزوّد لا يدعم البث). وتصل أحداث الوكلاء
الفرعيين موسومة بالحقل subagent.
الانتقال من onEvent / ExecutionEvent
الخيار ExecuteOptions.onEvent مع ExecutionEvent هو دالة رد النداء القديمة
للأحداث في الـ SDK. وهو مُهمَل: ما زال يعمل (ويسجّل تحذير console.warn مرة
واحدة)، وأحداثه تُشتق الآن من أحداث AgentEvent الخاصة بالتشغيل، وسيُزال في
إصدار رئيسي قادم. استبدل به onAgentEvent (أو createAgent({ onEvent })، وهو
يستقبل أحداث AgentEvent أصلًا):
ويُبلغ
AgentEvent كذلك عمّا لم يكن ExecutionEvent يُبلغ عنه قط: حدود
الخطوات، وطلبات الموافقة، وقرارات الصلاحيات، والميزانيات، وحواجز الحماية،
والمدخلات المنتظِرة والموجَّهة، وضغط السياق، والاستدلال، وإعادة المحاولة لدى
المزوّد، وانحراف الوكيل (انظر المخطط).
بث دورة في جلسة
تبثّsession.stream(input, { signal }) دورة واحدة من
جلسة متعددة الدورات وتُرجع المقبض AgentRun نفسه. يرى التشغيل
المحادثة حتى تلك اللحظة، وعند انتهائه تُحفظ دورته في مخزن الجلسة، تمامًا كما
تحفظها session.send(). ويُسلَّم run.done بعد الحفظ، فيكون سجل المحادثة
مكتملًا حين تنتهي الحلقة. أما التشغيل المُلغى أو الفاشل، أو الذي تتوقف عن
قراءته مبكرًا، فلا يُحفظ.
البث بعد الموافقة
التشغيل الذي توقف مؤقتًا بانتظار موافقة يمكن أن يُكمَل في صورة بث أيضًا. تأخذstreamResumeAfterApproval() وسائط resumeAfterApproval()
وتُرجع AgentRun قيمة result فيه هي ما تُرجعه resumeAfterApproval().
أحداثه هي run.start، ثم tool.start وtool.done للاستدعاء الذي حُسم أمره
(وtool.error في حالة الرفض)، ثم أحداث التتمّة تمامًا كما في تشغيل جديد، حتى
run.done؛ وأي توقف مؤقت آخر ينهيه بـ approval.requested. يعمل الإلغاء
وenqueue() وsteer() كما في أي تشغيل، ويمكن أن تأتي الموافقة من عملية
(process) أخرى، لأن كل شيء يُقرأ من مخزن الموافقات. مع الوكلاء المنشأين بـ
createAgent() استخدم agent.approvals.streamResolve() أو streamAnswer().
وإذا كان النموذج يُختار لكل تشغيل (model على هيئة دالة) فالنموذج المستخدَم هو
الذي استخدمه التشغيل المتوقف.
مخطط الأحداث (الإصدار 1)
لكل حدث الحقول التالية:
أنواع الأحداث وحقولها الإضافية:
الحقول الاختيارية تُحذف حين لا تكون لها قيمة. ولا تكون أبدًا
undefined،
ولذلك تُرجع JSON.parse(JSON.stringify(event)) كائنًا مساويًا.
ضمانات الترتيب
run.startهو الأول وrun.doneهو الأخير، وكلٌّ منهما يصدر مرة واحدة بالضبط.- كل
step.startيتبعهstep.doneواحد بالضبط يحمل قيمةstepنفسها، قبلstep.startالتالي. وكل ما تفعله الخطوة يقع بين الاثنين. - داخل الخطوة:
compaction.start/compaction.done(إذا ضُغط الطلب، قبل استدعاء النموذج)، ثم أحداثprovider.retry/provider.fallback(إذا فشل استدعاء النموذج)، ثم أحداثtext.delta، ثمtext.done، ثم أحداث الأدوات. - استدعاءات الأدوات في الخطوة الواحدة تعمل بالتوازي (انظر
toolConcurrency): تأتي أحداثtool.startبترتيب استدعاء النموذج لها، وأحداثtool.done/tool.errorبترتيب اكتمالها. طابِق بينها بواسطةtoolCallId. - يأتي
input.queuedعند استدعاءrun.enqueue()(بعدrun.start، حتى للمدخلات التي أُدرجت قبل أن ينطلق التشغيل)، ولذلك قد يقع داخل خطوة. أماinput.appliedالخاص به فيأتي بينstep.doneللخطوة التي كانت تعمل وstep.startالتالي، وهذا الأخير يحمل قيمةstepالتي يذكرها. - يأتي
input.steeredعند استدعاءrun.steer(). معmode: 'immediate'تنتهي الخطوة الجارية بعده بـstep.done('steered'، دون أحداث نص من استدعائها المُلغى)، ثمinput.appliedثمstep.startالتالي. - حين يحتاج استدعاء إلى موافقة، تُنفَّذ الاستدعاءات التي تسبقه وتُبلغ عن
نتائجها، ثم يتبعها
approval.requestedوstep.done('awaiting-approval') وrun.done('awaiting-approval'). أما الاستدعاءات التي تليه فلا تبدأ أبدًا. - حين يحجب حاجز حماية، يأتي
guardrail.trippedقبيل النهاية: في حاجز المدخلات يأتي بعدrun.startمباشرة (ولا تبدأ أي خطوة)؛ وفي حاجز المخرجات أو الأدوات يأتي داخل الخطوة، ويتبعهstep.doneثمrun.done('guardrail'). المخرجات المحجوبة لا يكون لهاtext.done؛ والاستدعاءات التي تلي استدعاء أداة محجوبًا لا تبدأ أبدًا.
TypeScript
النوعAgentEvent اتحاد مميَّز (discriminated union): تضييق النوع بناءً على
event.type يعطيك حمولة الحدث. وكل نوع حدث مُصدَّر كذلك (TextDeltaEvent،
ToolDoneEvent، RunDoneEvent، …)، وAgentEventOf<'tool.done'> ينتقي
واحدًا منها بالاسم.
إدارة الإصدارات
لا تتغيرv إلا حين يتغير حدث موجود تغييرًا غير متوافق (حقل حُذف أو أُعيدت
تسميته أو تغيّر نوعه). أما أنواع الأحداث الجديدة والحقول الاختيارية الجديدة
فيمكن أن تُضاف دون تغيير v، فتجاهَل أنواع الأحداث التي لا تعرفها.
الوكلاء الفرعيون
حين يفوّض الوكيل العمل عبر الأداةtask أو أداة منشأة بـ
createDelegateTool() (انظر الوكلاء الفرعيون)، يُبَث تشغيل
الوكيل الفرعي داخل البث نفسه: تظهر خطواته وأحداث text.delta وtext.done
وأحداث الأدوات والأخطاء الخاصة به بين tool.start وtool.done (أو
tool.error) لدى الوكيل الرئيسي لذلك الاستدعاء، وكلٌّ منها يحمل الحقل
subagent:
toolCallId هو استدعاء الأداة لدى الوكيل الرئيسي الذي بدأ الوكيل
الفرعي؛ والوكيل الفرعي لوكيل فرعي يحمل depth: 2 ويحمل الوكيلَ الذي يحتويه في
parent. الأحداث التي ليس فيها subagent تخص التشغيل في المستوى الأعلى:
ضمانات الترتيب أعلاه تسري عليها، وتسري على حدة على خطوات كل وكيل فرعي (وأحداث
عدة وكلاء فرعيين يعملون بالتوازي تتداخل). run.start وrun.done يخصّان
التشغيل في المستوى الأعلى وحده، فيظلان يصدران مرة واحدة بالضبط. والوكيل الفرعي
الذي يتوقف مؤقتًا بانتظار موافقة يُبلَغ عنه مرة واحدة، عبر approval.requested
في المستوى الأعلى (وهو يحمل استدعاء الوكيل الفرعي).
بث الرموز والمزوّدون
حين ينفّذ المزوّد الدالةstream() (وهذا حال كل المزوّدين المضمَّنين
وmockModel)، تُبَث كل خطوة نموذج وتصل أحداث text.delta أثناء إنتاج النموذج
للنص. وتُجمَّع استدعاءات الأدوات من البث. وحين لا يملك المزوّد stream()، أو
تُرجع دالته supportsStreaming(model) القيمة false، ترجع الخطوة إلى
generate() ويصل نصها في text.delta واحد يتبعه text.done.
البث لا يغيّر إلا طريقة الحصول على خطوة نموذج واحدة. الخطّافات، والتحقق من
الوسائط، والموافقات، واستدعاءات الأدوات المتوازية، ونقاط الحفظ، والإلغاء،
ومقاطع التتبّع (spans)، وسائر دوال رد النداء في execute() تتصرف تمامًا كما في
send(). أما send() وexecute() نفساهما فما زالتا تستخدمان generate().
ينبغي أن تُخرج الدالة stream() في المزوّد المخصَّص قطعًا من النوع
text-delta ثم قطعة finish ختامية فيها finishReason وusage. يمكن إخراج
استدعاءات الأدوات في قطع tool-call، أو إنجازها عبر الوعد toolCalls في
StreamResult. وقطعة error تُفشل الخطوة.
الإلغاء
مرّرsignal لإيقاف التشغيل من الخارج، أو اخرج من الحلقة. كلاهما ينهي التشغيل
بالطريقة نفسها التي ينتهي بها send() مُلغى: تُفحص الإشارة بين قطع البث، وقبل
كل استدعاء للنموذج وكل استدعاء أداة، وتصل إلى المزوّد وإلى الأدوات. ينتهي البث
بـ step.done ('aborted'، إن كانت هناك خطوة تعمل) ثم run.done
('aborted')، ويُنجَز result بـ finishReason: 'aborted'.
المدخلات في قائمة الانتظار
تضيفrun.enqueue(input) مدخلات المستخدم (سلسلة نصية، أو أجزاء محتوى، أو
Message[]) إلى تشغيل ما زال جاريًا، كرسالة متابعة يكتبها المستخدم أثناء عمل
الوكيل مثلًا. لا يتوقف التشغيل: تنضم المدخلات إلى سجل المحادثة عند النقطة
الآمنة التالية - بعد نتائج أدوات الخطوة الحالية، وليس أبدًا بين دورة استدعاء
أدوات ونتائجها - ويراها استدعاء النموذج التالي كما لو أن المستخدم كتبها.
والمدخلات التي تُدرج في قائمة الانتظار أثناء كتابة النموذج لرده النهائي تحصل
على خطوة إضافية، فيجيب النموذج عنها في التشغيل نفسه.
enqueue() الكائن { id, applied }. يَرِد id في حدثَي input.queued
وinput.applied. وتكون applied مساوية false إذا كان التشغيل قد انتهى
بالفعل: لم يُستلم المُدخل، فأرسِله بنفسك في دورة جديدة (session.send()). وفي
غير ذلك تكون وعدًا يُنجَز بـ true حالما يدخل المُدخل سجل المحادثة، أو بـ
false إذا توقف التشغيل قبل استدعاء النموذج التالي (أُلغي، أو توقف مؤقتًا
بانتظار موافقة، أو استنفد maxSteps أو الميزانية، أو فشل)؛ وعندها يُترك
المُدخل لك أيضًا.
- المدخلات المتعددة التي تُدرج قبل استدعاء النموذج نفسه تُطبَّق معًا، بترتيب إدراجها.
- مع نقاط الحفظ (
sessionIdأو جلسة متينة)، يُحفظ المُدخل الذي ما زال ينتظر في آخر نقطة حفظ التشغيل فورًا، وهو الموضع نفسه الذي تُحفظ فيه المدخلات المنتظِرة خلف استدعاءات أدوات لم يُجَب عنها (انظر التنفيذ المتين). فلا يضيع عند انهيار أو عند تشغيل يفشل: تطبّقهagent.resume()(مع أنappliedلتشغيل فشل داخل العملية نفسها يُنجَز بـfalse). أما التشغيل الذي ينتهي أو يتوقف مؤقتًا أو يُلغى فلا يحتفظ به. - دون بث، مرّر
InputQueueفيExecuteOptions.inputQueueواستدعِ دالتهاpush(): وهي القائمة نفسها التي تقف خلفrun.enqueue(). كل قائمة تخدم تشغيلًا واحدًا. - في الجلسات، يجعل
agent.session({ turnPolicy: 'queue' })أيsend()أوstream()يُستدعى أثناء عمل دورة (أو أثناء انتظارها أن تبدأ) ينضم إلى تلك الدورة بهذه الطريقة. انظر الجلسات.
التوجيه
تعيدrun.steer(input) توجيه تشغيل جارٍ إلى مدخلات مستخدم جديدة، كأن يغيّر
المستخدم رأيه والوكيل ما زال يفكر:
- إذا كان هناك استدعاء نموذج جارٍ لم يُصدر نصًا ولا استدعاءات أدوات بعدُ،
فإنه يُلغى (بإشارة خاصة به لا بإشارة التشغيل: فالتشغيل يستمر)، ويُهمَل ناتجه
الجزئي، ويُلحَق
inputكرسالة مستخدم ويُستدعى النموذج من جديد. تكونappliedمساوية'immediate'، وتنتهي خطوة الاستدعاء المُلغى بـstep.doneبالقيمة'steered'. والمزوّد الذي يتجاهل إشارة الإلغاء لا يُنتظَر؛ ويُهمَل رده المتأخر. - إذا كان النموذج قد أصدر نصًا (أو استدعاءات أدوات) بالفعل، ينتظر
inputالنقطة الآمنة التالية مثلenqueue(): وتكونappliedمساوية'queued'. - استدعاءات الأدوات الجارية تكتمل وتُحفظ نتائجها؛ أما استدعاءات تلك الدورة التي لم تبدأ بعدُ فلا تُنفَّذ وتحصل على النتيجة المعتادة «أُلغي قبل تشغيله» (“cancelled before it ran”). ثم يُطبَّق المُدخل.
- تكون
appliedمساويةfalseإذا كان التشغيل قد انتهى: أرسل المُدخل في دورة جديدة.
steer() الكائن { id, applied, joined }: يَرِد id في حدثَي
input.steered وinput.applied، وjoined وعد يخبرك هل وصل المُدخل إلى سجل
المحادثة (مثل applied في enqueue()). يُحتسب الاستدعاء المُلغى خطوةً من
maxSteps. ومع نقاط الحفظ، يُحفظ المُدخل في آخر نقطة الحفظ حالما يُستلم وقبل
أن يُستدعى النموذج من جديد، فلا يضيع إذا وقع انهيار أثناء إعادة التوجيه؛ أما
الدورة الجزئية المُهمَلة فلا تُحفظ في نقطة حفظ أبدًا. دون بث، استدعِ steer()
على ExecuteOptions.inputQueue الخاصة بالتشغيل. وفي الجلسات، يجعل
agent.session({ turnPolicy: 'steer' }) أي send() أو stream() يُستدعى أثناء
عمل دورة يوجّه تلك الدورة (انظر
الجلسات).
مثال: الطرفية
مثال: Server-Sent Events
الأحداث قابلة للتحويل إلى JSON، فنقطة نهاية SSE ليست أكثر منres.write واحد
لكل حدث. ألغِ التشغيل حين ينقطع اتصال العميل.
useLoushoAgent() المدخلات بطلب POST ويقرأ أسطر data: نفسها.
يقدّم lousho dev هذه الصيغة بجلسة لكل تبويب في المتصفح: الطلب POST /chat
مع { sessionId, input } يبثّ agent.session({ id }).stream(input) في أسطر
data: تنتهي بـ event: done، وتُحسم الموافقات عبر
POST /chat/:sessionId/approvals/:id (انظر CLI).
مثال: الواجهة الكاملة
تقبلAgentExecutor.stream() كل خيارات execute(). وهنا أداة تحتاج إلى موافقة
تنهي البث بـ approval.requested: