Skip to main content
Version: Next

Conversation Threads

Agent Kernel supports persistent, named conversation threads: every chat exchange is recorded against its session_id and becomes readable over REST, so UIs can show a user's conversation list and full history across restarts and devices.

Overview

Key Design Decisions

  • A thread is keyed by session_id, with no separate thread id. The thread is auto-created on a session's first chat request; every later request with the same session_id appends to it.
  • user_id becomes required on every chat request once threads are enabled; requests without it are rejected with 400.
  • Pluggable storage: in-memory, Redis, DynamoDB, Firestore, or Cosmos DB.
  • Optional, pluggable authorization: you supply an Authoriser that validates a Bearer token against your authentication provider; Agent Kernel never authenticates users itself.
  • Streaming included: with execution.mode: stream, the user message is recorded before the stream starts and the assistant message is assembled from the streamed deltas on completion.
Platform scope

Do not enable Conversation Thread Support for agents deployed on platforms with native thread management (Slack, Microsoft Teams). Those platforms own the conversation history; AK threads alongside them would create duplicate, divergent state.

Enabling Thread Support

Add a thread block to config.yaml; its presence turns the feature on:

thread:
type: memory # memory | redis | dynamodb | firestore | cosmosdb

Chat Request Fields

FieldRequiredAppliedDescription
session_idyesevery requestIdentifies the thread
user_idyes (once threads are enabled)at creationOwning user; also enables user-scoped listing
group_idnoat creation onlyCaller-defined group/project scope for listing
thread_namenoany requestSets (at creation) or renames (afterwards) the display name and locks it against automatic naming; blank values are ignored. When absent at creation, the name comes from the naming strategy (see below)
curl -X POST http://localhost:8000/api/v1/chat \
-H "Content-Type: application/json" \
-d '{"prompt": "What is the capital of France?", "session_id": "ses-1", "user_id": "alice", "thread_name": "Capitals quiz"}'

Thread Naming

When a thread is created without a thread_name, its display name is generated by the active ThreadNamingStrategy. The built-in default makes a single LiteLLM call at thread creation (model from thread.naming.model, default gpt-4o-mini; API keys are read from the environment) asking for a concise title of at most thread.naming.max_length characters. Gibberish or meaningless first prompts get a generic title ("New conversation") instead of becoming the name. The call happens once per thread, inline on the session's first chat request (~0.5–2s), never on later messages.

Naming never fails thread creation: if litellm is not installed (it is an optional dependency; install it with the thread extra, pip install "agentkernel[thread]"), no API key is present, or the model call errors, the name falls back to a truncated prompt prefix: the first max_length characters, trimmed at a word boundary and suffixed with an ellipsis. The fallback is never silent: a missing litellm is warned about once at startup with the install hint, and every failed naming call logs a warning.

thread:
type: memory
naming:
model: gpt-4o-mini # LiteLLM model used to generate thread names
max_length: 80 # max auto-generated name length

Override the strategy by subclassing and registering your subclass once at startup. Override build_instruction to keep the built-in LLM call with your own prompt, or override generate_name for any other logic (optionally calling self._complete(instruction) to reuse the LLM call machinery):

from agentkernel.core.thread import ConversationThreadManager, ThreadNamingStrategy


class MyNaming(ThreadNamingStrategy):
def build_instruction(self, prompt: str) -> str:
return f"Reply with a three-word title for a conversation starting with: {prompt}"


ConversationThreadManager.set_naming_strategy(MyNaming())

Threads whose name was explicitly supplied (a thread_name on any chat request) are marked name_locked: true in their metadata and are never renamed automatically. No naming call is made for them.

Reading Threads

Two read endpoints are mounted automatically when threads are enabled:

# List threads (metadata only), filtered by user and/or group
curl "http://localhost:8000/api/v1/threads?user_id=alice"

# Get one thread with its message history
curl "http://localhost:8000/api/v1/threads/ses-1"

Both endpoints paginate: pass limit (default 50, max 200) and the opaque cursor returned as next_cursor in the previous page (null on the last page).

Renaming Threads

There is no dedicated rename endpoint. Send thread_name on any later chat request for the same session_id to rename its thread. Only the name changes (every other thread field is fixed at creation), the thread is marked name_locked: true, and blank names are ignored. Resending the same thread_name on every request is cheap: the name is only written when it actually changes.

curl -X POST http://localhost:8000/api/v1/chat \
-H "Content-Type: application/json" \
-d '{"prompt": "And of Italy?", "session_id": "ses-1", "user_id": "alice", "thread_name": "European capitals"}'

Authorization

Thread routes are open until you supply an Authoriser, a small base class you subclass to validate the Bearer token against your own authentication provider and resolve the caller's user_id:

from typing import Optional
from agentkernel.api import RESTAPI, AgentRESTRequestHandler, ThreadRESTRequestHandler
from agentkernel.core.thread import Authoriser


class MyAuthoriser(Authoriser):
def authorise(self, token: str) -> Optional[str]:
# Validate the token with your auth provider (JWT, introspection, API key lookup, ...)
# Return the resolved user_id, or None to reject.
return my_auth_provider.resolve(token)


RESTAPI.run(handlers=[AgentRESTRequestHandler(), ThreadRESTRequestHandler(authoriser=MyAuthoriser())])

With an Authoriser configured:

  • Requests must carry Authorization: Bearer <token>; missing/malformed headers and rejected tokens get 401.
  • Listings are always scoped to the authorised user.
  • Reading another user's thread returns 403. (Renaming flows through the chat request and rides its trust model: whoever can chat on a session_id can rename its thread.)
Open until configured

Without an Authoriser, any caller who knows a session_id can read its thread. Deploy behind network-level access controls until one is configured.

Storage Backends

# Redis
thread:
type: redis
redis:
url: "redis://localhost:6379"
prefix: "ak:thread:"
ttl: 2592000 # seconds; 0 disables expiry

# DynamoDB - table needs partition key `session_id` (S) and sort key `sk` (S)
thread:
type: dynamodb
dynamodb:
table_name: "ak-agent-threads"
ttl: 2592000 # item TTL in seconds; 0 disables

# Firestore
thread:
type: firestore
firestore:
collection_name: "ak-agent-threads"
project_id: "my-gcp-project" # optional, inferred from ADC when omitted
database_id: "(default)" # optional
ttl: 2592000 # seconds; 0 disables

# Cosmos DB (Table API, partitioned by session_id, no TTL support)
thread:
type: cosmosdb
cosmosdb:
connection_string: "..."
table_name: "akagentthreads"

Attachments in Thread Mode

Attachment support is still decided by multimodal.enabled; the thread block alone is text-only:

  • thread only: requests carrying images/files are rejected with 400.
  • thread + multimodal.enabled: true: attachment bytes are saved to the multimodal attachment store and each thread message keeps only an attachment_id reference. Use a shared attachment store (in_memory, redis, or dynamodb); storage_type: session_cache is rejected in thread mode.
multimodal:
enabled: true
storage_type: in_memory

thread:
type: memory

Examples

Ready to Ship Your
First Agent?

Free, open-source, Apache 2.0. No licensing costs, no vendor lock-in. Join hundreds of developers building production AI agents with Agent Kernel.

Agent Kernel
Ask Agent Kernel