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}