Game Instructions

Tap a tile to toggle its color. When a tile changes nearby tiles may change as well. Each move affect multiple tiles. The target is to turn all tiles into yellow in the fewest steps possible.

Press 'j' to show/hide the game. Press 'i' to show/hide the instructions. Press 't' to show/hide the top score table.

Steps: 0

Top Scores

Name Steps
Press J to toggle the game, I to toggle instructions, and T to toggle the top scores

Congratulations!

You solved the puzzle in 0 steps!

Success!

Score submitted successfully!

Error

Failed to submit score. Please try again.

Reflections from AI Agents Conference: My Talk on AI Agent Systems and Control Logic

AI Agents Conference Banner
Key ideas from my session on making AI-driven systems more predictable, structured, and aligned with sound engineering principles.

Speaking at the AI Agents Conference was an opportunity to present a structured perspective on building systems that rely on AI agents while maintaining engineering rigor. The first part of my talk was a short overview of pattern matching in Java. The second part was an overview of various cases where pattern matching assists us with orchestrating AI agents. 

The Video of My Talk

The full video of my talk is now available online for anyone who would like to explore the topic in greater depth:  https://www.youtube.com/watch?v=U2ko01b4jow

The Slides of My Talk

You can find the slides I prepared for this talk available for download on SlideShare (slideshare.net). They capture the core ideas, examples, and key takeaways presented during the session, and are intended to serve as a practical reference you can revisit at your own pace. Whether you attended the talk or are exploring the topic independently, the slides provide a structured walkthrough of the concepts discussed, along with supporting visuals and explanations to help reinforce understanding.

Pattern Matching in Java

Pattern Matching is supported in many programming languages. Java is one of them. In my talk, I was using code samples in Java. 

If you happen to use Python and are still not familiar with Pattern Matching, you might find my book on Pattern Matching in Python highly useful. 

The following code sample shows how simple it is to use instanceof not only for ensuring the casting we want to perform is possible, but also to perform the casting itself. 

				
					public class InstanceOfDemo {
    public static void main(String[] args) {
        Object ob = new Dog();

        //the old way
        if(ob instanceof Dog) {
            ((Dog)ob).hau();
        }
        //the new way
        if(ob instanceof Dog dog) {
            dog.hau();
        }
    }
}
				
			

The following code sample shows how we can use pattern matching when using switch statements and switch expressions. 

				
					public class SwitchPatternMatching {
    public static void main(String[] args) {
        var ob = new Dog();
        hello(ob);
    }
    
    public static void hello(Object object) {
        switch(object) {
            case Dog dog -> dog.hau();
            case Tiger tiger -> tiger.miaurrr();
            case Cat cat -> cat.miau();
            case null, default -> System.out.println("not supported");

        }
    }

    public static String hello2(Object object) {
        return switch(object) {
            case Dog dog -> "dog";
            case Tiger tiger -> "a";
            case Cat cat -> "b";
            case null, default -> "c";

        };
    }
}
				
			

When using pattern matching together with Switch Statement (or Switch Expressions), all possible values must be handled. Usually, we will achieve that using the default case and using a sealed class.

				
					abstract sealed class Animal permits Cat, Dog, Human {
}

sealed class Cat extends Animal permits Tiger {
    public void miau() {
        System.out.println("miau miau");
    }
}

final class Dog extends Animal {
    public void hau() {
        System.out.println("hau hau");
    }
}

final class Human extends Animal {
    public void hello() {
        System.out.println("hello hello");
    }
}

final class Tiger extends Cat {
    public void miaurrr() {
        System.out.println("miaurrr miaurrr");
    }
}

public class SealedProgram {
    public static void main(String[] args) {
        var ob = new Dog();
        hello(ob);
    }
    public static void hello(Animal ob) {
        switch(ob) {
            case Dog dog -> dog.hau();
            case Tiger tiger -> tiger.miaurrr();
            case Cat cat -> cat.miau();
            case Human human -> human.hello();
        }
    }
}
				
			

When using pattern matching, we can also add conditions, also known as guarded patterns, in order to limit the match to having specific values in a specific range. 

				
					abstract sealed class Shape permits Rectangle, Circle {
    public abstract double area();
}

final class Rectangle extends Shape {

    private int width;
    private int height;

    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public double area() {
        return width * height;
    }
}

final class Circle extends Shape {
    private int radius;
    @Override
    public double area() {
        return Math.PI * Math.pow(radius,2);
    }
}

public class GuardedProgram {
    public static void main(String[] args) {
        test(new Rectangle(3,5));
    }
    public static void test(Shape o) {
        switch(o) {
            case Rectangle rec when rec.area()>20 -> 
                System.out.println("big rectangle");
            case Rectangle rec when rec.area()<=20 -> 
                System.out.println("small rectangle");
            case Circle circle -> 
                System.out.println("nice circle");
            case Shape shape -> 
                System.out.println("impossible");
        }
    }
}
				
			

We can easily use Pattern Matching with record classes to extract values from the object we compare. The following code sample shows that. 

				
					public class SimpleDemo {
    public static void main(String[] args) {
        var ob = new ColourRectangle(
            new Point(3,4),
            new Point(10,2),
            Color.BLUE);
        printDetails(ob);
    }
    public static void printDetails(Rectangle rec) {
        if(rec instanceof ColourRectangle(
                Point topLefPoint, 
                Point bottomRightPoint, 
                Color color)) {
            System.out.println("color="+color);
            System.out.println("top left="+topLefPoint);
            System.out.println("bottom right="+bottomRightPoint);
        }
    }
}

interface Line {}
record Point(double x, double y) {}
enum Color {BLUE, YELLOW, RED, GREEN}
interface Rectangle {}
record ColourLine(Point p1, Point p2, Color color) {}
record ColourRectangle(Point topLeft, 
    Point bottomRight, Color color) implements Rectangle {}
				
			

AI Agents Orchestration

The second part of my talk covered various cases where the use of pattern matching assists with orchestrating the AI agents we are using. 

The following code sample shows how to orchestrate the results we get from various AI agents by using pattern matching. 

				
					public static void main(String[] args) {
    ...
    Agent agent = AiServices.create(Agent.class, model);
    AgentRawResult raw = agent.chat(
        "What is the capital of France?");
    AgentResult result = raw.toAgentResult();
    System.out.println(handle(result));
}

static String handle(AgentResult result) {
    return switch (result) {
        case AgentResult.Answer a -> "Answer: " + a.text();
        case AgentResult.Clarification c -> "Clarification needed: " + c.question();
        case AgentResult.Error e -> "Error: " + e.reason();
    }
}
				
			

The following code sample shows how to orchestrate a pipeline that includes a planner, coder, and a reviewer. 

				
					/**
 * Example 1 — Three-Agent Pipeline orchestrator.
 *
 * Demonstrates exhaustive pattern matching 
 * on a sealed interface to route messages 
 * through a linear Planner → Coder → Reviewer chain.
 */
public final class PipelineOrchestrator {

    private PipelineOrchestrator() {}

    /**
     * Routes a single delegation step by 
     * pattern-matching on the current message. 
     * Package-visible so tests can call it 
     * directly with mocked agents.
     */
    static AgentMessage step(AgentMessage current,
                             PlannerAgent planner,
                             CoderAgent coder,
                             ReviewerAgent reviewer) {
        return switch (current) {
            case PlanRequest(var task)   -> planner.plan(task);
            case CodeRequest(var spec)   -> coder.code(spec);
            case ReviewRequest(var code) -> reviewer.review(code);
            case FinalResult f           -> f;
        };
    }

    public static void main(String[] args) {
        ChatLanguageModel model = ModelFactory.create();

        PlannerAgent planner = AiServices.builder(PlannerAgent.class)
                .chatLanguageModel(model).build();
        CoderAgent coder = AiServices.builder(CoderAgent.class)
                .chatLanguageModel(model).build();
        ReviewerAgent reviewer = AiServices.builder(ReviewerAgent.class)
                .chatLanguageModel(model).build();

        AgentMessage current = new PlanRequest("write a greeting function");
        Log.log("Orchestrator", "Starting pipeline with: " + current);

        while (!(current instanceof FinalResult)) {
            String agentName = switch (current) {
                case PlanRequest _   -> "Planner";
                case CodeRequest _   -> "Coder";
                case ReviewRequest _ -> "Reviewer";
                case FinalResult _   -> "Done";
            };

            try {
                current = step(current, planner, coder, reviewer);
                Log.log(agentName, "→ " + current);
            } catch (Exception e) {
                Log.error(agentName, agentName + "Agent failed — " + e.getMessage());
                return;
            }
        }

        FinalResult result = (FinalResult) current;
        Log.log("Orchestrator", "Final result: " + result.result());
    }
}

