use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use starweaver_core::{
AgentExecutionNode, CheckpointId, ConversationId, Metadata, RunId, RunLifecycle as RunStatus,
TraceContext,
};
use starweaver_usage::Usage;
use thiserror::Error;
use crate::AgentRunState;
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct AgentResumeCursor {
pub model_request_attempt: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_call_batch_id: Option<String>,
pub output_validation_attempt: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stream_cursor: Option<usize>,
pub message_cursor: usize,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct AgentResumeEvidence {
pub node: AgentExecutionNode,
pub status: RunStatus,
pub run_step: usize,
pub cursor: AgentResumeCursor,
pub usage: Usage,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context_revision: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub environment_ref: Option<String>,
pub pending_approval_count: usize,
pub deferred_tool_count: usize,
#[serde(default, skip_serializing_if = "TraceContext::is_empty")]
pub trace_context: TraceContext,
#[serde(default, skip_serializing_if = "Metadata::is_empty")]
pub metadata: Metadata,
}
impl AgentResumeEvidence {
#[must_use]
pub fn new(node: AgentExecutionNode, state: &AgentRunState) -> Self {
Self {
node,
status: state.status,
run_step: state.run_step,
cursor: AgentResumeCursor {
model_request_attempt: state.run_step,
tool_call_batch_id: (!state.pending_tool_calls.is_empty())
.then(|| format!("tool_batch_{}", state.run_step)),
output_validation_attempt: 0,
stream_cursor: None,
message_cursor: state.message_history.len(),
},
usage: state.usage.clone(),
context_revision: state
.metadata
.get("context_revision")
.and_then(serde_json::Value::as_str)
.map(str::to_string),
environment_ref: state
.metadata
.get("environment_ref")
.and_then(serde_json::Value::as_str)
.map(str::to_string),
pending_approval_count: state.pending_approval_tool_returns.len(),
deferred_tool_count: state.deferred_tool_returns.len(),
trace_context: TraceContext::default(),
metadata: Metadata::default(),
}
}
#[must_use]
pub const fn with_stream_cursor(mut self, stream_cursor: usize) -> Self {
self.cursor.stream_cursor = Some(stream_cursor);
self
}
#[must_use]
pub fn with_trace_context(mut self, trace_context: TraceContext) -> Self {
self.trace_context = trace_context;
self
}
#[must_use]
pub fn with_metadata(mut self, metadata: Metadata) -> Self {
self.metadata = metadata;
self
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct AgentCheckpoint {
pub checkpoint_id: CheckpointId,
pub run_id: RunId,
pub conversation_id: ConversationId,
pub node: AgentExecutionNode,
pub run_step: usize,
pub resume: AgentResumeEvidence,
pub state: AgentRunState,
#[serde(default, skip_serializing_if = "Metadata::is_empty")]
pub metadata: Metadata,
}
impl starweaver_core::VersionedRecord for AgentCheckpoint {
const SCHEMA: &'static str = "starweaver.runtime.checkpoint";
const ALLOW_BARE_V0: bool = true;
}
impl AgentCheckpoint {
#[must_use]
pub fn new(node: AgentExecutionNode, state: &AgentRunState) -> Self {
Self {
checkpoint_id: CheckpointId::new(),
run_id: state.run_id.clone(),
conversation_id: state.conversation_id.clone(),
node,
run_step: state.run_step,
resume: AgentResumeEvidence::new(node, state),
state: state.clone(),
metadata: Metadata::default(),
}
}
#[must_use]
pub fn with_metadata(mut self, metadata: Metadata) -> Self {
self.metadata = metadata;
self
}
#[must_use]
pub fn with_stream_cursor(mut self, stream_cursor: usize) -> Self {
self.resume = self.resume.with_stream_cursor(stream_cursor);
self
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum AgentExecutionDecision {
Continue,
Suspend {
reason: String,
},
}
#[derive(Debug, Error)]
pub enum AgentExecutorError {
#[error("executor failed: {0}")]
Failed(String),
}
#[async_trait]
pub trait AgentExecutor: Send + Sync {
#[must_use]
fn requires_durable_hitl_preparation(&self) -> bool {
false
}
async fn checkpoint(
&self,
checkpoint: AgentCheckpoint,
) -> Result<AgentExecutionDecision, AgentExecutorError>;
}