sim-lib-agent 0.1.3

Agent runtime surfaces for SIM.
Documentation
use super::shared::{
    agent_connection_for_value, agent_make_expr, optional_value, values_option,
    wire_step_connection,
};
use crate::{
    AGENT_REFLECT_CAPABILITY, AGENT_REPLACE_CAPABILITY, AGENT_SPAWN_CAPABILITY, BlackboardMemory,
    FileMemory, PersonaMemory, Tool, VectorMemory, WorkingMemory, capabilities_option,
    installed_codecs, parse_component_options, string_option, stringish_from_value,
    symbol_from_value, symbol_option,
};
use sim_kernel::{Args, CapabilityName, Cx, Error, Result, Symbol, Value};
use sim_lib_server::{
    BufferedStreamSink, Connection, PipelineEvalSite, ServerAddress, StreamHandle, StreamSink,
    server_frame_from_request,
};
use std::{collections::BTreeMap, sync::Arc};

use super::super::model::{
    Agent, AgentFabric, AgentManifest, LoopbackStream, agent_from_value, register_started_agent,
    resolve_agent_address, site_from_value,
};
use super::super::{
    build_agent_runtime_site, collect_agent_components, component_kind_matches, component_name,
    first_codec,
};
use super::agent_runtime::{create_agent_server, restart_agent_runtime};

pub(crate) fn agent_make_value(cx: &mut Cx, args: Args) -> Result<Value> {
    cx.require(&CapabilityName::new(AGENT_SPAWN_CAPABILITY))?;
    let options = parse_component_options(cx, args, "agent/make")?;
    let name = symbol_option(cx, &options, "name", Symbol::new("agent"))?;
    let policy = match options.get("policy") {
        Some(value) => sim_lib_server::IsolationPolicy::from_expr(&value.object().as_expr(cx)?)?,
        None => sim_lib_server::IsolationPolicy::default(),
    };
    let manifest = AgentManifest {
        runners: values_option(cx, &options, "runners")?,
        tools: values_option(cx, &options, "tools")?,
        memories: values_option(cx, &options, "memories")?,
        planner: optional_value(&options, "planner"),
        judge: optional_value(&options, "judge"),
        router: optional_value(&options, "router"),
        persona: optional_value(&options, "persona"),
        retrievers: values_option(cx, &options, "retrievers")?,
        sandbox: optional_value(&options, "sandbox"),
        recorders: values_option(cx, &options, "recorders")?,
        voice: optional_value(&options, "voice"),
        topology: optional_value(&options, "topology"),
        extras: BTreeMap::new(),
    };
    let capabilities = capabilities_option(cx, &options, "capable")?;
    let agent = Agent::new(name, manifest, capabilities, policy, installed_codecs(cx));
    cx.factory().opaque(Arc::new(agent))
}

pub(crate) fn agent_start_value(cx: &mut Cx, args: Args) -> Result<Value> {
    cx.require(&CapabilityName::new(AGENT_SPAWN_CAPABILITY))?;
    let [agent_value, rest @ ..] = args.values() else {
        return Err(Error::Eval(
            "agent/start expects an agent and optional key/value options".to_owned(),
        ));
    };
    let agent = agent_from_value(agent_value)?;
    let options = parse_component_options(cx, Args::new(rest.to_vec()), "agent/start")?;
    let address = match options.get("address") {
        Some(value) => ServerAddress::from_expr(&value.object().as_expr(cx)?)?,
        None => ServerAddress::Agent {
            agent: agent.name.to_string(),
        },
    };
    let selected_codec = match options.get("codec") {
        Some(value) => symbol_from_value(cx, value.clone(), "agent/start :codec expects a symbol")?,
        None => first_codec(&installed_codecs(cx)),
    };
    let supported_codecs = installed_codecs(cx);
    let manifest = agent.manifest_clone()?;
    let runtime_site = build_agent_runtime_site(&manifest, &supported_codecs, &agent.capabilities)?;
    let server = Arc::new(create_agent_server(
        agent,
        runtime_site.clone(),
        address.clone(),
        supported_codecs.clone(),
        selected_codec.clone(),
    )?);
    {
        let mut state = agent
            .state
            .lock()
            .map_err(|_| Error::PoisonedLock("agent state"))?;
        state.address = Some(address);
        state.server = Some(server);
        state.default_codec = selected_codec;
        state.supported_codecs = supported_codecs;
        state.runtime_site = runtime_site;
    }
    register_started_agent(agent)?;
    Ok(agent_value.clone())
}

