Live Execution Dashboard
The agentensemble-web module embeds a WebSocket server directly into the JVM process.
It broadcasts real-time execution events to any browser connected to the server and
optionally exposes browser-based review gates that replace the console prompt with an
interactive approval panel.
Overview
Section titled “Overview”Add agentensemble-web to your project and attach a WebDashboard to an ensemble via
.webDashboard(). The dashboard:
- Streams
TaskStarted,TaskCompleted,TaskFailed,ToolCalled,DelegationStarted,DelegationCompleted, andDelegationFailedevents to every connected browser client as the ensemble executes. - Optionally serves as the review handler for human-in-the-loop review gates. When a review gate fires, the browser shows an approval panel with the task output and Approve, Edit, and Exit Early controls. The JVM blocks until the browser decision arrives or the review times out.
- Sends a
Hellogreeting on WebSocket connect and aHeartbeatping every 15 seconds to keep connections alive through proxies and load balancers. - Runs entirely in-process. No external server, no Docker container, and no npm command is required.
Dependency
Section titled “Dependency”dependencies { implementation("net.agentensemble:agentensemble-core:2.1.0") implementation("net.agentensemble:agentensemble-web:2.1.0")
// agentensemble-review is included transitively through agentensemble-web. // Declare it explicitly only if you reference review types (ReviewHandler, // ReviewDecision, etc.) directly in your own code. // implementation("net.agentensemble:agentensemble-review:2.1.0")}<dependencies> <dependency> <groupId>net.agentensemble</groupId> <artifactId>agentensemble-core</artifactId> <version>2.1.0</version> </dependency> <dependency> <groupId>net.agentensemble</groupId> <artifactId>agentensemble-web</artifactId> <version>2.1.0</version> </dependency> <!-- Required if using browser-based review gates --> <dependency> <groupId>net.agentensemble</groupId> <artifactId>agentensemble-review</artifactId> <version>2.1.0</version> </dependency></dependencies>Quick Start
Section titled “Quick Start”import net.agentensemble.Ensemble;import net.agentensemble.Task;import net.agentensemble.web.WebDashboard;
EnsembleOutput output = Ensemble.builder() .chatLanguageModel(model) .task(Task.of("Research the latest AI trends")) .task(Task.of("Write a summary report")) .webDashboard(WebDashboard.onPort(7329)) // start server; stream all events .build() .run();Open http://localhost:7329 in a browser before or during execution. Events stream in
real time. The server shuts down automatically when the JVM exits via a registered
shutdown hook.
Builder API
Section titled “Builder API”WebDashboard.onPort(int port)
Section titled “WebDashboard.onPort(int port)”Factory method for the common case. Starts the server on localhost at the given port
with a 5-minute review timeout and CONTINUE on-timeout behavior.
WebDashboard dashboard = WebDashboard.onPort(7329);WebDashboard.builder()
Section titled “WebDashboard.builder()”Full control over all options:
WebDashboard dashboard = WebDashboard.builder() .port(7329) // (required) 0-65535; 0 = OS-assigned ephemeral .host("0.0.0.0") // (optional, default: "localhost") .reviewTimeout(Duration.ofMinutes(10)) // (optional, default: 5 minutes) .onTimeout(OnTimeoutAction.CONTINUE) // (optional, default: CONTINUE) .build();| Option | Type | Default | Description |
|---|---|---|---|
port | int | required | Listening port. 0 lets the OS assign an ephemeral port. |
host | String | "localhost" | Network interface to bind. Use "0.0.0.0" to accept all interfaces. |
reviewTimeout | Duration | Duration.ofMinutes(5) | How long to wait for a browser review decision before applying onTimeout. |
onTimeout | OnTimeoutAction | CONTINUE | What to do when a review times out: CONTINUE, EXIT_EARLY, or FAIL. |
OnTimeoutAction values
Section titled “OnTimeoutAction values”| Value | Effect when review times out |
|---|---|
CONTINUE | Continue as if the human approved. |
EXIT_EARLY | Stop the pipeline. output.getExitReason() returns USER_EXIT_EARLY. |
FAIL | Throw ReviewTimeoutException. |
Wiring to an Ensemble
Section titled “Wiring to an Ensemble”Call .webDashboard(WebDashboard) on the EnsembleBuilder. This single call:
- Starts the embedded server (if not already running).
- Registers the
WebSocketStreamingListeneras anEnsembleListener. - Wires the
WebReviewHandleras the ensemble’sReviewHandler. - Registers a JVM shutdown hook (idempotent across multiple ensembles).
import net.agentensemble.Ensemble;import net.agentensemble.Task;import net.agentensemble.review.Review;import net.agentensemble.review.OnTimeoutAction;import net.agentensemble.web.WebDashboard;
WebDashboard dashboard = WebDashboard.builder() .port(7329) .reviewTimeout(Duration.ofMinutes(5)) .onTimeout(OnTimeoutAction.CONTINUE) .build();
EnsembleOutput output = Ensemble.builder() .chatLanguageModel(model) .task(Task.builder() .description("Draft a press release for the product launch") .expectedOutput("A polished press release") .review(Review.required()) // pause here for browser approval .build()) .task(Task.builder() .description("Translate the press release to Spanish") .expectedOutput("Spanish-language press release") .build()) .webDashboard(dashboard) .build() .run();When the review gate fires after the first task, all connected browsers display an approval
panel. The JVM blocks until one browser submits a decision or the reviewTimeout elapses.
Browser-Based Review
Section titled “Browser-Based Review”When a review gate fires, the server broadcasts a ReviewRequested message to all connected
clients containing:
reviewId— a UUID identifying this specific gate.taskDescription— the task description.taskOutput— the agent’s raw output.timeoutMs— milliseconds until the review times out.
The browser displays an approval panel with three controls:
| Control | Browser sends | Effect |
|---|---|---|
| Approve | {"type":"review_decision","reviewId":"...","decision":"approve"} | Output passed downstream unchanged. |
| Edit | {"type":"review_decision","reviewId":"...","decision":"edit","revisedOutput":"..."} | Revised text used in place of the original output. |
| Exit Early | {"type":"review_decision","reviewId":"...","decision":"exit_early"} | Pipeline stops. output.getExitReason() returns USER_EXIT_EARLY. |
Only the first decision received for a given reviewId is used. Subsequent decisions
from other browser tabs are ignored.
Accessing the Actual Port
Section titled “Accessing the Actual Port”When port(0) is used, the OS assigns a free ephemeral port. Retrieve it after the server
starts:
WebDashboard dashboard = WebDashboard.builder() .port(0) .build();
// Start the server explicitly (or attach to an ensemble, which starts it automatically)dashboard.start();int assignedPort = dashboard.actualPort();System.out.printf("Dashboard running at http://localhost:%d%n", assignedPort);This is useful in tests and CI environments where a fixed port may already be in use.
Lifecycle
Section titled “Lifecycle”The dashboard server follows a simple lifecycle:
// Start explicitly (optional -- .webDashboard() starts it automatically)dashboard.start();
// Check if runningboolean running = dashboard.isRunning(); // true after start()
// Stop explicitly (optional -- the JVM shutdown hook stops it automatically)dashboard.stop();A JVM shutdown hook is registered the first time .start() is called or .webDashboard()
is called on an ensemble builder. The hook is registered only once even if multiple ensembles
share the same WebDashboard instance.
Sharing a Dashboard Across Multiple Ensembles
Section titled “Sharing a Dashboard Across Multiple Ensembles”A single WebDashboard can be attached to multiple ensembles running sequentially or
concurrently. Each .webDashboard() call on a builder checks isRunning() and only
starts the server once:
WebDashboard dashboard = WebDashboard.onPort(7329);
// First ensemble run -- server starts hereEnsemble.builder() .chatLanguageModel(model) .task(Task.of("Research trends")) .webDashboard(dashboard) .build() .run();
// Second ensemble run -- server already running, not restartedEnsemble.builder() .chatLanguageModel(model) .task(Task.of("Write report")) .webDashboard(dashboard) .build() .run();Heartbeat
Section titled “Heartbeat”The server sends a Heartbeat message to every connected client every 15 seconds. This
keeps long-lived WebSocket connections alive through NAT gateways, reverse proxies, and
browser idle timeouts. No action is required on the server side; the browser client should
respond with a Ping message and the server will reply with a Pong.
Origin Validation
Section titled “Origin Validation”The server validates the Origin header of each WebSocket upgrade request using exact
hostname comparison (not substring matching) to prevent subdomain spoofing attacks.
When the server is bound to a loopback address (localhost, 127.0.0.1, ::1, or
[::1]), only origins whose hostname is exactly one of those loopback hostnames are
accepted. DNS resolution is not performed, so custom hostnames that map to loopback
addresses are not allowed. All other origins are rejected with WebSocket close code 1008.
This protects against cross-site WebSocket hijacking (CSRF) from arbitrary web pages.
When the server is bound to any other address (e.g., 0.0.0.0 for all interfaces), all
origins are accepted. Security is delegated to the network layer (VPN, reverse proxy, etc.).
For local development on localhost this requires no configuration. For production
deployments, either bind to a specific non-loopback address and secure at the network
layer, or use a reverse proxy that applies its own origin policy.
WebSocket Protocol
Section titled “WebSocket Protocol”All messages are JSON objects with a "type" discriminator field.
Server-to-client messages
Section titled “Server-to-client messages”| Type | Description |
|---|---|
hello | Sent on WebSocket connect. Contains serverId and serverTime. |
ensemble_started | The ensemble began execution. |
task_started | A task started. Contains taskIndex, totalTasks, taskDescription, agentRole. |
task_completed | A task completed. Contains timing, rawOutput, toolCallCount. |
task_failed | A task failed. Contains errorMessage. |
tool_called | A tool was invoked. Contains toolName, input, output, duration. |
delegation_started | A delegation began. Contains delegationId, workerRole. |
delegation_completed | A delegation completed. Contains delegationId, timing. |
delegation_failed | A delegation failed. Contains delegationId, errorMessage. |
review_requested | A review gate fired. Contains reviewId, taskDescription, taskOutput, timeoutMs. |
review_timed_out | A review gate timed out before a decision was received. Contains reviewId. |
ensemble_completed | The ensemble run finished. Contains exitReason, totalDuration. |
heartbeat | Sent every 15 seconds. Contains serverTime. |
Client-to-server messages
Section titled “Client-to-server messages”| Type | Description |
|---|---|
review_decision | Browser approval decision. Contains reviewId, decision ("approve", "edit", or "exit_early"), and optional revisedOutput. |
ping | Client keepalive. Server responds with a pong. |
EnsembleDashboard Interface
Section titled “EnsembleDashboard Interface”WebDashboard implements net.agentensemble.dashboard.EnsembleDashboard, a stable
interface in agentensemble-core. If you need to swap implementations (e.g., a no-op
stub in unit tests), depend on the interface rather than the concrete class:
import net.agentensemble.dashboard.EnsembleDashboard;
public class MyService { private final EnsembleDashboard dashboard;
public MyService(EnsembleDashboard dashboard) { this.dashboard = dashboard; }
public EnsembleOutput runPipeline(ChatLanguageModel model) { return Ensemble.builder() .chatLanguageModel(model) .task(Task.of("Analyse data")) .webDashboard(dashboard) .build() .run(); }}Example: Ephemeral Port in Tests
Section titled “Example: Ephemeral Port in Tests”@Testvoid dashboardStreamsTaskCompletedEvent() throws Exception { WebDashboard dashboard = WebDashboard.builder() .port(0) // OS assigns a free port .build();
Ensemble.builder() .chatLanguageModel(mockModel) .task(Task.of("Do something")) .webDashboard(dashboard) .build() .run();
int port = dashboard.actualPort(); // Connect a WebSocket test client to ws://localhost:{port} and assert messages}Related Documentation
Section titled “Related Documentation”- Human-in-the-Loop Review Guide — Review timing, policies, and timeout actions
- Human-in-the-Loop Example — Browser-based approval walkthrough
- Live Dashboard Example — Full annotated example
- Design: Live Execution Dashboard — Architecture and protocol specification