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