Home / Session 7
session-7.md — ~/hacking-with-ai
7

The Build Team

Build a service with a parallel agent team coordinated via A2A

45 min Claude Agent SDK + A2A Protocol Loading...
Session 7 of 8

// Overview

Use the Agent-to-Agent (A2A) protocol to coordinate a multi-agent build team. A team leader runs an A2A hub, dispatches builder agents in two phases — MODELS first (other agents depend on its types), then API + DATABASE in parallel — sharing artifacts through the hub so downstream agents build on real types instead of stubs. Each agent works in its own git worktree, runs tests, and pushes if green. The team leader merges branches and resolves conflicts. All examples use the Claude Agent SDK (@anthropic-ai/claude-agent-sdk) and run with bun.

Learning Objectives

// Theory

What query() Does

5 min

query() is the single entry point of the SDK. You give it a prompt and options, it spawns a claude CLI subprocess, and returns an AsyncIterable that yields messages as the agent works. The agent loops — reason, tool call, get result, reason again — until it has an answer or hits maxTurns. You consume these messages with a standard for await loop. Each query() call is independent: own subprocess, own context, own tool permissions.

Key Concepts

  • query() spawns claude as a child process and streams messages back via AsyncIterable
  • The agent loops (reason → tool call → get result → reason again) until it has an answer or hits maxTurns
  • for await (const msg of q) gives you every message as it happens — tool calls, text, and the final result
  • allowedTools is a whitelist — the agent can only call tools you list
  • Each query() call is independent: own subprocess, own context, own tool permissions
  • systemPrompt defines the agent's role — a builder, resolver, or any specialist you need
  • cwd sets the working directory — point each agent at its own worktree for isolation
diagram
import { query } from '@anthropic-ai/claude-agent-sdk'

query({ prompt, options })
  │
  ├─ Spawns `claude` CLI as a child process
  ├─ Sends prompt via JSON-RPC over stdin
  ├─ Claude runs its agentic loop:
  │   ├─ { type: 'assistant', message: { content: [...] } }
  │   │     content: [{ type: 'text', text: '...' }]
  │   │              [{ type: 'tool_use', name: 'Write', input: {...} }]
  │   ├─ tool results fed back automatically
  │   └─ ... repeats until done or maxTurns hit
  ├─ Yields each message to your `for await` loop
  └─ Final message:
      { type: 'result', result: '...', total_cost_usd: 0.02 }

Options you control:
  allowedTools:    ['Read', 'Write', 'Edit', 'Bash']  // tool whitelist
  maxTurns:        25                                  // iteration cap
  systemPrompt:    'Build the model layer...'         // agent role
  cwd:             './worktrees/models'               // working directory (worktree)
  permissionMode:  'bypassPermissions'                // no tool approval prompts
  model:           'sonnet'                           // model selection

A2A Protocol: Why Agents Need to Talk

5 min

When agents build in parallel, they face a real dependency problem: the API controller needs model types, the repository needs model types, but the MODELS agent is building those types at the same time. Without coordination, downstream agents create stubs that may not match the real types — leading to merge conflicts. The Agent-to-Agent (A2A) protocol solves this by giving the team leader a hub to publish and fetch artifacts between build phases. MODELS builds first, its artifacts go into the hub, and the team leader distributes real types to downstream worktrees before they start building.

Key Concepts

  • A2A is a JSON-RPC 2.0 protocol over HTTP — agents register, publish artifacts, and fetch artifacts from other agents
  • The hub exposes /.well-known/agent.json for agent card discovery (name, capabilities, skills)
  • Artifacts are the key concept: built files (Java classes, configs) that other agents depend on
  • The team leader is the A2A client — it publishes and fetches on behalf of agents, making the flow reliable
  • Two-phase builds respect the dependency graph: MODELS first, then API + DATABASE in parallel with real types
  • Without A2A, agents create stubs → stubs diverge from real types → merge conflicts. With A2A, types flow through the hub → clean merges
diagram
// A2A hub sits between build phases:

  Phase 1                    A2A Hub                    Phase 2
  ───────                    ───────                    ───────
  MODELS agent               localhost:9100
  │ builds types             ┌───────────────┐
  │ runs tests               │  Agent Cards  │
  │ commits                  │  ───────────  │
  └───────────────────▶│  MODELS      │
     team leader              │  API          │
     publishes                │  DATABASE     │
     artifacts                │               │
                              │  Artifacts    │
                              │  ───────────  │
                              │  Payment.java │────▶ API agent
                              │  Status.java  │      │ has real types
                              │  Request.java │      │ builds controller
                              │              │      │ no stubs needed
                              │              │
                              │              │────▶ DATABASE agent
                              └───────────────┘      │ has real types
                                                    │ builds repository
                                                    │ no stubs needed

Parallel Execution + Worktrees

5 min

Each query() spawns its own subprocess — they don't share context or state. Git worktrees give each agent an isolated copy of the repo on its own branch. The two-phase approach means MODELS builds first (it has no dependencies), then merges into main. Phase-2 worktrees branch from the updated main, so model types are in git history — not copied files. Each agent writes integration tests and verifies their work before pushing.

Key Concepts

  • git worktree add creates a separate working copy with its own branch — agents can't step on each other
  • Two-phase build: MODELS first (phase 1), then API + DATABASE in parallel (phase 2)
  • After MODELS completes, merge feature/models into main — phase-2 worktrees branch from updated main
  • Model types are in git history, not copied files — agents treat them as committed code, not foreign stubs
  • Promise.allSettled() runs phase-2 agents concurrently — wall time = slowest agent
  • Each agent writes integration tests and verifies their work in their own worktree before pushing
  • Phase-2 merges are clean — model files are identical (same git ancestor), no conflicts on shared types
diagram
// Two-phase build with git-based type distribution:

    main branch (module + Application.java scaffolded)
      │
      ├── worktree: .worktrees/models  (feature/models)
      │
    Phase 1:
      ┌──────────┐
      │  MODELS  │  builds types + repo interface, tests, pushes
      └────┬─────┘
           │
           ▼
      A2A hub: publish MODELS artifacts
           │
      ┌────┴─────────────────────┐
      │  merge feature/models    │
      │  INTO main               │
      │  (types now in git)      │
      └────┬─────────────────────┘
           │
      main (updated — has model types in git history)
      │
      ├── worktree: .worktrees/api  (feature/api)
      └── worktree: .worktrees/db   (feature/database)

    Phase 2 (parallel):
      ┌──────────────┐    ┌──────────────┐
      │     API      │    │   DATABASE   │
      │ (model types │    │ (model types │
      │  in git      │    │  in git      │
      │  history)    │    │  history)    │
      │  tests POST/ │    │  tests CRUD  │
      │  GET endpts  │    │  operations  │
      └────┬─────────┘    └────┬─────────┘
           │                   │
           ▼                   ▼
    ┌─────────────────────────────┐
    │  merge phase-2 only        │
    │  (feature/models already   │
    │   merged — clean merges!)  │
    │  final: ./mvnw test        │
    └─────────────────────────────┘

// Setup

Install the SDK with bun and verify the import works.

bash
cd src/data/demo
bun install
bun -e "import { query } from '@anthropic-ai/claude-agent-sdk'; console.log('SDK imported')"
Verify: The bun -e command prints 'SDK imported' without errors. The SDK uses your Claude Code login — no ANTHROPIC_API_KEY needed. For production use outside Claude Code, set ANTHROPIC_API_KEY.

// Challenge

Build the Service with A2A

30m +5m bonus

Stand up an A2A hub, dispatch builder agents in two phases (MODELS first, then API + DATABASE in parallel with real types from the hub), merge branches, and run the service.

✓ Success Criteria

  • A2A hub runs and tracks agent registrations + artifact counts
  • MODELS agent builds first; its artifacts are published to the hub
  • API + DATABASE agents receive MODELS types from the hub before building
  • API + DATABASE run concurrently in separate git worktrees
  • Branches merge into main (conflicts resolved if needed)
  • ./mvnw spring-boot:run starts, curl localhost:3000/payments works
⚠ Hints (click to reveal)
  • Start the A2A hub (Bun.serve on port 9100) before creating worktrees
  • Register each agent on the hub with agent/register before dispatching
  • After MODELS finishes, collect its .java files and call artifacts/publish
  • Fetch MODELS artifacts with artifacts/get and write them into phase-2 worktrees
  • Promise.allSettled() prevents one failing agent from rejecting the whole batch
  • If merge conflicts happen, spawn a resolver agent with the spec and 'git diff' as context

Code Playground

Interactive

Try These Prompts

Hello query()
Simplest call: query({ prompt: 'What is 2+2?' }). Iterate, print text content.
No tools needed. for await (const msg of query({ prompt: '...' })) — check msg.type === 'assistant' and find blocks where type === 'text'.
Expected: Prints Claude's answer. Final message has type: 'result' with total_cost_usd.
Single Builder
One agent builds the domain models from spec.md. Give it Read, Write, Edit, Bash, Glob, Grep tools. It creates model files, writes tests, runs them, and commits.
Load spec.md with readFileSync(), inject into systemPrompt. Set cwd to the repo directory. Use allowedTools: ['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep'] and permissionMode: 'bypassPermissions'.
Expected: Agent creates model classes with validation, writes unit tests, runs ./mvnw test, and commits if green.
Worktree Setup
Use execSync() to create git worktrees: git worktree add .worktrees/models -b feature/models (repeat for api and database).
Clean up existing worktrees first with try/catch around git worktree remove. Create a run() helper that wraps execSync with cwd and timeout.
Expected: Three directories created under .worktrees/, each on its own branch. git worktree list shows all three.
A2A Hub
Start a Bun.serve on port 9100 with JSON-RPC 2.0 endpoint (/a2a) and agent card discovery (/.well-known/agent.json). Support methods: agent/register, agent/updateStatus, artifacts/publish, artifacts/get.
Store agents in a Map and artifacts in a Map. The hub is the team leader's coordination mechanism — agents don't call it directly.
Expected: curl http://localhost:9100/.well-known/agent.json returns the hub's agent card. POST to /a2a with JSON-RPC calls work.
Two-Phase Build
Run MODELS agent first (phase 1). Collect its .java files, publish to hub. Fetch from hub, write into API + DATABASE worktrees. Run both in parallel (phase 2).
After MODELS query() resolves, walk the worktree's src/ dir to collect .java files. Call artifacts/publish. Then artifacts/get and writeFileSync into phase-2 worktrees before spawning those agents.
Expected: API and DATABASE agents find real model types in their worktrees. They build on real types, not stubs. Fewer merge conflicts.
Streaming Progress
While agents build, print each tool_use block with an [AGENT_NAME] prefix.
Inside runAgent(), when you see a tool_use block: console.log(`[${agent.name}] ${block.name}(${detail})`). Use block.input.file_path ?? block.input.command ?? block.input.pattern for the detail.
Expected: Console shows [MODELS] tool calls first, then interleaved [API] and [DATABASE] tool calls in phase 2.
Merge & Resolve
After agents finish: git checkout main, then git merge each feature branch. If conflict, spawn a resolver agent.
Wrap git merge in try/catch. On conflict, spawn a new query() with systemPrompt 'You are a merge conflict resolver' and prompt it to run git diff, fix conflicts, and git add -A && git commit --no-edit.
Expected: Branches merge into main. Fewer conflicts than without A2A because types are shared. Final ./mvnw test passes.

Code Examples

Single Builder
import { query } from "@anthropic-ai/claude-agent-sdk";
import { readFileSync } from "fs";
import { resolve } from "path";

// 2-build-one.ts — Single builder agent.
// Reads a spec file, sends it as context, and lets one agent build a layer.
// Generic: swap the spec file to build anything.

const SPEC_PATH = resolve(import.meta.dir, "sample/spec.md");
const REPO_DIR = resolve(import.meta.dir, "../multi-agent");

const spec = readFileSync(SPEC_PATH, "utf-8");

const systemPrompt = `You are a domain modeler. Build the domain model layer for the project described in the spec below.

Create all model files: records, enums, value objects with proper validation.
Write unit tests for each model.

After building, run the test command from the spec.
If tests pass: git add -A && git commit -m "feat: add domain models"
If tests fail: fix and retry.

Only create files for the model layer — do not build controllers, repositories, or other layers.

## Spec
${spec}`;

console.log("Single builder agent");
console.log(`  Spec: ${SPEC_PATH}`);
console.log(`  Repo: ${REPO_DIR}\n`);

const q = query({
  prompt:
    "Build the domain model layer as described in your system prompt. Create all model classes with validation, write tests, run them, and commit if green.",
  options: {
    systemPrompt,
    allowedTools: ["Read", "Write", "Edit", "Bash", "Glob", "Grep"],
    maxTurns: 25,
    cwd: REPO_DIR,
    permissionMode: "bypassPermissions",
  },
});

for await (const msg of q) {
  if (msg.type === "assistant" && msg.message) {
    for (const block of msg.message.content) {
      if (block.type === "tool_use") {
        const detail =
          block.input?.file_path ??
          block.input?.command ??
          block.input?.pattern ??
          "";
        const short =
          typeof detail === "string"
            ? detail.slice(0, 100)
            : JSON.stringify(detail).slice(0, 100);
        console.log(`  [MODELS] ${block.name}(${short})`);
      }
    }
  }
  if (msg.type === "result") {
    console.log(
      `\nDone [${msg.subtype}, $${msg.total_cost_usd.toFixed(4)}]`
    );
  }
}
The Build Team (A2A)
import { query } from "@anthropic-ai/claude-agent-sdk";
import { readFileSync, writeFileSync, mkdirSync, readdirSync, existsSync } from "fs";
import { resolve, relative, join } from "path";
import { execSync } from "child_process";

// 3-build-team.ts — The Build Team with A2A coordination.
// Two-phase build: MODELS first → publish to A2A hub → API + DATABASE in parallel.

const SPEC_PATH = resolve(import.meta.dir, "sample/spec.md");
const REPO_DIR  = resolve(import.meta.dir, "../multi-agent");
const spec = readFileSync(SPEC_PATH, "utf-8");

const agents = [
  { name: "MODELS",   branch: "feature/models",   phase: 1, role: "Build the domain model layer..." },
  { name: "API",      branch: "feature/api",       phase: 2, role: "Build the REST controller layer..." },
  { name: "DATABASE", branch: "feature/database",  phase: 2, role: "Build the repository/storage layer..." },
];

// ── A2A Hub (JSON-RPC 2.0) ─────────────────────────────────────────
const hub = { agents: new Map(), artifacts: new Map() };
// ... handles agent/register, agent/updateStatus, artifacts/publish, artifacts/get

// ── Two-phase flow (git-based distribution) ───────────────────────────────

// Phase 1: MODELS builds first (only MODELS worktree exists)
const modelsResult = await runAgent(modelsAgent);

// Team leader publishes MODELS artifacts to A2A hub
const artifacts = collectJavaFiles(modelsWorktreeSrc);
a2a("artifacts/publish", { agent: "MODELS", artifacts });

// Merge feature/models INTO main — types are now in git history
run("git checkout main");
run("git merge feature/models --no-edit");

// Create phase-2 worktrees from updated main (types in git!)
run("git worktree add .worktrees/api -b feature/api");
run("git worktree add .worktrees/db -b feature/database");

// Build type summary from A2A artifacts for system prompts
const modelsSummary = buildModelFilesSummary(modelsArtifacts);

// Phase 2: API + DATABASE in parallel (model types in git history)
const results = await Promise.allSettled(
  phase2Agents.map(a => runAgent(a, modelsSummary))
);

// Merge phase-2 branches only (MODELS already merged), run final tests
// ... (see full source in src/data/demo/3-build-team.ts)
Architecture
3-build-team.ts (TEAM LEADER)
├─ Reads spec.md
├─ Scaffolds module + Application.java on main
├─ Starts A2A Hub (localhost:9100)
│
├─ Phase 1: MODELS agent builds alone
│   ├─ creates MODELS worktree only
│   ├─ cwd: .worktrees/models
│   ├─ builds: model/ + repository interface
│   └─ IT tests → commit → push
│
├─ A2A: publish MODELS artifacts to hub
├─ Merge feature/models INTO main (types in git history)
│
├─ Phase 2: create worktrees from updated main
│   ├─ API agent (feature/api)
│   │   └─ cwd: .worktrees/api (model types in git)
│   │   └─ builds: controller/ only
│   │   └─ @WebMvcTest: POST/GET endpoints
│   │   └─ IT tests → commit → push
│   └─ DATABASE agent (feature/database)
│       └─ cwd: .worktrees/db (model types in git)
│       └─ builds: InMemoryPaymentRepository only
│       └─ IT tests: CRUD operations
│       └─ IT tests → commit → push
│
├─ Merges phase-2 only (MODELS already merged — clean!)
├─ Runs final tests (./mvnw test)
└─ A2A Hub summary: agents, statuses, artifact counts

// Key Takeaways

// Resources

Claude Agent SDK (npm) Agent SDK Documentation CLI Reference Download: 1-hello.ts Download: 2-build-one.ts Download: 3-build-team.ts Download: spec.md