এই পাতাটি চীনা মূল থেকে অনূদিত। দুটির মধ্যে অমিল হলে মূল পাঠই প্রামাণ্য।

Confer — API নির্দেশনা

ক্লায়েন্ট ↔ সার্ভার এবং সার্ভার ↔ A2A peer-এর সমস্ত API এখানে সংজ্ঞায়িত।

সাধারণ রীতি

{
  "error": {
    "code": "invalid_request",
    "message": "Human-readable message",
    "details": { /* optional */ }
  }
}

প্রমাণীকরণ

ক্লায়েন্ট API (ব্যবহারকারীর ক্লায়েন্ট এটি ব্যবহার করে)

প্রমাণীকরণ

POST   /api/v1/auth/register
POST   /api/v1/auth/login
POST   /api/v1/auth/refresh
POST   /api/v1/auth/logout
POST   /api/v1/auth/password        # {current_password, new_password}
GET    /api/v1/auth/2fa             # {enabled, recovery_codes_left}
POST   /api/v1/auth/2fa/setup       # → {secret, otpauth_uri}
POST   /api/v1/auth/2fa/enable      # {code} → {recovery_codes}
POST   /api/v1/auth/2fa/disable     # {password, totp_code | recovery_code}
POST   /api/v1/auth/oauth/{provider}    # OAuth callback

POST /api/v1/auth/login অনুরোধ:

{
  "username": "laowang",
  "password": "...",
  "device_id": "ios-abc123",
  "device_info": { "platform": "ios", "model": "iPhone 15", "os": "17.1" }
}

উত্তর:

{
  "access_token": "eyJ...",
  "refresh_token": "...",
  "expires_in": 900,
  "user": { /* User object */ }
}

দুই-ধাপের সাইন-ইন (TOTP)। চালু থাকলে login সঠিক পাসওয়ার্ডের পর একটি totp_code (অথেনটিকেটর অ্যাপের ৬ অঙ্ক) বা recovery_code-ও চায়; কোনোটি না থাকলে 401 totp_required দেয়, যা দেখে ক্লায়েন্ট দ্বিতীয় ধাপ দেখায়, আর ভুল কোডে 401 invalid_second_factor। ভুল পাসওয়ার্ডে সবসময় আগে invalid_credentials আসে, তাই অ্যাকাউন্টে দুই-ধাপ চালু কি না তা কখনো প্রকাশ পায় না। প্রতিটি কোড একবারই চলে (ব্যবহৃত সময়-ধাপ লেখা থাকে) এবং প্রতিটি রিকভারি কোড ব্যবহারের পর বাদ যায়; দুটোই একটি শর্তযুক্ত UPDATE-এ খরচ হয়, তাই একসঙ্গে দুটি সাইন-ইন একই কোড দুবার চালাতে পারে না। setup-এর গোপন কী AES-256-GCM-এ এনক্রিপ্ট করে রাখা হয় এবং enable অ্যাপের কোড দিয়ে নিশ্চিত না করা পর্যন্ত কার্যকর হয় না; enable ১০টি রিকভারি কোড শুধু এই একবার দেয় এবং শুধু সেগুলোর হ্যাশ রাখা হয়। disable-এর জন্য পাসওয়ার্ড ও দ্বিতীয় ফ্যাক্টর লাগে। password-এর জন্য বর্তমান পাসওয়ার্ড লাগে এবং এটি বাকি সব সেশন বন্ধ করে দেয়। এগুলো সবই অডিট লগে লেখা হয় এবং প্রতি অ্যাকাউন্টে মিনিটে ১০ বারে সীমিত। enable-এর জন্যও password লাগে, তাই শুধু চুরি করা টোকেন দিয়ে কারও অ্যাকাউন্টের সামনে আক্রমণকারীর অথেনটিকেটর বসানো যায় না; দুই-ধাপ চালু থাকলে password-এর জন্যও দ্বিতীয় ফ্যাক্টর লাগে। পরপর ৫টি ভুল কোড সেই অ্যাকাউন্টের কোড চেষ্টা ১৫ মিনিটের জন্য আটকে দেয় (429 too_many_attempts), যা প্রতি-IP লগইন সীমার অতিরিক্ত। রিকভারি কোড সার্ভার কী দিয়ে HMAC হিসেবে রাখা হয়। যে ব্যবহারকারী অথেনটিকেটর ও রিকভারি কোড দুটোই হারিয়েছেন, তাঁর জন্য অ্যাডমিন POST /api/v1/admin/users/{id}/reset-2fa দিয়ে এটি বন্ধ করতে পারেন (অডিট হয়, আর অ্যাডমিনের নিজের অ্যাকাউন্টে কখনো নয়)।

GET    /api/v1/tokens              # [{id, name, prefix, created_at, last_used_at, expires_at}]
POST   /api/v1/tokens              # {name, expires_in_days?} → {token: {..., token}}
DELETE /api/v1/tokens/{id}

ব্যক্তিগত API টোকেন। যেসব স্ক্রিপ্ট ও টুল মালিক হিসেবে API ডাকে, তাদের জন্য: Authorization: Bearer cfr_…, সেশনের access token যে হেডার ব্যবহার করে সেটিই। টোকেন শুধু তৈরির সময় সাদা লেখায় ফেরত আসে; সার্ভার এর SHA-256 আর চেনার জন্য একটি প্রিফিক্স রাখে। ১–৩৬৫ দিনের ঐচ্ছিক মেয়াদ, প্রতি ব্যবহারকারীর সর্বোচ্চ ২০টি, আর মালিক নিষ্ক্রিয় হলেই টোকেন কাজ বন্ধ করে। টোকেন চ্যাটে মালিক যা পারেন তা করতে পারে, কিন্তু এই কাজগুলো শুধু সাইন-ইন সেশন গ্রহণ করে এবং টোকেনকে 403 session_required দেয়: লগআউট, পাসওয়ার্ড ও দুই-ধাপের সাইন-ইন, টোকেন পরিচালনা, অনুমতি অনুরোধ ও errand কার্ডে সিদ্ধান্ত, LLM কী বদলানো, এবং সব admin রুট। ফাঁস হওয়া টোকেন মালিকের কাজ করে দিতে পারে, কিন্তু তাঁর হয়ে সিদ্ধান্ত নিতে বা তাঁকে বাইরে আটকে দিতে পারে না। তৈরি ও বাতিল দুটোই অডিট হয়।

ব্যবহারকারী ও Agent-এর বিন্যাস

GET    /api/v1/users/me
PATCH  /api/v1/users/me
GET    /api/v1/users/me/keys          # signing keys: current / retiring / revoked
POST   /api/v1/users/me/keys/rotate   # session only
POST   /api/v1/users/me/keys/revoke   # {key_id}, session only
GET    /api/v1/agents/me
PATCH  /api/v1/agents/me
PUT    /api/v1/agents/me/policies
GET    /api/v1/agents/me/llm-keys      # প্রতিটি সরবরাহকারী বিন্যস্ত কি না (কেবল বুলিয়ান ফেরে, কখনও চাবি নয়)
PUT    /api/v1/agents/me/llm-keys      # LLM-এর API চাবি এনক্রিপ্ট করে রাখে
DELETE /api/v1/agents/me/llm-keys/{provider}
GET    /api/v1/agents/me/llm-keys/{provider}/models   # সরবরাহকারীকে সরাসরি জিজ্ঞেস করে তার কী কী মডেল আছে

provider-এর মান আসে @confer/shared-এর সরবরাহকারী-তালিকা থেকে (packages/shared/src/llm/catalog.ts), সঙ্গে টুল সেবা tavily। এই তালিকা গেটওয়ে, agent-runtime আর ক্লায়েন্ট — তিনেই পড়ে: base URL, মডেল-তালিকার পথ আর ডিফল্ট মডেল কেবল ওই এক জায়গাতেই লেখা, তাই নতুন সরবরাহকারী যোগ করা মানে শুধু তালিকাটি বদলানো।

/models সরবরাহকারীর নিজের মডেল-তালিকাই এগিয়ে দেয়; স্থানীয়ভাবে রাখা কোনো তালিকা কখনও ফেরায় না:

{ "models": [{ "id": "gpt-4o" }] }
// খালি তালিকা সবসময় কারণ সঙ্গে আনে; চারটি আলাদা, আর প্রতিটির প্রতিকারও আলাদা
{ "models": [], "error": "no_key" }        // এই সরবরাহকারীর চাবি এখনও বিন্যস্ত হয়নি
{ "models": [], "error": "unauthorized" }  // সরবরাহকারী চাবিটি নাকচ করেছে (401/403)
{ "models": [], "error": "unreachable" }   // সরবরাহকারীর নাগাল মেলেনি, কিংবা সে অন্য ত্রুটি ফিরিয়েছে
{ "models": [], "error": "unsupported" }   // এই সরবরাহকারী মডেল-তালিকার প্রান্তবিন্দুই দেয় না

পরিচিতজন / ভিন্ন Agent

GET    /api/v1/contacts                     # পরিচিতদের তালিকা। পৃষ্ঠাভাগ: ?limit=&offset=
POST   /api/v1/contacts                     # পরিচিত যোগ করুন
GET    /api/v1/contacts/{contact_id}        # একজন পরিচিতের বিবরণ (peer সহ)
DELETE /api/v1/contacts/{contact_id}
PATCH  /api/v1/contacts/{contact_id}        # alias / tags / pinned / muted-এর আংশিক বদল (যে ক্ষেত্র পাঠানো হয়নি তা মোছে না)

POST   /api/v1/contacts/lookup              # DID / ডোমেইন / ব্যবহারকারী-নাম দিয়ে খোঁজ

POST /api/v1/contacts/lookup অনুরোধ:

{
  "method": "domain",          // domain | did | username | qr_code | phone
  "value": "abc-industries.com"
}

GET /api/v1/contacts { contacts, total } ফেরায়। limit-এর ডিফল্ট ৫০, সর্বোচ্চ ১০০; offset-এর ডিফল্ট ০; সাজানো হয় id (ULID) অনুসারে অবরোহী ক্রমে, অর্থাৎ নতুনটি আগে — ক্রম অনন্য ও নির্ধারিত হওয়াই সেই জিনিস যা offset-এর জানালাকে সারি বাদ দেওয়া বা দুবার দেখানো থেকে ঠেকায়। total গোটা গণনা, এই পৃষ্ঠার নয়, আর তা থেকেই ক্লায়েন্ট বোঝে শেষ এসে গেছে। যে limit/offset পড়া যায় না, তা ত্রুটি না দিয়ে ডিফল্ট মান নেয়।

উত্তর: পাওয়া সম্ভাব্য Agent-দের তালিকা। খোঁজ যেসব peer পায় সেগুলো peer_agents-এ লিখে রাখে এবং প্রতিটি প্রার্থীর সঙ্গে স্থানীয় id (peer_id) দেয় — POST /api/v1/contacts ওই id দিয়েই পরিচিত যোগ করে। POST /contacts idempotent: একই peer আবার যোগ করলে ত্রুটি নয়, আগে থেকেই থাকা পরিচিত (200) ফেরে।

পরিচিত যোগ করা আসলে গ্রহীতার সেই সম্মতি যে অন্যজন তার Agent খরচ করতে পারে: পরিচিত হিসেবে যুক্ত peer-ই কেবল আমার Agent-কে উত্তর দেওয়াতে (এবং আমার LLM বাজেট খরচ করাতে) পারে। যুক্ত নয় এমন peer-এর A2A বার্তা অনুমোদনের অপেক্ষায় সংযোগের অনুরোধ হয়ে ঝুলে থাকে; দেখুন 03-protocol.md-এর «সংযোগ সম্মতির দরজা»।

POST   /api/v1/contacts/{contact_id}/policies   # স্থায়ী নীতি ঠিক করুন (গোটাটাই প্রতিস্থাপন, PUT-এর মতো অর্থ)

POST /contacts/{id}/policies-এর body রানটাইম রূপে { default?: 'allow'|'ask_user'|'deny', rules?: [{ action, peer_did?, decision }] } এবং গোটাটাই peer_contacts.policy_overrides_json-এ লেখা হয়। মিলনের অর্থ: ভিতরে আসা A2A অনুরোধে সিদ্ধান্ত নেওয়ার সময় এই প্রতি-পরিচিত অগ্রাহ্য নীতি Agent-এর নীতির উপরে বসে — contact.default থাকলে তা Agent-এর ডিফল্টের জায়গা নেয়, আর contact.rules Agent-এর নিয়মের আগে বসে বলে আগেই মেলে (কোনো পরিচিতের সুনির্দিষ্ট নিয়ম Agent-এর সাধারণ নিয়মের উপরে যায়)। খালি অগ্রাহ্য {} অভেদ: সিদ্ধান্ত বাইটে বাইটে ঠিক তা-ই থাকে যা অগ্রাহ্য ছাড়া হতো।

কথোপকথন

GET    /api/v1/conversations                       # আমার কথোপকথনের তালিকা (প্রথম পাতার জন্য)
POST   /api/v1/conversations                       # নতুন কথোপকথন
GET    /api/v1/conversations/{id}
PATCH  /api/v1/conversations/{id}
DELETE /api/v1/conversations/{id}

GET    /api/v1/conversations/{id}/messages         # পৃষ্ঠাভাগ: ?before=&limit=
POST   /api/v1/conversations/{id}/messages         # বার্তা পাঠান
GET    /api/v1/conversations/{id}/messages/{msg_id}/stream    # SSE দিয়ে LLM-এর উত্তর ধারা-আকারে নিন

POST   /api/v1/conversations/{id}/participants     # অংশগ্রহণকারী যোগ করুন
DELETE /api/v1/conversations/{id}/participants/{p_id}

POST   /api/v1/conversations/{id}/read             # পঠিত চিহ্নিত করুন

GET ডিফল্টভাবে আর্কাইভ না করা কথোপকথন দেয়, আর ?archived=1 দিলে আর্কাইভ করাগুলো; প্রতিটিতে থাকে unread_count, অর্থাৎ ব্যবহারকারী শেষবার পঠিত চিহ্নিত করার পর থেকে (কখনো না করে থাকলে যোগ দেওয়ার পর থেকে) অন্যরা যত বার্তা লিখেছে — নিজের বার্তা গোনা হয় না। PATCH নেয় { name?, archived? } এবং শুধু কথোপকথনের স্রষ্টা এটি ব্যবহার করতে পারেন। POST .../read কলকারীর last_read_at এখনকার সময়ে সেট করে। কোনো পরিচিতি আর্কাইভ করা কথোপকথনে লিখলে সেটি আর্কাইভ থেকে বেরিয়ে সবার উপরে চলে আসে।

GET /api/v1/search/messages?q=&limit= ব্যবহারকারী যেসব কথোপকথনে আছেন (আর্কাইভ করা সহ) সেগুলোর বার্তার লেখায় উপ-স্ট্রিং খোঁজে, নতুনগুলো আগে; q ১–২০০ অক্ষরের, limit ডিফল্ট ২০, সর্বোচ্চ ৫০। ফেরত দেয় { results: [{ message_id, conversation_id, conversation_name, sender_type, content, created_at }] }। Postgres-এর পূর্ণ-লেখা অনুসন্ধানের বদলে উপ-স্ট্রিং মিল ব্যবহার করা হয়, কারণ এর অন্তর্নির্মিত কনফিগারেশন চীনা লেখাকে শব্দে ভাগ করে না।

GET /api/v1/conversations/{id}/export শুধু অংশগ্রহণকারীদের পুরো কথোপকথন ফেরত দেয়, পুরোনোগুলো আগে এবং সর্বোচ্চ ৫০০০টি: { conversation, messages, truncated }; প্রতিটি বার্তায় থাকে sender_type, sender_did, অপর পক্ষের এজেন্টের sender_name, content, content_json ও citations_json। এটি শুধু তথ্য দেয়; Markdown ক্লায়েন্ট পাঠকের ভাষায় তৈরি করে (কে “আপনি” আর কে “আপনার এজেন্ট”)।

POST /api/v1/conversations/{id}/messages অনুরোধ:

{
  "content_type": "text",
  "content": "X100-এর 0x40 রেজিস্টারে কোন ফাংশন কোড লাগে?",
  "in_reply_to": null,
  "via": "web"
}

উত্তর:

{
  "id": "01HXKQ...",
  "delivery_status": "queued",
  "stream_url": "/api/v1/conversations/01HX.../messages/01HXK.../stream"
}

অনুমতি ব্যবস্থাপনা

GET    /api/v1/permissions/pending               # ঝুলে থাকা L2/L3 অনুরোধ
POST   /api/v1/permissions/{id}/decide           # অনুমোদন / নাকচ
GET    /api/v1/permissions/history               # ইতিহাস

POST /api/v1/permissions/{id}/decide অনুরোধ:

{
  "decision": "allow_always",       // allow_once | allow_always | deny | deny_always
  "scope": "peer_action"            // সিদ্ধান্তের পরিধি
}

ঝুলে থাকা অনুরোধের মধ্যে যেগুলোর action='connect', সেগুলো সংযোগের অনুরোধ (অচেনা peer প্রথমবার যোগাযোগ করলে A2A প্রবেশপথ এগুলো তৈরি করে)। অনুমোদন (allow_*) সেই peer-কে peer_contacts-এ লিখে সংযোগ গড়ে; নাকচ করলে গড়ে না।

যেগুলোর action='ask', সেগুলো আগে থেকেই যুক্ত peer-এর ঝুলে থাকা প্রশ্ন — Agent-এর নীতি সেই প্রশ্নে ask_user স্থির করলে A2A প্রবেশপথ এগুলো তৈরি করে (দেখুন 03-protocol.md-এর «ঝুলন্ত ইনবক্স (অনুপস্থিতিতে উত্তর)»)। অনুমোদন (allow_*) হলে Agent সেই ঝুলে থাকা প্রশ্নের উত্তর দেয়; নাকচ হলে দেয় না।

allow_always / deny_always হলো স্থায়ী সিদ্ধান্ত। আটকে থাকা প্রশ্নে এগুলো সেই পরিচিতির policy override-এর শুরুতে { action: 'ask', decision: 'allow' | 'deny' } নিয়ম লেখে (বা প্রতিস্থাপন করে), ফলে এই peer-এর পরের প্রশ্নগুলোর উত্তর দেওয়া হয় বা প্রত্যাখ্যান করা হয়, আটকে রাখা হয় না; মালিক পরিচিতির policy প্যানেলে নিয়মটি দেখতে ও বাতিল করতে পারেন। সংযোগ অনুরোধে deny_always দিলে সেই peer-এর পরের সংযোগ অনুরোধগুলো ইনবক্সে না এসে নিঃশব্দে বাদ পড়ে; DID দিয়ে হাতে peer যোগ করা এখনও কাজ করে।

GET /pending প্রতিটি অনুরোধের সঙ্গে একটি description দেয় (সংযোগের অনুরোধে কে শুরু করেছে ও তার প্রথম বার্তা; প্রশ্নে কে জিজ্ঞেস করছে ও প্রশ্নের পাঠ) যাতে মালিক বিচার করতে পারেন।

প্রকল্প-স্মৃতি (Claude Code সংযুক্তি সংক্রান্ত)

GET    /api/v1/projects/{project_id}/peers              # এই প্রকল্পে যেসব peer-এর স্মৃতি আছে (join থেকে name/did সহ)  ✅ বাস্তবায়িত
POST   /api/v1/projects/{project_id}/peers              # peer-কে প্রকল্পে স্পষ্টভাবে নিবন্ধন করুন   🔜 backlog
GET    /api/v1/projects/{project_id}/peers/{peer_id}/facts        # ✅ বাস্তবায়িত
PUT    /api/v1/projects/{project_id}/peers/{peer_id}/facts        # ✅ বাস্তবায়িত
GET    /api/v1/projects/{project_id}/peers/{peer_id}/decisions    # ✅ বাস্তবায়িত
PUT    /api/v1/projects/{project_id}/peers/{peer_id}/decisions    # ✅ বাস্তবায়িত

অর্থ সম্পর্কিত টীকা (v0.1):

জ্ঞানভাণ্ডার (RAG)

GET    /api/v1/knowledge-bases                                  # আমার জ্ঞানভাণ্ডারের তালিকা
POST   /api/v1/knowledge-bases                                  # নতুন তৈরি
PATCH  /api/v1/knowledge-bases/{kb_id}                          # নাম/বিবরণ বদলান, আর বাইরের Agent-এর জন্য খোলা কি না
DELETE /api/v1/knowledge-bases/{kb_id}                          # তার সব নথি ও ভেক্টর সহ মুছে ফেলুন

GET    /api/v1/knowledge-bases/{kb_id}/documents                # পৃষ্ঠাভাগ: ?limit=&offset=
POST   /api/v1/knowledge-bases/{kb_id}/documents                # multipart আপলোড, ক্ষেত্রের নাম file
DELETE /api/v1/knowledge-bases/{kb_id}/documents/{doc_id}
POST   /api/v1/knowledge-bases/{kb_id}/documents/{doc_id}/retry # আবার সূচিভুক্ত করুন

POST /knowledge-bases-এর body হলো { name, description? } (name ১–২৫৫ অক্ষর), উত্তর 201 + { knowledge_base }।

PATCH /knowledge-bases/{kb_id}-এর body হলো { name?, description?, shared_with_peers? }, উত্তর { knowledge_base }। shared_with_peers কেবল এখানেই বদলানো যায়, তৈরির সময় গ্রাহ্য হয় না: প্রতিটি ভাণ্ডার «কেবল নিজের জন্য» জন্মায়, আর বাইরে খোলা দ্বিতীয় একটি সচেতন কাজ।

shared_with_peers ঠিক করে ভিতরে আসা A2A প্রশ্ন এই ভাণ্ডারে খুঁজতে পারবে কি না, ডিফল্ট false। মালিক নিজে ওয়েবে কথা বললে এতে কিছু বদলায় না — তিনি সবসময় সবই খুঁজে পান। এই সীমারেখা খোঁজের পরিধিতে পড়তে হবে, প্রম্পটে নয়: অন্য পক্ষের প্রশ্ন আর মালিকের নির্দেশ মডেলের কাছে একই জাতের পাঠ হয়ে পৌঁছায়, তাই «Agent নিজেই বুঝে নেবে কী বলা যায়» কোনো সীমারেখাই নয়। একই কারণে ভিতরে আসা A2A প্রশ্ন কোনো দীর্ঘমেয়াদি স্মৃতি ডাকে না — দীর্ঘমেয়াদি স্মৃতি মালিকের নিজের কথোপকথন থেকে নিংড়ানো, আর তার একটি এন্ট্রিও এই ইনস্ট্যান্স ছেড়ে যাওয়ার যোগ্য বলে চিহ্নিত নয়।

GET /knowledge-bases { knowledge_bases } ফেরায় এবং পৃষ্ঠায় ভাগ হয় না: একজন ব্যবহারকারীর ভাণ্ডার হাতে তৈরি, সংখ্যায় সীমিত।

GET /{kb_id}/documents { documents, total } ফেরায়। limit-এর ডিফল্ট ৫০, সর্বোচ্চ ১০০; offset-এর ডিফল্ট ০; সাজানো id (ULID) অনুসারে অবরোহী ক্রমে, অর্থাৎ নতুনটি আগে — অনন্য ও নির্ধারিত ক্রমই offset-এর জানালাকে সারি বাদ দেওয়া বা দুবার দেখানো থেকে ঠেকায়। total গোটা গণনা, এই পৃষ্ঠার নয়। যে limit/offset পড়া যায় না তা ডিফল্ট নেয়। এই অংশে এটিই একমাত্র তালিকা যা অসীম বাড়ে, কারণ আপলোডের গন্তব্যই তো জ্ঞানভাণ্ডার।

আপলোড যায় multipart/form-data-য়, ফাইলের ক্ষেত্রের নাম সবসময় file, আর একটি ফাইলের সর্বোচ্চ ১০ MB (তার বেশি হলে 400 bad_request)। Content-Type ফর্মে থাকলে সেটিই, না থাকলে এক্সটেনশন থেকে অনুমান। উত্তর 201 + { document }, আর তখনই status processing হয়ে গেছে: টুকরো করা, ভেক্টর বানানো ও Qdrant-এ লেখা — সবই উত্তরের পরে অ্যাসিনক্রোনাসভাবে চলে, আপলোড প্রান্তবিন্দু সেগুলোর অপেক্ষা করে না। তাই ক্লায়েন্ট status না বদলানো পর্যন্ত নথির তালিকা ঘেঁটে দেখে।

status-এর মান:

মান অর্থ
processing সংরক্ষিত হয়ে গেছে, টুকরো/ভেক্টর হচ্ছে। আপলোড ও retry-এর পরের প্রাথমিক অবস্থা
ready খোঁজা যায়। chunk_count ওই নথির টুকরোর সংখ্যা
failed সূচিভুক্তি ব্যর্থ (পার্সিং, embedding চাবির অভাব, বা ভেক্টর-ভাণ্ডারে লেখা ব্যর্থ)

