# How embabel-agent Manages State in Conversations: A Deep Dive into Its Thread-Safe Architecture

> Discover how embabel-agent manages conversation state with its thread-safe architecture. Explore its layered approach, message storage, and persistence options.

- Repository: [Embabel/embabel-agent](https://github.com/embabel/embabel-agent)
- Tags: deep-dive
- Published: 2026-08-14

---

**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/embabel/embabel-agent/blob/main/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

```kotlin
// 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/embabel/embabel-agent/blob/main/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:

```kotlin
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/embabel/embabel-agent/blob/main/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/embabel/embabel-agent/blob/main/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 |

```kotlin
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/embabel/embabel-agent/blob/main/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:

```kotlin
@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/embabel/embabel-agent/blob/main/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.