SendMessage
SendMessage carries one message and a SendMessageConfiguration, and the agent answers with a Message or a Task that, by default, is terminal or interrupted.
Your planner sends code-reviewer two messages through the same method. The first asks which languages it reviews, and the answer arrives at once as a message. The second carries a diff, and the answer is a task with an id, a state, and later an artifact.
When you finish this section, you can build a SendMessage request and act on whichever shape comes back.
The request and its configuration
SendMessageRequest: a message, as Message defines it, an optional configuration, and metadata. The configuration has four fields:
acceptedOutputModes: the media types the client accepts.taskPushNotificationConfig: a webhook, as push notification configs explains.historyLength: how much history the returned task carries.returnImmediately: whether the server answers before the task settles.
A send blocks unless returnImmediately is true:
In 03-blocking-task.http, one SendMessage with no configuration gets task 3d8f7801 in TASK_STATE_COMPLETED, with every artifact. The client never sees TASK_STATE_SUBMITTED or TASK_STATE_WORKING. The specification sets no time limit, and a client that times out never learns the task id.
One sentence of the specification says the opposite (conflict D2): the operation must return at once with either task information or a response message. The proto makes false the default, and the proto is normative. The kit and the reference SDK block.
The SDKs disagree on whether historyLength in the configuration trims the returned task:
| SDK | configuration.historyLength on the returned task |
|---|---|
| a2a-python 1.2.2 | applied to the returned Task and to task events of a stream, and a negative value is InvalidParamsError |
| a2a-go | never applied, so the task returns with its full stored history |
| a2a-java | applied to the returned Task and to each Task event of a stream, and a negative value is InvalidParamsError |
| a2a-js | applied to the returned task and to task payloads of a stream, and 0 or less omits history |
| a2a-dotnet | never read by the server, so the task returns with its full stored history |
| a2a-rs | applied to the returned Task of a blocking send only, and a stream ignores it |
Message or Task
SendMessage returns a SendMessageResponse whose payload is a oneof of two members, task and message. The agent decides, and the specification gives it no rule. The agent is allowed to create a new Task and process the message asynchronously. It is also allowed to return a direct Message for a simple exchange.
In the kit, test-runner and deployer create a task for every message. code-reviewer answers with a message when the request has no text/x-diff part and its text ends with a question mark (the kit's agent code). Once a task exists, the choice is over: from then on, the agent returns only Task objects in response to messages.
A message is the whole answer, with no id to poll, cancel, or subscribe to. It carries a contextId, so keep it for a follow-up. A task is a handle on work that can outlive the call. Figure 4.1 puts the two answers from code-reviewer side by side. SendStreamingMessage makes the same choice in its first event, as SendStreamingMessage and SubscribeToTask shows.
SendMessage to code-reviewer returns a message for a question and a task for a diff, and only the task gives the client something to follow. Read top down. The agent's own logic picks the shape, here the review_quick rule of the kit's agent code. Left, the captured message reply. Right, the captured input-required task. Ids are shortened to 8 characters.Returning at once and polling with GetTask
returnImmediately: a boolean in SendMessageConfiguration that asks the server to answer as soon as the task exists. The operation must return right after it creates the task, even while the work goes on. The flag has no effect on a direct Message reply, on streaming operations, or on push notification configs.
Polling: reading a task with GetTask until its state tells you to stop. In 04-polling.http the kit sends with returnImmediately, then polls task d0c28826 twice with historyLength 0:
"configuration": {
"returnImmediately": true
}
…
"state": "TASK_STATE_SUBMITTED",
…
"method": "GetTask",
"params": {
"id": "d0c28826-28b8-4b15-8b53-4e01ae7a5a3c",
"historyLength": 0
}
…
"state": "TASK_STATE_WORKING",
…
"text": "collected 12 items\n"
…
"state": "TASK_STATE_COMPLETED",The send returns TASK_STATE_SUBMITTED, while the specification's examples name TASK_STATE_WORKING and TASK_STATE_INPUT_REQUIRED, so switch on the state you get. The first poll finds TASK_STATE_WORKING and one log part. The second finds TASK_STATE_COMPLETED with every artifact. Figure 4.2 sets the blocking call beside the immediate return and its two polls.
returnImmediately answers at TASK_STATE_SUBMITTED and leaves the client to poll. Both tasks are aligned at the request and run through the same states on the kit's clock. Filled squares are states a reply showed the client, and hollow squares are states it never saw. Green ticks are artifact updates, and violet bars are open requests.Dispatch on the result
The kit's planner tests which member is present before it touches either:
while True:
if "error" in reply:
self.note(f" error {reply['error']['code']}: {reply['error']['message']}")
return None
result = reply["result"]
if "message" in result:
self.note(" direct reply, no task to follow")
return result["message"]
task = result["task"] if "task" in result else result
state = task["status"]["state"]
self.note(f" task {task['id'][:8]} is {state}")
if state in TERMINAL:
return taskThe planner checks error first, because JSON-RPC errors arrive inside an HTTP 200 response. Then it checks message and stops. Only then does it branch on the task's state, as Task, TaskStatus, and TaskState defines it. A client walks through the rest of this loop.
A2A protocol 1.0.1 · 3303592 · 2026-05-28 · Edition 2026.10