Skip to main content

SessionTree

Struct SessionTree 

Source
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: String

The currently-active branch name — a key into Self::branches.

Implementations§

Source§

impl SessionTree

Source

pub fn new() -> SessionTree

A brand-new, empty tree: no nodes, one branch (MAIN_BRANCH) with no leaf yet, active.

Source

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).

Source

pub fn node(&self, id: &str) -> Option<&TreeNode>

Look up a node by id.

Source

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).

Source

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.”

Source

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).

Source

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).

Source

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.

Source

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).

Source

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.)

Source

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).

Source

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.

Source

pub fn path_of(&self, branch: &str) -> Result<Vec<String>, InterchangeError>

Self::active_path for any named branch.

Source

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).

Source

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).

Source

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.

Source

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.

Source

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

Source§

fn clone(&self) -> SessionTree

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for SessionTree

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>

Formats the value using the given formatter. Read more
Source§

impl Default for SessionTree

Source§

fn default() -> SessionTree

Returns the “default value” for a type. Read more
Source§

impl<'de> Deserialize<'de> for SessionTree

Source§

fn deserialize<__D>( __deserializer: __D, ) -> Result<SessionTree, <__D as Deserializer<'de>>::Error>
where __D: Deserializer<'de>,

Deserialize this value from the given Serde deserializer. Read more
Source§

impl Serialize for SessionTree

Source§

fn serialize<__S>( &self, __serializer: __S, ) -> Result<<__S as Serializer>::Ok, <__S as Serializer>::Error>
where __S: Serializer,

Serialize this value into the given Serde serializer. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> DeserializeOwned for T
where T: for<'de> Deserialize<'de>,

Source§

impl<T> DynClone for T
where T: Clone,

Source§

fn __clone_box(&self, _: Private) -> *mut ()

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more