How embabel-agent Manages State in Conversations: A Deep Dive into Its Thread-Safe Architecture
embel-agent manages conversation state through a layered architecture centered on the InMemoryConversation class, which uses a CopyOnWriteArrayList for thread-safe message storage and supports optional persistence, event publishing, and transient agent data via a blackboard.
The embabel-agent framework treats every chat session as a structured Conversation object that maintains chronological message history and optional metadata. This design enables concurrent access, extensible event handling, and flexible persistence strategies. The core implementation resides in embel-agent-agent-api, with clear separation between storage, observability, and agent-specific state.
Core Conversation Storage: InMemoryConversation
The foundation of state management is InMemoryConversation, located at [embel-agent-agent-api/src/main/kotlin/com/embel/chat/support/InMemoryConversation.kt](https://github.com/embel/embel-agent/blob/main/embel-agent-agent-api/src/main/kotlin/com/embel/chat/support/InMemoryConversation.kt).
This class provides:
- Thread-safe message storage via
CopyOnWriteArrayList<Message> - Sliced views through
last(n)for context window management - Persistence control via a
persistentBoolean flag - Asset tracking integration for file attachments or tool outputs
// Create a persistent conversation factory and instance
val factory = InMemoryConversationFactory()
val conv = factory.create(id = "session-42", persistent = true)
// Append user messages chronologically
conv.addMessage(UserMessage("What is the weather today?"))
// Retrieve recent context (e.g., for token-limited LLM calls)
val recent = conv.last(2)
The persistent flag determines lifecycle: when true, downstream ConversationStore implementations serialize the conversation; otherwise, it exists only for the current request.
Event Publishing and Observability
Conversation modifications can emit domain events through EventPublishingConversation, found at [embel-agent-agent-api/src/main/kotlin/com/embel/chat/support/EventPublishingConversation.kt](https://github.com/embel/embel-agent/blob/main/embel-agent-agent-api/src/main/kotlin/com/embel/chat/support/EventPublishingConversation.kt).
This decorator pattern wraps any Conversation implementation:
val publisher = RecordingEventPublisher()
val publishingConv = EventPublishingConversation(conv, publisher)
// All addMessage calls now trigger events for logging, metrics, or UI updates
publishingConv.addMessage(AssistantMessage("Here is the forecast..."))
Events enable loose coupling between state changes and cross-cutting concerns like audit logging or reactive interfaces.
Conversation Factory Pattern
InMemoryConversationFactory centralizes instantiation logic at [embel-agent-agent-api/src/main/kotlin/com/embel/chat/support/InMemoryConversationFactory.kt](https://github.com/embel/embel-agent/blob/main/embel-agent-agent-api/src/main/kotlin/com/embel/chat/support/InMemoryConversationFactory.kt).
The factory conditionally wires event publishing based on constructor arguments:
- Plain
InMemoryConversationfor minimal overhead EventPublishingConversationwrapper when an event publisher is provided
This indirection allows runtime configuration without changing consumer code.
Conversation Status and Flow Control
Agent decisions about session continuation rely on ConversationStatus, defined in [embel-agent-agent-api/src/main/kotlin/com/embel/chat/agent/ConversationStatus.kt](https://github.com/embel/embel-agent/blob/main/embel-agent-agent-api/src/main/kotlin/com/embel/chat/agent/ConversationStatus.kt).
Two concrete subclasses control flow:
| Status | Meaning | Use Case |
|---|---|---|
ConversationContinues |
Session remains active | Agent expects further user input |
ConversationOver |
Session terminated | Goal achieved or user ended chat |
val status: ConversationStatus = if (plan.requiresMoreInfo) {
ConversationContinues(AssistantMessage("What city are you asking about?"))
} else {
ConversationOver("Forecast delivered successfully")
}
Transient Agent State: The Blackboard
Beyond message history, agents need ephemeral working memory. The Blackboard provides this through a transient map attached to conversations when the --state flag is active.
AgentProcessChatbot (see [embel-agent-agent-api/src/main/kotlin/com/embel/chat/agent/AgentProcessChatbot.kt](https://github.com/embel/embel-agent/blob/main/embel-agent-agent-api/src/main/kotlin/com/embel/chat/agent/AgentProcessChatbot.kt)) activates this via:
@ShellOption("-s", "--state") enableState: Boolean
When enabled, agents can:
- Write derived data (plan results, tool outputs, partial computations)
- Read previous turn's state across request boundaries
- Avoid polluting the persistent message log with implementation details
Planner World State Representation
GOAP and utility-based planners operate on logical conditions extracted from conversation history. The WorldState and ConditionWorldState classes in [embel-agent-agent-api/src/main/kotlin/com/embel/plan/goap/WorldState.kt](https://github.com/embel/embel-agent/blob/main/embel-agent-agent-api/src/main/kotlin/com/embel/plan/goap/WorldState.kt) encode:
- Current beliefs about user goals
- Satisfiability of preconditions for plan actions
- Changes between planning iterations
This layer translates conversational state into a form suitable for automated planning without exposing raw message content to planner logic.
Complete State Management Flow
The embel-agent architecture processes conversation state through five sequential stages:
- Acquisition —
AgentProcessChatbotobtains or creates anInMemoryConversationviaInMemoryConversationFactory - Mutation —
addMessageappends to the thread-safe list; events fire if wrapped inEventPublishingConversation - Enrichment — Stateful tools write derived data to the blackboard (when
--stateenabled) - Continuation —
ConversationStatusevaluation determines whether further turns are expected - Persistence —
persistent=trueconversations serialize throughConversationStore; others are discarded
Summary
- Primary storage:
InMemoryConversationwithCopyOnWriteArrayListfor thread safety - Observability:
EventPublishingConversationdecorator emits domain events - Factory pattern:
InMemoryConversationFactorycentralizes instantiation with optional event wiring - Flow control:
ConversationStatushierarchy (ConversationContinues/ConversationOver) manages session lifecycle - Transient state: Blackboard map enables agent working memory with
--stateflag - Planner integration:
WorldStateabstracts conversation history into logical conditions
Frequently Asked Questions
How does embel-agent ensure thread safety for concurrent conversations?
embel-agent uses CopyOnWriteArrayList in InMemoryConversation for message storage. This collection creates a fresh copy on each modification, eliminating lock contention for read-heavy workloads typical in chat applications.
What is the difference between persistent and non-persistent conversations?
A persistent=true conversation is serialized via ConversationStore implementations for cross-request retrieval; persistent=false exists only in memory and is garbage-collected after the response. The flag is set at creation through InMemoryConversationFactory.
How can agents store temporary data without affecting message history?
Agents use the Blackboard, a transient map attached to the conversation when AgentProcessChatbot receives the --state flag. This stores plan results, tool outputs, and other derived data without persisting to the message log.
Where does planner state fit into conversation management?
Planners use WorldState and ConditionWorldState classes to represent logical conditions derived from messages. This abstraction layer enables GOAP/utility planning without direct coupling to Conversation implementations.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →