AIDA over A2A — confirmations and errors

How AIDA behaves once a request reaches her over A2A — the surface Gemini Enterprise dials at chat.neonlaw.com (and any other A2A client). The agent-card, OAuth, and one-time wiring live in gemini-enterprise-mcp.md; this doc is the runtime interaction model: how a free-form ask becomes a tool call, where AIDA pauses to ask yes/no, and how a failure's reason gets back to the user instead of a blank non-result.

It answers two questions that came out of real Gemini Enterprise use:

  1. When AIDA already has every value she needs, why does she still ask, and can that be a tap instead of a typed reply?
  2. When a tool fails (bulk import was the case in point), why did the chat show "an error" with no message — and how is the reason propagated now?

The request lifecycle

A message/send with free-form text (no metadata.skill) runs an agentic loop in web::a2a::drive_loop. The rule in three words: reads run; writes wait.

user: "send a welcome email to nick@neonlaw.com"
   │
   ▼  router (Vertex AI Gemini Flash) picks the next tool
show_person { email: "nick@neonlaw.com" }      ← read-only: runs inline, no prompt
   │  result fed back into history → router picks again
send_welcome_email { person_id: <uuid> }       ← side-effecting: PAUSES here
   │
   ▼  Task state = input-required
"Authorize this action? AIDA wants to Send Welcome Email for Nick (nick@neonlaw.com)…
 Choose yes to authorize, or no to cancel."   ← message also carries a structured yes/no choice (data Part)
   │  second message/send, same taskId + contextId, structured choice { confirmation: "yes" }
   ▼
send runs → Task state = completed, the send is the artifact

Read-only tools (tools::READ_ONLY_TOOLS) run unconfirmed, so a lookup→act chain only ever stops the user once — at the act. Everything else is side-effecting and waits.

The confirmation gate

When the router picks a side-effecting tool, drive_loop does not run it. It stashes the resolved call, returns the Task in the non-terminal input-required state, and the prompt rides in status.message. The follow-up message/send (same taskId/contextId) routes through resume_after_confirmation, which enforces the trust boundary and then runs, cancels, or re-prompts.

The gate is not decoration — it is a legal-supervision requirement. A client-facing act AIDA proposes is authorized by a licensed human (ABA Model Rule 5.3 supervision of a non-lawyer assistant). Two checks run before the call fires:

Every decision (proposed / authorized / declined / denied_identity / denied_unauthorized) is emitted as a target: "audit" event — that log, not the in-memory pending store, is the durable record of who authorized what.

The confirmation is a structured yes/no choice — no free-text command surface

The gate needs only a yes/no, so it does not ask for a free-text prompt. The input-required message carries two parts: the one-sentence prompt (human-readable) and a structured data Part — confirmation_choice_part — that declares the answer as a constrained JSON-Schema enum/oneOf (yes / no, each with a label). A constrained enum is the universal signal that tells a schema-aware client to render a one-tap choice rather than a text box.

extract_confirmation reads the chosen value from the structured data Part first (a {"confirmation":"yes"} object or a bare "yes"). If the client doesn't wrap the choice in a data Part but instead echoes the chosen token back as plain text (Gemini Enterprise's shape), that exact token is accepted too, so the gate behaves identically regardless of envelope — no external client behavior to verify. Only the exact tokens yes / no authorize or decline; a free-form sentence matches neither and re-prompts, so there is no natural-language command surface: the action needs only a yes, and only a yes is read.

The engineering council reviewed this. The findings, and the line between what we control and what we do not:

The consensus action: keep the gate, advertise the structured yes/no choice, accept only the exact yes/no token in either envelope, and pursue the internal-vs-client-facing split as a legal-council item.

Error propagation

A tool result is two parts (see tool_result_to_parts):

  1. a text Part from content[0].text — what a chat UI renders to the user;
  2. a data Part from structuredContent — for programmatic A2A clients.

Gemini Enterprise renders the text Part and effectively drops the structured one. So any failure reason that lives only in structuredContent is invisible — the user sees a bland line and reads it as "an error with no message."

Bulk import: the case that surfaced this

import::apply returns Ok(report) even when structural validation rejects the payload (then organizations/people are empty) or an individual row fails — the reasons live in report.diagnostics and each RowOutcome.detail. The tool used to render only the tally:

Bulk import: 0 created, 0 updated, 0 unchanged, 0 failed.

That is the silent non-result the user hit. The fix folds the reasons into the text Part via ImportReport::problem_lines, so aida_bulk_import now returns:

Bulk import: 0 created, 0 updated, 0 unchanged, 0 failed.

Problems:
• version (error): unsupported contract version 2 (this engine speaks 1)
• organization `njp` failed: unknown jurisdiction code `ZZ`
• person `abigail`: organization `njp` was not created; link skipped

Because the A2A bridge reads content[0].text, this reaches Gemini Enterprise with no A2A-specific code. The structured diagnostics/detail fields are still present for programmatic clients. problem_lines lives on the report (not the tool) so the cli import-contacts path and the future web upload route surface the same text.

The general rule

Put the why in content[0].text. A tool whose failure reason exists only in structuredContent will read as a message-less non-result on any text-only A2A client. The Gemini Enterprise MCP-server description already tells the planner to "show the user the error and ask whether to retry" (see gemini-enterprise-mcp.md) — that only works if the error text is actually in the result.

The Foundation workshop runs on this surface

The Foundation's workshop, Using the Navigator to Rapidly Solve Legal Outcomes (/foundation/workshops/navigator), is the canonical end-user entry into exactly this A2A path. Lawyers add AIDA through Gemini's "Add AIDA" connector — no install, no CLI — and every "tool call" is a Gemini prompt routed through AIDA's tools over A2A. Two behaviors from this doc are the ones a workshop attendee feels directly:

Cross-references — docs and the tests that ground them

Each behavior described above is grounded by a test or a BDD feature, so the doc and the executable spec stay in step: