Skip to content

Step 06 - Human-in-the-Loop Pattern

Human-in-the-Loop Pattern

In the previous step, you built a distributed agent system using Agent-to-Agent (A2A) communication, where the DispositionAgent ran as a remote service.

However, that system made autonomous decisions about vehicle disposition without human oversight. What if you need human approval before executing high-stakes decisions, especially for valuable assets?

In this step, you’ll learn about the Human-in-the-Loop (HITL) pattern - a critical approach where AI agents pause execution to request human approval before proceeding with significant actions.


New Requirement from Miles of Smiles Management: Human Approval for High-Value Dispositions

The Miles of Smiles management team has identified a risk: the system is making autonomous disposition decisions on high-value vehicles without human oversight.

They want to implement a human approval gate with these requirements:

  1. Value threshold: Any vehicle worth more than $15,000 requires human approval before disposition
  2. Two-phase workflow:
  3. Phase 1: AI creates a disposition proposal
  4. Phase 2: Human reviews and approves or rejects the proposal
  5. Execution control: Only execute approved dispositions
  6. Audit trail: Track approval status and reasoning for compliance

This ensures that expensive vehicles aren’t scrapped or sold without proper human review.


What You’ll Learn

In this step, you will:

  • Understand the Human-in-the-Loop (HITL) pattern and when to use it
  • Implement a two-phase approval workflow (proposal → review → execution)
  • Create a DispositionProposalAgent that generates proposals
  • Build a HumanApprovalAgent that simulates human decision-making
  • Modify the FleetSupervisorAgent to route high-value vehicles through approval
  • Add approval tracking to the data model
  • See how HITL provides safety and control in autonomous systems

Understanding Human-in-the-Loop

What is Human-in-the-Loop?

Human-in-the-Loop (HITL) is a pattern where:

  • AI agents perform analysis and create recommendations
  • Execution pauses to request human approval
  • Humans review proposals and make final decisions
  • System proceeds only after approval

Why Use Human-in-the-Loop?

HITL is essential when:

  • High-stakes decisions: Financial impact, safety concerns, legal implications
  • Regulatory compliance: Certain industries require human oversight
  • Trust building: Gradual transition from manual to autonomous processes
  • Edge cases: Unusual situations that AI might handle incorrectly
  • Accountability: Clear human responsibility for critical decisions

HITL vs. Fully Autonomous

Aspect Fully Autonomous Human-in-the-Loop
Speed Fast, immediate Slower, waits for human
Scalability Unlimited Limited by human capacity
Accuracy Consistent but may miss edge cases Human judgment for complex cases
Accountability System responsibility Human responsibility
Cost Lower operational cost Higher due to human involvement

The Two-Phase Workflow

sequenceDiagram
    participant System as Agentic System
    participant Proposal as Proposal Agent
    participant Human as Human Reviewer
    participant Execution as Execution Agent

    System->>Proposal: Analyze situation
    Proposal->>Proposal: Create recommendation
    Proposal-->>System: Proposal ready

    System->>Human: Request approval
    Note over Human: Human reviews<br/>proposal details
    Human-->>System: APPROVED/REJECTED

    alt Approved
        System->>Execution: Execute proposal
        Execution-->>System: Action completed
    else Rejected
        System->>System: Fallback action
        Note over System: Route to alternative<br/>processing path
    end
Hold "Alt" / "Option" to enable pan & zoom

Key Points:

  1. Proposal Phase: AI analyzes and creates a recommendation
  2. Approval Gate: Human reviews and decides
  3. Execution Phase: System acts only if approved
  4. Fallback: Alternative path if rejected

What is Being Added?

We’re enhancing our car management system with:

  • DispositionProposalAgent: Creates disposition proposals for review
  • HumanApprovalAgent: Simulates human approval decisions (in production, this would integrate with a real approval system)
  • Updated FleetSupervisorAgent: Routes high-value vehicles through the approval workflow
  • Enhanced CarConditions: Tracks approval status and reasoning
  • Value-based routing: Different paths for high-value vs. low-value vehicles

The Complete HITL Architecture

graph TB
    Start([Car Return]) --> A[CarProcessingWorkflow<br/>Sequential]

    A --> B[Step 1: FeedbackWorkflow<br/>Parallel Analysis]
    B --> B1[CleaningFeedbackAgent]
    B --> B2[MaintenanceFeedbackAgent]
    B --> B3[DispositionFeedbackAgent]
    B1 --> BEnd[All feedback complete]
    B2 --> BEnd
    B3 --> BEnd

    BEnd --> C[Step 2: FleetSupervisorAgent<br/>Autonomous Orchestration]
    C --> C1{Disposition<br/>Required?}

    C1 -->|Yes| PA[PricingAgent<br/>Estimate Value]
    PA --> VCheck{Value > $15k?}

    VCheck -->|Yes - HIGH VALUE| Proposal[DispositionProposalAgent<br/>Create Proposal]
    Proposal --> Approval[HumanApprovalAgent<br/>Review & Decide]
    Approval --> ApprovalCheck{Approved?}
    ApprovalCheck -->|Yes| Execute[Execute Disposition]
    ApprovalCheck -->|No| Fallback[Route to Maintenance/Cleaning]

    VCheck -->|No - LOW VALUE| Direct[DispositionAgent<br/>Direct Decision]
    Direct --> Execute

    C1 -->|No| Other[MaintenanceAgent or CleaningAgent]

    Execute --> CEnd[Supervisor Decision]
    Fallback --> CEnd
    Other --> CEnd

    CEnd --> D[Step 3: CarConditionFeedbackAgent<br/>Final Summary]
    D --> End([Updated Car with Approval Status])

    style A fill:#90EE90
    style B fill:#87CEEB
    style C fill:#FFB6C1
    style D fill:#90EE90
    style Proposal fill:#FFD700
    style Approval fill:#FF6B6B
    style VCheck fill:#FFA07A
    style ApprovalCheck fill:#FFA07A
    style Start fill:#E8E8E8
    style End fill:#E8E8E8
Hold "Alt" / "Option" to enable pan & zoom

The Key Innovation:

The FleetSupervisorAgent now implements value-based routing:

  • High-value path (>$15,000): PricingAgent → DispositionProposalAgent → HumanApprovalAgent → Execute if approved
  • Low-value path (≤$15,000): PricingAgent → DispositionAgent → Execute directly
  • Fallback: If approval rejected, route to maintenance or cleaning instead

Implementing the Human-in-the-Loop Pattern

Let’s build the HITL system step by step.

Create the DispositionProposalAgent

This agent creates disposition proposals that will be reviewed by humans.

Create src/main/java/com/carmanagement/agentic/agents/DispositionProposalAgent.java:

DispositionProposalAgent.java
package com.carmanagement.agentic.agents;

import dev.langchain4j.agentic.Agent;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;

/**
 * Agent that creates disposition proposals for vehicles requiring disposition.
 * This agent analyzes the vehicle and creates a proposal that will be reviewed
 * by the HumanApprovalAgent if the vehicle value exceeds the threshold.
 */
public interface DispositionProposalAgent {

    @SystemMessage("""
        You are a car disposition specialist for a car rental company.
        Your job is to create a disposition proposal based on the car's value, condition, age, and damage.

        Disposition Options:
        - SCRAP: Car is beyond economical repair or has severe safety concerns
        - SELL: Car has value but is aging out of the fleet or has moderate damage
        - DONATE: Car has minimal value but could serve a charitable purpose
        - KEEP: Car is worth keeping in the fleet

        Decision Criteria:
        - If estimated repair cost > 50% of car value: Consider SCRAP or SELL
        - If car is over 5 years old with significant damage: SCRAP
        - If car is 3-5 years old in fair condition: SELL
        - If car has low value (<$5,000) but functional: DONATE
        - If car is valuable and damage is minor: KEEP

        Your response must include:
        1. Proposed Action: SCRAP/SELL/DONATE/KEEP
        2. Reasoning: Clear explanation of your recommendation

        Format your response as:
        Proposed Action: [SCRAP/SELL/DONATE/KEEP]
        Reasoning: [Your detailed explanation]
        """)
    @UserMessage("""
        Create a disposition proposal for this vehicle:
        - Make: {carMake}
        - Model: {carModel}
        - Year: {carYear}
        - Car Number: {carNumber}
        - Current Condition: {carCondition}
        - Estimated Value: {carValue}
        - Damage/Feedback: {rentalFeedback}

        Provide your disposition proposal with clear reasoning.
        """)
    @Agent(outputKey = "dispositionProposal", description = "Creates disposition proposals for vehicles requiring disposition")
    String createDispositionProposal(
            String carMake,
            String carModel,
            Integer carYear,
            Long carNumber,
            String carCondition,
            String carValue,
            String rentalFeedback);
}

Key Points:

  • Creates proposals rather than final decisions
  • Uses same decision criteria as DispositionAgent
  • Output format includes “Proposed Action” and “Reasoning”
  • Stored in AgenticScope with key dispositionProposal

Create the HumanApprovalAgent

This agent implements TRUE Human-in-the-Loop by using a tool that actually pauses workflow execution until a human makes a decision through the UI.

Create src/main/java/com/carmanagement/agentic/agents/HumanApprovalAgent.java:

HumanApprovalAgent.java
package com.carmanagement.agentic.agents;

import com.carmanagement.agentic.tools.HumanApprovalTool;
import dev.langchain4j.agentic.Agent;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import io.quarkiverse.langchain4j.ToolBox;

/**
 * TRUE Human-in-the-Loop Agent that uses a tool to pause workflow execution
 * and wait for actual human approval through the UI.
 *
 * This agent has access to the requestHumanApproval tool which BLOCKS execution
 * until a human makes a decision via the REST API and UI.
 */
public interface HumanApprovalAgent {

    @SystemMessage("""
        You are a human approval coordinator for high-value vehicle dispositions.

        Your role is to request human approval for disposition proposals by using the requestHumanApproval tool.

        IMPORTANT: You MUST call the requestHumanApproval tool with ALL the provided information.
        The tool will pause the workflow and wait for a human to approve or reject the proposal through the UI.

        After calling the tool, you will receive the human's decision. Format your response as:

        Decision: [APPROVED or REJECTED]
        Reason: [The human's reasoning]

        Do not make decisions yourself - always use the tool to get human input.
        """)
    @UserMessage("""
        A disposition proposal needs human approval:

        Vehicle: {carYear} {carMake} {carModel} (#{carNumber})
        Estimated Value: {carValue}
        Current Condition: {carCondition}
        Damage Report: {rentalFeedback}

        Proposed Action: {proposedDisposition}
        Reasoning: {dispositionReason}

        Use the requestHumanApproval tool to get human approval for this proposal.
        Pass ALL the information to the tool.
        """)
    @Agent(outputKey = "approvalDecision", description = "Coordinates human approval for high-value vehicle dispositions using the requestHumanApproval tool")
    @ToolBox(HumanApprovalTool.class)
    String reviewDispositionProposal(
            String carMake,
            String carModel,
            Integer carYear,
            Long carNumber,
            String carValue,
            String proposedDisposition,
            String dispositionReason,
            String carCondition,
            String rentalFeedback);
}

Key Points:

  • Uses the requestHumanApproval tool which BLOCKS execution until human decides
  • The tool calls HumanInputService.requestInput() which returns a CompletableFuture
  • Workflow execution pauses by calling .get() on the future
  • Human sees pending approval in the UI and clicks Approve/Reject
  • The future completes, workflow resumes with the human’s decision
  • Returns structured decision: APPROVED/REJECTED with reasoning
  • Stored in AgenticScope with key approvalDecision

Create the HumanApprovalTool

This tool implements the actual blocking mechanism for TRUE HITL.

Create src/main/java/com/carmanagement/agentic/tools/HumanApprovalTool.java:

HumanApprovalTool.java
package com.carmanagement.agentic.tools;

import com.carmanagement.model.ApprovalProposal;
import com.carmanagement.service.ApprovalService;
import dev.langchain4j.agent.tool.Tool;
import io.quarkus.logging.Log;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * Tool that enables TRUE Human-in-the-Loop by blocking workflow execution
 * until a human makes an approval decision through the UI.
 */
@ApplicationScoped
public class HumanApprovalTool {

    @Inject
    ApprovalService approvalService;

