1use chrono::Utc;
4
5use agentyk_core::cancellation::CancellationToken;
6use agentyk_core::driver::{ModelSpec, Usage};
7use agentyk_core::error::Result;
8use agentyk_core::event::{EventData, EventRequest, notify_event_listeners};
9use agentyk_core::event_log::{EventLog, ExpectedVersion};
10use agentyk_core::id::{EventId, SessionId, TurnId};
11use agentyk_core::input::InputQueue;
12use agentyk_core::replay::History;
13
14use crate::agent::{AgentDefinition, AgentEnvironment};
15#[derive(Debug, Clone)]
17#[non_exhaustive]
18pub struct TurnResult {
19 pub turn_id: TurnId,
21 pub response: String,
23 pub iterations: usize,
25 pub tool_calls: usize,
27 pub usage: Usage,
29}
30
31impl TurnResult {
32 pub fn new(
34 turn_id: TurnId,
35 response: impl Into<String>,
36 iterations: usize,
37 tool_calls: usize,
38 usage: Usage,
39 ) -> Self {
40 Self {
41 turn_id,
42 response: response.into(),
43 iterations,
44 tool_calls,
45 usage,
46 }
47 }
48}
49
50#[non_exhaustive]
52pub struct TurnHost<'a> {
53 pub session_id: SessionId,
55 pub definition: &'a AgentDefinition,
57 pub environment: &'a AgentEnvironment,
59 pub model: &'a ModelSpec,
61 pub log: &'a dyn EventLog,
63 pub history: &'a mut History,
65 pub cancellation: CancellationToken,
67 pub input: InputQueue,
71}
72
73impl<'a> TurnHost<'a> {
74 pub fn new(
76 session_id: SessionId,
77 definition: &'a AgentDefinition,
78 environment: &'a AgentEnvironment,
79 log: &'a dyn EventLog,
80 history: &'a mut History,
81 ) -> Self {
82 Self {
83 session_id,
84 definition,
85 environment,
86 model: &definition.model,
87 log,
88 history,
89 cancellation: CancellationToken::new(),
90 input: InputQueue::new(),
91 }
92 }
93
94 pub fn model(mut self, model: &'a ModelSpec) -> Self {
96 self.model = model;
97 self
98 }
99
100 pub fn cancellation(mut self, token: CancellationToken) -> Self {
102 self.cancellation = token;
103 self
104 }
105
106 pub fn input(mut self, input: InputQueue) -> Self {
108 self.input = input;
109 self
110 }
111
112 pub async fn record(&mut self, turn_id: TurnId, effects: Vec<EventData>) -> Result<()> {
115 let mut durable = Vec::new();
116 let mut ephemeral = Vec::new();
117 for data in effects {
118 let request = EventRequest::with_turn(self.session_id, turn_id, data);
119 if request.data.is_ephemeral() {
120 ephemeral.push(request.into_ephemeral_event(EventId::new(), Utc::now()));
121 } else {
122 durable.push(request);
123 }
124 }
125
126 let recorded = if durable.is_empty() {
127 Vec::new()
128 } else {
129 let version = self.log.head(self.session_id).await?.sequence;
130 self.log
131 .append_batch(self.session_id, ExpectedVersion::Exact(version), durable)
132 .await?
133 };
134
135 for event in recorded.into_iter().chain(ephemeral) {
136 if event.sequence.is_some() {
137 self.history.apply(&event.data);
138 }
139 notify_event_listeners(&self.environment.listeners, &event).await;
140 }
141 Ok(())
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use std::pin::pin;
148 use std::sync::Arc;
149 use std::sync::atomic::{AtomicU32, Ordering};
150 use std::task::{Context, Poll, Waker};
151
152 use agentyk_core::context::PassthroughContextAssembler;
153 use agentyk_core::driver::{DriverRegistry, ModelSpec};
154 use agentyk_core::event::{Event, EventListener, EventRequest, event_types};
155 use agentyk_core::event_log::EventStore;
156 use agentyk_core::extensions::Extensions;
157 use agentyk_core::id::MessageId;
158
159 use super::*;
160 use crate::agent::{AgentDefinition, AgentEnvironment};
161
162 struct RejectingStore;
163
164 struct FilteredListener {
165 count: Arc<AtomicU32>,
166 }
167
168 #[async_trait::async_trait]
169 impl EventListener for FilteredListener {
170 async fn on_event(&self, _event: &Event) {
171 self.count.fetch_add(1, Ordering::SeqCst);
172 }
173
174 fn event_types(&self) -> Option<Vec<&'static str>> {
175 Some(vec![event_types::INPUT_MESSAGE])
176 }
177 }
178
179 #[async_trait::async_trait]
180 impl EventStore for RejectingStore {
181 async fn append_batch(
182 &self,
183 _session_id: SessionId,
184 _expected: ExpectedVersion,
185 _requests: Vec<EventRequest>,
186 ) -> Result<Vec<Event>> {
187 panic!("ephemeral events must not reach storage")
188 }
189
190 async fn read_after(
191 &self,
192 _session_id: SessionId,
193 _sequence: Option<u64>,
194 ) -> Result<Vec<Event>> {
195 panic!("ephemeral events must not read storage")
196 }
197 }
198
199 struct ConflictingStore;
200
201 #[async_trait::async_trait]
202 impl EventStore for ConflictingStore {
203 async fn append_batch(
204 &self,
205 _session_id: SessionId,
206 _expected: ExpectedVersion,
207 _requests: Vec<EventRequest>,
208 ) -> Result<Vec<Event>> {
209 Err(agentyk_core::error::Error::EventConflict {
210 expected: 0,
211 actual: 1,
212 })
213 }
214
215 async fn read_after(
216 &self,
217 _session_id: SessionId,
218 _sequence: Option<u64>,
219 ) -> Result<Vec<Event>> {
220 Ok(Vec::new())
221 }
222 }
223
224 fn block_on<F: std::future::Future>(future: F) -> F::Output {
225 let mut future = pin!(future);
226 let waker = Waker::noop();
227 let mut context = Context::from_waker(waker);
228 loop {
229 if let Poll::Ready(output) = future.as_mut().poll(&mut context) {
230 return output;
231 }
232 }
233 }
234
235 #[test]
236 fn ephemeral_events_bypass_the_store_entirely() {
237 let definition = AgentDefinition {
238 name: "test".into(),
239 instructions: String::new(),
240 model: ModelSpec::llmsim(),
241 capabilities: Vec::new(),
242 max_iterations: 4,
243 middleware: Vec::new(),
244 hooks: Vec::new(),
245 budget_checker: None,
246 context_assembler: Arc::new(PassthroughContextAssembler),
247 model_profile: None,
248 context_token_limit: None,
249 };
250 let environment = AgentEnvironment {
251 drivers: DriverRegistry::new(),
252 listeners: Vec::new(),
253 extensions: Extensions::new(),
254 };
255 let session_id = SessionId::new();
256 let turn_id = TurnId::new();
257 let mut history = History::new();
258 let mut host = TurnHost::new(
259 session_id,
260 &definition,
261 &environment,
262 &RejectingStore,
263 &mut history,
264 );
265
266 block_on(host.record(
267 turn_id,
268 vec![EventData::OutputMessageDelta {
269 message_id: MessageId::new(),
270 delta: "a".into(),
271 accumulated: "a".into(),
272 }],
273 ))
274 .unwrap();
275 }
276
277 #[test]
278 fn host_honors_listener_event_type_filters() {
279 let definition = AgentDefinition {
280 name: "test".into(),
281 instructions: String::new(),
282 model: ModelSpec::llmsim(),
283 capabilities: Vec::new(),
284 max_iterations: 4,
285 middleware: Vec::new(),
286 hooks: Vec::new(),
287 budget_checker: None,
288 context_assembler: Arc::new(PassthroughContextAssembler),
289 model_profile: None,
290 context_token_limit: None,
291 };
292 let count = Arc::new(AtomicU32::new(0));
293 let environment = AgentEnvironment {
294 drivers: DriverRegistry::new(),
295 listeners: vec![Arc::new(FilteredListener {
296 count: count.clone(),
297 })],
298 extensions: Extensions::new(),
299 };
300 let mut history = History::new();
301 let mut host = TurnHost::new(
302 SessionId::new(),
303 &definition,
304 &environment,
305 &RejectingStore,
306 &mut history,
307 );
308
309 block_on(host.record(
310 TurnId::new(),
311 vec![EventData::OutputMessageDelta {
312 message_id: MessageId::new(),
313 delta: "a".into(),
314 accumulated: "a".into(),
315 }],
316 ))
317 .unwrap();
318
319 assert_eq!(count.load(Ordering::SeqCst), 0);
320 }
321
322 #[test]
323 fn failed_append_does_not_advance_history() {
324 let definition = AgentDefinition {
325 name: "test".into(),
326 instructions: String::new(),
327 model: ModelSpec::llmsim(),
328 capabilities: Vec::new(),
329 max_iterations: 4,
330 middleware: Vec::new(),
331 hooks: Vec::new(),
332 budget_checker: None,
333 context_assembler: Arc::new(PassthroughContextAssembler),
334 model_profile: None,
335 context_token_limit: None,
336 };
337 let environment = AgentEnvironment {
338 drivers: DriverRegistry::new(),
339 listeners: Vec::new(),
340 extensions: Extensions::new(),
341 };
342 let session_id = SessionId::new();
343 let mut history = History::new();
344 let mut host = TurnHost::new(
345 session_id,
346 &definition,
347 &environment,
348 &ConflictingStore,
349 &mut history,
350 );
351
352 let error = block_on(host.record(
353 TurnId::new(),
354 vec![EventData::InputMessage {
355 message: agentyk_core::message::Message::user("not durable"),
356 }],
357 ))
358 .unwrap_err();
359
360 assert!(matches!(
361 error,
362 agentyk_core::error::Error::EventConflict { .. }
363 ));
364 assert!(history.messages().is_empty());
365 }
366}