In the first four posts in this blog series, we introduced the Agent2Agent protocol in stages; the motivation for Agent systems and A2A in Part 1, core A2A objects in Part 2, A2A runtime behavior in Part 3 then A2A diagrams in Part 4 to make it all understandable and visible without encountering any code yet.
Theory is useful. Running code is better.
This post walks through a practical Java A2A style demo that demonstrates how the Agent2Agent protocol works. The Clockwork Agent, a minimal A2A style demo available in this repository, is deliberately simple: no LLM, no framework SDK, no model inference, and no hidden orchestration. Just HttpServer, HttpClient, Jackson, and JSON-RPC envelopes shaped like the protocol. The goal is not to ship production agent infrastructure; it is to ground the diagrams in something you can clone, run, break, and evolve.
With the Clockwork Agent, we’ll explore agent discovery, Agent Cards, SendMessage, task lifecycles, polling patterns, and multi-turn interactions. This example also explores the boarder scaling question behind the series: once agent interactions become long-running, stateful, distributed, and high volume, how does infrastructure such as Apache Kafka becomes important for durability, coordination, and operational scale.
If you have not read the diagram post yet, keep the runtime sequence open while you read Part 4 (Figure 4). Each of the three examples exercises a different part of that A2A diagram.
What the Clockwork Agent is and isn’t
The Clockwork Agent is a deterministic Java server that pattern-matches on user text such as time, count down N seconds, and confirm. It is mechanical, not intelligent, by design. That makes it easier to observe the protocol without model non-determinism obscuring the request and response shapes.
It is:
- An example harness for A2A request shapes, response forks, and task lifecycle semantics
- Runnable in two terminals with Maven (A2aDemoServer + A2aDemoClient)
- Accompanied by captured traces
But it is not:
- An AI agent — there is no model inference, no tool calling, no MCP, no RAG
- The official a2a-java SDK (use that for production)
- A complete protocol implementation — several operations and transport paths are deliberately omitted
The Clockwork Agent is a deterministic Java server that pattern-matches on user text (time, count down N seconds, confirm). Mechanical, not intelligent — this is by design: you can see protocol mechanics without model non-determinism obscuring the wire format.
Introducing the Clockwork Agent examples
The sequence diagram in the previous blog motivates three different Clockwork Agent examples:
- Discover an agent
- Call
SendMessage - Follow one of three runtime paths
- Receive an immediate answer
- Follow a task as it evolves
- Continue a task after input branches for
- polling,
- streaming, and
- multi-turn input.
Here’s a concrete sequence diagram illustrating the examples:
The figure above is our starting point. A2aDemoClient fetches an Agent Card, then calls SendMessage. From there the flow splits into three examples — each one a different branch of the A2A runtime we sketched in part 4:
- Example 1: Time
"What time?"→ immediateMessage(one round trip, done). - Example 2: Countdown
"Count down 60s"→Task,GetTaskpoll loop, finalArtifact. - Example 3: Confirm
"Count down + confirm"→input-required, thenSendMessageagain withtaskId→working→completed.
Architecture at a glance
Before exploring the examples, it helps to map the Clockwork Agent onto the component diagram from Part 4 (Figure 1).
A2A components: client side, remote server side, bindings, security boundary.
| Diagram component | In this demo |
|---|---|
| Agent Card endpoint | GET /.well-known/agent.json |
| A2A API surface | POST /rpc — SendMessage, GetTask, CancelTask |
| Task manager | CountdownTask + in-memory TASKS map |
| Artifact store | Artifacts attached inline on the Task when complete |
| Push/stream notifier | Not implemented |
| Client SDK / binding | Hand-rolled HttpClient + JSON-RPC |
| JSON-RPC binding | Yes — REST and gRPC exist in the spec but are not used here |
| Security boundary | Not implemented — no auth on card or RPC |
The demo collapses several server-side boxes into one class (A2aDemoServer.java). This keeps the story readable. A production service would split card serving, RPC routing, task orchestration, artifact persistence, and notification delivery into separate modules — exactly as the components diagram suggests.
Step zero: discovery
Every client run starts the same way: fetch the Agent Card:
|
1 2 |
HttpRequest req = HttpRequest.newBuilder( URI.create(baseUrl + "/.well-known/agent.json")).GET().build(); |
This corresponds to the top of the runtime sequence diagram: Client → Agent Card endpoint → AgentCard.
The card advertises three skills — current-time, countdown, and countdown-confirm — plus capabilities declaring streaming: false and pushNotifications: false. That honesty matters later: the client will only poll, not subscribe to SSE or webhooks.
Simplification: The spec recommends /.well-known/agent-card.json; this demo uses agent.json on the same well-known path pattern. This is the same idea, just a shorter filename for a local demo.
Example 1: Synchronous time — the immediate Message path
Prompt: What is the current time?
What it does: One SendMessage call. The server matches “time” in the user text and returns result.message — an agent-role Message with a text Part. Done.
What it illustrates:
| Concept | Diagram reference |
|---|---|
| Discovery before work | Runtime sequence, steps 1–4 |
SendMessage → Message (not Task) |
Runtime sequence, alt: Immediate/short operation; Part 3 — left branch |
Message + Part composition |
C.f. The Simple Object Model — Dialogue box only; no Task or Artifact |
What it does not illustrate:
- Task lifecycle — there is no
Task, noGetTask, no terminal state - Artifacts — the reply is a
Message, not anArtifacton a task (a common first-read confusion from Part 2) - Streaming, push, or multi-turn dialogue
Why it matters: Not every agent interaction should become a long-running task. Example 1 is the fast path — the fork where the server has enough information to answer in one round trip. If your client assumes every SendMessage returns a Task, this example is the corrective.
Trace excerpt (full version here):
|
1 2 3 4 5 6 7 8 9 |
{ "result": { "message": { "kind": "message", "role": "agent", "parts": [{ "kind": "text", "text": "The current time is 2026-06-17 14:21:25 AEST." }] } } } |
Code pointers: A2aDemoServer.handleSendMessage → immediateMessage(...); A2aDemoClient.runSynchronousTimeExample(...).
Example 2: Async countdown — long-running Task + polling
Prompt: Count down 60 seconds
What it does:
SendMessagecreates a countdownTaskand returns it immediately(state: working). The constructor may set submitted internally, butcreateCountdownTask()callsstart()before responding — so the first client-visible snapshot is already working.- A background
ScheduledExecutorServicedecrements the timer every 10 seconds. - The client loops on
GetTask(taskId)every 5 seconds until a terminal state. - On completion, the task carries a final
Artifactwith the completion text.
What it illustrates:
| Concept | Diagram reference |
|---|---|
SendMessage → Task |
Runtime sequence, else: Long-running operation; Part 3— right branch |
| Polling fallback | Runtime sequence, loop: GetTask; Part 3— Poll lane only |
working → completed |
Task lifecycle diagram |
Progress via Task.status.message |
Object model — Task owns nested Message for status |
Final output via Task.artifacts[] |
Object model — Task → Artifact → Part |
What it does not illustrate:
- SSE / SendStreamingMessage — the sequence diagram’s parallel Streaming/push updates branch is absent. The Agent Card says so upfront.
- Push webhooks — no
CreateTaskPushNotificationConfig, no server-initiated HTTP callback to the client TaskStatusUpdateEventobjects — the spec’s event types appear in the detailed object model, but this demo does not emit discrete events. The client pulls full task snapshots viaGetTaskand diffs them implicitly by re-printing status text- failed, rejected, canceled terminal paths — happy path only in the default client (though
CancelTaskexists on the server)
Why it matters: This is the core asynchronous pattern behind most real agent workloads: accept work quickly, execute in the background, expose mutable state, deliver durable outputs at the end. Polling is chatty but always valid — and it is the easiest way to learn the task contract before adding streaming complexity.
Worth noting: The runtime sequence (Figure 4, TODO) shows streaming/push and polling as parallel ways to track a task; the Clockwork Agent implements polling only. The diagram shows every option; our demo implements only the simplest one on purpose.
Code pointers: createCountdownTask(...), CountdownTask.start(), handleGetTask(...), runAsyncCountdownExample(...).
Example 3: Input-required countdown — multi-turn on the same task
Prompts:
- Count down 20 seconds with confirm
confirm(secondSendMessage, sametaskId)
What it does:
- First
SendMessagereturns aTaskalready in input-required with a prompt instatus.message.createConfirmRequiredCountdownTask()sets that state in the constructor and does not callstart()first — so the client never sees a prior working snapshot. - Second
SendMessageincludesparams.taskIdplus the user’s confirmation text. - Server calls
confirmAndStart(), transitions to working, runs the same countdown logic as Example 2. - Client polls
GetTaskuntilcompleted+ artifact.
What it illustrates:
| Concept | Diagram reference |
|---|---|
| input-required state | Task lifecycle — client-visible path is input-required → working → completed (not working first) |
| Follow-up input on existing task | Runtime sequence, opt: Input required — second SendMessage |
| Same task context across turns | Detailed object model — SendMessageRequest with optional taskId |
What it does not illustrate:
- A task entering input-required from working mid-flight — this demo creates the task directly in input-required; the first client response is already there
- Rich conversational history — the demo does not populate a full
Task.historyarray; it only needs enough state to gate the countdown - Client-side UX for gathering input — the client blindly sends “confirm”; a real app would render the agent’s prompt and collect structured input
- Cancellation mid input-required — supported by lifecycle diagram and server handler, not exercised in the default client run
Why it matters: Many agent workflows need clarification before execution continues — policy confirmation, missing parameters, disambiguation. Example 3 shows the protocol-native pattern: stay on the same taskId, create or reach input-required, then resume when the client sends follow-up input. This is cleaner than starting a new task and trying to correlate IDs in application code.
Code pointers: createConfirmRequiredCountdownTask(...), sendMessageForTask(...) (client), CountdownTask.confirmAndStart().
Running the Clockwork Agent Examples
Run it in two terminals from the repository root:
Server:
|
1 |
<span lang="EN-AU">mvn -q compile exec:java -Dexec.mainClass=local.a2a.examples.A2aDemoServer</span> |
Client:
|
1 |
<span lang="EN-AU">mvn -q compile exec:java -Dexec.mainClass=local.a2a.examples.A2aDemoClient</span> |
The client prints the Agent Card summary, then runs all three examples in order. Override the base URL with A2A_DEMO_BASE_URL if needed.
Further walkthrough notes.
How the three examples map to A2A runtime paths
In the following recap table, each row ties a protocol concept from the earlier posts to what the Clockwork Agent actually does:
| Example 1 | Example 2 | Example 3 | |
|---|---|---|---|
| Response type | Message |
Task |
Task |
| Lifecycle states | — | working → completed |
input-required → working → completed |
| Client tracking | None | Poll GetTask |
Poll after resume |
| Artifacts | No | Yes, at completion | Yes, at completion |
| Runtime sequence branch | Immediate alt | Long-running + poll loop | Long-running + input-required opt |
| Message vs Task path | Message |
Task → Poll |
Task → Poll (after input) |
Together they cover the two top-level outcomes of SendMessage and two of the three client update strategies from Part 3 — poll yes, stream no, push no.
What is simplified or missing overall
So, have we seen a definitive set of A2A examples? No, not yet. Everything below is deliberately left out to keep the Clockwork Agent small and readable for a first pass (and to leave interesting things for later).
No AI
There is no LLM, embedding model, or tool loop. Intent routing is String.contains(...). That keeps traces stable and makes the post about protocol behavior, not prompt engineering.
No official SDK
The official Java SDK handles bindings, schema validation, and spec drift. This demo inlines JSON shapes so you can see every field. Migration to the SDK is next.
Transport and operations
The Clockwork Agent uses JSON-RPC over HTTP only — no REST or gRPC bindings, no SSE (SendStreamingMessage), no push webhooks, and no extra operations like GetExtendedAgentCard or ListTasks. CancelTask exists on the server, but the default client run skips it.
Persistence and scale
- Tasks live in an in-memory
ConcurrentHashMap— restart loses state - Single JVM, single node — no shared task store for horizontal scale
- No retention or cleanup policy for completed tasks
- Polling interval is fixed; no backoff or subscription handoff
Security
No authentication on the Agent Card or RPC endpoint. Production agents need the security boundary from the components diagram — AuthN/AuthZ, policy, and likely mTLS or token validation before SendMessage is accepted.
Spec fidelity
- Minimal validation of incoming envelopes
- Agent Card path simplified (
agent.json) - Status progress is embedded in
Task.status.messagerather than emitted as standaloneTaskStatusUpdateEventmessages
From demo to production: Reading the diagrams again
If you squint at the Component diagram after running the demo, there are some obvious improvements we can make in the future:
- Extract task management and artifact storage from the monolithic server class
- Add a push/stream notifier so clients are not poll-bound (re-enable the parallel branch in the runtime sequence)
- Swap hand-rolled HTTP for the official SDK and a real binding
- Insert AuthN/AuthZ at the security boundary
- Optionally back task events with a durable log — the kind of pattern this series will connect to Kafka down the track
The diagrams were the map. These three examples are the first walk along the path: deliberately short, deterministic, and AI-free so the A2A protocol stays in focus.
Key takeaways
SendMessageforks into two outcomes: an immediateMessage(Example 1) or a long-runningTask(Examples 2 and 3). Client code must handle both.- Polling is a first-class baseline for the A2A protocol. Example 2 implements the sequence diagram’s poll loop; streaming and push are optional upgrades, not prerequisites for learning A2A.
input-requiredis a state, not a new task. Example 3 shows continuation viataskIdon a follow-upSendMessage.- Artifacts belong to tasks. Completion output appears in
task.artifacts[], not as a bare top-level response. - Diagrams describe the available runtime paths; implementations may choose only the paths they need. This A2A demo intentionally focuses on polling to keep the protocol behavior easy to follow
The Clockwork Agent is not intelligent, but it keeps time. That’s the point. This practical Java A2A example strips away AI inference and framework complexity so the Agent2Agent protocol can be observed directly in code. One agent, three runtime paths, and the same predictable behavior every time.
Series Recap
Part 1 framed the challenge of scaling agent systems and how Kafka fits. Part 2 introduced A2A’s object model. Part 3 covered runtime behavior, including discovery, SendMessage, tasks, and updates. Part 4 added diagrams. Part 5 brought those concepts together through a runnable Java A2A example and A2A demo that shows how the Agent2Agent protocol works in practice.
Scaling agent systems eventually needs durable events, state, search and gateways — workloads Instaclustr already operates as managed Kafka, OpenSearch, PostgreSQL, Cassandra, ClickHouse, Cadence, and a new MCP gateway. A2A defines how agents communicate and hand work to one another; Apache Kafka and related infrastructure become increasingly important as those interactions grow in volume, complexity, and operational scope.
