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 active_path(&self) -> Result<Vec<String>, InterchangeError>
pub fn active_path(&self) -> Result<Vec<String>, InterchangeError>
BP-8 (catalog:151): the NODE IDS along the active branch, root-first
— the id-level twin of Self::linear_projection, which returns the
same nodes’ messages. A caller that knows a conversation POSITION
(an index into the linear view) needs this to name the node at that
position, which is what “move the leaf anywhere” requires. Same
cycle guard and same fail-closed posture as the projection.
Sourcepub fn path_of(&self, branch: &str) -> Result<Vec<String>, InterchangeError>
pub fn path_of(&self, branch: &str) -> Result<Vec<String>, InterchangeError>
Self::active_path for any named branch.
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