use std::collections::{BTreeSet, HashMap, VecDeque};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use misanthropic::model::ModelInfo;
use misanthropic::prompt::Prompt;
use misanthropic::prompt::message::Role;
use misanthropic::response::{self, StopReason};
use misanthropic::tool::Tool;
use serde::Serialize;
use super::backend::{Inference, SaveError, Storage};
use super::inference::Quirks;
use super::{Agent, AgentNotFound, Control, Outcome, RetryAfter, State};
use crate::ids::AgentId;
pub(crate) use super::{
ErrorKind, ErrorReport, MAX_INFER_RETRIES, Reactor, Report, Run,
load_agents,
};
mod errors;
mod mixed_models;
mod notifications;
mod persistence;
mod scheduling;
#[cfg(feature = "schemars")]
mod tools;
mod truncation;
#[derive(Debug, thiserror::Error)]
enum TestError {
#[error("tool: {0}")]
Tool(#[from] Box<dyn std::error::Error + Send + Sync>),
#[error("json: {0}")]
Json(#[from] serde_json::Error),
#[error("not found: {0}")]
NotFound(#[from] AgentNotFound),
#[error("{0}")]
Msg(String),
#[error("transient, retry after {0:?}")]
Transient(Duration),
}
impl RetryAfter for TestError {
fn retry_after(&self) -> Option<Duration> {
match self {
TestError::Transient(d) => Some(*d),
_ => None,
}
}
}
#[derive(
Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize,
)]
enum Behavior {
Complete,
ErrHandle,
Stall,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct TestState {
behavior: Behavior,
turns_left: usize,
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "poisoned"
)]
poison: Option<()>,
}
fn poisoned<S: serde::Serializer>(
_: &Option<()>,
_: S,
) -> Result<S::Ok, S::Error> {
Err(serde::ser::Error::custom("poisoned state"))
}
impl State for TestState {}
struct TestAgent {
id: AgentId,
state: TestState,
prompt: Prompt,
tools: misanthropic::tool::ToolBox,
model: ModelInfo,
teardowns: Arc<AtomicUsize>,
ctx: &'static str,
admitted: Arc<Mutex<Option<Quirks>>>,
}
impl TestAgent {
fn push_user(&mut self, text: &str) -> Result<(), TestError> {
self.prompt
.push_message((Role::User, text.to_string()))
.map_err(|e| TestError::Msg(e.to_string()))?;
Ok(())
}
}
#[async_trait::async_trait]
impl Agent for TestAgent {
type State = TestState;
type Context = &'static str;
type Error = TestError;
fn new(
id: AgentId,
state: TestState,
context: &'static str,
) -> Result<Self, TestError> {
let mut agent = Self {
id,
state,
prompt: Prompt::default(),
tools: misanthropic::tool::ToolBox::new(),
model: model_info(false),
teardowns: Arc::new(AtomicUsize::new(0)),
ctx: context,
admitted: Arc::new(Mutex::new(None)),
};
agent.push_user("start")?;
Ok(agent)
}
fn id(&self) -> AgentId {
self.id
}
fn state(&self) -> &TestState {
&self.state
}
fn prompt(&self) -> &Prompt {
&self.prompt
}
fn parts(&mut self) -> (&mut misanthropic::tool::ToolBox, &mut Prompt) {
(&mut self.tools, &mut self.prompt)
}
fn model(&self) -> ModelInfo {
self.model.clone()
}
fn on_admit(&mut self, _model: &ModelInfo, quirks: &Quirks) {
*self.admitted.lock().unwrap() = Some(*quirks);
}
async fn on_teardown(&mut self) -> Result<(), TestError> {
self.teardowns.fetch_add(1, Ordering::SeqCst);
let (tools, prompt) = self.parts();
tools.on_teardown(prompt).await?;
Ok(())
}
async fn on_quiesce(
&mut self,
_response: &response::Message,
) -> Result<Control, TestError> {
match self.state.behavior {
Behavior::ErrHandle => Err(TestError::Msg("boom".into())),
Behavior::Stall => {
self.push_user("retry")?;
Ok(Control::Stalled)
}
Behavior::Complete => {
if self.state.turns_left <= 1 {
Ok(Control::Done(Outcome::Complete))
} else {
self.state.turns_left -= 1;
self.push_user("next")?;
Ok(Control::Continue)
}
}
}
}
}
fn agent(behavior: Behavior, turns_left: usize) -> TestAgent {
TestAgent::new(
AgentId::new(),
TestState {
behavior,
turns_left,
poison: None,
},
"",
)
.unwrap()
}
fn batch_agent(behavior: Behavior, turns_left: usize) -> TestAgent {
let mut a = agent(behavior, turns_left);
a.model = model_info(true);
a
}
fn named_agent(name: &str, behavior: Behavior, turns_left: usize) -> TestAgent {
let mut a = agent(behavior, turns_left);
a.model = model_info_named(name, false);
a.prompt.model = a.model.id.clone();
a
}
fn named_batch_agent(
name: &str,
behavior: Behavior,
turns_left: usize,
) -> TestAgent {
let mut a = named_agent(name, behavior, turns_left);
a.model = model_info_named(name, true);
a
}
fn model_info(batch: bool) -> ModelInfo {
ModelInfo {
id: misanthropic::model::Model::default(),
..model_info_named("test-model", batch)
}
}
fn model_info_named(name: &str, batch: bool) -> ModelInfo {
use misanthropic::model::{Capabilities, Kind};
ModelInfo {
id: name.to_owned().into(),
display_name: name.to_owned().into(),
capabilities: Capabilities {
batch: batch.into(),
..Default::default()
},
max_input_tokens: 0,
max_tokens: 0,
kind: Kind::Model,
created_at: chrono::DateTime::from_timestamp(0, 0).unwrap(),
}
}
fn offered_models() -> misanthropic::model::Models {
[model_info(true)].into_iter().collect()
}
#[derive(Default, Clone)]
struct MemStore {
map: Arc<Mutex<HashMap<AgentId, serde_json::Value>>>,
}
#[async_trait::async_trait]
impl Storage for MemStore {
type Error = TestError;
async fn save_raw(
&mut self,
id: AgentId,
value: serde_json::Value,
) -> Result<(), TestError> {
self.map.lock().unwrap().insert(id, value);
Ok(())
}
async fn load_raw(
&self,
id: AgentId,
) -> Result<serde_json::Value, TestError> {
match self.map.lock().unwrap().get(&id) {
Some(value) => Ok(serde_json::from_value(value.clone())?),
None => Err(AgentNotFound(id).into()),
}
}
}
#[derive(Default, Clone)]
struct BulkStore {
map: Arc<Mutex<HashMap<AgentId, serde_json::Value>>>,
bulk_calls: Arc<AtomicUsize>,
last_batch: Arc<Mutex<usize>>,
}
#[async_trait::async_trait]
impl Storage for BulkStore {
type Error = TestError;
async fn save_raw(
&mut self,
id: AgentId,
value: serde_json::Value,
) -> Result<(), TestError> {
self.map.lock().unwrap().insert(id, value);
Ok(())
}
async fn load_raw(
&self,
id: AgentId,
) -> Result<serde_json::Value, TestError> {
match self.map.lock().unwrap().get(&id) {
Some(value) => Ok(serde_json::from_value(value.clone())?),
None => Err(AgentNotFound(id).into()),
}
}
async fn save_all_raw<It>(
&mut self,
items: It,
) -> Result<(), SaveError<TestError>>
where
It: ExactSizeIterator<Item = (AgentId, serde_json::Value)> + Send,
{
self.bulk_calls.fetch_add(1, Ordering::SeqCst);
*self.last_batch.lock().unwrap() = items.len();
let mut map = self.map.lock().unwrap();
for (id, value) in items {
map.insert(id, value);
}
Ok(())
}
}
#[derive(Default)]
struct MockInference {
script: Mutex<VecDeque<response::Message>>,
quirks: Quirks,
}
impl MockInference {
fn scripted(messages: impl IntoIterator<Item = response::Message>) -> Self {
Self {
script: Mutex::new(messages.into_iter().collect()),
..Default::default()
}
}
fn end_turns(n: usize) -> Self {
Self::scripted((0..n).map(|_| message(StopReason::EndTurn)))
}
}
fn stop_str(stop: StopReason) -> &'static str {
match stop {
StopReason::EndTurn => "end_turn",
StopReason::PauseTurn => "pause_turn",
StopReason::ToolUse => "tool_use",
StopReason::MaxTokens => "max_tokens",
StopReason::StopSequence => "stop_sequence",
StopReason::Refusal => "refusal",
}
}
fn message(stop: StopReason) -> response::Message {
let stop = stop_str(stop);
serde_json::from_value(serde_json::json!({
"id": "msg_test",
"role": "assistant",
"content": [{ "type": "text", "text": "ok" }],
"model": "claude-3-5-haiku-latest",
"stop_reason": stop,
"stop_sequence": null,
}))
.expect("valid response::Message fixture")
}
#[async_trait::async_trait]
impl Inference for MockInference {
type Error = TestError;
async fn infer<P>(&self, _prompt: P) -> Result<response::Message, TestError>
where
P: Serialize + Send,
{
Ok(self.script.lock().unwrap().pop_front().expect(
"mock inference script exhausted: more infer calls than scripted",
))
}
async fn infer_batch<P>(
&self,
prompts: &[&P],
) -> Result<Vec<Result<response::Message, TestError>>, TestError>
where
P: Serialize + Send + Sync,
{
Ok(prompts
.iter()
.map(|_| Ok(message(StopReason::EndTurn)))
.collect())
}
async fn models(&self) -> Result<misanthropic::model::Models, TestError> {
Ok(offered_models())
}
fn quirks(&self) -> Quirks {
self.quirks
}
}
#[derive(Default, Clone)]
struct SharedSizes(Arc<Mutex<Vec<usize>>>);
impl SharedSizes {
fn get(&self) -> Vec<usize> {
self.0.lock().unwrap().clone()
}
}
struct RecordingBatch {
sizes: SharedSizes,
}
#[async_trait::async_trait]
impl Inference for RecordingBatch {
type Error = TestError;
async fn infer<P>(&self, _prompt: P) -> Result<response::Message, TestError>
where
P: Serialize + Send,
{
Ok(message(StopReason::EndTurn))
}
async fn infer_batch<P>(
&self,
prompts: &[&P],
) -> Result<Vec<Result<response::Message, TestError>>, TestError>
where
P: Serialize + Send + Sync,
{
self.sizes.0.lock().unwrap().push(prompts.len());
Ok(prompts
.iter()
.map(|_| Ok(message(StopReason::EndTurn)))
.collect())
}
async fn models(&self) -> Result<misanthropic::model::Models, TestError> {
Ok(offered_models())
}
}
#[derive(Default, Clone)]
struct MixedRecorder {
infer_calls: Arc<AtomicUsize>,
batch_sizes: SharedSizes,
}
#[async_trait::async_trait]
impl Inference for MixedRecorder {
type Error = TestError;
async fn infer<P>(&self, _prompt: P) -> Result<response::Message, TestError>
where
P: Serialize + Send,
{
self.infer_calls.fetch_add(1, Ordering::SeqCst);
Ok(message(StopReason::EndTurn))
}
async fn infer_batch<P>(
&self,
prompts: &[&P],
) -> Result<Vec<Result<response::Message, TestError>>, TestError>
where
P: Serialize + Send + Sync,
{
self.batch_sizes.0.lock().unwrap().push(prompts.len());
Ok(prompts
.iter()
.map(|_| Ok(message(StopReason::EndTurn)))
.collect())
}
async fn models(&self) -> Result<misanthropic::model::Models, TestError> {
Ok(offered_models())
}
}
fn wire_model<P: Serialize + ?Sized>(prompt: &P) -> String {
serde_json::to_value(prompt).expect("prompt serializes")["model"]
.as_str()
.expect("prompt carries a string model id")
.to_owned()
}
#[derive(Clone)]
struct ModelRecorder {
offered: misanthropic::model::Models,
seq: Arc<Mutex<Vec<String>>>,
rounds: Arc<Mutex<Vec<Vec<String>>>>,
}
impl ModelRecorder {
fn offering(models: impl IntoIterator<Item = ModelInfo>) -> Self {
Self {
offered: models.into_iter().collect(),
seq: Arc::default(),
rounds: Arc::default(),
}
}
fn seq_models(&self) -> Vec<String> {
self.seq.lock().unwrap().clone()
}
fn round_models(&self) -> Vec<Vec<String>> {
self.rounds.lock().unwrap().clone()
}
}
#[async_trait::async_trait]
impl Inference for ModelRecorder {
type Error = TestError;
async fn infer<P>(&self, prompt: P) -> Result<response::Message, TestError>
where
P: Serialize + Send,
{
self.seq.lock().unwrap().push(wire_model(&prompt));
Ok(message(StopReason::EndTurn))
}
async fn infer_batch<P>(
&self,
prompts: &[&P],
) -> Result<Vec<Result<response::Message, TestError>>, TestError>
where
P: Serialize + Send + Sync,
{
self.rounds
.lock()
.unwrap()
.push(prompts.iter().map(|p| wire_model(*p)).collect());
Ok(prompts
.iter()
.map(|_| Ok(message(StopReason::EndTurn)))
.collect())
}
async fn models(&self) -> Result<misanthropic::model::Models, TestError> {
Ok(self.offered.clone())
}
}
#[derive(Default, Clone)]
struct PartialStore {
map: Arc<Mutex<HashMap<AgentId, serde_json::Value>>>,
commit: usize,
}
impl PartialStore {
fn commit(commit: usize) -> Self {
Self {
commit,
..Default::default()
}
}
}
#[async_trait::async_trait]
impl Storage for PartialStore {
type Error = TestError;
async fn save_raw(
&mut self,
id: AgentId,
value: serde_json::Value,
) -> Result<(), TestError> {
self.map.lock().unwrap().insert(id, value);
Ok(())
}
async fn load_raw(
&self,
id: AgentId,
) -> Result<serde_json::Value, TestError> {
match self.map.lock().unwrap().get(&id) {
Some(value) => Ok(serde_json::from_value(value.clone())?),
None => Err(AgentNotFound(id).into()),
}
}
async fn save_all_raw<It>(
&mut self,
items: It,
) -> Result<(), SaveError<TestError>>
where
It: ExactSizeIterator<Item = (AgentId, serde_json::Value)> + Send,
{
let mut saved = BTreeSet::new();
let mut map = self.map.lock().unwrap();
for (i, (id, value)) in items.enumerate() {
if i >= self.commit {
return Err(SaveError {
saved,
inner: TestError::Msg("out of space".into()),
});
}
map.insert(id, value);
saved.insert(id);
}
Ok(())
}
}
struct FlakyModels {
failures_left: AtomicUsize,
}
impl FlakyModels {
fn failing(n: usize) -> Self {
Self {
failures_left: AtomicUsize::new(n),
}
}
}
#[async_trait::async_trait]
impl Inference for FlakyModels {
type Error = TestError;
async fn infer<P>(&self, _prompt: P) -> Result<response::Message, TestError>
where
P: Serialize + Send,
{
Ok(message(StopReason::EndTurn))
}
async fn models(&self) -> Result<misanthropic::model::Models, TestError> {
let failing = self
.failures_left
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |n| {
n.checked_sub(1)
})
.is_ok();
if failing {
Err(TestError::Transient(Duration::from_millis(1)))
} else {
Ok(offered_models())
}
}
}
struct FlakyInfer {
failures_left: AtomicUsize,
calls: Arc<AtomicUsize>,
}
impl FlakyInfer {
fn failing(n: usize) -> Self {
Self {
failures_left: AtomicUsize::new(n),
calls: Arc::default(),
}
}
}
#[async_trait::async_trait]
impl Inference for FlakyInfer {
type Error = TestError;
async fn infer<P>(&self, _prompt: P) -> Result<response::Message, TestError>
where
P: Serialize + Send,
{
self.calls.fetch_add(1, Ordering::SeqCst);
let failing = self
.failures_left
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |n| {
n.checked_sub(1)
})
.is_ok();
if failing {
Err(TestError::Transient(Duration::from_millis(1)))
} else {
Ok(message(StopReason::EndTurn))
}
}
async fn models(&self) -> Result<misanthropic::model::Models, TestError> {
Ok(offered_models())
}
}
struct DeadBatch;
#[async_trait::async_trait]
impl Inference for DeadBatch {
type Error = TestError;
async fn infer<P>(&self, _prompt: P) -> Result<response::Message, TestError>
where
P: Serialize + Send,
{
Ok(message(StopReason::EndTurn))
}
async fn infer_batch<P>(
&self,
_prompts: &[&P],
) -> Result<Vec<Result<response::Message, TestError>>, TestError>
where
P: Serialize + Send + Sync,
{
Err(TestError::Transient(Duration::from_millis(1)))
}
async fn models(&self) -> Result<misanthropic::model::Models, TestError> {
Ok(offered_models())
}
}
struct FailingBatch {
calls: Arc<AtomicUsize>,
fatal: bool,
}
#[async_trait::async_trait]
impl Inference for FailingBatch {
type Error = TestError;
async fn infer<P>(&self, _prompt: P) -> Result<response::Message, TestError>
where
P: Serialize + Send,
{
Ok(message(StopReason::EndTurn))
}
async fn infer_batch<P>(
&self,
prompts: &[&P],
) -> Result<Vec<Result<response::Message, TestError>>, TestError>
where
P: Serialize + Send + Sync,
{
self.calls.fetch_add(1, Ordering::SeqCst);
Ok(prompts
.iter()
.map(|_| {
Err(if self.fatal {
TestError::Msg("fatal".into())
} else {
TestError::Transient(Duration::from_millis(1))
})
})
.collect())
}
async fn models(&self) -> Result<misanthropic::model::Models, TestError> {
Ok(offered_models())
}
}