    @Tool("Request human approval for a high-value vehicle disposition proposal. This will PAUSE the workflow until a human approves or rejects the proposal via the UI.")
    public String requestHumanApproval(
            Long carNumber,
            String carMake,
            String carModel,
            Integer carYear,
            String carValue,
            String proposedDisposition,
            String dispositionReason,
            String carCondition,
            String rentalFeedback) {

        Log.infof("🛑 HITL Tool: Creating approval proposal for car %d - %s %s %s", 
                carNumber, carYear, carMake, carModel);
        Log.info("⏸️  WORKFLOW PAUSED - Waiting for human approval decision via UI");

        try {
            // Create proposal and get CompletableFuture that completes when human decides
            CompletableFuture<ApprovalProposal> approvalFuture = 
                approvalService.createProposalAndWaitForDecision(
                    carNumber, carMake, carModel, carYear, carValue,
                    proposedDisposition, dispositionReason, carCondition, rentalFeedback
                );

            // BLOCK HERE until human makes decision (with 5 minute timeout)
            ApprovalProposal result = approvalFuture.get(5, TimeUnit.MINUTES);

            Log.infof("▶️  WORKFLOW RESUMED - Human decision received: %s", result.decision);

            // Format response for the agent
            return String.format("""
                Human Decision: %s
                Reason: %s
                Approved By: %s
                Decision Time: %s
                """,
                result.decision,
                result.approvalReason != null ? result.approvalReason : "No reason provided",
                result.approvedBy != null ? result.approvedBy : "Unknown",
                result.decidedAt != null ? result.decidedAt.toString() : "Unknown"
            );

        } catch (TimeoutException e) {
            Log.error("⏱️  TIMEOUT: No human decision received within 5 minutes, defaulting to REJECTED");
            return """
                Human Decision: REJECTED
                Reason: Timeout - No human decision received within 5 minutes. Defaulting to rejection for safety.
                Approved By: System (Timeout)
                """;
        } catch (Exception e) {
            Log.errorf(e, "❌ ERROR: Failed to get human approval for car %d", carNumber);
            return String.format("""
                Human Decision: REJECTED
                Reason: Error occurred while waiting for human approval: %s
                Approved By: System (Error)
                """, e.getMessage());
        }
    }
}

Key Points:

  • Creates an approval proposal in the database
  • Returns a CompletableFuture<ApprovalProposal> that completes when human decides
  • Blocks by calling .get(5, TimeUnit.MINUTES) - this is where the workflow pauses!
  • Workflow thread waits here until human clicks Approve/Reject in UI
  • Includes 5-minute timeout for safety
  • Returns human’s decision to the agent

Create the ApprovalService

This service manages the CompletableFutures that pause workflow execution.

Create src/main/java/com/carmanagement/service/ApprovalService.java:

ApprovalService.java
package com.carmanagement.service;

import com.carmanagement.model.ApprovalProposal;
import com.carmanagement.model.ApprovalProposal.ApprovalStatus;
import io.quarkus.logging.Log;
import io.quarkus.vertx.ConsumeEvent;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
import jakarta.transaction.Transactional;
import static jakarta.transaction.Transactional.TxType;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Service for managing approval proposals and the Human-in-the-Loop workflow.
 * This service handles the async nature of human approvals - creating proposals,
 * waiting for human decisions, and continuing workflow execution.
 */
@ApplicationScoped
public class ApprovalService {

    @Inject
    EntityManager entityManager;

    /**
     * Map to store CompletableFutures waiting for approval decisions.
     * Key: carNumber, Value: CompletableFuture that completes when decision is made
     */
    private final Map<Long, CompletableFuture<ApprovalProposal>> pendingApprovals = new ConcurrentHashMap<>();

    /**
     * Executor for async proposal creation to ensure transaction commits before blocking
     */
    private final ExecutorService executor = Executors.newCachedThreadPool();

    /**
     * Create a new approval proposal and return a CompletableFuture that will complete
     * when a human makes a decision.
     * 
     * @param carNumber The car number
     * @param carMake Car make
     * @param carModel Car model
     * @param carYear Car year
     * @param carValue Estimated car value
     * @param proposedDisposition Proposed action (SCRAP, SELL, DONATE, KEEP)
     * @param dispositionReason Reasoning for the proposal
     * @param carCondition Current car condition
     * @param rentalFeedback Rental feedback
     * @return CompletableFuture that completes when human approves/rejects
     */
    public CompletableFuture<ApprovalProposal> createProposalAndWaitForDecision(
            Long carNumber,
            String carMake,
            String carModel,
            Integer carYear,
            String carValue,
            String proposedDisposition,
            String dispositionReason,
            String carCondition,
            String rentalFeedback) {

        // Check if there's already a pending proposal for this car
        ApprovalProposal existing = ApprovalProposal.findPendingByCarNumber(carNumber);
        if (existing != null) {
            Log.warnf("Proposal already exists for car %d, returning existing future", carNumber);
            return pendingApprovals.computeIfAbsent(carNumber, k -> new CompletableFuture<>());
        }

        // Create CompletableFuture first
        CompletableFuture<ApprovalProposal> future = new CompletableFuture<>();
        pendingApprovals.put(carNumber, future);

        // Create proposal in separate thread with its own transaction
        // This ensures the transaction commits BEFORE we return the future
        executor.submit(() -> {
            try {
                createProposalInNewTransaction(carNumber, carMake, carModel, carYear, carValue,
                        proposedDisposition, dispositionReason, carCondition, rentalFeedback);
                Log.info("✅ Proposal creation transaction committed - now visible to queries");
            } catch (Exception e) {
                Log.errorf(e, "Failed to create proposal for car %d", carNumber);
                future.completeExceptionally(e);
                pendingApprovals.remove(carNumber);
            }
        });

        return future;
    }

    @Transactional(TxType.REQUIRES_NEW)
    void createProposalInNewTransaction(
            Long carNumber,
            String carMake,
            String carModel,
            Integer carYear,
            String carValue,
            String proposedDisposition,
            String dispositionReason,
            String carCondition,
            String rentalFeedback) {

        // Create new proposal
        ApprovalProposal proposal = new ApprovalProposal();
        proposal.carNumber = carNumber;
        proposal.carMake = carMake;
        proposal.carModel = carModel;
        proposal.carYear = carYear;
        proposal.carValue = carValue;
        proposal.proposedDisposition = proposedDisposition;
        proposal.dispositionReason = dispositionReason;
        proposal.carCondition = carCondition;
        proposal.rentalFeedback = rentalFeedback;
        proposal.status = ApprovalStatus.PENDING;
        proposal.createdAt = LocalDateTime.now();

        proposal.persist();
        entityManager.flush();

        Log.infof("Created approval proposal ID=%d for car %d - %s %s %s (Value: %s, Proposed: %s)",
                proposal.id, carNumber, carYear, carMake, carModel, carValue, proposedDisposition);
        Log.info("⏸️  WORKFLOW PAUSED - Waiting for human approval decision");
        Log.infof("Proposal persisted with ID: %d, status: %s", proposal.id, proposal.status);
    }

