Sausage: host, broker, commit-on-close filesystem, cargo actor, console, agent
Imported from the spike branch, with: - a library crate (Host API) for the CLI and the tests - JSON test scenarios for candidates - commit on close only for new or truncated files - a Nix flake: nix run -- init, nix run -- run
This commit is contained in:
+125
@@ -0,0 +1,125 @@
|
||||
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 sausage::world::{self, VersionMeta, World};
|
||||
use sausage::{Broker, Host, cargo_actor, console, runtime};
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// Reads SAUSAGE_API, SAUSAGE_BASE_URL, SAUSAGE_MODEL and
|
||||
/// SAUSAGE_API_TOKEN, and asks for every value that is missing.
|
||||
Init {
|
||||
/// Folder of the new world.
|
||||
#[arg(default_value = "world")]
|
||||
dir: PathBuf,
|
||||
},
|
||||
/// Start the broker, all actors, and the console.
|
||||
Run {
|
||||
#[arg(default_value = "world")]
|
||||
dir: PathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
/// The value of `var`, or the answer to `question`.
|
||||
fn setting(var: &str, question: &str, default: Option<&str>) -> Result<String> {
|
||||
if let Ok(v) = std::env::var(var)
|
||||
&& !v.is_empty()
|
||||
{
|
||||
return Ok(v);
|
||||
}
|
||||
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)?;
|
||||
match (line.trim(), default) {
|
||||
("", Some(d)) => Ok(d.to_string()),
|
||||
("", None) => bail!("{question} is required (or set {var})"),
|
||||
(v, _) => Ok(v.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn init(dir: PathBuf) -> Result<()> {
|
||||
let api = setting("SAUSAGE_API", "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 = setting("SAUSAGE_BASE_URL", "API base URL", None)?;
|
||||
let model = setting("SAUSAGE_MODEL", "Model", None)?;
|
||||
let token = setting("SAUSAGE_API_TOKEN", "API token", None)?;
|
||||
|
||||
let world = World::create(&dir)?;
|
||||
let mut reg = world.load_registry()?;
|
||||
reg.subscriptions.entry("agent".into()).or_default().insert("console".into());
|
||||
world.save_registry(®)?;
|
||||
|
||||
// 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))?;
|
||||
}
|
||||
|
||||
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 out = cargo_actor::Cargo::new(world.clone()).build(&source).await.context("build agent")?;
|
||||
println!("Built in {:.1} s.", out.seconds);
|
||||
|
||||
let broker = Broker::new(world.clone(), runtime::Runtime::new()?)?;
|
||||
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. Start it with: sausage run {}", dir.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run(dir: PathBuf) -> Result<()> {
|
||||
let host = Host::start(&dir).await?;
|
||||
let rx = host.mailbox(console::ID);
|
||||
let console = console::Console::new(host.broker.clone());
|
||||
tokio::spawn(Arc::clone(&console).run_mailbox(rx));
|
||||
console.run_input().await;
|
||||
println!("Stopping actors …");
|
||||
host.stop().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
match Cli::parse().command {
|
||||
Cmd::Init { dir } => init(dir).await,
|
||||
Cmd::Run { dir } => run(dir).await,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user