From 64175ff7f2747caa5e259cf314143773229f267b Mon Sep 17 00:00:00 2001 From: Sausage Agent Date: Fri, 16 Oct 2026 14:50:45 +0000 Subject: [PATCH] Remove in-world chat actor snapshot --- actors/chat/Cargo.toml | 8 -- actors/chat/src/main.rs | 208 ----------------------------------- actors/chat/src/page.html | 97 ---------------- actors/chat/tests/basic.json | 10 -- 4 files changed, 323 deletions(-) delete mode 100644 actors/chat/Cargo.toml delete mode 100644 actors/chat/src/main.rs delete mode 100644 actors/chat/src/page.html delete mode 100644 actors/chat/tests/basic.json diff --git a/actors/chat/Cargo.toml b/actors/chat/Cargo.toml deleted file mode 100644 index 90830de..0000000 --- a/actors/chat/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "chat" -version = "0.1.0" -edition = "2024" - -[dependencies] -sausage-actor = "0.1" -serde_json = "1" diff --git a/actors/chat/src/main.rs b/actors/chat/src/main.rs deleted file mode 100644 index ba10ad8..0000000 --- a/actors/chat/src/main.rs +++ /dev/null @@ -1,208 +0,0 @@ -//! Web chat UI for agent instances, served at /a/chat/ through `web`. -//! GET / -> list agents; GET // -> chat; GET //api/history?before= -> newest history chunk first-page / older chunks. -//! GET //api/models -> shared model profiles; POST //send with {text,model?} forwards [web chat] text to the agent. -//! The page uses EventSource /events/ for live history events. -use sausage_actor::{json, messages, reply, send, Message, Value}; -use std::collections::BTreeMap; -use std::io::{Read, Seek, SeekFrom}; - -const PAGE: &str = include_str!("page.html"); -const HISTORY_CHUNK: u64 = 256 * 1024; - -fn valid_agent(id: &str) -> bool { - (id == "agent" || id.starts_with("agent-")) - && id.bytes().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-' || b == b'_') -} - -fn actor_exists(id: &str) -> bool { - valid_agent(id) && std::fs::metadata(format!("/actor/{id}/actor.json")).is_ok() -} - -fn response(status: u16, body: Value) -> Value { - json!({"type":"http_response","status":status,"headers":{"Content-Type":"application/json; charset=utf-8","Cache-Control":"no-store"},"body":body}) -} - -fn text_response(status: u16, text: &str) -> Value { - json!({"type":"http_response","status":status,"headers":{"Content-Type":"text/plain; charset=utf-8","Cache-Control":"no-store"},"body":text}) -} - -fn html_response(body: &str) -> Value { - json!({"type":"http_response","status":200,"headers":{"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store"},"body":body}) -} - -fn query_param(query: &str, key: &str) -> Option { - query.split('&').find_map(|pair| { - let (k, v) = pair.split_once('=').unwrap_or((pair, "")); - (decode(k) == key).then(|| decode(v)) - }) -} - -fn decode(s: &str) -> String { - let bytes = s.as_bytes(); - let mut out = Vec::with_capacity(bytes.len()); - let mut i = 0; - while i < bytes.len() { - if bytes[i] == b'%' && i + 2 < bytes.len() { - if let Ok(hex) = std::str::from_utf8(&bytes[i + 1..i + 3]) { - if let Ok(value) = u8::from_str_radix(hex, 16) { - out.push(value); - i += 3; - continue; - } - } - } - out.push(if bytes[i] == b'+' { b' ' } else { bytes[i] }); - i += 1; - } - String::from_utf8_lossy(&out).into_owned() -} - -fn project_name(id: &str) -> String { - let tail = id.strip_prefix("agent-").unwrap_or(""); - let room = match tail.rsplit_once('-') { - Some((room, suffix)) - if suffix.len() > 1 - && (suffix.starts_with('w') || suffix.starts_with('r')) - && suffix[1..].bytes().all(|b| b.is_ascii_digit()) => room, - _ => tail, - }; - let board = format!("board-{room}"); - std::fs::read_to_string(format!("/actor/{board}/public/board.json")) - .ok() - .and_then(|s| serde_json::from_str::(&s).ok()) - .and_then(|v| v["name"].as_str().filter(|s| !s.is_empty()).or(v["room"].as_str()).map(str::to_string)) - .unwrap_or_default() -} - -fn is_lead(id: &str) -> bool { - if id == "agent" { return true; } - let Some(tail) = id.strip_prefix("agent-") else { return false }; - !tail.rsplit_once('-').is_some_and(|(_, suffix)| { - suffix.len() > 1 && (suffix.starts_with('w') || suffix.starts_with('r')) - && suffix[1..].bytes().all(|b| b.is_ascii_digit()) - }) -} - -fn index_page() -> String { - let mut ids: Vec = std::fs::read_dir("/actor") - .map(|rd| rd.flatten().map(|e| e.file_name().to_string_lossy().into_owned()) - .filter(|id| id == "agent" || id.starts_with("agent-")) - .filter(|id| actor_exists(id)).collect()) - .unwrap_or_default(); - ids.sort_by(|a, b| (!is_lead(a), a != "agent", a).cmp(&(!is_lead(b), b != "agent", b))); - let mut items = String::new(); - for id in ids { - let label = project_name(&id); - let title = if label.is_empty() { id.clone() } else { format!("{} · {}", escape(&label), escape(&id)) }; - items.push_str(&format!("{}", escape(&id), title)); - } - format!("Agent chat{}
💬 Agent chat

Choose an agent instance.

{}
", CSS, NAV, items) -} - -fn escape(s: &str) -> String { - s.replace('&', "&").replace('<', "<").replace('>', ">").replace('"', """).replace('\'', "'") -} - -fn history(agent: &str, before: Option) -> Value { - let path = format!("/actor/{agent}/public/history.jsonl"); - let mut file = match std::fs::File::open(&path) { - Ok(f) => f, - Err(e) if e.kind() == std::io::ErrorKind::NotFound => return response(200, json!({"agent":agent,"lines":[],"before":null,"total":0})), - Err(e) => return text_response(500, &format!("cannot open history: {e}")), - }; - let total = match file.metadata() { Ok(m) => m.len(), Err(e) => return text_response(500, &format!("cannot stat history: {e}")) }; - let end = before.unwrap_or(total).min(total); - let start = end.saturating_sub(HISTORY_CHUNK); - if let Err(e) = file.seek(SeekFrom::Start(start)) { return text_response(500, &format!("cannot seek history: {e}")); } - let mut bytes = Vec::new(); - if let Err(e) = file.take(end - start).read_to_end(&mut bytes) { return text_response(500, &format!("cannot read history: {e}")); } - let mut first = start; - if start > 0 { - if let Some(nl) = bytes.iter().position(|b| *b == b'\n') { - bytes.drain(..=nl); - first = start + nl as u64 + 1; - } else { - bytes.clear(); - first = end; - } - } - let lines: Vec = bytes.split(|b| *b == b'\n').filter(|line| !line.is_empty()) - .filter_map(|line| serde_json::from_slice::(line).ok()).collect(); - let older = if first > 0 { json!(first) } else { Value::Null }; - response(200, json!({"agent":agent,"lines":lines,"before":older,"total":total})) -} - -fn request_body(m: &Message) -> Result { - if let Some(file) = m.body["body_file"].as_str() { - let text = std::fs::read_to_string(file).map_err(|e| format!("cannot read request body: {e}"))?; - serde_json::from_str(&text).map_err(|e| format!("invalid JSON body: {e}")) - } else if let Some(body) = m.body.get("body") { - match body { Value::String(s) => serde_json::from_str(s).map_err(|e| format!("invalid JSON body: {e}")), other => Ok(other.clone()) } - } else { - Err("request body is required".into()) - } -} - -fn http(m: &Message, pending_models: &mut BTreeMap) -> Option { - let path = m.body["path"].as_str().unwrap_or("/"); - let method = m.body["method"].as_str().unwrap_or("GET"); - let path = path.trim_end_matches('/'); - if path.is_empty() || path == "/" { - return Some(html_response(&index_page())); - } - let sub = path.strip_prefix('/')?; - let (agent, rest) = sub.split_once('/').unwrap_or((sub, "")); - if !actor_exists(agent) { - return Some(text_response(404, "unknown agent\n")); - } - match (method, rest) { - ("GET", "") => { - let page = PAGE.replace("__AGENT__", &escape(agent)).replace("__AGENT_JSON__", &serde_json::to_string(agent).unwrap()); - Some(html_response(&page)) - } - ("GET", "api/history") => { - let before = query_param(m.body["query"].as_str().unwrap_or(""), "before").and_then(|s| s.parse::().ok()); - Some(history(agent, before)) - } - ("GET", "api/models") => { - let id = send("shared", json!({"type":"list_models"})); - pending_models.insert(id, m.clone()); - None - } - ("POST", "send") => { - let body = match request_body(m) { Ok(b) => b, Err(e) => return Some(text_response(400, &format!("{e}\n"))) }; - let text = body["text"].as_str().unwrap_or("").trim(); - if text.is_empty() { return Some(text_response(400, "text is required\n")); } - let mut msg = json!({"type":"text","text":format!("[web chat] {text}")}); - if let Some(model) = body.get("model").and_then(Value::as_str).filter(|m| !m.is_empty()) { - msg["model"] = json!(model); - } - let id = send(agent, msg); - Some(response(202, json!({"type":"sent","agent":agent,"id":id}))) - } - _ => Some(text_response(404, "not found\n")), - } -} - -const NAV: &str = r#" - -"#; -const CSS: &str = r#"*{box-sizing:border-box}body{margin:0;font:15px/1.5 system-ui,-apple-system,sans-serif;background:#fffaf3;color:#2b2117}header{position:sticky;top:30px;background:#fffaf3;border-bottom:1px solid #eadfce;padding:.7rem 1rem;z-index:2}header p{margin:.2rem 0;color:#8a7d6e}main{padding:1rem;max-width:50rem;margin:auto}.agent{display:block;border:1px solid #eadfce;border-radius:8px;padding:.7rem .9rem;margin:.5rem 0;background:#fffdf9;color:#a0461d;text-decoration:none}@media(prefers-color-scheme:dark){body,header{background:#17140f;color:#ece3d6}.agent{background:#221d16;border-color:#2e2820}}"#; - -fn main() { - let mut pending_models: BTreeMap = BTreeMap::new(); - for message in messages() { - if let Some(reply_to) = message.reply_to.as_ref() { - if let Some(request) = pending_models.remove(reply_to) { - let models = message.body["models"].clone(); - let _ = reply(&request, response(200, json!({"models":models}))); - } - continue; - } - if message.kind() == "ping" { - let _ = reply(&message, json!({"type":"pong"})); - } else if message.kind() == "http_request" { - if let Some(r) = http(&message, &mut pending_models) { let _ = reply(&message, r); } - } - } -} diff --git a/actors/chat/src/page.html b/actors/chat/src/page.html deleted file mode 100644 index 7cc9dd2..0000000 --- a/actors/chat/src/page.html +++ /dev/null @@ -1,97 +0,0 @@ - - - - - -Chat with __AGENT__ - - - - - - -
💬 __AGENT__connecting…
-
-
-
-
- - diff --git a/actors/chat/tests/basic.json b/actors/chat/tests/basic.json deleted file mode 100644 index e4def7a..0000000 --- a/actors/chat/tests/basic.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "description": "chat index, agent page, history paging and input validation", - "steps": [ - {"send":{"type":"ping"},"expect":{"type":"pong"}}, - {"send":{"type":"http_request","method":"GET","path":"/"},"expect":{"type":"http_response","status":200,"headers":{"Content-Type":"text/html; charset=utf-8"}}}, - {"send":{"type":"http_request","method":"GET","path":"/agent-test"},"expect":{"type":"http_response","status":200,"headers":{"Content-Type":"text/html; charset=utf-8"}}}, - {"send":{"type":"http_request","method":"GET","path":"/agent-test/api/history"},"expect":{"type":"http_response","status":200,"body":{"agent":"agent-test"}}}, - {"send":{"type":"http_request","method":"POST","path":"/agent-test/send","body":{"text":" "}},"expect":{"type":"http_response","status":400}} - ] -}