/ docs · functional specification

The full platform
functional reference.

Every functional area of AI API Mapper documented at implementation depth so architects, security reviewers, and platform engineers can evaluate the product with real technical context.

A2A · Task lifecycle

A2A Task Lifecycle

Purpose

This functional area lets a caller send work to the platform as an A2A Task, retrieve that task's outcome afterward, watch it happen live over a stream, and cancel it — the same lifecycle concepts the A2A specification defines, scoped to what this platform actually does today.

Main Capabilities

  • send a message and receive the task the agent has accepted (SendMessage)
  • send a message and watch the task's status stream live (SendStreamingMessage)
  • retrieve a task by id at any point in its life (GetTask)
  • reconnect to a task's status stream after the fact (SubscribeToTask)
  • request cancellation of a task (CancelTask)

How It Works

A task is now what the specification assumes it is: something the platform accepts, then works on, then reports. Every A2A message is addressed to an agent, and there are two kinds.

A hosted agent runs a model of its own. The turn — the model's reasoning and whatever governed tools it chooses to call — runs on the platform's own workers rather than on the caller's connection, moving the task to working and then to a terminal state, or pausing at input-required if the agent needs something from the caller. Which of those it reaches is the agent's own declaration, not something the platform infers from what the agent did along the way — see A2A Hosted Agent Mode. Whether the caller waits for that or is told about the task and follows it is the caller's choice, made with configuration.returnImmediately; either way the task exists throughout, which is what makes it something to ask about.

A remote agent is a proxy to a peer somewhere else, so the platform's task tracks the outbound call — it moves through working for the duration of that call and reaches its terminal state when the peer answers.

This shapes every capability in this area:

  • SendMessage waits by default. configuration.returnImmediately decides: false or absent — the specification's default — means the call does not return until the task reaches a terminal state or an interrupted one (input-required, auth-required), and the answer carries that state with its artifacts. true answers as soon as the task exists, in submitted, and leaves you to poll GetTask, stream, or wait for a push notification.

    A blocking call holds its connection for as long as the agent takes, bounded by the agent's own turn budget (five minutes by default). If a reverse proxy, firewall or load balancer between you and the platform has a shorter idle timeout, that component will cut the connection first — raise its timeout, or ask for returnImmediately: true and follow the task instead.

    The message it takes is A2A's own: a role, one or more parts — text, files, or structured data — and the sender's messageId, contextId, taskId and metadata. All of it is carried through; a message relayed to a remote agent arrives as what was sent. A message must have at least one part, and there is no skillId — that was never an A2A field, and what an agent does with a message is its own decision.

    A caller who is not authorized for the agent is refused rather than given a task that fails a moment later. That is deliberate: a task id is itself a handle to something they have just been told they may not have.

  • A message may name the task it continues. Put the task's id on the message (taskId) and the agent continues that exact task rather than starting another. You do not have to send the contextId as well — the conversation is inferred from the task, and the task you get back names it. If you do send both, they must agree; a contextId belonging to some other conversation is refused rather than quietly ignored.

    A taskId naming a task this agent does not have is not found, never a new task under the id you chose — task ids are the agent's to issue. A taskId naming a task that has already finished is refused as an unsupported operation: it still exists and GetTask will still serve it, but nothing more can be sent to it. And a taskId naming a task still being worked on is refused too, with a message saying so — wait for it to pause or finish, or send a message with no taskId to start something new.

  • A task that is waiting rather than finished speaks through its status. When a hosted agent needs more information (input-required), what it has to say is carried on status.message, not as an artifact — an artifact is a result, and a waiting task has not produced one. GetTask serves the same message, so a client that polls sees what the agent is waiting for. auth-required behaves identically, and is something only a relayed remote-agent task produces: a hosted agent never asks a caller for a credential, because the caller has no way to supply one.

  • SendStreamingMessage reports the same task as a stream: submitted first, then a frame for each status the task reaches, then the stream closes. The id in the first frame is the task's real id and does not change — a client can track the task from the first frame it receives. What streams is the task's status, not the model's tokens; token-level streaming is a separate piece of work.

  • GetTask serves the task's current state, which for a hosted agent genuinely changes while the turn runs. Polling it is a legitimate way to follow a task, and a completed task carries the agent's answer as its artifact — that is where the result lives now that the SendMessage response no longer carries it.

  • SubscribeToTask reconnects to a task by id, reports where it is now, and — if it is still being worked on — stays open and reports each further status until the turn ends. A task that has already finished, or that is paused waiting on the caller, gets one event and a closed stream, because nothing further will happen to it on its own.

  • CancelTask returns a "not cancelable" error for a task that has already reached a terminal state. This is correct A2A behavior, and a spec-compliant client is expected to handle it.

What bounds a turn

Once a turn no longer runs on the caller's connection, the caller hanging up no longer stops it — so the platform bounds it instead, under Runtime:A2a:HostedAgents. A limited number of turns run at once (MaxConcurrentTurns, 4 by default), a limited number may wait (QueueCapacity, 128), and no single turn may run longer than TurnTimeoutSeconds (300). A turn that exceeds its time fails that task and nothing else. A message that arrives when the queue is full is answered with a failed task and a reason, rather than accepted and quietly never run.

A task is bounded separately, by the per-task token ceilings on the agent — DefaultMaxInputTokensPerTask and DefaultMaxOutputTokensPerTask in the same section supply the fallback. A turn budget bounds each turn without bounding their sum, and a paused task can be resumed indefinitely; see A2A Hosted Agent Mode.

Push notifications

A terminal transition — and a pause waiting on the caller, which is equally a caller's turn to act — fires whatever push notification configuration is bound to the task. This is what makes a hosted agent usable by a caller that does not hold a connection open: it is told when there is something to come back for.

A webhook can be bound in either of the two ways A2A describes: with a CreateTaskPushNotificationConfig call naming an existing task, or inline on the message that creates the task, through configuration.taskPushNotificationConfig. The inline form carries no task id — the task does not exist yet — and the configuration binds to whichever task the call produces; a task id sent in it anyway is ignored rather than honoured, since a caller must not be able to attach a webhook to somebody else's task.

Either form may carry its own credential. A2A has the caller supply one in the configuration's authentication block — {"scheme": "Bearer", "credentials": "…"} — and makes presenting it on every delivery a requirement. That is a token you minted for your own endpoint so that you can recognize our delivery as ours; it is not a credential for anything of this platform's, and it never reaches a model. Bearer, Basic and ApiKey are presented as their respective headers. A scheme the platform cannot compose a header for — Digest, say — is registered without one and delivered unauthenticated, with a warning naming the scheme, because A2A requires the delivery to be attempted either way.

Such a credential is encrypted at rest and discarded when it is no longer needed — once the task has finished and no delivery for it is still waiting to be retried. Both halves matter: the final notification is sent the moment a task finishes, and a failed one is retried minutes later, so discarding at the transition alone would send those retries unauthenticated. A credential is also discarded after seven days whatever the task's state, so one belonging to a task whose host died is not held forever.

These are deliberately not kept in the platform's secret store. That store is an administered inventory — named things with owners and rotation — and a webhook token is per-task and often short-lived; at task rate they would bury the secrets an administrator needs to see. The alternative to registering one inline remains: CreateTaskPushNotificationConfig can bind a configuration to a secret the tenant already holds, which is the right choice for a long-lived webhook whose credential an operator manages.

A deployment must configure A2a:PushNotifications:Credentials:EncryptionKey — a base64 32-byte key shared by every service — for caller-supplied credentials to be stored at all. Without it the configuration is still registered and the delivery still attempted, but unauthenticated, and a warning says so. The key is not generated per process on purpose: a credential is encrypted by the host that took your message and decrypted by whichever process delivers, which for a retry is the jobs worker.

No HTTPS requirement. A2A makes HTTPS a should for webhook URLs, not a must, so a plain http:// webhook is accepted and its credential presented as given. The exposure of your own token to your own network is yours to weigh, and refusing a request the specification permits would make this platform the non-conformant one.

What arrives at your endpoint. A2A defines one body for a push notification, and it is the same wrapper a streaming event travels in. Yours will look like this:

