A2A Hosted Agent Mode
Purpose
This functional area lets an administrator turn a registered agent into an LLM-backed reasoning agent that
runs inside ApiMapper, instead of a proxy to a remote peer. Where Concentrator mode answers "who else can
our agents talk to," Hosted Agent mode answers "can our own agent understand an incomplete or ambiguous
request instead of just failing it" — the gap that made task resumption (input-required) impossible for
server-mode skills before this existed: recognizing "this request is missing something" is a semantic
judgment, and a deterministic Mapping-tool skill has no way to make one.
Main Capabilities
- give a registered agent a system prompt and an LLM connection, turning it into a
HostedAgent - define one or more Skills on a hosted agent — a name, a skill-level prompt, and an explicit, curated subset of the tenant's MCP toolsets and ESRA (knowledge-base retrieval) exposures marked available to Hosted Agents
- mark an MCP toolset or an ESRA exposure "Hosted Agents only" or "MCP clients + Hosted Agents" (in addition to the existing "MCP clients only") — a toolset built purely for agent use needs no MCP client application assigned to it at all
- the agent declares how each turn ended, using three tools it always has — the request is satisfied, it needs something only the caller can give, or it has run out of options — so a task's reported state is the one the agent meant rather than one inferred from which tools it happened to call
- a turn that pauses (
TASK_STATE_INPUT_REQUIRED) asks a clarifying question the caller can answer to resume the same conversation, on the same task - every tool the LLM invokes goes through the exact same governed invocation pipeline — authorization, credential brokering, downstream call, audit — a deterministic skill invocation uses; the LLM never gets a bypass path to a downstream API
- every LLM turn is audited with its actual decision content — which tool it chose (if any), the outcome it declared and how, the clarifying question it asked (if any), the downstream status worth looking at, and the turn's token spend — not just success or failure
- bound what one task may spend, with per-task input and output token ceilings on the agent that fall back to the deployment's defaults — the first real limit on a paused conversation that can be resumed forever
- read what a task cost: a running per-task token total that accumulates across a pause and a resume, shown per task and rolled up per agent on the Agents Monitoring screens
How It Works
Choosing a kind: RemoteAgent or HostedAgent
RegisteredAgent carries a Kind set once at creation and never changed afterward (delete and recreate to
change it — there is no migration ceremony for switching kinds). A RemoteAgent carries a target system
and authentication profile, exactly as Concentrator mode already describes. A HostedAgent carries a
system prompt and a reference to an existing LlmConnection instead — the same connection type the
Portal's AI Settings page already manages for semantic enrichment, reused as-is rather than duplicated.
Only one set of fields is ever populated for a given agent; the other kind's fields are always null.
Authoring Skills
A Skill is the unit an admin actually designs around: a display name, a published id derived from it
(so "Company Documents Search" is addressed as company-documents-search), a description, a prompt fragment appended to the agent's own system prompt for that skill's turns, and an
explicit set of individual tools it may call — picked from the tenant's MCP toolsets and ESRA exposures
that are marked available to Hosted Agents. A skill is not required to correspond to a single tool: one
skill can bundle a handful of search tools drawn from several different toolsets (e.g. "search job postings"
across three separate job-board toolsets) while deliberately leaving out each toolset's write-shaped tools
(e.g. "create job posting"), something a whole-toolset grant could never express. A sibling skill on the
same agent can draw from a completely different set of toolsets/exposures. This is the primary structural
defense against prompt injection: a turn can only ever see and invoke the exact tools an admin selected,
never a toolset's or exposure's full tool list.
Because a caller no longer names a skill (see below), a turn is scoped to the union of the skills that caller is authorized for, not to one of them. That makes what you put on an agent the boundary, rather than what you put on any single skill: two skills on one agent are two sets of tools a caller authorized for both can use together. Where that is not what you want, the answer is two agents, or an access policy that narrows one of the skills.
Binding a Skill to individual tools drawn from McpToolsets and ESRA exposures — rather than to whole
Mappings — means a Hosted Agent's tools reuse exactly the same downstream call configuration (target
system, credential binding, schema discovery, per-endpoint overrides) that MCP clients already use for the
same toolset, and the same governed knowledge-base retrieval configuration ESRA exposures already resolve.
There is no separate resolution path to keep in sync.
Running a turn
A caller sends the agent a message. It does not name a skill — A2A has no field for one, and choosing what
to do is the agent's job, not the caller's. The platform works out which of the agent's skills that caller
is authorized for, and answers immediately with a task in TASK_STATE_SUBMITTED. The turn itself —
building a reasoning agent from the authorized skills' prompts and the union of their tools, running the
caller's message through it — happens after that, and the task moves to TASK_STATE_WORKING while it does.
That ordering is what makes the task real. A caller can ask about a task while the agent is still thinking, watch it change over a stream, or hang up entirely and be told by a push notification when there is something to come back for. It also means an agent may take as long as reasoning and calling tools actually takes, rather than as long as one HTTP request may reasonably be held open.
Authorization is the part that does not wait. A caller authorized for none of an agent's skills is refused before anything is accepted or any model consulted, and the refusal does not name the skills they were not allowed to use — being handed a task id for work you were just told you may not do would give away the same thing the refusal is careful not to. An agent with no skills configured is a legitimate agent — a model with a system prompt and no tools — and runs normally.
The agent says how the turn ended
A turn ends when the agent declares what happened, and there is no other way for it to end. Every hosted agent is given three tools it always has, whatever its skills are, and its instructions oblige it to finish every turn by calling exactly one of them:
| Tool | The agent is saying | The task reaches |
|---|---|---|
agent_task_completed |
the request is satisfied | TASK_STATE_COMPLETED |
agent_request_more_input |
I am blocked on something only you can give me | TASK_STATE_INPUT_REQUIRED |
agent_report_unable_to_proceed |
I have run out of options | TASK_STATE_FAILED |
If it declares more than one, the least optimistic wins: unable-to-proceed over more-input over completed. A model that contradicts itself is not treated as having finished the work.
agent_task_completed takes no argument — the agent's written answer is what the caller receives, so
restating it inside a tool call would only invite the model to truncate it. The other two carry the question
or the reason, and that text is what the caller is told.
These three tools reach no downstream API, resolve no credential and add nothing to what the agent can do.
They exist so it can say what it did. The agent_ prefix is reserved platform-wide for exactly this reason:
a mapping toolset may not take it, and is refused while it is being named rather than after it is published.
What the agent called along the way decides nothing. A tool that succeeded may have been purely preparatory — confirming a customer exists before an order can be created is not creating the order. A tool that failed is something the agent reads and reasons about: it may correct its arguments and retry, and a retry that succeeds is a success. And an agent that answers a question needing no tool at all has still answered it. Only the agent knows which of those happened, so only the agent says so.
If a turn ends without a declaration, the agent is asked once — and only once — to declare one. If it still does not, the task fails saying exactly that: the agent did not declare an outcome. That is deliberately not a guess in either direction. Calling it completed would report success for work nobody confirmed; calling it input-required would leave a task nobody will ever finish.
The clarifying question, and the reason a task failed, are carried on the task's status message
(status.message), which is where A2A puts an agent's own words. Neither is an artifact: an artifact is a
result, and a task that paused or failed has not produced one.
The agent's own name is not sent to the model
An agent's directory name is yours to choose and can hold anything — spaces, slashes, ampersands. Model providers disagree about what may appear in a message's author field, and some reject values that are perfectly ordinary display names. The platform therefore does not send it: a hosted agent's turn has one assistant, and its instructions already say who it is. Nothing is lost, and no agent can be made unusable by the name someone gave it.
A hosted agent never asks for credentials
TASK_STATE_AUTH_REQUIRED is a state this platform understands and will return for a task relayed from a
remote peer that produced one. A hosted agent of its own never produces it, and the reasoning matters more
than the rule.
An agent's access to a downstream system is configured by an administrator — a target system and a credential binding, resolved by the runtime at call time. The caller of an A2A agent has no part in that and could not supply a credential if asked. Telling them credentials are required would ask them to fix something they cannot reach, and worse, invite them to put a credential in a message. An external agent should never do that, and this platform should never solicit it.
So a downstream 401 or 403 is what it actually is: an ordinary tool error. The agent reads it, and decides whether to report that it cannot proceed. The status code is recorded on the turn's audit entry, which is where the administrator whose misconfiguration it is will find it.
Every transition is persisted
Every one of those transitions is persisted before anything else happens with it, so GetTask on any
instance sees it, a subscriber following the task is told, and — for a terminal state or a pause waiting on
the caller — the task's push notification configuration fires.
A paused task's full conversation state is serialized and persisted alongside the task record. A caller's follow-up message, correlated by the same context id the original request used, resumes that exact conversation rather than starting over — the LLM sees the original request and the new information together in one continuous session, so ApiMapper never has to mechanically merge two partial payloads itself.
Prompt-injection defense
Two layers, both mandatory, neither sufficient alone:
- Structural tool scoping — a turn only ever has the tools an admin explicitly assigned to the skills this caller is authorized for. This bounds the damage a successful injection can do regardless of what the caller's message says, because there is nothing outside that set to redirect the LLM toward. Authorization is resolved before the model runs, so the model chooses within an already-authorized set and can never widen one.
- Message-role separation — the admin-authored system prompt and the caller's message text travel on distinct channels into the model; caller-supplied text is submitted only as the conversation's user turn and can never rewrite or append to the system-level instructions.
The three control tools sit outside both layers without weakening either. They reach no downstream API and resolve no credential, so the set of things a successful injection could redirect the agent toward is unchanged. The platform's own instruction block is prepended to the agent's system prompt rather than mixed into it, so an agent's author can reinforce it and cannot remove it.
Governance and audit
A hosted agent's skill invocation is authorized through the same access-policy check Concentrator mode
uses (agent, optionally narrowed to a skill, caller, client application) — one governance model for both
kinds of registered agent, not two. The check now runs once per skill the agent declares, and its answers
are what the turn is built from. Every turn is audited under the platform's existing runtime category,
carrying the LLM's actual decision (the tool it chose, the outcome it declared and which control tool it
declared it with, the clarifying question it asked, if any, and the downstream status of the tool call worth
looking at) and the turn's token spend, alongside the same allow/deny/error outcome every other governed
call records. It also records whether the agent declared an outcome at all, and whether it had to be asked
twice — which is what makes a model that will not follow the contract visible as a number rather than as a
run of confusing task states.
The tools an agent calls are audited exactly as they are when a client calls them directly over MCP: the
same tool.invoke, credential resolution, downstream call and transform entries, on the same path. An agent
does not get a second, quieter trail.
Three gates decide whether a call reaches the agent at all, and all three apply. Publishing decides whether a route to the agent exists. Client access decides which client applications that route answers on — leave it empty and any application in the tenant may address the agent, or name applications to narrow it. The access policies above decide each individual call. A request that fails either of the first two gets the same response as one for an agent that does not exist, including for the Agent Card, so a restriction never advertises what it is hiding. The endpoint-URL builder in the Portal offers only the applications a narrowed agent answers on, so a copied URL is never an address that agent refuses.
The conversation is not kept
A hosted agent's tasks carry no history, and asking for one with historyLength returns none. This is a
deliberate declination of something the specification leaves optional. The only record of a turn is the
model's own working thread, and that holds the tool calls the agent made along with the raw downstream
responses that came back — exactly what the boundary between an agent's skills and its tools exists to keep
away from the caller. Filtering another library's serialization format and hoping is a worse answer than
not offering the field. You already hold the messages you sent, and every turn is audited, so the
compliance record exists without being handed to whoever is on the other end of the conversation.
What bounds a turn
A turn that no longer runs on the caller's connection is no longer bounded by it: the caller can hang up
and the model keeps going. So the platform bounds it, under Runtime:A2a:HostedAgents — how many turns
run at once (MaxConcurrentTurns, 4), how many may wait (QueueCapacity, 128), and how long any one may
run (TurnTimeoutSeconds, 300). A turn that exceeds its time fails that task and leaves everything else
alone. A message that arrives when nothing can be queued is answered with a failed task saying so, rather
than accepted and never run — an agent that quietly loses requests would be worse than one that says it is
busy.
What one task may spend
A turn budget bounds a turn. It does not bound a task, and a task is the thing that can be resumed indefinitely: a conversation paused waiting for the caller can be continued as many times as the caller likes, and nothing else limits what it accumulates.
So a hosted agent carries two ceilings — the most input tokens and the most output tokens one of its tasks may spend. Leave either blank on the agent and the deployment's default applies; blank means the platform's answer, never unbounded, so an agent nobody has edited is still bounded. The defaults ship deliberately generous, because per-task spend has never been measured on this platform before now and the safe direction for an estimate that reaches every agent at once is high. Tighten them from the real figures once you have them.
A task that has reached its ceiling fails, with a reason naming the limit and the value it was set to,
so the fix is visible from the task itself. Pausing at input-required instead was considered and declined:
it would be friendlier, but it is a lie about the state — nothing the caller can supply will unblock it,
and a caller told input-required waits for a prompt that never comes.
The limit is enforced by counting, not by asking the model to cap itself. The task's running total is checked before each turn, and again after every model call the turn makes — so a turn that runs away is stopped at the call that crosses the line, not allowed to finish and be caught on the next one. What cannot be avoided is the cost of that one call, because a call's price is only known once it has been made.
The platform deliberately does not send the limit to the model as its own output cap. That parameter is
provider-specific and not even stable within one provider's model range — the OpenAI connector sends
max_tokens, which newer models reject and want max_completion_tokens instead — so a ceiling set that way
fails the turn before the model sees the prompt. Counting what comes back needs nothing of the provider but
the usage figure it already returns, and bounds the whole task rather than one response.
A provider that reports no usage at all is never estimated into a breach: inferring a number would let a guess terminate a caller's task. On such a deployment the turn timeout still applies, and the limit still stops a task that was already over it before the turn began.
Which raises the question of where that usage figure comes from. It is whatever the model provider returns alongside its answer, and the platform reads it in each of the several places a provider may report it — including the one Semantic Kernel's chat-client adapter actually uses, which is not the one its own type system suggests. That mismatch is why every agent's token totals read zero until it was found; see the 1.2 changelog. Nothing is inferred when a provider genuinely reports nothing: the call is still counted, so a zero on these screens is a zero and not a guess.
A ceiling breach is not the agent's declaration — the turn was not permitted to run, so there is nothing to read. It is recorded distinctly in the audit trail from a failure the agent declared itself, because one is a budget you can raise and the other is the agent saying it cannot do the work.
What a task cost
Every task now carries a running total of what it spent: input tokens, output tokens and model calls, accrued by the same write that records each turn's outcome and accumulating across a pause and a resume. Before this, a turn's spend existed only in the governance audit trail — which is scanned in a bounded window and purged on a retention schedule — so a task's cost was readable while its trace survived and gone afterwards, and a task spanning several turns never had a total at all.
The Agents Monitoring screens show it: per task on the tasks grid, and rolled up per agent, for the window being viewed, on the agents grid. The agent's lifetime total is a different figure and lives where it always has, on the Agent Directory.
One thing to know when you plan an agent's access: which client application a request arrived through does not narrow a hosted agent's tools. Tools are gated by the caller's own identity — their issuer, roles, scopes, groups and claims — so two callers with the same claims see the same tools whether they reached the agent through one client application or another. If you need to keep an agent's capability away from some callers, express that on their identity (a scope, a role, a group) rather than on the application they connect from; only identity is read here.
Functional Value
This closes the gap that made server-mode task resumption impossible: a caller talking to a hosted agent gets a genuine back-and-forth instead of an all-or-nothing call, without the platform ever having to mechanically approximate "did the caller give me enough information" — the judgment an LLM is actually good at — and without giving the LLM any capability outside the exact governed tools an administrator explicitly curated for it.
