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
لكل أداة مسموحة، مبني من مخطط مدخلها، ووصفها تعليقًا:
ما يستطيع البرنامج فعله وما لا يستطيع
الشيفرة هي متن دالة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يسمّي السبب؛ يحصل عليه النموذج ويتابع التشغيل.