{"slug": "give-your-java-agents-a-memory-session-management-with-solon-ai", "title": "Give Your Java Agents a Memory - Session Management with Solon AI", "summary": "Solon AI v4.0.5 introduces session management abstractions that give Java-based AI agents persistent memory across turns, process restarts, and horizontal scaling. The framework's ChatSession and AgentSession interfaces model conversation state and execution snapshots, with pluggable backends including in-memory, file, and Redis storage. This enables multi-turn customer support agents to remember context without hand-rolled history tables.", "body_md": "Most LLM demos are amnesiacs. The user says \"my name is noear and I like blue\" in turn one, asks \"what's my name?\" in turn two, and the model shrugs - because every HTTP call to the chat API is stateless, and nobody fed the history back in. In production this is not a cosmetic issue: a support agent that forgets the ticket the customer opened 30 seconds ago is worse than no agent at all.\n\nSolon AI (v4.0.5) treats conversation state as a first-class, pluggable construct. In this article we build a multi-turn customer support agent whose memory survives process restarts and horizontal scaling, using only the framework's session abstractions - no hand-rolled history tables.\n\nThe naive fix is to append every message to a `List<ChatMessage>`\n\nin your own code and resend it with each request. That works until it doesn't:\n\nSolon AI answers each of these with a dedicated layer.\n\nAt the core sits `ChatSession`\n\n(`org.noear.solon.ai.chat`\n\n, since 3.1) - a deliberately small interface that models the conversation as an append-only message sequence:\n\n```\npublic interface ChatSession {\n    String getSessionId();\n\n    List<ChatMessage> getMessages();\n    List<ChatMessage> getLatestMessages(int windowSize);\n    void removeLatestMessage(int windowSize);\n\n    void addMessage(Collection<? extends ChatMessage> messages);\n    void addMessage(String userMessage);          // convenience: user role\n\n    boolean isEmpty();\n    void clear();\n\n    Map<String, Object> attrs();                  // transient, never persisted\n}\n```\n\nTwo details are worth calling out:\n\n`getLatestMessages(windowSize)`\n\n`attrs()`\n\nAgents need more than a transcript. `AgentSession`\n\n(since 3.8.1) extends `ChatSession`\n\nwith the state of the agent's execution flow:\n\n```\npublic interface AgentSession extends ChatSession {\n    void updateSnapshot();          // sync execution snapshot\n    FlowContext getContext();       // live flow context\n\n    void pending(boolean pending, String reason); // suspend / resume\n    boolean isPending();\n    String getPendingReason();\n}\n```\n\nThe `pending(...)`\n\nfamily is how human-in-the-loop agents park themselves mid-plan: suspend with a reason (\"waiting for expense approval\"), serialize the whole session, and resume the exact step when the human answers. Because the snapshot lives *inside* the session object, one storage backend covers both transcript and workflow state.\n\nEvery agent request carries its session explicitly:\n\n```\nChatModel chatModel = ...;\n\nSimpleAgent agent = SimpleAgent.of(chatModel)\n        .name(\"SupportAgent\")\n        .role(\"A customer support assistant\")\n        .instruction(\"Track the customer's issue across the whole conversation.\")\n        .sessionWindowSize(10)   // inject last 10 messages as history\n        .build();\n\nAgentSession session = InMemoryAgentSession.of(\"customer-8837\");\n\n// Turn 1\nagent.prompt(\"My order #5521 arrived broken, I want a replacement.\")\n     .session(session)\n     .call()\n     .getMessage();\n\n// Turn 2 - minutes later, same session: the agent already knows the order number\nString answer = agent.prompt(\"It was the blue ceramic mug, by the way.\")\n     .session(session)\n     .call()\n     .getContent();\n```\n\nWhat the framework does per call (from the `SimpleAgent`\n\nsource):\n\n`session.getLatestMessages(config.getSessionWindowSize())`\n\nas history (default window: 5).`__sessionId`\n\ninto both the prompt attributes and the tool context - so custom tools you write can correlate database writes with the conversation.`session.addMessage(...)`\n\nand calls `updateSnapshot()`\n\n- your code never mutates history manually.Sessions are an interface, and three backends ship in the box:\n\n| Backend | Messages | Snapshot | Use case |\n|---|---|---|---|\n`InMemoryAgentSession` |\nJVM heap | JVM heap | tests, single-node demos |\n`FileAgentSession` |\nNDJSON append log | JSON file | single instance, zero infra |\n`RedisAgentSession` |\nRedis list (`<id>:messages` ) |\nRedis key (`<id>:snapshot` ) |\nproduction, multi-instance |\n\nThe `FileAgentSession`\n\nbehaves like a proper write-ahead log. The official test suite demonstrates the property that matters most - restart recovery:\n\n```\nFileAgentSession session = new FileAgentSession(sessionId, tempDir);\nsession.addMessage(ChatMessage.ofUser(\"hello\"),\n                   ChatMessage.ofAssistant(\"hi, how can I help?\"));\nsession.getContext().put(\"user_name\", \"noear\");\nsession.updateSnapshot();\n\n// simulate a process restart: new instance, same directory\nFileAgentSession recovered = new FileAgentSession(sessionId, tempDir);\n\nrecovered.getMessages().size();                 // 2 - transcript survived\nrecovered.getContext().get(\"user_name\");        // \"noear\" - snapshot survived\n```\n\nNote the subtlety verified by the same tests: **system messages are filtered out of the NDJSON log**. Persisted history contains only the real conversation (user/assistant/tool), so reloading never stacks stale system prompts.\n\n`RedisAgentSession`\n\nadds an in-memory cache layer with per-session locking, so hot conversations don't pay a network round trip per message, while the canonical state lives in Redis - which is what you want when the support team's traffic lands on a load balancer and turn two may hit a different pod than turn one.\n\nThe last piece is `AgentSessionProvider`\n\n- a one-method factory the framework uses to resolve sessions by business ID:\n\n```\n@Bean\npublic AgentSessionProvider redisSession(RedisClient redisClient) {\n    Map<String, AgentSession> map = new ConcurrentHashMap<>();\n    return sessionId -> map.computeIfAbsent(\n            sessionId, k -> new RedisAgentSession(k, redisClient));\n}\n```\n\nThe contract is lazy loading: return the existing session if there is one (keeping context continuous), create one on demand otherwise. Inject it wherever agents are used:\n\n```\n@Inject AgentSessionProvider sessionProvider;\n@Inject ReActAgent supportAgent;\n\npublic String reply(String customerId, String message) throws Throwable {\n    AgentSession session = sessionProvider.getSession(\"customer-\" + customerId);\n    return supportAgent.prompt(message)\n            .session(session)\n            .call()\n            .getContent();\n}\n```\n\nBecause the session ID is your business key (\"customer-8837\"), memory becomes addressable: the same customer chatting from the app and from email can be routed to the same session, and your data retention tooling can age sessions out by exactly the keys it already knows.\n\nA common misconception is that windowing means deleting history. In Solon AI the two are separate concerns:\n\n`getLatestMessages(n)`\n\n- what the model `getMessages()`\n\n- what your system So a support platform can show the customer their full transcript in the UI, feed the agent only the last 10 messages for cost, and keep everything in NDJSON or Redis for the retention policy - one session object, three views.\n\nStatelessness is the LLM's constraint, not yours. Solon AI's session layer turns \"agent with memory\" from a hand-rolled liability (growing lists, lost state on deploy, no audit trail) into a configuration decision: pick a backend, set a window, inject a provider. The transcript, the workflow snapshot and the human-in-the-loop suspension point all ride in the same persistent object - and swapping the demo `InMemoryAgentSession`\n\nfor the production `RedisAgentSession`\n\nis a one-line change.\n\nIf you want to dig into the agent framework, chat models, RAG or tool calling, the docs live at [solon.noear.org](https://solon.noear.org).", "url": "https://wpnews.pro/news/give-your-java-agents-a-memory-session-management-with-solon-ai", "canonical_source": "https://dev.to/solonjava/give-your-java-agents-a-memory-session-management-with-solon-ai-2nja", "published_at": "2026-08-20 15:17:07+00:00", "updated_at": "2026-08-20 15:45:05.776478+00:00", "lang": "en", "topics": ["artificial-intelligence", "ai-agents", "developer-tools", "large-language-models"], "entities": ["Solon AI", "ChatSession", "AgentSession", "SimpleAgent", "InMemoryAgentSession", "FileAgentSession", "RedisAgentSession"], "alternates": {"html": "https://wpnews.pro/news/give-your-java-agents-a-memory-session-management-with-solon-ai", "markdown": "https://wpnews.pro/news/give-your-java-agents-a-memory-session-management-with-solon-ai.md", "text": "https://wpnews.pro/news/give-your-java-agents-a-memory-session-management-with-solon-ai.txt", "jsonld": "https://wpnews.pro/news/give-your-java-agents-a-memory-session-management-with-solon-ai.jsonld"}}