الكتابات/tutorial/2026/07
Tutorial6 يوليو 2026·28 دقيقة

بروتوكول A2A بلغة TypeScript: بناء وكلاء ذكاء اصطناعي متوافقين مع معيار Agent2Agent (2026)

تعلّم كيفية بناء خادم وعميل وكيل متوافقين مع بروتوكول A2A بلغة TypeScript باستخدام @a2a-js/sdk. يغطي هذا الدليل العملي بطاقات الوكيل Agent Cards وواجهة AgentExecutor وبث تحديثات المهام والمخرجات Artifacts وربط وكيلك بنموذج Claude للحصول على ذكاء حقيقي.

يتكاثر وكلاء الذكاء الاصطناعي داخل الشركات: وكيل يصنّف تذاكر الدعم، وآخر يدقق الفواتير، وثالث يكتب ملاحظات الإصدارات. المشكلة أنهم لا يستطيعون التحدث مع بعضهم البعض. كل وكيل يعيش خلف واجهة REST مخصصة بأشكال حمولات خاصة به، وخصوصيات مصادقة مختلفة، وتعريفه الخاص لما تعنيه «المهمة». وربط الوكيل «أ» بالوكيل «ب» يعني كتابة تكامل هش جديد في كل مرة.

بروتوكول Agent2Agent (اختصارًا A2A) يحل هذه المشكلة تحديدًا. أعلنت عنه Google في الأصل عام 2025 وتحتضنه اليوم مؤسسة Linux Foundation، وهو معيار مفتوح يتيح لوكلاء «معتمين» (لا يكشفون بنيتهم الداخلية) اكتشاف بعضهم البعض وتبادل الرسائل وتفويض المهام وبث تحديثات التقدم — دون كشف حالتهم الداخلية أو تعليماتهم أو أدواتهم. يمكنك اعتباره الضلع الثالث المفقود في منظومة توافقية الوكلاء:

  • MCP يربط الوكيل بـأدواته وبياناته (غطينا ذلك في درس بناء خادم MCP)
  • ACP يربط المحرر أو الواجهة بالوكيل (راجع درس وكيل ACP)
  • A2A يربط الوكلاء بوكلاء آخرين، عبر الفرق والموردين والسحابات المختلفة

في هذا الدرس ستبني منظومة A2A كاملة بلغة TypeScript باستخدام الحزمة الرسمية @a2a-js/sdk: خادم وكيل ينشر بطاقة وكيل Agent Card وينفّذ المهام ويبث تحديثات الحالة والمخرجات، بالإضافة إلى عميل يكتشف الوكيل ويستهلك تدفق أحداثه. وفي الخطوة الأخيرة ستوصل Claude بالمنفّذ ليُنتج وكيلك إجابات حقيقية بدلًا من نصوص جاهزة.

المتطلبات المسبقة

قبل البدء، تأكد من توفر ما يلي:

  • Node.js إصدار 20 أو أحدث
  • أساسيات TypeScript (الواجهات، async/await، الأنواع العامة)
  • إلمام بإطار Express أو أي إطار خادم HTTP آخر
  • مفتاح API من Anthropic للخطوة الأخيرة (اختياري لكنه موصى به)
  • محرر أكواد (يُنصح بـ VS Code)

ما الذي ستبنيه

ستبني وكيل تلخيص بحثي مكشوفًا عبر بروتوكول A2A:

  1. خادم وكيل على المنفذ 4000 يعلن عن نفسه عبر بطاقة وكيل على عنوان URL معروف
  2. منفّذ AgentExecutor يستقبل موضوعًا ويمر بدورة حياة المهمة كاملة (مُرسلة، قيد العمل، مكتملة) ويُصدر مستند تلخيص كمخرج Artifact
  3. عميل يكتشف الوكيل من بطاقته ويرسل الرسائل ويستهلك التحديثات المتدفقة في الوقت الفعلي
  4. نسخة من المنفّذ مدعومة بـ Claude تولّد ملخصات حقيقية

