ARA

September 24, 2026 · View on GitHub

ARA — Agent Runtime Architecture

ARA — Agent Runtime Architecture for Java

Build autonomous AI agents and multi-agent systems in plain Java 21. LLM integration, tool calling, deterministic I/O contracts, RAG and human-in-the-loop — no annotations, no reflection, no Spring, no Kotlin.

Maven Central Java License Last update Stars

Quick start · Documentation · Examples · Website · Discussions


ARA is a Java 21 agent runtime: a place where agents actually run, with sessions, memory, execution strategies, cost budgets, approval gates and a full execution trace — not a thin wrapper around a chat completion endpoint.

It works with OpenAI, Anthropic, Ollama, Mistral, LM Studio, Groq and any OpenAI-compatible endpoint, and it is published on Maven Central as io.github.xmor:ara-runtime.

Your code ──▶ AraRuntime ──▶ Agent (strategy + contract + session)
                                │
                     ┌──────────┼───────────┐
                     ▼          ▼           ▼
                 LlmClient   Tools      Retriever
             OpenAI/Claude/  (yours,   (in-memory
             Ollama/Mistral  parallel   or Qdrant)
                             on vthreads)

Quick start — 60 seconds, no API key

1. Add the dependency (check the badge above for the latest version):

<dependency>
    <groupId>io.github.xmor</groupId>
    <artifactId>ara-runtime</artifactId>
    <version>1.0.2</version>
</dependency>

2. Run an agent offline. ScriptedLlmClient replays canned responses, so the first run needs no key, no network and no local model:

import io.ara.runtime.AraRuntime;
import io.ara.runtime.stubs.ScriptedLlmClient;

try (var runtime = AraRuntime.builder()
        .llmClient(ScriptedLlmClient.script()
                .thenFinalAnswer("Virtual threads are lightweight JVM threads.")
                .build())
        .build()) {

    System.out.println(runtime.askText("Explain virtual threads"));
}

Virtual threads are lightweight JVM threads.

That is the whole loop, runnable as basics/MinimalAgentExample. What just happened: runtime.askText(...) runs the prompt on a shared default agent and returns the answer string directly — the runtime auto-started on first use, and close() shuts it down. The default react strategy called the LLM, ended at a final answer, and returned it.

When the agent needs a role, a system prompt, tools or a specific model, create one explicitly and call agent.execute(...): you get the full AgentResponse — iteration count, token usage, cost and step trace — instead of just the string. AgentConfig.of("assistant", "...") is the one-line form; reach for AgentConfig.defaults() as soon as a third field is involved. See basics/AraSimpleExample for that fuller shape.

3. Swap in a real model — one line changes, everything else stays:

LlmClient gpt4o = AraLlmClientFactory.openAi()
        .apiKey(System.getenv("OPENAI_API_KEY"))
        .modelName("gpt-4o")
        .build();

// or fully local, no API key:
LlmClient llama = AraLlmClientFactory.ollama()
        .model(OllamaLlmClient.Models.LLAMA_3_2)
        .build();

Requires Java 21+ and Maven 3.9+. Add ara-adapters for real providers → Providers guide.

Build from source
git clone https://github.com/xmor/ara.git && cd ara
mvn clean install -DskipTests

One step further — three agents at once

crew/CodeReviewCrewExample is the same runtime doing multi-agent work. Three specialist reviewers — security, performance, style — run concurrently on virtual threads, then a lead agent merges their findings into a prioritised report. The fan-out is a pipeline step; there are no threads to wire up:

AgentPipeline pipeline = AgentPipeline.builder()
        .parallel("review", List.of(security, performance, style),
                  runtime.executor(), AgentChain.MergeStrategy.joining("\n\n"))
        .step("synthesize", lead)
        .build();

AraAgent crew = PipelineAgents.of(AgentId.of("code-review-crew"), config, pipeline);

Run it offline with main(): the console shows the three reviewers starting together on different threads and finishing in an order decided by their work, then the lead's report and the crew's token/cost summary. Pass live to run the same crew against a real model.


