Глава 26

Поднимите A2A-server с JSON-RPC binding

Сервер A2A публикует две вещи: Agent Card и протокольный endpoint. В companion-проекте Express подключает официальные handlers, task store и наш ReleaseReviewAgentExecutor, а порт выбирается динамически - чтобы тесты были изолированы и не дрались за один адрес.

Полная сборка сервера умещается в одну функцию: поднять HTTP, собрать card, подключить card-route и JSON-RPC binding.

TypeScript
export async function startA2AServer(): Promise<RunningA2AServer> {
  const app = express();
  const httpServer = createServer(app);
  httpServer.listen(0, "127.0.0.1");
  await once(httpServer, "listening");

  const address = httpServer.address();
  if (!address || typeof address === "string") {
    await closeServer(httpServer);
    throw new Error("Unable to resolve A2A server address");
  }

  const baseUrl = `http://127.0.0.1:${address.port}`;
  const card = createReleaseAgentCard(baseUrl);
  const taskStore: TaskStore = new InMemoryTaskStore();
  const handler = new DefaultRequestHandler(card, taskStore, new ReleaseReviewAgentExecutor());

  app.get("/docs", (_request, response) => {
    response.type("text/plain").send("Release Review Agent 1.0.0");
  });
  app.use(`/${AGENT_CARD_PATH}`, agentCardHandler({ agentCardProvider: handler }));
  app.use(jsonRpcHandler({ requestHandler: handler, userBuilder: UserBuilder.noAuthentication }));

  return {
    baseUrl,
    card,
    close: () => closeServer(httpServer)
  };
}

А вот как executor во время работы публикует события - от working до completed с готовым artifact между ними.

TypeScript
    eventBus.publish(AgentEvent.statusUpdate({
      taskId,
      contextId,
      status: {
        state: TaskState.TASK_STATE_WORKING,
        timestamp: new Date().toISOString(),
        message: agentMessage(`Проверяю ${reference.service}@${reference.version}.`, taskId, contextId)
      },
      metadata: undefined
    }));

    if (this.cancelledTasks.has(taskId)) {
      this.cancelledTasks.delete(taskId);
      return;
    }

    try {
      const review = reviewRelease(reference.service, reference.version);
      const artifact: Artifact = {
        artifactId: randomUUID(),
        name: "release-review.md",
        description: "Детерминированный отчёт о готовности релиза",
        parts: [
          {
            content: { $case: "text", value: reviewToMarkdown(review) },
            metadata: undefined,
            filename: "release-review.md",
            mediaType: "text/markdown"
          },
          {
            content: { $case: "data", value: review },
            metadata: undefined,
            filename: "release-review.json",
            mediaType: "application/json"
          }
        ],
        metadata: { decision: review.decision },
        extensions: []
      };

      eventBus.publish(AgentEvent.artifactUpdate({
        taskId,
        contextId,
        artifact,
        append: false,
        lastChunk: true,
        metadata: undefined
      }));
      eventBus.publish(AgentEvent.statusUpdate({
        taskId,
        contextId,
        status: {
          state: TaskState.TASK_STATE_COMPLETED,
          timestamp: new Date().toISOString(),
          message: undefined
        },
        metadata: undefined
      }));

Порядок middleware здесь - вопрос безопасности, а не стиля, и его стоит держать строго.

  • Edge controls: TLS, лимиты тела, request id, rate limit.
  • Публичная или контролируемая route для Agent Card.
  • Authentication, которое строит verified user/principal.
  • A2A binding handler и capability validation.
  • Request handler, task store, executor.
  • Telemetry и redaction.

Две оговорки отделяют демо от продакшена. Первая - про аутентификацию.

Demo намеренно без authentication. UserBuilder.noAuthentication нужен только для localhost integration-теста. Production Agent Card обязана объявить security scheme, а middleware - построить verified principal до любого доступа к tasks.

Вторая - про то, что binding не является частью контракта.

Binding можно заменить. Data model и операции A2A отделены от JSON-RPC, REST и gRPC bindings. Выберите тот, который поддерживают обе стороны, и проверьте supportedInterfaces - агент не привязан к одному транспорту.

Сервер отвечает - напишем клиента, который его находит по card и отправляет цель.

Ссылки