Examples

Nineteen agents you can run today

A progressive tutorial. Each example is a single class with a main(), adding exactly one idea to the one before it. All nineteen ship in org.jopenagent.examples and have been run for real against a live model, not merely compiled.

E01

The smallest possible agent

One class, one LLM-backed capability. The @Generative value is the prompt, the return type is the contract, and generate(name) is where the harness takes over.

Note what is not here: no client wiring inside the agent, no prompt template file, no output parser, no registration call.

E01_FirstGenerationMethod.javaPREDICT
@SystemPrompt("You are a warm, concise greeter.")
public class E01_FirstGenerationMethod extends Agent {

    public E01_FirstGenerationMethod(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) {
        E01_FirstGenerationMethod agent = new E01_FirstGenerationMethod(
                AgentConfig.builder().llmClient(ExampleSetup.createLlmClient()).build());
        System.out.println(agent.greet("Ada"));
    }
}

E02

Structured output from a record

A record return type is all it takes. ObjectBinder binds the model’s JSON straight onto the canonical constructor, validating types as it goes, and retries with the validation error fed back if the shape does not fit.

List<String> is bound element by element. Nested generics, enums, Optional and further records all work the same way.

E02_StructuredOutput.javatyped binding
@SystemPrompt("You extract structured information from restaurant reviews.")
public class E02_StructuredOutput extends Agent {

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

    @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);
    }

    public static void main(String[] args) {
        E02_StructuredOutput agent = new E02_StructuredOutput(
                AgentConfig.builder().llmClient(ExampleSetup.createLlmClient()).build());

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

        System.out.println("rating: "     + summary.rating());
        System.out.println("sentiment: "  + summary.sentiment());
        System.out.println("highlights: " + summary.highlights());
    }
}

E03

Tools are just your methods

Annotate ordinary methods with @Doc, pick StrategyKind.TOOL_CALLING, and the harness derives JSON-schema tool definitions from the real signatures and runs the function-calling loop against them.

The agent’s own inventory map is private state the model never sees directly. It reaches that data only through the two methods you chose to expose.

E03_ToolsViaSelf.javaTOOL_CALLING
@SystemPrompt("You are an inventory assistant.")
public class E03_ToolsViaSelf extends Agent {

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

    public E03_ToolsViaSelf(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 java.util.List<String> knownItems() {
        return new java.util.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);
    }
}

E07

Context blocks, static and live

put stores a block once. setDynamic stores an expression that is re-evaluated every single time a prompt is built, so when the backlog changes the model’s view of it changes too, with no refresh call anywhere in your code.

That is the difference between context that is configured and context that is current.

E07_ContextBlocks.javaself.context
@SystemPrompt("You are a sprint-planning assistant.")
public class E07_ContextBlocks extends Agent {

    private int backlogSize = 5;

    public String formatBacklog() {
        return backlogSize + " item(s) remaining";
    }

    public void addBacklogItem() {
        backlogSize++;
    }

    @Generative("Given the sprint plan and current backlog in context, "
              + "suggest what to tackle next.")
    public String suggestNextStep() {
        return generate();
    }

    public static void main(String[] args) {
        E07_ContextBlocks agent = new E07_ContextBlocks(
                AgentConfig.builder().llmClient(ExampleSetup.createLlmClient()).build());

        agent.context.put("plan", "Sprint goal: ship the billing export by Friday.");
        agent.context.setDynamic("backlog", "self.formatBacklog()");

        System.out.println(agent.suggestNextStep());

        agent.addBacklogItem();
        System.out.println(agent.context.get("backlog"));   // "6 item(s) remaining"
    }
}

E08

Code as action

The instruction tells the model to write code that reads self.orderTotalsCents(). It does exactly that: real Java, executed in a JShell sandbox with self bound to this live instance, iterating until it calls return_result with a value that fits the declared int.

Needs one VM flag. Add --add-modules jdk.jshell to the run configuration. jdk.jshell is a JDK tool module and is not resolved by default for classpath code.
E08_CodeAsAction.javaCODE_ACT
@SystemPrompt("You are a data-processing agent.")
public class E08_CodeAsAction extends Agent {

    private final List<Integer> orderTotalsCents = new ArrayList<>();

    public E08_CodeAsAction(AgentConfig config) {
        super(config);
        orderTotalsCents.add(1299);
        orderTotalsCents.add(499);
        orderTotalsCents.add(2599);
    }

    @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();
    }
}
E18_OutOfProcessSandbox.javareal hard-kill timeout
SandboxConfig outOfProcess = SandboxConfig.builder()
        .mode(SandboxMode.OUT_OF_PROCESS)
        .timeoutMillis(10_000L)
        .build();

AgentConfig config = AgentConfig.builder()
        .llmClient(ExampleSetup.createLlmClient())
        .sandboxConfig(outOfProcess)
        .build();

// E18 then proves the timeout with no LLM involved at all:
try (CodeExecutor sandbox = new OutOfProcessCodeSandbox(agent, 2000L)) {
    SandboxResult result = sandbox.execute("while (true) { }");
    System.out.println(result.getStatus());   // killed, not left hanging
}

E12

Memory across conversations

Two ordinary methods wrap self.memory, and the model calls them as tools. Ask the agent something in the second call and it recalls what you told it in the first. Across sessions too, if you back it with JsonFileMemoryStore.

By default this runs completely offline on a deterministic HashingEmbedder. Pass a real Embedder to AgentConfig and nothing else changes.

E12_Memory.javaself.memory
@SystemPrompt("You are a support agent with long-term memory of past interactions.")
public class E12_Memory extends Agent {

    @Doc("Remembers a fact about a customer for future reference.")
    public String rememberFact(String fact) {
        Memory memory = this.memory.remember(fact);
        return "stored as " + memory.getId();
    }

    @Doc("Recalls the most relevant remembered facts for a query.")
    public String recallFacts(String query) {
        StringBuilder sb = new StringBuilder();
        for (Memory memory : this.memory.recall(query, 3)) {
            sb.append("- ").append(memory.getContent()).append("\n");
        }
        return sb.length() == 0 ? "(nothing relevant remembered)" : sb.toString();
    }

    @Generative(value = "Using self.recallFacts if useful, answer the customer's "
                      + "question. Remember any new fact they share via self.rememberFact.",
            strategy = StrategyKind.TOOL_CALLING)
    public String converse(String customerMessage) {
        return generate(customerMessage);
    }

    public static void main(String[] args) {
        E12_Memory agent = new E12_Memory(AgentConfig.builder()
                .llmClient(ExampleSetup.createLlmClient())
                .embedder(ExampleSetup.createEmbedder())
                .build());

        System.out.println(agent.converse(
                "Hi, I'm on the Enterprise plan and prefer email over phone."));
        System.out.println(agent.converse("How do you prefer to reach me again?"));
    }
}

E10

Skills, loaded on demand

A skill is a SKILL.md bundle: frontmatter plus a markdown body. The model sees only the name and description until it needs the rest.

E10_Skills.javaself.skills
Files.writeString(skillDir.resolve("SKILL.md"),
        "---\n"
      + "name: reply-tone\n"
      + "description: House style for customer replies.\n"
      + "---\n\n"
      + "Always thank the customer first, then answer directly. "
      + "Never apologize more than once.\n");

agent.skills.load(TextSkill.load(skillDir));

for (Skill skill : agent.skills.all()) {
    System.out.println(skill.getId() + ": " + skill.getDescription());
}

TextSkill toneSkill = (TextSkill) agent.skills.get("reply-tone");
System.out.println(toneSkill.getInstructions());   // pulled on demand

Same file format as the skill we ship for AI coding assistants.

E13

Images as real parameters

An Image parameter gets attached to the request as native media content in the provider’s own wire format, instead of being stringified into the prompt.

E13_Multimodal.javaorg.jopenagent.llm.media
@SystemPrompt("You describe the contents of images concisely.")
public class E13_Multimodal extends Agent {

    @Generative("Describe what shape and color is shown in the image, "
              + "in one short sentence.")
    public String describeImage(Image image) {
        return generate(image);
    }

    public static void main(String[] args) throws IOException {
        Image image = Image.fromBytes(renderRedCirclePng(), "image/png");
        // ...or Image.fromFile(path) / Image.fromUrl(url)

        E13_Multimodal agent = new E13_Multimodal(
                AgentConfig.builder().llmClient(ExampleSetup.createLlmClient()).build());
        System.out.println(agent.describeImage(image));
    }
}

E15

Self-correction, as a strategy

REFLEXION generates an answer, produces a structured critique of it, then retries with that critique as feedback, up to three attempts, before returning. The whole cycle is traced, so you can see exactly what the model talked itself out of.

Note the return type: int. The critique loop and the type contract compose, and the caller still just gets a number.

E15_Reflexion.javaREFLEXION
@SystemPrompt("You are a precise arithmetic word-problem solver. "
            + "Show your final numeric answer only.")
public class E15_Reflexion extends Agent {

    @Generative(value = "Solve the word problem and return just the final integer answer.",
            strategy = StrategyKind.REFLEXION)
    public int solve(String wordProblem) {
        return generate(wordProblem);
    }

    public static void main(String[] args) {
        E15_Reflexion agent = new E15_Reflexion(
                AgentConfig.builder().llmClient(ExampleSetup.createLlmClient()).build());

        int answer = agent.solve(
                "A warehouse had 84 pallets. It shipped out 3 trucks with 12 pallets "
              + "each, then received a delivery of 25 more pallets. How many pallets "
              + "are in the warehouse now?");

        System.out.println("Answer: " + answer);   // 84 - 36 + 25 = 73
    }
}

E19

Evaluate it like software

An EvalSuite of tiered cases, a Scorer, and a run tagged with the model name. Results come back as a plain list, and because every case is also its own trace span, the trace viewer’s Experiments tab shows the same run as a pass-rate table you can click into.

Tier.STABLE versus Tier.FRONTIER lets you separate “this must never regress” from “this is the hard case we are chasing”.

E19_EvalHarness.javaorg.jopenagent.eval
TraceViewerServer viewer = new TraceViewerServer();
Tracer.addExporter(viewer);
viewer.start(5001);

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 for my subscription.");
            }
        }).displayName("Duplicate charge")
          .expected("billing")
          .tier(Tier.STABLE)
          .metadata("difficulty", "easy").build())
        .addCase(EvalCase.builder("ambiguous-1", new EvalTask<String>() {
            @Override
            public String run() {
                return agent.classify("My invoice shows a technical-looking "
                                    + "error code instead of the amount due.");
            }
        }).displayName("Invoice shows an error code")
          .expected("billing")
          .tier(Tier.FRONTIER)
          .metadata("difficulty", "hard").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());
}

The full set

All nineteen examples

Each one is a single class in org.jopenagent.examples with its own main(), runnable straight from your IDE once an LLM is configured.

E01First generation methodThe smallest agent: one @Generative method
E02Structured outputA record return type, bound and validated
E03Tools via selfYour methods as function-calling tools
E04Comparing strategiesThe same task under each strategy
E05Progressive disclosuredescribe() and controlling visibility
E06TracingSpan trees and JSON export
E07Context blocksStatic and dynamic context
E08Code as actionGenerated Java in the JShell sandbox
E09History + summarizationCross-call history under a token budget
E10SkillsSKILL.md bundles loaded on demand
E11MCP toolsA real MCP server over stdio JSON-RPC
E12Memoryremember / recall across conversations
E13MultimodalAn image as a typed parameter
E14ATIF trajectory exportThe ATIF v1.7 interchange format
E15ReflexionGenerate, critique, retry
E16Trace viewer (web)The built-in browser trace viewer
E17Trace viewer (desktop)The native Swing viewer
E18Out-of-process sandboxA real hard-kill timeout in a child JVM
E19Eval harnessExperiments, scorers and pass rates
Two things worth knowing before you run them. E08 and anything else using CODE_ACT needs the --add-modules jdk.jshell VM argument. E11 needs Node’s npx on your PATH to launch a reference MCP server, which is a requirement of that example rather than of jOpenAgent.
Small local models will struggle with some of these. The multi-turn CODE_ACT and TOOL_CALLING loops need a model that reliably follows tool-calling conventions. In testing, a 4B model tended to invent data instead of calling the real method, while a 30B code-specialised model got every example right. That is a model-capability limit rather than a framework bug, and when a model never converges the harness raises GenerationException instead of hanging or guessing. Register a JsonTraceExporter or open the trace viewer and you can watch exactly where it went wrong.

Ready to write your own?

One dependency, one class, five minutes. Or hand it to your AI assistant and describe what you want.