Skip to content

Repository files navigation

Agentle

This docs was updated at: 2026-03-21

A Java agent framework built on OpenAI's Responses API.

Coverage Java License

The problem

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.

Installation

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.

See it in action

An agent with tools

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

Structured streaming with partial JSON

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();

Structured contracts for delegating agents

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.
  • SubAgentTool remains 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.

Tool planning with parallel execution

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");

Human-in-the-loop

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

Multi-agent patterns

Six patterns, all implementing Interactable. Swap any pattern without changing your service code.

Multi-agent patterns overview

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.

Dynamic tool selection

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.

Blueprints — agents as JSON

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.

JSON-first agent definitions

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();

LLM-generated agents

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.

Harness engineering

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.

Self-correction loop

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");

Lifecycle hooks

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();

Shell verification tools

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();

Durable memory

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();

Progress logs and artifact stores

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_artifacts

Harness builder — compose everything

Interactable 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.

Everything else

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, or JdbcMemory. 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.

Design choices

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

Provider support

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();

Get started

<dependency>
    <groupId>io.github.paragon-intelligence</groupId>
    <artifactId>agentle4j</artifactId>
    <version>0.10.1</version>
</dependency>

Then explore:

Contributing

make build      # Build
make test       # Run tests
make format     # Format code

License

MIT

About

Creating AI agents with the simplicity of Python in Java.

Resources

Stars

11 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages