Skip to main content
الوكيل الفرعي (sub-agent) وكيل مستقل، له تعليماته ونموذجه وأدواته ومهاراته، يسند إليه الوكيل الرئيسي مهمة مكتفية بذاتها. يبدأ الوكيل الفرعي بسياق نظيف (لا يرى إلا موجّه المهمة، ولا يرى محادثة الوكيل الرئيسي أبدًا)، وينفّذ ما يحتاج إليه من خطوات، ثم تعود إجابته النهائية إلى الوكيل الرئيسي كنتيجة أداة واحدة.

وكلاء فرعيون أم مهارات أم مسارات عمل؟

البدء السريع

أعطِ كل وكيل فرعي description (يقرؤه نموذج الوكيل الرئيسي ليختار)، ثم مرّر الوكلاء الفرعيين إلى الوكيل الرئيسي في subagents:
تقبل AgentExecutor.execute() الخيارين نفسيهما subagents وmaxSubagentDepth. استخدمها حين تحتاج إلى خطّافات أو تتبّع على تشغيل الوكيل الرئيسي، فالوكلاء الفرعيون يرثونها (انظر الوراثة). والموافقات تعمل مع createAgent() أيضًا (انظر الموافقات).

كيف يعمل

مع subagents يحصل الوكيل الرئيسي على:
  1. كتلة Available sub-agents في موجّه النظام: سطر لكل وكيل فرعي (- name: description)، وجملة تقول إن الوكيل الفرعي لا يرى إلا الموجّه الذي يُعطى له.
  2. أداة واحدة فقط، task، مدخلها { agent: <one of the names>, prompt: string, description: string, background?: boolean, taskId?: string, mode?: 'new' | 'resume' | 'fork' } (description تسمية من 3 إلى 5 كلمات تُستخدم في الأحداث والخطّافات؛ وtaskId وmode يتابعان مهمة سابقة، انظر متابعة مهمة).
  3. ثلاث أدوات للمهام الخلفية: agent_status وagent_await وagent_cancel (انظر الوكلاء الفرعيون في الخلفية).
حين يستدعي الوكيل الرئيسي task، يعمل الوكيل الفرعي المسمّى على prompt وحده. ونتيجة الأداة هي النص النهائي للوكيل الفرعي متبوعًا بتذييل صغير يستطيع الوكيل الرئيسي الاستفادة منه:
إذا لم يُكمل الوكيل الفرعي عمله (رمى خطأً، أو استنفد maxSteps، أو أُوقف) يحصل الوكيل الرئيسي على نتيجة خطأ (isError: true) تذكر السبب، مثل Sub-agent 'researcher' used all 10 of its steps (maxSteps) without giving a final answer. واسم وكيل غير معروف هو أيضًا نتيجة خطأ تسرد الأسماء الصحيحة. ويُضاف استهلاك الوكيل الفرعي من الرموز (tokens) إلى result.usage الخاص بالوكيل الرئيسي. أخطاء الإعداد تُرمى ومعها طريقة إصلاحها: وكيل فرعي بلا description، أو قيمة ليست وكيلًا من createAgent()، أو أداة من أدواتك تحمل أصلًا الاسم task (أو agent_status أو agent_await أو agent_cancel).

متابعة مهمة

كل استدعاء لـtask يعمل في محادثة فرعية لها taskId (task_1، task_2، … في التذييل). ويستطيع الوكيل الرئيسي العودة إليها: يعرف النموذج ذلك من وصف الأداة task، فطرح سؤال متابعة لا يحتاج إلى أي شيفرة. أما الوكيل الرئيسي الذي يحتفظ بعمله عبر الدورات أو عبر إعادة التشغيل فيحتاج إلى مخزن وجلسة:
أين تُحفظ المحادثات:
  • مع createAgent({ store }) (store.sessions) وتشغيل للوكيل الرئيسي له sessionId (استدعاء send(message, { sessionId }) تُحفظ له نقاط حفظ (checkpoints)، أو دورة في جلسة)، تُحفظ كل مهمة في store.sessions بعد تشغيلها، تحت مفتاح مشتق بالتجزئة (hash) من معرّف جلسة الوكيل الرئيسي ومن taskId. وهي تبقى بعد انتهاء تشغيل الوكيل الرئيسي: تستطيع متابعتَها الدوراتُ اللاحقة في الجلسة نفسها، ووكيلٌ رئيسي استُؤنف بـagent.resume() أو approvals.resolve()، وعمليةٌ جديدة تعمل على المخزن الدائم نفسه. والـtaskId القادم من جلسة وكيل رئيسي أخرى لا يطابق أبدًا. مخزن الجلسات لا يوفّر واجهة لسرد محتوياته، ولذلك لا تُسرد هذه المدخلات ولا تُحذف مع جلسة الوكيل الرئيسي؛ احذف بيانات المخزن للتخلص منها. ومع AgentExecutor.execute() عيّن المخزن بـwithSubagentOptions(subagents, { sessions }).
  • في غير ذلك (لا مخزن جلسات، أو تشغيل للوكيل الرئيسي بلا sessionId) تعيش المهمة في الذاكرة طوال ذلك الاستدعاء الواحد لـexecute() / send(): والتشغيل الذي يتوقف مؤقتًا بانتظار موافقة ثم يُستأنف يبدأ بلا أي مهمة.
لا تُحفظ إلا المهام التي انتهت (stop أو length أو max-steps)، فالمهمة التي فشلت أو أُلغيت لا يمكن متابعتها. وتصل الأخطاء إلى الوكيل الرئيسي كأخطاء أداة منظَّمة: الـtaskId غير المعروف، أو التابع لجلسة وكيل رئيسي أخرى، أو التابع لوكيل فرعي آخر، يفشل بـLOUSHO_SUBAGENT_TASK_NOT_FOUND؛ والمهمة التي ما زالت تعمل (مهمة خلفية لم تنتهِ) تفشل بـLOUSHO_SUBAGENT_TASK_BUSY، مع توجيه النموذج إلى استدعاء agent_await أو agent_cancel عليها أولًا. المهمة المستأنفة أو المتفرّعة هي في ما عدا ذلك استدعاء task عادي: يمكن أن تعمل في الخلفية (وتحتفظ بـtaskId نفسه)، والموافقة داخلها توقف الوكيل الرئيسي مؤقتًا وتستأنفه كأي موافقة لوكيل فرعي، واستهلاكها يُحتسب ضمن usage وlimits الخاصين بالوكيل الرئيسي. ويرى الوكيل الفرعي سجل محادثته كاملًا من جديد عند كل استئناف، فالمهمة طويلة العمر تكلّف رموزًا أكثر في كل مرة.

المهام المتوازية

استدعاءات الأدوات في دورة نموذج واحدة تعمل بالتزامن (انظر toolConcurrency)، فحين يستدعي الوكيل الرئيسي task عدة مرات في دورة واحدة يعمل أولئك الوكلاء الفرعيون بالتوازي. وتصل النتائج إلى سجل محادثة الوكيل الرئيسي بالترتيب الذي أجرى به النموذج الاستدعاءات. ضع سقفًا لذلك بـtoolConcurrency على الوكيل الرئيسي (القيمة 1 تشغّلهم واحدًا تلو الآخر).

الوكلاء الفرعيون في الخلفية

مع background: true تبدأ task الوكيل الفرعي وتعود فورًا بـ{ taskId, status: 'running', agent }، فيستطيع الوكيل الرئيسي مواصلة عمله (أو بدء مهام أخرى) أثناء تشغيله. ثم يستخدم الوكيل الرئيسي: يعمل في الوقت نفسه maxConcurrent وكيلًا فرعيًا خلفيًا على الأكثر لكل تشغيل للوكيل الرئيسي (الافتراضي 3)؛ والاستدعاءات الإضافية بـbackground: true تحصل على status: 'queued' وتبدأ كلما شغر مكان. عيّن القيمة عبر subagentOptions في createAgent():
مع AgentExecutor.execute()، أرفق الخيارات نفسها بقيمة subagents باستخدام withSubagentOptions(subagents, options) التي تعيد تلك القيمة. وفي createAgent() تتقدّم subagentOptions على الخيارات المرفقة بتلك الطريقة. يرث الوكيل الفرعي الخلفي من تشغيل الوكيل الرئيسي كما يرث المتزامن (الخطّافات، والتتبّع، ومستمعو الأحداث، وتجميع الاستهلاك)، وينطبق maxSubagentDepth بالطريقة نفسها. وإيقاف إشارة الوكيل الرئيسي (abort) يلغي وكلاءه الفرعيين الخلفيين، سواء كانوا في قائمة الانتظار أو قيد التشغيل.

عند انتهاء تشغيل الوكيل الرئيسي

المهام الخلفية تتبع تشغيلًا واحدًا (استدعاء واحد لـexecute() / send() / stream()). وحين ينتهي ذلك التشغيل، أيًّا كانت طريقة انتهائه، تُلغى افتراضيًا المهام التي ما زالت في قائمة الانتظار أو قيد التشغيل (يُوقَف تشغيلها). أما مع awaitBackgroundOnFinish: true فينتظر التشغيل انتهاءها قبل أن يكتمل (وتشغيل الوكيل الرئيسي الذي أُوقف أو فشل يلغيها مع ذلك). وفي الحالتين لا يكتمل التشغيل إلا بعد أن تتوقف تشغيلاتها، فلا يصل أي حدث لوكيل فرعي إلى مستمعي التشغيل بعد اكتماله؛ ومع awaitBackgroundOnFinish تصل أحداثها قبل حدث run.done الخاص بالوكيل الرئيسي. تسرد النتيجة كل مهمة خلفية في التشغيل مع حالتها النهائية:
يحتوي result.backgroundTasks على { taskId, agent, status, elapsedMs, error?, approvalId?, toolName? } لكل مهمة (بنية agent_status نفسها)، ويغيب حين لا يبدأ التشغيل أي مهمة خلفية. استخدم agent_await في الوكيل الرئيسي (وموجّه النظام يطلب منه ذلك) لإدخال إجاباتها في المحادثة. خطّاف المنفِّذ الذي يقف وراء ذلك متاح للاستخدام العام: يُستدعى ExecuteOptions.onRunEnd مرة واحدة بالضبط لكل تشغيل، مع { result } (أيًّا كان finishReason) أو { error }؛ ويُحسم التشغيل بعد عودته. وتُدرج المهام الخلفية في result قبل أن يراه onRunEnd الذي كتبته. القيود:
  • تشغيل الوكيل الرئيسي الذي يتوقف مؤقتًا بانتظار موافقة (finishReason: 'awaiting-approval') ينتهي عند تلك النقطة: تُلغى مهامه الخلفية (أو يُنتظر انتهاؤها) كما في أي نهاية أخرى، ويبدأ التشغيل المستأنف بلا أي مهمة. فالتشغيل المستأنف تشغيل مستقل لا صلة له بمهام التشغيل المتوقف (وقد يعمل في عملية أخرى)، ولذلك لا يمكن إبقاؤها حيّة خلال التوقف.
  • الوكيل الفرعي الخلفي الذي يحتاج إلى موافقة يتوقف بـstatus: 'awaiting-approval' (مع approvalId وtoolName الخاصين بالوكيل الفرعي)؛ فلا يُنفَّذ استدعاؤه، ولا يمكن استئنافه عبر مخزن الموافقات. شغّل تلك المهمة بشكل متزامن (background: false) إذا كان من المحتمل أن تحتاج إلى موافقة.

الفهارس الديناميكية

بدلًا من كائن ثابت (record)، مرّر فهرسًا (catalog) فيه list() وresolve(name). تُستدعى list() عند بداية كل تشغيل يعرض الأداة task، فيمكن أن تتغير المجموعة بين تشغيل وآخر؛ وتُستدعى resolve() حين يختار الوكيل الرئيسي اسمًا (أعِد undefined للاسم غير المعروف).

الوكلاء الفرعيون عن بُعد

تستخدم remoteAgent() وكيلًا سبق أن نشرته (lousho deploy: خادم node أو Docker أو Cloudflare Worker) بوصفه وكيلًا فرعيًا. يوضع في subagents إلى جانب الوكلاء المحليين، ويفوّض إليه الوكيل الرئيسي بالأداة task نفسها، بما في ذلك background: true.
تفتح كل مهمة جلسة جديدة على الوكيل البعيد (POST <url>/chat مع { sessionId, input }، وتُقرأ الاستجابة كتدفق أحداث SSE، انظر النشر)، وترسل موجّه المهمة وتعيد النص النهائي للوكيل البعيد (أو كائنه بصيغة JSON حين يكون للوكيل المنشور مخطط output، انظر المخرجات المنظَّمة)، متبوعًا بتذييل يسمّي الجلسة وtaskId. واستدعاء task الذي يستأنف ذلك الـtaskId يرسل إلى الجلسة البعيدة نفسها، فيواصل الوكيل البعيد بسجل محادثته الخاص (احفظه في مخزن على الطرف البعيد)؛ ولا يمكن تفريع مهمة بعيدة. لا يرى الوكيل البعيد إلا ذلك الموجّه، ويعمل بنموذجه وأدواته وحدوده الخاصة، فبيئة تشغيل الوكيل الرئيسي (الخطّافات، والبيئة المعزولة، ومخزن الموافقات) لا تصل إليه. وإشارة الإيقاف الخاصة بتشغيل الوكيل الرئيسي توقف الطلب. الخيارات: url وauth وname وdescription وheaders وfetch (احقن دالة fetch خاصة بك في الاختبارات؛ فمسارات serveFetch() لوكيل يعمل داخل العملية نفسها تصلح نشرًا زائفًا). حالات الفشل (تعذّر الوصول إلى الوكيل، أو رد 401 أو أي رد آخر خارج نطاق 2xx، أو تدفق مشوَّه، أو تشغيل بعيد ينتهي بخطأ) تصل إلى الوكيل الرئيسي كخطأ أداة منظَّم برمز الخطأ LOUSHO_REMOTE_REQUEST_FAILED (وLOUSHO_REMOTE_UNAUTHORIZED في حالة 401)؛ ولا يكون رمز الوصول جزءًا من أي خطأ أو حدث أبدًا.

الموافقات عن بُعد

إذا توقف التشغيل البعيد مؤقتًا بانتظار موافقة، يتوقف تشغيل الوكيل الرئيسي عندها تمامًا كما في حالة الوكيل الفرعي المحلي: تكون قيمة result.finishReason هي 'awaiting-approval'، والموافقة المعلّقة في agent.approvals.list() (وفي أزرار القنوات، ومحادثة التطوير، وطلبات الصلاحية في ACP) تحمل اسم الأداة البعيدة ومدخلها، مع subagentPath: ['researcher']. واستدعاء ask_question على الطرف البعيد يصل كسؤال (kind: 'question') تجيب عنه agent.approvals.answer(). والبتّ فيها على الوكيل الرئيسي (resolve أو streamResolve أو answer) يرسل القرار إلى مسار الموافقات لدى الوكيل البعيد (POST <url>/chat/<session>/approvals/<id>)، ويقرأ تتمة التشغيل، وتصبح إجابته النهائية نتيجة task؛ والتتمة التي تتوقف مرة أخرى توقف الوكيل الرئيسي مرة أخرى.
لا تحتفظ لقطة الموافقة (snapshot) لدى الوكيل الرئيسي إلا بمعرّف الجلسة البعيدة، ومعرّف الموافقة البعيدة، وtaskId، واسم الوكيل الفرعي، فتستطيع عملية جديدة للوكيل الرئيسي تعمل على المخزن نفسه أن تبتّ فيها؛ ولا يُخزَّن رمز الوصول (bearer token) أبدًا (بل يُقرأ من خيارات remoteAgent() من جديد). والفشل أثناء البتّ (رد 401، أو أي رد آخر خارج نطاق 2xx مثل رد 404 من الطرف البعيد على موافقة لم تعد معلّقة، أو خطأ في الشبكة) هو خطأ الأداة المنظَّم لذلك الاستدعاء لـtask برموز الخطأ المذكورة أعلاه، ويواصل تشغيل الوكيل الرئيسي عمله. والتشغيل الذي بلا مخزن موافقات (AgentExecutor مجرّد) هو وحده الذي ما زال يُفشل المهمة بـLOUSHO_SESSION_AWAITING_APPROVAL، مع ذكر الجلسة البعيدة ومعرّف الموافقة ليُبتّ فيها على الوكيل البعيد.

