Trim the harness to what it needs to keep going

Host:
- test first: a new version is tested where it lies; only an installed
  version goes to versions/, and instances run from there
- data/ and public/ are shared by all versions; no snapshots; rollback
  restores code only
- no re-delivery, status.json, list, remove/archive, smoke test, data/
  hiding, or stdin timeout; rollback needs a version
- supervisor.rs split out; one place for installing a version
- init only writes the agent folder; run installs it like any actor
- console: @actor, text to the agent, /help, /quit

Agent:
- every text goes out as a broadcast; 5-line prompt; 5 tools that describe
  themselves; turn.json only marks an active turn

Docs: PROTOCOL.md, README.md, DECISIONS.md, AGENTS.md rewritten.
Examples: only notes and notes-v2.
This commit is contained in:
2026-10-15 13:35:45 +01:00
parent cee862275c
commit 11144b947e
35 changed files with 909 additions and 2038 deletions
+23 -115
View File
@@ -9,7 +9,7 @@ use std::process::Command;
use std::sync::OnceLock;
use std::time::Duration;
use common::{create_world, receive_replies, receive_reply, request, source_with_test, start};
use common::{create_world, receive_message, receive_reply, request, source_with_test, start};
use sausage::{Envelope, Host, World};
use serde_json::{Value, json};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
@@ -319,6 +319,13 @@ async fn start_agent(
spawned.body["type"], "spawned",
"agent spawn failed: {spawned:?}"
);
let subscribed = request(
&host,
&mut mailbox,
json!({"type": "subscribe", "actor": "agent"}),
)
.await;
assert_eq!(subscribed.body["type"], "subscribed");
(world, host, mailbox)
}
@@ -332,11 +339,12 @@ fn send_text(host: &Host, text: &str) -> String {
)
}
async fn assert_answer(mailbox: &mut mpsc::Receiver<Envelope>, id: &str, text: &str) {
let reply = receive_reply(mailbox, id, Duration::from_secs(20)).await;
assert_eq!(reply.to, "console");
assert_eq!(reply.reply_to.as_deref(), Some(id));
assert_eq!(reply.body, json!({"type": "text", "text": text}));
async fn assert_published_text(mailbox: &mut mpsc::Receiver<Envelope>, text: &str) {
let message = receive_message(mailbox, Duration::from_secs(20)).await;
assert_eq!(message.from, "agent");
assert_eq!(message.to, "console");
assert_eq!(message.reply_to, None);
assert_eq!(message.body, json!({"type": "text", "text": text}));
}
fn history(world: &World) -> Vec<Value> {
@@ -439,9 +447,9 @@ async fn text_answers_use_both_inference_apis() {
.await;
let (_, host, mut mailbox) =
start_agent(api, &fake, &format!("agent-text-{}", api.name())).await;
let id = send_text(&host, "say hello");
send_text(&host, "say hello");
api.check_request(&fake.next_request().await);
assert_answer(&mut mailbox, &id, "hello from the fake model").await;
assert_published_text(&mut mailbox, "hello from the fake model").await;
host.stop().await;
}
}
@@ -455,7 +463,7 @@ async fn tool_calls_pair_with_results_in_both_inference_apis() {
.await;
let (world, host, mut mailbox) =
start_agent(api, &fake, &format!("agent-tools-{}", api.name())).await;
let id = send_text(&host, "list actors");
send_text(&host, "list actors");
api.check_request(&fake.next_request().await);
fake.answer(api.text_response("the actor list is ready"))
.await;
@@ -470,8 +478,9 @@ async fn tool_calls_pair_with_results_in_both_inference_apis() {
.iter()
.any(|entry| entry["name"] == "agent")
);
assert_answer(&mut mailbox, &id, "the actor list is ready").await;
assert_published_text(&mut mailbox, "the actor list is ready").await;
host.stop().await;
let history = history(&world);
assert_eq!(
history
@@ -482,7 +491,6 @@ async fn tool_calls_pair_with_results_in_both_inference_apis() {
);
assert_eq!(history[1]["id"], "list-1");
assert_eq!(history[2]["id"], "list-1");
host.stop().await;
}
}
@@ -513,8 +521,8 @@ async fn restart_resumes_a_turn_with_completed_tool_history() {
serde_json::from_slice::<Value>(
&std::fs::read(world.data_dir("agent").join("turn.json")).unwrap()
)
.unwrap()["active"]["id"],
id
.unwrap(),
json!(true)
);
let _ = release.send(());
fake.answer(api.text_response("resumed answer")).await;
@@ -528,22 +536,9 @@ async fn restart_resumes_a_turn_with_completed_tool_history() {
let resumed = fake.next_request().await;
api.check_request(&resumed);
assert_tool_pair(&resumed, api, "list-1", "list_dir", &args);
let replies = receive_replies(
&mut mailbox,
&[start_id.clone(), id.clone()],
Duration::from_secs(20),
)
.await;
let started = replies
.iter()
.find(|reply| reply.reply_to.as_deref() == Some(start_id.as_str()))
.unwrap();
let started = receive_reply(&mut mailbox, &start_id, Duration::from_secs(20)).await;
assert_eq!(started.body["type"], "started");
let answer = replies
.iter()
.find(|reply| reply.reply_to.as_deref() == Some(id.as_str()))
.unwrap();
assert_eq!(answer.body["text"], "resumed answer");
assert_published_text(&mut mailbox, "resumed answer").await;
let history = history(&world);
assert_history_tools_have_results(&history);
@@ -558,90 +553,3 @@ async fn restart_resumes_a_turn_with_completed_tool_history() {
host.stop().await;
}
}
#[tokio::test]
async fn queued_text_resumes_after_the_active_turn_across_restart() {
let wait_args = json!({"seconds": 10});
for api in [Api::Anthropic] {
let mut fake = FakeServer::start().await;
fake.answer(api.tool_response("wait-1", "wait_for_messages", &wait_args))
.await;
let (world, host, mut mailbox) =
start_agent(api, &fake, &format!("agent-queue-{}", api.name())).await;
let first_id = send_text(&host, "first turn");
api.check_request(&fake.next_request().await);
let release = fake.hold(api.text_response("discarded response")).await;
let second_id = send_text(&host, "second turn");
let second_request = fake.next_request().await;
api.check_request(&second_request);
let output = assert_tool_pair(
&second_request,
api,
"wait-1",
"wait_for_messages",
&wait_args,
);
let queued: Value = serde_json::from_str(&output).unwrap();
assert!(
queued["messages"]
.as_array()
.unwrap()
.iter()
.any(|message| message["message"]["id"] == second_id)
);
let turn: Value = serde_json::from_slice(
&std::fs::read(world.data_dir("agent").join("turn.json")).unwrap(),
)
.unwrap();
assert_eq!(turn["queue"][0]["id"], second_id);
let stopped = request(
&host,
&mut mailbox,
json!({"type": "stop", "actor": "agent"}),
)
.await;
assert_eq!(stopped.body["type"], "stopped");
let _ = release.send(());
fake.answer(api.text_response("first answer")).await;
fake.answer(api.text_response("second answer")).await;
let start_id = host.broker.send(
"console",
Some("broker"),
None,
None,
json!({"type": "start", "actor": "agent"}),
);
let resumed_first = fake.next_request().await;
api.check_request(&resumed_first);
assert!(resumed_first.body.to_string().contains("first turn"));
let next_turn = fake.next_request().await;
api.check_request(&next_turn);
assert!(next_turn.body.to_string().contains("second turn"));
let replies = receive_replies(
&mut mailbox,
&[start_id.clone(), first_id.clone(), second_id.clone()],
Duration::from_secs(20),
)
.await;
let started = replies
.iter()
.find(|reply| reply.reply_to.as_deref() == Some(start_id.as_str()))
.unwrap();
assert_eq!(started.body["type"], "started");
let first_position = replies
.iter()
.position(|reply| reply.reply_to.as_deref() == Some(first_id.as_str()))
.unwrap();
let second_position = replies
.iter()
.position(|reply| reply.reply_to.as_deref() == Some(second_id.as_str()))
.unwrap();
assert!(first_position < second_position);
assert_eq!(replies[first_position].body["text"], "first answer");
assert_eq!(replies[second_position].body["text"], "second answer");
assert_history_tools_have_results(&history(&world));
host.stop().await;
}
}