JamJet
Open Source

Java SDK

Build durable, governed agents on the JVM with dev.jamjet:jamjet-agent — tools, policy, budgets, and the durable run loop.

Java SDK

Author agents in Java, run them on the JamJet engine. Tools are plain annotated methods, and every run is durable: the engine owns the loop, so a crash resumes from the event log instead of starting over.

Installation

Requires Java 21+.

<dependency>
    <groupId>dev.jamjet</groupId>
    <artifactId>jamjet-agent</artifactId>
    <version>0.4.0</version>
</dependency>

Gradle:

implementation("dev.jamjet:jamjet-agent:0.4.0")

Earlier docs referenced dev.jamjet:jamjet-sdk. That module is frozen and no longer built from this repository. jamjet-agent is the maintained artifact.


Quick start

A tool is any object with @Tool methods. Hand instances to the builder and run.

import dev.jamjet.agent.Agent;
import dev.jamjet.agent.Tool;

public class SearchTools {
    @Tool(name = "web_search", description = "Search the web for a query.")
    public String webSearch(String query) {
        return "results for " + query;
    }
}
import dev.jamjet.agent.Agent;
import dev.jamjet.agent.AgentResult;

var agent = Agent.builder("research_agent")
        .model("anthropic/claude-sonnet-4-6")
        .instructions("You are a helpful research assistant.")
        .tools(new SearchTools())
        .build();

AgentResult result = agent.runDurable("What is JamJet?");
System.out.println(result.output());

runDurable creates the workflow, starts an execution, polls it to a terminal state, and extracts the answer. It needs a running engine — jamjet dev starts one on http://127.0.0.1:7700, the default.

There is no in-process run() in Java. runDurable is the only run, and that is deliberate: the durable path is the one with retries, approvals, budgets and an audit trail. Java authors agents; the engine executes them.


Agents

Agent.builder(name) returns a fluent builder. Only name and model are required.

import dev.jamjet.agent.Agent;
import dev.jamjet.agent.Budget;
import java.util.List;

var agent = Agent.builder("support_agent")
        .model("anthropic/claude-sonnet-4-6")
        .instructions("Answer support questions. Escalate refunds.")
        .tools(new SearchTools(), new RefundTools())
        .strategy("react")
        .budget(new Budget(100_000, 2.50))
        .approvalRequired(List.of("issue_refund", "send_*"))
        .pii(true)
        .timeoutSeconds(120)
        .build();
Builder methodEffect
model(String)Provider-routed model ref, e.g. anthropic/claude-sonnet-4-6
instructions(String)System prompt
tools(Object...) / tools(List<?>)Tool-holder instances to scan for @Tool
registry(ToolRegistry)Supply a registry directly instead of holders
strategy(String)Loop strategy, e.g. react
policy(PolicySetIr)Attach a full policy set
approvalRequired(boolean)Require human approval for every tool call
approvalRequired(List<String>)Require approval for tools matching these globs
budget(Budget)Token and/or cost ceiling for the run
pii(boolean)Enable PII redaction
timeoutSeconds(int)Run timeout

Accessors mirror the builder — agent.name(), agent.model(), agent.registry(), agent.budget(), agent.approvalGlobs(), and so on.

Budgets

import dev.jamjet.agent.Budget;

new Budget(100_000, 2.50);   // both ceilings
Budget.ofTokens(100_000);    // tokens only
Budget.ofCostUsd(2.50);      // cost only

A run that would exceed a ceiling is stopped by the engine before the call is made, not after.


Tools

@Tool goes on a method. The registry derives an OpenAI-format schema from the method signature, so parameters are typed rather than stringly-typed.

import dev.jamjet.agent.Tool;

public class MathTools {
    @Tool(name = "add_numbers", description = "Add two integers.")
    public String addNumbers(int a, int b) {
        return String.valueOf(a + b);
    }

    // No annotation: never exposed, never callable.
    public String internalHelper() {
        return "not a tool";
    }
}

Omit name and the tool takes the method name verbatimaddNumbers would be offered to the model as addNumbers. Set it explicitly when you want the snake_case convention the Python SDK produces.

Only declared @Tool methods are ever invoked reflectively. A model cannot reach an unannotated method, and the durable worker refuses any dispatch coordinate other than the fixed one the compiler emits.

Tool registry

