This commit is contained in:
2026-07-31 11:41:53 +07:00
parent c9a7661e93
commit 7876ff21eb
8 changed files with 1671 additions and 2168 deletions
+305 -188
View File
@@ -27,15 +27,16 @@
│ └─► storage.appendEntry() → JSONL file │
│ │
│ To navigate to E2 (fork point): │
Session.moveTo(E2) │
session.moveTo(E2) │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ E1 │────▶│ E2 │────▶│ E3' │────▶│ E4' │────▶│ E5' │ (new branch) │
│ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │
│ │ │ │
│ │ ▼ create BranchSummary │
│ │ ┌─────┐ │
└──────│ E6 │ (branch summary) │
└─────┘ │
│ E6 │ (branch summary) │
└─────┘ │
│ └───────────────────────────────────────────────────────────────────────┘
└─────────────────────────────────────────────────────────────────────────────┘
```
@@ -49,43 +50,45 @@ AgentState.messages::Vector{AgentMessage}
│ ▼
│ ┌──────────────────────────────────────────────────────────────┐
│ │ appendMessage(session, AgentMessage) │
│ │ Input: message::AgentMessage
│ │ Input: session::Session, message::AgentMessage │
│ │ Output: entry_id::String │
│ │ │
│ │ Steps: │
│ │ 1. Create MessageEntry: │
│ │ - type: "message"
│ │ - id: createEntryId(storage)
│ │ - parent_id: getLeafId(storage) │
│ │ - timestamp: create_timestamp() │
│ │ - message: copy(message) │
│ │ - base: SessionTreeEntryBase(type, id, leaf_id, time)
│ │ - message: the AgentMessage
│ │ 2. storage.appendEntry(entry) │
│ │ - Write JSONL line to file
│ │ - Update leaf_id
│ │ - In-memory: push to entries vector, update by_id dict
│ │ - JSONL: would append to file (TODO)
│ │ 3. Return entry.id │
│ └──────────────────────────────────────────────────────────────┘
└─► Entry stored in JSONL:
└─► Entry stored in JSONL (conceptual):
{"type":"message","id":"msg_1","parent_id":null,"timestamp":"...","message":{...}}
```
## Entry Types
## Entry Types
All entry types extend `abstract type SessionTreeEntry end` and embed a
`base::SessionTreeEntryBase` struct containing `type`, `id`, `parent_id`, and `timestamp`.
```julia
abstract type SessionTreeEntry end
struct SessionTreeEntryBase
type::String
id::String
parent_id::Union{String, Nothing}
timestamp::String
end
```
### 1. MessageEntry
```julia
struct MessageEntry <: SessionTreeEntry
type::String # "message"
id::String # Unique entry ID
parent_id::Union{String, Nothing}
timestamp::String # ISO 8601 timestamp
message::AgentMessage # The actual message
base::SessionTreeEntryBase
message::AgentMessage
end
```
@@ -95,11 +98,8 @@ end
```julia
struct ThinkingLevelChangeEntry <: SessionTreeEntry
type::String # "thinking_level_change"
id::String
parent_id::Union{String, Nothing}
timestamp::String
thinking_level::String # "off", "minimal", "low", "medium", etc.
base::SessionTreeEntryBase
thinking_level::String
end
```
@@ -109,12 +109,9 @@ end
```julia
struct ModelChangeEntry <: SessionTreeEntry
type::String # "model_change"
id::String
parent_id::Union{String, Nothing}
timestamp::String
provider::String # "openai", "anthropic", etc.
model_id::String # Model identifier
base::SessionTreeEntryBase
provider::String
model_id::String
end
```
@@ -124,10 +121,7 @@ end
```julia
struct ActiveToolsChangeEntry <: SessionTreeEntry
type::String # "active_tools_change"
id::String
parent_id::Union{String, Nothing}
timestamp::String
base::SessionTreeEntryBase
active_tool_names::Vector{String}
end
```
@@ -137,18 +131,15 @@ end
### 5. CompactionEntry
```julia
struct CompactionEntry <: SessionTreeEntry
type::String # "compaction"
id::String
parent_id::Union{String, Nothing}
timestamp::String
summary::String # Summary of compacted history
struct CompactionEntry{T} <: SessionTreeEntry
base::SessionTreeEntryBase
summary::String
first_kept_entry_id::Union{String, Nothing}
tokens_before::Int64 # Context size before compaction
tokens_before::Int64
retained_tail::Union{Vector{AgentMessage}, Nothing}
details::Union{Any, Nothing}
details::Union{T, Nothing}
usage::Union{Usage, Nothing}
from_hook::Bool # Whether triggered by hook
from_hook::Bool
end
```
@@ -163,14 +154,11 @@ end
### 6. BranchSummaryEntry
```julia
struct BranchSummaryEntry <: SessionTreeEntry
type::String # "branch_summary"
id::String
parent_id::Union{String, Nothing}
timestamp::String
from_id::String # Branch point entry ID
summary::String # Summary of branch history
details::Union{Any, Nothing}
struct BranchSummaryEntry{T} <: SessionTreeEntry
base::SessionTreeEntryBase
from_id::String
summary::String
details::Union{T, Nothing}
usage::Union{Usage, Nothing}
from_hook::Bool
end
@@ -181,13 +169,10 @@ end
### 7. CustomEntry
```julia
struct CustomEntry <: SessionTreeEntry
type::String # Custom type
id::String
parent_id::Union{String, Nothing}
timestamp::String
struct CustomEntry{T} <: SessionTreeEntry
base::SessionTreeEntryBase
custom_type::String
data::Union{Any, Nothing}
data::Union{T, Nothing}
end
```
@@ -196,14 +181,11 @@ end
### 8. CustomMessageEntry
```julia
struct CustomMessageEntry <: SessionTreeEntry
type::String
id::String
parent_id::Union{String, Nothing}
timestamp::String
struct CustomMessageEntry{T} <: SessionTreeEntry
base::SessionTreeEntryBase
custom_type::String
content::String
details::Union{Any, Nothing}
details::Union{T, Nothing}
display::Bool
end
```
@@ -214,11 +196,8 @@ end
```julia
struct LabelEntry <: SessionTreeEntry
type::String
id::String
parent_id::Union{String, Nothing}
timestamp::String
target_id::String # Entry being labeled
base::SessionTreeEntryBase
target_id::String
label::Union{String, Nothing}
end
```
@@ -229,10 +208,7 @@ end
```julia
struct SessionInfoEntry <: SessionTreeEntry
type::String
id::String
parent_id::Union{String, Nothing}
timestamp::String
base::SessionTreeEntryBase
name::Union{String, Nothing}
end
```
@@ -243,10 +219,7 @@ end
```julia
struct LeafEntry <: SessionTreeEntry
type::String
id::String
parent_id::Union{String, Nothing}
timestamp::String
base::SessionTreeEntryBase
target_id::Union{String, Nothing}
end
```
@@ -259,86 +232,95 @@ end
abstract type SessionStorage{T<:SessionMetadata} end
```
### Storage Methods
### Storage Methods (actual implementation signatures)
```julia
# Metadata
getMetadata(storage::SessionStorage)::Promise{T}
getMetadata(storage::SessionStorage)::T
# Leaf management
getLeafId(storage::SessionStorage)::Promise{Union{String, Nothing}}
setLeafId(storage::SessionStorage, leaf_id::String)::Promise{Nothing}
getLeafId(storage::SessionStorage)::Union{String, Nothing}
setLeafId(storage::SessionStorage, leaf_id::Union{String, Nothing})::Nothing
# Entry management
createEntryId(storage::SessionStorage)::Promise{String}
appendEntry(storage::SessionStorage, entry::SessionTreeEntry)::Promise{Nothing}
getEntry(storage::SessionStorage, id::String)::Promise{Union{SessionTreeEntry, Nothing}}
createEntryId(storage::SessionStorage)::String
appendEntry(storage::SessionStorage, entry::SessionTreeEntry)::Nothing
getEntry(storage::SessionStorage, id::String)::Union{SessionTreeEntry, Nothing}
# Query
findEntries(storage::SessionStorage, type::String)::Promise{Vector{SessionTreeEntry}}
getLabel(storage::SessionStorage, id::String)::Promise{Union{String, Nothing}}
getSessionName(storage::SessionStorage)::Promise{Union{String, Nothing}}
findEntries(storage::SessionStorage, type::String)::Vector{SessionTreeEntry}
getLabel(storage::SessionStorage, id::String)::Union{String, Nothing}
getSessionName(storage::SessionStorage)::Union{String, Nothing}
# Branch navigation
getPathToRootOrCompaction(
storage::SessionStorage,
leaf_id::String,
)::Promise{Vector{SessionTreeEntry}}
getEntries(storage::SessionStorage, options::Dict{String, Any})::Promise{Vector{SessionTreeEntry}}
getPathToRootOrCompaction(storage::SessionStorage, leaf_id::Union{String, Nothing})::Vector{SessionTreeEntry}
getEntries(storage::SessionStorage, options::Dict{String, Any})::Vector{SessionTreeEntry}
# Stats
getSessionStats(storage::SessionStorage)::Promise{SessionStats}
getSessionStats(storage::SessionStorage)::SessionStats
```
## JsonlSessionStorage
```
mutable struct JsonlSessionStorage{T<:SessionMetadata} <: SessionStorage{T}
file_path::String
metadata::T
entries::Vector{SessionTreeEntry} # ordered list
by_id::Dict{String, SessionTreeEntry} # fast lookup by id
labels_by_id::Dict{String, String} # label cache
current_leaf_id::Union{String, Nothing} # current branch tip
end
```
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ JSONL Storage Format │
└─────────────────────────────────────────────────────────────────────────────┘
File: session.jsonl
File: session.jsonl (conceptual - not yet implemented)
Entry 1 (Metadata):
{"type":"session","id":"meta_1","created_at":"2024-01-01T00:00:00Z","cwd":"/path","path":"/path/session.jsonl"}
Entry 1 (Metadata via SessionHeader):
{"type":"session","version":3,"id":"meta_1","timestamp":"...","cwd":"/path","parent_session":null,"metadata":{}}
Entry 2 (Message):
{"type":"message","id":"msg_1","parent_id":null,"timestamp":"2024-01-01T00:00:01Z","message":{"role":"user","content":[{"type":"text","text":"Hello"}]}}
{"type":"message","id":"msg_1","parent_id":null,"timestamp":"...","message":{"role":"user",...}}
Entry 3 (Thinking Level):
{"type":"thinking_level_change","id":"tl_1","parent_id":"msg_1","timestamp":"2024-01-01T00:00:02Z","thinking_level":"medium"}
{"type":"thinking_level_change","id":"tl_1","parent_id":"msg_1","timestamp":"...","thinking_level":"medium"}
Entry 4 (Model Change):
{"type":"model_change","id":"mc_1","parent_id":"tl_1","timestamp":"2024-01-01T00:00:03Z","provider":"openai","model_id":"gpt-4"}
{"type":"model_change","id":"mc_1","parent_id":"tl_1","timestamp":"...","provider":"openai","model_id":"gpt-4"}
Entry 5 (Compaction):
{"type":"compaction","id":"comp_1","parent_id":"mc_1","timestamp":"2024-01-01T00:00:04Z","summary":"Previous messages summarized...","first_kept_entry_id":"msg_3","tokens_before":100000,"tokens_after":50000}
{"type":"compaction","id":"comp_1","parent_id":"mc_1","timestamp":"...","summary":"...","first_kept_entry_id":"msg_3","tokens_before":100000}
Entry 6 (Branch Summary):
{"type":"branch_summary","id":"branch_1","parent_id":"comp_1","timestamp":"2024-01-01T00:00:05Z","from_id":"msg_3","summary":"Branch from message 3"}
{"type":"branch_summary","id":"branch_1","parent_id":"comp_1","timestamp":"...","from_id":"msg_3","summary":"..."}
Entry 7 (Active Tools):
{"type":"active_tools_change","id":"tools_1","parent_id":"branch_1","timestamp":"2024-01-01T00:00:06Z","active_tool_names":["bash","read"]}
{"type":"active_tools_change","id":"tools_1","parent_id":"branch_1","timestamp":"...","active_tool_names":["bash","read"]}
Entry 8 (Leaf):
{"type":"leaf","id":"leaf_1","parent_id":"tools_1","timestamp":"2024-01-01T00:00:07Z","target_id":"msg_5"}
{"type":"leaf","id":"leaf_1","parent_id":"tools_1","timestamp":"...","target_id":"msg_5"}
Notes:
- Each line is a JSON object (JSONL format)
- Each line is a JSON object (JSONL format) - TODO: file I/O not yet implemented
- parent_id references previous entry (linked list structure)
- Leaf entry points to current position in tree
- To fork, create new branch from any entry
- In-memory mode uses Vector + Dict by_id for fast access
```
## InMemorySessionStorage
```julia
mutable struct InMemorySessionStorage
metadata::SessionMetadata
mutable struct InMemorySessionStorage{T<:SessionMetadata} <: SessionStorage{T}
metadata::T
entries::Vector{SessionTreeEntry}
by_id::Dict{String, SessionTreeEntry}
labels_by_id::Dict{String, String}
leaf_id::Union{String, Nothing}
entries::Dict{String, SessionTreeEntry}
labels::Dict{String, String}
end
```
@@ -355,6 +337,19 @@ end
mutable struct Session{T<:SessionMetadata}
storage::SessionStorage{T}
context_build_options::SessionContextBuildOptions
function Session(storage::SessionStorage, context_build_options=SessionContextBuildOptions(nothing, nothing))
new{typeof(storage.metadata)}(storage, context_build_options)
end
end
```
### SessionContextBuildOptions
```julia
mutable struct SessionContextBuildOptions
entry_transforms::Union{Vector{Function}, Nothing}
entry_projectors::Union{Dict{String, Function}, Nothing}
end
```
@@ -364,14 +359,10 @@ end
```julia
function appendMessage(session::Session, message::AgentMessage)::String
entry = MessageEntry(
"message",
createEntryId(session.storage),
getLeafId(session.storage),
create_timestamp(),
return appendTypedEntry(session, MessageEntry(
SessionTreeEntryBase("message", createEntryId(session.storage), getLeafId(session.storage), create_timestamp()),
message,
)
return appendTypedEntry(session, entry)
))
end
```
@@ -392,18 +383,34 @@ tool_id = appendMessage(session, ToolResultMessage(...))
#### appendThinkingLevelChange()
```julia
function appendThinkingLevelChange(
session::Session,
thinking_level::String,
)::String
entry = ThinkingLevelChangeEntry(
"thinking_level_change",
createEntryId(session.storage),
getLeafId(session.storage),
create_timestamp(),
function appendThinkingLevelChange(session::Session, thinking_level::String)::String
return appendTypedEntry(session, ThinkingLevelChangeEntry(
SessionTreeEntryBase("thinking_level_change", createEntryId(session.storage), getLeafId(session.storage), create_timestamp()),
thinking_level,
)
return appendTypedEntry(session, entry)
))
end
```
#### appendModelChange()
```julia
function appendModelChange(session::Session, provider::String, model_id::String)::String
return appendTypedEntry(session, ModelChangeEntry(
SessionTreeEntryBase("model_change", createEntryId(session.storage), getLeafId(session.storage), create_timestamp()),
provider,
model_id,
))
end
```
#### appendActiveToolsChange()
```julia
function appendActiveToolsChange(session::Session, active_tool_names::Vector{String})::String
return appendTypedEntry(session, ActiveToolsChangeEntry(
SessionTreeEntryBase("active_tools_change", createEntryId(session.storage), getLeafId(session.storage), create_timestamp()),
active_tool_names,
))
end
```
@@ -420,11 +427,8 @@ function appendCompaction(
usage::Union{Usage, Nothing}=nothing,
retained_tail::Union{Vector{AgentMessage}, Nothing}=nothing,
)::String
entry = CompactionEntry(
"compaction",
createEntryId(session.storage),
getLeafId(session.storage),
create_timestamp(),
return appendTypedEntry(session, CompactionEntry(
SessionTreeEntryBase("compaction", createEntryId(session.storage), getLeafId(session.storage), create_timestamp()),
summary,
first_kept_entry_id,
tokens_before,
@@ -432,8 +436,7 @@ function appendCompaction(
details,
usage,
from_hook,
)
return appendTypedEntry(session, entry)
))
end
```
@@ -445,25 +448,24 @@ function moveTo(
entry_id::Union{String, Nothing},
summary::Union{Dict{String, Any}, Nothing}=nothing,
)::Union{String, Nothing}
# Set new leaf
setLeafId(session.storage, entry_id)
# Optionally create branch summary
if !isnothing(summary)
return appendTypedEntry(session, BranchSummaryEntry(
"branch_summary",
createEntryId(session.storage),
entry_id,
create_timestamp(),
entry_id,
summary["summary"],
get(summary, "details", nothing),
get(summary, "usage", nothing),
get(summary, "from_hook", false),
))
# Validate entry exists
if !isnothing(entry_id) && isnothing(getEntry(session, entry_id))
throw(SessionError("not_found", "Entry $(entry_id) not found"))
end
return nothing
# Set new leaf (creates a LeafEntry)
setLeafId(session.storage, entry_id)
# Optionally create branch summary
if isnothing(summary)
return nothing
end
return appendTypedEntry(session, BranchSummaryEntry(
SessionTreeEntryBase("branch_summary", createEntryId(session.storage), entry_id, create_timestamp()),
entry_id,
summary["summary"],
get(summary, "details", nothing),
get(summary, "usage", nothing),
get(summary, "from_hook", false),
))
end
```
@@ -482,12 +484,18 @@ session.moveTo(
)
```
**How it works**:
1. Validates the target entry exists
2. Calls `setLeafId()` which creates a `LeafEntry` with `target_id = entry_id`
3. If `summary` is provided, creates a `BranchSummaryEntry` as a child of the target entry
4. The new leaf now points to `entry_id`, making it the root of a new branch
## Build Session Context
```julia
function buildSessionContext(
path_entries::Vector{SessionTreeEntry},
options::SessionContextBuildOptions=SessionContextBuildOptions(),
options::SessionContextBuildOptions=SessionContextBuildOptions(nothing, nothing),
)::SessionContext
state = deriveSessionContextState(path_entries)
context_entries = buildContextEntries(path_entries, options)
@@ -497,14 +505,36 @@ function buildSessionContext(
end
return SessionContext(messages, state.thinking_level, state.model, state.active_tool_names)
end
function deriveSessionContextState(path_entries::Vector{SessionTreeEntry})::Dict{String, Any}
thinking_level = "off"
model = nothing
active_tool_names = nothing
for entry in path_entries
if entry isa ThinkingLevelChangeEntry
thinking_level = entry.thinking_level
elseif entry isa ModelChangeEntry
model = Dict("provider" => entry.provider, "modelId" => entry.model_id)
elseif entry isa MessageEntry && entry.message.role == "assistant"
model = Dict("provider" => entry.message.provider, "modelId" => entry.message.model)
elseif entry isa ActiveToolsChangeEntry
active_tool_names = copy(entry.active_tool_names)
end
end
return Dict(
"thinking_level" => thinking_level,
"model" => model,
"active_tool_names" => active_tool_names,
)
end
```
### Context Entry Transform
```julia
function defaultContextEntryTransform(
path_entries::Vector{SessionTreeEntry},
)::Vector{SessionTreeEntry}
function defaultContextEntryTransform(path_entries::Vector{SessionTreeEntry})::Vector{SessionTreeEntry}
compaction = nothing
for entry in path_entries
if entry isa CompactionEntry
@@ -512,25 +542,26 @@ function defaultContextEntryTransform(
break
end
end
if isnothing(compaction)
return copy(path_entries)
end
# Include compaction entry
entries = [compaction]
# Include retained tail if present
entries::Vector{SessionTreeEntry} = [compaction]
compaction_idx = findfirst(
(entry) -> entry isa CompactionEntry && entry.id == compaction.id,
path_entries,
)
if !isnothing(compaction.retained_tail)
compaction_idx = findfirst(e -> e.id == compaction.id, path_entries)
append!(entries, path_entries[compaction_idx+1:end])
for i in compaction_idx+1:length(path_entries)
push!(entries, path_entries[i])
end
return entries
end
# Otherwise include entries after first_kept_entry_id
if !isnothing(compaction.first_kept_entry_id)
found_first_kept = false
compaction_idx = findfirst(e -> e.id == compaction.id, path_entries)
for i in 1:compaction_idx-1
entry = path_entries[i]
if entry.id == compaction.first_kept_entry_id
@@ -541,11 +572,26 @@ function defaultContextEntryTransform(
end
end
end
# Include entries after compaction
compaction_idx = findfirst(e -> e.id == compaction.id, path_entries)
append!(entries, path_entries[compaction_idx+1:end])
for i in compaction_idx+1:length(path_entries)
push!(entries, path_entries[i])
end
return entries
end
function buildContextEntries(
path_entries::Vector{SessionTreeEntry},
options::SessionContextBuildOptions=SessionContextBuildOptions(nothing, nothing),
)::Vector{SessionTreeEntry}
entries = defaultContextEntryTransform(path_entries)
if !isnothing(options.entry_transforms)
for transform in options.entry_transforms
entries = transform(entries)
end
end
return entries
end
```
@@ -557,12 +603,12 @@ function sessionEntryToContextMessages(
entry::SessionTreeEntry,
index::Int64,
entries::Vector{SessionTreeEntry},
options::SessionContextBuildOptions=SessionContextBuildOptions(),
options::SessionContextBuildOptions=SessionContextBuildOptions(nothing, nothing),
)::Vector{AgentMessage}
if entry isa MessageEntry
return [entry.message]
end
if entry isa CustomMessageEntry
return [createCustomMessage(
entry.custom_type,
@@ -572,7 +618,7 @@ function sessionEntryToContextMessages(
entry.timestamp,
)]
end
if entry isa CompactionEntry
messages = [createCompactionSummaryMessage(
entry.summary,
@@ -584,7 +630,7 @@ function sessionEntryToContextMessages(
end
return messages
end
if entry isa BranchSummaryEntry
return [createBranchSummaryMessage(
entry.summary,
@@ -592,16 +638,15 @@ function sessionEntryToContextMessages(
entry.timestamp,
)]
end
if entry isa CustomEntry
# Custom projectors can transform custom entries
if !isnothing(options.entry_projectors) && haskey(options.entry_projectors, entry.custom_type)
projector = options.entry_projectors[entry.custom_type]
return projector(entry, index, entries)
end
return AgentMessage[]
end
return AgentMessage[]
end
```
@@ -648,6 +693,16 @@ Key Points:
- Each branch has independent tail
```
### getPathToRootOrCompaction
Walks from a leaf back to the root, handling compaction entries:
```julia
# When encountering a CompactionEntry:
# - If retained_tail is set: stop (compaction covers the tail)
# - Otherwise: skip to first_kept_entry_id and continue walking
```
## Compaction Strategy
### Why Compaction?
@@ -679,7 +734,7 @@ LLM context windows have limits:
# 4. Update storage
# - Append CompactionEntry
# - Update leaf to CompactionEntry
# - Leaf automatically points to CompactionEntry (leafIdAfterEntry)
```
### Compaction Example
@@ -735,8 +790,10 @@ using AgentCore
# 1. Create storage
storage = JsonlSessionStorage(
SessionMetadata("session_1", "2024-01-01T00:00:00Z"),
"/path/to/session.jsonl",
SessionHeader("session", 3, "session_1", created_at, "/path", nothing, nothing),
SessionTreeEntry[],
nothing,
)
# 2. Create session
@@ -756,7 +813,7 @@ mc_id = appendModelChange(session, "openai", "gpt-4")
msg3_id = appendMessage(session, UserMessage("user", [TextContent("What can you do?")], timestamp))
msg4_id = appendMessage(session, AssistantMessage("assistant", [TextContent("I can...")], ...))
# 7. Compact context (100K tokens 20K)
# 7. Compact context (100K tokens -> 20K)
compact_id = appendCompaction(
session,
"User asked about capabilities and assistant explained",
@@ -771,31 +828,91 @@ compact_id = appendCompaction(
# 8. Fork and branch
session.moveTo(msg2_id) # Go back to msg2
# 9. Create new branch
branch_id = appendBranchSummary(
# 9. Continue on new branch (moveTo creates branch summary when summary is provided)
branch_id = moveTo(
session,
"User changed direction to focus on file operations",
msg2_id,
Dict("focus" => "files"),
Dict("summary" => "User changed direction", "details" => Dict("focus" => "files")),
)
# 10. Continue on new branch
msg5_id = appendMessage(session, UserMessage("user", [TextContent("Let's work with files")], timestamp))
# 11. Query session context
context = buildSessionContext(session)
context = buildContext(session)
# 12. Get stats
stats = getSessionStats(session)
println("Messages: $(stats.message_count)")
println("Total tokens: $(stats.total_tokens)")
println("Cost: $$(stats.cost_total)")
println("Cost: \$(stats.cost_total)")
```
## Session Repo Interface
### Session Repository Methods
```julia
# Create a new session
create(repo::SessionRepo, options::TCreateOptions)::Session
# Open an existing session
open(repo::SessionRepo, metadata::TMetadata)::Session
# List sessions
list(repo::SessionRepo, options::TListOptions)::Vector{TMetadata}
# Delete a session
delete(repo::SessionRepo, metadata::TMetadata)::Nothing
# Fork a session (copy branch from entry)
fork(repo::SessionRepo, source::TMetadata, options::Dict{String, Any})::Session
```
### JSONL vs In-Memory Repos
| Feature | JsonlSessionRepo | InMemorySessionRepo |
|---------|------------------|---------------------|
| Persistence | File-based (TODO) | In-memory only |
| Use case | Production | Testing |
| Fork | Not implemented | Uses getEntriesToFork |
| Metadata | JsonlSessionMetadata | SessionMetadata |
### Fork Behavior (`getEntriesToFork`)
```julia
function getEntriesToFork(storage, options)::Vector{SessionTreeEntry}
# If no entryId specified, fork from current leaf (full copy)
if !haskey(options, :entryId) || isnothing(options[:entryId])
return getEntries(storage, Dict{String, Any}())
end
target = getEntry(storage, options[:entryId])
position = get(options, "position", "before")
if position == "at"
# Fork includes the target entry
effective_leaf_id = target.id
else
# Fork before the target (parent)
# Target must be a user message
if target isa MessageEntry && target.message.role != "user"
throw(SessionError("invalid_fork_target", "Not a user message"))
end
effective_leaf_id = target.parent_id
end
return getPathToRootOrCompaction(storage, effective_leaf_id)
end
```
## Best Practices
1. **Use compaction** for long conversations to stay within context limits
2. **Create branch summaries** when forking to document divergent paths
3. **Retain tail messages** after compaction for context
2. **Create branch summaries** when forking to document divergent paths (via `moveTo()` with summary)
3. **Retain tail messages** after compaction for context (`retained_tail` field)
4. **Track token usage** to optimize compaction timing
5. **Use InMemorySessionStorage** for testing
6. **Use `getBranch(session)`** to get the current path from leaf to root/compaction
7. **Use `buildContext(session)`** as the convenient Session method for building context
8. **Use `mergeContextBuildOptions(session, options)`** to combine session-level and call-level transforms/projectors