العمق

يحدّ maxSubagentDepth (الافتراضي 1) من عمق تداخل الوكلاء الفرعيين. بالقيمة الافتراضية يستطيع الوكيل الرئيسي استدعاء وكلاء فرعيين، لكنهم لا يستطيعون استدعاء وكلاء فرعيين خاصين بهم: فالتشغيل الذي بلغ الحد لا تُعرض عليه الأداة task أصلًا، حتى لو أُنشئ مع subagents. والقيمة maxSubagentDepth: 2 تتيح للوكلاء الفرعيين التابعين للوكيل الرئيسي أن يفوّضوا مرة إضافية. قيمة التشغيل في المستوى الأعلى تنطبق على الشجرة كلها؛ أما maxSubagentDepth الخاص بوكيل فرعي فلا أثر له إلا حين يعمل هذا الوكيل في المستوى الأعلى.

ما يرثه الوكيل الفرعي

يحتفظ الوكيل الفرعي بتعليماته، ونموذجه/مزوّده، وأدواته، ومهاراته، وmaxSteps الخاص به، وسياقه معزول. ويرث من التشغيل الذي استدعاه: أبناء createDelegateTool() يرثون بالطريقة نفسها.

الخطّافات داخل الوكلاء الفرعيين

تنطبق خطّافات الأب على وكلائه الفرعيين افتراضيًا. ويُعلِم ctx.subagent الخطّافَ بأنه يعمل داخل وكيل فرعي: فيه name اسم الوكيل الفرعي، وdepth عمقه (1 لوكيل فرعي تابع لتشغيل المستوى الأعلى)، وtoolCallId الخاص بالوكيل الرئيسي الذي بدأه، والوكيل الفرعي المحيط به في parent عند تداخل أعمق. والخطّاف الذي ينبغي ألا يرى إلا تشغيل المستوى الأعلى يعود مبكرًا:

الأحداث

استدعاء agent.stream() / AgentExecutor.stream() على الوكيل الرئيسي يبثّ تشغيل كل وكيل فرعي داخل التدفق نفسه (الخطوات، وأجزاء النص المتتابعة، وأحداث الأدوات، والأخطاء) مع حقل subagent في كل حدث من أحداثه، فتستطيع واجهة المستخدم أن تعرض نشاط الوكيل الفرعي متداخلًا تحت استدعاء task الخاص بالوكيل الرئيسي (subagent.toolCallId). انظر البث: الوكلاء الفرعيون. المستمع المسجَّل على الوكيل الرئيسي (createAgent({ onEvent }) أو onAgentEvent) يتلقى الأحداث نفسها، ومنها أحداث الوكلاء الفرعيين، لحظة وقوعها. انظر الاستماع من دون المرور على التدفق.

الموافقات داخل وكيل فرعي

