4.1

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:

SDKconfiguration.historyLength on the returned task
a2a-python 1.2.2applied to the returned Task and to task events of a stream, and a negative value is InvalidParamsError
a2a-gonever applied, so the task returns with its full stored history
a2a-javaapplied to the returned Task and to each Task event of a stream, and a negative value is InvalidParamsError
a2a-jsapplied to the returned task and to task payloads of a stream, and 0 or less omits history
a2a-dotnetnever read by the server, so the task returns with its full stored history
a2a-rsapplied 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.

Fig. 4.1one method, two result shapesdecision
One method, two result shapes The planner sends SendMessage to code-reviewer, and the agent logic decides the shape. A question with no text/x-diff part gets result.message, message 3b4b1206 in context dc423c62, and the client can only read its parts and send a new message in that context. A diff gets result.task, task e5c95b09 in context 49c5a238 in state TASK_STATE_INPUT_REQUIRED, and the client can answer with that taskId or follow the task with GetTask, SubscribeToTask, or CancelTask. SendMessage planner to code-reviewer, one method code-reviewer decides a question with no text/x-diff part: a message anything else: a task result.message messageId 3b4b1206 contextId dc423c62, no task id a question (02) result.task id e5c95b09, contextId 49c5a238 TASK_STATE_INPUT_REQUIRED a diff (06) WHAT THE CLIENT CAN DO NEXT a new SendMessage with contextId dc423c62 to continue Nothing to poll, cancel, or subscribe to. The message is the whole answer. act on task e5c95b09 SendMessage with its taskId answers GetTask and SubscribeToTask follow it CancelTask asks to stop it
The same 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:

a send that returns at once, then two pollsjsonFull log
    "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 and the first GetTask request are cut to their key lines. The second GetTask request matches the first apart from its JSON-RPC id. Each reply is cut to its state, and the first poll also shows its one log part.

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.

Fig. 4.2a blocking call and a call that returns at oncetimeline
A blocking call and a call that returns at once Two timelines aligned at the request, on the kit clock. Track A: one blocking SendMessage for task 3d8f7801 stays open while the task passes TASK_STATE_SUBMITTED and TASK_STATE_WORKING, which the client never sees, and six artifact chunks arrive. The reply comes at TASK_STATE_COMPLETED, 09:00:00.750. Track B: SendMessage with returnImmediately for task d0c28826 returns at TASK_STATE_SUBMITTED, 09:00:01.000. A first GetTask sees TASK_STATE_WORKING at 09:00:01.250 with one log part, and a second GetTask sees TASK_STATE_COMPLETED at 09:00:01.500 with all artifacts. A · BLOCKING SendMessage with no configuration, task 3d8f7801 B · AT ONCE returnImmediately, then GetTask twice, task d0c28826 TASK_STATE_SUBMITTED TASK_STATE_WORKING TASK_STATE_COMPLETED KIT CLOCK 1 SendMessage, blocking: one reply, the completed task with test-log.txt and summary.json 2 SendMessage with returnImmediately: the reply holds TASK_STATE_SUBMITTED 3 GetTask, historyLength 0: TASK_STATE_WORKING and the first of five log parts 4 GetTask, historyLength 0: TASK_STATE_COMPLETED, five log parts, and summary.json 1 one request, held open until the reply not seen not seen 09:00:00.750 2 SendMessage 09:00:01.000 3 09:00:01.250 4 09:00:01.500
A blocking send holds one request open until this task completes, while 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:

the planner's dispatch on a SendMessage resultpythonFull log
        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 task
The top of the loop in Planner.delegate. The interrupted states are handled below this cut.

The 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