    /**
     * Process a human's approval decision and complete the waiting CompletableFuture.
     * This resumes the workflow execution.
     * 
     * @param proposalId The proposal ID
     * @param approved Whether approved or rejected
     * @param reason Human's reasoning
     * @param approvedBy Who made the decision
     * @return The updated proposal
     */
    @Transactional(TxType.REQUIRES_NEW)
    public ApprovalProposal processDecision(Long proposalId, boolean approved, String reason, String approvedBy) {
        ApprovalProposal proposal = ApprovalProposal.findById(proposalId);
        if (proposal == null) {
            throw new IllegalArgumentException("Proposal not found: " + proposalId);
        }

        if (proposal.status != ApprovalStatus.PENDING) {
            throw new IllegalStateException("Proposal is not pending: " + proposalId);
        }

        // Update proposal with decision
        proposal.status = approved ? ApprovalStatus.APPROVED : ApprovalStatus.REJECTED;
        proposal.decision = approved ? "APPROVED" : "REJECTED";
        proposal.approvalReason = reason;
        proposal.approvedBy = approvedBy;
        proposal.decidedAt = LocalDateTime.now();

        proposal.persist();

        Log.infof("Human decision received for car %d: %s - %s",
                proposal.carNumber, proposal.decision, reason);
        Log.info("▶️  WORKFLOW RESUMED - Continuing with approval decision");

        // Complete the CompletableFuture to resume workflow
        CompletableFuture<ApprovalProposal> future = pendingApprovals.remove(proposal.carNumber);
        if (future != null) {
            future.complete(proposal);
        }

        return proposal;
    }

    /**
     * Get all pending approval proposals.
     */
    public List<ApprovalProposal> getPendingProposals() {
        return ApprovalProposal.findAllPending();
    }

    /**
     * Get a specific proposal by ID.
     */
    public ApprovalProposal getProposal(Long proposalId) {
        return ApprovalProposal.findById(proposalId);
    }

    /**
     * Check if there's a pending approval for a car.
     */
    public ApprovalProposal getPendingProposalForCar(Long carNumber) {
        return ApprovalProposal.findPendingByCarNumber(carNumber);
    }
}

Key Points:

  • Stores CompletableFuture<ApprovalProposal> in a map keyed by car number
  • createProposalAndWaitForDecision() creates the future and returns it
  • Proposal is persisted in a separate transaction to ensure it’s visible to UI queries
  • processDecision() completes the future when human makes a decision
  • This completion resumes the workflow that was blocked on .get()

Create the ApprovalProposal Entity

This entity stores proposals in the database so the UI can display them.

Create src/main/java/com/carmanagement/model/ApprovalProposal.java:

ApprovalProposal.java
package com.carmanagement.model;

import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Column;
import java.time.LocalDateTime;

/**
 * Entity representing a disposition proposal awaiting human approval.
 * This is the core of the Human-in-the-Loop pattern - proposals are stored
 * and the workflow pauses until a human makes an approval decision.
 */
@Entity
public class ApprovalProposal extends PanacheEntity {

    /**
     * The car number this proposal is for
     */
    @Column(nullable = false)
    public Long carNumber;

    /**
     * Car make
     */
    public String carMake;

    /**
     * Car model
     */
    public String carModel;

    /**
     * Car year
     */
    public Integer carYear;

    /**
     * Estimated car value
     */
    public String carValue;

    /**
     * Proposed disposition action (SCRAP, SELL, DONATE, KEEP)
     */
    @Column(nullable = false)
    public String proposedDisposition;

    /**
     * Reasoning for the proposed disposition
     */
    @Column(length = 2000)
    public String dispositionReason;

    /**
     * Current car condition
     */
    @Column(length = 1000)
    public String carCondition;

    /**
     * Rental feedback that triggered this proposal
     */
    @Column(length = 2000)
    public String rentalFeedback;

    /**
     * Current status of the approval
     */
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    public ApprovalStatus status = ApprovalStatus.PENDING;

    /**
     * Human's decision (APPROVED or REJECTED)
     */
    public String decision;

    /**
     * Human's reasoning for their decision
     */
    @Column(length = 1000)
    public String approvalReason;

    /**
     * Who approved/rejected (for audit trail)
     */
    public String approvedBy;

    /**
     * When the proposal was created
     */
    @Column(nullable = false)
    public LocalDateTime createdAt = LocalDateTime.now();

    /**
     * When the decision was made
     */
    public LocalDateTime decidedAt;

    /**
     * Find pending proposal for a specific car
     */
    public static ApprovalProposal findPendingByCarNumber(Long carNumber) {
        return find("carNumber = ?1 and status = ?2", carNumber, ApprovalStatus.PENDING).firstResult();
    }

    /**
     * Find all pending proposals
     */
    public static java.util.List<ApprovalProposal> findAllPending() {
        return find("status", ApprovalStatus.PENDING).list();
    }

    /**
     * Approval status enum
     */
    public enum ApprovalStatus {
        PENDING,
        APPROVED,
        REJECTED
    }
}

Create the ApprovalResource

This REST resource allows the UI to fetch pending approvals and submit decisions.

Create src/main/java/com/carmanagement/resource/ApprovalResource.java:

ApprovalResource.java
package com.carmanagement.resource;

import com.carmanagement.model.ApprovalProposal;
import com.carmanagement.service.ApprovalService;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import io.quarkus.logging.Log;

import java.util.List;
import java.util.Map;

/**
 * REST resource for managing approval proposals.
 * Provides endpoints for humans to view and approve/reject proposals.
 */
