use std::sync::Arc;
use a2a_types::Message;
use crate::agent::{AgentDefinition, LlmFunction};
use crate::errors::AgentResult;
use crate::models::{BaseLlm, Content, Event, Thread};
use crate::runtime::context::AuthContext;
use crate::runtime::core::negotiator::{NegotiationDecision, Negotiator};
#[must_use]
pub fn default_negotiation_prompt() -> Content {
Content::from_text(
r"You are an agent negotiator. Your role is to understand the user's intent and determine which skill should handle their request.
Available information:
- Agent skills: A list of available skills with their descriptions
- User message: The user's current message
- Context: Previous messages in this conversation (if any)
- Related tasks: IDs of related tasks in this context (if any)
Your responsibilities:
1. Analyze the user's message to understand their intent
2. Match the intent against the available skills
3. If a skill is clearly identified and you have all necessary information, respond with the skill ID
4. If the intent is ambiguous or you need more information, ask clarifying questions
Response format:
- If ready to start a task: Return the skill_id and any processed content
- If more information is needed: Return a clarifying question for the user
Be helpful, concise, and professional in your responses.",
)
}
#[derive(Clone)]
pub struct DefaultNegotiator {
llm: Arc<dyn BaseLlm>,
custom_prompt: Option<Content>,
}
impl DefaultNegotiator {
pub fn new(llm: Arc<dyn BaseLlm>) -> Self {
Self {
llm,
custom_prompt: None,
}
}
#[must_use]
pub fn with_prompt(mut self, prompt: Content) -> Self {
self.custom_prompt = Some(prompt);
self
}
fn build_negotiation_prompt(agent_def: &AgentDefinition) -> String {
let mut prompt = format!(
r#"You are the negotiator for the "{}" agent.
Agent Description: {}
Your role is to analyze user requests and determine the appropriate action."#,
agent_def.name(),
agent_def.description().unwrap_or("A helpful AI agent")
);
prompt.push_str("\n\n## Available Skills\n\n");
if agent_def.skills().is_empty() {
prompt.push_str("No skills are currently available.\n");
} else {
use std::fmt::Write;
for skill in agent_def.skills() {
let metadata = skill.metadata();
let _ = writeln!(
prompt,
"- **{}** (`{}`): {}",
metadata.name, metadata.id, metadata.description
);
if !metadata.examples.is_empty() {
prompt.push_str(" Examples:\n");
for example in &metadata.examples {
let _ = writeln!(prompt, " - {example}");
}
}
}
}
prompt.push_str(
r"
## Your Task
Analyze the user's message and decide on ONE of the following actions:
1. **START_TASK** - If you can clearly identify which skill should handle this request and you have sufficient information to proceed.
- Return the skill_id of the matching skill
- Provide reasoning for your selection
2. **ASK_CLARIFICATION** - If the user's intent is unclear, ambiguous, or missing critical information.
- Ask specific, helpful questions
- Guide the user toward providing the information needed
- Be concise but thorough
3. **REJECT** - If the request is outside the scope of all available skills.
- Politely explain why this cannot be handled
- Suggest what the agent CAN do (mention available skills)
- Be helpful and professional
## Guidelines
- Be decisive: Don't ask for clarification if the intent is clear
- Be specific: When asking questions, explain exactly what information you need
- Be helpful: When rejecting, explain what the agent can actually help with
- Consider context: If this is a follow-up message, take the conversation history into account
- Match accurately: Only suggest a skill if it genuinely matches the user's intent
Respond with a structured decision following the NegotiationDecision schema."
);
prompt
}
fn messages_to_thread(messages: Vec<Message>) -> Thread {
use a2a_types::Role;
let events: Vec<Event> = messages
.into_iter()
.map(|msg| {
let role = msg.role;
let content = Content::from(msg);
if role == Role::User as i32 {
Event::user(content)
} else {
Event::assistant(content)
}
})
.collect();
Thread::new(events)
}
async fn execute_negotiation(
&self,
system_prompt: String,
thread: Thread,
) -> AgentResult<NegotiationDecision> {
let llm_function = LlmFunction::<NegotiationDecision>::new_with_shared_model(
Arc::clone(&self.llm),
Some(system_prompt),
);
llm_function.run(thread).await
}
}
#[cfg_attr(all(target_os = "wasi", target_env = "p1"), async_trait::async_trait(?Send))]
#[cfg_attr(
not(all(target_os = "wasi", target_env = "p1")),
async_trait::async_trait
)]
impl Negotiator for DefaultNegotiator {
async fn negotiate(
&self,
_auth_ctx: &AuthContext,
agent_def: &AgentDefinition,
_context_id: &str,
content: Content,
history: Vec<Message>,
) -> AgentResult<NegotiationDecision> {
let system_prompt = Self::build_negotiation_prompt(agent_def);
let thread = if history.is_empty() {
let user_event = Event::user(content);
Thread::new(vec![user_event])
} else {
let thread = Self::messages_to_thread(history);
thread.add_event(Event::user(content))
};
self.execute_negotiation(system_prompt, thread).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::agent::{Agent, OnRequestResult, RegisteredSkill, SkillHandler, SkillMetadata};
use crate::errors::{AgentError, AgentResult};
use crate::models::LlmResponse;
use crate::runtime::context::{ProgressSender, State};
use crate::runtime::AgentRuntime;
use crate::test_support::FakeLlm;
use std::sync::Arc;
struct StubSkill;
#[cfg_attr(
all(target_os = "wasi", target_env = "p1"),
async_trait::async_trait(?Send)
)]
#[cfg_attr(
not(all(target_os = "wasi", target_env = "p1")),
async_trait::async_trait
)]
impl SkillHandler for StubSkill {
async fn on_request(
&self,
_state: &mut State,
_progress: &ProgressSender,
_runtime: &dyn AgentRuntime,
_content: Content,
) -> Result<OnRequestResult, AgentError> {
Ok(OnRequestResult::Completed {
message: None,
artifacts: Vec::new(),
})
}
}
impl RegisteredSkill for StubSkill {
fn metadata() -> std::sync::Arc<SkillMetadata> {
std::sync::Arc::new(SkillMetadata::new(
"stub-skill",
"Stub Skill",
"A skill used for negotiation tests",
&[],
&[],
&[],
&[],
))
}
}
fn negotiation_response(skill_id: &str) -> AgentResult<LlmResponse> {
let json_str = format!(
r#"{{"type": "start_task", "skill_id": "{skill_id}", "reasoning": "clear intent"}}"#
);
FakeLlm::content_response(Content::from_text(json_str))
}
#[tokio::test(flavor = "current_thread")]
async fn negotiate_returns_start_task_decision() {
let llm: Arc<dyn BaseLlm> = Arc::new(FakeLlm::with_responses(
"negotiator",
[negotiation_response("stub-skill")],
));
let negotiator = DefaultNegotiator::new(llm);
let agent = Agent::builder()
.with_name("Agent")
.with_skill(StubSkill)
.build();
let decision = negotiator
.negotiate(
&AuthContext {
app_name: "app".into(),
user_name: "user".into(),
},
&agent,
"ctx",
Content::from_text("perform stub-skill"),
Vec::new(),
)
.await
.expect("decision");
match decision {
NegotiationDecision::StartTask { skill_id, .. } => {
assert_eq!(skill_id, "stub-skill");
}
other => panic!("unexpected decision: {other:?}"),
}
}
}