use std::sync::Arc;
use serde_json::{Map, Value};
use crate::chat::{Chat, Fallback};
use crate::config::Config;
use crate::context::Context;
use crate::error::{ErrorKind, Result};
use crate::protocols::{ToolCalls, ToolChoice};
use crate::providers::ProtocolName;
use crate::thinking::ThinkingConfig;
use crate::tool::SharedTool;
pub trait Agent {
fn model(&self) -> Option<&str> {
None
}
fn provider(&self) -> Option<&str> {
None
}
fn protocol(&self) -> Option<ProtocolName> {
None
}
fn assume_model_exists(&self) -> bool {
false
}
fn context(&self) -> Option<Context> {
None
}
fn instructions(&self) -> Option<String> {
None
}
fn name(&self) -> String {
let full = std::any::type_name::<Self>();
let base = full.split('<').next().unwrap_or(full);
base.rsplit("::").next().unwrap_or(base).to_string()
}
fn prompt_locals(&self) -> Value {
Value::Object(Default::default())
}
fn instruction_declarations(
&self,
config: &Arc<Config>,
chat: &Value,
) -> Result<Vec<InstructionDeclaration>> {
if let Some(text) = self.instructions() {
return Ok(vec![InstructionDeclaration::new(text)]);
}
let prompt = crate::prompt::Prompt::with_config(
config.clone(),
format!("{}/instructions", prompt_agent_path(&self.name())),
);
if !prompt.exists() {
return Ok(Vec::new());
}
let mut locals = Map::new();
locals.insert("chat".into(), chat.clone());
if let Value::Object(declared) = self.prompt_locals() {
locals.extend(declared);
}
Ok(vec![InstructionDeclaration::new(
prompt.render(Value::Object(locals))?,
)])
}
fn render_prompt(&self, name: &str, locals: Value) -> Result<String> {
let mut merged = match self.prompt_locals() {
Value::Object(m) => m,
_ => Default::default(),
};
if let Value::Object(extra) = locals {
merged.extend(extra);
}
crate::prompt::render_prompt(
&format!("{}/{name}", prompt_agent_path(&self.name())),
Value::Object(merged),
)
}
fn tools(&self) -> Vec<SharedTool> {
Vec::new()
}
fn tool_choice(&self) -> Option<ToolChoice> {
None
}
fn tool_calls(&self) -> Option<ToolCalls> {
None
}
fn tool_concurrency(&self) -> Option<bool> {
None
}
fn temperature(&self) -> Option<f64> {
None
}
fn max_output_tokens(&self) -> Option<i64> {
None
}
fn thinking(&self) -> Option<ThinkingConfig> {
None
}
fn schema(&self) -> Option<Value> {
None
}
fn citations(&self) -> Option<bool> {
None
}
fn caching(&self) -> Option<Value> {
None
}
fn compaction(&self) -> Option<Value> {
None
}
fn end_user(&self) -> Option<String> {
None
}
fn provider_options(&self) -> Option<Value> {
None
}
fn headers(&self) -> Vec<(String, String)> {
Vec::new()
}
fn fallbacks(&self) -> Vec<Fallback> {
Vec::new()
}
fn fallback_errors(&self) -> Option<Vec<ErrorKind>> {
None
}
fn mcp(&self) -> Vec<crate::mcp::Mcp> {
Vec::new()
}
fn mcp_defer(&self) -> Option<bool> {
None
}
fn provider_tools(&self) -> Vec<crate::provider_tools::ProviderTool> {
Vec::new()
}
fn apply(&self, chat: Chat) -> Result<Chat> {
let chat = self.apply_except_instructions(chat)?;
apply_instructions(self, chat)
}
fn apply_except_instructions(&self, chat: Chat) -> Result<Chat> {
apply_configuration(self, chat, self.context())
}
fn chat(&self) -> Result<Chat> {
let context = self.context();
let config = context
.as_ref()
.map(|c| c.config().clone())
.unwrap_or_else(crate::config);
let chat = Chat::with_config(
config,
self.model(),
self.provider(),
self.assume_model_exists(),
)?;
let chat = apply_configuration(self, chat, context)?;
apply_instructions(self, chat)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct InstructionDeclaration {
pub text: String,
pub append: bool,
pub persist: bool,
pub cache_until_here: bool,
}
impl InstructionDeclaration {
pub fn new(text: impl Into<String>) -> Self {
InstructionDeclaration {
text: text.into(),
append: false,
persist: true,
cache_until_here: false,
}
}
}
fn apply_instructions<A: Agent + ?Sized>(agent: &A, mut chat: Chat) -> Result<Chat> {
let config = chat.config().clone();
for d in agent.instruction_declarations(&config, &Value::Null)? {
if !d.text.trim().is_empty() {
chat.set_instructions(Some(d.text), d.append, d.cache_until_here);
}
}
Ok(chat)
}
fn apply_configuration<A: Agent + ?Sized>(
agent: &A,
mut chat: Chat,
context: Option<Context>,
) -> Result<Chat> {
if let Some(context) = context.filter(|c| !Arc::ptr_eq(c.config(), chat.config())) {
chat = chat.with_context(Some(&context))?;
}
chat = chat.with_tools(agent.tools());
for server in agent.mcp() {
chat = chat.with_mcp_deferred(server, agent.mcp_defer());
}
if let Some(choice) = agent.tool_choice() {
chat = chat.with_tool_choice(choice)?;
}
if let Some(calls) = agent.tool_calls() {
chat = chat.with_tool_calls(calls);
}
if let Some(enabled) = agent.tool_concurrency() {
chat = chat.with_tool_concurrency(enabled);
}
chat = chat.with_provider_tools(agent.provider_tools());
if let Some(t) = agent.temperature() {
chat = chat.with_temperature(t);
}
if let Some(m) = agent.max_output_tokens() {
chat = chat.with_max_output_tokens(m);
}
if let Some(t) = agent.thinking() {
chat = chat.with_thinking(t);
}
if let Some(c) = agent.citations() {
chat = chat.with_citations(c);
}
if let Some(u) = agent.end_user() {
chat = chat.with_end_user(Some(&u));
}
if let Some(c) = agent.caching() {
chat = chat.with_caching(c)?;
}
if let Some(c) = agent.compaction() {
chat = chat.with_compaction(c)?;
}
if let Some(o) = agent.provider_options() {
chat = chat.with_provider_options(o);
}
let headers = agent.headers();
if !headers.is_empty() {
chat = chat.with_headers(headers);
}
if let Some(s) = agent.schema() {
chat = chat.with_schema(s);
}
let fallbacks = agent.fallbacks();
if !fallbacks.is_empty() {
chat = chat.with_fallbacks(fallbacks);
if let Some(kinds) = agent.fallback_errors() {
chat = chat.with_fallback_errors(kinds);
}
}
if let Some(p) = agent.protocol() {
chat = chat.with_protocol(p);
}
Ok(chat)
}
pub fn prompt_agent_path(name: &str) -> String {
crate::tool::underscore(&name.replace("::", "/")).replace('-', "_")
}