needsApproval فيتوقف التشغيل مؤقتًا قبل استدعائها، إلى أن
يوافق إنسان (أو شيفرتك) على الاستدعاء أو يرفضه. يُحفظ التوقف المؤقت في
ApprovalStore، فيمكن أن يأتي القرار بعد دقائق أو أيام، من طلب آخر
أو من عملية أخرى، ثم يتابع التشغيل من حيث توقف.
أي الاستدعاءات تتوقف مؤقتًا
قيمةneedsApproval هي true، أو دالة شرطية تستقبل الوسائط بعد التحقق
منها، وأنواعها مستمدة من input المعرَّف بـ zod للأداة:
needsApproval من تعليقات الأداة التوصيفية (annotations) على الخادم: readOnlyHint: true تُنفَّذ مباشرة، أما destructiveHint بقيمة true أو غير المحدَّدة (وهو افتراضي MCP) فتطلب الموافقة. اختر
السلوك لكل خادم عبر approval: 'annotations' | 'always' | 'never' أو دالة؛ انظر
موافقة أدوات MCP.
سياسات الصلاحيات
يضعpermissions قواعد للوكيل كله بدل تحديدها أداةً أداة: قائمة من قواعد
{ tool, when?, action, reason? } تُفحص بالترتيب عند كل استدعاء أداة
قبل needsApproval الخاص بالأداة. وأول قاعدة تنطبق هي التي تحسم:
allowتنفّذ الاستدعاء، دون موافقة حتى لو كانneedsApprovalسيطلبها. أماneedsApprovalالذي يمنع الاستدعاء فيبقى مانعًا له.denyلا تنفّذه. يتلقى النموذج خطأ أداة فيهkind: 'denied'وreasonالخاص بالقاعدة (انظر أخطاء الأدوات)، ويظهر في البث الحدثtool.error.askتوقف التشغيل مؤقتًا بانتظار الموافقة، تمامًا مثلneedsApproval(أو تسأل دالةapprove).
needsApproval الخاص بالأداة، كما كان من قبل.
tool اسم، أو قائمة أسماء، أو RegExp يُختبر على الاسم، أو
'*' لكل الأدوات. وwhen يحصر القاعدة في بعض الاستدعاءات: يستقبل
الوسائط بعد التحقق منها (بعد خطّافات preToolCall) و{ toolName, toolCallId, sessionId }، ويجوز أن يكون غير متزامن؛ وإذا رمى خطأً فشل الاستدعاء بذلك
الخطأ. الدوال allow(tools) وdeny(tools, reason?) وask(tools) تبني
القواعد الشائعة.
onPermissionDecision هو سجل التدقيق: يُستدعى مرة لكل استدعاء أداة
(عدا الاستدعاءات التي رُفضت أصلًا لعدم صلاحية وسائطها) مع
{ toolName, toolCallId, decision, rule?, args?, at }. وdecision هو
إجراء القاعدة المنطبقة أو 'default' حين لا تنطبق أي قاعدة، وrule هو
{ index, reason? } لتلك القاعدة، وat طابع زمني بصيغة ISO، ويُحذف args حين
يضبط التشغيل الخيار redactContent. ويتلقى البث المُدخَل نفسه في حدث permission.decision
(انظر البث). ولا يُنتَج أيٌّ منهما إلا حين يضبط الوكيل
permissions أو onPermissionDecision.
الخياران متاحان أيضًا في AgentExecutor.execute() / stream() وفي
resumeAfterApproval(). يرث الوكلاء الفرعيون قواعد الوكيل الرئيسي، وتُفحص
قبل قواعد الوكيل الفرعي نفسه، ويبلّغون قراراتهم إلى
onPermissionDecision الخاص بالوكيل الرئيسي.
الموافقة أو المنع أو السؤال
يمكن لدالةneedsApproval أن تُرجع أكثر من قيمة منطقية. تستقبل
الوسائط بعد التحقق منها و{ toolName, toolCallId, sessionId, messages }،
وتُرجع (أو تُنجَز بـ):
'ask'أوtrue: التوقف المؤقت بانتظار الموافقة، كما كان من قبل.'approve'أوfalse: تنفيذ الاستدعاء.'deny'أو{ deny: reason }: لا يُنفَّذ الاستدعاء ولا يتوقف التشغيل. يتلقى النموذج خطأ أداة فيهkind: 'denied'وreason، فيمكنه أن يجرّب شيئًا آخر؛ ويظهر في البث الحدثtool.error، ويسجّلonPermissionDecisionالقيمةdecision: 'deny'معreason(ومن دونrule).
always() (مثل true)،
وnever() (false) وonce(). تسأل once() في أول مرة تُستدعى فيها الأداة
في الجلسة؛ وبعد أن يوافق إنسان على استدعاء، تُنفَّذ الاستدعاءات اللاحقة لتلك الأداة في
الجلسة نفسها دون سؤال. الرفض لا يُحفظ، والجلسة
الجديدة تسأل من جديد. أما once({ per: 'args' }) فتحفظ الموافقات لكل أداة مع
وسائطها، فالاستدعاء بوسائط مختلفة يسأل من جديد. هذه الذاكرة
محفوظة في سجل المحادثة (رسالة tool للاستدعاء الموافَق عليه تحمل
metadata.approval)، فتُحفظ حيثما حُفظت الجلسة أو نقطة الحفظ أو
لقطة الموافقة، وتبقى سارية بعد الاستئناف في عملية أخرى. ومن دون
جلسة، كل send() سجل محادثة قائم بذاته. وإذا أزال ضغط السياق تلك الرسالة
عادت الأداة إلى السؤال.
permissions، قاعدة deny تغلب دائمًا، ولا يُستدعى needsApproval
الخاص بالأداة. وفيما عدا ذلك، 'deny' (أو { deny }) الصادر عن الأداة نفسها
يمنع دائمًا، أيًّا كانت القاعدة المنطبقة. قاعدة allow تحل محل سؤال الأداة (فتتجاوز
'ask' وtrue وonce())، وقاعدة ask توقف التشغيل مؤقتًا حتى لو كانت
الأداة ستوافق. وإن لم تنطبق أي قاعدة، سرت نتيجة needsApproval
الخاص بالأداة. ويعمل خطّاف preToolCall قبل هذا كله، ويمكنه أن يمنع الاستدعاء
أولًا (انظر نتائج الخطّافات). أدوات MCP
تحتفظ بقيمة needsApproval المستمدة من تعليقاتها التوصيفية (قيمة منطقية). ويقيّم الوكلاء الفرعيون
needsApproval لأدواتهم بالطريقة نفسها، وموافقة once()
الممنوحة عبر الوكيل الرئيسي تُحفظ لبقية مهمة ذلك الوكيل
الفرعي.
وكلاء createAgent()
send()المتوقف مؤقتًا يُنجَز (ولا يرمي خطأً) بـfinishReason: 'awaiting-approval'ومعهapprovalId.agent.approvals.list()تُرجع الاستدعاءات المعلّقة التي توقف عندها هذا الوكيل في هذه العملية، الأقدم أولًا:{ id, toolCallId, toolName, args, createdAt }، معsubagentPathحين يكون الاستدعاء تابعًا لوكيل فرعي.agent.approvals.resolve({ id, approved, note? })تنفّذ الاستدعاء (عند الموافقة) أو تعطي النموذج رفضًا معnoteالذي كتبته (عند الرفض)، ثم تتابع التشغيل، وتُنجَز بنتيجة التشغيل المتابَع، وقد يتوقف مؤقتًا من جديد. وترمي خطأً حين يكونidغير معروف أو سبق البتّ فيه.- التشغيل الذي توقف مؤقتًا داخل
agent.session()يتابع في تلك الجلسة: استدعاء الأداة ونتيجته والإجابة النهائية تنضم إلى سجل محادثة الجلسة. agent.stream()وsession.stream()ينتهيان عند التوقف المؤقت بحدثapproval.requestedثمrun.done('awaiting-approval')؛ ويُبتّ فيه بالطريقة نفسها.
InMemoryApprovalStore خاص بكل وكيل ما لم تمرّر
approvalStore أو store فيه approvals. وللبتّ في توقف مؤقت بعد إعادة
التشغيل، أعطِ الوكيل مخزنًا دائمًا، مثل مخزن SQLite (انظر
اختيار المخزن):
list() إلا التوقفات التي أحدثها كائن الوكيل هذا؛ فاحتفظ بـ
approvalId (أو اقرأ المخزن) للبتّ في توقف مؤقت من مكان آخر. ولا ينضم
التشغيل المتابَع إلى جلسة إلا حين يُبتّ فيه عبر الوكيل الذي
يملك كائن تلك الجلسة.
الاستئناف بوكيل تغيّر
تحمل لقطة الموافقة بصمة الوكيل المتوقف، والبتّ فيها بوكيل يختلف نموذجه أو أدواته أو تعليماته يُصدر تحذيرًا ('warn'، وهو
الافتراضي)، أو يُرفض بالخطأ LOUSHO_AGENT_DRIFT (onAgentDrift: 'error'، وتبقى
الموافقة معلّقة)، أو يمضي دون اعتراض ('ignore'). والاستدعاء الموافَق عليه الذي لم تعد
أداته موجودة يُرفض دائمًا بالخطأ LOUSHO_RESUME_TOOL_MISSING. انظر
الاستئناف بوكيل تغيّر.
عند استئناف استدعاء موافَق عليه تعمل خطّافات ما قبل الأداة من جديد، والوسائط التي
تتركها (سواء عبر { input } أو بتعديل ctx.args في موضعه) يجب أن تساوي
ما وُوفق عليه، بصرف النظر عن ترتيب المفاتيح؛ وإلا رُفض الاستدعاء بخطأ أداة
فيه kind: 'validation'. والخطّاف الذي يحجب أجزاءً من مدخلاته أو يوحّد صيغتها
ينبغي أن يعمل في التشغيل المتوقف أيضًا، فيوافق الإنسان على المدخلات بعد الحجب.
اتخاذ القرار في الشيفرة
مرّرapprove للبتّ في كل استدعاء لحظة ظهوره بدل التوقف المؤقت: true
تنفّذ الأداة، وfalse ترسل رفضًا إلى النموذج. يسري ذلك على send()
والجلسات وagent.approvals.resolve()؛ أما stream() فيبقى ينتهي عند التوقف المؤقت.
بث التشغيل المتابَع
agent.approvals.streamResolve(decision, { signal }) و
agent.approvals.streamAnswer({ id, answer }, { signal }) تتابعان التشغيل
مثل resolve() وanswer()، لكنهما تُرجعان AgentRun نفسه الذي
تُرجعه agent.stream() (انظر البث):
run.start، ثم tool.start وtool.done للاستدعاء الذي بُتّ فيه (وtool.error عند
الرفض)، ثم أحداث المتابعة حتى run.done. وrun.result
هو ما تُنجَز به resolve(). والمتابعة التي تتوقف مؤقتًا من جديد تنتهي بـ
approval.requested ثم run.done ('awaiting-approval')، حتى مع وجود
دالة approve، كما يفعل stream(). والتوقف المؤقت الذي حدث داخل جلسة
يتابع في تلك الجلسة، ويأتي run.done بعد حفظ سجل المحادثة.
طرح سؤال على المستخدم
createAgent({ askQuestion: true }) يعطي الوكيل الأداة المضمّنة
ask_question (معطّلة افتراضيًا؛ وaskQuestionTool() تُرجع الأداة نفسها
لتمرّرها في tools بنفسك). مدخلاتها هي
{ question: string; options?: string[]; allowFreeText?: boolean }. واستدعاؤها
يوقف التشغيل مؤقتًا عبر آلية الموافقات، فينتظر تمامًا كما تنتظر
الموافقة: في الجلسات، وفي المخازن الدائمة، وعبر إعادة التشغيل.
- يحمل السجل المعلّق
kind: 'question'وquestion: { text, options?, allowFreeText? }، فيagent.approvals.list()وفي دالةapproveوفي الحدثapproval.requested، فتستطيع واجهة المستخدم أن تعرض سؤالًا بدل زر موافقة. agent.approvals.answer({ id, answer })(وهي مثلresolve({ id, approved: true, note: answer })) تتابع التشغيل. يتلقى النموذج{ answer, option? }، حيثoptionهو فهرس العنصر المطابق فيoptions(دون تمييز بين حالة الأحرف). ومعallowFreeText: false، الإجابة الخارجة عن الخيارات تصل إلى النموذج خطأَ أداة.resolve({ id, approved: false, note? })تمتنع عن الإجابة: يتلقى النموذج خطأ أداة فيهkind: 'rejected'يفيد بأن المستخدم امتنع عن الإجابة.- يجوز لدالة
approveأن تُرجع نصًا للإجابة من الشيفرة، وهذا مفيد في الاختبارات والوكلاء المُبرمَجة ردودهم سلفًا.
allow على ask_question تتجاوز التوقف المؤقت، فيفشل
الاستدعاء بالرسالة “No answer”؛ فاترك الأداة على سلوكها الافتراضي.
AgentExecutor وresumeAfterApproval()
عند استخدام AgentExecutor.execute() مباشرة، مرّر approvalStore. عند استدعاء
يتطلب موافقة يحفظ المنفّذ ExecutionSnapshot بدل استدعاء
الأداة. استأنف لاحقًا، بعد إعادة تشغيل حقيقية إن شئت، عبر
resumeAfterApproval():
resumeAfterApproval(decision, store, registry, provider, options?, checkpointStore?)
تقبل معظم خيارات التنفيذ في execute() (مثل signal و
onAgentEvent وexporter). ومع sessionId + checkpointStore، تُسجَّل للتوقف المؤقت
نقطة حفظ أيضًا، ويرمي execute() بذلك الـ sessionId الخطأ
SessionAwaitingApprovalError إلى أن يُبتّ في الموافقة، فلا يمكن
تجاوز موافقة معلّقة (انظر التنفيذ المتين).
وstreamResumeAfterApproval() تقبل الوسائط نفسها وتبث
التشغيل المتابَع في AgentRun (انظر
البث بعد الموافقة).
في مواضع أخرى
- الوكلاء الفرعيون. الأداة التي تتطلب موافقة داخل وكيل فرعي توقف تشغيل الوكيل الرئيسي مؤقتًا؛
ويُبتّ فيها عبر
agent.approvalsالخاص بالوكيل الرئيسي. انظر الموافقات داخل وكيل فرعي. - أدوات مساحة العمل. أداة الصدفة (shell) تتطلب موافقة افتراضيًا. انظر أدوات مساحة العمل.
- MCP. الأدوات التي تتطلب موافقة لا يمكن الموافقة عليها عبر MCP؛ انظر تقديم وكيل عبر MCP.
- Agent Forge يعرض الموافقات المعلّقة بطاقاتٍ مضمّنة في محادثته؛ انظر Agent Forge.