How are turns inside sessions stored?
how do turns inside sessions get stored? cause there's ai SDK's "UIMessage". wondering how stuff is stored.
Now I have the full picture. Let me summarize how turns/messages are stored:
##How Turns/Messages Are Stored in OpenCode
OpenCode has its own storage format, separate from AI SDK's UIMessage. Here's how it works:
###Storage Structure
Messages are stored as JSON files in a hierarchical file structure under ~/.opencode/storage/ (via the Global.Path.data directory):
storage/
├── session/{projectID}/{sessionID}.json # Session metadata
├── message/{sessionID}/{messageID}.json # Individual messages
├── part/{messageID}/{partID}.json # Message parts
├── session_diff/{sessionID}.json # File diffs for session
└── todo/{sessionID}.json # TODOs for session
###Key Data Types
1. Session Info (Session.Info in index.ts:41-76):
- Contains metadata like
id,title,projectID,directory, timestamps, and optional share/revert info
2. Messages (MessageV2.Info in message-v2.ts:385-390):
A discriminated union of two roles:
-
User Message (
MessageV2.User):role: "user"agent,model(providerID + modelID),systemprompt,tools,variant- Timestamp
-
Assistant Message (
MessageV2.Assistant):role: "assistant"parentID(links to user message),providerID,modelID,agentcost,tokens(input/output/reasoning/cache)- Optional
errorfield - Optional
summary,finishstatus
3. Parts (MessageV2.Part in message-v2.ts:330-344):
Parts are stored separately from messages. A discriminated union including:
TextPart- actual text contentReasoningPart- model's reasoning/thinkingFilePart- file attachments (images, etc.)ToolPart- tool calls with state (pending/running/completed/error)StepStartPart,StepFinishPart- step boundaries with cost/tokensSnapshotPart,PatchPart- git snapshotsAgentPart,SubtaskPart,CompactionPart,RetryPart
###Conversion to AI SDK's UIMessage
The toModelMessages() function (message-v2.ts:437-606) converts OpenCode's format to AI SDK's UIMessage[]:
export function toModelMessages(input: WithParts[], model: Provider.Model): ModelMessage[] {
const result: UIMessage[] = []
for (const msg of input) {
if (msg.info.role === "user") {
const userMessage: UIMessage = {
id: msg.info.id,
role: "user",
parts: [], // Built from TextPart, FilePart, etc.
}
// ...convert parts
}
if (msg.info.role === "assistant") {
const assistantMessage: UIMessage = {
id: msg.info.id,
role: "assistant",
parts: [], // Built from TextPart, ToolPart, etc.
}
// ...convert parts including tool calls
}
}
return convertToModelMessages(result, { tools }) // AI SDK function
}
###Storage Operations (via Storage namespace)
// Write message
await Storage.write(["message", msg.sessionID, msg.id], msg)
// Write part
await Storage.write(["part", part.messageID, part.id], part)
// Read messages for session
for (const item of await Storage.list(["message", sessionID])) {
const msg = await Storage.read<MessageV2.Info>(item)
}
// Read parts for message
for (const item of await Storage.list(["part", messageID])) {
const part = await Storage.read<MessageV2.Part>(item)
}
###Key Takeaways
- Separation of concerns: Messages (
Info) and their content (Part) are stored separately - Rich part types: Much richer than UIMessage - includes tool states, reasoning, snapshots, costs
- Conversion layer:
toModelMessages()handles the translation to AI SDK format when needed for LLM calls - File-based storage: JSON files with file locking for concurrent access
- Parts are ordered by ID: IDs are ascending identifiers, sorted when retrieved
END OF POST