The Java community spent this week arguing about the OpenJDK interim policy on AI-generated code, and the policy's own FAQ explains the real reason it exists. "Generative AI tools, by their nature, make it easy to create large quantities of plausible-looking code, with plausible-looking tests, which is nonetheless incorrect," it says. "Reviewing submissions of such code can easily become a drain on the already limited time of human reviewers."
That is the whole argument in one sentence: AI output is cheap, and reviewing it is expensive. The policy bans content "generated, in part or in full, by large language models, diffusion models, or similar deep-learning systems" from OpenJDK repositories, pull requests, email, wiki pages, and JBS issues. The thread on Hacker News is at 396 points and 266 comments, and most of the heat comes from the contrast with Oracle's own position. Larry Ellison told Oracle AI World 2025: "The code that Oracle is writing, Oracle isn't writing. Our AI models are writing." Meanwhile Oracle cut 21,000 jobs in June citing AI deployment and is borrowing tens of billions for AI datacenters, as The Register laid out.
AI code is fine inside Oracle. AI code is not fine in the JDK. The difference is the review burden, the thing nobody wants to scale.
That is the same wall I have been hitting in this agent series. Part 6 opened with the numbers from a browser game where players approve or deny commands from an AI agent: 40,000 runs, 409,000 decisions, mean accuracy 66.3%. Humans missed one in three threats. The OpenJDK reviewers and the game players are facing the identical problem: human review does not scale, and attention dies with volume.
So where does a human belong in an agent loop? That is this part. The answer in one line: the agent does everything reversible, and the human gets exactly one approval per irreversible action, with full context, enforced in code, not in a system prompt.
I am a Senior Software Engineer II at BS23 in Dhaka, and I have been building production AI agents with Spring Boot and Spring AI for over a year. This is the same e-commerce agent as Parts 1 through 6: same nine tools, same supervisor, same memory, same test suite. The only change is where the human sits.
The Line That Decides Everything
Every agent design decision starts with one question: is this action reversible?
- Search, browse, product details, recommendations. Reversible. Nothing leaves the conversation, and a wrong answer costs a retry.
- Add to cart, remove from cart, view cart. Reversible. The cart is scratch state, same as a draft document.
- Checkout, cancel an order, refund a payment, change a shipping address after the order is placed. Irreversible, or expensive enough to count. A refund moves real money. A checkout moves stock and creates a commitment.
- Transfer to a new account, delete records with no undo, change credentials. Irreversible and high-consequence. These should not even live inside the agent's toolset.
The design rule: reversible actions run without a human. Irreversible actions get one approval gate. The machine does everything it can do safely, and the human makes the one decision machines are still bad at: whether the irreversible action is actually wanted.
The OpenJDK policy draws the same line from the other side. Its FAQ argues that AI tools are best used for analysis, not creation: "analysis of existing code, rather than creation of new code, is where generative AI tools shine for established projects with large code bases." The community that knows LLMs best is using them to review and understand, not to produce and commit. The agent loop should work the same way. The model prepares. The human commits.
Step 1: The Confirmation Gate That Already Exists
The current codebase already has a gate, and it is a good start. The checkout tool in ShoppingTools carries its rule in the tool description, because the model reads descriptions to decide how to behave:
@Tool(description = """
Check out the shopper's cart: creates an order, decrements stock and clears the cart.
Always confirm the cart contents and total with the shopper BEFORE calling this.""")
public String checkout(
@ToolParam(description = "Full shipping address the shopper provided") String shippingAddress,
ToolContext toolContext) { ... }
The system prompt repeats the rule in the workflow section:
- **Checkout**: Before calling checkout, show cart contents and total,
confirm the shopper wants to proceed, and ask for a shipping address
if you don't have one.
This is the confirmation gate in its soft form. It is prompt rules. The model is asked to get a yes before it calls the tool, and it works most of the time, because the checkout description is unambiguous and the workflow is simple.
My own project notes on that page are explicit about the trade: the confirmation is enforced by the system prompt, not code, a reminder that prompt rules and code rules protect different layers.
That sentence is the whole problem in miniature. Prompt rules are requests. A model can skip them when it is confused, when the shopper pressures it ("just check out, I already said yes"), or when text that arrives mid-conversation steers it. Every agent that depends on a prompt rule for its irreversible step is one bad tool call away from placing an order nobody approved.
The chat-level confirmation is still worth keeping. It is the first line of defense, and it makes the second line rarely fire. But the approval that matters has to be structural.
Step 2: Move the Gate Into Code
The order domain already has a state machine, and that is where the gate belongs:
public enum OrderStatus {
PENDING,
CONFIRMED,
SHIPPED,
DELIVERED,
CANCELLED;
public OrderStatus next() {
return switch (this) {
case PENDING -> CONFIRMED;
case CONFIRMED -> SHIPPED;
case SHIPPED -> DELIVERED;
case DELIVERED, CANCELLED -> this;
};
}
}
The upgrade is one new state in front of PENDING: the order exists, but nothing has been executed. Checkout creates the order in AWAITING_APPROVAL, copies the cart contents into it, and returns an approval link instead of a done message. Stock is not touched yet. The human opens the link, sees exactly what the order contains, and clicks confirm. Only then does the transaction run.
public enum OrderStatus {
AWAITING_APPROVAL, // created by the agent, waiting for the human
PENDING, // confirmed by the human, awaiting fulfilment
CONFIRMED,
SHIPPED,
DELIVERED,
CANCELLED;
}
The next() transition gains one line: AWAITING_APPROVAL -> PENDING.
Checkout becomes two phases. The first phase creates the order and returns the approval URL:
@Transactional
public ShopOrder checkout(String conversationId, String shippingAddress) {
List<CartItem> cart = cartService.getItems(conversationId);
if (cart.isEmpty()) {
throw new IllegalStateException("Cannot check out an empty cart");
}
ShopOrder order = new ShopOrder(conversationId, shippingAddress);
for (CartItem item : cart) {
order.addItem(productService.getById(item.productId()), item.quantity());
}
order.setStatus(OrderStatus.AWAITING_APPROVAL);
order.setApprovalToken(UUID.randomUUID().toString());
ShopOrder saved = orderRepository.save(order);
cartService.clear(conversationId);
return saved;
}
The second phase is the approval itself, and it is a transaction with three guards:
@Transactional
public ShopOrder approve(Long orderId, String approvalToken) {
ShopOrder order = getById(orderId);
if (!order.getStatus().equals(OrderStatus.AWAITING_APPROVAL)) {
throw new IllegalStateException("Order " + orderId + " is not awaiting approval");
}
if (!order.getApprovalToken().equals(approvalToken)) {
throw new IllegalStateException("Invalid approval token");
}
for (OrderItem item : order.getItems()) {
Product product = productService.getById(item.productId());
product.decrementStock(item.quantity());
}
order.setStatus(OrderStatus.PENDING);
return orderRepository.save(order);
}
Three things changed, and each one matters.
The gate is a state, not a sentence. The model cannot skip it, because there is nothing to skip. The tool returns an approval URL, and the order simply does not move until a human confirms. The system prompt rule from Step 1 becomes a nicety instead of a safety mechanism. Even a hallucinating model cannot place the order, because the order it creates is not an order yet.
Stock is checked at approval time, inside the same transaction. If the last unit sold while the order waited, decrementStock throws, the whole approval rolls back, and the buyer learns "sorry, sold out" instead of receiving a phantom order. This is the honest cost of not holding stock: overselling is possible between checkout and approval. A reservation table fixes that, but only add one when the numbers justify it.
The token makes the approval idempotent and one-time. The status guard fails the second call before the token check even runs, so approving twice is a no-op with an error message. Replaying the same link cannot double-decrement stock.
The tool description changes to match the new behavior:
@Tool(description = """
Check out the shopper's cart: creates an order in AWAITING_APPROVAL
and returns an approval link. Tell the shopper the order is created
and awaiting their confirmation on the link. Only the approval page
can confirm the order, never you.""")
Step 3: Decide What Earns an Approval
The gate is cheap to build and expensive to overuse. Every approval is a context switch, and Part 6 measured what context switches do to human attention. So the routing rule has to be explicit, and it has to be simple enough that nobody needs to interpret it.
- Reversible, no approval: search, browse, details, recommendations, add to cart, remove from cart, view cart, drafts, summaries.
- Irreversible, one approval: checkout, cancel an order, refund, change a shipping address after the order is placed, anything that spends money or sends data outside the conversation.
- Never in the agent at all: transferring money to a new account, deleting records with no undo, changing credentials. These need a human workflow that exists entirely outside the agent.
The threshold question comes up every time: should a $5 checkout skip the gate? My answer is no. The cost of running the rule is the same at every price, and the benefit is binary. What changes with price is the approval screen, not the gate. A $5 order gets the same five-second confirmation. A $5,000 order gets that plus a warning line about the amount.
And the model never decides "this one is small enough to skip". The gate is structural, so the model has no vote. The routing rules exist to reduce how often the gate fires in the first place, which mostly happens through conversation design: the agent only calls checkout when the shopper asks for it.
Step 4: Design the Approval So It Can Be Read in Five Seconds
An approval screen that requires reading a paragraph is an approval screen that gets rubber-stamped. The whole point of the Part 6 game data was that attention is scarce, so the screen has to fit one glance:
- What the agent did. The tool calls that led here. "Searched 'aurora headphones', added 2 to cart."
- What will happen. The irreversible consequence. "Order 4821 will be placed for $159.98."
- The money. Total, shipping, taxes, as plain numbers.
- The destination. Shipping address, payment method, recipient.
- A plain-language summary line. "2x Aurora Headphones, $159.98, ships to 12 Spring Lane."
Five lines. If a reviewer needs more than that, the approval is not the problem. The action is.
Three more rules keep the gate honest:
One approval per action. Never ask the same question twice. If an order already has a pending approval, a second request waits or fails. It does not stack.
Expiry. A pending approval dies after fifteen minutes, and the shopper re-requests instead of the system nagging. Expiry is the garbage collector of the state machine. Without it, AWAITING_APPROVAL rows accumulate forever and the support queue becomes the approval queue.
Batch when you can. If the agent's plan needs three irreversible actions, an order, a refund, and a coupon, show them on one screen as one decision. Three separate approvals is exactly how fatigue wins.
Step 5: Make the Audit Trail the Approval's Skeleton
An approval is only as good as the record around it. My hardening notes for this project say it plainly: log every tool call with its arguments, who added what and when, because that log is the audit trail when the agent misbehaves.
The approval token is the join key. It ties together the order id, the tool call log, and the human's decision. When a customer calls and says "I did not approve that", the answer is one query instead of an afternoon of log spelunking:
log.info("approval decision order={} token={} decided={} by={} at={}",
orderId, approvalToken, decision, user, Instant.now());
The same record makes the series of checks from Part 6 testable: the approval endpoint asserts the status guard, the token match, and the stock decrement, all in fast unit tests with a mocked repository, no model call anywhere in the path.
The Honest Cost Section
This pattern costs something, and the cost is friction.
Checkout becomes two round trips. The agent creates the order, the human confirms, the transaction runs. That is slower than one message, which is exactly why one-click checkout exists in real stores. The two-phase gate is for actions where the cost of a wrong execution exceeds the cost of a confirmation. It is not for every action, and it should not be.
The state machine grows. AWAITING_APPROVAL orders need an expiry job, cancellation semantics, and a decision about stock, decrement at approval versus reservation. None of it is hard. All of it is real work, and the ordering matters. Design the expiry job before the second deployment, not after the first complaint.
The gate concentrates the human instead of removing the human. If the one approval becomes a rubber stamp, nothing was gained. The OpenJDK FAQ makes the same point about reviewers in a different way: "reliably distinguishing human-generated content from AI-generated content is impossible." No gate is a guarantee. It is a checkpoint.
Prompt injection is the reason the gate has to be in code. A model that can be steered by text arriving in the conversation must never be the last line of defense for an irreversible action. The state machine does not care what the model was told. That is the entire point.
The Checklist
If you take nothing else from this part, take this list.
- Sort every tool by reversibility. Reversible runs free. Irreversible gets one gate. High-consequence leaves the agent.
- Enforce the gate in code. A state and a token beat any system prompt sentence.
- Show the full context in the approval request. What the agent did, what will happen, the money, the destination, in five lines.
- One approval per action, with an expiry. No stacking, no nagging, no orphaned pending orders.
- Check stock and idempotency inside the approval transaction. The status guard makes replay safe.
- Log every tool call with arguments. The approval token is the join key of the audit trail.
- Keep the prompt rule too. It does not protect anything, but it reduces how often the gate fires.
What Comes Next
Part 6 tested the loop without an LLM. This part put the human at the one decision point that matters. The remaining gap is the one the tests cannot touch: whether the agent is actually good at its job, which is a different question from whether it is bug-free. The next part is building an evaluation harness for the agent, so "is it good" stops being a feeling and becomes a score. Spring AI has real support for this in its evaluation testing reference, and it is the natural sequel to the test suite from Part 6.
Where is the line in your agent today? What runs without asking, and what do you still approve by hand? I read every response.
I write about Java, Spring Boot, and AI agents every week. Subscribe, it's free.
Bookmark this one. The checklist is the part you will re-read the week your agent creates an order nobody approved.
Top comments (0)