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 persistent Boolean 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 InMemoryConversation for minimal overhead
  • EventPublishingConversation wrapper 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:

  1. Acquisition — AgentProcessChatbot obtains or creates an InMemoryConversation via InMemoryConversationFactory
  2. Mutation — addMessage appends to the thread-safe list; events fire if wrapped in EventPublishingConversation
  3. Enrichment — Stateful tools write derived data to the blackboard (when --state enabled)
  4. Continuation — ConversationStatus evaluation determines whether further turns are expected
  5. Persistence — persistent=true conversations serialize through ConversationStore; others are discarded

Summary

  • Primary storage: InMemoryConversation with CopyOnWriteArrayList for thread safety
  • Observability: EventPublishingConversation decorator emits domain events
  • Factory pattern: InMemoryConversationFactory centralizes instantiation with optional event wiring
  • Flow control: ConversationStatus hierarchy (ConversationContinues/ConversationOver) manages session lifecycle
  • Transient state: Blackboard map enables agent working memory with --state flag
  • Planner integration: WorldState abstracts 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →