Поднимите A2A-server с JSON-RPC binding
Сервер A2A публикует две вещи: Agent Card и протокольный endpoint. В companion-проекте Express подключает официальные handlers, task store и наш ReleaseReviewAgentExecutor, а порт выбирается динамически - чтобы тесты были изолированы и не дрались за один адрес.
Полная сборка сервера умещается в одну функцию: поднять HTTP, собрать card, подключить card-route и JSON-RPC binding.
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 между ними.
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 и отправляет цель.