@Path("/api/approvals")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class ApprovalResource {

    @Inject
    ApprovalService approvalService;

    /**
     * Get all pending approval proposals.
     * This is called by the UI to display proposals awaiting human decision.
     */
    @GET
    @Path("/pending")
    public List<ApprovalProposal> getPendingProposals() {
        return approvalService.getPendingProposals();
    }

    /**
     * Get a specific proposal by ID.
     */
    @GET
    @Path("/{proposalId}")
    public Response getProposal(@PathParam("proposalId") Long proposalId) {
        ApprovalProposal proposal = approvalService.getProposal(proposalId);
        if (proposal == null) {
            return Response.status(Response.Status.NOT_FOUND)
                    .entity(Map.of("error", "Proposal not found"))
                    .build();
        }
        return Response.ok(proposal).build();
    }

    /**
     * Approve a proposal.
     * This is called when a human clicks the "Approve" button in the UI.
     * 
     * @param proposalId The proposal ID
     * @param request Request body containing reason and approvedBy
     */
    @POST
    @Path("/{proposalId}/approve")
    public Response approveProposal(
            @PathParam("proposalId") Long proposalId,
            Map<String, String> request) {

        try {
            String reason = request.getOrDefault("reason", "Approved by human reviewer");
            String approvedBy = request.getOrDefault("approvedBy", "Workshop User");

            Log.infof("Approval request received for proposal %d by %s", proposalId, approvedBy);

            ApprovalProposal proposal = approvalService.processDecision(
                    proposalId, true, reason, approvedBy);

            return Response.ok(proposal).build();
        } catch (IllegalArgumentException e) {
            return Response.status(Response.Status.NOT_FOUND)
                    .entity(Map.of("error", e.getMessage()))
                    .build();
        } catch (IllegalStateException e) {
            return Response.status(Response.Status.BAD_REQUEST)
                    .entity(Map.of("error", e.getMessage()))
                    .build();
        } catch (Exception e) {
            Log.error("Error approving proposal", e);
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
                    .entity(Map.of("error", "Error processing approval: " + e.getMessage()))
                    .build();
        }
    }

    /**
     * Reject a proposal.
     * This is called when a human clicks the "Reject" button in the UI.
     * 
     * @param proposalId The proposal ID
     * @param request Request body containing reason and approvedBy
     */
    @POST
    @Path("/{proposalId}/reject")
    public Response rejectProposal(
            @PathParam("proposalId") Long proposalId,
            Map<String, String> request) {

        try {
            String reason = request.getOrDefault("reason", "Rejected by human reviewer");
            String approvedBy = request.getOrDefault("approvedBy", "Workshop User");

            Log.infof("Rejection request received for proposal %d by %s", proposalId, approvedBy);

            ApprovalProposal proposal = approvalService.processDecision(
                    proposalId, false, reason, approvedBy);

            return Response.ok(proposal).build();
        } catch (IllegalArgumentException e) {
            return Response.status(Response.Status.NOT_FOUND)
                    .entity(Map.of("error", e.getMessage()))
                    .build();
        } catch (IllegalStateException e) {
            return Response.status(Response.Status.BAD_REQUEST)
                    .entity(Map.of("error", e.getMessage()))
                    .build();
        } catch (Exception e) {
            Log.error("Error rejecting proposal", e);
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
                    .entity(Map.of("error", "Error processing rejection: " + e.getMessage()))
                    .build();
        }
    }
}

REST API Endpoints:

  • GET /api/approvals/pending - Returns all pending approval proposals
  • POST /api/approvals/{id}/approve - Approve a proposal
  • POST /api/approvals/{id}/reject - Reject a proposal

TRUE Human-in-the-Loop Implementation

This is a TRUE HITL implementation where:

  • ✅ Workflow execution actually pauses when approval is needed
  • ✅ The workflow thread blocks on CompletableFuture.get()
  • ✅ Human sees pending approvals in the real-time UI
  • ✅ Human clicks Approve/Reject buttons in the UI
  • ✅ The future completes with the human’s decision
  • ✅ Workflow resumes and continues with the decision
  • ✅ Includes timeout handling (5 minutes)
  • ✅ Full audit trail in the database

Update the FleetSupervisorAgent

Modify the supervisor to implement value-based routing with the approval workflow.

Update src/main/java/com/carmanagement/agentic/agents/FleetSupervisorAgent.java:

FleetSupervisorAgent.java
package com.carmanagement.agentic.agents;

import dev.langchain4j.agentic.declarative.SupervisorAgent;
import dev.langchain4j.agentic.declarative.SupervisorRequest;
import dev.langchain4j.service.SystemMessage;

/**
 * Supervisor agent that orchestrates the entire car processing workflow.
 * Coordinates feedback analysis agents and action agents based on car condition.
 * Implements TRUE human-in-the-loop pattern for high-value vehicle dispositions.
 */
public interface FleetSupervisorAgent {

