YAML configuration, init without a wizard, and openai-chat-completions
ci / check (push) Successful in 15m52s
ci / check (push) Successful in 15m52s
- /config/agent.yaml and /config/boot.yaml replace the JSON files, read with serde_yaml_ng (also in the agent, inside WASM). - sausage init asks nothing and reads no SAUSAGE_* variables. It writes commented templates with an example for each API, and prints the path to edit. Until the agent is configured, agent.chat names the missing field. allow_no_token: true permits a local model without a key. - The agent speaks openai-chat-completions, the format of OpenRouter, Ollama, and most providers. base_url follows each SDK: with /v1 for the OpenAI shapes, without it for Anthropic. - Checked against the proxy with glm-5.3-flash: the agent wrote, built, and activated a counter plugin in 55 s.
This commit is contained in:
+23
-40
@@ -3,18 +3,14 @@ use anyhow::{Result, bail};
|
||||
use clap::{Parser, Subcommand};
|
||||
use include_dir::{Dir, include_dir};
|
||||
use sausage::{Host, world::World};
|
||||
use serde_json::json;
|
||||
use std::{
|
||||
io::Write,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
static AGENT: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../agent/src");
|
||||
mod test_runner;
|
||||
#[derive(Parser)]
|
||||
#[command(version)]
|
||||
struct Cli {
|
||||
#[arg(long, global = true, env = "SAUSAGE_HEADLESS", value_parser = headless)]
|
||||
#[arg(long, global = true)]
|
||||
headless: bool,
|
||||
#[arg(long, global = true)]
|
||||
cache_dir: Option<PathBuf>,
|
||||
@@ -60,33 +56,8 @@ impl Drop for TerminalMode {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
print!("{question}: ");
|
||||
std::io::stdout().flush()?;
|
||||
let mut line = String::new();
|
||||
std::io::stdin().read_line(&mut line)?;
|
||||
if !line.trim().is_empty() {
|
||||
return Ok(line.trim().into());
|
||||
}
|
||||
match default {
|
||||
Some(v) => Ok(v.into()),
|
||||
None => bail!("{question} is required"),
|
||||
}
|
||||
}
|
||||
/// Creates a configured world and builds its embedded agent through the standalone builder.
|
||||
/// Creates a world with editable configuration and builds its embedded agent.
|
||||
async fn init(dir: &Path, cache: Option<&Path>) -> Result<()> {
|
||||
let api = setting("SAUSAGE_API", "API shape", Some("openai-responses"))?;
|
||||
if api != "openai-responses" && api != "anthropic-messages" {
|
||||
bail!("unknown API shape {api:?}");
|
||||
}
|
||||
let base_url = setting("SAUSAGE_BASE_URL", "Base URL", None)?;
|
||||
let model = setting("SAUSAGE_MODEL", "Model", None)?;
|
||||
let token = setting("SAUSAGE_API_TOKEN", "API token", None)?;
|
||||
if dir.exists() {
|
||||
bail!("world already exists: {}", dir.display());
|
||||
}
|
||||
@@ -100,10 +71,12 @@ async fn init(dir: &Path, cache: Option<&Path>) -> Result<()> {
|
||||
.tempdir_in(parent)?;
|
||||
let world = World::create(&staging.path().join("world"))?;
|
||||
std::fs::write(
|
||||
world.root.join("config/agent.json"),
|
||||
serde_json::to_vec_pretty(
|
||||
&json!({"api":api,"base_url":base_url.trim_end_matches('/'),"model":model,"token":token}),
|
||||
)?,
|
||||
world.root.join("config/agent.yaml"),
|
||||
include_str!("agent.yaml"),
|
||||
)?;
|
||||
std::fs::write(
|
||||
world.root.join("config/boot.yaml"),
|
||||
include_str!("boot.yaml"),
|
||||
)?;
|
||||
let cache = cache.map(Path::to_path_buf).unwrap_or_else(|| {
|
||||
let base = std::env::var_os("XDG_CACHE_HOME")
|
||||
@@ -129,6 +102,10 @@ async fn init(dir: &Path, cache: Option<&Path>) -> Result<()> {
|
||||
let output = builder.build(&source).await?;
|
||||
std::fs::copy(output.wasm, world.plugin_dir("agent").join("plugin.wasm"))?;
|
||||
std::fs::rename(&world.root, dir)?;
|
||||
println!(
|
||||
"Edit {} before chatting.",
|
||||
dir.join("config/agent.yaml").display()
|
||||
);
|
||||
println!("Start the world with: sausage run {}", dir.display());
|
||||
Ok(())
|
||||
}
|
||||
@@ -140,7 +117,7 @@ async fn main() -> Result<std::process::ExitCode> {
|
||||
Some(command) => command,
|
||||
None => {
|
||||
let dir = PathBuf::from("world");
|
||||
if !dir.join("config/boot.json").is_file() {
|
||||
if !dir.join("config/boot.yaml").is_file() {
|
||||
init(&dir, cli.cache_dir.as_deref()).await?;
|
||||
}
|
||||
Cmd::Run { dir }
|
||||
@@ -154,6 +131,12 @@ async fn main() -> Result<std::process::ExitCode> {
|
||||
}
|
||||
Cmd::Init { dir } => init(&dir, cli.cache_dir.as_deref()).await?,
|
||||
Cmd::Run { dir } => {
|
||||
let headless = cli.headless
|
||||
|| std::env::var("SAUSAGE_HEADLESS")
|
||||
.ok()
|
||||
.map(|value| headless(&value).map_err(anyhow::Error::msg))
|
||||
.transpose()?
|
||||
.unwrap_or(false);
|
||||
let mut term =
|
||||
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?;
|
||||
let mut interrupt =
|
||||
@@ -170,7 +153,7 @@ async fn main() -> Result<std::process::ExitCode> {
|
||||
_=interrupt.recv()=>return Ok(std::process::ExitCode::SUCCESS),
|
||||
};
|
||||
let mut notifications = host.subscribe();
|
||||
let notification_task = cli.headless.then(|| {
|
||||
let notification_task = headless.then(|| {
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match notifications.recv().await {
|
||||
@@ -186,8 +169,8 @@ async fn main() -> Result<std::process::ExitCode> {
|
||||
let exit = tokio::select! {
|
||||
_=host.exit_requested()=>true,
|
||||
_=term.recv()=>false,
|
||||
_=interrupt.recv(), if cli.headless=>false,
|
||||
result = console, if !cli.headless => {
|
||||
_=interrupt.recv(), if headless=>false,
|
||||
result = console, if !headless => {
|
||||
console_result=result;
|
||||
false
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user