pub mod cache;
mod state;
pub use state::State;
#[cfg(feature = "seed")]
pub mod seed;
use std::num::NonZeroU32;
use misanthropic::{
model::ModelInfo,
prompt::{
Prompt,
message::{Block, Role},
},
response::{self, StopReason},
tool::{Notification, Notifications, Tool, ToolBox, Use},
};
use super::inference;
use crate::ids::AgentId;
fn boxed<E: std::error::Error + Send + Sync + 'static>(
e: E,
) -> Box<dyn std::error::Error + Send + Sync> {
Box::new(e)
}
fn seat_notifications(
prompt: &mut Prompt,
notes: Vec<Notification>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut blocks: Vec<Block> = Vec::new();
for mut note in notes {
blocks.push(format!("[notification: {}]", note.source).into());
blocks.append(&mut note.content);
}
match prompt.messages.last_mut() {
Some(last) if last.role == Role::User => {
last.extend(blocks);
Ok(())
}
_ => prompt
.push_message((Role::User, blocks))
.map(|_| ())
.map_err(boxed),
}
}
pub async fn default_handle<A: Agent>(
agent: &mut A,
response: response::Message,
) -> Result<Control, A::Error> {
if matches!(response.stop_reason, Some(StopReason::PauseTurn)) {
return Ok(Control::Continue);
}
if matches!(response.stop_reason, Some(StopReason::MaxTokens)) {
return agent.on_truncate(&response).await;
}
let calls: Vec<Use> = response
.inner
.content
.iter()
.filter_map(|block| block.tool_use().cloned())
.collect();
if calls.is_empty() {
{
let (_, prompt) = agent.parts();
prompt
.push_message(response.inner.clone())
.map_err(|e| A::Error::from(boxed(e)))?;
}
let notes = agent.drain_notifications();
if !notes.is_empty() {
let (_, prompt) = agent.parts();
seat_notifications(prompt, notes).map_err(A::Error::from)?;
return Ok(Control::Continue);
}
return agent.on_quiesce(&response).await;
}
let (tools, prompt) = agent.parts();
prompt
.push_message(response.inner)
.map_err(|e| A::Error::from(boxed(e)))?;
let mut results = Vec::with_capacity(calls.len());
let mut progressed = false;
for call in calls {
let result = tools.call(call).await;
progressed |= !result.is_error;
results.push(Block::from(result));
}
prompt
.push_message((Role::User, results))
.map_err(|e| A::Error::from(boxed(e)))?;
Ok(if progressed {
Control::Continue
} else {
Control::Stalled
})
}
#[async_trait::async_trait]
pub trait Agent: Sized + Send {
type State: State;
type Context: Clone + Send;
type Error: super::Error + From<Box<dyn std::error::Error + Send + Sync>>;
fn new(
id: AgentId,
state: Self::State,
context: Self::Context,
) -> Result<Self, Self::Error>;
fn id(&self) -> AgentId;
fn state(&self) -> &Self::State;
fn prompt(&self) -> &Prompt;
fn parts(&mut self) -> (&mut ToolBox, &mut Prompt);
fn notifications(&mut self) -> Option<&mut Notifications> {
None
}
fn drain_notifications(&mut self) -> Vec<Notification> {
let mut notes = Vec::new();
if let Some(rx) = self.notifications() {
while let Ok(note) = rx.try_recv() {
notes.push(note);
}
}
notes
}
async fn handle(
&mut self,
response: response::Message,
) -> Result<Control, Self::Error> {
default_handle(self, response).await
}
async fn on_quiesce(
&mut self,
response: &response::Message,
) -> Result<Control, Self::Error> {
let _ = response;
Ok(Control::Done(Outcome::Complete))
}
async fn on_truncate(
&mut self,
response: &response::Message,
) -> Result<Control, Self::Error> {
let _ = response;
let ceiling = self.model().max_tokens;
let (_, prompt) = self.parts();
let current = prompt.max_tokens.get();
let mut raised = current.saturating_mul(2);
if ceiling != 0 {
raised = raised.min(ceiling);
}
if let Some(raised) =
NonZeroU32::new(raised).filter(|r| r.get() > current)
{
prompt.max_tokens = raised;
}
Ok(Control::Stalled)
}
fn prime_prompt(&self) -> Option<Prompt> {
let p = self.prompt();
let system = p.system.as_ref()?;
if !system.has_cache() {
tracing::warn!("system has no cache breakpoint; skipping prime");
return None;
}
let mut prime = p.clone();
prime.messages.clear();
prime.max_tokens = NonZeroU32::new(1).expect("nonzero");
prime.push_message((Role::User, "ping")).ok()?;
Some(prime)
}
async fn on_init(&mut self) -> Result<(), Self::Error> {
let (tools, prompt) = self.parts();
tools.prepare(prompt).await?;
Ok(())
}
async fn on_turn(&mut self) -> Result<(), Self::Error> {
{
let (tools, prompt) = self.parts();
tools.on_turn(prompt).await?;
}
let notes = self.drain_notifications();
if !notes.is_empty() {
let (_, prompt) = self.parts();
seat_notifications(prompt, notes).map_err(Self::Error::from)?;
}
let quirks = self.quirks().unwrap_or_default();
let (_, prompt) = self.parts();
cache::roll_breakpoints(&quirks, prompt);
Ok(())
}
async fn on_teardown(&mut self) -> Result<(), Self::Error> {
let (tools, prompt) = self.parts();
tools.on_teardown(prompt).await?;
Ok(())
}
fn model(&self) -> ModelInfo;
fn on_admit(&mut self, model: &ModelInfo, quirks: &inference::Quirks) {
let _ = (model, quirks);
}
fn quirks(&self) -> Option<inference::Quirks> {
None
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Control {
Continue,
Stalled,
Done(Outcome),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Outcome {
Complete,
Failed,
}