lousho build ملف مواصفات الوكيل (انظر الإعدادات) إلى ناتج بناء قابل للنشر على منصة مستهدفة واحدة:
--out، والافتراضي .lousho/build/<target>)، والبناء (build) (التحزيم بـtsup)، والوصف (describe) (طباعة أمر تشغيل الناتج أو نشره). يجب أن يكون tsup مثبّتًا (npm install --save-dev tsup).
كل هدف يجيب عن
GET /health (200 ok) ويقدّم واجهة HTTP الكاملة أدناه: الجلسات، وبث SSE، والموافقات، والمصادقة برمز الوصول (bearer token).
مجلدات الوكيل
يقبلnode-server وdocker أيضًا مجلد وكيل بدل ملف المواصفات (يمكن أن يُمرَّر المسار وسيطًا موضعيًا أو عبر --agent):
server.ts المولَّد resolveAgentDir() على dist/agent ثم createDeployedServer(agent, { schedules, channels })، فتبدأ العملية الجداول الزمنية الخاصة بالمجلد حين تبدأ الاستماع، وتركّب قنواته تحت /channels (وهي تتحقق من طلباتها بنفسها؛ أما مسارات المحادثة فتبقى محمية برمز الوصول)، وتكتب في السجل schedules: ...; channels: .... تُحزَّم ملفات الشيفرة في المجلد مسبقًا في خطوة tsup ضمن البناء إلى dist/agent/**.js (بناء ESM واحد يتشارك نسخة واحدة من SDK، فالاستيراد from '@lousho/build-ai-agent' في أداة ما يشير إلى SDK نفسها التي يشغّلها الخادم)، ويُنسخ باقي المجلد؛ ولا شيء يحتاج إلى محمِّل TypeScript وقت التشغيل. ويصبح dist/ عندئذ ESM (وهذا ما يصرّح به dist/package.json). وتنسخه صورة Docker كما في السابق. البيئة المعزولة الاختيارية dockerode لا تُحزَّم (فهي تُحمَّل عند الحاجة فقط)، فالمجلد الذي يستخدم SubprocessSandbox يجب أن يثبّتها حيث يعمل. أما هدف Cloudflare Worker فما زال يقبل ملفات المواصفات فقط.
واجهة HTTP
كل هدف يقدّم بروتوكول/chat نفسه الذي يقدّمه lousho dev (واجهة سطر الأوامر)، ومن الشيفرة نفسها (src/server/fetchRoutes.ts المبنية على Fetch مباشرة، ويستدعيها خادم Node والـWorker معًا)، فالصفحة أو السكربت المكتوب للعمل مع خادم التطوير يعمل مع الخادم المنشور.
الأجسام التي تتجاوز 1MB تحصل على
413، و JSON غير الصالح يحصل على 400.
المصادقة
عيّنLOUSHO_API_TOKEN (وعلى هدف Worker، كسرّ: npx wrangler secret put LOUSHO_API_TOKEN) فيشترط كل مسار عدا /health الترويسة Authorization: Bearer <token>؛ وأي شيء آخر يحصل على 401 مع خطأ بصيغة JSON (يُقارَن الرمز في زمن ثابت). ومن دونه يكون الخادم مفتوحًا: لا بأس بذلك على 127.0.0.1، لكن اعتبر الرمز إلزاميًا لكل ما ليس على localhost (يسجّل الخادم تحذيرًا حين يستمع على واجهة شبكة أخرى بلا رمز، وصورة docker تستمع على جميع الواجهات). أنهِ اتصال TLS أمام الخادم، لأن رمز الوصول ينتقل نصًا صريحًا عبر HTTP غير المشفَّر. مرّر الرمز وقت التشغيل (docker run -e LOUSHO_API_TOKEN=...).
برمجيًا، يضمّن adapter.scaffold(agentPath, outDir, { auth: { token } }) رمزًا داخل خادم node-server أو docker المبني، يُستخدم حين لا يكون المتغير معيّنًا. الأولوية للمتغير، والرمز المضمَّن مقروء في dist/server.js، ففضِّل المتغير. أما Worker فيقرأ الرمز من ارتباطه (binding) LOUSHO_API_TOKEN فقط.
الجلسات والمخزن
يحدّدLOUSHO_STORE مكان حفظ الجلسات ونقاط الحفظ (checkpoints) والموافقات:
SQLite ملف واحد على قرص واحد، فشغّل نسخة واحدة فقط لكل ملف قاعدة بيانات.
node-server
الملف dist/server.js حزمة واحدة مكتفية بذاتها (تتضمن SDK واعتمادياتها)، فيعمل بلا npm install:
lousho dev، يرتبط الخادم بـ127.0.0.1 ما لم تختر صراحةً واجهة أخرى بـ--host=<h> (أو HOST=<h>)؛ والمنفذ يأتي من --port أو PORT، وإلا فالافتراضي 3000. وإشارتا SIGINT/SIGTERM تغلقان الوكيل قبل الخروج. تُقرأ بيانات اعتماد المزوّد من متغيرات البيئة نفسها المستخدمة في كل مكان آخر (OPENAI_API_KEY، …).
docker
يعيد استخدام هيكل node-server وحزمته ويضيف Dockerfile مبنيًا على node:22-slim ينسخ dist/ ويشغّل node dist/server.js على المنفذ 3000. تعيّن الصورة HOST=0.0.0.0، إذ يجب أن يستمع الخادم داخل الحاوية على جميع الواجهات ليصل إليه docker run -p. مرّر بيانات اعتماد المزوّد وقت التشغيل:
cloudflare-worker
يولّد Worker بصيغة الوحدة (export default { fetch }) ويحزّمه كوحدة ES لمنصة المتصفح؛ ويفشل البناء إذا انتهى أي استيراد node: إلى dist/worker.js. يشير wrangler.toml بـmain إلى dist/worker.js مع no_bundle = true، فتُرفع الحزمة المتحقَّق منها بعينها. وهو يُبنى ويعمل مع ai v4 (التثبيت الافتراضي) أو ai v7 (مع @ai-sdk/openai/@ai-sdk/anthropic v4) مثبّتة، بلا أي راية توافق (بلا nodejs_compat):
- المزوّدون:
mockوopenaiوanthropic. المزوّدانopenai/anthropicمبنيان علىgenerateText/streamTextمن VercelaiSDK مع@ai-sdk/openai/@ai-sdk/anthropic، وهي تنفيذات خالصة تعتمد علىfetch()ومعايير الويب، بلا أي استيرادnode:*في أي موضع من شجرة اعتمادياتها، فتُحزَّم وتعمل على Workers بلا مشكلات. أماollamaوopenrouterفغير مدعومين هنا:ollamaيستخدم افتراضيًا نقطة نهاية محليةhttp://localhost:11434لا يستطيع Worker الوصول إليها، وopenrouterلم يخضع بعد لتدقيق توافق مع Workers؛ استخدمnode-serverأوdockerلهما؛ - الأدوات:
current-dateوday-name. الأداةhttpغير مدعومة: فحمايتها من SSRF تستعلم عن عناوين اسم المضيف عبرnode:dnsوتفحص كل عنوان ناتج مقابل قائمة حظر قبل الاتصال (لسدّ ثغرة DNS rebinding)، ثم، في حالةvalidateSSL: false، تثبّت إعداد TLS ذاك لكل طلب عبرAgentمخصَّص منundici. والدالةfetch()الأصلية في Workers لا توفّر وسيلة مكافئة للاستعلام عن اسم المضيف مسبقًا وتثبيت الاتصال على عنوان IP المتحقَّق منه، فنسخة Workers من هذه الأداة لو بُنيت علىfetch()العادية لأسقطت تلك الحماية بصمت، لا أن تفقد وظيفة مريحة فحسب؛ ولذلك تُركت غير مدعومة بدل أن تُطرح أضعف تحت الاسم نفسه.
lousho build ملف المواصفات الذي يستخدم أي شيء آخر، بخطأ يسمّي المزوّد أو الأداة غير المدعومة. تُقرأ مفاتيح API للمزوّدين من ارتباطات Worker المسماة <TYPE>_API_KEY (مثل wrangler secret put OPENAI_API_KEY، wrangler secret put ANTHROPIC_API_KEY)، ويجب أن تكون الحزم النظيرة الخاصة بـopenai/anthropic (@ai-sdk/openai/@ai-sdk/anthropic، ai) مثبّتة إلى جانب @lousho/build-ai-agent ليتمكن lousho build من تحزيمها.
الارتباطات والجلسات وواجهة API على Workers
يقدّم Worker واجهة HTTP المذكورة أعلاه:GET /health، وPOST /chat مبثوثة بصيغة SSE (ReadableStream)، وGET /chat/:sessionId، ونقطة نهاية الموافقات، وجسم الطلب المُهمَل { "message" }. وله ارتباطان:
يُولَّد
wrangler.toml وفيه كتلة [[kv_namespaces]] الخاصة بـAGENT_CHECKPOINTS معطّلة بتعليق، مع أوامر إنشاء فضاء الأسماء (npx wrangler kv namespace create AGENT_CHECKPOINTS، ونسخة أخرى بـ--preview) وموضع لصق المعرّفات الناتجة. أزل التعليق عنها واملأ المعرّفات.
KVStore(kvBinding, { prefix?, ttl? }) (src/deploy/kvStore.ts) هو AgentStore الذي يبنيه Worker المُولَّد من الارتباط. وهو غير مُصدَّر من أي مدخل في الحزمة، ولذلك فاستيراده في Worker تكتبه بنفسك غير مدعوم بعد. مفاتيحه، مع prefix اختياري قبل كل منها:
الخيار
ttl: { sessions?, checkpoints?, approvals? } (بالثواني، ويقبل KV 60 فأكثر) يجعل كل نوع من القيود تنتهي صلاحيته بعد تلك المدة من آخر كتابة له؛ وافتراضيًا تبقى القيود حتى تُحذف.
الموافقة التي توقف دورة في طلبٍ ما يمكن أن يبتّ فيها طلب لاحق على isolate آخر: فالجلسة المحفوظة بنقطة حفظ تسمّي موافقتها المعلّقة، ونقطة نهاية الموافقات تواصلها من KV. وأحداث تلك التتمة لا تتضمن tool.done الخاص باستدعاء الأداة المبتوت فيه؛ ويحمل run.done فيها النص النهائي.
الصيغة المُهمَلة POST /chat { "message", "sessionId"? } تحتفظ بسلوكها السابق على Workers: مع sessionId تُحفظ للتشغيل نقطة حفظ في checkpoints/<sessionId> بعد كل نتيجة أداة، ويستعيدها طلب لاحق يعيد استخدام sessionId (بعد تعطّل أو إعادة تدوير isolate).
الاعتماديات النظيرة الاختيارية في بناءي node و docker
يحزّمdist/server.js الـSDK لكنه يُبقي اعتمادياتها النظيرة الاختيارية (مدخلات peerDependenciesMeta في package.json الخاص بها: حزم المزوّدين، وdockerode، و MCP SDK، وprompts، …) خارج الحزمة، فلا يحتاج أي بناء إلى اعتمادية لا تستخدمها. ثبّت، حيث يعمل الخادم، الاعتماديات النظيرة التي يحتاج إليها وكيله فقط (مثل @ai-sdk/openai لوكيل OpenAI)؛ ومسار الشيفرة الذي يحتاج إلى اعتمادية مفقودة يرمي خطأ SDK ذا الرمز الخاص بالاعتمادية النظيرة المفقودة. ومشغِّلات cron في ملف المواصفات تعمل على هدفي node-server و docker كما تعمل على Workers (الجداول الزمنية).
مشغِّلات cron وhandleScheduled
مشغِّلات cron في ملف المواصفات (triggers: [{ type: 'cron', cron: '0 9 * * MON', input: '...' }]) تصبح [triggers] crons = [...] في wrangler.toml، ويصدّر Worker المولَّد معالِج scheduled() يشغّلها كدورات للوكيل (الجلسة schedule:<name>، انظر الجداول الزمنية). يقيّم Cloudflare التعابير بتوقيت UTC وبدقة دقيقة واحدة؛ ويرفض البناء timezone، أو حقل الثواني، أو اختصارًا مثل @daily، أو يومًا من الأسبوع بصيغة رقمية (LOUSHO_SCHEDULE_INVALID).
في Worker تكتبه بنفسك، اربط وكيلًا معرَّفًا في الشيفرة باستخدام handleScheduled(agent, schedules, controller, ctx) (وهي مصدَّرة أيضًا من @lousho/build-ai-agent/deploy-runtime-worker). تشغّل هذه الدالة الجداول الزمنية التي يساوي cron فيها controller.cron داخل ctx.waitUntil() ولا ترمي خطأً أبدًا؛ وأدرج التعابير نفسها تحت [triggers] crons بنفسك:
التنفيذ المتين (التوقف/الاستئناف) على Workers
عمر الطلب في Worker أقصر من أن يناسبهCheckpointStore يعمل في الذاكرة أو على نظام الملفات (انظر الإعدادات / src/execution/checkpoint.ts لمعرفة ما هو CheckpointStore ولماذا يحتاج إليه التشغيل ليصمد أمام تعطّل أو توقف عند بوابة موافقة). AGENT_CHECKPOINTS هو ذلك المخزن، مبنيًا على KV (KVCheckpointStore)، والارتباط الواحد المذكور أعلاه هو كل ما يلزم لتفعيله.
لماذا KV، لا D1 ولا Durable Objects: الـCheckpoint كتلة JSON واحدة مفتاحها sessionId، تُقرأ وتُكتب كاملة؛ وهذا بالضبط الشكل الذي صُمّم له Workers KV، بلا أي بنية تحتية إضافية سوى ارتباط فضاء أسماء. D1 كان سيوفّر قدرة استعلام علائقية لا يحتاج إليها هذا المخزن أبدًا؛ و Durable Object كان سيوفّر اتساقًا صارمًا لكل جلسة مقابل تجهيز صنف DO وترحيله (migration) ودفع تكلفة كائن ذي حالة لكل جلسة. إذا كان حمل عملك يحتاج فعلًا إلى اتساق صارم من نوع القراءة بعد الكتابة (read-after-write) عبر المواقع الطرفية (انظر التحفّظ أدناه)، فإن CheckpointStore مبنيًا على Durable Object هو مسار الترقية الطبيعي: تنفيذ واجهة CheckpointStore نفسها (save/load/delete) على فضاء أسماء Durable Object بدل فضاء أسماء KV.
الاتساق النهائي، اقرأ هذا قبل الاعتماد عليه في سير عمل الموافقات: Workers KV مخزن متّسق اتساقًا نهائيًا (eventually consistent). عملية put() تظهر فورًا للموقع الطرفي الذي كتبها، لكن انتشارها إلى مواقع Cloudflare الطرفية الأخرى حول العالم قد يستغرق حتى نحو 60 ثانية. عمليًا يعني ذلك: إذا كُتبت نقطة حفظ جلسة في موقع طرفي ووصل بعد قليل طلب لاحق للـsessionId نفسه إلى موقع طرفي آخر، فقد يرى ذلك الطلب بيانات قديمة (نقطة حفظ أقدم، أو لا شيء) بدل ما كُتب للتو. وأكثر ما يهم ذلك في التوقفات عند بوابة الموافقة، حيث يكون التوقف والاستئناف اللاحق الذي تطلقه موافقة الإنسان طلبين منفصلين بطبيعتهما قد يصلان إلى موقعين مختلفين. هذه الـSDK لا تَعِد، ولا تستطيع أن تَعِد بالنظر إلى ضمانات KV، باتساق صارم من نوع القراءة بعد الكتابة هنا. إذا كان سير عمل الموافقات لديك لا يحتمل تلك النافذة الزمنية، فوجِّه طلبات الجلسة الواحدة إلى موقع Cloudflare واحد بنفسك (مثلًا عبر توجيه الطلبات المبني على Durable Object) أو استخدم مخزنًا قويّ الاتساق بدل AGENT_CHECKPOINTS/KV. والأمر نفسه ينطبق على الجلسات: طلبان لجلسة واحدة في اللحظة نفسها قد يكتب أحدهما فوق دورة الآخر، لأن عملية «قراءة ثم تعديل ثم كتابة» في KV ليست ذرّية.
المخازن المبنية على KV (KVStore وKVCheckpointStore وCHECKPOINT_KV_BINDING، في src/deploy/kvStore.ts وsrc/deploy/kvCheckpointStore.ts وsrc/deploy/checkpointBinding.ts) لا تتضمن أي إشارة إلى node:* في أي موضع من شجرة اعتمادياتها. يشغّل Worker ملف المواصفات كوكيل createAgent()، ويوجّه البناءُ استيراداته الخاصة بـNode وحدها (تعليمات المشروع، ومخزن الجلسات المبني على الملفات، ورقع حواجز الحماية، و MCP عبر stdio) إلى بديل (shim) يفشل عند استخدامه (src/deploy/shims/node.worker.ts). ثم تُفحص حزمة dist/worker.js المبنية بحثًا عن محدِّدات node: ومحدِّدات وحدات Node المدمجة المجرّدة ضمن lousho build، ويفشل البناء إذا وُجد أي منها. استثناء واحد: ai v7 و@ai-sdk/provider-utils v5 تبحثان عن node:module وnode:dns وnode:diagnostics_channel وnode:async_hooks وقت التشغيل بـprocess.getBuiltinModule()، فقط حين تكتشفان Node، وتعودان إلى fetch() (أو تتخطيان تتبّع القياس عن بُعد) في غير ذلك. وتُقبل هذه المعرّفات الأربعة وسيطًا لمثل هذا الاستدعاء ولا تُقبل في أي موضع آخر. الحزمة أكبر من Worker ذي الدورة الواحدة (نحو 1.7 MB خام و340 KB بضغط gzip لوكيل mock على ai v4؛ ونحو 3.4 MB خام و630 KB بضغط gzip على ai v7): يعرض lousho build حجمها، ويقارنه describe() بحد حجم السكربت لدى Cloudflare.
الأهداف المخصَّصة
الأهداف كائناتDeploymentAdapter (scaffold، build، describe) تُسجَّل بالاسم عبر registerAdapter()؛ وكلاهما مصدَّر من جذر الحزمة.