Why ARA

  • Plain Java, no magic. Pure interfaces — zero annotations, zero reflection, no Kotlin runtime, no Spring. The call stack you debug is the call stack you wrote.
  • Deterministic I/O contracts. AgentContract validates, sanitises and transforms in plain Java before and after every call, spending zero tokens.
  • Java 21 by design. Virtual threads are the concurrency model, not an option: when the LLM asks for several tools at once they are dispatched in parallel automatically, with no executor to wire up.
  • A runtime, not a toolkit. Execution strategies, FSM pipelines, session isolation, human-in-the-loop and cost budgets come in the box rather than assembled from parts.
  • Built on LangChain4j, not against it. Provider integration is inherited through ara-adapters, so you get LangChain4j's provider coverage plus the runtime on top.

Where ARA fits in the JVM ecosystem

If you use…ARA's relationship
LangChain4jARA builds on it. ara-adapters wraps LangChain4j clients and adds the runtime layer above: sessions, strategies, contracts, HITL.
A Spring-based AI stackARA has no DI container and no Spring dependency. It drops into a Spring app as an ordinary library, or into a plain main() with none.
Python agent frameworksARA keeps agentic workloads on the JVM stack you already deploy, monitor and secure — no Python service to operate alongside it.
Raw provider SDKsEverything above the HTTP call — retries, failover, circuit breaking, tracing, output validation, approval gates — is already written.

Capabilities that rarely come as one piece

  • Human-in-the-loop as a runtime primitive — an ApprovalGate wired into the tool dispatch chain; ApprovalDecision is a sealed interface, so approve / reject / modify is exhaustive at compile time. → HITL guide
  • Conversation and self-correction inside the loop — "respact" asks a clarifying question mid-task and resumes on the same session; "reflact" recovers from a failed tool call without discarding what the run already accomplished.
  • Private per-agent data — AgentInstanceContext holds API keys or tenant ids that both prompt shaping and tool execution can read, and that never reach the LLM.
  • Per-agent cost accounting — unit prices and a costBudget cap are part of the LLM profile, not an afterthought.
  • Multimodal input — attach a PDF or an image and the model reads it natively; a provider that can't handle the type fails loudly before the request goes out.
  • A full execution trace, always — AgentResponse.steps() records the reasoning and tool trace on every run, including the partial trace when a run fails.
  • LLM failover and circuit breaking — an ordered model chain with a passive breaker per candidate, so an outage stops costing a timeout per request.

What you can build

  • Single agents on any LLM (OpenAI, Anthropic, Ollama, Mistral, LM Studio, Groq, …)
  • Deterministic I/O contracts: sanitize input, validate output, strip markdown fences — zero tokens consumed
  • Multi-agent pipelines with conditional routing and FSM-style state machines
  • Parallel specialist crews: a fan-out step runs several agents at once on virtual threads and merges their answers with a pluggable strategy
  • Classify-and-act triage: one classification decides the single worker that handles the task, escalating from keyword rules to a model to a human as confidence drops — the whole dispatch table loadable as a JSON document
  • Tool calling from LLM responses, including parallel dispatch on virtual threads
  • Conversational agents that ask clarifying questions mid-task ("respact") and self-correcting ones that recover from failed tool calls without restarting ("reflact")
  • RAG as a strategy decorator — retrieval before every LLM call, no tool configuration needed
  • Fully offline testing with ScriptedLlmClient and AssociativeLlmClient

Core concepts in 4 snippets

Tool calling — implement AraTool, opt in per agent
class WeatherTool implements AraTool {
    @Override public String toolId()      { return "get_weather"; }
    @Override public String description() { return "Returns current weather for a city."; }
    @Override public String argumentSchema() {
        return """
               {"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}
               """;
    }
    @Override public ToolResult execute(String argumentJson) {
        return ToolResult.success(toolId(), "Milan: Sunny, 22°C");
    }
}

AgentConfig config = AgentConfig.defaults()
        .agentType("travel-assistant")
        .plannerStrategy("react")
        .enabledTools(List.of("get_weather"))
        .build();

When the LLM requests several tools in one response, ARA dispatches them concurrently on virtual threads — no configuration. → Tool calling in depth

Deterministic I/O contracts — validate and transform without spending tokens
AgentContract contract = AgentContract.builder()
        .addInputProcessor(InputSanitizer.instance())      // blocks prompt-injection patterns
        .addInputProcessor(ContentTruncator.to(4000))
        .addPromptShaper(PromptTemplate.withDefaults(Map.of("date", LocalDate.now().toString())))
        .outputSchema(JsonSchemaValidator.forOutput(SCHEMA))
        .addOutputProcessor(MarkdownFenceStripper.instance())
        .build();

AraAgent agent = runtime.createAgent(config, contract);

Validators, extractors, PII redaction and media limits ship built in. → Contracts & processors

Multi-agent pipeline — an FSM over agents
AgentPipeline pipeline = AgentPipeline.fsmBuilder()
        .state("draft",  draftAgent)
        .state("review", reviewAgent)
        .state("revise", reviseAgent)
        .state("done",   doneAgent)
        .initial("draft")
        .terminal("done")
        .transition("draft",  "review")
        .transition("review", execution -> execution.lastOutput().contains("APPROVED") ? "done" : "revise")
        .transition("revise", "review")
        .maxSteps(12)
        .build();

PipelineAgents.of(pipeline) hosts it inside a real agent, so it gains session isolation, cancellation and telemetry — and can be nested inside another pipeline. → Pipeline README

Classify-and-act — triage with confidence-driven escalation
IntentRouter router = IntentRouter.onField("intent")
        .route("BILLING", "billing")
        .route("TECH",    "tech")
        .confidenceField("confidence")
        .escalateBelow(0.7, "human")       // low confidence → escalate, not guess
        .orElse("fallback");

AgentPipeline triage = AgentPipeline.builder()
        .classify("classify", classifierAgent, router)
        .worker("billing", billingAgent)
        .worker("tech",    techAgent)
        .worker("human",   humanReviewAgent)
        .worker("fallback", fallbackAgent)
        .build();

Three interchangeable classifiers fill the same slot — keyword rules (zero tokens), a prompted model, or a human behind an ApprovalGate — and the whole dispatch table can be loaded from JSON. → Classify-and-act


Execution strategies

StrategyplannerStrategyWhat it does
ReactStrategy"react"Reasoning–Action loop: Think → Act → Observe. The default
ReSpActStrategy"respact"ReAct + a speak action: converse with the user mid-task without closing it
ReflActStrategy"reflact"ReAct + in-loop self-correction on tool failures or stalled reasoning
PlanExecuteStrategy"plan_execute"Generate a structured plan, then execute each step
ReflexionStrategy"reflexion"Generate → critique → revise, restarting the whole episode
RetrievalAugmentedStrategy"rag+<name>"Inject retrieved context before every LLM call

All strategies support cooperative cancellation and record a full execution trace, including on failure paths. → Strategy README


Modules

ModuleWhat's inside
ara-corePure interfaces and domain model: AraAgent, LlmClient, LlmException, MemoryManager, ToolRegistry, AgentContract, ExecutionStrategy, …
ara-runtimeAraRuntime, the execution strategies, ContractEnforcer, AgentPipeline, the classify-and-act building blocks, the offline LLM stubs and the built-in processors
ara-adaptersLangChain4j-backed LlmClient adapters for OpenAI, Anthropic, Ollama, Mistral and ChatJimmy. No Kotlin, no OkHttp, no Spring
ara-examplesRunnable examples for offline (stub) and live (real LLM) scenarios

ara-gateway — an optional HTTP layer (Javalin/Jetty) for HTTP-side HITL approvals — ships separately and is not part of this build.


Runnable examples

Everything below lives in ara-examples and runs with main().

