# ACP Spring Boot Autoconfiguration
Source: https://lab.pollack.ai/docs/acp-java-sdk/autoconfig
Spring Boot autoconfiguration for the ACP Java SDK — auto-configured clients, agents, and transports with property-driven configuration.
Spring Boot autoconfiguration for the [ACP Java SDK](/projects/acp-java-sdk). Provides auto-configured clients, agents, and transports with property-driven configuration.
## Quick Start
Add the starter dependency:
```xml theme={null}
org.springaicommunityacp-spring-boot-starter0.11.1
```
### Client
Configure the transport in `application.properties` and inject the client:
```properties theme={null}
spring.acp.client.transport.stdio.command=java
spring.acp.client.transport.stdio.args=-jar,my-agent.jar
```
```java theme={null}
@Component
public class MyService {
private final AcpSyncClient client;
public MyService(AcpSyncClient client) {
this.client = client;
}
public void run() {
client.initialize();
var session = client.newSession(new NewSessionRequest(cwd, List.of()));
var response = client.prompt(new PromptRequest(session.sessionId(), content));
}
}
```
### Agent
Annotate a Spring bean with `@AcpAgent` and add handler methods:
```java theme={null}
@Component
@AcpAgent(name = "my-agent", version = "1.0")
public class MyAgent {
@Initialize
public InitializeResponse initialize(InitializeRequest request) {
return InitializeResponse.ok();
}
@NewSession
public NewSessionResponse newSession(NewSessionRequest request) {
return new NewSessionResponse(UUID.randomUUID().toString(), null, null);
}
@Prompt
public PromptResponse prompt(PromptRequest request, SyncPromptContext context) {
context.sendMessage("Hello!");
return PromptResponse.endTurn();
}
}
```
For stdio agents, redirect logging to stderr and keep the JVM alive:
```properties theme={null}
spring.main.banner-mode=off
spring.main.keep-alive=true
```
## Configuration Properties
### Client
| Property | Default | Description |
| ------------------------------------------------------- | ----------- | ---------------------------------------------- |
| `spring.acp.client.request-timeout` | `30s` | Request timeout |
| `spring.acp.client.transport.type` | auto-detect | `stdio` or `websocket` |
| `spring.acp.client.transport.stdio.command` | — | Command to launch agent process |
| `spring.acp.client.transport.stdio.args` | — | Command arguments (comma-separated) |
| `spring.acp.client.transport.stdio.env.*` | — | Environment variables for the process |
| `spring.acp.client.transport.websocket.uri` | — | WebSocket URI (e.g. `ws://localhost:8080/acp`) |
| `spring.acp.client.transport.websocket.connect-timeout` | `10s` | WebSocket connection timeout |
| `spring.acp.client.capabilities.read-text-file` | `true` | Advertise file read capability |
| `spring.acp.client.capabilities.write-text-file` | `true` | Advertise file write capability |
| `spring.acp.client.capabilities.terminal` | `false` | Advertise terminal capability |
### Agent
| Property | Default | Description |
| ---------------------------------- | ------- | ------------------------------ |
| `spring.acp.agent.enabled` | `true` | Enable agent autoconfiguration |
| `spring.acp.agent.request-timeout` | `60s` | Request processing timeout |
| `spring.acp.agent.transport.type` | `stdio` | Transport type |
## Transport Selection
The client transport is selected automatically based on which properties are set:
* Set `spring.acp.client.transport.stdio.command` → stdio transport
* Set `spring.acp.client.transport.websocket.uri` → WebSocket transport
* Set `spring.acp.client.transport.type` → explicit selection (takes precedence)
The agent defaults to stdio transport. Set `spring.acp.agent.enabled=false` to disable.
## Overriding Beans
All auto-configured beans back off when you provide your own. Define a custom `AcpClientTransport`, `AcpSyncClient`, `AcpAsyncClient`, or `AcpAgentTransport` bean and the autoconfiguration will use yours instead.
## Tutorial
Build an ACP agent as a Spring Boot application
Use the autoconfigured ACP client in Spring Boot
## Requirements
* Java 21+
* Spring Boot 4.0+
* ACP Java SDK 0.14.0+
## Resources
* [GitHub Repository](https://github.com/spring-ai-community/acp-autoconfig) — Source code
* [ACP Java SDK](/projects/acp-java-sdk) — The underlying SDK
* [Maven Central](https://central.sonatype.com/artifact/org.springaicommunity/acp-spring-boot-starter) — Published artifacts
# ACP Java SDK
Source: https://lab.pollack.ai/docs/acp-java-sdk/index
A Java SDK for the Agent Client Protocol — build both clients and agents that work with Zed, JetBrains, VS Code, and any ACP-compliant editor.
A pure Java implementation of the [Agent Client Protocol (ACP)](https://agentclientprotocol.com/) specification. Build clients that connect to ACP agents, or build agents that run inside code editors.
## How ACP Works
ACP uses a subprocess model. A **client** (your application, or an editor like Zed) launches an **agent** as a child process and communicates over stdin/stdout using JSON-RPC messages. The protocol has three phases:
1. **Initialize** — client and agent exchange protocol versions and capabilities
2. **Session** — client creates a session with a working directory context
3. **Prompt** — client sends messages, agent streams back responses
This is the same mechanism that Zed, JetBrains, and VS Code use to talk to AI agents. The SDK lets you build either side of that conversation.
## Overview
The ACP Java SDK provides:
* **Client SDK** — connect to and interact with any ACP-compliant agent
* **Agent SDK** — build ACP-compliant agents that work in Zed, JetBrains, and VS Code
* **Test utilities** — in-memory transports for fast, deterministic testing
## Quick Start
### Try it now
The fastest way to see ACP in action is to clone the tutorial and run a module. The client example talks to [Gemini CLI](https://github.com/google-gemini/gemini-cli) (requires `GEMINI_API_KEY`). The agent example runs locally with no API key.
```bash theme={null}
git clone https://github.com/markpollack/acp-java-tutorial.git
cd acp-java-tutorial
# Client: talk to Gemini as an ACP agent
export GEMINI_API_KEY=your-key-here
./mvnw exec:java -pl module-01-first-contact
# Agent: build and run your own (no API key needed)
./mvnw package -pl module-12-echo-agent -q
./mvnw exec:java -pl module-12-echo-agent
```
### Client — Connect to an Agent
This example launches [Gemini CLI](https://github.com/google-gemini/gemini-cli) as an ACP agent subprocess and sends it a prompt. Any CLI tool that speaks ACP over stdin/stdout works here — Gemini CLI is one such tool.
`AgentParameters` builds the command line (`gemini --experimental-acp`). `StdioAcpClientTransport` spawns the process and handles the JSON-RPC framing. The `sessionUpdateConsumer` receives the agent's response text as it streams in — without it, you'd get the stop reason but no visible output.
```java theme={null}
import com.agentclientprotocol.sdk.client.*;
import com.agentclientprotocol.sdk.client.transport.*;
import com.agentclientprotocol.sdk.spec.AcpSchema.*;
import java.util.List;
// Launch Gemini CLI as an ACP agent subprocess
var params = AgentParameters.builder("gemini").arg("--experimental-acp").build();
var transport = new StdioAcpClientTransport(params);
AcpSyncClient client = AcpClient.sync(transport)
.sessionUpdateConsumer(notification -> {
// Print the agent's response as it streams in
if (notification.update() instanceof AgentMessageChunk msg) {
System.out.print(((TextContent) msg.content()).text());
}
})
.build();
client.initialize();
var session = client.newSession(new NewSessionRequest(".", List.of()));
var response = client.prompt(new PromptRequest(
session.sessionId(),
List.of(new TextContent("What is 2+2? Reply with just the number."))
));
System.out.println("\nStop reason: " + response.stopReason());
client.close();
// Output: 4
// Stop reason: END_TURN
```
Run this yourself: [Module 01: First Contact](https://github.com/markpollack/acp-java-tutorial/tree/main/module-01-first-contact) — full source with error handling and setup.
### Agent — Build Your Own
This is the other side of the conversation. When you build an agent, **editors and clients launch your code** as a subprocess and send it JSON-RPC messages over stdin. Your agent handles three request types: initialize, new session, and prompt.
The stdio transport reads from stdin and writes to stdout. `run()` blocks until the client disconnects. The module includes a demo client that launches this agent as a subprocess and exercises it — you'll see `Echo: Hello!` printed when you run it.
```java theme={null}
import com.agentclientprotocol.sdk.annotation.*;
import com.agentclientprotocol.sdk.agent.SyncPromptContext;
import com.agentclientprotocol.sdk.agent.support.AcpAgentSupport;
import com.agentclientprotocol.sdk.spec.AcpSchema.*;
@AcpAgent
class EchoAgent {
@Initialize
InitializeResponse init() {
return InitializeResponse.ok();
}
@NewSession
NewSessionResponse newSession() {
return new NewSessionResponse(UUID.randomUUID().toString(), null, null);
}
@Prompt
PromptResponse prompt(PromptRequest req, SyncPromptContext ctx) {
ctx.sendMessage("Echo: " + req.text());
return PromptResponse.endTurn();
}
}
// Reads JSON-RPC from stdin, writes to stdout — editors connect here
AcpAgentSupport.create(new EchoAgent())
.transport(new StdioAcpAgentTransport())
.run();
```
Run this yourself: [Module 12: Echo Agent](https://github.com/markpollack/acp-java-tutorial/tree/main/module-12-echo-agent) — includes a demo client that launches the agent and sends test prompts. No API key required.
### Adding the SDK to your project
```xml theme={null}
com.agentclientprotocolacp-core0.15.0
```
For annotation-based agents (as shown above), add `acp-agent-support` instead — it includes `acp-core` transitively:
```xml theme={null}
com.agentclientprotocolacp-agent-support0.15.0
```
Version `0.12.0` added session/list, session/close, session/resume, elicitation, session/fork, and session/set\_config\_option. Version `0.14.0` (superseding the never-released 0.13.0) adds logout, session/delete, additional workspace directories, per-chunk message IDs, and the unstable providers/\* configuration methods, promotes session/set\_config\_option to stable, and deprecates the removed session/set\_model API (use session/set\_config\_option with a `"model"` category option). See the [API Reference](/docs/acp-java-sdk/reference/java#installation) for installation.
## Three Agent API Styles
| Style | Entry Point | Best For |
| -------------------- | ---------------------- | --------------------------------------------- |
| **Annotation-based** | `@AcpAgent`, `@Prompt` | Least boilerplate, declarative style |
| **Sync** | `AcpAgent.sync()` | Blocking handlers, plain return values |
| **Async** | `AcpAgent.async()` | Reactive applications, Project Reactor `Mono` |
All three styles produce identical protocol behavior. Choose based on programming preference.
## Documentation
Client API, Agent API (all three styles), protocol types, transports, errors
30-module progressive tutorial from client basics to IDE integration
## Resources
* [GitHub Repository](https://github.com/agentclientprotocol/java-sdk) — Source code
* [ACP Java Tutorial](https://github.com/markpollack/acp-java-tutorial) — 30 hands-on modules
### ACP Ecosystem
* [Agent Client Protocol](https://agentclientprotocol.com/) — Official specification
* [ACP Specification](https://agentclientprotocol.com/protocol/overview) — Protocol details (initialization, sessions, prompt turns)
* [Agents Directory](https://agentclientprotocol.com/overview/agents) — ACP-compliant agents
* [Clients Directory](https://agentclientprotocol.com/overview/clients) — Editors and clients that support ACP
### Other ACP SDKs
* [Kotlin SDK](https://github.com/agentclientprotocol/kotlin-sdk)
* [Python SDK](https://github.com/agentclientprotocol/python-sdk)
* [TypeScript SDK](https://github.com/agentclientprotocol/typescript-sdk)
* [Rust SDK](https://github.com/agentclientprotocol/rust-sdk)
### Editor Documentation
* [Zed — External Agents](https://zed.dev/docs/ai/external-agents)
* [JetBrains — ACP Support](https://www.jetbrains.com/help/ai-assistant/acp.html)
* [VS Code — ACP Extension](https://github.com/formulahendry/vscode-acp)
# Java API Reference
Source: https://lab.pollack.ai/docs/acp-java-sdk/reference/java
Complete API reference for the ACP Java SDK — client, agent, protocol types, transports, and test utilities.
Complete API reference for the ACP Java SDK, covering client, agent (all three styles), protocol types, transports, errors, and test utilities.
***
## Installation
### Maven (0.15.0 — stable)
Core SDK (client + sync/async agent APIs):
```xml theme={null}
com.agentclientprotocolacp-core0.15.0
```
Annotation-based agent support (includes `acp-core` transitively):
```xml theme={null}
com.agentclientprotocolacp-agent-support0.15.0
```
Test utilities:
```xml theme={null}
com.agentclientprotocolacp-test0.15.0test
```
WebSocket server transport for agents:
```xml theme={null}
com.agentclientprotocolacp-websocket-jetty0.15.0
```
### Gradle
```groovy theme={null}
// build.gradle
implementation 'com.agentclientprotocol:acp-core:0.15.0'
// Optional modules
implementation 'com.agentclientprotocol:acp-agent-support:0.15.0'
implementation 'com.agentclientprotocol:acp-websocket-jetty:0.15.0'
testImplementation 'com.agentclientprotocol:acp-test:0.15.0'
```
```kotlin theme={null}
// build.gradle.kts
implementation("com.agentclientprotocol:acp-core:0.15.0")
// Optional modules
implementation("com.agentclientprotocol:acp-agent-support:0.15.0")
implementation("com.agentclientprotocol:acp-websocket-jetty:0.15.0")
testImplementation("com.agentclientprotocol:acp-test:0.15.0")
```
### Snapshot (0.15.0-SNAPSHOT)
For unreleased features, add the snapshot repository and use the snapshot version:
```xml theme={null}
central-snapshotshttps://central.sonatype.com/repository/maven-snapshots/truefalse
```
Then use `0.16.0-SNAPSHOT` in place of `0.15.0` in your dependencies.
***
## Three Agent API Styles
### Quick Comparison
| Feature | Annotation-based | Sync | Async |
| :---------------- | :------------------------------------------- | :----------------------- | :-------------------------------- |
| **Entry Point** | `@AcpAgent` class | `AcpAgent.sync()` | `AcpAgent.async()` |
| **Handler Style** | Annotated methods | Lambda callbacks | Lambda callbacks returning `Mono` |
| **Return Values** | Auto-converted (`String` → `PromptResponse`) | Direct protocol types | `Mono` |
| **Boilerplate** | Lowest | Moderate | Moderate |
| **Best For** | Most applications | Simple blocking handlers | Reactive applications |
| **Runtime** | `AcpAgentSupport` | `AcpSyncAgent` | `AcpAsyncAgent` |
All three produce identical protocol behavior and support the same capabilities.
### When to Use Each
* **Annotation-based** — default choice. Least boilerplate, auto-converts return types, supports interceptors and custom argument resolvers.
* **Sync** — when you want explicit control over every handler without annotations. Blocking void methods for sending updates.
* **Async** — when your agent needs non-blocking I/O. Uses Project Reactor `Mono` for composable async chains.
***
## Client API
### `AcpClient` — Factory
| Method | Return Type | Description |
| :----------------- | :----------------------- | :----------------------------- |
| `sync(transport)` | `AcpSyncClient.Builder` | Create blocking client builder |
| `async(transport)` | `AcpAsyncClient.Builder` | Create reactive client builder |
### `AcpSyncClient` — Blocking Client
| Method | Return Type | Description |
| :-------------------------------- | :------------------------------- | :---------------------------------------------------------------------- |
| `initialize()` | `InitializeResponse` | Protocol handshake with defaults |
| `initialize(request)` | `InitializeResponse` | Handshake with custom capabilities |
| `authenticate(request)` | `AuthenticateResponse` | Authenticate with a method ID |
| `logout(request)` | `LogoutResponse` | Clear stored credentials *(0.14.0)* |
| `newSession(request)` | `NewSessionResponse` | Create a new session |
| `loadSession(request)` | `LoadSessionResponse` | Resume an existing session (replays history) |
| `listSessions(request)` | `ListSessionsResponse` | List sessions, optional cwd filter *(0.12.0)* |
| `resumeSession(request)` | `ResumeSessionResponse` | Reconnect to session without history replay *(0.12.0)* |
| `closeSession(request)` | `CloseSessionResponse` | Close session and free resources *(0.12.0)* |
| `deleteSession(request)` | `DeleteSessionResponse` | Permanently delete a stored session *(0.14.0)* |
| `forkSession(request)` | `ForkSessionResponse` | Fork a session into a new branch *(0.12.0, unstable)* |
| `setSessionConfigOption(request)` | `SetSessionConfigOptionResponse` | Set a session config value *(0.12.0)* |
| `listProviders(request)` | `ListProvidersResponse` | List configurable model/backend providers *(0.14.0, unstable)* |
| `setProvider(request)` | `SetProviderResponse` | Configure a provider (protocol, base URL, headers) *(0.14.0, unstable)* |
| `disableProvider(request)` | `DisableProviderResponse` | Disable a provider by id *(0.14.0, unstable)* |
| `prompt(request)` | `PromptResponse` | Send prompt, block until response |
| `cancel(notification)` | `void` | Cancel current prompt (fire-and-forget) |
| `getAgentCapabilities()` | `NegotiatedCapabilities` | Capabilities reported by agent |
| `close()` | `void` | Close connection |
### Builder Configuration
```java theme={null}
AcpSyncClient client = AcpClient.sync(transport)
.sessionUpdateConsumer(notification -> {
// Handle streaming updates during prompt()
})
.readTextFileHandler(req -> {
// Agent requests a file read
return new ReadTextFileResponse(Files.readString(Path.of(req.path())));
})
.writeTextFileHandler(req -> {
// Agent requests a file write
Files.writeString(Path.of(req.path()), req.content());
return new WriteTextFileResponse();
})
.requestPermissionHandler(req -> {
// Agent requests permission
return new RequestPermissionResponse(req.options().getFirst().id());
})
.createElicitationHandler(req -> {
// Agent requests structured user input (unstable)
return CreateElicitationResponse.accept(Map.of("choice", "option-a"));
})
.build();
```
### Example — Complete client lifecycle
This launches Gemini CLI as an ACP agent subprocess and sends it a prompt. `AgentParameters` builds the command line; `StdioAcpClientTransport` spawns the process and handles JSON-RPC framing over stdin/stdout.
```java theme={null}
// Launch "gemini --experimental-acp" as a subprocess
var params = AgentParameters.builder("gemini")
.arg("--experimental-acp")
.build();
var transport = new StdioAcpClientTransport(params);
AcpSyncClient client = AcpClient.sync(transport)
.sessionUpdateConsumer(notification -> {
var update = notification.update();
if (update instanceof AgentMessageChunk msg) {
System.out.print(((TextContent) msg.content()).text());
}
})
.build();
client.initialize();
var session = client.newSession(new NewSessionRequest("/workspace", List.of()));
var response = client.prompt(new PromptRequest(
session.sessionId(),
List.of(new TextContent("Hello, world!"))
));
System.out.println("Stop reason: " + response.stopReason());
client.close();
```
***
## Agent API — Annotation-Based
The `acp-agent-support` module provides a declarative programming model using annotations.
### Annotations
#### Class-Level
| Annotation | Description |
| ----------- | ------------------------------------------------------------------------ |
| `@AcpAgent` | Marks a class as an ACP agent. Optional `name` and `version` attributes. |
#### Handler Methods
| Annotation | JSON-RPC Method | Description |
| ------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------ |
| `@Initialize` | `initialize` | Protocol initialization and capability negotiation |
| `@Logout` | `logout` | Clears stored credentials *(0.14.0)* |
| `@NewSession` | `session/new` | Creates a new agent session |
| `@LoadSession` | `session/load` | Loads an existing session by ID (replays history) |
| `@ListSessions` | `session/list` | Lists sessions with optional cwd filter *(0.12.0)* |
| `@ResumeSession` | `session/resume` | Reconnects to session without history replay *(0.12.0)* |
| `@CloseSession` | `session/close` | Closes session and frees resources *(0.12.0)* |
| `@DeleteSession` | `session/delete` | Permanently deletes a stored session *(0.14.0)* |
| `@ForkSession` | `session/fork` | Forks a session into a new branch *(0.12.0, unstable)* |
| `@SetSessionConfigOption` | `session/set_config_option` | Sets a session config value *(0.12.0)* |
| `@ListProviders` | `providers/list` | Lists configurable providers *(0.14.0, unstable)* |
| `@SetProvider` | `providers/set` | Configures a provider *(0.14.0, unstable)* |
| `@DisableProvider` | `providers/disable` | Disables a provider by id *(0.14.0, unstable)* |
| `@Prompt` | `session/prompt` | Handles user prompts |
| `@SetSessionMode` | `session/set_mode` | Changes operational mode |
| `@SetSessionModel` | `session/set_model` | **Deprecated** — removed from the spec; use `@SetSessionConfigOption` with a `"model"` category option |
| `@Cancel` | `session/cancel` | Cancellation notification (fire-and-forget) |
> **Deprecated: the session-model API (0.14.0).** `session/set_model` and the related types
> (`@SetSessionModel`, `SetSessionModelRequest`/`Response`, `SessionModelState`, `ModelInfo`, and the
> `models` field on session responses) were removed from the ACP spec in June 2026 and are marked
> `@Deprecated(forRemoval = true)`. They still work for now but will be removed in a future release.
> Expose model selection through `session/set_config_option` instead: advertise a `select` config
> option whose `category` is `"model"`, and switch models with `setSessionConfigOption(...)`. This is
> the same mechanism used for session modes (`category: "mode"`) and reasoning level
> (`category: "thought_level"`).
#### Parameter Annotations
| Annotation | Description |
| --------------- | ------------------------------------------ |
| `@SessionId` | Injects the current session ID as `String` |
| `@SessionState` | Injects session-specific state |
### Flexible Method Signatures
Handler methods support flexible parameter resolution:
```java theme={null}
// Minimal
@Initialize
InitializeResponse init() { return InitializeResponse.ok(); }
// With request
@Prompt
PromptResponse answer(PromptRequest req) { ... }
// With context
@Prompt
PromptResponse answer(PromptRequest req, SyncPromptContext ctx) { ... }
// Auto-converted return types
@Prompt
String simpleAnswer(PromptRequest req) { ... } // → PromptResponse.text(value)
@Prompt
void streaming(PromptRequest req, SyncPromptContext ctx) { ... } // → endTurn()
```
### Return Value Handling
| Return Type | Conversion |
| ---------------------- | ----------------------------------------- |
| Protocol response type | Passed through directly |
| `String` | Converted to `PromptResponse.text(value)` |
| `void` | Converted to `PromptResponse.endTurn()` |
| `Mono` | Unwrapped and returned |
### `SyncPromptContext`
Available in `@Prompt` handlers. Provides blocking methods for agent-client interaction:
```java theme={null}
@Prompt
PromptResponse handle(PromptRequest req, SyncPromptContext ctx) {
// Session info
String sessionId = ctx.getSessionId();
NegotiatedCapabilities caps = ctx.getClientCapabilities();
// Messages and thoughts
ctx.sendMessage("Working on it...");
ctx.sendThought("Let me analyze this...");
// Tag streamed chunks with a message ID (0.14.0) — chunks sharing an id
// form one logical message; a new id starts a new message
ctx.sendMessage("First part...", "msg-1");
ctx.sendThought("Reasoning...", "msg-1");
// File operations (requires client capabilities)
String content = ctx.readFile("/path/to/file.txt");
ctx.writeFile("/path/to/output.txt", "content");
Optional maybe = ctx.tryReadFile("/path/to/file.txt");
// Permissions
boolean allowed = ctx.askPermission("Delete files in /tmp?");
String choice = ctx.askChoice("Which format?", "JSON", "XML", "YAML");
// Terminal execution (requires client capabilities)
CommandResult result = ctx.execute("ls", "-la");
return PromptResponse.endTurn();
}
```
### `AcpAgentSupport` — Bootstrap
```java theme={null}
AcpAgentSupport.create(new MyAgent())
.transport(StdioAcpAgentTransport.create())
.requestTimeout(Duration.ofSeconds(60)) // Optional
.interceptor(new LoggingInterceptor()) // Optional
.argumentResolver(new UserResolver()) // Optional
.returnValueHandler(new FutureHandler()) // Optional
.run(); // Blocks until client disconnects
```
### Interceptors
Cross-cutting concerns like logging, metrics, or error handling:
```java theme={null}
public class LoggingInterceptor implements AcpInterceptor {
@Override
public boolean preInvoke(AcpInvocationContext context) {
log.info("Invoking: {}", context.getAcpMethod());
return true; // Continue processing
}
@Override
public Object postInvoke(AcpInvocationContext context, Object result) {
log.info("Result: {}", result);
return result;
}
@Override
public int getOrder() { return 0; } // Lower values execute first
}
```
### Example — Complete annotation-based agent
```java theme={null}
@AcpAgent(name = "code-assistant", version = "1.0.0")
class CodeAssistant {
private final Map> sessionHistory = new ConcurrentHashMap<>();
@Initialize
InitializeResponse init() { return InitializeResponse.ok(); }
@NewSession
NewSessionResponse newSession(NewSessionRequest req) {
String sessionId = UUID.randomUUID().toString();
sessionHistory.put(sessionId, new ArrayList<>());
return new NewSessionResponse(sessionId, List.of(), List.of());
}
@Prompt
PromptResponse prompt(PromptRequest req, SyncPromptContext ctx) {
ctx.sendThought("Analyzing the code...");
if (ctx.getClientCapabilities().supportsReadTextFile()) {
ctx.sendMessage("I can access files if needed.");
}
ctx.sendMessage("Here's my analysis...");
return PromptResponse.endTurn();
}
@Cancel
void onCancel(CancelNotification notification, @SessionId String sessionId) {
sessionHistory.remove(sessionId);
}
}
```
***
## Agent API — Sync (Builder)
Blocking handlers with plain return values. No annotations.
### Builder Methods
| Method | Description |
| :--------------------------------------- | :------------------------------------------------------- |
| `initializeHandler(handler)` | Handle `initialize` requests |
| `authenticateHandler(handler)` | Handle `authenticate` requests |
| `logoutHandler(handler)` | Handle `logout` requests *(0.14.0)* |
| `newSessionHandler(handler)` | Handle `session/new` requests |
| `loadSessionHandler(handler)` | Handle `session/load` requests |
| `listSessionsHandler(handler)` | Handle `session/list` requests *(0.12.0)* |
| `resumeSessionHandler(handler)` | Handle `session/resume` requests *(0.12.0)* |
| `closeSessionHandler(handler)` | Handle `session/close` requests *(0.12.0)* |
| `deleteSessionHandler(handler)` | Handle `session/delete` requests *(0.14.0)* |
| `forkSessionHandler(handler)` | Handle `session/fork` requests *(0.12.0, unstable)* |
| `setSessionConfigOptionHandler(handler)` | Handle `session/set_config_option` requests *(0.12.0)* |
| `listProvidersHandler(handler)` | Handle `providers/list` requests *(0.14.0, unstable)* |
| `setProviderHandler(handler)` | Handle `providers/set` requests *(0.14.0, unstable)* |
| `disableProviderHandler(handler)` | Handle `providers/disable` requests *(0.14.0, unstable)* |
| `promptHandler(handler)` | Handle `session/prompt` requests |
| `cancelHandler(handler)` | Handle `session/cancel` notifications |
### Example
```java theme={null}
AcpSyncAgent agent = AcpAgent.sync(transport)
.initializeHandler(req -> InitializeResponse.ok())
.newSessionHandler(req ->
new NewSessionResponse(UUID.randomUUID().toString(), null, null))
.promptHandler((req, context) -> {
context.sendMessage("Hello!");
return PromptResponse.endTurn();
})
.build();
agent.run(); // Blocks until client disconnects
```
### Prompt Handler Context
The `context` parameter in `promptHandler` provides:
| Method | Description |
| :------------------------------ | :------------------------ |
| `getSessionId()` | Current session ID |
| `sendMessage(text)` | Send `AgentMessageChunk` |
| `sendThought(text)` | Send `AgentThoughtChunk` |
| `sendUpdate(sessionId, update)` | Send any `SessionUpdate` |
| `readFile(path, offset, limit)` | Read file from client |
| `writeFile(path, content)` | Write file on client |
| `requestPermission(request)` | Ask client for permission |
| `getClientCapabilities()` | Check client capabilities |
***
## Agent API — Async (Builder)
Reactive handlers returning `Mono`. Uses Project Reactor.
### Example
```java theme={null}
AcpAsyncAgent agent = AcpAgent.async(transport)
.initializeHandler(req ->
Mono.just(InitializeResponse.ok()))
.newSessionHandler(req ->
Mono.just(new NewSessionResponse(
UUID.randomUUID().toString(), null, null)))
.promptHandler((req, context) ->
context.sendMessage("Hello!")
.then(Mono.just(PromptResponse.endTurn())))
.build();
agent.start().then(agent.awaitTermination()).block();
```
The async context's `sendMessage()`, `sendUpdate()`, etc. return `Mono`, composable with `.then()` and `.flatMap()`.
***
## Convenience Methods vs Full API
The SDK provides convenience methods that cover the most common operations. Use these by default — they produce cleaner code and handle the protocol details for you.
### When convenience methods are enough (\~80% of cases)
```java theme={null}
// Response factories
InitializeResponse.ok() // default capabilities
InitializeResponse.ok(customCapabilities) // custom capabilities
PromptResponse.endTurn() // stop reason END_TURN
PromptResponse.text("response") // message + endTurn in one call
// Sending updates (on SyncPromptContext or async equivalent)
context.sendMessage("Hello"); // AgentMessageChunk with TextContent
context.sendThought("Analyzing..."); // AgentThoughtChunk with TextContent
// File operations
String content = context.readFile("pom.xml"); // read with defaults
context.writeFile("output.txt", "content"); // write file
// Terminal
CommandResult result = context.execute("ls", "-la");
// Permissions
boolean ok = context.askPermission("Delete temp files?");
String choice = context.askChoice("Format?", "JSON", "XML", "YAML");
```
### When to use the full API (\~20% of cases)
Drop to the full API when you need control that convenience methods don't expose:
```java theme={null}
// Custom AgentCapabilities with specific MCP and prompt settings
var caps = new AgentCapabilities(
true, // loadSession
new McpCapabilities(true, true), // HTTP + SSE
new PromptCapabilities(true, false, true) // audio, embeddedContext, image
);
return InitializeResponse.ok(caps);
// Send non-text update types (Plan, ToolCall, AvailableCommandsUpdate, etc.)
context.sendUpdate(sessionId, new Plan("plan", List.of(
new PlanEntry("Analyze code", PlanEntryPriority.HIGH, PlanEntryStatus.IN_PROGRESS),
new PlanEntry("Generate tests", PlanEntryPriority.MEDIUM, PlanEntryStatus.PENDING)
)));
context.sendUpdate(sessionId, new ToolCall("tool_call",
"search-1", "code-search", ToolKind.SEARCH, ToolCallStatus.IN_PROGRESS,
null, null, null, null, null));
// Read file with offset and line limit
var response = context.readTextFile(
new ReadTextFileRequest(sessionId, "large-file.txt", 100, 50));
// Request permission with custom options
var permResponse = context.requestPermission(new RequestPermissionRequest(
sessionId, "Run deployment script?", List.of(
new PermissionOption("allow-once", "Allow once", PermissionOptionKind.ALLOW_ONCE),
new PermissionOption("always", "Always allow", PermissionOptionKind.ALLOW_ALWAYS),
new PermissionOption("reject", "Reject", PermissionOptionKind.REJECT_ONCE)
)));
```
The convenience methods are wrappers around the full API — they call the same underlying protocol methods. You can mix and match freely within a single handler.
***
## Protocol Types
All protocol types are defined in `AcpSchema` as Java records.
### Request/Response Types
| Type | Fields |
| :-------------------------------- | :--------------------------------------------------------------------------------------------------- |
| `InitializeRequest` | `protocolVersion`, `clientCapabilities` |
| `InitializeResponse` | `protocolVersion`, `agentCapabilities`, `authMethods`, `agentInfo` |
| `AuthenticateRequest` | `methodId` |
| `AuthenticateResponse` | *(empty)* |
| `LogoutRequest` | *(empty)* *(0.14.0)* |
| `LogoutResponse` | *(empty)* *(0.14.0)* |
| `NewSessionRequest` | `cwd`, `mcpServers`, `additionalDirectories` *(`additionalDirectories` 0.14.0)* |
| `NewSessionResponse` | `sessionId`, `modes`, ~~`models`~~ *(`models` deprecated — see below)* |
| `LoadSessionRequest` | `sessionId`, `cwd`, `mcpServers`, `additionalDirectories` *(`additionalDirectories` 0.14.0)* |
| `LoadSessionResponse` | `modes`, ~~`models`~~ *(deprecated)* |
| `ListSessionsRequest` | `cwd` (optional filter), `cursor` (pagination) *(0.12.0)* |
| `ListSessionsResponse` | `sessions` (list of `SessionInfo`), `nextCursor` *(0.12.0)* |
| `ResumeSessionRequest` | `sessionId`, `cwd`, `mcpServers`, `additionalDirectories` *(0.12.0; `additionalDirectories` 0.14.0)* |
| `ResumeSessionResponse` | `modes`, ~~`models`~~ *(0.12.0; `models` deprecated)* |
| `CloseSessionRequest` | `sessionId` *(0.12.0)* |
| `CloseSessionResponse` | *(empty)* *(0.12.0)* |
| `DeleteSessionRequest` | `sessionId` *(0.14.0)* |
| `DeleteSessionResponse` | *(empty)* *(0.14.0)* |
| `ForkSessionRequest` | `sessionId`, `cwd`, `mcpServers`, `additionalDirectories` *(0.12.0, unstable)* |
| `ForkSessionResponse` | `sessionId`, `modes`, ~~`models`~~, `configOptions` *(0.12.0, unstable; `models` deprecated)* |
| `SetSessionConfigOptionRequest` | `sessionId`, `configId`, `value`, `type` *(0.12.0)* |
| `SetSessionConfigOptionResponse` | `configOptions` (full config state) *(0.12.0)* |
| `ListProvidersRequest` | *(empty)* *(0.14.0, unstable)* |
| `ListProvidersResponse` | `providers` (list of `ProviderInfo`) *(0.14.0, unstable)* |
| `SetProviderRequest` | `id`, `apiType`, `baseUrl`, `headers` *(0.14.0, unstable)* |
| `SetProviderResponse` | *(empty)* *(0.14.0, unstable)* |
| `DisableProviderRequest` | `id` *(0.14.0, unstable)* |
| `DisableProviderResponse` | *(empty)* *(0.14.0, unstable)* |
| `CreateElicitationRequest` | `sessionId`, `message`, `mode`, `requestedSchema` *(0.12.0, unstable)* |
| `CreateElicitationResponse` | `action` (accept/decline/cancel), `content` *(0.12.0, unstable)* |
| `CompleteElicitationNotification` | `elicitationId` *(0.12.0, unstable)* |
| `PromptRequest` | `sessionId`, `prompt` (list of `ContentBlock`) |
| `PromptResponse` | `stopReason` |
| `CancelNotification` | `sessionId` |
### Session Types *(0.12.0)*
| Type | Fields | Description |
| :-------------------- | :------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------- |
| `SessionInfo` | `sessionId`, `cwd`, `title`, `updatedAt`, `additionalDirectories` | Session metadata returned by `session/list` *(`additionalDirectories` 0.14.0)* |
| `SessionCapabilities` | `list`, `close`, `resume`, `delete`, `additionalDirectories`, `fork` | Nested capability flags on `AgentCapabilities` *(`delete`, `additionalDirectories` 0.14.0; `fork` unstable)* |
### Config Option Types *(0.12.0)*
`session/set_config_option` is stable. The `select` variant is the stable shape; `boolean` is an SDK extension.
| Type | Description |
| :-------------------------- | :--------------------------------------------------------------------- |
| `SessionConfigOption` | Polymorphic: `SessionConfigSelect` or `SessionConfigBoolean` |
| `SessionConfigSelect` | Select-type config with `currentValue`, `category`, and `options` list |
| `SessionConfigBoolean` | Boolean toggle with `currentValue` *(unstable extension)* |
| `SessionConfigSelectOption` | A named value within a select config (`value`, `name`) |
| `ConfigOptionUpdate` | SessionUpdate variant for agent-pushed config changes |
### Provider Types *(0.14.0, unstable)*
Model/backend routing configuration. The agent advertises support with a `providers` capability; the
client manages providers via `listProviders` / `setProvider` / `disableProvider`.
| Type | Description |
| :---------------------- | :------------------------------------------------------------------------------- |
| `ProviderInfo` | A configurable provider: `id`, `supported` (protocol ids), `required`, `current` |
| `ProviderCurrentConfig` | Current effective routing: `apiType`, `baseUrl` |
| `ProvidersCapabilities` | Agent capability marker (presence = supported), on `AgentCapabilities` |
`apiType` / `supported` use well-known `LlmProtocol` ids (`anthropic`, `openai`, `azure`, `vertex`, `bedrock`) or a custom string, modeled as `String` in the SDK.
### Elicitation Types *(0.12.0, unstable)*
| Type | Description |
| :-------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------- |
| `ElicitationSchema` | JSON Schema describing form fields (`properties`, `required`) |
| `ElicitationPropertySchema` | Polymorphic: `StringPropertySchema`, `NumberPropertySchema`, `IntegerPropertySchema`, `BooleanPropertySchema`, `MultiSelectPropertySchema` |
| `EnumOption` | Named value for select/multi-select (`const`, `title`) |
| `ElicitationCapabilities` | Client capability for form and/or URL elicitation |
| `ElicitationAction` | Enum: `ACCEPT`, `DECLINE`, `CANCEL` |
### Content Types
| Type | Description |
| :------------- | :----------------------------- |
| `TextContent` | Text content with `text` field |
| `ImageContent` | Image content (base64 or URL) |
### Session Update Types
| Type | Description |
| :--------------------------- | :-------------------------------------------------------- |
| `UserMessageChunk` | Incremental user message text (optional `messageId`) |
| `AgentMessageChunk` | Incremental response text (optional `messageId` — 0.14.0) |
| `AgentThoughtChunk` | Agent thinking process (optional `messageId` — 0.14.0) |
| `ToolCall` | Tool execution start |
| `ToolCallUpdateNotification` | Tool progress update |
| `Plan` | Agent's planned steps |
| `AvailableCommandsUpdate` | Advertised slash commands |
| `CurrentModeUpdate` | Agent mode change |
| `UsageUpdate` | Context window and cost usage |
| `ConfigOptionUpdate` | Session config option changes |
Content chunks (`UserMessageChunk`, `AgentMessageChunk`, `AgentThoughtChunk`) carry an optional `messageId`: chunks sharing the same id belong to one logical message, and a change in id starts a new message *(0.14.0)*.
### Stop Reasons
| Value | Description |
| :----------- | :------------------------ |
| `END_TURN` | Agent finished responding |
| `MAX_TOKENS` | Token limit reached |
| `REFUSAL` | Agent refused the request |
| `CANCELLED` | Prompt was cancelled |
### Convenience Methods
```java theme={null}
// Static factory methods
InitializeResponse.ok()
PromptResponse.endTurn()
PromptResponse.text("response")
```
***
## Capabilities
### Client Capabilities
Advertised during `initialize`:
```java theme={null}
client.initialize(new InitializeRequest(1,
new ClientCapabilities(
new FileSystemCapability(true, true), // read, write
true // terminalExecution
)));
```
### `NegotiatedCapabilities`
Check capabilities before using them:
```java theme={null}
NegotiatedCapabilities caps = context.getClientCapabilities();
if (caps.supportsReadTextFile()) {
String content = context.readFile("file.txt");
}
if (caps.supportsWriteTextFile()) {
context.writeFile("output.txt", "content");
}
```
Or use `require` methods that throw `AcpCapabilityException` if unsupported:
```java theme={null}
caps.requireWriteTextFile();
context.writeFile("output.txt", "content");
```
### Session Capabilities *(0.12.0)*
Agents advertise session management support via `SessionCapabilities`. The
`NegotiatedCapabilities` accessors are: `supportsListSessions()`, `supportsCloseSession()`,
`supportsResumeSession()`, `supportsDeleteSession()` *(0.14.0)*,
`supportsAdditionalDirectories()` *(0.14.0)*, and `supportsForkSession()` *(unstable)* — each with a
matching `require*()` that throws `AcpCapabilityException`.
```java theme={null}
NegotiatedCapabilities caps = client.getAgentCapabilities();
if (caps.supportsListSessions()) {
client.listSessions(new ListSessionsRequest(null));
}
if (caps.supportsDeleteSession()) {
client.deleteSession(new DeleteSessionRequest(sessionId));
}
if (caps.supportsAdditionalDirectories()) {
client.newSession(new NewSessionRequest(cwd, List.of(), List.of("/extra/dir")));
}
```
### Elicitation Capabilities *(0.12.0, unstable)*
Clients advertise elicitation support during initialization:
```java theme={null}
// Client: advertise form-mode elicitation support
var caps = new ClientCapabilities(
new FileSystemCapability(), false,
new ElicitationCapabilities(), null);
client.initialize(new InitializeRequest(1, caps));
```
```java theme={null}
// Agent: check before sending elicitation
if (context.getClientCapabilities().supportsElicitation()) {
var response = context.createElicitation(
CreateElicitationRequest.form(sessionId, "Pick one:", schema));
}
```
### `@UnstableAcpApi`
APIs marked `@UnstableAcpApi` correspond to protocol elements in `schema.unstable.json`. They are public and functional but may change in any minor release. When the protocol element stabilizes, the annotation is removed (compatible change). See [Versioning](#versioning) for the full policy.
IntelliJ users can configure the *Unstable API Usage* inspection (*Settings > Inspections > JVM languages*) to flag usages.
***
## Transports
| Transport | Client Class | Agent Class | Module |
| ------------- | ----------------------------- | ---------------------------- | ------------------------------ |
| **Stdio** | `StdioAcpClientTransport` | `StdioAcpAgentTransport` | acp-core |
| **WebSocket** | `WebSocketAcpClientTransport` | `WebSocketAcpAgentTransport` | acp-core / acp-websocket-jetty |
| **In-Memory** | via `InMemoryTransportPair` | via `InMemoryTransportPair` | acp-test |
### Stdio Transport
The default transport. The client launches the agent as a subprocess and communicates via JSON-RPC over stdin/stdout. This is the same mechanism Zed, JetBrains, and VS Code use to talk to agents.
**Client side** — `AgentParameters` specifies the command to launch. Any executable that speaks ACP over stdin/stdout works (Gemini CLI, your own agent JAR, etc.):
```java theme={null}
var params = AgentParameters.builder("gemini")
.arg("--experimental-acp")
.build();
var transport = new StdioAcpClientTransport(params);
```
**Agent side** — reads JSON-RPC from stdin, writes responses to stdout. The agent doesn't need to know what launched it:
```java theme={null}
var transport = new StdioAcpAgentTransport();
```
### WebSocket Transport
For network-based communication.
**Client (JDK-native, no extra dependencies):**
```java theme={null}
var transport = new WebSocketAcpClientTransport(
URI.create("ws://localhost:8080/acp"),
AcpJsonMapper.createDefault()
);
```
**Agent (requires acp-websocket-jetty):**
```java theme={null}
var transport = new WebSocketAcpAgentTransport(
8080, "/acp", AcpJsonMapper.createDefault()
);
```
### In-Memory Transport
For testing. No subprocess or network I/O.
```java theme={null}
var pair = InMemoryTransportPair.create();
// pair.clientTransport() — for client
// pair.agentTransport() — for agent
// pair.closeGracefully() — cleanup
```
***
## Errors
### Exception Hierarchy
| Exception | Description |
| :----------------------- | :-------------------------------------------- |
| `AcpProtocolException` | JSON-RPC protocol error with code and message |
| `AcpCapabilityException` | Tried to use an unsupported capability |
| `AcpConnectionException` | Transport-level connection failure |
### Error Codes
```java theme={null}
try {
client.prompt(request);
} catch (AcpProtocolException e) {
if (e.isConcurrentPrompt()) {
// Another prompt is already in progress
} else if (e.isMethodNotFound()) {
// Agent doesn't support this method
}
System.err.println("Error " + e.getCode() + ": " + e.getMessage());
} catch (AcpCapabilityException e) {
System.err.println("Not supported: " + e.getCapability());
}
```
### Agent-Side Error Handling
Throw `AcpProtocolException` from handlers to send structured errors to clients:
```java theme={null}
.promptHandler((req, context) -> {
if (req.prompt().isEmpty()) {
throw new AcpProtocolException(
AcpErrorCodes.INVALID_PARAMS, "Empty prompt");
}
// ...
})
```
***
## Test Utilities
The `acp-test` module provides utilities for testing without subprocesses.
### `InMemoryTransportPair`
```java theme={null}
var pair = InMemoryTransportPair.create();
// Wire up agent
AcpAsyncAgent agent = AcpAgent.async(pair.agentTransport())
.initializeHandler(req -> Mono.just(InitializeResponse.ok()))
.newSessionHandler(req -> Mono.just(
new NewSessionResponse(UUID.randomUUID().toString(), null, null)))
.promptHandler((req, context) ->
context.sendMessage("response")
.then(Mono.just(PromptResponse.endTurn())))
.build();
agent.start().subscribe();
// Wire up client
AcpSyncClient client = AcpClient.sync(pair.clientTransport()).build();
client.initialize();
// ... test ...
pair.closeGracefully().block();
```
***
## Packages
| Package | Description |
| ------------------------------------------- | ------------------------------------------------------------- |
| `com.agentclientprotocol.sdk.spec` | Protocol types (`AcpSchema.*`) |
| `com.agentclientprotocol.sdk.client` | Client SDK (`AcpClient`, `AcpAsyncClient`, `AcpSyncClient`) |
| `com.agentclientprotocol.sdk.agent` | Agent SDK (`AcpAgent`, `AcpAsyncAgent`, `AcpSyncAgent`) |
| `com.agentclientprotocol.sdk.agent.support` | Annotation-based agent runtime (`AcpAgentSupport`) |
| `com.agentclientprotocol.sdk.annotation` | Agent annotations (`@AcpAgent`, `@Prompt`, etc.) |
| `com.agentclientprotocol.sdk.capabilities` | Capability negotiation (`NegotiatedCapabilities`) |
| `com.agentclientprotocol.sdk.error` | Exceptions (`AcpProtocolException`, `AcpCapabilityException`) |
| `com.agentclientprotocol.sdk.test` | Test utilities (`InMemoryTransportPair`) |
## Maven Artifacts
| Artifact | Description |
| --------------------- | -------------------------------------------------------------------- |
| `acp-core` | Client and Agent SDKs, stdio and WebSocket client transports |
| `acp-annotations` | `@AcpAgent`, `@Prompt`, and other annotations |
| `acp-agent-support` | Annotation-based agent runtime (includes acp-annotations + acp-core) |
| `acp-test` | In-memory transport and test utilities |
| `acp-websocket-jetty` | Jetty-based WebSocket server transport for agents |
***
## See Also
* [ACP Java SDK GitHub](https://github.com/agentclientprotocol/java-sdk) — Source code
* [ACP Java Tutorial](https://github.com/markpollack/acp-java-tutorial) — 30 hands-on modules
* [Agent Client Protocol](https://agentclientprotocol.com/) — Official specification
# 01 first contact
Source: https://lab.pollack.ai/docs/acp-java-sdk/tutorial/01-first-contact
# Module 01: First Contact
Your first ACP client — launch an agent as a subprocess and send it a prompt.
## What You'll Learn
* How ACP communication works (subprocess + stdin/stdout JSON-RPC)
* Configuring agent process parameters with `AgentParameters`
* Registering a `sessionUpdateConsumer` to see the agent's response
* The three-phase lifecycle: initialize → newSession → prompt
## Prerequisites
1. **[Gemini CLI](https://github.com/google-gemini/gemini-cli) with ACP support** — the tutorial uses Gemini as a real ACP agent. Your client will launch it as a subprocess and talk to it over stdin/stdout.
```bash theme={null}
gemini --experimental-acp --version
```
2. **API key**
```bash theme={null}
export GEMINI_API_KEY=your-key-here
```
3. **Java 17 or later**
## The Code
The client launches `gemini --experimental-acp` as a child process. `AgentParameters` builds the command line. `StdioAcpClientTransport` spawns the process and handles JSON-RPC message framing over its stdin/stdout.
The `sessionUpdateConsumer` is how you see the agent's response. During `prompt()`, the agent streams back `AgentMessageChunk` updates containing the response text. Without a consumer, the prompt completes but you only get the stop reason — not the actual answer.
From there, ACP follows a three-phase lifecycle: initialize the connection, create a session (with a working directory context), then send prompts.
```java theme={null}
import com.agentclientprotocol.sdk.client.*;
import com.agentclientprotocol.sdk.client.transport.*;
import com.agentclientprotocol.sdk.spec.AcpSchema.*;
import java.util.List;
// 1. Build the command: "gemini --experimental-acp"
var params = AgentParameters.builder("gemini")
.arg("--experimental-acp")
.build();
// 2. Launch it as a subprocess, communicate over stdin/stdout
var transport = new StdioAcpClientTransport(params);
// 3. Build client with update consumer to print the agent's response
AcpSyncClient client = AcpClient.sync(transport)
.sessionUpdateConsumer(notification -> {
if (notification.update() instanceof AgentMessageChunk msg) {
System.out.print(((TextContent) msg.content()).text());
}
})
.build();
// 4. Initialize — exchange protocol versions and capabilities
client.initialize();
// 5. Create session — set working directory context
var session = client.newSession(
new NewSessionRequest(".", List.of()));
// 6. Send prompt — blocks until agent responds, updates stream to consumer
var response = client.prompt(new PromptRequest(
session.sessionId(),
List.of(new TextContent("What is 2+2? Reply with just the number."))
));
System.out.println("\nStop reason: " + response.stopReason());
client.close();
// Output: 4
// Stop reason: END_TURN
```
## How It Works
ACP communication follows a three-phase lifecycle:
1. **Initialize** — client and agent exchange protocol versions and capabilities
2. **New Session** — establishes a working directory context for the conversation
3. **Prompt** — sends content and receives a response with a stop reason
The stdio transport is not Gemini-specific. Any executable that speaks ACP over stdin/stdout works — Gemini CLI, a custom agent JAR, or any other ACP-compliant tool. This is the same mechanism Zed, JetBrains, and VS Code use to talk to agents.
## Source Code
[View on GitHub](https://github.com/markpollack/acp-java-tutorial/tree/main/module-01-first-contact)
## Running the Example
```bash theme={null}
export GEMINI_API_KEY=your-key-here
./mvnw exec:java -pl module-01-first-contact
```
## Next Module
[Module 05: Streaming Updates](/docs/acp-java-sdk/tutorial/05-streaming-updates) — receive real-time updates while the agent processes your prompt.
Or skip to [Module 12: Echo Agent](/docs/acp-java-sdk/tutorial/12-echo-agent) to build your own agent (no API key required).
# 02 protocol basics
Source: https://lab.pollack.ai/docs/acp-java-sdk/tutorial/02-protocol-basics
# Module 02: Protocol Basics
Deep dive into the ACP initialize handshake and version negotiation.
## What You'll Learn
* The `InitializeRequest` and `InitializeResponse` structure
* Protocol version negotiation semantics
* Client and agent capability exchange
## How It Works
The initialize handshake is the first message exchange in ACP — it must complete before any session or prompt operations. Both sides exchange:
1. **Protocol version** — they agree on a compatible version
2. **Client capabilities** — what the client can provide (file system access, terminal execution)
3. **Agent capabilities** — what the agent supports (session loading, image content, MCP)
## The Code
Module 01 called `client.initialize()` with defaults. Here we use the explicit form to control exactly what capabilities we advertise. The `InitializeResponse` tells us what the agent supports:
```java theme={null}
// Initialize with explicit protocol version and capabilities
var initResponse = client.initialize(
new InitializeRequest(1, new ClientCapabilities(
new FileSystemCapability(true, true), // read, write
false // terminalExecution
)));
System.out.println("Protocol version: " + initResponse.protocolVersion());
System.out.println("Agent capabilities: " + initResponse.agentCapabilities());
System.out.println("Existing sessions: " + initResponse.sessionIds().size());
// Output: Protocol version: 1
// Agent capabilities: AgentCapabilities[...]
// Existing sessions: 0
```
## Source Code
[View on GitHub](https://github.com/markpollack/acp-java-tutorial/tree/main/module-02-protocol-basics)
## Running the Example
```bash theme={null}
export GEMINI_API_KEY=your-key-here
./mvnw exec:java -pl module-02-protocol-basics
```
## Next Module
[Module 03: Sessions](/docs/acp-java-sdk/tutorial/03-sessions) — create and manage conversation sessions.
# 03 sessions
Source: https://lab.pollack.ai/docs/acp-java-sdk/tutorial/03-sessions
# Module 03: Sessions
Understanding session creation and lifecycle in ACP.
## What You'll Learn
* Creating sessions with `NewSessionRequest`
* Working directory context and context documents
* Multiple independent sessions
## How Sessions Work
Sessions are workspaces for conversations. Each session has a unique ID assigned by the agent, a working directory context, and its own conversation history. Multiple sessions on the same connection are fully independent.
## The Code
After initializing (Module 01/02), the next step is creating a session. `NewSessionRequest` takes a working directory path and an optional list of MCP server configs. The agent returns a session ID that you use for all subsequent prompts:
```java theme={null}
// Create a session with working directory context
var session = client.newSession(
new NewSessionRequest("/workspace", List.of()));
System.out.println("Session ID: " + session.sessionId());
// Output: Session ID: 53d1a1ee-c7eb-4500-b25c-7bc2fdffa0e4
```
You can create multiple sessions on the same connection. Each maintains its own conversation history — prompts in one session don't affect the other:
```java theme={null}
var session1 = client.newSession(new NewSessionRequest("/project-a", List.of()));
var session2 = client.newSession(new NewSessionRequest("/project-b", List.of()));
// Prompts in session1 don't affect session2
client.prompt(new PromptRequest(session1.sessionId(),
List.of(new TextContent("Analyze this project"))));
```
## Source Code
[View on GitHub](https://github.com/markpollack/acp-java-tutorial/tree/main/module-03-sessions)
## Running the Example
```bash theme={null}
export GEMINI_API_KEY=your-key-here
./mvnw exec:java -pl module-03-sessions
```
## Next Module
[Module 04: Prompts](/docs/acp-java-sdk/tutorial/04-prompts) — prompt requests and response handling in depth.
# 04 prompts
Source: https://lab.pollack.ai/docs/acp-java-sdk/tutorial/04-prompts
# Module 04: Prompts
Deep dive into prompt requests and response handling.
## What You'll Learn
* `PromptRequest` structure — session ID and content list
* `PromptResponse` and `StopReason` values
* Content types for prompts
## The Code
`PromptRequest` takes a session ID (from Module 03) and a list of content items. The response includes a `StopReason` that tells you why the agent stopped generating — check this to know if the response is complete, truncated, or refused:
```java theme={null}
// Send a prompt with text content
var response = client.prompt(new PromptRequest(
session.sessionId(),
List.of(new TextContent("Explain ACP in one sentence."))
));
System.out.println("Stop reason: " + response.stopReason());
// Output: Stop reason: END_TURN
```
## Stop Reasons
The `StopReason` tells you why the agent finished:
| StopReason | Description |
| ------------ | ---------------------------------- |
| `END_TURN` | Agent finished responding normally |
| `MAX_TOKENS` | Token limit reached |
| `REFUSAL` | Agent refused the request |
| `CANCELLED` | Prompt was cancelled by the client |
Understanding stop reasons helps handle different agent behaviors. `END_TURN` is the normal case. `MAX_TOKENS` means the response was truncated. `REFUSAL` may require rephrasing the prompt.
## Source Code
[View on GitHub](https://github.com/markpollack/acp-java-tutorial/tree/main/module-04-prompts)
## Running the Example
```bash theme={null}
export GEMINI_API_KEY=your-key-here
./mvnw exec:java -pl module-04-prompts
```
## Next Module
[Module 05: Streaming Updates](/docs/acp-java-sdk/tutorial/05-streaming-updates) — receive real-time updates during prompt processing.
# 05 streaming updates
Source: https://lab.pollack.ai/docs/acp-java-sdk/tutorial/05-streaming-updates
# Module 05: Streaming Updates
Receive real-time updates from an agent while it processes your prompt.
## What You'll Learn
* Registering a `sessionUpdateConsumer` on the client
* Dispatching on `SessionUpdate` types with `instanceof`
* Handling message chunks, thoughts, tool calls, and plans
## The Code
The client registers an update consumer that receives each `SessionUpdate` as it arrives:
```java theme={null}
AcpSyncClient client = AcpClient.sync(transport)
.sessionUpdateConsumer(notification -> {
handleSessionUpdate(notification.update());
})
.build();
```
The handler uses `instanceof` to dispatch on the `SessionUpdate` type:
```java theme={null}
private static void handleSessionUpdate(SessionUpdate update) {
if (update instanceof AgentMessageChunk msg) {
System.out.print(((TextContent) msg.content()).text());
} else if (update instanceof AgentThoughtChunk thought) {
System.out.println("[Thought] " +
((TextContent) thought.content()).text());
} else if (update instanceof ToolCall tool) {
System.out.println("[Tool] " + tool.title() +
" (" + tool.status() + ")");
} else if (update instanceof ToolCallUpdateNotification toolUpdate) {
System.out.println("[Tool Update] " + toolUpdate.title() +
" -> " + toolUpdate.status());
} else if (update instanceof Plan plan) {
System.out.println("[Plan] " + plan.entries().size() + " steps");
} else if (update instanceof AvailableCommandsUpdate commands) {
System.out.println("[Commands] " + commands.availableCommands().size() +
" available");
} else if (update instanceof CurrentModeUpdate mode) {
System.out.println("[Mode] " + mode.currentModeId());
} else if (update instanceof UsageUpdate usage) {
System.out.println("[Usage] " + usage.used() + "/" + usage.size());
}
}
```
## Session Update Types
| Type | Description |
| ---------------------------- | ------------------------------------------------ |
| `AgentMessageChunk` | Incremental response text (the main output) |
| `AgentThoughtChunk` | Agent's thinking process |
| `ToolCall` | Tool execution starting |
| `ToolCallUpdateNotification` | Tool progress update |
| `Plan` | Agent's planned steps with priorities and status |
| `AvailableCommandsUpdate` | Slash commands the agent supports |
| `CurrentModeUpdate` | Agent mode change |
| `UsageUpdate` | Context window and cost usage |
Updates arrive during `client.prompt()`. The prompt call blocks until the agent returns a `PromptResponse`, but updates stream in continuously through the consumer.
## Source Code
[View on GitHub](https://github.com/markpollack/acp-java-tutorial/tree/main/module-05-streaming-updates)
## Running the Example
```bash theme={null}
export GEMINI_API_KEY=your-key-here
./mvnw exec:java -pl module-05-streaming-updates
```
## Next Module
[Module 12: Echo Agent](/docs/acp-java-sdk/tutorial/12-echo-agent) — build your first ACP agent (no API key required).
# 06 update types
Source: https://lab.pollack.ai/docs/acp-java-sdk/tutorial/06-update-types
# Module 06: Update Types
Comprehensive coverage of all `SessionUpdate` types in ACP.
## What You'll Learn
* All `SessionUpdate` types and when they appear
* Dispatching on update types with `instanceof`
* Building rich UIs that show agent activity
## The Code
The client registers a `sessionUpdateConsumer` and uses `instanceof` to handle each type:
```java theme={null}
AcpSyncClient client = AcpClient.sync(transport)
.sessionUpdateConsumer(notification -> {
SessionUpdate update = notification.update();
if (update instanceof AgentMessageChunk msg) {
System.out.print(((TextContent) msg.content()).text());
} else if (update instanceof AgentThoughtChunk thought) {
System.out.println("[Thought] " +
((TextContent) thought.content()).text());
} else if (update instanceof ToolCall tc) {
System.out.println("[Tool] " + tc.title() +
" | " + tc.kind() + " | " + tc.status());
} else if (update instanceof ToolCallUpdateNotification tcUpdate) {
System.out.println("[ToolUpdate] " +
tcUpdate.toolCallId() + " -> " + tcUpdate.status());
} else if (update instanceof Plan plan) {
System.out.println("[Plan] " + plan.entries().size() + " entries:");
plan.entries().forEach(entry ->
System.out.println(" - " + entry.content() +
" [" + entry.status() + "]"));
} else if (update instanceof AvailableCommandsUpdate commands) {
System.out.println("[Commands] " +
commands.availableCommands().size() + " available");
} else if (update instanceof CurrentModeUpdate mode) {
System.out.println("[Mode] " + mode.currentModeId());
} else if (update instanceof UsageUpdate usage) {
System.out.printf("[Usage] %d/%d tokens%n", usage.used(), usage.size());
if (usage.cost() != null) {
System.out.printf(" Cost: %.4f %s%n",
usage.cost().amount(), usage.cost().currency());
}
}
})
.build();
```
## Update Types Reference
| Type | Content | Typical Use |
| ---------------------------- | ------------------------- | --------------------------------------------- |
| `AgentMessageChunk` | Incremental response text | Main output, streamed word by word |
| `AgentThoughtChunk` | Agent's thinking process | Show reasoning in a collapsible panel |
| `ToolCall` | Tool execution start | Show tool name, kind, and status |
| `ToolCallUpdateNotification` | Tool progress | Update status of in-progress tool |
| `Plan` | Agent's planned steps | Show step list with priorities and completion |
| `AvailableCommandsUpdate` | Slash commands | Populate command palette |
| `CurrentModeUpdate` | Mode change | Update UI mode indicator |
| `UsageUpdate` | Token usage and cost | Show context window usage bar |
This module extends Module 05 by handling every update type rather than just messages.
## Source Code
[View on GitHub](https://github.com/markpollack/acp-java-tutorial/tree/main/module-06-update-types)
## Running the Example
```bash theme={null}
export GEMINI_API_KEY=your-key-here
./mvnw exec:java -pl module-06-update-types
```
## Next Module
[Module 07: Agent Requests](/docs/acp-java-sdk/tutorial/07-agent-requests) — handle file read/write requests from agents.
# 07 agent requests
Source: https://lab.pollack.ai/docs/acp-java-sdk/tutorial/07-agent-requests
# Module 07: Agent Requests (Client Side)
Handle file read/write requests from agents on the client side.
## What You'll Learn
* Registering `readTextFileHandler` and `writeTextFileHandler`
* Advertising file system capabilities via `ClientCapabilities`
* The inverted request flow: agents request, clients serve
## Inverted Request Flow
In ACP, the request direction is inverted for file operations compared to traditional client-server: the **agent** requests files from the **client**. This allows agents to access the user's local filesystem through a controlled interface — the client decides which files to expose and how to handle writes.
## The Code
To enable this, the client registers file handlers on its builder and advertises file system capabilities during initialize. When the agent calls `context.readFile()` or `context.writeFile()`, these handlers are invoked:
```java theme={null}
AcpSyncClient client = AcpClient.sync(transport)
.readTextFileHandler(req -> {
Path path = Path.of(req.path());
if (!Files.exists(path)) {
throw new RuntimeException("File not found: " + req.path());
}
return new ReadTextFileResponse(Files.readString(path));
})
.writeTextFileHandler(req -> {
Files.writeString(Path.of(req.path()), req.content());
return new WriteTextFileResponse();
})
.sessionUpdateConsumer(notification -> { /* handle updates */ })
.build();
// Advertise file system capabilities
client.initialize(new InitializeRequest(1,
new ClientCapabilities(
new FileSystemCapability(true, true), // read=true, write=true
false // terminalExecution
)));
```
Throw exceptions from handlers for errors. The SDK converts exceptions to JSON-RPC error responses. Do not return error strings as content.
## Source Code
[View on GitHub](https://github.com/markpollack/acp-java-tutorial/tree/main/module-07-agent-requests)
## Running the Example
```bash theme={null}
export GEMINI_API_KEY=your-key-here
./mvnw exec:java -pl module-07-agent-requests
```
## Next Module
[Module 08: Permissions](/docs/acp-java-sdk/tutorial/08-permissions) — handle permission requests from agents.
# 08 permissions
Source: https://lab.pollack.ai/docs/acp-java-sdk/tutorial/08-permissions
# Module 08: Permissions
Handle permission requests from agents on the client side.
## What You'll Learn
* Registering a `requestPermissionHandler`
* `PermissionOption` and `PermissionOptionKind` types
* Permission outcomes: selected vs cancelled
## The Code
When an agent wants to perform a sensitive operation (like writing a file or running a command), it can ask the client for permission first. The client registers a `requestPermissionHandler` that receives the request details and a list of options (allow once, allow always, reject). Your handler decides which option to select — typically by showing a dialog to the user:
```java theme={null}
AcpSyncClient client = AcpClient.sync(transport)
.requestPermissionHandler(req -> {
System.out.println("Agent wants to: " + req.toolCall().title());
System.out.println("Options:");
for (var option : req.options()) {
System.out.println(" " + option.id() + ": " +
option.name() + " (" + option.kind() + ")");
}
// Auto-approve with first option
return new RequestPermissionResponse(
req.options().getFirst().id());
})
.readTextFileHandler(req -> /* file handler */)
.writeTextFileHandler(req -> /* file handler */)
.build();
```
## Permission Option Kinds
| Kind | Description |
| --------------- | ----------------------------- |
| `ALLOW_ONCE` | Allow this specific operation |
| `ALLOW_ALWAYS` | Allow all similar operations |
| `REJECT_ONCE` | Deny this specific operation |
| `REJECT_ALWAYS` | Deny all similar operations |
The agent sends a `RequestPermissionRequest` containing the tool call details and a list of options. The client presents choices to the user and returns the selected option ID. This completes the bidirectional request flow (with Module 07 for files).
## Source Code
[View on GitHub](https://github.com/markpollack/acp-java-tutorial/tree/main/module-08-permissions)
## Running the Example
```bash theme={null}
export GEMINI_API_KEY=your-key-here
./mvnw exec:java -pl module-08-permissions
```
## Next Module
[Module 09: Session Resume](/docs/acp-java-sdk/tutorial/09-session-resume) — load and resume existing sessions.
# 09 session resume
Source: https://lab.pollack.ai/docs/acp-java-sdk/tutorial/09-session-resume
# Module 09: Session Resume
Load and resume existing sessions.
## What You'll Learn
* Using `client.loadSession()` to resume a session by ID
* `LoadSessionRequest` and `LoadSessionResponse` structure
* Session state persistence across load operations
## How It Works
`loadSession()` tells the agent to resume a previously created session. The agent looks up its stored state for that session ID and restores the conversation context. Whether state actually persists depends on the agent implementation — the demo includes a `StatefulAgent` that stores session history in a `ConcurrentHashMap`.
## The Code
This example creates a session, sends messages to build history, then loads the same session by ID. After loading, the agent has access to the previous conversation context:
```java theme={null}
// Create a session and send some messages
var session = client.newSession(new NewSessionRequest(".", List.of()));
client.prompt(new PromptRequest(session.sessionId(),
List.of(new TextContent("Remember: my favorite color is blue."))));
// Later: resume the same session by ID
var loadResponse = client.loadSession(
new LoadSessionRequest(session.sessionId(), ".", List.of()));
// Continue the conversation with context preserved
client.prompt(new PromptRequest(session.sessionId(),
List.of(new TextContent("What is my favorite color?"))));
// The agent remembers the previous conversation
```
## Source Code
[View on GitHub](https://github.com/markpollack/acp-java-tutorial/tree/main/module-09-session-resume)
## Running the Example
```bash theme={null}
./mvnw package -pl module-09-session-resume -q
./mvnw exec:java -pl module-09-session-resume
```
## Next Module
[Module 10: Cancellation](/docs/acp-java-sdk/tutorial/10-cancellation) — cancel an in-progress prompt.
# 10 cancellation
Source: https://lab.pollack.ai/docs/acp-java-sdk/tutorial/10-cancellation
# Module 10: Cancellation
Cancel an in-progress prompt from the client side.
## What You'll Learn
* Sending `CancelNotification` to interrupt a running prompt
* Running prompts in background threads
* How cancellation affects `StopReason`
## The Code
```java theme={null}
// Run prompt in a background thread
AtomicReference responseRef = new AtomicReference<>();
CompletableFuture promptFuture = CompletableFuture.runAsync(() -> {
var response = client.prompt(new PromptRequest(
sessionId,
List.of(new TextContent("Do a long task"))));
responseRef.set(response);
});
// Wait, then cancel
Thread.sleep(1500);
client.cancel(new CancelNotification(sessionId));
// Wait for prompt to finish
promptFuture.join();
System.out.println("Stop reason: " + responseRef.get().stopReason());
```
## How It Works
`client.cancel()` sends a one-way notification (not a request) to the agent. The agent's `cancelHandler` receives it and sets a flag. The prompt handler checks this flag between steps and stops early when cancelled.
On the agent side:
```java theme={null}
// Track cancellation per session
Map cancelledSessions = new ConcurrentHashMap<>();
AcpSyncAgent agent = AcpAgent.sync(transport)
.cancelHandler(notification -> {
// Set flag — no response needed (notification, not request)
cancelledSessions.put(notification.sessionId(), true);
})
.promptHandler((req, context) -> {
cancelledSessions.put(req.sessionId(), false);
for (int i = 1; i <= 10; i++) {
// Check flag before each step
if (cancelledSessions.getOrDefault(req.sessionId(), false)) {
context.sendMessage("[Cancelled at step " + i + "]");
return PromptResponse.endTurn();
}
context.sendMessage("Step " + i + "/10... ");
Thread.sleep(500);
}
context.sendMessage("All steps completed!");
return PromptResponse.endTurn();
})
.build();
```
Cancellation is cooperative. The agent must check for it — the SDK does not forcefully interrupt handler execution.
## Source Code
[View on GitHub](https://github.com/markpollack/acp-java-tutorial/tree/main/module-10-cancellation)
## Running the Example
```bash theme={null}
./mvnw package -pl module-10-cancellation -q
./mvnw exec:java -pl module-10-cancellation
```
## Next Module
[Module 11: Error Handling](/docs/acp-java-sdk/tutorial/11-error-handling) — handle protocol errors from agents.
# 11 error handling
Source: https://lab.pollack.ai/docs/acp-java-sdk/tutorial/11-error-handling
# Module 11: Error Handling
Handle protocol errors from agents on the client side.
## What You'll Learn
* Catching `AcpClientSession.AcpError`
* Standard error codes in `AcpErrorCodes`
* Throwing `AcpProtocolException` from agent handlers
* Error recovery — continuing after errors
## The Code
ACP uses structured errors based on JSON-RPC error codes. On the **client side**, protocol errors arrive as `AcpClientSession.AcpError` exceptions. You can inspect the error code to determine what went wrong:
```java theme={null}
// Client: catch protocol errors
try {
client.prompt(new PromptRequest(sessionId,
List.of(new TextContent("this is invalid input"))));
} catch (AcpClientSession.AcpError e) {
System.out.println("Code: " + e.getCode());
System.out.println("Message: " + e.getMessage());
// Output: Code: -32602
// Message: Invalid parameter in prompt: 'this is invalid input'
}
```
On the **agent side**, throw `AcpProtocolException` with a standard error code. The SDK converts it to a JSON-RPC error response:
```java theme={null}
// Agent: throw protocol errors
.promptHandler((req, context) -> {
String text = /* extract text from prompt */;
if (text.contains("invalid")) {
throw new AcpProtocolException(
AcpErrorCodes.INVALID_PARAMS,
"Invalid parameter in prompt: '" + text + "'");
}
if (text.contains("internal")) {
throw new AcpProtocolException(
AcpErrorCodes.INTERNAL_ERROR,
"Simulated internal error");
}
context.sendMessage("Success! Processed: " + text);
return PromptResponse.endTurn();
})
```
## Error Codes
| Code | Constant | When to Use |
| -------- | ------------------- | ------------------------ |
| `-32602` | `INVALID_PARAMS` | Bad input from client |
| `-32603` | `INTERNAL_ERROR` | Unexpected agent failure |
| `-32001` | `SESSION_NOT_FOUND` | Unknown session ID |
| `-32002` | `PERMISSION_DENIED` | Client lacks permission |
Agents throw `AcpProtocolException` with one of these codes. The SDK converts it to a JSON-RPC error response. Clients catch it as `AcpClientSession.AcpError`.
## Error Recovery
Errors do not terminate the connection. After catching an error, the client can continue sending requests on the same session:
```java theme={null}
// This works — errors don't break the connection
try {
client.prompt(/* bad input */);
} catch (AcpClientSession.AcpError e) {
// handle error
}
// Continue normally
var response = client.prompt(/* good input */);
// Works fine
```
## Source Code
[View on GitHub](https://github.com/markpollack/acp-java-tutorial/tree/main/module-11-error-handling)
## Running the Example
```bash theme={null}
./mvnw package -pl module-11-error-handling -q
./mvnw exec:java -pl module-11-error-handling
```
## Next Module
[Module 12: Echo Agent](/docs/acp-java-sdk/tutorial/12-echo-agent) — build your first agent.
# 12 echo agent
Source: https://lab.pollack.ai/docs/acp-java-sdk/tutorial/12-echo-agent
# Module 12: Echo Agent
Build a minimal ACP agent in \~25 lines. No API key required.
## What You'll Learn
* Building an agent with `AcpAgent.sync()`
* Implementing the three required handlers: initialize, newSession, prompt
* Sending messages back to the client
* Running a self-contained agent + client demo
## The Agent
```java theme={null}
import com.agentclientprotocol.sdk.agent.*;
import com.agentclientprotocol.sdk.agent.transport.*;
import com.agentclientprotocol.sdk.spec.AcpSchema.*;
import java.util.UUID;
var transport = new StdioAcpAgentTransport();
AcpSyncAgent agent = AcpAgent.sync(transport)
.initializeHandler(req -> InitializeResponse.ok())
.newSessionHandler(req ->
new NewSessionResponse(UUID.randomUUID().toString(), null, null))
.promptHandler((req, context) -> {
// Echo it back using convenience method
context.sendMessage("Echo: " + req.text());
return PromptResponse.endTurn();
})
.build();
agent.run(); // Blocks until client disconnects
```
## How It Works
An ACP agent needs three handlers:
| Handler | Purpose |
| ------------------- | ------------------------------------------------------- |
| `initializeHandler` | Protocol handshake — return capabilities |
| `newSessionHandler` | Create a session — return a unique session ID |
| `promptHandler` | Process prompts — send updates and return a stop reason |
`agent.run()` starts the agent and blocks. The agent reads JSON-RPC requests from stdin and writes responses to stdout. This is the stdio transport — the same mechanism Zed and JetBrains use to talk to agents.
The `context` parameter in the prompt handler gives access to `sendMessage()`, `sendThought()`, and other convenience methods for sending updates back to the client.
## The Demo Client
The module also includes `EchoAgentDemo.java`, which launches the echo agent as a subprocess and exercises it:
```java theme={null}
// Launch echo agent as subprocess (from packaged JAR)
var params = AgentParameters.builder("java")
.arg("-jar")
.arg(jarPath)
.build();
var transport = new StdioAcpClientTransport(params);
AcpSyncClient client = AcpClient.sync(transport)
.sessionUpdateConsumer(notification -> {
if (notification.update() instanceof AgentMessageChunk msg) {
System.out.println(((TextContent) msg.content()).text());
}
})
.build();
client.initialize();
var session = client.newSession(new NewSessionRequest(".", List.of()));
client.prompt(new PromptRequest(
session.sessionId(),
List.of(new TextContent("Hello, Echo Agent!"))
));
// Output: Echo: Hello, Echo Agent!
```
## Source Code
[View on GitHub](https://github.com/markpollack/acp-java-tutorial/tree/main/module-12-echo-agent)
## Running the Example
```bash theme={null}
./mvnw package -pl module-12-echo-agent -q
./mvnw exec:java -pl module-12-echo-agent
```
## Key Points
* **No API key** — the agent runs entirely locally
* **Stdio transport** — same protocol mechanism used by Zed, JetBrains, VS Code
* **`agent.run()`** — combines `start()` and `awaitTermination()`
* **`context.sendMessage()`** — convenience for sending `AgentMessageChunk` updates
## Next Module
[Module 13: Agent Handlers](/docs/acp-java-sdk/tutorial/13-agent-handlers) — implement all handler types including load session and cancel.
# 13 agent handlers
Source: https://lab.pollack.ai/docs/acp-java-sdk/tutorial/13-agent-handlers
# Module 13: Agent Handlers
Implement all handler types that an ACP agent can provide.
## What You'll Learn
* All five handler types: initialize, newSession, loadSession, prompt, cancel
* Session tracking with `ConcurrentHashMap`
* Logging to stderr (stdout is reserved for the protocol)
## The Code
An ACP agent has five handler types. Three are required (`initialize`, `newSession`, `prompt`) and two are optional (`loadSession`, `cancel`). This example shows all five wired up with the sync builder API. Note that `stdout` is reserved for the JSON-RPC protocol — agent logging goes to `stderr`:
```java theme={null}
import com.agentclientprotocol.sdk.agent.*;
import com.agentclientprotocol.sdk.agent.transport.*;
import com.agentclientprotocol.sdk.spec.AcpSchema.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
var sessions = new ConcurrentHashMap();
var transport = new StdioAcpAgentTransport();
AcpSyncAgent agent = AcpAgent.sync(transport)
.initializeHandler(req -> {
System.err.println("Protocol version: " + req.protocolVersion());
return InitializeResponse.ok();
})
.newSessionHandler(req -> {
String sessionId = UUID.randomUUID().toString();
sessions.put(sessionId, req.cwd());
System.err.println("New session: " + sessionId);
return new NewSessionResponse(sessionId, null, null);
})
.loadSessionHandler(req -> {
System.err.println("Loading session: " + req.sessionId());
return new LoadSessionResponse(List.of(), List.of());
})
.promptHandler((req, context) -> {
context.sendMessage("Received: " + req.text());
return PromptResponse.endTurn();
})
.cancelHandler(notification -> {
System.err.println("Cancel requested for: " +
notification.sessionId());
})
.build();
agent.run();
```
## Handler Reference
| Handler | Method | Required | Description |
| -------------------- | ---------------- | -------- | -------------------------------------------------- |
| `initializeHandler` | `initialize` | Yes | Protocol handshake, capability exchange |
| `newSessionHandler` | `session/new` | Yes | Create session with working directory |
| `loadSessionHandler` | `session/load` | No | Resume an existing session by ID |
| `promptHandler` | `session/prompt` | Yes | Process user prompts |
| `cancelHandler` | `session/cancel` | No | Handle cancellation (fire-and-forget notification) |
The `cancelHandler` receives a `CancelNotification`, not a request — it has no response. This is the JSON-RPC notification pattern: the client sends it and does not expect a reply.
## Source Code
[View on GitHub](https://github.com/markpollack/acp-java-tutorial/tree/main/module-13-agent-handlers)
## Running the Example
```bash theme={null}
./mvnw package -pl module-13-agent-handlers -q
./mvnw exec:java -pl module-13-agent-handlers
```
## Next Module
[Module 14: Sending Updates](/docs/acp-java-sdk/tutorial/14-sending-updates) — send all types of session updates to clients.
# 14 sending updates
Source: https://lab.pollack.ai/docs/acp-java-sdk/tutorial/14-sending-updates
# Module 14: Sending Updates
Send all types of session updates from an agent to its client.
## What You'll Learn
* Convenience methods: `sendMessage()`, `sendThought()`
* Full API: `sendUpdate()` for complex types (plans, tool calls, commands)
* All `SessionUpdate` types from the agent's perspective
## The Code
The prompt handler demonstrates each update type:
```java theme={null}
.promptHandler((req, context) -> {
String sessionId = context.getSessionId();
// 1. Thought — show thinking process (convenience method)
context.sendThought("Let me analyze this request...");
// 2. Plan — show steps and progress (full API)
context.sendUpdate(sessionId,
new Plan("plan", List.of(
new PlanEntry("Analyze the prompt",
PlanEntryPriority.HIGH, PlanEntryStatus.IN_PROGRESS),
new PlanEntry("Generate response",
PlanEntryPriority.HIGH, PlanEntryStatus.PENDING),
new PlanEntry("Format output",
PlanEntryPriority.MEDIUM, PlanEntryStatus.PENDING)
)));
// 3. Tool Call — show tool execution starting
context.sendUpdate(sessionId,
new ToolCall("tool_call",
"tool-1", "Analyzing prompt", ToolKind.THINK,
ToolCallStatus.IN_PROGRESS,
List.of(), null, null, null, null));
// 4. Tool Call Update — show progress
context.sendUpdate(sessionId,
new ToolCallUpdateNotification("tool_call_update",
"tool-1", "Analyzing prompt", ToolKind.THINK,
ToolCallStatus.COMPLETED,
List.of(), null, null, null, null));
// 5. Available Commands — advertise slash commands
context.sendUpdate(sessionId,
new AvailableCommandsUpdate("available_commands_update", List.of(
new AvailableCommand("help", "Show help",
new AvailableCommandInput("topic")),
new AvailableCommand("clear", "Clear context", null)
)));
// 6. Mode Update — report current mode
context.sendUpdate(sessionId,
new CurrentModeUpdate("current_mode_update", "default"));
// 7. Usage Update — report token usage and cost
context.sendUpdate(sessionId,
new UsageUpdate("usage_update", 53000L, 200000L));
// 8. Message chunks — the actual response (convenience method)
context.sendMessage("Here is my response ");
context.sendMessage("streamed in ");
context.sendMessage("multiple chunks.");
return PromptResponse.endTurn();
})
```
## Convenience vs Full API
| Method | Sends | When to Use |
| --------------------------------------- | ------------------- | ---------------------------------- |
| `context.sendMessage(text)` | `AgentMessageChunk` | Response text |
| `context.sendThought(text)` | `AgentThoughtChunk` | Thinking process |
| `context.sendUpdate(sessionId, update)` | Any `SessionUpdate` | Plans, tool calls, commands, modes |
Convenience methods handle wrapping in `TextContent` and setting the `type` field. Use `sendUpdate()` for complex types that need full control over their structure.
## Source Code
[View on GitHub](https://github.com/markpollack/acp-java-tutorial/tree/main/module-14-sending-updates)
## Running the Example
```bash theme={null}
./mvnw package -pl module-14-sending-updates -q
./mvnw exec:java -pl module-14-sending-updates
```
## Next Module
[Module 15: Agent Requests](/docs/acp-java-sdk/tutorial/15-agent-requests) — read files, write files, and request permissions from the client.
# 15 agent requests
Source: https://lab.pollack.ai/docs/acp-java-sdk/tutorial/15-agent-requests
# Module 15: Agent Requests
Agents can request file operations and permissions from the client.
## What You'll Learn
* Reading files from the client with `context.readFile()`
* Writing files with `context.writeFile()`
* Requesting permissions with `context.requestPermission()`
* The client-side handler pattern for responding to agent requests
## The Agent
The prompt handler uses `SyncPromptContext` convenience methods:
```java theme={null}
.promptHandler((req, context) -> {
String sessionId = context.getSessionId();
// 1. Read a file (convenience method)
context.sendMessage("Reading pom.xml from your system...\n");
String content = context.readFile("pom.xml", 0, 10);
context.sendMessage("File content (first 10 lines):\n" + content + "\n\n");
// 2. Request permission (full API for complex permissions)
ToolCallUpdate toolCall = new ToolCallUpdate(
"tool-write-1", "Create summary.txt",
ToolKind.EDIT, ToolCallStatus.PENDING,
null, null, null, null
);
List options = List.of(
new PermissionOption("allow", "Allow this once",
PermissionOptionKind.ALLOW_ONCE),
new PermissionOption("allow_always", "Always allow",
PermissionOptionKind.ALLOW_ALWAYS),
new PermissionOption("deny", "Deny",
PermissionOptionKind.REJECT_ONCE)
);
var permissionResponse = context.requestPermission(
new RequestPermissionRequest(sessionId, toolCall, options));
context.sendMessage("Permission: " + permissionResponse.outcome() + "\n");
// 3. Write a file (convenience method)
context.writeFile("summary.txt",
"Created by the FileRequestingAgent.\n");
context.sendMessage("Successfully wrote summary.txt!\n");
return PromptResponse.endTurn();
})
```
## The Client
The client registers handlers to respond to agent requests:
```java theme={null}
AcpSyncClient client = AcpClient.sync(transport)
.readTextFileHandler(req -> {
String fileContent = Files.readString(Path.of(req.path()));
return new ReadTextFileResponse(fileContent);
})
.writeTextFileHandler(req -> {
Files.writeString(Path.of(req.path()), req.content());
return new WriteTextFileResponse();
})
.requestPermissionHandler(req -> {
// Auto-approve with first option
return new RequestPermissionResponse(
req.options().getFirst().id());
})
.build();
```
Throw exceptions from handlers for errors. The SDK converts exceptions to JSON-RPC error responses. Do not return error strings as content — agents will misinterpret them as file content.
## Client Capabilities
The client must advertise file system support during initialization:
```java theme={null}
client.initialize(new InitializeRequest(1,
new ClientCapabilities(
new FileSystemCapability(true, true), // read=true, write=true
false // terminalExecution
)));
```
Agents can check capabilities before using them:
```java theme={null}
NegotiatedCapabilities caps = context.getClientCapabilities();
if (caps.supportsReadTextFile()) {
String content = context.readFile("file.txt");
}
```
## Source Code
[View on GitHub](https://github.com/markpollack/acp-java-tutorial/tree/main/module-15-agent-requests)
## Running the Example
```bash theme={null}
./mvnw package -pl module-15-agent-requests -q
./mvnw exec:java -pl module-15-agent-requests
```
## Next Module
[Module 16: In-Memory Testing](/docs/acp-java-sdk/tutorial/16-in-memory-testing) — test agents without subprocess launching.
# 16 in memory testing
Source: https://lab.pollack.ai/docs/acp-java-sdk/tutorial/16-in-memory-testing
# Module 16: In-Memory Testing
Test client-agent communication without subprocesses or I/O.
## What You'll Learn
* Using `InMemoryTransportPair` from `acp-test`
* Wiring a client and agent together in-process
* Fast, deterministic testing without external dependencies
## The Code
`InMemoryTransportPair` from the `acp-test` module creates a pair of connected transports — one for the client, one for the agent. Messages pass through in-memory buffers instead of subprocess stdin/stdout. This makes tests fast, deterministic, and free of external dependencies. The pattern below wires up an agent and client in the same JVM, sends a prompt, and verifies the round-trip:
```java theme={null}
import com.agentclientprotocol.sdk.test.InMemoryTransportPair;
import com.agentclientprotocol.sdk.agent.*;
import com.agentclientprotocol.sdk.client.*;
import com.agentclientprotocol.sdk.spec.AcpSchema.*;
import reactor.core.publisher.Mono;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
// 1. Create in-memory transport pair
var transportPair = InMemoryTransportPair.create();
// 2. Create agent (using async API here)
AtomicReference receivedPrompt = new AtomicReference<>();
AcpAsyncAgent agent = AcpAgent.async(transportPair.agentTransport())
.initializeHandler(req -> Mono.just(InitializeResponse.ok()))
.newSessionHandler(req ->
Mono.just(new NewSessionResponse(
UUID.randomUUID().toString(), null, null)))
.promptHandler((req, context) -> {
String text = req.text();
receivedPrompt.set(text);
return context.sendMessage("Echo: " + text)
.then(Mono.just(PromptResponse.endTurn()));
})
.build();
agent.start().subscribe();
// 3. Create client
AtomicReference receivedMessage = new AtomicReference<>();
AcpSyncClient client = AcpClient.sync(transportPair.clientTransport())
.sessionUpdateConsumer(notification -> {
if (notification.update() instanceof AgentMessageChunk msg) {
receivedMessage.set(((TextContent) msg.content()).text());
}
})
.build();
// 4. Run test
client.initialize();
var session = client.newSession(new NewSessionRequest(".", List.of()));
client.prompt(new PromptRequest(
session.sessionId(),
List.of(new TextContent("Hello from in-memory test!"))
));
// 5. Verify
assert "Hello from in-memory test!".equals(receivedPrompt.get());
assert "Echo: Hello from in-memory test!".equals(receivedMessage.get());
// 6. Cleanup
client.close();
transportPair.closeGracefully().block();
```
## How It Works
`InMemoryTransportPair.create()` returns a pair of connected transports:
| Transport | Used By | Description |
| --------------------------------- | ------- | ------------------------------------- |
| `transportPair.clientTransport()` | Client | Sends to agent, receives from agent |
| `transportPair.agentTransport()` | Agent | Receives from client, sends to client |
Messages pass through in-memory buffers. No subprocess launching, no stdin/stdout, no network I/O. This makes tests fast and deterministic.
## Maven Dependency
```xml theme={null}
com.agentclientprotocolacp-test0.15.0test
```
## Source Code
[View on GitHub](https://github.com/markpollack/acp-java-tutorial/tree/main/module-16-in-memory-testing)
## Running the Example
```bash theme={null}
./mvnw exec:java -pl module-16-in-memory-testing
```
## Next Module
[Module 17: Capability Negotiation](/docs/acp-java-sdk/tutorial/17-capability-negotiation) — advertise and check capabilities between client and agent.
# 17 capability negotiation
Source: https://lab.pollack.ai/docs/acp-java-sdk/tutorial/17-capability-negotiation
# Module 17: Capability Negotiation
Client and agent agree on what each supports during initialization.
## What You'll Learn
* Advertising `ClientCapabilities` during `initialize()`
* Checking `NegotiatedCapabilities` from the agent side
* Graceful degradation when a capability is missing
## The Code
### Client: Advertise capabilities
```java theme={null}
// Tell the agent what we support
var clientCaps = new ClientCapabilities(
new FileSystemCapability(true, true), // readTextFile, writeTextFile
true // terminal
);
client.initialize(new InitializeRequest(1, clientCaps));
// Check what the agent supports
NegotiatedCapabilities agentCaps = client.getAgentCapabilities();
System.out.println("loadSession: " + agentCaps.supportsLoadSession());
System.out.println("mcpHttp: " + agentCaps.supportsMcpHttp());
System.out.println("mcpSse: " + agentCaps.supportsMcpSse());
```
### Agent: Advertise and check capabilities
```java theme={null}
.initializeHandler(req -> {
// Read what the client supports
var clientCaps = req.clientCapabilities();
// Advertise our own capabilities
var agentCaps = new AgentCapabilities(
true, // loadSession
new McpCapabilities(false, false), // no MCP
new PromptCapabilities(false, false, true) // embeddedContext only
);
return InitializeResponse.ok(agentCaps);
})
.promptHandler((req, context) -> {
// Check capabilities before attempting operations
NegotiatedCapabilities caps = context.getClientCapabilities();
if (caps.supportsReadTextFile()) {
String content = context.readFile("/etc/hostname");
} else {
// Graceful degradation
context.sendMessage("File read not supported by client");
}
return PromptResponse.endTurn();
})
```
## Capability Categories
| Capability | Client | Agent |
| ---------------------- | --------------------------- | ------------------------------------------- |
| `FileSystemCapability` | readTextFile, writeTextFile | — |
| Terminal | terminal execution | — |
| `AgentCapabilities` | — | loadSession |
| `McpCapabilities` | — | HTTP, SSE |
| `PromptCapabilities` | — | imageContent, audioContent, embeddedContext |
Clients advertise file system and terminal support. Agents advertise session resume, MCP server types, and content format support. Both sides can call `getClientCapabilities()` or `getAgentCapabilities()` after initialization to check what was negotiated.
## Source Code
[View on GitHub](https://github.com/markpollack/acp-java-tutorial/tree/main/module-17-capability-negotiation)
## Running the Example
```bash theme={null}
./mvnw package -pl module-17-capability-negotiation -q
./mvnw exec:java -pl module-17-capability-negotiation
```
## Next Module
[Module 18: Terminal Operations](/docs/acp-java-sdk/tutorial/18-terminal-operations) — execute shell commands through the terminal API.
# 18 terminal operations
Source: https://lab.pollack.ai/docs/acp-java-sdk/tutorial/18-terminal-operations
# Module 18: Terminal Operations
Execute shell commands on the client through the terminal API.
## What You'll Learn
* The four-step terminal lifecycle: create, wait, output, release
* Implementing terminal handlers on the client
* Using the terminal API from the agent side
## The Code
### Client: Implement terminal handlers
```java theme={null}
var clientCaps = new ClientCapabilities(
new FileSystemCapability(false, false),
true // terminal enabled
);
AcpSyncClient client = AcpClient.sync(transport)
.createTerminalHandler(req -> {
List cmd = new ArrayList<>();
cmd.add(req.command());
if (req.args() != null) cmd.addAll(req.args());
Process process = new ProcessBuilder(cmd)
.redirectErrorStream(true)
.start();
terminals.put(terminalId, process);
return new CreateTerminalResponse(terminalId);
})
.waitForTerminalExitHandler(req -> {
Process process = terminals.get(req.terminalId()).process();
int exitCode = process.waitFor();
return new WaitForTerminalExitResponse(exitCode, null);
})
.terminalOutputHandler(req -> {
String output = capturedOutput.get(req.terminalId());
return new TerminalOutputResponse(output, false, null);
})
.releaseTerminalHandler(req -> {
Process process = terminals.remove(req.terminalId()).process();
process.destroyForcibly();
return new ReleaseTerminalResponse();
})
.build();
client.initialize(new InitializeRequest(1, clientCaps));
```
### Agent: Use terminal API
```java theme={null}
.promptHandler((req, context) -> {
// Check capability first
if (!context.getClientCapabilities().supportsTerminal()) {
context.sendMessage("Terminal not supported");
return PromptResponse.endTurn();
}
String terminalId = null;
try {
// Step 1: Create terminal
var createResp = context.createTerminal(
new CreateTerminalRequest(
context.getSessionId(),
"sh", List.of("-c", command),
null, null, null));
terminalId = createResp.terminalId();
// Step 2: Wait for exit
var exitResp = context.waitForTerminalExit(
new WaitForTerminalExitRequest(context.getSessionId(), terminalId));
// Step 3: Get output
var outputResp = context.getTerminalOutput(
new TerminalOutputRequest(context.getSessionId(), terminalId));
context.sendMessage("Exit: " + exitResp.exitCode() +
"\nOutput:\n" + outputResp.output());
} finally {
// Step 4: Always release
if (terminalId != null) {
context.releaseTerminal(
new ReleaseTerminalRequest(context.getSessionId(), terminalId));
}
}
return PromptResponse.endTurn();
})
```
## Terminal Lifecycle
| Step | Agent calls | Client handles | Purpose |
| ---- | ----------------------- | ---------------------------- | ------------------ |
| 1 | `createTerminal()` | `createTerminalHandler` | Spawn process |
| 2 | `waitForTerminalExit()` | `waitForTerminalExitHandler` | Block until done |
| 3 | `getTerminalOutput()` | `terminalOutputHandler` | Read stdout/stderr |
| 4 | `releaseTerminal()` | `releaseTerminalHandler` | Clean up resources |
The agent requests command execution, but the client controls what actually runs. This keeps command execution under the user's control — the client decides whether to allow, sandbox, or deny terminal requests.
The SDK also provides `context.execute()` as a convenience method that combines all four steps.
## Source Code
[View on GitHub](https://github.com/markpollack/acp-java-tutorial/tree/main/module-18-terminal-operations)
## Running the Example
```bash theme={null}
./mvnw package -pl module-18-terminal-operations -q
./mvnw exec:java -pl module-18-terminal-operations
```
## Next Module
[Module 19: MCP Servers](/docs/acp-java-sdk/tutorial/19-mcp-servers) — pass MCP server configurations to agents.
# 19 mcp servers
Source: https://lab.pollack.ai/docs/acp-java-sdk/tutorial/19-mcp-servers
# Module 19: MCP Servers
Pass MCP server configurations to agents when creating sessions.
## What You'll Learn
* Passing `McpServer` configs via `NewSessionRequest`
* Three MCP server types: `McpServerStdio`, `McpServerHttp`, `McpServerSse`
* Checking agent MCP capabilities
* Receiving MCP configs on the agent side
## The Code
### Client: Pass MCP servers to agent
```java theme={null}
// Check what MCP transports the agent supports
NegotiatedCapabilities agentCaps = client.getAgentCapabilities();
System.out.println("HTTP: " + agentCaps.supportsMcpHttp());
System.out.println("SSE: " + agentCaps.supportsMcpSse());
// Session with STDIO MCP server
var session1 = client.newSession(new NewSessionRequest(cwd, List.of(
new McpServerStdio(
"filesystem",
"npx",
List.of("-y", "@modelcontextprotocol/server-filesystem", "/tmp"),
List.of())
)));
// Session with multiple server types
var session2 = client.newSession(new NewSessionRequest(cwd, List.of(
new McpServerStdio("git", "npx",
List.of("-y", "@modelcontextprotocol/server-git"), List.of()),
new McpServerHttp("weather-api",
"https://api.weather.example.com/mcp", List.of()),
new McpServerSse("live-data",
"https://stream.example.com/mcp/events", List.of())
)));
```
### Agent: Receive and advertise MCP support
```java theme={null}
.initializeHandler(req -> {
var mcpCaps = new McpCapabilities(true, true); // HTTP and SSE
var agentCaps = new AgentCapabilities(
true, mcpCaps, new PromptCapabilities());
return InitializeResponse.ok(agentCaps);
})
.newSessionHandler(req -> {
// MCP servers arrive with the session
List servers = req.mcpServers();
for (McpServer server : servers) {
switch (server) {
case McpServerStdio s ->
System.out.println("STDIO: " + s.name() + " " + s.command());
case McpServerHttp h ->
System.out.println("HTTP: " + h.name() + " " + h.url());
case McpServerSse s ->
System.out.println("SSE: " + s.name() + " " + s.url());
default -> {}
}
}
return new NewSessionResponse(sessionId, null, null);
})
```
## MCP Server Types
| Type | Transport | Use Case |
| ---------------- | ----------------------- | ----------------------------- |
| `McpServerStdio` | Stdin/stdout subprocess | Local tools (filesystem, git) |
| `McpServerHttp` | HTTP endpoint | Remote APIs |
| `McpServerSse` | Server-sent events | Streaming data |
The client tells the agent which MCP servers are available. The agent is responsible for connecting to them using an MCP client library. ACP handles the configuration exchange — not the MCP connection itself.
## Source Code
[View on GitHub](https://github.com/markpollack/acp-java-tutorial/tree/main/module-19-mcp-servers)
## Running the Example
```bash theme={null}
./mvnw package -pl module-19-mcp-servers -q
./mvnw exec:java -pl module-19-mcp-servers
```
## Next Module
[Module 21: Async Client](/docs/acp-java-sdk/tutorial/21-async-client) — use the reactive, non-blocking client API.
# 21 async client
Source: https://lab.pollack.ai/docs/acp-java-sdk/tutorial/21-async-client
# Module 21: Async Client
The reactive, non-blocking version of Module 01.
## What You'll Learn
* `AcpClient.async()` instead of `AcpClient.sync()`
* Chaining operations with `Mono` and `flatMap`
* Async session update consumers returning `Mono`
* When to use async vs sync
## The Code
```java theme={null}
// Build async client — note AcpAsyncClient return type
AcpAsyncClient client = AcpClient.async(transport)
.sessionUpdateConsumer(notification -> {
// Async consumer must return Mono
var update = notification.update();
if (update instanceof AgentMessageChunk msg) {
if (msg.content() instanceof TextContent text) {
System.out.print(text.text());
}
}
return Mono.empty();
})
.build();
// Chain operations reactively with flatMap
client.initialize()
.flatMap(init -> client.newSession(
new NewSessionRequest(".", List.of())))
.flatMap(session -> client.prompt(
new PromptRequest(session.sessionId(),
List.of(new TextContent("What is 2+2?")))))
.flatMap(response -> client.closeGracefully())
.subscribe(
unused -> {},
error -> System.err.println("Error: " + error.getMessage()),
() -> System.out.println("Done!")
);
```
## Sync vs Async Comparison
| Aspect | `AcpClient.sync()` | `AcpClient.async()` |
| --------------- | --------------------- | ------------------------------ |
| Return type | `T` | `Mono` |
| Chaining | Sequential statements | `flatMap` |
| Update consumer | `void` | `Mono` |
| Blocking | Yes | No (unless you call `block()`) |
The async client wraps every operation in Project Reactor's `Mono`. If you're already using a reactive framework, the async client integrates naturally. For CLI tools and scripts, the sync client is simpler.
## Alternative: block()
For scripts where you want async types but don't care about non-blocking I/O:
```java theme={null}
var init = client.initialize().block();
var session = client.newSession(new NewSessionRequest(".", List.of())).block();
var response = client.prompt(new PromptRequest(
session.sessionId(),
List.of(new TextContent("Hello")))).block();
client.closeGracefully().block();
```
This defeats the purpose of async but can be useful during prototyping.
## Source Code
[View on GitHub](https://github.com/markpollack/acp-java-tutorial/tree/main/module-21-async-client)
## Running the Example
```bash theme={null}
export GEMINI_API_KEY=your-key-here
./mvnw compile exec:java -pl module-21-async-client
```
## Next Module
[Module 22: Async Agent](/docs/acp-java-sdk/tutorial/22-async-agent) — build an agent with reactive handlers.
# 22 async agent
Source: https://lab.pollack.ai/docs/acp-java-sdk/tutorial/22-async-agent
# Module 22: Async Agent
The reactive, non-blocking version of Module 12 (Echo Agent).
## What You'll Learn
* `AcpAgent.async()` instead of `AcpAgent.sync()`
* Handlers returning `Mono` instead of `T`
* Chaining `sendMessage()` with `then()` before returning the response
* Agent lifecycle: `start().block()` + `awaitTermination().block()`
## The Code
```java theme={null}
AcpAsyncAgent agent = AcpAgent.async(transport)
// Returns Mono
.initializeHandler(req ->
Mono.just(InitializeResponse.ok()))
// Returns Mono
.newSessionHandler(req ->
Mono.just(new NewSessionResponse(
UUID.randomUUID().toString(), null, null)))
// Returns Mono
.promptHandler((req, context) -> {
String text = req.text();
// sendMessage() returns Mono — must chain with then()
return context.sendMessage("Async Echo: " + text)
.then(Mono.just(PromptResponse.endTurn()));
})
.build();
// Start and block until transport closes
agent.start().block();
agent.awaitTermination().block();
```
## Sync vs Async Agent Comparison
| Aspect | `AcpAgent.sync()` | `AcpAgent.async()` |
| --------------- | ----------------- | ------------------------------------------------------ |
| Handler return | `T` | `Mono` |
| `sendMessage()` | `void` (blocking) | `Mono` (must chain) |
| Lifecycle | `agent.run()` | `agent.start().block()` + `awaitTermination().block()` |
The critical difference: in the async agent, `context.sendMessage()` returns `Mono`. You must chain it with `.then()` before returning the response `Mono`. If you skip the chain, the message won't be sent before the response.
```java theme={null}
// Wrong — message may not be sent before response
context.sendMessage("text");
return Mono.just(PromptResponse.endTurn());
// Right — message is sent, then response follows
return context.sendMessage("text")
.then(Mono.just(PromptResponse.endTurn()));
```
## Source Code
[View on GitHub](https://github.com/markpollack/acp-java-tutorial/tree/main/module-22-async-agent)
## Running the Example
```bash theme={null}
./mvnw package -pl module-22-async-agent -q
./mvnw exec:java -pl module-22-async-agent
```
## Key Points
* **No API key** — the async agent runs entirely locally, same as Module 12
* **Same protocol** — sync and async agents are interchangeable from the client's perspective
* **Reactive integration** — async agents work naturally with Project Reactor, R2DBC, and other reactive libraries
## Previous Module
[Module 21: Async Client](/docs/acp-java-sdk/tutorial/21-async-client) — the reactive client API.
# 23 spring boot agent
Source: https://lab.pollack.ai/docs/acp-java-sdk/tutorial/23-spring-boot-agent
# Module 23: Spring Boot Agent
Build an ACP agent as a Spring Boot application. No manual transport or lifecycle wiring required.
## Prerequisites
* Java 21+ (Spring Boot 4.x requirement)
* Completed [Module 12: Echo Agent](/docs/acp-java-sdk/tutorial/12-echo-agent)
## What You'll Learn
* Using `@AcpAgent` annotations with Spring Boot autoconfiguration
* How the starter eliminates boilerplate transport and lifecycle code
* Redirecting logging to stderr for stdio agents
## Dependencies
Add the ACP Spring Boot Starter:
```xml theme={null}
org.springaicommunityacp-spring-boot-starter0.11.1
```
## The Agent
Compare this with [Module 12's builder-based agent](/docs/acp-java-sdk/tutorial/12-echo-agent). The annotation approach replaces the builder chain with annotated methods on a Spring bean:
```java theme={null}
@Component
@AcpAgent(name = "echo-agent", version = "1.0")
public class EchoAgentBean {
@Initialize
public InitializeResponse initialize(InitializeRequest request) {
return InitializeResponse.ok();
}
@NewSession
public NewSessionResponse newSession(NewSessionRequest request) {
return new NewSessionResponse(UUID.randomUUID().toString(), null, null);
}
@Prompt
public PromptResponse prompt(PromptRequest request, SyncPromptContext context) {
context.sendMessage("Echo: " + request.text());
return PromptResponse.endTurn();
}
}
```
The application class is a standard `@SpringBootApplication`:
```java theme={null}
@SpringBootApplication
public class EchoAgentApplication {
public static void main(String[] args) {
SpringApplication.run(EchoAgentApplication.class, args);
}
}
```
## What the Autoconfiguration Does
When Spring Boot starts, the ACP autoconfiguration:
1. **Creates a `StdioAcpAgentTransport`** — the default for agents (reads stdin, writes stdout)
2. **Discovers the `@AcpAgent` bean** — scans the application context for exactly one `@AcpAgent`-annotated bean
3. **Wires through `AcpAgentSupport`** — resolves `@Initialize`, `@NewSession`, `@Prompt` handler methods
4. **Starts via `SmartLifecycle`** — the agent starts after the application context refreshes and stops on shutdown
No explicit `agent.run()` call. No manual transport creation. Spring manages it all.
## Stdio and Logging
Agent stdout is reserved for the JSON-RPC protocol. Spring Boot's default logging writes to stdout, which would corrupt the protocol stream.
Three configuration changes fix this:
**application.properties:**
```properties theme={null}
# Disable banner — stdout is reserved for JSON-RPC
spring.main.banner-mode=off
# Keep the JVM alive (no web server to block)
spring.main.keep-alive=true
```
The `keep-alive` setting is essential. Without it, the Spring Boot application starts the agent, then exits immediately because there's no web server keeping the JVM alive.
**logback-spring.xml:**
```xml theme={null}
System.err%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
```
## Build & Run
```bash theme={null}
# Package the Spring Boot agent
./mvnw package -pl module-23-spring-boot-agent -q
# Run the demo (launches agent as subprocess and talks to it)
./mvnw exec:java -pl module-23-spring-boot-agent
```
## Module 12 vs Module 23
| Aspect | Module 12 (Builder) | Module 23 (Spring Boot) |
| ------------- | ------------------------------------- | ---------------------------- |
| Transport | Manual `new StdioAcpAgentTransport()` | Autoconfigured |
| Handlers | Lambda callbacks via builder | Annotated methods on a bean |
| Lifecycle | Explicit `agent.run()` | `SmartLifecycle` (automatic) |
| Configuration | Hardcoded in Java | `application.properties` |
| Dependencies | `acp-core` only | `acp-spring-boot-starter` |
## Configuration Properties
| Property | Default | Description |
| ---------------------------------- | ------- | --------------------------------------- |
| `spring.acp.agent.enabled` | `true` | Enable/disable agent autoconfiguration |
| `spring.acp.agent.request-timeout` | `60s` | Request processing timeout |
| `spring.acp.agent.transport.type` | `stdio` | Transport type (currently only `stdio`) |
## Next
[Module 24: Spring Boot Client](/docs/acp-java-sdk/tutorial/24-spring-boot-client) — use the autoconfigured client to connect to agents.
[View on GitHub](https://github.com/markpollack/acp-java-tutorial/tree/main/module-23-spring-boot-agent)
# 24 spring boot client
Source: https://lab.pollack.ai/docs/acp-java-sdk/tutorial/24-spring-boot-client
# Module 24: Spring Boot Client
Use the autoconfigured ACP client in a Spring Boot application. Connect to agents with just properties — no manual transport or client construction.
## Prerequisites
* Java 21+ (Spring Boot 4.x requirement)
* [Module 23: Spring Boot Agent](/docs/acp-java-sdk/tutorial/23-spring-boot-agent) built and available
## What You'll Learn
* Injecting `AcpSyncClient` from autoconfiguration
* Configuring the client transport via `application.properties`
* Property-driven transport selection (stdio vs WebSocket)
## Dependencies
Same starter as the agent side:
```xml theme={null}
org.springaicommunityacp-spring-boot-starter0.11.1
```
## The Client
The autoconfigured `AcpSyncClient` is injected like any Spring bean:
```java theme={null}
@SpringBootApplication
public class ClientApplication {
public static void main(String[] args) {
SpringApplication.run(ClientApplication.class, args);
}
@Bean
CommandLineRunner demo(AcpSyncClient client) {
return args -> {
// Initialize the connection
client.initialize();
// Create a session
String cwd = System.getProperty("user.dir");
var session = client.newSession(new NewSessionRequest(cwd, List.of()));
// Send a prompt
var response = client.prompt(new PromptRequest(
session.sessionId(),
List.of(new TextContent("Hello from Spring Boot client!"))));
System.out.println("Stop reason: " + response.stopReason());
};
}
}
```
## Configuration
**application.properties:**
```properties theme={null}
# Connect to the module-23 agent via stdio transport
spring.acp.client.transport.stdio.command=java
spring.acp.client.transport.stdio.args=-jar,module-23-spring-boot-agent/target/module-23-spring-boot-agent-1.0.0-SNAPSHOT.jar
# Increase timeout for Spring Boot agent startup
spring.acp.client.request-timeout=60s
```
The autoconfiguration detects the `stdio.command` property and creates a `StdioAcpClientTransport` that launches the agent as a subprocess.
## What the Autoconfiguration Does
1. **Detects transport properties** — `stdio.command` triggers stdio transport; `websocket.uri` triggers WebSocket
2. **Creates `AcpSyncClient` and `AcpAsyncClient`** — configured with timeout and client capabilities
3. **Manages shutdown** — `DisposableBean` calls `closeGracefully()` on context close
## Transport Selection
The autoconfiguration picks the transport based on which properties are set:
| Properties Set | Transport Created |
| -------------------------------------------- | ----------------------------- |
| `spring.acp.client.transport.stdio.command` | `StdioAcpClientTransport` |
| `spring.acp.client.transport.websocket.uri` | `WebSocketAcpClientTransport` |
| `spring.acp.client.transport.type=stdio` | Explicit stdio selection |
| `spring.acp.client.transport.type=websocket` | Explicit WebSocket selection |
## Build & Run
```bash theme={null}
# Build both the agent and client
./mvnw package -pl module-23-spring-boot-agent,module-24-spring-boot-client -q
# Run the client (from repo root)
./mvnw spring-boot:run -pl module-24-spring-boot-client
```
## Client Configuration Properties
| Property | Default | Description |
| ------------------------------------------------------- | ----------- | ------------------------------- |
| `spring.acp.client.request-timeout` | `30s` | Request timeout |
| `spring.acp.client.transport.type` | auto-detect | `stdio` or `websocket` |
| `spring.acp.client.transport.stdio.command` | — | Command to launch agent |
| `spring.acp.client.transport.stdio.args` | — | Command arguments |
| `spring.acp.client.transport.stdio.env.*` | — | Environment variables |
| `spring.acp.client.transport.websocket.uri` | — | WebSocket URI |
| `spring.acp.client.transport.websocket.connect-timeout` | `10s` | Connection timeout |
| `spring.acp.client.capabilities.read-text-file` | `true` | Advertise file read capability |
| `spring.acp.client.capabilities.write-text-file` | `true` | Advertise file write capability |
| `spring.acp.client.capabilities.terminal` | `false` | Advertise terminal capability |
[View on GitHub](https://github.com/markpollack/acp-java-tutorial/tree/main/module-24-spring-boot-client)
# 28 zed integration
Source: https://lab.pollack.ai/docs/acp-java-sdk/tutorial/28-zed-integration
# Module 28: Zed Integration
Run your Java ACP agent inside the Zed editor.
## What You'll Learn
* Building an agent JAR for editor integration
* Configuring Zed to launch your agent
* The stdio transport mechanism editors use to communicate with agents
## Prerequisites
* [Zed editor](https://zed.dev/download) installed
* Java 17+
* Agent JAR built (see below)
## The Agent
The agent is the same sync builder pattern from Module 12, with conversational responses:
```java theme={null}
var transport = new StdioAcpAgentTransport();
AcpSyncAgent agent = AcpAgent.sync(transport)
.initializeHandler(req -> {
System.err.println("[ZedAgent] Received initialize request");
return InitializeResponse.ok();
})
.newSessionHandler(req -> {
System.err.println("[ZedAgent] Creating session for cwd: " + req.cwd());
return new NewSessionResponse(
UUID.randomUUID().toString(), null, null);
})
.promptHandler((req, context) -> {
String promptText = req.text();
context.sendThought("Processing your request...");
String response = generateResponse(promptText);
context.sendMessage(response);
return PromptResponse.endTurn();
})
.build();
agent.run();
```
Logging goes to stderr because Zed captures stdout for the JSON-RPC protocol.
## Build and Configure
### 1. Build the JAR
```bash theme={null}
./mvnw package -pl module-28-zed-integration -q
```
### 2. Get the absolute path
```bash theme={null}
realpath module-28-zed-integration/target/zed-agent.jar
```
### 3. Configure Zed
Open Zed settings (`Ctrl+,` on Linux, `Cmd+,` on Mac) and add:
```json theme={null}
{
"agent_servers": {
"Java Tutorial Agent": {
"type": "custom",
"command": "java",
"args": ["-jar", "/absolute/path/to/zed-agent.jar"]
}
}
}
```
### 4. Use in Zed
Open the Agent Panel (`Ctrl+?`), click **+**, select **Java Tutorial Agent**, and start chatting.
## How It Works
Zed launches your agent as a subprocess and communicates via JSON-RPC over stdio. This is the same mechanism used by LSP language servers. Your agent receives `initialize`, `session/new`, and `session/prompt` requests, and responds with JSON-RPC responses and notifications.
No code changes are needed for different editors. The same JAR works in Zed, JetBrains, and VS Code.
## Source Code
[View on GitHub](https://github.com/markpollack/acp-java-tutorial/tree/main/module-28-zed-integration)
## Running the Example
```bash theme={null}
./mvnw package -pl module-28-zed-integration -q
./mvnw exec:java -pl module-28-zed-integration
```
## Next Module
[Module 29: JetBrains Integration](/docs/acp-java-sdk/tutorial/29-jetbrains-integration) — configure the same agent for IntelliJ, PyCharm, and other JetBrains IDEs.
# 29 jetbrains integration
Source: https://lab.pollack.ai/docs/acp-java-sdk/tutorial/29-jetbrains-integration
# Module 29: JetBrains Integration
Run your Java ACP agent in IntelliJ IDEA, PyCharm, WebStorm, and other JetBrains IDEs.
## What You'll Learn
* Configuring JetBrains IDEs for ACP agents
* The `~/.jetbrains/acp.json` configuration format
* MCP server access from JetBrains
## Prerequisites
* JetBrains IDE version 25.3 RC or later
* JetBrains AI Assistant plugin enabled
* Java 17+
## The Agent
Same code as Module 28 (Zed). The agent is identical — only the IDE configuration differs:
```java theme={null}
AcpSyncAgent agent = AcpAgent.sync(transport)
.initializeHandler(req -> InitializeResponse.ok())
.newSessionHandler(req ->
new NewSessionResponse(UUID.randomUUID().toString(), null, null))
.promptHandler((req, context) -> {
String promptText = /* extract from req */;
context.sendThought("Analyzing your request...");
context.sendMessage(generateResponse(promptText));
return PromptResponse.endTurn();
})
.build();
agent.run();
```
## Build and Configure
### 1. Build the JAR
```bash theme={null}
./mvnw package -pl module-29-jetbrains-integration -q
```
### 2. Configure JetBrains
Create or edit `~/.jetbrains/acp.json`:
```json theme={null}
{
"agent_servers": {
"Java Tutorial Agent": {
"command": "java",
"args": ["-jar", "/absolute/path/to/jetbrains-agent.jar"]
}
}
}
```
Or use the IDE: AI Chat tool window → gear icon → Configure ACP Agents.
### 3. Use the agent
Open AI Chat (`Alt+Shift+C`), select **Java Tutorial Agent** from the agent dropdown.
## Configuration Options
### With environment variables
```json theme={null}
{
"agent_servers": {
"Java Tutorial Agent": {
"command": "java",
"args": ["-jar", "/path/to/jetbrains-agent.jar"],
"env": {
"MY_API_KEY": "your-key-here"
}
}
}
}
```
### With IDE MCP server access
```json theme={null}
{
"agent_servers": {
"Java Tutorial Agent": {
"command": "java",
"args": ["-jar", "/path/to/jetbrains-agent.jar"],
"use_idea_mcp": true,
"use_custom_mcp": true
}
}
}
```
## Supported IDEs
All JetBrains IDEs with AI Assistant support ACP in version 25.3 RC and later: IntelliJ IDEA, PyCharm, WebStorm, GoLand, PhpStorm, Rider, CLion, RubyMine, DataGrip.
## Source Code
[View on GitHub](https://github.com/markpollack/acp-java-tutorial/tree/main/module-29-jetbrains-integration)
## Running the Example
```bash theme={null}
./mvnw package -pl module-29-jetbrains-integration -q
./mvnw exec:java -pl module-29-jetbrains-integration
```
## Next Module
[Module 30: VS Code Integration](/docs/acp-java-sdk/tutorial/30-vscode-integration) — connect to VS Code using the community extension.
# 30 vscode integration
Source: https://lab.pollack.ai/docs/acp-java-sdk/tutorial/30-vscode-integration
# Module 30: VS Code Integration
Run your Java ACP agent in VS Code using the community vscode-acp extension.
## What You'll Learn
* Installing the vscode-acp extension
* Creating a PATH-discoverable wrapper script
* The differences between VS Code, Zed, and JetBrains ACP configuration
## Prerequisites
* VS Code installed
* Java 17+
## The Agent
Same code as Modules 28-29. No code changes needed for different editors.
## Build and Configure
### 1. Install the vscode-acp extension
```bash theme={null}
code --install-extension omercnet.vscode-acp
```
Or search "VSCode ACP" in the Extensions panel (`Ctrl+Shift+X`).
### 2. Build the JAR
```bash theme={null}
./mvnw package -pl module-30-vscode-integration -q
```
### 3. Create a wrapper script
The extension auto-detects agents from your PATH. Create a wrapper:
**Linux/macOS:**
```bash theme={null}
cat > ~/.local/bin/java-tutorial-agent << 'EOF'
#!/bin/bash
exec java -jar /absolute/path/to/vscode-agent.jar "$@"
EOF
chmod +x ~/.local/bin/java-tutorial-agent
```
Replace `/absolute/path/to/vscode-agent.jar` with the output of:
```bash theme={null}
realpath module-30-vscode-integration/target/vscode-agent.jar
```
**Windows:**
Create `%USERPROFILE%\bin\java-tutorial-agent.cmd`:
```cmd theme={null}
@echo off
java -jar C:\path\to\vscode-agent.jar %*
```
### 4. Use in VS Code
Click the **VSCode ACP** icon in the Activity Bar, click **Connect**, select your agent.
## Configuration Comparison
| IDE | Configuration | Discovery |
| ------------- | ----------------------------------------------- | ----------------------- |
| **Zed** | `settings.json` — direct command + args | Explicit in settings |
| **JetBrains** | `~/.jetbrains/acp.json` — direct command + args | Explicit in config file |
| **VS Code** | PATH wrapper script | Auto-detected from PATH |
The same agent JAR works across all three editors. The only difference is how each editor finds and launches the agent.
## Native VS Code Support
VS Code does not have native ACP support yet. Microsoft is tracking it in [Issue #265496](https://github.com/microsoft/vscode/issues/265496). The community extension provides ACP functionality until native support arrives.
## Source Code
[View on GitHub](https://github.com/markpollack/acp-java-tutorial/tree/main/module-30-vscode-integration)
## Running the Example
```bash theme={null}
./mvnw package -pl module-30-vscode-integration -q
./mvnw exec:java -pl module-30-vscode-integration
```
# Tutorial
Source: https://lab.pollack.ai/docs/acp-java-sdk/tutorial/index
A progressive tutorial for learning the ACP Java SDK — from client basics to IDE integration.
A progressive, hands-on tutorial. Each module focuses on one concept and includes runnable source code.
## Prerequisites
* Java 17 or later
* Maven 3.8+ (or use the included `./mvnw` wrapper)
* For client modules (01-11, 21): [Gemini CLI](https://github.com/google-gemini/gemini-cli) with `--experimental-acp` flag, and a `GEMINI_API_KEY`. The tutorial uses Gemini as a real ACP agent to talk to — the SDK launches it as a subprocess and communicates over stdin/stdout.
* For agent modules (12-19, 22): no external dependencies. You build the agent and the tutorial provides a test client that launches it.
## Tutorial Structure
| Part | Modules | Topics |
| ---------------------- | ------- | -------------------------------------------------------------------------------------------------- |
| **1. Client Basics** | 01-11 | Connect, sessions, prompts, streaming, updates, file handlers, permissions, resume, cancel, errors |
| **2. Building Agents** | 12-19 | Echo agent, handlers, updates, requests, testing, capabilities, terminal, MCP |
| **3. Advanced** | 21-22 | Async client, async agent (Project Reactor) |
| **4. IDE Integration** | 28-30 | Zed, JetBrains, VS Code |
## Getting the Code
```bash theme={null}
git clone https://github.com/markpollack/acp-java-tutorial.git
cd acp-java-tutorial
./mvnw compile
```
## Running a Module
Agent modules run locally with no API key:
```bash theme={null}
./mvnw package -pl module-12-echo-agent -q
./mvnw exec:java -pl module-12-echo-agent
```
Client modules require `GEMINI_API_KEY`:
```bash theme={null}
export GEMINI_API_KEY=your-key-here
./mvnw exec:java -pl module-01-first-contact
```
## Three Agent API Styles
The SDK provides three ways to build agents:
| Style | Entry Point | Programming Model |
| -------------------- | ---------------------- | -------------------------------------- |
| **Annotation-based** | `@AcpAgent`, `@Prompt` | Declarative, least boilerplate |
| **Sync** | `AcpAgent.sync()` | Blocking handlers, plain return values |
| **Async** | `AcpAgent.async()` | Reactive, Project Reactor `Mono` |
The tutorial uses Sync for agent examples (most accessible to most developers) and introduces annotations and async where relevant.
## Start Learning
Begin with [Module 01: First Contact](/docs/acp-java-sdk/tutorial/01-first-contact) to connect to your first ACP agent.
Or jump to [Module 12: Echo Agent](/docs/acp-java-sdk/tutorial/12-echo-agent) to build an agent without any API key.
# What's New
Source: https://lab.pollack.ai/docs/acp-java-sdk/whats-new
Release notes for ACP Java SDK — auto-generated from git history
## 0.15.0 (2026-08-21)
Correctness and supply-chain hygiene. No protocol or public API changes — a drop-in upgrade from 0.14.0.
* Fix non-deterministic notification ordering — notifications are serialized through a sink drained by `concatMap`, preserving arrival order (thanks @ljiro)
* Drain queued notifications on graceful close instead of discarding them; `closeGracefully()` now waits for the drain, bounded by the session `requestTimeout`
* Distinguish expected from anomalous notification-sink emission failures in logging
* Security: Jackson 2.21.2 → 2.21.5 and Jetty 12.0.14 → 12.0.37, clearing 17 known advisories (5 high) in the published dependency closure
* Ship the verbatim Apache License 2.0, and package LICENSE and NOTICE under `META-INF` in every artifact
* Run the integration tests — three `*IT` classes existed but were never executed by `mvn verify`
* Pin GitHub Actions to commit SHAs; bound workflow jobs to 30 minutes
## 0.14.0 (2026-06-11)
* Docs: 0.14.0 release notes in README and CHANGELOG
* Add unstable providers/\* methods
* Add messageId overloads to prompt context send methods
* Promote session config-option API to stable
* Deprecate the session-model API for removal
* Add logout and session/delete agent methods
* Add additionalDirectories and per-chunk messageId fields
* Add WebSocket edge case tests, increase max message size to 4MB
* Fix WebSocket client transport echoing agent requests back to agent
* Update version references to 0.12.0, add snapshot instructions
* Bump version to 0.13.0-SNAPSHOT
## 0.12.0 (2026-05-25)
* Add tutorial cross-references for module-20 and module-31 in release notes
* Add elicitation decline and cancel integration tests
* Update README with versioning policy and 0.12.0-SNAPSHOT release notes
* Add session/fork and session/set\_config\_option (unstable)
* Add elicitation/create and elicitation/complete (unstable)
* Add @UnstableAcpApi stability marker annotation
* Add session/list, session/close, session/resume methods
* Bump version to 0.12.0-SNAPSHOT
## 0.11.0 (2026-05-12)
* Remove MCP SDK dependency, add own JSON abstraction layer
* Update .gitignore with local tool config
* Bump version to 0.11.0-SNAPSHOT
## 0.10.0 (2026-04-10)
* Restore Duration import removed by PR #1
* Add Implementation type with clientInfo/agentInfo fields
* Add UsageUpdate support to SessionUpdate types
* Add support for UsageUpdate in SessionUpdate interface and related tests
* Update README for 0.9.0 release
* Bump version to 0.10.0-SNAPSHOT
* style: remove unused imports
## 0.9.0 (2026-03-31)
* Initial release.
# Agent Configuration
Source: https://lab.pollack.ai/docs/agent-bench/agent-config
Configure a local CLI process as an Agent Bench agent
## Agent YAML Format
This page describes the agent process contract in the 0.6.0 release.
```yaml theme={null}
command:
timeout:
```
Agent Bench passes the command to `bash -c` with the task workspace as the working directory. The
command should read `INSTRUCTION.md`, change the workspace, and exit. It runs as a local host
process with the invoking user's permissions.
The workspace directory is an organizational boundary, not a security boundary. Agent Bench does
not provide container execution or isolation. Use your own disposable VM, CI runner, or other
externally managed isolation for untrusted agents or benchmark definitions. Benchmark `setup` and
`post` commands have the same host authority as the configured agent command.
## Examples
```yaml theme={null}
# Claude Code
command: claude --print --dangerously-skip-permissions "Read INSTRUCTION.md and follow the instructions precisely."
timeout: PT45M
```
```yaml theme={null}
# Gemini CLI
command: gemini -p "Read INSTRUCTION.md and follow the instructions."
timeout: PT30M
```
```yaml theme={null}
# Local script
command: ./my-agent.sh
timeout: PT10M
```
```bash theme={null}
#!/bin/bash
instruction=$(cat INSTRUCTION.md)
# Use the instruction in your agent logic.
printf '%s\n' 'Hello World!' > hello.txt
```
## Filesystem Contract
After `provide`, the workspace contains:
| File | Description |
| --------------------- | -------------------------------------------------- |
| `INSTRUCTION.md` | Task description |
| `.bench-context.yaml` | Benchmark, task, version, and timeout context |
| Source files | Workspace-template files when supplied by the task |
The external agent may create or modify files in this directory. Agent Judge implementations
inspect that result during grading.
## Optional Agent Journal
If the agent writes `journal.yaml`, Agent Bench records efficiency metrics with the trial:
```yaml theme={null}
schema: bench.journal.v1
totalTurns: 8
totalInputTokens: 4000
totalOutputTokens: 2000
totalCostUsd: 0.12
durationMs: 15000
```
Agents without a journal are still graded; only the journal-derived efficiency fields are absent.
## Optional Trajectory Reference
If the agent writes `trajectory-ref.txt` containing a path or URI, Agent Bench copies the reference
into the trial result for later analysis. Agent Bench does not upload or manage the referenced
trace.
# CLI Reference
Source: https://lab.pollack.ai/docs/agent-bench/cli-reference
Agent Bench core and agents-module entry points, commands, and flags
## Entry Points
The 0.6.0 release publishes both CLI entry points. Run them from a `v0.6.0` source checkout:
```bash theme={null}
# BenchMain: deterministic core CLI
./mvnw -q -pl agent-bench-core exec:java -Dexec.args=" [flags]"
# BenchApp: core commands plus the real Agent Client-backed LLM judge for run/resume
./mvnw -q -pl agent-bench-agents exec:java -Dexec.args=" [flags]"
```
`BenchMain` registers Agent Judge's deterministic file, build, and coverage judges. During `run`
and `resume`, its `test-quality-llm` registration abstains because no model-backed judge is present.
`BenchApp` replaces that registration with `TestQualityJudge`, backed by Agent Client and the Claude
agent implementation. Other commands delegate to the core CLI.
All benchmark setup, post-processing, and configured agent commands execute as local host processes
with the invoking user's permissions. Workspaces do not provide isolation.
## Discovery Commands
### `list`
```bash theme={null}
./mvnw -q -pl agent-bench-core exec:java -Dexec.args="list"
```
### `tasks`
```bash theme={null}
./mvnw -q -pl agent-bench-core exec:java \
-Dexec.args="tasks --benchmark hello-world"
```
`--benchmark` is required.
## End-to-End Commands
### `run`
Prepares each selected task, runs setup scripts and the configured agent, runs post-processing, and
grades the result.
```bash theme={null}
./mvnw -q -pl agent-bench-agents exec:java \
-Dexec.args="run --benchmark code-coverage --agent agents/claude-code.yaml"
```
| Flag | Required | Description |
| -------------- | -------- | -------------------------------------------------------- |
| `--benchmark` | Yes | Benchmark name |
| `--agent` | No | Agent config YAML; omission creates a manual-mode result |
| `--task` | No | Run one task ID |
| `--difficulty` | No | Filter tasks by `easy`, `medium`, or `hard` |
### `resume`
Resumes `runs/`, preserving completed trial results and running missing trials.
```bash theme={null}
./mvnw -q -pl agent-bench-agents exec:java \
-Dexec.args="resume --run-id 386aba4a-4285-45d2-bbf4-a159c00c3f3b"
```
`--run-id` is required.
### `compare`
```bash theme={null}
./mvnw -q -pl agent-bench-core exec:java \
-Dexec.args="compare --runs runs/ runs/"
```
`--runs` accepts one or more run-directory paths and prints accuracy, pass\@k, cost, duration, and
trial-count comparisons.
## Split Workflow
### `provide`
Creates the workspace, copies an optional template, and writes `INSTRUCTION.md` and
`.bench-context.yaml`.
```bash theme={null}
./mvnw -q -pl agent-bench-core exec:java \
-Dexec.args="provide --benchmark hello-world --task hello-world --workspace /tmp/ws"
```
### `grade`
Evaluates an existing workspace with the benchmark's deterministic Agent Judge configuration.
```bash theme={null}
./mvnw -q -pl agent-bench-core exec:java \
-Dexec.args="grade --benchmark hello-world --task hello-world --workspace /tmp/ws"
```
Both commands require `--benchmark` and `--workspace`; `--task` is optional only for a single-task
benchmark. The built-in split CLI is for deterministic juries. Use agents-module `run` or `resume`
when the jury includes the real `test-quality-llm` judge.
## Output Structure
```text theme={null}
runs//
result.json
run-metadata.json
bench.lock
tasks/
/
result.json
workspace/
```
Trial failures use the result model's `FailureMode` values, including agent timeout/error, setup or
grade errors, build/test failures, context exhaustion, and unknown failures.
# Getting Started with Agent Bench
Source: https://lab.pollack.ai/docs/agent-bench/getting-started
Run a benchmark end-to-end, split the agent step, or use the Java API
## Prerequisites
* Java 21
* An AI coding agent with a CLI for agent-driven runs
Clone the repository, select the 0.6.0 release tag, and run its verified build:
```bash theme={null}
git clone https://github.com/markpollack/agent-bench.git
cd agent-bench
git checkout v0.6.0
./mvnw clean verify
```
Benchmark setup, post-processing, and configured agent commands run as local host processes with
your permissions. A workspace directory organizes files; it does not isolate them. For untrusted
benchmark definitions or agents, supply your own disposable VM, CI runner, or other externally
managed isolation.
## Pattern 1: End-to-End `run`
Create an agent configuration:
```yaml theme={null}
# agents/my-agent.yaml
command: claude --print --dangerously-skip-permissions "Read INSTRUCTION.md and follow the instructions precisely."
timeout: PT5M
```
Run the deterministic hello-world benchmark through the core CLI:
```bash theme={null}
./mvnw -q -pl agent-bench-core exec:java \
-Dexec.args="run --benchmark hello-world --agent agents/my-agent.yaml"
```
The lifecycle is `provide → setup → agent → post-processing → grade`. Results are written under
`runs//`, including aggregate metadata and the preserved task workspace.
For a jury containing `test-quality-llm`, use the agents-module entry point. Its `BenchApp` wires
the real Agent Client-backed judge:
```bash theme={null}
./mvnw -q -pl agent-bench-agents exec:java \
-Dexec.args="run --benchmark code-coverage --agent agents/my-agent.yaml"
```
That path may invoke a model provider and requires the corresponding CLI credentials and runtime.
## Pattern 2: `provide`, External Agent, `grade`
The split workflow lets another system own agent execution:
```bash theme={null}
./mvnw -q -pl agent-bench-core exec:java \
-Dexec.args="provide --benchmark hello-world --task hello-world --workspace /tmp/hello-bench"
cd /tmp/hello-bench
your-agent "Read INSTRUCTION.md and complete the task"
cd /path/to/agent-bench
./mvnw -q -pl agent-bench-core exec:java \
-Dexec.args="grade --benchmark hello-world --task hello-world --workspace /tmp/hello-bench"
```
`provide` writes `INSTRUCTION.md` and `.bench-context.yaml`; the external agent changes the
workspace; `grade` uses the benchmark's Agent Judge configuration. The split CLI path is intended
for deterministic juries. LLM-backed judging is wired for agents-module `run` and `resume`.
## Pattern 3: Java API
Applications can discover definitions and materialize the same Agent Judge jury directly:
```java theme={null}
BenchmarkCatalog catalog = new BenchmarkCatalog(Path.of("benchmarks"));
Benchmark benchmark = catalog.discover().stream()
.filter(candidate -> candidate.name().equals("hello-world"))
.findFirst()
.orElseThrow();
JudgeFactory factory = new JudgeFactory();
Judge judge = factory.createFromConfig(benchmark.juryConfig());
Judgment judgment = judge.judge(
JudgmentContext.builder().workspace(Path.of("/tmp/hello-bench")).build());
```
`RunCommand`, `ProvideCommand`, `GradeCommand`, `CompareCommand`, and the benchmark/result records
are also available for programmatic orchestration.
## Result Layout
```text theme={null}
runs//
result.json
run-metadata.json
bench.lock
tasks/
/
result.json
workspace/
```
Commands, timeouts, journals, and the trust boundary
Commands and entry-point differences
Agent Judge tiers, policies, and custom judges
# Jury System
Source: https://lab.pollack.ai/docs/agent-bench/jury-system
How Agent Bench grades task workspaces with Agent Judge
## Task, Agent, Verifier
Agent Bench follows a Terminal-Bench-inspired separation:
1. A task defines instructions, optional workspace material, setup/post-processing, and metadata.
2. An agent changes the prepared workspace.
3. A verifier grades the result.
The verifier is an [Agent Judge](/projects/agent-judge) `Judge` or jury created by `JudgeFactory`.
Agent Bench preserves Agent Judge's `Judgment`, jury, tier-policy, and voting semantics.
The 0.6.0 release retains deterministic judges, simple and cascaded juries, and the agents module's
Agent Client-backed LLM judge.
## Cascaded Tiers
| Policy | Behavior |
| -------------------- | ---------------------------------------------------- |
| `REJECT_ON_ANY_FAIL` | Stop the cascade when any check fails |
| `ACCEPT_ON_ALL_PASS` | Continue when all checks pass |
| `FINAL_TIER` | Use the last tier's aggregated result as the verdict |
```yaml theme={null}
jury:
tiers:
- name: build
policy: REJECT_ON_ANY_FAIL
checks:
- type: maven-build
goals: [clean, test]
- name: test-quality
policy: FINAL_TIER
checks:
- type: test-quality-llm
prompt: prompts/judge-practice-adherence.txt
model: claude-sonnet-4-6
```
## Registered Judge Types
| Type | Implementation | Purpose |
| ----------------------- | ------------------------------- | ------------------------------------------------------------------ |
| `file-exists` | Agent Judge | Verify a workspace path exists |
| `file-content` | Agent Judge | Compare file content exactly or by containment |
| `maven-build` | Agent Judge `BuildSuccessJudge` | Run Maven goals and require success |
| `coverage-preservation` | Agent Judge | Require coverage not to regress from the baseline |
| `coverage-improvement` | Agent Judge | Require coverage to meet a threshold |
| `test-quality-llm` | `agent-bench-agents` | Score practice adherence with a real Agent Client-backed LLM judge |
The core CLI uses deterministic Agent Judge implementations and an abstaining LLM registration for
end-to-end execution. The agents-module CLI registers `TestQualityJudge` for LLM-graded `run` and
`resume` operations.
## Custom Judges
Custom judges implement Agent Judge's `Judge` interface and can be registered without changing the
benchmark format:
```java theme={null}
public final class MyJudge implements Judge {
@Override
public Judgment judge(JudgmentContext context) {
return Judgment.builder()
.pass()
.reasoning("Workspace satisfies the custom rule")
.build();
}
}
JudgeFactory factory = new JudgeFactory();
factory.register("my-check", config -> new MyJudge());
```
## Benchmark and Task YAML
```yaml theme={null}
schema: bench.benchmark.v1
name: my-benchmark
version: "1.0"
default-timeout: PT10M
jury:
checks:
- type: file-exists
path: expected.txt
```
```yaml theme={null}
schema: bench.task.v1
id: my-task
difficulty: easy
instruction: |
Create expected.txt.
timeout: PT10M
metadata:
key: value
setup:
- "command before the agent"
post:
- "command after the agent"
```
Setup, post-processing, configured agents, and process-based judges execute locally with the
invoking user's permissions. A task workspace is not a security boundary; externally isolate
untrusted definitions or agents.
# What's New
Source: https://lab.pollack.ai/docs/agent-bench/whats-new
Release notes for Agent Bench
## 0.6.0 (2026-08-18)
* Removes the unused `DockerSandbox` API and the direct Testcontainers/docker-java dependency.
* Keeps local process execution, the `Sandbox` abstraction, `LocalSandbox`, Agent Judge grading,
the agents module, and its real Agent Client-backed LLM judge.
* Documents that workspaces are not security boundaries and recommends externally managed
isolation for untrusted benchmark definitions or agents.
* Aligns the dependency floors with Jackson 2.21.6, Jackson 3.1.6, Agent Judge 0.14.0,
and Agent Client 0.26.0.
* Publishes `agent-bench-core` and `agent-bench-agents` 0.6.0, plus the aggregate CycloneDX 1.6
SBOM.
## 0.5.0 (2026-08-17)
* Added the Spring Boot upgrade benchmark candidate and retained its owner-operated external
baseline-aware grader distinction.
* Published the root aggregate CycloneDX SBOM and aligned Agent Judge and Agent Client dependencies.
* Released two Maven modules: `agent-bench-core` and `agent-bench-agents`.
## 0.4.0 (2026-06-06)
* Aligned Jackson 2 and Agent Client dependencies.
## 0.3.0 (2026-05-19)
* Migrated to the `markpollack` organization, `io.github.markpollack` Maven coordinates, and the
Business Source License 1.1 for new development.
* Added Central metadata and documentation links.
## 0.2.1 (2026-03-31)
* Initial release.
# Defaults Philosophy
Source: https://lab.pollack.ai/docs/agent-client/explanation/defaults-philosophy
LOOSE vs STRICT mode — how AgentClient manages default permissiveness across providers
## The Problem
Each agentic CLI has its own safety controls with different defaults. Without coordination, some providers work out of the box while others block on preconditions that users don't expect:
| Provider | Autonomous Flag | Non-Git Directory | Out of Box? |
| ----------- | --------------- | ----------------------------- | ----------- |
| Claude Code | `yolo=true` | Works | Yes |
| Codex | `fullAuto=true` | **Blocked** by `skipGitCheck` | **No** |
| Gemini CLI | `yolo=true` | Works | Yes |
A user switching from Claude to Codex hits a wall on the simplest task — "create a file" — with no obvious fix.
## AgentClientMode
`AgentClientMode` is a portable enum that controls default permissiveness across all providers:
```java theme={null}
public enum AgentClientMode {
LOOSE, // Works out of the box in any directory
STRICT // Requires explicit opt-in to risky operations
}
```
**Default: `LOOSE`** — optimized for evaluation and development, where friction during onboarding is the primary failure mode.
### What Each Mode Does
Permissive defaults that minimize preconditions:
| Provider | Effect |
| -------- | -------------------------------------------- |
| Codex | `skipGitCheck=true` — works in any directory |
| Claude | No change (already permissive) |
| Gemini | No change (already permissive) |
Conservative defaults for production environments:
| Provider | Effect |
| -------- | ---------------------------------------------- |
| Codex | `skipGitCheck=false` — requires git repository |
| Claude | No change currently |
| Gemini | No change currently |
### Configuration
Set the mode via Spring properties:
```yaml theme={null}
agent-client:
codex:
mode: strict # or loose (default)
```
## Precedence Rules
Options are resolved in this order (first wins):
1. **Explicit goal options** — passed at call time via `AgentClient.run(goal, options)`
2. **Builder defaults** — set on `AgentClient.builder().defaultOptions()`
3. **Spring properties** — provider-specific values in `application.yml`
4. **Mode-derived defaults** — LOOSE or STRICT baseline
5. **Hardcoded defaults** — built into each provider's options class
### STRICT Is a Baseline, Not a Lock
Explicit property overrides **always** take precedence over mode-derived defaults:
```yaml theme={null}
agent-client:
codex:
mode: strict
skip-git-check: true # This wins — overrides STRICT's default
```
In this configuration, Codex gets STRICT defaults for everything **except** the git check, which the user explicitly enabled. This is intentional — if you set a property explicitly, you made a conscious choice.
If you set `mode: strict` expecting it to enforce all safety controls, audit your explicit property overrides. Any property set directly will pierce the mode.
### SDK Layer Stays Neutral
The mode system operates at the **agent-models** layer, not the SDK layer. Provider SDKs (`codex-cli-sdk`, `claude-agent-sdk`, `gemini-cli-sdk`) always reflect their CLI's native defaults. Direct SDK consumers are never affected by `AgentClientMode`.
This means:
* `ExecuteOptions.builder().build()` → `skipGitCheck=false` (Codex CLI native default)
* `CodexAgentProperties` with `mode=LOOSE` → `isSkipGitCheck()` returns `true` (mode-derived)
## Migration from Pre-0.14.0
Before 0.14.0, Codex defaulted to `skipGitCheck=false`. If you relied on this:
```yaml theme={null}
# Restore pre-0.14.0 Codex behavior
agent-client:
codex:
mode: strict
```
Or override the specific property:
```yaml theme={null}
agent-client:
codex:
skip-git-check: false
```
# Getting Started
Source: https://lab.pollack.ai/docs/agent-client/howto/getting-started
Run your first agent task with Spring AI Agent Client in under 5 minutes
## Prerequisites
* Java 17+
* Maven 3.9+ (or use the Maven wrapper)
* A CLI agent installed and authenticated:
* **Claude**: `claude auth login` ([install guide](https://docs.anthropic.com/en/docs/claude-code))
* **Codex**: `export OPENAI_API_KEY=sk-...` ([install guide](https://github.com/openai/codex))
* **Gemini**: `export GEMINI_API_KEY=...` ([install guide](https://github.com/google-gemini/gemini-cli))
## 1. Add the Dependency
Pick your provider. You need the agent model module for your provider:
```xml theme={null}
io.github.markpollackagent-claude0.29.0
```
```xml theme={null}
io.github.markpollackagent-codex0.29.0
```
```xml theme={null}
io.github.markpollackagent-gemini0.29.0
```
## 2. Create an Agent and Run a Task
No Spring Boot required. Build a model, create a client, run a goal:
```java theme={null}
import io.github.markpollack.agents.client.AgentClient;
import io.github.markpollack.agents.client.AgentClientResponse;
import io.github.markpollack.agents.claude.ClaudeAgentModel;
import io.github.markpollack.agents.claude.ClaudeAgentOptions;
public class HelloAgent {
public static void main(String[] args) {
ClaudeAgentOptions options = ClaudeAgentOptions.builder()
.model("claude-sonnet-4-5")
.yolo(true)
.build();
ClaudeAgentModel model = ClaudeAgentModel.builder()
.defaultOptions(options)
.build();
AgentClient client = AgentClient.create(model);
AgentClientResponse response = client.run(
"Create a file named hello.txt with 'Hello from Agent Client!'"
);
System.out.println("Success: " + response.isSuccessful());
}
}
```
```java theme={null}
import io.github.markpollack.agents.client.AgentClient;
import io.github.markpollack.agents.client.AgentClientResponse;
import io.github.markpollack.agents.codex.CodexAgentModel;
import io.github.markpollack.agents.codex.CodexAgentOptions;
import io.github.markpollack.agents.codexsdk.CodexClient;
public class HelloAgent {
public static void main(String[] args) {
CodexAgentOptions options = CodexAgentOptions.builder()
.model("gpt-5-codex")
.skipGitCheck(true)
.build();
CodexAgentModel model = new CodexAgentModel(
CodexClient.create(), options, null
);
AgentClient client = AgentClient.create(model);
AgentClientResponse response = client.run(
"Create a file named hello.txt with 'Hello from Agent Client!'"
);
System.out.println("Success: " + response.isSuccessful());
}
}
```
```java theme={null}
import io.github.markpollack.agents.client.AgentClient;
import io.github.markpollack.agents.client.AgentClientResponse;
import io.github.markpollack.agents.gemini.GeminiAgentModel;
import io.github.markpollack.agents.gemini.GeminiAgentOptions;
import io.github.markpollack.agents.geminisdk.GeminiClient;
public class HelloAgent {
public static void main(String[] args) {
GeminiAgentOptions options = GeminiAgentOptions.builder()
.model("gemini-2.5-flash")
.yolo(true)
.build();
GeminiAgentModel model = new GeminiAgentModel(
GeminiClient.create(), options, null
);
AgentClient client = AgentClient.create(model);
AgentClientResponse response = client.run(
"Create a file named hello.txt with 'Hello from Agent Client!'"
);
System.out.println("Success: " + response.isSuccessful());
}
}
```
## 3. Run It
```bash theme={null}
mvn compile exec:java -Dexec.mainClass="HelloAgent"
```
The agent will create `hello.txt` in your working directory.
## With Spring Boot
If you're building a Spring Boot application, use the starter dependencies instead. They auto-configure the `AgentApi` and provide an injectable `AgentClient.Builder`:
```xml theme={null}
io.github.markpollackagent-starter-claude0.29.0
```
```java theme={null}
@Component
public class MyAgentRunner implements CommandLineRunner {
private final AgentClient.Builder agentClientBuilder;
public MyAgentRunner(AgentClient.Builder agentClientBuilder) {
this.agentClientBuilder = agentClientBuilder;
}
@Override
public void run(String... args) {
AgentClient client = agentClientBuilder.build();
AgentClientResponse response = client.run("Create hello.txt");
System.out.println("Success: " + response.isSuccessful());
}
}
```
Configuration goes in `application.yml`:
```yaml theme={null}
agent-client:
claude:
model: claude-sonnet-4-5
yolo: true
```
## Next Steps
* [Switching Providers](/docs/agent-client/howto/switching-providers) — Run the same task with different providers
* [Structured Output](/docs/agent-client/howto/structured-output) — Get structured JSON responses from agents
* [Configuration Reference](/docs/agent-client/reference/portable-options) — All available configuration options
* [Tutorial: First Task](/docs/agent-client/tutorial/01-first-task) — Step-by-step walkthrough with verification
# Structured Output
Source: https://lab.pollack.ai/docs/agent-client/howto/structured-output
How to get structured JSON responses from agent tasks using JSON Schema
## The Problem
You want the agent to return structured data (not free-form text) so your application can parse and act on the result programmatically.
## Solution: JSON Schema
Pass a JSON schema to the agent via Spring properties or the fluent API. The agent constrains its output to match the schema.
### Via Spring Properties
```yaml theme={null}
agent-client:
claude:
json-schema:
type: object
properties:
summary:
type: string
description: "A brief summary of what was done"
files_changed:
type: array
items:
type: string
description: "List of files that were created or modified"
success:
type: boolean
required:
- summary
- success
```
### Via the Fluent API
```java theme={null}
AgentClient agentClient = agentClientBuilder.build();
AgentClientResponse response = agentClient.goal("Analyze the project structure")
.workingDirectory(projectDir)
.run();
// Parse the structured response
String result = response.getResult();
JsonNode json = objectMapper.readTree(result);
String summary = json.get("summary").asText();
```
**Provider support varies.** JSON schema structured output is currently supported by the **Claude** provider. Codex and Gemini may return free-form text even when a schema is configured. Check the provider-specific reference pages for current support.
## Provider-Specific Details
| Provider | Structured Output | Configuration |
| -------- | -------------------------------------- | ----------------------------------------------------------------------------------- |
| Claude | JSON Schema via `json-schema` property | [Claude Reference](/docs/agent-client/reference/claude-reference#structured-output) |
| Codex | Not currently supported | — |
| Gemini | Not currently supported | — |
# Switching Providers
Source: https://lab.pollack.ai/docs/agent-client/howto/switching-providers
How to run the same agent task with Claude, Codex, or Gemini using Maven profiles
## The Problem
You want to run the same agent task with different providers without changing your Java code.
## Solution: Maven Profiles
Define one Maven profile per provider. Each profile puts a different starter on the classpath. Your Java code uses only the portable `AgentClient` API.
### 1. Define Profiles in pom.xml
```xml theme={null}
claudetrueio.github.markpollackagent-starter-claude${agent-client.version}codexio.github.markpollackagent-starter-codex${agent-client.version}geminiio.github.markpollackagent-starter-gemini${agent-client.version}
```
### 2. Add Per-Provider Configuration
Create profile-specific YAML files alongside your main `application.yml`:
```yaml theme={null}
agent-client:
claude:
model: claude-sonnet-4-5
yolo: true
```
```yaml theme={null}
agent-client:
codex:
model: gpt-5-codex
full-auto: true
```
```yaml theme={null}
agent-client:
gemini:
model: gemini-2.5-flash
yolo: true
```
Activate the matching Spring profile in your main `application.yml`:
```yaml theme={null}
spring:
profiles:
active: claude # Matches the Maven profile
```
### 3. Run with a Specific Provider
```bash theme={null}
# Claude (default profile)
./mvnw spring-boot:run
# Codex
./mvnw spring-boot:run -Pcodex -P'!claude' \
-Dspring-boot.run.arguments="--spring.profiles.active=codex"
# Gemini
./mvnw spring-boot:run -Pgemini -P'!claude' \
-Dspring-boot.run.arguments="--spring.profiles.active=gemini"
```
Use `-P'!claude'` to deactivate the default Claude profile when switching to another provider. Only one provider starter should be on the classpath.
## Your Java Code Stays the Same
The key design principle: **your Java code never imports provider-specific classes**. It only uses:
* `AgentClient` / `AgentClient.Builder`
* `AgentClientResponse`
* `AgentOptions` (for portable options)
Provider selection happens entirely through Maven profiles and Spring configuration.
## Working Example
See [samples/create-file-multi-provider](https://github.com/markpollack/agent-client/tree/main/samples/create-file-multi-provider) for a complete working example, or the [agent-client-tutorial](https://github.com/markpollack/agent-client-tutorial) for progressive examples.
# Antigravity Reference
Source: https://lab.pollack.ai/docs/agent-client/reference/antigravity-reference
Complete configuration reference for the Google Antigravity agent provider
## Overview
The Antigravity agent wraps the [Google Antigravity CLI](https://antigravity.google) via `AntigravityAgentModel`. The binary is `agy`. Configure it through Spring properties under `agent-client.antigravity.*`.
```yaml theme={null}
agent-client:
antigravity:
model: gemini-3.1-pro-high
timeout: PT15M
```
## Configuration Properties
Prefix: `agent-client.antigravity`
| Property | Type | Default | Description |
| ------------------------------ | ----------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `mode` | `AgentClientMode` | — | Controls default permissiveness. Inherits from `agent-client.mode` if not set (default: `LOOSE`). |
| `model` | `String` | `gemini-3.1-pro-high` | Model slug (`--model`). List them with `agy models`. |
| `effort` | `String` | — | Reasoning effort (`--effort`): `low`, `medium`, `high`. **Ignored when the model slug already encodes it** — see below. |
| `timeout` | `Duration` | `15m` | Timeout. Also drives `--print-timeout`. |
| `dangerously-skip-permissions` | `Boolean` | — | `--dangerously-skip-permissions`. When not explicitly set, derived from mode. |
| `execution-mode` | `ExecutionMode` | — | `--mode`: `accept-edits` or `plan` |
| `sandbox` | `boolean` | `false` | `--sandbox` — terminal restrictions |
| `executable-path` | `String` | — | Path to `agy` (auto-discovered if not set) |
## Effort is encoded in the model slug
Antigravity names effort twice: in `--effort` and in slugs like `gemini-3.1-pro-high`. Passing both is a **hard error**:
```
invalid model selection (--model "gemini-3.1-pro-high" --effort "low"):
--model gemini-3.1-pro-high conflicts with --effort=low
```
The run fails immediately with `"status": "ERROR"` and no output. So when the slug ends in `-low`, `-medium` or `-high`, the provider drops the flag and the slug wins. Portable `getEffort()` therefore behaves here exactly as it does for every other provider, and a caller does not need to know that this one CLI names effort twice.
## dangerouslySkipPermissions and Mode Interaction
| `dangerously-skip-permissions` | `mode` | Effective Value |
| ------------------------------ | -------- | --------------- |
| explicit | any | as set |
| unset | `STRICT` | `false` |
| unset | `LOOSE` | `true` |
Declining to auto-approve does not do what the flag name suggests. Antigravity's headless default is not to stop on an unapprovable tool call — it is to **refuse that call and carry on**. A `STRICT` unattended run is therefore not safer, it is quietly partial. It remains the default only because a caller that has not thought about permissions should not be handed unrestricted execution, and because the provider reports the refusals rather than swallowing them.
## Detecting a refused run
The provider scans both the envelope's `error` field and stderr for refusal notices, and surfaces the result:
| Field | Notes |
| ------------------- | ---------------------------------------------- |
| `softDenied` | `true` when at least one tool call was refused |
| `permissionNotices` | The refusal messages, verbatim |
The documentation says these notices go to stderr. agy 1.1.13 puts some of them in the envelope's `error` field with stderr empty, and others on stderr with `status: CANCELED`. Both channels are read.
## The status field is not the verdict
In agy 1.1.13, `status` behaves like a sticky execution-error indicator, not a verdict on the completed task. A tool call that fails can leave the terminal envelope at `"status":"ERROR"` even after later tool calls complete and the agent produces a valid review. Conversely, a refused tool call has been observed with `"status":"SUCCESS"` and no response. Trusting the field discards good recovered work; ignoring it hides real failures.
`isSuccessful()` therefore requires all of the following:
* the process exited with code 0
* the run produced a non-empty response
* no tool call was refused
* the stream contains no tool error left unrecovered by a later completed tool call
An `ERROR` envelope without stream evidence is treated conservatively as unrecovered. The CLI's own account remains available as `status`, `reportedSuccessful` and `error`; `softDenied` and `unrecoveredError` expose the two independent reasons the derived result can reject it.
## Portable Option Mapping
| Portable option | Antigravity flag |
| ------------------------- | ------------------------------------------------------------- |
| `getModel()` | `--model` |
| `getEffort()` | `--effort`, **dropped when the model slug encodes effort** |
| `getJsonSchema()` | `--json-schema` (serialized inline) |
| `isAutoApprove()` | `--dangerously-skip-permissions` |
| `getTimeout()` | process timeout **and** `--print-timeout` |
| `getWorkingDirectory()` | `--add-dir` **and** the process working directory — see below |
| `getSystemInstructions()` | prepended to the goal (no system-prompt flag exists) |
| `getMaxTurns()` | not supported |
`--print-timeout` defaults to five minutes, well short of a real task, so it is always set from the caller's timeout. Left alone, a truncated run would be indistinguishable from the agent finishing early.
## The working directory must be declared, not just set
There is no `--cwd`, but setting the process working directory is **not sufficient**. With no
active workspace, `agy` diverts every write to a shared scratch directory under
`~/.gemini/antigravity-cli/` and still reports success. The CLI says so plainly in its own
output — *"I placed it in your scratch directory … since there wasn't an active workspace"* —
and artifacts from earlier runs accumulate there, so work leaks between runs.
Reads resolve against the process working directory, which is why a narrow check looks fine
while writes are going somewhere else entirely.
The provider therefore declares the working directory with `--add-dir` as well as setting the
process cwd. Both are required.
This was found by running the provider parity TCK, not by reading the CLI's documentation. A
run that writes to the wrong place and reports success is the failure mode this provider is
most exposed to — see also soft denial below.
## Result Metadata
`getProviderFields()` carries `inputTokens`, `outputTokens`, `thinkingTokens`, `cacheReadTokens`, `totalTokens`, `numTurns`, `status`, `reportedSuccessful`, `error`, `softDenied`, `unrecoveredError`, `permissionNotices`, `exitCode` and `structured`. `getSessionId()` carries the conversation id, which `AntigravityClient.resume(conversationId, ...)` accepts via `--conversation`.
## Availability
Since **0.28.0**.
## Installation
Install from [antigravity.google](https://antigravity.google) — the npm distribution is discontinued — and authenticate once interactively. Discovery checks `ANTIGRAVITY_CLI_PATH`, then `which agy`, then `~/.local/bin/agy`.
Support files live under `~/.gemini/antigravity-cli`, shared with the Gemini CLI, so that directory existing proves nothing about `agy` being installed.
# Claude Reference
Source: https://lab.pollack.ai/docs/agent-client/reference/claude-reference
Complete configuration reference for the Claude Code agent provider
## Overview
The Claude agent wraps the [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) via `ClaudeAgentModel`. Configure it through Spring properties under `agent-client.claude.*`.
```yaml theme={null}
agent-client:
claude:
model: claude-sonnet-4-5
timeout: PT5M
yolo: true
```
## Configuration Properties
Prefix: `agent-client.claude`
| Property | Type | Default | Description |
| ---------------------- | -------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `model` | `String` | `claude-sonnet-4-5` | Claude model to use for agent tasks |
| `timeout` | `Duration` | `5m` | Timeout for agent task execution |
| `yolo` | `boolean` | `true` | Bypass all permission checks |
| `executable-path` | `String` | — | Path to the Claude CLI executable (auto-discovered if not set) |
| `effort` | `String` | — | Reasoning effort level (`--effort`): `low`, `medium`, `high`, `xhigh`, `max`. Overrides the portable `effort` when both are set |
| `max-thinking-tokens` | `Integer` | — | Maximum thinking tokens for extended thinking mode |
| `system-prompt` | `String` | — | System prompt to use for the agent |
| `allowed-tools` | `List` | `[]` | Tools that are allowed to be used |
| `disallowed-tools` | `List` | `[]` | Tools that are not allowed to be used |
| `permission-mode` | `String` | — | Permission mode for tool execution (overrides `yolo` if set) |
| `json-schema` | `Map` | — | JSON schema for structured output |
| `max-tokens` | `Integer` | — | Maximum tokens for the response |
| `max-turns` | `Integer` | — | Maximum number of agentic turns before stopping |
| `max-budget-usd` | `Double` | — | Maximum budget in USD before stopping |
| `fallback-model` | `String` | — | Fallback model if the primary model is unavailable |
| `append-system-prompt` | `String` | — | Additional text appended to the default system prompt |
### Advanced Options
These options provide full parity with the Claude Code Python SDK:
| Property | Type | Default | Description |
| ----------------------------- | -------------------- | ------- | ----------------------------------------------------------------- |
| `add-dirs` | `List` | `[]` | Additional directories to include in Claude's context |
| `settings` | `String` | — | Custom settings file path |
| `permission-prompt-tool-name` | `String` | — | Permission prompt tool name for interactive permission handling |
| `extra-args` | `Map` | — | Arbitrary extra CLI arguments (keys are flag names without `--`) |
| `env` | `Map` | — | Custom environment variables for the CLI process |
| `max-buffer-size` | `Integer` | — | Maximum buffer size for JSON parsing in bytes (default 1MB) |
| `user` | `String` | — | Unix user to run the CLI process as (requires sudo configuration) |
| `trace-dir` | `String` | — | Directory for JSONL trace files. Each invocation writes one file. |
## Permission Modes
The `permission-mode` property overrides `yolo` when set. Available modes:
| Mode | Description |
| ------------------- | ------------------------------------------------------ |
| `bypassPermissions` | Skip all permission checks (equivalent to `yolo=true`) |
| `default` | Use Claude Code's default permission behavior |
```yaml theme={null}
agent-client:
claude:
permission-mode: bypassPermissions
# yolo is ignored when permission-mode is set
```
## Tool Filtering
Control which tools Claude can use:
```yaml theme={null}
agent-client:
claude:
allowed-tools:
- Read
- Write
- Bash
disallowed-tools:
- WebSearch
```
`allowed-tools` and `disallowed-tools` are mutually exclusive in practice. If both are set, `allowed-tools` takes precedence.
## Budget Controls
Limit agent execution costs and turns:
```yaml theme={null}
agent-client:
claude:
max-turns: 10 # Stop after 10 agentic turns
max-budget-usd: 0.50 # Stop after $0.50 spent
max-tokens: 4096 # Limit response tokens
```
## Structured Output
Request structured JSON output using a JSON schema:
```yaml theme={null}
agent-client:
claude:
json-schema:
type: object
properties:
summary:
type: string
files_changed:
type: array
items:
type: string
```
## Trace Files
Enable JSONL trace output to capture tool calls, thinking blocks, and result metrics per invocation:
```yaml theme={null}
agent-client:
claude:
trace-dir: /path/to/traces
```
Each `call()` writes one file like `agent-run-20260528-143000-123-a1b2c3d4.jsonl` containing Claude-specific events (`tool_use`, `tool_result`, `text`, `thinking`, `result`). The file path is available in the response metadata:
```java theme={null}
String tracePath = (String) response.getMetadata().getProviderFields().get("tracePath");
```
Or via the builder API:
```java theme={null}
ClaudeAgentModel model = ClaudeAgentModel.builder()
.traceDir(Path.of("traces"))
.build();
```
Trace files are flushed per event during parsing, so you can `tail -f` them during long-running sessions. When used with [Agent Workflow](/docs/agent-workflow/trace-capture), trace paths propagate automatically through the workflow journal.
These are Claude-specific JSONL traces, not provider-neutral journal events. See [Agent Journal](/projects/agent-journal) for the portable event layer.
## Authentication
```bash theme={null}
claude auth login
```
The Claude CLI uses its own session token. No environment variables needed.
```bash theme={null}
export ANTHROPIC_API_KEY=sk-ant-...
```
Do not mix session auth and API key auth — this can cause authentication conflicts.
# Codex Reference
Source: https://lab.pollack.ai/docs/agent-client/reference/codex-reference
Complete configuration reference for the OpenAI Codex agent provider
## Overview
The Codex agent wraps the [OpenAI Codex CLI](https://github.com/openai/codex) via `CodexAgentApi`. Configure it through Spring properties under `agent-client.codex.*`.
```yaml theme={null}
agent-client:
codex:
model: gpt-5-codex
timeout: PT5M
full-auto: true
```
## Configuration Properties
Prefix: `agent-client.codex`
| Property | Type | Default | Description |
| ------------------ | ----------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mode` | `AgentClientMode` | — | Controls default permissiveness. Inherits from `agent-client.mode` if not set (default: `LOOSE`). |
| `model` | `String` | `gpt-5-codex` | Model to use for Codex execution |
| `timeout` | `Duration` | `5m` | Timeout for agent task execution |
| `full-auto` | `boolean` | `true` | Enable full-auto mode (workspace-write sandbox + never approval) |
| `skip-git-check` | `Boolean` | — | Skip git repository check. When not explicitly set, derived from mode (see below). |
| `reasoning-effort` | `String` | — | Model reasoning effort (`model_reasoning_effort` config override): `minimal`, `low`, `medium`, `high`, `xhigh`. Overrides the portable `effort` when both are set |
| `executable-path` | `String` | — | Path to the Codex CLI executable (auto-discovered if not set) |
## skipGitCheck and Mode Interaction
The `skip-git-check` property has special behavior — its effective value depends on `AgentClientMode` when not explicitly set:
| `skip-git-check` | `mode` | Effective Value | Behavior |
| ------------------ | ----------------- | --------------- | ----------------------- |
| `true` (explicit) | any | `true` | Works in any directory |
| `false` (explicit) | any | `false` | Requires git repository |
| not set | `LOOSE` (default) | `true` | Works in any directory |
| not set | `STRICT` | `false` | Requires git repository |
**Explicit always wins.** Setting `skip-git-check` directly always overrides the mode-derived value. See [Defaults Philosophy](/docs/agent-client/explanation/defaults-philosophy) for the precedence rationale.
### Why This Matters
Codex is the only provider that blocks execution in non-git directories by default. Without `skipGitCheck=true`, attempting to create a file in a temporary directory fails:
```
Error: Not a git repository (or any of the parent directories)
```
The `LOOSE` mode (default) automatically resolves this, so users can start with Codex without hitting this wall.
**Migration note (pre-0.14.0):** Before the mode system, Codex required `skip-git-check: true` explicitly for non-git directories. With `LOOSE` mode (now the default), this is automatic. To restore the old behavior, set `mode: strict` or `skip-git-check: false`.
## Full-Auto Mode
When `full-auto=true` (default), Codex runs with:
* **Workspace-write sandbox** — can write files in the working directory
* **Never asks for approval** — autonomous execution
```yaml theme={null}
agent-client:
codex:
full-auto: true # Default — autonomous execution
```
The Codex CLI `exec` subcommand does **not** support `--ask-for-approval`. Only `--full-auto` and `--sandbox` control execution behavior.
## Authentication
```bash theme={null}
export OPENAI_API_KEY=sk-...
```
The Codex CLI requires an OpenAI API key via the `OPENAI_API_KEY` environment variable.
# Gemini Reference
Source: https://lab.pollack.ai/docs/agent-client/reference/gemini-reference
Complete configuration reference for the Google Gemini agent provider
## Overview
The Gemini agent wraps the [Gemini CLI](https://github.com/google-gemini/gemini-cli) via `GeminiAgentApi`. Configure it through Spring properties under `agent-client.gemini.*`.
```yaml theme={null}
agent-client:
gemini:
model: gemini-2.5-flash
timeout: PT5M
yolo: true
```
## Configuration Properties
Prefix: `agent-client.gemini`
| Property | Type | Default | Description |
| ----------------- | ---------- | ------------------ | -------------------------------------------------------------- |
| `model` | `String` | `gemini-2.5-flash` | Gemini model to use for agent tasks |
| `timeout` | `Duration` | `5m` | Timeout for agent task execution |
| `yolo` | `boolean` | `true` | Bypass all permission checks |
| `executable-path` | `String` | — | Path to the Gemini CLI executable (auto-discovered if not set) |
| `temperature` | `Double` | — | Temperature for controlling response randomness (0.0–1.0) |
| `max-tokens` | `Integer` | — | Maximum number of tokens to generate in the response |
## Yolo Mode
When `yolo=true` (default), Gemini runs autonomously without permission prompts:
```yaml theme={null}
agent-client:
gemini:
yolo: true # Default — no permission prompts
```
Set `yolo: false` if you want Gemini to ask for confirmation before executing commands.
## Temperature
Control response randomness with the `temperature` property:
```yaml theme={null}
agent-client:
gemini:
temperature: 0.2 # More deterministic
```
| Value | Behavior |
| ----- | --------------------------------------- |
| `0.0` | Most deterministic — consistent outputs |
| `0.5` | Balanced |
| `1.0` | Most creative — varied outputs |
Temperature support depends on the Gemini CLI version. Check your CLI version with `gemini --version`.
## Mode System
Gemini works in any directory with default settings — no mode-derived behavior currently exists. The `LOOSE` and `STRICT` modes are a no-op for Gemini today.
Future `STRICT` mode knobs for Gemini will be added when there is a concrete safety control to gate (following the [promotion rubric](/docs/agent-client/reference/portable-options#promotion-rubric)).
## Authentication
```bash theme={null}
export GEMINI_API_KEY=...
# or
export GOOGLE_API_KEY=...
```
The Gemini CLI accepts either `GEMINI_API_KEY` or `GOOGLE_API_KEY` environment variables.
# Grok Reference
Source: https://lab.pollack.ai/docs/agent-client/reference/grok-reference
Complete configuration reference for the xAI Grok agent provider
## Overview
The Grok agent wraps the [xAI Grok CLI](https://grok.com/cli) via `GrokAgentModel`. Configure it through Spring properties under `agent-client.grok.*`.
```yaml theme={null}
agent-client:
grok:
model: grok-4.6
reasoning-effort: high
timeout: PT10M
```
## Configuration Properties
Prefix: `agent-client.grok`
| Property | Type | Default | Description |
| -------------------- | ----------------- | ---------- | ----------------------------------------------------------------------------------------------------------------- |
| `mode` | `AgentClientMode` | — | Controls default permissiveness. Inherits from `agent-client.mode` if not set (default: `LOOSE`). |
| `model` | `String` | `grok-4.6` | Model to use (`--model`) |
| `reasoning-effort` | `String` | — | Reasoning effort (`--reasoning-effort`): `low`, `medium`, `high`. The portable `effort` values map here directly. |
| `timeout` | `Duration` | `5m` | Timeout for agent task execution |
| `permission-mode` | `PermissionMode` | — | `--permission-mode`. When not explicitly set, derived from mode (see below). |
| `max-turns` | `Integer` | — | Maximum agent turns (`--max-turns`) |
| `disable-web-search` | `boolean` | `false` | Disable web search and fetch tools (`--disable-web-search`) |
| `executable-path` | `String` | — | Path to the Grok CLI executable (auto-discovered if not set) |
## permissionMode and Mode Interaction
| `permission-mode` | `mode` | Effective Value | Behavior |
| ----------------- | -------- | ------------------- | ------------------------------------------------------------------ |
| explicit | any | as set | Always wins |
| unset | `STRICT` | `default` | The CLI's own default — a tool call needing approval stops the run |
| unset | `LOOSE` | `bypassPermissions` | Every tool call is approved without prompting |
The portable `isAutoApprove()` maps to `bypassPermissions`, the only mode that grants every tool call without a prompt.
## Portable Option Mapping
| Portable option | Grok flag |
| ------------------------- | ------------------------------------------------------------------------------- |
| `getModel()` | `--model` |
| `getEffort()` | `--reasoning-effort` (direct passthrough — `low`/`medium`/`high` are all valid) |
| `getMaxTurns()` | `--max-turns` |
| `getJsonSchema()` | `--json-schema` (serialized inline; no temp file) |
| `isAutoApprove()` | `--permission-mode bypassPermissions` |
| `getTimeout()` | process timeout |
| `getWorkingDirectory()` | `--cwd` and the process working directory |
| `getSystemInstructions()` | prepended to the goal — see below |
### System instructions are prepended, not passed as a flag
Grok has `--system-prompt-override`, but it *replaces* the CLI's own system prompt rather than adding to it, which strips the agent's tool instructions. Portable `systemInstructions` mean "also tell it this", so they are prepended to the goal — the same choice every other provider here makes.
## Result Metadata
Grok returns a JSON envelope in headless mode, so nothing is scraped from log text. `AgentResponseMetadata.getProviderFields()` carries:
| Field | Notes |
| ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `costUsd` | **A real per-run cost.** Grok is the only CLI in this family that reports one, so callers do not need a price table. |
| `inputTokens`, `outputTokens`, `thinkingTokens` | From the envelope's `usage` block |
| `cacheReadTokens`, `cacheCreationTokens`, `totalTokens` | |
| `numTurns`, `stopReason`, `exitCode`, `successful` | |
| `structured` | `false` when the output was not the expected envelope; the raw text is preserved as the response |
`getModel()` reports the model that actually ran (from the envelope's `modelUsage` key), which may differ from the requested slug when the CLI expands an alias. `getSessionId()` carries the session UUID.
## Sessions
Grok accepts a caller-supplied session UUID (`--session-id`) for a new conversation and resumes with `--resume `, so the id never has to be recovered from output. `GrokClient.resume(sessionId, prompt, options)` exposes this. Grok does not currently implement `AgentSessionRegistry`.
`grok models` reports "You are not authenticated" even on an authenticated install. The provider's health check therefore uses `grok --version`; using `models` would reject a working CLI.
## Availability
Since **0.28.0**.
## Installation
Install the CLI from [grok.com/cli](https://grok.com/cli) and authenticate once with `grok login`. The provider discovers the executable via `GROK_CLI_PATH`, then `which grok`, then `~/.local/bin/grok` and `~/.grok/bin/grok`.
# Portable Options Reference
Source: https://lab.pollack.ai/docs/agent-client/reference/portable-options
AgentOptions interface, mode system, and the precedence rules that govern agent behavior across providers
## AgentOptions
`AgentOptions` is the portable interface that all provider-specific options extend. It defines the common surface that works identically across Claude, Codex, and Gemini.
```java theme={null}
public interface AgentOptions {
String getModel();
Duration getTimeout();
}
```
Each provider adds its own options beyond this interface — see the provider-specific reference pages for details.
## Configuration Precedence
Agent behavior is determined by a layered precedence system. Higher layers override lower ones:
```
1. Explicit goal options (highest — per-request)
2. Builder configuration (per-client instance)
3. Spring properties (application.yml / env vars)
4. Mode-derived defaults (from AgentClientMode)
5. Hardcoded defaults (lowest — in code)
```
**Explicit always wins.** If you set `agent-client.codex.skip-git-check=true` alongside `agent-client.mode=strict`, the explicit property wins. See [Defaults Philosophy](/docs/agent-client/explanation/defaults-philosophy) for the full rationale.
### Example: Precedence in Action
```yaml theme={null}
agent-client:
mode: strict
codex:
skip-git-check: true # Explicit property overrides STRICT
model: o3-mini # Overrides hardcoded default (gpt-5-codex)
```
Result: Codex runs in STRICT mode **except** `skipGitCheck` is `true` because the explicit property takes precedence.
## Mode System
`AgentClientMode` is a portable enum controlling default permissiveness:
| Mode | Behavior | When to Use |
| ----------------- | ----------------------------------------------- | ----------------------------------- |
| `LOOSE` (default) | Minimize preconditions — works in any directory | Evaluation, development, tutorials |
| `STRICT` | Conservative — requires explicit opt-in | Production, CI, shared environments |
```yaml theme={null}
agent-client:
mode: loose # or strict
```
See [Defaults Philosophy](/docs/agent-client/explanation/defaults-philosophy) for detailed mode-vs-property interaction examples.
## Promotion Rubric
Provider-specific options graduate to the portable `AgentOptions` interface when **all three** conditions hold:
1. **Two or more providers** have a semantic equivalent
2. **Absence causes failures** on easy-tier benchmarks (evidence, not speculation)
3. The option **can be expressed** without leaking provider-specific concepts
Options that don't meet the rubric remain in the provider-specific namespace indefinitely. This is intentional — not every option should be portable.
### Current Portable Options
| Option | Claude | Codex | Gemini | Status |
| --------- | ------------------- | ------------------------ | ------------------------ | --------------------------------------------------------- |
| `model` | `claude-sonnet-4-5` | `gpt-5-codex` | `gemini-2.5-flash` | Portable |
| `timeout` | 5m | 5m | 5m | Portable |
| `effort` | `--effort` | `model_reasoning_effort` | ignored (no effort knob) | Portable (`low`/`medium`/`high`; native ranges are wider) |
### Provider-Specific (Not Promoted)
| Option | Provider | Why Not Portable |
| ------------------- | -------------- | ------------------------------------------- |
| `skipGitCheck` | Codex | Only Codex has a git directory gate |
| `maxThinkingTokens` | Claude | Claude-specific extended thinking |
| `yolo` | Claude, Gemini | Codex uses `fullAuto` — different semantics |
| `temperature` | Gemini | Not exposed by Claude/Codex CLIs |
## Provider Reference Pages
18 properties — `agent-client.claude.*`
6 properties — `agent-client.codex.*`
6 properties — `agent-client.gemini.*`
# Agent Sessions
Source: https://lab.pollack.ai/docs/agent-client/reference/sessions
Persistent, multi-turn agent conversations with session lifecycle management
## Overview
Agent Sessions enable **persistent, multi-turn conversations** with CLI agents. Think of them like `HttpSession` — the session maintains state across prompts so you can have iterative dialogues with an agent instead of fire-and-forget single tasks.
Without sessions, each `AgentClient.goal().run()` call starts a fresh conversation. With sessions, you can:
* Send follow-up prompts that build on previous context
* Resume conversations after transport failures
* Manage session lifecycle with automatic stale cleanup
**Available since version 0.10.0.** Currently implemented for the **Claude** agent provider. Other providers will follow.
## Core Interfaces
### AgentSession
A single persistent conversation. Created via `AgentSessionRegistry.create()` — never instantiated directly.
```java theme={null}
public interface AgentSession extends AutoCloseable {
String getSessionId();
Path getWorkingDirectory();
AgentSessionStatus getStatus();
AgentResponse prompt(String message);
AgentSession resume();
AgentSession fork();
void close();
}
```
| Method | Description |
| ----------------------- | ---------------------------------------------------------- |
| `getSessionId()` | Unique session ID, assigned eagerly at creation |
| `getWorkingDirectory()` | Immutable working directory the session operates in |
| `getStatus()` | Current lifecycle status (`ACTIVE`, `DEAD`, or `RESUMED`) |
| `prompt(message)` | Send a follow-up in the same conversation context |
| `resume()` | Resurrect a `DEAD` session — restores conversation history |
| `fork()` | Branch the conversation (not yet implemented) |
| `close()` | Close the session and release resources |
### AgentSessionRegistry
Factory and lifecycle manager for sessions. Analogous to `SessionRepository` in Spring Session.
```java theme={null}
public interface AgentSessionRegistry {
AgentSession create(Path workingDirectory);
Optional find(String sessionId);
void evict(String sessionId);
void evictStale(Duration inactiveSince);
}
```
| Method | Description |
| ---------------------- | --------------------------------------------------------------------- |
| `create(path)` | Start a new session — eagerly connects to CLI and captures session ID |
| `find(id)` | Look up an existing session |
| `evict(id)` | Remove and close a session |
| `evictStale(duration)` | Evict sessions inactive longer than the threshold |
### AgentSessionStatus
```
ACTIVE → Session is connected and ready for prompts
DEAD → Transport died; call resume() to resurrect
RESUMED → Was dead, now active again after resume()
```
## Usage
### Create a Registry and Session
```java theme={null}
import io.github.markpollack.agents.claude.ClaudeAgentSessionRegistry;
import io.github.markpollack.agents.model.AgentSession;
import io.github.markpollack.agents.model.AgentResponse;
// Build a registry (configure once, create many sessions)
ClaudeAgentSessionRegistry registry = ClaudeAgentSessionRegistry.builder()
.timeout(Duration.ofMinutes(5))
.build();
// Create a session — eagerly establishes CLI connection
AgentSession session = registry.create(Path.of("/my/project"));
System.out.println("Session ID: " + session.getSessionId());
```
### Multi-Turn Conversation
```java theme={null}
// First prompt
AgentResponse r1 = session.prompt("Create a Spring Boot REST controller for /api/users");
// Follow-up — Claude remembers the previous context
AgentResponse r2 = session.prompt("Add input validation with @Valid");
// Another follow-up
AgentResponse r3 = session.prompt("Now write tests for the controller");
// Clean up
session.close();
registry.evict(session.getSessionId());
```
Each `prompt()` call continues the same conversation — the agent sees the full history of what it built in earlier turns.
### Resuming a Dead Session
If the CLI process dies (crash, timeout, network issue), the session transitions to `DEAD`. You can resurrect it:
```java theme={null}
AgentSession session = registry.create(Path.of("/my/project"));
String savedId = session.getSessionId();
try {
session.prompt("Refactor the service layer");
} catch (IllegalStateException e) {
// Transport died — session is now DEAD
if (session.getStatus() == AgentSessionStatus.DEAD) {
session.resume(); // Spawns fresh process, restores history
// Status is now RESUMED — ready for prompts again
session.prompt("Continue the refactoring");
}
}
```
### Finding an Existing Session
```java theme={null}
// Later in the application lifecycle
Optional found = registry.find(savedId);
found.ifPresent(s -> {
AgentResponse response = s.prompt("What files did you modify?");
System.out.println(response.getText());
});
```
## Session Lifecycle
```
create()
│
▼
┌────────┐
│ ACTIVE │◄─────────────┐
└───┬────┘ │
│ │
prompt() works resume()
transport dies │
│ │
▼ │
┌────────┐ ┌────────┐
│ DEAD │────────►│RESUMED │
└───┬────┘ └────────┘
│
close() /
evict()
│
▼
[removed]
```
## Spring Integration
### Bean Configuration
```java theme={null}
@Configuration
@EnableScheduling
public class AgentSessionConfig {
@Bean
public ClaudeAgentSessionRegistry agentSessionRegistry() {
return ClaudeAgentSessionRegistry.builder()
.timeout(Duration.ofMinutes(10))
.build();
}
}
```
### Stale Session Cleanup
Use `@Scheduled` to periodically evict inactive sessions and prevent resource leaks:
```java theme={null}
@Component
public class SessionCleanup {
private final ClaudeAgentSessionRegistry registry;
public SessionCleanup(ClaudeAgentSessionRegistry registry) {
this.registry = registry;
}
@Scheduled(fixedRate = 300_000) // Every 5 minutes
public void cleanupStaleSessions() {
registry.evictStale(Duration.ofMinutes(30));
}
}
```
### Startup Health Probe
Use a disposable session to verify CLI availability at startup:
```java theme={null}
@Component
public class AgentHealthCheck {
private final ClaudeAgentSessionRegistry registry;
public AgentHealthCheck(ClaudeAgentSessionRegistry registry) {
this.registry = registry;
}
@EventListener(ApplicationReadyEvent.class)
public void verifyCliAvailable() {
try {
AgentSession probe = registry.create(Path.of("/tmp"));
probe.close();
registry.evict(probe.getSessionId());
// CLI is installed, authenticated, and responsive
} catch (IllegalStateException e) {
throw new IllegalStateException("Claude CLI not available: " + e.getMessage(), e);
}
}
}
```
## Limitations
`fork()` is **not yet implemented** — calling it throws `UnsupportedOperationException`. This will be added in a future release.
* **Claude-only**: Sessions are currently only implemented for the Claude agent provider. Other providers will be added as their CLIs support persistent sessions.
* **In-memory registry**: `ClaudeAgentSessionRegistry` stores sessions in a `ConcurrentHashMap`. Sessions do not survive application restarts — use `resume()` with a persisted session ID if you need cross-restart continuity.
* **One working directory per session**: A session is anchored to the working directory specified at creation time. It cannot be changed.
* **No context pruning**: Resumed sessions include the full conversation history. You cannot trim earlier turns to reduce token usage.
## Related
* [Agent Client Overview](/projects/agent-client) — high-level project documentation
* [Claude Agent SDK Tutorial: Session Resume](https://github.com/markpollack/claude-agent-sdk-java-tutorial/tree/main/module-11-session-resume) — lower-level SDK session resume
* [Claude Agent SDK Tutorial: Session Fork](https://github.com/markpollack/claude-agent-sdk-java-tutorial/tree/main/module-12-session-fork) — lower-level SDK session fork
# Lesson 1: Your First Agent Task
Source: https://lab.pollack.ai/docs/agent-client/tutorial/01-first-task
Create a file using an agent — set up, run, and verify
`agent-client-tutorial/01-create-file`
## What You'll Build
A Java program that asks an agent to create a file, then verifies the file was created. No Spring Boot required — just plain Java with the AgentClient API.
## Step 1: Set Up the Project
Create a Maven project with the Claude agent dependency:
```xml theme={null}
io.github.markpollackagent-claude0.29.0
```
Or clone the tutorial repo:
```bash theme={null}
git clone https://github.com/markpollack/agent-client-tutorial.git
cd agent-client-tutorial/01-create-file
```
## Step 2: Understand the Code
Three lines do the real work:
```java theme={null}
// 1. Build the model — this is the provider-specific part
ClaudeAgentModel model = ClaudeAgentModel.builder()
.defaultOptions(ClaudeAgentOptions.builder()
.model("claude-sonnet-4-5")
.yolo(true)
.build())
.build();
// 2. Create the client — this is the portable part
AgentClient client = AgentClient.create(model);
// 3. Run a goal
AgentClientResponse response = client.run(
"Create a file named hello.txt with the content 'Hello from Agent Client!'"
);
```
Notice the separation:
* **Model construction** is provider-specific (you import `ClaudeAgentModel`)
* **Client usage** is portable (`AgentClient.create()` and `.run()` work with any model)
* **The goal** is a plain English string — describe *what*, not *how*
## Step 3: Run It
```bash theme={null}
./mvnw compile exec:java -Dexec.mainClass="HelloAgent"
```
## Step 4: Verify
Check that `hello.txt` was created:
```bash theme={null}
cat hello.txt
# Output: Hello from Agent Client!
```
## What Just Happened
1. `ClaudeAgentModel` found the Claude CLI on your system
2. `AgentClient.create(model)` wrapped it in the portable client API
3. `.run(goal)` sent your English instruction to the Claude CLI
4. The CLI created the file, and the response told you it succeeded
The agent did the same thing a developer would do — it created a file. The difference is you described *what* to do, not *how*.
## Next
[Lesson 2: Multi-Provider →](/docs/agent-client/tutorial/02-multi-provider) — Run this same task with Codex and Gemini without changing the client code.
# Lesson 2: Multi-Provider
Source: https://lab.pollack.ai/docs/agent-client/tutorial/02-multi-provider
Run the same task with Claude, Codex, and Gemini — the client code stays the same
This lesson is doc-only — concepts apply to any tutorial module
## What You'll Learn
How to run the exact same goal with three different providers. The model construction changes, but `AgentClient` usage is identical.
## The Pattern
Provider-specific code is isolated to model construction. Everything after `AgentClient.create(model)` is portable:
```java theme={null}
// Provider-specific: build the model
AgentModel model = buildModel(provider);
// Portable: same code regardless of provider
AgentClient client = AgentClient.create(model);
AgentClientResponse response = client.run("Create hello.txt with 'Hello!'");
```
## Step 1: Run with Claude
```java theme={null}
ClaudeAgentModel model = ClaudeAgentModel.builder()
.defaultOptions(ClaudeAgentOptions.builder()
.model("claude-sonnet-4-5")
.yolo(true)
.build())
.build();
AgentClient client = AgentClient.create(model);
AgentClientResponse response = client.run(
"Create a file named hello.txt with 'Hello from Claude!'"
);
```
## Step 2: Run with Codex
Swap the model — the client code is identical:
```java theme={null}
CodexAgentModel model = new CodexAgentModel(
CodexClient.create(),
CodexAgentOptions.builder()
.model("gpt-5-codex")
.skipGitCheck(true)
.build(),
null
);
AgentClient client = AgentClient.create(model);
AgentClientResponse response = client.run(
"Create a file named hello.txt with 'Hello from Codex!'"
);
```
`skipGitCheck(true)` lets Codex work in any directory. Without it, Codex requires a git repository. See [Codex Reference](/docs/agent-client/reference/codex-reference#skipgitcheck-and-mode-interaction) for details.
## Step 3: Run with Gemini
```java theme={null}
GeminiAgentModel model = new GeminiAgentModel(
GeminiClient.create(),
GeminiAgentOptions.builder()
.model("gemini-2.5-flash")
.yolo(true)
.build(),
null
);
AgentClient client = AgentClient.create(model);
AgentClientResponse response = client.run(
"Create a file named hello.txt with 'Hello from Gemini!'"
);
```
## What Stayed the Same
Across all three providers, these two lines are identical:
```java theme={null}
AgentClient client = AgentClient.create(model);
AgentClientResponse response = client.run(goal);
```
The only difference is *which model you construct*. That's the portable API — `AgentClient` doesn't know or care which provider is behind it.
## Provider Differences
While the client API is portable, providers have different capabilities:
| Capability | Claude | Codex | Gemini |
| ----------------- | ----------- | --------------------------- | ------------- |
| Non-git directory | Works | Works (`skipGitCheck=true`) | Works |
| Structured output | JSON Schema | Not supported | Not supported |
| Session resume | Supported | Not supported | Not supported |
| Permission modes | Multiple | full-auto only | yolo only |
## With Spring Boot
In Spring Boot applications, you don't construct models manually. Use starter dependencies and Maven profiles to switch providers without changing code at all — see the [Switching Providers](/docs/agent-client/howto/switching-providers) how-to guide.
## Next Steps
* [Switching Providers](/docs/agent-client/howto/switching-providers) — The Spring Boot profile pattern for zero-code provider switching
* [Configuration Reference](/docs/agent-client/reference/portable-options) — All configuration options across providers
* [Defaults Philosophy](/docs/agent-client/explanation/defaults-philosophy) — Why LOOSE mode exists and how it affects each provider
# Lesson 3: Read and Transform
Source: https://lab.pollack.ai/docs/agent-client/tutorial/03-read-and-transform
Read files, perform data transformation, and write structured output
`agent-client-tutorial/02-read-and-transform`
## What You'll Build
A Spring Boot application that asks an agent to read log files, count severity levels (ERROR, WARNING, INFO), and produce a summary CSV. This demonstrates agents working with existing files and producing structured output.
## Step 1: Set Up Sample Data
Clone the tutorial repository and navigate to the lesson:
```bash theme={null}
git clone https://github.com/markpollack/agent-client-tutorial.git
cd agent-client-tutorial/02-read-and-transform
```
The application creates sample log files automatically if they don't exist. Three service logs (`auth.log`, `api.log`, `worker.log`) with mixed severity levels.
## Step 2: Understand the Goal
The goal is a multi-line prompt that specifies the input, the transformation, and the expected output format:
```java theme={null}
String goal = """
In the logs/ directory there are multiple .log files from different services.
Scan all .log files and count how many lines contain "ERROR", "WARNING", and "INFO".
Output your results to summary.csv with the following structure:
severity,count
ERROR,
WARNING,
INFO,
The output should be a valid CSV file with exactly four lines.""";
```
Notice the specificity: file format, column names, exact line count. Agents perform better with precise output specifications.
## Step 3: Run It
```bash theme={null}
# With Claude (default)
./mvnw spring-boot:run
# Or with a different provider
./mvnw spring-boot:run -Dspring.profiles.active=codex
```
## Step 4: Verify
Check the output CSV:
```bash theme={null}
cat summary.csv
# severity,count
# ERROR,4
# WARNING,4
# INFO,7
```
## What's Different from Lesson 1
* **Agent reads existing files** — not just creating from scratch
* **Multi-file input** — the agent scans a directory of logs
* **Structured output** — the result is a well-defined CSV, not free text
* **Data transformation** — counting, aggregating, formatting
This pattern — read existing state, transform it, write structured output — is the core of most agent tasks in production.
## Next
[Lesson 4: Git Operations →](/docs/agent-client/tutorial/04-git-operations) — Use an agent to inspect git history and recover lost changes.
# Lesson 4: Git Operations
Source: https://lab.pollack.ai/docs/agent-client/tutorial/04-git-operations
Find lost git changes and merge them — agents working with version control
`agent-client-tutorial/03-git-operations`
## What You'll Build
An agent that inspects git history, finds commits on a detached HEAD, and merges them into the main branch. This demonstrates agents working with version control — a common real-world task.
## Step 1: Set Up the Repository
Clone the tutorial and run the setup script that creates a git repo with "lost" changes:
```bash theme={null}
git clone https://github.com/markpollack/agent-client-tutorial.git
cd agent-client-tutorial/03-git-operations
bash setup.sh
```
The setup script creates a git repository in `workspace/` with a commit on a detached HEAD — simulating changes that were accidentally "lost" when switching back to master.
## Step 2: Understand the Goal
The prompt describes the problem in natural language, as a developer would:
```java theme={null}
String goal = """
I just made some changes to my personal site and checked out master,
but now I can't find those changes. Please help me find them and
merge them into master.""";
```
The agent needs to:
1. Inspect the git reflog to find the detached commit
2. Identify the lost changes
3. Merge or cherry-pick them into master
## Step 3: Run It
```bash theme={null}
./mvnw spring-boot:run
```
The agent uses tools like `Bash` to run `git reflog`, `git log`, `git merge`, etc.
## Step 4: Verify
```bash theme={null}
cd workspace
git log --oneline -5
# Should show the recovered changes merged into master
```
## What's Different from Previous Lessons
* **Stateful environment** — the agent works in an existing git repository, not a clean directory
* **Multi-step reasoning** — finding lost commits requires inspecting reflog, understanding branch state, and choosing a merge strategy
* **Tool-heavy task** — the agent runs multiple shell commands, reads their output, and decides next steps
* **STRICT mode applicable** — this task genuinely requires a git repository, making it a natural fit for `AgentClientMode.STRICT`
## The LOOSE/STRICT Connection
Lessons 1-3 work in any directory — they don't need git. That's why LOOSE mode (the default) is appropriate. This lesson requires a git repository. If you were building a system that only runs git-aware tasks, STRICT mode would prevent accidental execution outside a git repo.
See [Defaults Philosophy](/docs/agent-client/explanation/defaults-philosophy) for more on when to use each mode.
## Next Steps
* [Getting Started](/docs/agent-client/howto/getting-started) — Recap the quick start for all three providers
* [Switching Providers](/docs/agent-client/howto/switching-providers) — Run any tutorial lesson with a different provider
* [Claude Reference](/docs/agent-client/reference/claude-reference) — Full configuration options including trace files
# Agent Client Tutorial
Source: https://lab.pollack.ai/docs/agent-client/tutorial/index
Learn Agent Client step by step — from your first task to git operations
Clone the repo and follow along: `git clone https://github.com/markpollack/agent-client-tutorial.git`
## Learning Path
This tutorial takes you from zero to running real agent tasks. Each lesson builds on the previous one, progressing from simple file creation to multi-step git operations.
Create a file using an agent — the simplest possible task. Set up a project, configure a provider, and verify the result.
[Start Lesson 1 →](/docs/agent-client/tutorial/01-first-task)
Run the same task with Claude, Codex, and Gemini. The client code stays the same — only the model construction changes.
[Start Lesson 2 →](/docs/agent-client/tutorial/02-multi-provider)
Read log files, count severity levels, and write a structured CSV summary. Agents working with existing files and producing structured output.
[Start Lesson 3 →](/docs/agent-client/tutorial/03-read-and-transform)
Find lost commits on a detached HEAD and merge them into master. Agents working with version control and multi-step reasoning.
[Start Lesson 4 →](/docs/agent-client/tutorial/04-git-operations)
## Prerequisites
* Java 17+
* Maven 3.9+
* At least one CLI agent installed:
* Claude Code: `npm install -g @anthropic-ai/claude-code`
* Codex CLI: `npm install -g @openai/codex`
* Gemini CLI: `npm install -g @google/gemini-cli`
## Source Code
All tutorial code is available in the [agent-client-tutorial](https://github.com/markpollack/agent-client-tutorial) repository:
```bash theme={null}
git clone https://github.com/markpollack/agent-client-tutorial.git
cd agent-client-tutorial
```
| Lesson | Directory | What it demonstrates |
| ------ | ------------------------ | ---------------------------------------------------- |
| 01 | `01-create-file/` | Simplest task — create a file |
| 02 | (doc-only) | Multi-provider portability |
| 03 | `02-read-and-transform/` | File reading, data transformation, structured output |
| 04 | `03-git-operations/` | Git history inspection, merge recovery |
# What's New
Source: https://lab.pollack.ai/docs/agent-client/whats-new
Release notes for Agent Client — auto-generated from git history
## 0.29.0 (2026-08-22)
* Grok, Codex, and Antigravity now publish their parsed run trajectory through `AgentClientResponse.getPhaseCapture()`, matching the Claude facade contract; live facade integration gates verify that all three return a capture containing tool uses
* Grok reads its native `streaming-json` ACP stream and Antigravity its `stream-json`; both SDKs retain the full stream for capture while preserving the existing terminal text, status, token, and session fields
* Codex harvests its durable rollout JSONL after execution, locating the matching file by session id or working directory with a bounded wait for delayed flushes
* The three provider adapters consume the released Agent Journal 1.8.0 capture modules; capture stays provider-owned, and `agent-model` and the production surface of `agent-client-core` remain free of journal dependencies
* Repair LOOSE-mode Codex execution: current Codex versions reject `--full-auto` after `exec`, so full-auto now maps to the global `--sandbox workspace-write` and `--ask-for-approval never` options placed before the subcommand
* Keep full-auto distinct from the explicit unrestricted `--dangerously-bypass-approvals-and-sandbox` level, so portable auto-approve no longer grants full-disk access implicitly; add repeated `--add-dir` support for explicitly named writable roots
* Document that provider trajectories come from the `AgentClient` facade response rather than a direct `AgentApi.call()`, and are independent of raw trace-file configuration
* Remove the inert module-local deploy skip from `agent-tck` — the artifact remains published — and delete the orphaned sandbox TCK package, which tested a sandbox SPI this repository no longer owns
## 0.28.0 (2026-08-22)
* Add a Grok CLI provider: `agent-grok`, `grok-cli-sdk`, and `agent-starter-grok`. Headless runs return a real JSON envelope, so session id, token usage and a genuine per-run USD cost are read rather than scraped, and a caller-supplied session UUID makes resume possible without recovering an id from output
* Add an Antigravity CLI provider for Google's `agy`: `agent-antigravity`, `antigravity-cli-sdk`, and `agent-starter-antigravity`
* Drop `--effort` when the Antigravity model slug already encodes it; passing both is a hard error that returns no output, so a portable low-effort request would otherwise produce nothing at all
* Declare the Antigravity working directory with `--add-dir`: setting the process working directory alone lets the CLI divert writes to a shared scratch directory while reporting success
* Derive Antigravity run success from whether work was produced and nothing was refused, rather than from its `status` field, which reports ERROR alongside complete and correct responses
* Detect Antigravity soft denials from both the result envelope and stderr, so a run whose tool calls were refused is not mistaken for one that found nothing to do
* Add `GROK` and `ANTIGRAVITY` to the provider parity TCK and enrol both in the parity scenarios
* Both new providers pass the full provider parity TCK — ten scenarios each, zero skips, zero failures — against live CLIs, joining Claude Code, Codex, and Gemini as the only adapters with parity coverage. CI re-verifies those three on every commit; Grok and Antigravity cannot run there because both CLIs authenticate interactively and cache credentials rather than reading an API key
## 0.27.0 (2026-08-21)
* Repair published no-BOM consumer graphs with direct Jackson and Log4j declarations that survive flattened child POMs
* Add one generated CI gate that discovers and resolves all 23 published runtime modules as fresh parentless, no-BOM consumers
* Adopt Claude SDK 1.5.0, Agent Journal and Capture 1.7.0, and Agent Sandbox Core 0.10.0 while retaining the Java 21 contract
* Produce one root CycloneDX 1.6 JSON SBOM and keep authoritative offline vulnerability analysis on the actual consumer JAR closures
* Align current owned source headers and binary/source/Javadoc archive payloads with BSL 1.1 while retaining Apache history
* Preserve immutable 0.26.0 as released; its Maven Central artifacts were not rebuilt or repaired
## 0.26.0 (2026-08-17)
* Remove the experimental Vendir context advisor and Git-repository DSL as an intentional breaking change
* Retire the stale `agents-runtime` container build, Docker integration TCK, and unused Docker dependencies
* Keep Claude Code, Codex, and Gemini CLI as the actively verified provider set; classify the remaining adapters as experimental
* Upgrade source-reactor Jackson and Log4j management; later public-consumer reproduction showed that flattened child POMs did not preserve every reviewed floor, which 0.27.0 corrects
* Pin owned release automation and complete the repository-specific BSL 1.1 license text while preserving the historical Apache record
## 0.22.0 (2026-06-14)
* agent-client 0.22.0: Spring AI 2.0.0 GA + Spring Boot 4.0.7; declare Jackson 2 explicitly
* Upgrade default Gemini model to gemini-3.5-flash
* Default Gemini model to gemini-3-flash-preview
* Stop tracking generated .flattened-pom.xml; let parity runs finish
## 0.21.0 (2026-06-07)
* Trust the workspace when Gemini yolo mode is enabled
* Fix parity workflow JDK: project targets Java 21
* Fix agents-runtime Maven download: bump to 3.9.16 with archive fallback
* Make Provider Parity real; add Codex to agents-runtime image; weekly CLI canary
* Fix CLITransportCommandTest on machines without a Codex CLI
* Add reasoning effort option: portable plus provider-native
## 0.20.0 (2026-06-06)
* Step 3.1-3.3: Trace content mode, transcript archival, capture IT upgrades
## 0.19.0 (2026-05-28)
* Verify traceDir in journal capture integration test
* Add traceDir parser-level and uniqueness tests
* Add trace-dir Spring Boot property and autoconfiguration
* Add traceDir support to ClaudeAgentModel
## 0.18.0 (2026-05-15)
* Remove judge-dependent modules from AgentClient
* Update agent-journal deps to 1.1.0-SNAPSHOT
* Skip tests on snapshot publish — CI handles test validation
* Add agent-clis and API key secrets to CI and snapshot workflows
* Migrate to markpollack org: package rename, BSL license, standalone POM, build-tools workflows
## 0.16.0 (2026-04-30)
* Fix autoconfigure parent relativePath, remove plans from tracking
* Add session journal for 2026-04-29
* Finalize roadmap with cross-framework stage and handoff notes
* Add deprecated property migrator and update documentation
* Rename property prefix from spring.ai.agents to agent-client
* Introduce AgentApi interface, deprecate AgentModel
* Extract AgentClientAutoConfiguration into separate module
* Add cross-framework portability design document
* Complete Stage 4 consolidation
* Record LOOSE permission discoveries and add CLI validation to plan
* Fix Gemini working directory not propagated to CLI process
* LOOSE mode: bypass Codex sandbox, update model to gpt-5.4-mini
* Reframe Stage 4 as LOOSE permission discovery via terminal-bench
* Add property prefix rename to development plan
* Update README with current docs, providers, and configuration
## 0.15.0 (2026-04-29)
* Remove samples directory — covered by tutorial repo and external projects
* Fix ClaudeProviderParityIT compilation error
* Fix sample SNAPSHOT reference blocking release
* Update project documentation and learnings
* Add docgen tool and reference documentation infrastructure
* Consolidate multi-provider sample and tutorial artifacts
* Consolidate parity TCK and mode system infrastructure
* Add provider parity CI matrix workflow
* Wire ProviderParityTCK into Claude, Codex, Gemini IT modules
* Add ProviderParityTCK with @ProviderCapability annotation
* feat(codex): add AgentClientMode and default skipGitCheck to true
## 0.14.0 (2026-04-28)
* Expose validate-commits as overridable release input
* Remove formatPrompt boilerplate wrapping — pass goal verbatim
* Add portable maxTurns, autoApprove, systemInstructions to AgentOptions
## 0.13.0 (2026-04-17)
* Add portable jsonSchema support to AgentClient request spec
* Add jsonSchema to portable AgentOptions interface
## 0.12.2 (2026-04-11)
* fix(codex): add sandbox bypass flag and graceful error handling
## 0.12.1 (2026-04-10)
* fix(codex): fix --model flag ordering and working directory propagation
## 0.12.0 (2026-04-10)
* feat(qwen-code): add Qwen Code agent model and starter
* feat(codex): surface outputSchema in CodexAgentOptions (#27)
* fix(gemini,codex): fix response parsing and CLI compatibility (#29, #30)
* Update README docs links to mintlify documentation site
* feat(agent-claude): add ClaudeAgentSession and ClaudeAgentSessionRegistry
* feat(agent-model): add AgentSession and AgentSessionRegistry interfaces
* Update README.md
## 0.11.0 (2026-03-30)
* Rename all project artifactIds: drop spring-ai- prefix
* Rename spring-ai-agents-core to spring-ai-agent-launcher
* Clean up dead module references and update README agent list
* Update README with released 0.10.0 Maven coordinates
## 0.10.0 (2026-03-30)
* Initial release.
# What's New
Source: https://lab.pollack.ai/docs/agent-experiment/whats-new
Release notes for Agent Experiment
## 0.6.0 (2026-08-18)
* Adds the `experiment-workflow` module for typed Agent Workflow invocation with journaled step
cost/token evidence adapted to `AgentInvoker`.
* Makes Agent Journal capture a first-class per-item lifecycle owned by `AgentExperiment`, using
Agent Journal 1.7.0 and preserving variant, item, model, session, tool-use, and derived cost data.
* Migrates runtime evaluation to Agent Judge 0.14.0 and stores normalized, Agent
Experiment-owned judgments and verdicts. Existing 0.5 / Judge 0.13 results load automatically;
re-saving writes the normalized format with the documented lossy boundary for obsolete metadata.
* Updates to Agent Client 0.26.0, Agent Workflow 0.10.0, and Claude Code SDK 1.4.0.
* Gives standalone consumers Jackson 2.21.6 and Jackson 3.1.6 safety floors without requiring a
BOM; public 25-, 26-, and 57-JAR module closures have zero findings against the validated scan
snapshot.
* Publishes signed binary, source, and Javadoc artifacts plus one aggregate CycloneDX 1.6 JSON SBOM.
* Requires Java 21. The credential-free reactor passed 544 tests; paid live Claude execution was
not part of the release gate.
## 0.5.0 (2026-06-06)
* Jackson 2.21.2, member pins (sdk 1.3.0, capture 1.4.0, judge 0.12.0), release 0.5.0
* Collect status: 2026-06-03
* Brief: abstain-aware consensus in judge aggregation
## 0.4.0 (2026-05-29)
* Update dependencies to released versions for 0.4.0 release
* Relax git dirty check to warn instead of throw for non-critical paths
* Bump dependencies to next SNAPSHOT versions
## 0.3.0 (2026-05-28)
* Update dependencies to released versions for 0.3.0 release
* Fix RunLogManager forcing logback on all experiment-core consumers
* Bump dependencies to next SNAPSHOT versions
## 0.2.0 (2026-05-19)
* Update dependencies to released versions for 0.2.0 release
* Bump agent-judge dependency from 0.10.0-SNAPSHOT to 0.10.0
* Step 3.4: Final consolidation — all stages complete
* Step 3.3: Jackson polymorphic serialization for both ExecutionDetail subtypes
* Step 3.2: Add JudgeExperiment and JudgeExperimentResult
* Step 3.1: Add JudgeScorer, supporting types, and JudgeExecutionDetail
* Step 3.0: Stage 3 entry — verify ExecutionDetail supports JudgeExecutionDetail
* Step 2.3: Stage 2 consolidation — verify re-eval→compare pipeline
* Step 2.2: Add ReEvaluator for post-hoc re-scoring without re-invocation
* Step 2.1: Add ReEvaluationContextFactory and AgentReEvaluationContextFactory
* Update ROADMAP.md checkboxes for Step 2.0
* Step 2.0: Stage 2 entry — verify prerequisites, document scope adjustments
* Migrate dependencies from org.springaicommunity to io.github.markpollack
* Step 1.3: Stage 1 consolidation — verify boundaries, compact learnings
* Step 1.2: Rename ExperimentRunner to AgentExperiment
* Ignore plans scratch files (META-ROADMAP.md, status.md)
* Step 1.1: Decouple ItemResult from InvocationResult via ExecutionDetail
* Step 1.0: Design review — map types to shared vs agent-specific boundaries
* Ignore entire plans/ directory in .gitignore
## 0.1.0 (2026-03-30)
* Initial release.
# What's New
Source: https://lab.pollack.ai/docs/agent-hooks/whats-new
Release notes for Agent Hooks
## 0.7.0 (2026-08-19)
* **Standalone consumers get safe Jackson versions.** A consumer that resolved
`agent-hooks-spring` or `agent-hooks-gemini` without importing a BOM previously selected
Jackson versions with known advisories, because the parent's `dependencyManagement` does not
travel through the flattened POM published to Maven Central. The floors are now 2.21.6 and
3.1.6, with the Jackson 3 floor declared directly on `agent-hooks-spring`.
* **Published POMs no longer carry repositories.** 0.6.x shipped a `` block on
every module POM, so consumers inherited two snapshot repositories and a milestone
repository. The build resolves from Maven Central alone.
* **`agent-hooks-claude` is Java 21.** Every published `claude-code-sdk` version is Java 21
bytecode, so the module never could be used on Java 17; it had been shipping as Java 17
bytecode. `agent-hooks-core`, `-spring`, and `-gemini` remain Java 17.
* Claude Agent SDK moves to 1.5.0 (still `provided` scope). An actual no-BOM consumer of
Agent Hooks plus that supplied SDK resolves the corrected Jackson 2.21.6 and 3.1.6 floors.
* Every distributed archive now contains the BSL 1.1 license text.
* The parent artifact publishes one aggregate CycloneDX 1.6 SBOM; child modules publish none.
* A hook that throws, and a steering decision returned from an observation-only event, are now
logged; both were documented as logged and were in fact silent.
* The Gemini dispatcher writes a protocol response for every invocation, so an unchecked hook
failure can no longer leave the CLI waiting on empty stdout.
* Adapter log and exception messages no longer embed tool-input payloads.
* `AgentHookBridge` gains `evictSession(String)` and `activeSessionCount()`.
## 0.6.4 (2026-06-15)
* Spring AI 2.0.0 GA and Spring Boot 4.0.7
## 0.6.3 (2026-06-06)
* Add .flattened-pom.xml to .gitignore
* Prepare for next development iteration 0.6.3-SNAPSHOT
## 0.6.2 (2026-04-10)
* Fix stale javadoc reference to removed AgentHookEvent enum
* Step 6.K: Consolidation — docs, quality, landscape fix
* Step 6.3: Cross-adapter proof — 3 runtimes
* Step 6.2: GeminiHookDispatcher + GeminiDecisionMapper
* Step 6.1: Gemini module skeleton + 7 event types
* Stop tracking CLAUDE.md and plans/ — internal-only files
* Stage 5: Claude Agent SDK adapter — write once, run on both runtimes
## 0.5.0 (2026-04-09)
* Initial release.
# Analyze Runs with DuckDB
Source: https://lab.pollack.ai/docs/agent-journal/analyzing-runs
Query Agent Journal 1.8.0 event streams with executable DuckDB examples
`JsonFileStorage` writes one `events.jsonl` file per run.
DuckDB can read all runs for an experiment directly; no import step or fixed table schema is required.
The examples below assume this path:
```text theme={null}
.agent-journal/experiments/{experiment-id}/runs/{run-id}/events.jsonl
```
Pass `union_by_name = true` because different event types have different fields.
Quote `"@type"` because the discriminator contains `@`.
Header rows have `@type = 'header'`, so event filters exclude them.
## Count tool calls
```sql theme={null}
SELECT
toolName AS tool,
COUNT(*) AS calls,
ROUND(AVG(durationMs), 0) AS average_ms,
COUNT(*) FILTER (WHERE NOT success) AS failures
FROM read_ndjson_auto(
'.agent-journal/experiments/*/runs/*/events.jsonl',
union_by_name = true)
WHERE "@type" = 'tool_call'
GROUP BY toolName
ORDER BY calls DESC, tool;
```
## Summarize LLM usage and cost by run file
Ask DuckDB to add the source filename explicitly:
```sql theme={null}
SELECT
filename,
SUM(tokenUsage.inputTokens) AS input_tokens,
SUM(tokenUsage.outputTokens) AS output_tokens,
SUM(
cost.inputCostUsd
+ cost.outputCostUsd
+ cost.thinkingCostUsd
- cost.cacheSavingsUsd
) AS total_cost_usd
FROM read_ndjson_auto(
'.agent-journal/experiments/*/runs/*/events.jsonl',
union_by_name = true,
filename = true)
WHERE "@type" = 'llm_call'
GROUP BY filename
ORDER BY filename;
```
## Extract ordered tool sequences
```sql theme={null}
SELECT
regexp_extract(filename, 'runs/([^/]+)/events', 1) AS run_id,
ROW_NUMBER() OVER (
PARTITION BY filename
ORDER BY timestamp
) AS sequence_number,
toolName AS tool
FROM read_ndjson_auto(
'.agent-journal/experiments/*/runs/*/events.jsonl',
union_by_name = true,
filename = true)
WHERE "@type" = 'tool_call'
ORDER BY run_id, sequence_number;
```
## Count adjacent tool transitions
```sql theme={null}
WITH tool_sequence AS (
SELECT
filename,
toolName AS tool,
ROW_NUMBER() OVER (
PARTITION BY filename
ORDER BY timestamp
) AS sequence_number
FROM read_ndjson_auto(
'.agent-journal/experiments/*/runs/*/events.jsonl',
union_by_name = true,
filename = true)
WHERE "@type" = 'tool_call'
)
SELECT
current_step.tool AS from_tool,
next_step.tool AS to_tool,
COUNT(*) AS transitions
FROM tool_sequence current_step
JOIN tool_sequence next_step
ON current_step.filename = next_step.filename
AND next_step.sequence_number = current_step.sequence_number + 1
GROUP BY from_tool, to_tool
ORDER BY transitions DESC, from_tool, to_tool;
```
Transition counts describe recorded behavior; they do not establish a quality threshold by themselves.
Interpret them with task outcomes, run configuration, and enough repeated trials for the comparison you intend to make.
## Analyze derived events separately
Per-step cost attribution and step outcomes live in `analysis.jsonl`, not `events.jsonl`.
Read that file with the same `read_ndjson_auto(..., union_by_name = true)` pattern.
Use the run directory or stream header for run identity, and join an analysis `stepId` to the execution event's stable `id` when the event has one.
DuckDB reads the JSONL files directly, but Agent Journal's Java `loadEvents` and `loadDerivedEvents` methods currently load the requested file into memory.
Use DuckDB or another streaming data tool for large cross-run analysis.
# API Reference
Source: https://lab.pollack.ai/docs/agent-journal/api-reference
Agent Journal 1.8.0 core, storage, event, evaluation, and capture APIs
This page describes the public API shipped in Agent Journal 1.8.0.
## Coordinates
| Artifact | Purpose | Java |
| ----------------------------------------------------- | ------------------------------------------------------ | ----------- |
| `io.github.markpollack:journal-core:1.8.0` | Core domain, storage, evaluation, feedback, and traces | 17+ |
| `io.github.markpollack:claude-code-capture:1.8.0` | Claude Code capture | 21+ runtime |
| `io.github.markpollack:gemini-cli-capture:1.8.0` | Gemini CLI capture | 21+ runtime |
| `io.github.markpollack:grok-cli-capture:1.8.0` | Grok CLI capture | 17+ |
| `io.github.markpollack:codex-cli-capture:1.8.0` | Codex CLI capture | 17+ |
| `io.github.markpollack:antigravity-cli-capture:1.8.0` | Antigravity CLI capture | 17+ |
## `Journal`
| Method | Result | Notes |
| ---------------------------------------------------------- | ---------------- | ------------------------------------------------------- |
| `run(String experimentId)` | `RunBuilder` | Primary run factory |
| `experiment(String experimentId)` | `Experiment` | Gets or creates an experiment |
| `experiment(String id, Experiment.Builder builder)` | `Experiment` | Builder is ignored if the experiment already exists |
| `configure(JournalStorage storage)` | `void` | Call before creating runs |
| `storage()` | `JournalStorage` | Returns the global backend |
| `registerEventType(String, Class extends JournalEvent>)` | `void` | Registers an external subtype on the configured storage |
| `reset()` | `void` | Resets storage configuration and the experiment cache |
## `RunBuilder`
| Method | Purpose |
| ------------------------ | ----------------------------------------------------------------- |
| `name(String)` | Human-readable run name |
| `agent(String)` | Agent identifier |
| `task(String)` | Task identifier |
| `repository(String)` | Accepts a repository path; 1.8.0 does not persist it or act on it |
| `config(Config)` | Replace the run configuration |
| `config(String, Object)` | Add one immutable input value |
| `tags(Tags)` | Replace tags |
| `tag(String, String)` | Add one tag |
| `previousRun(String)` | Link a retry to the prior run ID |
| `parentRun(String)` | Link a child run to a parent run ID |
| `start()` | Create a `RUNNING` `Run` |
There is no `config(Map)` overload in 1.8.0.
The `repository(String)` setter is present, but the 1.8.0 run implementation does not consume the stored builder value.
## `Run`
| Area | Methods |
| ----------- | --------------------------------------------------------------------------------- |
| Identity | `id()`, `name()`, `experiment()`, `agentId()`, `previousRunId()`, `parentRunId()` |
| Data | `config()`, `summary()`, `tags()`, `status()` |
| Execution | `logEvent(JournalEvent)`, `logMetric(...)` |
| Analysis | `logDerivedEvent(DerivedEvent)` |
| Artifacts | `logArtifact(String, String)`, `logArtifact(String, byte[], Map)` |
| Instruments | `metrics()`, `calls()` |
| Lifecycle | `setSummary(...)`, `fail(Throwable)`, `finish(RunStatus)`, `close()` |
## Event factories
`JournalEvent` is extensible.
Built-in event records include timestamps and expose their Java `type()` value; file serialization uses the registered `@type` name.
| Event | Common factory or builder |
| --------------------- | -------------------------------------------------------------------------------------------------- |
| `LLMCallEvent` | `of(model, inputTokens, outputTokens, costUsd)`, `builder()` |
| `ToolCallEvent` | `success(tool, input, output, durationMs)`, `failure(tool, input, error, durationMs)`, `builder()` |
| `StateChangeEvent` | `of(from, to, reason)` |
| `MetricEvent` | `of(name, value, tags)` |
| `CustomEvent` | `of(name)`, `of(name, attributes)` |
| `GitPatchEvent` | `of(baseBranch, fileChanges)` |
| `GitCommitEvent` | `of(sha, message, branch)` |
| `GitBranchEvent` | `created(branchName, fromRef)`, `checkedOut(branchName)`, `deleted(branchName)` |
| `GitPullRequestEvent` | `created(...)`, `updated(...)`, `merged(...)`, `closed(...)` |
### Token, cost, and timing values
```java theme={null}
TokenUsage simple = TokenUsage.of(1200, 450);
TokenUsage withThinking = TokenUsage.of(1200, 450, 300);
TokenUsage allFields = new TokenUsage(
1200, 450, 300, 200, 800, 0);
CostBreakdown cost = CostBreakdown.of(0.018, 0.00675);
TimingInfo timing = TimingInfo.of(2500, 2100, 400);
```
The `TokenUsage` constructor order is input, output, thinking, cache creation, cache read, and tool-use tokens.
There is no five-argument `TokenUsage.of(...)` overload in 1.8.0.
## `JournalStorage`
| Area | Operations |
| -------------------- | ------------------------------------------------------------------------- |
| Experiments | `saveExperiment`, `loadExperiment`, `listExperiments`, `experimentExists` |
| Runs | `saveRun`, `loadRun`, `listRuns`, `runExists` |
| Execution events | `appendEvent`, `loadEvents` |
| Derived events | `appendDerivedEvent`, `loadDerivedEvents`, `persistsDerivedEvents` |
| Feedback | `appendFeedback`, `loadFeedback` |
| Artifacts | `saveArtifact`, `loadArtifact`, `listArtifacts` |
| External event types | `registerEventSubtype` |
`JsonFileStorage` implements every operation and reports durable derived-event persistence.
`InMemoryStorage` implements every operation in memory and reports the default non-durable derived-event behavior.
## Evaluation API
`EvalSubjectSources.fromJournal(storage, experimentId, runId)` reads a run and produces subjects.
`EvalSubjectSources.fromEvents(events, runId)` adapts an existing event list.
`EvalSubjectQuery` supports:
| Method | Purpose |
| ------------------------------- | ------------------------------- |
| `from(EvalSubjectSource)` | Start a query |
| `kind(EvalSubjectKind)` | Filter by kind |
| `where(Predicate)` | Add a predicate |
| `toSet()` | Materialize an `EvalSubjectSet` |
| `groupBy(Function)` | Group into subject sets |
| `countBy(Function)` | Count by classifier |
| `count()` | Count matches |
## Capture parsers
### Claude Code
```java theme={null}
SessionLogParser.parse(response, phaseName, promptText)
SessionLogParser.parse(response, phaseName, promptText, traceFile)
SessionLogParser.parse(response, phaseName, promptText, traceFile, contentMode)
SessionLogParser.parse(response, phaseName, promptText, traceFile, contentMode, rawMode)
```
`response` is an `Iterator`.
The default content mode for trace-writing overloads is `TRUNCATED`; the default raw mode is `NONE`.
`RunRecorder` wraps an open `Run`, inherits `recordPhase(PhaseCapture)`, exposes `run()` and `lenient()`, and owns `finish()`/`close()`.
### Gemini CLI
```java theme={null}
GeminiSessionParser.parse(result, phaseName, promptText)
GeminiSessionParser.parse(result, phaseName, promptText, traceFile)
GeminiSessionParser.parse(result, phaseName, promptText, traceFile, contentMode)
```
`result` is a Gemini SDK `QueryResult`.
`GeminiRunRecorder.recordPhase(GeminiPhaseCapture)` records the core and derived projections.
## Storage cautions
* `JsonFileStorage` does not validate path containment for caller-supplied identifiers or artifact names in 1.8.0.
* Use one writer per run and do not assume multi-process safety.
* File-backed load methods read the requested JSONL file into memory.
* Portable traces can contain sensitive content; see [Capture SDK Sessions](/docs/agent-journal/capture-sessions).
# Capture SDK Sessions
Source: https://lab.pollack.ai/docs/agent-journal/capture-sessions
Record Claude Code, Gemini CLI, Grok, Codex, and Antigravity results with Agent Journal 1.8.0
Agent Journal provides five vendor adapters that write compatible core events and portable traces.
The Claude and Gemini adapters parse a typed SDK object: Claude consumes the [Claude Agent SDK for Java](/projects/claude-agent-sdk) and Gemini the `gemini-cli-sdk` published with [Agent Client](/projects/agent-client).
Both require a Java 21 runtime, because those SDK dependencies are published as Java 21 bytecode.
The Grok, Codex, and Antigravity adapters parse their CLI's durable JSONL directly and carry no vendor SDK dependency, so they run on Java 17 like `journal-core`.
All three produce an ordered tool trajectory; Gemini does not.
## Claude Code
Add the adapter:
```xml theme={null}
io.github.markpollackclaude-code-capture1.8.0
```
`SessionLogParser` converts an `Iterator` from the Claude Code SDK into a `PhaseCapture`.
The six-argument overload also writes a portable trace and controls modeled and raw content independently:
```java theme={null}
import io.github.markpollack.claude.agent.sdk.parsing.ParsedMessage;
import io.github.markpollack.journal.claude.PhaseCapture;
import io.github.markpollack.journal.claude.SessionLogParser;
import io.github.markpollack.journal.trace.TraceContentMode;
import io.github.markpollack.journal.trace.TraceRawMode;
import java.nio.file.Path;
import java.util.Iterator;
PhaseCapture phase = SessionLogParser.parse(
response,
"implement",
promptText,
Path.of(".agent-journal/traces/implement.jsonl"),
TraceContentMode.TRUNCATED,
TraceRawMode.NONE);
```
Here, `response` is an `Iterator` returned by the SDK and `promptText` is the exact prompt or `null`.
Record the capture into a durable journal with `RunRecorder`:
```java theme={null}
import io.github.markpollack.journal.Journal;
import io.github.markpollack.journal.claude.RunRecorder;
import io.github.markpollack.journal.storage.JsonFileStorage;
import java.nio.file.Path;
Journal.configure(new JsonFileStorage(Path.of(".agent-journal")));
try (RunRecorder recorder = new RunRecorder(
Journal.run("capture-experiment").agent("claude-code").start())) {
recorder.recordPhase(phase);
}
```
The recorder writes execution events to `events.jsonl` and per-step cost attribution to `analysis.jsonl`.
It fails at finish when it produced derived events but the configured backend does not persist them durably.
Call `lenient()` only when losing the derived stream after process exit is intentional.
## Gemini CLI
Add the adapter:
```xml theme={null}
io.github.markpollackgemini-cli-capture1.8.0
```
`GeminiSessionParser` converts the Gemini SDK's synchronous `QueryResult` into a `GeminiPhaseCapture`:
```java theme={null}
import io.github.markpollack.agents.geminisdk.types.QueryResult;
import io.github.markpollack.journal.Run;
import io.github.markpollack.journal.gemini.GeminiPhaseCapture;
import io.github.markpollack.journal.gemini.GeminiRunRecorder;
import io.github.markpollack.journal.gemini.GeminiSessionParser;
GeminiPhaseCapture phase = GeminiSessionParser.parse(
result, "query", "summarize the repository");
new GeminiRunRecorder(run).recordPhase(phase);
```
Here, `result` is a `QueryResult` and `run` is an open Agent Journal `Run`.
Configure `JsonFileStorage` if the derived `StepCostEvent` must survive process exit.
Gemini's typed SDK exposes result-level text, usage, cost, duration, model, and status, but it does not expose per-tool calls.
The adapter therefore records one turn-level step and does not invent tool detail.
It has no `TraceRawMode` because the typed Gemini message does not retain a verbatim wire envelope.
## Grok CLI
Add the adapter:
```xml theme={null}
io.github.markpollackgrok-cli-capture1.8.0
```
`GrokSessionParser` reads Grok's ACP-shaped `streaming-json` output from a file or a `BufferedReader`:
```java theme={null}
import io.github.markpollack.journal.Run;
import io.github.markpollack.journal.grok.GrokPhaseCapture;
import io.github.markpollack.journal.grok.GrokRunRecorder;
import io.github.markpollack.journal.grok.GrokSessionParser;
import java.nio.file.Path;
GrokPhaseCapture phase = GrokSessionParser.parse(
Path.of("grok-stream.jsonl"), "implement", promptText);
new GrokRunRecorder(run).recordPhase(phase);
```
The parser pairs `tool_call` and `tool_call_update` records by `toolCallId`, keeping both the tool name Grok reports and its ACP semantic kind.
Grok's stream carries a real session cost in `end.total_cost_usd` but no durable join from a turn to the tools it ran, so the session total is retained and attributed evenly across the captured tool steps.
## Codex CLI
Add the adapter:
```xml theme={null}
io.github.markpollackcodex-cli-capture1.8.0
```
`CodexSessionParser` reads a durable Codex rollout JSONL file:
```java theme={null}
import io.github.markpollack.journal.Run;
import io.github.markpollack.journal.codex.CodexPhaseCapture;
import io.github.markpollack.journal.codex.CodexRunRecorder;
import io.github.markpollack.journal.codex.CodexSessionParser;
import java.nio.file.Path;
CodexPhaseCapture phase = CodexSessionParser.parse(
Path.of("rollout.jsonl"), "implement", promptText);
new CodexRunRecorder(run).recordPhase(phase);
```
Codex records most tool invocations under the outer function name `exec`, which would collapse every step into one undifferentiated name.
The parser deliberately classifies the nested `payload.input` call instead, extracting `exec_command` arguments without evaluating them and assigning semantic names such as `Search`, `Read`, `Inspect`, `Test`, `Build`, and `Git`.
The classifier is conservative about shell syntax.
Aliases, shell functions, quoted operators, and work hidden behind an interpreter fall back to `Shell`.
The raw input and command are retained on the record, so callers can reclassify them later.
Codex rollout token counts, including cached and reasoning tokens, are preserved.
## Antigravity CLI
Add the adapter:
```xml theme={null}
io.github.markpollackantigravity-cli-capture1.8.0
```
`AntigravitySessionParser` reads Antigravity's streaming JSON:
```java theme={null}
import io.github.markpollack.journal.Run;
import io.github.markpollack.journal.antigravity.AntigravityPhaseCapture;
import io.github.markpollack.journal.antigravity.AntigravityRunRecorder;
import io.github.markpollack.journal.antigravity.AntigravitySessionParser;
import java.nio.file.Path;
AntigravityPhaseCapture phase = AntigravitySessionParser.parse(
Path.of("antigravity-stream.jsonl"), "implement", promptText);
new AntigravityRunRecorder(run).recordPhase(phase);
```
The parser pairs active and terminal updates by `step_index` and handles both `DONE` and `ERROR` steps, recording parameters, output or error text, duration, and terminal token usage.
Stable identities are derived from the conversation and step positions.
## Cost provenance
Neither Codex nor Antigravity reports monetary cost in these streams.
Their cost records are zero-valued and explicitly marked `costAvailable=false` with an `unreported` source, so a downstream reader can distinguish "this run cost nothing" from "this CLI never said."
## Portable trace modes
`TraceContentMode` controls content bodies on modeled trace lines:
| Mode | Modeled content behavior |
| ----------- | ---------------------------------------------------------------------- |
| `FULL` | Writes complete assistant text, thinking text, and tool-result content |
| `TRUNCATED` | Default; caps each modeled content body at 60,000 characters |
| `LENGTHS` | Omits those modeled content bodies and records their lengths |
`TraceRawMode` is Claude-only and orthogonal:
| Mode | Raw vendor messages |
| ------ | ---------------------------------------------------------------- |
| `NONE` | Default; no raw vendor-message lines |
| `FULL` | Writes each available vendor message as an unredacted `raw` line |
All trace modes require a data-handling decision.
The capture record and journal events can contain the caller-supplied prompt text.
Tool inputs are present on portable `tool_use` lines even in `LENGTHS` mode.
`TRUNCATED` and `FULL` portable traces can include assistant text, file contents, tool results, and command output.
`TraceRawMode.FULL` adds the complete available vendor message and can include fields omitted from the typed model.
Treat trace and journal files as sensitive, restrict filesystem access, and keep them out of version control.
## Two schemas
Do not conflate the two JSONL formats:
* Canonical journal streams use `@type` and currently have schema version 1.
* Portable capture traces use `type`, `ts`, and `seq` and currently have schema version 2.
Both formats have their own header and evolution contract.
# Core Concepts
Source: https://lab.pollack.ai/docs/agent-journal/concepts
Experiments, runs, event streams, derived analysis, feedback, and storage boundaries
Agent Journal models a bounded execution as a `Run` and groups comparable runs in an `Experiment`.
```text theme={null}
Experiment
└── Run
├── Config immutable inputs
├── events.jsonl recorded execution facts
├── analysis.jsonl derived interpretations
├── feedback.jsonl reviewer feedback
├── Summary mutable outputs in run.json
└── Artifacts caller-named byte content
```
## Experiments and runs
An experiment is a stable grouping identifier for repeated trials.
A run is one execution with a start time, an optional finish time, and a `RUNNING`, `FINISHED`, or `FAILED` status.
Inputs belong in `Config`, which becomes immutable when the run starts.
Outputs belong in `Summary`, where the latest value for a key wins.
Runs can link to a prior attempt with `previousRun(...)` or to a parent execution with `parentRun(...)`.
These are identifiers recorded on the run; Agent Journal does not schedule retries or child agents.
## Execution events
`JournalEvent` is an extensible interface, not a closed or sealed hierarchy.
Agent Journal 1.8.0 registers these built-in JSON subtypes:
| `@type` | Java type | Purpose |
| -------------- | --------------------- | ------------------------------------------------------------------- |
| `llm_call` | `LLMCallEvent` | Provider, model, token, cost, timing, and response metadata |
| `tool_call` | `ToolCallEvent` | Tool name, input, output or error, duration, and optional stable ID |
| `state_change` | `StateChangeEvent` | Named state transition and reason |
| `metric` | `MetricEvent` | Point-in-time numeric measurement with tags |
| `custom` | `CustomEvent` | Application-defined attributes |
| `git_patch` | `GitPatchEvent` | File-level patch summary |
| `git_commit` | `GitCommitEvent` | Commit identity and metadata |
| `git_branch` | `GitBranchEvent` | Branch operation |
| `git_pr` | `GitPullRequestEvent` | Pull request operation |
| `feedback` | `FeedbackEvent` | Human feedback when serialized as an event |
Register an external event implementation before reading it from file storage:
```java theme={null}
Journal.registerEventType("workflow_step", WorkflowStepEvent.class);
```
The type name and class are supplied by the integration that owns the external event.
## Recorded and derived data
`events.jsonl` is the append-only record of what the application logged during execution.
`analysis.jsonl` is a separate append-only stream for interpretations computed about that execution.
Agent Journal 1.8.0 provides two derived event types:
* `StepCostEvent` records a cost allocation and preserves the actual run cost separately from the attributed share.
* `StepOutcomeEvent` records caller-supplied goal distance and outcome metrics for a step.
Both streams begin with an independent schema header when the first event is appended.
`JsonFileStorage` skips header lines when loading events.
The canonical stream schema version is independent from the portable trace schema version.
## Metrics and calls
Every run exposes a `MetricRegistry` with counters, timers, and gauges.
`run.logMetric(...)` also appends a `MetricEvent` to the execution stream.
`CallTracker` records an in-memory hierarchy of named operations and durations within a run.
It does not create distributed spans, a collector, or a monitoring service.
## Evaluation subjects and feedback
`EvalSubjectSources` converts selected journal events into source-neutral subjects for evaluation.
The core adapter maps LLM calls, tool calls, state changes, and custom events; metric and git events are skipped.
The `EvalSubjectKind` enum also reserves kinds used by other adapters, such as workflow steps, router decisions, retrieval results, final outputs, and feedback.
The feedback API stores reviewer judgments in `feedback.jsonl`.
It supports binary, numerical, and categorical scores and can export reviewed items for labeled datasets.
## Storage behavior
| Behavior | `InMemoryStorage` | `JsonFileStorage` |
| ----------------- | ------------------------ | ----------------------------------- |
| Survives JVM exit | No | Yes |
| Execution events | In memory | `events.jsonl` |
| Derived events | In memory only | `analysis.jsonl` |
| Feedback | In memory | `feedback.jsonl` |
| Artifacts | Cloned byte arrays | Files under `artifacts/` |
| Intended use | Tests and ephemeral work | Local development and research runs |
`JsonFileStorage` appends one JSON object per line without rewriting the existing stream.
Its load methods currently read the whole requested file into memory before deserializing it.
## Operational and security boundaries
`JsonFileStorage` is a local filesystem backend, not a database, access-control layer, or multi-tenant security boundary.
* Treat journal directories as potentially sensitive operational data.
* Use trusted caller-controlled experiment IDs, run IDs, and artifact names; 1.8.0 does not enforce path containment.
* Use one writer per run for file-backed storage.
* Do not infer concurrent-append or multi-process safety from the append-only format; current tests do not establish those guarantees.
* Large journals can require substantial heap when loaded because file-backed reads use whole-file loading.
For content captured from vendor SDKs, see [Capture SDK Sessions](/docs/agent-journal/capture-sessions).
# Getting Started
Source: https://lab.pollack.ai/docs/agent-journal/getting-started
Record and persist a first Agent Journal run with journal-core 1.8.0
This tutorial records one LLM call and one tool call, then persists the run to local JSON and JSONL files.
## Requirements
* Java 17 or later for `journal-core`
* Maven 3.9 or the Maven Wrapper
## 1. Add `journal-core`
```xml theme={null}
io.github.markpollackjournal-core1.8.0
```
## 2. Configure local storage
Configure storage before creating a run:
```java theme={null}
import io.github.markpollack.journal.Journal;
import io.github.markpollack.journal.storage.JsonFileStorage;
import java.nio.file.Path;
Journal.configure(new JsonFileStorage(Path.of(".agent-journal")));
```
The default backend is `InMemoryStorage`, so a run is not durable unless you configure storage.
## 3. Record a run
```java theme={null}
import io.github.markpollack.journal.Journal;
import io.github.markpollack.journal.Run;
import io.github.markpollack.journal.event.LLMCallEvent;
import io.github.markpollack.journal.event.ToolCallEvent;
import java.util.Map;
try (Run run = Journal.run("first-experiment")
.name("attempt-1")
.agent("example-agent")
.task("summarize-repository")
.config("model", "claude-opus-4-5")
.tag("environment", "local")
.start()) {
run.logEvent(LLMCallEvent.of("claude-opus-4-5", 1200, 450, 0.02475));
run.logEvent(ToolCallEvent.success(
"Bash",
Map.of("command", "git status --short"),
Map.of("exitCode", 0, "output", ""),
150));
run.setSummary("success", true);
run.logArtifact("result.txt", "Repository is clean.");
}
```
`Run` implements `AutoCloseable`.
Normal closure produces `FINISHED`; call `run.fail(exception)` to record `FAILED` explicitly.
## 4. Inspect the files
The example produces this layout:
```text theme={null}
.agent-journal/
└── experiments/
└── first-experiment/
├── experiment.json
└── runs/
└── {run-id}/
├── run.json
├── events.jsonl
└── artifacts/
└── result.txt
```
The first `events.jsonl` line is a schema header.
Each following line is one event and carries an `@type` discriminator.
```json theme={null}
{"@type":"header","schemaVersion":1,"stream":"events","runId":"..."}
{"@type":"llm_call","timestamp":"...","provider":null,"model":"claude-opus-4-5","tokenUsage":{"inputTokens":1200,"outputTokens":450,"thinkingTokens":0,"cacheCreationTokens":0,"cacheReadTokens":0,"toolUseTokens":0},"cost":{"inputCostUsd":0.02475,"outputCostUsd":0.0,"thinkingCostUsd":0.0,"cacheSavingsUsd":0.0},"timing":null,"finishReason":null,"responseId":null,"metadata":{}}
```
Use caller-controlled trusted identifiers for `experimentId`, `runId`, and artifact names.
Version 1.8.0 does not enforce path containment for those values, so do not pass untrusted path fragments or separators.
## 5. Isolate tests
Use the in-memory backend and reset global state around each test:
```java theme={null}
import io.github.markpollack.journal.Journal;
import io.github.markpollack.journal.storage.InMemoryStorage;
Journal.reset();
Journal.configure(new InMemoryStorage());
try {
try (var run = Journal.run("test-experiment").start()) {
run.setSummary("result", "ok");
}
} finally {
Journal.reset();
}
```
## Next steps
Learn what is persisted and what is derived.
Query the generated event files with DuckDB.
# What's New
Source: https://lab.pollack.ai/docs/agent-journal/whats-new
Release notes for Agent Journal — auto-generated from git history
## 1.8.0 (2026-08-22)
* Three new tool-trajectory capture adapters — `grok-cli-capture`, `codex-cli-capture`, and `antigravity-cli-capture` — turn each CLI's durable JSONL stream into ordered `ToolCallEvent` and `StepCostEvent` records with stable tool-call identities
* Grok capture parses the ACP-shaped `streaming-json` output, pairing `tool_call` and `tool_call_update` by `toolCallId` and preserving both the reported tool name and the ACP semantic kind. The stream has no durable turn-to-tool cost join, so the real session total from `end.total_cost_usd` is retained and attributed evenly across captured tool steps
* Codex capture parses durable rollout JSONL and deliberately classifies the nested `payload.input` call rather than the outer `exec` function name that Codex records for most tool invocations, assigning semantic names such as `Search`, `Read`, `Inspect`, `Test`, `Build`, and `Git`. The classifier is conservative around shell syntax — aliases, shell functions, quoted operators, and work hidden behind an interpreter fall back to `Shell`, with raw input and command retained for later reclassification
* Antigravity capture pairs active and terminal updates by `step_index`, handles both `DONE` and `ERROR` steps, records parameters, output or error text, duration, and terminal token usage, and derives stable identities from conversation and step positions
* Codex and Antigravity report no monetary cost in these streams, so cost records are zero-valued and explicitly marked `costAvailable=false` with an `unreported` source
* Gemini CLI capture remains turn-level and provides no tool trajectory, so Gemini is not part of this release's multi-CLI tool-trajectory claim
* The Claude Code SDK dependency moves from 1.4.0 to 1.5.0
* Additive on the existing event and trace contracts. `journal-core` and the three new capture modules target Java 17; the Claude Code and Gemini CLI capture modules still require Java 21 for their vendor SDKs. 488 tests pass across the seven-module reactor
## 1.7.0 (2026-08-18)
* Standalone consumers now resolve CVE-clear Jackson floors without an external BOM: Jackson 2.21.6 for all three modules and Jackson 3.1.6 on Claude capture's transitive Jackson 3 path. Agent Journal's source and stored formats continue to use Jackson 2; this is not a Jackson 3 migration.
* Every binary and source JAR now includes `META-INF/LICENSE`, and every Javadoc JAR includes `resources/LICENSE`. The BSL 1.1 terms are unchanged.
* The parent artifact publishes an aggregate CycloneDX 1.6 JSON SBOM for all three modules under the `cyclonedx` classifier.
* Capture-module POMs now declare their actual Java 21 runtime requirement; `journal-core` remains Java 17.
* Snapshot publishing now runs the full test suite, and shared build/release workflows are pinned to an immutable commit.
* No API, event-schema, or trace-format change: both journal streams remain schema version 1 and portable traces remain schema version 2. All 482 tests pass.
## 1.6.0 (2026-07-01)
* First-class journal-capture primitives (slice 1): `PhaseCapture.stepCosts()`, `JournalSteps.fromEvents()`, production fail-loud `RunRecorder`, `JournalStorage.persistsDerivedEvents()`, per-turn usage in the immutable log
* A1: `AttributionMethod.EVEN_SPLIT` — coarse fallback when per-turn tokens are unavailable
* A5: per-file `{"@type":"header","schemaVersion":1}` line on `events.jsonl` and `analysis.jsonl` (loaders skip it)
* Cost-metering fix: headline `LLMCallEvent.tokenUsage` is now the cost-bearing Σ-per-turn-by-type aggregate (incl. cache) instead of the last-`ResultMessage` snapshot, which under-counted \~2× on long runs — same field, corrected value, no schema change
* Designed as an additive change to the capture contract; 482 tests green. No cross-version reader test was run. Ships in agentworks-bom 1.13.0
## 1.5.0 (2026-06-17)
* docs: fix TraceContentMode import path (moved to journal.trace in capture-fidelity R2)
* R2.10 emission: wire StepCostEvent into analysis.jsonl from capture
* Stage R2.9: Implement computeJudgeAgreement (judge\<->human calibration)
* Stage R2.6: Per-step goal/outcome channel (StepOutcomeEvent)
* Stage R2.10: DerivedEvent hierarchy + StepCostEvent + analysis.jsonl sidecar
* Stage R2.8: First-class Gemini capture (gemini-cli-capture module)
* Stage R2.7: Move portable TraceWriter into journal-core
* Stage R2.5a: Delineate sub-agent spawns (interiors are out-of-stream)
* Stage R2.4: Emit derived step\_cost lines into the trace (epic gate)
* Stage R2.3b: Per-step cost attribution + JournalStep
* Stage R2.3a: Stable step identity for events (fixes subjectId fragility)
* Stage R2.2: Per-turn usage capture (TurnUsage + ModelCost from rawJson)
* Stage R2.1: Consume rawJson — verbatim raw trace line (rawMode=NONE|FULL)
* Stage R2.0: Bump claude-code-sdk to 1.4.0 (unblocks rawJson consumption)
* CI: enforce OWASP CVE gate via reusable security-scan workflow
* Add OWASP dependency-check CVE gate (-Powasp) + Trivy security-scan script
* docs: version pins to 1.4.0
## 1.4.0 (2026-06-06)
* Fix CI: build on Java 21 to match claude-code-sdk artifact class version
* Steps 1.1-1.2: TraceWriter v2 content capture with TraceContentMode
## 1.3.0 (2026-06-04)
* Include tool inputs in TraceWriter JSONL and escape control characters
## 1.2.0 (2026-05-19)
* Add feedback event type, eval subjects, and feedback service
## 1.1.0 (2026-05-15)
* Update claude-code-sdk dependency to released 1.1.0
* Skip tests on snapshot publish — CI handles test validation
* Update the `claude-code-sdk` dependency and imports to its `io.github.markpollack` coordinates
## 1.0.1 (2026-04-30)
* Bump version to 1.0.1-SNAPSHOT
* Open JournalEvent to external implementations; add registerEventType API
* Update README: released version 0.9.0
* Fix release workflow: add contents write permission for git tagging
## 0.9.0 (2026-03-29)
* Initial release.
# API Reference
Source: https://lab.pollack.ai/docs/agent-judge/api-reference
Main public contracts and packages in Agent Judge 0.15
This page is a map of the 0.15 API.
The generated aggregate Javadoc remains the detailed signature reference, and the [tutorial source](https://github.com/markpollack/agent-judge-tutorial) supplies compiled usage examples.
## Packages
| Package | Main contents |
| --------------------------------------------- | ---------------------------------------------------------------- |
| `io.github.markpollack.judge` | `Judge`, `AsyncJudge`, metadata, naming, and composition |
| `io.github.markpollack.judge.context` | `JudgmentContext` and `ExecutionStatus` |
| `io.github.markpollack.judge.result` | `Judgment`, `JudgmentStatus`, and `Check` |
| `io.github.markpollack.judge.fs` | Core file-system judges |
| `io.github.markpollack.judge.jury` | Juries, verdicts, voting strategies, and policies |
| `io.github.markpollack.judge.ai` | Model-backed judge composition and classifiers |
| `io.github.markpollack.judge.ai.model` | Framework-neutral model requests, responses, messages, and usage |
| `io.github.markpollack.judge.ai.prompt` | Prompt templates, renderers, variables, and text sources |
| `io.github.markpollack.judge.exec` | Build, command, and class-version judges |
| `io.github.markpollack.judge.coverage` | JaCoCo coverage judges and parser |
| `io.github.markpollack.judge.file` | Semantic file judges |
| `io.github.markpollack.judge.file.comparator` | Java, Maven, and XML comparators |
| `io.github.markpollack.judge.llm` | Spring AI judging adapter and correctness judge |
| `io.github.markpollack.judge.rag` | RAG context helpers and judges |
| `io.github.markpollack.judge.springai` | Spring AI evaluated-side bridge |
| `io.github.markpollack.judge.langchain4j` | LangChain4j evaluated-side bridge |
| `io.github.markpollack.judge.koog` | Koog evaluated-side bridge |
| `io.github.markpollack.judge.agentclient` | AgentClient evaluated-side bridge and judging adapter |
## Judge contracts
`Judge` is a functional interface whose single method maps `JudgmentContext` to `Judgment`.
`AsyncJudge` supplies the `CompletableFuture` variant.
`JudgeWithMetadata` marks a judge that exposes `JudgeMetadata`.
`DeterministicJudge` is the reusable base for rule-based implementations, and `NamedJudge` attaches identity to another judge without changing its evaluation.
`Judges` contains logical composition and naming helpers.
## JudgmentContext
`JudgmentContext` is the immutable in-process input record.
It carries goal, workspace, execution duration, start time, optional agent output, execution status, optional error, and arbitrary context metadata.
The context metadata may contain framework-native objects because it does not cross the result boundary.
Bridge modules use it to expose relevant runtime evidence to ordinary judges.
`ExecutionStatus` values are `SUCCESS`, `FAILED`, `TIMEOUT`, `CANCELLED`, `REFUSED`, and `UNKNOWN`.
## Judgment
`Judgment` is the immutable portable output record.
| Component | Contract |
| ----------- | ---------------------------------------------------- |
| `status` | Required `PASS`, `FAIL`, `ABSTAIN`, or `ERROR` |
| `score` | Optional finite `Double` normalized to `[0.0, 1.0]` |
| `label` | Optional non-blank classification |
| `reasoning` | Required text and non-blank for `ABSTAIN` or `ERROR` |
| `checks` | Immutable granular Boolean checks |
| `metadata` | Recursively portable and immutable evidence |
Direct factories are `pass(reasoning)`, `fail(reasoning)`, `abstain(reasoning)`, and `error(reasoning)`.
`verdict(boolean)` starts an enrichable Boolean result without storing a duplicate score.
`scored(normalizedScore).passingAt(threshold)` builds a measured result whose status follows an explicit threshold.
The raw-scale overload normalizes a bounded source measurement before the threshold is applied.
`effectiveScore()` returns the explicit score when present, derives `1.0` or `0.0` for an unscored `PASS` or `FAIL`, and is empty for `ABSTAIN` or `ERROR`.
`elapsed()` derives `Duration` from the portable `elapsedMillis` metadata value.
## Check
`Check` is a named Boolean sub-assertion with a message.
Use its passing and failing factories to explain the evidence contributing to an overall result.
## Jury contracts
`Jury.vote(context)` returns a `Verdict` containing aggregate and individual evidence.
`SimpleJury` runs peer judges, while `CascadedJury` evaluates ordered tiers.
Voting strategies are `MajorityVotingStrategy`, `ConsensusStrategy`, `AverageVotingStrategy`, `WeightedAverageStrategy`, and `MedianVotingStrategy`.
`TiePolicy`, `ErrorPolicy`, and `TierPolicy` make edge behavior explicit.
See [Jury System](/docs/agent-judge/jury-system) for their reduction rules.
## AI composition
`ModelBackedJudge` combines `JudgePromptTemplate`, `JudgeModel`, and `JudgmentClassifier`.
`JudgmentClassifiers` supplies common classifiers, and `LabelJudgmentClassifier` maps explicit labels to outcomes and optional declared scores.
`JudgeModelRequest` and `JudgeModelResponse` are framework-neutral.
`Usage` records optional provider-reported input, output, reasoning, cache-creation, cache-read, and total token quantities, and projects them to portable metadata.
It never derives price or assumes that the categories sum to the reported total.
## Framework bridges
| Evaluator | Context builder | Backend type |
| ---------------------- | ----------------------------------- | ------------------------ |
| `SpringAiEvaluator` | `SpringAiJudgmentContextBuilder` | Spring AI `ChatResponse` |
| `LangChain4jEvaluator` | `LangChain4jJudgmentContextBuilder` | LangChain4j `Result` |
| `KoogEvaluator` | `KoogJudgmentContextBuilder` | Koog `AIAgent` |
| `AgentClientEvaluator` | `AgentClientJudgmentContextBuilder` | AgentClient |
`SpringAiJudgeModel` and `AgentClientJudgeModel` adapt those backends to the judging-side `JudgeModel` interface.
## Version and migration
Use all Agent Judge artifacts at version `0.15.0`.
Consumers migrating from the former score hierarchy should follow the [normalized Judgment migration guide](https://github.com/markpollack/agent-judge/blob/main/consumer-handoff-normalized-judgment.md).
# Built-in Judges
Source: https://lab.pollack.ai/docs/agent-judge/built-in-judges
Deterministic, execution, file, LLM, RAG, and requirements judges in Agent Judge 0.16
Every built-in judge returns the normalized `Judgment` model.
Status is required, while a stored score and label are present only when that judge completed a measurement or classification.
## Core file-system judges
Artifact: `agent-judge-core`.
| Judge | Purpose |
| ------------------- | ----------------------------------------------------------------------------------- |
| `FileExistsJudge` | Check that a workspace-relative path exists |
| `FileContentJudge` | Compare file content with exact, contains, or regular-expression matching |
| `SupersetDiffJudge` | Require output to contain a byte-exact reference project plus any allowed additions |
These judges produce Boolean outcomes and therefore normally store no score.
The executable starting point is [tutorial module 01](https://github.com/markpollack/agent-judge-tutorial/tree/main/module-01-single-judge).
**Changed in 0.16.0 — degenerate inputs no longer pass.** An empty path, `.`, a directory, an
absolute path outside the workspace, and a parent traversal are each refused rather than treated as
a satisfied check. Previously some of these returned `PASS` without examining anything.
This is the failure mode judges are most prone to: **a broken judge fails open with a plausible
result.** A test that cannot run reports a failure, but a judge that examines nothing still returns
a status, and `PASS` on an unexamined thing is indistinguishable from `PASS` on a verified one. If
you have suites that were quietly passing on a mistyped or empty path, they will start failing —
that is the fix working, not a regression.
## Command and coverage judges
Artifact: `agent-judge-exec`.
| Judge | Purpose |
| --------------------------- | ---------------------------------------------------------------------------- |
| `BuildSuccessJudge` | Run Maven, Gradle, or a configured build command and judge its exit status |
| `CommandJudge` | Execute an arbitrary command through Agent Sandbox and compare the exit code |
| `ClassVersionJudge` | Validate Java class-file bytecode versions |
| `CoveragePreservationJudge` | Reject a JaCoCo coverage regression beyond the configured tolerance |
| `CoverageImprovementJudge` | Measure normalized coverage improvement with an optional floor |
Build and command judges run real processes in the context workspace.
Use the project wrapper when present and choose timeouts appropriate to untrusted work.
[Tutorial module 02](https://github.com/markpollack/agent-judge-tutorial/tree/main/module-02-build-judge) executes the Maven build path.
## Semantic file judges
Artifact: `agent-judge-file`.
| Judge | Purpose |
| --------------------- | ----------------------------------------------------- |
| `FileComparisonJudge` | Dispatch files to the appropriate semantic comparator |
| `JavaSemanticJudge` | Compare Java syntax trees rather than formatting |
| `MavenSemanticJudge` | Compare important Maven model structure |
| `XmlSemanticJudge` | Compare XML document structure |
| `TextFileJudge` | Compare whitespace-normalized text |
These judges expect reference and actual file locations in the evaluation context as documented in the aggregate Javadoc.
## LLM judge
Artifact: `agent-judge-llm`.
| Judge | Purpose |
| ------------------ | -------------------------------------------------------------- |
| `CorrectnessJudge` | Ask a Spring AI-backed model whether output satisfies the goal |
| `LLMJudge` | Base template for specialized Spring AI LLM judges |
For a framework-neutral composition, use `ModelBackedJudge` from `agent-judge-ai-core` with a `JudgePromptTemplate`, `JudgeModel`, and `JudgmentClassifier`.
[Tutorial module 08](https://github.com/markpollack/agent-judge-tutorial/tree/main/module-08-model-backed-judge) runs that complete path with no credentials.
## RAG judges
Artifact: `agent-judge-rag`.
| Judge | Question answered |
| -------------------------- | ----------------------------------------------------------- |
| `FaithfulnessJudge` | Is the generated answer supported by the retrieved context? |
| `ContextualRelevanceJudge` | Is the retrieved context relevant to the question? |
| `HallucinationJudge` | Does the answer introduce unsupported claims? |
`RagContext` defines the `rag.question`, `rag.context`, and `rag.answer` metadata keys and the framework fallback conventions.
When required evidence is absent, these judges abstain rather than inventing a score.
## Framework evaluators
Evaluators are bridges rather than judges.
They execute or adapt framework output into `JudgmentContext`, then apply an ordinary `Judge` or `Jury`.
| Artifact | Evaluator | Evaluated value |
| -------------------------- | ---------------------- | ------------------------------------ |
| `agent-judge-spring-ai` | `SpringAiEvaluator` | Spring AI `ChatResponse` |
| `agent-judge-langchain4j` | `LangChain4jEvaluator` | LangChain4j `Result` |
| `agent-judge-koog` | `KoogEvaluator` | Koog `AIAgent` output |
| `agent-judge-agent-client` | `AgentClientEvaluator` | AgentClient responses and workspaces |
The credential-free bridge samples are [Koog module 09](https://github.com/markpollack/agent-judge-tutorial/tree/main/module-09-koog-evaluation) and [LangChain4j module 10](https://github.com/markpollack/agent-judge-tutorial/tree/main/module-10-langchain4j-evaluation).
## Requirements judges
New in 0.16.0. Artifact: `agent-judge-ai-core`.
These run a **written requirements document back against the implementation it describes**. The
input is something somebody wrote before the code existed — acceptance criteria, architectural
constraints — and the question is whether the implementation satisfies it.
| Type | Purpose |
| ------------------- | ---------------------------------------------------------- |
| `EarsCriterion` | One acceptance criterion, parsed from the specification |
| `EarsJudge` | Answers every acceptance criterion |
| `Rfc2119Constraint` | One architectural constraint, with its RFC 2119 keyword |
| `Rfc2119Judge` | Answers every architectural constraint |
| `Observation` | Non-binding evidence noticed while establishing a judgment |
### The document supplies the roster
This is what separates these from the other judges, and it changes how they aggregate. Elsewhere a
jury samples a population and an `ABSTAIN` is dropped as not applicable. Here the roster is fixed:
the document says the requirement applies, so *"could not be established"* is not *"does not
apply."*
The rollup is therefore strict, and abstentions are **not** dropped:
```
any ERROR -> ERROR
else any FAIL -> FAIL
else any ABSTAIN -> ABSTAIN
else -> PASS
```
Fifty-one criteria established and one unsettled is `ABSTAIN`, not `PASS`. The specification has not
been shown to hold — which is a different statement from it having been shown to fail, and both are
different from success. Understand this before composing these judges with anything else, because a
strategy that drops abstentions will silently convert "unverified" into "fine."
## Related
* [Getting started](/docs/agent-judge/getting-started)
* [Writing custom judges](/docs/agent-judge/custom-judge)
* [Jury system](/docs/agent-judge/jury-system)
# Writing Custom Judges
Source: https://lab.pollack.ai/docs/agent-judge/custom-judge
Implement status-first deterministic and model-backed judges
Choose the smallest extension point that expresses your policy.
| Need | Use |
| ----------------------------------------- | --------------------------- |
| A local rule with no reusable identity | A `Judge` lambda |
| A named lambda | `Judges.named(...)` |
| A reusable rule-based class | Extend `DeterministicJudge` |
| Prompt, model, and classifier composition | `ModelBackedJudge` |
| A specialized Spring AI template method | Extend `LLMJudge` |
## Return a status-first result
For a Boolean policy, return `Judgment.pass(reasoning)`, `Judgment.fail(reasoning)`, or begin with `Judgment.verdict(boolean)` when you also need checks or metadata.
Do not store `1.0` or `0.0` merely to repeat the status.
For a real measurement, begin with `Judgment.scored(normalizedScore)` and finish by declaring the passing threshold.
For a categorical policy, set an optional label in addition to the required status.
Use `Judgment.abstain(reasoning)` when the judge is not applicable or lacks required evidence.
Use `Judgment.error(reasoning)` when evaluation could not complete, and log the originating exception where it was caught.
## Start with a lambda
The complete compiled lambda examples live in [tutorial module 06](https://github.com/markpollack/agent-judge-tutorial/blob/main/module-06-lambda-judge/src/main/java/io/github/markpollack/judge/tutorial/module06/LambdaJudgeDemo.java).
They cover a three-line file rule, a named wrapper, jury participation, and error conversion.
A plain lambda has no discoverable identity.
Wrap it with `Judges.named` when verdicts and logs need a stable name, description, and `JudgeType`.
## Build a reusable deterministic judge
Extend `DeterministicJudge` when the rule has constructor configuration, helper methods, or granular checks.
Its constructor supplies `JudgeMetadata`; the subclass implements only `judge(JudgmentContext)`.
[PackageStructureJudge.java](https://github.com/markpollack/agent-judge-tutorial/blob/main/module-07-deterministic-judge/src/main/java/io/github/markpollack/judge/tutorial/module07/PackageStructureJudge.java) is the canonical compiled example.
It resolves paths from the context workspace, records three `Check` values, derives the overall status with `Judgment.verdict`, and converts file-reading failures to a reasoned `ERROR`.
## Compose a model-backed judge
`ModelBackedJudge` keeps three concerns independent.
1. `JudgePromptTemplate` renders evaluation inputs from a context.
2. `JudgeModel` invokes a backend.
3. `JudgmentClassifier` converts the response into a normalized judgment.
[ModelBackedJudgeDemo.java](https://github.com/markpollack/agent-judge-tutorial/blob/main/module-08-model-backed-judge/src/main/java/io/github/markpollack/judge/tutorial/module08/ModelBackedJudgeDemo.java) compiles and runs this pipeline with a stub model.
Production adapters include `SpringAiJudgeModel` and `AgentClientJudgeModel`.
`LabelJudgmentClassifier` requires an explicit label-to-status policy.
Assign a normalized score only when the label policy genuinely declares a measurement.
Unknown labels produce a reasoned `ABSTAIN` with the raw judge output in metadata and neither a label nor a score.
## Keep metadata portable
Judgment metadata may contain only portable primitives, arrays, and string-keyed objects.
Convert SDK objects to stable fields before attaching them.
Keep live exceptions and framework-native objects in logs or `JudgmentContext`, not in the result.
## Test all outcome paths
A custom judge test should cover at least its passing, failing, abstaining, and error paths when those paths exist.
Assert the status first, then assert optional score, label, checks, reasoning, and portable metadata that the policy intentionally produces.
## Related
* [Executable tutorial](/docs/agent-judge/tutorial)
* [Result API](/docs/agent-judge/api-reference#judgment)
* [Jury system](/docs/agent-judge/jury-system)
# Design Philosophy
Source: https://lab.pollack.ai/docs/agent-judge/design-philosophy
Status-first results, portable evidence, small contracts, and cost-aware composition
## Judges are executable acceptance criteria
A judge plays the role that an assertion plays in ordinary software.
It receives the evidence from an agent run and produces an explicit outcome with reasoning.
Agent Judge keeps that contract small: `Judge` is a functional interface with one `judge(JudgmentContext)` method.
Lambdas work directly, metadata can be added through composition, and richer implementations can extend `DeterministicJudge` or `LLMJudge`.
## Status is the required outcome
A `Judgment` records three independent facts.
| Fact | Presence | Meaning |
| -------- | -------- | -------------------------------------------------- |
| `status` | Required | `PASS`, `FAIL`, `ABSTAIN`, or `ERROR` |
| `score` | Optional | A completed measurement normalized to `[0.0, 1.0]` |
| `label` | Optional | The exact category assigned by a classifier |
A Boolean decision stores its status without also storing `1.0` or `0.0`.
This avoids two fields representing the same fact and later disagreeing.
`effectiveScore()` derives a numeric view when a voting strategy needs one.
The constructor enforces the cross-field rules.
`ABSTAIN` and `ERROR` cannot carry a score, `ERROR` cannot carry a label, and both require non-blank reasoning.
## Errors are outcomes, not exception transport
`ERROR` means a judge did not complete its evaluation.
The judgment carries human-readable reasoning but no `Throwable`.
The original exception should be logged where it was caught.
This keeps the result deterministic and portable while preserving framework-native diagnostics in the in-process `JudgmentContext` when a judge needs them.
## Result metadata crosses boundaries
Result metadata accepts only values that preserve their meaning across JSON and process boundaries.
Supported values are strings, booleans, interoperable integers, finite numbers, arrays, and string-keyed objects, recursively.
The result constructor normalizes, copies, and freezes this graph.
SDK response objects, exceptions, enums, arbitrary-precision values, and Java time objects are rejected rather than failing later during serialization.
Timing demonstrates the pattern.
The portable result stores `elapsedMillis`, and `Judgment.elapsed()` derives a Java `Duration` view.
## Framework-neutral does not mean dependency-free
The core API has no dependency on an agent runtime such as Spring AI, LangChain4j, Koog, or AgentClient.
It does use Jackson, SLF4J, and JSpecify to implement serialization, logging, and declared nullness.
Framework-specific bridges sit in separate modules.
Applications add only the bridge and judge families that they need.
## Composition separates policy from evidence
Judges produce individual evidence.
Voting strategies decide how that evidence becomes a jury verdict.
Status-counting policies such as majority and consensus reason directly about outcomes.
Numeric policies such as weighted average and median use each eligible judgment's `effectiveScore()`.
Abstentions and errors are not silently converted into zero measurements; the selected strategy and error policy decide how they participate.
## Cascades make cost explicit
A `CascadedJury` evaluates tiers in order.
Cheap deterministic or structural checks can reject invalid work before an LLM-backed tier incurs latency and token use.
| Tier | Typical work | Relative cost |
| ------------- | -------------------------------- | ------------------------ |
| Deterministic | File existence and content rules | Microseconds |
| Structural | Java, Maven, and XML comparison | Milliseconds |
| Execution | Builds, tests, and coverage | Seconds to minutes |
| Semantic | LLM and RAG evaluation | Model latency and tokens |
The final tier is explicit, and each earlier tier declares whether it can reject or accept without escalation.
## Examples remain executable
The separate [Agent Judge Tutorial](https://github.com/markpollack/agent-judge-tutorial) owns canonical examples.
Its ten Maven modules compile and run without credentials, so documentation can link to maintained source instead of carrying divergent sample programs.
## Related
* [Result and package reference](/docs/agent-judge/api-reference)
* [Jury system](/docs/agent-judge/jury-system)
* [Writing a custom judge](/docs/agent-judge/custom-judge)
* [0.14 migration guide](https://github.com/markpollack/agent-judge/blob/main/consumer-handoff-normalized-judgment.md)
# Getting Started with Agent Judge
Source: https://lab.pollack.ai/docs/agent-judge/getting-started
Add status-first evaluation to a Java 21 agent pipeline
Agent Judge evaluates an agent execution after it runs.
A `Judge` receives a `JudgmentContext` and returns a `Judgment` whose required status is independent from its optional normalized score and optional label.
## Prerequisites
* Java 21 or newer
* Maven 3.9 or the Maven Wrapper
* Credentials only when you choose a live LLM-backed judge
The canonical tutorial modules use deterministic inputs and require no credentials.
## Add the dependency
```xml theme={null}
io.github.markpollackagent-judge-core0.15.0
```
All ten artifacts use the same group and version.
| Artifact | Add it for |
| -------------------------- | ------------------------------------------------------------- |
| `agent-judge-core` | Core contracts, status-first results, file checks, and juries |
| `agent-judge-ai-core` | Framework-neutral model-backed judges |
| `agent-judge-exec` | Command, build, and coverage checks |
| `agent-judge-file` | Semantic file comparison |
| `agent-judge-llm` | Spring AI as a judging backend |
| `agent-judge-rag` | RAG quality judges |
| `agent-judge-spring-ai` | Evaluate Spring AI output |
| `agent-judge-langchain4j` | Evaluate LangChain4j output |
| `agent-judge-koog` | Evaluate Koog output |
| `agent-judge-agent-client` | Evaluate or judge through AgentClient |
The framework bridge dependencies are optional at the application boundary.
Your application supplies the framework runtime version it uses.
## Build a context and run one judge
This excerpt is compiled as [tutorial module 01](https://github.com/markpollack/agent-judge-tutorial/blob/main/module-01-single-judge/src/main/java/io/github/markpollack/judge/tutorial/module01/SingleJudgeDemo.java).
```java theme={null}
Path workspace = Path.of("test-workspace");
JudgmentContext context = JudgmentContext.builder()
.goal("Add a HelloController class")
.workspace(workspace)
.status(ExecutionStatus.SUCCESS)
.startedAt(Instant.now())
.executionTime(Duration.ofSeconds(5))
.build();
Judge fileJudge = new FileExistsJudge(
"src/main/java/com/example/HelloController.java");
Judgment result = fileJudge.judge(context);
```
Read `result.status()` first.
`result.score()` and `result.label()` may be `null` because a Boolean decision does not duplicate its outcome as a stored number or category.
Use `result.effectiveScore()` only when an aggregation algorithm deliberately needs a numeric view of `PASS` or `FAIL`.
## Understand result portability
A result can carry strings, booleans, interoperable integers, finite numbers, arrays, and string-keyed objects in metadata.
Agent Judge recursively copies and freezes those values.
Live exceptions, SDK responses, `Duration` objects, enums, and other Java identities do not belong in result metadata.
`JudgmentContext` is an in-process input and may hold framework-native objects needed by a judge.
`Judgment` is the portable output boundary.
## Run the tutorial
Clone `https://github.com/markpollack/agent-judge-tutorial.git`, enter the checkout, run `./mvnw clean test`, and then run `./mvnw exec:java -pl module-01-single-judge`.
The tutorial contains ten modules and progresses through composition, juries, custom judges, model-backed evaluation, Koog, and LangChain4j.
## Next
Run every compiled sample
Learn the normalized result invariants
Choose a judge family
Aggregate multiple judgments
# Jury System
Source: https://lab.pollack.ai/docs/agent-judge/jury-system
Aggregate status-first judgments with explicit voting and error policies
A `Jury` applies multiple judges and returns a `Verdict`.
The verdict retains the aggregated judgment, ordered individual judgments, named evidence, weights, and any nested tier verdicts.
## SimpleJury
`SimpleJury` runs peer judges, optionally in parallel, and delegates aggregation to a `VotingStrategy`.
Its builder accepts named or anonymous judges, per-judge weights, a strategy, a parallel flag, and an optional executor.
The canonical compiled construction and verdict inspection live in [tutorial module 04](https://github.com/markpollack/agent-judge-tutorial/blob/main/module-04-simple-jury/src/main/java/io/github/markpollack/judge/tutorial/module04/SimpleJuryDemo.java).
Read `verdict.aggregated().status()` for the outcome.
Use `individualByName()` and `individual()` for diagnostics, and use the aggregate metadata's `aggregation` object for portable policy evidence.
## Voting strategies
| Strategy | Reduction |
| ------------------------- | ----------------------------------------------------------------- |
| `MajorityVotingStrategy` | Counts applicable `PASS` and `FAIL` statuses |
| `ConsensusStrategy` | Requires every applicable judgment to agree |
| `AverageVotingStrategy` | Averages eligible `effectiveScore()` values |
| `WeightedAverageStrategy` | Computes a weighted average of eligible `effectiveScore()` values |
| `MedianVotingStrategy` | Uses the median eligible `effectiveScore()` |
| `ConjunctiveStrategy` | Reduces eligible `effectiveScore()` values with `min` |
| `AllMustPassStrategy` | Requires every applicable judgment to be `PASS` |
Status-counting strategies do not consult stored scores.
Numeric strategies use an explicit score when present and derive `1.0` or `0.0` from `PASS` or `FAIL` otherwise.
`ABSTAIN` and `ERROR` never become numeric zero by default because they are not completed measurements.
### Compensatory and non-compensatory aggregation
The distinction that decides which of these you want, and the one a default quietly makes for you.
`AverageVotingStrategy` is **compensatory**: a high score on one criterion offsets a low score on
another. That is right when the criteria trade off against each other — a slower solution that is
markedly clearer may genuinely be the better one.
`ConjunctiveStrategy` is **non-compensatory**: it reduces with `min`, so a single low assessment
decides the aggregate and nothing else can lift it. That is right when the criteria are
independently necessary. Correctness is not offset by elegance; a migration that loses data is not
redeemed by good naming.
`AllMustPassStrategy` is the same conjunction taken over **outcomes** rather than scores. Every
applicable judgment must return `PASS`, and a mixed jury is a rejection. Use it as a gate, where the
question is admissibility rather than quality.
Averaging a jury whose criteria are independently necessary is the most common way a rubric reports
a healthy number for work that is unusable. If any single criterion failing should sink the result,
an average will not say so — it will report the mean and look reasonable doing it.
## Tie and error policies
`TiePolicy` selects `PASS`, `FAIL`, or `ABSTAIN` when a strategy has no winner.
The default tie policy for majority voting is `FAIL`.
`ErrorPolicy` makes error handling observable.
| Policy | Effect |
| ------------------ | ---------------------------------------------------------------- |
| `PROPAGATE` | Return an aggregate `ERROR` immediately; this is the default |
| `TREAT_AS_FAIL` | Include the errored judgment as a failure |
| `TREAT_AS_ABSTAIN` | Convert it to a non-vote while recording that conversion |
| `IGNORE` | Remove it from the aggregation population and weight denominator |
The original error judgment remains in the verdict's individual evidence even when a policy excludes it from reduction.
## Consensus semantics
Consensus returns `PASS` when all applicable judges pass and `FAIL` when all applicable judges fail.
A mixed applicable panel returns `ABSTAIN` because it has no collective finding; a downstream gate or tier decides whether disagreement rejects or escalates.
If no applicable judges remain after abstention and error handling, the result is also `ABSTAIN`, with reasoning and aggregation evidence distinguishing the cases.
## CascadedJury
`CascadedJury` orders juries into named tiers.
Each tier has one control-flow policy.
| Tier policy | Behavior |
| -------------------- | ------------------------------------------------- |
| `REJECT_ON_ANY_FAIL` | Stop and reject if any judge in the tier fails |
| `ACCEPT_ON_ALL_PASS` | Stop and accept if every judge in the tier passes |
| `FINAL_TIER` | Always produce the final verdict and must be last |
This separates cost control from a judge's evaluation logic.
A typical cascade runs deterministic checks, then structural checks, then an LLM-backed final tier only when earlier evidence cannot decide.
[Tutorial module 05](https://github.com/markpollack/agent-judge-tutorial/blob/main/module-05-cascaded-jury/src/main/java/io/github/markpollack/judge/tutorial/module05/CascadedJuryDemo.java) is the canonical compiled two-tier example.
## Identity and composition
Wrap a lambda with `Judges.named` so verdicts have stable identities.
The `Juries` utility builds a jury from judges and combines existing juries into a meta-jury.
`Verdict.single(name, judgment)` creates complete evidence for a one-member jury without duplicating builder calls.
## Choosing a policy
| Situation | Suggested policy |
| --------------------------------------- | ---------------- |
| Equal peer decisions | Majority |
| Every requirement must hold | Consensus |
| Measurements have different importance | Weighted average |
| Outlier-resistant numeric reduction | Median |
| Cheap gates before expensive evaluation | Cascaded jury |
## Related
* [Executable tutorial](/docs/agent-judge/tutorial)
* [Built-in judges](/docs/agent-judge/built-in-judges)
* [API reference](/docs/agent-judge/api-reference)
# Research Foundations
Source: https://lab.pollack.ai/docs/agent-judge/research-foundations
Why Agent Judge favors explicit requirements, executable evidence, staged evaluation, and independent checks
Agent Judge evaluates whether work satisfies an explicit definition of done, using executable evidence and independent checks—not whether it resembles one reference answer.
## The motivating failure
The investigation began when exact reference-file comparison rated two independently successful Spring Boot migrations as failures. Both implementations built, but each made valid choices that differed from the reference, including the framework version it targeted.
The comparison answered *“Does this look like the reference?”* when the useful question was *“Does this satisfy the migration goal?”*
Exact comparison is still appropriate when exact output is part of the specification. For open-ended generation, refactoring, and migration, it should be one possible check rather than the complete definition of correctness.
## Five principles
| Principle | What it means | Agent Judge mechanism |
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| Evidence before opinion | Prefer builds, tests, runtime behavior, bytecode, and coverage when those facts can answer the question directly. | Command, build, class-version, and coverage judges |
| Judge requirements, not resemblance | Express the invariants the result must satisfy instead of assuming one reference implementation is uniquely correct. | Composable deterministic, file-semantic, execution, RAG, and model-backed judges |
| Escalate in stages | Run cheap, decisive checks before slower or more subjective evaluation. | `CascadedJury` with explicit tier policies |
| Compose independent perspectives | Combine checks with different failure modes and make their aggregation policy visible. | `SimpleJury`, `MetaJury`, majority, consensus, average, weighted-average, and median strategies |
| Preserve uncertainty | Keep disagreement, lack of applicability, and evaluator failure distinct from a negative finding. | `PASS`, `FAIL`, `ABSTAIN`, and `ERROR` outcomes with structured aggregation evidence |
These are design principles, not claims that Agent Judge implements every technique proposed in the literature. Calibrated guarantees of human agreement, dynamic judge-team selection, multi-round debate, and formal program-equivalence proofs remain research or application-level concerns.
## Ground semantic judgment in tools
Research on tool-augmented LLM evaluation found that web search and code execution can improve judge performance in many, though not all, settings. Code-evaluation systems likewise use execution and other tools to supplement model judgment. The practical implication is bounded: a semantic judge can add value, but it should not replace a build, test, or runtime check when one is available.
Agent Judge therefore treats deterministic and execution-based checks as first-class judges. Model-backed judges are intended for criteria-based questions that the available tools cannot settle directly.
## Allow more than one correct implementation
Program-equivalence research shows that structural difference is not evidence of behavioral difference, while current models still struggle with difficult equivalence cases. Migration benchmarks and production migration systems evaluate combinations of compilation, tests, dependency state, structural invariants, and runtime behavior rather than relying on textual similarity alone.
This leads to a requirements-based evaluation model: specify what must remain true, what must change, and the evidence that demonstrates both.
## Use cascades and juries deliberately
Selective-evaluation research motivates starting with lower-cost evaluators and escalating uncertain cases. Ensemble-judge research highlights the value—and limitations—of combining perspectives. Agent Judge exposes those choices as application policy: which judges participate, how their findings aggregate, how errors are handled, and when another tier runs.
A jury is therefore a composition of visible judgments, not merely several models producing one opaque score.
## Protect the definition of done
Migration research also documents why weak success criteria are dangerous. An agent can appear to improve a metric by removing tests, reducing exercised behavior, or satisfying only a superficial similarity check. Coverage preservation, compilation, test execution, and project-specific structural invariants make such shortcuts observable.
Agent Judge supplies mechanisms for these safeguards. The application still owns its definition of done; no generic judge suite can infer all of a project's requirements automatically.
## Source map
### Evaluation architectures
* [Findeis et al., *Can External Validation Tools Improve Annotation Quality for LLM-as-a-Judge?*](https://aclanthology.org/2025.acl-long.779/)
* [Jung et al., *Trust or Escalate: LLM Judges with Provable Guarantees for Human Agreement*](https://arxiv.org/abs/2407.18370)
* [Li et al., *LLMs-as-Judges: A Comprehensive Survey on LLM-based Evaluation Methods*](https://arxiv.org/abs/2412.05579)
* [Wang et al., *CodeVisionary: An Agent-based Framework for Evaluating Large Language Models in Code Generation*](https://arxiv.org/abs/2504.13472)
* [Zhou et al., *An LLM-as-Judge Metric for Bridging the Gap with Human Evaluation in SE Tasks*](https://arxiv.org/abs/2505.20854)
* [Yu, *When AIs Judge AIs: The Rise of Agent-as-a-Judge Evaluation for LLMs*](https://arxiv.org/abs/2508.02994)
* [He et al., *LLM-as-a-Judge for Software Engineering: Literature Review, Vision, and the Road Ahead*](https://arxiv.org/abs/2510.24367)
### Equivalence and verification
* [Wei et al., *EquiBench: Benchmarking Code Reasoning Capabilities of Large Language Models via Equivalence Checking*](https://arxiv.org/abs/2502.12466)
* [Dilhara et al., *Unprecedented Code Change Automation: The Fusion of LLMs and Transformation by Example*](https://doi.org/10.1145/3643755)
### Migration evaluation
* [Ziftci et al., *Migrating Code At Scale With LLMs At Google*](https://arxiv.org/abs/2504.09691)
* [Liu et al., *MIGRATION-BENCH: Repository-Level Code Migration Benchmark from Java 8*](https://arxiv.org/abs/2505.09569)
* [May et al., *FreshBrew: A Benchmark for Evaluating AI Agents on Java Code Migration*](https://arxiv.org/abs/2510.04852)
* [Amin et al., *JMigBench: A Benchmark for Evaluating LLMs on Source Code Migration*](https://arxiv.org/abs/2602.09930)
* [Cheng et al., *CODEMENV: Benchmarking Large Language Models on Code Migration*](https://arxiv.org/abs/2506.00894)
Results and numerical claims in these sources belong to their respective studies. They are research inputs to Agent Judge's design, not Agent Judge benchmark results.
## Related
* [Design philosophy](/docs/agent-judge/design-philosophy)
* [Built-in judges](/docs/agent-judge/built-in-judges)
* [Jury system](/docs/agent-judge/jury-system)
* [Executable tutorial](/docs/agent-judge/tutorial)
# Executable Tutorial
Source: https://lab.pollack.ai/docs/agent-judge/tutorial
Ten credential-free Maven modules for Agent Judge 0.15
The [Agent Judge Tutorial repository](https://github.com/markpollack/agent-judge-tutorial) is the canonical sample suite.
Each module is a small executable Maven project compiled against Agent Judge 0.15.
Clone the repository, run `./mvnw clean test`, and then run a module with `./mvnw exec:java -pl module-01-single-judge`.
No module needs an API key or a live model endpoint.
## Learning path
| Module | Focus | Canonical source |
| ------ | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 01 | Build a `JudgmentContext` and apply one `FileExistsJudge` | [SingleJudgeDemo.java](https://github.com/markpollack/agent-judge-tutorial/blob/main/module-01-single-judge/src/main/java/io/github/markpollack/judge/tutorial/module01/SingleJudgeDemo.java) |
| 02 | Run a real Maven build through `BuildSuccessJudge` | [BuildJudgeDemo.java](https://github.com/markpollack/agent-judge-tutorial/blob/main/module-02-build-judge/src/main/java/io/github/markpollack/judge/tutorial/module02/BuildJudgeDemo.java) |
| 03 | Compose judges with `Judges.and`, `or`, and `negate` | [CompositionDemo.java](https://github.com/markpollack/agent-judge-tutorial/blob/main/module-03-composition/src/main/java/io/github/markpollack/judge/tutorial/module03/CompositionDemo.java) |
| 04 | Aggregate named judges with majority and weighted-average policies | [SimpleJuryDemo.java](https://github.com/markpollack/agent-judge-tutorial/blob/main/module-04-simple-jury/src/main/java/io/github/markpollack/judge/tutorial/module04/SimpleJuryDemo.java) |
| 05 | Arrange cost-aware tiers with `CascadedJury` | [CascadedJuryDemo.java](https://github.com/markpollack/agent-judge-tutorial/blob/main/module-05-cascaded-jury/src/main/java/io/github/markpollack/judge/tutorial/module05/CascadedJuryDemo.java) |
| 06 | Write lambda judges and attach discoverable metadata | [LambdaJudgeDemo.java](https://github.com/markpollack/agent-judge-tutorial/blob/main/module-06-lambda-judge/src/main/java/io/github/markpollack/judge/tutorial/module06/LambdaJudgeDemo.java) |
| 07 | Implement a reusable `DeterministicJudge` with granular checks | [PackageStructureJudge.java](https://github.com/markpollack/agent-judge-tutorial/blob/main/module-07-deterministic-judge/src/main/java/io/github/markpollack/judge/tutorial/module07/PackageStructureJudge.java) |
| 08 | Compose a prompt, model adapter, and classifier with `ModelBackedJudge` | [ModelBackedJudgeDemo.java](https://github.com/markpollack/agent-judge-tutorial/blob/main/module-08-model-backed-judge/src/main/java/io/github/markpollack/judge/tutorial/module08/ModelBackedJudgeDemo.java) |
| 09 | Evaluate deterministic Koog agent output through `KoogEvaluator` | [KoogEvaluationDemo.java](https://github.com/markpollack/agent-judge-tutorial/blob/main/module-09-koog-evaluation/src/main/java/io/github/markpollack/judge/tutorial/module09/KoogEvaluationDemo.java) |
| 10 | Evaluate a deterministic LangChain4j `Result` through `LangChain4jEvaluator` | [LangChain4jEvaluationDemo.java](https://github.com/markpollack/agent-judge-tutorial/blob/main/module-10-langchain4j-evaluation/src/main/java/io/github/markpollack/judge/tutorial/module10/LangChain4jEvaluationDemo.java) |
## What to observe
Modules 01 through 07 establish the framework-neutral contract.
Read the required status first and treat a stored score or label as optional evidence.
Module 08 demonstrates that AI-backed evaluation is composition rather than inheritance.
Its stub model makes the complete prompt-to-classifier path executable without credentials.
Modules 09 and 10 replace the former README-only bridge samples.
They execute real adapter code with deterministic framework objects, so bridge construction and metadata extraction remain verifiable offline.
## Framework versions used by the samples
| Integration | Tutorial target |
| ----------- | --------------- |
| Agent Judge | `0.15.0` |
| Koog | `1.1.1` |
| LangChain4j | `1.19.0` |
Spring AI and AgentClient integration APIs are covered by the library's bridge modules and tests.
The tutorial's framework examples deliberately avoid online calls and credentials.
## Continue
Choose an aggregation policy
Turn domain rules into judgments
# What's New
Source: https://lab.pollack.ai/docs/agent-judge/whats-new
Curated Agent Judge release notes
## 0.15.0
Agent Judge 0.15 is a compatibility-preserving dependency-alignment and release-reproducibility
update. It keeps the 0.14 status-first `Judgment` API, the Java 21 baseline, and all ten published
modules unchanged.
### Consumer dependency alignment
* Standalone consumers continue to resolve Jackson 2.21.6, with Jackson Annotations on its 2.21
release line.
* `agent-judge-llm` and `agent-judge-rag` now declare Jackson 3.1.6 in their flattened POMs so
consumers without the AgentWorks BOM receive the accepted Jackson 3 floor.
* The provided Agent Client integration baseline advances to 0.26.0. Applications still choose and
provide their Agent Client runtime.
### Agent Sandbox Core
`agent-judge-exec` now uses `agent-sandbox-core` 0.10.0. That Core release protects environment
values in `ExecSpec.toString()` and avoids forwarding the whole parent process environment when
caller overrides are present.
Agent Judge's own execution paths construct specs with a command and timeout only; they do not set
environment variables. This release does not add the Sandbox Docker or E2B modules.
### Reproducible release evidence
* Build and snapshot workflows are pinned to the reviewed Build Tools commit, and the Build workflow
supports exact-SHA manual verification.
* Every module publishes binary, source, and generated Javadoc JARs with the project-specific
Business Source License terms.
* Maven Central attaches one aggregate CycloneDX 1.6 SBOM to the parent coordinate. It is supporting
inventory for the reactor and included dependency graph; provided-scope inventory is not a claim
that every listed component enters an application's runtime closure.
See the complete [0.15 release notes](https://github.com/markpollack/agent-judge/blob/v0.15.0/RELEASE_NOTES_0.15.md)
and the [GitHub release](https://github.com/markpollack/agent-judge/releases/tag/v0.15.0).
## 0.14.0
Agent Judge 0.14 replaces the former sealed score hierarchy with a normalized, portable `Judgment` contract.
This is a breaking pre-1.0 release, so consumers must recompile and follow the [migration guide](https://github.com/markpollack/agent-judge/blob/main/consumer-handoff-normalized-judgment.md).
### Result model
* Every judgment has a required `PASS`, `FAIL`, `ABSTAIN`, or `ERROR` status.
* A normalized score and a classification label are independent optional facts.
* Boolean outcomes do not duplicate status as a stored `1.0` or `0.0`; `effectiveScore()` supplies an intentional numeric view for aggregation.
* Errors carry reasoned outcomes rather than live exceptions.
### Portable evidence
* Result metadata is validated as portable primitives, arrays, and string-keyed objects, then recursively copied and frozen.
* Invalid values identify their exact metadata path.
* Timing is stored as integer `elapsedMillis`, with `Judgment.elapsed()` providing the Java `Duration` view.
* Model usage records separately reported input, output, reasoning, cache-creation, cache-read, and total token quantities without deriving price.
### Aggregation
* Every voting strategy exposes an `ErrorPolicy`, defaulting to `PROPAGATE`.
* Aggregates contain structured population and error-accounting evidence.
* Consensus returns `ABSTAIN` when applicable judges disagree, leaving rejection or escalation to a downstream gate or tier.
### API and dependencies
* The `io.github.markpollack.judge.score` package and `ReactiveJudge` are removed.
* The core remains agent-runtime-neutral and declares its real Jackson Databind, SLF4J API, and JSpecify dependencies.
* JSpecify exposes optional score and label nullness in the Java API, with NullAway enforcing the adopted packages.
### Framework baselines
The release candidate is verified against the final stable framework matrix:
| Integration | Version |
| ------------ | ------------------------------------------------------------- |
| Spring AI | `2.0.0` with Spring Boot `4.1.0` and Spring Framework `7.0.8` |
| LangChain4j | `1.19.0` |
| Koog | `1.1.1` |
| Agent Client | `0.25.0` |
Framework bridge dependencies remain `provided` where practical so applications select their runtime.
### Samples and reference
* The [Agent Judge Tutorial](https://github.com/markpollack/agent-judge-tutorial) now owns ten canonical credential-free Maven modules.
* Executable Koog and LangChain4j modules replace the former README-only bridge samples.
* Aggregate Javadoc is warning-clean, warnings are release-breaking, and every published module attaches a Javadoc JAR.
* See the complete [0.14 release notes](https://github.com/markpollack/agent-judge/blob/main/RELEASE_NOTES_0.14.md).
### Release packaging
* The current source tree uses the project-specific Business Source License terms in the repository's [root `LICENSE`](https://github.com/markpollack/agent-judge/blob/main/LICENSE). Earlier published releases keep the licenses shipped with those releases.
* Maven Central publication attaches one CycloneDX 1.6 aggregate SBOM to the parent coordinate, covering the ten reactor modules and their included external dependencies.
* The exact-pinned release workflow runs the full test-bearing verification before deploy and uses the curated `RELEASE_NOTES_0.14.md` body.
* The GitHub Release is notes and source archives only; the signed Maven artifacts and aggregate SBOM belong on Maven Central, with no separately uploaded GitHub release assets.
## 0.13.0
* Upgraded to Spring AI 2.0.0 GA and its supported Spring Boot 4.1.0 baseline.
* Added an OWASP dependency-check gate.
## 0.12.0
* Aligned Jackson dependencies for the release line.
## 0.11.0
* Added `agent-judge-ai-core` and framework-neutral model-backed judge composition.
## 0.10.0
* Added Spring AI, LangChain4j, Koog, and AgentClient bridges.
* Added RAG evaluation judges and the nine-module architecture that preceded `agent-judge-ai-core`.
## 0.9.x
* Moved public coordinates to `io.github.markpollack`.
* Added superset project comparison and the initial public release.
# What's New
Source: https://lab.pollack.ai/docs/agent-memory/whats-new
Curated release notes for Agent Memory
## 0.4.0 (2026-08-18)
**First Business Source License 1.1 release, with supply-chain and disclosure work. No behavioural change to Tier-1 compaction.**
### License
* Current development and 0.4.0 and later are licensed under **Business Source License 1.1**. **0.3.0 and earlier remain Apache License 2.0**, and published tags and artifacts are not retroactively relicensed. The historical Apache text is retained in the repository as `LICENSE-APACHE.txt`.
* Both license files are staged into every published binary, source, and Javadoc archive.
### Dependency resolution
* Jackson 2 raised to 2.21.6 and Jackson 3 to 3.1.6.
* Jackson 3 core and databind are now declared **directly** on `memory-core`, so a standalone consumer with no AgentWorks BOM and no Jackson management of its own resolves 3.1.6 by Maven nearest-wins. Parent BOM imports align the build reactor but do not travel with a published module to a downstream consumer — that gap is what this change closes.
* A repository-owned standalone-consumer resolution gate is committed and runs in CI after the clean build.
* The unused Spring milestone and snapshot resolution repositories are removed; Spring AI 2.0.0 GA resolves from Maven Central.
### Supply chain
* The parent artifact now publishes one aggregate **CycloneDX 1.6 JSON SBOM** (`classifier=cyclonedx`) covering both modules and their shipped dependency closure.
* **Hosted OWASP/NVD dependency scanning in GitHub Actions has been removed.** Vulnerability analysis is now a local offline Trivy procedure run against a validated database snapshot. There is no hosted CVE gate in this project's CI.
* Reusable build workflows are pinned to a build-tools commit SHA, and the external actions this repository references directly are pinned to full-length commit SHAs. The pinned reusable workflows still resolve some actions internally by moving reference, so the complete executed CI path is not yet immutable.
### Documented operating boundary
The filesystem store is **local, plaintext, and single-writer**. There is no locking, no atomic index replacement, and no crash recovery: a second concurrent writer, or a crash during an index write, can corrupt or truncate `_index.json`. Stored memory is injected verbatim into the model prompt, so only trusted content should be written to it. Multi-process or concurrent-writer use requires external coordination. These are disclosed limits, not shipped features.
### Unchanged
`FileSystemMemoryStore`, `ProgressFileMemoryStore`, `MemoryCompactor` and `CompactionMemoryAdvisor` behaviour, the public API, and the on-disk format are unchanged. This release is not a Jackson 3 source migration.
## 0.3.0 (2026-06-15)
* Upgraded to Spring AI 2.0.0 GA.
* Added a hosted OWASP dependency-check CVE gate. **This gate was removed again in 0.4.0** in favour of a local offline procedure.
## 0.2.0 (2026-06-06)
* Aligned on the Jackson 2.21.2 BOM. No functional change to compaction.
## 0.1.0 (2026-04-02)
* Initial release. Tier-1 compaction: `MemoryStore`, `FileSystemMemoryStore`, `MemoryCompactor`, `TokenEstimator`, and the `CompactionMemoryAdvisor` Spring AI `BaseAdvisor`.
# What's New
Source: https://lab.pollack.ai/docs/agent-sandbox/whats-new
Release notes for Agent Sandbox
## 0.10.0 (2026-08-18)
* Restore the convenient local Docker backend on current Docker Engine versions with
Testcontainers 1.21.4, and add its core-and-Docker TCK run to ordinary CI.
* Require callers to select the Docker image. The no-argument `DockerSandbox()`
constructor is removed, `builder().build()` fails before Docker access unless
`.image(...)` is set, and explicit-image constructors continue to work. Agent Sandbox
does not ship or maintain a runtime image; the caller owns the selected image's
security and update policy.
* Preserve argument-vector semantics across backends and harden Docker environment and
file-list execution against shell injection.
* Stop copying the complete host environment into `LocalSandbox`, update Jackson and
commons-compress, package license texts in distributed archives, and attach a
graph-derived CycloneDX 1.6 SBOM to the parent artifact.
Three disclosed upstream findings remain in Apache HttpComponents classes embedded and
relocated inside the Java Docker transport. The HTTP/2/HPACK finding is not reachable on
the Docker HTTP/1.1 path examined. The other two require a malicious response from the
trusted, root-equivalent local Docker daemon; no path from untrusted code inside the
selected container to those advisory conditions was identified under this trust model.
Published docker-java/Testcontainers combinations examined do not contain fixed embedded
versions, and ordinary dependency management cannot override classes already relocated
inside the zerodep JAR. The findings remain visible and are accepted for the
trusted-local-daemon use case. Changing the selected container image cannot remove them.
The CycloneDX SBOM is derived from the Maven dependency graph and does not enumerate
components shaded into dependency JARs. For this release, its scan must be read alongside
the actual-JAR scan that detects the three transport findings; a zero-finding SBOM scan
is a coverage limitation, not a clean closure.
## 0.9.3 (2026-06-06)
* Align Jackson to 2.21.2 via jackson-bom import; release 0.9.3
## 0.9.2 (2026-05-15)
* Migrate to markpollack org: package rename, BSL license, standalone POM, build-tools workflows
* Add README with Maven Central coordinates and docs link
* Fix release workflow: add contents write permission for git tagging
## 0.9.1 (2026-03-05)
* Initial release.
# AgentLoop
Source: https://lab.pollack.ai/docs/agent-workflow/agent-loop
Ready-to-use SWE agent — tools, session memory, observability, and a simple run/chat API backed by Spring AI's agent loop
## What is AgentLoop?
`AgentLoop` is a ready-to-use agent that packages everything you need to run an autonomous coding task: tools, system prompt, turn limits, cost limits, session memory, and observability. Under the hood it delegates to Spring AI's `ChatClient` + `AgentLoopAdvisor` for the actual tool-calling loop.
Think of it as "batteries included" — one class, one builder call, working agent:
```java theme={null}
AgentLoop agent = AgentLoop.builder()
.config(AgentLoop.Config.builder()
.maxTurns(10)
.workingDirectory(Path.of("/my/project"))
.build())
.model(chatModel)
.build();
AgentLoop.Result result = agent.run("Add unit tests for the UserService class");
System.out.println(result.status()); // COMPLETED
System.out.println(result.output()); // "I've added 5 tests..."
System.out.println(result.totalTokens()); // 12400
```
## Built-in Tools
AgentLoop ships with a full set of SWE tools:
| Tool | Source | Description |
| -------------------------------- | --------------------- | ---------------------------------------- |
| `Bash` | workflow-tools | Shell commands (git, mvn, docker) |
| `Read` / `Write` / `Edit` / `LS` | spring-ai-agent-utils | File operations |
| `Glob` | spring-ai-agent-utils | Find files by pattern |
| `Grep` | spring-ai-agent-utils | Search file contents |
| `TodoWrite` | spring-ai-agent-utils | Task planning and tracking |
| `Task` | spring-ai-agent-utils | Delegate to sub-agents |
| `Submit` | AgentLoop (internal) | Submit final answer, terminates the loop |
## Configuration
`AgentLoop.Config` is a Java record with a builder:
```java theme={null}
AgentLoop.Config config = AgentLoop.Config.builder()
.maxTurns(20) // default: 20
.costLimit(1.0) // default: $1.00
.commandTimeout(Duration.ofSeconds(30)) // default: 30s
.workingDirectory(Path.of(".")) // default: user.dir
.systemPrompt("Custom prompt...") // default: built-in SWE prompt
.build();
```
| Parameter | Default | Description |
| ------------------ | ------------------- | -------------------------------------- |
| `maxTurns` | 20 | Max LLM invocations before termination |
| `costLimit` | 1.0 | Max estimated cost in USD |
| `commandTimeout` | 30s | Timeout for shell commands |
| `workingDirectory` | `user.dir` | Root directory for file/shell tools |
| `systemPrompt` | Built-in SWE prompt | System instructions for the agent |
Configs are immutable records. Use `toBuilder()` to derive variants:
```java theme={null}
AgentLoop.Config extended = config.toBuilder()
.maxTurns(50)
.build();
```
## Session Memory
Enable session memory to preserve conversation context across multiple calls:
```java theme={null}
AgentLoop agent = AgentLoop.builder()
.config(config)
.model(chatModel)
.sessionMemory() // default in-memory implementation
.build();
agent.run("Read the UserService class and summarize it");
agent.run("Now add tests for the methods you found"); // remembers context
agent.clearSession(); // reset when done
```
You can also pass a custom `ChatMemory` implementation:
```java theme={null}
.sessionMemory(myJdbcChatMemory)
```
## Interactive Mode
For TUI/CLI applications, interactive mode enables human-in-the-loop via `AskUserQuestionTool`:
```java theme={null}
AgentCallback callback = new AgentCallback() {
@Override
public void onThinking() { System.out.print("Thinking..."); }
@Override
public String onQuestion(List questions) {
// Present questions to user, return their answer
return scanner.nextLine();
}
@Override
public void onComplete() { System.out.println("Done."); }
};
AgentLoop agent = AgentLoop.builder()
.config(config)
.model(chatModel)
.interactive(true)
.agentCallback(callback)
.sessionMemory()
.build();
agent.chat("Review this PR and ask me about anything unclear", callback);
```
## Result
`AgentLoop.Result` is a record with execution details:
```java theme={null}
AgentLoop.Result result = agent.run(task);
result.status(); // COMPLETED, TURN_LIMIT_REACHED, TIMEOUT, STUCK, etc.
result.output(); // Agent's final text output
result.turnsCompleted(); // Number of LLM invocations
result.toolCallsExecuted();// Total tool calls across all turns
result.totalTokens(); // Total tokens consumed
result.estimatedCost(); // Estimated cost in USD
result.isSuccess(); // true if COMPLETED
result.isFailure(); // true if FAILED
```
## Observability
AgentLoop wires Micrometer observations for tool call tracking. You can also provide a custom `ToolCallListener`:
```java theme={null}
AgentLoop agent = AgentLoop.builder()
.config(config)
.model(chatModel)
.toolCallListener(new LoggingToolCallListener())
.build();
```
## Relationship to LoopPattern
`AgentLoop` is **not** a `LoopPattern` implementation. It's a higher-level construct:
* **`LoopPattern`** — the abstract interface for loop algorithms (`TurnLimitedLoop`, `EvaluatorOptimizerLoop`, `StateMachineLoop`). Lives in `workflow-api`.
* **`AgentLoop`** — a concrete, runnable agent that uses `AgentLoopAdvisor` internally. Lives in `workflow-agents`.
`AgentLoop` is what you use when you want to *run* an agent. `LoopPattern` is what you implement when you want to *create a new loop algorithm*.
## Maven Coordinates
```xml theme={null}
io.github.markpollackworkflow-agents0.10.0
```
# Annotation Model
Source: https://lab.pollack.ai/docs/agent-workflow/annotation-model
Declare agents with @Agent, compose workflows inside AgentHandler, and wire exception handling with @ExceptionHandler and @AgentAdvice
## Why an Annotation Model?
Steps and workflows are the plumbing. Agents are the product — the thing you expose to HTTP endpoints, MCP servers, message listeners, and other agents. The annotation model gives you a programming model for declaring agents, naming them, describing them, handling their errors, and looking them up by name at runtime.
If you've used `@Controller` + `@ControllerAdvice` in Spring MVC, you already know how this works. `@Agent` is to `AgentHandler` what `@Controller` is to your handler methods.
## AgentHandler\
The entry-point contract for agents. A `@FunctionalInterface` that takes context and input, returns output:
```java theme={null}
@FunctionalInterface
public interface AgentHandler {
O handle(AgentContext ctx, I input);
}
```
**Lambda form** — for simple cases:
```java theme={null}
AgentHandler echo = (ctx, input) -> input.toUpperCase();
String result = echo.handle(AgentContext.create(), "hello"); // "HELLO"
```
**Class-based form** — for production agents that compose a `Workflow` internally:
```java theme={null}
public class CodeReviewAgent implements AgentHandler {
private final Step fetchDiff = Step.named("fetch-diff",
(ctx, prUrl) -> gitHub.fetchDiff(prUrl));
private final Step analyze = Step.named("analyze",
(ctx, diff) -> llm.analyze(diff));
@Override
public ReviewReport handle(AgentContext ctx, String prUrl) {
return Workflow.define("code-review")
.step(fetchDiff)
.then(analyze)
.run(prUrl, ctx);
}
}
```
`AgentHandler` is pure Java — no Spring dependency. It lives in `workflow-api` so it's portable across any runtime.
## @Agent
Marks a class as a named agent and registers it as a Spring bean:
```java theme={null}
@Agent("code-review")
public class CodeReviewAgent implements AgentHandler {
// ...
}
```
`@Agent` is a Spring `@Component` stereotype — classes annotated with it are picked up by component scanning. The `value()` is the unique agent name used for registry lookup and protocol addressing.
## @StepName and @Description
Two metadata annotations for steps and agents:
```java theme={null}
@StepName("analyze-diff")
@Description("Parses a PR diff and extracts changed files and hunks")
public class AnalyzeDiffStep implements Step {
// ...
}
```
**@StepName** pins a stable name for checkpoint and Temporal keys. When you rename the class, the checkpoint key stays the same — crash recovery and Temporal replay still match the right execution record. Without it, `Step.name()` (which defaults to the class simple name) is used.
**@Description** provides a human-readable description for traces, visualization, and protocol tool listings. Can be placed on types or methods.
Both are pure Java annotations — no Spring dependency, no runtime cost.
### StepNames utility
`StepNames.resolve(step)` reads the `@StepName` annotation; falls back to `step.name()` if absent:
```java theme={null}
StepNames.resolve(new AnalyzeDiffStep()); // "analyze-diff" (from annotation)
StepNames.resolve(Step.named("foo", ...)); // "foo" (from name() method)
```
## AgentRegistry
An immutable map of name → handler. Manual construction — you decide what goes in:
```java theme={null}
AgentRegistry registry = new AgentRegistry(Map.of(
"code-review", codeReviewAgent,
"security-audit", securityAuditAgent
));
AgentHandler, ?> agent = registry.get("code-review").orElseThrow();
Set names = registry.names(); // ["code-review", "security-audit"]
```
The registry is defensive-copied at construction (`Map.copyOf()`). Subsequent changes to the source map don't affect it.
Auto-configuration that scans `@Agent` beans and populates `AgentRegistry` automatically is planned but not yet implemented. For now, construct the registry manually or in a `@Configuration` class.
## Exception Handling
Exception handling mirrors Spring MVC's `@ExceptionHandler` + `@ControllerAdvice` pattern exactly. Two scopes:
1. **Per-agent** — `@ExceptionHandler` methods inside an `@Agent` class handle that agent's exceptions
2. **Cross-cutting** — `@ExceptionHandler` methods inside an `@AgentAdvice` class handle exceptions from any agent
Per-agent handlers take priority. When multiple handlers match, the most specific exception type wins (subclass beats superclass).
### Per-agent handling
```java theme={null}
@Agent("code-review")
public class CodeReviewAgent implements AgentHandler {
@Override
public ReviewReport handle(AgentContext ctx, String prUrl) {
return Workflow.define("code-review")
.step(fetchDiff)
.then(analyze)
.run(prUrl, ctx);
}
@ExceptionHandler(RateLimitException.class)
public ReviewReport handleRateLimit(RateLimitException ex) {
return new ReviewReport("Rate limited — try again later", "SKIP");
}
@ExceptionHandler(IllegalStateException.class)
public ReviewReport handleBudget(IllegalStateException ex, AgentContext ctx) {
log.warn("Agent {} exceeded budget", ctx.runId());
return new ReviewReport("Budget exceeded: " + ex.getMessage(), "SKIP");
}
}
```
Handler methods support two signatures:
* `ReturnType method(ExceptionType ex)` — just the exception
* `ReturnType method(ExceptionType ex, AgentContext ctx)` — exception plus context
The exception type can be declared explicitly in `@ExceptionHandler(SomeException.class)` or inferred from the method's first parameter.
### Cross-cutting handling
```java theme={null}
@AgentAdvice
public class GlobalErrorHandler {
@ExceptionHandler(Exception.class)
public Object handleAny(Exception ex, AgentContext ctx) {
log.error("Agent {} failed: {}", ctx.runId(), ex.getMessage());
return Map.of("error", ex.getMessage(), "runId", ctx.runId());
}
}
```
`@AgentAdvice` is a Spring `@Component` — picked up by component scanning, applies to all agents.
### Using the resolver
`AgentExceptionHandlerResolver` ties it together. Pass it the `@AgentAdvice` instances at construction, then call `resolve()` on exception:
```java theme={null}
var resolver = new AgentExceptionHandlerResolver(List.of(globalErrorHandler));
try {
return agent.handle(ctx, input);
} catch (Exception ex) {
return resolver.resolve(agent, ex, ctx)
.orElseThrow(() -> ex); // re-throw if no handler matched
}
```
Resolution order: per-agent handlers on the agent instance first, then cross-cutting advice in registration order.
## Spring Boot Wiring
In a Spring Boot application, wire the registry and resolver as beans in a `@Configuration` class:
```java theme={null}
@Configuration(proxyBeanMethods = false)
public class AgentConfig {
@Bean
AgentExceptionHandlerResolver agentExceptionHandlerResolver(
MyErrorAdvice errorAdvice) { // @AgentAdvice bean
return new AgentExceptionHandlerResolver(List.of(errorAdvice));
}
@Bean
AgentRegistry agentRegistry(PrReviewAgent prReviewAgent) {
return new AgentRegistry(Map.of("pr-review", prReviewAgent));
}
}
```
Spring injects the `@Agent` and `@AgentAdvice` beans by type. The registry and resolver become injectable anywhere you need them — HTTP controllers, message listeners, MCP tool handlers.
### Disambiguating steps with @Qualifier
When two steps share the same `Step` signature, Spring can't tell them apart by type alone. Use `@Qualifier` on both the bean definition and the injection point:
```java theme={null}
@Component
@Qualifier("assess-code-quality")
@StepName("assess-code-quality")
public class AssessCodeQualityStep implements Step {
// ...
}
@Component
@Qualifier("assess-backport")
@StepName("assess-backport")
public class AssessBackportStep implements Step {
// ...
}
```
Then inject by qualifier in the agent constructor:
```java theme={null}
@Agent("pr-review")
public class PrReviewAgent implements AgentHandler {
public PrReviewAgent(
@Qualifier("assess-code-quality") Step quality,
@Qualifier("assess-backport") Step backport) {
// ...
}
}
```
This is the standard Spring pattern — `@StepName` gives you a stable checkpoint key, `@Qualifier` gives you injection disambiguation.
## Injecting Durability with withExecutor()
By default, `Workflow.run()` creates a `WorkflowExecutor` with `LocalStepRunner` (in-process, no persistence). Use `withExecutor()` to inject a configured executor with a durable `StepRunner`:
```java theme={null}
// Configure executor with JDBC checkpointing
WorkflowExecutor executor = new WorkflowExecutor(
checkpointingStepRunner,
jdbcTraceRecorder
);
// Inject into workflow — both run() and build() paths work
String result = Workflow.define("durable-review")
.withExecutor(executor)
.step(fetchDiff)
.then(analyze)
.run(prUrl, ctx);
// Or build a reusable Workflow that remembers the executor
Workflow workflow = Workflow.define("review")
.withExecutor(executor)
.step(fetchDiff)
.then(analyze)
.build();
workflow.execute(ctx, prUrl); // uses the injected executor
```
This works on both `WorkflowBuilder` and `SupervisorBuilder`. See [Durability](/docs/agent-workflow/durability) for the full graduation path from `LocalStepRunner` → `CheckpointingStepRunner` → `TemporalStepRunner`.
## Full Example
A complete PR review agent using all 5 annotations:
```java theme={null}
// -- Domain types --
record PrEvent(String prUrl, String diff) {}
record ReviewReport(String summary, String recommendation) {}
// -- Steps with stable names --
@StepName("analyze-diff")
@Description("Parses a PR diff and extracts changed files")
public class AnalyzeDiffStep implements Step {
@Override
public String execute(AgentContext ctx, PrEvent event) {
return llm.analyze(event.diff());
}
@Override public String name() { return "AnalyzeDiffStep"; }
}
@StepName("produce-review")
@Description("Produces a structured review report")
public class ProduceReviewStep implements Step {
@Override
public ReviewReport execute(AgentContext ctx, String analysis) {
return new ReviewReport(analysis, "LGTM");
}
@Override public String name() { return "ProduceReviewStep"; }
}
// -- Agent --
@Agent("pr-review")
@Description("Reviews a pull request and produces a structured report")
public class PrReviewAgent implements AgentHandler {
private final Step analyze = new AnalyzeDiffStep();
private final Step review = new ProduceReviewStep();
@Override
public ReviewReport handle(AgentContext ctx, PrEvent event) {
return Workflow.define("pr-review")
.step(analyze)
.then(review)
.run(event, ctx);
}
@ExceptionHandler(IllegalStateException.class)
public ReviewReport handleBudget(IllegalStateException ex) {
return new ReviewReport("Budget exceeded: " + ex.getMessage(), "SKIP");
}
}
// -- Cross-cutting error handling --
@AgentAdvice
public class GlobalErrorHandler {
@ExceptionHandler(Exception.class)
public Object handleAny(Exception ex, AgentContext ctx) {
return new ReviewReport("Error: " + ex.getMessage(), "ERROR");
}
}
// -- Wiring --
AgentRegistry registry = new AgentRegistry(Map.of(
"pr-review", new PrReviewAgent()
));
var resolver = new AgentExceptionHandlerResolver(
List.of(new GlobalErrorHandler())
);
```
A runnable version of this example — with real LLM calls, `AgentRegistry` lookup, `StepNames` resolution, and exception handler tests — is in [AnnotationModelIT.java](https://github.com/markpollack/workflow-dsl-examples/blob/main/src/test/java/io/github/markpollack/workflow/examples/ours/AnnotationModelIT.java) in the [workflow-dsl-examples](https://github.com/markpollack/workflow-dsl-examples) repo.
## Annotation Summary
| Annotation | Target | Module | Purpose |
| ------------------- | ------------ | -------------- | -------------------------------------------------- |
| `@StepName` | Type | workflow-api | Stable checkpoint/Temporal key |
| `@Description` | Type, Method | workflow-api | Human-readable description |
| `@ExceptionHandler` | Method | workflow-api | Marks exception handler methods |
| `@Agent` | Type | workflow-flows | Spring stereotype for `AgentHandler` beans |
| `@AgentAdvice` | Type | workflow-flows | Spring stereotype for cross-cutting error handlers |
The first three are pure Java — zero Spring dependency, portable across any runtime. The last two are Spring stereotypes that enable component scanning and auto-discovery.
## What's Next
Crash recovery with CheckpointingStepRunner and Temporal integration
Branch, loop, parallel, decision, gate, supervisor — 10+ composable patterns
Step, AgentContext, Gate, WorkflowGraph, StepRunner
Steps, context, your first workflow
# Agent Workflow API Reference
Source: https://lab.pollack.ai/docs/agent-workflow/api-reference
Step interface, Workflow builder, WorkflowGraph IR, gates, context, StepRunner, and TraceRecorder
## Step\
The atomic unit of a workflow.
```java theme={null}
public interface Step {
String name();
O execute(AgentContext ctx, I input);
default AgentContext updateContext(AgentContext ctx, O output) { return ctx; }
default Class> inputType() { return Object.class; }
default Class> outputType() { return Object.class; }
static Step named(String name, BiFunction fn);
static Step noop(); // anonymous pass-through
static Step noop(String name); // named pass-through — referenceable in IR
}
```
**`updateContext()`** — override to publish side-channel metadata alongside the primary output. Called by the executor after `execute()`. The `output` parameter is the value returned by `execute()` — derive metadata from it directly rather than capturing state in fields:
```java theme={null}
class ClassifierStep implements Step {
@Override
public ClassificationResult execute(AgentContext ctx, String input) {
return runModel(input); // rich result carries all data
}
@Override
public AgentContext updateContext(AgentContext ctx, ClassificationResult output) {
return ctx.mutate()
.with(CONFIDENCE, output.confidence())
.with(DETECTED_LABEL, output.label())
.build();
}
}
```
The primary output (`ClassificationResult`) flows forward as the next step's input; metadata (confidence, label) flows via context keys. Default implementation returns `ctx` unchanged.
**AgentStep** marker — implement on steps that make LLM calls for correct `NodeType.AGENT` cost tracking:
```java theme={null}
public interface AgentStep { /* marker — no methods */ }
```
## Built-in Step Types
| Type | Factory | What it wraps |
| ---------- | -------------------------------------------------- | ---------------------------------------- |
| Lambda | `Step.named("n", (ctx, in) -> ...)` | Any function |
| Chat | `ChatClientStep.of(chat, "template {input}")` | Single Spring AI call |
| Claude | `ClaudeStep.of("template").workingDirectory(path)` | Full Claude CLI session |
| A2A | `A2AStep.of(url)` | Remote agent via Agent-to-Agent protocol |
| Function | `Steps.of(fn)` | Pure deterministic function |
| Retry | `Steps.retrying(3, step)` | Retry wrapper |
| Output ref | `Steps.outputOf("step-name")` | Read prior step result from context |
| Terminate | `Steps.terminate(status, msg)` | Early workflow exit |
### ClaudeStep options
```java theme={null}
ClaudeStep.of("Fix the tests in {input}")
.workingDirectory(projectPath)
.permissionMode(PermissionMode.ACCEPT_EDITS)
.withMcp(mcpConfig)
.withA2a(a2aEndpoint);
```
## Workflow Builder
```java theme={null}
Workflow.define("name")
.step(step) // first step
.then(step) // chain
.branch(predicate).then(a).otherwise(b) // conditional
.gather(step1, step2) // homogeneous fan-out → List