09-mini-projects

Mini Project 11: Agentic Software Delivery Pipeline

Four agents take an SRS to a reviewed pull request: parallel dev and test agents from one refined spec, a review loop that reworks code until it passes.

August 15, 2026
spring-aimulti-agenthuman-in-the-loopparallel-executionreview-loopsdlcmini-project

Tests That Mirror the Code's Blind Spots

The obvious way to build an agentic dev pipeline is a straight line: an agent writes code, then an agent writes tests for that code. It looks like it works — tests pass, green checkmark, ship it. The problem shows up the first time the code has a subtle bug: a test written against the code tends to encode the same wrong assumption the code made, because both came from the same misreading of the requirement. Green tests, wrong behavior, nobody catches it until production.

This project's real design decision is the fix for exactly that: the Test Agent writes its tests against the acceptance criteria, independently of the Developer Agent's implementation — so a bug in the code and a matching blind spot in the tests aren't the same mistake made twice. Four agents, a real human-in-the-loop clarification step, and a genuine review-and-rework loop — the most complete multi-agent showcase in this roadmap.


Setup

groovy
// build.gradle
dependencies {
    implementation 'org.springframework.ai:spring-ai-ollama-spring-boot-starter'
    implementation 'dev.langchain4j:langchain4j-spring-boot-starter'  // parallelBuilder, loopBuilder
    implementation 'org.springframework.boot:spring-boot-starter-web'
}

This project draws on human-in-the-loop patterns and loop-with-exit-condition orchestration from this roadmap's agentic-patterns guide, parallel agent execution (LangChain4j's parallelBuilder, contrasted with hand-rolled CompletableFuture fan-out), and tool calling against a Git hosting API — all four are prerequisites this project composes rather than reintroduces.


1. Requirements Agent: Refining Ambiguity With a Human in the Loop

A raw SRS document is rarely unambiguous enough to code against directly. The Requirements Agent's job is turning it into a structured, unambiguous spec — and explicitly stopping to ask when it can't:

java
public record RefinedSpec(
    String feature,
    List<String> acceptanceCriteria,
    List<String> outOfScope,
    List<String> openQuestions
) {}
java
public RefinedSpec refine(String rawSrs) {
    RefinedSpec draft = requirementsAgent.refine(rawSrs);
 
    if (!draft.openQuestions().isEmpty()) {
        List<String> answers = humanClarificationService.askAndWait(draft.openQuestions());
        return requirementsAgent.incorporateAnswers(draft, answers);
    }
    return draft;
}

humanClarificationService.askAndWait(...) is deliberately a blocking, synchronous-feeling step in this outline even though a real implementation is asynchronous (post to Slack, wait for a reply, resume the pipeline via a webhook or a polling job) — the pipeline's correctness depends on genuinely pausing for ambiguous requirements rather than the agent guessing and moving on. A guessed answer to an ambiguous requirement is exactly the kind of mistake that then propagates identically into both the Developer and Test agents downstream, since they're both working from the same (wrong) refined spec.


2. Developer and Test Agents: Parallel, From the Same Spec, Independently

This is the section that matters most. Both agents receive the same RefinedSpec — but the Test Agent never sees the Developer Agent's code, only the acceptance criteria:

java
interface DeveloperAgent {
    @SystemMessage("Implement the feature described by the given acceptance criteria. Return complete, compilable code.")
    CodeArtifact implement(RefinedSpec spec);
}
 
interface TestAgent {
    @SystemMessage("""
        Write end-to-end tests that verify EACH acceptance criterion independently.
        You have NOT seen any implementation — write tests against the criteria as
        specified, not against assumed implementation details.
        """)
    TestArtifact writeTests(RefinedSpec spec);
}
java
UntypedAgent devAndTestInParallel = AgenticServices.parallelBuilder()
    .subAgents(developerAgent, testAgent)
    .outputName("implementation")
    .build();
🚨

The independence here is the entire point, not an implementation detail. If the Test Agent's prompt or context ever includes the Developer Agent's code — even accidentally, via a shared conversation memory or a careless prompt template — the two agents stop being independent checks on the same spec and become one agent's blind spot copied into two artifacts. Verify this explicitly in your own implementation: the Test Agent's call should be constructible and callable with zero dependency on the Developer Agent's output.

parallelBuilder() runs both agents concurrently since neither depends on the other's output — a straightforward efficiency win here, and the same LangChain4j primitive this roadmap's agentic-patterns guide contrasts against a hand-rolled CompletableFuture.allOf(...) fan-out. Reach for the hand-rolled version when you need finer control over partial-failure handling than the builder exposes; parallelBuilder() otherwise.


3. Review Agent: Evaluate Together, Loop on Failure

The Review Agent sees both artifacts together — code and tests — and makes the approve-or-rework call:

