use phi_core::agent_loop::{agent_loop, AgentLoopConfig};
use phi_core::provider::{AnthropicProvider, ModelConfig};
use phi_core::tools;
use phi_core::types::*;
use phi_core::LlmMessage;
use std::sync::Arc;
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
fn api_key() -> String {
std::env::var("ANTHROPIC_API_KEY").expect("ANTHROPIC_API_KEY must be set")
}
fn make_config() -> AgentLoopConfig {
let key = api_key();
AgentLoopConfig {
model_config: ModelConfig::anthropic("claude-sonnet-4-20250514", "Claude Sonnet 4", &key),
provider_override: Some(Arc::new(AnthropicProvider)),
thinking_level: ThinkingLevel::Off,
max_tokens: Some(1024),
temperature: Some(0.0),
convert_to_llm: None,
transform_context: None,
get_steering_messages: None,
get_follow_up_messages: None,
context_config: None,
execution_limits: None,
cache_config: CacheConfig::default(),
tool_execution: ToolExecutionStrategy::default(),
tool_timeout: None,
response_format: phi_core::provider::ResponseFormat::Text,
retry_config: phi_core::RetryConfig::default(),
before_turn: None,
after_turn: None,
on_error: None,
before_loop: None,
after_loop: None,
before_tool_execution: None,
after_tool_execution: None,
before_tool_execution_update: None,
after_tool_execution_update: None,
before_compaction_start: None,
after_compaction_end: None,
input_filters: vec![],
first_turn_trigger: TurnTrigger::User,
config_id: None,
context_translation: None,
prun_pending: None,
revert_pending: None,
current_tool: None,
revert_render_policy: phi_core::RevertRenderPolicy::default(),
}
}
fn extract_assistant_text(messages: &[AgentMessage]) -> String {
messages
.iter()
.filter_map(|m| {
if let AgentMessage::Llm(LlmMessage {
message: Message::Assistant { content, .. },
..
}) = m
{
Some(
content
.iter()
.filter_map(|c| {
if let Content::Text { text } = c {
Some(text.as_str())
} else {
None
}
})
.collect::<Vec<_>>()
.join(""),
)
} else {
None
}
})
.collect::<Vec<_>>()
.join(" ")
}
fn has_assistant_message(messages: &[AgentMessage]) -> bool {
messages.iter().any(|m| {
matches!(
m,
AgentMessage::Llm(LlmMessage {
message: Message::Assistant { .. },
..
})
)
})
}
#[tokio::test]
#[ignore]
async fn test_anthropic_simple_text() {
let config = make_config();
let (tx, mut rx) = mpsc::unbounded_channel();
let cancel = CancellationToken::new();
let mut context = AgentContext {
system_prompt: "Reply with exactly one word.".into(),
messages: Vec::new(),
tools: Vec::new(),
agent_id: None,
session_id: None,
loop_id: None,
parent_loop_id: None,
continuation_kind: None,
session: None,
user_context: Vec::new(),
inrun_context: Vec::new(),
active_node_id: None,
next_node_id: 0,
};
let prompt = AgentMessage::Llm(LlmMessage::new(Message::user("What color is the sky?")));
let new_messages = agent_loop(vec![prompt], &mut context, &config, tx, cancel).await;
assert!(
!new_messages.is_empty(),
"Expected at least one new message"
);
assert!(
has_assistant_message(&new_messages),
"Expected an assistant message"
);
let text = extract_assistant_text(&new_messages);
assert!(!text.is_empty(), "Expected non-empty text response");
println!("Response: {}", text);
let mut got_start = false;
let mut _got_text_delta = false;
let mut got_end = false;
while let Ok(event) = rx.try_recv() {
match event {
AgentEvent::AgentStart { .. } => got_start = true,
AgentEvent::MessageUpdate {
delta: StreamDelta::Text { .. },
..
} => _got_text_delta = true,
AgentEvent::AgentEnd { .. } => got_end = true,
_ => {}
}
}
assert!(got_start, "Expected AgentStart event");
assert!(got_end, "Expected AgentEnd event");
}
#[tokio::test]
#[ignore]
async fn test_anthropic_tool_use() {
let config = make_config();
let (tx, mut rx) = mpsc::unbounded_channel();
let cancel = CancellationToken::new();
let mut context = AgentContext {
system_prompt:
"You are a helpful assistant. Use the bash tool to answer questions. Be concise.".into(),
messages: Vec::new(),
tools: tools::default_tools(),
agent_id: None,
session_id: None,
loop_id: None,
parent_loop_id: None,
continuation_kind: None,
session: None,
user_context: Vec::new(),
inrun_context: Vec::new(),
active_node_id: None,
next_node_id: 0,
};
let prompt = AgentMessage::Llm(LlmMessage::new(Message::user(
"What is the output of `echo hello_phi-core`? Use bash to run it.",
)));
let new_messages = agent_loop(vec![prompt], &mut context, &config, tx, cancel).await;
assert!(
new_messages.len() >= 3,
"Expected at least 3 messages (tool call + result + response), got {}",
new_messages.len()
);
let mut got_tool_start = false;
let mut got_tool_end = false;
let mut tool_names: Vec<String> = Vec::new();
while let Ok(event) = rx.try_recv() {
match event {
AgentEvent::ToolExecutionStart { tool_name, .. } => {
got_tool_start = true;
tool_names.push(tool_name);
}
AgentEvent::ToolExecutionEnd { .. } => {
got_tool_end = true;
}
_ => {}
}
}
assert!(got_tool_start, "Expected ToolExecutionStart event");
assert!(got_tool_end, "Expected ToolExecutionEnd event");
assert!(
tool_names.contains(&"bash".to_string()),
"Expected bash tool to be called, got: {:?}",
tool_names
);
let final_text = extract_assistant_text(&new_messages);
assert!(
final_text.contains("hello_phi-core"),
"Expected response to contain 'hello_phi-core', got: {}",
final_text
);
println!("Full text: {}", final_text);
}
#[tokio::test]
#[ignore]
async fn test_anthropic_multi_turn() {
let config = make_config();
let cancel = CancellationToken::new();
let mut context = AgentContext {
system_prompt: "Reply with exactly one word.".into(),
messages: Vec::new(),
tools: Vec::new(),
agent_id: None,
session_id: None,
loop_id: None,
parent_loop_id: None,
continuation_kind: None,
session: None,
user_context: Vec::new(),
inrun_context: Vec::new(),
active_node_id: None,
next_node_id: 0,
};
let (tx1, _rx1) = mpsc::unbounded_channel();
let prompt1 = AgentMessage::Llm(LlmMessage::new(Message::user("What color is grass?")));
let msgs1 = agent_loop(vec![prompt1], &mut context, &config, tx1, cancel.clone()).await;
assert!(!msgs1.is_empty(), "Turn 1 should produce messages");
let (tx2, _rx2) = mpsc::unbounded_channel();
let prompt2 = AgentMessage::Llm(LlmMessage::new(Message::user("And the sky?")));
let msgs2 = agent_loop(vec![prompt2], &mut context, &config, tx2, cancel.clone()).await;
assert!(!msgs2.is_empty(), "Turn 2 should produce messages");
assert!(
context.messages.len() >= 4,
"Expected at least 4 messages in context (2 user + 2 assistant), got {}",
context.messages.len()
);
println!(
"Context has {} messages after 2 turns",
context.messages.len()
);
}