Java 21 · Apache 2.0 · zero dependencies

The agent harness for Java.

This is not another thin client for calling an LLM API.
jOpenAgent is the full harness around the model: typed input and output, code-as-action, pluggable reasoning strategies, durable object state, memory, skills, MCP, tracing and evaluation. All of it written as ordinary Java classes that your debugger, your test runner and your IDE already understand.

FeedbackAgent.javaa complete agent
@SystemPrompt("You analyze customer feedback.")
public class FeedbackAgent extends Agent {

    public FeedbackAgent(AgentConfig config) {
        super(config);
    }

    @Generative("Extract a rating out of 5, an overall "
              + "sentiment word, and up to 3 highlights.")
    public ReviewSummary summarize(String review) {
        return generate(review);   // interception point
    }

    public record ReviewSummary(
            int rating, String sentiment,
            List<String> highlights) { }
}

No proxy, no bytecode generation, no annotation processor. The method body is real, and generate(…) is where it hands off to the harness.

Why the harness is the product

The model is not the agent. The harness is.

NVIDIA’s research on agent architecture puts it bluntly: the harness around a model “can account for double-digit swings in benchmark results and significant differences in token cost, with the same underlying model.” Swapping to a smarter model is the small lever. How you feed it context, type its outputs, let it act and persist its state is the big one.

Java teams have had excellent LLM integration libraries for years. jOpenAgent sits in a different category. It brings the six capabilities that separate an agent from a chat completion call onto the JVM, and it does so without asking you to learn a new programming model first.

A raw LLM SDK gives you

  • A String in and a String out. You write the parser, the retry and the repair prompt yourself.
  • A context window you fill by hand, with whole objects serialised into text.
  • A fixed tool menu, so anything conditional or iterative goes back through the model one turn at a time.
  • State that lives only in a growing message list you have to prune.
  • No trace and no span tree, so “why did it do that?” is unanswerable after the fact.
  • No way to tell whether last week’s prompt change made things better or worse.

jOpenAgent gives you

  • A method signature as a typed contract, bound and validated by reflection, with bounded retry that feeds the error back to the model.
  • Progressive disclosure, so the model sees a bounded description of a live object rather than a text dump.
  • Code as action: the model writes Java and it runs against a live self in a JShell sandbox.
  • Durable, typed state on the agent object, plus cross-session memory and self-summarising history.
  • Every LLM call, tool call and code execution as a nested span, exported as JSON, ATIF, OTLP or Langfuse.
  • A built-in eval harness, so a prompt change becomes a measurement instead of a hunch.

The six harness capabilities

All six. In Java. In one jar.

These are the capabilities the research community converged on for high-performing agent harnesses. Each one maps to a concrete, documented jOpenAgent API, not a roadmap item.

1

Typed input & output

Agentic calls carry typed arguments and validated return values instead of unstructured text. The declared return type of a @Generative method is the schema.

In jOpenAgent: ObjectBinder and SchemaGenerator bind LLM JSON onto records, POJOs, collections, maps, Optional and enums, with automatic retry when the output does not fit.
2

Pass by reference

The model works on live objects and sees bounded previews, not serialised dumps. Full values stay in the runtime instead of burning context window.

In jOpenAgent: Agent#describe() and {doc(self)} render visible fields and methods. @Hidden and @Shown control exactly what the model sees.
3

Code as action

The model acts by writing code with real control flow and inline method calls, so loops, conditionals and composition happen in one place instead of one tool call per turn.

In jOpenAgent: the CODE_ACT strategy runs generated Java in a JShell sandbox with a live self bound, in-process or in a child JVM with a real hard-kill timeout.
4

Programmable loop engineering

The orchestration loop is ordinary code you can read, change and version, not an opaque agent loop you configure from the outside.

In jOpenAgent: four strategies ship (PREDICT, CODE_ACT, TOOL_CALLING, REFLEXION), selectable per method, and Strategy is a plain interface you can implement.
5

Explicit object state

Durable, typed state lives on the agent object itself, not only in a conversation transcript that grows until something truncates it.

In jOpenAgent: fields are state. Add self.memory for cross-session recall and self.history, which summarises itself once it passes a token budget.
6

Model-callable harness APIs

Context blocks and event history are APIs the model can inspect and manage, which gives the agent authority over its own working memory.

In jOpenAgent: self.context (static and dynamic blocks, re-evaluated on every prompt build), plus self.memory, self.history and self.skills, all reachable from generated code and tool calls.

Agents are objects

Your agent is a class. That is the whole idea.

Fields are state. Ordinary methods are deterministic capabilities the model can call. Methods annotated @Generative are delegated to an LLM at runtime. That is the entire mental model, and it means every tool you already own keeps working.

  • Step through an agent in your debugger. Breakpoints land in real frames, because they are real frames.
  • Unit-test with JUnit. Swap in a mock LlmClient and assert on the typed return value.
  • Refactor with confidence. Rename a capability and the compiler finds every caller.
  • No magic. There is no dynamic proxy, no bytecode weaving, no annotation processor and no reflection framework to learn.
InventoryAgent.javatools via self
@SystemPrompt("You are an inventory assistant.")
public class InventoryAgent extends Agent {

    private final Map<String, Integer> inventory = new HashMap<>();

    @Doc("Returns the current stock count for an item.")
    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 question using the tools.",
                strategy = StrategyKind.TOOL_CALLING)
    public String answer(String question) {
        return generate(question);
    }
}

There is no tool-registration step. Visible methods on the agent are the tools, and their JSON schemas come from the real signatures.

Beyond one tool call per turn

Let the model write Java

With StrategyKind.CODE_ACT the model does not pick a tool from a menu. It writes Java that calls your methods with loops, conditionals and composition, executes it against a live self, sees the real result, and iterates until it calls return_result. One round trip instead of six.

Execution runs through a JShell-backed sandbox with a source-level validator. Set SandboxMode.OUT_OF_PROCESS and generated code runs in a child JVM, which means a timeout becomes a real process kill rather than a polite request that an infinite loop can ignore.

OrdersAgent.javaCODE_ACT
@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();
}
generated & executed in the sandboxby the model
List<Integer> totals = self.orderTotalsCents();
int sum = 0;
for (Integer t : totals) { sum += t; }
return_result(Math.round(sum / (float) totals.size() / 100f));

Traced by default

Observability you do not have to bolt on

Every LLM call, every tool invocation, every sandbox execution and every generative method is a span with explicit parent and child nesting. You do not configure anything to get the tree. You only pick an exporter to get it out.

  • Four exporters. Plain JSON span trees, ATIF v1.7 agent-trace interchange, standard OTLP/HTTP to any collector, and a preconfigured Langfuse exporter.
  • A real trace viewer, built in. It runs on the JDK’s own com.sun.net.httpserver, with no Node, no React and no build step. It lists traces, renders span trees, accepts OTLP posts, browses memory and holds annotations.
  • A desktop viewer too. A zero-dependency Swing frame, live in-process or polling a running viewer server.
  • And a terminal one. TerminalTraceExplorer pretty-prints the span tree to any PrintStream.

Measured, not guessed

A built-in eval harness

Define an EvalSuite of EvalCases, run it with EvalRunner against a Scorer, and get pass and fail results back directly. Exact-match, contains and numeric-tolerance scorers ship with it, and writing your own takes one method.

There is no separate results database, because there does not need to be one. Each case is its own eval_case trace span, and the trace viewer’s Experiments tab reads those same spans to show pass rates, a per-model breakdown, and a link straight into the underlying trace of any individual failure.

EvalRunnerorg.jopenagent.eval
EvalSuite<String> suite = EvalSuite.<String>builder("ticket-triage-v1")
        .addCase(EvalCase.builder("billing-1", task)
                .expected("billing").tier(Tier.STABLE).build())
        .addCase(EvalCase.builder("ambiguous-1", task)
                .expected("billing").tier(Tier.FRONTIER).build())
        .build();

List<EvalResult<String>> results =
        EvalRunner.run(suite, new ContainsScorer(), agent.getLlmClient().modelName());

Included in the box

Your AI assistant already knows this framework

jOpenAgent ships a skill file that teaches coding assistants how to write jOpenAgent code. Copy it into your assistant’s skills or rules folder, describe the agent you want in plain English, and you get back Java that compiles and follows the conventions.

This matters more than it sounds. Ask any assistant for a Java agent today and it will reach for the frameworks it has seen thousands of times, which is not this one. Even with the README pasted in, models tend to write a @Generative method with a real implementation in the body, or put the prompt in a javadoc comment that Java discards at compile time.

The skill covers the trampoline rule, the strategy decision table, tool visibility, the project constraints, and a symptom-to-cause table for the six things that usually go wrong. It works for reviewing existing agent code as well as writing new code.

installone copy
cp -r skills/jopenagent .claude/skills/
then just askplain English
Add a @Generative method to InvoiceAgent that extracts
line items into a record, and retry twice on invalid output.

Convert this batch job into a CODE_ACT agent that reads
self.ledgerEntries() and reports discrepancies.

Review this agent class against the jOpenAgent conventions.

Native SKILL.md support in Claude Code, or paste the body into a Cursor rule, a Copilot instructions file, or the chat itself.

How it compares

Everything the Python harnesses do, natively on the JVM

The best ideas in agent engineering shipped in Python first. jOpenAgent is not a bridge, a sidecar or a REST hop into a Python service. It is the same capability set, implemented in Java, running in the JVM you already deploy.

Capability jOpenAgent Strands Agents
+ AI Functions
Pydantic AI
+ Harness
magentic NVIDIA NOOA
Language / runtime Java 21, JVM Python, TypeScript Python Python Python
Agent definition A plain class: fields = state, methods = capabilities Agent object + @tool / @ai_function Agent object + composable capabilities @prompt / @chatprompt functions A plain class with ... method bodies
Typed, validated output Java records & POJOs, reflective binding Pydantic models Pydantic models Pydantic models Pydantic models
Automatic retry on invalid output bounded, error fed back post-conditions validation retries
Code as action (sandbox) JShell, live self, optional child JVM local code execution mode Code Mode (Monty) no exec() + optional sandbox
Multiple reasoning strategies, per call PREDICT / CODE_ACT / TOOL_CALLING / REFLEXION partial one agent loop partial composed capabilities partial per-decorator three strategies
Self-critique / reflexion built-in REFLEXION post-condition loops guardrails no
Cross-session memory self.memory, pluggable embedder memory capability no nooa-memory
Auto-summarising history token-budget summariser conversation managers context management no
Skills (on-demand instructions) SKILL.md bundles + real subprocess scripts via tools skills capability no
MCP client native stdio JSON-RPC, no SDK no wraps the Python MCP SDK
Multimodal input image / audio / video / files vision
Tracing JSON, ATIF v1.7, OTLP/HTTP, Langfuse OpenTelemetry OpenTelemetry / Logfire OpenTelemetry / Logfire OTel + ATIF
Trace viewer in the box web, desktop and terminal external Logfire (hosted) Logfire (hosted) FastAPI + React
Eval harness in the box suites, scorers, experiments tab separate pydantic-evals no eval pipeline
AI coding-assistant skill ships in the repository no docs lookup capability no authoring skills
Runtime dependencies Java 21+ Python runtime + package tree Python runtime + package tree Python runtime + package tree Python runtime + package tree
License Apache 2.0 Apache 2.0 MIT MIT Apache 2.0

For Java teams

Your agent belongs where your domain lives

The interesting agents are the ones wired into a real system: the order book, the ledger, the scheduler, the ERP. In most enterprises that system is Java. Rewriting it in Python to reach an agent framework, or bolting a Python service beside it, buys you a network hop, a second runtime to operate and a serialisation boundary between the model and your domain objects.

No second runtime

One JVM, one process model, one deployment artefact, one set of ops runbooks. There is no Python interpreter to install.

Your real objects, in reach

The model’s generated code calls self.repository.findOrder(id) directly. No DTO layer, no JSON round trip, and no drift between two definitions of the same entity.

Tooling you already have

Eclipse and IntelliJ debuggers, JUnit, javadoc, your profiler, your APM agent, your build. An agent is just another class in the project.

Records as the schema layer

Java 21 records give you the concise, immutable, typed shapes that Python needs a validation library to express. The language is the schema.

Any model, local or hosted

Anthropic’s Messages API and the OpenAI Chat Completions API, which also covers Ollama and LM Studio. Develop against a local model for free, then switch by changing one factory call.

Honest about the limits

Generated-code execution is defence-in-depth, not containment. We document that plainly instead of implying a sandbox is a security boundary.

Quickstart

Three things and you are running

Add one dependency, point it at a model, and write a class. A local LM Studio or Ollama server needs no API key at all, so you can work through the whole quickstart for free. There is no server to start, no configuration file and no registry to populate.

pom.xmlMaven Central
<dependency>
    <groupId>org.jopenagent</groupId>
    <artifactId>jopenagent</artifactId>
    <version>1.0.0</version>
</dependency>
Main.javarun it
LlmClient llm = OpenAiClient.forLmStudio("qwen/qwen3-coder-30b", "http://localhost:1234");

FeedbackAgent agent = new FeedbackAgent(
        AgentConfig.builder().llmClient(llm).build());

ReviewSummary summary = agent.summarize(
        "The ramen was incredible and the service fast, "
      + "but the room was loud and cramped.");

System.out.println(summary.rating());      // 4
System.out.println(summary.sentiment());   // "positive"
System.out.println(summary.highlights());  // [incredible ramen, fast service, ...]

Build your first Java agent today

Apache 2.0, on Maven Central, with a complete manual in English and in French.