pub(crate) fn agent_connect_value(cx: &mut Cx, args: Args) -> Result<Value> {
    let [target, rest @ ..] = args.values() else {
        return Err(Error::Eval("agent/connect expects a target".to_owned()));
    };
    let mut codec = None;
    if !rest.len().is_multiple_of(2) {
        return Err(Error::Eval(
            "agent/connect options must be key/value pairs".to_owned(),
        ));
    }
    for pair in rest.chunks(2) {
        let key = crate::keyword(&pair[0].object().as_expr(cx)?)?;
        if key == "codec" {
            codec = Some(symbol_from_value(
                cx,
                pair[1].clone(),
                "agent/connect :codec expects a symbol",
            )?);
        }
    }
    let connection = if let Ok(agent) = agent_from_value(target) {
        let state = agent
            .state
            .lock()
            .map_err(|_| Error::PoisonedLock("agent state"))?;
        let selected_codec = codec.unwrap_or_else(|| state.default_codec.clone());
        let supported_codecs = state.supported_codecs.clone();
        drop(state);
        Connection::with_session(
            ServerAddress::Local,
            selected_codec,
            supported_codecs,
            agent.site()?,
            None,
            agent.policy.clone(),
        )?
    } else {
        let address = ServerAddress::from_expr(&target.object().as_expr(cx)?)?;
        if let Some(resolved) = resolve_agent_address(cx, &address, &installed_codecs(cx))? {
            Connection::with_session(
                address,
                codec.unwrap_or(resolved.selected_codec),
                resolved.supported_codecs,
                resolved.site,
                None,
                sim_lib_server::IsolationPolicy::default(),
            )?
        } else {
            let site = site_from_value(target)?;
            Connection::with_session(
                address,
                codec.unwrap_or_else(|| first_codec(site.codecs())),
                site.codecs().to_vec(),
                site,
                None,
                sim_lib_server::IsolationPolicy::default(),
            )?
        }
    };
    cx.factory().opaque(Arc::new(connection))
}

pub(crate) fn agent_call_value(cx: &mut Cx, args: Args) -> Result<Value> {
    let [target, expr, ..] = args.values() else {
        return Err(Error::Eval(
            "agent/call expects an agent and an expression".to_owned(),
        ));
    };
    let connection = agent_connection_for_value(target.clone())?;
    let request_expr = expr.object().as_expr(cx)?;
    connection.request(cx, request_expr, None, Vec::new())
}

pub(crate) fn agent_stream_value(cx: &mut Cx, args: Args) -> Result<Value> {
    let [target, expr, ..] = args.values() else {
        return Err(Error::Eval(
            "agent/stream expects an agent and an expression".to_owned(),
        ));
    };
    let connection = agent_connection_for_value(target.clone())?;
    let request_expr = expr.object().as_expr(cx)?;
    let consistency =
        if connection.address().is_remote_like() || connection.site().address().is_remote_like() {
            sim_kernel::Consistency::RemoteOnly
        } else {
            sim_kernel::Consistency::LocalFirst
        };
    let mut frame = server_frame_from_request(
        cx,
        connection.default_codec(),
        sim_kernel::EvalRequest {
            expr: request_expr,
            mode: sim_kernel::EvalMode::Eval,
            result_shape: None,
            answer_limit: None,
            stream_buffer: None,
            stream: true,
            required_capabilities: Vec::new(),
            deadline: None,
            consistency,
            trace: false,
        },
    )?;
    frame.envelope.role = connection.role().cloned();
    let handle = Arc::new(StreamHandle::default());
    let mut sink = BufferedStreamSink::new(handle.clone());
    connection.site().stream(cx, frame, &mut sink)?;
    sink.end(cx)?;
    let stream = LoopbackStream::new(handle);
    cx.factory().opaque(Arc::new(stream))
}

pub(crate) fn agent_component_value(cx: &mut Cx, args: Args) -> Result<Value> {
    let [agent_value, kind_value, maybe_name @ ..] = args.values() else {
        return Err(Error::Eval(
            "agent/component expects an agent, kind, and optional name".to_owned(),
        ));
    };
    let agent = agent_from_value(agent_value)?;
    let kind = stringish_from_value(cx, kind_value.clone(), "agent/component expects a kind")?;
    let name = maybe_name
        .first()
        .map(|value| stringish_from_value(cx, value.clone(), "agent/component expects a name"))
        .transpose()?;
    for component in collect_agent_components(&agent.manifest_clone()?) {
        if component_kind_matches(cx, &component, &kind)?
            && name.as_ref().is_none_or(|expected| {
                component_name(&component)
                    .map(|symbol| symbol.to_string() == *expected)
                    .unwrap_or(false)
            })
        {
            return Ok(component);
        }
    }
    Err(Error::Eval(format!(
        "agent {} has no component for kind {}",
        agent.name, kind
    )))
}

pub(crate) fn agent_components_value(cx: &mut Cx, args: Args) -> Result<Value> {
    let [agent_value] = args.values() else {
        return Err(Error::Eval("agent/components expects one agent".to_owned()));
    };
    let agent = agent_from_value(agent_value)?;
    let components = collect_agent_components(&agent.manifest_clone()?);
    cx.factory().list(components)
}