في النهاية، سيتمكن أي منسّق متوافق مع A2A — مثل LangGraph أو CrewAI أو Google ADK أو حتى وكيل آخر كتبته بنفسك — من اكتشاف وكيلك وتفويض العمل إليه دون أي كود تكامل مخصص.

الخطوة 1: إعداد المشروع

أنشئ مشروعًا جديدًا وثبّت الحزمة. إطار Express تبعية نظيرة مطلوبة لتكامل خادم HTTP:

mkdir a2a-research-agent && cd a2a-research-agent
npm init -y
npm install @a2a-js/sdk express uuid
npm install -D typescript tsx @types/express @types/node @types/uuid
npx tsc --init

اضبط "type": "module" في ملف package.json وأضف سكربتين:

{
  "type": "module",
  "scripts": {
    "server": "tsx src/server.ts",
    "client": "tsx src/client.ts"
  }
}

تأتي حزمة @a2a-js/sdk بثلاث نقاط دخول: التصدير الجذري للأنواع المشتركة (Message وTask وAgentCard)، و@a2a-js/sdk/server لأصناف المنفّذ ومعالج الطلبات، و@a2a-js/sdk/client لمصنع العملاء. الاستيراد من نقطة الدخول الخاطئة هو أكثر أخطاء التشغيل الأول شيوعًا.

الخطوة 2: تعريف بطاقة الوكيل Agent Card

بطاقة الوكيل هي الهوية العامة لوكيلك: مستند JSON يُقدَّم على المسار /.well-known/agent-card.json ويخبر الوكلاء الآخرين من أنت، وما المهارات التي تقدمها، وما وسائل النقل التي تتحدثها، وما أنماط الإدخال والإخراج التي تقبلها. الاكتشاف في A2A يبدأ من هنا — يجلب العميل البطاقة أولًا ثم يتفاوض على كل شيء آخر انطلاقًا منها.

أنشئ الملف src/card.ts:

// src/card.ts
import { AgentCard } from '@a2a-js/sdk';
 
export const researchAgentCard: AgentCard = {
  name: 'Research Summary Agent',
  description:
    'Accepts a topic and returns a structured research summary as a markdown artifact.',
  protocolVersion: '0.3.0',
  version: '1.0.0',
  // عنوان URL العام لنقطة نهاية وسيلة النقل الافتراضية
  url: 'http://localhost:4000/a2a/jsonrpc',
  skills: [
    {
      id: 'summarize-topic',
      name: 'Summarize Topic',
      description:
        'Produces a concise, sourced summary for a given research topic.',
      tags: ['research', 'summarization'],
      examples: ['Summarize the current state of solid-state batteries'],
    },
  ],
  capabilities: {
    streaming: true,
    pushNotifications: false,
  },
  defaultInputModes: ['text'],
  defaultOutputModes: ['text'],
  additionalInterfaces: [
    { url: 'http://localhost:4000/a2a/jsonrpc', transport: 'JSONRPC' },
    { url: 'http://localhost:4000/a2a/rest', transport: 'HTTP+JSON' },
  ],
};

بعض الحقول تستحق الانتباه:

  • skills هي آلية توجيه العمل لدى المنسّقات. المنسّق الذي يحمل عشر بطاقات وكلاء يختار الوكيل الذي تتطابق وسوم مهاراته وأوصافها بشكل أفضل مع المهمة الفرعية المراد تفويضها. اكتبها كما تكتب أوصاف أدوات جيدة لنموذج لغوي — لأن ما يقرؤها غالبًا هو نموذج لغوي فعلًا.
  • capabilities.streaming يخبر العملاء أن بإمكانهم استدعاء نقطة نهاية البث واستقبال أحداث Server-Sent Events بدلًا من استجابة واحدة حاجبة.
  • additionalInterfaces يسرد كل وسائل النقل المكشوفة. تدعم حزمة JS وسيلة JSON-RPC (الافتراضية) وHTTP+JSON/REST وgRPC.

الخطوة 3: تنفيذ واجهة AgentExecutor

