Overview
Multi-agent frameworks assume all agents live in one process, one codebase, one trust domain. The more interesting (and realistic) enterprise question is delegation across boundaries: our procurement agent asking a supplier's quoting agent for pricing, where neither side sees the other's internals. I ran a research spike implementing A2A-style delegation between deliberately opaque agents to find out where the protocol abstraction actually holds up.
The setup: a 'requester' orchestration agent and two 'contractor' agents (data-analysis, document-drafting) that publish capability cards, accept task proposals, and stream status updates over JSON-RPC. Each contractor is internally a LangGraph app, but the requester only sees the protocol surface. The core finding: lifecycle and discovery are the easy parts — the unsolved problems are semantic capability matching, calibrated trust in self-reported results, and cost/latency negotiation before committing to a delegation.
Architecture
- Each agent serves a capability card at a well-known endpoint: skills, input/output modes, auth requirements, and declared latency class.
- Task lifecycle is a small state machine — submitted -> working -> input-required | completed | failed — with the requester polling or subscribing to status events.
- Contractors are opaque: the requester sees task status and artifacts only, never internal reasoning, tools, or model choice.
- The requester keeps a per-contractor scorecard (result-acceptance rate, deadline adherence) and biases future routing toward higher-scoring contractors.
- Artifacts are typed and hashed; the requester independently validates schema and spot-checks results rather than trusting contractor self-grades.
- OpenTelemetry traces propagate a delegation ID across both sides, which proved essential for debugging cross-agent failures.
Code Snippets
AGENT_CARD = {
"name": "quote-analysis-agent",
"version": "0.3.0",
"skills": [
{
"id": "analyze_supplier_quotes",
"description": "Compare supplier quotes on price, lead time, "
"and contract-term risk. Input: quote PDFs or "
"structured line items. Output: ranked comparison.",
"input_modes": ["application/pdf", "application/json"],
"output_modes": ["application/json"],
"latency_class": "minutes",
}
],
"auth": {"schemes": ["bearer"]},
}
async def delegate_task(contractor_url: str, skill_id: str, payload: dict) -> Task:
resp = await rpc_client.call(contractor_url, "tasks/send", {
"skill_id": skill_id,
"message": {"role": "user", "parts": [{"type": "data", "data": payload}]},
"metadata": {"delegation_id": current_trace_id(),
"deadline": iso_in(minutes=10)},
})
return Task.model_validate(resp)async def accept_result(task: Task, artifact: Artifact) -> Verdict:
# 1. Structural: does the artifact match the skill's declared schema?
try:
report = QuoteComparison.model_validate(artifact.data)
except ValidationError as e:
return Verdict.reject(f"schema violation: {e.error_count()} errors")
# 2. Sanity invariants a wrong-but-plausible result would break.
if report.recommended.total_cost > min(q.total_cost for q in report.quotes):
return Verdict.reject("recommended quote is not cost-minimal "
"and no risk justification was provided")
# 3. Spot-check one line item against the source document ourselves.
sample = random.choice(report.quotes)
extracted = await extract_line_total(task.source_docs[sample.doc_id])
if abs(extracted - sample.total_cost) / extracted > 0.02:
return Verdict.reject(f"spot-check mismatch on {sample.doc_id}")
scorecard.record_acceptance(task.contractor_id)
return Verdict.accept(report)Lessons Learned
- Capability discovery is the weakest link. Matching a task to a skill description is itself an LLM judgment call, and a vague capability card poisons everything downstream.
- Never trust a contractor's self-grade. Independent schema validation plus cheap spot-checks caught every bad result our injected-fault tests produced; self-reports caught none.
- Deadlines and cost must be negotiated before delegation, not discovered after. The protocol needs an estimate phase — we bolted one on and it changed routing decisions materially.
- Opaque agents make debugging brutal without shared trace IDs. Propagating one delegation ID through both sides' telemetry was the difference between debugging and guessing.
- The protocol is the easy 30%. State machines and JSON-RPC are solved; calibrated trust between organizations is the actual research frontier, and it looks more like vendor management than computer science.