Skip to main content

ملفات مواصفات الوكيل (AgentSpec)

الوكيل التصريحي (declarative) ملف YAML (.yaml/.yml) أو JSON (.json)، تتحقق منه loadSpec() بواسطة zod (src/spec/schema.ts). وهو الصيغة التي يشغّلها lousho dev وينشرها lousho build. الحقل الناقص أو غير الصالح يُفشل التحميل بخطأ يسمّي الحقل بعينه، مثل 'prompt': Required.

الأدوات التي يمكن لملف المواصفات الإشارة إليها

الأداتان github وjira تحتاجان إلى بيانات اعتماد ليس لها حقل في ملف المواصفات؛ والإشارة إليهما ترمي خطأً يطلب منك بناء الوكيل بـ createAgent() وتمرير أداة مضبوطة الإعدادات بدلًا من ذلك.

السياسة (policy)

تحوّل specToAgent() الحقل policy إلى خيارات لـ createAgent()، فيفرض ملف المواصفات ما يصرّح به. الحقول المعروفة تتحقق منها loadSpec()؛ والمفاتيح الأخرى يُحتفظ بها من أجل مولِّدات بيئات التشغيل (harness) الأخرى (Claude Code و Codex و Pi)، وهي تقرأ السياسة الخام.
أسماء حواجز الحماية (انظر حواجز حماية المدخلات والمخرجات): كل حاجز حماية يقبل أيضًا on: input أو output أو tools (وسائط استدعاء أداة)، أو قائمة منها. القيمة الافتراضية [input, output]. الاسم أو الخيار غير المعروف يُفشل التحقق، مع ذكر حواجز الحماية المتاحة واقتراح «هل تقصد» (“did you mean”): 'policy.guardrails.0': unknown guardrail 'deny-topic' (did you mean 'deny-topics'?). يوقف requiresApproval التشغيل مؤقتًا (finishReason: 'awaiting-approval') إلى أن تُستدعى agent.approvals.resolve()؛ وقبل هذا التغيير كان ملف المواصفات الذي يضبطه يشغّل أدواته دون أن يسأل. ويطبع lousho doctor agent.yaml سطرًا واحدًا لكل قسم من السياسة.

خوادم MCP (mcpServers)

يصرّح mcpServers بخوادم MCP التي يستخدمها الوكيل، في صورة خريطة من اسم الخادم (وهو يشكّل نطاق أسماء لأدوات ذلك الخادم) إلى خادم stdio (command، ومعه اختياريًا args وenv) أو خادم HTTP (url، ومعه اختياريًا headers). كل مُدخل يضبط واحدًا فقط من command / url؛ وargs/env يخصّان stdio وحده وheaders يخصّ HTTP وحده. تتحقق loadSpec() من الحقل، والمُدخل غير الصالح يفشل برسالة فيها اسم المُدخل، مثل 'mcpServers.files': AgentSpec validation failed: missing 'command' (stdio server) or 'url' (HTTP server). والحقل الاختياري approval (annotations أو always أو never) يحدد أيّ أدوات الخادم تطلب الموافقة؛ انظر الموافقة على أدوات MCP. ويتحقق lousho doctor من أن كل command لخادم stdio يمكن العثور عليه.
تمرّر specToAgent() الخوادم إلى createAgent({ mcpServers }) الموصوف في القسم التالي، فتحصل وكلاء lousho dev وlousho mcp على أدواتها.

ربط خوادم MCP (mcpServers، connectMcp())