pub(crate) fn agent_replace_value(cx: &mut Cx, args: Args) -> Result<Value> {
    cx.require(&CapabilityName::new(AGENT_REPLACE_CAPABILITY))?;
    let [agent_value, slot, replacement] = args.values() else {
        return Err(Error::Eval(
            "agent/replace expects an agent, slot, and component".to_owned(),
        ));
    };
    let agent = agent_from_value(agent_value)?;
    let slot = stringish_from_value(cx, slot.clone(), "agent/replace expects a slot name")?;
    {
        let mut manifest = agent
            .manifest
            .lock()
            .map_err(|_| Error::PoisonedLock("agent manifest"))?;
        match slot.as_str() {
            "runners" | "runner" => manifest.runners = vec![replacement.clone()],
            "planner" => manifest.planner = Some(replacement.clone()),
            "judge" => manifest.judge = Some(replacement.clone()),
            "router" => manifest.router = Some(replacement.clone()),
            "persona" => manifest.persona = Some(replacement.clone()),
            "sandbox" => manifest.sandbox = Some(replacement.clone()),
            "voice" => manifest.voice = Some(replacement.clone()),
            "topology" => manifest.topology = Some(replacement.clone()),
            "tools" | "tool" => manifest.tools = vec![replacement.clone()],
            "memories" | "memory" => manifest.memories = vec![replacement.clone()],
            "retrievers" | "retriever" => manifest.retrievers = vec![replacement.clone()],
            "recorders" | "recorder" => manifest.recorders = vec![replacement.clone()],
            other => {
                manifest
                    .extras
                    .insert(Symbol::new(other), replacement.clone());
            }
        }
    }
    restart_agent_runtime(cx, agent)?;
    Ok(agent_value.clone())
}

pub(crate) fn agent_restart_value(cx: &mut Cx, args: Args) -> Result<Value> {
    let [agent_value] = args.values() else {
        return Err(Error::Eval("agent/restart expects one agent".to_owned()));
    };
    let agent = agent_from_value(agent_value)?;
    restart_agent_runtime(cx, agent)?;
    Ok(agent_value.clone())
}

pub(crate) fn agent_derive_value(cx: &mut Cx, args: Args) -> Result<Value> {
    cx.require(&CapabilityName::new(AGENT_SPAWN_CAPABILITY))?;
    let [agent_value, rest @ ..] = args.values() else {
        return Err(Error::Eval("agent/derive expects an agent".to_owned()));
    };
    let source = agent_from_value(agent_value)?;
    let options = parse_component_options(cx, Args::new(rest.to_vec()), "agent/derive")?;
    let mut manifest = source.manifest_clone()?;
    if let Some(persona) = options.get("persona") {
        manifest.persona = Some(persona.clone());
    }
    let name = match options.get("name") {
        Some(value) => symbol_from_value(cx, value.clone(), "agent/derive :name expects a symbol")?,
        None => Symbol::new(format!("{}-derived", source.name)),
    };
    let derived = Agent::new(
        name,
        manifest,
        source.capabilities.clone(),
        source.policy.clone(),
        installed_codecs(cx),
    );
    cx.factory().opaque(Arc::new(derived))
}

pub(crate) fn agent_lisp_value(cx: &mut Cx, args: Args) -> Result<Value> {
    let [agent_value] = args.values() else {
        return Err(Error::Eval("agent/lisp expects one agent".to_owned()));
    };
    let agent = agent_from_value(agent_value)?;
    let expr = agent_make_expr(cx, agent)?;
    cx.factory().expr(expr)
}

pub(crate) fn agent_reflect_value(cx: &mut Cx, args: Args) -> Result<Value> {
    cx.require(&CapabilityName::new(AGENT_REFLECT_CAPABILITY))?;
    let [agent_value] = args.values() else {
        return Err(Error::Eval("agent/reflect expects one agent".to_owned()));
    };
    agent_value.object().as_table(cx)
}

pub(crate) fn agent_wire_value(cx: &mut Cx, args: Args) -> Result<Value> {
    let options = parse_component_options(cx, args, "agent/wire")?;
    let steps = values_option(cx, &options, "steps")?;
    let role_tag = string_option(cx, &options, "role-tag", "auto")?;
    let connections = steps
        .into_iter()
        .map(|step| wire_step_connection(step, &role_tag))
        .collect::<Result<Vec<_>>>()?;
    let codecs = installed_codecs(cx);
    let connection = Connection::with_session(
        ServerAddress::Pipeline {
            steps: connections
                .iter()
                .map(|step| step.address().clone())
                .collect(),
        },
        first_codec(&codecs),
        codecs.clone(),
        Arc::new(PipelineEvalSite::new(
            ServerAddress::Pipeline {
                steps: connections
                    .iter()
                    .map(|step| step.address().clone())
                    .collect(),
            },
            codecs.clone(),
            connections,
        )),
        None,
        sim_lib_server::IsolationPolicy::default(),
    )?;
    cx.factory().opaque(Arc::new(connection))
}

#[allow(dead_code)]
fn _assert_types(
    _: &Tool,
    _: &WorkingMemory,
    _: &FileMemory,
    _: &VectorMemory,
    _: &BlackboardMemory,
    _: &PersonaMemory,
    _: &AgentFabric,
) {
}