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}