واجهة AgentExecutor هي قلب حزمة الخادم. تنفّذ دالة واحدة هي execute، تستقبل RequestContext (الرسالة الواردة ومعرّف المهمة ومعرّف السياق) وExecutionEventBus تنشر عليه الأحداث. تتكفل الحزمة نيابة عنك بتحليل JSON-RPC وحفظ المهام وتفاصيل بث SSE.

ابدأ بدورة حياة المهمة الكاملة. أنشئ الملف src/executor.ts:

// src/executor.ts
import { Task } from '@a2a-js/sdk';
import {
  AgentExecutor,
  RequestContext,
  ExecutionEventBus,
} from '@a2a-js/sdk/server';
 
export class ResearchExecutor implements AgentExecutor {
  async execute(
    requestContext: RequestContext,
    eventBus: ExecutionEventBus
  ): Promise<void> {
    const { taskId, contextId, userMessage, task } = requestContext;
 
    // استخراج الموضوع من أول جزء نصي في رسالة المستخدم.
    const firstPart = userMessage.parts[0];
    const topic = firstPart.kind === 'text' ? firstPart.text : 'unknown topic';
 
    // 1. نشر المهمة الأولية إن كان هذا دورًا جديدًا في المحادثة.
    if (!task) {
      const initialTask: Task = {
        kind: 'task',
        id: taskId,
        contextId,
        status: {
          state: 'submitted',
          timestamp: new Date().toISOString(),
        },
        history: [userMessage],
      };
      eventBus.publish(initialTask);
    }
 
    // 2. الإشارة إلى بدء العمل.
    eventBus.publish({
      kind: 'status-update',
      taskId,
      contextId,
      status: { state: 'working', timestamp: new Date().toISOString() },
      final: false,
    });
 
    // 3. تنفيذ العمل الفعلي (نص مؤقت حاليًا — Claude يصل في الخطوة 7).
    const summary = await this.summarize(topic);
 
    // 4. إصدار النتيجة كمخرج Artifact.
    eventBus.publish({
      kind: 'artifact-update',
      taskId,
      contextId,
      artifact: {
        artifactId: 'summary.md',
        name: 'Research Summary',
        parts: [{ kind: 'text', text: summary }],
      },
    });
 
    // 5. وسم المهمة كمكتملة وإغلاق تدفق الأحداث.
    eventBus.publish({
      kind: 'status-update',
      taskId,
      contextId,
      status: { state: 'completed', timestamp: new Date().toISOString() },
      final: true,
    });
    eventBus.finished();
  }
 
  private async summarize(topic: string): Promise<string> {
    await new Promise((r) => setTimeout(r, 800)); // محاكاة عمل
    return `# Summary: ${topic}\n\nThis is a placeholder summary.`;
  }
 
  cancelTask = async (): Promise<void> => {
    // تُستدعى عندما يلغي عميل مهمة قيد التنفيذ.
  };
}

تسلسل الأحداث هنا — مهمة، حالة قيد العمل، مخرج، حالة نهائية مكتملة، ثم finished() — هو دورة الحياة القياسية لمهمة A2A. قاعدتان مهمتان:

  1. اضبط دائمًا final: true على تحديث الحالة الختامي. فهذا ما يخبر الحزمة (وتدفق العميل) بأن المهمة وصلت إلى حالة نهائية.
  2. استدعِ دائمًا eventBus.finished() عندما ينتهي منفّذك من النشر. نسيانها يترك عملاء البث معلّقين حتى انتهاء مهلتهم.

للوكلاء السريعين من نمط طلب/استجابة يمكنك تجاوز آلية المهام كليًا: انشر حدث Message واحدًا بدور «agent» ثم استدعِ finished(). عندها يستقبل العميل رسالة مباشرة بدلًا من مهمة. استخدم المهام كلما استغرق العمل أكثر من لحظة أو أنتج مخرجات تستحق الحفظ.

الخطوة 4: تجميع خادم Express

