Conversation Management
Agents run in an environment where they are provisioned on-demand, can be recycled at any time, and multiple requests from the same conversation may land on different instances. Storing data solely in process memory leads to three unavoidable issues: chat history disappears as soon as the frontend refreshes; when a request lands on a different instance, the model loses context from the previous round, manifesting as "amnesia"; and in scenarios like approval workflows or human-in-the-loop tasks that require "pausing and resuming later," the state is lost after instance recycling.
context.store is a built-in session management capability of Makers Agent. It enables saving and reading conversation history with zero configuration and provides native adapters for the Claude, OpenAI, LangGraph, and DeepAgents Agent frameworks.Its core feature is cross-instance persistence: Data is stored in the platform's Blob storage and is not bound to a process. Data written during a request remains accessible. Even after the current request ends, the write instance is recycled, or a request lands on a different instance, a new request can still retrieve the data as long as it carries the same
conversation_id. You do not need to manage any external database or cache yourself.Cross-instance persistence is supported starting from EdgeOne CLI 1.6.26. Please upgrade the CLI to this version or later (npm i -g edgeone@latest).The underlying layer is built on the platform's Blob storage. The APIs for Node and Python are fully mirrored (JavaScriptcamelCase, Pythonsnake_case). The framework integration section focuses on TypeScript, with each section including an equivalent Python sample.
Using in Two Runtimes
In the code within both the
agents/ and cloud-functions/ directories, store points to the same data.Directory | Access Point | Typical Use |
agents/ | context.store | Primary LLM conversation path, message appending, and Checkpointer |
cloud-functions/ | context.agent.store | Conversation list API and message query |
Common Prerequisite: Conversation Identity
All cross-instance persistence uses
conversation_id as the key. You do not need to parse it yourself: the platform parses it from the request header makers-conversation-id, injects it into context.conversation_id, and echoes makers-conversation-id and makers-run-id in the response header. Simply use context.conversation_id directly. Do not construct another one from the request body. When used as a storage key, the conversation ID has a maximum length of 256 characters.Framework Integration
context.store provides both framework-native adapters and generic APIs.framework | Recommended Usage | Entry Point (TS) |
claude-sdk | Framework-native SessionStore | context.store.claudeSessionStore() |
openai-sdk | Framework-native Session | context.store.openaiSession(sessionId) |
langgraph | Framework-native Checkpointer + BaseStore | context.store.langgraphCheckpointer / .langgraphStore |
deepagents | Reuse LangGraph Checkpointer + BaseStore | context.store.langgraphCheckpointer / .langgraphStore |
crewai | Not supported. Use the generic API for self-management. | - |
Claude Agent SDK
claudeSessionStore() returns an implementation of the SessionStore protocol (append / load / listSessions / delete / listSubkeys).// typescriptconst sessionStore = context.store.claudeSessionStore()// pythonsession_store = context.store.claude_session_store()
The
sessionId for the Claude SDK must be a valid UUID. If your conversation_id is not guaranteed to be a UUID (for example, something like chat-2026-08 -12-abc), use claudeSessionBinding to map it to a stable UUID: if it is already a UUID, use it as is; if not, generate one and persist the mapping. This ensures the same conversation_id always maps back to the same sessionId.TS Example:
// agents/chat/index.tsconst conversationId = context.conversation_id ?? ''const sessionStore = context.store.claudeSessionStore()// Maps any business ID to a stable UUID sessionId (returns it as is if it's already a UUID; otherwise, generates one and persists it).const sessionId = await context.store.claudeSessionBinding(conversationId)// Checks if this sessionId has been persisted before: if yes, resumes the session; if not, treats it as a new session.// The `dir` parameter cannot be omitted. The blob key for SessionStore is derived from the project directory. If `dir` is missing, the data can never be retrieved.const info = await getSessionInfo(sessionId, { dir: process.cwd(), sessionStore })const options: Record<string, any> = { model, systemPrompt, sessionStore }if (info) {options.resume = sessionId // Resumes an existing session} else {options.sessionId = sessionId // Creates a new session}const q = query({ prompt: message, options })
Python example:
session_store = context.store.claude_session_store()session_id = await context.store.claude_session_binding(conversation_id)# Similarly, when calling `getSessionInfo`, determine whether to resume or create a new session. Then, include `session_store` and either `resume` or `session_id` in the options.
OpenAI Agents SDK
openaiSession(sessionId, { maxItems }) returns an implementation of the Session protocol from the OpenAI Agents SDK (getItems / addItems / popItem / clearSession).// openaiSession automatically resumes sessions based on the conversation id.// typescriptconst session = context.store.openaiSession(context.conversation_id)// pythonsession = context.store.openai_session(context.conversation_id)
Resuming from a breakpoint (approval flow / human-in-the-loop) requires the additional use of
store.state. The openaiSession above only persists conversation context; **it does not persist the RunState at the point of interruption**. Therefore, when a tool requires manual approval and the run() is interrupted midway, you must serialize the RunState and store it into context.store.state. In the next round, read it back, restore it, and then run() again:// agents/hitl/index.tsimport { RunState, run } from '@openai/agents'const RUN_STATE_KEY = 'openai.run-state'// — First round: Start the run. If it is interrupted, store the RunState into state. —const result = await run(agent, message, { signal })if (result.state.getInterruptions().length > 0) {await context.store.state.set(RUN_STATE_KEY, result.state.toString()) // Key: Store the RunStatereturn json({ status: 'awaiting_approval' })}await context.store.state.delete(RUN_STATE_KEY) // No interruption, run completed, clean up// — Next round: Bring in the approval/rejection, read back the RunState to restore it, and resume the run. —const stored = await context.store.state.get<string>(RUN_STATE_KEY)const state = await RunState.fromString(agent, stored) // Restore the interrupted stateconst [approval] = state.getInterruptions()body.approved ? state.approve(approval) : state.reject(approval)const resumed = await run(agent, state, { signal }) // Resume the run from the breakpoint
LangGraph
langgraphCheckpointer implements BaseCheckpointSaver (getTuple / list / put / putWrites). langgraphStore implements BaseStore (get / put / search / listNamespaces / batch). Semantic search is temporarily supported by the search method of BaseStore.// typescriptconst checkpointer = context.store.langgraphCheckpointerconst store = context.store.langgraphStore// pythoncheckpointer = context.store.langgraph_checkpointerstore = context.store.langgraph_store
DeepAgents
DeepAgents is built on LangGraph and directly reuses the two adapters:
langgraphCheckpointer + langgraphStore.// typescriptconst checkpointer = context.store.langgraphCheckpointerconst store = context.store.langgraphStore// pythoncheckpointer = context.store.langgraph_checkpointerstore = context.store.langgraph_store
CrewAI
CrewAI's built-in Memory has a strong dependency on a vector database (default: LanceDB + embedder). A vector layer has not yet been implemented for
context.store. You can manage conversation history using the generic API of context.store. Core capabilities such as multi-agent orchestration, task chaining, and tool invocation remain completely unaffected.To restore a session across instances, serialize the framework's session state into JSON and store it at the end of each round. At the start of the next round, read it back first to restore the state. As long as the state can be expressed as a JSON-serializable object, the same cross-instance multi-turn memory and checkpoint resumption can be achieved. A typical implementation is an approval flow (human-in-the-loop). When a tool invocation requires manual approval, store the runtime state into
state and return "Awaiting Approval". Next time, with the same conversation_id (approved or rejected), read back the state and resume the run from the checkpoint. Delete the state after the run completes. The serialized state always remains only on the server side.General APIs
Method Overview
All methods are attached tocontext.store. The Node side uses object destructuring for parameters (camelCase), while the Python side uses keyword arguments (snake_case). Their semantics are identical.
Node Method | Python Method | Description |
appendMessage | append_message | Appends a message; automatically creates a conversation if it does not exist. |
getMessages | get_messages | Retrieves a list of messages and supports cursor-based pagination. |
updateMessage | update_message | Overwrites a specified message. |
deleteMessage | delete_message | Deletes a single message. |
clearMessages | clear_messages | Clears messages but retains conversation metadata. |
getConversation | get_conversation | Retrieves conversation metadata. |
listConversations | list_conversations | Lists conversations in descending order of lastMessageAt. |
updateConversation | update_conversation | Updates conversation metadata with a shallow merge. |
deleteConversation | delete_conversation | Deletes the entire conversation (irrecoverable). |
toAnthropicMessages | to_anthropic_messages | Converts a list of messages to the Anthropic Messages format. |
toOpenAIInput | to_openai_input | Converts a list of messages to the OpenAI Chat Completions format. |
appendMessage / append_message
Append a message to the specified conversation. If the conversation does not exist, it is automatically created, and a user index is established as needed.
Parameter
Parameter | Type | Required | Description |
conversationId / conversation_id | string | Yes | Business-side conversation ID, length ≤ 256 bytes |
role | 'user' | 'assistant' | 'system' | 'tool' | Yes | Message role |
content | string | string[] | object | Yes | Message body, supporting plain text / string arrays / multimodal dict, with a serialized size ≤ 50 MB. |
metadata | Record<string, any> | No | Business-defined fields (such as number of tokens, tool_call, source tags, and so on) |
userId / user_id | string | No | Associated user. After being passed in, it is written to the user index to facilitate listing conversations by user. |
Return Value
The
messageId / message_id of the new message (in the format msg_xxx).TS Example:
const messageId = await context.store.appendMessage({conversationId: context.conversation_id,role: 'user',content: context.request.body.message,userId: context.request.body.userId,metadata: { source: 'web' },})
Python example:
message_id = await context.store.append_message(conversation_id=context.conversation_id,role="user",content=context.request.body["message"],user_id=context.request.body.get("user_id"),metadata={"source": "web"},)
getMessages / get_messages
Retrieve the message list for a specified conversation, supporting cursor-based pagination. If the conversation does not exist, no exception is thrown and an empty list is returned. By default, the list is sorted in chronological order (
order='asc', earliest first) to facilitate direct prompt assembly.Parameter
Parameter | Type | Required | Description |
conversationId / conversation_id | string | Yes | Conversation ID |
limit | number | No | Number of items per page, default 20, range [1, 100]. |
order | 'asc' | 'desc' | No | Sorting direction, default 'asc' (earliest first). |
after | string | No | A cursor that fetches messages after the specified messageId; mutually exclusive with before. |
before | string | No | A cursor that fetches messages before the specified messageId; mutually exclusive with after. |
Return Value
list[Message] A message array. An empty array [] is returned if the conversation does not exist.TS Example:
const messages = await context.store.getMessages({conversationId: context.conversation_id,limit: 50,})const reply = await openai.chat.completions.create({model: 'gpt-4o',messages: context.store.toOpenAIInput(messages),})
Python example:
messages = await context.store.get_messages(conversation_id=context.conversation_id,limit=50,)reply = await openai_client.chat.completions.create(model="gpt-4o",messages=context.store.to_openai_input(messages),)
updateMessage / update_message
Overwrite a message. Only the fields you provide are overwritten, while fields not provided retain their original values. The
updatedAt / updated_at timestamps are automatically refreshed.Parameter
Parameter | Type | Required | Description |
conversationId / conversation_id | string | Yes | Conversation ID |
messageId / message_id | string | Yes | Target message ID |
content | string | string[] | object | No | New content; retains the original value if not provided. |
metadata | Record<string, any> | No | Overwrites the metadata entirely (not merged); retains the original value if not provided. |
Return Value
Message — The complete, updated message object.TS Example:
const updated = await context.store.updateMessage({conversationId: context.conversation_id,messageId: 'msg_abc123',content: 'corrected answer',metadata: { edited: true },})
Python example:
updated = await context.store.update_message(conversation_id=context.conversation_id,message_id="msg_abc123",content="corrected answer",metadata={"edited": True},)
deleteMessage / delete_message
Delete a single message.
Parameter
Parameter | Type | Required | Description |
conversationId / conversation_id | string | Yes | Conversation ID |
messageId / message_id | string | Yes | Target message ID |
TS Example:
await context.store.deleteMessage({conversationId: context.conversation_id,messageId: 'msg_abc123',})
Python example:
await context.store.delete_message(conversation_id=context.conversation_id,message_id="msg_abc123",)
clearMessages / clear_messages
Clear all messages in the conversation, but retain the
ConversationMeta. To delete it completely, use deleteConversation.Parameter
Parameter | Type | Required | Description |
conversationId / conversation_id | string | Yes | Conversation ID |
TS Example:
await context.store.clearMessages({ conversationId: context.conversation_id })
Python example:
await context.store.clear_messages(conversation_id=context.conversation_id)
getConversation / get_conversation
Obtain conversation metadata.
Parameter
Parameter | Type | Required | Description |
conversationId / conversation_id | string | Yes | Conversation ID |
Return Value
ConversationMeta Contains fields such as conversationId / createdAt / lastMessageAt / messageCount / metadata.TS Example:
const meta = await context.store.getConversation({conversationId: context.conversation_id,})console.log(meta.messageCount, meta.metadata?.title)
Python example:
meta = await context.store.get_conversation(conversation_id=context.conversation_id,)print(meta.message_count, (meta.metadata or {}).get("title"))
listConversations / list_conversations
List conversations, sorted by
lastMessageAt, with support for cursor-based pagination and user-based filtering.
Parameter
Parameter | Type | Required | Description |
limit | number | No | Number of items per page, default 20, range [1, 100]. |
order | 'asc' | 'desc' | No | Sorting direction, default 'desc' (latest first). |
after | string | No | A cursor that passes the nextCursor returned from the previous page. |
before | string | No | A cursor that passes the previousCursor returned from the previous page. |
userId / user_id | string | No | Lists only the conversations under the specified user (hits the user index). |
Return Value
ListConversationsResult { items, nextCursor, previousCursor }.
TS Example:
const { items, nextCursor } = await context.store.listConversations({userId: 'u_123',limit: 20,})
Python example:
result = await context.store.list_conversations(user_id="u_123", limit=20)items, next_cursor = result.items, result.next_cursor
updateConversation / update_conversation
Shallow merge metadata: Identical keys are overwritten, while different keys are retained.
Parameter
Parameter | Type | Required | Description |
conversationId / conversation_id | string | Yes | Conversation ID |
metadata | Record<string, any> | Yes | Fields to be merged; the key is deleted when the value is null / None. |
Return Value
ConversationMeta The merged conversation metadata.TS Example:
await context.store.updateConversation({conversationId: context.conversation_id,metadata: { title: 'Product Inquiry', tag: null }, // Sets the title and removes the tag.})
Python example:
await context.store.update_conversation(conversation_id=context.conversation_id,metadata={"title": "Product Inquiry", "tag": None},)
deleteConversation / delete_conversation
Delete the entire conversation and synchronously clean up the message index, conversation metadata, and global conversation index. This action is irreversible.
Parameter:
Parameter | Type | Required | Description |
conversationId / conversation_id | string | Yes | Conversation ID |
TS Example:
await context.store.deleteConversation({conversationId: context.conversation_id,})
Python example:
await context.store.delete_conversation(conversation_id=context.conversation_id,)
toAnthropicMessages / to_anthropic_messages
Convert the message list returned by
getMessages into the format of the messages field for the Anthropic Messages API.Parameter
Parameter | Type | Required | Description |
messages | Message[] | Yes | Return result of getMessages / get_messages |
Return Value
Array<{ role: string; content: unknown }> — This can be directly used as the input parameter for anthropic.messages.create({ messages }).TS Example:
const history = await context.store.getMessages({conversationId: context.conversation_id,})const resp = await anthropic.messages.create({model: 'claude-sonnet-4',max_tokens: 1024,messages: context.store.toAnthropicMessages(history),})
Python example:
history = await context.store.get_messages(conversation_id=context.conversation_id,)resp = await anthropic_client.messages.create(model="claude-sonnet-4",max_tokens=1024,messages=context.store.to_anthropic_messages(history),)
toOpenAIInput / to_openai_input
Convert the message list returned by
getMessages into the format of the messages field for the OpenAI Chat Completions API. Retain role values that belong to user / assistant / system / tool, and pass through the content field unchanged.Parameter
Parameter | Type | Required | Description |
messages | Message[] | Yes | Return result of getMessages / get_messages |
Return Value
Array<{ role: string, content: any }> — This can be directly used as the input parameter for openai.chat.completions.create({ messages }).TS Example:
const history = await context.store.getMessages({conversationId: context.conversation_id,})const resp = await openai.chat.completions.create({model: 'gpt-4o',messages: context.store.toOpenAIInput(history),})
Python example:
history = await context.store.get_messages(conversation_id=context.conversation_id,)resp = await openai_client.chat.completions.create(model="gpt-4o",messages=context.store.to_openai_input(history),)
state — General State Storage
The previous methods all revolve around "messages".
context.store.state is another type of capability: a conversation-isolated JSON key-value store where you decide what to store. It is suitable for any framework, particularly those without native adapters (such as CrewAI), and for scenarios requiring storage of business-custom states (step counts, temporary variables, serialized framework runtime states).The conversationId is automatically bound to the current conversation. You do not need to pass it in method calls, and writes are isolated by the conversationId namespace. Like message history and session context, values in
state are also persisted across instances — even if you switch instances, reading the same conversationId will still retrieve them.Node Method | Python Method | Description |
state.get(key) | state.get(key) | Reads; returns null / None when the key does not exist. |
state.set(key, value) | state.set(key, value) | Writes; the value must be JSON-serializable. |
state.delete(key) | state.delete(key) | Deletes the specified key. |
All three methods are asynchronous.
Parameters and Constraints
Item | Description |
key | String, 1–256 characters in length |
value | Must be JSON-serializable: null / string / boolean / finite number / plain object / array. Functions, circular references, NaN / Infinity are rejected. |
TS Example:
// Request start: read back the state from the previous round (or null if none exists)const saved = await context.store.state.get<MyState>('session')// ... Run a conversation round ...// Request end: save the latest state back.await context.store.state.set('session', latestState)
Python example:
saved = await context.store.state.get("session")# ... Run a round ...await context.store.state.set("session", latest_state)
General Approach: Implementing Multi-turn Memory / Checkpoint Resumption with Other Frameworks. When no native adapter is available, the idea is to serialize the framework's session state into JSON and store it in
state at the end of each round. At the start of the next round, read it back first to restore the state. As long as the state can be expressed as a JSON-serializable object, cross-process multi-turn memory and checkpoint resumption can be achieved without relying on any framework-specific adapters. A typical implementation is an approval flow (human-in-the-loop). When a tool invocation requires manual approval, serialize the framework's runtime state and store it in state, then return "Awaiting Approval". The next time, with the same conversationId (approved or rejected), read back the state, resume execution from the checkpoint, and delete the state after completion. The serialized state always remains only on the server side, while the frontend only sends messages and the "approve/reject" decision.Data Structure
interface Message {messageId: string // msg_xxx (automatically generated by appendMessage)role: 'user' | 'assistant' | 'system' | 'tool'content: any // string / array / object (multimodal)createdAt: number // Timestamp in millisecondsmetadata?: Record<string, any> // Custom (e.g., token count, tool_call, etc.)updatedAt?: number // Present only after updateMessage}interface ConversationMeta {conversationId: stringcreatedAt: number // Timestamp in millisecondslastMessageAt: number // Timestamp in millisecondsmessageCount: numbermetadata?: Record<string, any> // Business-defined (e.g., title, user, tags, etc.)}interface ListConversationsResult {items: ConversationMeta[]nextCursor?: string // The cursor for the next page, to be passed to the after parameter.previousCursor?: string // The cursor for the previous page, to be passed to the before parameter.}
The Python-side fields aremessage_id / conversation_id / created_at / last_message_at / message_count / next_cursor / previous_cursor, and their overall structure is consistent.
Limits and Quotas
Item | Default Value | Exceedance Action |
conversation_id length | ≤ 256 characters | Throws a MemoryValidationError. |
Size of a single content item | ≤ 50 MB (after serialization) | Throws a MemoryValidationError. |
Maximum Messages per Conversation | 10000 | Throws a MemoryQuotaExceededError. |
limit Upper Bound | 100 | Throws a MemoryValidationError. |
limit Lower Bound | 1 | Throws a MemoryValidationError. |
For a single
content item that is excessively large (such as long documents and raw image data), it is recommended to store it in object storage and pass the URL into the message.Conversation Summary
getMessages can retrieve a maximum of 100 messages per call. For long conversations, the recommended practice is to keep the most recent N pieces of original text + compress earlier content into a summary, then assemble the prompt from the Store with each request. The Store itself does not call the LLM or generate summaries; it only provides the JSON container ConversationMeta.metadata.Responsibility Division
Responsible Party | Task/Responsibility |
Store | Provide the metadata JSON field + shallow merge writes. Define two keys: summary (summary text) and summarizedUntil (the messageId up to which summarization has been performed). |
Business side | Determine when to summarize, invoke a cost-effective model to generate the summary, and call updateConversation to write back. |
summary/summarizedUntilare conventional keys, not part of the schema. The Store simply stores them as arbitrary JSON. You can rename them, provided that the business logic maintains consistency before and after the change.
Implementation Example
You can use a cheaper model for summarization—compression + preserving key facts do not require the reasoning capabilities of a primary model. None of the four requirements in the prompt can be omitted; otherwise, issues such as divergent summaries, inclusion of casual greetings, language inconsistencies, and increasingly lengthy summaries may occur.
async function summarizeWithLLM(previousSummary, newMessages) {const transcript = newMessages.filter(m => m.role === 'user' || m.role === 'assistant').map(m => `${m.role}: ${typeof m.content === 'string' ? m.content : JSON.stringify(m.content)}`).join('\n')const prompt = previousSummary? `Below are the [Existing Summary] and [New Dialogue] of a conversation. Please merge them into a new summary.Requirements:- Retain: key facts, user preferences, incomplete tasks, and important contextual settings.- Delete: casual greetings, acknowledgments of receipt, duplicate content, and tool invocation details.- Write in the same language as the most recent conversation.- Keep it within 500 words.[Existing Summary]${previousSummary}[New Dialogue]${transcript}[New Summary]`: `Please summarize the following conversation into a summary.Requirements:- Retain: key facts, user preferences, incomplete tasks, and important contextual settings.- Delete: casual greetings, acknowledgments of receipt, duplicate content, and tool invocation details.- Write in the same language as the conversation.- Keep it within 500 words.[Dialogue]${transcript}[Summary]`const { text } = await generateText({model: openai('gpt-4o-mini'),prompt,maxTokens: 800,})return text.trim()}
Difference Between Message Storage and Session Context Storage
context.store provides two types of independent, non-interchangeable persistence, and most conversational applications require both:What to Store | Effect | How to Store |
Message history | After the frontend page is refreshed, the chat history persists and can be re-rendered, and the sidebar can list historical sessions. | General API appendMessage / getMessages is data for pure read/write operations without invoking the model. |
Conversation context | Model remembers previous conversation content (multi-turn continuous dialogue) and supports resumption after interruption. | Hand it over to the framework's native adapter (see "Cross-Instance Session Recovery"). The framework reads and writes it in the native format by itself. |
The two most common misconceptions:
Only message history is recorded, without connecting to the conversation context → Records are visible in
/history, but the model loses memory in each round because the context is not fed back to it.Only the conversation context is connected, without writing message history → The model can remember, but the chat history appears blank after the frontend refreshes because the framework's session format is not designed for frontend rendering.
In a nutshell: Message history handles "visibility after refresh", while conversation context handles "model memory". Each manages its own domain, and both are persisted across instances.
