1use std::fmt;
4use std::future::Future;
5use std::path::PathBuf;
6use std::pin::Pin;
7use std::sync::Arc;
8
9use ag_protocol::{AgentResponse, ProtocolRequestProfile, TurnPrompt};
10use tokio::sync::mpsc;
11
12use crate::model::agent::ReasoningLevel;
13
14pub type AgentFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>;
16
17pub trait LiveTranscript: fmt::Debug + Send + Sync {
19 fn replay_text(&self) -> Option<String>;
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum AgentRequestKind {
26 SessionStart,
28 SessionResume,
30 UtilityPrompt,
36 AccountRead,
38}
39
40impl AgentRequestKind {
41 #[must_use]
43 pub fn protocol_profile(&self) -> ProtocolRequestProfile {
44 match self {
45 Self::SessionStart | Self::SessionResume => ProtocolRequestProfile::SessionTurn,
46 Self::UtilityPrompt | Self::AccountRead => ProtocolRequestProfile::UtilityPrompt,
47 }
48 }
49
50 #[must_use]
52 pub fn is_resume(&self) -> bool {
53 matches!(self, Self::SessionResume)
54 }
55}
56
57#[derive(Clone, Debug)]
62pub struct TurnContinuation {
63 kind: TurnContinuationKind,
64}
65
66#[derive(Clone, Debug, Default, Eq, PartialEq)]
68pub struct PersonalityPrompt {
69 current: Option<String>,
70 update: PersonalityPromptUpdate,
71}
72
73impl PersonalityPrompt {
74 #[must_use]
77 pub fn active(prompt: String, changed: bool) -> Self {
78 let update = if changed {
79 PersonalityPromptUpdate::Set(prompt.clone())
80 } else {
81 PersonalityPromptUpdate::Unchanged
82 };
83
84 Self {
85 current: Some(prompt),
86 update,
87 }
88 }
89
90 #[must_use]
95 pub fn cleared(changed: bool) -> Self {
96 Self {
97 current: None,
98 update: if changed {
99 PersonalityPromptUpdate::Clear
100 } else {
101 PersonalityPromptUpdate::Unchanged
102 },
103 }
104 }
105
106 #[must_use]
108 pub fn current(&self) -> Option<&str> {
109 self.current.as_deref()
110 }
111
112 pub(crate) fn update(&self) -> &PersonalityPromptUpdate {
113 &self.update
114 }
115}
116
117#[derive(Clone, Debug, Default, Eq, PartialEq)]
119pub(crate) enum PersonalityPromptUpdate {
120 Clear,
122 Set(String),
124 #[default]
126 Unchanged,
127}
128
129impl TurnContinuation {
130 #[must_use]
132 pub fn fresh() -> Self {
133 Self {
134 kind: TurnContinuationKind::Fresh,
135 }
136 }
137
138 #[must_use]
140 pub fn replaying(replay_transcript: String) -> Self {
141 Self {
142 kind: TurnContinuationKind::Replay { replay_transcript },
143 }
144 }
145
146 #[must_use]
149 pub fn provider(
150 live_transcript: Option<Arc<dyn LiveTranscript>>,
151 persisted_instruction_conversation_id: Option<String>,
152 provider_conversation_id: Option<String>,
153 replay_transcript: Option<String>,
154 ) -> Self {
155 Self {
156 kind: TurnContinuationKind::Provider {
157 live_transcript,
158 persisted_instruction_conversation_id,
159 provider_conversation_id,
160 replay_transcript,
161 },
162 }
163 }
164
165 #[must_use]
167 pub fn replay_transcript(&self) -> Option<&str> {
168 match &self.kind {
169 TurnContinuationKind::Fresh => None,
170 TurnContinuationKind::Provider {
171 replay_transcript, ..
172 } => replay_transcript.as_deref(),
173 TurnContinuationKind::Replay { replay_transcript } => Some(replay_transcript.as_str()),
174 }
175 }
176
177 #[must_use]
179 pub fn provider_conversation_id(&self) -> Option<&str> {
180 match &self.kind {
181 TurnContinuationKind::Provider {
182 provider_conversation_id,
183 ..
184 } => provider_conversation_id.as_deref(),
185 TurnContinuationKind::Fresh | TurnContinuationKind::Replay { .. } => None,
186 }
187 }
188
189 #[must_use]
192 pub fn persisted_instruction_conversation_id(&self) -> Option<&str> {
193 match &self.kind {
194 TurnContinuationKind::Provider {
195 persisted_instruction_conversation_id,
196 ..
197 } => persisted_instruction_conversation_id.as_deref(),
198 TurnContinuationKind::Fresh | TurnContinuationKind::Replay { .. } => None,
199 }
200 }
201
202 pub(crate) fn into_parts(self) -> TurnContinuationParts {
203 match self.kind {
204 TurnContinuationKind::Fresh => TurnContinuationParts::default(),
205 TurnContinuationKind::Replay { replay_transcript } => TurnContinuationParts {
206 replay_transcript: Some(replay_transcript),
207 ..TurnContinuationParts::default()
208 },
209 TurnContinuationKind::Provider {
210 live_transcript,
211 persisted_instruction_conversation_id,
212 provider_conversation_id,
213 replay_transcript,
214 } => TurnContinuationParts {
215 live_transcript,
216 persisted_instruction_conversation_id,
217 provider_conversation_id,
218 replay_transcript,
219 },
220 }
221 }
222}
223
224#[derive(Clone, Debug)]
225enum TurnContinuationKind {
226 Fresh,
227 Provider {
228 live_transcript: Option<Arc<dyn LiveTranscript>>,
229 persisted_instruction_conversation_id: Option<String>,
230 provider_conversation_id: Option<String>,
231 replay_transcript: Option<String>,
232 },
233 Replay {
234 replay_transcript: String,
235 },
236}
237
238#[derive(Default)]
239pub(crate) struct TurnContinuationParts {
240 pub(crate) live_transcript: Option<Arc<dyn LiveTranscript>>,
241 pub(crate) persisted_instruction_conversation_id: Option<String>,
242 pub(crate) provider_conversation_id: Option<String>,
243 pub(crate) replay_transcript: Option<String>,
244}
245
246#[derive(Debug, Clone)]
248pub struct TurnRequest {
249 pub continuation: TurnContinuation,
251 pub folder: PathBuf,
253 pub main_checkout_root: Option<PathBuf>,
256 pub model: String,
258 pub personality: PersonalityPrompt,
260 pub prompt: TurnPrompt,
262 pub reasoning_level: ReasoningLevel,
266 pub request_kind: AgentRequestKind,
269}
270
271#[derive(Clone, Debug, PartialEq)]
277pub enum TurnEvent {
278 ThoughtDelta(String),
281 Completed {
283 context_reset: bool,
285 input_tokens: u64,
287 output_tokens: u64,
289 },
290 Failed(String),
292 PidUpdate(Option<u32>),
298}
299
300#[derive(Debug)]
302pub struct TurnResult {
303 pub assistant_message: AgentResponse,
305 pub context_reset: bool,
307 pub input_tokens: u64,
309 pub output_tokens: u64,
311 pub provider_conversation_id: Option<String>,
316}
317
318pub struct SessionRef {
320 pub session_id: String,
322}
323
324pub struct StartSessionRequest {
326 pub folder: PathBuf,
328 pub session_id: String,
330}
331
332#[derive(Debug, thiserror::Error)]
337pub enum AgentError {
338 #[error(transparent)]
341 AppServer(#[from] crate::app_server::AppServerError),
342
343 #[error("{0}")]
345 Backend(String),
346
347 #[error("{0}")]
349 InterruptedByUser(String),
350
351 #[error("{0}")]
353 Io(String),
354}
355
356#[cfg_attr(any(test, feature = "test-utils"), mockall::automock)]
362pub trait AgentChannel: Send + Sync {
363 fn start_session(
368 &self,
369 req: StartSessionRequest,
370 ) -> AgentFuture<Result<SessionRef, AgentError>>;
371
372 fn run_turn(
382 &self,
383 session_id: String,
384 req: TurnRequest,
385 events: mpsc::UnboundedSender<TurnEvent>,
386 ) -> AgentFuture<Result<TurnResult, AgentError>>;
387
388 fn shutdown_session(&self, session_id: String) -> AgentFuture<Result<(), AgentError>>;
393}
394
395#[cfg(test)]
396mod tests {
397 use super::*;
398
399 #[test]
400 fn test_personality_prompt_tracks_active_change_and_clear_state() {
401 let changed = PersonalityPrompt::active("Review carefully.".to_string(), true);
403 let unchanged = PersonalityPrompt::active("Review carefully.".to_string(), false);
404 let cleared = PersonalityPrompt::cleared(true);
405 let empty = PersonalityPrompt::cleared(false);
406
407 assert_eq!(changed.current(), Some("Review carefully."));
409 assert_eq!(
410 changed.update(),
411 &PersonalityPromptUpdate::Set("Review carefully.".to_string())
412 );
413 assert_eq!(unchanged.update(), &PersonalityPromptUpdate::Unchanged);
414 assert_eq!(cleared.current(), None);
415 assert_eq!(cleared.update(), &PersonalityPromptUpdate::Clear);
416 assert_eq!(empty.update(), &PersonalityPromptUpdate::Unchanged);
417 }
418
419 #[test]
420 fn test_turn_continuation_fresh_has_no_context() {
421 let continuation = TurnContinuation::fresh();
423
424 assert_eq!(continuation.replay_transcript(), None);
426 assert_eq!(continuation.provider_conversation_id(), None);
427 assert_eq!(continuation.persisted_instruction_conversation_id(), None);
428 }
429
430 #[test]
431 fn test_turn_continuation_replaying_exposes_transcript_only() {
432 let continuation = TurnContinuation::replaying("prior turn".to_string());
434
435 let parts = continuation.clone().into_parts();
437
438 assert_eq!(continuation.replay_transcript(), Some("prior turn"));
440 assert_eq!(continuation.provider_conversation_id(), None);
441 assert!(parts.live_transcript.is_none());
442 assert_eq!(parts.persisted_instruction_conversation_id, None);
443 assert_eq!(parts.provider_conversation_id, None);
444 assert_eq!(parts.replay_transcript.as_deref(), Some("prior turn"));
445 }
446
447 #[test]
448 fn test_turn_continuation_provider_exposes_persisted_context() {
449 let continuation = TurnContinuation::provider(
451 None,
452 Some("instruction-1".to_string()),
453 Some("thread-1".to_string()),
454 Some("prior turn".to_string()),
455 );
456
457 assert_eq!(continuation.replay_transcript(), Some("prior turn"));
459 assert_eq!(continuation.provider_conversation_id(), Some("thread-1"));
460 assert_eq!(
461 continuation.persisted_instruction_conversation_id(),
462 Some("instruction-1")
463 );
464 }
465
466 #[test]
467 fn test_agent_request_kind_session_variants_use_session_protocol_profile() {
470 let start = AgentRequestKind::SessionStart;
472 let resume = AgentRequestKind::SessionResume;
473
474 let start_profile = start.protocol_profile();
476 let resume_profile = resume.protocol_profile();
477
478 assert_eq!(start_profile, ProtocolRequestProfile::SessionTurn);
480 assert_eq!(resume_profile, ProtocolRequestProfile::SessionTurn);
481 }
482
483 #[test]
484 fn test_agent_request_kind_utility_prompt_uses_utility_protocol_profile() {
486 let request_kind = AgentRequestKind::UtilityPrompt;
488
489 let protocol_profile = request_kind.protocol_profile();
491
492 assert_eq!(protocol_profile, ProtocolRequestProfile::UtilityPrompt);
494 }
495
496 #[test]
497 fn test_agent_request_kind_account_read_uses_utility_protocol_profile() {
499 let request_kind = AgentRequestKind::AccountRead;
501
502 let protocol_profile = request_kind.protocol_profile();
504
505 assert_eq!(protocol_profile, ProtocolRequestProfile::UtilityPrompt);
507 }
508}