Skip to main content
تشغّل agent.stream() الوكيل تمامًا كما تفعل agent.send()، وتُبلغ عن التشغيل في صورة بثّ من الأحداث محدَّدة الأنواع: نص النموذج فور وصوله، وكل استدعاء أداة، وحدود الخطوات، وطلبات الموافقة، ثم الحدث الختامي run.done. كل حدث كائن JSON عادي، فيمكنك تمريره إلى المتصفح عبر Server-Sent Events أو WebSocket دون أي تحويل.
الواجهة الكاملة (full API) فيها الدالة نفسها: تقبل 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 واحد لكل حدث. ألغِ التشغيل حين ينقطع اتصال العميل.
في المتصفح:
لواجهة محادثة بـ React فوق نقطة نهاية من هذا النوع، انظر React: يرسل 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: