Agent Runtime Architecture · Apache 2.0

Agentic architecture in plain Java. No magic.

ARA is a Java 21 runtime for autonomous agents and multi-agent systems. Zero annotations. Zero reflection. No Kotlin runtime, no Spring. The call stack you debug is the call stack you wrote.

4 modules 6 execution strategies 0 annotations 0 reflection 0 tokens for I/O contracts
HelloAgent.java runs offline — no API key
try (AraRuntime runtime = AraRuntime.builder()
        .llmClient(ScriptedLlmClient.script()
                .thenFinalAnswer("Virtual threads are lightweight JVM threads.")
                .build())
        .build()) {

    AraAgent agent = runtime.createAgent(AgentConfig.defaults()
            .agentType("assistant")
            .systemPrompt("You are a concise technical assistant.")
            .build());

    AgentResponse response = agent.execute(AgentTask.of("Explain virtual threads"));

    System.out.println(response.content());  // deterministic, in CI, for free
}

Why ARA

Java already runs your business logic.
It should run your agents too.

Most JVM agent stacks are integration toolkits: they give you a client and leave orchestration, determinism and testing to you. ARA is the other half — a runtime, built on top of LangChain4j providers rather than against them.

Deterministic I/O contracts

AgentContract runs a processor chain before and after every call — sanitise input, strip markdown fences, validate JSON against a schema, redact PII. Pure Java, zero tokens. No other JVM framework ships an equivalent.

Virtual threads, not an option

When the model asks for five tools in one response, ARA dispatches all five concurrently on Java 21 virtual threads. No executor to wire, no pool to tune, no flag to flip.

Four execution strategies

ReAct, ReSpAct (converse mid-task), ReflAct (self-correct inside the loop), PlanExecute, Reflexion, plus a RAG decorator — "rag+react". Swap by changing one string in AgentConfig.

Tested without an LLM

ScriptedLlmClient replays scripted turns — tool calls included — so an entire multi-agent flow is a plain JUnit test. Deterministic CI, no API key, no bill.

Secrets the model never sees

AgentInstanceContext holds API keys and tenant ids that prompt shaping and tool execution can read — never in the prompt text, never in a tool's argument schema. Updatable live, without recreating the agent.

The code tour

Four things people build first

Every snippet below is copy-paste from the README. Interfaces you can read in one sitting, no starter template required.

Multi-provider runtime
// One runtime, many providers — agents reference them by name.
AraRuntime runtime = AraRuntime.builder()
        .llmClient("fast",  AraLlmClientFactory.openAi().apiKey(KEY).modelName("gpt-4o-mini").build())
        .llmClient("smart", AraLlmClientFactory.openAi().apiKey(KEY).modelName("gpt-4o").build())
        .llmClient("local", AraLlmClientFactory.ollama().modelName("gpt-oss-20b").build())
        .build();

AgentConfig config = AgentConfig.defaults()
        .agentType("analyst")
        .primaryLlm(LlmProfile.of("smart"))   // zero credentials inside AgentConfig
        .build();

Providers come from ara-adapters (OpenAI · Anthropic · Ollama · any OpenAI-compatible endpoint), built on LangChain4j — no Kotlin, no OkHttp, no Spring.

Execution strategies

The agent loop is a strategy, not a rewrite

One string in AgentConfig.plannerStrategy(...) changes how the agent thinks. All of them support cooperative cancellation and record a full execution trace — including the partial trace on failure.

StrategyValueWhat it does
ReactStrategy"react"Think → Act → Observe. The default.
ReSpActStrategy"respact"ReAct plus a speak action — ask a clarifying question mid-task and resume on the same session instead of restarting.
ReflActStrategy"reflact"ReAct plus in-loop self-correction: a failed tool call injects a course-correction into the same working memory, keeping everything already accomplished.
PlanExecuteStrategy"plan_execute"Produce a structured plan, then execute it step by step.
ReflexionStrategy"reflexion"Generate → critique → revise, restarting the episode with the critique injected.
RetrievalAugmented"rag+<name>"Decorator: inject retrieved context before every LLM call — "rag+react", "rag+respact", "rag+plan_execute", "rag+reflact".

Two flavours of self-critique — and they compose

"reflexion" reacts to a whole failed pass: wipe working memory, retry the episode. "reflact" reacts inside one pass and the loop simply continues. Use reflact when individual steps fail recoverably, reflexion when failure is only detectable after a complete pass.

Human-in-the-loop is a runtime primitive

AgentState.WAITING, an approval gate and pluggable notifiers. ApprovalDecision is a sealed interface — handling approve / reject / modify exhaustively is enforced by the compiler, not by convention.

ara-private · not public

Beyond the open-source runtime

ARA Gateway and ARA Voice — the same agent, other channels

The runtime above is the core — public, and enough on its own. On top of it, two private modules expose the same AraAgent to channels other than a direct Java call — already in production, but outside the public repository.

ara-gateway — one runtime, many channels

Exposes an AraRuntime over HTTP with synchronous, background and streaming (SSE) runs, plus an opt-in AgentOS surface: an unmodified Agno RemoteAgent — or the os.agno.com control plane — can drive an ARA agent as if it were a native Agno one. A runtime with no gateway around it opens no port: ara-runtime never gains an HTTP dependency, the gateway stays an optional layer on top of it.

ara-voice — real-time voice channel

A separate module that brings the same agent onto a phone call: WebRTC signaling, streaming speech recognition and speech synthesis with barge-in, so the user can interrupt the agent while it's still talking. It lives outside the public repository and is not distributed.

Example — Agno as client and as server

The same integration, in both directions

1. Agno as client — driving an ARA agent

Expose the AGENTOS surface: an unmodified agno.agent.RemoteAgent treats it like any other AgentOS instance.

Gateway.java
AraRuntime runtime = AraRuntime.builder().llmClient(llm).build();
runtime.start();

AraAgent agent = runtime.createAgent(AgentConfig.defaults()
        .agentId(AgentId.of("research-agent"))
        .agentType("research")
        .name("Research Agent")
        .build());

AraGateway gateway = AraGateway.builder(runtime)
        .port(8090)
        .expose(Surface.AGENTOS)          // Agno-compatible wire format
        .build();
gateway.start();
// -> http://localhost:8090/agentos
client.py — Agno side
from agno.agent import RemoteAgent

remote = RemoteAgent(
    base_url="http://localhost:8090/agentos",
    agent_id="research-agent",
    protocol="agentos",
)

response = remote.run("Summarize the latest ARA release notes")
print(response.content)

2. Agno as server — ARA delegates to an Agno agent

No dedicated Agno client needed: any AraTool that speaks the same wire format, in the opposite direction, is enough to let the ARA agent delegate to a specialist hosted on Agno.

AgnoAgentTool.java
class AgnoAgentTool implements AraTool {
    private final HttpClient http = HttpClient.newHttpClient();
    private final String agnoBaseUrl;   // e.g. an Agno AgentOS instance
    private final String agnoAgentId;

    @Override public String toolId()      { return "ask_agno_specialist"; }
    @Override public String description() { return "Delegates a question to a specialist hosted on Agno."; }
    @Override public String argumentSchema() {
        return """
               {"type":"object","properties":{"question":{"type":"string"}},"required":["question"]}
               """;
    }

    @Override
    public ToolResult execute(String argumentJson) {
        String question = JsonFieldExtractor.field(argumentJson, "question");
        String body = "message=" + URLEncoder.encode(question, UTF_8) + "&stream=false";
        HttpRequest req = HttpRequest.newBuilder(
                        URI.create(agnoBaseUrl + "/agents/" + agnoAgentId + "/runs"))
                .header("Content-Type", "application/x-www-form-urlencoded")
                .POST(HttpRequest.BodyPublishers.ofString(body))
                .build();
        HttpResponse resp = http.send(req, HttpResponse.BodyHandlers.ofString());
        return ToolResult.success(toolId(), resp.body());
    }
}

// Register it like any other tool — the ARA agent can now call Agno.
AgentConfig config = AgentConfig.defaults()
        .agentType("coordinator")
        .enabledTools(List.of("ask_agno_specialist"))
        .build();

All gateway surfaces

Eight independent surfaces, each toggled on its own

Every route is always registered — whether it answers or returns 404 surface_disabled is decided by configuration, per request. No surface is on by accident.

SurfaceMain endpointsAuthTypical client
SYSTEMGET /health, /infononemonitoring, load balancer
DISCOVERYGET /agents, /agents/{id}, /agents/type/{type}, /agents/state/{state}nonedashboard, service discovery
RUNSPOST /agents/{id}/runs (sync, ?background=true, ?stream=true), GET/DELETE /runs/{taskId}bearer/customyour own frontend, internal backend
CONTROLPOST .../terminate, session management, PUT .../configbearer/customadmin panel
SESSIONSGET .../history, .../state, .../memorybearer/customdebugging, audit, support
AGENTOSGET/POST under /agentosbearer/customAgno RemoteAgent, os.agno.com
A2AGET /.well-known/agent.json, POST /.well-known/a2a (JSON-RPC 2.0)bearer/custom (AgentCard excluded)AWS Bedrock AgentCore, Google ADK, LangGraph, ag2
APPROVALSGET /approvals, POST /approvals/{id}/decisionbearer/customhuman-in-the-loop panel

3. Native REST — synchronous and streaming

No compatibility wire format: plain JSON in, JSON or Server-Sent Events out — for any frontend or backend that speaks HTTP.

sync
curl -X POST http://localhost:8080/agents/research-agent/runs \
  -H "Authorization: Bearer $ARA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"input": "Summarize the latest ARA release notes"}'
streaming — event: token / tool_call / speak / final
curl -N -X POST "http://localhost:8080/agents/research-agent/runs?stream=true" \
  -H "Authorization: Bearer $ARA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"input": "Summarize the latest ARA release notes", "sessionId": "s-1"}'
# event: token       {"text":"ARA"}
# event: token       {"text":" is"}
# event: tool_call   {"toolId":"web_search"}
# event: final       {"content":"...", "tokensUsed":...}

4. Agent2Agent (A2A) — cross-vendor interoperability

JSON-RPC 2.0 over the official A2A protocol types — the same standard AWS Bedrock AgentCore, Google ADK, LangGraph and ag2 already speak, no ARA-specific code required on their side.

message/send
curl -X POST http://localhost:8080/.well-known/a2a \
  -H "Authorization: Bearer $ARA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "message/send",
    "params": {
      "message": {
        "parts": [{"kind": "text", "text": "Summarize the latest ARA release notes"}],
        "metadata": {"agentId": "research-agent"}
      }
    }
  }'

Current status

  • ara-gateway and ara-voice are part of ara-private, a separate, non-public repository.
  • They share the same ara-core interfaces: an agent written for the open-source runtime runs on both channels unchanged.
  • The Agno integration is ara-gateway-only — it exposes the AgentOS surface, no changes required on the Agno side.
  • Whether they'll ever become public hasn't been decided.

How ARA compares

Honest table. Including the rows we lose.

Compared against LangChain4j 1.17, Spring AI 2.0 and Koog (JetBrains) — last verified 2026-08-03. This ecosystem moves fast; open an issue if a row has gone stale.

ARALangChain4jSpring AIKoog
Primary focus Multi-agent runtime + orchestration LLM integration toolkit + agentic workflows LLM integration for Spring apps Multiplatform agent framework
Deterministic I/O contracts ✓ AgentContract, zero tokens ———
FSM pipeline with routing ✓ fsmBuilder() —— ✓ typed FSM graphs
Parallel tool dispatch ✓ automatic, virtual threads opt-in executeToolsConcurrently(Executor) sequential by default coroutines; per-stage ExecutorService
Annotations none none Spring annotations @Tool / @LLMDescription
Framework dependency none — pure Java interfaces none Spring Boot required Kotlin runtime; Spring optional
Agent execution loop 6 built-in strategies + RAG decorator workflows + supervisor (langchain4j-agentic) advisor chain + Spring AI Agents graph and functional strategies
Multi-agent graph with cycles ✓ parallel branches + back-edges composable workflows; no cyclic graph API not built in ✓ nodes, edges, subgraphs
Test without an LLM ✓ ScriptedLlmClient partialpartial ✓ MockLLMBuilder
Typed error handling LlmException · isRetryable() · ErrorType Retriable / NonRetriable hierarchy provider-specific built-in retries and fault tolerance
Java version 21+17+21+17+
Durable checkpointing / resume ✗ not implemented via LangGraph4j via LangGraph4j ✓ persistence + restore points

Where the alternatives win

  • Koog covers much of the same ground with a mature graph model, agent persistence and a richer mocking API — and it runs beyond the JVM.
  • Need durable checkpointing today? ARA doesn't have it yet. Koog or LangGraph4j is the more direct fit.
  • Already Spring Boot end to end? Spring AI will feel more native than anything here.
  • The wider JVM space is bigger than this table: LangGraph4j brings LangGraph-style StateGraph, Embabel takes the Goal-Oriented Action Planning route, and Google's ADK for Java targets hierarchical multi-agent systems around A2A.

Architecture

Four modules. Take only what you need.

ara-core is interfaces and domain model — nothing else. Everything above it is an implementation you can replace.

ara-core

Pure interfaces: AraAgent, LlmClient, MemoryManager, ToolRegistry, AgentContract, ExecutionStrategy.

ara-runtime

AraRuntime, the four strategies, ContractEnforcer, AgentPipeline, ScriptedLlmClient, built-in processors.

ara-adapters

LangChain4j-backed clients for OpenAI, Anthropic and Ollama. No Kotlin, no OkHttp, no Spring.

ara-examples

Runnable demos: offline stubs, live LLMs, and an autonomous coding loop with quality gates.

Quickstart

From clone to a running agent

Java 21+ and Maven 3.9+. The first agent needs no API key at all.

Build it

ARA is currently a 1.0.0 — install it into your local repository.

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

Add the runtime

ara-runtime pulls in ara-core transitively. Add ara-adapters when you want a real model.

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

Point it at a real model

OpenAI, Anthropic, Ollama, or any OpenAI-compatible endpoint — LM Studio, Groq, Together AI.

LlmClient claude = AraLlmClientFactory.anthropic()
        .apiKey(System.getenv("ANTHROPIC_API_KEY"))
        .model(AnthropicLlmClient.Models.CLAUDE_SONNET_4_6)
        .build();

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

Read the rest

The README is the full manual — sessions, cost budgets, telemetry, the knowledge base and every processor. Design decisions live as ADRs in docs/adr/.

If the JVM is where your systems live,
it's where your agents should live too.

ARA is Apache 2.0, built in the open, and developed with heavy AI assistance under human architectural review. Issues, ADR arguments and PRs all welcome.