185 lines
6.1 KiB
Rust
185 lines
6.1 KiB
Rust
//! Sausage: grow a system of WASI actors by talking to it.
|
|
|
|
mod broker;
|
|
mod candidate;
|
|
mod cargo_actor;
|
|
mod console;
|
|
mod fs;
|
|
mod runtime;
|
|
mod world;
|
|
|
|
use std::io::Write;
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
|
|
use anyhow::{Context, Result, bail};
|
|
use clap::{Parser, Subcommand};
|
|
use include_dir::{Dir, include_dir};
|
|
use serde_json::json;
|
|
|
|
use crate::broker::Broker;
|
|
use crate::world::{NATIVE, Registry, VersionMeta, World};
|
|
|
|
/// The source of the agent actor. `init` builds it like any other actor.
|
|
static AGENT_SOURCE: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../agent");
|
|
|
|
#[derive(Parser)]
|
|
#[command(version, about)]
|
|
struct Cli {
|
|
#[command(subcommand)]
|
|
command: Cmd,
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
enum Cmd {
|
|
/// Create a new world and build the agent actor.
|
|
Init {
|
|
/// Folder of the new world.
|
|
#[arg(default_value = "world")]
|
|
dir: PathBuf,
|
|
/// API shape: `openai-responses` or `anthropic-messages`.
|
|
#[arg(long)]
|
|
api: Option<String>,
|
|
/// Base URL of the inference API (for example `https://api.openai.com`).
|
|
#[arg(long)]
|
|
base_url: Option<String>,
|
|
/// Model ID.
|
|
#[arg(long)]
|
|
model: Option<String>,
|
|
},
|
|
/// Start the broker, all actors, and the console.
|
|
Run {
|
|
#[arg(default_value = "world")]
|
|
dir: PathBuf,
|
|
},
|
|
}
|
|
|
|
fn prompt(question: &str, default: Option<&str>) -> Result<String> {
|
|
match default {
|
|
Some(d) => print!("{question} [{d}]: "),
|
|
None => print!("{question}: "),
|
|
}
|
|
std::io::stdout().flush()?;
|
|
let mut line = String::new();
|
|
std::io::stdin().read_line(&mut line)?;
|
|
let v = line.trim().to_string();
|
|
match (v.is_empty(), default) {
|
|
(true, Some(d)) => Ok(d.to_string()),
|
|
(true, None) => bail!("{question} is required"),
|
|
_ => Ok(v),
|
|
}
|
|
}
|
|
|
|
async fn init(dir: PathBuf, api: Option<String>, base_url: Option<String>, model: Option<String>) -> Result<()> {
|
|
let world = World::new(std::path::absolute(&dir)?);
|
|
if world.exists() {
|
|
bail!("{} is already a Sausage world", dir.display());
|
|
}
|
|
let api = match api {
|
|
Some(a) => a,
|
|
None => prompt("API shape (openai-responses or anthropic-messages)", Some("openai-responses"))?,
|
|
};
|
|
if api != "openai-responses" && api != "anthropic-messages" {
|
|
bail!("unknown API shape {api:?}");
|
|
}
|
|
let base_url = match base_url {
|
|
Some(u) => u,
|
|
None => prompt("API base URL", None)?,
|
|
};
|
|
let model = match model {
|
|
Some(m) => m,
|
|
None => prompt("Model", None)?,
|
|
};
|
|
let token = match std::env::var("SAUSAGE_API_TOKEN") {
|
|
Ok(t) if !t.is_empty() => t,
|
|
_ => prompt("API token", None)?,
|
|
};
|
|
|
|
std::fs::create_dir_all(&world.root)?;
|
|
let mut reg = Registry::default();
|
|
reg.subscriptions.entry("agent".into()).or_default().insert("console".into());
|
|
world.save_registry(®)?;
|
|
for id in NATIVE {
|
|
world.ensure_actor_dirs(id)?;
|
|
world::write_atomic(
|
|
&world.actor_dir(id).join("status.json"),
|
|
serde_json::to_string_pretty(&json!({"id": id, "kind": "native", "state": "built-in"}))?.as_bytes(),
|
|
)?;
|
|
}
|
|
|
|
// The agent's private configuration. Only the agent can read /data.
|
|
world.ensure_actor_dirs("agent")?;
|
|
let config = json!({"api": api, "base_url": base_url.trim_end_matches('/'), "model": model, "token": token});
|
|
let config_path = world.data_dir("agent").join("config.json");
|
|
std::fs::write(&config_path, serde_json::to_string_pretty(&config)?)?;
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
std::fs::set_permissions(&config_path, std::fs::Permissions::from_mode(0o600))?;
|
|
}
|
|
|
|
// Build the agent with the same path as every other Rust actor.
|
|
let source = world.root.join("broker").join("seed").join("agent");
|
|
std::fs::create_dir_all(&source)?;
|
|
AGENT_SOURCE.extract(&source).context("extract agent source")?;
|
|
println!("Building the agent actor (the first build takes a few minutes) …");
|
|
let cargo = cargo_actor::Cargo::new(world.clone());
|
|
let out = cargo.build(&source).await.context("build agent")?;
|
|
println!("Built in {:.1} s.", out.seconds);
|
|
|
|
let runtime = runtime::Runtime::new()?;
|
|
let broker = Broker::new(world.clone(), runtime)?;
|
|
let meta = VersionMeta {
|
|
version: 1,
|
|
wasm: world.global_path(&out.wasm).unwrap_or_default(),
|
|
source: Some(source.display().to_string()),
|
|
built_by: Some(cargo_actor::ID.into()),
|
|
requested_by: "init".into(),
|
|
created_at: world::now(),
|
|
};
|
|
broker.create_actor("agent", &out.wasm, Some(&source), meta, false).await?;
|
|
println!("World ready at {}. Start it with: sausage run {}", world.root.display(), dir.display());
|
|
Ok(())
|
|
}
|
|
|
|
async fn run(dir: PathBuf) -> Result<()> {
|
|
let world = World::new(std::path::absolute(&dir)?);
|
|
if !world.exists() {
|
|
bail!("{} is not a Sausage world. Create one with: sausage init {}", dir.display(), dir.display());
|
|
}
|
|
// One host process per world.
|
|
let lock = std::fs::File::create(world.root.join(".lock"))?;
|
|
if lock.try_lock().is_err() {
|
|
bail!("another sausage process runs {}", dir.display());
|
|
}
|
|
let runtime = runtime::Runtime::new()?;
|
|
let broker = Broker::new(world.clone(), runtime)?;
|
|
|
|
let rx = broker.open_mailbox("broker");
|
|
tokio::spawn(broker.clone().run_actor(rx));
|
|
|
|
let rx = broker.open_mailbox(cargo_actor::ID);
|
|
let cargo = Arc::new(cargo_actor::Cargo::new(world.clone()));
|
|
tokio::spawn(cargo.run_actor(broker.clone(), rx));
|
|
|
|
let rx = broker.open_mailbox(console::ID);
|
|
let console = console::Console::new(broker.clone());
|
|
tokio::spawn(console.clone().run_mailbox(rx));
|
|
|
|
broker.start_all();
|
|
console.run_input().await;
|
|
println!("Stopping actors …");
|
|
broker.shutdown_all().await;
|
|
drop(lock);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<()> {
|
|
let cli = Cli::parse();
|
|
match cli.command {
|
|
Cmd::Init { dir, api, base_url, model } => init(dir, api, base_url, model).await,
|
|
Cmd::Run { dir } => run(dir).await,
|
|
}
|
|
}
|