use std::sync::Arc;
use agentyk_core::cancellation::CancellationToken;
use agentyk_core::capability::{CommandContext, CommandDescriptor};
use agentyk_core::controls::TurnControls;
use agentyk_core::error::Result;
use agentyk_core::event::{Event, EventData, EventRequest, notify_event_listeners};
use agentyk_core::event_log::{EventLog, ExpectedVersion, SessionPoint};
use agentyk_core::hook::{HookContext, HookEvent};
use agentyk_core::id::SessionId;
use agentyk_core::input::InputQueue;
use agentyk_core::message::Message;
use agentyk_core::replay::History;
use agentyk_core::tool::ToolOutput;
use agentyk_core::turn::TurnState;
use crate::agent::Agent;
use crate::hooks;
use crate::host::{TurnHost, TurnResult};
use crate::in_process::InProcessExecutor;
#[derive(Default)]
#[non_exhaustive]
pub struct RunOptions {
pub cancellation: CancellationToken,
pub controls: TurnControls,
}
impl RunOptions {
pub fn new() -> Self {
Self::default()
}
pub fn cancellation(mut self, token: CancellationToken) -> Self {
self.cancellation = token;
self
}
pub fn controls(mut self, controls: TurnControls) -> Self {
self.controls = controls;
self
}
}
pub struct Session {
agent: Agent,
id: SessionId,
log: Arc<dyn EventLog>,
history: History,
input: InputQueue,
started: bool,
closed: bool,
}
#[derive(Debug, Clone)]
pub struct SessionView {
point: SessionPoint,
events: Vec<Event>,
history: History,
}
impl SessionView {
pub fn point(&self) -> SessionPoint {
self.point
}
pub fn events(&self) -> &[Event] {
&self.events
}
pub fn messages(&self) -> &[Message] {
self.history.messages()
}
}
impl Session {
pub(crate) fn new(agent: Agent, log: Arc<dyn EventLog>) -> Self {
Self {
agent,
id: SessionId::new(),
log,
history: History::new(),
input: InputQueue::new(),
started: false,
closed: false,
}
}
pub(crate) async fn resume(
agent: Agent,
log: Arc<dyn EventLog>,
session_id: SessionId,
) -> Result<Self> {
let events = log.read(session_id).await?;
let started = !events.is_empty();
Ok(Self {
agent,
id: session_id,
log,
history: History::from_events(&events),
input: InputQueue::new(),
started,
closed: false,
})
}
pub fn id(&self) -> SessionId {
self.id
}
pub fn input(&self) -> InputQueue {
self.input.clone()
}
pub fn is_closed(&self) -> bool {
self.closed
}
pub async fn close(&mut self) -> Result<()> {
if self.closed {
return Ok(());
}
let warnings = hooks::run_advisory_hooks(
&self.agent.definition().hooks,
HookEvent::SessionEnd,
HookContext::session(self.id),
serde_json::json!({ "reason": "closed" }),
)
.await;
self.record_session_events(warnings).await?;
self.closed = true;
Ok(())
}
pub async fn point(&self) -> Result<SessionPoint> {
self.log.head(self.id).await
}
pub async fn inspect(&self, point: SessionPoint) -> Result<SessionView> {
if point.session_id != self.id {
return Err(agentyk_core::error::Error::EventLog(format!(
"point belongs to {}, not session {}",
point.session_id, self.id
)));
}
let head = self.log.head(self.id).await?;
if point.sequence > head.sequence {
return Err(agentyk_core::error::Error::EventLog(format!(
"point {} is past session head {}",
point.sequence, head.sequence
)));
}
let events = self.log.read_through(point).await?;
Ok(SessionView {
point,
history: History::from_events(&events),
events,
})
}
pub async fn fork(&self, point: SessionPoint) -> Result<Self> {
let view = self.inspect(point).await?;
if !is_completed_turn_boundary(view.events()) {
return Err(agentyk_core::error::Error::EventLog(
"sessions can only fork from an empty stream or a completed-turn boundary".into(),
));
}
let child_id = self.log.fork(point).await?;
let mut child = Self::resume(self.agent.clone(), self.log.clone(), child_id).await?;
child.started = false;
Ok(child)
}
pub async fn resume_pending(&mut self) -> Result<Option<TurnResult>> {
self.resume_pending_with_options(RunOptions::default())
.await
}
pub async fn resume_pending_with_options(
&mut self,
options: RunOptions,
) -> Result<Option<TurnResult>> {
let agent = self.agent.clone();
self.resume_pending_with_agent_and_options(&agent, options)
.await
}
pub async fn resume_pending_with_agent(&mut self, agent: &Agent) -> Result<Option<TurnResult>> {
self.resume_pending_with_agent_and_options(agent, RunOptions::default())
.await
}
pub async fn resume_pending_with_agent_and_options(
&mut self,
agent: &Agent,
options: RunOptions,
) -> Result<Option<TurnResult>> {
let events = self.log.read(self.id).await?;
let Some(turn_id) = events.iter().rev().find_map(|event| {
matches!(event.data, EventData::TurnStarted { .. })
.then_some(event.turn_id)
.flatten()
}) else {
return Ok(None);
};
let state = TurnState::replay(&events, turn_id)?;
if state.is_complete() {
return Ok(None);
}
let definition = agent.definition();
let effective_model = options.controls.resolve(&definition.model);
let mut host = TurnHost::new(
self.id,
definition,
self.agent.environment(),
self.log.as_ref(),
&mut self.history,
)
.model(&effective_model)
.cancellation(options.cancellation);
InProcessExecutor::new()
.resume_turn(&mut host, state)
.await
.map(Some)
}
pub fn messages(&self) -> &[Message] {
self.history.messages()
}
pub async fn events(&self) -> Result<Vec<Event>> {
self.log.read(self.id).await
}
pub fn commands(&self) -> Vec<CommandDescriptor> {
self.agent
.definition()
.capabilities
.iter()
.flat_map(|capability| capability.commands())
.collect()
}
pub async fn execute_command(&self, name: &str, args: &str) -> Result<ToolOutput> {
let context = CommandContext::new(self.id);
for capability in &self.agent.definition().capabilities {
if capability.commands().iter().any(|c| c.name == name)
&& let Some(output) = capability.execute_command(name, args, &context).await
{
return Ok(output);
}
}
Ok(ToolOutput::error(format!("unknown command `/{name}`")))
}
pub async fn run(&mut self, input: impl Into<Message>) -> Result<TurnResult> {
self.run_with_options(input, RunOptions::default()).await
}
pub async fn run_cancellable(
&mut self,
input: impl Into<Message>,
token: CancellationToken,
) -> Result<TurnResult> {
self.run_with_options(input, RunOptions::new().cancellation(token))
.await
}
pub async fn run_controlled(
&mut self,
input: impl Into<Message>,
controls: TurnControls,
) -> Result<TurnResult> {
self.run_with_options(input, RunOptions::new().controls(controls))
.await
}
pub async fn run_with_options(
&mut self,
input: impl Into<Message>,
options: RunOptions,
) -> Result<TurnResult> {
let agent = self.agent.clone();
self.run_with_agent_and_options(&agent, input, options)
.await
}
pub async fn run_with_agent(
&mut self,
agent: &Agent,
input: impl Into<Message>,
) -> Result<TurnResult> {
self.run_with_agent_and_options(agent, input, RunOptions::default())
.await
}
pub async fn run_with_agent_and_options(
&mut self,
agent: &Agent,
input: impl Into<Message>,
options: RunOptions,
) -> Result<TurnResult> {
if self.closed {
return Err(agentyk_core::error::Error::Other(
"session is closed".into(),
));
}
if !self.started {
let warnings = hooks::run_advisory_hooks(
&self.agent.definition().hooks,
HookEvent::SessionStart,
HookContext::session(self.id),
serde_json::json!({ "agent": self.agent.name() }),
)
.await;
self.record_session_events(warnings).await?;
self.started = true;
}
let definition = agent.definition();
let effective_model = options.controls.resolve(&definition.model);
let mut host = TurnHost::new(
self.id,
definition,
self.agent.environment(),
self.log.as_ref(),
&mut self.history,
)
.model(&effective_model)
.cancellation(options.cancellation)
.input(self.input.clone());
InProcessExecutor::new()
.run_turn(&mut host, input.into())
.await
}
async fn record_session_events(&mut self, events: Vec<EventData>) -> Result<()> {
if events.is_empty() {
return Ok(());
}
let requests = events
.into_iter()
.map(|data| EventRequest::new(self.id, data))
.collect();
let version = self.log.head(self.id).await?.sequence;
let recorded = self
.log
.append_batch(self.id, ExpectedVersion::Exact(version), requests)
.await?;
for event in recorded {
self.history.apply(&event.data);
notify_event_listeners(&self.agent.environment().listeners, &event).await;
}
Ok(())
}
}
fn is_completed_turn_boundary(events: &[Event]) -> bool {
let Some(turn_id) = events.iter().rev().find_map(|event| event.turn_id) else {
return true;
};
events.iter().any(|event| {
event.turn_id == Some(turn_id)
&& matches!(
event.data,
EventData::TurnCompleted { .. }
| EventData::TurnFailed { .. }
| EventData::TurnCancelled
| EventData::TurnSealed { .. }
)
})
}