update
This commit is contained in:
+305
-188
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user