POST /{doc_id}/retry বস্তু-ভাণ্ডার থেকে মূল ফাইল এনে আবার সূচিভুক্ত করে; আগে ওই নথির থাকা ভেক্টরগুলো মুছে দেয়, তাই টুকরো দুবার হয় না। মূল ফাইল আর না থাকলে (storage_key খালি) কিংবা নথি এখনও processing-এ থাকলে 400 ফেরে। উত্তর { document }, status আবার processing আর chunk_count শূন্য।

জ্ঞানভাণ্ডার মুছলে তার সব নথি-সারি আর Qdrant-এর ভেক্টর ধারাবাহিকভাবে মুছে যায়; একটি নথি মুছলে তার ভেক্টর ও বস্তু-ভাণ্ডারের মূল ফাইলও পরিষ্কার হয়। ভেক্টর বা বস্তু-ভাণ্ডারের পরিষ্কার ব্যর্থ হলেও ডেটাবেসের মোছা আটকায় না — মুছে যাওয়া তথ্যের দিকে তাক করা সারির চেয়ে অনাথ বস্তু রেখে দেওয়া ভালো।

সব প্রান্তবিন্দু user.sub-এ সীমাবদ্ধ: অন্যের ভাণ্ডার বা নথিতে হাত দিলে 404 ফেরে (403 নয়, যাতে অস্তিত্বই ফাঁস না হয়)।

রিভার্স প্রক্সিকে ১০ MB body যেতে দিতে হবে। infra/nginx.conf-এ /api/-তে client_max_body_size 10m দেওয়া আছে; nginx-এর ডিফল্ট ১ MB-তে ১–১০ MB ফাইল গেটওয়ে পর্যন্ত পৌঁছায়ই না, আর ব্রাউজার পায় nginx-এর নিজের 413 পাতা।

সংযুক্তি

POST   /api/v1/attachments                       # multipart upload
GET    /api/v1/attachments/{id}/content          # ফাইলটি নিজেই, আপলোডকারী বা সেই বার্তার কথোপকথনের অংশগ্রহণকারীদের জন্য

আপলোড শুরুতে কোনো বার্তার সঙ্গে যুক্ত থাকে না; POST /conversations/{id}/messages attachment_ids (সর্বোচ্চ ৫টি) দিয়ে সেটি দাবি করে, আর শুধু প্রেরকের নিজের, এখনও না-পাঠানো আপলোডই দাবি করা যায়। লেখা ছাড়া শুধু সংযুক্তিসহ বার্তাও বৈধ। ছবি বাইট দেখে চেনা হয় (PNG / JPEG / GIF / WebP, প্রতিটি সর্বোচ্চ ৫ MB) এবং মডেলের কাছে ছবি ব্লক হিসেবে যায়; PDF / DOCX / XLSX / লেখা আপলোডের সময় পড়া হয় (সর্বোচ্চ ১০ MB) এবং প্রতিটি টার্নে তাদের লেখা <attachment> হিসেবে বার্তার সঙ্গে যায়। SVG লেখা হিসেবে ধরা হয় এবং কখনো ছবি হিসেবে ইনলাইন দেখানো হয় না। content শুধু চেনা ছবিই ইনলাইন দেয়; বাকি সব application/octet-stream ডাউনলোড। তালিকার প্রতিটি বার্তায় attachments থাকে। কথোপকথন মুছলে তার সংযুক্তিও মুছে যায়।

ব্যবহার

GET /api/v1/usage?month=YYYY-MM    # এক মাসে এজেন্ট টার্নের টোকেন ব্যবহার; ডিফল্ট চলতি মাস

{ month, rows } ফেরত দেয়। প্রতিটি সারি প্রদানকারী × মডেল × শ্রোতার একটি গোষ্ঠী: turns, unreported (যেসব টার্নের ব্যবহার প্রদানকারী জানায়নি), failed, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens। মাস UTC অনুযায়ী ভাগ হয়; month যদি YYYY-MM না হয় তবে 400 ফেরত আসে। কেবল অনুরোধকারীর নিজের ডেটা ফেরত আসে।

WebSocket

প্রান্তবিন্দু

WSS  /ws?token=<access_token>&device_id=<device_id>

হ্যান্ডশেকের প্রমাণীকরণ REST-এর মতোই, «স্বাক্ষর মিলেছে তো ছেড়ে দাও» নয়: typ হতে হবে access, sid এমন একটি সেশনের দিকে হতে হবে যা এখনও আছে, আর অ্যাকাউন্ট disabled হতে পারবে না। তিনটিই অপরিহার্য — এগুলো ছাড়া নিষিদ্ধ অ্যাকাউন্টের কেবল টোকেন না ফুরালেই চলে, সে বারবার যুক্ত হয়ে বার্তা পেতে থাকবে, অথচ নিষেধাজ্ঞা নিজে (সব সেশন মুছে দেওয়া) এই পথে কিছুই বাতিল করে না। নিষেধাজ্ঞা ওই ব্যবহারকারীর আগে থেকে খোলা socket-ও বন্ধ করে: nginx /ws-কে এক দিনের proxy_read_timeout দেয়, আর পরের হ্যান্ডশেক আটকালে আগে জোড়া সংযোগ আটকায় না।

হ্যান্ডশেকে যাচাই হওয়া টোকেন socket-এর হয়ে চিরকাল জামিন থাকে না: access token-এর মেয়াদ ফুরালে সার্ভার 4001 কোডে সংযোগ বন্ধ করে, আর ক্লায়েন্ট আবার যুক্ত হওয়ার আগে টোকেন নবায়ন করে। লগআউট, বা refresh token পুনর্ব্যবহার ধরা পড়া, যে সেশন মুছে দেয় তার অধীনে খোলা socket-গুলো 1008 কোডে বন্ধ করে।

বার্তার বিন্যাস

সব WS বার্তা JSON, আর তাতে type ক্ষেত্র থাকে:

{ "type": "message.new", "data": { /* ... */ } }

ক্লায়েন্ট → সার্ভার

