Build LLM agents in Java with the jOpenAgent framework (org.jopenagent), plain Java classes where fields are state, ordinary methods are capabilities, and @Generative methods are delegated to an LLM. Use when writing, reviewing, or debugging agents in the JOOA project, or whenever @Generative / @SystemPrompt / AgentConfig / Agent / generate(...) / CODE_ACT / TOOL_CALLING / PredictStrategy appear. Covers structured output, strategies, tools, context, memory, skills, MCP, tracing and the projec...
Installs into .claude/skills of the current project.
Are you the author of Jopenagent?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/openconcerto-jopenagent)
---
name: jopenagent
description: Build LLM agents in Java with the jOpenAgent framework (org.jopenagent), plain Java classes where fields are state, ordinary methods are capabilities, and @Generative methods are delegated to an LLM. Use when writing, reviewing, or debugging agents in the JOOA project, or whenever @Generative / @SystemPrompt / AgentConfig / Agent / generate(...) / CODE_ACT / TOOL_CALLING / PredictStrategy appear. Covers structured output, strategies, tools, context, memory, skills, MCP, tracing and the project's hard constraints.
---
# jOpenAgent: writing agents in Java
jOpenAgent is a Java port of NVIDIA's NOOA (Object-Oriented Agents). The whole
idea: **an agent is a plain Java class.** Fields are state, ordinary methods are
deterministic capabilities, and methods annotated `@Generative` are delegated to
an LLM at runtime.
Package root: `org.jopenagent`. Target JDK 21. Sources under `src/`, tests under
`test/`, runnable tutorial examples in `org.jopenagent.examples` (E01..E19).
## The minimal agent
```java
package org.jopenagent.examples;
import org.jopenagent.core.Agent;
import org.jopenagent.core.AgentConfig;
import org.jopenagent.core.Generative;
import org.jopenagent.core.SystemPrompt;
@SystemPrompt("You are an agent specializing in analyzing customer feedback.")
public class FeedbackAgent extends Agent {
public FeedbackAgent(AgentConfig config) {
super(config);
}
@Generative("Analyze customer feedback for sentiment and key topics in one sentence.")
public String analyzeFeedback(String text) {
return generate(text);
}
}
```
```java
FeedbackAgent agent = new FeedbackAgent(
AgentConfig.builder().llmClient(ExampleSetup.createLlmClient()).build());
String result = agent.analyzeFeedback("Great product, but shipping was slow");
```
The `@Generative` **annotation value is the prompt**: it plays the role of a
NOOA method's docstring. Java strips real javadoc at compile time, so the
instruction must live in the annotation, not in a `/** ... */` comment.
Likewise `@SystemPrompt`'s value is the class-level system prompt.
## Rule 1 (the trampoline): always `return generate(...)`
This is the single most important mechanic, and the easiest thing to get wrong.
A `@Generative` method has a **real body** that must end in
`return generate(...)`. There is no proxy and no bytecode generation:
`Agent#generate` walks the stack (`StackWalker`) to find the calling method,
reads its `@Generative` instruction and its **declared return type** by
reflection, and dispatches to the strategy.
```java
@Generative("Classify the ticket into one of the known categories.")
protected Category classify(String ticketText) {
return generate(ticketText); // interception point
}
```
- Pass to `generate(...)` exactly the values the prompt should see. Arguments are
matched back to the enclosing method's **parameter names**, so
`generate(ticketText)` is bound as `ticketText`.
- Statements **before** the `generate(...)` call run as ordinary Java (NOOA's
"pre-ellipsis code", setup, prefill, validation).
- A `@Generative` method with no inputs is fine: `return generate();` (it then
relies on the system prompt and context blocks).
- `generate` is `protected final <T> T`: the target type is inferred from the
method's declared return type. Never cast its result; declare the return type
instead.
Do **not**: write a `@Generative` method that returns something else, call
`generate()` from a non-`@Generative` method, or wrap it behind a helper. The
stack walk resolves the *immediate* caller.
## Rule 2: structured output is just the return type
Declare a `record` (or POJO, `List`, `Set`, `Map`, `Optional`, enum, primitive)
as the return type. `org.jopenagent.json.ObjectBinder` binds and validates the
LLM's JSON onto it, and on mismatch retries with the validation error fed back
to the model (bounded by `maxRetries`).
```java
public record ReviewSummary(int rating, String sentiment, List<String> highlights) {
}
@Generative("Extract a rating out of 5, an overall sentiment word, and up to 3 highlight phrases.")
public ReviewSummary summarize(String review) {
return generate(review);
}
```
No schema registration is needed: the schema is generated by reflection
(`SchemaGenerator`). When the model never produces valid output, a
`GenerationException` (unchecked) is thrown rather than returning `null`.
## Strategies
Selected per method: `@Generative(value = "...", strategy = StrategyKind.X)`, or
globally via `AgentConfig.builder().defaultStrategy(...)`. Default is `PREDICT`;
`INHERIT` means "use the agent's default".
| Strategy | What it does | Use when |
|---|---|---|
| `PREDICT` | One structured-output call + retry on invalid output | The default; pure input → typed output |
| `TOOL_CALLING` | Native function-calling loop over the agent's visible ordinary methods (+ attached MCP tools) | The model must consult real code/data before answering |
| `CODE_ACT` | The model writes Java, executed by a JShell sandbox with a live `self` bound to the agent, looping until `return_result` | Multi-step work combining several methods, computation |
| `REFLEXION` | Base strategy (`PREDICT` by default) → structured self-critique → retry with feedback, up to 3 attempts | Quality matters more than latency |
`CODE_ACT` requires the VM argument `--add-modules jdk.jshell` on the run
configuration. It runs in-process by default; set
`SandboxConfig.mode(SandboxMode.OUT_OF_PROCESS)` for a child JVM, where a
timeout is a real process kill instead of a cooperative request a tight loop can
ignore.
## Exposing tools
Ordinary public methods are visible to the model automatically. Annotate them
with `@Doc("...")` to describe them: that text becomes the tool description.
```java
@Doc("Returns the current stock count for an item, or 0 if unknown.")
public int getStock(String item) {
return inventory.getOrDefault(item, 0);
}
@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);
}
```
Visibility follows NOOA's visible-by-default model: `@Hidden` removes a
field/method from what the model sees, `@Shown` forces it back in. `@NoTrace`
suppresses tracing for a member. `Agent#describe()` renders the visible surface
(progressive disclosure), also available inside a prompt as `{doc(self)}`.
## Built-in agent facilities
All are public final fields on `Agent`, usable from ordinary code and from
`CODE_ACT`-generated code via `self`:
- **`context`** (`ContextApi`): blocks folded into the system prompt.
`context.put("plan", "...")` for static, `context.setDynamic("backlog",
"self.formatBacklog()")` for a block re-evaluated on every prompt build.
- **`memory`** (`MemoryManager`): `remember(...)`, `recall(query, k)`,
`update(id, text)`, `forget(...)`, with dedup-on-write and
importance/recency-weighted recall. Offline `HashingEmbedder` by default;
swap in a real one with `AgentConfig.builder().embedder(...)`.
- **`skills`** (`SkillRegistry`): `load(skill)`, `discoverAndLoadAll(rootDir)`.
A `TextSkill` is a `SKILL.md` bundle with sandboxed `readFile` and
`runScript(...)`.
- **`history`** (`ConversationHistory`): accumulates across `CODE_ACT` /
`TOOL_CALLING` calls on one instance; auto-summarized by `HistorySummarizer`
past a configurable threshold.
- **MCP**: attach an `McpClient` as a plain field. Its tools are callable from
`CODE_ACT` code (`self.wiki.callTool(...)`) and merged into `TOOL_CALLING`'s
schema list as `fieldName__toolName`.
Prompt templating supports exactly two forms (not arbitrary evaluation):
`{self.field}` and `{doc(self)}`.
## Configuration
```java
AgentConfig config = AgentConfig.builder()
.llmClient(new AnthropicClient(apiKey, "claude-sonnet-4-5"))
.maxRetries(3)
.defaultStrategy(StrategyKind.PREDICT)
.maxCodeActIterations(10)
.historyConfig(historyConfig)
.memoryManager(memoryManager)
.embedder(embedder)
.sandboxConfig(sandboxConfig)
.build();
```
LLM clients (`org.jopenagent.llm`, all on `java.net.http` only):
```java
new AnthropicClient(apiKey, model); // Messages API
new OpenAiClient(apiKey, model); // Chat Completions
OpenAiClient.forLmStudio(model, baseUrl); // local, no key
OpenAiClient.forOllama(model, baseUrl); // local, no key
```
In examples, prefer `ExampleSetup.createLlmClient()`, which reads
`ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `JOPENAGENT_LMSTUDIO_MODEL` /
`JOPENAGENT_OLLAMA_MODEL` and `JOPENAGENT_MODEL`.
## Project constraints: non-negotiable
These are properties of this codebase, not style preferences. Violating them
breaks the build or the design:
1. **No lambda expressions and no method references, anywhere.** Use anonymous
inner classes and explicit `for`/`while` loops, including for `Function`,
`ThreadFactory`, `Comparator`, `Runnable`.
2. **No external dependencies.** No Maven/Gradle, no HTTP library (use
`java.net.http`), no JSON library (use the bundled `org.jopenkit.json` +
`org.jopenagent.json`). JUnit 5 is for tests only.
3. **Single Eclipse project, no VCS.** Do not introduce a build file.
4. **JDK 21**, records and pattern matching available.
5. **`-parameters` must stay on** (`org.eclipse.jdt.core.compiler.codegen.methodParameters=generate`).
Without it, `generate(data)` cannot match `data` to the real parameter name
and tool/record schemas lose their field names.
6. **`--add-modules jdk.jshell`** on any run/debug/JUnit configuration touching
`org.jopenagent.sandbox` (i.e. `CODE_ACT`).
7. **No stubs, no TODOs, no `UnsupportedOperationException` placeholders** in
delivered code. If something is unimplemented, say so explicitly.
8. **Javadoc is not optional on agent classes and `@Generative` methods**, but
remember the *prompt* lives in the annotation; javadoc is for humans.
9. Record any deliberate divergence from NOOA in `DESIGN_NOTES.md`.
## Tracing, eval, debugging
Everything is traced by default: each LLM call, code/tool execution and
generation method is a `org.jopenagent.trace.Span` with explicit parent-child
nesting. Register an exporter to see it:
- `JsonTraceExporter`: span-tree JSON (see `E06`).
- `AtifExporter`: ATIF-v1.7, the format NOOA itself produces.
- `OtlpHttpExporter` / `LangfuseExporter`: OTLP/HTTP JSON.
- `TraceViewerServer`: embedded web viewer (`E16`), `jdk.httpserver` only.
- `TraceViewerSwingApp`: native desktop viewer (`E17`).
- `TerminalTraceExplorer`: pretty-print a span tree.
`org.jopenagent.eval` is the eval harness: an `EvalSuite` of `EvalCase`s run by
`EvalRunner` against a `Scorer` (`ExactMatchScorer`, `ContainsScorer`,
`NumericToleranceScorer`, or your own). Each case is an `eval_case` span, so the
viewer's "Experiments" tab reads results straight from traces (`E19`).
When an agent misbehaves, look at the trace before suspecting the framework: a
small local model that never calls `return_result`, or invents data instead of
calling a real method, is a model-capability limit. jOpenAgent raises
`GenerationException` rather than hanging or guessing.
## Common mistakes
| Symptom | Cause |
|---|---|
| Instruction ignored | Prompt written in javadoc instead of `@Generative`'s value |
| `generate` resolves the wrong method | Called from a helper instead of directly in the `@Generative` method |
| Parameters named `arg0`, `arg1` | `-parameters` compiler setting turned off |
| `CODE_ACT` fails at startup | Missing `--add-modules jdk.jshell` VM argument |
| Endless retries then `GenerationException` | Return type the model cannot satisfy; simplify the record or raise `maxRetries` |
| Tool never called | Method not visible (`@Hidden`), missing `@Doc`, or strategy left at `PREDICT` |
## Safety
jOpenAgent is research/experimental software. `CODE_ACT` executes
LLM-generated code and MCP servers run as real subprocesses with the JVM's own
permissions. `CodeValidator` is a source-level denylist: **defense in depth,
not a containment boundary**. Run agents that execute generated code inside OS
isolation (container or VM). `SandboxMode.OUT_OF_PROCESS` upgrades *timeout*
containment only.
## Where to look next
- `README.md`: full feature list and NOOA parity table.
- `DESIGN_NOTES.md`: every divergence from NOOA and why.
- `org.jopenagent.examples` E01..E19: a progressive, runnable tutorial:
E01 first generation method, E02 structured output, E03 tools, E04 strategy
comparison, E05 progressive disclosure, E06 tracing, E07 context blocks,
E08 code-as-action, E09 history, E10 skills, E11 MCP, E12 memory,
E13 multimodal, E14 ATIF, E15 reflexion, E16/E17 trace viewers,
E18 out-of-process sandbox, E19 eval harness.