يقبل createAgent({ mcpServers }) الخريطة نفسها. تتصل الخوادم عند await agent.ready() أو، تلقائيًا، عند أول send() / stream()؛ وتُضاف أدوات كل خادم بالصيغة <server>__<tool> (مثل docs__search). الخادم الذي يتعذر الاتصال به يُفشل ذلك الاستدعاء، والاستدعاء التالي يحاول من جديد. تقطع agent.close() الاتصال بها (وتوقف عمليات stdio)؛ وأي استدعاء أداة لاحق يعيد الاتصال. ودون mcpServers لا تفعل ready() وclose() شيئًا.
لمشاركة الخوادم بين عدة وكلاء، أو لاختيار طريقة معالجة الإخفاقات، استدعِ connectMcp(servers, options?) ومرّر tools الناتجة عنها بنفسك. فهي تتصل بكل خادم وتسرد أدواته قبل أن يُنجَز وعدها، فتكون الأدوات معروفة سلفًا. الخيارات:
  • onError: القيمة 'throw' (الافتراضية) ترفض الوعد حين يتعذر الاتصال بخادم، بعد إغلاق الخوادم الأخرى؛ والقيمة 'skip' تستبعد ذلك الخادم وتحذّر عبر logger.
  • lazy (الافتراضي true): بعد close() أو انقطاع الاتصال، يعيد استدعاء الأداة التالي الاتصال. ومع false يفشل ذلك الاستدعاء بدلًا من ذلك. سرد الأدوات يحتاج إلى اتصال، ولذلك لا يؤخّر lazy الاتصال الأول أبدًا.
  • logger: يتلقى تحذيرات الخوادم المتخطّاة والأدوات المتخطّاة (الافتراضي: لا شيء).
تُرجع { tools, close(), status() }؛ وstatus() تقرن كل خادم بإحدى القيم 'idle' أو 'connected' أو 'failed'.
تُشغَّل خوادم stdio بـ command وargs؛ ويُضاف env إلى البيئة الافتراضية (PATH وما شابه) ولا يحل محلها. أما خوادم HTTP فتستخدم النقل streamable HTTP مع headers في كل طلب. والحزمة @modelcontextprotocol/sdk اعتمادية نظيرة اختيارية: ثبّتها لاستخدام MCP.

الموافقة على أدوات MCP (approval)

تصف خوادم MCP كل أداة بتوصيفات (annotations) هي readOnlyHint وdestructiveHint وidempotentHint وopenWorldHint وtitle. وهي مجرد تلميحات، لكن الـ SDK يتخذها أساسًا للسلوك الافتراضي في الموافقات: الأداة التي لها readOnlyHint: true تُنفَّذ؛ والأداة التي لها destructiveHint: true، أو التي لا ترسل destructiveHint (الافتراضي في مواصفة MCP أنها مُتلِفة)، توقف التشغيل مؤقتًا إلى أن يوافق إنسان؛ و destructiveHint: false تُنفَّذ. وعليه فالأداة التي بلا توصيفات تطلب الموافقة. تبقى التوصيفات الخام في descriptor.metadata.mcp.annotations، ويصبح title هو displayName. اضبط approval في مُدخل الخادم (mcpServers، createAgent، connectMcp()) أو في loadMcpTools(client, name, { approval }):
  • 'annotations' (الافتراضي): كما سبق.
  • 'always' / 'never': اطلب الموافقة لكل الأدوات / لا تطلبها لأيٍّ منها.
  • دالة ({ name, annotations }) => boolean تقرر لكل أداة على حدة (name هو اسم الأداة المجرّد؛ وannotations تكون {} إذا لم يرسل الخادم شيئًا). متاحة في الشيفرة فقط؛ أما ملف المواصفات فيقبل السلاسل النصية الثلاث.
تُطبَّق قواعد الصلاحيات أولًا، ويبقى في وسعها أن تقرر allow أو deny أو ask.

أدوات MCP (Model Context Protocol)

يمكن أيضًا تحميل أدوات Client اتصلت به بنفسك يدويًا - اتصل بـ Client من @modelcontextprotocol/sdk بنفسك وحمّل أدواته بـ loadMcpTools()، ثم مرّر الناتج إلى createAgent() (أو سجّله في ToolRegistry):
الدالة loadMcpTools متاحة كذلك من جذر الحزمة ومن @lousho/build-ai-agent/tools.

تقديم وكيل عبر MCP

الدالة serveMcp() هي عكس loadMcpTools(): تعرض وكيلًا (ومعه، اختياريًا، بعض أدواته) في صورة خادم MCP، فيتمكن Claude Code و Cursor وغيرهما من عملاء MCP من استدعائه.
  • بلا حالة. كل استدعاء لأداة الوكيل محادثة جديدة.
  • الإلغاء. إلغاء طلب MCP يُلغي تشغيل الوكيل (agent.send(message, { signal })).
  • الأخطاء. فشل الوكيل يعود في صورة نتيجة MCP فيها isError: true.
  • الموافقات. الأدوات المشروطة بموافقة لا يمكن الموافقة عليها عبر MCP. التشغيل الذي يتوقف مؤقتًا بانتظار موافقة يُرجع isError: true مع رسالة تقول ذلك. والأدوات الموسومة بـ needsApproval لا تُعرَض مباشرة إلا إذا مرّرت allowApprovalTools: true؛ وإن فعلت، شغّلها العملاء دون أي رقابة بشرية.
  • stdio. لا يُكتب إلى stdout شيء سوى بروتوكول MCP؛ والتحذيرات تذهب إلى stderr.
  • HTTP. يرتبط بالعنوان 127.0.0.1 افتراضيًا. أضف auth: { type: 'bearer', token } لاشتراط الترويسة Authorization: Bearer؛ والارتباط بمضيف غير محلي (non-loopback) دون auth يسجّل تحذيرًا.

التوصيفات

يحمل tools/list توصيفات MCP ليعرف العملاء ما تفعله الأداة. الأداة التي تحتاج إلى موافقة يُعلَن عنها بـ readOnlyHint: false, destructiveHint: true؛ وحدّد التلميحات بنفسك عبر annotations (تُرسل كما هي حرفيًا). الأداة التي بلا تلميحات لا ترسل شيئًا، فيظل وكيل Lousho الذي يستهلك هذا الخادم يسأل قبل تشغيلها (انظر approval في connectMcp())؛ وreadOnlyHint: true تُنفَّذ دون سؤال. والأداة التي تحتاج إلى موافقة لا يُعلَن عنها أبدًا أنها للقراءة فقط. الأدوات المضمَّنة read_file وlist_dir وglob وgrep وtodo_read وcurrent_date وday_name للقراءة فقط.
من سطر الأوامر، يقدّم lousho mcp ملف مواصفات وكيل (عبر stdio افتراضيًا):
لاستخدامه من عميل MCP، أضفه إلى إعدادات MCP لدى العميل (مثل .mcp.json في Claude Code):

بيانات اعتماد المزوّدين

المزوّدون الحقيقيون تحدّدهم resolveProvider('<provider>/<model>') (وتُستخدم أيضًا لملفات المواصفات)، وهي تقرأ بيانات الاعتماد من متغيرات البيئة: المزوّد mock لا يحتاج إلى بيانات اعتماد ويُرجع ردودًا معدّة سلفًا؛ وهو ما تستخدمه الأمثلة ودليل البدء السريع افتراضيًا.

إعادة المحاولة والبديل الاحتياطي لدى المزوّد

تعيد createAgent() محاولة استدعاءات النموذج الفاشلة من تلقاء نفسها، ويمكنها اللجوء إلى نماذج أخرى كبديل احتياطي:
  • يقبل retry خيارات withRetry() المذكورة أدناه. وهو يسري على السلسلة النصية model (أو النموذج المختار من البيئة) وعلى كل مُدخل في fallbackModels. تبني createAgent هؤلاء المزوّدين مع تعطيل إعادة المحاولة الخاصة بـ SDK الـ ai (maxRetries: 0)، فتجري إعادة المحاولة في مكان واحد، ويُجري الافتراضي { maxRetries: 2 } العدد نفسه من الاستدعاءات كما في السابق.
  • نسخة provider التي تمرّرها تحتفظ بسلوكها الخاص في إعادة المحاولة؛ ولا تُغلَّف بـ withRetry() إلا إذا ضبطت retry. وتعمل fallbackModels معها أيضًا.
  • قيم fallbackModels سلاسل نصية بالصيغة provider/model، تُحدَّد مثل model عند إنشاء الوكيل. يشغّل الوكيل withFallback([withRetry(primary), withRetry(fallback1), ...]): كل استدعاء يبدأ بالنموذج الأساسي.
  • تُبلغ stream() وsession.stream() عن كل إعادة محاولة بحدث provider.retry وعن كل انتقال بحدث provider.fallback (انظر البث). أما send() فتُرجع النتيجة النهائية كما في السابق.