حين يستدعي وكيل فرعي أداة لها needsApproval، يتوقف التشغيل كله مؤقتًا: يكتمل execute() الخاص بالوكيل الرئيسي بـfinishReason: 'awaiting-approval' ومعه approvalId، ويحتفظ مخزن الموافقات بقيد واحد فقط، استدعاؤه المعلّق هو استدعاء الوكيل الفرعي (toolName، args) مع subagentPath يسمّي الوكلاء الفرعيين الذين يعمل داخلهم (مثل ['researcher']). وافق عليه أو ارفضه بـresumeAfterApproval()، مع تمرير خيار subagents نفسه الذي استخدمه التشغيل المتوقف:
الاستئناف ينفّذ الاستدعاء المعلّق للوكيل الفرعي (أو يرفضه)، ويدع الوكيل الفرعي يُكمل عمله، ويعيد إجابته النهائية إلى الوكيل الرئيسي نتيجةً لـtask، ثم يواصل الوكيل الرئيسي. وإذا احتاج الوكيل الفرعي إلى موافقة أخرى، يتوقف التشغيل مرة أخرى بـapprovalId جديد. يعمل ذلك عند أي عمق، ومع أبناء createDelegateTool() (مرّر السجل (registry) الذي يحتوي أداة التفويض). مع وكيل رئيسي من createAgent()، يتوقف lead.send() بالطريقة نفسها ويستأنفه lead.approvals.resolve({ id, approved }) (فالوكيل الرئيسي يعرف subagents الخاصة به مسبقًا)؛ وخيار approve على الوكيل الرئيسي يبتّ في استدعاءات وكلائه الفرعيين أيضًا. الضمانات والقيود:
  • لا يُنفَّذ أي استدعاء مرتين ولا يضيع أي استدعاء: تُخزَّن حالة الوكيل الفرعي المتوقف داخل قيد الموافقة الخاص بالوكيل الرئيسي (JSON عادي، فيصلح أي ApprovalStore)، ويُنفَّذ الاستدعاء الموافَق عليه مرة واحدة بالضبط، عند الاستئناف.
  • استدعاءات الأدوات الأخرى التي اكتملت في دورة الوكيل الرئيسي نفسها تحتفظ بنتائجها ولا تُنفَّذ من جديد.
  • تكون موافقة واحدة فقط معلّقة لكل تشغيل. إذا توقف وكيلان فرعيان في الدورة نفسها، يتوقف التشغيل عند الأول (بترتيب الاستدعاء)؛ ويتوقف الوكيل الفرعي الثاني ويحصل الوكيل الرئيسي على نتيجة خطأ لذلك الاستدعاء لـtask تفيد بأن استدعاءه لم يُنفَّذ، فيستطيع أن يطلبه من جديد بعد الموافقة.
  • خطّافات ما قبل الأداة وما بعدها الخاصة باستدعاء task تُطلَق من جديد عند الاستئناف، كخطّافات أي أداة موافَق عليها.
  • تحتاج الموافقات إلى approvalStore على تشغيل الوكيل الرئيسي، وهو خيار لا تقبله createAgent() بعد؛ استخدم AgentExecutor.execute() للوكيل الرئيسي. ومن دونه يصبح استدعاء الوكيل الفرعي نتيجة خطأ ولا يُنفَّذ شيء.
  • يستمر تراكم استهلاك الرموز عبر التوقف: استهلاك الوكيل الفرعي قبل التوقف موجود في نتيجة التشغيل المتوقف، وما ينفقه بعد الاستئناف يُضاف إلى result.usage الخاص بالوكيل الرئيسي المستأنف.

createDelegateTool()

تغلّف createDelegateTool({ agent, provider, toolRegistry }) وكيلًا ابنًا واحدًا في أداة تسمّيها وتسجّلها بنفسك، مع حارس maxDepth خاص بها. وهي تعمل على نواة التفويض نفسها التي تعمل عليها task، فترث بيئة تشغيل الأب بالطريقة نفسها، وبنية نتيجتها ({ text, usage }) لم تتغير. فضِّل subagents في الشيفرة الجديدة: أداة واحدة، وسرد للوكلاء في الموجّه، ومهام متوازية، وحدود للعمق، كلها تأتي بلا جهد إضافي.