Features
Everything in the harness
This is not a roadmap. Every subsystem below is implemented, documented and exercised by a
runnable example, and none of them adds a single jar to your classpath.
The core mechanism
Four annotations and a method call
There is no DSL to learn here. An agent is a class. The annotations tell the harness which parts
of it the model can see, and generate(…) is where control passes from your
code to the model and back again.
@SystemPrompt
Goes on the class, and its value is the system prompt. Supports two templating forms: {self.field} and {doc(self)}.
@Generative
Goes on a method. Its value is the instruction, its strategy selects how the call runs, and the method signature is the contract.
@Doc
Goes on an ordinary method or field to describe it to the model, playing the role a docstring would in Python.
@Hidden / @Shown
Control visibility. Members are visible to the model by default, so you hide the ones that are implementation detail.
the interception pointhow generate() works
@Generative("Write a one-sentence, friendly greeting for the given person.")
public String greet(String name) {
return generate(name);
}
No magic, deliberately
generate(…) uses StackWalker to identify its calling
method, reads that method’s @Generative instruction and declared
return type by reflection, and dispatches to the configured strategy.
That is the whole trick. No dynamic proxy, no bytecode generation, no AOP weaving and
no annotation processor, which means your stack traces are real, your breakpoints land,
and there is nothing to debug but your own code.
Evaluation
Prompt changes become measurements
Define an EvalSuite, which is an experiment, made of EvalCases.
Run it with EvalRunner against a Scorer and get results back
directly. ExactMatchScorer, ContainsScorer and
NumericToleranceScorer ship with it, and a custom scorer is one method.
There is deliberately no separate results database. Each case is its own
eval_case trace span carrying experiment and
eval.* attributes, so the results are traces. The trace
viewer’s Experiments tab reads them to show pass rates and a
per-model, per-tier breakdown, and links straight into the underlying trace of any
individual failure.
- One artefact to store, browse and diff instead of two.
- A failing case sits one click away from the exact prompt and response that caused it.
- Compare models on the same suite without changing a line of agent code.
an experimentorg.jopenagent.eval
EvalSuite<String> suite = EvalSuite.<String>builder("ticket-triage-v1")
.addCase(EvalCase.builder("billing-1", new EvalTask<String>() {
@Override
public String run() {
return agent.classify("I was charged twice this month.");
}
}).displayName("Duplicate charge")
.expected("billing")
.tier(Tier.STABLE)
.metadata("difficulty", "easy").build())
.build();
List<EvalResult<String>> results =
EvalRunner.run(suite, new ContainsScorer(), agent.getLlmClient().modelName());
for (EvalResult<String> result : results) {
System.out.println((result.isPassed() ? "PASS" : "FAIL")
+ " " + result.getCaseId()
+ " expected=" + result.getExpected()
+ " actual=" + result.getActual());
}
Open the Experiments tab in the built-in trace viewer to see the same run
as a pass-rate table with a link into every individual trajectory.
Sandbox & safety
Two execution modes, and an honest account of both
IN_PROCESS, the default
Generated Java runs in an embedded JShell engine in the current JVM, with
self bound to your live agent. It is fast, simple, and the right choice
while you are developing.
CodeValidator applies a source-level denylist of dangerous packages and
classes before anything executes.
OUT_OF_PROCESS
Generated code runs in a child JVM, talking to the parent over a bidirectional
JSON-RPC channel on a loopback socket, so calls back into self still work
transparently.
The reason to switch is that a timeout becomes a real process kill. The default engine
can only ask a running snippet to yield at its next safepoint, which a tight infinite
loop will never do.
What the sandbox is not. CodeValidator is defence-in-depth, not a
containment boundary. A static source check cannot stop a determined program from reaching the
filesystem or the network through reflection or another indirect route. OUT_OF_PROCESS
upgrades timeout containment only, and adds no filesystem or network isolation.
Always run agents that execute generated code inside OS-level isolation, meaning
a container or a VM. The same applies to MCP: attaching a server grants that subprocess whatever
permissions the JVM itself holds, so only attach servers you trust.
LLM clients
Model-agnostic, built on the JDK
LlmClient is a small interface with two implementations, both written
entirely against java.net.http.HttpClient. There is no vendor SDK, no HTTP
library and no reactive runtime, so nothing here can conflict with whatever your
application already uses.
AnthropicClient covers the Messages API, including native media content.
OpenAiClient covers the Chat Completions API, which means real OpenAI and, by extension, any OpenAI-compatible server.
- Local models are first class.
OpenAiClient.forLmStudio(…) and .forOllama(…) need no API key and no account.
- Embeddings follow the same pattern, with matching factories on
OpenAiEmbedder.
switching providerone line
// development: free, local, nothing leaves the machine
LlmClient llm = OpenAiClient.forLmStudio("qwen/qwen3-coder-30b", "http://localhost:1234");
// staging: a small hosted model
LlmClient llm = new OpenAiClient(key, "gpt-4o-mini");
// production: a frontier model
LlmClient llm = new AnthropicClient(key, "claude-sonnet-4-5");
Test without a network at all
LlmClient is an interface, so a mock implementation is a few lines. The
project’s own test suite uses exactly that to assert on retry behaviour, tool
dispatch, strategy selection and wire formats, hermetically, in CI, with no keys.
Development experience
A skill so your AI assistant writes it correctly
This one is not a runtime feature, but it is the one that changes your day the most. The
repository ships a SKILL.md file that teaches AI coding assistants the
framework: the trampoline rule, which strategy fits which problem, how tool visibility
works, the project’s hard constraints, and a table mapping six common symptoms to
their real cause.
Copy it into your assistant’s skills folder or rules file, then describe the agent
you want. It also works the other way round: point an assistant at an
existing agent class and ask it to review against the conventions.
installone copy
cp -r skills/jopenagent .claude/skills/
Why it is needed
Models have read thousands of LangChain4j and Spring AI examples and almost no
jOpenAgent. Without the skill they 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. Both are reasonable guesses. Both are wrong here.
See it all running
Nineteen examples, each with a main(), each run for real against a live model.