Agent: durable turn and queue state, visible inbox closure, OpenAI phase; prompt fixes; fetch/bench fixes
This commit is contained in:
+203
-87
@@ -4,6 +4,7 @@ mod llm;
|
||||
|
||||
use llm::{Config, Entry, Tool};
|
||||
use sausage_actor::{Inbox, Message, Value, json, reply, send};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{HashSet, VecDeque};
|
||||
use std::io::Write;
|
||||
use wstd::future::FutureExt;
|
||||
@@ -106,11 +107,18 @@ fn copy_tree(from: &str, to: &str) -> std::io::Result<u64> {
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
struct TurnState {
|
||||
active: Option<Message>,
|
||||
queue: VecDeque<Message>,
|
||||
notes: Vec<Message>,
|
||||
}
|
||||
|
||||
struct Agent {
|
||||
inbox: Inbox,
|
||||
history: Vec<Entry>,
|
||||
queue: VecDeque<Message>,
|
||||
notes: Vec<Message>,
|
||||
pending: TurnState,
|
||||
shutdown: bool,
|
||||
}
|
||||
|
||||
@@ -121,13 +129,32 @@ impl Agent {
|
||||
.lines()
|
||||
.filter_map(|line| serde_json::from_str(line).ok())
|
||||
.collect();
|
||||
Self {
|
||||
let pending = std::fs::read_to_string(TURN)
|
||||
.ok()
|
||||
.and_then(|contents| {
|
||||
let value: Value = serde_json::from_str(&contents).ok()?;
|
||||
let mut pending = serde_json::from_value::<TurnState>(value.clone()).ok()?;
|
||||
if pending.active.is_none() {
|
||||
if let (Some(id), Some(from)) = (value["id"].as_str(), value["from"].as_str()) {
|
||||
pending.active = Some(Message {
|
||||
id: id.to_string(),
|
||||
from: from.to_string(),
|
||||
reply_to: None,
|
||||
body: Value::Null,
|
||||
});
|
||||
}
|
||||
}
|
||||
Some(pending)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let agent = Self {
|
||||
inbox: Inbox::start(),
|
||||
history,
|
||||
queue: VecDeque::new(),
|
||||
notes: Vec::new(),
|
||||
pending,
|
||||
shutdown: false,
|
||||
}
|
||||
};
|
||||
agent.persist_turn();
|
||||
agent
|
||||
}
|
||||
|
||||
fn add_history(&mut self, entry: Entry) {
|
||||
@@ -147,39 +174,27 @@ impl Agent {
|
||||
self.history.push(entry);
|
||||
}
|
||||
|
||||
async fn run(mut self) {
|
||||
self.resume().await;
|
||||
while !self.shutdown {
|
||||
let msg = match self.queue.pop_front() {
|
||||
Some(msg) => msg,
|
||||
None => match self.inbox.recv().await {
|
||||
Some(msg) => msg,
|
||||
None => break,
|
||||
},
|
||||
};
|
||||
if starts_turn(&msg) {
|
||||
self.turn(msg).await;
|
||||
} else {
|
||||
self.notes.push(msg);
|
||||
fn persist_turn(&self) {
|
||||
if self.pending.active.is_none()
|
||||
&& self.pending.queue.is_empty()
|
||||
&& self.pending.notes.is_empty()
|
||||
{
|
||||
if let Err(e) = std::fs::remove_file(TURN) {
|
||||
if e.kind() != std::io::ErrorKind::NotFound {
|
||||
eprintln!("agent: cannot remove turn state: {e}");
|
||||
}
|
||||
}
|
||||
} else if let Err(e) = std::fs::write(TURN, serde_json::to_vec(&self.pending).unwrap()) {
|
||||
eprintln!("agent: cannot save turn state: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
async fn resume(&mut self) {
|
||||
let Ok(turn) = std::fs::read_to_string(TURN) else {
|
||||
return;
|
||||
};
|
||||
let Ok(turn) = serde_json::from_str::<Value>(&turn) else {
|
||||
return;
|
||||
};
|
||||
let Ok(contents) = std::fs::read_to_string(CONFIG) else {
|
||||
return;
|
||||
};
|
||||
let Ok(cfg) = serde_json::from_str::<Config>(&contents) else {
|
||||
return;
|
||||
};
|
||||
let _ = std::fs::remove_file(TURN);
|
||||
fn finish_turn(&mut self) {
|
||||
self.pending.active = None;
|
||||
self.persist_turn();
|
||||
}
|
||||
|
||||
fn repair_dangling_tools(&mut self) {
|
||||
let answered: HashSet<String> = self
|
||||
.history
|
||||
.iter()
|
||||
@@ -204,62 +219,135 @@ impl Agent {
|
||||
.to_string(),
|
||||
});
|
||||
}
|
||||
let version = std::env::var("SAUSAGE_VERSION").unwrap_or_default();
|
||||
self.add_history(Entry::User {
|
||||
text: format!(
|
||||
"[note from your runtime] You restarted and now run version {version}. The restart interrupted your last turn (probably your own upgrade). Check the state you need, then finish the turn and answer the original message."
|
||||
),
|
||||
});
|
||||
let message = Message {
|
||||
id: turn["id"].as_str().unwrap_or_default().to_string(),
|
||||
from: turn["from"].as_str().unwrap_or_default().to_string(),
|
||||
reply_to: None,
|
||||
body: Value::Null,
|
||||
};
|
||||
self.steps(&cfg, &message).await;
|
||||
}
|
||||
|
||||
async fn turn(&mut self, message: Message) {
|
||||
let cfg = match std::fs::read_to_string(CONFIG)
|
||||
.map_err(|e| e.to_string())
|
||||
.and_then(|contents| serde_json::from_str(&contents).map_err(|e| e.to_string()))
|
||||
{
|
||||
Ok(cfg) => cfg,
|
||||
Err(e) => {
|
||||
let _ = reply(
|
||||
&message,
|
||||
json!({"type": "text", "text": format!("I am not configured: cannot read {CONFIG}: {e}")}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
fn turn_text(&self, message: &Message) -> String {
|
||||
let mut text = format!(
|
||||
"Message {} from {}:\n{}",
|
||||
message.id,
|
||||
message.from,
|
||||
message.body["text"].as_str().unwrap_or("")
|
||||
);
|
||||
if !self.notes.is_empty() {
|
||||
if !self.pending.notes.is_empty() {
|
||||
let notes = self
|
||||
.pending
|
||||
.notes
|
||||
.drain(..)
|
||||
.map(|note| serde_json::to_string(¬e).unwrap())
|
||||
.iter()
|
||||
.map(|note| serde_json::to_string(note).unwrap())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
text = format!("Messages that arrived while you were idle:\n{notes}\n\n{text}");
|
||||
}
|
||||
self.add_history(Entry::User { text });
|
||||
text
|
||||
}
|
||||
|
||||
fn has_turn_user(&self, message: &Message) -> bool {
|
||||
let marker = format!("Message {} from {}:\n", message.id, message.from);
|
||||
self.history.iter().any(|entry| match entry {
|
||||
Entry::User { text } => text.contains(&marker),
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
fn not_configured(&mut self, message: &Message, error: String) {
|
||||
let _ = reply(
|
||||
message,
|
||||
json!({"type": "text", "text": format!("I am not configured: cannot read {CONFIG}: {error}")}),
|
||||
);
|
||||
self.finish_turn();
|
||||
}
|
||||
|
||||
async fn run(mut self) {
|
||||
self.repair_dangling_tools();
|
||||
self.resume().await;
|
||||
while !self.shutdown {
|
||||
let message = match self.pending.queue.pop_front() {
|
||||
Some(message) => message,
|
||||
None => match self.inbox.recv().await {
|
||||
Some(message) => message,
|
||||
None => {
|
||||
self.shutdown = true;
|
||||
break;
|
||||
}
|
||||
},
|
||||
};
|
||||
if starts_turn(&message) {
|
||||
self.turn(message).await;
|
||||
} else {
|
||||
self.pending.notes.push(message);
|
||||
self.persist_turn();
|
||||
}
|
||||
}
|
||||
self.persist_turn();
|
||||
}
|
||||
|
||||
async fn resume(&mut self) {
|
||||
let Some(message) = self.pending.active.clone() else {
|
||||
return;
|
||||
};
|
||||
if !self.has_turn_user(&message) {
|
||||
let text = self.turn_text(&message);
|
||||
self.add_history(Entry::User { text });
|
||||
self.pending.notes.clear();
|
||||
self.persist_turn();
|
||||
}
|
||||
let version = std::env::var("SAUSAGE_VERSION").unwrap_or_default();
|
||||
self.add_history(Entry::User {
|
||||
text: format!(
|
||||
"[note from your runtime] You restarted and now run version {version}. The restart interrupted your last turn (probably your own upgrade). Check the state you need, then finish the turn and answer the original message."
|
||||
),
|
||||
});
|
||||
let cfg = match load_config() {
|
||||
Ok(cfg) => cfg,
|
||||
Err(error) => {
|
||||
self.not_configured(&message, error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
self.steps(&cfg, &message).await;
|
||||
if !self.shutdown {
|
||||
self.finish_turn();
|
||||
}
|
||||
}
|
||||
|
||||
async fn turn(&mut self, message: Message) {
|
||||
self.pending.active = Some(message.clone());
|
||||
self.persist_turn();
|
||||
let cfg = match load_config() {
|
||||
Ok(cfg) => cfg,
|
||||
Err(error) => {
|
||||
self.not_configured(&message, error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let text = self.turn_text(&message);
|
||||
self.add_history(Entry::User { text });
|
||||
self.pending.notes.clear();
|
||||
self.persist_turn();
|
||||
self.steps(&cfg, &message).await;
|
||||
if !self.shutdown {
|
||||
self.finish_turn();
|
||||
}
|
||||
}
|
||||
|
||||
async fn steps(&mut self, cfg: &Config, message: &Message) {
|
||||
let tools = tools();
|
||||
loop {
|
||||
let entries = match llm::complete(cfg, SYSTEM, &tools, &self.history).await {
|
||||
if self.inbox.is_closed() {
|
||||
self.preserve_closed_messages();
|
||||
return;
|
||||
}
|
||||
let response = llm::complete(cfg, SYSTEM, &tools, &self.history).await;
|
||||
if self.inbox.is_closed() {
|
||||
self.preserve_closed_messages();
|
||||
return;
|
||||
}
|
||||
let entries = match response {
|
||||
Ok(entries) => entries,
|
||||
Err(e) => {
|
||||
self.add_history(Entry::Assistant {
|
||||
text: format!("(inference failed: {e})"),
|
||||
phase: None,
|
||||
});
|
||||
let _ = reply(
|
||||
message,
|
||||
@@ -282,7 +370,7 @@ impl Agent {
|
||||
let answer: Vec<String> = entries
|
||||
.iter()
|
||||
.filter_map(|entry| match entry {
|
||||
Entry::Assistant { text } => Some(text.clone()),
|
||||
Entry::Assistant { text, .. } if !text.is_empty() => Some(text.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
@@ -299,18 +387,24 @@ impl Agent {
|
||||
return;
|
||||
}
|
||||
for (id, name, args) in calls {
|
||||
if self.shutdown || self.inbox.is_closed() {
|
||||
if !self.shutdown {
|
||||
self.preserve_closed_messages();
|
||||
}
|
||||
return;
|
||||
}
|
||||
let output = self.tool(&name, &args).await;
|
||||
self.add_history(Entry::ToolResult {
|
||||
id,
|
||||
output: output.to_string(),
|
||||
});
|
||||
}
|
||||
if self.shutdown {
|
||||
let _ = std::fs::write(
|
||||
TURN,
|
||||
json!({"from": message.from, "id": message.id}).to_string(),
|
||||
);
|
||||
return;
|
||||
if self.shutdown {
|
||||
return;
|
||||
}
|
||||
if self.inbox.is_closed() {
|
||||
self.preserve_closed_messages();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -318,12 +412,36 @@ impl Agent {
|
||||
fn stash(&mut self, message: Message, others: &mut Vec<Value>) {
|
||||
if starts_turn(&message) {
|
||||
others.push(json!({"queued_for_a_later_turn": true, "message": message}));
|
||||
self.queue.push_back(message);
|
||||
self.pending.queue.push_back(message);
|
||||
self.persist_turn();
|
||||
} else {
|
||||
others.push(serde_json::to_value(message).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
fn drain_inbox(&mut self, others: &mut Vec<Value>) {
|
||||
while let Some(message) = self.inbox.try_recv() {
|
||||
self.stash(message, others);
|
||||
}
|
||||
if self.inbox.is_closed() {
|
||||
self.shutdown = true;
|
||||
}
|
||||
self.persist_turn();
|
||||
}
|
||||
|
||||
fn preserve_closed_messages(&mut self) {
|
||||
while let Some(message) = self.inbox.try_recv() {
|
||||
if starts_turn(&message) {
|
||||
self.pending.queue.push_back(message);
|
||||
} else {
|
||||
self.pending.notes.push(message);
|
||||
}
|
||||
self.persist_turn();
|
||||
}
|
||||
self.shutdown = self.inbox.is_closed();
|
||||
self.persist_turn();
|
||||
}
|
||||
|
||||
async fn tool(&mut self, name: &str, args: &str) -> Value {
|
||||
let a: Value = match serde_json::from_str(args) {
|
||||
Ok(value) => value,
|
||||
@@ -342,15 +460,11 @@ impl Agent {
|
||||
.timeout(Duration::from_secs_f64(seconds))
|
||||
.await
|
||||
{
|
||||
Ok(Some(message)) => {
|
||||
self.stash(message, &mut others);
|
||||
while let Some(message) = self.inbox.try_recv() {
|
||||
self.stash(message, &mut others);
|
||||
}
|
||||
}
|
||||
Ok(Some(message)) => self.stash(message, &mut others),
|
||||
Ok(None) => self.shutdown = true,
|
||||
Err(_) => {}
|
||||
}
|
||||
self.drain_inbox(&mut others);
|
||||
json!({"messages": others})
|
||||
}
|
||||
"read_file" => {
|
||||
@@ -437,12 +551,7 @@ impl Agent {
|
||||
reply = serde_json::to_value(message).unwrap();
|
||||
break;
|
||||
}
|
||||
Ok(Some(message)) => {
|
||||
self.stash(message, &mut others);
|
||||
if self.shutdown {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(Some(message)) => self.stash(message, &mut others),
|
||||
Ok(None) => {
|
||||
self.shutdown = true;
|
||||
break;
|
||||
@@ -451,10 +560,17 @@ impl Agent {
|
||||
}
|
||||
}
|
||||
}
|
||||
self.drain_inbox(&mut others);
|
||||
json!({"id": id, "reply": reply, "waited": wait > 0.0, "other_messages": others})
|
||||
}
|
||||
}
|
||||
|
||||
fn load_config() -> Result<Config, String> {
|
||||
std::fs::read_to_string(CONFIG)
|
||||
.map_err(|e| e.to_string())
|
||||
.and_then(|contents| serde_json::from_str(&contents).map_err(|e| e.to_string()))
|
||||
}
|
||||
|
||||
fn starts_turn(message: &Message) -> bool {
|
||||
message.kind() == "text" && message.reply_to.is_none()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user