DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Newsletter
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Microservices With Apache Camel and Quarkus (Part 4)
  • The Hidden Production Risks of Third-Party SDKs
  • A Senior Engineer’s Guide to Foundry IQ, MCP, and the OpenAI Agents SDK
  • Part 1: Building Governed MCP Tool Services With Quarkus LangChain4j and Goose

Trending

  • Build Software Faster With Three Simple Principles
  • Everything About HTTPS and SSL in Java
  • Embabel vs LangGraph4j: Two Agentic Philosophies for Investment and Risk Analysis in BFSI
  • Building a CI/CD Pipeline With Kubernetes: A Development Guide With Deployment Considerations for Practitioners

Draft This article hasn't been published, so readers can't see it.

In moderation This article is waiting for review, so readers can't see it yet.

Deleted This article has been deleted, so readers can't see it.

  1. DZone
  2. Coding
  3. Java
  4. Part 4: Multi-Agent Orchestration — Pairing Goose With Quarkus Flow State Machines

Part 4: Multi-Agent Orchestration — Pairing Goose With Quarkus Flow State Machines

Bridge Goose agent interactions with Quarkus Flow state machines governed by AGENTS.md rules and integrated via the A2A protocol to enforce human-in-the-loop approvals.

By 
Daniel Oh user avatar
Daniel Oh
DZone Core CORE ·
Oct. 27, 26 · Tutorial
Post on X (opens in a new tab) Share on LinkedIn (opens in a new tab)
187 Views

Join the DZone community Save articles, join the discussion, and download Refcards and Trend Reports with a free account.

Join for free Already a member? Log in

The Core Problem

In Part 1, we built a Quarkus MCP tool server. In Part 2, we secured it with agentgateway. In Part 3, we added distributed tracing. The architecture handles single-request tool calls well — but enterprise processes are rarely single-step.

Consider what happens when a developer tells Goose: "Migrate the users table to add an email column, then deploy the updated service to production." This triggers a chain of operations:

  1. Goose calls migrate-schema to alter the production database
  2. Goose calls deploy-production to push the new code
  3. Both operations succeed — without any human ever reviewing the schema change or approving the production deployment

Local developer agents like Goose excel at interactive reasoning, but long-running business processes require:

  • Strict state machines to track multi-step workflows through well-defined phases
  • Max-iteration caps to prevent runaway agent loops that consume resources indefinitely
  • Formal human-in-the-loop (HITL) approvals before destructive operations like database writes, schema migrations, or financial transactions
  • Standardized agent-to-agent delegation so the client agent (Goose) and the backend agent (Quarkus) communicate through a shared protocol, not ad-hoc HTTP calls

Without these guardrails, autonomous agents operate in a governance vacuum — the exact gap that turns helpful automation into unaudited risk.

The Solution: A2A Protocol + Quarkus Flow + AGENTS.md

A2A delegates a business task to an agent; Part 6's model routing selects a model backend for inference. Model fallback does not replace this chapter's workflow policy or human approval gate.

The fix combines three standards into a governed multi-agent architecture:

  • A2A (Agent2Agent) – the Linux Foundation standard for multi-agent interoperability. Goose discovers the Quarkus backend via an Agent Card and delegates tasks using JSON-RPC over HTTP. The A2A Java SDK (@PublicAgentCard + AgentExecutor) handles protocol compliance automatically.
  • Quarkus Flow – a lightweight state machine implementing the CNCF Serverless Workflow concepts: states, transitions, and action handlers. Each delegated task flows through governance validation, optional HITL approval, and execution.
  • MCP Tool Delegation – auto-approved operations like analyze-logs, health-check, and generate-report delegate to Part 1's MCP server for live tool execution. The quarkus-langchain4j-mcp extension provides a managed McpClient that handles Streamable HTTP transport and MCP sessions automatically.
  • AGENTS.md – a declarative governance file that defines which operations are auto-approved, which require human approval, and which are permanently blocked. The Quarkus server parses this file at startup and enforces it on every incoming A2A task.

The A2A task lifecycle drives the entire flow:

Plain Text
 