لبناء الشيء نفسه يدويًا، أو لمزوّدين تنشئهم بنفسك، تغلّف withRetry(provider, options) وwithFallback(providers, options) أي LLMProvider وتُرجعان مزوّدًا آخر، فيمكن تركيبهما معًا وتمريرهما في أي موضع يُقبل فيه مزوّد:
  • تعيد withRetry محاولة generate() وstream() (الافتراضي maxRetries: 2) عند تجاوز حدود المعدّل (rate limits)، وانقضاء المهلة، وأخطاء الشبكة، واستجابات 5xx، بالتصنيف نفسه الذي تستخدمه compactProviderError(). لا تُعاد المحاولة عند فشل المصادقة، ولا الطلبات غير الصالحة، ولا أخطاء طول السياق؛ ولا عند الإلغاء. وتلميح retryAfterMs من المزوّد (الترويسة Retry-After) يحل محل مدة التراجع (backoff). مرّر retryOn(error, attempt) لتغيير القاعدة، وtimeoutMs لتحديد مهلة لكل محاولة، وsignal لإيقاف إعادة المحاولة.
  • لا تُعاد محاولة استدعاء stream() إلا إذا رُفض وعده. والخطأ الذي يقع داخل بث أُرجع بالفعل لا تُعاد محاولته.
  • تجرّب withFallback المزوّدين واحدًا تلو الآخر بالترتيب، وترمي الخطأ الأخير من جديد إذا فشلوا جميعًا. افتراضيًا تنتقل إلى البديل الاحتياطي عند أي خطأ ما عدا الإلغاء (ويغيّر fallbackOn ذلك). كل بديل احتياطي يعمل بقيمة defaultModel الخاصة به. وname وdefaultModel يعكسان المزوّد الذي خدم آخر استدعاء.
  • تطبّق resilientProvider(provider, { maxRetries, timeout }) حقلَي LLMProviderConfig اللذين يحملان الاسمين نفسيهما.
  • المزوّدون المضمَّنون يمرّرون قيمة maxRetries من إعداداتهم (الافتراضي 2) إلى إعادة المحاولة الداخلية في SDK الـ ai، وهي تجري داخل كل محاولة من محاولات withRetry. أنشئ المزوّد الذي تغلّفه مع maxRetries: 0 (new OpenAIProvider({ apiKey, maxRetries: 0 })) لتجري إعادة المحاولة في مكان واحد. وتفعل createAgent() ذلك للنماذج التي تحدّدها هي.

خيارات createAgent()

إذا لم يُحدَّد model ولا provider، تحدّد createAgent() المزوّد من البيئة: LOUSHO_MODEL (سلسلة نصية بالصيغة 'provider/model') إن كان مضبوطًا، وإلا فأول مزوّد ضُبط متغيره، وتُفحص المتغيرات بهذا الترتيب: OPENAI_API_KEY (openai/gpt-4o-mini)، ثم ANTHROPIC_API_KEY (anthropic/claude-3-5-sonnet-latest)، ثم OPENROUTER_API_KEY (openrouter/openai/gpt-4o-mini)، ثم OLLAMA_BASE_URL (ollama/llama3). وإذا لم يكن أيٌّ منها مضبوطًا رمت خطأً يسرد بدقة الخيارات أو المتغيرات التي تحل المشكلة. أخطاء سوء الإعداد تبيّن طريقة إصلاحها: المفتاح الناقص يسمّي المتغير (createAgent: OPENAI_API_KEY is not set. ...)، والبادئة غير المعروفة تسرد البادئات المدعومة وتقترح أقربها، والاعتمادية النظيرة الاختيارية الناقصة تطبع أمر npm install بنصّه.

الميزانيات

