AG-UI events.
The communication is typically asynchronous and streaming. As the LangGraph4j agent produces results or requires further input, langgraph4j-copilotkit streams these events back to the frontend in the AG-UI format. CopilotKit then dynamically updates the UI, creating a responsive and interactive user experience.
This architecture decouples the frontend presentation layer from the backend agentic logic, allowing developers to build sophisticated AI agents with LangGraph4j while providing a rich, standardized user interface with CopilotKit.
Example - Approval action (Human-in-the-Loop)
This example demonstrates how to implement a "Human-in-the-Loop" (HITL) approval action using langgraph4j-copilotkit. The user will be asked to approve an action (sendEmail) before it is executed.
Frontend
The frontend uses @copilotkit/react-core and @copilotkit/react-ui to create the chat interface and handle the approval action. The useCopilotAction hook is used to define the sendEmail action. When the action is triggered, it renders a confirmation dialog asking the user for approval.
chatApproval.tsx
"use client";
import { useCopilotAction } from "@copilotkit/react-core";
import { CopilotChat } from "@copilotkit/react-ui";
export function SimpleChatWithApproval() {
useCopilotAction( {
name: "sendEmail",
description: "Sends an email after user approval.",
parameters: [
{ name: "address", type: "string" },
{ name: "subject", type: "string" },
{ name: "body", type: "string" },
],
renderAndWaitForResponse: ({ args, status, respond }) => {
console.debug( "renderAndWaitForResponse", respond, status, args );
if (status === "inProgress") {
return (
<div>
<h2>Sending Email</h2>
<p>Preparing to send email...</p>
</div>
);
}
if (status === "executing") {
return (
<div>
<h2>Confirm Email</h2>
<p>Send email to <b>{args.address}</b> with subject "<b>{args.subject}</b>"?</p>
<p><i>{args.body}</i></p>
<div>
<button onClick={() => respond?.('APPROVED')}>
Approve
</button>
<button onClick={() => respond?.('REJECTED')}>
Cancel
</button>
</div>
</div>
);
}
return <></>
}});
return (
<CopilotChat
instructions={"You are assisting the user as best as you can. Answer in the best way possible given the data you have."}
labels={{
title: "Your Assistant",
initial: "Hi! 👋 How can I assist you today?",
}}
className="w-full"
/>
);
}
LangGraph4j Agent
The backend is a LangGraph4j agent that defines the sendEmail tool. The AgentExecutorEx is configured to require approval for the sendEmail tool. When the tool is about to be executed, the agent will be interrupted, and an approval request will be sent to the frontend.
AGUIAgentExecutor.java:
public class AGUIAgentExecutor extends AGUILangGraphAgent {
// define tools
public static class Tools {
@Tool( description = "Send an email to someone")
public String sendEmail(
@ToolParam( description = "destination address") String to,
@ToolParam( description = "subject of the email") String subject,
@ToolParam( description = "body of the email") String body
) {
// This is a placeholder for the actual implementation
return format("mail sent to %s with subject %s", to, subject);
}
}
@Override
GraphData buildStateGraph() throws GraphStateException {
// Create agent
var agent = AgentExecutorEx.builder()
.chatModel(LLM, true)
.toolsFromObject(new Tools())
.approvalOn( "sendEmail",
(nodeId, state ) ->
InterruptionMetadata.builder( nodeId, state )
.build()
)
.build();
return new GraphData( agent ) ;
}
// invoked on interruption to provide approval information back to the client
@Override
<State extends AgentState> List<Approval> onInterruption(AGUIType.RunAgentInput input, InterruptionMetadata<State> metadata ) {
var messages = metadata.state().value("messages");
.orElseThrow( () -> new IllegalStateException("messages not found into given state"));
return lastOf(messages)
.flatMap(MessageUtil::asAssistantMessage)
.filter(AssistantMessage::hasToolCalls)
.map(AssistantMessage::getToolCalls)
.map( toolCalls ->
toolCalls.stream().map( toolCall -> {
var id = toolCall.id().isBlank() ?
UUID.randomUUID().toString() :
toolCall.id();
return new Approval( id, toolCall.name(), toolCall.arguments() );
}).toList()
)
.orElseGet(List::of);
}
}
This setup allows for a seamless HITL workflow where the user has the final say on critical actions, all orchestrated through the AG-UI protocol and the langgraph4j-copilotkit integration.
Demo
demo
Conclusion
We can state that the future of AI development will be to have a Unified AI Agent Ecosystem.
The convergence of these standards like MCP, A2A, and AG-UI is a step toward this vision.
We consider crucial keep working to improve support of them in LangGraph4j to provide a complete infrastructure for AI Agents to communicate with tools, collaborate with each other, and engage users in a standardized and interoperable manner.
As more frameworks and tools adopt these protocols, we can expect to see a new generation of AI-powered applications that are more flexible, extensible and, with AG-UI, more user-centric than ever before.
Hope this could help your AI Java developing journey, in the meanwhile happy AI coding! 👋
Originally published at https://bsorrentino.github.io on August 21, 2025.