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:
@@ -17,6 +17,7 @@ http-body-util = "0.1"
|
||||
include_dir = "0.7"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_yaml_ng = "0.10"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
toml_edit = "0.25"
|
||||
wasmtime = "49"
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# The configuration of the agent. Edit it before the first chat.
|
||||
# api selects the request format. Choose one complete example below.
|
||||
api: openai-responses
|
||||
# OpenAI base URLs include /v1. Do not include /responses or /chat/completions.
|
||||
base_url: https://api.openai.com/v1
|
||||
# model is the provider's model identifier.
|
||||
model: gpt-4.1
|
||||
# token is the API key. Replace CHANGE_ME with your key.
|
||||
# An empty token requires allow_no_token: true. Then no authentication header is sent.
|
||||
token: CHANGE_ME
|
||||
allow_no_token: false
|
||||
# Optional output token cap. Anthropic defaults to 32000 if omitted.
|
||||
# max_tokens: 32000
|
||||
# Optional thinking effort: off, low, medium, high, xhigh, or max.
|
||||
# The agent retries a rejected effort with the nearest accepted level.
|
||||
# thinking: high
|
||||
# Optional OpenAI override for thinking effort.
|
||||
# reasoning_effort: high
|
||||
|
||||
# OpenAI Responses example (base_url includes the version):
|
||||
# api: openai-responses
|
||||
# base_url: https://api.openai.com/v1
|
||||
# model: gpt-4.1
|
||||
# token: YOUR_OPENAI_KEY
|
||||
# allow_no_token: false
|
||||
|
||||
# OpenRouter example (OpenAI Chat Completions, the format of most providers):
|
||||
# api: openai-chat-completions
|
||||
# base_url: https://openrouter.ai/api/v1
|
||||
# model: anthropic/claude-sonnet-4.5
|
||||
# token: YOUR_OPENROUTER_KEY
|
||||
# allow_no_token: false
|
||||
|
||||
# Local Ollama example (OpenAI Chat Completions, no key):
|
||||
# api: openai-chat-completions
|
||||
# base_url: http://localhost:11434/v1
|
||||
# model: llama3.2
|
||||
# token: ""
|
||||
# allow_no_token: true
|
||||
|
||||
# Anthropic Messages example (base_url excludes the version):
|
||||
# api: anthropic-messages
|
||||
# base_url: https://api.anthropic.com
|
||||
# model: claude-sonnet-4-5
|
||||
# token: YOUR_ANTHROPIC_KEY
|
||||
# allow_no_token: false
|
||||
@@ -0,0 +1,9 @@
|
||||
# The core starts these services in order. An empty list starts no services.
|
||||
# Each method is a plugin.method name. params contains its named arguments.
|
||||
# The core waits for each result before it starts the next service.
|
||||
# After a plugin upgrade, the core restarts that plugin's services.
|
||||
# Example (replace [] with this list):
|
||||
# - method: www.start
|
||||
# params:
|
||||
# port: 8080
|
||||
[]
|
||||
+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
|
||||
},
|
||||
|
||||
+2
-2
@@ -12,8 +12,8 @@ impl Host {
|
||||
/// Starts discovery in the background, then waits for each boot service in order.
|
||||
pub(crate) async fn boot(&self) -> anyhow::Result<()> {
|
||||
self.watch_plugins()?;
|
||||
let boot: Vec<Value> = serde_json::from_slice(
|
||||
&tokio::fs::read(self.0.world.root.join("config/boot.json")).await?,
|
||||
let boot: Vec<Value> = serde_yaml_ng::from_slice(
|
||||
&tokio::fs::read(self.0.world.root.join("config/boot.yaml")).await?,
|
||||
)?;
|
||||
for service in boot {
|
||||
self.start_service(
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ impl World {
|
||||
for dir in ["plugins", "state/core", "config"] {
|
||||
std::fs::create_dir_all(path.join(dir))?;
|
||||
}
|
||||
std::fs::write(path.join("config/boot.json"), "[]\n")?;
|
||||
std::fs::write(path.join("config/boot.yaml"), "[]\n")?;
|
||||
Self::open(path)
|
||||
}
|
||||
/// Return the folder that contains a plugin component.
|
||||
|
||||
@@ -232,7 +232,7 @@ async fn review_shutdown_suppresses_hooks() {
|
||||
old["x-sausage-grace"] = json!(60);
|
||||
world.plugin("notes", old, document("serve"));
|
||||
std::fs::write(
|
||||
world.0.join("config/boot.json"),
|
||||
world.0.join("config/boot.yaml"),
|
||||
json!([{"method":"notes.serve","params":{}}]).to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
@@ -766,7 +766,7 @@ async fn restarts_service() {
|
||||
let world = TestWorld::new();
|
||||
world.plugin("notes", document("serve"), document("serve"));
|
||||
std::fs::write(
|
||||
world.0.join("config/boot.json"),
|
||||
world.0.join("config/boot.yaml"),
|
||||
json!([{"method":"notes.serve","params":{}}]).to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
+84
-15
@@ -13,18 +13,22 @@ use tokio::{
|
||||
};
|
||||
|
||||
/// The two HTTP contracts supported by the agent.
|
||||
const APIS: [&str; 2] = ["openai-responses", "anthropic-messages"];
|
||||
const APIS: [&str; 3] = [
|
||||
"openai-responses",
|
||||
"anthropic-messages",
|
||||
"openai-chat-completions",
|
||||
];
|
||||
/// A captured request and the script's delayed HTTP response.
|
||||
type Exchange = (String, Value, oneshot::Sender<(u16, Value)>);
|
||||
/// The script controls each response, including concurrent requests.
|
||||
struct Llm {
|
||||
url: String,
|
||||
pub(super) struct Llm {
|
||||
pub(super) url: String,
|
||||
api: &'static str,
|
||||
requests: mpsc::UnboundedReceiver<Exchange>,
|
||||
task: tokio::task::JoinHandle<()>,
|
||||
}
|
||||
impl Llm {
|
||||
async fn new(api: &'static str) -> Self {
|
||||
pub(super) async fn new(api: &'static str) -> Self {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let url = format!("http://{}", listener.local_addr().unwrap());
|
||||
let (tx, requests) = mpsc::unbounded_channel::<Exchange>();
|
||||
@@ -37,6 +41,11 @@ impl Llm {
|
||||
service_fn(move |request: hyper::Request<hyper::body::Incoming>| {
|
||||
let tx = tx.clone();
|
||||
async move {
|
||||
assert_eq!(request.method(), "POST");
|
||||
// The local Chat Completions fixture explicitly disables authentication.
|
||||
if api == APIS[2] {
|
||||
assert!(!request.headers().contains_key("authorization"));
|
||||
}
|
||||
let path = request.uri().path().to_string();
|
||||
let bytes = request.into_body().collect().await.unwrap().to_bytes();
|
||||
let body = serde_json::from_slice(&bytes).unwrap();
|
||||
@@ -66,7 +75,7 @@ impl Llm {
|
||||
task,
|
||||
}
|
||||
}
|
||||
async fn next(&mut self) -> (Value, oneshot::Sender<(u16, Value)>) {
|
||||
pub(super) async fn next(&mut self) -> (Value, oneshot::Sender<(u16, Value)>) {
|
||||
let (path, body, reply) =
|
||||
tokio::time::timeout(Duration::from_secs(60), self.requests.recv())
|
||||
.await
|
||||
@@ -76,6 +85,8 @@ impl Llm {
|
||||
path,
|
||||
if self.api == APIS[0] {
|
||||
"/v1/responses"
|
||||
} else if self.api == APIS[2] {
|
||||
"/v1/chat/completions"
|
||||
} else {
|
||||
"/v1/messages"
|
||||
}
|
||||
@@ -83,8 +94,11 @@ impl Llm {
|
||||
assert_eq!(body["model"], "scripted-test");
|
||||
(body, reply)
|
||||
}
|
||||
fn answer(&self, text: &str, calls: &[(&str, Value)]) -> Value {
|
||||
if self.api == APIS[0] {
|
||||
pub(super) fn answer(&self, text: &str, calls: &[(&str, Value)]) -> Value {
|
||||
if self.api == APIS[2] {
|
||||
let calls: Vec<Value> = calls.iter().enumerate().map(|(n,(name,args))| json!({"id":format!("tool-{n}"),"type":"function","function":{"name":name,"arguments":args.to_string()}})).collect();
|
||||
json!({"choices":[{"message":{"role":"assistant","content":text,"tool_calls":calls}}]})
|
||||
} else if self.api == APIS[0] {
|
||||
let mut output =
|
||||
vec![json!({"type":"message","content":[{"type":"output_text","text":text}]})];
|
||||
for (n, (name, args)) in calls.iter().enumerate() {
|
||||
@@ -128,7 +142,7 @@ impl TestWorld {
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
std::fs::write(path.join("config/agent.json"),json!({"api":server.api,"base_url":server.url,"model":"scripted-test","token":"fake-token"}).to_string()).unwrap();
|
||||
std::fs::write(path.join("config/agent.yaml"), serde_yaml_ng::to_string(&json!({"api":server.api,"base_url":if server.api == APIS[1] {server.url.clone()} else {format!("{}/v1",server.url)},"model":"scripted-test","token":if server.api == APIS[2] {""} else {"fake-token"},"allow_no_token":server.api == APIS[2]})).unwrap()).unwrap();
|
||||
Self(path)
|
||||
}
|
||||
async fn start(&self) -> Host {
|
||||
@@ -165,7 +179,15 @@ fn input(body: &Value, api: &str) -> Value {
|
||||
body[if api == APIS[0] { "input" } else { "messages" }].clone()
|
||||
}
|
||||
fn outputs(body: &Value, api: &str) -> Vec<Value> {
|
||||
if api == APIS[0] {
|
||||
if api == APIS[2] {
|
||||
body["messages"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|e| e["role"] == "tool")
|
||||
.map(|e| serde_json::from_str(e["content"].as_str().unwrap()).unwrap())
|
||||
.collect()
|
||||
} else if api == APIS[0] {
|
||||
body["input"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
@@ -185,7 +207,54 @@ fn outputs(body: &Value, api: &str) -> Vec<Value> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Both API shapes return final text and retain the complete history after a core restart.
|
||||
/// Invalid configuration names the file and the field before any inference request.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn unconfigured_chat_names_the_configuration() {
|
||||
let server = Llm::new(APIS[0]).await;
|
||||
let world = TestWorld::new(&server);
|
||||
let path = world.0.join("config/agent.yaml");
|
||||
std::fs::remove_file(&path).unwrap();
|
||||
let host = world.start().await;
|
||||
let error = host
|
||||
.call("agent.chat", json!({"text":"hello"}))
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
error["message"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.contains("/config/agent.yaml")
|
||||
);
|
||||
for (config, field) in [
|
||||
("", "api"),
|
||||
("api: unknown", "api"),
|
||||
("api: openai-responses", "base_url"),
|
||||
(
|
||||
"api: openai-responses\nbase_url: http://localhost/v1",
|
||||
"model",
|
||||
),
|
||||
(
|
||||
"api: openai-responses\nbase_url: http://localhost/v1\nmodel: test\ntoken: ''",
|
||||
"token",
|
||||
),
|
||||
(
|
||||
"api: openai-responses\nbase_url: http://localhost/v1\nmodel: test\ntoken: CHANGE_ME",
|
||||
"token",
|
||||
),
|
||||
] {
|
||||
std::fs::write(&path, config).unwrap();
|
||||
let error = host
|
||||
.call("agent.chat", json!({"text":"hello"}))
|
||||
.await
|
||||
.unwrap_err();
|
||||
let message = error["message"].as_str().unwrap();
|
||||
assert!(message.contains("/config/agent.yaml"), "{message}");
|
||||
assert!(message.contains(field), "{message}");
|
||||
}
|
||||
host.stop().await;
|
||||
}
|
||||
|
||||
/// All API shapes return final text and retain the complete history after a core restart.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn final_answers_and_history_survive_restart() {
|
||||
for api in APIS {
|
||||
@@ -435,14 +504,14 @@ async fn oversized_current_turn_preserves_the_previous_cursor() {
|
||||
/// Provider-private items are replayed only for their original API and model.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn private_items_are_scoped_to_the_model() {
|
||||
for api in APIS {
|
||||
for api in [APIS[0], APIS[1]] {
|
||||
let mut server = Llm::new(api).await;
|
||||
let world = TestWorld::new(&server);
|
||||
let config_path = world.0.join("config/agent.json");
|
||||
let config_path = world.0.join("config/agent.yaml");
|
||||
let mut config: Value =
|
||||
serde_json::from_slice(&std::fs::read(&config_path).unwrap()).unwrap();
|
||||
serde_yaml_ng::from_slice(&std::fs::read(&config_path).unwrap()).unwrap();
|
||||
config["thinking"] = json!("high");
|
||||
std::fs::write(config_path, config.to_string()).unwrap();
|
||||
std::fs::write(config_path, serde_yaml_ng::to_string(&config).unwrap()).unwrap();
|
||||
std::fs::create_dir_all(world.0.join("state/agent")).unwrap();
|
||||
let mut entries = vec![json!({"kind":"user","text":"old turn"})];
|
||||
for (model, marker) in [
|
||||
@@ -480,7 +549,7 @@ async fn private_items_are_scoped_to_the_model() {
|
||||
/// Separate assistant messages retain a separator in the final answer.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn final_texts_have_a_separator() {
|
||||
for api in APIS {
|
||||
for api in [APIS[0], APIS[1]] {
|
||||
let mut server = Llm::new(api).await;
|
||||
let world = TestWorld::new(&server);
|
||||
let host = world.start().await;
|
||||
|
||||
+42
-11
@@ -59,11 +59,12 @@ fn install_probe(world: &Path) {
|
||||
async fn init_builds_agent() {
|
||||
let temporary = tempfile::tempdir().unwrap();
|
||||
let world = temporary.path().join("world");
|
||||
// Legacy configuration variables must not affect the templates.
|
||||
let configured = || {
|
||||
let mut command = binary();
|
||||
command
|
||||
.current_dir(temporary.path())
|
||||
.env("SAUSAGE_API", "anthropic-messages")
|
||||
.env("SAUSAGE_API", "not-an-api")
|
||||
.env("SAUSAGE_BASE_URL", "http://127.0.0.1:1/")
|
||||
.env("SAUSAGE_MODEL", "init-test")
|
||||
.env("SAUSAGE_API_TOKEN", "fake-token")
|
||||
@@ -72,11 +73,13 @@ async fn init_builds_agent() {
|
||||
};
|
||||
let failed = configured()
|
||||
.arg("init")
|
||||
.env("SAUSAGE_HEADLESS", "not-a-boolean")
|
||||
.env("RUSTC", "/nonexistent-sausage-rustc")
|
||||
.output()
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(!failed.status.success());
|
||||
assert!(String::from_utf8_lossy(&failed.stderr).contains("nonexistent-sausage-rustc"));
|
||||
assert!(!world.exists());
|
||||
let output = configured().output().await.unwrap();
|
||||
assert!(
|
||||
@@ -88,15 +91,14 @@ async fn init_builds_agent() {
|
||||
assert!(
|
||||
String::from_utf8_lossy(&output.stdout).contains("first agent build takes a few minutes")
|
||||
);
|
||||
let config: Value =
|
||||
serde_json::from_slice(&std::fs::read(world.join("config/agent.json")).unwrap()).unwrap();
|
||||
let mut config: Value =
|
||||
serde_yaml_ng::from_slice(&std::fs::read(world.join("config/agent.yaml")).unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(config["token"], "CHANGE_ME");
|
||||
assert_eq!(config["api"], "openai-responses");
|
||||
assert!(String::from_utf8_lossy(&output.stdout).contains("config/agent.yaml"));
|
||||
assert_eq!(
|
||||
config,
|
||||
json!({"api":"anthropic-messages","base_url":"http://127.0.0.1:1","model":"init-test","token":"fake-token"})
|
||||
);
|
||||
assert!(String::from_utf8_lossy(&output.stdout).contains("sausage run"));
|
||||
assert_eq!(
|
||||
serde_json::from_slice::<Value>(&std::fs::read(world.join("config/boot.json")).unwrap())
|
||||
serde_yaml_ng::from_slice::<Value>(&std::fs::read(world.join("config/boot.yaml")).unwrap())
|
||||
.unwrap(),
|
||||
json!([])
|
||||
);
|
||||
@@ -131,6 +133,35 @@ async fn init_builds_agent() {
|
||||
.iter()
|
||||
.any(|method| method["name"] == "chat")
|
||||
);
|
||||
let error = host
|
||||
.call("agent.chat", json!({"text":"hello"}))
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
error["message"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.contains("/config/agent.yaml")
|
||||
);
|
||||
assert!(error["message"].as_str().unwrap().contains("token"));
|
||||
let mut server = super::agent::Llm::new("anthropic-messages").await;
|
||||
config["api"] = json!("anthropic-messages");
|
||||
config["base_url"] = json!(server.url);
|
||||
config["model"] = json!("scripted-test");
|
||||
config["token"] = json!("fake-token");
|
||||
std::fs::write(
|
||||
world.join("config/agent.yaml"),
|
||||
serde_yaml_ng::to_string(&config).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let caller = host.clone();
|
||||
let turn =
|
||||
tokio::spawn(async move { caller.call("agent.chat", json!({"text":"hello"})).await });
|
||||
let (_, reply) = server.next().await;
|
||||
reply
|
||||
.send((200, server.answer("configured agent", &[])))
|
||||
.unwrap();
|
||||
assert_eq!(turn.await.unwrap().unwrap(), "configured agent");
|
||||
host.stop().await;
|
||||
}
|
||||
|
||||
@@ -178,7 +209,7 @@ async fn signals_work_during_boot() {
|
||||
] {
|
||||
let (_folder, world) = world();
|
||||
std::fs::write(
|
||||
world.join("config/boot.json"),
|
||||
world.join("config/boot.yaml"),
|
||||
json!([
|
||||
{"method":"core.lock","params":{"path":"boot"}},
|
||||
{"method":"core.lock","params":{"path":"boot"}}
|
||||
@@ -251,7 +282,7 @@ async fn console_error_stops_host() {
|
||||
let (_folder, world) = world();
|
||||
install_probe(&world);
|
||||
std::fs::write(
|
||||
world.join("config/boot.json"),
|
||||
world.join("config/boot.yaml"),
|
||||
json!([{"method":"probe.serve","params":{}}]).to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -667,8 +667,8 @@ async fn reserved_world_roots_prevent_startup() {
|
||||
async fn boot_waits_for_readiness_in_order() {
|
||||
let w = TestWorld::new();
|
||||
std::fs::write(
|
||||
w.0.join("config/boot.json"),
|
||||
serde_json::to_vec(&json!([
|
||||
w.0.join("config/boot.yaml"),
|
||||
serde_yaml_ng::to_string(&json!([
|
||||
{"method":"probe.write","params":{"path":"/state/boot","text":"ready"}},
|
||||
{"method":"probe.read","params":{"path":"/state/boot"}}
|
||||
]))
|
||||
@@ -1399,7 +1399,7 @@ async fn failed_boot_services_are_logged_and_boot_continues() {
|
||||
{"method":"probe.write","params":{"path":"/state/continued","text":"yes"}}
|
||||
]);
|
||||
std::fs::write(
|
||||
w.0.join("config/boot.json"),
|
||||
w.0.join("config/boot.yaml"),
|
||||
serde_json::to_vec(&services).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1423,7 +1423,7 @@ async fn failed_boot_services_are_logged_and_boot_continues() {
|
||||
async fn boot_services_are_detached() {
|
||||
let w = TestWorld::new();
|
||||
std::fs::write(
|
||||
w.0.join("config/boot.json"),
|
||||
w.0.join("config/boot.yaml"),
|
||||
br#"[{"method":"probe.serve","params":{}}]"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1855,7 +1855,7 @@ async fn nested_parent_environment() {
|
||||
async fn service_parent_environment() {
|
||||
let w = TestWorld::new();
|
||||
std::fs::write(
|
||||
w.0.join("config/boot.json"),
|
||||
w.0.join("config/boot.yaml"),
|
||||
br#"[{"method":"probe.forgotten","params":{}}]"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1936,7 +1936,7 @@ async fn detached_processes_need_explicit_termination() {
|
||||
async fn core_exit_sets_executable_status_75() {
|
||||
let w = TestWorld::new();
|
||||
std::fs::write(
|
||||
w.0.join("config/boot.json"),
|
||||
w.0.join("config/boot.yaml"),
|
||||
br#"[{"method":"core.exit","params":{}}]"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
Reference in New Issue
Block a user