kulikowski.me
← Writing

When agents talk: tool calls, handoffs, and two wallets

11 min read#agentic-web #a2a #webmcpview as markdown

Last time I wrote about three homes of agents and ended on two questions: when a visiting agent meets a website's on-site agent, how do they actually talk? And - who does the on-site agent really work for?

So I did the only thing that ever settles these for me: I started experimenting with it 🛠️.

In this blog post I am sharing my observations and takeaways. Remember - this is my perspective - your business is unique and I can never foresee everything - build and test what works best for you! 👍

I'd love to hear how these ideas work out for you, so if you try them, please reach out 😀.

Demo: Agents talking to each other

The setup: a web page that is two things at once - a WebMCP tool provider (it registers tools like add_note on document.modelContext) and an A2A server that holds real agent-to-agent conversations.

The whole thing is on GitHub if you want to run it yourself: Kulikowski/agents-talking-to-each-other.

The visiting agent (the extension) can work with the page in three different ways.

1️⃣ Direct tools (WebMCP). The visiting agent grabs the page's tools and does the thinking itself - add_note, list_notes, one call each. All the reasoning happens in the extension.

2️⃣ Ask the page's agent (ask_page_agent). The page also exposes its own agent as a WebMCP tool. So the visiting agent makes one tool call - and behind it, a whole other agent loop runs and hands back an answer. It's delegation via a plain tool call. You can already see this pattern in other demos: André Bandarra's Flyby exposes an on-page flight-filtering agent as a WebMCP tool called filter_flights - and that agent runs right in the browser on the built-in Prompt API with structured output.

3️⃣ A2A. A proper peer conversation: messages, tasks, an Agent Card that advertises what the agent is good at, and a contextId that ties turns together. This is where the handoff becomes visible.

