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}