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...
Scanned 9/3/2026
Install to Claude Code
npx -y skills add openconcerto/jOpenAgent --skill jopenagent --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Jopenagent?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/openconcerto-jopenagent)More formats (shields.io, HTML) on the badges page.
---
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.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!