Spring Integration¶
The backend is a set of Spring Boot libraries. Your application declares the agents it wants to serve as beans, and the libraries serve them over the REST API that the Angular library uses. They stream everything the agents do back to the browser as it happens.
Agents run on an engine. Two engines are available:
| Engine | Artifact | Built on | Agent bean type |
|---|---|---|---|
springai |
aeon-assistant-springai |
Spring AI | SpringAiAgent |
koog |
aeon-assistant-koog |
Koog | KoogAgent |
Requirements¶
- Java 25
- Spring Boot 4.1 (Spring MVC)
- Spring AI 2.0 for the
springaiengine, or Koog 1.3 for thekoogengine - Spring Security (optional, but required for multi-user access control)
Installation¶
All artifacts share the com.aeontronix.ai group id. Add the engine you run your agents on. It pulls in
aeon-assistant-core and aeon-assistant-api.
<dependency>
<groupId>com.aeontronix.ai</groupId>
<artifactId>aeon-assistant-springai</artifactId>
<version>1.0.0-beta1-SNAPSHOT</version>
</dependency>
<!-- Spring AI model starter of your choice, it provides the ChatModel -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-anthropic</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>com.aeontronix.ai</groupId>
<artifactId>aeon-assistant-koog</artifactId>
<version>1.0.0-beta1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>ai.koog</groupId>
<artifactId>koog-spring-boot-starter</artifactId>
<version>1.3.0-beta</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
The engines declare their frameworks (Spring AI, Koog, Spring MVC) as provided dependencies, so your application must
add them itself.
Everything is auto-configured. Adding the dependency to a @SpringBootApplication is enough for /assistant/** to be
served. There is nothing to enable.
Don't component-scan the library
Don't add the com.aeontronix.ai.assistant packages to your component scan. The auto-configuration registers
everything the library needs.
Alpha, beta and snapshot versions¶
Stable releases are published on Maven Central. Alpha and beta releases (for example 1.0.0-beta2), and the snapshots
built from the default branch (for example 1.0.0-beta2-SNAPSHOT), are only published to the package registry of the
aeontronix GitLab group. The registry is public, so you don't need credentials.
Add the registry as a repository, then use the alpha, beta or snapshot version in your dependencies:
<repositories>
<repository>
<id>aeontronix-gitlab</id>
<url>https://gitlab.com/api/v4/groups/5527027/-/packages/maven</url>
<releases>
<enabled>true</enabled>
</releases>
<!-- Only needed for -SNAPSHOT versions -->
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
5527027 is the id of the aeontronix group. To use the registry in all your projects instead of a single one, add
the repository to a profile in ~/.m2/settings.xml.
Snapshots
A -SNAPSHOT version changes with every build of the default branch. Maven checks for a newer one once a day by
default. Run mvn -U ... to force the update, and don't depend on snapshots in releases of your own application.
Choosing an engine¶
If one engine is on the classpath, it is used. If several are, select one with aeon.assistant.engine. Startup fails,
listing the available engines, if the property is missing or unknown:
Declaring agents¶
An agent is a bean of the engine's agent type, SpringAiAgent or KoogAgent. Both extend Agent. Every such bean is
served, and the first public agent is the default one.
The engines don't ship any chat agent. They only provide the private title-agent, which generates the conversation
titles.
Spring AI agent¶
A SpringAiAgent wraps a preconfigured ChatClient. The system prompt, tools, memory and options are all set on the
client, and the engine only passes it the messages of the request. The engine requires a ChatModel bean, usually
provided by a Spring AI model starter.
@Bean
public SpringAiAgent supportAgent(ChatModel chatModel,
ObjectProvider<ToolCallbackProvider> toolProviders,
ChatObserver observer) {
List<ToolCallback> tools = toolProviders.orderedStream()
.flatMap(provider -> Arrays.stream(provider.getToolCallbacks()))
.toList();
SpringAiAgent agent = new SpringAiAgent("support-agent", "Support Agent", ChatClient.builder(chatModel)
.defaultSystem("You help the user with their orders.")
.defaultToolCallbacks(SpringAiActivityToolCallback.wrap(tools, observer))
.build());
agent.setDescription("Answers questions about orders");
return agent;
}
- Tool calls. Wrap the tools with
SpringAiActivityToolCallback.wrap(...)so the client sees the tools the agent runs. Unwrapped tools still work but aren't reported. - Thinking and text. The engine adds an advisor to every run that reports the model's reasoning and intermediate text. You don't need to do anything.
-
Memory. Add Spring AI's memory advisor and mark the agent as having memory, so the client only sends the new message:
The conversation id of the memory is agentId:sessionId, so the turns of a session are kept together.
- Context. Each run passes the agent id and the session id to the tool context and advisor parameters, under
SpringAiAgent.AGENT_IDandSpringAiAgent.RUN_ID.
Koog agent¶
Build the agent with Koog's own builder and wrap it in a KoogAgent. The KoogAgent takes the name shown to the user,
and its id is the one given to the Koog agent:
@Bean
public KoogAgent supportAgent(MultiLLMPromptExecutor executor, ChatObserver observer) {
var builder = AIAgent.builder()
.id("support-agent")
.promptExecutor(executor)
.llmModel(AnthropicModels.Opus_4_7)
.systemPrompt("You help the user with their orders.")
.functionalStrategy("support-agent", SupportAgent::strategy);
KoogChatObserver.install(builder, observer);
return new KoogAgent("Support Agent", builder.build());
}
- Call
KoogChatObserver.install(builder, observer)beforebuild(). It reports tool calls, thinking and text to the client. Without it the final answer still reaches the client, but nothing else does. - The session id is used as the Koog run id.
- The title agent runs on Anthropic and needs a
MultiLLMPromptExecutorbean. Choose its model withaeon.assistant.koog.title-model(defaultclaude-haiku-4-5). Without the executor, no titles are generated.
Agent properties¶
Agent carries what the user interface needs to present an agent and decide what to show of its runs:
| Property | Default | Effect |
|---|---|---|
id |
— | Identifies the agent in the API paths. |
name |
— | Shown as the author of the answers. |
description |
null |
Shown next to the name when the user picks an agent. |
icon |
null |
Reserved for the user interface. |
privateAgent |
false |
Hides the agent from GET /assistant/agents. Clients can't reach it. |
hasMemory |
false |
Tells the client the agent remembers the previous turns of the session. |
reportToolCalls |
true |
When false, the tool and text events of its runs are not streamed. |
reportThinking |
true |
When false, the reasoning of its runs is not streamed. |
hasMemory is only information for the client: it doesn't give the agent a memory. The report flags only control what
the client sees. The session history still records everything.
Asking the user questions¶
Agents can stop and ask the user questions with the ask_user_question tool. The client receives a question event,
shows a question card, and posts the answers back (see Answer a question). The
run waits until the user answers, the timeout expires, or the run is cancelled.
- Spring AI: the tool is registered as a
ToolCallbackProviderbean. An agent given allToolCallbackProviderbeans, as in the example above, gets it automatically. - Koog: the tool is the
AskUserQuestionToolbean. Add it to your agent's tool registry.
If the user doesn't answer in time (aeon.assistant.question.timeout, default 10m), the tool tells the agent to
continue without the answer and to state the assumption it makes.
Session history¶
Without a SessionManager bean, no history is kept. The chat works, but the session endpoints answer 404 and session
ownership isn't checked.
In-memory history¶
For development and demos, enable the built-in InMemorySessionManager. Its history is lost when the application
restarts.
aeon:
assistant:
sessions:
provider: memory
memory:
max-sessions: 1000 # most recently used sessions kept
max-events-per-session: 2000 # latest events kept per session
Custom storage¶
For persistent history, declare your own SessionManager bean. It replaces the in-memory one. Only
append(agentId, sessionId, event) is required. The other methods (reading history, listing sessions, owners, grants,
closing and deleting) have no-op defaults, so implement the ones you need. Implementations must be thread-safe. A
failing append is logged and doesn't break the chat.
Expiring closed sessions¶
Closed sessions can be deleted automatically after a time-to-live. Open sessions never expire.
aeon:
assistant:
sessions:
ttl: 86400000 # delete closed sessions after one day (ms), -1 = never
expiry-check-interval: 60000 # how often to check (ms)
Security¶
The library doesn't authenticate requests
Secure /assistant/** in your application, for example with a Spring Security filter chain. Restrict CORS to
your own front ends, and never allow every origin together with credentials.
When Spring Security is on the classpath, the library adds two layers of checks on HTTP requests.
Authorization¶
A ChatAuthorizationService decides whether a caller may use the API. The default implementation,
ChatAuthorizationServiceServletImpl, allows any authenticated, non-anonymous user. A denied unauthenticated caller
gets 401, and a denied authenticated caller gets 403.
To change the rules, declare your own ChatAuthorizationService bean. Only isAuthenticated(request) is required. The
other methods (canUseAgent, canStartSession, canMessage, canViewSession, canDeleteSession, canClearSessions,
canManageGrants and more) have defaults you can override.
Clearing all sessions
By default, any authenticated user can delete every session of an agent. Override canClearSessions to restrict
it, for example to administrators.
Session ownership¶
When both a SessionManager and a SessionOwnerResolver are present, every session belongs to its owner: the caller it
was first recorded for. Other callers get 403.
The default SessionOwnerResolverPrincipalNameImpl uses the name of the Spring Security principal. To identify owners
differently, for example by tenant or by a token claim, declare your own SessionOwnerResolver bean that returns a
SessionOwner(ownerType, ownerId).
The owner can share a session through grants: with one user (USER) or with every allowed user (ALL). See
Session grants.
| Action | Owner | Grantee |
|---|---|---|
| Send messages, view history, close, answer questions | Yes | Yes |
| Rename, delete, manage grants | Yes | No |
Listing sessions only returns the sessions the caller can access.
Without Spring Security
If Spring Security is not on the classpath, no authorization or ownership checks run. Anyone who knows a session
id can read or act on it. You can still declare your own ChatAuthorizationService and SessionOwnerResolver
beans.
Other safeguards¶
systemanddevelopermessages sent by the client are dropped, unlessaeon.assistant.allow-client-system-messagesis enabled.- Attachment URLs must be
http,httpsor base64data:URIs. - A question can only be answered in the session that asked it.
- Private agents can't be reached by clients.
- A session runs one request at a time, and a closed session rejects new messages.
Extension points¶
Every bean below is auto-configured and backs off when your application declares its own.
| Bean | Default | Purpose |
|---|---|---|
SessionManager |
none, or InMemorySessionManager |
Stores sessions, history, owners and grants. |
SessionOwnerResolver |
SessionOwnerResolverPrincipalNameImpl |
Identifies the owner of a session from the current request. Requires Spring Security. |
ChatAuthorizationService |
ChatAuthorizationServiceServletImpl |
Decides who may use the API. Requires Spring Security. |
ChatObserver |
built in | The single path every event takes to the history and to the client. |
UserQuestionService |
built in | Holds the questions agents are waiting on and hands them the answers. |
AgentService |
the engine | Runs the agents. Implement it to plug in your own engine, selected with aeon.assistant.engine. |
Agents can also report events themselves through the ChatObserver, for example with thinking(...),
toolCallStarted(...), toolCallCompleted(...) or message(...).
Configuration reference¶
| Property | Default | Description |
|---|---|---|
aeon.assistant.engine |
none | Engine to use when several are on the classpath: springai, koog, or a custom engine id. |
aeon.assistant.allow-client-system-messages |
false |
Keep the system and developer messages sent by the client. |
aeon.assistant.question.timeout |
10m |
How long an agent waits for the user to answer a question. |
aeon.assistant.sessions.provider |
none | memory enables the in-memory session history. |
aeon.assistant.sessions.memory.max-sessions |
1000 |
In-memory history: most recently used sessions kept. |
aeon.assistant.sessions.memory.max-events-per-session |
2000 |
In-memory history: latest events kept per session. |
aeon.assistant.sessions.ttl |
-1 |
Milliseconds after which closed sessions are deleted. -1 never deletes them. |
aeon.assistant.sessions.expiry-check-interval |
60000 |
Milliseconds between two checks for expired sessions. |
aeon.assistant.authorization.provider |
servlet |
servlet enables the default ChatAuthorizationService when Spring Security is present. |
aeon.assistant.koog.title-model |
claude-haiku-4-5 |
Anthropic model of the Koog title agent. |
Demo application¶
The ../../backend.tmp module is a small runnable application on the Spring AI engine with Anthropic. It declares two
agents, simple-agent and memory-agent (with memory), a few product catalog tools, and the in-memory history.
The API is served on http://localhost:8080/assistant and allows the Angular dev server on http://localhost:4200
through CORS. Set ANTHROPIC_MODEL to change the model.
Note
The demo lets every request through, without authentication. Don't use its security configuration in production.
The ../../backend.tmp file holds ready-to-run requests for every endpoint.