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::types::{Session, SessionMetadata};
14
15impl Session {
16    /// Create a new empty session rooted at the current working directory.
17    ///
18    /// # Errors
19    ///
20    /// Returns an error if the current working directory cannot be resolved.
21    ///
22    /// # Examples
23    ///
24    /// ```rust,no_run
25    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
26    /// use codetether_agent::session::Session;
27    ///
28    /// let session = Session::new().await.unwrap();
29    /// assert!(!session.id.is_empty());
30    /// assert_eq!(session.agent, "build");
31    /// assert!(session.messages.is_empty());
32    /// # });
33    /// ```
34    pub async fn new() -> Result<Self> {
35        let id = Uuid::new_v4().to_string();
36        let now = Utc::now();
37        let provenance = Some(ExecutionProvenance::for_session(&id, "build"));
38
39        Ok(Self {
40            id,
41            title: None,
42            created_at: now,
43            updated_at: now,
44            messages: Vec::new(),
45            tool_uses: Vec::<ToolUse>::new(),
46            usage: Usage::default(),
47            agent: "build".to_string(),
48            metadata: SessionMetadata {
49                directory: Some(std::env::current_dir()?),
50                provenance,
51                ..Default::default()
52            },
53            max_steps: None,
54            bus: None,
55        })
56    }
57
58    /// Attach an agent bus for publishing agent thinking/reasoning events.
59    pub fn with_bus(mut self, bus: Arc<crate::bus::AgentBus>) -> Self {
60        self.bus = Some(bus);
61        self
62    }
63
64    /// Reattach the process-global bus after loading a session from disk.
65    pub(crate) fn attach_global_bus_if_missing(&mut self) {
66        if self.bus.is_none() {
67            self.bus = crate::bus::global();
68        }
69    }
70
71    /// Seed session metadata from a loaded [`crate::config::Config`].
72    ///
73    /// Currently copies [`crate::config::Config::rlm`] into
74    /// [`SessionMetadata::rlm`] so RLM compaction and tool-output routing
75    /// honour user-configured thresholds, iteration limits, and model
76    /// selectors.
77    ///
78    /// Also attempts to resolve [`RlmConfig::subcall_model`] against the
79    /// given provider registry. When resolution succeeds the resolved
80    /// provider is cached on [`SessionMetadata`] (not serialised) so
81    /// every `AutoProcessContext` built from this session can cheaply
82    /// reference it. On failure the subcall provider is left as `None`
83    /// and the resolution failure is logged.
84    ///
85    /// Idempotent: re-applying the same config is a no-op.
86    ///
87    /// # Examples
88    ///
89    /// ```rust,no_run
90    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
91    /// use codetether_agent::config::Config;
92    /// use codetether_agent::session::Session;
93    ///
94    /// let cfg = Config::default();
95    /// let mut session = Session::new().await.unwrap();
96    /// session.apply_config(&cfg, None);
97    /// assert_eq!(session.metadata.rlm.mode, cfg.rlm.mode);
98    /// # });
99    /// ```
100    pub fn apply_config(
101        &mut self,
102        config: &crate::config::Config,
103        registry: Option<&crate::provider::ProviderRegistry>,
104    ) {
105        self.metadata.rlm = config.rlm.clone();
106
107        // Resolve subcall_model into a provider, if configured.
108        self.metadata.subcall_provider = None;
109        self.metadata.subcall_model_name = None;
110
111        if let Some(ref subcall_model_str) = config.rlm.subcall_model
112            && let Some(reg) = registry
113        {
114            match reg.resolve_model(subcall_model_str) {
115                Ok((provider, model_name)) => {
116                    self.metadata.subcall_provider = Some(provider);
117                    self.metadata.subcall_model_name = Some(model_name);
118                }
119                Err(e) => {
120                    tracing::warn!(
121                        configured = %subcall_model_str,
122                        error = %e,
123                        "RLM subcall_model resolution failed; subcalls will use root model"
124                    );
125                }
126            }
127        }
128    }
129
130    /// Attempt to resolve [`RlmConfig::subcall_model`] against the given
131    /// provider registry, storing the result on metadata.
132    ///
133    /// Called by session helpers right before building an
134    /// [`AutoProcessContext`](crate::rlm::router::AutoProcessContext) if
135    /// `subcall_provider` is still `None` but `subcall_model` is configured.
136    /// This deferred resolution avoids requiring the registry at session
137    /// creation time.
138    ///
139    /// # Errors
140    ///
141    /// Does **not** return errors — resolution failure is logged.
142    pub fn resolve_subcall_provider(&mut self, registry: &crate::provider::ProviderRegistry) {
143        if self.metadata.subcall_provider.is_some() {
144            return; // Already resolved.
145        }
146        if let Some(ref subcall_model_str) = self.metadata.rlm.subcall_model {
147            match registry.resolve_model(subcall_model_str) {
148                Ok((provider, model_name)) => {
149                    tracing::debug!(
150                        subcall_model = %model_name,
151                        "RLM: resolved subcall provider"
152                    );
153                    self.metadata.subcall_provider = Some(provider);
154                    self.metadata.subcall_model_name = Some(model_name);
155                }
156                Err(e) => {
157                    tracing::warn!(
158                        configured = %subcall_model_str,
159                        error = %e,
160                        "RLM subcall_model resolution failed; subcalls will use root model"
161                    );
162                }
163            }
164        }
165    }
166
167    /// Set the agent persona owning this session. Also updates the
168    /// provenance record so audit logs reflect the new agent.
169    pub fn set_agent_name(&mut self, agent_name: impl Into<String>) {
170        let agent_name = agent_name.into();
171        self.agent = agent_name.clone();
172        if let Some(provenance) = self.metadata.provenance.as_mut() {
173            provenance.set_agent_name(&agent_name);
174        }
175    }
176
177    /// Tag the session as having been dispatched by a specific A2A worker
178    /// for a specific task.
179    pub fn attach_worker_task_provenance(&mut self, worker_id: &str, task_id: &str) {
180        if let Some(provenance) = self.metadata.provenance.as_mut() {
181            provenance.apply_worker_task(worker_id, task_id);
182        }
183    }
184
185    /// Attach a claim-provenance record to the session's execution
186    /// provenance.
187    pub fn attach_claim_provenance(&mut self, claim: &ClaimProvenance) {
188        if let Some(provenance) = self.metadata.provenance.as_mut() {
189            provenance.apply_claim(claim);
190        }
191    }
192
193    /// Append a message to the transcript and bump `updated_at`.
194    pub fn add_message(&mut self, message: Message) {
195        self.messages.push(message);
196        self.updated_at = Utc::now();
197    }
198}