Skip to main content
كيف تترابط المكوّنات:

بناء الوكلاء وتشغيلهم

الإعداد الديناميكي

كلٌّ من model وinstructions (أو prompt) وtools في createAgent() يقبل القيمة الثابتة أو دالة تأخذ سياق التشغيل، (ctx) => value | Promise<value> (النوع PerRun<T>). وctx هو { sessionId?, input, metadata? } (النوع RunConfigContext): أي sessionId المُمرَّر إلى send() / stream() أو معرّف agent.session()، ومدخلات المستخدم في هذا التشغيل، وخيار الاستدعاء metadata في send() وstream() وsession.send() وsession.stream(). تُنفَّذ هذه الدوال مرة واحدة عند بدء التشغيل، قبل أول استدعاء للنموذج، ثم مرة أخرى في كل دورة من دورات الجلسة. وكل ما عدا ذلك يُطبَّق على ما تعيده: fallbackModels وretry وprojectInstructions والذاكرة والمهارات والوكلاء الفرعيون وأدوات MCP والصلاحيات والموافقات وحواجز الحماية. والوكيل الديناميكي المستخدَم وكيلًا فرعيًا تُحسَب إعداداته باعتبار موجّه المهمة هو input.
الدالة التي ترمي خطأً تُفشل التشغيل بالخطأ LOUSHO_CONFIG_RESOLVER_FAILED (يحمل error.field اسم الخيار، وerror.cause هو الخطأ المرمي): يُرفَض وعد send()، وينتهي stream() بحدث error، وتحتفظ الجلسة بسجل محادثتها كما كان. والتشغيل المتوقف مؤقتًا بانتظار موافقة أو سؤال يحتفظ في لقطة التوقف بـ ctx وبالنموذج الذي انتهت إليه الدالة: فاستئنافه، ولو من عملية أخرى، يستخدم ذلك النموذج (لا يتبدّل النموذج أبدًا في منتصف الدورة) ويستدعي دالة tools من جديد بالـ ctx نفسه. أما التشغيل المستأنَف من نقطة حفظ بعد انهيار (agent.resume(id)) فتُحسَب إعداداته من جديد مع input: [] ومن دون metadata. وإذا كانت القيم كلها ثابتة فلا يتغير شيء: يُبنى الوكيل مرة واحدة عند إنشائه.

الربط بواجهات المستخدم

يصدّر @lousho/build-ai-agent/react الدالة useLoushoAgent(source, options?)، وهي خطّاف React يشغّل وكيلًا داخل العملية نفسها ({ agent, sessionId? }) أو عبر HTTP ({ url }) ويعيد messages وstatus وpendingApproval وsend() وstop() وapprove() وreject(). وتُصدَّر كذلك أجزاؤه المستقلة عن أي إطار عمل، reduceAgentEvents() وparseEventStream(). انظر React. ويصدّر @lousho/build-ai-agent/vue الدالة useLoushoAgent() نفسها في صورة composable لـ Vue 3، مع الحالة في صورة refs. انظر Vue. ويصدّر @lousho/build-ai-agent/svelte الدالة loushoAgent()، وهي الحالة والإجراءات نفسها في صورة مخزن Svelte ($agent). انظر Svelte.

الوكلاء الفرعيون

مرِّر subagents: { researcher, writer } (وكلاء من createAgent() لكل منهم description، أو فهرسًا بصيغة { list, resolve }) إلى createAgent() أو AgentExecutor.execute(): فيحصل الوكيل الرئيسي على أداة task واحدة وعلى قائمة بالوكلاء الفرعيين في موجّهه، ويعمل كل وكيل فرعي على موجّه المهمة وحده، ويرث من تشغيل الوكيل الرئيسي الإشارة (signal) والخطّافات (ctx.subagent) والتتبّع ومخزن الموافقات ومستمعي الأحداث (event.subagent). ويحدّ maxSubagentDepth (الافتراضي 1) من عمق التداخل. انظر الوكلاء الفرعيون.

الموافقات

الوكيل المُنشأ بـ createAgent() يتوقف مؤقتًا عند أداة تحمل needsApproval بدل أن يفشل: يُحَلّ send() (وكذلك session.send()) بـ finishReason: 'awaiting-approval' ومعه approvalId. يعيد agent.approvals.list() الاستدعاءات المعلّقة، أما agent.approvals.resolve({ id, approved, note? }) فينفّذ الاستدعاء أو يرفضه ثم يُحَلّ بنتيجة التشغيل بعد متابعته (مواصلًا الجلسة التي توقف فيها). تُحفَظ حالات التوقف في InMemoryApprovalStore خاص بكل وكيل ما لم تمرِّر approvalStore (مثل SqliteStore.approvals) أو store؛ والخيار approve: (call) => boolean | string يبتّ في كل استدعاء برمجيًا من دون توقف (لكنّ stream() يظل ينتهي عند نقطة التوقف). ومع askQuestion: true يستطيع الوكيل أن يطرح سؤالًا على المستخدم (kind: 'question')، ويُجاب عنه بـ agent.approvals.answer({ id, answer }). انظر الموافقات.

المخرجات المنظَّمة

