Skip to content

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 springai engine, or Koog 1.3 for the koog engine
  • 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.

pom.xml
<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>
pom.xml
<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:

pom.xml
<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>
build.gradle.kts
repositories {
    mavenCentral()
    maven("https://gitlab.com/api/v4/groups/5527027/-/packages/maven")
}

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:

application.yaml
aeon:
  assistant:
    engine: springai

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:

    ChatClient client = ChatClient.builder(chatModel)
            .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
            .build();
    SpringAiAgent agent = new SpringAiAgent("memory-agent", "Memory Agent", client);
    agent.setHasMemory(true);
    

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_ID and SpringAiAgent.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) before build(). 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 MultiLLMPromptExecutor bean. Choose its model with aeon.assistant.koog.title-model (default claude-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 ToolCallbackProvider bean. An agent given all ToolCallbackProvider beans, as in the example above, gets it automatically.
  • Koog: the tool is the AskUserQuestionTool bean. 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.

application.yaml
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.

application.yaml
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

  • system and developer messages sent by the client are dropped, unless aeon.assistant.allow-client-system-messages is enabled.
  • Attachment URLs must be http, https or base64 data: 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.

export ANTHROPIC_API_KEY=...
cd backend
mvn -pl demo -am spring-boot:run

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.