Quickstart
Your first Java agent, in five minutes
There is no server to start, no configuration file, no registry to populate and no code generation step. Add one dependency, point it at a model, and write a class.
Step 1
Prerequisites
Two things, and one of them you almost certainly already have.
A JDK 21 or newer
jOpenAgent targets the JavaSE-21 execution environment. Records and
pattern matching are used throughout, and everything else is plain Java. Any
distribution works: Temurin, Corretto, Zulu, Liberica, Oracle.
Access to a model
An Anthropic or OpenAI API key, or a local LM Studio or Ollama server, which needs no key and no account at all. Local models are the recommended way to work through this quickstart, since it costs nothing and nothing leaves your machine.
SKILL.md that makes coding assistants fluent in jOpenAgent, so they stop reaching for
LangChain4j or Spring AI patterns that will not compile here. One copy and you are done:
cp -r skills/jopenagent .claude/skills/, or paste the body into your Cursor rule or
Copilot instructions file. More about the skill.
Step 2
Add the dependency
jOpenAgent is published to Maven Central. There is exactly one artefact, and it pulls in nothing else.
<dependency>
<groupId>org.jopenagent</groupId>
<artifactId>jopenagent</artifactId>
<version>1.0.0</version>
</dependency>
dependencies {
implementation 'org.jopenagent:jopenagent:1.0.0'
}
Two compiler and runtime settings
Both are one line. The first is strongly recommended for every project. The second only matters
if you use the CODE_ACT strategy.
Compile with -parameters
This lets the harness match generate(review) back to the real parameter
name of the enclosing method, and it means tool and record schemas carry your actual
field names instead of arg0.
<compilerArgs><arg>-parameters</arg></compilerArgs>
In Eclipse: Java Compiler → Store information about method parameters.
Run with --add-modules jdk.jshell
Only for agents that use StrategyKind.CODE_ACT.
jdk.jshell is a JDK tool module and is not resolved by default for
classpath code, so it has to be requested explicitly.
java --add-modules jdk.jshell -cp ... Main
The trace viewer needs no such flag, because jdk.httpserver is in
the default module set.
Step 3
Point it at a model
LlmClient is a small interface with two implementations, both built entirely on
java.net.http. Switching providers is one line.
// Anthropic: the Messages API
LlmClient llm = new AnthropicClient(System.getenv("ANTHROPIC_API_KEY"), "claude-sonnet-4-5");
// OpenAI: the Chat Completions API
LlmClient llm = new OpenAiClient(System.getenv("OPENAI_API_KEY"), "gpt-4o-mini");
// Local LM Studio: no API key, nothing leaves the machine
LlmClient llm = OpenAiClient.forLmStudio("qwen/qwen3-coder-30b", "http://localhost:1234");
// Local Ollama: same deal
LlmClient llm = OpenAiClient.forOllama("llama3.1", "http://localhost:11434");
// ...or any other OpenAI-compatible server
LlmClient llm = OpenAiClient.forLocalServer("my-model", "http://gpu-box:8000");
GenerationException rather than guessing.
Step 4
Write your first agent
An agent is a class extending Agent. The @SystemPrompt annotation value
is the system prompt. A method annotated @Generative is delegated to the model: its
annotation value is the instruction, and its body ends in generate(…), which
is the hand-off point.
import org.jopenagent.core.Agent;
import org.jopenagent.core.AgentConfig;
import org.jopenagent.core.Generative;
import org.jopenagent.core.SystemPrompt;
@SystemPrompt("You are a warm, concise greeter.")
public class GreeterAgent extends Agent {
public GreeterAgent(AgentConfig config) {
super(config);
}
@Generative("Write a one-sentence, friendly greeting for the given person.")
public String greet(String name) {
return generate(name);
}
}
public static void main(String[] args) {
LlmClient llm = OpenAiClient.forLmStudio("qwen/qwen3-coder-30b", "http://localhost:1234");
GreeterAgent agent = new GreeterAgent(
AgentConfig.builder().llmClient(llm).build());
System.out.println(agent.greet("Ada"));
}
What just happened
generate(name) walked the stack to find its calling method, read the
@Generative instruction and the declared return type by reflection, built
the prompt, dispatched to the default PREDICT strategy and bound the
result back to String. No proxy, no bytecode generation, no annotation
processor.
@Generative value
is the prompt. Edit it, recompile, and the capability behaves differently, with the method
signature still enforcing the contract.
Step 5
Ask for a typed result
Declare a record as the return type and you are done. The harness generates the JSON schema from the record, asks the model for structured output, binds the response onto the canonical constructor, and if the model returns something that does not fit, retries with the validation error fed back to it, up to a configurable bound.
@SystemPrompt("You extract structured information from restaurant reviews.")
public class ReviewAgent extends Agent {
public record ReviewSummary(int rating, String sentiment,
List<String> highlights) { }
public ReviewAgent(AgentConfig config) {
super(config);
}
@Generative("Extract a rating out of 5, an overall sentiment word, "
+ "and up to 3 highlight phrases from the review.")
public ReviewSummary summarize(String review) {
return generate(review);
}
}
summary.rating() -> 4
summary.sentiment() -> "positive"
summary.highlights() -> ["incredible ramen",
"fast service",
"loud room"]
What binds
Records, POJOs, List, Set, Map,
Optional, enums, primitives and nested combinations of all of them.
Reflectively, with no per-class registration and no code generation.
Step 6
Give it tools by writing methods
There is no tool-registration API. Ordinary visible methods on the agent are the tools.
Annotate them with @Doc to describe them to the model, switch the generative method
to StrategyKind.TOOL_CALLING, and the harness derives JSON schemas from the real
signatures and runs the function-calling loop.
@SystemPrompt("You are an inventory assistant.")
public class InventoryAgent extends Agent {
private final Map<String, Integer> inventory = new HashMap<>();
public InventoryAgent(AgentConfig config) {
super(config);
inventory.put("widget", 12);
inventory.put("gadget", 0);
inventory.put("gizmo", 5);
}
@Doc("Returns the current stock count for an item, or 0 if unknown.")
public int getStock(String item) {
return inventory.getOrDefault(item, 0);
}
@Doc("Returns the full list of known item names.")
public List<String> knownItems() {
return new ArrayList<>(inventory.keySet());
}
@Generative(value = "Answer the user's question about inventory using the available tools.",
strategy = StrategyKind.TOOL_CALLING)
public String answer(String question) {
return generate(question);
}
}
McpClient as a plain
field and its tools are merged into the same schema list automatically, named
fieldName__toolName. They are also callable directly from generated code as
self.wiki.callTool(…).
Step 7
Let it write code instead
When a task needs loops, conditionals or composition, one tool call per turn is the wrong shape.
StrategyKind.CODE_ACT hands the model a live self and lets it write
Java, executed in a JShell-backed sandbox, iterating until it calls return_result.
@Doc("Returns the list of order totals, in cents.")
public List<Integer> orderTotalsCents() {
return new ArrayList<>(orderTotalsCents);
}
@Generative(value = "Compute the average order total in whole "
+ "dollars (rounded), by writing code that "
+ "reads self.orderTotalsCents().",
strategy = StrategyKind.CODE_ACT)
public int averageOrderDollars() {
return generate();
}
AgentConfig config = AgentConfig.builder()
.llmClient(llm)
.sandboxConfig(SandboxConfig.builder()
.mode(SandboxMode.OUT_OF_PROCESS)
.build())
.build();
OUT_OF_PROCESS upgrades timeout
containment, so a hung generated call gets its whole child JVM killed, but it adds no
filesystem or network isolation. Run agents that execute generated code inside a container
or VM.
Step 8
See exactly what happened
Tracing is on by default, so every LLM call, tool invocation and sandbox execution is already a nested span. All you choose is where it goes. To watch a run live, start the built-in trace viewer: it is a real web app on the JDK’s own HTTP server, with no Node toolchain anywhere in sight.
TraceViewerServer viewer = new TraceViewerServer(); Tracer.addExporter(viewer); viewer.start(5001); // ... run your agent, then open http://localhost:5001 viewer.stop();
// Exporters are registered globally on the Tracer.
Tracer.addExporter(new JsonTraceExporter(new File("trace.jsonl")));
Tracer.addExporter(new AtifExporter(new File("trace.atif.json")));
Tracer.addExporter(new OtlpHttpExporter("http://collector:4318"));
Tracer.addExporter(new LangfuseExporter(publicKey, secretKey));
// Or pretty-print any span tree straight to a console:
TerminalTraceExplorer.print(rootSpan, System.out);
Where next
Keep going
The 19 examples
A progressive tutorial from a one-method agent to memory, MCP, multimodal input, reflexion, out-of-process sandboxing and the eval harness. Every one has a main() and has been run for real.
The AI skill
Teach your coding assistant the framework in one file, then describe the agents you want in plain English. It reviews existing agent code just as well.
The manual
Sixteen chapters plus appendices covering every subsystem, a full class reference and the design rationale behind each decision. Available as a PDF in English and in French.
The feature tour
Strategies, memory, skills, MCP, context blocks, history summarisation, tracing, evaluation and the sandbox: what each one does and when to reach for it.