    @SystemMessage("""
        You are a fleet supervisor for a car rental company. You coordinate action agents based on feedback analysis.

        The feedback has already been analyzed and you have these inputs:
        - cleaningRequest: What cleaning is needed (or "CLEANING_NOT_REQUIRED")
        - maintenanceRequest: What maintenance is needed (or "MAINTENANCE_NOT_REQUIRED")
        - dispositionRequest: Whether severe damage requires disposition (or "DISPOSITION_NOT_REQUIRED")

        ═══════════════════════════════════════════════════════════════════════════
        CRITICAL RULE #1: CHECK dispositionRequest FIRST - DO NOT MAKE YOUR OWN DECISION
        ═══════════════════════════════════════════════════════════════════════════

        IF dispositionRequest contains "DISPOSITION_NOT_REQUIRED":
           → The car does NOT need disposition
           → DO NOT invoke PricingAgent
           → DO NOT invoke DispositionProposalAgent
           → DO NOT invoke HumanApprovalAgent
           → DO NOT invoke DispositionAgent
           → ONLY handle cleaning/maintenance if needed
           → Include "APPROVAL_NOT_REQUIRED" in your response
           → STOP - Do not continue to disposition logic

        IF dispositionRequest contains "DISPOSITION_REQUIRED":
           → Continue with disposition workflow below

        ═══════════════════════════════════════════════════════════════════════════
        DISPOSITION WORKFLOW (ONLY if dispositionRequest = "DISPOSITION_REQUIRED")
        ═══════════════════════════════════════════════════════════════════════════

        Step 1: Invoke PricingAgent to estimate car value
        Step 2: Extract the NUMERIC value (e.g., $9,180 = 9180, $20,400 = 20400)
        Step 3: Compare ONLY the numeric value to 15000

        IF numeric value > 15000 (HIGH-VALUE - requires human approval):
           a) Invoke DispositionProposalAgent to create proposal
           b) Invoke HumanApprovalAgent (workflow will PAUSE for human decision)
           c) If HumanApprovalAgent returns APPROVED:
              - You MUST include the exact keyword "APPROVED_BY_USER" in your final response
              - Execute the approved disposition action
              - DO NOT invoke any other agents unless the action is KEEP
           d) If HumanApprovalAgent returns REJECTED:
              - You MUST include the exact keyword "REJECTED_BY_USER" in your final response
              - The human wants to keep and repair the vehicle instead
              - Invoke MaintenanceAgent to repair the vehicle
              - DO NOT invoke DispositionAgent
              - DO NOT try to disposition the vehicle in any other way

        IF numeric value <= 15000 (LOW-VALUE - no human approval needed):
           a) DO NOT invoke DispositionProposalAgent
           b) DO NOT invoke HumanApprovalAgent
           c) Invoke DispositionAgent DIRECTLY (it will decide: SCRAP/SELL/DONATE/KEEP)
           d) You MUST include the exact keyword "APPROVAL_NOT_REQUIRED" in your final response
           e) If DispositionAgent says KEEP: invoke MaintenanceAgent or CleaningAgent as needed

        CRITICAL: The threshold is EXACTLY 15000. Values like 9180, 14999 are LOW-VALUE (<=15000).
        Values like 15001, 16830, 20400 are HIGH-VALUE (>15000).

        ═══════════════════════════════════════════════════════════════════════════
        CLEANING/MAINTENANCE (when disposition not required)
        ═══════════════════════════════════════════════════════════════════════════

        - If maintenanceRequest ≠ "MAINTENANCE_NOT_REQUIRED": Invoke MaintenanceAgent
        - If cleaningRequest ≠ "CLEANING_NOT_REQUIRED": Invoke CleaningAgent
        - Can invoke both if both needed
        - If both "NOT_REQUIRED": No action needed, car is ready

        ═══════════════════════════════════════════════════════════════════════════
        REQUIRED KEYWORDS IN YOUR FINAL RESPONSE
        ═══════════════════════════════════════════════════════════════════════════

        Your final response MUST include EXACTLY ONE of these keywords:
        - "APPROVED_BY_USER" - if human approved disposition (high-value car, value > 15000)
        - "REJECTED_BY_USER" - if human rejected disposition (high-value car, value > 15000)
        - "APPROVAL_NOT_REQUIRED" - if no approval needed (low-value car value <= 15000, or no disposition)

        CRITICAL: These keywords are case-sensitive and must appear EXACTLY as shown above.
        The system uses these keywords to route the vehicle correctly.
        """)
    @SupervisorAgent(
        outputKey = "supervisorDecision",
        subAgents = {
            PricingAgent.class,
            DispositionProposalAgent.class,
            HumanApprovalAgent.class,
            DispositionAgent.class,
            MaintenanceAgent.class,
            CleaningAgent.class
        }
    )
    String superviseCarProcessing(
        String carMake,
        String carModel,
        Integer carYear,
        Long carNumber,
        String carCondition,
        String rentalFeedback,
        String cleaningFeedback,
        String maintenanceFeedback,
        String cleaningRequest,
        String maintenanceRequest,
        String dispositionRequest
    );

    @SupervisorRequest
    static String request(
        String carMake,
        String carModel,
        Integer carYear,
        Long carNumber,
        String carCondition,
        String cleaningRequest,
        String maintenanceRequest,
        String dispositionRequest,
        String rentalFeedback
    ) {
        // Determine if disposition is required
        boolean dispositionRequired = dispositionRequest != null &&
                                     dispositionRequest.toUpperCase().contains("DISPOSITION_REQUIRED");

        if (!dispositionRequired) {
            // No disposition needed - simple path
            return String.format("""
                ═══════════════════════════════════════════════════════════════════════════
                ✅ NO DISPOSITION REQUIRED
                ═══════════════════════════════════════════════════════════════════════════

                Car: %d %s %s (#%d)
                Current Condition: %s

                Disposition Request: %s
                Cleaning Request: %s
                Maintenance Request: %s

                INSTRUCTIONS:
                - DO NOT invoke PricingAgent
                - DO NOT invoke DispositionProposalAgent
                - DO NOT invoke HumanApprovalAgent
                - DO NOT invoke DispositionAgent
                - Only invoke MaintenanceAgent if maintenance needed
                - Only invoke CleaningAgent if cleaning needed
                - Include "APPROVAL_NOT_REQUIRED" in your response
                """,
                carYear, carMake, carModel, carNumber, carCondition,
                dispositionRequest, cleaningRequest, maintenanceRequest
            );
        }

        // Disposition required - complex path
        return String.format("""
            ═══════════════════════════════════════════════════════════════════════════
            ⚠️  DISPOSITION REQUIRED - FOLLOW WORKFLOW
            ═══════════════════════════════════════════════════════════════════════════

            Car: %d %s %s (#%d)
            Current Condition: %s
            Rental Feedback: %s

            Disposition Request: %s
            Cleaning Request: %s
            Maintenance Request: %s

            STEP 1: Invoke PricingAgent to get car value
            STEP 2: Extract numeric value for comparison (e.g., "$10,710" → 10710)
            STEP 3: Compare to threshold:
                    - If value > 15000: HIGH-VALUE → Invoke DispositionProposalAgent + HumanApprovalAgent
                    - If value <= 15000: LOW-VALUE → Invoke DispositionAgent directly (NO approval)

            IMPORTANT: When invoking DispositionAgent or DispositionProposalAgent:
            - Pass carValue as a STRING with dollar sign (e.g., "$10,710" not 10710)
            - Use the EXACT format from PricingAgent's response

            REMEMBER: You MUST include one of these keywords in your final response:
            - "APPROVED_BY_USER" (if human approved)
            - "REJECTED_BY_USER" (if human rejected)
            - "APPROVAL_NOT_REQUIRED" (if value <= 15000, no approval needed)
            """,
            carYear, carMake, carModel, carNumber, carCondition, rentalFeedback,
            dispositionRequest, cleaningRequest, maintenanceRequest
        );
    }
}

Key Changes:

  • Added DispositionProposalAgent and HumanApprovalAgent to subAgents
  • Implemented two-path routing based on vehicle value
  • High-value path: Proposal → Approval → Execute if approved
  • Low-value path: Direct disposition decision
  • Stores approval status in AgenticScope for tracking

Update the CarConditions Model

Add approval tracking fields to the data model.

Update src/main/java/com/carmanagement/model/CarConditions.java:

CarConditions.java
package com.carmanagement.model;

/**
 * Record representing the conditions of a car.
 *
 * @param generalCondition   A description of the car's general condition
 * @param carAssignment      Indicates the action required
 * @param approvalStatus     Status of human approval (APPROVED/REJECTED/NOT_REQUIRED)
 * @param approvalReason     Reason for approval decision
 */
public record CarConditions(
    String generalCondition, 
    CarAssignment carAssignment,
    String approvalStatus,
    String approvalReason
) {
    /**
     * Constructor for backward compatibility without approval fields.
     */
    public CarConditions(String generalCondition, CarAssignment carAssignment) {
        this(generalCondition, carAssignment, "NOT_REQUIRED", null);
    }
}

Key Points:

  • Added approvalStatus field (APPROVED/REJECTED/NOT_REQUIRED)
  • Added approvalReason field for audit trail
  • Backward-compatible constructor for existing code

Update Application Configuration

Add configuration for the approval threshold.

Update src/main/resources/application.properties:

# Human-in-the-Loop configuration
# Threshold for requiring human approval on high-value dispositions
car-management.approval.threshold=15000

This makes the threshold configurable without code changes.


Try the Complete Solution

Now let’s see the Human-in-the-Loop pattern in action!

Keep the Remote A2A Service Running

Before starting the step-06 application, make sure you have the remote A2A service from step-05 running in a separate terminal.

Why? The DispositionAgent used in this step is still running as a remote service (from step-05) and communicates via Agent-to-Agent (A2A) protocol.

Port Configuration:

  • Remote A2A service (step-05): http://localhost:8888
  • Main application (step-06): http://localhost:8080

To start the remote service:

cd section-2/step-05/remote-a2a-agent
./mvnw quarkus:dev

Keep this terminal running while you work with step-06. Both services need to be running for the Human-in-the-Loop workflow to work correctly.

Start the Application

  1. Navigate to the step-06 directory:
cd section-2/step-06
  1. Start the application:
./mvnw quarkus:dev
mvnw quarkus:dev
  1. Open http://localhost:8080

Test HITL Scenarios

Try these scenarios to see how the approval workflow handles different vehicle values:

Scenario 1: High-Value Vehicle Requiring Approval

Enter the following text in the feedback field for the Honda Civic:

The car was in a serious collision. Front end is completely destroyed and airbags deployed.

What happens:

flowchart TD
    Start([Input: Serious collision<br/>Front end destroyed])

    Start --> FW[FeedbackWorkflow<br/>Detects: DISPOSITION_REQUIRED]

    FW --> FSA[FleetSupervisorAgent<br/>Orchestration]
    FSA --> PA[PricingAgent]
    PA --> Value[Estimate: ~$18,000<br/>2020 Honda Civic]

    Value --> Check{Value > $15,000?}
    Check -->|Yes| Proposal[DispositionProposalAgent<br/>Creates Proposal]
    Proposal --> PropResult[Proposed: SCRAP<br/>Reasoning: Severe damage]

    PropResult --> Human[HumanApprovalAgent<br/>Reviews Proposal]
    Human --> Decision{Decision}
    Decision -->|APPROVED| Execute[Execute SCRAP]
    Decision -->|REJECTED| Fallback[Route to Maintenance]

    Execute --> Result([Status: PENDING_DISPOSITION<br/>Approval: APPROVED])
    Fallback --> Result2([Status: IN_MAINTENANCE<br/>Approval: REJECTED])

    style FW fill:#FAE5D3
    style FSA fill:#D5F5E3
    style PA fill:#F9E79F
    style Proposal fill:#FFD700
    style Human fill:#FF6B6B
    style Check fill:#FFA07A
    style Decision fill:#FFA07A
    style Result fill:#D2B4DE
    style Result2 fill:#D2B4DE
Hold "Alt" / "Option" to enable pan & zoom

Expected Result:

  • PricingAgent estimates value at ~$18,000 (above threshold)
  • DispositionProposalAgent creates SCRAP proposal
  • HumanApprovalAgent reviews and likely APPROVES (severe damage justifies scrapping)
  • Status: PENDING_DISPOSITION
  • Condition includes approval status and reasoning

Scenario 2: High-Value Vehicle - Approval Rejected

Enter the following text in the Mercedes Benz feedback field:

Minor fender bender, small dent in rear bumper

What happens:

flowchart TD
    Start([Input: Minor fender bender<br/>small dent])

    Start --> FW[FeedbackWorkflow<br/>Detects: DISPOSITION_REQUIRED]

    FW --> FSA[FleetSupervisorAgent]
    FSA --> PA[PricingAgent]
    PA --> Value[Estimate: ~$25,000<br/>2021 Mercedes Benz]

    Value --> Check{Value > $15,000?}
    Check -->|Yes| Proposal[DispositionProposalAgent<br/>Creates Proposal]
    Proposal --> PropResult[Proposed: SELL or KEEP<br/>Minor damage]

    PropResult --> Human[HumanApprovalAgent<br/>Reviews Proposal]
    Human --> Decision[Decision: REJECTED<br/>Too valuable for minor damage]

    Decision --> Fallback[Route to Maintenance<br/>Repair instead]
    Fallback --> Result([Status: IN_MAINTENANCE<br/>Approval: REJECTED])

    style FW fill:#FAE5D3
    style FSA fill:#D5F5E3
    style PA fill:#F9E79F
    style Proposal fill:#FFD700
    style Human fill:#FF6B6B
    style Check fill:#FFA07A
    style Result fill:#D2B4DE
Hold "Alt" / "Option" to enable pan & zoom

Expected Result:

  • PricingAgent estimates value at ~$25,000 (high value)
  • DispositionProposalAgent suggests SELL or KEEP
  • HumanApprovalAgent REJECTS (too valuable for disposition with minor damage)
  • Fallback: Routes to MaintenanceAgent instead
  • Status: IN_MAINTENANCE
  • Approval status: REJECTED with reasoning