اجمع الآن القطع: البطاقة ومخزن المهام ومنفّذك ومعالجات النقل. أنشئ الملف src/server.ts:

// src/server.ts
import express from 'express';
import { AGENT_CARD_PATH } from '@a2a-js/sdk';
import {
  DefaultRequestHandler,
  InMemoryTaskStore,
} from '@a2a-js/sdk/server';
import {
  agentCardHandler,
  jsonRpcHandler,
  restHandler,
  UserBuilder,
} from '@a2a-js/sdk/server/express';
import { researchAgentCard } from './card.js';
import { ResearchExecutor } from './executor.js';
 
const requestHandler = new DefaultRequestHandler(
  researchAgentCard,
  new InMemoryTaskStore(),
  new ResearchExecutor()
);
 
const app = express();
 
// يقدّم البطاقة على /.well-known/agent-card.json
app.use(
  `/${AGENT_CARD_PATH}`,
  agentCardHandler({ agentCardProvider: requestHandler })
);
 
// وسيلة النقل JSON-RPC (الافتراضية في A2A)
app.use(
  '/a2a/jsonrpc',
  jsonRpcHandler({ requestHandler, userBuilder: UserBuilder.noAuthentication })
);
 
// وسيلة النقل HTTP+JSON/REST
app.use(
  '/a2a/rest',
  restHandler({ requestHandler, userBuilder: UserBuilder.noAuthentication })
);
 
app.listen(4000, () => {
  console.log('A2A research agent listening on http://localhost:4000');
});

شغّله وتحقق من إمكانية اكتشاف البطاقة:

npm run server
# في طرفية أخرى:
curl http://localhost:4000/.well-known/agent-card.json

يجب أن ترى بطاقة الوكيل كاملة بصيغة JSON. هذا العنوان الوحيد هو كل ما يحتاجه وكيل آخر ليبدأ العمل مع وكيلك.

يحتفظ InMemoryTaskStore بحالة المهام في ذاكرة العملية، وهو مثالي أثناء التطوير. في الإنتاج ستنفّذ واجهة TaskStore فوق Redis أو Postgres كي تنجو المهام من إعادة التشغيل وتتوسع عبر عدة نسخ من الخادم.

الخيار UserBuilder.noAuthentication مقبول على localhost لكنه ممنوع في الإنتاج. تُفوّض مواصفة A2A المصادقة لآليات HTTP القياسية — حقل securitySchemes في بطاقة الوكيل يعلن ما تقبله (رموز bearer أو OAuth 2 أو مفاتيح API)، وخطاف UserBuilder في الحزمة هو المكان الذي تتحقق فيه من بيانات الاعتماد الواردة.

الخطوة 5: بناء العميل

جانب العميل صغير بشكل منعش. تجلب ClientFactory.createFromUrl بطاقة الوكيل وتختار وسيلة نقل مدعومة من الطرفين وتعيد عميلًا جاهزًا. أنشئ الملف src/client.ts:

// src/client.ts
import { ClientFactory } from '@a2a-js/sdk/client';
import { MessageSendParams, Task } from '@a2a-js/sdk';
import { v4 as uuidv4 } from 'uuid';
 
const factory = new ClientFactory();
const client = await factory.createFromUrl('http://localhost:4000');
 
const params: MessageSendParams = {
  message: {
    kind: 'message',
    messageId: uuidv4(),
    role: 'user',
    parts: [{ kind: 'text', text: 'The state of small modular reactors' }],
  },
};
 
const result = await client.sendMessage(params);
 
if (result.kind === 'task') {
  const task = result as Task;
  console.log(`Task ${task.id} finished: ${task.status.state}`);
  for (const artifact of task.artifacts ?? []) {
    const part = artifact.parts[0];
    if (part.kind === 'text') {
      console.log(`--- ${artifact.name} ---\n${part.text}`);
    }
  }
} else {
  console.log('Direct message reply:', result);
}

