এই পাতাটি চীনা মূল থেকে অনূদিত। দুটির মধ্যে অমিল হলে মূল পাঠই প্রামাণ্য।
Confer — প্রোটোকলের নকশা
Confer ইনস্ট্যান্সগুলোর মধ্যে এবং ব্যবহারকারীর ক্লায়েন্ট ও সার্ভারের মধ্যে যত প্রোটোকল আছে, সবই এখানে সংজ্ঞায়িত। সবগুলোই উন্মুক্ত মানদণ্ডের উপর দাঁড়ানো, যাতে ভবিষ্যতে ফেডারেশন সহজ হয়।
Agent-এর পরিচয়
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"
}
]
}
ব্যবহারকারীর Agent-এর 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 document-এ থাকে।POST /api/v1/users/me/keys/revoke {key_id}অবসরে যাচ্ছে এমন কী সঙ্গে সঙ্গে সরিয়ে দেয় (ব্যবহৃত কী সরাসরি বাতিল করা যায় না; আগে বদলান)। দুটোই শুধু সাইন-ইন সেশন নেয় এবং অডিট হয়। - যাচাইকারী দিক: যে স্বাক্ষরের keyId আমাদের ক্যাশে রাখা অপর পক্ষের DID document-এ নেই, সেটি একবার নতুন করে আনায় (প্রতি DID-এ মিনিটে সর্বোচ্চ একবার), তাই কী বদলানোর পর অপর পক্ষের প্রথম বার্তা আমাদের ক্যাশের কারণে প্রত্যাখ্যাত হয় না।
AgentFacts (NANDA সঙ্গতিপূর্ণ)
প্রত্যেক Agent নিজের বর্ণনা দিয়ে একটি 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: এই Agent কী পারে। Claude Codescopeক্ষেত্র ধরে কীওয়ার্ড রাউটিং করে (X100 সংক্রান্ত কোড লেখার সময় আপনা-আপনি এই Agent-কে জিজ্ঞেস করে)।languages: সমর্থিত ভাষা। অনুবাদ কৌশলের জন্য।trust.verifiedBy: তৃতীয় পক্ষের আস্থা-সমর্থন (ঐচ্ছিক; ভবিষ্যতে NANDA দেবে)।publicKey: A2A যোগাযোগের স্বাক্ষরের সর্বজনীন কী।
A2A প্রোটোকল
প্রোটোকলের স্তর
সব A2A যোগাযোগ HTTPS POST/GET দিয়ে চলে, JSON-এ এনকোড করা। /a2a/v1-এর নিচে দুটি বাইন্ডিং পাশাপাশি থাকে:
- Linux Foundation-এর A2A v1.0-এর HTTP+JSON বাইন্ডিং (
routes/a2a-rest.ts), যার পথগুলো স্পেসিফিকেশনের §11.3-এ যেমন আছে ঠিক তেমন:POST /message:send,GET /tasks/{id}ইত্যাদি। Agent Card এটিই ঘোষণা করে, আর মানসম্মত ক্লায়েন্টরা এটিই ডাকে। দেখুন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 ?-এ গিয়ে থেমে যায়, আর REST বাইন্ডিংয়ের GET /tasks পুরোপুরি কোয়েরি প্যারামিটার দিয়েই ছাঁকে ও পাতা ওল্টায়; সেগুলো না ঢাকা মানে মাঝপথের কাউকে ইচ্ছেমতো সেগুলো বদলাতে দেওয়া, অথচ স্বাক্ষর তবুও মিলে যাবে। স্বাক্ষরের প্যারামিটারে প্রতি অনুরোধে একটি এলোমেলো nonce-ও থাকে — created-এর সূক্ষ্মতা সেকেন্ড পর্যন্তই, তাই একই সেকেন্ডে দুটি অভিন্ন অনুরোধ একই বাইটে স্বাক্ষর করত, আর অপর পক্ষের রিপ্লে ক্যাশে সেটিকে আক্রমণ ধরে ফিরিয়ে দিত (কোনো task-এর খোঁজ নেওয়া বা যেকোনো পুনঃচেষ্টা ঠিক এখানেই ঠেকত)। আসল পুনঃপ্রেরণ — একই বাইট হুবহু আবার পাঠানো — এখনও ধরা পড়ে, কারণ তখন স্বাক্ষর বাইটে বাইটে এক।
এই স্তরে স্পেসিফিকেশনের securitySchemes নেই: ওগুলো API কী, HTTP প্রমাণীকরণ, OAuth2, OIDC বা mTLS, আর একটিও অনুরোধ-স্বাক্ষর নয়। আসল দাবিটি Card-এ আবশ্যিক এক্সটেনশন হিসেবে ঘোষিত হয়, আর 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": "RTU মোডে X100-এর ভোল্টেজ পরিসর কত?",
"language": "bn",
"context": {
"via": "claude-code",
"project_hint": "modbus integration"
}
}
}
যাচাইয়ের ধাপ (প্রাপকের দিকে)
১. Signature-Input ও Signature হেডার পড়ুন।
২. Signature-Input-এর keyid প্যারামিটার থেকে DID বের করুন।
৩. DID নথি আনুন (ক্যাশসহ: ETag + ৬০ সেকেন্ড TTL)।
৪. সর্বজনীন কী নিন, RFC 9421 §2.5 অনুসারে স্বাক্ষরের ভিত্তি-স্ট্রিং আবার গড়ুন, আর স্বাক্ষর যাচাই করুন।
৫. Content-Digest বডির হ্যাশের সঙ্গে মেলে কি না দেখুন।
৬. Date ৫ মিনিটের মধ্যে কি না দেখুন (পুনঃপ্রেরণ ঠেকাতে)।
৭. capability টোকেন যাচাই করুন (macaroon ধাঁচের, নিচে বিস্তারিত)।
৮. সংযোগ সম্মতির ফটক: প্রেরক কি প্রাপকের পরিচিতিতে আগে থেকেই আছে? না থাকলে → LLM চলে না; অনুরোধটি সংযোগের আবেদন হিসেবে ঝুলে থাকে (নিচে দেখুন)।
৯. থাকলে → নীতি ইঞ্জিন ঠিক করে সাড়া দেওয়া হবে কি না।
Capability টোকেন
Capability টোকেন দিয়ে প্রেরক Agent বলতে পারে “আমি ব্যবহারকারী 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: প্রতিনিধিত্বের মাধ্যমে কতবার পাঠানো হয়েছে (অসীম শৃঙ্খল ঠেকাতে)।
ধারায় উত্তর
LLM উত্তর তৈরি করে ধারায়, আর 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 ইনস্টলেশন ম্যানুয়াল পৃ. ১২", "url": "..."}
event: done
data: {"thread_id": "thread_8f3a9c"}
অনুমতির মডেল (Claude Code থেকে অনুপ্রাণিত)
তিন স্তর:
L1 — স্বয়ংক্রিয় (নিশ্চিতকরণ লাগে না)
- আমার Agent আমারই তথ্য পড়ে।
- অপর পক্ষের Agent নিজের নথির উদ্ধৃতি দিয়ে উত্তর দেয়।
- Agent-দের মধ্যে নিছক জিজ্ঞাসার কথোপকথন (কোনো পার্শ্বপ্রতিক্রিয়া নেই, তথ্য ভাগাভাগিও নেই)।
L2 — একবার জিজ্ঞেস করা
- কোনো ডিরেক্টরি বা ফাইল অপর পক্ষের Agent-এর সঙ্গে ভাগ করা।
- অপর পক্ষের Agent-কে আমার কথোপকথনের প্রেক্ষাপট দেখতে দেওয়া।
- অন্য ইনস্ট্যান্সে তথ্য পাঠানো।
- কোনো সরঞ্জাম চালু করা (প্রথমবার)।
ইন্টারফেসে: একটি অনুমতি-কার্ড খোলে, চারটি বিকল্প নিয়ে:
- এইবারের জন্য অনুমতি
- সবসময় অনুমতি (ওই peer ও ওই পরিসরে সীমিত)
- বিস্তারিত দেখুন
- প্রত্যাখ্যান
L3 — স্পষ্ট সম্মতি (প্রতিবারই জিজ্ঞেস করা হয়)
- আমার Agent আমার হয়ে আমন্ত্রণ গ্রহণ করে, টাকা দেয়, চুক্তিতে সই করে।
- অপরিবর্তনীয় কাজ (মুছে ফেলা, টাকা পাঠানো, বাইরের কাউকে প্রতিশ্রুতি দেওয়া)।
- অর্থ বা আইন জড়িত প্রতিশ্রুতি।
ইন্টারফেসে: মোডাল জানালা + কাজের বিস্তারিত তালিকা + উল্টো গণনা (ভুল করে চাপা ঠেকাতে)।
স্থায়ী নীতি
ব্যবহারকারী আগেভাগে নিয়ম ঠিক করে দিতে পারেন, যা ডিফল্ট আচরণের উপরে বসে:
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 বার্তার উত্তর দেওয়া প্রাপকের LLM বাজেট খরচ করে। অচেনা কোনো Agent যাতে মালিকের অজান্তে বেপরোয়া বার্তা পাঠিয়ে তাঁর টোকেন পুড়িয়ে দিতে না পারে, সেজন্য এই খরচের পূর্বশর্ত হলো সংযুক্ত থাকা:
- আগে থেকেই সংযুক্ত peer (প্রাপকের
peer_contacts-এ আছে) → সংযোগই সম্মতি; নীতি ইঞ্জিনে গিয়ে স্বাভাবিকভাবে প্রক্রিয়াজাত হয়। - সংযুক্ত নয় এমন peer →
POST /a2a/v1/messagesফেরায়202, বডি{ "status": "pending_connection" }; কোনো কথোপকথন তৈরি হয় না, বার্তা সংরক্ষিত হয় না, LLM চলে না। একই সঙ্গেaction='connect'সহ একটি ঝুলন্ত সংযোগ-আবেদন অপেক্ষমাণ বাক্সে রাখা হয় (peer ধরে ডুপ্লিকেট বাদ দিয়ে, যাতে বারবার আসা বার্তা তা ভরিয়ে না ফেলে)। - মালিক নিজের অনুমতি-বাক্সে দেখেন «অমুক Agent সংযোগ চাইছে + তার প্রথম বার্তা»। অনুমোদন করলে
peer_contacts-এ লেখা হয় (সংযোগ হয়ে গেল), এরপর ওই peer-এর বার্তা স্বাভাবিকভাবে প্রক্রিয়াজাত হয়; প্রত্যাখ্যান করলে কোনো সংযোগ হয় না।
মডেলটি LinkedIn বা কোনো প্রাতিষ্ঠানিক ফেডারেশনের মতো: আবিষ্কারের স্তর উন্মুক্ত (যে কেউ agents.json ও AgentFacts পড়তে পারে), মিথস্ক্রিয়ার স্তরে সম্মতি লাগে (সংযুক্ত হওয়ার পরেই অন্যের গণনা খরচ হয়)।
«সংযুক্ত» হওয়ার দুটি পথ:
১. প্রাপক নিজে POST /contacts/lookup → POST /contacts দিয়ে ওই peer-কে যোগ করেন;
২. peer আগে উদ্যোগ নেয়, আর প্রাপক বাক্স থেকে তার আবেদন অনুমোদন করেন।
থ্রেডের বাঁধন (thread_id-এর পরিসর)
আগত বার্তার thread_id peer-এর একটি অনুরোধ, কর্তৃত্বময় নির্দেশ নয়। gateway সেটিকে মূল মানেই পুনর্ব্যবহার করে কেবল যখন দুটি শর্ত একসঙ্গে মেলে:
১. ওই peer আগে থেকেই সেই কথোপকথনের অংশগ্রহণকারী;
২. সেই কথোপকথন যে Agent-কে বার্তা পাঠানো হয়েছে তার মালিকের (conversations.created_by)।
দ্বিতীয়টি বাদ দেওয়া চলে না: peer_agents DID ধরে বিশ্বজুড়ে অনন্য, তাই একই peer একসঙ্গে একাধিক মালিকের সঙ্গে সংযুক্ত থাকতে পারে। কেবল প্রথমটি যাচাই করলে, A ও B দুজনের সঙ্গেই সংযুক্ত কোনো peer B-র Agent-কে বার্তা পাঠানোর সময় A-র thread_id জুড়ে দিয়ে বার্তাটি A-র কথোপকথনে ঢেলে দিতে পারত — B-র Agent A-র ইতিহাসকে প্রেক্ষাপট ধরে উত্তর দিত, উত্তরটি A-র থ্রেডে লেখা হয়ে A-র কাছে ছড়িয়ে পড়ত, আর তার উপর A-র কথোপকথনের বিষয়বস্তু B-র দীর্ঘমেয়াদি স্মৃতিতে জমা হয়ে যেত।
দুটোই মিললে বোঝা যায় অপর পক্ষ আমাদের পাঠানো বার্তারই উত্তর দিচ্ছে (আর তখন thread_id আমাদের নিজেদের কথোপকথনের id)। না মিললে সেটি peer-এর নিজস্ব সংখ্যায়নের থ্রেড — এখানে কিছুই নির্দেশ করে না, কিন্তু peer-এর কাছে স্থিতিশীল, তাই আমাদের কথোপকথনের id উদ্ভূত হয় sha256('a2a-thread:<মালিকের id>:<peer সারির id>:<peer-এর thread_id>') থেকে (lib/derived-id.ts, ফেরত দেয় ২৬ অক্ষরের Crockford, ULID-এর মতোই চেহারা)। ফলে ওই একই peer থ্রেডের পরের বার্তাগুলো সবসময় একই কথোপকথনে ফিরে আসে।
আগে «অচেনা thread_id»-কে «থ্রেড নেই» ধরা হতো, তাই প্রাপক যত বার্তা আসত ততবার নতুন কথোপকথন খুলত: পরের প্রশ্ন কখনও আসল প্রশ্নের পাশে থাকত না, মালিকের কথোপকথনের তালিকা এক লাইনের থ্রেডে ভরে যেত, loadA2AHistory কোনো ইতিহাস পেত না, আর Agent প্রতিটি দফায় এমনভাবে উত্তর দিত যেন এই প্রথম কথা হচ্ছে।
«একটি ম্যাপিং টেবিল রাখা»-র বদলে উদ্ভূত করার দুটি সুবিধা: কোনো মাইগ্রেশন লাগে না, আর প্রতিযোগিতাও অসম্ভব — একসঙ্গে আসা দুটি বার্তা প্রাথমিক কী-তে ঠোকাঠুকি করে, প্রত্যেকে আলাদা কথোপকথন বানায় না (এজন্যই কথোপকথন তৈরি হয় লেনদেনের ভেতরে onConflictDoNothing দিয়ে)। জোড়া লাগানো স্ট্রিংয়ে কেবল শেষ খণ্ডটির দৈর্ঘ্য পরিবর্তনশীল হতে পারে এবং সেটিই peer-এর নিয়ন্ত্রণে; আগের খণ্ডগুলো ২৬ অক্ষরের নির্দিষ্ট id, তাই এস্কেপ না করা কোলন কোনো দ্ব্যর্থতা তৈরি করে না।
কথোপকথন তৈরির সময় conversation_participants-এ মালিক ও peer দুজনকেই একসঙ্গে লেখা হয়। মালিকের অংশগ্রহণের সারিটিই কথোপকথনের তালিকা ও প্রতি-কথোপকথন পড়ার ফটকের ভিত্তি; সেটি না থাকলে মালিক সেই থ্রেডই দেখতে পাবেন না যার উত্তর তাঁরই Agent দিচ্ছে।
তাই thread_id হলো প্রতিটি পক্ষের নিজস্ব কথোপকথনের id, দুই দিকে এক নয়। এ থেকে দুটি নিয়ম আসে, যেগুলো বাদ দেওয়া চলে না:
- উত্তরে সেই
thread_id-ই ফেরত দিতে হবে যেটি প্রশ্নকারী পাঠিয়েছিল, নিজেরটি নয়। উপরের দ্বিতীয় শর্তটি (যথার্থভাবেই) এমন থ্রেড ফিরিয়ে দেবে যা তার নয়, তাই আমাদের কথোপকথনের id নিয়ে ফেরা উত্তর অপর প্রান্তে একেবারে নতুন কথোপকথনে জমা হবে; প্রশ্নকারী তখনও নিজের বানানো কথোপকথনটির খোঁজ করতে থাকবে, আর/api/v1/consult/{id}/replyচিরকালpending-এ আটকে থাকবে — অথচ দুই যন্ত্রেই দিব্যি একটা ভালো উত্তর পড়ে আছে। messages.thread_root-এ স্থানীয় কথোপকথনের id লেখা হয়, peer-এর কাঁচা মান কখনও নয়। ওই কলামটিchar(26), বানানো আমাদের নিজেদের ULID-এর জন্য: বাইরের মান রাখলে তা এমন কথোপকথনের দিকে দেখাত যা হয়তো আমাদেরই নয়, আর যেকোনো peer-কে সুযোগ দিত ২৬ অক্ষরের চেয়ে বড়thread_idপাঠিয়ে এই endpoint-কে 500-এ ফেলে দিতে। আগতthread_id-এর দৈর্ঘ্যের আলাদা যাচাইও আছে।
উত্তর দিতে না পারাও একটি উত্তর
যে Agent-কে বার্তা পাঠানো হয়েছে সে যখন দফাটি চালাতেই পারে না (মডেল ঠিক করা নেই, সরবরাহকারী অচেনা, সরবরাহকারী আছে কিন্তু কী নেই, বা মডেলের কল ব্যর্থ), তখন সে পাঠায় 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-এ একই কোড, আর বাক্যটি ক্লায়েন্ট i18n দিয়ে গড়ে)। পাঠিয়ে না লিখলে তিনটি পরিণতি হয়, আর তিনটিই সত্যিই ঘটেছে: মালিক নিজের IM-এ অপর পক্ষের প্রশ্নের পরে কিছুই দেখেন না, আর কখনও জানতেই পারেন না যে মডেলটি তিনিই ঠিক করেননি; A2A REST বাইন্ডিংয়ের task FAILED-এর বদলে WORKING-এ দাঁড়িয়ে থাকে, আর ক্লায়েন্ট এমন কিছুর খোঁজ করতে থাকে যা কখনও শেষ হবে না; আর GET /a2a/v1/stream/{id} অনন্তকাল pending ফেরায়। এই নোটটি থাকলে তিনটিই একসঙ্গে নির্ধারণযোগ্য চূড়ান্ত অবস্থায় পৌঁছে যায়।
ঠিকানা: দুটি DID একই Agent-কে বোঝায়
to গ্রহণ করে Agent DID (did:web:<host>:agents:<user>:agent, যা সর্বজনীন তালিকা /.well-known/agents.json-এ থাকে) এবং মালিকের DID (did:web:<host>:agents:<user>) — দুটোই। শেষেরটিই একমাত্র শনাক্তকারী যা DID নথিতে সমাধান হয়, আর ক্লায়েন্ট ব্যবহারকারীকে কপি করার জন্য সেটিই দেখায়: কেবল প্রথমটি মানলে «DID পেস্ট করে পরিচিতি যোগ করা» এমন একটি পরিচিতি দেয় যার সঙ্গে সংযোগও হয়, স্বাক্ষরও মেলে, আর উত্তর আসে 404।
একই কারণে, প্রেরক peer সংযুক্ত কি না ঠিক করার সময় from (Agent DID) আর স্বাক্ষর যাচাই থেকে পাওয়া স্বাক্ষরকারীর DID (মালিকের DID) — দুটোই মানতে হবে: peer_agents-এ সারি তৈরি হয় DID ধরে, আর তার মধ্যে কোনটি সেখানে জমা হয়েছে তা নির্ভর করে পরিচিতিটি তখন কীভাবে যোগ হয়েছিল তার উপর; কেবল from মানলে অপর পক্ষের উত্তর হয়ে যায় «অচেনা কারও সংযোগ-আবেদন»।
অপেক্ষমাণ বাক্স (মালিকের অনুপস্থিতিতে উত্তর)
আগে থেকেই সংযুক্ত peer-এর প্রশ্ন এল আর মালিক নেই — তখন সিদ্ধান্ত নেয় নীতি ইঞ্জিন (evaluatePolicy, action=ask, L2):
allow(ডিফল্ট, কারণ সংযোগই সম্মতি) → Agent সরাসরি উত্তর দেয় (201+ স্বয়ংক্রিয় উত্তরের চক্র)।ask_user(মালিক স্পষ্টভাবেpolicies_json.default='ask_user'বা{action:'ask',decision:'ask_user'}নিয়ম দিয়েছেন) → বাস্তবায়িত হয়ে গেছে: আগত প্রশ্ন তবু সংরক্ষিত ও সম্প্রচারিত হয় (মালিক IM-এ দেখতে পান), কিন্তু স্বয়ংক্রিয় উত্তর যায় না; বাক্সেaction='ask'সহ একটি ঝুলন্ত অনুমতি রাখা হয়, আরPOST /a2a/v1/messagesফেরায়202 { "status": "pending_approval", "message_id" }। মালিক প্রশ্নটি দেখেনGET /permissions/pending-এ;POST /permissions/{id}/decide-এallow_*দিলে Agent তাঁর হয়ে উত্তর দেয় (in_reply_toসহ উত্তর + বহির্গামী প্রেরণ),denyদিলে কোনো উত্তর নয়। peer-এর দিকেGET /a2a/v1/stream/{message_id}অনুমোদনের আগেstatus:'pending'ফেরায়, পরে উত্তর।deny(স্পষ্ট প্রত্যাখ্যানের নিয়ম) →403 policy_denied।
A2A উত্তরের সামর্থ্য: আগত A2A উত্তর আর ওয়েব চ্যাট একই ভাগাভাগি করা অর্কেস্ট্রেশন দিয়ে চলে (
orchestration/agent-orchestrator.ts-এরrunAgentTurn), কিন্তু তাদের সামর্থ্য এক নয়।runAgentTurnএকটি আবশ্যিকaudienceনেয় ('owner' | 'peer'— আবশ্যিক, ডিফল্টযুক্ত নয়, কারণ ডিফল্ট মানটাই হতো ঢিলেঢালাটি), আর সেটিই ঠিক করে সরঞ্জামের সেট এবং নাগালে থাকা তথ্য-তল দুটোই:
- মালিকের দফা (ওয়েব চ্যাট):
web_search,search_knowledge_base(সব জ্ঞানভাণ্ডার),list_knowledge_bases,search_memory,list_contacts, সঙ্গে দীর্ঘমেয়াদি স্মৃতি আপনা-আপনি যুক্ত হয়।- peer-এর দফা (আগত A2A উত্তর): কেবল
web_search,search_knowledge_baseওlist_knowledge_bases, আর অনুসন্ধান কেবল সেই ভাণ্ডারগুলোতেই সীমিত যেগুলোতেshared_with_peersচিহ্ন আছে; দীর্ঘমেয়াদি স্মৃতি তোলা হয় না, আরsearch_memoryবাlist_contactsদেওয়াই হয় না — প্রথমটি মালিকের ব্যক্তিগত কথোপকথন থেকে জমা তথ্য, দ্বিতীয়টি তাঁর সামাজিক জাল; অচেনা কারও প্রশ্নের উত্তর দিতে দুটোর কোনোটিই লাগে না।সীমানা দাঁড়ায় তথ্য-তলের উপর, প্রম্পটের উপর নয়: peer-এর প্রশ্ন আর মালিকের নির্দেশ মডেলের কাছে একই ধরনের লেখা হয়ে পৌঁছায়, তাই «Agent বলতে অস্বীকার করবে» টেকে না; টেকে কেবল এটাই যে অনুসন্ধান শারীরিকভাবে সেখানে পৌঁছাতেই পারে না। একই কারণে, কোনো সরঞ্জাম না দেওয়া মানেই প্রবেশ-নিয়ন্ত্রণ নয়: মডেল এমন সরঞ্জামের নামেও কল করতে পারে যা তাকে কখনও দেওয়া হয়নি, তাই কেবল-মালিকের শাখাগুলো
executeToolCall-এর ভেতরেaudienceআবার যাচাই করে।দুই ধরনের দফাই মালিকের কী ব্যবহার করে, প্রশ্নকারী peer-এর নয়। মিলে যাওয়া জ্ঞানভাণ্ডারের অংশ
messages.citations_json-এ উদ্ধৃতি হিসেবে রাখা হয়, আর উত্তর শেষ হলে এই দফার তথ্য অ্যাসিনক্রোনাসভাবে দীর্ঘমেয়াদি স্মৃতিতে জমা হয় (peer-এর দফা থেকে আসা সারিতেa2aচিহ্ন বসে, আর তোলার সময় উৎস জানানো হয়)। মালিকের embedding / জ্ঞানভাণ্ডার / tavily কী না থাকলে সুন্দরভাবে কেবল-LLM উত্তরে নেমে আসে (ত্রুটি নেই, উদ্ধৃতি নেই)।allow-এর এবং অনুমোদিতask_user-এর উত্তরের পথ এই একই অর্কেস্ট্রেশন ভাগ করে।
ask='ask'ঝুলন্ত অনুমতিরscope_jsonএই চেহারার:{ kind:'a2a_question', conversation_id, inbound_message_id, sender_did, peer_id, content }— অনুমোদনের সময় উত্তর আবার গড়ে এগিয়ে নিতে এটুকুই যথেষ্ট (agent ও peeruser_id/peer_idধরে তখনই পড়া হয়; পুনরাবৃত্তি-নিরাপদ: উত্তর আগে থেকে থাকলে বাদ)। স্থায়ী নীতি ঠিক করার UI, «সম্পাদনা করে উত্তর», আর push বিজ্ঞপ্তি এখনও backlog-এ।
ফেডারেটেড আবিষ্কার
ডোমেইন ধরে খোঁজা
ডোমেইন acme.com পেলে ক্লায়েন্ট:
১. মূল DID পেতে https://acme.com/.well-known/did.json আনে।
২. ওই ডোমেইনের সব সর্বজনীন Agent-এর তালিকা পেতে https://acme.com/.well-known/agents.json আনে।
৩. একটি বেছে পরিচিতিতে যোগ করে।
ব্যবহারকারীর DID সমাধান
কোনো ব্যবহারকারীর Agent-এর উপ-শনাক্তকারীযুক্ত 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-র স্বাক্ষর যাচাই ঠিক এই পথেই চলে: Signature-Input-এর keyid থেকে স্বাক্ষরকারীর DID তুলুন → উপরের URL-এ সমাধান করুন → verificationMethod থেকে keyid-এর সঙ্গে মেলা সর্বজনীন কী নিয়ে যাচাই করুন। ওই নথি কেবল সর্বজনীন কী-উপাদানই প্রকাশ করে, আর verificationMethod[*].id হলো সংরক্ষিত key_id (অনুরোধের Host থেকে আবার গড়া হয় না), তাই অন্য ইনস্ট্যান্স থেকে সমাধান করে পাওয়া id আর স্থানীয়ভাবে পাওয়া id সবসময় এক।
সর্বজনীন রেজিস্ট্রি (v2+)
NANDA Index বা তেমন কোনো সর্বজনীন রেজিস্ট্রির সঙ্গে যুক্ত হওয়া, যাতে সম্ভব হয়:
- সামর্থ্য ধরে খোঁজা («Modbus বোঝে এমন Agent খুঁজে দাও»)।
- প্রতিষ্ঠান ধরে খোঁজা («ABC Industries-এর Agent»)।
- অবস্থান ধরে খোঁজা («কাছাকাছি সেবা Agent»)।
আস্থার জাল (v2+)
- আমার পরিচিতদের Agent আগে আসে।
- আমার সহকর্মীদের কোম্পানির Agent আগে আসে।
- তৃতীয় পক্ষের সমর্থন (NANDA যাচাই করা) পায় আস্থার ব্যাজ।
স্প্যাম প্রতিরোধ
- প্রতি peer-ডোমেইনে প্রতি মিনিটে হার-সীমা (প্রসেস-লোকাল কাউন্টার; গেটওয়ে একক ইনস্ট্যান্স)।
- শ্বেততালিকায় নেই এমন peer ডিফল্টে নিম্ন অগ্রাধিকারের।
- ব্যবহারকারী কোনো peer-ডোমেইন আটকে দিতে পারেন।
- সুনামের স্কোর (v2+): আর কতগুলো ইনস্ট্যান্স তাকে স্প্যাম বলে চিহ্নিত করেছে।
অনুবাদ কৌশল
- প্রত্যেক Agent নিজের AgentFacts-এ
primary_languageওstyleঘোষণা করে। - ভাষার সীমা পেরোনো কথোপকথনে অনুবাদ হয় লক্ষ্য Agent-এর ভেতরেই (নিজের পরিভাষা ও নথি সে-ই সবচেয়ে ভালো জানে)।
- উদ্ধৃত অংশ সবসময় মূল রূপেই থাকে: ব্যবহারকারী অনুবাদের আগের প্রামাণ্য ভাষ্যটি দেখতে পারেন।
- ডিফল্ট আচরণ
preserve-style(ভঙ্গি অপরিবর্তিত, কেবল ভাষা বদলায়); ব্যবহারের প্রেক্ষাপটেlocalize-styleঘোষণা করা যায় (স্থানীয় রীতিতে মানিয়ে নেওয়া)।
প্রোটোকল বিবর্তনের কৌশল
- সব প্রোটোকলে
@contextবাversionক্ষেত্র থাকে। - ক্লায়েন্ট ও সার্ভার দুই-ই পশ্চাৎ-সঙ্গতি রাখে (অচেনা ক্ষেত্র গ্রহণ করে এবং উপেক্ষা করে)।
- ভাঙা পরিবর্তন হয় মূল সংস্করণ বাড়িয়ে (যেমন
/a2a/v2/)। - NANDA ও Google-এর A2A-র স্কিমা বিবর্তনের সঙ্গে সঙ্গতিপূর্ণ (আমরা উন্মুক্ত বাস্তুতন্ত্রে বাজি ধরছি)।