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
66impl TurnContinuation {
67 #[must_use]
69 pub fn fresh() -> Self {
70 Self {
71 kind: TurnContinuationKind::Fresh,
72 }
73 }
74
75 #[must_use]
77 pub fn replaying(replay_transcript: String) -> Self {
78 Self {
79 kind: TurnContinuationKind::Replay { replay_transcript },
80 }
81 }
82
83 #[must_use]
86 pub fn provider(
87 live_transcript: Option<Arc<dyn LiveTranscript>>,
88 persisted_instruction_conversation_id: Option<String>,
89 provider_conversation_id: Option<String>,
90 replay_transcript: Option<String>,
91 ) -> Self {
92 Self {
93 kind: TurnContinuationKind::Provider {
94 live_transcript,
95 persisted_instruction_conversation_id,
96 provider_conversation_id,
97 replay_transcript,
98 },
99 }
100 }
101
102 #[must_use]
104 pub fn replay_transcript(&self) -> Option<&str> {
105 match &self.kind {
106 TurnContinuationKind::Fresh => None,
107 TurnContinuationKind::Provider {
108 replay_transcript, ..
109 } => replay_transcript.as_deref(),
110 TurnContinuationKind::Replay { replay_transcript } => Some(replay_transcript.as_str()),
111 }
112 }
113
114 #[must_use]
116 pub fn provider_conversation_id(&self) -> Option<&str> {
117 match &self.kind {
118 TurnContinuationKind::Provider {
119 provider_conversation_id,
120 ..
121 } => provider_conversation_id.as_deref(),
122 TurnContinuationKind::Fresh | TurnContinuationKind::Replay { .. } => None,
123 }
124 }
125
126 #[must_use]
129 pub fn persisted_instruction_conversation_id(&self) -> Option<&str> {
130 match &self.kind {
131 TurnContinuationKind::Provider {
132 persisted_instruction_conversation_id,
133 ..
134 } => persisted_instruction_conversation_id.as_deref(),
135 TurnContinuationKind::Fresh | TurnContinuationKind::Replay { .. } => None,
136 }
137 }
138
139 pub(crate) fn into_parts(self) -> TurnContinuationParts {
140 match self.kind {
141 TurnContinuationKind::Fresh => TurnContinuationParts::default(),
142 TurnContinuationKind::Replay { replay_transcript } => TurnContinuationParts {
143 replay_transcript: Some(replay_transcript),
144 ..TurnContinuationParts::default()
145 },
146 TurnContinuationKind::Provider {
147 live_transcript,
148 persisted_instruction_conversation_id,
149 provider_conversation_id,
150 replay_transcript,
151 } => TurnContinuationParts {
152 live_transcript,
153 persisted_instruction_conversation_id,
154 provider_conversation_id,
155 replay_transcript,
156 },
157 }
158 }
159}
160
161#[derive(Clone, Debug)]
162enum TurnContinuationKind {
163 Fresh,
164 Provider {
165 live_transcript: Option<Arc<dyn LiveTranscript>>,
166 persisted_instruction_conversation_id: Option<String>,
167 provider_conversation_id: Option<String>,
168 replay_transcript: Option<String>,
169 },
170 Replay {
171 replay_transcript: String,
172 },
173}
174
175#[derive(Default)]
176pub(crate) struct TurnContinuationParts {
177 pub(crate) live_transcript: Option<Arc<dyn LiveTranscript>>,
178 pub(crate) persisted_instruction_conversation_id: Option<String>,
179 pub(crate) provider_conversation_id: Option<String>,
180 pub(crate) replay_transcript: Option<String>,
181}
182
183#[derive(Debug, Clone)]
185pub struct TurnRequest {
186 pub continuation: TurnContinuation,
188 pub folder: PathBuf,
190 pub main_checkout_root: Option<PathBuf>,
193 pub model: String,
195 pub prompt: TurnPrompt,
197 pub reasoning_level: ReasoningLevel,
201 pub request_kind: AgentRequestKind,
204}
205
206#[derive(Clone, Debug, PartialEq)]
212pub enum TurnEvent {
213 ThoughtDelta(String),
216 Completed {
218 context_reset: bool,
220 input_tokens: u64,
222 output_tokens: u64,
224 },
225 Failed(String),
227 PidUpdate(Option<u32>),
233}
234
235#[derive(Debug)]
237pub struct TurnResult {
238 pub assistant_message: AgentResponse,
240 pub context_reset: bool,
242 pub input_tokens: u64,
244 pub output_tokens: u64,
246 pub provider_conversation_id: Option<String>,
251}
252
253pub struct SessionRef {
255 pub session_id: String,
257}
258
259pub struct StartSessionRequest {
261 pub folder: PathBuf,
263 pub session_id: String,
265}
266
267#[derive(Debug, thiserror::Error)]
272pub enum AgentError {
273 #[error(transparent)]
276 AppServer(#[from] crate::app_server::AppServerError),
277
278 #[error("{0}")]
280 Backend(String),
281
282 #[error("{0}")]
284 InterruptedByUser(String),
285
286 #[error("{0}")]
288 Io(String),
289}
290
291#[cfg_attr(any(test, feature = "test-utils"), mockall::automock)]
297pub trait AgentChannel: Send + Sync {
298 fn start_session(
303 &self,
304 req: StartSessionRequest,
305 ) -> AgentFuture<Result<SessionRef, AgentError>>;
306
307 fn run_turn(
317 &self,
318 session_id: String,
319 req: TurnRequest,
320 events: mpsc::UnboundedSender<TurnEvent>,
321 ) -> AgentFuture<Result<TurnResult, AgentError>>;
322
323 fn shutdown_session(&self, session_id: String) -> AgentFuture<Result<(), AgentError>>;
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333
334 #[test]
335 fn test_turn_continuation_fresh_has_no_context() {
336 let continuation = TurnContinuation::fresh();
338
339 assert_eq!(continuation.replay_transcript(), None);
341 assert_eq!(continuation.provider_conversation_id(), None);
342 assert_eq!(continuation.persisted_instruction_conversation_id(), None);
343 }
344
345 #[test]
346 fn test_turn_continuation_replaying_exposes_transcript_only() {
347 let continuation = TurnContinuation::replaying("prior turn".to_string());
349
350 let parts = continuation.clone().into_parts();
352
353 assert_eq!(continuation.replay_transcript(), Some("prior turn"));
355 assert_eq!(continuation.provider_conversation_id(), None);
356 assert!(parts.live_transcript.is_none());
357 assert_eq!(parts.persisted_instruction_conversation_id, None);
358 assert_eq!(parts.provider_conversation_id, None);
359 assert_eq!(parts.replay_transcript.as_deref(), Some("prior turn"));
360 }
361
362 #[test]
363 fn test_turn_continuation_provider_exposes_persisted_context() {
364 let continuation = TurnContinuation::provider(
366 None,
367 Some("instruction-1".to_string()),
368 Some("thread-1".to_string()),
369 Some("prior turn".to_string()),
370 );
371
372 assert_eq!(continuation.replay_transcript(), Some("prior turn"));
374 assert_eq!(continuation.provider_conversation_id(), Some("thread-1"));
375 assert_eq!(
376 continuation.persisted_instruction_conversation_id(),
377 Some("instruction-1")
378 );
379 }
380
381 #[test]
382 fn test_agent_request_kind_session_variants_use_session_protocol_profile() {
385 let start = AgentRequestKind::SessionStart;
387 let resume = AgentRequestKind::SessionResume;
388
389 let start_profile = start.protocol_profile();
391 let resume_profile = resume.protocol_profile();
392
393 assert_eq!(start_profile, ProtocolRequestProfile::SessionTurn);
395 assert_eq!(resume_profile, ProtocolRequestProfile::SessionTurn);
396 }
397
398 #[test]
399 fn test_agent_request_kind_utility_prompt_uses_utility_protocol_profile() {
401 let request_kind = AgentRequestKind::UtilityPrompt;
403
404 let protocol_profile = request_kind.protocol_profile();
406
407 assert_eq!(protocol_profile, ProtocolRequestProfile::UtilityPrompt);
409 }
410
411 #[test]
412 fn test_agent_request_kind_account_read_uses_utility_protocol_profile() {
414 let request_kind = AgentRequestKind::AccountRead;
416
417 let protocol_profile = request_kind.protocol_profile();
419
420 assert_eq!(protocol_profile, ProtocolRequestProfile::UtilityPrompt);
422 }
423}