submitted → working → input-required → working → completed
                                     → failed (rejected)
                    → completed (auto-approved)
                    → failed (blocked by governance


Prerequisites

Everything from Parts 1–3, plus the A2A Java SDK. Part 4 adds the a2a-java-sdk-reference-jsonrpc dependency for A2A protocol support and connects to Part 1's MCP server for tool execution.

Verify your environment:

Shell
 
java -version    # Java 25+
mvn --version    # Maven 3.9+


Step 1: Agent Discovery With A2A Agent Cards

The A2A protocol starts with discovery. Every A2A-compliant agent publishes an Agent Card at a well-known URL that describes its capabilities, skills, and endpoint:

Shell
 
curl -s http://localhost:8082/.well-known/agent-card.json | jq .
JSON
 
{
  "name": "enterprise-workflow-agent",
  "description": "Multi-step enterprise workflow orchestration with HITL approval gates...",
  "supportedInterfaces": [{ "protocol": "JSONRPC", "url": "http://localhost:8082" }],
  "version": "1.0.0",
  "capabilities": { "streaming": false, "pushNotifications": false },
  "skills": [
    { "id": "analyze-logs", "name": "Log Analysis", "description": "Analyze logs via MCP delegation" },
    { "id": "health-check", "name": "Health Check", "description": "Verify service health via MCP" },
    { "id": "migrate-schema", "name": "Schema Migration", "description": "Requires HITL approval" },
    { "id": "deploy-production", "name": "Production Deployment", "description": "Requires HITL approval" }
  ]
}


The Agent Card tells Goose (or any A2A client) three critical things:

Field Purpose
supportedInterfaces The JSON-RPC endpoint URL and transport protocol
skills The operations the agent can perform — Goose uses these to decide which tasks to delegate
capabilities Whether the agent supports streaming, push notifications, etc.


With the A2A Java SDK, the Agent Card is declared using the @PublicAgentCard CDI qualifier on a producer method — the SDK automatically serves it at /.well-known/agent-card.json and registers the JSON-RPC transport handler:

Java
 
@ApplicationScoped
public class AgentCardProducer {

    @ConfigProperty(name = "agent.url", defaultValue = "http://localhost:8082")
    String agentUrl;

    @Produces
    @PublicAgentCard
    public AgentCard agentCard() {
        return AgentCard.builder()
                .name("enterprise-workflow-agent")
                .description("Multi-step enterprise workflow orchestration with HITL approval gates...")
                .supportedInterfaces(Collections.singletonList(
                        new AgentInterface("JSONRPC", agentUrl)))
                .version("1.0.0")
                .capabilities(AgentCapabilities.builder()
                        .streaming(false).pushNotifications(false).build())
                .defaultInputModes(Collections.singletonList("text"))
                .defaultOutputModes(Collections.singletonList("text"))
                .skills(List.of(
                        AgentSkill.builder().id("analyze-logs").name("Log Analysis")
                                .description("Analyze logs via MCP tool delegation (auto-approved)")
                                .tags(List.of("observability", "auto-approved")).build(),
                        AgentSkill.builder().id("migrate-schema").name("Schema Migration")
                                .description("Execute database schema migrations (requires HITL approval)")
                                .tags(List.of("database", "hitl-required")).build()
                        // ... additional skills
                ))
                .build();
    }
}


No JAX-RS endpoint needed — the SDK handles agent card serving and JSON-RPC routing automatically.

Step 2: Defining Governance Rules With AGENTS.md

AGENTS.md is a declarative governance file that lives alongside the application code. The Quarkus server reads it at startup and enforces its rules on every incoming A2A task.

Create src/main/resources/AGENTS.md:

Markdown
 
# AGENTS.md - Governance Rules for Enterprise Workflow Agent

## Execution Bounds
max-iterations: 10

## Operations

### Auto-Approved
- analyze-logs: Read-only log analysis across services
- health-check: Service health and resource utilization verification
- generate-report: Compliance and metrics report generation from existing data

### Requires Human Approval
- migrate-schema: Database schema migrations on production tables
- process-refund: Customer refund processing (auto-approved below $1000 threshold)
- scale-infrastructure: Infrastructure replica scaling operations
- deploy-production: Production environment deployments

### Blocked
- drop-database: Destructive operation — never allowed via agent delegation
- truncate-table: Destructive operation — never allowed via agent delegation
- delete-all-data: Destructive operation — never allowed via agent delegation


Three categories, three outcomes:

Category What Happens Example
Auto-Approved Task executes immediately, no human involved analyze-logs, health-check
Requires Human Approval Task pauses at input-required state until a human approves or rejects migrate-schema, deploy-production
Blocked Task fails immediately with a governance violation drop-database, truncate-table


The GovernanceEngine parses this file at startup and validates every operation:

Java
 
public GovernanceResult validate(String operation, Map<String, String> args) {
    if (blocked.containsKey(operation)) {
        return new GovernanceResult(Decision.BLOCKED,
            "Operation '" + operation + "' is blocked by governance policy", "critical");
    }

    if (requiresApproval.containsKey(operation)) {
        // Conditional: process-refund auto-approves below $1000
        if (operation.equals("process-refund")) {
            int amount = Integer.parseInt(args.getOrDefault("amount", "0"));
            if (amount <= 1000) {
                return new GovernanceResult(Decision.AUTO_APPROVED, "...", "low");
            }
        }
        return new GovernanceResult(Decision.REQUIRES_APPROVAL, reason, "high");
    }

    if (autoApproved.containsKey(operation)) {
        return new GovernanceResult(Decision.AUTO_APPROVED, "...", "low");
    }

    // Unknown operations default to requiring approval
    return new GovernanceResult(Decision.REQUIRES_APPROVAL, "...", "medium");
}


Notice the conditional logic for process-refund — governance rules can include thresholds, not just binary allow/deny. A $500 refund flows through automatically; a $2,500 refund pauses for human review.

Step 3: Implementing the Workflow State Machine

Each A2A task flows through a state machine with six possible states:

The WorkflowEngine manages this lifecycle:

Java
 
public Map<String, Object> submitTask(String taskId, String messageText) {
    TaskInstance task = tasks.computeIfAbsent(taskId, TaskInstance::new);
    task.incrementIteration();

    // Safety bound: prevent runaway agent loops
    if (task.getIteration() > governance.getMaxIterations()) {
        task.setState(TaskState.FAILED);
        task.addMessage("agent", "Max iterations exceeded. Task terminated.");
        return task.toA2AResponse();
    }

    String operation = parseOperation(messageText);
    Map<String, String> args = parseArguments(messageText);
    task.setState(TaskState.WORKING);

    // Governance gate
    GovernanceResult gov = governance.validate(operation, args);

    return switch (gov.decision()) {
        case BLOCKED -> {
            task.setState(TaskState.FAILED);
            task.addMessage("agent", gov.reason());
            yield task.toA2AResponse();
        }
        case REQUIRES_APPROVAL -> {
            task.setState(TaskState.INPUT_REQUIRED);
            task.addMessage("agent", "HITL approval required: " + gov.reason());
            yield task.toA2AResponse();
        }
        case AUTO_APPROVED -> {
            String result = execute(operation, args);
            task.setState(TaskState.COMPLETED);
            task.addMessage("agent", result);
            yield task.toA2AResponse();
        }
    };
}


The A2A Java SDK handles JSON-RPC routing automatically. You implement the AgentExecutor interface — the SDK calls your execute() method for each incoming SendMessage request:

Java
 
@ApplicationScoped
public class AgentExecutorProducer {

    @Inject WorkflowEngine engine;

    @Produces
    public AgentExecutor agentExecutor() {
        return new AgentExecutor() {
            @Override
            public void execute(RequestContext context, AgentEmitter emitter) throws A2AError {
                String messageText = extractText(context.getMessage());
                String taskId = context.getTaskId();

                // HITL follow-up: task already exists in INPUT_REQUIRED state
                if (context.getTask() != null &&
                    context.getTask().status().state() == TaskState.TASK_STATE_INPUT_REQUIRED) {
                    handleHitlFollowUp(taskId, messageText, emitter);
                    return;
                }

                // New task: submit to workflow engine for governance check
                TaskInstance task = engine.submitTask(taskId, messageText);

                switch (task.getState()) {
                    case FAILED -> emitter.fail(agentMessage(emitter, task.getLastAgentMessage()));
                    case INPUT_REQUIRED -> {
                        emitter.startWork();
                        emitter.requiresInput(agentMessage(emitter, task.getLastAgentMessage()));
                    }
                    case COMPLETED -> {
                        emitter.startWork();
                        emitter.addArtifact(List.of(new TextPart(task.getLastAgentMessage())));
                        emitter.complete();
                    }
                }
            }
            // ...
        };
    }
}


The AgentEmitter manages the A2A task lifecycle — startWork(), requiresInput(), complete(), fail() — while the WorkflowEngine handles governance validation and MCP tool execution.

Step 4: Human-in-the-Loop Approval Gates

When Goose submits a high-risk operation, the workflow pauses at input-required. The task stays frozen until a human explicitly approves or rejects it.

Triggering HITL

Submit a schema migration — this is a high-risk operation that requires approval:

Shell
 
curl -s http://localhost:8082/ \
  -H "Content-Type: application/json" \
  -H "A2A-Version: 1.0" \
  -d '{"jsonrpc":"2.0","id":1,"method":"SendMessage","params":{
    "message":{"messageId":"migration-msg-1","role":"ROLE_USER","parts":[{"text":"migrate-schema --database production --table users --changes add-column-email"}]},
    "configuration":{"returnImmediately":true}
  }}' | jq .result.task.status