مرِّر output: zodSchema إلى createAgent() (أو AgentExecutor.execute() / stream()): عندها يجب أن يكون الرد النهائي كائن JSON مطابقًا للمخطط، ويُحَلّ send() / run.result به بعد التحقق منه في result.object، بالنوع z.output<typeof schema> (ويحتفظ result.text بنص JSON الخام). وتُنفَّذ الأدوات أولًا كالمعتاد. يحمل كل استدعاء للنموذج التلميح responseFormat: { type: 'json', schema }، وتحوّله المزوّدات المبنية على ai SDK إلى وضع JSON. وإذا كان الرد غير صالح مُنح النموذج خطوة إصلاح واحدة تسرد المشكلات (وتُحتسَب من maxSteps)؛ فإن بقي غير صالح انتهى التشغيل بـ finishReason: 'output-invalid' و outputError: { message, issues }. انظر المخرجات المنظَّمة.

المهارات

مرِّر skills: [defineSkill({ name, description, content }), ...(await loadSkills(dir))] إلى createAgent() أو AgentExecutor.execute(): فلا يدخل في موجّه النظام سوى الأسماء والأوصاف، ويحمّل النموذج محتوى المهارات عبر أداة load_skill تُسجَّل تلقائيًا. انظر المهارات.

الذاكرة

مرِّر memory: [defineMemory({ name, scope, provider })] إلى createAgent() للاحتفاظ بعناصر عبر المحادثات: يستحضر كل تشغيل أحدث عناصر كل خانة (slot) إلى موجّه النظام عند أول استدعاء للنموذج، ويحصل النموذج على الأداتين remember_<name> / recall_<name>. وscope إما 'global' أو 'session' أو دالة تأخذ { sessionId, metadata }؛ وinMemoryMemory() و fileMemory({ dir }) هما المزوّدان المضمّنان. انظر الذاكرة.

التنفيذ المتين

يجعل الخياران sessionId + checkpointStore التشغيل آمنًا من الانهيار والجلسة متعددة الدورات: تُسجَّل نقطة حفظ للتشغيل بعد كل رد من النموذج وكل نتيجة أداة وكل توقف مؤقت، واستدعاء execute() مرة أخرى بالـ sessionId نفسه يستأنف تشغيلًا لم يكتمل (من دون إعادة استدعاء النموذج من أجل دورة حصل عليها من قبل)، أو يواصل محادثة منتهية بالمدخلات الجديدة، أو يرمي SessionAwaitingApprovalError ما دامت هناك موافقة معلّقة. وتُنفَّذ الأدوات مرة واحدة على الأقل (at-least-once) عند وقوع انهيار؛ ويتلقى execute المعرّف toolCallId الخاص بالاستدعاء لاستخدامه مفتاحًا لمنع التكرار (idempotency key). انظر التنفيذ المتين لمعرفة الضمانات بدقة.

الإلغاء

مرِّر AbortSignal لإيقاف تشغيل: agent.send(input, { signal }) أو AgentExecutor.execute({ ..., signal }) أو resumeAfterApproval(..., { signal }).
لتحديد مهلة زمنية، استخدم AbortSignal.timeout(ms):
كيف يتصرف الإلغاء:
  • تُفحَص الإشارة قبل كل استدعاء للنموذج وكل استدعاء أداة. وتُمرَّر إلى المزوّد (GenerateOptions.signal، وتُرسَل إلى ai SDK باسم abortSignal) وإلى كل أداة في صورة execute(args, { abortSignal })، فيمكن إيقاف العمل الجاري مبكرًا. الأداة المضمّنة httpTool تمرّرها إلى fetch، والوكلاء المُنشأون بـ createDelegateTool() يُلغَون مع الوكيل الأب.
  • التشغيل المُلغى يُحَلّ (ولا يُرفَض) بـ finishReason: 'aborted' وبالرسائل والخطوات المنجزة حتى تلك اللحظة. والرفض الناتج عن الإلغاء، مثل AbortError، لا يُعامَل على أنه إخفاق: فلا تعيد محاولتَه مغلِّفات إعادة المحاولة والبديل الاحتياطي الخاصة بالمزوّد، ولا يُختزَل إلى خطأ مزوّد.
  • تنتهي أحداث التشغيل بـ run.done مع finishReason: 'aborted'.
  • مع sessionId + checkpointStore تُسجَّل الحالة في نقطة حفظ. واستدعاء execute() مرة أخرى بالـ sessionId نفسه يستأنف من حيث توقف التشغيل؛ ويُضاف input الجديد بوصفه رسالة المستخدم التالية (انظر التنفيذ المتين). أما استدعاءات الأدوات التي لم يبلغها التشغيل فتحصل على نتيجة { error } تفيد بأنها أُلغيت، فتبقى المحادثة صالحة لدى المزوّد.
  • إذا كانت الإشارة مُلغاة مسبقًا عاد الاستدعاء فورًا من دون استدعاء المزوّد.

أسباب الانتهاء

يبيّن result.finishReason سبب انتهاء التشغيل: السبب الذي أعطاه النموذج نفسه لدورته الأخيرة ('stop'، 'length'، 'tool_calls'، 'content_filter'، 'error')، أو 'awaiting-approval' (توقف مؤقت عند استدعاء أداة يحتاج إلى إنسان)، أو 'aborted' (أُلغي بـ signal)، أو 'max-steps'، أو 'output-invalid' (لم يطابق الرد مخطط output حتى بعد خطوة الإصلاح، انظر المخرجات المنظَّمة)، أو 'budget-exceeded' (تجاوُز إحدى ميزانيات limits مثل maxTokens أو maxCostUsd؛ ويبيّن result.budget أيّها، انظر الميزانيات)، أو 'guardrail' (حظرٌ من حاجز حماية للمدخلات أو المخرجات أو الأدوات؛ ويبيّن result.guardrail أيّها، انظر حواجز حماية المدخلات والمخرجات). وتعني 'max-steps' أن ميزانية maxSteps (الافتراضي 10) نفدت والنموذج ما زال يريد المتابعة، فقد يكون الرد فارغًا أو جزئيًا؛ أما التشغيل الذي ينتهي طبيعيًا ضمن الميزانية فيحتفظ بـ 'stop'. والخطوات المرحَّلة عبر initialSteps أو عبر استئناف بعد موافقة تُحتسَب من الميزانية، وresult.steps هو عدد الخطوات المنفَّذة. ويظهر السبب نفسه في حدث finish وفي run.done عند البث.

استدعاءات الأدوات المتوازية

عندما يطلب النموذج عدة أدوات في دورة واحدة تُنفَّذ بالتزامن. ويحدّد toolConcurrency (في createAgent() وAgentExecutor.execute()) الحد الأقصى لعدد ما يُنفَّذ منها في آن واحد: عدد صحيح موجب، أو 'unbounded' (القيمة الافتراضية). استخدم 1 للتنفيذ التسلسلي الصارم، مثلًا حين تتشارك أدواتك حالة لا يصحّ المساس بها بالتزامن.
الضمانات، أيًّا كان الحد:
  • ترتيب سجل المحادثة هو ترتيب الاستدعاء. تُضاف نتائج الأدوات بالترتيب الذي طلب به النموذج الاستدعاءات، لا بترتيب انتهائها، فيكون الطلب التالي إلى المزوّد حتميًّا (deterministic).
  • الأحداث. تبدأ الاستدعاءات بترتيب طلبها. حدث tool-call الخاص بالاستدعاء، وonToolCall، والتحقق من الوسائط، وخطّافات preToolCall، وفحص needsApproval تُنفَّذ كلها قبيل بدئه مباشرة، استدعاءً واحدًا في كل مرة. أما حدث tool-result فيُطلَق عند انتهاء الاستدعاء، فتصل النتائج بترتيب اكتمالها. ومع toolConcurrency: 1 تتناوب الأحداث استدعاءً فنتيجةً تمامًا كما في السابق.
  • الموافقات. أول استدعاء يحتاج إلى موافقة يوقف الدفعة: تُنفَّذ الاستدعاءات التي قبله (بالتزامن) وتُسجَّل نتائجها، ثم يتوقف التشغيل مؤقتًا عند ذلك الاستدعاء (finishReason: 'awaiting-approval'). أما الاستدعاءات التي بعده فلا تبدأ في هذا التشغيل. ويسجّل resumeAfterApproval() نتيجة الاستدعاء المتوقف (أو رفضه) ثم ينفّذ تلك الاستدعاءات اللاحقة بالطريقة نفسها، فيحصل كل استدعاء في الدورة على نتيجة واحدة بالضبط - انظر التنفيذ المتين.
  • الإخفاقات معزولة. الأداة التي ترمي خطأً تحصل على نتيجة خطأ خاصة بها؛ وتواصل الاستدعاءات المجاورة عملها. أما الخطأ المنتشر (PropagatingToolError، مثل حارس عمق التفويض، أو خطّاف يرمي خطأً) فيمنع بدء استدعاءات جديدة، وينتظر حتى تنتهي الاستدعاءات الجارية، ثم يرفض التشغيل. ولا تُترَك أي أداة تعمل منفصلةً عن التشغيل.
  • الإلغاء. الإلغاء أثناء تنفيذ دفعة يُحَلّ بـ finishReason: 'aborted'. الاستدعاءات التي انتهت تحتفظ بنتائجها؛ والبقية تحصل على نتيجة تفيد بأنها أُلغيت (“cancelled”). وترى الأدوات الجارية الإلغاء عبر abortSignal الخاص بها.
  • نقاط الحفظ. مع sessionId + checkpointStore تُسجَّل نقطة حفظ لدورة النموذج قبل بدء أي استدعاء، ثم مرة أخرى كلما طالت سلسلة الاستدعاءات المنتهية المتتالية بترتيب طلبها (ومع 1، بعد كل استدعاء). والتشغيل المستأنَف لا يعيد تنفيذ استدعاء مسجَّل أبدًا، ولا يسأل النموذج من جديد عن دورة سبق أن سُجّلت لها نقطة حفظ.

ملفات المواصفات التصريحية

المزوّدون

Message.content سلسلة نصية أو قائمة من عناصر ContentPart (text، image، file)؛ والمزوّدون المضمّنون يرسلون أجزاء الصور في رسائل المستخدم. انظر المدخلات متعددة الوسائط. انظر المزوّدون لمعرفة كيف تُفسَّر سلسلة النموذج النصية وأيّ نموذج يُشغَّل.

الاختبار

تُصدَّر من @lousho/build-ai-agent/testing (انظر اختبار الوكلاء).

الأدوات

انظر الأدوات للاطلاع على دليل تعريف الأدوات وتسجيلها.
الحقول الاختيارية: displayName وneedsApproval (قيمة منطقية أو دالة شرطية) وrequiresSandbox وsandboxExecute. تعريف الأداة يتحقق فورًا من اسمها ووصفها وinput المعرَّف بـ zod؛ وتسجيل أداتين بالاسم نفسه يرمي خطأً يذكر موضع التعارض. الأداة المعرَّفة تحمل مخططها في inputSchema (وهو مخطط zod نفسه الموجود في input) وتحمل دالة execute مباشرة. هذان هما الحقلان المعتمَدان في ToolDescriptor؛ أما الكائن .tool (وهو { description, parameters, execute } بصيغة ai v4) فقديم، وما زال يُبنى حفاظًا على التوافق، ولا يُستخدَم إلا مع الواصفات التي لا تحدّد inputSchema / execute.
دعم المخططات. يُحوَّل inputSchema (بصيغة JSON Schema) الخاص بكل أداة إلى مخطط zod، ويُتحقَّق من وسائط النموذج وفقه قبل استدعاء الخادم. يعالج المحوِّل type (بما فيه المصفوفات مثل ["string", "null"])، وenum بأنواع مختلطة، وconst، وanyOf / oneOf (اتحاد؛ وanyOf: [X, { type: "null" }] يصبح X.nullable())، وallOf (دمج / تقاطع)، و$ref المحلي إلى $defs / definitions، وproperties / required، وadditionalProperties (قيمة منطقية أو مخطط)، وitems، وdefault، والقيود minimum وmaximum وexclusiveMinimum وexclusiveMaximum وminLength وmaxLength وpattern وminItems وmaxItems. وحيث يكون JSON Schema ملتبسًا يكون التحويل متساهلًا: الكلمات المفتاحية غير المعروفة والمخططات الفارغة وitems بصيغة الصفوف (tuple) والمراجع التي يتعذّر حلّها تصبح z.any()، و$ref التعاودي يُوسَّع مرة واحدة ويُستخدَم z.any() للموضع الداخلي، ويُتجاهَل التعبير النمطي غير الصالح في pattern، وتحتفظ الكائنات بالخصائص الإضافية ما لم يكن additionalProperties مساويًا false. ولا يرمي المحوِّل خطأً أبدًا بسبب محتوى المخطط. الأدوات المتخطّاة. إذا تعذّر مع ذلك تحويل أداة ما فتُتخطّى تلك الأداة وحدها؛ وتُحمَّل بقية أدوات الخادم. مرِّر logger لتتلقى تحذيرًا يذكر الخادم والأداة والسبب، وonSkip لتجمع ما استُبعد:
النتائج. نتيجة MCP التي تحمل isError: true هي إخفاق أداة عادي: يتلقى النموذج { "error": "McpToolError", "toolName": "...", "message": "..." } حيث message هو المحتوى النصي الوارد من الخادم. والنتائج الناجحة قابلة للتسلسل بصيغة JSON: إذا أعاد الخادم structuredContent فهو كائن النتيجة؛ وإلا فالنتيجة { text, content }، حيث يجمع text كل الأجزاء النصية ويحتفظ content بكل الأجزاء بترتيبها (text، image، audio، resource، resource_link؛ والجزء الذي لا يُعرَف نوعه يُحتفَظ به في صورة { type: 'unknown', raw }). وعندما يرد structuredContent مع أجزاء غير نصية تكون النتيجة { structuredContent, text, content } ويضم content الأجزاء غير النصية فقط، فلا تُسقَط الصور ولا الصوت ولا الموارد أبدًا.

أدوات قائمة المهام

يمنح createTodoTools(options?) الوكلاء طويلي التشغيل خطة يتابعونها: todo_write تستبدل القائمة كلها (عناصر بصيغة { id?, content, status }، والحالة pending | in_progress | completed، وعنصر in_progress واحد على الأكثر) وتعيد القائمة مع الأعداد؛ وtodo_read تعيدها. تُعيَّن المعرّفات تلقائيًا وتبقى ثابتة عندما تكرّر كتابة لاحقة محتوى عنصر ما. والقوائم غير الصالحة (مثل عنصرين in_progress) تصل إلى النموذج في صورة خطأ أداة منظَّم ليعيد المحاولة. تبقى القائمة في الذاكرة لكل استدعاء؛ مرِّر store ({ get, set }) لحفظها بصورة دائمة، وonChange لتحديث واجهة المستخدم.

أخطاء الأدوات

قد يفشل استدعاء الأداة بطريقتين. وفي كلتيهما يستمر التشغيل ويتلقى النموذج خطأ JSON منظَّمًا بوصفه نتيجة الأداة (وليس السلسلة النصية null أبدًا)، فيتمكن من التعافي. وترى أحداث tool-result والتتبّع وonToolResult وخطّافات postToolCall الاستدعاء على أنه خطأ. التحقق من الوسائط. قبل تنفيذ الأداة تُحلَّل وسائط النموذج بمخطط zod inputSchema الخاص بالأداة (وفي الواصف القديم، tool.parameters). يجري التحقق أولًا، فتتلقى خطّافات ما قبل الأداة والدالة الشرطية needsApproval وexecute كلها القيمة المحلَّلة (بعد تطبيق القيم الافتراضية والتحويلات القسرية (coercions) والتحويلات (transforms)). أما الأدوات التي ليس لها مخطط zod فتُمرَّر وسائطها كما هي بلا تغيير. إذا لم تطابق الوسائط المخطط فلا تُستدعى execute، ويستمر التشغيل، ويتلقى النموذج خطأً منظَّمًا بوصفه نتيجة الأداة ليعيد المحاولة (أحداث tool-result والتتبّع وخطّافات postToolCall تراه نتيجة خطأ؛ وتُتخطّى خطّافات preToolCall لعدم وجود استدعاء صالح):
يُصدَّر ToolArgumentsValidationError (مع مصفوفة issues محدّدة النوع) من جذر الحزمة. الأخطاء المرمية. إذا رمت execute خطأً فيتلقى النموذج اسم الخطأ واسم الأداة والرسالة فقط (ولا يتلقى تتبّع المكدّس (stack trace) أبدًا). يُحَدّ طول الرسائل بـ 2,000 حرف، وتنتهي بـ ... (truncated) عند اقتطاعها:
كل إخفاق آخر (أداة غير معروفة، موافقة مرفوضة، استدعاء لم يُنفَّذ، خطأ MCP، أداة تتطلب بيئة معزولة رُفض تنفيذها) يستخدم الصيغة نفسها { error, toolName, message, kind }؛ انظر الأخطاء لمعرفة قيم kind. الأخطاء التي ترث من PropagatingToolError (مثل حارس عمق التفويض) هي الاستثناء: يُعاد رميها وتُنهي التشغيل بدل أن تُعرَض على النموذج.

النماذج والرموز والتكلفة

دوال مساعدة بلا اعتماديات لحساب الميزانيات واتخاذ قرارات السياق.
  • estimateTokens(input, { model?, estimator? }) تقدير تقريبي (نحو 4 أحرف لكل رمز (token) في الإنجليزية، وأكثر في لغات CJK والكتابات الأخرى، مع إضافة ثابتة لكل رسالة ومع JSON استدعاءات الأدوات). توقّع خطأً بنحو 15-20% في الإنجليزية: وهذا مناسب لضغط السياق والميزانيات، لا للفوترة. اربط مُرمِّزًا (tokenizer) حقيقيًا عبر setTokenEstimator(fn) أو options.estimator.
  • getModelInfo(id) يطابق المعرّف بحرفيّته، ثم provider/id، ثم اللقطات المؤرَّخة (gpt-4o-mini-2024-07-18 يُردّ إلى gpt-4o-mini). والنماذج غير المعروفة تعيد undefined.
  • registerModel(info) يضيف مدخلة أو يستبدلها؛ ويُعتمَد آخر تسجيل.
  • estimateCost(usage, model) يعيد التكلفة بالدولار الأمريكي، أو undefined (لا 0) عندما يكون النموذج أو أسعاره غير معروفة.
نوافذ السياق والأسعار المضمّنة لقطة مؤرَّخة (انظر تاريخ الاسترجاع والمصادر في أعلى src/models/modelData.ts). والمزوّدون يغيّرون الأسعار والنماذج، فاستبدل المدخلات عبر registerModel عندما تحتاج إلى أرقام بدقة تصلح للفوترة.

استهلاك التشغيل وتكلفته

كل ExecutionResult (وكذلك نتيجة agent.send()) يحمل usage، وهو المجموع التراكمي للتشغيل كله:
  • المُبلَّغ عنه مقابل المقدَّر. يبلّغ المزوّدون عن promptTokens/completionTokens؛ والمزوّدون المضمّنون يمرّرون كذلك cachedInputTokens/reasoningTokens عندما تحملها بيانات المزوّد الوصفية في ‘ai’ SDK (ولا يظهر usage.cachedInputTokens وusage.reasoningTokens إلا حينها). والخلفية (backend) التي لا تبلّغ عن شيء تعطي استهلاكًا قيمته undefined، لا أصفارًا أبدًا. وفي تلك الخطوة يلجأ المنفّذ إلى estimateTokens ويضبط usage.estimated (تسبق الناتجَ علامة ~ في formatUsage). وعلى المزوّد المخصّص أن يترك GenerateResult.usage بلا قيمة بدل أن يملأه بأصفار.
  • التكلفة. costUsd هو مجموع estimateCost لكل نموذج. ويكون undefined، لا مجموعًا جزئيًا مضلِّلًا أبدًا، ما إن يكون تسعير أي نموذج مستخدَم غير معروف؛ ويبيّن byModel أيّ النماذج مسعَّر.
  • التفويض. يُضاف استهلاك الوكيل الابن المفوَّض إلى مجاميع الأب وإلى byModel، ويُعرَض كذلك منفردًا في usage.delegated ({ inputTokens, outputTokens, totalTokens, costUsd, modelCalls, estimated, runs }).
  • الاستئناف. التشغيل المستأنَف من نقطة حفظ، أو بعد موافقة، يتابع من المجاميع المحفوظة بدل أن يبدأ من الصفر. ونقاط الحفظ التي كتبتها إصدارات أقدم تبدأ من أعداد الرموز المحفوظة فيها مع تكلفة غير معروفة.
  • الأحداث والتتبّعات. حدث finish (وكل حدث من أحداث دورة الحياة كان يحمل usage) يحمل الآن المجاميع التراكمية؛ ويحمل text-complete كذلك stepUsage، ويتلقى onLLMResponse استهلاك الاستدعاء وسيطًا ثالثًا. وسمات gen_ai.usage.* في مقطع التتبّع (span) chat تستخدم الأرقام نفسها، مع ضبط lousho.usage.estimated على true عندما تكون تقديرات.
  • البث. أحداث agent.stream() تحمل المحاسبة نفسها: في step.done وrun.done حقل usage يضم inputTokens وoutputTokens وestimated وcostUsd (وعلى مستوى التشغيل modelCalls أيضًا)، إلى جانب promptTokens/completionTokens الأقدم.
  • يبقى promptTokens وcompletionTokens في usage اسمين بديلين مُهمَلين لـ inputTokens وoutputTokens.
