بناء الوكلاء وتشغيلهم
الإعداد الديناميكي
كلٌّ من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، وتُرسَل إلىaiSDK باسم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 لتجمع ما استُبعد:
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) عند اقتطاعها:
{ 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.
ضغط السياق
يعيد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.
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(انظر النشر).