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:
+38
-178
@@ -3,205 +3,65 @@
|
||||
//! - `@<actor> <json>` sends a JSON body. `@<actor> <text>` sends
|
||||
//! `{"type": "text", "text": ...}`.
|
||||
//! - A line without `@` goes to `agent` as text.
|
||||
//! - `/deploy <actor> <folder>` builds a Rust folder with `cargo`, then spawns
|
||||
//! or upgrades the actor.
|
||||
//! - `/list`, `/sleep <seconds>`, `/help`, `/quit`.
|
||||
//! - `/help`, `/quit`. EOF on stdin also quits.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Result, anyhow, bail};
|
||||
use serde_json::{Value, json};
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use crate::broker::{Broker, Envelope};
|
||||
|
||||
pub const ID: &str = "console";
|
||||
|
||||
const HELP: &str = "\
|
||||
@<actor> <json> send a JSON body to an actor
|
||||
@<actor> <text> send {\"type\":\"text\",\"text\":...}
|
||||
<text> send text to the agent
|
||||
/deploy <actor> <dir> build a Rust folder with cargo, then spawn or upgrade
|
||||
/list list actors
|
||||
/sleep <seconds> wait (for scripts)
|
||||
/await <actor> <secs> wait for the next message from an actor (for scripts)
|
||||
/quit stop all actors and exit";
|
||||
|
||||
pub struct Console {
|
||||
broker: Arc<Broker>,
|
||||
waiters: Mutex<HashMap<String, oneshot::Sender<Envelope>>>,
|
||||
seq: AtomicU64,
|
||||
/// Number of messages received per sender, for `/await`.
|
||||
seen: Mutex<HashMap<String, u64>>,
|
||||
arrived: tokio::sync::Notify,
|
||||
}
|
||||
@<actor> <json> send a JSON body to an actor
|
||||
@<actor> <text> send {\"type\":\"text\",\"text\":...}
|
||||
<text> send text to the agent
|
||||
/quit stop all actors and exit";
|
||||
|
||||
/// Print a message: text bodies as text, everything else as JSON.
|
||||
fn show(env: &Envelope) {
|
||||
let reply = if env.reply_to.is_some() { " ↩" } else { "" };
|
||||
let b = &env.body;
|
||||
if b["type"] == "text"
|
||||
&& let Some(t) = b["text"].as_str()
|
||||
{
|
||||
println!("[{}{reply}] {t}", env.from);
|
||||
return;
|
||||
}
|
||||
let mut compact = b.clone();
|
||||
let mut extra = Vec::new();
|
||||
if let Some(obj) = compact.as_object_mut() {
|
||||
for key in ["errors", "warnings", "reason"] {
|
||||
if let Some(Value::String(s)) = obj.get(key)
|
||||
&& s.contains('\n')
|
||||
{
|
||||
extra.push((key, s.clone()));
|
||||
obj.remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("[{}{reply}] {compact}", env.from);
|
||||
for (k, s) in extra {
|
||||
println!(" {k}:\n{}", s.lines().map(|l| format!(" {l}")).collect::<Vec<_>>().join("\n"));
|
||||
match env.body["text"].as_str() {
|
||||
Some(text) if env.body["type"] == "text" => println!("[{}{reply}] {text}", env.from),
|
||||
_ => println!("[{}{reply}] {}", env.from, env.body),
|
||||
}
|
||||
}
|
||||
|
||||
impl Console {
|
||||
pub fn new(broker: Arc<Broker>) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
broker,
|
||||
waiters: Mutex::new(HashMap::new()),
|
||||
seq: AtomicU64::new(1),
|
||||
seen: Mutex::new(HashMap::new()),
|
||||
arrived: tokio::sync::Notify::new(),
|
||||
})
|
||||
/// Print every message that arrives for the console.
|
||||
pub async fn run_mailbox(mut mailbox: mpsc::Receiver<Envelope>) {
|
||||
while let Some(env) = mailbox.recv().await {
|
||||
show(&env);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_mailbox(self: Arc<Self>, mut mailbox: mpsc::Receiver<Envelope>) {
|
||||
while let Some(env) = mailbox.recv().await {
|
||||
show(&env);
|
||||
*self.seen.lock().unwrap().entry(env.from.clone()).or_default() += 1;
|
||||
self.arrived.notify_waiters();
|
||||
if let Some(r) = &env.reply_to
|
||||
&& let Some(tx) = self.waiters.lock().unwrap().remove(r)
|
||||
{
|
||||
let _ = tx.send(env);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send(&self, to: &str, body: Value) -> String {
|
||||
let own = format!("c{}", self.seq.fetch_add(1, Ordering::Relaxed));
|
||||
self.broker.send(ID, Some(to), Some(&own), None, body)
|
||||
}
|
||||
|
||||
async fn request(&self, to: &str, body: Value, timeout: Duration) -> Result<Envelope> {
|
||||
let own = format!("c{}", self.seq.fetch_add(1, Ordering::Relaxed));
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.waiters.lock().unwrap().insert(format!("{ID}/{own}"), tx);
|
||||
self.broker.send(ID, Some(to), Some(&own), None, body);
|
||||
tokio::time::timeout(timeout, rx)
|
||||
.await
|
||||
.map_err(|_| anyhow!("no reply from {to} within {timeout:?}"))?
|
||||
.map_err(|_| anyhow!("reply channel closed"))
|
||||
}
|
||||
|
||||
async fn deploy(&self, actor: &str, dir: &str) -> Result<()> {
|
||||
let path = std::path::absolute(dir)?;
|
||||
let source = path.display().to_string();
|
||||
println!("· building {source} …");
|
||||
let built = self.request(crate::cargo_actor::ID, json!({"type": "build", "source": source}), Duration::from_secs(900)).await?;
|
||||
if built.body["type"] != "built" {
|
||||
bail!("build failed");
|
||||
}
|
||||
let exists = self.broker.meta(actor).is_some_and(|m| m.version.is_some());
|
||||
let ty = if exists { "upgrade" } else { "spawn" };
|
||||
println!("· {ty} {actor} …");
|
||||
let body = json!({
|
||||
"type": ty,
|
||||
"actor": actor,
|
||||
"wasm": built.body["wasm"],
|
||||
"source": source,
|
||||
"built_by": built.body["built_by"],
|
||||
});
|
||||
let r = self.request("broker", body, Duration::from_secs(300)).await?;
|
||||
if r.body["type"] == "error" {
|
||||
bail!("{ty} failed");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read commands until EOF or `/quit`.
|
||||
pub async fn run_input(self: Arc<Self>) {
|
||||
println!("Sausage console. Type /help for commands.");
|
||||
let mut lines = BufReader::new(tokio::io::stdin()).lines();
|
||||
while let Ok(Some(line)) = lines.next_line().await {
|
||||
let line = line.trim();
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
/// Read lines until EOF or `/quit`, and send them as messages.
|
||||
pub async fn run_input(broker: Arc<Broker>) {
|
||||
println!("Sausage console. Type /help for commands.");
|
||||
let mut lines = BufReader::new(tokio::io::stdin()).lines();
|
||||
while let Ok(Some(line)) = lines.next_line().await {
|
||||
let line = line.trim();
|
||||
let (to, body) = match line {
|
||||
"" => continue,
|
||||
"/quit" => break,
|
||||
"/help" => {
|
||||
println!("{HELP}");
|
||||
continue;
|
||||
}
|
||||
if let Err(e) = self.command(line).await {
|
||||
if e.to_string() == "quit" {
|
||||
break;
|
||||
}
|
||||
println!("! {e:#}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn command(&self, line: &str) -> Result<()> {
|
||||
if let Some(rest) = line.strip_prefix('@') {
|
||||
let (to, text) = rest.split_once(char::is_whitespace).unwrap_or((rest, ""));
|
||||
let text = text.trim();
|
||||
let body = match serde_json::from_str::<Value>(text) {
|
||||
Ok(v @ Value::Object(_)) => v,
|
||||
_ => json!({"type": "text", "text": text}),
|
||||
};
|
||||
self.send(to, body);
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(cmd) = line.strip_prefix('/') {
|
||||
let mut parts = cmd.split_whitespace();
|
||||
match parts.next() {
|
||||
Some("help") => println!("{HELP}"),
|
||||
Some("quit") => bail!("quit"),
|
||||
Some("list") => {
|
||||
self.send("broker", json!({"type": "list"}));
|
||||
}
|
||||
Some("sleep") => {
|
||||
let secs: f64 = parts.next().and_then(|s| s.parse().ok()).unwrap_or(1.0);
|
||||
tokio::time::sleep(Duration::from_secs_f64(secs)).await;
|
||||
}
|
||||
Some("await") => {
|
||||
let from = parts.next().unwrap_or("agent").to_string();
|
||||
let secs: f64 = parts.next().and_then(|s| s.parse().ok()).unwrap_or(60.0);
|
||||
let count = |c: &Self| c.seen.lock().unwrap().get(&from).copied().unwrap_or(0);
|
||||
let before = count(self);
|
||||
let deadline = tokio::time::Instant::now() + Duration::from_secs_f64(secs);
|
||||
loop {
|
||||
let notified = self.arrived.notified();
|
||||
tokio::pin!(notified);
|
||||
notified.as_mut().enable();
|
||||
if count(self) != before {
|
||||
break;
|
||||
}
|
||||
if tokio::time::timeout_at(deadline, notified).await.is_err() {
|
||||
bail!("no message from {from} within {secs} s");
|
||||
}
|
||||
}
|
||||
}
|
||||
Some("deploy") => {
|
||||
let (Some(actor), Some(dir)) = (parts.next(), parts.next()) else {
|
||||
bail!("usage: /deploy <actor> <dir>");
|
||||
_ => match line.strip_prefix('@') {
|
||||
Some(rest) => {
|
||||
let (to, text) = rest.split_once(char::is_whitespace).unwrap_or((rest, ""));
|
||||
let body = match serde_json::from_str::<Value>(text.trim()) {
|
||||
Ok(v @ Value::Object(_)) => v,
|
||||
_ => json!({"type": "text", "text": text.trim()}),
|
||||
};
|
||||
self.deploy(actor, dir).await?;
|
||||
(to, body)
|
||||
}
|
||||
_ => bail!("unknown command. Type /help."),
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
self.send("agent", json!({"type": "text", "text": line}));
|
||||
Ok(())
|
||||
None => ("agent", json!({"type": "text", "text": line})),
|
||||
},
|
||||
};
|
||||
broker.send(ID, Some(to), None, None, body);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user