ping                          // হৃৎস্পন্দন
subscribe.conversation        // কোনো কথোপকথনে সাবস্ক্রাইব (সার্ভার যাচাই করে আপনি অংশগ্রহণকারী কি না)
unsubscribe.conversation
typing.start                  // কেবল সাবস্ক্রাইব করা কথোপকথনেই কাজ করে
typing.stop
read.ack                      // পড়ার স্বীকৃতি

typing.*-এর সম্প্রচার ওই socket-এর সাবস্ক্রিপশনের সেট মেনে চলে। সাবস্ক্রিপশনে দরজা থাকলে অথচ টাইপিংয়ের ঘটনায় না থাকলে, কেবল একটি কথোপকথনের id জানাই যথেষ্ট — তাতে «অমুক লিখছে» ঢুকিয়ে দেওয়া যায়, নিজের ব্যবহারকারী-নাম সমেত।

সার্ভার → ক্লায়েন্ট

pong
message.new                   // নতুন বার্তা
message.updated               // সংরক্ষিত, এখনও পাঠানো হয় না
message.deleted               // সংরক্ষিত, এখনও পাঠানো হয় না
typing.update                 // কে লিখছে
presence.update               // পরিচিতজনের আসা-যাওয়া
permission.request            // যে অনুমতির অনুরোধে ব্যবহারকারীকে সিদ্ধান্ত নিতে হবে
agent.status                  // আমার Agent একটি পরিচিতিকে উত্তর দিচ্ছে: {conversation_id, state: thinking|tool|idle, tool?}; লেখা তৈরি করে ক্লায়েন্ট
conversation.updated          // একটি পরিচিতি {conversation_id}-এ লিখেছে (হয়তো নতুন কথোপকথন); ক্লায়েন্ট তালিকা হালনাগাদ করে

message.new উদাহরণ:

{
  "type": "message.new",
  "data": {
    "id": "01HXKQ...",
    "conversation_id": "01HX...",
    "sender_type": "peer_agent",
    "sender_id": "01HY...",
    "sender_did": "did:web:acme.com:agents:support",
    "content_type": "text",
    "content": "0x03, Read Holding Registers দিয়ে…",
    "citations": [
      {
        "source": "X100 যোগাযোগ নির্দেশিকা v3.2",
        "page": 87,
        "url": "https://acme.com/manuals/x100-v3.2.pdf#page=87",
        "trust_level": "authoritative"
      }
    ],
    "language": "bn",
    "created_at": "2024-11-15T14:30:00Z"
  }
}

permission.request উদাহরণ:

{
  "type": "permission.request",
  "data": {
    "id": "01HXP...",
    "level": "L2",
    "action": "share_files",
    "scope": {
      "peer": "did:web:acme.com:agents:support",
      "paths": ["src/modbus/"],
      "exclude": [".env", "secrets/"]
    },
    "peer_name": "ABC Agent",
    "peer_did": "did:web:acme.com:agents:support",
    "requested_at": "2024-11-15T14:30:00Z"
  }
}

পেলোডে description নেই, আর সেটি ইচ্ছাকৃত। সার্ভার জানে না পাঠক কোন ভাষায় পড়েন, তাই সে কেবল কাঠামোবদ্ধ তথ্য পাঠায় (action + peer-এর পরিচয় + সংরক্ষিত scope), আর অনুমোদনের সময় যে বাক্যটি পড়া হয় তা ক্লায়েন্ট নিজের i18n দিয়ে গড়ে (packages/client/src/lib/permission-text.ts)। এই চুক্তিটি @confer/shared-এর permissionRequestEventSchema-র নিজস্ব: গেটওয়ে পাঠানোর আগে তা দিয়েই parse করে, ক্লায়েন্ট পাওয়ার পর তা দিয়েই।

GET /api/v1/permissions/pending-এর প্রতিটি সারির আকার হুবহু একই (বাড়তি একটি decision ক্ষেত্র সহ) এবং একই নির্মাতা থেকে আসে, তাই ঘেঁটে পাওয়া সারি আর socket-এ ঠেলে দেওয়া সারি বাইটে বাইটে মেলে।

SSE (LLM streaming)

GET  /api/v1/conversations/{id}/messages/{msg_id}/stream
Accept: text/event-stream

ঘটনার ধরন:

event: token
data: {"text":"0x03, "}

event: token
data: {"text":"0x03 "}

event: tool_call
data: {"tool":"agent_network.ask_peer","args":{...}}

event: tool_result
data: {"result":"..."}

event: citation
data: {"source":"X100 যোগাযোগ নির্দেশিকা v3.2","page":87}

event: done
data: {"finish_reason":"stop","tokens_used":523}

A2A API (বাইরের দিকে — Confer-এর অন্য ইনস্ট্যান্স এটি ডাকে)

বিস্তারিত docs/03-protocol.md-এ। এখানে কেবল প্রান্তবিন্দুগুলোর তালিকা।

একই উপসর্গের নিচে দুটি বাইন্ডিং পাশাপাশি থাকে আর একই দরজাগুলোর সেট (a2a/inbound.ts) দিয়েই যায়; তফাত কেবল তারের উপরের বিন্যাসে।

A2A-র প্রমিত HTTP+JSON বাইন্ডিং (পথগুলো নির্দেশনার §11.3 থেকে হুবহু নেওয়া; Agent Card এটিই ঘোষণা করে):

POST   /a2a/v1/message:send              # SendMessage → Task
GET    /a2a/v1/tasks/{id}                # GetTask
GET    /a2a/v1/tasks                     # ListTasks(কার্সর দিয়ে পৃষ্ঠাভাগ)
POST   /a2a/v1/tasks/{id}:cancel         # CancelTask → TaskNotCancelable
POST   /a2a/v1/message:stream            # SendStreamingMessage → SSE: Task, then statusUpdate
GET|POST /a2a/v1/tasks/{id}:subscribe    # SubscribeToTask → SSE
POST   /a2a/v1/tasks/{id}/pushNotificationConfigs            # → TaskPushNotificationConfig
GET    /a2a/v1/tasks/{id}/pushNotificationConfigs            # → {configs, nextPageToken}
GET    /a2a/v1/tasks/{id}/pushNotificationConfigs/{configId}
DELETE /a2a/v1/tasks/{id}/pushNotificationConfigs/{configId} # idempotent
GET    /a2a/v1/extendedAgentCard         # অবাস্তবায়িত → UnsupportedOperation

স্ট্রিমিং ও পুশ। message:stream একই যাচাই ও গেটের মধ্য দিয়ে যায় যেমন message:send, এবং text/event-stream-এ উত্তর দেয়, প্রতিটি StreamResponse-এর জন্য একটি data: লাইন: প্রথমে {task}, তারপর কাজ শেষ হলে (বা কারও অনুমোদনের অপেক্ষায় AUTH_REQUIRED-এ পৌঁছালে) একটি {statusUpdate}, তারপর স্ট্রিম বন্ধ। এই পথে উত্তর আংশিক আউটপুট ছাড়া একটি LLM টার্ন, তাই গ্রানুলারিটি কাজ-ঘটনা স্তরের। :subscribe GET ও POST দুটোই নেয় (proto-তে GET, spec-এর টেবিলে POST) এবং শেষ হওয়া কাজে UnsupportedOperation দেয়। পুশ: peer কোনো কাজে webhook নিবন্ধন করে (বা message:send-এ configuration.taskPushNotificationConfig দেয়), আর কাজ যে অবস্থায় পৌঁছায় তা একবার {task} হিসেবে POST হয়, Authorization: {scheme} {credentials} সহ এবং token থাকে X-A2A-Notification-Token-এ। webhook অবশ্যই https এবং পাবলিক ঠিকানায় রিজলভ হতে হবে (আউটবাউন্ড A2A-র একই SSRF নিয়ম, ডেলিভারির সময় আবার যাচাই হয়, রিডাইরেক্ট মানা হয় না); ক্রেডেনশিয়াল সিল করে রাখা হয় আর শুধু scheme ফেরত দেওয়া হয়। প্রতি কাজে সর্বোচ্চ ৫টি, শুধু নিবন্ধনকারী peer দেখতে বা মুছতে পারে।

Confer-এর নিজস্ব উপভাষা (ইনস্ট্যান্সে-ইনস্ট্যান্সে; /.well-known/agents.json দিয়ে খুঁজে পাওয়া যায়):

POST   /a2a/v1/messages                  # বাইরের Agent-এর বার্তা নেয়
GET    /a2a/v1/stream/{message_id}       # উত্তর ধারা-আকারে টানে (SSE)
GET    /a2a/v1/agent-facts/{agent_did}   # প্রকাশ্য AgentFacts

সব A2A প্রান্তবিন্দুতে HTTP বার্তা-স্বাক্ষরের যাচাই বাধ্যতামূলক, ব্যতিক্রম কেবল agent-facts: /.well-known/agents.json-এর মতোই এটি একই দৃশ্যমানতার প্রকাশ্য আবিষ্কার-নথি, তাই অপ্রকাশ্য বা নিষ্ক্রিয় Agent সবসময় 404 দেয়। এর capabilities-এ থাকে কেবল মালিকের সংরক্ষিত NANDA আকারের ({type, scope, languages}) সক্ষমতা।

.well-known endpoints

GET    /.well-known/did.json                # মূল DID নথি
GET    /.well-known/agents.json             # এই ইনস্ট্যান্সের সব প্রকাশ্য Agent-এর তালিকা
GET    /.well-known/agent-card.json         # A2A প্রমিত Agent Card (কেবল যখন ইনস্ট্যান্সে একটিই প্রকাশ্য Agent)
GET    /.well-known/openid-configuration    # ভবিষ্যতে: OIDC সঙ্গতি (v2)

A2A প্রমিত Agent Card (আন্তঃক্রিয়াশীল আবিষ্কার-স্তর)

GET    /agents/{username}/agent-card.json   # ওই Agent-এর A2A প্রমিত Card
GET    /.well-known/agent-card.json         # একই, কেবল যখন এই ইনস্ট্যান্সে একটিই প্রকাশ্য Agent

Linux Foundation-এর Agent2Agent v1.0-এর AgentCard অনুসারে (ক্ষেত্রগুলো a2aproject/A2A-র specification/a2a.proto @ v1.0.1 থেকে, proto3-এর JSON মানচিত্রণে, তাই camelCase)। উদ্দেশ্য হলো A2A বাস্তুতন্ত্র যেন এই ইনস্ট্যান্সের Agent খুঁজে পায় — নাম মিলত কিন্তু প্রোটোকল মিলত না: অন্য পক্ষের আবিষ্কার-নথি থাকে /.well-known/agent-card.json-এ, অথচ এই ইনস্ট্যান্সে ছিল কেবল /.well-known/agents.json।

কয়েকটি সচেতন সিদ্ধান্ত:

বার্তা-স্তর (Task-এর অর্থ)

POST /a2a/v1/message:send নির্দেশনার SendMessageRequest নেয় আর Task ফেরায়। একটি task মানেই একটি ভিতরে আসা প্রশ্ন: তার id সেই বার্তার id, contextId সেই কথোপকথন যেখানে সেটি জমা থাকে, আর অবস্থা পরে যা ঘটে তা থেকে বেরিয়ে আসে — একই ঘটনার ছায়া রাখতে আলাদা tasks টেবিল বানানো হয় না।

Confer-এর অ্যাসিনক্রোনাস + সম্মতির দরজা মডেলটি নির্দেশনার অবস্থা-যন্ত্রে ঠিকঠাক বসে যায়:

পরিস্থিতি অবস্থা
Agent উত্তর দিচ্ছে TASK_STATE_WORKING
উত্তর শেষ TASK_STATE_COMPLETED
এই পালাটা শুরুই হতে পারছে না (মডেল বিন্যস্ত নেই, কিংবা সরবরাহকারী ত্রুটি দিয়েছে) TASK_STATE_FAILED
ask_user নীতিতে ঝুলে আছে, মালিকের অনুমোদনের অপেক্ষায় TASK_STATE_AUTH_REQUIRED(বিঘ্নের অবস্থা, চূড়ান্ত নয়)
মালিক নাকচ করেছেন TASK_STATE_REJECTED

দুই জায়গায় ফেরানোর মতো task-ই নেই, কারণ সারিই তৈরি হয়নি: অচেনা peer (যা ঝুলন্ত সংযোগ-অনুরোধ হয়ে থাকে) আর নীতির সরাসরি নাকচ। দুটোই 403 PERMISSION_DENIED ফেরায় এবং ErrorInfo.metadata.confer_status দিয়ে আলাদা হয় — এমন একটি task id বানিয়ে দেওয়া যা পরের ডাকেই 404 দেবে, তার চেয়ে এ ভালো।

বাকি আচরণ নির্দেশনার সঙ্গে বিন্দু ধরে ধরে মেলানো: ত্রুটির শরীর google.rpc.Status-এর আকারের আর তাতে ErrorInfo.reason সবসময় থাকে (একাধিক A2A ত্রুটি একই HTTP কোড ভাগ করে, আর reason-ই একমাত্র ক্ষেত্র যা তাদের আলাদা করে); যে ক্লায়েন্ট আবশ্যিক এক্সটেনশন ঘোষণা করেনি তাকে §3.3.4 অনুসারে ExtensionSupportRequiredError দেওয়া হয়, কিছুই ব্যাখ্যা করে না এমন 401 নয়; historyLength=0 মানে গোটা ক্ষেত্রটাই বাদ দেওয়া, খালি অ্যারে পাঠানো নয়; আর nextPageToken সবসময় থাকে, পরের পাতা না থাকলে খালি স্ট্রিং হয়ে।

দুটি সচেতন বিচ্যুতি, দুটোই কোডের মন্তব্যে লেখা: অবরোধক message:send-এর অপেক্ষার উপরের সীমা আছে (৫৫ সেকেন্ড, তারপর এখনও WORKING অবস্থার task ফিরিয়ে ক্লায়েন্টকে ঘেঁটে দেখতে দেওয়া হয়) — §3.2.2 সময়ের বেরোনোর পথ দেয় না, অথচ LLM-এর একটি ডাকের কোনো ঊর্ধ্বসীমা নেই; আর messageId দিয়ে idempotency (§3.3.1-এর একটি MAY) করা হয়নি, কারণ ভাড়াটে-নিরাপদ অনন্য চাবির জন্য মালিকের পরিধি দরকার, আর প্রথম বার্তার তার-বিন্যাসে তা মেলে না।

Webhooks (ঐচ্ছিক, v1.5+)

বাইরের তন্ত্র যাতে ঘটনায় সাবস্ক্রাইব করতে পারে:

POST   /api/v1/webhooks
GET    /api/v1/webhooks
DELETE /api/v1/webhooks/{id}

সমর্থিত ঘটনা: message.new.peer, permission.granted, thread.archived।

হার-সীমার নীতি

পথ সীমা
/api/v1/auth/login ১০/মিনিট প্রতি IP
/api/v1/auth/register ৩/ঘণ্টা প্রতি IP
/api/v1/conversations/*/messages POST ৬০/মিনিট প্রতি ব্যবহারকারী
/a2a/v1/* ৬০/মিনিট প্রতি IP ও পাথ, পুরো /a2a/v1 জুড়ে ৩০০/মিনিট প্রতি IP
A2A প্রশ্ন (দুটি বাইন্ডিংই) প্রতি peer DID প্রতি মালিক ২০/মিনিট ও ৩০০/দিন; প্রতি peer ডোমেইন ৬০/মিনিট। ছাড়ালে → 429 / RESOURCE_EXHAUSTED, Retry-After সহ
/api/v1/consult/{peer} POST ৩০/মিনিট প্রতি ব্যবহারকারী
WSS প্রতি ব্যবহারকারীতে সর্বোচ্চ ১০টি যুগপৎ সংযোগ

সীমা ছাড়ালে উত্তর:

HTTP/1.1 429 Too Many Requests
Retry-After: 30
Content-Type: application/json

{ "error": { "code": "rate_limited", "message": "Too many requests" } }

পরামর্শ API (ব্যবহারকারীর উদ্যোগে বাইরে যাওয়া A2A)

ব্যবহারকারী (কিংবা তাঁর হয়ে MCP সার্ভার) যেন আগে থেকেই পরিচিত কোনো peer Agent-কে নিজের উদ্যোগে প্রশ্ন করতে এবং পরে অ্যাসিনক্রোনাস উত্তর নিতে পারেন। স্বাক্ষর ও পৌঁছে দেওয়া পুরোটাই গেটওয়ের ভিতরে হয়; ব্যক্তিগত চাবি গেটওয়ে ছাড়ে না।

«কথোপকথন API»-র সঙ্গে তফাত: /api/v1/conversations + /api/v1/stream হলো নিজের স্থানীয় LLM সহকারীর সঙ্গে কথা বলা; /api/v1/consult হলো যা A2A দিয়ে অন্যের Agent-এর কাছে যায়।

POST /api/v1/consult/:peerId

type='consult' কথোপকথন শুরু করে বা চালিয়ে যায় (প্রতি peer-এ সেই একই কথোপকথন কাজে লাগে), আর message.type='question'-এ স্বাক্ষর করে তা পৌঁছে দেয়।

// অনুরোধের body (consultRequestSchema)
{ "question": "চাবি কীভাবে ঘোরাব?", "code_context": "…ঐচ্ছিক কোড…", "language": "bn" }
উত্তর অর্থ
201 { conversation_id, message_id, status: "sent" } স্বাক্ষরিত ও পৌঁছে দেওয়া হয়েছে
502 { ..., status: "failed", error } পৌঁছে দেওয়া ব্যর্থ (peer অফলাইন / endpoint নেই / স্বাক্ষরের সমস্যা)
403 not_a_contact peer বর্তমান ব্যবহারকারীর পরিচিত নয়

GET /api/v1/consult/:conversationId/reply?after=:messageId&wait=:seconds

peer-এর অ্যাসিনক্রোনাস উত্তরের জন্য দীর্ঘ অপেক্ষা (উত্তর ভিতরে আসা /a2a/v1/messages দিয়ে তার thread_id সমেত আসে, আর গেটওয়ে সেটিকে ঠিক সুতোয় ফিরিয়ে ঝোলায়)। wait-এর সীমা ৫৫ সেকেন্ড।

GET /api/v1/consult/:conversationId

ওই পরামর্শ-সুতোর পুরো বার্তা-ইতিহাস ফেরায় (সর্বোচ্চ ২০০টি)।

চুক্তি: ভিতরে আসা A2A কেবল message.type==='question'-এ স্থানীয় agent-এর স্বয়ংক্রিয় উত্তর চালু করে; answer/notification কেবল জমা হয় ও সম্প্রচারিত হয়, যাতে পরামর্শের উত্তর অনন্ত প্রশ্নোত্তর শুরু করে না দেয়।

← Confer-এ ফিরে যান A2A · DID:web · RFC 9421 · NANDA