Skip to main content

codetether_agent/session/
lifecycle.rs

1//! Session construction, agent-name / provenance, and message appending.
2
3use std::sync::Arc;
4
5use anyhow::Result;
6use chrono::Utc;
7use uuid::Uuid;
8
9use crate::agent::ToolUse;
10use crate::provenance::{ClaimProvenance, ExecutionProvenance};
11use crate::provider::{Message, Usage};
12
13use super::pages::{PageKind, classify, classify_all};
14use super::types::{Session, SessionMetadata};
15
16impl Session {
17    /// Create a new empty session rooted at the current working directory.
18    ///
19    /// # Errors
20    ///
21    /// Returns an error if the current working directory cannot be resolved.
22    ///
23    /// # Examples
24    ///
25    /// ```rust,no_run
26    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
27    /// use codetether_agent::session::Session;
28    ///
29    /// let session = Session::new().await.unwrap();
30    /// assert!(!session.id.is_empty());
31    /// assert_eq!(session.agent, "build");
32    /// assert!(session.messages.is_empty());
33    /// # });
34    /// ```
35    pub async fn new() -> Result<Self> {
36        let id = Uuid::new_v4().to_string();
37        let now = Utc::now();
38        let provenance = Some(ExecutionProvenance::for_session(&id, "build"));
39        let history_sink = crate::session::history_sink::HistorySinkConfig::from_env()
40            .ok()
41            .flatten();
42
43        Ok(Self {
44            id,
45            title: None,
46            created_at: now,
47            updated_at: now,
48            messages: Vec::new(),
49            pages: Vec::new(),
50            summary_index: super::index::SummaryIndex::new(),
51            tool_uses: Vec::<ToolUse>::new(),
52            usage: Usage::default(),
53            agent: "build".to_string(),
54            metadata: SessionMetadata {
55                directory: Some(std::env::current_dir()?),
56                provenance,
57                history_sink,
58                ..Default::default()
59            },
60            max_steps: None,
61            bus: None,
62        })
63    }
64
65    /// Attach an agent bus for publishing agent thinking/reasoning events.
66    pub fn with_bus(mut self, bus: Arc<crate::bus::AgentBus>) -> Self {
67        self.bus = Some(bus);
68        self
69    }
70
71    /// Reattach the process-global bus after loading a session from disk.
72    pub(crate) fn attach_global_bus_if_missing(&mut self) {
73        if self.bus.is_none() {
74            self.bus = crate::bus::global();
75        }
76    }
77
78    /// Rebuild / hydrate runtime sidecars that legacy sessions do not
79    /// carry on disk.
80    pub(crate) fn normalize_sidecars(&mut self) {
81        self.attach_global_bus_if_missing();
82        if self.pages.len() != self.messages.len() {
83            self.pages = classify_all(&self.messages);
84        }
85        if self.metadata.history_sink.is_none() {
86            self.metadata.history_sink =
87                crate::session::history_sink::HistorySinkConfig::from_env()
88                    .ok()
89                    .flatten();
90        }
91    }
92
93    /// Seed session metadata from a loaded [`crate::config::Config`].
94    ///
95    /// Currently copies [`crate::config::Config::rlm`] into
96    /// [`SessionMetadata::rlm`] so RLM compaction and tool-output routing
97    /// honour user-configured thresholds, iteration limits, and model
98    /// selectors.
99    ///
100    /// Also attempts to resolve [`RlmConfig::subcall_model`] against the
101    /// given provider registry. When resolution succeeds the resolved
102    /// provider is cached on [`SessionMetadata`] (not serialised) so
103    /// every `AutoProcessContext` built from this session can cheaply
104    /// reference it. On failure the subcall provider is left as `None`
105    /// and the resolution failure is logged.
106    ///
107    /// Idempotent: re-applying the same config is a no-op.
108    ///
109    /// # Examples
110    ///
111    /// ```rust,no_run
112    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
113    /// use codetether_agent::config::Config;
114    /// use codetether_agent::session::Session;
115    ///
116    /// let cfg = Config::default();
117    /// let mut session = Session::new().await.unwrap();
118    /// session.apply_config(&cfg, None);
119    /// assert_eq!(session.metadata.rlm.mode, cfg.rlm.mode);
120    /// # });
121    /// ```
122    pub fn apply_config(
123        &mut self,
124        config: &crate::config::Config,
125        registry: Option<&crate::provider::ProviderRegistry>,
126    ) {
127        self.metadata.rlm = config.rlm.clone();
128
129        // Resolve subcall_model into a provider, if configured.
130        self.metadata.subcall_provider = None;
131        self.metadata.subcall_model_name = None;
132
133        if let Some(ref subcall_model_str) = config.rlm.subcall_model
134            && let Some(reg) = registry
135        {
136            match reg.resolve_model(subcall_model_str) {
137                Ok((provider, model_name)) => {
138                    self.metadata.subcall_provider = Some(provider);
139                    self.metadata.subcall_model_name = Some(model_name);
140                }
141                Err(e) => {
142                    tracing::warn!(
143                        configured = %subcall_model_str,
144                        error = %e,
145                        "RLM subcall_model resolution failed; subcalls will use root model"
146                    );
147                }
148            }
149        }
150    }
151
152    /// Attempt to resolve [`RlmConfig::subcall_model`] against the given
153    /// provider registry, storing the result on metadata.
154    ///
155    /// Called by session helpers right before building an
156    /// [`AutoProcessContext`](crate::rlm::router::AutoProcessContext) if
157    /// `subcall_provider` is still `None` but `subcall_model` is configured.
158    /// This deferred resolution avoids requiring the registry at session
159    /// creation time.
160    ///
161    /// # Errors
162    ///
163    /// Does **not** return errors — resolution failure is logged.
164    pub fn resolve_subcall_provider(&mut self, registry: &crate::provider::ProviderRegistry) {
165        if self.metadata.subcall_provider.is_some() {
166            return; // Already resolved.
167        }
168        if let Some(ref subcall_model_str) = self.metadata.rlm.subcall_model {
169            match registry.resolve_model(subcall_model_str) {
170                Ok((provider, model_name)) => {
171                    tracing::debug!(
172                        subcall_model = %model_name,
173                        "RLM: resolved subcall provider"
174                    );
175                    self.metadata.subcall_provider = Some(provider);
176                    self.metadata.subcall_model_name = Some(model_name);
177                }
178                Err(e) => {
179                    tracing::warn!(
180                        configured = %subcall_model_str,
181                        error = %e,
182                        "RLM subcall_model resolution failed; subcalls will use root model"
183                    );
184                }
185            }
186        }
187    }
188
189    /// Set the agent persona owning this session. Also updates the
190    /// provenance record so audit logs reflect the new agent.
191    pub fn set_agent_name(&mut self, agent_name: impl Into<String>) {
192        let agent_name = agent_name.into();
193        self.agent = agent_name.clone();
194        if let Some(provenance) = self.metadata.provenance.as_mut() {
195            provenance.set_agent_name(&agent_name);
196        }
197    }
198
199    /// Tag the session as having been dispatched by a specific A2A worker
200    /// for a specific task.
201    pub fn attach_worker_task_provenance(&mut self, worker_id: &str, task_id: &str) {
202        if let Some(provenance) = self.metadata.provenance.as_mut() {
203            provenance.apply_worker_task(worker_id, task_id);
204        }
205    }
206
207    /// Attach a claim-provenance record to the session's execution
208    /// provenance.
209    pub fn attach_claim_provenance(&mut self, claim: &ClaimProvenance) {
210        if let Some(provenance) = self.metadata.provenance.as_mut() {
211            provenance.apply_claim(claim);
212        }
213    }
214
215    /// Append a message to the transcript and bump `updated_at`.
216    pub fn add_message(&mut self, message: Message) {
217        if self.pages.len() != self.messages.len() {
218            self.pages = classify_all(&self.messages);
219        }
220        self.pages.push(classify(&message));
221        let appended_idx = self.messages.len();
222        self.messages.push(message);
223        self.summary_index.append(appended_idx);
224        self.updated_at = Utc::now();
225    }
226
227    /// Borrow the chat-history transcript as an immutable slice.
228    ///
229    /// Preferred read path now that [`Self::messages`] is scheduled for
230    /// visibility tightening — see the Phase A plan for details. Callers
231    /// who need mutable, append-only access should wrap the buffer in a
232    /// [`History`](super::history::History) handle obtained via
233    /// [`Self::history_mut`].
234    ///
235    /// # Examples
236    ///
237    /// ```rust,no_run
238    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
239    /// use codetether_agent::session::Session;
240    ///
241    /// let session = Session::new().await.unwrap();
242    /// assert!(session.history().is_empty());
243    /// # });
244    /// ```
245    pub fn history(&self) -> &[Message] {
246        &self.messages
247    }
248
249    /// Borrow the per-message page sidecar.
250    pub fn pages(&self) -> &[PageKind] {
251        &self.pages
252    }
253
254    /// Borrow the chat-history transcript as an append-only
255    /// [`History`](super::history::History) handle.
256    ///
257    /// The returned handle can only grow the buffer via
258    /// [`History::append`](super::history::History::append). Any mutation
259    /// that would violate the Phase A invariant (destructive in-place
260    /// rewrite) is not reachable through this API surface.
261    pub fn history_mut(&mut self) -> super::history::History<'_> {
262        super::history::History::with_pages(&mut self.messages, &mut self.pages)
263    }
264}