JSON
 
{
  "state": "TASK_STATE_INPUT_REQUIRED",
  "message": {
    "role": "ROLE_AGENT",
    "parts": [{
      "text": "HITL approval required. Reason: Database schema migrations on production tables. Workflow paused at state INPUT_REQUIRED. Awaiting human decision."
    }]
  }
}


Polling for Status

Goose polls GetTask and sees the task is paused. It can alert the developer:

Shell
 
curl -s http://localhost:8082/ \
  -H "Content-Type: application/json" \
  -H "A2A-Version: 1.0" \
  -d '{"jsonrpc":"2.0","id":2,"method
Shell
 
":"GetTask","params":{"id":"<task-id-from-above>"}}' \
  | jq .result.status.state
Plain Text
 
"TASK_STATE_INPUT_REQUIRED"


Approving the Task

A human reviews the operation and approves it through the admin API (or the SPA):

Shell
 
curl -s -X POST http://localhost:8082/api/tasks/migration-1/approve | jq .status
JSON
 
{
  "state": "COMPLETED",
  "message": {
    "role": "agent",
    "parts": [{
      "text": "Schema migration completed successfully. Database: production. Table: users. Changes: add-column-email..."
    }]
  }
}


Rejecting a Task

If the human decides the migration is unsafe:

Shell
 