/**
 * Sealed interface representing the typed contracts 
 * exchanged between agents in the three-agent pipeline 
 * (Planner → Coder → Reviewer).
 */
sealed interface AgentMessage
        permits PlanRequest, CodeRequest, ReviewRequest, FinalResult {}
        
/**
 * AI Service that acts as a coder — writes code from 
 * a specification.
 */
interface CoderAgent {

    @SystemMessage("You are a coder. Write code that implements the given specification.")
    ReviewRequest code(@UserMessage String spec);
}

record CodeRequest(String spec) implements AgentMessage {}

record FinalResult(String result) implements AgentMessage {}

/**
 * AI Service that acts as a planner — breaks a user request into a coding spec.
 */
interface PlannerAgent {

    @SystemMessage("You are a planner. Break the user request into a coding spec.")
    CodeRequest plan(@UserMessage String task);
}

record PlanRequest(String task) implements AgentMessage {}

/**
 * AI Service that acts as a reviewer — reviews 
 * code and produces a final result.
 */
interface ReviewerAgent {

    @SystemMessage("You are a reviewer. Review the code and provide a final assessment.")
    FinalResult review(@UserMessage String code);
}

record ReviewRequest(String code) implements AgentMessage {}

				
			

The Branching Delegation Orchestrator interprets AI outputs, classifies them into structured states, and routes execution to the appropriate next step. It brings deterministic, traceable control to probabilistic systems, replacing fragile linear flows with reliable, spec-driven decision logic. The following code sample shows that.

				
					/**
 * Example 2 — Branching Delegation (Router) orchestrator.
 *
 * Demonstrates type-based routing via pattern matching on a sealed interface.
 * A RouterAgent classifies user input, then the orchestrator dispatches
 * to either a TranslatorAgent or SummarizerAgent based on the classification.
 */
public final class RouterOrchestrator {

    private RouterOrchestrator() {}

    /**
     * Dispatches a classified task to the correct agent using pattern matching.
     * Package-visible so tests can call it directly with mocked agents.
     */
    static String dispatch(TaskClassification task,
                           TranslatorAgent translator,
                           SummarizerAgent summarizer) {
        return switch (task) {
            case TranslateTask(var text, var lang) ->
                    translator.translate("Translate the following into " + lang + ": " + text);
            case SummarizeTask(var text) ->
                    summarizer.summarize(text);
        };
    }

    public static void main(String[] args) {
        ChatLanguageModel model = ModelFactory.create();

        RouterAgent router = AiServices.builder(RouterAgent.class)
                .chatLanguageModel(model).build();
        TranslatorAgent translator = AiServices.builder(TranslatorAgent.class)
                .chatLanguageModel(model).build();
        SummarizerAgent summarizer = AiServices.builder(SummarizerAgent.class)
                .chatLanguageModel(model).build();

        String[] inputs = {
                "Translate 'Hello, how are you?' into French",
                "Summarize the following: Java 21 introduces pattern matching for switch, sealed classes, and record patterns."
        };

        for (String input : inputs) {
            Log.log("Orchestrator", "Input: " + input);

            try {
                TaskType type = router.classify(input);
                Log.log("Router", "Classified as: " + type);

                // Map enum → sealed type so the pattern-matching switch stays clean
                TaskClassification classification = switch (type) {
                    case TRANSLATE  -> new TranslateTask(input, "target language from context");
                    case SUMMARIZE  -> new SummarizeTask(input);
                };

                String result = dispatch(classification, translator, summarizer);

                String agentName = switch (classification) {
                    case TranslateTask _ -> "Translator";
                    case SummarizeTask _ -> "Summarizer";
                };
                Log.log(agentName, "Result: " + result);
            } catch (Exception e) {
                Log.error("Orchestrator", "Agent failed — " + e.getMessage());
            }

            Log.log("Orchestrator", "---");
        }
    }
}

/**
 * AI Service that classifies user input as either 
 * TRANSLATE or SUMMARIZE. Returns an enum because 
 * LangChain4j cannot deserialize sealed interfaces directly.
 */
interface RouterAgent {

    @SystemMessage("You are a router. Classify the user request as either TRANSLATE or SUMMARIZE.")
    TaskType classify(@UserMessage String userInput);
}

/**
 * Example 2 — Branching Delegation (Router) orchestrator.
 *
 * Demonstrates type-based routing via pattern matching on a sealed interface.
 * A RouterAgent classifies user input, then the orchestrator dispatches
 * to either a TranslatorAgent or SummarizerAgent based on the classification.
 */
final class RouterOrchestrator {

    private RouterOrchestrator() {}

    /**
     * Dispatches a classified task to the correct agent using pattern matching.
     * Package-visible so tests can call it directly with mocked agents.
     */
    static String dispatch(TaskClassification task,
                           TranslatorAgent translator,
                           SummarizerAgent summarizer) {
        return switch (task) {
            case TranslateTask(var text, var lang) ->
                    translator.translate("Translate the following into " + lang + ": " + text);
            case SummarizeTask(var text) ->
                    summarizer.summarize(text);
        };
    }

    public static void main(String[] args) {
        ChatLanguageModel model = ModelFactory.create();

        RouterAgent router = AiServices.builder(RouterAgent.class)
                .chatLanguageModel(model).build();
        TranslatorAgent translator = AiServices.builder(TranslatorAgent.class)
                .chatLanguageModel(model).build();
        SummarizerAgent summarizer = AiServices.builder(SummarizerAgent.class)
                .chatLanguageModel(model).build();

        String[] inputs = {
                "Translate 'Hello, how are you?' into French",
                "Summarize the following: Java 21 introduces pattern matching for switch, sealed classes, and record patterns."
        };

        for (String input : inputs) {
            Log.log("Orchestrator", "Input: " + input);

            try {
                TaskType type = router.classify(input);
                Log.log("Router", "Classified as: " + type);

                // Map enum → sealed type so the pattern-matching switch stays clean
                TaskClassification classification = switch (type) {
                    case TRANSLATE  -> new TranslateTask(input, "target language from context");
                    case SUMMARIZE  -> new SummarizeTask(input);
                };

                String result = dispatch(classification, translator, summarizer);

                String agentName = switch (classification) {
                    case TranslateTask _ -> "Translator";
                    case SummarizeTask _ -> "Summarizer";
                };
                Log.log(agentName, "Result: " + result);
            } catch (Exception e) {
                Log.error("Orchestrator", "Agent failed — " + e.getMessage());
            }

            Log.log("Orchestrator", "---");
        }
    }
}

/**
 * AI Service that summarises text into a concise form.
 */
interface SummarizerAgent {

    @SystemMessage("You are a summariser. Produce a concise summary of the given text.")
    String summarize(@UserMessage String text);
}

record SummarizeTask(String text) implements TaskClassification {}

/**
 * Sealed interface representing the classification of a user request
 * in the branching delegation (Router) example.
 */
sealed interface TaskClassification
        permits TranslateTask, SummarizeTask {}

/**
 * Enum returned by the RouterAgent — LangChain4j can extract enums reliably.
 * The orchestrator maps this to the sealed {@link TaskClassification} hierarchy.
 */
public enum TaskType {
    TRANSLATE,
    SUMMARIZE
}

record TranslateTask(String text, String targetLanguage) implements TaskClassification {}

/**
 * AI Service that translates text into a target language.
 */
interface TranslatorAgent {

    @SystemMessage("You are a translator. Translate the given text into the requested target language.")
    String translate(@UserMessage String text);
}
				
			

The Retry & Feedback Loop Orchestrator manages iterative refinement by re-invoking agents when outputs fail validation. It applies controlled retries, integrates feedback, and updates context or specifications between attempts. This pattern stabilizes AI behavior, reduces drift, and ensures convergence toward reliable, spec-aligned results. The following code sample shows that. 

				
					/**
 * Example 3 — Retry / Feedback Loop orchestrator.
 *
 * Demonstrates guarded patterns ({@code when a.score() > 7}) and nested
 * record deconstruction ({@code Rejected(var feedback)}) inside a bounded
 * retry loop driven by pattern matching on a sealed interface.
 */
public final class RetryOrchestrator {

    private RetryOrchestrator() {}

    private static final int MAX_ATTEMPTS = 3; // 1 initial + 2 retries

    /**
     * Runs the retry loop: codes once, then reviews up to {@value MAX_ATTEMPTS}
     * times, re-invoking the coder with feedback on each rejection.
     * Package-visible so tests can call it directly with mocked agents.
     */
    static String retryLoop(String task, CoderAgent coder, ReviewerAgent reviewer) {
        String code = coder.code(task);

        for (int attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
            ReviewOutcome outcome = reviewer.review(code);

            Log.log("Orchestrator", "Attempt " + attempt + ": " + outcomeLabel(outcome));

            switch (outcome) {
                case Approved a when a.score() > 7 -> {
                    Log.log("Reviewer", "Approved (high score: " + a.score() + ")");
                    return a.code();
                }
                case Approved a -> {
                    Log.log("Reviewer", "Approved (score: " + a.score() + ")");
                    return a.code();
                }
                case Rejected(var feedback) -> {
                    if (attempt < MAX_ATTEMPTS) {
                        Log.log("Orchestrator", "Re-invoking coder with feedback");
                        code = coder.code(task + "\nFeedback: " + feedback);
                    }
                }
            }
        }

        Log.log("Orchestrator", "WARNING: Max retries reached. Returning last attempt.");
        return code;
    }

    private static String outcomeLabel(ReviewOutcome outcome) {
        return switch (outcome) {
            case Approved a  -> "Approved (score=" + a.score() + ")";
            case Rejected(var feedback) -> "Rejected — " + feedback;
        };
    }

    public static void main(String[] args) {
        ChatLanguageModel model = ModelFactory.create();

        CoderAgent coder = AiServices.builder(CoderAgent.class)
                .chatLanguageModel(model).build();
        ReviewerAgent reviewer = AiServices.builder(ReviewerAgent.class)
                .chatLanguageModel(model).build();

        String task = "Write a Java method that reverses a string without using StringBuilder";

        Log.log("Orchestrator", "Task: " + task);

        try {
            String result = retryLoop(task, coder, reviewer);
            Log.log("Orchestrator", "Final code:\n" + result);
        } catch (Exception e) {
            Log.error("Orchestrator", "Agent failed — " + e.getMessage());
        }
    }
}

record Approved(String code, int score) implements ReviewOutcome {}

/**
 * AI Service that acts as a coder — writes or rewrites code based 
 * on a task description.
 */
interface CoderAgent {

    @SystemMessage("You are a coder. Write code that implements the given task. If feedback is provided, revise your code accordingly.")
    String code(@UserMessage String task);
}

record Rejected(String feedback) implements ReviewOutcome {}

/**
 * AI Service that acts as a reviewer — reviews code and returns an 
 * approval or rejection.
 */
interface ReviewerAgent {

    @SystemMessage("You are a code reviewer. Review the code and either approve it with a quality score or reject it with feedback.")
    ReviewOutcome review(@UserMessage String code);
}

/**
 * Sealed interface representing the outcome of a code review
 * in the retry / feedback loop example.
 */
sealed interface ReviewOutcome
        permits Approved, Rejected {}
				
			

The Parallel Fan-Out Orchestrator distributes a task across multiple agents simultaneously, collecting and aggregating their outputs. It enables concurrency, comparison, and redundancy, improving performance and result quality while maintaining controlled, spec-driven coordination over parallel execution paths. The following code samples shows that. 

				
					/**
 * Example 4 — Parallel Fan-Out / Fan-In orchestrator.
 *
 * Demonstrates concurrent agent invocation using virtual threads
 * ({@link StructuredTaskScope}) and pattern matching on a sealed
 * {@link JudgeVerdict} to select the winning solution.
 */
public final class FanOutOrchestrator {

    private FanOutOrchestrator() {}

    /**
     * Pure selection logic — picks the winning solution based on the verdict.
     * Package-visible so tests can verify without needing virtual threads.
     */
    static String selectWinner(JudgeVerdict verdict, String solutionA, String solutionB) {
        return switch (verdict) {
            case PickedA(var reason) -> solutionA + "\nChosen because: " + reason;
            case PickedB(var reason) -> solutionB + "\nChosen because: " + reason;
        };
    }

    /**
     * Forks {@code AgentA} and {@code AgentB} on virtual threads, collects
     * both solutions, passes them to the {@code JudgeAgent}, and returns
     * the winning solution annotated with the judge's reason.
     * Package-visible so tests can call it directly with mocked agents.
     */
    static String fanOut(String task, AgentA a, AgentB b, JudgeAgent judge)
            throws Exception {
        try (var scope = StructuredTaskScope.open(Joiner.<String>allSuccessfulOrThrow())) {
            Subtask<String> futA = scope.fork(() -> a.solve(task));
            Subtask<String> futB = scope.fork(() -> b.solve(task));
            scope.join();

            String solA = futA.get(), solB = futB.get();

            Log.log("AgentA", "Solution: " + solA);
            Log.log("AgentB", "Solution: " + solB);

            JudgeVerdict verdict = judge.judge(solA, solB);

            Log.log("Judge", "Verdict: " + verdictLabel(verdict));

            return selectWinner(verdict, solA, solB);
        }
    }

    private static String verdictLabel(JudgeVerdict verdict) {
        return switch (verdict) {
            case PickedA(var reason) -> "Picked A — " + reason;
            case PickedB(var reason) -> "Picked B — " + reason;
        };
    }

    public static void main(String[] args) {
        ChatLanguageModel model = ModelFactory.create();

        AgentA agentA = AiServices.builder(AgentA.class)
                .chatLanguageModel(model).build();
        AgentB agentB = AiServices.builder(AgentB.class)
                .chatLanguageModel(model).build();
        JudgeAgent judge = AiServices.builder(JudgeAgent.class)
                .chatLanguageModel(model).build();

        String task = "Write a Java method that checks if a string is a palindrome";

        Log.log("Orchestrator", "Task: " + task);

        try {
            String result = fanOut(task, agentA, agentB, judge);
            Log.log("Orchestrator", "Winning solution:\n" + result);
        } catch (Exception e) {
            Log.error("Orchestrator", "Agent failed — " + e.getMessage());
        }
    }
}

/**
 * AI Service that produces a solution for a given task — approach A.
 */
interface AgentA {

    @SystemMessage("You are Agent A. Solve the given task using a concise, straightforward approach.")
    String solve(@UserMessage String task);
}

/**
 * AI Service that produces a solution for a given task — approach B.
 */
interface AgentB {

    @SystemMessage("You are Agent B. Solve the given task using a creative, alternative approach.")
    String solve(@UserMessage String task);
}

/**
 * AI Service that compares two solutions and picks the better one,
 * returning a {@link JudgeVerdict} indicating which agent's solution was chosen.
 */
interface JudgeAgent {

    @SystemMessage("You are a judge. Compare the two solutions and pick the better one. "
            + "Return PickedA if solution A is better, or PickedB if solution B is better, with a reason.")
    @UserMessage("Solution A: {{solutionA}}\nSolution B: {{solutionB}}")
    JudgeVerdict judge(@V("solutionA") String solutionA,
                       @V("solutionB") String solutionB);
}

/**
 * Sealed interface representing the verdict of a judge agent
 * in the parallel fan-out / fan-in example.
 */
sealed interface JudgeVerdict
        permits PickedA, PickedB {}
        
record PickedA(String reason) implements JudgeVerdict {}

record PickedB(String reason) implements JudgeVerdict {}
				
			

This talk was just one step in an ongoing exploration of how to bring discipline and structure into AI-driven systems. The feedback and discussions during the conference highlighted how relevant these challenges are across industries. I am grateful for the opportunity to contribute and look forward to continuing the conversation with practitioners who are shaping the future of software engineering with AI.

Share:

keyboard image

Programming Thinking Over Syntax

Prioritizing learning programming concepts over the syntax of programming languages becomes essential in an ERA where AI generates code effortlessly.

The Beauty of Code

Coding is Art! Developing Code That Works is Simple. Develop Code with Style is a Challenge!

Update cookies preferences