One thing about that third option: in this demo A2A actually shows up twice. There's the textbook version - a backend agent sitting behind a real HTTP endpoint - and a scrappier one, the page's own agent reached over a postMessage transport (a web page can't be a real HTTP server, so I had to improvise). Same A2A payloads, two very different pipes - and the next two sections dig into each 👇.

The demo running: on the left, the web page - a shared sticky-notes board, a chat, and a protocol-traffic log. On the right, the browser extension's side panel with a Caller: Alice identity and a Quick actions row: 'Summarize notes directly' (extension agent, WebMCP tools), 'Ask the page agent to summarize' (page agent, WebMCP delegation), 'Summarize notes over A2A' (page agent, A2A), and 'Start booking a launch meeting' (backend agent, A2A, asks for the day). Below them a per-turn summary reads Reasoned by: page agent, Transport: WebMCP, Model calls: 2, Tool calls: 1, Final: completed.
The whole thing running side by side: the page (left) and the extension's agent (right). Three of those quick actions run the same task through each channel - direct tools, agent-as-tool, and A2A - and the fourth kicks off the backend's book-a-meeting flow that pauses for the missing day. After each turn the panel spells out who reasoned, over which transport, and how many model and tool calls it took.

Wait, can a web page even be an A2A server?

Short answer: not really - and that gap turned out to be the most interesting part 😅.

A2A is a layered protocol. There's a data model (messages, tasks, Agent Cards), a set of operations (SendMessage, SendStreamingMessage, GetTask, CancelTask), and transports that carry them. The current spec includes JSON-RPC over HTTP, HTTP+JSON and gRPC - different pipes, but all of them assume the same thing: an addressable network service.

A web page is not that. It can open connections and receive data through them, but it can't expose its own listening endpoint - there's no address where an outside A2A client can initiate a request directly to that tab. So to make my on-page agent speak A2A, I kept the first two layers exactly as the spec defines them and swapped only the third: the JSON-RPC payloads ride inside window.postMessage instead of an HTTP request.

Here's a shortened version of the actual streaming envelope passed between the extension's content script and the page:

{
  "channel": "a2a-postmessage",
  "type": "stream-request",
  "payload": {
    "jsonrpc": "2.0",
    "id": "...",
    "method": "SendStreamingMessage",
    "params": {
      "message": {
        "messageId": "...",
        "role": "ROLE_USER",
        "parts": [{ "text": "..." }]
      }
    }
  }
}

That payload preserves A2A's JSON-RPC message shape. Here's the fun bit: for a fresh message, lift the JSON-RPC body out of the envelope, add the backend's bearer token, POST it to the backend agent's real /a2a endpoint, and it is accepted unchanged 😎. The message is A2A-shaped; only the pipe is my own experiment. Task and context state still belong to the agent that created them.

So why not declare victory and call the page an A2A server? Because that postMessage pipe sits outside three mechanisms A2A normally builds on through the web stack: discovery (no dialable URL, so my card just points at a made-up postmessage:// address), auth (I can check the sending window, but that isn't authenticating a caller), and reach (only code with access to that tab and my custom bridge can reach it - a standard A2A client cannot dial it from outside - and it's all gone when the tab closes). Fine for a demo; nowhere near a trust boundary 🙂.

But this leaves me with a question: could the browser eventually provide a native, local channel for agents meeting in the same tab (for example, a page's own agent and a visiting browser or extension agent)? What would agent cooperation look like if the browser handled discovery and auth - and this postMessage pipe was no longer needed? Genuinely interesting 🤔.

The real thing lives on the backend

To see where A2A truly shines, we have to look backend-side.

In my demo project, the Node server (server/server.mjs) implements the A2A JSON-RPC operations used by the experiment. It exposes a proper HTTP POST endpoint (/a2a), streams events over SSE and publishes an Agent Card at /.well-known/agent-card.json. Any compatible A2A client on the network can reach it without the web page or extension - it just has to present one of the demo's bearer tokens (demo-alice or demo-bob, so callers actually have an identity) - and the LLM API keys stay hidden on the server.

The fun part is that the on-page agent and the backend agent run the exact same code (shared/agent-core.js). They share the same core reasoning, but we gave them different "hands" through their toolsets:

  • The on-page agent is given tools (list_notes, add_note) that read and edit the DOM in the tab. It has hands on the live page, but it is bounded by the tab's lifespan.
  • The backend agent is given server-side tools (list_memos, add_memo, book_meeting, get_server_time). It can't see the sticky notes on the web page at all - but it can write memos to the server's memory that survive tab refreshes.

If you ask the backend agent about the stickies on the page, it will tell you, honestly: "I don't have access to the screen. You'll need to ask my client-side colleague."

To be clear, that blindness is a demo choice, not a hard limit. The page could keep a live connection open to the backend - Server-Sent Events for the backend to push updates down to the page, or a WebSocket for two-way traffic where the backend could even drive the board. I just left it out to keep the two agents in their own lanes 🙂.

The moment that made A2A click for me was much smaller, though. Ask the backend agent to "book a meeting about the launch" without giving it a day. The task moves from working to input-required and asks you a question. Reply "Friday" and the client sends that answer back with the same taskId - the same task wakes up and finishes.

That is not just two chat messages that happen to follow each other. The pause itself is a task the server holds onto - a real object with its own id and a state - so you can look it up with GetTask, cancel it with CancelTask, or answer it later and watch that same task pick back up (while the demo's in-memory backend is still running). In a plain tool call, a pause is just "the last message was a question"; here it's something the protocol tracks and can hand back to you by id. That, to me, is the clearest reason A2A exists 💡.

A close-up of the extension side panel showing the input-required flow over A2A HTTP. The user asks the backend agent to book a meeting about the launch; the agent calls SendStreamingMessage, then its ask_user tool, and replies 'Which day would you like to book the launch meeting for?'. The user answers 'Friday'; the agent calls book_meeting and confirms it booked the meeting for Friday. A task panel shows the task id and working to completed, with GetTask and CancelTask buttons.
The same flow up close: the backend agent pauses with an ask_user question (input-required), waits for 'Friday', then resumes the very same task and finishes - GetTask, CancelTask and the task id all sitting right there.

Three ways to expose your site to agents

After all this experimentation, I keep coming back to one question that feels really central: does a business even need to build its own agent at all - and if it does, how should it expose that agent to the outside world?

The same three-way choice shows up wherever you stand - MCP vs. agent-as-MCP-tool vs. A2A on the backend, WebMCP vs. ask_page_agent vs. A2A in the browser. Different pipes, same decision. Here's how I think about it:

1️⃣ Raw tools. You register add_note, list_notes etc as plain tools and let the visiting agent do all the driving - it's the only one doing the thinking. If the tools themselves don't run a model, this leaves the inference bill with the visiting agent. It is also the most broadly compatible option - any client that understands your tool interface can use it. The catch: the visitor's agent has to understand your whole system from tool descriptions alone (I might experiment with exposing SKILLS for WebMCP 🤔), and there's nowhere to put your own judgment - the policies, the edge cases, the context a stranger shouldn't need to know.

2️⃣ Agent as a tool. You wrap your own agent as a single tool - ask_page_agent, or one ask_agent over MCP. The visitor makes one call; a whole agent loop runs behind it and hands back an answer. Now your business logic stays encapsulated - policies and catalog knowledge live inside your agent, not leaked into a schema - and the expensive reasoning runs on your meter, not the visitor's. A plain tool interface usually starts as one invocation and one result. MCP now has a Tasks extension for adding task handles, progress and input requests to that lifecycle, although client support is not universal. In my WebMCP demo I would still have to build caller isolation and conversation continuity myself.

3️⃣ A2A. You give your agent a real identity - an Agent Card with a name, skills, a version - and let visitors hold multi-turn conversations, track task state (working → input-required → completed), and tie related turns together via contextId. It's a counterpart, not a function. The task lifecycle is first-class, and - the big one - served over HTTP it is reachable from outside the browser entirely. The server still has to authenticate callers and bind each contextId to the right one - a context id is continuity, not a security boundary. The price is real implementation complexity and a younger ecosystem: not every framework speaks A2A yet.

My rule of thumb starts with one question: is there an agent of your own in the loop - one you already run, or one you'd want to? If not - the visiting agent can do all the driving, and you've no judgment to add - stay with raw tools; you don't need a site-side agent at all. But if you already have one, or you decide you want one (to hold the policies, the catalog knowledge, the house rules), the question stops being whether and becomes how you expose it.

And the low-friction answer to that how is almost always the same: wrap your agent as a single tool (option 2️⃣). If you already built one, the protocol surface is tiny - one endpoint, one description - and suddenly lots of tool-calling clients can reach it. Operating that endpoint safely still means auth, rate limits, caller isolation, observability and cost controls, of course. But there is no new agent protocol to implement, no Agent Card to maintain, and no reason to break your agent's internals.

A2A is worth layering on top once you know what you need it for - either way, starting with agent-as-tool gives you reach and real feedback today, and you can graduate to A2A when a single-turn tool becomes the real bottleneck - your agent's logic won't change when you do, only the interface around it 💡.

So, whose wallet?

Notice the thread running through all three options: who does the thinking. That turns out to be the whole game - because whoever runs the agent usually pays for its model calls.

In my demo, one API key paid for all of it - a little lie the demo tells. In reality there are two wallets:

The user's agent - the one in the browser - runs on the user's side of the meter every time it reasons about the page. Exactly whose pocket that is can vary - the user's own API key, a subscription, or a browser or agent vendor absorbing it on some free tier - but the point is it's not the site's. The site's agent, meanwhile, spends the website owner's money.

So delegation isn't just a tidy abstraction. It transfers the immediate inference cost and operational responsibility. When the visiting agent calls ask_page_agent, the expensive multi-step thinking runs on the site's meter, not the user's - unless the site decides to charge it back somehow, of course 😅.

Which flips the incentives depending on which chair you're sitting in. Sites might favor option 1️⃣ - visitors bring their own inference, and you get an "agent-ready" site for the price of some static tool definitions. Users might prefer delegation - let the site pay to run its own complexity. My take is that it is up to each business to understand what its users actually need and expose the right interaction channel - raw tools, delegation, or both.

In my "Agents talking to each other" demo you can steer the extension agent down either path - just tell it which channel to use. Toss it a task like "summarize my layout and clear the board":

  • Ask for direct tools, and you'll watch the extension's own agent call the page tools one by one - every step on its meter.
  • Ask for A2A, and it hands the whole task to the page agent and gets one tidy result back - a proper A2A task, working → completed, on its own conversation thread.

So who does the on-site agent work for?

The wallets split cleanly - and the loyalties split the exact same way.

When your agent and a site's agent talk, it's tempting to picture one tidy system cooperating. It's richer than that - it's two agents, each an expert on its own side. Yours knows your budget, your history, what you're actually after. The site's knows its catalog, its stock, its policies cold. Neither could do the other's job - which is exactly why putting them together is powerful 🤝.

It's the same happy arrangement as a great shop assistant: they genuinely want to help you, and they know the store's range better than anyone - both true at once, no contradiction. So when your agent leans on the site's, it does the natural thing: gladly take the expertise, while remembering it speaks for its shelves. That's not a reason for suspicion - it's the reason having your own agent in the room is so handy.

Two things I'm still thinking about

Two threads I pulled and had to put back down before this turned into a book 😅:

Prompt injection has a direction. Every tool description and every reply the page hands to the visiting agent is site-authored text entering the user's model context. And everything the user types to the site's agent is user text entering the business's model context. Two directions, two threat models. Worth its own post.

My take is that the real case for on-site agents isn't intelligence - the visitors who show up with their own agent already bring that. It's coverage and judgment. Coverage, because if the site gives its agent a normal human-facing interface too, it can work for everyone who lands on the page - no extension, no special browser, no setup - including the (still huge) majority who don't travel with an agent at all. And judgment, because it's the natural home for what only the site has - the user's purchase history, the private docs, the tools too privileged to hand a stranger, the house rules.

I think of the on-site agent as the site's API with judgment which can also help people who visit the site without bringing their own agent. Interesting! 🤔

Exciting to see how this space evolves 🎉

More soon. ✨