The A2A Client Starts With the Card, Then Sends a Message
An A2A client never starts by sending a message - it starts with the card. The ClientFactory resolves the Agent Card by URL, picks a compatible interface, and creates a typed client. And only then does the request state the accepted output modes and decide whether to wait for the result or get an immediate reply.
The working client from the demo runs the whole path: resolve the card, send the message, verify that a Task came back.
export async function runA2ADemo(messageText = "Check the release web-portal@2.4.0"): Promise<A2ADemoResult> {
const running = await startA2AServer();
try {
const client = await new ClientFactory().createFromUrl(running.baseUrl);
const request: SendMessageRequest = {
tenant: "",
message: userMessage(messageText),
configuration: {
acceptedOutputModes: ["text/markdown", "application/json"],
taskPushNotificationConfig: undefined,
returnImmediately: false
},
metadata: { traceId: randomUUID() }
};
const result = await client.sendMessage(request);
if (!isTask(result) || !result.status) {
throw new Error("A2A agent returned a Message instead of a Task");
}
return {
agentName: running.card.name,
state: result.status.state,
taskId: result.id,
artifactName: result.artifacts[0]?.name ?? "missing",
decision: readDecision(result)
};
} finally {
await running.close();
}The result is deterministic and can be checked in a test - the agent, the state, the taskId, the artifact name, and the decision.
{
"agentName": "Release Review Agent",
"state": "completed",
"taskId": "<generated UUID>",
"artifactName": "release-review.md",
"decision": "ready"
}Before SendMessage the client must go through its own policy - this isn't bureaucracy but a defense against the foreign and the malicious.
- Check the protocol version and binding.
- Check required extensions and optional capabilities.
- Match media modes to local decoders.
- Apply the declared authentication scheme.
- Limit redirects and the card URL policy.
- Keep the taskId/contextId separate from the UI message id.
After the response the discipline doesn't end. The result may be a Message or a Task - use a type guard, then check the terminal or interrupted state and the media type of each artifact Part. Don't execute a data Part as code and don't trust the filename when saving.
The card URL is external input. Agent discovery may lead the client to an internal address or an unexpected redirect. Apply an SSRF policy, TLS, and an allowlist in corporate orchestrators - otherwise discovery becomes a door into the perimeter.
The client and server are talking. What's left is to decide how exactly it learns about updates to a long task - and that's a deliberate choice among three mechanisms.