Build a registry directly when you want to inspect or share it:

import dev.jamjet.agent.tools.ToolRegistry;

var registry = ToolRegistry.of(new SearchTools(), new MathTools());

registry.tools();               // List<RegisteredTool>
registry.byName("web_search");  // RegisteredTool
registry.openAiToolSchemas();   // the schemas sent to the model
registry.isEmpty();

ToolRegistry.of(...) takes holder instances or a List<?>; register(Object) adds one to an existing registry.


Run options

RunOptions controls where the run goes and how long it waits.

import dev.jamjet.agent.RunOptions;
import java.time.Duration;

var options = RunOptions.defaults()
        .withRuntimeUrl("https://engine.internal:7700")
        .withAuth(System.getenv("JAMJET_TOKEN"), "acme")
        .withMaxTurns(8)
        .withPollInterval(Duration.ofMillis(250))
        .withTimeout(Duration.ofMinutes(5));

var result = agent.runDurable("Summarise this quarter's incidents.", options);

Defaults: runtime URL http://127.0.0.1:7700, 500 ms poll interval.

The result

import dev.jamjet.agent.AgentResult;

AgentResult result = agent.runDurable("What is JamJet?");

result.output();          // final assistant text
result.toolCalls();       // List<AgentResult.ToolCall>
result.terminalState();   // ExecutionState: executionId, status, currentState

for (AgentResult.ToolCall call : result.toolCalls()) {
    System.out.println(call.tool() + " " + call.input() + " -> " + call.output());
}

A run that ends failed or limit_exceeded throws AgentRunException rather than returning a hollow result; a run that never terminates throws AgentRunTimeoutException.


Runtime client

JamjetEngineClient is the supported Java client for the engine's HTTP API. Use it when you want the lifecycle rather than the one-shot runDurable.

import dev.jamjet.agent.client.JamjetEngineClient;
import dev.jamjet.agent.client.ExecutionState;
import java.util.Map;

try (var client = new JamjetEngineClient("http://127.0.0.1:7700")) {
    var created = client.createWorkflow(agent.compileToIr());

    var started = client.startExecution(
            created.workflowId(), Map.of("topic", "incidents"), created.version());

    ExecutionState state = client.getExecution(started.executionId());
    System.out.println(state.status());          // running | completed | failed | ...
    System.out.println(state.currentState());

    client.listEvents(started.executionId());    // the full event log
}

compileToIr() produces the WorkflowIr for an agent; compileToIr(int maxTurns) bounds the loop.

Workflows themselves are authored in Python or YAML — there is no Java workflow DSL. Java builds agents, compiles them to IR, and runs them. The IR record types under dev.jamjet.runtime.core.ir exist for serialisation, not as an authoring surface.


Durable tool worker

java_tool nodes execute outside the engine. JavaToolWorker claims them, dispatches to your @Tool methods, and settles each item.

import dev.jamjet.agent.client.JamjetEngineClient;
import dev.jamjet.agent.tools.ToolRegistry;
import dev.jamjet.agent.worker.JavaToolWorker;

try (var client = new JamjetEngineClient("http://127.0.0.1:7700");
     var worker = new JavaToolWorker(client, "java-worker-0",
             ToolRegistry.of(new SearchTools(), new MathTools()))) {
    worker.run();   // claim / dispatch / settle, until closed
}

runOnce() processes a single item and returns an ItemResult, which is useful in tests and in externally scheduled loops.

The worker handles lease renewal, treats a reclaimed lease as a no-op rather than a failure, and echoes the engine's idempotency key so a replay does not fire your tool twice. See the worker protocol if you are implementing your own.


Spring Boot

dev.jamjet:jamjet-agent-spring-boot-starter auto-configures the client and the tool worker from application properties. See the Spring Boot integration guide.


Python vs Java

CapabilityPythonJava
Author an agentyesyes
Tools from annotated functions/methods@jamjet.tool@Tool
In-process runyesnorunDurable only
Durable runyesyes
Policy, approvals, budgets, PIIyesyes
Author workflowsyes (Python / YAML)no — no Java workflow DSL
Run evalsyesno — evals are configured in IR and run engine-side
Engine HTTP clientyesyes (JamjetEngineClient)
Durable tool workeryesyes (JavaToolWorker)

Examples

Runnable examples live in the examples/ directory of the Java repository.

On this page