ClassLLMWhat it shows
basics/MinimalAgentExamplestubThe smallest program: runtime + askText(...) — two lines
basics/AraSimpleExamplestubEnd-to-end: ReAct loop, tool call, interceptor, agent reuse
basics/SimpleStreamingExamplestub / liveThe smallest streaming agent, tokens printed as they arrive
basics/StreamingWithToolExamplestub / liveToken streaming through a ReAct loop that calls a tool
basics/InterceptorEventsExamplestubEvery AgentInterceptor event in order, around one run
pipeline/ClassifyAndActExamplenoneClassify-and-act at its smallest — no model, no API key
pipeline/TicketTriageCascadeExamplestubThe three-tier cascade: rules → model → human
crew/CodeReviewCrewExamplestub / liveThree specialist reviewers fan out on virtual threads; a lead agent merges their findings
hitl/HumanInTheLoopExamplestubA tool call parked on an ApprovalGate until an operator decides
memory/MemoryAgentExamplestubToken-budgeted working memory: summarise, offload, and recall
rag/RagAgentExamplestubrag+react over an InMemoryDocumentStore, plus delegation
failover/FailoverExamplestubFailover and circuit breaking across LLM and embedding endpoints
multimodal/MultimodalInputExampleliveA PDF to Mistral and an image to Ollama, one provider-agnostic method
scheduler/AgentSchedulerExamplenoneInterval and cron schedules, with pause / resume / trigger
web/StreamingChatWebExamplestub / liveA chat page on a JDK HttpServer, streaming over SSE

Documentation — read it in this order

The guides form a path, not a reference dump. Start at the top and stop when your use case is covered.

StepGuideWhat you'll be able to do
1Quick start + Runnable examplesRun your first agent offline, then read the example closest to your goal
2Tool callingGive an agent a tool, with parallel dispatch on virtual threads
3Contracts & processorsValidate, sanitise and transform I/O without spending tokens; PromptShaper, multimodal input
4Configuration referenceEvery AgentConfig and LlmProfile field: sessions, concurrency, cancellation, instance context, scheduling
5RAG & human-in-the-loopKnowledge bases (in-memory or Qdrant), retrieval as a strategy vs. a tool, approval gates and notifiers
6Providers & resilienceReal endpoints (OpenAI / Anthropic / Ollama / Mistral / compatible), failover, circuit breaker, I/O logging, OpenTelemetry
7Advanced usageCustom strategies and extension points
—Coding guidelinesWhat a PR is expected to look like

FAQ

Do I need Spring, or any DI container?

No. ARA is a plain library with a builder API. It runs in a bare main(), and drops into a Spring or Quarkus application as an ordinary dependency.

Can I run it without any API key?

Yes, two ways. ScriptedLlmClient and AssociativeLlmClient replay scripted responses for tests and demos, and the Ollama adapter talks to a local model with no key at all.

Which providers are supported?

OpenAI, Anthropic, Ollama, Mistral and ChatJimmy have first-class adapters, and the OpenAI client points at any OpenAI-compatible endpoint — LM Studio, Groq, Together AI, a corporate gateway. Provider coverage is inherited from LangChain4j.

Why Java 21 and not 17?

Virtual threads. Parallel tool dispatch, session concurrency and the cheap parking of a thread waiting on a human approval are all built on them.

How is this different from calling a chat API in a loop?

The loop is the easy part. Session isolation, cooperative cancellation, output schema validation, failover with circuit breaking, cost budgets, approval gates and a complete execution trace are the parts you would otherwise write, and they are what ARA is.


Contributing

Issues, discussions and PRs are welcome — including README and documentation fixes, which are the easiest first contribution.

Code style is enforced through docs/CODING-GUIDELINES.md: simplicity first, no premature abstractions, short single-responsibility functions, honest naming. Read it before opening a PR.

ARA is developed with heavy AI assistance under human architectural review.

If ARA is useful to you, a ⭐ helps other Java developers find it.


License

Apache 2.0 — see LICENSE.

Keywords — Java AI agent framework · LLM orchestration on the JVM · multi-agent systems in Java · ReAct agents · tool calling · RAG · human-in-the-loop · OpenAI · Anthropic Claude · Ollama · Mistral · Java 21 virtual threads