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 method | Effect |
|---|---|
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 onlyA 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 verbatim — addNumbers 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
| Capability | Python | Java |
|---|---|---|
| Author an agent | yes | yes |
| Tools from annotated functions/methods | @jamjet.tool | @Tool |
| In-process run | yes | no — runDurable only |
| Durable run | yes | yes |
| Policy, approvals, budgets, PII | yes | yes |
| Author workflows | yes (Python / YAML) | no — no Java workflow DSL |
| Run evals | yes | no — evals are configured in IR and run engine-side |
| Engine HTTP client | yes | yes (JamjetEngineClient) |
| Durable tool worker | yes | yes (JavaToolWorker) |
Examples
Runnable examples live in the examples/
directory of the Java repository.