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

kinlan1 pts0 comments

When agents talk: tool calls, handoffs, and two wallets · kulikowski.me

← WritingLast 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 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.<br>"On-site" is about ownership, not where the agent loop runs. In this demo the on-page agent's loop runs right in the tab, powered by an API key you can paste in - great for a demo, but you'd never ship it (a key in client-side JS is public within minutes 🙈). In production the sensitive part usually lives on the site's server. The browser might still coordinate the UI and parts of the loop, while the backend makes the model calls and injects any private instructions. "On-site" just means the site owns and operates the agent, wherever each part actually executes.

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",<br>"type": "stream-request",<br>"payload": {<br>"jsonrpc": "2.0",<br>"id": "...",<br>"method": "SendStreamingMessage",<br>"params": {<br>"message": {<br>"messageId": "...",<br>"role": "ROLE_USER",<br>"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...

agent page tool site agents thing

Related Articles