curl -s -X POST http://localhost:8082/api/tasks/migration-2/reject \
  -H "Content-Type: application/json" \
  -d '{"reason":"Schema change not reviewed by DBA team"}' | jq .status.state
Plain Text
 
"failed"


The rejection reason is recorded in the task history — Goose receives it on the next GetTask poll and can relay it to the developer.

Contrast: Auto-Approved and Blocked Operations

Not every operation triggers HITL. Compare three scenarios:

Shell
 
# Auto-approved: executes immediately
curl -s http://localhost:8082/ \
  -H "Content-Type: application/json" -H "A2A-Version: 1.0" \
  -d '{"jsonrpc":"2.0","id":3,"method":"SendMessage","params":{
    "message":{"messageId":"logs-msg-1","role":"ROLE_USER","parts":[{"text":"analyze-logs --service api-gateway --timeframe 24h"}]}
  }}' | jq .result.task.status.state
# → "TASK_STATE_COMPLETED"

# Blocked: fails immediately
curl -s http://localhost:8082/ \
  -H "Content-Type: application/json" -H "A2A-Version: 1.0" \
  -d '{"jsonrpc":"2.0","id":4,"method":"SendMessage","params":{
    "message":{"messageId":"drop-msg-1","role":"ROLE_USER","parts":[{"text":"drop-database --target production"}]},
    "configuration":{"returnImmediately":true}
  }}' | jq .result.task.status.state
# → "TASK_STATE_FAILED"


Step 5: Running the Complete Demo

Start all services with the one-command script:


Shell
 
cd part4-multi-agent
./start-all.sh


The script builds Part 1's MCP server and Part 4's A2A Flow server, starts both (MCP on :8080, A2A on :8082), and launches the interactive demo SPA. Auto-approved tasks like analyze-logs delegate to Part 1's MCP tools for live data.

Open the A2A Multi-Agent Console at http://localhost:8889/index.html.