شغّل npm run client والخادم يعمل وسترى المهمة تكتمل وتطبع مخرج التلخيص المؤقت. لاحظ ما لم تكتبه: لا مسارات نقاط نهاية، ولا مخططات حمولات، ولا منطق استعلام دوري. البطاقة أخبرت العميل بكل شيء.

الخطوة 6: بث التقدم في الوقت الفعلي

الانتظار الحاجب على sendMessage مناسب للمهام السريعة، لكن الأعمال البحثية قد تستغرق وقتًا. يبث A2A أحداث المهمة عبر Server-Sent Events، ويعرضها العميل كمولّد غير متزامن. أضف الملف src/client-stream.ts:

// src/client-stream.ts
import { ClientFactory } from '@a2a-js/sdk/client';
import { MessageSendParams } from '@a2a-js/sdk';
import { v4 as uuidv4 } from 'uuid';
 
const factory = new ClientFactory();
const client = await factory.createFromUrl('http://localhost:4000');
 
const params: MessageSendParams = {
  message: {
    kind: 'message',
    messageId: uuidv4(),
    role: 'user',
    parts: [{ kind: 'text', text: 'Progress in desalination tech' }],
  },
};
 
for await (const event of client.sendMessageStream(params)) {
  if (event.kind === 'task') {
    console.log(`[task] created ${event.id} (${event.status.state})`);
  } else if (event.kind === 'status-update') {
    console.log(`[status] ${event.status.state}`);
  } else if (event.kind === 'artifact-update') {
    console.log(`[artifact] received ${event.artifact.artifactId}`);
  }
}
console.log('Stream closed.');

تصل الأحداث بالترتيب الذي نشرها به منفّذك تمامًا: مهمة أُنشئت، قيد العمل، مخرج، مكتملة. في واجهة منسّق حقيقية سترسم هذه الأحداث على مؤشر تقدم، ويمكن للوكلاء طويلي المدة نشر أي عدد من تحديثات الحالة الوسيطة والمخرجات الجزئية.

الخطوة 7: منح الوكيل ذكاءً حقيقيًا مع Claude

حتى الآن يعيد المنفّذ نصًا جاهزًا. لنجعله يستحق بطاقته. ثبّت حزمة Anthropic:

npm install @anthropic-ai/sdk
export ANTHROPIC_API_KEY=sk-ant-...

ثم استبدل الدالة summarize في src/executor.ts:

// src/executor.ts (محدّث)
import Anthropic from '@anthropic-ai/sdk';
 
const anthropic = new Anthropic();
 
// ...داخل ResearchExecutor:
private async summarize(topic: string): Promise<string> {
  const response = await anthropic.messages.create({
    model: 'claude-sonnet-5',
    max_tokens: 1024,
    system:
      'You are a research assistant. Produce a concise markdown summary ' +
      'with sections: Overview, Key Developments, Open Questions.',
    messages: [{ role: 'user', content: `Topic: ${topic}` }],
  });
 
  const block = response.content[0];
  return block.type === 'text' ? block.text : 'No summary generated.';
}

وللتوليدات الأطول يمكنك الذهاب خطوة أبعد وبث مخرجات Claude عبر ناقل أحداث A2A كتحديثات مخرجات تدريجية:

private async streamSummary(
  topic: string,
  publish: (chunk: string) => void
): Promise<void> {
  const stream = anthropic.messages.stream({
    model: 'claude-sonnet-5',
    max_tokens: 1024,
    messages: [{ role: 'user', content: `Summarize: ${topic}` }],
  });
 
  stream.on('text', (delta) => publish(delta));
  await stream.finalMessage();
}

كل جزء يصبح حدث artifact-update، ويعرض العميل المتدفق الملخص كلمة بكلمة — نفس تجربة تطبيقات الدردشة، لكنها تتدفق من وكيل إلى وكيل عبر بروتوكول قياسي.

هذا التركيب هو الدرس الأهم. المنفّذ مجرد دالة غير متزامنة: بداخلها يمكنك استدعاء Claude أو الاستعلام من قاعدة بيانات متجهية أو استدعاء أدوات MCP أو التفويض إلى وكيل A2A آخر. لا يهتم البروتوكول بكيفية إنجاز العمل، بل فقط بكيفية وصفه وتتبعه وتسليمه.

اختبار التنفيذ

تحقق من الحلقة كاملة من طرف إلى طرف:

  1. اكتشاف البطاقة: الأمر curl http://localhost:4000/.well-known/agent-card.json يعيد بطاقتك مع حقلي protocolVersion وskills معبأين.
  2. التدفق الحاجب: الأمر npm run client يطبع مهمة مكتملة مع مخرج summary.md.
  3. تدفق البث: الأمر tsx src/client-stream.ts يطبع أحداث دورة الحياة الأربعة بالترتيب ثم «Stream closed».
  4. حالة المهمة: أرسل رسالة ودوّن معرّف المهمة ثم اجلبها عبر استدعاء JSON-RPC باسم tasks/get للتأكد من احتفاظ المخزن بالسجل والمخرجات.

للتحقق على مستوى البروتوكول، ينشر مشروع A2A عدة توافق تقني TCK تختبر خادمك مقابل المواصفة — يجدر تشغيلها قبل إعلان جاهزية وكيلك للإنتاج.

استكشاف الأخطاء وإصلاحها

العميل يرمي خطأ عند createFromUrl. فشل جلب البطاقة. تأكد من أن الخادم يعمل وأنك لم تركّب agentCardHandler على مسار مخصص بينما يتوقع العميل المسار المعروف الافتراضي.

عملاء البث يبقون معلّقين بعد آخر حدث. منفّذك لم يستدعِ eventBus.finished() قط، أو أن تحديث الحالة الختامي ينقصه final: true.

أخطاء «Cannot find module» في استيرادات الخادم. راجع نقطة الدخول: أصناف المنفّذ والمعالج تأتي من @a2a-js/sdk/server، ومعالجات Express من @a2a-js/sdk/server/express، والأنواع المشتركة من جذر الحزمة.

المخرجات تصل فارغة. كل جزء يحتاج حقل kind صريحًا (text أو file أو data). الجزء الذي ينقصه هذا الحقل تتجاهله العملاء الصارمة بصمت.

أخطاء استيراد ESM مع tsx. تأكد من ضبط "type": "module" ومن أن الاستيرادات المحلية تستخدم الامتداد .js (مثل ./card.js)، الذي تحلّه TypeScript إلى مصدر .ts وفق نمط تحليل الوحدات الحديث.

الخطوات التالية

  • نفّذ TaskStore دائمًا فوق Redis وأضف منطق cancelTask يوقف فعليًا استدعاءات Claude الجارية
  • أعلن عن securitySchemes في بطاقتك وتحقق من رموز bearer في UserBuilder مخصص
  • ابنِ وكيلًا ثانيًا واجعل الأول يفوّض إليه مهام فرعية — خطوط الوكلاء المتعددين ليست سوى عملاء A2A داخل المنفّذات
  • امزج البروتوكولات: اكشف مصادر بيانات وكيلك عبر خادم MCP واحتفظ بـ A2A للتفويض من وكيل إلى وكيل
  • استكشف Claude Agent SDK إن أردت حلقة وكيل كاملة خلف منفّذك

الخلاصة

لقد بنيت وكيل A2A كاملًا بلغة TypeScript: بطاقة وكيل للاكتشاف، ومنفّذًا يمر بدورة حياة المهمة الكاملة مع تحديثات حالة ومخرجات متدفقة، وعميلًا يستهلك التدفقين الحاجب والمتدفق، وعقلًا مدعومًا بـ Claude خلف كل ذلك. النمط الذي يجب تذكره هو فصل المسؤوليات — يوحّد A2A طريقة تحدث الوكلاء ويترك لك بالكامل طريقة تفكيرهم. ومع تقارب أطر التنسيق ومنصات الشركات نحو هذا البروتوكول، يصبح الوكيل المتوافق مع A2A وكيلًا يستطيع أي منها اكتشافه والوثوق به وتشغيله.