Scenario 3: Low-Value Vehicle - No Approval Needed

Enter the following text in the Ford F-150 feedback field (Maintenance Returns tab):

The truck is totaled, completely inoperable, very old

What happens:

flowchart TD
    Start([Input: Totaled truck<br/>very old])

    Start --> FW[FeedbackWorkflow<br/>Detects: DISPOSITION_REQUIRED]

    FW --> FSA[FleetSupervisorAgent]
    FSA --> PA[PricingAgent]
    PA --> Value[Estimate: ~$8,000<br/>2019 Ford F-150, totaled]

    Value --> Check{Value > $15,000?}
    Check -->|No| Direct[DispositionAgent<br/>Direct Decision]
    Direct --> Decision[Decision: SCRAP<br/>Beyond economical repair]

    Decision --> Result([Status: PENDING_DISPOSITION<br/>Approval: NOT_REQUIRED])

    style FW fill:#FAE5D3
    style FSA fill:#D5F5E3
    style PA fill:#F9E79F
    style Direct fill:#87CEEB
    style Check fill:#FFA07A
    style Result fill:#D2B4DE
Hold "Alt" / "Option" to enable pan & zoom

Expected Result:

  • PricingAgent estimates value at ~$8,000 (below threshold)
  • Skips approval workflow (low value)
  • DispositionAgent makes direct SCRAP decision
  • Status: PENDING_DISPOSITION
  • Approval status: NOT_REQUIRED

Check the Logs

Watch the console output to see the approval workflow execution:

FeedbackWorkflow executing...
  ├─ DispositionFeedbackAgent: DISPOSITION_REQUIRED
FleetSupervisorAgent orchestrating...
  ├─ PricingAgent: Estimated value $18,000
  ├─ Value check: $18,000 > $15,000  Approval required
  ├─ DispositionProposalAgent: Proposed SCRAP
  ├─ HumanApprovalAgent: Reviewing proposal...
  └─ Decision: APPROVED - Severe damage justifies scrapping
CarConditionFeedbackAgent updating...
  └─ Approval status: APPROVED

Notice how high-value vehicles go through the proposal → approval → execution flow!


Why Human-in-the-Loop Matters

Safety and Control

HITL provides a safety net for autonomous systems:

  • Prevents costly mistakes: Human review catches edge cases
  • Builds trust: Gradual transition from manual to autonomous
  • Maintains accountability: Clear human responsibility for critical decisions

Compliance and Audit

Many industries require human oversight:

  • Financial services: Large transactions need approval
  • Healthcare: Treatment decisions require physician review
  • Legal: Contract terms need lawyer approval
  • Audit trails: Track who approved what and when

Balancing Automation and Control

HITL lets you tune the automation level:

graph LR
    A[Fully Manual] --> B[HITL - High Threshold]
    B --> C[HITL - Low Threshold]
    C --> D[Fully Autonomous]

    style A fill:#FF6B6B
    style B fill:#FFD700
    style C fill:#87CEEB
    style D fill:#90EE90
Hold "Alt" / "Option" to enable pan & zoom
  • Start with low threshold (approve everything)
  • Gradually increase threshold as confidence grows
  • Eventually move to fully autonomous for routine cases
  • Keep HITL for high-stakes decisions

Optional: Implement It Yourself

If you want hands-on practice implementing the HITL pattern, you can build it step-by-step.

Short on time?

The complete solution is available in section-2/step-06. You can explore the code there if you prefer to move forward quickly.

Prerequisites

Before starting:

  • Completed Step 05 (or have the section-2/step-05/multi-agent-system code available)
  • Application from Step 05 is stopped (Ctrl+C)

Implementation Steps

  1. Copy the step-05 code to create step-06 base
  2. Create DispositionProposalAgent.java with proposal generation logic
  3. Create HumanApprovalAgent.java with approval simulation
  4. Update FleetSupervisorAgent.java to add value-based routing
  5. Update CarConditions.java to add approval fields
  6. Update application.properties with approval threshold
  7. Test with different vehicle values

Follow the code examples shown earlier in this guide.


Experiment Further

1. Implement Real Human Approval

Replace the simulated HumanApprovalAgent with a real approval system:

  • Store proposals in a database with PENDING status
  • Send email/Slack notifications to approvers
  • Create a web UI for reviewing proposals
  • Use async processing to wait for human response

2. Add Approval Workflows

Implement multi-level approval:

  • \(15,000-\)25,000: Single approver
  • \(25,000-\)50,000: Two approvers
  • $50,000: Manager approval required

3. Track Approval Metrics

Add monitoring:

  • Approval rate by value range
  • Average approval time
  • Rejection reasons analysis
  • Approver performance metrics

4. Implement Approval Timeouts

Add time limits:

  • Auto-reject after 24 hours
  • Escalate to manager after 48 hours
  • Send reminder notifications

5. Add Approval History

Track all approvals:

  • Who approved/rejected
  • When the decision was made
  • Reasoning provided
  • Outcome of the decision

Troubleshooting

All vehicles going through approval workflow

Check that the value threshold is correctly configured in application.properties and that the PricingAgent is returning numeric values that can be compared.

Approval agent always approving/rejecting

Review the approval criteria in HumanApprovalAgent’s system message. The simulated logic may need adjustment based on your test scenarios.

Approval status not being tracked

Verify that:

  • FleetSupervisorAgent stores approvalStatus and approvalReason in AgenticScope
  • CarConditions model has the new fields
  • CarProcessingWorkflow retrieves these values from the scope
Low-value vehicles still requiring approval

Check the value comparison logic in FleetSupervisorAgent. Ensure the PricingAgent output is being parsed correctly as a number.


What’s Next?

Congratulations! You’ve completed the final step of Section 2 and implemented the Human-in-the-Loop pattern for safe, controlled autonomous decision-making!

The system now:

  • Routes high-value vehicles through human approval
  • Creates proposals for human review
  • Tracks approval decisions for audit trails
  • Provides fallback paths for rejected proposals
  • Balances automation with human oversight

Ready to wrap up? Head to the conclusion to review everything you’ve learned and see how these patterns apply to real-world scenarios!

Continue to Conclusion - Mastering Agentic Systems