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 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> 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 = Infallible

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

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

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