Skip to content

gdunhao/order-status

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Order Status — Multi-Agent A2A Demo

Spring Boot 4.1 · Spring AI 2.0 · AWS Bedrock · Agent-to-Agent Protocol

Java Spring Boot Spring AI AWS Bedrock Build License

A self-contained demonstration of the Agent-to-Agent (A2A) protocol built on Spring Boot 4.1. Two specialised agents collaborate transparently to answer a single customer question:

"Where is my order ORD-1001?"

The order-status agent acts simultaneously as an A2A server (answering the caller) and an A2A client (delegating shipment tracking to the downstream shipping-tracker agent). The caller never knows a second agent is involved — that encapsulation is exactly what makes A2A composable.


Table of Contents


Architecture

%%{init: {
  "theme": "base",
  "themeVariables": {
    "primaryColor":        "#1e2a3a",
    "primaryTextColor":    "#e2e8f0",
    "primaryBorderColor":  "#4a9eff",
    "lineColor":           "#64b5f6",
    "secondaryColor":      "#162032",
    "tertiaryColor":       "#0d1b2a",
    "clusterBkg":          "#1a2535",
    "clusterBorder":       "#334155",
    "titleColor":          "#e2e8f0",
    "edgeLabelBackground": "#1e2a3a",
    "nodeBorder":          "#4a9eff",
    "fontFamily":          "ui-monospace, monospace"
  }
}}%%
flowchart TB
    subgraph CLIENT["⚙️  JVM 3 — profile: order-client  (port: random)"]
        OCR["OrderClientRunner\nasked a question → prints the answer → exits"]
    end

    subgraph STATUS["🔄  JVM 2 — profile: order-status  (port: 9601)"]
        OSA["order-status-agent\nskill: order-status\nA2A server ↔ A2A client simultaneously"]
    end

    subgraph SHIPPING["📦  JVM 1 — profile: order-shipping  (port: 9600)"]
        STA["shipping-tracker-agent\nskill: track-shipment"]
    end

    OCR -- "A2A · JSON-RPC · message/send" --> STATUS
    OSA -- "A2A · JSON-RPC · message/send" --> SHIPPING

    style CLIENT   fill:#0d1b2a,stroke:#334155,color:#94a3b8
    style STATUS   fill:#0a1f35,stroke:#4a9eff,color:#e2e8f0
    style SHIPPING fill:#0a2a1f,stroke:#22c55e,color:#e2e8f0
Loading

📊 See DIAGRAMS.md for detailed Mermaid diagrams covering the system architecture, sequence flows, A2A protocol contract, task state machine, and package structure.


Prerequisites

Tool Version Notes
JDK 25 (or 21+) Build targets --release 25; runs on any JDK ≥ 21
Maven Wrapper bundled Use ./mvnw — no Maven installation required
AWS credentials optional Only needed for live LLM calls; the demo skills are deterministic

Note: AWS credentials are resolved from the standard provider chain — environment variables, ~/.aws/credentials, IAM instance profile, etc. The two demo profiles do not call the LLM directly; Bedrock is wired so you can add LLM-backed reasoning without touching infrastructure.


Quick Start

# 1 — Clone and build
git clone <repo-url>
cd order-status
./mvnw clean package -DskipTests

# 2 — Run all tests
./mvnw clean test

# 3 — Start both agents (two separate terminals)
./mvnw spring-boot:run -Dspring-boot.run.profiles=order-shipping   # terminal 1
./mvnw spring-boot:run -Dspring-boot.run.profiles=order-status     # terminal 2

# 4 — Ask a question (third terminal)
./mvnw spring-boot:run \
  -Dspring-boot.run.profiles=order-client \
  '-Dspring-boot.run.arguments=Where is my order ORD-1003?'

Expected output:

Order ORD-1003 — Running shoes (carrier: DHL, tracking #DHL7788991122).
  → Delivered — signed for by 'M. SILVA' at 09:42.

Available Profiles

Profile Role Port Key Bean
order-shipping Downstream A2A server — answers tracking queries 9600 ShippingTrackerConfig
order-status Upstream A2A server and A2A client 9601 OrderStatusConfig
order-client CLI test client — one shot, then exits random OrderClientRunner

Only one profile is active per JVM process; the three processes collaborate over HTTP.


Running the Demo

1 · Start the Shipping Tracker agent (port 9600)

./mvnw spring-boot:run -Dspring-boot.run.profiles=order-shipping

Exposes:

  • GET /.well-known/agent-card.json — agent discovery
  • POST /a2a — JSON-RPC endpoint (message/send, message/stream, tasks/get)

2 · Start the Order Status agent (port 9601)

./mvnw spring-boot:run -Dspring-boot.run.profiles=order-status

This agent registers itself as an A2A server on port 9601 and wires an A2aClient that points to the shipping-tracker at port 9600.

3 · Run the test client

# Default question
./mvnw spring-boot:run -Dspring-boot.run.profiles=order-client

# Custom question (override via program arguments)
./mvnw spring-boot:run \
  -Dspring-boot.run.profiles=order-client \
  '-Dspring-boot.run.arguments=Where is ORD-1002?'

API Reference

Both agents expose the same HTTP surface (A2A protocol):

Agent Card Discovery

GET /.well-known/agent-card.json
curl -s http://localhost:9601/.well-known/agent-card.json | jq
Sample response
{
  "name": "order-status-agent",
  "description": "Answers 'where is my order?' by delegating shipment tracking to a downstream A2A agent.",
  "url": "http://localhost:9601",
  "version": "1.0.0",
  "capabilities": { "streaming": false, "pushNotifications": false },
  "defaultInputModes": ["text/plain"],
  "defaultOutputModes": ["text/plain"],
  "skills": [
    {
      "id": "order-status",
      "name": "Order Status",
      "description": "Tells a customer where their order is — combines order data with live shipment tracking.",
      "tags": ["ecommerce", "customer-service", "a2a-to-a2a"],
      "examples": ["ORD-1001", "Where is ORD-1003?"]
    }
  ]
}

Send a Message (message/send)

POST /a2a
Content-Type: application/json
curl -s -X POST http://localhost:9601/a2a \
  -H 'Content-Type: application/json' \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "message/send",
    "params": {
      "message": {
        "messageId": "m1",
        "role": "user",
        "parts": [{ "kind": "text", "text": "Where is ORD-1002?" }]
      }
    }
  }' | jq

Stream a Message (message/stream)

POST /a2a/stream
Content-Type: application/json
Accept: text/event-stream
curl -sN -X POST http://localhost:9601/a2a/stream \
  -H 'Content-Type: application/json' \
  -H 'Accept: text/event-stream' \
  -d '{
    "jsonrpc": "2.0",
    "id": 2,
    "method": "message/stream",
    "params": {
      "message": {
        "messageId": "m2",
        "role": "user",
        "parts": [{ "kind": "text", "text": "Where is ORD-1001?" }]
      }
    }
  }'

Retrieve a Task (tasks/get)

curl -s -X POST http://localhost:9601/a2a \
  -H 'Content-Type: application/json' \
  -d '{
    "jsonrpc": "2.0",
    "id": 3,
    "method": "tasks/get",
    "params": { "id": "<task-uuid>" }
  }' | jq

Health & Metrics (Spring Boot Actuator)

curl http://localhost:9601/actuator/health
curl http://localhost:9601/actuator/metrics

Demo Orders

Order ID Product Carrier Tracking # Status
ORD-1001 Mechanical keyboard UPS 1Z999AA10123456784 In transit
ORD-1002 Coffee beans (1 kg) FedEx FX555000123 Out for delivery
ORD-1003 Running shoes DHL DHL7788991122 Delivered
ORD-1004 USB-C cable USPS USPS940010000000 Label created

Configuration

Environment Variables

