This docs was updated at: 2026-03-21
A Java agent framework built on OpenAI's Responses API.
Agentle is built around the Responses API: item-based conversation state, native tool calling, structured output, streaming, and APIs that fit agent workflows.
The library keeps a direct abstraction over the Responses API instead of layering on top of older chat-oriented request shapes. If you need tool planning, multi-agent orchestration, structured streaming, or human-in-the-loop workflows, that's the part of the codebase the framework optimizes for.
Maven:
<dependency>
<groupId>io.github.paragon-intelligence</groupId>
<artifactId>agentle4j</artifactId>
<version>0.10.1</version>
</dependency>Gradle:
implementation 'io.github.paragon-intelligence:agentle4j:0.10.1'Requires Java 25+ with preview features enabled. The Maven build compiles, tests, and generates Javadocs with --enable-preview.
Agent agent = Agent.builder()
.name("Assistant")
.model("openai/gpt-4o")
.instructions("You are a helpful assistant.")
.responder(Responder.builder().openRouter().apiKey(key).build())
.addTool(new GetWeatherTool())
.build();
AgentResult result = agent.interact("What's the weather in Tokyo?");
System.out.println(result.output());
System.out.println("Transcript size: " + result.messages().size());
System.out.println("Last user message: " + result.lastUserMessageText("[none]"));
// Agents never throw. Errors live in the result.
if (result.isError()) {
System.err.println(result.error().getMessage());
}Stream structured output and watch fields populate in real-time. The parser auto-completes incomplete JSON as it arrives, so your UI updates progressively. No other Java framework does this.
record Person(String name, int age, String occupation) {}
var payload = CreateResponsePayload.builder()
.model("openai/gpt-4o")
.addUserMessage("Create a fictional software engineer")
.withStructuredOutput(Person.class)
.streaming()
.build();
responder.respond(payload)
.onPartialJson(fields -> {
// Fields arrive as they generate: first "name", then "age", then "occupation"
if (fields.containsKey("name"))
updateUI(fields.get("name").toString());
})
.onParsedComplete(parsed -> {
Person p = parsed.outputParsed(); // Fully typed
})
.start();When an agent can produce some structured outputs locally and only propagate others through terminal handoffs, model those contracts separately:
outputType(...)or.structured(...)defines what the current agent itself may produce. This schema is sent to that agent's LLM.returns(...)defines the final contract exposed to your backend. It parses the final result without changing the request schemas sent by the source interactable.Handoff.propagatedOutput(...)declares which structured outputs may flow back through a terminal handoff.SubAgentToolremains text-only. Use handoffs, not sub-agent tools, for terminal structured propagation.
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "kind")
@JsonSubTypes({
@JsonSubTypes.Type(value = DirectAnswer.class, name = "direct_answer"),
@JsonSubTypes.Type(value = Escalation.class, name = "escalation")
})
sealed interface MainDirectOutput permits DirectAnswer, Escalation {}
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "kind")
@JsonSubTypes({
@JsonSubTypes.Type(value = DirectAnswer.class, name = "direct_answer"),
@JsonSubTypes.Type(value = Escalation.class, name = "escalation"),
@JsonSubTypes.Type(value = ActivityResult.class, name = "activity_result")
})
sealed interface MainFinalOutput permits DirectAnswer, Escalation, ActivityResult {}
record DirectAnswer(String kind, String message) implements MainDirectOutput, MainFinalOutput {}
record Escalation(String kind, String team) implements MainDirectOutput, MainFinalOutput {}
record ActivityResult(String kind, String activityId) implements MainFinalOutput {}
Agent activities = Agent.builder()
.name("Activities")
.model("openai/gpt-4o")
.instructions("Return the final activity result as JSON.")
.responder(responder)
.outputType(ActivityResult.class)
.build();
Agent main = Agent.builder()
.name("Main")
.model("openai/gpt-4o")
.instructions("Answer directly when possible, otherwise hand off.")
.responder(responder)
.outputType(MainDirectOutput.class) // local produces contract
.addHandoff(Handoff.to(activities)
.withDescription("Use for activity workflows")
.propagatedOutput(ActivityResult.class)
.build())
.build();
StructuredAgentResult<MainFinalOutput> result =
main.returns(MainFinalOutput.class).interact("Open activity act_123");
System.out.println(result.outputOrigin()); // LOCAL or DELEGATED
System.out.println(result.outputProducerName()); // "Main" or "Activities"
System.out.println(result.delegationPath()); // ["Main"] or ["Main", "Activities"]For discriminated unions, keep the discriminator field (kind above) in the serialized payload so the final branch can be parsed correctly.
One line enables tool planning. The LLM batches tool calls into a dependency graph, the framework topologically sorts and executes them in parallel waves, and $ref references resolve between steps. One LLM round-trip instead of five.
Agent agent = Agent.builder()
.name("Researcher")
.model("openai/gpt-4o")
.instructions("You gather and compare data from multiple sources.")
.responder(responder)
.addTool(new GetWeatherTool())
.addTool(new GetNewsTool())
.addTool(new CompareDataTool())
.enableToolPlanning()
.build();
// LLM plans: getWeather("Tokyo") || getWeather("London") -> compareData(results)
// Framework executes in parallel, resolves references, returns final output
AgentResult result = agent.interact("Compare weather in Tokyo vs London");Agents pause at sensitive tools. State is serializable — save it to any database, resume hours or days later.
@FunctionMetadata(name = "send_email", description = "Sends an email",
requiresConfirmation = true)
public class SendEmailTool extends FunctionTool<EmailParams> { ... }
AgentResult result = agent.interact("Send the quarterly report to the team");
if (result.isPaused()) {
AgentRunState state = result.pausedState();
saveToDatabase(state); // Serializable — persist anywhere
}
// Hours later, after approval in your web UI...
AgentRunState state = loadFromDatabase(runId);
state.approveToolCall("User approved via dashboard");
AgentResult resumed = agent.resume(state);Six patterns, all implementing Interactable. Swap any pattern without changing your service code.
| Pattern | Example | Use case |
|---|---|---|
| Router | RouterAgent.builder().addRoute(billing, "invoices, payments")... |
Classify and route to specialists |
| Supervisor | SupervisorAgent.builder().addWorker(writer, "writes content")... |
Central coordinator with workers |
| Parallel | ParallelAgents.of(researcher, analyst).runAll("analyze") |
Concurrent independent work |
| Network | AgentNetwork.builder().addPeer(optimist).addPeer(pessimist)... |
Peer-to-peer multi-round debate |
| Hierarchical | HierarchicalAgents.builder().executive(ceo).addDepartment(...) |
Org-chart workflows |
| Sub-agent | .addSubAgent(analyst, "for deep analysis") |
Delegate, get result, continue |
For runtime flow diagrams, context propagation notes, and compile-aligned examples, see the Agentic Patterns Visual Guide.
// Your service works with any pattern — same interface
public class AgentService {
private final Interactable agent;
public String process(String input) {
return agent.interact(input).output();
}
}
new AgentService(singleAgent);
new AgentService(router);
new AgentService(supervisor);
new AgentService(parallelTeam);All patterns support streaming. See the Agents Guide for full documentation. For the behavior-first walkthrough, see the Agentic Patterns Visual Guide.
For structured terminal delegation, returns(...) works with any Interactable, including Agent, RouterAgent, SupervisorAgent, and HierarchicalAgents.
50 tools? 500? ToolRegistry sends only the relevant ones per request. No context window explosion.
ToolRegistry registry = ToolRegistry.builder()
.strategy(new BM25ToolSearchStrategy(5)) // Top 5 most relevant
.eagerTool(helpTool) // Always available
.deferredTools(List.of(tool1, tool2, ...)) // Only when relevant
.build();
Agent agent = Agent.builder()
.name("Assistant")
.toolRegistry(registry)
.responder(responder)
.build();Pluggable strategies: BM25, semantic similarity, regex, or write your own. See the Tool Search Guide.
Serialize any agent (or entire multi-agent constellation) to JSON. Store in a database, version in git, share across services, load at runtime. No recompilation.
// Agent → JSON
String json = agent.toBlueprint().toJson();
// JSON → Agent (API keys auto-resolved from environment variables)
Interactable agent = new ObjectMapper()
.readValue(json, InteractableBlueprint.class)
.toInteractable();
agent.interact("Hello!");Works with every pattern — Agent, RouterAgent, SupervisorAgent, AgentNetwork, ParallelAgents, HierarchicalAgents. Nested constellations serialize recursively: a Router containing a Supervisor containing three Agents becomes one JSON file.
Skip Java builders entirely. Write a JSON file, deserialize, run.
{
"type": "agent",
"name": "CustomerSupport",
"model": "openai/gpt-4o",
"instructions": "You are a professional support agent for Acme Corp.",
"maxTurns": 15,
"responder": {
"provider": "OPEN_ROUTER",
"apiKeyEnvVar": "OPENROUTER_API_KEY"
},
"toolClassNames": ["com.acme.tools.SearchKnowledgeBase", "com.acme.tools.CreateTicket"],
"handoffs": [],
"inputGuardrails": [{ "registryId": "profanity_filter" }],
"outputGuardrails": []
}String json = Files.readString(Path.of("agents/support.json"));
Interactable agent = new ObjectMapper()
.readValue(json, InteractableBlueprint.class)
.toInteractable();AgentDefinition is designed for structured output — an LLM creates agents at runtime.
Interactable.Structured<AgentDefinition> metaAgent = Agent.builder()
.name("AgentFactory")
.model("openai/gpt-4o")
.instructions("You create agent definitions. Available tools: search_kb, create_ticket.")
.structured(AgentDefinition.class)
.responder(responder)
.build();
AgentDefinition def = metaAgent.interact(
"Create a Spanish-speaking support agent"
).output();
// LLM decides behavior — you provide infrastructure
Interactable agent = def.toInteractable(responder, "openai/gpt-4o", availableTools);
agent.interact("¿Cómo puedo recuperar mi contraseña?");See the Blueprints Guide for the full JSON schema reference, multi-agent serialization examples, and Spring Boot integration.
The bottleneck is infrastructure, not intelligence. Agentle ships a full harness layer — constraints, verification loops, and feedback systems that let agents do reliable long-horizon work.
When an agent fails (guardrail violation, tool error, bad output), inject the error back and retry. LangChain benchmarks show this one feature gives the largest accuracy improvement.
Interactable correcting = SelfCorrectingInteractable.wrap(agent,
SelfCorrectionConfig.builder()
.maxRetries(3)
.retryOn(result -> result.isError())
.feedbackTemplate("Your attempt failed:\n{error}\nPlease fix it and try again.")
.build());
AgentResult result = correcting.interact("Write a sorting algorithm");Inject logging, cost tracking, rate limiting, or circuit breakers around any agent run or tool call — without modifying the agent.
HookRegistry hooks = HookRegistry.create()
.add(new AgentHook() {
@Override
public void beforeToolCall(FunctionToolCall call, AgenticContext ctx) {
System.out.println("Calling: " + call.name());
}
@Override
public void afterToolCall(FunctionToolCall call, ToolExecution exec, AgenticContext ctx) {
System.out.println("Done: " + exec.isSuccess() + " in " + exec.duration().toMillis() + "ms");
}
});
Agent agent = Agent.builder()
.hookRegistry(hooks)
.build();Give agents the ability to run their own tests and linters. The command is fixed at construction time — the agent can trigger it but cannot inject arguments.
Agent agent = Agent.builder()
.name("CodeWriter")
.instructions("Write code, then run_tests. If tests fail, fix the code and run again.")
.addTool(ShellVerificationTool.builder()
.name("run_tests")
.command("mvn", "test", "-q")
.workingDir(Path.of("/my/project"))
.timeoutSeconds(120)
.build())
.build();InMemoryMemory is fine for prototyping. For production long-running agents:
// Filesystem: survives JVM restarts
Memory memory = FilesystemMemory.create(Path.of("/var/agent-data/memory"));
// JDBC: any SQL database
Memory memory = JdbcMemory.create(hikariDataSource);
// Both plug into the same interface — no agent code changes
Agent agent = Agent.builder().addMemoryTools(memory).build();For Anthropic-style long-running multi-session agents that need to track work across restarts:
ProgressLog log = ProgressLog.create();
ArtifactStore store = FilesystemArtifactStore.create(Path.of("./artifacts"));
Agent agent = Agent.builder()
.addTools(ProgressLogTool.all(log).toArray(new FunctionTool[0]))
.addTools(ArtifactStoreTool.all(store).toArray(new FunctionTool[0]))
.build();
// Agent can now: read_progress_log, append_progress_log, read_artifact, write_artifact, list_artifactsInteractable harnessedAgent = Harness.builder()
.selfCorrection() // 3 retries on error
.addHook(new LoggingHook())
.artifactStore(FilesystemArtifactStore.create(Path.of("./artifacts")))
.progressLog(ProgressLog.create())
.reportExporter(RunReportExporter.create(Path.of("./reports")))
.wrap(myAgent);See the Harness API Reference for the full package documentation.
Agentle ships with more than agents. Each feature has a dedicated guide.
- MCP Client — Connect to Model Context Protocol servers via stdio or HTTP. Tools appear as native
FunctionTools. - Skills — Modular expertise (SKILL.md files) injected into agent prompts. Reusable knowledge, not isolated sub-agents.
- Web Extraction — Playwright renders the page, LLM extracts structured data.
WebExtractor.create(responder, model). - Guardrails — Input/output validation. Block dangerous prompts, enforce constraints, fail before the LLM runs.
- Context Management — Sliding window or LLM-powered summarization for long conversations. Pluggable strategies.
- Memory — Persistent cross-conversation memory.
InMemoryMemory,FilesystemMemory, orJdbcMemory. The agent stores and retrieves on its own. - Prompt Builder — Fluent API with chain-of-thought, few-shot examples, templates, and multi-language support.
- Observability — Built-in OpenTelemetry. Traces span across agent handoffs and parallel execution. One line:
.addTelemetryProcessor(LangfuseProcessor.fromEnv()). - Vision — Multi-modal input with
Image.fromUrl(), base64, or file ID. Control detail level per image. - Messaging — WhatsApp integrations with adaptive batching, rate limiting, and conversation history.
- Embeddings — Text embeddings with automatic retry on 429/5xx and provider fallbacks.
- Streaming — Text deltas, tool call events, structured output — all via virtual-thread callbacks.
- Tools — Type-safe function tools using Java records. Auto-generated JSON schemas from generics.
- Tool Planning — DAG-based parallel tool execution with reference resolution between steps.
- Harness Engineering — Self-correction loops, lifecycle hooks, shell verification, durable memory, progress logs, artifact stores, and run reports.
| Area | What the code does today |
|---|---|
| API layer | Targets the Responses API directly |
| Java baseline | Requires Java 25+ with preview features enabled |
| Concurrency model | Synchronous-first APIs designed to run well on virtual threads |
| Agent errors | Agent.interact(...) returns AgentResult instead of throwing |
| Streaming | ResponseStream and AgentStream expose callback-based streaming |
| Tool execution | Function tools, DAG-based tool planning, and parallel waves |
| Human approval | Serializable pause/resume via AgentRunState |
| Messaging | MessagingProvider abstraction with WhatsApp implementation included |
| Web extraction | Playwright rendering plus typed extraction through WebExtractor |
Works with any provider that implements the Responses API:
// OpenRouter — 300+ models
Responder.builder().openRouter().apiKey(key).build();
// OpenAI direct
Responder.builder().openAi().apiKey(key).build();
// Groq
Responder.builder()
.baseUrl(HttpUrl.parse("https://api.groq.com/openai/v1"))
.apiKey(key).build();
// Local Ollama
Responder.builder()
.baseUrl(HttpUrl.parse("http://localhost:11434/v1"))
.build();<dependency>
<groupId>io.github.paragon-intelligence</groupId>
<artifactId>agentle4j</artifactId>
<version>0.10.1</version>
</dependency>Then explore:
- Getting Started Guide — First agent in 5 minutes
- Full Documentation — Guides, API reference, examples
- Code Examples — Copy-paste ready snippets
make build # Build
make test # Run tests
make format # Format codeMIT
