Skip to main content

lash_lashlang_runtime/
bridge.rs

1use lashlang::{ExecutionHostError, Value as LashlangValue};
2
3pub fn lashlang_value_to_json(
4    value: &LashlangValue,
5) -> Result<serde_json::Value, ExecutionHostError> {
6    serde_json::to_value(value)
7        .map_err(|err| ExecutionHostError::new(format!("failed to serialize value: {err}")))
8}
9
10pub fn protocol_tool_reply_to_lashlang_value(
11    reply: lash_core::ToolInvocationReply,
12) -> Result<LashlangValue, ExecutionHostError> {
13    protocol_tool_output_to_lashlang_value(&reply.output)
14}
15
16pub fn protocol_tool_output_to_lashlang_value(
17    output: &lash_core::ToolCallOutput,
18) -> Result<LashlangValue, ExecutionHostError> {
19    let value = output.value_for_projection();
20    if output.is_success() {
21        Ok(lashlang::from_json(value))
22    } else {
23        Err(ExecutionHostError::new(json_error_message(value)))
24    }
25}
26
27pub fn process_event_payload(
28    value: &LashlangValue,
29) -> Result<serde_json::Value, ExecutionHostError> {
30    Ok(serde_json::json!({
31        "value": lashlang_value_to_json(value)?,
32        "text": value.to_string(),
33    }))
34}
35
36pub fn sleep_duration_ms(
37    kind: lashlang::SleepKind,
38    value: &LashlangValue,
39) -> Result<u64, ExecutionHostError> {
40    match kind {
41        lashlang::SleepKind::For => duration_value_ms(value),
42        lashlang::SleepKind::Until => {
43            let target = deadline_value_ms(value)?;
44            let now = chrono::Utc::now().timestamp_millis();
45            Ok(target.saturating_sub(now.max(0) as u64))
46        }
47    }
48}
49
50fn duration_value_ms(value: &LashlangValue) -> Result<u64, ExecutionHostError> {
51    match value {
52        LashlangValue::Number(value) if value.is_finite() && *value >= 0.0 => {
53            Ok(value.round() as u64)
54        }
55        LashlangValue::String(value) => parse_duration_ms(value),
56        other => Err(ExecutionHostError::new(format!(
57            "`sleep for` expects a non-negative millisecond number or duration string, got {other}"
58        ))),
59    }
60}
61
62fn deadline_value_ms(value: &LashlangValue) -> Result<u64, ExecutionHostError> {
63    match value {
64        LashlangValue::Number(value) if value.is_finite() && *value >= 0.0 => {
65            Ok(value.round() as u64)
66        }
67        LashlangValue::String(value) => chrono::DateTime::parse_from_rfc3339(value)
68            .map(|deadline| deadline.timestamp_millis().max(0) as u64)
69            .map_err(|err| {
70                ExecutionHostError::new(format!(
71                    "`sleep until` expects RFC3339 text or Unix epoch milliseconds: {err}"
72                ))
73            }),
74        other => Err(ExecutionHostError::new(format!(
75            "`sleep until` expects RFC3339 text or Unix epoch milliseconds, got {other}"
76        ))),
77    }
78}
79
80fn parse_duration_ms(value: &str) -> Result<u64, ExecutionHostError> {
81    let value = value.trim();
82    let (number, multiplier) = if let Some(number) = value.strip_suffix("ms") {
83        (number, 1.0)
84    } else if let Some(number) = value.strip_suffix('s') {
85        (number, 1_000.0)
86    } else if let Some(number) = value.strip_suffix('m') {
87        (number, 60_000.0)
88    } else if let Some(number) = value.strip_suffix('h') {
89        (number, 3_600_000.0)
90    } else {
91        (value, 1.0)
92    };
93    let parsed = number
94        .trim()
95        .parse::<f64>()
96        .map_err(|err| ExecutionHostError::new(format!("invalid duration `{value}`: {err}")))?;
97    if !parsed.is_finite() || parsed < 0.0 {
98        return Err(ExecutionHostError::new(format!(
99            "invalid duration `{value}`: expected a non-negative finite number"
100        )));
101    }
102    Ok((parsed * multiplier).round() as u64)
103}
104
105pub fn json_error_message(value: serde_json::Value) -> String {
106    match value {
107        serde_json::Value::String(text) => text,
108        other => other.to_string(),
109    }
110}