atman-runtime 1.5.0

atman flow execution runtime: evaluator, tool dispatch, provider dispatch, executor, memory stores
Documentation
use crate::error::RuntimeError;
use crate::message::{Message, MessageRole};
use crate::tool::{BoxFut, Tier, Tool, ToolArgs, ToolCtx, ToolResult};
use crate::value::Value;

pub struct SessionPush;

impl Tool for SessionPush {
    fn name(&self) -> &str {
        "session.push"
    }

    fn tier(&self) -> Tier {
        Tier::Zero
    }

    fn description(&self) -> Option<&str> {
        Some(
            "Push a Message value into the current session's message history. \
             Use after dispatch_all to persist tool results so the next \
             llm { context: session } call can see them. The message role \
             (user/assistant/tool/system) is preserved. Returns unit.",
        )
    }

    fn input_schema(&self) -> serde_json::Value {
        serde_json::json!({
            "type": "object",
            "properties": {
                "message": {
                    "type": "object",
                    "description": "The Message value to push (e.g. a tool_result from dispatch_all). Pass the value returned by dispatch_all directly."
                }
            },
            "required": ["message"]
        })
    }

    fn call<'a>(&'a self, args: ToolArgs, ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
        Box::pin(async move {
            let val = match args.named("message").or_else(|| args.positional(0).ok()) {
                Some(v) => v.clone(),
                None => {
                    return Err(RuntimeError::MissingArg("session.push: message".into()));
                }
            };
            let msgs = match val {
                Value::Message(m) => vec![m],
                Value::List(items) => items
                    .into_iter()
                    .filter_map(|v| match v {
                        Value::Message(m) => Some(m),
                        _ => None,
                    })
                    .collect(),
                Value::Str(s) => {
                    let turn_id = ctx
                        .turn_id
                        .clone()
                        .unwrap_or_else(crate::event::TurnId::now);
                    vec![Message::assistant_text(turn_id, s)]
                }
                other => {
                    return Err(RuntimeError::TypeMismatch {
                        expected: "message, list of message, or string".into(),
                        actual: other.kind_name().into(),
                    });
                }
            };
            let Some(handle) = &ctx.session_messages_handle else {
                return Err(RuntimeError::ToolFailed(
                    "session.push: no session messages handle available".into(),
                ));
            };
            let _compact_guard = match &ctx.compact_lock_handle {
                Some(lock) => Some(lock.lock().await),
                None => None,
            };
            for msg in msgs {
                let msg = crate::tools::tool_output::maybe_truncate_tool_message(
                    &msg,
                    ctx.session_dir.as_deref(),
                );
                emit_message_event(ctx, &msg);
                if let Some(tx) = &ctx.stream_tx {
                    let _ = tx.send(crate::stream::StreamFrame::ToolResultMsg {
                        flow_run_id: ctx.flow_run_id.as_ref().map(|r| r.0.to_string()),
                        message: msg.clone(),
                    });
                }
                handle.lock().unwrap().push(msg);
            }
            Ok(Value::Unit)
        })
    }
}

fn emit_message_event(ctx: &ToolCtx, msg: &Message) {
    use crate::event::{Event, TurnId};
    let Some(sink) = &ctx.events else {
        return;
    };
    let msg =
        crate::tools::tool_output::maybe_truncate_tool_message(msg, ctx.session_dir.as_deref());
    let turn_id = ctx.turn_id.clone().unwrap_or_else(TurnId::now);
    let event = match msg.role {
        MessageRole::User => Event::UserMsg {
            turn_id,
            message: msg.clone(),
        },
        MessageRole::Assistant => Event::AssistantMsg {
            turn_id,
            flow_run_id: ctx.flow_run_id.clone(),
            message: msg.clone(),
        },
        MessageRole::Tool => Event::ToolResultMsg {
            turn_id,
            flow_run_id: ctx.flow_run_id.clone(),
            message: msg.clone(),
        },
        MessageRole::System => Event::SystemMsg {
            turn_id,
            message: msg.clone(),
        },
    };
    sink.emit(event);
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn session_push_name_and_tier() {
        let tool = SessionPush;
        assert_eq!(tool.name(), "session.push");
        assert_eq!(tool.tier(), Tier::Zero);
        assert!(tool.description().is_some());
    }
}