مترجَم عن الأصل الصيني. وعند الاختلاف بينهما، يُعتد بالأصل.
Confer — تصميم البروتوكول
يعرّف كل البروتوكولات بين نسخ Confer، وبين عميل المستخدم والخادم. وجميعها قائمة على معايير مفتوحة، تيسيراً للاتحاد مستقبلاً.
هوية الوكيل
صيغة DID:web
كل نسخة، لفرد أو لمؤسسة، تستضيف وثيقة DID خاصة بها:
https://acme.com/.well-known/did.json
بنية وثيقة DID (متوافقة مع W3C DID v1.0):
{
"@context": ["https://www.w3.org/ns/did/v1"],
"id": "did:web:acme.com",
"verificationMethod": [
{
"id": "did:web:acme.com#key-1",
"type": "Ed25519VerificationKey2020",
"controller": "did:web:acme.com",
"publicKeyMultibase": "z6MkpTHR8VNsBxYAAWHut2Geadd9jSrue..."
}
],
"service": [
{
"id": "did:web:acme.com#confer-agent",
"type": "ConferAgent",
"serviceEndpoint": "https://acme.com/a2a/v1"
}
]
}
ويأخذ DID وكيل المستخدم هذه الصورة: did:web:acme.com:agents:laowang — النسخة الأساسية يليها مقطع مسار. وبهذا تستطيع نسخة واحدة أن تحمل عدّة مستخدمين.
وفق مواصفة did:web، فإن DID ذا المعرّف الفرعي (أي الذي يحمل مقاطع مسار) يُحَلّ إلى الوثيقة الموافقة لذلك المسار، لا إلى .well-known في جذر النسخة:
did:web:acme.com:agents:laowang→https://acme.com/agents/laowang/did.json(النقطتان ← شرطة مائلة، ثم/did.jsonفي النهاية)- وDID النسخة المجرّد
did:web:acme.com→https://acme.com/.well-known/did.json - ويُرمَّز المنفذ الحقيقي بـ
%3A:did:web:acme.com%3A3000:agents:laowang→https://acme.com:3000/agents/laowang/did.json(أما نقطتان مجرّدتان مثل:8080فهما مقطع مسار، لا منفذ)
تدوير المفاتيح
- تقبل وثيقة DID إعلان عدّة طرق تحقّق، فيجري التدوير دون انقطاع.
- ويُحتفَظ بالمفتاح القديم ثلاثين يوماً على الأقل، لئلا تفشل الطلبات التي ما زالت في الطريق.
- ويتم الإبطال بإزالة طريقة التحقّق من الوثيقة.
- التنفيذ: يُنشئ
POST /api/v1/users/me/keys/rotateالمفتاح#key-<n>ويوقّع به فورًا؛ ويُعلَّم المفتاح القديم بأنه في طور التقاعد ويبقى في مستند DID لمدة 30 يومًا للتحقق فقط. ويُخرجPOST /api/v1/users/me/keys/revoke {key_id}مفتاحًا في طور التقاعد فورًا (لا يمكن إبطال المفتاح المستخدم مباشرة؛ بدّله أولًا). كلاهما لا يقبل إلا جلسة مسجّلة ويُسجَّل في سجل التدقيق. - جهة التحقق: التوقيع الذي لا يوجد keyId الخاص به في نسختنا المخزّنة من مستند DID للطرف الآخر يؤدي إلى جلب جديد واحد (مرة في الدقيقة كحد أقصى لكل DID)، فلا تُرفض أول رسالة من الطرف بعد تبديل مفتاحه بسبب ذاكرتنا المؤقتة.
AgentFacts (متوافق مع NANDA)
كل وكيل يُعلن AgentFacts تصفه. الموضع:
https://acme.com/agents/{slug}/agent.json
أو الدليل العام تحت well-known:
https://acme.com/.well-known/agents.json
مثال على البنية:
{
"@context": "https://nanda.dev/schemas/agent/v1",
"did": "did:web:acme.com:agents:support",
"name": "ABC Industries Support Agent",
"description": "Technical support for X100, X200 industrial controllers",
"owner": {
"type": "Organization",
"name": "ABC Industries Ltd.",
"url": "https://acme.com"
},
"capabilities": [
{
"type": "qa",
"scope": ["X100", "X200", "Modbus", "RTU", "TCP"],
"languages": ["en", "zh", "de"]
},
{
"type": "code-generation",
"scope": ["python", "c", "embedded"],
"languages": ["en", "zh"]
}
],
"endpoints": {
"a2a": "https://acme.com/a2a/v1",
"stream": "https://acme.com/a2a/v1/stream"
},
"trust": {
"verifiedBy": ["did:web:nanda.org"],
"issuedAt": "2024-10-01T00:00:00Z"
},
"publicKey": {
"id": "did:web:acme.com#key-1",
"type": "Ed25519VerificationKey2020"
}
}
شرح الحقول:
capabilities: ما يُحسنه هذا الوكيل. ويستعمل Claude Code حقلscopeللتوجيه بالكلمات المفتاحية (فحين تكتب شيفرة تخصّ X100 يستشير هذا الوكيل تلقائياً).languages: اللغات المدعومة. تُستخدم في سياسة الترجمة.trust.verifiedBy: تزكية ثقة من طرف ثالث (اختيارية؛ ستوفّرها NANDA لاحقاً).publicKey: مفتاح التوقيع العام لاتصال A2A.
بروتوكول A2A
طبقة البروتوكول
كل اتصال A2A يمرّ عبر HTTPS بطريقتَي POST/GET، بترميز JSON. وتحت /a2a/v1 يتعايش ربطان:
- ربط HTTP+JSON من A2A v1.0 التابع لمؤسسة لينكس (
routes/a2a-rest.ts)، بالمسارات كما هي في §11.3 من المواصفة:POST /message:sendوGET /tasks/{id}وغيرهما. وهو ما تُعلنه بطاقة الوكيل وما تستدعيه العملاء القياسية. انظرdocs/05-api.md. - لهجة Confer الخاصة (
routes/a2a.ts)، تُستعمل بين النسخ ويُكتشَف عبر/.well-known/agents.json.
ويتشارك الاثنان البوابات نفسها — التحقّق من التوقيع، وبوابة الموافقة، وقرار السياسة، وملكية الخيط — وكلها في a2a/inbound.ts؛ ولا يختلف إلا شكل الرسالة على السلك. وكتابة هذه البوابات مرّة لكل ربط هي بعينها السبب في أن خللَ حقن الخيوط بين المستأجرين كُتب أربع مرات؛ فلا تفصلوها ثانية.
الجوهر: تُستعمل HTTP Message Signatures (RFC 9421) لا رمز bearer. والسبب:
- رمز bearer ينتهي أمره بمجرد اعتراضه.
- توقيع HTTP مربوط بطلب بعينه (الطريقة + المسار + سلسلة الاستعلام + بصمة الجسم + الطابع الزمني).
- ومنعاً لإعادة الإرسال: يجب أن يقع ترويسة
Dateداخل نافذة خمس دقائق، ويُسجَّل كل توقيع مُتحقَّق منه في ذاكرة إعادة الإرسال (nonce)، فتُرفَض إعادة تقديم الطلب نفسه داخل النافذة؛ والتحقّق من التوقيع وحده يكفي لإثبات هوية المرسل.
والمكوّنات التي يغطيها التوقيع هي @method و@authority و@path، ويُضاف @query متى حمل الطلب سلسلة استعلام، وcontent-digest متى حمل جسماً، ثم date. و@query ليست ترفاً اختيارياً: @path يقف عند ?، بينما GET /tasks في ربط REST يرشِّح ويصفّح بالكامل عبر معاملات الاستعلام؛ فعدم تغطيتها يعني ترك وسيط يعيد كتابتها كما يشاء والتوقيع صحيح على حاله. كما تحمل معاملات التوقيع قيمة nonce عشوائية لكل طلب: فدقّة created لا تتعدّى الثانية، ولذلك يوقّع طلبان متطابقان في الثانية نفسها البايتات نفسها، فتحسبهما ذاكرة إعادة الإرسال عند الطرف الآخر هجوماً وترفض الثاني (وهو ما يصطدم به أي استقصاء لمهمّة، وأي إعادة محاولة). أما إعادة الإرسال الحقيقية — البايتات نفسها تُبعَث كما هي — فما زالت تُضبَط، لأن التوقيع حينها مطابق بايتاً ببايت.
وليست في هذه الطبقة securitySchemes الواردة في المواصفة: فتلك مفاتيح واجهة، ومصادقة HTTP، وOAuth2، وOIDC، وmTLS، وليس فيها واحد هو توقيع طلب. والشرط الحقيقي مُعلَن على البطاقة بوصفه امتداداً إلزامياً، ويفرضه ربط REST وفق §3.3.4: فالعميل الذي لا يُعلن ذلك الامتداد يتلقّى ExtensionSupportRequiredError، لا رمز 401 لا يشرح شيئاً.
مثال على طلب وارد
POST /a2a/v1/messages HTTP/1.1
Host: acme.com
Content-Type: application/json
Date: Sun, 24 Nov 2024 14:30:00 GMT
Content-Digest: sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:
Signature-Input: sig1=("@method" "@authority" "@path" "content-digest" "date");keyid="did:web:vendor-x.com#key-1";created=1732458600;alg="ed25519"
Signature: sig1=:aBcDeF...:
Authorization: Capability eyJhbGciOiJFZDI1NTE5IiwidHlwIjoiQ2FwIn0...
{
"from": "did:web:vendor-x.com:agents:engineer-li",
"to": "did:web:acme.com:agents:support",
"thread_id": "thread_8f3a9c",
"message": {
"type": "question",
"content": "ما مدى الجهد في X100 ضمن وضع RTU؟",
"language": "ar",
"context": {
"via": "claude-code",
"project_hint": "modbus integration"
}
}
}
مسار التحقّق (عند المستقبِل)
- تحليل ترويستَي
Signature-InputوSignature. - استخراج DID من معامل
keyidفيSignature-Input. - جلب وثيقة DID (مع ذاكرة مؤقتة: ETag ومدّة صلاحية 60 ثانية).
- أخذ المفتاح العام، وإعادة بناء سلسلة التوقيع الأساس وفق RFC 9421 §2.5، ثم التحقّق من التوقيع.
- التأكّد من مطابقة
Content-Digestلبصمة الجسم. - التأكّد من وقوع
Dateضمن خمس دقائق (منعاً لإعادة الإرسال). - التحقّق من رمز الصلاحية (على طريقة macaroon، وتفصيله أدناه).
- بوابة الموافقة على الاتصال: هل المرسل مضاف أصلاً في جهات اتصال المستقبِل؟ إن لم يكن → لا يُشغَّل النموذج اللغوي؛ ويُعلَّق الطلب بوصفه طلب اتصال (انظر أدناه).
- وإن كان → يمضي إلى محرّك السياسات ليقرّر أيُجيب أم لا.
رمز الصلاحية
يتيح رمز الصلاحية للوكيل المرسل أن يقول: «جئت نيابةً عن المستخدم X لأسأل سؤالاً من النوع Y»، ويقيّد الأذونات تقييداً دقيقاً.
على هيئة JWT، وبفكرة macaroon:
{
"iss": "did:web:vendor-x.com",
"sub": "did:web:vendor-x.com:users:engineer-li",
"aud": "did:web:acme.com",
"scope": ["ask:technical", "ask:product:X100"],
"exp": 1737000000,
"ctx": {
"thread_id": "thread_8f3a9c",
"delegation_depth": 1
}
}
scope: أي نوع من الأسئلة يجوز طرحه.delegation_depth: كم مرة أُعيد تمريره بالتفويض (منعاً لسلسلة لا تنتهي).
الردّ المتدفّق
يولّد النموذج اللغوي الجواب تدفّقاً، وA2A يدعم SSE كذلك:
GET /a2a/v1/stream/{message_id} HTTP/1.1
Host: acme.com
Signature: ...
فيعيد text/event-stream:
event: token
data: {"text": "جهاز X100 "}
event: token
data: {"text": "في وضع RTU "}
event: citation
data: {"source": "دليل تركيب X100 ص 12", "url": "..."}
event: done
data: {"thread_id": "thread_8f3a9c"}
نموذج الأذونات (مستوحى من Claude Code)
ثلاث طبقات:
L1 — تلقائي (بلا تأكيد)
- وكيلي يقرأ بياناتي أنا.
- ووكيل الطرف الآخر يجيب مستشهداً بوثائقه هو.
- وحوار بين وكيلين استفساري محض (بلا أثر جانبي وبلا مشاركة بيانات).
L2 — السؤال مرة واحدة
- مشاركة مجلد أو ملف مع وكيل الطرف الآخر.
- إطلاع وكيل الطرف الآخر على سياق حواري.
- تمرير بيانات إلى نسخة أخرى.
- تفعيل أداة (في أول مرة).
في الواجهة: تظهر بطاقة إذن بأربعة خيارات:
- السماح هذه المرة
- السماح دائماً (مقيَّداً بذلك القرين وذلك النطاق)
- عرض التفاصيل
- الرفض
L3 — موافقة صريحة (يُسأل في كل مرة)
- أن يقبل وكيلي دعوة نيابةً عني، أو يدفع، أو يوقّع عقداً.
- العمليات التي لا رجعة فيها (الحذف، التحويل، الالتزام تجاه الغير).
- الالتزامات ذات الأثر المالي أو القانوني.
في الواجهة: نافذة حاجبة + قائمة تفصيلية بالعملية + عدّ تنازلي (اتّقاءً للنقر الخاطئ).
السياسات الدائمة
يستطيع المستخدم أن يضع قواعد مسبقة تعلو على السلوك الافتراضي:
peer.acme-industries:
allow:
- read: "src/modbus/**"
- ask: "technical:*"
deny:
- read: ".env"
- read: "**/secrets/**"
- ask: "personal:*"
always_consult: true
peer.unknown:
default: ask_user
require_human_in_loop: true
بوابة الموافقة على الاتصال
الردّ على رسالة A2A يستهلك ميزانية النموذج اللغوي عند المستقبِل. وحتى لا يستطيع وكيل مجهول أن يُغرِق صاحب الحساب برسائل ويحرق رصيده دون علمه، فالاتصال شرط سابق على هذا الإنفاق:
- قرين متصل أصلاً (موجود في
peer_contactsعند المستقبِل) → الاتصال هو الموافقة؛ فيمضي إلى محرّك السياسات ويُعالَج معالجة عادية. - قرين غير متصل → يعيد
POST /a2a/v1/messagesالرمز202وجسمه{ "status": "pending_connection" }؛ فلا يُنشَأ حوار، ولا تُحفَظ رسالة، ولا يُشغَّل نموذج لغوي. ويُودَع في الوقت نفسه طلب اتصال بـaction='connect'في صندوق المعلّقات (مع إزالة التكرار حسب القرين، حتى لا تُغرقه الرسائل المعادة). - ويرى صاحب الحساب في صندوق أذوناته: «الوكيل الفلاني يطلب الاتصال + أول رسالة منه». فإن وافق كُتب في
peer_contacts(وتمّ الاتصال)، وصارت رسائل ذلك القرين تُعالَج معالجة عادية؛ وإن رفض فلا اتصال.
والنموذج أشبه بـ LinkedIn أو باتحاد مؤسسي: طبقة اكتشاف مفتوحة (لأيّ أحد أن يقرأ agents.json وAgentFacts)، وطبقة تفاعل تشترط الموافقة (فلا يُستهلَك حَوسبة الغير إلا بعد الاتصال).
وإلى حال «متصل» طريقان:
- أن يبادر المستقبِل فيضيف القرين عبر
POST /contacts/lookupثمPOST /contacts؛ - أن يبادر القرين، فيوافق المستقبِل على طلبه من الصندوق.
ارتباط الخيط (نطاق thread_id)
thread_id في الرسالة الواردة طلبٌ من القرين، لا أمرٌ ذو حجية. ولا تعيد البوابة استعماله بقيمته الأصلية إلا إذا تحقّق شرطان معاً:
- أن يكون ذلك القرين مشاركاً في هذا الحوار أصلاً؛
- وأن يكون هذا الحوار مملوكاً لصاحب الوكيل المُخاطَب (
conversations.created_by).
والثاني لا يجوز إسقاطه: فجدول peer_agents فريد عالمياً حسب DID، فقد يكون القرين نفسه متصلاً بعدّة أصحاب في آن. ولو اكتُفي بالأول لاستطاع قرينٌ متصل بـ A وبـ B، وهو يراسل وكيل B، أن يرفق thread_id الخاص بـ A فيصبّ الرسالة في حوار A: فيجيب وكيل B متّخذاً تاريخ A سياقاً، ويُكتَب الجواب في خيط A ويُبَثّ إلى A، بل ويُترسَّب مضمون حوار A في الذاكرة بعيدة المدى لدى B.
وتحقّق الشرطين معاً معناه أن الطرف الآخر يجيب عن رسالة خرجت من عندنا (فيكون thread_id عندئذٍ معرّف حوارنا نحن). وإن لم يتحقّقا فهو خيط في ترقيم القرين نفسه: لا يشير عندنا إلى شيء، لكنه ثابت عنده، ولذلك يُشتقّ معرّف حوارنا من sha256('a2a-thread:<معرّف الصاحب>:<معرّف صفّ القرين>:<thread_id القرين>') (lib/derived-id.ts، ومخرَجه 26 محرفاً بترميز Crockford، على هيئة ULID). وبهذا تعود الرسائل التالية في خيط القرين نفسه دائماً إلى الحوار نفسه.
وقد كان thread_id غير المعروف يُعامَل سابقاً على أنه «لا خيط»، فكان المستقبِل يُنشئ حواراً جديداً مع كل رسالة تصله: فلا يجتمع الاستفسار اللاحق بالسؤال الأصلي أبداً، وتمتلئ قائمة حوارات صاحب الحساب بخيوط من سطر واحد، ولا تجد loadA2AHistory تاريخاً تأخذه، ويجيب الوكيل في كل دورة كأنها أول حديث.
وللاشتقاق — بدل «حفظ جدول مطابقة» — فائدتان: لا يحتاج ترحيلاً، ولا يقبل تسابقاً بطبيعته؛ فرسالتان تصلان معاً تتصادمان على المفتاح الأساسي بدل أن تُنشئ كلٌّ منهما حواراً (ولذلك يجري إنشاء الحوار داخل معاملة مع onConflictDoNothing). وفي السلسلة المركّبة لا يُسمح إلا للمقطع الأخير بأن يكون متغيّر الطول وتحت سيطرة القرين؛ أما ما قبله فمعرّفات ثابتة من 26 محرفاً، فلا تُنتج النقطتان غير المهرَّبتان أي التباس.
وعند إنشاء الحوار يُكتَب في conversation_participants الصاحب والقرين معاً. وصفّ مشاركة الصاحب هو ما تقوم عليه قائمة الحوارات وبوابة القراءة لكل حوار؛ وبدونه لا يرى الصاحب الخيط الذي يجيب عنه وكيله هو.
فـ thread_id إذن هو معرّف الحوار لدى كل جانب على حدة، ولا يتطابق الجانبان. وينبني على ذلك حكمان لا يجوز إغفالهما:
- يجب أن يعيد الردّ
thread_idالذي أرسله السائل، لا معرّف الرادّ. فالشرط الثاني أعلاه سيرفض — عن حق — خيطاً ليس ملكه، ومن ثمّ يُصنَّف الردّ العائد بمعرّف حوارنا في حوار جديد كلياً عند الطرف الآخر؛ ويظل السائل يستقصي الحوار الذي أنشأه هو، فيبقى/api/v1/consult/{id}/replyعندpendingإلى الأبد، بينما جوابٌ سليم راقد على الجهازين كليهما. - يُكتَب في
messages.thread_rootمعرّف الحوار المحلي، ولا تُكتَب فيه قيمة القرين الخام أبداً. فذلك العمود من نوعchar(26)وُضع لمعرّفات ULID الخاصة بنا: وحفظ قيمة أجنبية فيه يشير إلى حوار قد لا نملكه، ويتيح لأي قرين أن يُسقِط هذه النقطة في خطأ 500 عبرthread_idيتجاوز 26 محرفاً. وللـthread_idالوارد تحقّق مستقل من الطول أيضاً.
العجز عن الجواب جوابٌ كذلك
حين يعجز الوكيل المُخاطَب عن تنفيذ الدورة (لا نموذج مضبوط، أو مزوّد مجهول، أو مزوّد مضبوط بلا مفتاح، أو استدعاء النموذج أخفق)، يردّ بـ type: 'notification' يحمل context.error فيه رمزاً مقروءاً آلياً (no_model_configured / unknown_provider / no_key_for_provider / agent_error)، ويحمل content جملة تفسيرية بالإنجليزية. واختير notification لأنه لا يستثير عند الطرف الآخر ردّاً تلقائياً آخر (وهذا ما يفعله question وحده).
وترك ذلك لا يعني «نقص تنبيه»: فالإخفاق يبقى سطر سجلّ عند المجيب، ولا يخرج شيء على السلك، ويظل /api/v1/consult/{id}/reply عند السائل يستقصي حتى تنتهي المهلة فيعيد pending — وهكذا في كل إعادة محاولة، بلا أي سبيل للتمييز بين «ما زال يفكّر» و«لن يأتي أبداً».
والطرف الآخر نسخة أخرى لا تشاركنا لغتنا، فالمعوَّل عليه رمز context.error؛ أما content فمجرّد نصّ احتياطي يقرأه إنسان. ولا يعارض هذا قاعدة «الخادم لا يصوغ نصاً للمستخدم»: فتلك تحكم ما يُرسَل إلى عميل هذه النسخة نفسها.
ويُسجَّل الإخفاق نفسه في قاعدة البيانات أيضاً، رسالةً بـ content_type: 'system_notice' داخل الحوار (يشير in_reply_to إلى السؤال، ويحمل content_json الرمز نفسه، ويصوغ العميل الجملة عبر التعريب). وللإرسال دون تسجيل ثلاث عواقب، وقعت كلها فعلاً: يرى الصاحب في مراسلته سؤال الطرف الآخر ولا شيء بعده، ولا يعلم أبداً أنه هو من لم يضبط نموذجاً؛ وتبقى مهمّة ربط REST في A2A عند WORKING بدل FAILED، فيستقصي العميل شيئاً لن يكتمل؛ ويظل GET /a2a/v1/stream/{id} يعيد pending بلا نهاية. وبهذه الملاحظة تنتقل الثلاثة دفعةً واحدة إلى حال نهائية قابلة للحسم.
العنونة: معرّفان يشيران إلى الوكيل نفسه
يقبل to كلاً من DID الوكيل (did:web:<host>:agents:<user>:agent، وهو ما يسرده الدليل العام /.well-known/agents.json) وDID الصاحب (did:web:<host>:agents:<user>). والأخير هو المعرّف الوحيد الذي يُحَلّ إلى وثيقة DID، وهو الذي يعرضه العميل على المستخدم لينسخه: فقبول الأول وحده يجعل «إضافة جهة اتصال بلصق معرّفها» تُنتج جهةً يتمّ الاتصال بها، ويصحّ توقيعها، ثم تردّ 404.
وللسبب نفسه، عند الحكم بأن القرين المرسل متصل أم لا، يجب قبول from (DID الوكيل) وDID الموقِّع المستخلَص من التحقّق (DID الصاحب) معاً: فالصفّ في peer_agents يُنشَأ حسب DID، وأيّهما حُفِظ يتوقّف على طريقة إضافة جهة الاتصال يومها؛ وقبول from وحده يحوّل ردّ الطرف الآخر إلى «طلب اتصال من مجهول».
صندوق المعلّقات (الإجابة في غياب الصاحب)
حين يصل سؤال من قرين متصل أصلاً والصاحب غائب، يقرّر محرّك السياسات (evaluatePolicy، action=ask، L2):
allow(وهو الافتراضي، لأن الاتصال موافقة أصلاً) → يجيب الوكيل مباشرة (201مع حلقة الردّ التلقائي).ask_user(وضع الصاحب صراحةًpolicies_json.default='ask_user'أو قاعدة{action:'ask',decision:'ask_user'}) → منفَّذ فعلاً: يُحفَظ السؤال الوارد ويُبَثّ كالمعتاد (فيراه الصاحب في مراسلته)، لكن بلا ردّ تلقائي؛ ويُودَع في الصندوق إذنٌ معلَّق بـaction='ask'، ويعيدPOST /a2a/v1/messagesالرمز202 { "status": "pending_approval", "message_id" }. ويرى الصاحب السؤال فيGET /permissions/pending؛ فإن حكم بـallow_*فيPOST /permissions/{id}/decideأجاب الوكيل نيابةً عنه (ردّ يحملin_reply_toمع تسليم صادر)، وإن حكم بـdenyفلا جواب. وعند القرين يعيدGET /a2a/v1/stream/{message_id}قبل الموافقةstatus:'pending'، ثم يعيد الجواب بعدها.deny(قاعدة رفض صريحة) →403 policy_denied.
ما يقدر عليه ردّ A2A: يمرّ الردّ الوارد عبر A2A ودردشة الويب بـالتنسيق المشترك نفسه (
runAgentTurnفيorchestration/agent-orchestrator.ts)، غير أن قدراتهما ليست واحدة. فـrunAgentTurnيأخذaudienceإلزامياً ('owner' | 'peer'— إلزامياً لا افتراضياً، لأن القيمة الافتراضية ستكون هي المتساهلة)، وبه تتحدّد مجموعة الأدوات وسطح البيانات الذي يمكن بلوغه معاً:
- دورة الصاحب (دردشة الويب):
web_searchوsearch_knowledge_base(كل قواعد المعرفة) وlist_knowledge_basesوsearch_memoryوlist_contacts، مع استحضار تلقائي للذاكرة بعيدة المدى.- دورة القرين (الردّ الوارد عبر A2A):
web_searchوsearch_knowledge_baseوlist_knowledge_basesفقط، والبحث محصور في القواعد الموسومة بـshared_with_peers؛ ولا تُستحضَر ذاكرة بعيدة المدى، ولا تُقدَّمsearch_memoryولاlist_contacts— فالأولى حقائق ترسّبت من محادثات الصاحب الخاصة، والثانية شبكته الاجتماعية؛ ولا حاجة إلى واحدة منهما للإجابة عن سؤال شخص غريب.والحدّ قائم على سطح البيانات لا على التوجيه النصي: فسؤال القرين وتعليمات الصاحب يصلان إلى النموذج بوصفهما نصاً من النوع نفسه، فلا تصحّ عبارة «سيمتنع الوكيل عن الإفصاح»؛ إنما يصحّ ألّا يبلغ البحث ذلك مادياً. وللسبب نفسه، عدم تقديم أداة ليس تحكّماً في الوصول: فالنموذج قد يصدر استدعاءً لاسم أداة لم يُعطَه قط، ولذلك تعيد الفروع الخاصة بالصاحب فحص
audienceداخلexecuteToolCall.وتستعمل الدورتان مفتاح الصاحب لا مفتاح القرين السائل. وتُحفَظ مقاطع قواعد المعرفة المطابِقة بوصفها استشهادات في
messages.citations_json، وبعد تمام الجواب تترسّب حقائق الدورة في الذاكرة بعيدة المدى على نحو غير متزامن (وتُوسَم الصفوف الآتية من دورة قرين بـa2a، ويُذكَر مصدرها عند الاستحضار). وإن لم تكن لدى الصاحب مفاتيح للتضمين أو قواعد المعرفة أو Tavily، انحدر الأمر انحداراً لطيفاً إلى ردّ من النموذج وحده (بلا خطأ وبلا استشهادات). ويتشارك مسارُ الردّ في حالةallowومسارُه بعد الموافقة علىask_userهذا التنسيق نفسه.
ويأتي
scope_jsonللإذن المعلَّق بـask='ask'على هذه الصورة:{ kind:'a2a_question', conversation_id, inbound_message_id, sender_did, peer_id, content }، وهو يكفي لإعادة بناء الجواب واستئنافه عند الموافقة (ويُقرَأ الوكيل والقرين لحظتها حسبuser_id/peer_id؛ والعملية عديمة الأثر عند التكرار: فإن وُجد ردّ سابق تُتخطّى). أما واجهة ضبط السياسات الدائمة، وخيار «التعديل ثم الردّ»، وإشعارات الدفع، فما زالت في قائمة الأعمال المؤجّلة.
الاكتشاف الاتحادي
البحث بالنطاق
عند إدخال النطاق acme.com، يقوم العميل بـ:
- جلب
https://acme.com/.well-known/did.jsonللحصول على DID الرئيسي. - جلب
https://acme.com/.well-known/agents.jsonلسرد كل الوكلاء العلنيين تحت ذلك النطاق. - اختيار واحد وإضافته إلى جهات الاتصال.
حلّ DID الخاص بمستخدم
بعد الحصول على DID ذي المعرّف الفرعي لوكيل مستخدم، تُحَلّ وثيقة DID الخاصة به وفق مواصفة did:web:
did:web:acme.com:agents:laowang→GET https://acme.com/agents/laowang/did.json- وDID النسخة المجرّد
did:web:acme.com→GET https://acme.com/.well-known/did.json
والتحقّق من توقيع A2A الوارد يسلك هذا الطريق بعينه: يُستخرج DID الموقِّع من keyid في Signature-Input ← ثم يُحَلّ إلى العنوان أعلاه ← ثم يُؤخذ من verificationMethod المفتاح العام المطابق لـ keyid ويُتحقَّق به. وتلك الوثيقة لا تُظهر إلا مادة المفتاح العام، وverificationMethod[*].id هو key_id المخزَّن (ولا يُعاد تركيبه من ترويسة Host في الطلب)، ولذلك يكون المعرّف المستخرَج بالحلّ من نسخة أخرى والمستخرَج محلياً واحداً على الدوام.
السجلّ العام (v2 فما فوق)
الارتباط بـ NANDA Index أو سجلّ عام مماثل، بما يدعم:
- البحث بالقدرة («جد لي وكيلاً يعرف Modbus»).
- البحث بالمؤسسة («وكيل ABC Industries»).
- البحث بالموقع («وكلاء خدمة قريبون»).
شبكة الثقة (v2 فما فوق)
- وكلاء جهات اتصالي يتقدّمون في الترتيب.
- ووكلاء شركات زملائي يتقدّمون في الترتيب.
- وتزكيات الأطراف الثالثة (المُتحقَّق منها لدى NANDA) تحمل شارة ثقة.
مكافحة الإزعاج
- تحديد معدّل لكل نطاق قرين في الدقيقة (عدّاد داخل العملية؛ البوابة أحادية النسخة).
- والأقران خارج القائمة البيضاء ذوو أولوية دنيا افتراضياً.
- وللمستخدم أن يحجب نطاق قرين بعينه.
- ودرجة سمعة (v2 فما فوق): كم نسخة أخرى وسمته بالإزعاج.
سياسة الترجمة
- يُعلن كل وكيل في AgentFacts الخاصة به
primary_languageوstyle. - وفي الحوار عبر اللغات تجري الترجمة داخل الوكيل الهدف (فهو أعرف الناس بمصطلحاته ووثائقه).
- ويبقى الجزء المقتبَس بأصله دائماً: فللمستخدم أن يطالع الصياغة ذات الحجية قبل الترجمة.
- والسلوك الافتراضي
preserve-style(يُحفَظ الأسلوب وتتبدّل اللغة وحدها)؛ ولسياق الاستهلاك أن يُعلنlocalize-style(الموافقة على العادة المحلية).
سياسة تطوّر البروتوكول
- كل البروتوكولات تحمل حقل
@contextأوversion. - والعميل والخادم كلاهما يحفظ التوافق مع السابق (يقبل الحقول المجهولة ويتجاهلها).
- والتغييرات الكاسرة تمرّ عبر رفع الإصدار الرئيسي (مثل
/a2a/v2/). - والتوافق قائم مع تطوّر مخطّطات NANDA وA2A من Google (فنحن نراهن على المنظومة المفتوحة).