If discovery reports Failed to fetch, stop the launcher with Ctrl+C, update the code, run ./start-all.sh again, and reload the page. The console needs the backend's quarkus.http.cors.enabled=true setting to read responses across ports.

This single-user lab binds the A2A server to localhost and explicitly sets a2a.authorization.required=false. A2A SDK 1.3+ requires a TaskAuthorizationProvider by default; shared deployments need authenticated users and that provider. The lab's AGENTS.md operation rules and HITL gates remain active.

Demo Walkthrough

The SPA provides four guided steps that showcase the full governance spectrum:

Step 1: Discover Agent

Click Discover Agent to fetch the Agent Card from /.well-known/agent-card.json. The architecture diagram animates the discovery flow and loads the AGENTS.md governance rules. The stat tiles show the agent is online and governance rules are loaded.

Step 2: Auto-Approved Task (analyze-logs)

Click Auto-Approved Task to submit a read-only log analysis operation. The task flows through AGENTS.md validation (auto-approved), skips the HITL gate, and delegates to Part 1's MCP tools via McpToolClient. The state machine transitions: SUBMITTED → WORKING → GOVERNANCE → EXECUTING → COMPLETED.

Step 3: HITL Task (migrate-schema)

Click HITL Task to submit a high-risk database migration. The task pauses at INPUT_REQUIRED after governance flags it for human review. An approval panel appears with Approve and Reject buttons, using the task ID returned by the server. Click Approve to run the simulated schema migration, or Reject to terminate the task. State machine: SUBMITTED → WORKING → GOVERNANCE → HITL GATE → (approve) → EXECUTING → COMPLETED.

Step 4: Blocked Task (drop-database)

Click Blocked Task to submit a destructive operation. AGENTS.md governance rejects it immediately — the task never reaches the HITL gate or MCP delegation. State machine: SUBMITTED → WORKING → GOVERNANCE → FAILED.

Connecting Goose

When Goose adds A2A support, configure it to discover the backend agent:

Shell
 
goose session --a2a-discover http://localhost:8082


Goose will fetch the Agent Card, enumerate available skills, and delegate operations via SendMessage. High-risk operations will pause until a human approves them through the SPA or CLI.

What We Achieved

Starting from the observable architecture in Part 3, we added multi-agent orchestration with governance and HITL without changing any existing MCP tool code:

Layer What We Added Key File
A2A Protocol Agent Card + JSON-RPC via A2A Java SDK AgentCardProducer.java, AgentExecutorProducer.java
Governance AGENTS.md rule parsing and enforcement GovernanceEngine.java, AGENTS.md
Workflow State machine with HITL approval gates WorkflowEngine.java
MCP Integration Tool delegation via quarkus-langchain4j-mcp McpToolClient.java, application.properties
Admin REST API + SPA for human approvals AdminEndpoint.java, index.html


Production Considerations

Concern Local (this tutorial) Production
Task storage In-memory ConcurrentHashMap PostgreSQL + Flyway migrations
HITL notifications Browser polling from SPA WebSocket push or Slack/Teams integration
Governance rules AGENTS.md file on classpath Policy engine (OPA, Cedar, or Keycloak policies)
A2A transport Plain HTTP JSON-RPC mTLS + OAuth2 client credentials
Workflow engine Custom state machine SonataFlow (CNCF Serverless Workflow on Quarkus)
Audit trail In-memory task history Append-only event store with Kafka


Coming Up in Part 5

With governance, security, tracing, and multi-agent orchestration in place, the stack is production-capable — but how do you know it stays correct across code changes? In Part 5, we will add automated agent evaluation and regression testing — using golden datasets to verify that MCP tool outputs, Bean Validation boundaries, and multi-step agent workflows produce correct, consistent results as the codebase evolves.

Quarkus Software development kit

Opinions expressed by DZone contributors are their own.

Related

  • Microservices With Apache Camel and Quarkus (Part 4)
  • The Hidden Production Risks of Third-Party SDKs
  • A Senior Engineer’s Guide to Foundry IQ, MCP, and the OpenAI Agents SDK
  • Part 1: Building Governed MCP Tool Services With Quarkus LangChain4j and Goose

Partner Resources

×

Comments

Likes

Loading

The next likes didn't load. Please try again.

The likes didn't load

Please try again in a moment.

See who liked this

Log in or join DZone for free to see who liked this article.

Log in Join for free
  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook