pub struct SessionTree {
pub nodes: BTreeMap<String, TreeNode>,
pub root: Option<String>,
pub branches: BTreeMap<String, Branch>,
pub active_branch: String,
/* private fields */
}Expand description
The native in-place conversation tree (module 21). See the module doc comment for the full design (C7 tree-with-linear-projection, lossless rewind, branch summaries).
Fields§
§nodes: BTreeMap<String, TreeNode>Every node in the tree, keyed by id. A BTreeMap (not a
std::collections::HashMap) so iteration/serialization order is
deterministic — load-bearing for the lossless round-trip tests
(assert_eq! on two independently-loaded trees must not flake on
hash-iteration order).
root: Option<String>The tree’s single root node id. None only for a brand-new, empty
tree.
branches: BTreeMap<String, Branch>Every branch, keyed by name. Always has at least MAIN_BRANCH once
SessionTree::new/SessionTree::from_linear have run.
active_branch: StringThe currently-active branch name — a key into Self::branches.
Implementations§
Source§impl SessionTree
impl SessionTree
Sourcepub fn new() -> SessionTree
pub fn new() -> SessionTree
A brand-new, empty tree: no nodes, one branch (MAIN_BRANCH) with
no leaf yet, active.
Sourcepub fn from_linear(messages: &[ChatMessage], created_at_ms: i64) -> SessionTree
pub fn from_linear(messages: &[ChatMessage], created_at_ms: i64) -> SessionTree
Build a tree from an existing LINEAR message sequence — the
degenerate single-path tree (C7): each message becomes a node,
chained to the previous one, with MAIN_BRANCH’s leaf ending at the
last message. Self::linear_projection on the result is
byte-for-byte messages (see
the linear-projection regression test) —
this is the bridge a caller uses to materialize a tree lazily out of
an ordinary crate::session::Session::messages, the FIRST time a
tree operation (rewind/branch/label) is actually invoked on it.
created_at_ms is stamped on every synthesized node (a single
timestamp for the whole import, since the source linear messages
carry no per-turn timestamp of their own).
Sourcepub fn append_message(
&mut self,
message: ChatMessage,
created_at_ms: i64,
) -> String
pub fn append_message( &mut self, message: ChatMessage, created_at_ms: i64, ) -> String
Append a new turn as a child of the ACTIVE branch’s current leaf
(ordinary turn continuation — the tree’s analog of pushing onto
crate::session::Session::messages). Returns the new node’s id.
This is the only way Self::root is ever set (on the very first
node the whole tree ever gets).
Sourcepub fn rewind(
&mut self,
node_id: &str,
timestamp_ms: i64,
) -> Result<Option<String>, InterchangeError>
pub fn rewind( &mut self, node_id: &str, timestamp_ms: i64, ) -> Result<Option<String>, InterchangeError>
Rewind-anywhere (module 21): move the ACTIVE branch’s current-leaf
pointer back to node_id. node_id must already exist in the tree —
an unknown id is an error, never silently ignored or treated as a
no-op (the “never corrupt/dangling” requirement).
Lossless. No node is ever deleted by this. If the active branch’s
leaf was pointing somewhere other than node_id before the call, that
OLD leaf — and therefore the whole path back to (but not past) the
nearest still-referenced ancestor — is preserved under a fresh
sibling branch name (using the internal fresh-name allocator) so it stays
independently addressable, not merely still-linked-in-but-unnamed.
Returns that sibling branch’s name, or None if the rewind was a
no-op (node_id was already the active leaf, or the branch had no
leaf yet).
The next Self::append_message after a rewind creates a NEW child
of node_id — a sibling of whatever child used to follow it, exactly
“rewind = fork at the rewind point.”
Sourcepub fn branch(
&mut self,
from_node: &str,
name: Option<String>,
timestamp_ms: i64,
) -> Result<String, InterchangeError>
pub fn branch( &mut self, from_node: &str, name: Option<String>, timestamp_ms: i64, ) -> Result<String, InterchangeError>
Explicit branch (module 21): fork the conversation at from_node,
creating a NEW branch (named name, or an auto-generated
"branch-N" if None) whose leaf starts at from_node, and switch
the active branch to it. Errors if from_node doesn’t exist, or if
name is Some and already taken (an explicit name collision is a
caller mistake worth surfacing, unlike Self::rewind’s
auto-generated names which always self-disambiguate).
Sourcepub fn switch_branch(&mut self, name: &str) -> Result<(), InterchangeError>
pub fn switch_branch(&mut self, name: &str) -> Result<(), InterchangeError>
Switch the active branch to an already-existing one. Errors if name
doesn’t name a branch (no silent fallback to main).
Sourcepub fn label(
&mut self,
node_id: &str,
label: impl Into<String>,
) -> Result<(), InterchangeError>
pub fn label( &mut self, node_id: &str, label: impl Into<String>, ) -> Result<(), InterchangeError>
Label (module 21 “entry labels”) a node — a human/agent annotation,
persisted on the node itself (so it round-trips with the rest of the
tree, §1.13). Errors if node_id doesn’t exist.
Sourcepub fn clear_label(&mut self, node_id: &str) -> Result<(), InterchangeError>
pub fn clear_label(&mut self, node_id: &str) -> Result<(), InterchangeError>
Clear a node’s label, if any. Errors if node_id doesn’t exist (same
existence-checking posture as Self::label).
Sourcepub fn linear_projection(&self) -> Result<Vec<ChatMessage>, InterchangeError>
pub fn linear_projection(&self) -> Result<Vec<ChatMessage>, InterchangeError>
The linear projection of the ACTIVE branch (C7): walk from the root to
the active branch’s leaf via parent pointers, returning the messages
in root→leaf order. This is what any linear consumer (the agent loop,
an exporter) must see. Vec::new() for an empty tree (no leaf yet).
Fail-closed. This is a thin self.active_branch-bound wrapper
around Self::linear_projection_of and propagates its Err
(a missing active branch, a cycle, a dangling leaf) rather than
masking it to an empty Vec — a structurally-corrupt tree must ERROR,
never silently look like a session with zero messages. (An earlier
version of this method used .unwrap_or_default() here, which let a
corrupt-but-valid-JSON .tree.json sidecar pass Self::linear_projection
straight through to crate::session::Session::apply_session_tree
and silently EMPTY crate::session::Session::messages — see that
method’s doc comment.)
Sourcepub fn linear_projection_of(
&self,
branch: &str,
) -> Result<Vec<ChatMessage>, InterchangeError>
pub fn linear_projection_of( &self, branch: &str, ) -> Result<Vec<ChatMessage>, InterchangeError>
The linear projection of any named branch (not just the active one) —
the general form Self::linear_projection is built on. Errors if
branch doesn’t exist; returns Ok(Vec::new()) for a branch with no
leaf yet (a fresh, still-empty tree’s main).
Defensively cycle-guarded: a malformed/hand-edited tree with a parent
cycle returns an error instead of looping forever — this ties into
the “a rewind to a nonexistent node is an error, not corruption”
requirement’s sibling guarantee (no API in this module can ever
CREATE a cycle — Self::append_message’s parent is always the
pre-existing leaf, Self::rewind/Self::branch only ever move a
leaf POINTER to an existing node, never rewrite a parent link — but
a tree loaded from a hand-edited or corrupted .tree.json sidecar
could still contain one, and this must not hang or panic on it).
Sourcepub fn has_branches(&self) -> bool
pub fn has_branches(&self) -> bool
Whether this tree has actually branched (more than just the implicit
MAIN_BRANCH) — i.e. it is no longer the degenerate single-path
case. A caller can use this to decide whether a .tree.json sidecar
is even worth persisting (a never-branched tree is exactly the
pre-existing linear session, byte for byte, so the C7 default-off
posture never requires writing one).
Sourcepub fn summarize_branch(
&mut self,
branch: &str,
summary: impl Into<String>,
model_id: Option<String>,
timestamp_ms: i64,
) -> Result<(), InterchangeError>
pub fn summarize_branch( &mut self, branch: &str, summary: impl Into<String>, model_id: Option<String>, timestamp_ms: i64, ) -> Result<(), InterchangeError>
Attach a caller-provided summary to branch directly (module 21
“branch summaries”). node_id records which node the summary is
as-of (the branch’s current leaf, normally); model_id is None for
a caller-provided (not model-generated) summary. Errors if branch
doesn’t exist.
Errors if branch has no leaf yet (a brand-new, still-empty branch) —
a leafless branch has no node to summarize as-of, and recording a
BranchSummary::node_id of "" would be a pointer to a node that
doesn’t exist (F4: never fabricate a dangling pointer).
Sourcepub fn render_branch_text(
&self,
branch: &str,
) -> Result<String, InterchangeError>
pub fn render_branch_text( &self, branch: &str, ) -> Result<String, InterchangeError>
Render a branch’s linear projection into plain text (one line per
turn, role: content) — the input a BranchSummarizer side-call
summarizes, mirroring reduce/summarize.rs’s render_span_text
shape.
Sourcepub fn summarize_branch_with(
&mut self,
branch: &str,
summarizer: &dyn BranchSummarizer,
timestamp_ms: i64,
) -> Result<(), InterchangeError>
pub fn summarize_branch_with( &mut self, branch: &str, summarizer: &dyn BranchSummarizer, timestamp_ms: i64, ) -> Result<(), InterchangeError>
Summarize branch via a small-model side-call (D-9, the mechanism
an optional caller-supplied branch summarizer
also uses): renders the branch’s text
(Self::render_branch_text) and calls summarizer. Never fails
the caller — mirroring reduce/summarize.rs’s “never blocks, never
fails the pass” posture: if summarizer errors (a timeout, a
provider error, budget exhaustion — whatever it models), this falls
back to a deterministic stub summary ("[N turns, unsummarized]")
rather than propagating the error, so a C7 export can always
complete. Errors only if branch itself doesn’t exist.
Sourcepub fn splice_for_linear_export(
&self,
) -> Result<(Vec<ChatMessage>, Vec<BranchSummary>), InterchangeError>
pub fn splice_for_linear_export( &self, ) -> Result<(Vec<ChatMessage>, Vec<BranchSummary>), InterchangeError>
C7 export mechanism: splice the ACTIVE branch’s messages (exactly
Self::linear_projection — what a strictly-linear export target,
e.g. the CX rollout shape, can represent) plus a BranchSummary
for every OFF-path branch (every branch other than the active one).
An off-path branch that already carries a Branch::summary reuses
it as-is; one that doesn’t gets a fresh deterministic stub summary
("[N turn(s), unsummarized]") — this method takes &self (read
only) precisely so it never needs a live BranchSummarizer side-call
inline; a caller wanting model-generated summaries should call
Self::summarize_branch_with on each off-path branch FIRST, then
call this. Nothing here mutates or drops any node — see the module
doc’s “Lossless rewind” / C7 sections: the full multi-branch
SessionTree (this method’s &self receiver) remains the
recoverable source of truth regardless of what the caller does with
the returned linear messages.
Fail-closed on the active path, same posture as
Self::linear_projection: a corrupt active branch errors instead of
silently exporting an empty transcript (F2). Off-path branches are
summarized best-effort (see Self::summarize_branch_with’s “never
blocks” contract) — a corrupt OFF-path branch does not fail the whole
export, but never claims false turn-count precision either; see
BranchSummary’s construction below.
Trait Implementations§
Source§impl Clone for SessionTree
impl Clone for SessionTree
Source§fn clone(&self) -> SessionTree
fn clone(&self) -> SessionTree
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more