1use std::sync::Arc;
10
11use agentyk_core::cancellation::CancellationToken;
12use agentyk_core::capability::{CommandContext, CommandDescriptor};
13use agentyk_core::controls::TurnControls;
14use agentyk_core::error::Result;
15use agentyk_core::event::{Event, EventData, EventRequest, notify_event_listeners};
16use agentyk_core::event_log::{EventLog, ExpectedVersion, SessionPoint};
17use agentyk_core::hook::{HookContext, HookEvent};
18use agentyk_core::id::SessionId;
19use agentyk_core::input::InputQueue;
20use agentyk_core::message::Message;
21use agentyk_core::replay::History;
22use agentyk_core::tool::ToolOutput;
23use agentyk_core::turn::TurnState;
24
25use crate::agent::Agent;
26use crate::hooks;
27use crate::host::{TurnHost, TurnResult};
28use crate::in_process::InProcessExecutor;
29
30#[derive(Default)]
34#[non_exhaustive]
35pub struct RunOptions {
36 pub cancellation: CancellationToken,
39 pub controls: TurnControls,
41}
42
43impl RunOptions {
44 pub fn new() -> Self {
46 Self::default()
47 }
48
49 pub fn cancellation(mut self, token: CancellationToken) -> Self {
51 self.cancellation = token;
52 self
53 }
54
55 pub fn controls(mut self, controls: TurnControls) -> Self {
57 self.controls = controls;
58 self
59 }
60}
61
62pub struct Session {
68 agent: Agent,
69 id: SessionId,
70 log: Arc<dyn EventLog>,
71 history: History,
72 input: InputQueue,
73 started: bool,
74 closed: bool,
75}
76
77#[derive(Debug, Clone)]
79pub struct SessionView {
80 point: SessionPoint,
81 events: Vec<Event>,
82 history: History,
83}
84
85impl SessionView {
86 pub fn point(&self) -> SessionPoint {
88 self.point
89 }
90
91 pub fn events(&self) -> &[Event] {
93 &self.events
94 }
95
96 pub fn messages(&self) -> &[Message] {
98 self.history.messages()
99 }
100}
101
102impl Session {
103 pub(crate) fn new(agent: Agent, log: Arc<dyn EventLog>) -> Self {
104 Self {
105 agent,
106 id: SessionId::new(),
107 log,
108 history: History::new(),
109 input: InputQueue::new(),
110 started: false,
111 closed: false,
112 }
113 }
114
115 pub(crate) async fn resume(
116 agent: Agent,
117 log: Arc<dyn EventLog>,
118 session_id: SessionId,
119 ) -> Result<Self> {
120 let events = log.read(session_id).await?;
121 let started = !events.is_empty();
122 Ok(Self {
123 agent,
124 id: session_id,
125 log,
126 history: History::from_events(&events),
127 input: InputQueue::new(),
128 started,
132 closed: false,
133 })
134 }
135
136 pub fn id(&self) -> SessionId {
139 self.id
140 }
141
142 pub fn input(&self) -> InputQueue {
165 self.input.clone()
166 }
167
168 pub fn is_closed(&self) -> bool {
170 self.closed
171 }
172
173 pub async fn close(&mut self) -> Result<()> {
179 if self.closed {
180 return Ok(());
181 }
182 let warnings = hooks::run_advisory_hooks(
183 &self.agent.definition().hooks,
184 HookEvent::SessionEnd,
185 HookContext::session(self.id),
186 serde_json::json!({ "reason": "closed" }),
187 )
188 .await;
189 self.record_session_events(warnings).await?;
190 self.closed = true;
191 Ok(())
192 }
193
194 pub async fn point(&self) -> Result<SessionPoint> {
196 self.log.head(self.id).await
197 }
198
199 pub async fn inspect(&self, point: SessionPoint) -> Result<SessionView> {
201 if point.session_id != self.id {
202 return Err(agentyk_core::error::Error::EventLog(format!(
203 "point belongs to {}, not session {}",
204 point.session_id, self.id
205 )));
206 }
207 let head = self.log.head(self.id).await?;
208 if point.sequence > head.sequence {
209 return Err(agentyk_core::error::Error::EventLog(format!(
210 "point {} is past session head {}",
211 point.sequence, head.sequence
212 )));
213 }
214 let events = self.log.read_through(point).await?;
215 Ok(SessionView {
216 point,
217 history: History::from_events(&events),
218 events,
219 })
220 }
221
222 pub async fn fork(&self, point: SessionPoint) -> Result<Self> {
228 let view = self.inspect(point).await?;
229 if !is_completed_turn_boundary(view.events()) {
230 return Err(agentyk_core::error::Error::EventLog(
231 "sessions can only fork from an empty stream or a completed-turn boundary".into(),
232 ));
233 }
234 let child_id = self.log.fork(point).await?;
235 let mut child = Self::resume(self.agent.clone(), self.log.clone(), child_id).await?;
236 child.started = false;
237 Ok(child)
238 }
239
240 pub async fn resume_pending(&mut self) -> Result<Option<TurnResult>> {
246 self.resume_pending_with_options(RunOptions::default())
247 .await
248 }
249
250 pub async fn resume_pending_with_options(
253 &mut self,
254 options: RunOptions,
255 ) -> Result<Option<TurnResult>> {
256 let agent = self.agent.clone();
257 self.resume_pending_with_agent_and_options(&agent, options)
258 .await
259 }
260
261 pub async fn resume_pending_with_agent(&mut self, agent: &Agent) -> Result<Option<TurnResult>> {
268 self.resume_pending_with_agent_and_options(agent, RunOptions::default())
269 .await
270 }
271
272 pub async fn resume_pending_with_agent_and_options(
274 &mut self,
275 agent: &Agent,
276 options: RunOptions,
277 ) -> Result<Option<TurnResult>> {
278 let events = self.log.read(self.id).await?;
279 let Some(turn_id) = events.iter().rev().find_map(|event| {
280 matches!(event.data, EventData::TurnStarted { .. })
281 .then_some(event.turn_id)
282 .flatten()
283 }) else {
284 return Ok(None);
285 };
286 let state = TurnState::replay(&events, turn_id)?;
287 if state.is_complete() {
288 return Ok(None);
289 }
290
291 let definition = agent.definition();
292 let effective_model = options.controls.resolve(&definition.model);
293 let mut host = TurnHost::new(
294 self.id,
295 definition,
296 self.agent.environment(),
297 self.log.as_ref(),
298 &mut self.history,
299 )
300 .model(&effective_model)
301 .cancellation(options.cancellation);
302 InProcessExecutor::new()
303 .resume_turn(&mut host, state)
304 .await
305 .map(Some)
306 }
307
308 pub fn messages(&self) -> &[Message] {
311 self.history.messages()
312 }
313
314 pub async fn events(&self) -> Result<Vec<Event>> {
316 self.log.read(self.id).await
317 }
318
319 pub fn commands(&self) -> Vec<CommandDescriptor> {
322 self.agent
323 .definition()
324 .capabilities
325 .iter()
326 .flat_map(|capability| capability.commands())
327 .collect()
328 }
329
330 pub async fn execute_command(&self, name: &str, args: &str) -> Result<ToolOutput> {
336 let context = CommandContext::new(self.id);
337 for capability in &self.agent.definition().capabilities {
338 if capability.commands().iter().any(|c| c.name == name)
339 && let Some(output) = capability.execute_command(name, args, &context).await
340 {
341 return Ok(output);
342 }
343 }
344 Ok(ToolOutput::error(format!("unknown command `/{name}`")))
345 }
346
347 pub async fn run(&mut self, input: impl Into<Message>) -> Result<TurnResult> {
355 self.run_with_options(input, RunOptions::default()).await
356 }
357
358 pub async fn run_cancellable(
364 &mut self,
365 input: impl Into<Message>,
366 token: CancellationToken,
367 ) -> Result<TurnResult> {
368 self.run_with_options(input, RunOptions::new().cancellation(token))
369 .await
370 }
371
372 pub async fn run_controlled(
375 &mut self,
376 input: impl Into<Message>,
377 controls: TurnControls,
378 ) -> Result<TurnResult> {
379 self.run_with_options(input, RunOptions::new().controls(controls))
380 .await
381 }
382
383 pub async fn run_with_options(
387 &mut self,
388 input: impl Into<Message>,
389 options: RunOptions,
390 ) -> Result<TurnResult> {
391 let agent = self.agent.clone();
392 self.run_with_agent_and_options(&agent, input, options)
393 .await
394 }
395
396 pub async fn run_with_agent(
406 &mut self,
407 agent: &Agent,
408 input: impl Into<Message>,
409 ) -> Result<TurnResult> {
410 self.run_with_agent_and_options(agent, input, RunOptions::default())
411 .await
412 }
413
414 pub async fn run_with_agent_and_options(
416 &mut self,
417 agent: &Agent,
418 input: impl Into<Message>,
419 options: RunOptions,
420 ) -> Result<TurnResult> {
421 if self.closed {
422 return Err(agentyk_core::error::Error::Other(
423 "session is closed".into(),
424 ));
425 }
426 if !self.started {
427 let warnings = hooks::run_advisory_hooks(
428 &self.agent.definition().hooks,
429 HookEvent::SessionStart,
430 HookContext::session(self.id),
431 serde_json::json!({ "agent": self.agent.name() }),
432 )
433 .await;
434 self.record_session_events(warnings).await?;
435 self.started = true;
436 }
437 let definition = agent.definition();
438 let effective_model = options.controls.resolve(&definition.model);
439 let mut host = TurnHost::new(
440 self.id,
441 definition,
442 self.agent.environment(),
443 self.log.as_ref(),
444 &mut self.history,
445 )
446 .model(&effective_model)
447 .cancellation(options.cancellation)
448 .input(self.input.clone());
449 InProcessExecutor::new()
450 .run_turn(&mut host, input.into())
451 .await
452 }
453
454 async fn record_session_events(&mut self, events: Vec<EventData>) -> Result<()> {
455 if events.is_empty() {
456 return Ok(());
457 }
458 let requests = events
459 .into_iter()
460 .map(|data| EventRequest::new(self.id, data))
461 .collect();
462 let version = self.log.head(self.id).await?.sequence;
463 let recorded = self
464 .log
465 .append_batch(self.id, ExpectedVersion::Exact(version), requests)
466 .await?;
467 for event in recorded {
468 self.history.apply(&event.data);
469 notify_event_listeners(&self.agent.environment().listeners, &event).await;
470 }
471 Ok(())
472 }
473}
474
475fn is_completed_turn_boundary(events: &[Event]) -> bool {
476 let Some(turn_id) = events.iter().rev().find_map(|event| event.turn_id) else {
477 return true;
478 };
479 events.iter().any(|event| {
480 event.turn_id == Some(turn_id)
481 && matches!(
482 event.data,
483 EventData::TurnCompleted { .. }
484 | EventData::TurnFailed { .. }
485 | EventData::TurnCancelled
486 | EventData::TurnSealed { .. }
487 )
488 })
489}