تأتي الأسعار من سجل النماذج المذكور أعلاه، فللحصول على تكلفة نموذج مخصّص أو مضبوط بدقة (fine-tuned)، سجّله بالمعرّف الذي تمرّره بوصفه النموذج:

ضغط السياق

يعيد createCompactionHook({ thresholdPercent?, contextWindow?, protectedTokens?, strategy?, onCompaction? }) خطّافًا من النوع AgentHook يضع، قبل كل استدعاء للنموذج يتجاوز 90% (افتراضيًا) من نافذة سياق النموذج، علامة بصيغة [pruned: <tool> result, N chars] مكان نتائج الأدوات الأقدم من أحدث 40,000 رمز. وهو يعدّل سجل محادثة التشغيل في موضعه، فيبقى التشذيب (pruning) محفوظًا في نقاط الحفظ وفي result.messages. وtwoPhaseStrategy({ model }) (الموصى بها) تشذّب أولًا، وإذا بقي التشغيل أكبر مما ينبغي وضعت مكان الدورات القديمة ملخصًا يكتبه model؛ أما summarizeStrategy() فتلخّص فقط. وpinMessage(message) تعلّم رسالة فلا تُشذَّب ولا تُلخَّص أبدًا. وcompactMessages(messages, options) تفعل الشيء نفسه مرة واحدة يدويًا (وهي غير متزامنة)، وCompactionStrategy هي الواجهة القابلة للاستبدال (ويجوز أن تكون compact() غير متزامنة). وcreateAgent({ compaction: true }) يثبّت الخطّاف على وكيل (ويقبل createAgent({ hooks }) أي خطّافات AgentHook أخرى)، ويُصدر stream() الحدثين compaction.start / compaction.done. انظر ضغط السياق.

مسارات العمل والتقييمات والمراقبة والأمان

  • FlowBuilder / FlowExecutor - مخططات بيانية (graphs) لمسارات عمل متعددة الخطوات؛ انظر مسارات العمل.
  • defineEval()، ودوال التقييم (scorers) مثل exactMatch وtoolCallOrder وbudget، والفحوص مثل includes وatLeast، وllmJudge() - تقييمات للوكلاء تُشغَّل تحت vitest أو lousho eval؛ انظر التقييمات.
  • withSpan() وTraceExporter - التتبّع لـ AgentExecutor.execute(). وTraceExporter واجهة تأتي أنت بمصدِّرها (لا يأتي مع الحزمة أي مصدِّر افتراضيًا)؛ وللحصول على مقاطع تتبّع (spans) حقيقية في OpenTelemetry، استورد createOtelTraceExporter() من المسار الفرعي @lousho/build-ai-agent/otel (يتطلب الاعتمادية النظيرة الاختيارية @opentelemetry/api) بدل أن تكتب جسر OTel بنفسك - انظر examples/tracing/run-otel.ts. تتبع المقاطع الاصطلاحات الدلالية لـ GenAI في OpenTelemetry (ومسارات العمل تُتتبَّع أيضًا)؛ انظر المراقبة.
  • NoopSandbox / SubprocessSandbox - العزل للأدوات التي تختاره عبر requiresSandbox؛ وrunGuardrails() وحواجز حماية مثل createCommandGuardrail() وcreateDiffSizeGuardrail() و secretScanGuardrail. انظر حواجز الحماية والعزل.
  • HookRegistry، AgentHook، HookContext، ToolCallHookContext، GenerateHookContext - خطّافات الوكيل القبلية والبعدية (تُنفَّذ قبل استدعاء أداة أو استدعاء generate للنموذج وبعده، ويمكنها تعديل الوسائط/الرسائل/النتائج أو رمي خطأ لإلغاء الخطوة؛ وخطّافات استدعاء الأدوات يمكنها أيضًا رفض استدعاء أو استبدال نتيجته أو تعديل مدخلاته، انظر قرارات الخطّافات). متاحة من جذر الحزمة ومن @lousho/build-ai-agent/hooks. محرّر الخطّافات في لوحة Agent Forge (docs/agent-forge.md) يحوّل بهذه الطريقة الخطّافات التي يربطها المستخدم بعقدة إلى HookRegistry، وتُنفَّذ في بيئة معزولة عبر SandboxAdapter لا في عملية المضيف.
  • EncryptionUtils، sha256، StorageService - أدوات مساعدة داعمة؛ انظر الأدوات المساعدة.

قرارات الخطّافات

خطّاف استدعاء الأداة يستطيع أن يغيّر ما يحدث، لا أن يراقبه فحسب. يجوز لخطّاف preToolCall أن يعيد:
  • لا شيء: يمضي الاستدعاء بلا تغيير (وتعديل ctx.args في موضعه ما زال يعمل).
  • { deny: reason }: لا يُنفَّذ الاستدعاء. يتلقى النموذج خطأ الأداة نفسه ذا kind: 'denied' الذي ينتج عن رفض needsApproval (انظر الموافقات)، وترى تدفقات البث tool.error، ويسجّل onPermissionDecision القيمة decision: 'deny' مع hook وreason.
  • { result: value }: لا يُنفَّذ الاستدعاء وتكون value نتيجته. ويحمل حدث tool.done ورسالة tool في سجل المحادثة (metadata) القيمة replacedByHook: '<hook name>'.
  • { input: args }: يُنفَّذ الاستدعاء بـ args. ويُتحقَّق منها مجددًا وفق مخطط مدخلات الأداة؛ وعدم المطابقة يصبح خطأ أداة ذا kind: 'validation' تذكر رسالته اسم الخطّاف، ولا تُنفَّذ الأداة.
يجوز لخطّاف postToolCall أن يعيد { result: value } ليستبدل النتيجة التي يراها النموذج (لحجب أجزاء منها أو اقتطاعها)؛ وإن لم يُعِد شيئًا بقيت كما هي. تُنفَّذ الخطّافات بترتيب تسجيلها: أول deny أو result من خطّاف قبلي يتخطى الخطّافات القبلية التي بعده، وقيم input تتسلسل (يرى كل خطّاف مدخلات سابقه في ctx.args)، ويرى كل خطّاف بعدي النتيجة التي استبدلها خطّاف سابق. والخطّاف الذي يرمي خطأً ما زال يرفض التشغيل، كما في السابق. ويرث الوكلاء الفرعيون خطّافات الوكيل الرئيسي وقراراتها. في استدعاء الأداة الواحد يكون الترتيب: التحقق من الوسائط، ثم خطّافات preToolCall، ثم قواعد الصلاحيات (permissions)، ثم حواجز حماية الأدوات، ثم needsApproval الخاص بالأداة، ثم التوقف المؤقت للموافقة، ثم التنفيذ، ثم خطّافات postToolCall. تُنفَّذ الخطّافات قبل كل قرار موافقة، فالقواعد وneedsApproval والإنسان كلهم يرون المدخلات التي أنتجها الخطّاف (ويوافقون عليها)؛ وتُظهرها args في الموافقة المعلّقة. وعند استئناف استدعاء تمت الموافقة عليه تُنفَّذ الخطّافات القبلية مرة أخرى: فما زال بإمكانها رفضه أو تقديم نتيجته، لكنّ { input } المختلف عن المدخلات الموافَق عليها يُرفَض بخطأ أداة بدل أن يُنفَّذ.

تعبيرات مسارات العمل

شروط فروع oneOf (وشروط فروع عقدة الموجِّه (router) في Agent Forge) وتعبيرات عقدة evaluator يقيّمها مقيِّم تعبيرات صغير مضمّن. وهو لا يصرّف شيفرة المضيف ولا ينفّذها أبدًا: فلا وجود لـ eval ولا new Function ولا vm في src/flows. العناصر النائبة {{name}} تُربَط بوصفها قيمًا، ولا تُلصَق أبدًا في نص التعبير: فالتعبير المجرّد {{score}} >= 90 يستخدم قيمة المتغير، وداخل سلسلة نصية حرفية، يُدرِج '{{classify}}' === 'refund' نص القيمة في تلك السلسلة بعد أن يكون التعبير قد قُطِّع إلى وحداته (tokenized). ولذلك فالمتغير الذي تحتوي قيمته على علامات اقتباس أو شرطات مائلة عكسية أو عوامل (مثل x' === 'x' || 'a) مجرد بيانات ولا يستطيع تغيير منطق الشرط. والمتغير المفقود أو الذي قيمته null يكون سلسلة فارغة داخل السلسلة الحرفية؛ وإذا استُخدم مجرّدًا لم يُضِف شيئًا، فيكون {{missing}} >= 90 خطأً في الصياغة ويُعَدّ الشرط غير متحقق. ويُقيَّم التعبير على متغيرات مسار العمل. الأسبقية، من الأضعف ربطًا إلى الأقوى: ||، &&، المساواة، المقارنة العلائقية، + -، * / %، العوامل الأحادية، الوصول إلى الأعضاء. غير مدعوم، ويُرفَض بخطأ ExpressionError يذكر التعبير وموضع الحرف وهذه القائمة من الصيغ المدعومة: أي استدعاء آخر لدالة أو تابع، والإسناد (=، +=، ++)، والتعبيرات الثلاثية، والسلاسل القالبية، والقيم الحرفية للكائنات/المصفوفات، والوصول إلى constructor أو __proto__ أو prototype، والكائنات العامة (process، require، globalThis، …). ولا يمكن الوصول إلا إلى متغيرات مسار العمل نفسه، وإلى خصائصها الذاتية فقط. سلوك الإخفاق لم يتغير: شرط oneOf الذي يتعذّر تقييمه يُعَدّ غير متحقق (false)، وتعبير evaluator الذي يتعذّر تقييمه يُفشل مسار العمل بالرسالة Failed to evaluate expression: ...، متضمّنةً تفاصيل ExpressionError.

المُشغِّلات

محوِّلات المُشغِّلات (@lousho/build-ai-agent/triggers) توقظ الوكيل عند ورود webhook أو حلول موعد في جدول زمني أو وصول رسالة Slack. اربط أيًّا منها بـ listen(agent, onEvent)، حيث تشغّل onEvent الوكيل. وإذا كانت الواجهة مما يتحدث الناس إليه فاستخدم قناة بدلًا من ذلك: defineChannel() وmountChannels() (من جذر الحزمة) تربطان كل محادثة على تلك الواجهة بجلسة، وتعيدان إليها الرد وأي موافقة أو سؤال يتوقف الوكيل عنده. وhttpChannel() وwebhookChannel() مضمّنتان؛ ويستخدم WebhookTriggerAdapter القناة webhookChannel() للمصادقة.

مصادقة Webhook

يشغّل WebhookTriggerAdapter خادم HTTP. اضبط auth دائمًا لأي webhook يمكن الوصول إليه من خارج جهازك: فمن دونه يستطيع كل من يصل إلى المنفذ أن يشغّل وكيلك (وأن ينفق رموزك). وإذا استمعت على مضيف غير محلي (non-loopback) بلا auth سجّل المحوِّل تحذيرًا لمرة واحدة عبر options.logger.
خيارات HMAC: header (الافتراضي x-signature-256)، وalgorithm (sha256 أو sha1، والافتراضي sha256)، وprefix (الافتراضي sha256=؛ و'' لخلاصة (digest) مجرّدة)، وtimestampHeader وtoleranceSeconds. تُقارَن التوقيعات ورموز bearer في زمن ثابت. والطلب الذي يفشل في المصادقة يحصل على رد عام 401 {"error":"Unauthorized"} - ولا يذكر الرد أبدًا أيّ فحص فشل - ويُسجَّل السبب (لا سرّ ولا توقيع أبدًا) بمستوى warn. قدِّم الـ webhooks عبر HTTPS (أنهِ اتصال TLS أمام المحوِّل) كي لا تُرسَل الرموز والحمولات نصًّا واضحًا غير مشفَّر.

توقيعات طلبات Slack

يعالج SlackTriggerAdapter.handleRequest({ headers, rawBody }) طلبًا خامًا من Slack Events API ويعيد { status, body } المطلوب إرساله ردًّا. اضبط signingSecret لأي نقطة نهاية يمكن الوصول إليها من خارج جهازك: فمن دونه يستطيع كل من يصل إلى نقطة النهاية أن يشغّل وكيلك، ويسجّل listen() تحذيرًا لمرة واحدة عبر options.logger. ومعه يُتحقَّق من كل طلب كما توثّق Slack قبل تحليل الجسم: HMAC-SHA256 على v0:{X-Slack-Request-Timestamp}:{raw body}، يُقارَن في زمن ثابت مع X-Slack-Signature (v0=<hex>)، وتُرفَض الطلبات التي مضى عليها أكثر من خمس دقائق. وتحصل الإخفاقات على رد عام 401 {"error":"Unauthorized"}؛ ويُسجَّل السبب (لا سرّ ولا توقيع أبدًا) بمستوى warn. أما مصافحة url_verification الموقَّعة فيُرَدّ عليها بعد التحقق.
يردّ handleRequest على Slack بعد أن ينتهي الوكيل؛ وSlack تتوقع ردًّا خلال ثلاث ثوانٍ، فمع الوكلاء البطيئين أرسل الإقرار بالاستلام أولًا ثم شغّل الوكيل في الخلفية.

جداول Cron الزمنية

يقبل CronTriggerAdapter إما intervalMs ثابتًا أو تعبير cron حقيقيًا:
الصيغة المدعومة: *، والقوائم (1,15)، والنطاقات (1-5)، والخطوات (*/15، 10-40/10)، وأسماء الأشهر (JAN) وأسماء أيام الأسبوع (MON)، مع دلالة كلٍّ من 0 و7 على الأحد، إضافةً إلى @hourly و@daily و@weekly و@monthly. وكما في cron التقليدي، عندما يُقيَّد يوم الشهر ويوم الأسبوع معًا يطابق اليوم إذا تحقق أحدهما. والتعبير غير الصالح يرمي CronExpressionError يذكر الحقل ويعرض مثالًا صالحًا. وتُصدَّر parseCronExpression(expr, timezone).nextRun(after) إن احتجت إلى موعد الإطلاق التالي. عند تغيّرات التوقيت الصيفي، الوقت الذي لا وجود له (عند تقديم الساعة) يُتخطّى في ذلك اليوم، والوقت الذي يتكرر مرتين (عند تأخير الساعة) يُطلَق مرة واحدة؛ والجدول الزمني الذي يعمل كل ساعة يظل يُطلَق كل ساعة. يُعاد ضبط المؤقِّت بعد كل تشغيل انطلاقًا من الموعد المجدوَل (بلا انحراف ولا إطلاق مزدوج)، وstop() يلغيه.

النشر

  • DeploymentAdapter، registerAdapter()، getAdapter()، listAdapters() - سجل المحوِّلات الذي يقوم عليه lousho build (انظر النشر).