use serde::Serialize;
use crate::sansio::EffectId;
use crate::{
CausalRef, RuntimeEffectControllerError, RuntimeEffectKind, RuntimeInvocation, RuntimeReplay,
RuntimeScope, RuntimeSubject,
};
pub(crate) fn turn_effect_invocation(
session_id: &str,
turn_id: &str,
turn_index: usize,
protocol_iteration: usize,
effect_id: EffectId,
effect_kind: RuntimeEffectKind,
) -> RuntimeInvocation {
RuntimeInvocation::effect(
RuntimeScope::for_turn(session_id, turn_id, turn_index, protocol_iteration),
effect_id.0.to_string(),
effect_kind,
turn_effect_replay_key(
session_id,
turn_id,
turn_index,
protocol_iteration,
effect_kind,
effect_id,
),
)
}
fn turn_effect_replay_key(
session_id: &str,
turn_id: &str,
turn_index: usize,
protocol_iteration: usize,
kind: RuntimeEffectKind,
effect_id: EffectId,
) -> String {
format!(
"{session_id}:{turn_id}:{turn_index}:{protocol_iteration}:{}:{}",
kind.as_str(),
effect_id.0
)
}
pub(crate) fn child_effect_invocation(
parent: &RuntimeInvocation,
effect_id: impl Into<String>,
kind: RuntimeEffectKind,
replay_suffix: impl AsRef<str>,
) -> RuntimeInvocation {
let replay_base = parent
.replay_key()
.or_else(|| parent.effect_id())
.unwrap_or("effect");
RuntimeInvocation {
scope: parent.scope.clone(),
subject: RuntimeSubject::Effect {
effect_id: effect_id.into(),
kind,
},
caused_by: parent.causal_ref(),
replay: Some(RuntimeReplay {
key: format!("{replay_base}:{}", replay_suffix.as_ref()),
}),
}
}
pub(crate) fn child_tool_effect_invocation(
parent: &RuntimeInvocation,
parent_effect_id: EffectId,
call_id: &str,
) -> RuntimeInvocation {
child_effect_invocation(
parent,
format!("{}:{call_id}", parent_effect_id.0),
RuntimeEffectKind::ToolCall,
call_id,
)
}
pub(crate) fn tool_retry_sleep_invocation(
parent: &RuntimeInvocation,
tool_name: &str,
attempt: u32,
) -> RuntimeInvocation {
let parent_effect_id = parent.effect_id().unwrap_or("effect");
child_effect_invocation(
parent,
format!("{parent_effect_id}:{tool_name}:attempt:{attempt}:sleep"),
RuntimeEffectKind::Sleep,
format!("{tool_name}:attempt:{attempt}:sleep"),
)
}
pub(crate) fn lashlang_sleep_invocation(
session_id: &str,
parent: Option<&RuntimeInvocation>,
scope: &str,
sequence: u64,
) -> RuntimeInvocation {
let suffix = format!("lashlang:{scope}:sleep:{sequence}");
if let Some(parent) = parent {
let parent_effect_id = parent.effect_id().unwrap_or("effect");
return child_effect_invocation(
parent,
format!("{parent_effect_id}:{suffix}"),
RuntimeEffectKind::Sleep,
suffix,
);
}
RuntimeInvocation::effect(
RuntimeScope::new(session_id),
suffix.clone(),
RuntimeEffectKind::Sleep,
suffix,
)
}
pub(crate) fn process_effect_invocation(
session_id: &str,
parent: Option<RuntimeInvocation>,
effect_id: &str,
) -> RuntimeInvocation {
if let Some(parent) = parent {
let scope = if let Some(turn_id) = parent.scope.turn_id.clone() {
RuntimeScope {
session_id: session_id.to_string(),
turn_id: Some(turn_id),
turn_index: parent.scope.turn_index,
protocol_iteration: parent.scope.protocol_iteration,
}
} else {
RuntimeScope::new(session_id)
};
let replay_base = parent.replay_key().unwrap_or("process");
return RuntimeInvocation {
scope,
subject: RuntimeSubject::Effect {
effect_id: effect_id.to_string(),
kind: RuntimeEffectKind::Process,
},
caused_by: parent.causal_ref(),
replay: Some(RuntimeReplay {
key: format!("{replay_base}:{effect_id}"),
}),
};
}
RuntimeInvocation::effect(
RuntimeScope::new(session_id),
effect_id.to_string(),
RuntimeEffectKind::Process,
format!("{session_id}:{effect_id}"),
)
}
pub fn process_event_invocation(
owner_session_id: &str,
process_id: &str,
sequence: u64,
event_type: &str,
replay: Option<RuntimeReplay>,
) -> RuntimeInvocation {
RuntimeInvocation {
scope: RuntimeScope::new(owner_session_id),
subject: RuntimeSubject::ProcessEvent {
process_id: process_id.to_string(),
sequence,
event_type: event_type.to_string(),
},
caused_by: Some(CausalRef::Process {
process_id: process_id.to_string(),
}),
replay,
}
}
pub(crate) fn host_event_invocation(session_id: &str, occurrence_id: &str) -> RuntimeInvocation {
RuntimeInvocation {
scope: RuntimeScope::new(session_id),
subject: RuntimeSubject::HostEvent {
occurrence_id: occurrence_id.to_string(),
},
caused_by: None,
replay: Some(RuntimeReplay {
key: format!("host_event:{occurrence_id}"),
}),
}
}
pub(crate) fn direct_effect_invocation(
session_id: &str,
usage_source: &str,
replay_discriminator: String,
turn_id: Option<&str>,
caused_by: Option<CausalRef>,
) -> RuntimeInvocation {
let replay_key = match turn_id.filter(|value| !value.is_empty()) {
Some(turn_id) => {
format!("{session_id}:{turn_id}:direct:{usage_source}:{replay_discriminator}")
}
None => format!("{session_id}:direct:{usage_source}:{replay_discriminator}"),
};
RuntimeInvocation::effect(
RuntimeScope {
session_id: session_id.to_string(),
turn_id: turn_id.map(str::to_string),
turn_index: None,
protocol_iteration: None,
},
replay_discriminator,
RuntimeEffectKind::Direct,
replay_key,
)
.with_caused_by(caused_by)
}
pub(crate) fn direct_request_discriminator<T>(
request: &T,
explicit_replay: Option<&RuntimeReplay>,
caused_by: Option<&CausalRef>,
) -> Result<String, RuntimeEffectControllerError>
where
T: Serialize,
{
let cause_discriminator = caused_by
.map(causal_replay_discriminator)
.unwrap_or_default();
if let Some(replay) = explicit_replay.filter(|replay| !replay.key.is_empty()) {
return Ok(format!("{cause_discriminator}request:{}", replay.key));
}
let digest = crate::stable_hash::stable_json_sha256_hex(request).map_err(|err| {
RuntimeEffectControllerError::new(
"runtime_effect_discriminator",
format!("failed to serialize runtime effect discriminator: {err}"),
)
})?;
Ok(format!("{cause_discriminator}sha256:{digest}"))
}
fn causal_replay_discriminator(caused_by: &CausalRef) -> String {
match caused_by {
CausalRef::Turn {
session_id,
turn_id,
} => format!("cause:turn:{session_id}:{turn_id}:"),
CausalRef::Effect {
session_id,
turn_id,
effect_id,
} => {
let turn = turn_id.as_deref().unwrap_or("");
format!("cause:effect:{session_id}:{turn}:{effect_id}:")
}
CausalRef::ToolCall {
session_id,
call_id,
} => format!("cause:tool_call:{session_id}:{call_id}:"),
CausalRef::Process { process_id } => format!("cause:process:{process_id}:"),
CausalRef::ProcessEvent {
process_id,
sequence,
} => format!("cause:process_event:{process_id}:{sequence}:"),
CausalRef::HostEvent { occurrence_id } => format!("cause:host_event:{occurrence_id}:"),
CausalRef::SessionNode {
session_id,
node_id,
} => format!("cause:session_node:{session_id}:{node_id}:"),
}
}