Variable Default Description
AWS_REGION us-east-1 AWS region for Bedrock
AWS_ACCESS_KEY_ID AWS access key (or use instance profile)
AWS_SECRET_ACCESS_KEY AWS secret key
BEDROCK_MODEL anthropic.claude-3-5-sonnet-20241022-v2:0 Bedrock model ID

Key application.yaml Properties

Property Default Description
spring.ai.a2a.server.enabled false Enable the A2A RestController
spring.ai.a2a.server.endpoint-path /a2a JSON-RPC endpoint path
spring.ai.a2a.server.card.* Agent Card metadata
spring.ai.a2a.client.base-url http://localhost:9601 Default upstream A2A base URL
order-status.shipping-url http://localhost:9600 Shipping-tracker URL for the order-status agent
spring.ai.bedrock.aws.region us-east-1 Overridden by AWS_REGION
spring.ai.bedrock.converse.chat.options.model Claude 3.5 Sonnet Override with BEDROCK_MODEL

Override at Runtime

# Use a different Bedrock model
BEDROCK_MODEL=anthropic.claude-3-haiku-20240307-v1:0 \
  ./mvnw spring-boot:run -Dspring-boot.run.profiles=order-status

# Point to a different shipping-tracker URL
./mvnw spring-boot:run \
  -Dspring-boot.run.profiles=order-status \
  -Dspring-boot.run.arguments=--order-status.shipping-url=http://tracker.internal:9600

Test Suite

./mvnw clean test
Test Class Type What It Covers
OrderStatusSkillsTest Unit order-status skill logic with a mocked A2aClient
ShippingTrackerConfigTest Unit Agent card metadata + track-shipment skill handler
TaskAssemblerTest Unit Folding of streaming TaskEvents into a final Task
TaskStoreTest Unit In-memory task-store contract
OrderStatusApplicationTests Spring context Smoke-test — server disabled, Bedrock stubbed
A2aControllerIntegrationTest Integration Full HTTP test: boots order-shipping profile, exercises message/send + agent-card endpoint

Test reports land in target/surefire-reports/.


Project Layout

src/main/java/com/example/order_status/
├── OrderStatusApplication.java         ← @SpringBootApplication entry point
├── OrderStatusConfig.java              ← Upstream agent (profile: order-status, port 9601)
│                                         Skills: order-status
│                                         Depends on: A2aClient → shipping-tracker
├── ShippingTrackerConfig.java          ← Downstream agent (profile: order-shipping, port 9600)
│                                         Skills: track-shipment
├── OrderClientRunner.java              ← CLI test client (profile: order-client)
└── a2a/
    ├── spec/                           ← A2A protocol records (immutable value types)
    │   ├── AgentCard.java              ← Agent identity + capabilities metadata
    │   ├── AgentSkill.java             ← Discrete capability descriptor
    │   ├── AgentCapabilities.java      ← Supported protocol features
    │   ├── Message.java                ← User/agent message with multi-modal Parts
    │   ├── Part.java                   ← TextPart | DataPart | FilePart (sealed)
    │   ├── Task.java                   ← Mutable task with status + artifacts
    │   ├── TaskEvent.java              ← SSE event (StatusUpdate | ArtifactUpdate)
    │   └── JsonRpc.java                ← JSON-RPC 2.0 Request / Response / Error
    ├── server/                         ← Auto-configured server side
    │   ├── A2aController.java          ← RestController: /a2a + /.well-known/agent-card.json
    │   ├── A2aServerAutoConfiguration.java ← Registers TaskStore + SkillHandler wiring
    │   ├── A2aServerProperties.java    ← Binds spring.ai.a2a.server.*
    │   ├── SkillHandler.java           ← Strategy interface for skill implementations
    │   ├── TaskAssembler.java          ← Folds SSE stream → complete Task
    │   └── TaskStore.java              ← In-memory task repository
    └── client/                         ← Thin A2A HTTP client
        ├── A2aClient.java              ← fetchAgentCard / sendMessage / streamMessage / getTask
        ├── A2aClientBuilder.java       ← Fluent builder (RestClient + WebClient)
        └── A2aClientAutoConfiguration.java ← Auto-wires default A2aClient bean

How It Works

The A2A Pattern

The A2A protocol defines how agents discover and communicate with each other using JSON-RPC 2.0 over HTTP. Every agent:

  1. Advertises itself at GET /.well-known/agent-card.json — name, skills, input/output modes.
  2. Accepts work at POST /a2a via message/send (synchronous) or /a2a/stream via message/stream (SSE).
  3. Returns a Task — a structured object with status (submitted → working → completed/failed) and output artifacts.

Request Flow

%%{init: {
  "theme": "base",
  "themeVariables": {
    "primaryColor":        "#1e2a3a",
    "primaryTextColor":    "#e2e8f0",
    "primaryBorderColor":  "#4a9eff",
    "lineColor":           "#64b5f6",
    "secondaryColor":      "#162032",
    "tertiaryColor":       "#0d1b2a",
    "noteBkgColor":        "#1a2535",
    "noteTextColor":       "#94a3b8",
    "noteBorderColor":     "#334155",
    "activationBkgColor":  "#1e3a5f",
    "activationBorderColor":"#4a9eff",
    "actorBkg":            "#0d1b2a",
    "actorBorder":         "#4a9eff",
    "actorTextColor":      "#e2e8f0",
    "actorLineColor":      "#334155",
    "signalColor":         "#64b5f6",
    "signalTextColor":     "#e2e8f0",
    "fontFamily":          "ui-monospace, monospace"
  }
}}%%
sequenceDiagram
    autonumber
    participant OCR as OrderClientRunner
    participant OSA as order-status-agent<br/>:9601
    participant OB  as Order Book<br/>(in-memory)
    participant STA as shipping-tracker-agent<br/>:9600

    OCR ->> OSA : message/send<br/>"Where is ORD-1003?"
    activate OSA

    OSA ->> OSA : 1. Parse order ID<br/>(regex: ORD-\d+)
    OSA ->> OB  : 2. lookup("ORD-1003")
    OB -->> OSA : Order {product, carrier, tracking#}

    OSA ->> STA : 3. message/send — track-shipment skill
    activate STA
    STA -->> OSA : Task {state: completed, artifact: "Delivered..."}
    deactivate STA

    OSA ->> OSA : 4. Compose customer-facing reply
    OSA -->> OCR : 5. Task {state: completed,<br/>artifact: "Order ORD-1003 — Running shoes..."}
    deactivate OSA

    Note over OCR : Prints artifact text → exits
Loading

Why Both Sync and Streaming?

  • message/sendPOST /a2a — fire and wait; best for short, deterministic skills.
  • message/streamPOST /a2a/stream — Server-Sent Events; best for LLM-backed skills where the response is generated token by token.

Both APIs surface the same Task and TaskEvent data model.


Extending the Agents

Add a New Skill

  1. Implement SkillHandler:
@Component
@Profile("order-status") // attach to the right agent profile
public class RefundStatusSkill implements SkillHandler {

    @Override
    public String skillId() { return "refund-status"; }

    @Override
    public Task handle(Message request, Task seed) {
        // ... look up refund data ...
        return /* completed Task with artifact */;
    }
}
  1. Register the skill ID in the AgentCard bean inside OrderStatusConfig.

Swap the LLM

Change BEDROCK_MODEL to any model enabled in your AWS account:

# Switch to Claude 3 Haiku for lower latency
export BEDROCK_MODEL=anthropic.claude-3-haiku-20240307-v1:0

# Switch to Amazon Titan
export BEDROCK_MODEL=amazon.titan-text-express-v1

Connect to a Real Carrier API

Replace the TRACKING_DB map in ShippingTrackerConfig with a call to your carrier's REST/SOAP API. The SkillHandler contract is unchanged.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages