Skip to main content
مع وضع الشيفرة (code mode) يحصل النموذج على أداة إضافية واحدة هي run_code. فبدلًا من استدعاء الأدوات واحدة تلو أخرى، مع ذهاب وإياب إلى النموذج بعد كل منها، يكتب برنامج JavaScript قصيرًا يستدعي أدوات الوكيل كدوال غير متزامنة، ويكرّر ويرشّح ويجمع نتائجها، ويعيد قيمة واحدة. فتكلّف استدعاءات الأدوات الكثيرة ذهابًا وإيابًا واحدًا إلى النموذج، ولا تدخل النتائج الوسيطة الكبيرة إلى السياق: لا تدخله إلا القيمة التي يعيدها البرنامج. يعمل البرنامج في عزل (isolate) QuickJS على WebAssembly داخل عمليتك، لا في Docker. وكل استدعاء أداة يجريه يمرّ بالفحوص نفسها التي يمرّ بها استدعاء يجريه النموذج مباشرة: التحقق من الوسائط، والخطّافات، وقواعد الصلاحيات وأوضاعها، وحواجز الحماية، وneedsApproval.

متى تستخدمه

  • تحتاج الإجابة إلى عدة استدعاءات تعتمد مدخلاتها على نتائج سابقة (ابحث عن ثلاثة أسعار، ثم حوّل المجموع).
  • تعيد أداة أكثر بكثير مما يحتاج إليه النموذج (قائمة طويلة للترشيح، أو تقرير للعدّ).
  • يُجرى الاستدعاء نفسه على قائمة من العناصر.
أما للاستدعاء الواحد، أو حين تحتاج كل خطوة إلى تقدير النموذج، فاستدعاءات الأدوات المباشرة أبسط، والنموذج أجود فيها.

تفعيله

ثبّت الاعتمادية النظيرة الاختيارية quickjs-emscripten واضبط codeMode في createAgent():
برنامج قد يكتبه النموذج لهذا السؤال:
تعيد run_code الكائن { result, logs, toolCalls }: أي قيمة البرنامج المعادة، والأسطر التي سجّلها بـ console.log() (وبـ console.error() / warn()، مسبوقة بمستواها)، وعدد استدعاءات الأدوات التي أجراها. إذا لم تكن quickjs-emscripten مثبّتة، يفشل أول تشغيل لوكيل مفعَّل فيه codeMode بالخطأ MissingPeerDependencyError الذي يذكر اسم الحزمة وأمر التثبيت.

الخيارات

codeMode: true يستخدم القيم الافتراضية. ويمكن لكائن أن يضبط أيًّا مما يلي: بلا tools، يجوز للبرنامج استدعاء كل أدوات التشغيل ما عدا run_code وask_question وأدوات الوكلاء الفرعيين (task وagent_status وagent_await وagent_cancel وdelegate_to_*) وtool_search وload_skill والأدوات التي يؤجّلها البحث عن الأدوات. سمِّ أداة في tools لتسمح بها رغم ذلك: الأداة المؤجَّلة المسمّاة هناك يمكن استدعاؤها من البرامج ويظهر توقيعها في وصف run_code (فلا تعود محجوبة عن السياق في وضع الشيفرة)، بينما تبقى خارج قائمة أدوات النموذج نفسه إلى أن تحمّلها tool_search. يعرف النموذج ما يستطيع استدعاءه من وصف run_code: توقيع شبيه بـ TypeScript لكل أداة مسموحة، مبني من مخطط مدخلها، ووصفها تعليقًا:
ليس للنتائج نوع مصرَّح به، لذا حين يفشل برنامج يسرد الخطأ أول خمسة استدعاءات أدوات أجراها البرنامج مع وسائطها ونتائجها (يُقتطع كل منها عند 200 حرف). ويستطيع النموذج قراءة الأشكال هناك وإصلاح البرنامج.

ما يستطيع البرنامج فعله وما لا يستطيع

الشيفرة هي متن دالة async. يمكنها استخدام لغة JavaScript ومكوّناتها المضمَّنة (JSON وMath وDate وPromise والمصفوفات والخرائط والتعابير النمطية)، واستدعاء الأدوات، والتسجيل، وإعادة قيمة.
  • await tools.<name>(args) تنتهي بنتيجة الأداة، أو ترمي Error (name: 'ToolError') رسالته هي رسالة خطأ الأداة. ولا يحتوي tools إلا على الأدوات المسموحة ولا يمكن تعديله.
  • تعبر الوسائط والنتائج الحدّ نسخًا بصيغة JSON: فـ Date تصل نصًّا، وتُحذف الدوال والحقول undefined، وتعديل نتيجة داخل البرنامج لا يغيّر شيئًا خارجه.
  • Promise.all تشغّل الاستدعاءات في وقت واحد، بحد أقصى toolConcurrency منها (إعداد الوكيل) في البرنامج الواحد.
  • يجب أن تكون القيمة المعادة قابلة للتحويل إلى JSON؛ وundefined تصبح null.
لا يوجد require ولا import ولا process ولا fetch ولا نظام ملفات ولا شبكة ولا setTimeout ولا أي مؤقّت آخر، ولا أي كائن من المضيف. ولا يستطيع البرنامج بلوغ العالم الخارجي إلا عبر الأدوات الممنوحة له.

نموذج الأمان

  • العزل. يحصل كل برنامج على بيئة تشغيل وسياق QuickJS جديدين في WebAssembly، بلا شيء من المضيف فيهما سوى دالتين (استدعاء أداة، وسطر سجل) تسحبهما شيفرة الإعداد الخاصة بـ SDK من الكائن العام قبل أن يعمل البرنامج. ولا يُستخدم node:vm: فهو ليس حدًّا أمنيًا.
  • الحدود. يفرض العزل حدّ الذاكرة وحدّ المكدّس البالغ 512 KiB. ويفحص معالج مقاطعة الموعد النهائي أثناء حساب البرنامج، فتُوقَف الحلقة التي لا تنتهي عند timeoutMs (ولا يستطيع try / catch اعتراض هذا الإيقاف)، ويفحصه مؤقّت أثناء انتظاره أداة. وكثرة استدعاءات الأدوات توقف البرنامج أيضًا، حتى لو التقط الخطأ.
  • البوابة لكل استدعاء. كل tools.x(args) استدعاء أداة داخلي في التشغيل: يُتحقَّق منه مقابل مخطط الأداة، ثم تأتي خطّافات ما قبل الأداة وقواعد الصلاحيات ووضع الصلاحيات وحواجز حماية الأدوات وneedsApproval، ثم تعمل الأداة (عبر البيئة المعزولة للتشغيل إن كانت requiresSandbox) بمُنفِّذ (principal) التشغيل، ثم خطّافات ما بعد الأداة. والاستدعاء المرفوض يرمي خطأً في البرنامج مع سبب الرفض. وحاجز الحماية الذي يمنع استدعاءً داخليًا يوقف التشغيل كله، كما يفعل مع استدعاء مباشر.
  • وضع التخطيط. run_code معلَّمة للقراءة فقط: فهي لا تغيّر شيئًا بنفسها. وفي وضع التخطيط يُفحص كل استدعاء داخلي على حدة، فيستطيع البرنامج استدعاء أدوات القراءة فقط، أما استدعاء أي أداة أخرى فيرمي denied by plan mode.
  • الأخطاء. يصل خطأ الأداة إلى البرنامج رسالةً فقط، لا أثر مكدّس للمضيف أبدًا. والرموز التي حصلت عليها الأداة من ctx.getToken() تُحجب من نتيجتها، كما في الاستدعاء المباشر.
البرنامج الذي يحسب دون انتظار يعمل على خيط عمليتك: فيحجب حلقة الأحداث إلى أن ينتهي أو يوقفه timeoutMs. وعلى خادم يعالج طلبات أخرى، أبقِ timeoutMs منخفضًا.

الموافقات وتسجيل الدخول وحالات التوقف الأخرى

لا يستطيع البرنامج أن يتوقف. فعزل QuickJS لا يمكن حفظه واستئنافه لاحقًا، لذا فإن الاستدعاء الداخلي الذي كان سيوقف التشغيل يرمي خطأً في البرنامج بدلًا من ذلك، ولا يتوقف التشغيل: يستطيع النموذج بعدها استدعاء تلك الأداة مباشرة، فيتوقف التشغيل كالمعتاد. وقد تحتاج run_code نفسها إلى موافقة (مثلًا مع permissions: [ask('*')]): فيتوقف التشغيل قبل أن يعمل البرنامج، ويعمل البرنامج بعد البتّ في القرار، مع فحص كل استدعاء داخلي.

الأحداث والتتبّع

تُصدر الاستدعاءات الداخلية أحداث tool.start وtool.partial وtool.done وtool.error المعتادة، مع ضبط parentToolCallId على معرّف استدعاء run_code. ومعرّفاتها <run_code call id>:<n>، مرقّمة بترتيب استدعاءات البرنامج. ويأتي tool.done أو tool.error لكل استدعاء داخلي قبل الخاص بـ run_code نفسها: فحين ينتهي البرنامج (أو يُوقَف) وما زالت استدعاءات بدأها تعمل، يُلغى abortSignal الخاص بها وتنتظر run_code حتى تستقر. كما تنتج الاستدعاءات الداخلية أحداث permission.decision، وتراها الخطّافات بمعرّفاتها الخاصة. تذهب النتائج الداخلية إلى الأحداث وإلى البرنامج فقط. أما السجل والجلسة والنموذج فلا يحصلون إلا على نتيجة run_code وحدها. مع التتبّع، يكون لكل استدعاء داخلي مقطع تتبّع (span) خاص execute_tool، ابنٌ لمقطع run_code، مع السمة lousho.tool.parent_call_id.

القيود والمتانة

  • لا تقدّم جزئي. حالة البرنامج تعيش في العزل فقط. وبعد تعطل، يستدعي التشغيل المستأنف run_code من جديد فيعمل البرنامج كله مرة أخرى، بما فيه استدعاءات الأدوات التي كان قد أجراها. اجعل الأدوات التي يستدعيها البرنامج متساوية الأثر (idempotent) (قيمة ctx.toolCallId لكل استدعاء داخلي هي نفسها عند إعادة التشغيل: استخدمها مفتاحًا لتساوي الأثر)، أو استدعِ الأدوات ذات الآثار الجانبية مباشرة.
  • JavaScript فقط. يكتب النموذج JavaScript؛ ولا يُحوَّل TypeScript.
  • أين يعمل. تشغيلات وكيل createAgent(): send() وstream() والجلسات والاستئناف والموافقات، على Node. أما الهدف cloudflare-worker في lousho build فلا يضمّ QuickJS: وتشغيل مفعَّل فيه codeMode يفشل هناك عند بدايته. والوكيل الذي يبدأ وكيلًا فرعيًا (أداة task أو أداة التفويض) يعمل بلا وضع الشيفرة، وcodeMode الخاص بالوكيل الرئيسي لا يصل إلى أدوات الوكيل الفرعي.
  • الأخطاء نتائج. خطأ في البرنامج، أو انتهاء المهلة، أو بلوغ حدّ الذاكرة، أو كثرة استدعاءات الأدوات، أو قيمة معادة تتجاوز maxOutputChars، هو خطأ أداة في run_code يسمّي السبب؛ يحصل عليه النموذج ويتابع التشغيل.