يضع limits سقفًا لما يجوز للتشغيل أن ينفقه. اضبطه في createAgent() (لكل تشغيلات الوكيل) أو في AgentExecutor.execute() / stream(): تُفحص الحدود قبل كل استدعاء للنموذج (أي بعد كل دفعة أدوات) وبعد استدعاء النموذج الذي يطلب أدوات؛ كما يُلغي maxDurationMs استدعاء النموذج أو الأداة الجاري عبر إشارة التشغيل. يُعدّ الحد متجاوَزًا حالما يبلغه التشغيل. واستهلاك الوكلاء الفرعيين يُحتسب من ميزانية وكيلهم الرئيسي: يُضاف حين يعود الوكيل الفرعي، فيتوقف الوكيل الرئيسي قبل استدعائه التالي للنموذج. والتشغيل الذي ينتهي من تلقاء نفسه في الخطوة التي بلغت حدًّا يحتفظ بسبب انتهائه هو، كما في maxSteps. حين يُتجاوَز حد، يتوقف التشغيل بـ finishReason: 'budget-exceeded' مع result.budget ({ limit, value, max, scope }). استدعاءات الأدوات التي طلبها النموذج في تلك الخطوة تحصل على نتيجة «أُلغي» (“cancelled”)، فيبقى سجل المحادثة صالحًا، ومع مخزن نقاط حفظ يُحفظ في نقطة حفظ على أنه منتهٍ، مثل 'max-steps'. تُصدر stream() الحدث budget.exceeded قبل run.done. ومع onExceeded: 'throw' يُرفَض وعد التشغيل بالخطأ BudgetExceededError (LOUSHO_BUDGET_EXCEEDED، ومعه قيمة budget نفسها) بدلًا من ذلك.
حدود التشغيل وحدود الجلسة. يسري createAgent({ limits }) على كل تشغيل على حدة: كل send()، وكل دورة في جلسة، يبدأ من الصفر. ويضيف agent.session({ id, limits }) حدودًا تشمل كل دورات الجلسة: تتوقف الدورة حالما يبلغ ما أنفقته الجلسة، في دوراتها كلها، حدًّا (وعندها تكون budget.scope مساوية 'session')، والدورات اللاحقة تتوقف قبل استدعاء النموذج. ما أنفقته الدورات (الرموز والتكلفة والخطوات وزمن التشغيل) يُحفظ مع سجل المحادثة، في metadata.sessionUsage على آخر رسالة فيه، فالجلسة التي تُستكمل من مخزنها تحتفظ بميزانيتها. يسري النوعان معًا؛ وأول حد يُبلَغ يوقف الدورة. والدورة التي أُلغيت لا تُحتسب.

تعليمات المشروع

كثير من المستودعات يحفظ إرشادات لوكلاء البرمجة في ملف AGENTS.md (أو CLAUDE.md). يستطيع createAgent إلحاقه بتعليمات الوكيل:
يُضاف الملف بعد تعليماتك أنت تحت العنوان ## Project instructions (from AGENTS.md). ويُقرأ مرة واحدة، عند إنشاء الوكيل. يصعد البحث من cwd (الافتراضي process.cwd()) ويستخدم أقرب مجلد فيه أحد ملفات files (الافتراضي ['AGENTS.md', 'CLAUDE.md']، وأول تطابق يفوز)، ويتوقف عند أقرب مجلد يحتوي .git أو عند جذر نظام الملفات. المحتوى الذي يزيد على 32,000 حرف يُقتطع مع علامة تدل على الاقتطاع. وإذا لم يُعثر على ملف لم يُضَف شيء. هذه الميزة اختيارية (opt-in) عن قصد: قراءة الملفات من القرص افتراضيًا ستفاجئ من يضمّنون الـ SDK في خادم، حيث مجلد العمل ليس المشروع الذي يُعنى به الوكيل. وللعثور على الملف بنفسك (لعرضه مثلًا)، استخدم loadProjectInstructions({ cwd, files, stopAt, maxChars })، وهي تُرجع { path, content } أو undefined.

خيارات AgentExecutor.execute()

الصنف AgentExecutor ساكن (static): استدعِ AgentExecutor.execute(options). المطلوب فقط agent وinput وprovider. الخيارات الشائعة الاستخدام: يُنجَز وعدها بكائن ExecutionResult: { text, messages, toolCalls, usage, finishReason, steps, approvalId? }.

CLI

الأوامر lousho dev وlousho build وlousho mcp وسائر الأوامر، مع راياتها (flags)، موصوفة في CLI.