Skip to content
Forward Deployed

Forward Deployed
Lessons and contents
A2A: Agent-to-Agent ProtocolLesson 4 of 6
6 lessons
Learn

Cross-framework agent interop

Source
From the RAG & Agents Engineering course
By
Sourav Banerjee
Last updated
9 Nov 2026

Report an issue

Cross-framework interop

A Larkspur orchestrator asks a handbook agent for facts and a separate writer for a concise reply. You will discover each agent through its card and inspect the exact payload passed between hops, including Task replies and errors.

Step 1: Agree on the wire contract instead of a framework

The two-hop example serves two agents that share no code. The research agent is a LangGraph ReAct agent with one tool, lookup_facts, on port 10101. The writer agent is a single get_llm(temperature=0.7).invoke(...) call behind a copywriter prompt, with no graph and no tools, on port 10102. Each has its own AgentExecutor and its own Agent Card. An orchestrator reads a registry file listing the two base URLs, fetches both cards, sends a topic to the research agent (hop 1) and passes the research agent's output to the writer agent (hop 2).

Two agents from different frameworks, two hops over A2A The orchestrator reads a registry of two base URLs and fetches both Agent Cards. Hop 1 sends the topic to the research agent, a LangGraph ReAct agent on port 10101, which replies with a Task. The orchestrator reads the answer from status.message. Hop 2 sends that text to the writer agent, one LLM call on port 10102, which replies with a Task holding the paragraph. agent_registry.json 2 base URLs AgentDiscovery2 Agent Cards Orchestrator Research agentLangGraph + lookup_facts hop 1: topic Task readstatus.message Writer agentone LLM call hop 2: notes Task: paragraph
  1. The orchestrator starts from a file of two base URLs and nothing else.
  2. It fetches each agent-card.json and picks the two agents by card name.
  3. It sends the topic to the research agent, a LangGraph ReAct agent on port 10101.
  4. The reply is a Task. The answer is its status message; the notebook’s connector forwards str(Task) instead.
  5. It sends the research notes to the writer agent: one LLM call, no graph, no tools, port 10102.
  6. The writer’s Task carries the paragraph. The orchestrator imported neither agent’s code.

The two agents agree on four things: the card's shape, the message/send method, the Message and Task objects, and the well-known card path. They differ in everything else (framework, prompt, tools, temperature), and the orchestrator imports none of it. Replace the writer's single call with a CrewAI crew or a Google ADK agent, keep the card and the reply shape, and the orchestrator does not change.

On Larkspur Cloud the same layout is a support desk orchestrator, the handbook agent (the T9.1 to T9.4 agentic RAG graph, in LangGraph) as hop 1, and a reply drafter that turns the handbook answer into a customer email as hop 2. The drafter's team can pick any framework.

Step 2: Load skills from cards at runtime

The registry holds URLs only. Capabilities come from each card at run time, so a new agent joins by adding one URL, and a changed skill shows up without an orchestrator release. The example picks cards by card.name. Picking by skill (id or tags) survives a rename and lets two agents offer the same skill. The registry itself is a trust list: only agents on it get your traffic.

Reading the reply

Step 1: Extract answer text from the correct reply shape

The orchestrator talks to both agents through an AgentConnector with one method, send_task(message, session_id). Its reading of the reply decides what hop 2 receives:

Implementation excerpt from the configured application. The practice material supplies its models, data and session setup:

result = response.root.result
if hasattr(result, "parts"):          # a Message
    return "\n".join(part.root.text for part in result.parts if hasattr(part.root, "text"))
return str(result)                    # a Task object; fall back to its string representation

Step 2: Find the answer inside the returned Task

Both executors in the example enqueue a Task before their answer, so every reply is a Task, and a Task has no parts. The connector returns str(result), the Python repr of the whole Task, and the orchestrator forwards that to the writer. The interactive lab shows it with the handbook agent: the repr is 1,492 characters of context_id=…, history=[Message(…)] and TaskStatus(…), around a 185-character answer. Running the example's own AgentConnector cell against the same server returns the same 1,492 characters. The writer, told to synthesise research notes, receives Pydantic field names as its notes.

Step 3: Handle error and input-required replies

Two more paths in the same connector:

  • An error reply. When the agent fails, response.root is a JSONRPCErrorResponse, which has no result. The AttributeError lands in the connector's except, and send_task returns the string Error communicating with agent: 'JSONRPCErrorResponse' object has no attribute 'result', which the orchestrator passes to the writer as research notes.
  • The session. send_task takes a session_id and never sends it, so each hop gets a fresh contextId. To keep the two hops in one conversation, set context_id on the outgoing Message; the server then files the new task under that context.

Step 4: Branch on reply kind before extracting text

A reader that checks for an error first and then branches on the reply's kind fixes the first two:

Implementation excerpt from the configured application. The practice material supplies its models, data and session setup:

root = response.root
if hasattr(root, "error"):
    raise RuntimeError(f"A2A error {root.error.code}: {root.error.message}")
result = root.result
message = result if result.kind == "message" else result.status.message
if result.kind == "task" and result.status.state != "completed":
    raise RuntimeError(f"task {result.id} ended in {result.status.state}")
return "\n".join(p.root.text for p in message.parts if p.root.kind == "text")

An agent that publishes its output as artifacts (TaskUpdater.add_artifact) puts it in result.artifacts instead; read those the same way.

Try it yourself

Give the connector a Task with text only in status.message. Check that it has no parts attribute and that stringifying the whole Task is not answer extraction. Compare a corrected reader that checks errors first, then Message parts, Task status and artifacts, before passing plain answer text to the writer.

Practice notebook

Run the complete implementation from the practice material after checking the trace or calculation above. Use its stated environment for live model or server calls; recorded model choices and timings are observations, not guaranteed outputs.

Run the notebooks

The code for this lesson is in these notebooks. Colab runs one in your browser: install the packages its first cell imports and set the API keys it reads. To run every notebook as written, clone the repository and use its uv environment.

Notebook GitHub Colab
Cross framework agent interop Open on GitHub Open in Colab

Key Takeaways

  • Agents built in different frameworks interoperate when they share the card shape, message/send, the Message and Task objects and the card path. The orchestrator imports none of their code.
  • Keep only URLs in the registry and read capabilities from each card at run time. Pick agents by skill, and treat the registry as a trust list.
  • Read replies carefully: check for an error first, branch on the reply's kind, and set context_id to keep hops in one conversation. Never forward str(result) as content.

Check yourself

1. What must the research and writer agents agree on for one orchestrator to call both, and what may differ?

Four things: the card's shape, the message/send method, the Message and Task objects, and the well-known card path. Framework, prompt, tools and temperature may all differ, and the orchestrator imports none of them, so a CrewAI crew could replace the writer without an orchestrator change.

2. The writer agent receives 1,492 characters of field names and TaskStatus text instead of research notes. What went wrong in the connector?

Both executors enqueue a Task, and a Task has no parts, so AgentConnector falls through to str(result), the Python repr of the whole Task around a 185-character answer. Check for an error first, then branch on result.kind and read the text parts of result.status.message for a task.

3. Why pick agents from the registry by skill id or tags rather than by card name, as the notebook does?

Picking by skill survives a rename and lets two agents offer the same skill. The registry holds URLs only and capabilities come from each card at run time, so a new agent joins by adding one URL. The registry is also a trust list: only agents on it get your traffic.

Rate your confidence and keep notes on the module page.