use super::RunId;
use super::run_primitive::RunPrimitive;
use super::run_receipt::RunBoundaryReceipt;
use crate::error::AgentError;
use crate::service::SessionError;
use crate::turn_execution_authority::{TurnTerminalCauseKind, TurnTerminalOutcome};
use crate::types::RunResult;
use serde_json::Value;
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum CoreApplyFailureCauseKind {
PrimitiveRejected,
RuntimeContextApply,
RuntimeTurn,
HookDenied,
HookRuntimeFailure,
ExecutorStopped,
ExecutorControlFailed,
ExecutorInternal,
Unknown,
}
impl CoreApplyFailureCauseKind {
pub fn as_str(self) -> &'static str {
match self {
Self::PrimitiveRejected => "PrimitiveRejected",
Self::RuntimeContextApply => "RuntimeContextApply",
Self::RuntimeTurn => "RuntimeTurn",
Self::HookDenied => "HookDenied",
Self::HookRuntimeFailure => "HookRuntimeFailure",
Self::ExecutorStopped => "ExecutorStopped",
Self::ExecutorControlFailed => "ExecutorControlFailed",
Self::ExecutorInternal => "ExecutorInternal",
Self::Unknown => "Unknown",
}
}
pub fn from_wire_str(value: &str) -> Option<Self> {
match value {
"PrimitiveRejected" => Some(Self::PrimitiveRejected),
"RuntimeContextApply" => Some(Self::RuntimeContextApply),
"RuntimeTurn" => Some(Self::RuntimeTurn),
"HookDenied" => Some(Self::HookDenied),
"HookRuntimeFailure" => Some(Self::HookRuntimeFailure),
"ExecutorStopped" => Some(Self::ExecutorStopped),
"ExecutorControlFailed" => Some(Self::ExecutorControlFailed),
"ExecutorInternal" => Some(Self::ExecutorInternal),
"Unknown" => Some(Self::Unknown),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CoreApplyFailureCause {
pub kind: CoreApplyFailureCauseKind,
pub message: String,
}
impl CoreApplyFailureCause {
pub fn new(kind: CoreApplyFailureCauseKind, message: impl Into<String>) -> Self {
Self {
kind,
message: message.into(),
}
}
pub fn primitive_rejected(message: impl Into<String>) -> Self {
Self::new(CoreApplyFailureCauseKind::PrimitiveRejected, message)
}
pub fn runtime_context_apply(message: impl Into<String>) -> Self {
Self::new(CoreApplyFailureCauseKind::RuntimeContextApply, message)
}
pub fn runtime_turn(message: impl Into<String>) -> Self {
Self::new(CoreApplyFailureCauseKind::RuntimeTurn, message)
}
pub fn hook_denied(message: impl Into<String>) -> Self {
Self::new(CoreApplyFailureCauseKind::HookDenied, message)
}
pub fn hook_runtime_failure(message: impl Into<String>) -> Self {
Self::new(CoreApplyFailureCauseKind::HookRuntimeFailure, message)
}
pub fn executor_stopped() -> Self {
Self::new(
CoreApplyFailureCauseKind::ExecutorStopped,
"executor is stopped",
)
}
pub fn executor_control_failed(message: impl Into<String>) -> Self {
Self::new(CoreApplyFailureCauseKind::ExecutorControlFailed, message)
}
pub fn executor_internal(message: impl Into<String>) -> Self {
Self::new(CoreApplyFailureCauseKind::ExecutorInternal, message)
}
pub fn unknown(message: impl Into<String>) -> Self {
Self::new(CoreApplyFailureCauseKind::Unknown, message)
}
pub fn from_agent_error(error: &AgentError) -> Self {
match error {
AgentError::HookDenied { .. } => Self::hook_denied(error.to_string()),
AgentError::HookTimeout { .. }
| AgentError::HookExecutionFailed { .. }
| AgentError::HookConfigInvalid { .. } => Self::hook_runtime_failure(error.to_string()),
_ => Self::runtime_turn(error.to_string()),
}
}
pub fn from_session_error(error: &SessionError) -> Self {
match error {
SessionError::Agent(agent_error) => Self::from_agent_error(agent_error),
_ => Self::runtime_turn(error.to_string()),
}
}
pub fn message(&self) -> &str {
&self.message
}
}
impl std::fmt::Display for CoreApplyFailureCause {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum CoreControlFailureCauseKind {
RuntimeControl,
ExecutorInternal,
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CoreControlFailureCause {
pub kind: CoreControlFailureCauseKind,
pub message: String,
}
impl CoreControlFailureCause {
pub fn new(kind: CoreControlFailureCauseKind, message: impl Into<String>) -> Self {
Self {
kind,
message: message.into(),
}
}
pub fn runtime_control(message: impl Into<String>) -> Self {
Self::new(CoreControlFailureCauseKind::RuntimeControl, message)
}
pub fn executor_internal(message: impl Into<String>) -> Self {
Self::new(CoreControlFailureCauseKind::ExecutorInternal, message)
}
pub fn unknown(message: impl Into<String>) -> Self {
Self::new(CoreControlFailureCauseKind::Unknown, message)
}
}
impl std::fmt::Display for CoreControlFailureCause {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message)
}
}
#[derive(Debug, Clone, thiserror::Error)]
#[non_exhaustive]
pub enum CoreExecutorError {
#[error("Apply failed: {cause}")]
ApplyFailed { cause: CoreApplyFailureCause },
#[error("Terminal failure: {outcome:?} ({cause_kind:?}): {message}")]
TerminalFailure {
outcome: TurnTerminalOutcome,
cause_kind: TurnTerminalCauseKind,
message: String,
},
#[error("Control failed: {cause}")]
ControlFailed { cause: CoreControlFailureCause },
#[error("Executor is stopped")]
Stopped,
#[error("Run was cancelled")]
Cancelled,
#[error("Internal error: {0}")]
Internal(String),
}
impl CoreExecutorError {
pub fn apply_failed(cause: CoreApplyFailureCause) -> Self {
Self::ApplyFailed { cause }
}
pub fn apply_failed_primitive_rejected(message: impl Into<String>) -> Self {
Self::apply_failed(CoreApplyFailureCause::primitive_rejected(message))
}
pub fn apply_failed_runtime_context(message: impl Into<String>) -> Self {
Self::apply_failed(CoreApplyFailureCause::runtime_context_apply(message))
}
pub fn apply_failed_runtime_turn(message: impl Into<String>) -> Self {
Self::apply_failed(CoreApplyFailureCause::runtime_turn(message))
}
pub fn terminal_failure(
outcome: TurnTerminalOutcome,
cause_kind: TurnTerminalCauseKind,
message: impl Into<String>,
) -> Self {
Self::TerminalFailure {
outcome,
cause_kind,
message: message.into(),
}
}
pub fn apply_failed_from_session_error(error: SessionError) -> Self {
match error {
SessionError::Agent(AgentError::Cancelled) => Self::Cancelled,
SessionError::Agent(AgentError::TerminalFailure {
outcome,
cause_kind,
message,
}) if cause_kind.is_specific_failure_cause() => {
Self::terminal_failure(outcome, cause_kind, message)
}
SessionError::Agent(AgentError::TerminalFailure { cause_kind, .. }) => Self::Internal(
format!("runtime turn returned unknown machine terminal cause: {cause_kind:?}"),
),
error => Self::apply_failed(CoreApplyFailureCause::from_session_error(&error)),
}
}
pub fn apply_failed_unknown(message: impl Into<String>) -> Self {
Self::apply_failed(CoreApplyFailureCause::unknown(message))
}
pub fn cancelled() -> Self {
Self::Cancelled
}
pub fn is_cancelled(&self) -> bool {
matches!(self, Self::Cancelled)
}
pub fn control_failed(cause: CoreControlFailureCause) -> Self {
Self::ControlFailed { cause }
}
pub fn control_failed_runtime(message: impl Into<String>) -> Self {
Self::control_failed(CoreControlFailureCause::runtime_control(message))
}
pub fn apply_failure_cause(&self) -> CoreApplyFailureCause {
match self {
Self::ApplyFailed { cause } => cause.clone(),
Self::TerminalFailure { cause_kind, .. } => {
CoreApplyFailureCause::executor_internal(format!(
"typed machine terminal failure escaped runtime-loop handling: {cause_kind:?}"
))
}
Self::ControlFailed { cause } => {
CoreApplyFailureCause::executor_control_failed(cause.message.clone())
}
Self::Stopped => CoreApplyFailureCause::executor_stopped(),
Self::Cancelled => CoreApplyFailureCause::runtime_turn("cancelled"),
Self::Internal(message) => CoreApplyFailureCause::executor_internal(message.clone()),
}
}
}
#[derive(Debug, Clone)]
pub enum CoreApplyTerminal {
RunResult(Box<RunResult>),
NoPendingBoundary,
CallbackPending { tool_name: String, args: Value },
}
#[derive(Debug, Clone)]
pub struct CoreApplyOutput {
pub receipt: RunBoundaryReceipt,
pub session_snapshot: Option<Vec<u8>>,
pub terminal: Option<CoreApplyTerminal>,
}
impl CoreApplyOutput {
pub fn with_run_result(
receipt: RunBoundaryReceipt,
session_snapshot: Option<Vec<u8>>,
run_result: RunResult,
) -> Self {
Self {
receipt,
session_snapshot,
terminal: Some(CoreApplyTerminal::RunResult(Box::new(run_result))),
}
}
pub fn with_callback_pending(
receipt: RunBoundaryReceipt,
session_snapshot: Option<Vec<u8>>,
tool_name: impl Into<String>,
args: Value,
) -> Self {
Self {
receipt,
session_snapshot,
terminal: Some(CoreApplyTerminal::CallbackPending {
tool_name: tool_name.into(),
args,
}),
}
}
pub fn without_terminal(
receipt: RunBoundaryReceipt,
session_snapshot: Option<Vec<u8>>,
) -> Self {
Self {
receipt,
session_snapshot,
terminal: None,
}
}
}
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
pub trait CoreExecutorBoundaryHandle: Send + Sync {
async fn cancel_after_boundary(&self, reason: String) -> Result<(), CoreExecutorError>;
}
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
pub trait CoreExecutorInterruptHandle: Send + Sync {
async fn hard_cancel_current_run(&self, reason: String) -> Result<(), CoreExecutorError>;
}
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
pub trait CoreExecutor: Send + Sync {
fn boundary_handle(&self) -> Option<Arc<dyn CoreExecutorBoundaryHandle>> {
None
}
fn interrupt_handle(&self) -> Option<Arc<dyn CoreExecutorInterruptHandle>> {
None
}
async fn apply(
&mut self,
run_id: RunId,
primitive: RunPrimitive,
) -> Result<CoreApplyOutput, CoreExecutorError>;
async fn cancel_after_boundary(&mut self, reason: String) -> Result<(), CoreExecutorError>;
async fn stop_runtime_executor(&mut self, reason: String) -> Result<(), CoreExecutorError>;
}
#[cfg(test)]
#[allow(clippy::panic)]
mod tests {
use super::*;
fn _assert_object_safe(_: &dyn CoreExecutor) {}
#[test]
fn core_executor_error_display() {
let err = CoreExecutorError::ApplyFailed {
cause: CoreApplyFailureCause::runtime_turn("bad input"),
};
assert_eq!(err.to_string(), "Apply failed: bad input");
let err = CoreExecutorError::ControlFailed {
cause: CoreControlFailureCause::runtime_control("not running"),
};
assert_eq!(err.to_string(), "Control failed: not running");
let err = CoreExecutorError::Stopped;
assert_eq!(err.to_string(), "Executor is stopped");
let err = CoreExecutorError::Cancelled;
assert_eq!(err.to_string(), "Run was cancelled");
let err = CoreExecutorError::Internal("oops".into());
assert_eq!(err.to_string(), "Internal error: oops");
}
#[test]
fn apply_failed_carries_typed_cause() {
let err = CoreExecutorError::ApplyFailed {
cause: CoreApplyFailureCause::runtime_context_apply("context write failed"),
};
match err {
CoreExecutorError::ApplyFailed { cause } => {
assert_eq!(cause.kind, CoreApplyFailureCauseKind::RuntimeContextApply);
assert_eq!(cause.message(), "context write failed");
}
other => panic!("expected typed apply failure, got {other:?}"),
}
}
#[test]
fn cancelled_session_error_remains_typed_at_runtime_executor_boundary() {
let err = CoreExecutorError::apply_failed_from_session_error(SessionError::Agent(
AgentError::Cancelled,
));
assert!(err.is_cancelled());
assert_eq!(
err.apply_failure_cause().kind,
CoreApplyFailureCauseKind::RuntimeTurn
);
}
#[test]
fn hook_denial_agent_error_maps_to_typed_apply_failure_cause() {
let error = AgentError::HookDenied {
hook_id: crate::hooks::HookId::new("guard"),
point: crate::hooks::HookPoint::PreToolExecution,
reason_code: crate::hooks::HookReasonCode::PolicyViolation,
message: "blocked by hook".to_string(),
payload: None,
};
let cause = CoreApplyFailureCause::from_agent_error(&error);
assert_eq!(cause.kind, CoreApplyFailureCauseKind::HookDenied);
assert!(cause.message().contains("blocked by hook"));
}
#[test]
fn hook_runtime_agent_error_maps_to_typed_apply_failure_cause() {
let error = AgentError::HookExecutionFailed {
hook_id: crate::hooks::HookId::new("guard"),
reason: "missing runtime".to_string(),
};
let cause = CoreApplyFailureCause::from_agent_error(&error);
assert_eq!(cause.kind, CoreApplyFailureCauseKind::HookRuntimeFailure);
assert!(cause.message().contains("missing runtime"));
}
}