use anyhow::{Result, anyhow, ensure};
use serde_json::Value;
use std::fmt::{Display, Formatter};
use std::path::Path;
use std::sync::{Arc, OnceLock};
use crate::workflow::machine::RunCommand;
use crate::{BindingValue, RuntimeKind};
pub trait ModelFactsPort: Send + Sync {
fn model_display_name(&self, model: &str) -> String;
fn project_allowlisted_model(&self, model: &str) -> Option<Value>;
fn task_tags_for_model(&self, model: &str) -> Vec<String>;
}
pub trait UsageLedgerPort: Send + Sync {
fn begin_graph_run(&self, payload: &Value) -> Result<()>;
fn record_graph_command(&self, payload: &Value) -> Result<()>;
fn workflow_report(&self, payload: &Value) -> Result<Value>;
}
pub trait ResolvedRuntime: Send + Sync {
fn fingerprint(&self) -> &str;
fn as_any(&self) -> &dyn std::any::Any;
}
pub trait EffectPort: Send + Sync {
fn runtime_descriptors(&self) -> Vec<Value>;
fn compatible_runtime_id(&self, kind: RuntimeKind, version_requirement: &str)
-> Option<String>;
fn resolve_runtime(
&self,
runtime_id: &str,
kind: RuntimeKind,
version_requirement: &str,
) -> Result<Arc<dyn ResolvedRuntime>>;
fn actor_fingerprint(
&self,
value_id: &str,
model: &str,
reasoning_effort: &str,
) -> Result<String>;
fn actor_capabilities(&self, value_id: &str) -> Result<Value>;
fn admit_strategy_cwd(&self, cwd: &str) -> Result<()>;
fn execute_script(
&self,
command: &RunCommand,
authorization_digest: &str,
runtime: &Arc<dyn ResolvedRuntime>,
revision_content: &Path,
runtime_state_root: &Path,
permit: &mut StrategyEffectPermit,
) -> Result<Value>;
fn execute_actor(
&self,
command: &RunCommand,
authorization_digest: &str,
binding: &BindingValue,
permit: &mut StrategyEffectPermit,
cwd: Option<&str>,
) -> Result<Value>;
fn predecessor_locator(&self, facts: &Value) -> Value;
fn dispatch_lane_operation(&self, operation: &str, params: &Value) -> Result<Value>;
fn new_correlation_id(&self) -> String;
fn record_run_stop(
&self,
portable_root: &Path,
correlation_id: &str,
confirmed: bool,
) -> Result<()>;
}
#[derive(Clone, Debug)]
pub struct StrategyEffectPermit {
command_id: String,
authorization_digest: String,
effect_fingerprint: String,
consumed: bool,
}
impl StrategyEffectPermit {
pub fn issue(
command_id: &str,
authorization_digest: &str,
effect_fingerprint: &str,
) -> Result<Self> {
ensure!(
!command_id.is_empty()
&& authorization_digest.len() == 64
&& effect_fingerprint.len() == 64,
"strategy_permit_invalid"
);
Ok(Self {
command_id: command_id.to_owned(),
authorization_digest: authorization_digest.to_owned(),
effect_fingerprint: effect_fingerprint.to_owned(),
consumed: false,
})
}
pub fn consume(
&mut self,
command: &RunCommand,
authorization_digest: &str,
effect_fingerprint: &str,
) -> Result<()> {
ensure!(!self.consumed, "strategy_permit_consumed");
ensure!(
self.command_id == command.id
&& self.authorization_digest == authorization_digest
&& self.effect_fingerprint == effect_fingerprint,
"strategy_permit_stale"
);
self.consumed = true;
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ActorTurnError {
detail: String,
}
impl ActorTurnError {
pub fn dispatch_failed(detail: impl Into<String>) -> Self {
Self {
detail: detail.into(),
}
}
pub fn detail(&self) -> &str {
&self.detail
}
}
impl Display for ActorTurnError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter.write_str(&self.detail)
}
}
impl std::error::Error for ActorTurnError {}
#[derive(Clone)]
pub struct HostPorts {
pub model: Arc<dyn ModelFactsPort>,
pub usage: Arc<dyn UsageLedgerPort>,
pub effect: Arc<dyn EffectPort>,
}
impl HostPorts {
pub fn unavailable() -> Self {
Self {
model: Arc::new(UnavailableModelFacts),
usage: Arc::new(UnavailableUsageLedger),
effect: Arc::new(UnavailableEffect),
}
}
}
impl std::fmt::Debug for HostPorts {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.debug_struct("HostPorts").finish_non_exhaustive()
}
}
static HOST_PORTS: OnceLock<HostPorts> = OnceLock::new();
pub fn install_host_ports(ports: HostPorts) {
let _ = HOST_PORTS.set(ports);
}
pub fn host_ports() -> &'static HostPorts {
HOST_PORTS.get_or_init(HostPorts::unavailable)
}
struct UnavailableModelFacts;
impl ModelFactsPort for UnavailableModelFacts {
fn model_display_name(&self, model: &str) -> String {
crate::util::model_naming::model_display_name(model)
}
fn project_allowlisted_model(&self, _model: &str) -> Option<Value> {
None
}
fn task_tags_for_model(&self, _model: &str) -> Vec<String> {
Vec::new()
}
}
struct UnavailableUsageLedger;
impl UsageLedgerPort for UnavailableUsageLedger {
fn begin_graph_run(&self, _payload: &Value) -> Result<()> {
Ok(())
}
fn record_graph_command(&self, _payload: &Value) -> Result<()> {
Ok(())
}
fn workflow_report(&self, _payload: &Value) -> Result<Value> {
Err(anyhow!("workflow_host_port_unavailable"))
}
}
struct UnavailableEffect;
impl EffectPort for UnavailableEffect {
fn runtime_descriptors(&self) -> Vec<Value> {
Vec::new()
}
fn compatible_runtime_id(
&self,
_kind: RuntimeKind,
_version_requirement: &str,
) -> Option<String> {
None
}
fn resolve_runtime(
&self,
_runtime_id: &str,
_kind: RuntimeKind,
_version_requirement: &str,
) -> Result<Arc<dyn ResolvedRuntime>> {
Err(anyhow!("workflow_host_port_unavailable"))
}
fn actor_fingerprint(
&self,
_value_id: &str,
_model: &str,
_reasoning_effort: &str,
) -> Result<String> {
Err(anyhow!("workflow_host_port_unavailable"))
}
fn actor_capabilities(&self, _value_id: &str) -> Result<Value> {
Err(anyhow!("workflow_host_port_unavailable"))
}
fn admit_strategy_cwd(&self, _cwd: &str) -> Result<()> {
Err(anyhow!("workflow_host_port_unavailable"))
}
fn execute_script(
&self,
_command: &RunCommand,
_authorization_digest: &str,
_runtime: &Arc<dyn ResolvedRuntime>,
_revision_content: &Path,
_runtime_state_root: &Path,
_permit: &mut StrategyEffectPermit,
) -> Result<Value> {
Err(anyhow!("workflow_host_port_unavailable"))
}
fn execute_actor(
&self,
_command: &RunCommand,
_authorization_digest: &str,
_binding: &BindingValue,
_permit: &mut StrategyEffectPermit,
_cwd: Option<&str>,
) -> Result<Value> {
Err(anyhow!("workflow_host_port_unavailable"))
}
fn predecessor_locator(&self, _facts: &Value) -> Value {
Value::Null
}
fn dispatch_lane_operation(&self, _operation: &str, _params: &Value) -> Result<Value> {
Err(anyhow!("workflow_host_port_unavailable"))
}
fn new_correlation_id(&self) -> String {
String::new()
}
fn record_run_stop(
&self,
_portable_root: &Path,
_correlation_id: &str,
_confirmed: bool,
) -> Result<()> {
Ok(())
}
}