java
public record ReviewResult(
    boolean approved,
    List<String> feedback,
    boolean testsRunClean
) {}
java
interface ReviewAgent {
    @SystemMessage("""
        Evaluate whether the code satisfies the acceptance criteria and whether the
        tests genuinely exercise those criteria (not just that tests exist). Run the
        tests if possible and report pass/fail. Approve only if both the implementation
        is correct AND the tests would catch a regression.
        """)
    ReviewResult review(RefinedSpec spec, CodeArtifact code, TestArtifact tests);
}
java
UntypedAgent reviewLoop = AgenticServices.loopBuilder()
    .subAgents(devAndTestInParallel, reviewAgent)
    .exitCondition(agenticScope -> {
        ReviewResult result = (ReviewResult) agenticScope.readState("reviewResult");
        return result.approved();
    })
    .maxIterations(4) // don't loop forever on a spec the agents can't satisfy
    .build();

On rejection, the Review Agent's feedback flows back to the Developer Agent's next iteration — loopBuilder() is Embabel's replanning idea and this roadmap's agentic-patterns "loop with exit condition" pattern, expressed as a LangChain4j builder rather than hand-written. maxIterations(4) is the same reasoning as every bounded-retry decision elsewhere in this phase: an unbounded loop against a spec the agents genuinely can't satisfy is a cost and latency risk, not a robustness feature.


4. Opening the Pull Request

An approved iteration is the only path that reaches this tool call — everything before it is gated behind the review loop's exit condition:

java
@Component
public class GitHostingTool {
 
    @Tool(description = "Open a pull request with the given branch, title, description, and file changes")
    public PullRequestResult openPullRequest(String branch, String title, String description, List<FileChange> changes) {
        return gitHostingApi.createPullRequest(branch, title, description, changes);
    }
}
java
public PullRequestResult deliverFeature(String rawSrs) {
    RefinedSpec spec = refine(rawSrs); // section 1
    Map<String, Object> result = reviewLoop.invoke(Map.of("spec", spec)); // sections 2-3
 
    ReviewResult finalReview = (ReviewResult) result.get("reviewResult");
    if (!finalReview.approved()) {
        return escalateToHuman(spec, result); // hit maxIterations without approval
    }
 
    CodeArtifact code = (CodeArtifact) result.get("code");
    TestArtifact tests = (TestArtifact) result.get("tests");
    return gitTool.openPullRequest(
        branchNameFor(spec),
        spec.feature(),
        buildPrDescription(spec, finalReview),
        combineChanges(code, tests)
    );
}
⚠️

This is the natural predecessor to this phase's GitHub PR Triage Agent — the pull request this pipeline opens is exactly the kind of artifact that agent would then pick up downstream. Keep the same tool-authorization discipline that project established: this pipeline's GitHostingTool opens a PR, it does not merge one. A merge is a separate, human-gated action regardless of how confident the Review Agent's approval was.


Putting It Together

The full pipeline: Requirements Agent refines the SRS with a human clarification step for anything ambiguous → Developer and Test Agents work in parallel from that same refined spec, the Test Agent genuinely blind to the implementation → Review Agent evaluates both together and either approves (opening a real pull request) or sends specific feedback back into another Developer/Test iteration, bounded to 4 attempts before escalating to a human. Every agent boundary in this pipeline exists to prevent one specific failure mode — guessed requirements, tests that mirror code's blind spots, an unreviewed merge — not as agent-count theater.


What's Next

This is the last project in this phase and the most complete multi-agent showcase in the roadmap. Mini Project 4 (GitHub PR Triage Agent over MCP) is the natural companion — once this pipeline's Review Agent opens a pull request, that project's triage agent is one of the things that would evaluate it in a real team's actual review queue.

Frequently asked questions

Why does the Test Agent need to be blind to the Developer Agent's code — isn't seeing the code helpful for writing more targeted tests?

It's helpful for writing tests that pass against that specific code, which is the opposite of the goal. A test suite's value is catching cases the implementation gets wrong — if the Test Agent can see the code, its tests are pulled toward validating whatever the code already does rather than independently verifying what the acceptance criteria actually require. This is the same reasoning behind writing tests from a spec before implementation in human-driven TDD, applied to two independent agents instead of one developer wearing two hats sequentially.

What happens if the Review Agent approves code that actually has a bug the tests didn't catch?

The Review Agent's approval is only as good as its own evaluation — this pipeline reduces that risk (independent tests, an explicit "tests would catch a regression" bar in the review prompt) but doesn't eliminate it. Treat the opened pull request the way you'd treat one from a junior engineer: a real human reviewer in your normal review process, not an autopublish-on-agent-approval pipeline. Nothing in this project's tool set lets the pipeline merge its own PR.

How is loopBuilder's maxIterations different from the single retry used in this phase's Weekly Report and Postmortem projects?

Same underlying idea — bound automatic iteration and hand off to a human once the bound is hit — different number because the failure cost and recoverability differ. A report or postmortem failing its check twice is a documented text artifact a human edits directly; code failing review four times might have a spec problem the Requirements Agent's clarification step should have caught earlier, which is exactly why escalateToHuman() at the end routes back to spec review, not just "try harder."

Could this pipeline use Embabel's GOAP planning instead of LangChain4j's parallelBuilder and loopBuilder?

Yes — this project deliberately showcases LangChain4j's workflow builders since Mini Project 5 (Fraud Investigation Pipeline) already showcases Embabel's GOAP planning in depth elsewhere in this phase. The underlying ideas (parallel independent branches, a bounded replanning loop) exist in both frameworks; picking one per project here is about roadmap coverage, not a claim that one framework is uniquely correct for this shape of problem.