Architecture Guide¶
Multi-agent coordination for the JVM. Autonomous agents that negotiate, delegate, and execute — with pluggable LLM reasoning when you need it.
This document describes Agenor's architecture: an interface-first, Java 21+ multi-agent framework with native support for MCP, A2A, Guardrails, and Human-in-the-Loop.
- Audience: developers evaluating or building on Agenor
- Scope: high-level structure, core abstractions, runtime behavior, and extension points
1. Architectural Overview¶
Agenor embraces an interface‑first, modular architecture. Core contracts live in agenor-core, while minimal, ready‑to‑use implementations live in agenor-runtime. Per ADR-027, LLM-aware pieces, extended behaviors/persistence, and classpath scanning were split out of agenor-runtime into agenor-runtime-llm, agenor-runtime-ext, and agenor-runtime-scanning respectively — each depends only on agenor-runtime, so a pure multi-agent-system consumer can depend on agenor-core + agenor-runtime alone. Adapters (LLM providers, A2A) live in agenor-adapters.
| Module | Holds | You name |
|---|---|---|
| agenor-core | every contract, as an interface or a record | Agent, Message, MessageDispatcher, directory.AgentDirectory |
| agenor-runtime | the in-memory implementation of those contracts | AgenorRuntime, BaseAgent |
| agenor-runtime-llm | LLM-aware pieces (ADR-027) | LLMAgent |
| agenor-runtime-ext | extended pieces: store, HITL, composites (ADR-027) | InMemoryStore, FSMBehavior |
| agenor-runtime-scanning | classpath scanning, isolated for native-image (ADR-027) | — reached through scanPackages |
| agenor-adapters | LLM providers, MCP, A2A | LLMProviderFactory |
The right-hand column is the point: most of the framework is reached through something else, and a type you never name is not a concept you had to learn.
Design goals: - Start simple, scale smart (ADR-004) - Interface‑first contracts (ADR-002) - Modern Java 21, virtual threads (ADR-001) - JSON record-based messages (ADR-005) - Annotation-based configuration (ADR-006)
2. Modules¶
- agenor-bom: Bill of Materials for dependency version management across the framework.
- agenor-core: Pure interfaces, records, annotations, and exceptions. No heavy dependencies.
- agenor-runtime: Minimal, production‑ready in‑memory implementations to get started fast.
- agenor-runtime-llm (ADR-027): LLM-aware runtime pieces —
LLMAgent, LLM memory, guardrails, reflection. Depends on agenor-runtime. - agenor-runtime-ext (ADR-027): Extended runtime pieces —
InMemoryStore, filters, persistence, composite behaviors, HITL, knowledge. Depends on agenor-runtime. - agenor-runtime-scanning (ADR-027): Classpath scanning and DI-based agent discovery, isolated for GraalVM native-image friendliness. Depends on agenor-runtime.
- agenor-adapters: LLM providers and A2A adapter.
- agenor-adapters-persistence (ADR-022): JDBC-backed agent directory and persistent HITL approval queue.
- agenor-spring-boot-starter: Spring Boot 4.0.x auto-configuration for Agenor.
- agenor-examples: Demonstrates usage patterns and best practices.
- agenor-tools: Web console (Jetty) and CLI (PicoCLI) utilities.
3. Core Abstractions (agenor-core)¶
Five contracts carry the framework. Everything else in agenor-core supports one of them, and
is reached through it rather than named directly.
- Agent: Lifecycle contract for autonomous entities; exposes id, status, and context.
- Behavior: Unit of work owned by an Agent. Its type says when the work runs —
ONE_SHOT,CYCLIC,FSM— andBehaviorSchedulerdrives it. - Message: Transport-agnostic payload record (topic, headers, content, metadata).
- MessageDispatcher (since 0.20.0): Topic publish/subscribe and direct agent-to-agent
messaging. Split into
TopicPublisher,TopicSubscriber,DirectMessengerandDirectReceiverso a distributed backend can implement one capability at a time;FilterableSubscriberis a separate capability for predicate-based subscriptions. - directory.AgentDirectory (since 0.20.0): Registration, resolution, discovery and presence, split the same way and for the same reason.
Annotations: @Agent, @Behavior, @AgenorMessageHandler, @Persist, @PersistenceConfig,
@DialogueHandler.
These are deliberately small to keep adapters swappable without breaking user code.
4. Runtime Implementations (agenor-runtime, agenor-runtime-llm, agenor-runtime-ext)¶
Agent Base Classes¶
- BaseAgent (
agenor-runtime): Convenience base class wiring message handling, behavior registration, services injection, and lifecycle hooks (onStart()/onStop()). - LLMAgent (
agenor-runtime-llm): ExtendsBaseAgentwith conversation history management, context window budgeting, and long-term fact storage. Requires aLLMMemoryManagerto be injected before start.
Behaviors¶
A behavior's type answers when its work runs: ONE_SHOT, CYCLIC, and FSM for a state machine
that decides its own transitions. SequentialBehavior and ParallelBehavior compose children and
tell the scheduler how to drive them through SchedulingHint rather than through their type —
which is why that enum exists. See Behaviors, which also lists the concerns
that are deliberately not behavior types.
Messaging¶
- InMemoryMessageDispatcher (since 0.20.0): Production implementation of
MessageDispatcherandFilterableSubscriber. Delivers messages using virtual threads. RoutessendTocalls viaAgentResolver; throwsAgentNotFoundExceptionfor unknown agents. Emitsmessage.sendOTel spans. See Messaging.
Agent Directory and Scheduler¶
- InMemoryAgentDirectory (since 0.20.0): Implements
dev.agenor.core.directory.AgentDirectory(all four capability interfaces). AssignsAgentEndpoint.local(nodeId)to newly registered agents automatically. Emitsdirectory.resolveOTel spans. See Agent Directory. - SimpleBehaviorScheduler: drives every scheduled behavior, honouring the initial delay a behavior declares before its first execution.
- AgentScanner + AgentFactory (
agenor-runtime-scanning, optional): classpath scanning and DI-based construction forcreateAgent(Class)/scanPackage(...). - AgentAnnotationProcessor (
agenor-runtime) + ExtBehaviorAnnotationExtension (agenor-runtime-ext, optional): wire@Behavior/@AgenorMessageHandleron any registered agent, independent of classpath scanning. Runs unconditionally; behavior types implemented inagenor-runtime-extfail loudly if that module is absent. - AgenorRuntime: Entry point to bootstrap, start, and stop the agent system.
Memory¶
- InMemoryStore (
agenor-runtime-ext): Thread-safeMemoryStoreimplementation backed byConcurrentHashMap. StoresMemoryEntryobjects with topic, scope (SHORT_TERM/LONG_TERM), content, and optional TTL. Does not persist to disk. - DefaultLLMMemoryManager (
agenor-runtime-llm): Bridges aMemoryStoreand the LLM conversation history. Three context window strategies:FixedWindowkeeps the N most recent messages,SlidingWindowkeeps messages within a rolling token budget (the default),Summarizationauto-summarizes older messages with an LLM call. See Memory Management.
Filters (agenor-runtime-ext)¶
Filters select which messages a subscription receives — build them from MessageFilter in
agenor-core and pass them to FilterableSubscriber.subscribeFiltered. See
Message Filtering.
Dialogue¶
Package dev.agenor.runtime.dialogue:
- DialogueCapability: composable capability that adds full dialogue support to any
BaseAgent. Providesrequest(),query(),callForProposals(),reply(),agree(),refuse(),inform(),failure(),propose(). - DefaultConversation: tracks a single conversation's state and message history.
- DefaultConversationManager: manages all active conversations for an agent; implements the request/response, query, and Contract-Net flows.
- DialogueHandlerRegistry: scans an agent for
@DialogueHandlerannotations and dispatches incomingDialogueMessageobjects to the correct handler byPerformative.
Lifecycle¶
Package dev.agenor.runtime.lifecycle:
- LifecycleManager: manages agent status transitions with timeout support (
startAgent,stopAgent); notifies registeredLifecycleListenerimplementations. - LifecycleListener: functional interface receiving
(agentId, oldStatus, newStatus). - LoggingLifecycleListener: built-in listener that logs every status change at INFO level via SLF4J.
5. Adapters (agenor-adapters)¶
The agenor-adapters module provides concrete implementations of core interfaces that integrate external services.
LLM Providers¶
All three providers implement LLMProvider from agenor-core:
- OpenAIProvider: OpenAI REST API (GPT-4, GPT-3.5, etc.). Supports streaming and function calling.
- AnthropicProvider: Anthropic API (Claude 3 Opus, Sonnet, Haiku). Supports streaming.
- OllamaProvider: Local Ollama server. Supports any model available on the local instance.
LLMProviderFactory is the recommended entry point. It creates the correct provider from a name string and API key, avoiding direct dependency on implementation classes:
LLMProvider openAI = LLMProviderFactory.create("openai", System.getenv("OPENAI_API_KEY"));
LLMProvider anthropic = LLMProviderFactory.create("anthropic", System.getenv("ANTHROPIC_API_KEY"));
LLMProvider ollama = LLMProviderFactory.create("ollama", null); // no key needed
ToolConversionUtils: converts Agenor FunctionDefinition objects to the vendor-specific JSON schemas required by each provider.
A2A Adapter¶
Implements the Agent-to-Agent (A2A) protocol:
- AgenorA2AAdapter: exposes a Agenor agent as an A2A server, built from
A2AAdapterConfig. - AgenorA2AClient: sends A2A messages to remote agents.
- AgenorAgentExecutor: handles incoming A2A tasks and routes them to a local agent.
For the full A2A guide see docs/dialog-protocol.md.
Redis Messaging Adapter (since 0.21.0)¶
Implements TopicPublisher, TopicSubscriber, and MessageTransport on top of Redis Streams,
providing at-least-once delivery and fan-out pub/sub across JVM nodes. Requires lettuce-core
on the classpath per ADR-018 (opt-in). Activated via agenor.messaging.provider=redis in Spring Boot,
or directly via RedisMessagingFactory.
Key classes in dev.agenor.adapters.messaging.redis:
- RedisMessagingFactory: builder; manages the shared Lettuce connection and lifecycle.
- RedisTopicPublisher: implements
TopicPublisher+TopicSubscriber; fan-out via per-subscription consumer groups. - RedisMessageTransport: implements
MessageTransport; point-to-point via node-scoped streams. - ConsumerLoop: virtual-thread blocking
XREADGROUPloop with DLQ aftermaxDeliveryAttempts.
For the full guide see docs/adapters/redis.md.
JDBC Persistence Adapter (agenor-adapters-persistence)¶
Implements dev.agenor.core.directory.AgentDirectory and the HITL ApprovalService on top of a
relational database via plain JDBC, for deployments that need a durable, queryable agent registry
and approval queue (ADR-022).
- JdbcAgentDirectory: persistent implementation of
AgentDirectory(ADR-023). - JdbcApprovalGate: persistent HITL approval queue (ADR-024).
For the full guides see JDBC Directory Adapter and Persistent HITL.
Extension Points¶
All core contracts are interfaces. Custom implementations can be plugged in without changing agent code:
MessageDispatcher→ Redis Streams, Kafka, JMS, or any custom transportdev.agenor.core.directory.AgentDirectory→ JDBC, Consul, etcd, or any registryBehaviorScheduler→ Quartz, cron, or any schedulerMemoryStore→ any SQL or NoSQL backend
Community adapters are welcome. See CONTRIBUTING.md.
6. Concurrency Model¶
- Agenor targets Java 21 virtual threads (Project Loom) for lightweight concurrency.
- Behaviors are executed in virtual threads by the scheduler when appropriate.
- Blocking operations in behaviors do not monopolize platform threads, simplifying the programming model.
- Message handlers should remain responsive; long‑running work can be delegated to behaviors or separate virtual threads.
7. Messaging Flow¶
- An Agent publishes a
MessageviaMessageDispatcher.publish(msg)(topic broadcast, routing onmsg.topic()) orsendTo(msg)(point-to-point, routing onmsg.receiverId()). - For point-to-point, the dispatcher calls
AgentResolver.resolveEndpoint(agentId)to obtain the targetAgentEndpoint. - Agents subscribe via
subscribeTopic(topic, handler)orsubscribeRecipient(agentId, handler), both returning aSubscription. @AgenorMessageHandler(topic)annotations are also supported; the runtime registers the handler automatically.- Optional predicate filtering is available via
FilterableSubscriber.subscribeFiltered(filter, handler)(in-memory only). - The in-memory implementation delivers messages on virtual threads within the JVM; custom backends (Redis, Kafka) plug in without changing agent code.
See Messaging Guide for the complete API reference.
8. Discovery & Lifecycle¶
- AgentDirectory registers agents at startup and maintains status (STARTING, RUNNING, STOPPED, etc.).
- Agents may query other agents via AgentDirectory using AgentQuery.
- AgenorRuntime orchestrates:
- scanning configured base packages
- constructing agents via AgentFactory
- registering agents in the directory
- scheduling declared behaviors
- wiring message handlers to the MessageDispatcher
9. Configuration¶
- Minimal configuration via code (builder) and/or YAML. Example keys:
- agenor.runtime.name
- agenor.agents.auto-discovery
- agenor.agents.base-package
- agenor.messaging.provider (in-memory)
- agenor.directory.provider (local)
Implementations are selected by configuration while code depends only on core interfaces.
10. Extensibility Points¶
To integrate enterprise technologies, implement core contracts:
- MessageDispatcher (or individual TopicPublisher / DirectMessenger capabilities): swap transport (Redis Streams, Kafka, JMS)
- dev.agenor.core.directory.AgentDirectory (or individual AgentRegistry / AgentDiscovery / AgentResolver / AgentPresence capabilities): swap discovery (JDBC, Consul, etcd)
- BehaviorScheduler: advanced scheduling (Quartz, cron, priority queues)
- LLMProvider: add new model providers (implement the interface, register with factory)
Guidelines: - Keep adapters dependency‑isolated within agenor-adapters submodules. - Avoid leaking implementation types into user code; rely on core interfaces.
11. Error Handling & Observability¶
- Exceptions derive from AgenorException hierarchy (AgentException, LLMException).
- Logging via SLF4J with pluggable backend (logback in tests/examples).
- Planned: metrics for behavior execution, message throughput, and directory health.
12. Guardrails Pipeline¶
Added in 0.13.0 (ADR-014). The Guardrails Layer intercepts content at two hook points
inside every LLMAgent subclass:
User input
→ InputGuardrailChain applyInputGuardrails(input, ctx)
→ LLMProvider.chat()
→ OutputGuardrailChain applyOutputGuardrails(output, ctx)
→ Consumer
Core types (agenor-core / dev.agenor.core.guardrail)¶
| Type | Role |
|---|---|
GuardrailResult |
Sealed interface: Passed \| Modified(newContent) \| Blocked(reason) |
InputGuardrail |
@FunctionalInterface — validates/transforms user input |
OutputGuardrail |
@FunctionalInterface — validates/transforms LLM output |
GuardrailContext |
Immutable record: agentId, topic, metadata |
GuardrailViolationException |
Unchecked, extends AgenorException |
@WithGuardrails |
Annotation for declarative chain wiring |
Implementations (agenor-runtime-llm / dev.agenor.runtime.guardrail, split out of agenor-runtime per ADR-027)¶
| Class | Type |
|---|---|
GuardrailChain |
Fluent builder + sequential execution with short-circuit |
PiiRedactionGuardrail |
Input + Output |
ContentPolicyGuardrail |
Input + Output (YAML blocklist) |
MaxTokensInputGuardrail |
Input (3 truncation strategies) |
GuardrailAnnotationProcessor |
Reads @WithGuardrails, injects chain at registration |
Wiring¶
AgenorRuntime.registerAgent() calls GuardrailAnnotationProcessor.process(agent) for every
LLMAgent instance. The processor reads @WithGuardrails, instantiates the listed guardrail
classes via their no-arg constructors, and injects the resulting GuardrailChain. Programmatic
chains set before registration are merged (annotation chain prepended).
Design decisions (ADR-014)¶
GuardrailResultas a sealed interface (Java 21) enforces exhaustiveswitchat compile time, making silent mishandling ofBlockedimpossible.LLMAgentexposesapplyInputGuardrails/applyOutputGuardrailsasprotectedhooks; subclasses call them around their ownllmProvider.chat()invocation.GuardrailViolationExceptionis unchecked, consistent with theAgenorExceptionhierarchy.
See docs/guardrails.md for the full developer guide.
13. Human-in-the-Loop Checkpoint¶
Suspends agent behavior execution pending human approval.
Agent → HumanCheckpointBehavior → ApprovalGate (virtual thread parks)
→ ApprovalNotifier (fire-and-forget)
← ApprovalService.submit() (from external system)
→ resumes with ApprovalDecision (Approved | Rejected | Modified)
Core types (agenor-core / dev.agenor.core.hitl): ApprovalRequest, ApprovalDecision (sealed), ApprovalGate, ApprovalNotifier, ApprovalTimeoutException, @RequiresApproval
Implementations (agenor-runtime-ext / dev.agenor.runtime.hitl, split out of agenor-runtime per ADR-027): InMemoryApprovalGate, ApprovalService, HumanCheckpointBehavior, HitlAnnotationProcessor
Access via: runtime.getApprovalService() See Human-In-The-Loop guide for the full developer guide.
14. Evolution Path¶
- MVP: in‑memory runtime for simple single‑JVM systems.
- See
CONTRIBUTING.mdfor how to build and share custom adapters.
See the ADRs in docs/adr/ (repository only) for rationale and decisions.
15. Example Bootstrapping¶
public class Main {
public static void main(String[] args) {
var runtime = AgenorRuntime.builder()
.scanPackage("dev.agenor.examples")
.build();
runtime.start();
}
}
Agents are discovered, registered, and their behaviors scheduled automatically.
16. Glossary¶
- Agent: Autonomous unit of computation and coordination.
- Behavior: Unit of work owned by an Agent; its type says when the work runs.
- Message: Topic‑addressed payload exchanged between agents.
- Mailbox: The single inbound path an agent's messages arrive on (ADR-032).
- Directory: Registry that enables discovery and status tracking of agents.
- Scheduler: Component responsible for driving behaviors.
- DialogueCapability: Composable component adding structured conversation support to an agent.