{
  "statusUpdate": {
    "taskId": "…",
    "status": {
      "state": "TASK_STATE_COMPLETED",
      "timestamp": "2026-09-09T15:34:00.0000000+00:00",
      "message": { "role": "ROLE_AGENT", "parts": [{ "text": "…" }] }
    }
  }
}

status.message is present only when the agent had something to say about the transition — a failure reason, or a question it is waiting on. The wrapper's other members (task, message, artifactUpdate) are omitted rather than sent as nulls, so a schema-validating receiver sees exactly one variant.

A delivery that is attempted appears on Governance → Agents Monitoring → Webhook deliveries, whatever its outcome — including one that succeeded on its first attempt. An absence of rows there means no configuration was bound to the task at all, which is a different problem from a delivery that failed and is worth telling apart before looking at the network.

Persistence

Every task is recorded (A2aTaskRecord) the moment it's created, tenant/client-scoped, keyed by the external task id the caller sees on the wire. This is what makes GetTask/SubscribeToTask possible after the fact — without it, a task's outcome would only ever be visible in the original SendMessage response, with no way to look it up again.

Listing tasks

ListTasks answers "what has this agent been doing", one page at a time, newest status change first. It is scoped to the agent whose address the call arrived at — the task store is keyed on tenant and agent, so a listing cannot reach past the address it was asked at, and no per-task check is involved.

Everything is optional. With no filter at all you get the agent's most recent tasks; contextId narrows to one conversation, status to one TASK_STATE_* value, and statusTimestampAfter to what has changed since you last looked. A status value A2A does not define is refused rather than queried for — an empty result reads like a data problem when it is really a typo.

Artifacts are omitted unless you ask for them. includeArtifacts defaults to false, and that default matters: a listing that carried every task's artifacts would return the whole output of every task on the page to a caller that asked only what exists.

Pages are cursors, not offsets. nextPageToken encodes the last row's sort position rather than a count of rows consumed, because tasks are written continuously by definition: with an offset, a task created between two pages shifts everything down and the next page silently repeats a task you have already seen, while a task leaving a filtered listing silently skips one you have not. Neither shows up as an error. The token is opaque — hand it back as you received it. One that this platform did not issue is rejected as an invalid argument rather than quietly restarting the listing, which would look like success while re-delivering rows.

The final page carries no token. pageSize defaults to 50 and is clamped to 100; the response reports the size actually applied, alongside totalSize — the number of tasks matching the filter, which the cursor deliberately does not carry.

Streaming Transport

SubscribeToTask follows a task by re-reading its record every couple of seconds (Runtime:A2a:Sse:TaskPollIntervalSeconds) rather than by listening in the process running the turn. That is what lets a caller reconnect to any instance behind the gateway and still be followed correctly: the record is the one view of a task that every instance shares.

SendStreamingMessage and SubscribeToTask answer over Server-Sent Events, reusing the exact same session/stream infrastructure the MCP protocol's Streamable HTTP transport already uses — extracted into a shared, protocol-neutral form specifically so A2A could add streaming without re-deriving or duplicating proven session, backplane, and multi-instance delivery machinery. See Runtime Protocols And MCP Support for how that underlying transport behaves; A2A's streaming endpoints behave the same way, under their own session identity and rate-limit policy.

A subscription to a task that does not exist — or to one belonging to a different agent, which A2A requires be indistinguishable from it — is refused before the response commits to an event stream, as an ordinary JSON-RPC error carrying TaskNotFoundError. Opening the stream and putting the error in a frame would tell the caller the subscription succeeded, which is the opposite of the answer.

Response shapes

SendMessage does not answer with a task at the top of its response. A2A 1.0 wraps it: over JSON-RPC the result is {"task": {…}}, and over REST the body is the same object. The wrapper is a union — the other member is message, for an agent that answers without creating a task — so a client reads which key is present rather than assuming. GetTask is different, and returns the task directly.

Streaming frames are wrapped the same way, in a StreamResponse: {"task": …}, {"message": …}, {"statusUpdate": …} or {"artifactUpdate": …}. This platform emits whole task snapshots rather than deltas, so its frames use task. Over JSON-RPC each frame is additionally a complete JSON-RPC response — {"jsonrpc": "2.0", "id": …, "result": {"task": …}} — while over REST the frame is the bare StreamResponse.

Fields the platform has nothing to put in are omitted rather than written as null: A2A states this strictly for artifacts, and the same applies to history and metadata.

status.timestamp is ISO 8601 in UTC and reports when the status was recorded, not when you read it. It was previously written as the time of the read, which made a polling client see the timestamp advance on every call while the state stood still — and would have made ListTasks' ordering meaningless, since every task would have shared the instant of the query.

Functional Value

This area gives an A2A caller a task they can actually follow: ask about while it runs, watch as it changes, or be told about when it finishes. For a hosted agent — which reasons and calls tools and takes as long as that takes — that is the difference between a protocol the platform speaks and one it means.

Task history

A task can carry the conversation that produced it, and historyLength says how much of it you want: absent leaves the amount to the platform, 0 asks for none, and a positive number returns at most that many of the most recent messages. It is read on all three transports — inside configuration on message/send, and as ?historyLength=n (REST) or the equivalent field (gRPC and JSON-RPC) on tasks/get.

The platform's default is no history, and what you get beyond that depends on which kind of agent owns the task.

A hosted agent has none. This is deliberate. The only record of such a turn is the model's own working thread, which holds the tool calls the agent made and the raw downstream responses that came back — the material the boundary between an agent's skills and its tools exists to keep away from the caller. Rather than filter someone else's serialization format and hope, the platform keeps no history for hosted agents at all. You already hold the messages you sent, and the governance audit trail records the rest for the compliance case without putting it on the wire.

A remote agent's history stays with that agent. The transcript belongs to whoever produced it, so this platform never stores a copy. Ask a relayed task for history and the request goes to the owning agent with your cap attached: it trims, and only the trimmed result passes through here. Ask for none — or say nothing — and no outbound call is made at all, so polling a finished task stays as cheap as it was. If the owning agent cannot be reached, the task is still returned, without its history; a network failure between two platforms says nothing about the task itself.

Finding a task afterwards

tasks/list answers "what has this agent been doing", because that is what a caller standing at one agent's address is entitled to ask. An administrator's question is the other one — which agent is failing — and the Portal answers it on Governance → Agents Monitoring.

Choosing a tenant is a filter, not a first step: an administrator who can see the whole platform starts across every tenant, because an operator investigating an incident does not yet know which tenant it is in. Every row names its own tenant, so a cross-tenant page still reads unambiguously. An administrator scoped to one tenant sees only theirs, whatever they ask for.

It opens on a roll-up of every registered agent: task counts by outcome, and when each was last active. Agents that have never been called are listed too, and say so, because that is exactly what you are looking for when a caller reports silence. From an agent's row you drop into its tasks; from a task you reach the detail page, with its status timeline and its webhook delivery history.

Three further searches run across tasks rather than within one, which is what makes them worth having:

  • Tasks — every task in the tenant, filtered by state, direction, agent, skill, conversation, task id or time window. The task id filter matches a fragment, since an id pasted from a log or a support ticket is usually partial.
  • Status changes — everything that entered a given state, across every task. The per-task timeline answers "what happened to this task"; this answers "which tasks did this happen to".
  • Webhook deliveries — push notification attempts across tasks, so a consistently failing endpoint is findable without already knowing which task to open. A task with no rows here had no webhook bound to it, rather than a webhook that could not be reached.

The screen refreshes itself on the same cadences as every other monitoring page on the platform, defaulting to ten seconds — long enough that the agents roll-up, which aggregates across every task in the window, is not re-aggregated faster than a model can answer. Only the tab being looked at is polled.

The applied search lives in the page address, so the screen is linkable: a filtered view pasted into a ticket opens as the filtered view. Only the applied search is written there, not the draft being typed, and it replaces rather than pushes — the address is a bookmark of where you are, not a log of how you got there. Opening a task from any of these grids returns you here, to that same search, rather than to the Call Flow explorer the shared task-detail page had originally been reached from.

The screen is read-only, and deliberately so. A2A has the calling agent drive its own tasks, so there is no administrator control here to cancel one or push it along: an administrator doing that would be acting outside the protocol the peer is speaking. It is also gated on the Governance read role rather than the one that administers agents, so an auditor can review what agents have been doing without also being able to change them.