diff --git a/embabel-common-chat/pom.xml b/embabel-common-chat/pom.xml
new file mode 100644
index 0000000..30fa198
--- /dev/null
+++ b/embabel-common-chat/pom.xml
@@ -0,0 +1,29 @@
+
+
+ 4.0.0
+
+ com.embabel.common
+ embabel-common-parent
+ 0.1.10-SNAPSHOT
+
+ embabel-common-chat
+ jar
+ Embabel Common Chat
+ Common chat and conversation abstractions for storage-agnostic persistence
+ https://github.com/embabel/embabel-common
+
+
+ https://github.com/embabel/embabel-common
+ scm:git:https://github.com/embabel/embabel-common.git
+ scm:git:https://github.com/embabel/embabel-common.git
+ HEAD
+
+
+
+
+ com.embabel.common
+ embabel-common-core
+ ${project.version}
+
+
+
\ No newline at end of file
diff --git a/embabel-common-chat/src/main/kotlin/com/embabel/common/chat/ConversationFactory.kt b/embabel-common-chat/src/main/kotlin/com/embabel/common/chat/ConversationFactory.kt
new file mode 100644
index 0000000..67c9d62
--- /dev/null
+++ b/embabel-common-chat/src/main/kotlin/com/embabel/common/chat/ConversationFactory.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2024-2026 Embabel Pty Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.embabel.common.chat
+
+/**
+ * Type of conversation storage.
+ */
+enum class ConversationStoreType {
+
+ /**
+ * Conversations are stored in memory only.
+ * Fast and simple, suitable for testing and ephemeral sessions.
+ */
+ IN_MEMORY,
+
+ /**
+ * Conversations are persisted to a backing store.
+ * The specific store (e.g., Neo4j) is configured at factory level.
+ */
+ STORED
+}
+
+/**
+ * Factory for creating [StorableConversation] instances.
+ *
+ * Implementations provide different storage strategies (in-memory, persistent, etc.).
+ * Use [ConversationFactoryProvider] to obtain factories by type.
+ */
+interface ConversationFactory {
+
+ /**
+ * The storage type this factory provides.
+ */
+ val storeType: ConversationStoreType
+
+ /**
+ * Create a new conversation with the given ID.
+ *
+ * @param id unique identifier for the conversation
+ * @return a new StorableConversation instance
+ */
+ fun create(id: String): StorableConversation
+
+ /**
+ * Create a conversation for a 1-1 chat between a user and an agent.
+ *
+ * Messages can be automatically attributed based on role when participants are set.
+ *
+ * @param id the conversation/session ID
+ * @param user the human user participant
+ * @param agent the AI/system user participant (optional)
+ * @param title the session title (optional)
+ * @return a new StorableConversation instance
+ */
+ fun createForParticipants(
+ id: String,
+ user: MessageAuthor,
+ agent: MessageAuthor? = null,
+ title: String? = null
+ ): StorableConversation = create(id)
+}
diff --git a/embabel-common-chat/src/main/kotlin/com/embabel/common/chat/ConversationFactoryProvider.kt b/embabel-common-chat/src/main/kotlin/com/embabel/common/chat/ConversationFactoryProvider.kt
new file mode 100644
index 0000000..1665dd4
--- /dev/null
+++ b/embabel-common-chat/src/main/kotlin/com/embabel/common/chat/ConversationFactoryProvider.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2024-2026 Embabel Pty Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.embabel.common.chat
+
+/**
+ * Provider for [ConversationFactory] instances by type.
+ *
+ * Implementations resolve factories based on [ConversationStoreType],
+ * typically backed by Spring beans registered via autoconfiguration.
+ *
+ * To use conversation factories, either:
+ * - Inject [ConversationFactoryProvider] directly via Spring DI
+ * - Use the fluent API in agent frameworks: `context.ai().conversationFactory(type)`
+ */
+interface ConversationFactoryProvider {
+
+ /**
+ * Get a conversation factory for the given store type.
+ *
+ * @param type the conversation store type
+ * @return the factory for that type
+ * @throws IllegalArgumentException if no factory is registered for the type
+ */
+ fun getFactory(type: ConversationStoreType): ConversationFactory
+
+ /**
+ * Get a conversation factory for the given store type, or null if not available.
+ *
+ * @param type the conversation store type
+ * @return the factory for that type, or null
+ */
+ fun getFactoryOrNull(type: ConversationStoreType): ConversationFactory?
+
+ /**
+ * Get all registered factory types.
+ */
+ fun availableTypes(): Set
+}
+
+/**
+ * Simple map-based implementation of [ConversationFactoryProvider].
+ */
+class MapConversationFactoryProvider(
+ private val factories: Map
+) : ConversationFactoryProvider {
+
+ constructor(vararg factories: ConversationFactory) : this(
+ factories.associateBy { it.storeType }
+ )
+
+ constructor(factories: List) : this(
+ factories.associateBy { it.storeType }
+ )
+
+ override fun getFactory(type: ConversationStoreType): ConversationFactory {
+ return factories[type]
+ ?: throw IllegalArgumentException(
+ "No ConversationFactory registered for type $type. Available: ${factories.keys}"
+ )
+ }
+
+ override fun getFactoryOrNull(type: ConversationStoreType): ConversationFactory? {
+ return factories[type]
+ }
+
+ override fun availableTypes(): Set = factories.keys
+}
diff --git a/embabel-common-chat/src/main/kotlin/com/embabel/common/chat/MessageAuthor.kt b/embabel-common-chat/src/main/kotlin/com/embabel/common/chat/MessageAuthor.kt
new file mode 100644
index 0000000..e84166f
--- /dev/null
+++ b/embabel-common-chat/src/main/kotlin/com/embabel/common/chat/MessageAuthor.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2024-2026 Embabel Pty Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.embabel.common.chat
+
+/**
+ * Represents the author of a message in a conversation.
+ *
+ * This interface provides a persistence-agnostic abstraction for user identity
+ * in chat contexts. Implementations can extend this interface with additional
+ * persistence-specific annotations.
+ *
+ * ## Usage
+ *
+ * For simple cases, use the provided [SimpleMessageAuthor] data class:
+ * ```kotlin
+ * val author = SimpleMessageAuthor(id = "user-123", displayName = "Alice")
+ * conversation.addMessageFrom(message, author)
+ * ```
+ */
+interface MessageAuthor {
+
+ /**
+ * Unique identifier for the author.
+ */
+ val id: String
+
+ /**
+ * Human-readable display name for the author.
+ */
+ val displayName: String
+}
+
+/**
+ * Simple implementation of [MessageAuthor] for non-persistent use cases.
+ */
+data class SimpleMessageAuthor(
+ override val id: String,
+ override val displayName: String
+) : MessageAuthor
diff --git a/embabel-common-chat/src/main/kotlin/com/embabel/common/chat/MessageRole.kt b/embabel-common-chat/src/main/kotlin/com/embabel/common/chat/MessageRole.kt
new file mode 100644
index 0000000..c276ff1
--- /dev/null
+++ b/embabel-common-chat/src/main/kotlin/com/embabel/common/chat/MessageRole.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2024-2026 Embabel Pty Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.embabel.common.chat
+
+/**
+ * Role of a message sender in a conversation.
+ */
+enum class MessageRole {
+ USER,
+ ASSISTANT,
+ SYSTEM
+}
diff --git a/embabel-common-chat/src/main/kotlin/com/embabel/common/chat/StorableConversation.kt b/embabel-common-chat/src/main/kotlin/com/embabel/common/chat/StorableConversation.kt
new file mode 100644
index 0000000..1412f93
--- /dev/null
+++ b/embabel-common-chat/src/main/kotlin/com/embabel/common/chat/StorableConversation.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2024-2026 Embabel Pty Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.embabel.common.chat
+
+/**
+ * Minimal conversation interface for storage-agnostic persistence.
+ *
+ * This interface captures the essential properties needed to persist a conversation
+ * without coupling to agent-specific types like PromptContributor or AssetTracker.
+ *
+ * Implementations in agent frameworks (e.g., embabel-agent) can extend this
+ * interface with richer functionality while still being storable.
+ */
+interface StorableConversation {
+
+ /**
+ * Unique identifier for this conversation.
+ */
+ val id: String
+
+ /**
+ * Messages in the conversation in chronological order.
+ */
+ val messages: List
+
+ /**
+ * Whether this conversation is backed by persistent storage.
+ *
+ * Returns `true` for database-backed conversations, `false` for in-memory.
+ */
+ fun persistent(): Boolean = false
+
+ /**
+ * Add a message to the conversation.
+ *
+ * @param message the message to add
+ * @return the added message (may be wrapped or enhanced by the implementation)
+ */
+ fun addMessage(message: StorableMessage): StorableMessage
+
+ /**
+ * Add a message with explicit author attribution.
+ *
+ * @param message the message to add
+ * @param author the author of this message
+ * @return the added message
+ */
+ fun addMessageFrom(message: StorableMessage, author: MessageAuthor?): StorableMessage =
+ addMessage(message)
+
+ /**
+ * Add a message with explicit author and recipient.
+ *
+ * @param message the message to add
+ * @param from the author of this message
+ * @param to the recipient of this message
+ * @return the added message
+ */
+ fun addMessageFromTo(
+ message: StorableMessage,
+ from: MessageAuthor?,
+ to: MessageAuthor?
+ ): StorableMessage = addMessage(message)
+
+ /**
+ * Create a view of this conversation with only the last n messages.
+ *
+ * Implementations may optimize this (e.g., database query with LIMIT)
+ * or use the default in-memory slicing.
+ *
+ * @param n the number of messages to include
+ * @return a conversation view with the last n messages
+ */
+ fun last(n: Int): StorableConversation = SlicedStorableConversation(
+ id = id,
+ slicedMessages = messages.takeLast(n)
+ )
+}
+
+/**
+ * A read-only slice of a conversation with a subset of messages.
+ */
+private class SlicedStorableConversation(
+ override val id: String,
+ private val slicedMessages: List
+) : StorableConversation {
+
+ override val messages: List
+ get() = slicedMessages
+
+ override fun persistent(): Boolean = false
+
+ override fun addMessage(message: StorableMessage): StorableMessage {
+ throw UnsupportedOperationException("Cannot add messages to a conversation slice")
+ }
+
+ override fun last(n: Int): StorableConversation = SlicedStorableConversation(
+ id = id,
+ slicedMessages = slicedMessages.takeLast(n)
+ )
+}
diff --git a/embabel-common-chat/src/main/kotlin/com/embabel/common/chat/StorableMessage.kt b/embabel-common-chat/src/main/kotlin/com/embabel/common/chat/StorableMessage.kt
new file mode 100644
index 0000000..d594ce2
--- /dev/null
+++ b/embabel-common-chat/src/main/kotlin/com/embabel/common/chat/StorableMessage.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2024-2026 Embabel Pty Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.embabel.common.chat
+
+import java.time.Instant
+
+/**
+ * Minimal message interface for storage-agnostic persistence.
+ *
+ * This interface captures the essential properties needed to persist a message
+ * without coupling to agent-specific types like Awaitable or ActionContext.
+ *
+ * Implementations in agent frameworks (e.g., embabel-agent) can extend this
+ * interface with richer functionality while still being storable.
+ */
+interface StorableMessage {
+
+ /**
+ * The role of the message sender.
+ */
+ val role: MessageRole
+
+ /**
+ * The text content of the message.
+ */
+ val content: String
+
+ /**
+ * When the message was created.
+ */
+ val timestamp: Instant
+
+ /**
+ * Optional identifier of the message author.
+ * Useful for multi-user conversations.
+ */
+ val authorId: String?
+ get() = null
+
+ /**
+ * Optional identifier of the message recipient.
+ * Useful for directed messages in group chats.
+ */
+ val recipientId: String?
+ get() = null
+}
+
+/**
+ * Simple implementation of [StorableMessage] for basic use cases.
+ */
+data class SimpleStorableMessage(
+ override val role: MessageRole,
+ override val content: String,
+ override val timestamp: Instant = Instant.now(),
+ override val authorId: String? = null,
+ override val recipientId: String? = null
+) : StorableMessage
diff --git a/embabel-common-dependencies/pom.xml b/embabel-common-dependencies/pom.xml
index 6c26f70..0576268 100644
--- a/embabel-common-dependencies/pom.xml
+++ b/embabel-common-dependencies/pom.xml
@@ -42,6 +42,12 @@
${project.version}
+
+ com.embabel.common
+ embabel-common-chat
+ ${project.version}
+
+
diff --git a/pom.xml b/pom.xml
index 6d43888..b30f8f0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,6 +21,7 @@
embabel-common-dependencies
embabel-common-core
+ embabel-common-chat
embabel-common-util
embabel-common-textio
embabel-common-test