use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use lc_core::language_models::BaseChatModel;
use lc_core::runnables::RunnableConfig;
use lc_rag::RetrieverTrait;
use serde_json::Value;
use tokio::sync::Semaphore;
use crate::task::AgentTask;
use crate::{
AdaptiveRAG, AdaptiveRAGResult, AgentError, CRAGResult, CorrectiveRAGAgent, DeepResearchAgent,
PlanExecuteAgent, ResearchReport,
};
#[async_trait]
pub trait Orchestrator: Send + Sync {
type Input;
type Output;
async fn run_with_context(
&self,
input: Self::Input,
ctx: &RunContext,
) -> Result<Self::Output, AgentError>;
}
#[derive(Debug, Clone)]
pub struct RunContext {
pub trace_id: String,
pub shared_state: Option<Arc<Mutex<Value>>>,
}
pub fn generate_trace_id() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
format!("trace-{:x}", nanos)
}
impl RunContext {
pub fn new(trace_id: impl Into<String>) -> Self {
Self {
trace_id: trace_id.into(),
shared_state: None,
}
}
pub fn new_random() -> Self {
Self::new(generate_trace_id())
}
pub fn with_shared_state(mut self, shared_state: Arc<Mutex<Value>>) -> Self {
self.shared_state = Some(shared_state);
self
}
pub fn from_config(config: &RunnableConfig) -> Self {
let trace_id = config
.metadata
.get("trace_id")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.unwrap_or_else(generate_trace_id);
Self::new(trace_id)
}
}
#[async_trait]
impl Orchestrator for PlanExecuteAgent {
type Input = String;
type Output = String;
async fn run_with_context(
&self,
input: Self::Input,
ctx: &RunContext,
) -> Result<Self::Output, AgentError> {
log::debug!(
target: "lc_agents::orchestrator",
"PlanExecuteAgent start, trace_id = {}",
ctx.trace_id
);
self.run(&input)
.await
.map_err(|e| AgentError::Other(format!("PlanExecute: {e}")))
}
}
#[async_trait]
impl<M, R> Orchestrator for AdaptiveRAG<M, R>
where
M: BaseChatModel + Send + Sync,
M::Error: Send + Sync,
R: RetrieverTrait + Send + Sync,
{
type Input = String;
type Output = AdaptiveRAGResult;
async fn run_with_context(
&self,
input: Self::Input,
ctx: &RunContext,
) -> Result<Self::Output, AgentError> {
log::debug!(
target: "lc_agents::orchestrator",
"AdaptiveRAG start, trace_id = {}",
ctx.trace_id
);
self.invoke(&input)
.await
.map_err(|e| AgentError::Other(format!("AdaptiveRAG: {e}")))
}
}
#[async_trait]
impl<M, R> Orchestrator for CorrectiveRAGAgent<M, R>
where
M: BaseChatModel + Send + Sync,
M::Error: Send + Sync,
R: RetrieverTrait + Send + Sync,
{
type Input = String;
type Output = CRAGResult;
async fn run_with_context(
&self,
input: Self::Input,
ctx: &RunContext,
) -> Result<Self::Output, AgentError> {
log::debug!(
target: "lc_agents::orchestrator",
"CorrectiveRAGAgent start, trace_id = {}",
ctx.trace_id
);
self.invoke(&input)
.await
.map_err(|e| AgentError::Other(format!("CorrectiveRAG: {e}")))
}
}
#[async_trait]
impl<M> Orchestrator for DeepResearchAgent<M>
where
M: BaseChatModel + Send + Sync,
M::Error: Send + Sync,
{
type Input = String;
type Output = ResearchReport;
async fn run_with_context(
&self,
input: Self::Input,
ctx: &RunContext,
) -> Result<Self::Output, AgentError> {
log::debug!(
target: "lc_agents::orchestrator",
"DeepResearchAgent start, trace_id = {}",
ctx.trace_id
);
self.research(&input)
.await
.map_err(|e| AgentError::Other(format!("DeepResearch: {e}")))
}
}
pub struct FanOutFanIn {
workers: Vec<Arc<dyn Orchestrator<Input = AgentTask, Output = String>>>,
aggregator: Box<dyn Fn(Vec<String>) -> String + Send + Sync>,
max_concurrency: usize,
semaphore: Arc<Semaphore>,
}
impl FanOutFanIn {
pub fn new(workers: Vec<Arc<dyn Orchestrator<Input = AgentTask, Output = String>>>) -> Self {
let n = workers.len().max(1);
Self {
workers,
aggregator: Box::new(|results| results.join("\n")),
max_concurrency: n,
semaphore: Arc::new(Semaphore::new(n)),
}
}
pub fn with_aggregator(
mut self,
aggregator: impl Fn(Vec<String>) -> String + Send + Sync + 'static,
) -> Self {
self.aggregator = Box::new(aggregator);
self
}
pub fn with_max_concurrency(mut self, n: usize) -> Self {
self.max_concurrency = n.max(1);
self.semaphore = Arc::new(Semaphore::new(self.max_concurrency));
self
}
}
#[async_trait]
impl Orchestrator for FanOutFanIn {
type Input = AgentTask;
type Output = String;
async fn run_with_context(
&self,
input: Self::Input,
ctx: &RunContext,
) -> Result<Self::Output, AgentError> {
if self.workers.is_empty() {
return Err(AgentError::Other(
"FanOutFanIn requires at least one worker".to_string(),
));
}
log::debug!(
target: "lc_agents::orchestrator",
"FanOutFanIn start workers={} concurrency={} trace_id={}",
self.workers.len(),
self.max_concurrency,
ctx.trace_id
);
let futures = self.workers.iter().enumerate().map(|(i, worker)| {
let worker = worker.clone();
let input = input.clone();
let ctx = ctx.clone();
let sem = self.semaphore.clone();
async move {
let _permit = sem
.acquire_owned()
.await
.map_err(|e| AgentError::Other(format!("FanOutFanIn semaphore: {e}")))?;
worker
.run_with_context(input, &ctx)
.await
.map_err(|e| AgentError::Other(format!("worker {i} failed: {e}")))
}
});
let outputs = futures_util::future::join_all(futures).await;
let mut results = Vec::with_capacity(outputs.len());
for output in outputs {
results.push(output?);
}
Ok((self.aggregator)(results))
}
}
pub struct SequentialPipeline {
stages: Vec<Arc<dyn Orchestrator<Input = AgentTask, Output = String>>>,
}
impl SequentialPipeline {
pub fn new(stages: Vec<Arc<dyn Orchestrator<Input = AgentTask, Output = String>>>) -> Self {
Self { stages }
}
pub fn push_stage(
mut self,
stage: Arc<dyn Orchestrator<Input = AgentTask, Output = String>>,
) -> Self {
self.stages.push(stage);
self
}
}
#[async_trait]
impl Orchestrator for SequentialPipeline {
type Input = AgentTask;
type Output = String;
async fn run_with_context(
&self,
input: Self::Input,
ctx: &RunContext,
) -> Result<Self::Output, AgentError> {
let mut current = input;
for (i, stage) in self.stages.iter().enumerate() {
log::debug!(
target: "lc_agents::orchestrator",
"SequentialPipeline stage {i} trace_id = {}",
ctx.trace_id
);
let output = stage
.run_with_context(current.clone(), ctx)
.await
.map_err(|e| AgentError::Other(format!("SequentialPipeline stage {i}: {e}")))?;
let mut next = AgentTask::new(output);
if let Some(expected) = current.expected_output.clone() {
next = next.with_expected_output(expected);
}
next = next.with_allowed_tools(current.allowed_tools.clone());
current = next;
}
Ok(current.objective)
}
}
pub struct TaskAdapter {
inner: Arc<dyn Orchestrator<Input = String, Output = String>>,
}
impl TaskAdapter {
pub fn new(inner: Arc<dyn Orchestrator<Input = String, Output = String>>) -> Self {
Self { inner }
}
}
#[async_trait]
impl Orchestrator for TaskAdapter {
type Input = AgentTask;
type Output = String;
async fn run_with_context(
&self,
task: Self::Input,
ctx: &RunContext,
) -> Result<Self::Output, AgentError> {
log::debug!(
target: "lc_agents::orchestrator",
"TaskAdapter dispatch objective='{}' trace_id = {}",
task.objective,
ctx.trace_id
);
self.inner.run_with_context(task.objective, ctx).await
}
}
pub fn task_adapter(
inner: Arc<dyn Orchestrator<Input = String, Output = String>>,
) -> Arc<dyn Orchestrator<Input = AgentTask, Output = String>> {
Arc::new(TaskAdapter::new(inner))
}
#[derive(Debug, Clone, PartialEq)]
pub struct ReviewVerdict {
pub passed: bool,
pub feedback: String,
}
impl ReviewVerdict {
pub fn pass() -> Self {
Self {
passed: true,
feedback: String::new(),
}
}
pub fn fail(feedback: impl Into<String>) -> Self {
Self {
passed: false,
feedback: feedback.into(),
}
}
}
pub fn review_envelope(task: &AgentTask, output: &str) -> String {
serde_json::json!({
"objective": task.objective,
"expected_output": task.expected_output,
"output": output,
})
.to_string()
}
pub fn parse_review_verdict(text: &str) -> Option<ReviewVerdict> {
let text = text.trim();
if let Ok(value) = serde_json::from_str::<Value>(text) {
if let Some(passed) = value.get("passed").and_then(Value::as_bool) {
let feedback = value
.get("feedback")
.and_then(Value::as_str)
.unwrap_or("")
.to_string();
return Some(ReviewVerdict { passed, feedback });
}
}
if let Some(inner) = between(text, "<<<VERDICT>>>", "<<<END_VERDICT>>>") {
if inner.eq_ignore_ascii_case("PASS") || inner.eq_ignore_ascii_case("FAIL") {
let passed = inner.eq_ignore_ascii_case("PASS");
let feedback = between(text, "<<<FEEDBACK>>>", "<<<END_FEEDBACK>>>")
.unwrap_or("")
.to_string();
return Some(ReviewVerdict { passed, feedback });
}
}
let upper = text.to_uppercase();
if upper.starts_with("PASS") {
return Some(ReviewVerdict::pass());
}
if upper.starts_with("FAIL") {
let feedback = text
.trim_start_matches("FAIL")
.trim()
.trim_start_matches(':')
.trim()
.to_string();
return Some(ReviewVerdict::fail(feedback));
}
None
}
fn between<'a>(text: &'a str, start: &str, end: &str) -> Option<&'a str> {
let s = text.find(start)?;
let rest = &text[s + start.len()..];
let e = rest.find(end)?;
Some(rest[..e].trim())
}
pub struct ReviewOrchestrator {
worker: Arc<dyn Orchestrator<Input = AgentTask, Output = String>>,
reviewer: Arc<dyn Orchestrator<Input = String, Output = String>>,
max_attempts: usize,
fail_on_unresolved: bool,
}
impl ReviewOrchestrator {
pub fn new(
worker: Arc<dyn Orchestrator<Input = AgentTask, Output = String>>,
reviewer: Arc<dyn Orchestrator<Input = String, Output = String>>,
max_attempts: usize,
) -> Self {
Self {
worker,
reviewer,
max_attempts: max_attempts.max(1),
fail_on_unresolved: true,
}
}
pub fn with_max_attempts(mut self, max_attempts: usize) -> Self {
self.max_attempts = max_attempts.max(1);
self
}
pub fn keep_last_output(mut self) -> Self {
self.fail_on_unresolved = false;
self
}
pub fn max_attempts(&self) -> usize {
self.max_attempts
}
}
#[async_trait]
impl Orchestrator for ReviewOrchestrator {
type Input = AgentTask;
type Output = String;
async fn run_with_context(
&self,
input: Self::Input,
ctx: &RunContext,
) -> Result<Self::Output, AgentError> {
let mut task = input;
let mut last_output = String::new();
let mut last_feedback = String::new();
for attempt in 0..self.max_attempts {
log::debug!(
target: "lc_agents::orchestrator",
"ReviewOrchestrator attempt {}/{} trace_id = {}",
attempt + 1,
self.max_attempts,
ctx.trace_id
);
last_output = self
.worker
.run_with_context(task.clone(), ctx)
.await
.map_err(|e| {
AgentError::Other(format!(
"ReviewOrchestrator worker (attempt {}): {e}",
attempt + 1
))
})?;
let review_text = self
.reviewer
.run_with_context(review_envelope(&task, &last_output), ctx)
.await
.map_err(|e| {
AgentError::Other(format!(
"ReviewOrchestrator reviewer (attempt {}): {e}",
attempt + 1
))
})?;
let verdict = parse_review_verdict(&review_text).ok_or_else(|| {
AgentError::Other(format!(
"ReviewOrchestrator: 无法解析评审结论: {review_text}"
))
})?;
if verdict.passed {
log::debug!(
target: "lc_agents::orchestrator",
"ReviewOrchestrator passed on attempt {}",
attempt + 1
);
return Ok(last_output);
}
last_feedback = verdict.feedback.clone();
if attempt + 1 >= self.max_attempts {
log::warn!(
target: "lc_agents::orchestrator",
"ReviewOrchestrator unresolved after {} attempts",
self.max_attempts
);
break;
}
let feedback_suffix = if verdict.feedback.trim().is_empty() {
"[评审未通过,请修订输出质量]".to_string()
} else {
format!("[评审未通过,请根据反馈修订: {}]", verdict.feedback.trim())
};
let mut next = AgentTask::new(format!("{}\n{}", task.objective, feedback_suffix));
if let Some(expected) = task.expected_output.clone() {
next = next.with_expected_output(expected);
}
next = next.with_allowed_tools(task.allowed_tools.clone());
task = next;
}
if self.fail_on_unresolved {
let detail = if last_feedback.trim().is_empty() {
"(无)".to_string()
} else {
last_feedback.trim().to_string()
};
Err(AgentError::Other(format!(
"ReviewOrchestrator: 重做 {} 次后仍未达标, 最近反馈: {}",
self.max_attempts, detail
)))
} else {
Ok(last_output)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
struct DummyOrchestrator;
#[async_trait]
impl Orchestrator for DummyOrchestrator {
type Input = String;
type Output = String;
async fn run_with_context(
&self,
input: Self::Input,
ctx: &RunContext,
) -> Result<Self::Output, AgentError> {
Ok(format!("{} via {}", input, ctx.trace_id))
}
}
#[tokio::test]
async fn test_orchestrator_basic() {
let orch = DummyOrchestrator;
let ctx = RunContext::new("trace-1");
let out = orch.run_with_context("hi".to_string(), &ctx).await.unwrap();
assert_eq!(out, "hi via trace-1");
}
#[test]
fn test_run_context_from_config() {
let mut cfg = RunnableConfig::new();
let mut meta = HashMap::new();
meta.insert("trace_id".to_string(), Value::String("cfg-trace".into()));
cfg.metadata = meta;
let ctx = RunContext::from_config(&cfg);
assert_eq!(ctx.trace_id, "cfg-trace");
}
#[test]
fn test_run_context_from_config_missing_trace() {
let cfg = RunnableConfig::new();
let ctx = RunContext::from_config(&cfg);
assert!(ctx.trace_id.starts_with("trace-"), "{}", ctx.trace_id);
}
#[test]
fn test_generate_trace_id_unique() {
let a = generate_trace_id();
let b = generate_trace_id();
assert_ne!(a, b);
}
struct MockOrch {
tag: &'static str,
}
#[async_trait]
impl Orchestrator for MockOrch {
type Input = AgentTask;
type Output = String;
async fn run_with_context(
&self,
task: Self::Input,
ctx: &RunContext,
) -> Result<Self::Output, AgentError> {
Ok(format!("{}:{}:{}", self.tag, task.objective, ctx.trace_id))
}
}
struct MockOrchFail;
#[async_trait]
impl Orchestrator for MockOrchFail {
type Input = AgentTask;
type Output = String;
async fn run_with_context(
&self,
_input: Self::Input,
_ctx: &RunContext,
) -> Result<Self::Output, AgentError> {
Err(AgentError::Other("boom".to_string()))
}
}
fn mock_orch(tag: &'static str) -> Arc<dyn Orchestrator<Input = AgentTask, Output = String>> {
Arc::new(MockOrch { tag })
}
struct CapturingOrch {
tag: &'static str,
seen: Arc<Mutex<Vec<AgentTask>>>,
}
#[async_trait]
impl Orchestrator for CapturingOrch {
type Input = AgentTask;
type Output = String;
async fn run_with_context(
&self,
task: Self::Input,
_ctx: &RunContext,
) -> Result<Self::Output, AgentError> {
self.seen
.lock()
.unwrap_or_else(|e| e.into_inner())
.push(task.clone());
Ok(format!("{}:{}", self.tag, task.objective))
}
}
#[tokio::test]
async fn test_fanout_broadcast_and_join() {
let orch = FanOutFanIn::new(vec![mock_orch("a"), mock_orch("b")]);
let ctx = RunContext::new("t1");
let out = orch
.run_with_context(AgentTask::new("task"), &ctx)
.await
.unwrap();
assert_eq!(out, "a:task:t1\nb:task:t1");
}
#[tokio::test]
async fn test_fanout_custom_aggregator() {
let orch = FanOutFanIn::new(vec![mock_orch("a"), mock_orch("b")])
.with_aggregator(|vs| vs.join(" + "));
let ctx = RunContext::new("t2");
let out = orch
.run_with_context(AgentTask::new("x"), &ctx)
.await
.unwrap();
assert_eq!(out, "a:x:t2 + b:x:t2");
}
#[tokio::test]
async fn test_fanout_worker_error_fails_all() {
let orch = FanOutFanIn::new(vec![mock_orch("ok"), Arc::new(MockOrchFail)]);
let ctx = RunContext::new("t3");
let err = orch
.run_with_context(AgentTask::new("y"), &ctx)
.await
.unwrap_err();
assert!(err.to_string().contains("worker 1 failed"));
}
#[tokio::test]
async fn test_fanout_empty_workers_errors() {
let orch = FanOutFanIn::new(vec![]);
let ctx = RunContext::new("t4");
let err = orch
.run_with_context(AgentTask::new("z"), &ctx)
.await
.unwrap_err();
assert!(err.to_string().contains("at least one worker"));
}
#[tokio::test]
async fn test_pipeline_order_and_data_flow() {
let pipe = SequentialPipeline::new(vec![mock_orch("s1"), mock_orch("s2")]);
let ctx = RunContext::new("t5");
let out = pipe
.run_with_context(AgentTask::new("seed"), &ctx)
.await
.unwrap();
assert_eq!(out, "s2:s1:seed:t5:t5");
}
#[tokio::test]
async fn test_pipeline_push_stage() {
let pipe = SequentialPipeline::new(vec![mock_orch("s1")]).push_stage(mock_orch("s2"));
let ctx = RunContext::new("t6");
let out = pipe
.run_with_context(AgentTask::new("p"), &ctx)
.await
.unwrap();
assert_eq!(out, "s2:s1:p:t6:t6");
}
#[tokio::test]
async fn test_pipeline_stage_error_reports_index() {
let pipe = SequentialPipeline::new(vec![mock_orch("s1"), Arc::new(MockOrchFail)]);
let ctx = RunContext::new("t7");
let err = pipe
.run_with_context(AgentTask::new("q"), &ctx)
.await
.unwrap_err();
assert!(err.to_string().contains("stage 1"));
}
#[tokio::test]
async fn test_fanout_nested_in_pipeline() {
let fanout = FanOutFanIn::new(vec![mock_orch("a"), mock_orch("b")]);
let pipe = SequentialPipeline::new(vec![Arc::new(fanout), mock_orch("tail")]);
let ctx = RunContext::new("t8");
let out = pipe
.run_with_context(AgentTask::new("in"), &ctx)
.await
.unwrap();
assert_eq!(out, "tail:a:in:t8\nb:in:t8:t8");
}
#[tokio::test]
async fn test_task_adapter_bridges_string_orchestrator() {
let inner =
Arc::new(DummyOrchestrator) as Arc<dyn Orchestrator<Input = String, Output = String>>;
let worker = task_adapter(inner);
let orch = FanOutFanIn::new(vec![worker]);
let ctx = RunContext::new("t9");
let out = orch
.run_with_context(
AgentTask::new("适配目标").with_allowed_tools(["calc"]),
&ctx,
)
.await
.unwrap();
assert_eq!(out, "适配目标 via t9");
}
#[tokio::test]
async fn test_fanout_dispatches_task_with_constraints() {
let seen = Arc::new(Mutex::new(Vec::new()));
let cap = |tag: &'static str| -> Arc<dyn Orchestrator<Input = AgentTask, Output = String>> {
Arc::new(CapturingOrch {
tag,
seen: seen.clone(),
})
};
let orch = FanOutFanIn::new(vec![cap("a"), cap("b")]);
let ctx = RunContext::new("t10");
let task = AgentTask::new("研究X")
.with_expected_output("给出一页结论")
.with_allowed_tools(["web_search", "calculator"]);
let out = orch.run_with_context(task, &ctx).await.unwrap();
assert_eq!(out, "a:研究X\nb:研究X");
let seen = seen.lock().unwrap_or_else(|e| e.into_inner());
assert_eq!(seen.len(), 2);
for t in seen.iter() {
assert_eq!(t.objective(), "研究X");
assert_eq!(t.expected_output(), Some("给出一页结论"));
assert_eq!(
t.allowed_tools(),
&["web_search".to_string(), "calculator".to_string()]
);
}
}
#[tokio::test]
async fn test_pipeline_carries_constraints_through_stages() {
let seen = Arc::new(Mutex::new(Vec::new()));
let stage = Arc::new(CapturingOrch {
tag: "s",
seen: seen.clone(),
}) as Arc<dyn Orchestrator<Input = AgentTask, Output = String>>;
let pipe = SequentialPipeline::new(vec![stage.clone(), stage.clone()]);
let ctx = RunContext::new("t11");
let task = AgentTask::new("起点")
.with_expected_output("要点")
.with_allowed_tools(["calc"]);
let out = pipe.run_with_context(task, &ctx).await.unwrap();
assert_eq!(out, "s:s:起点");
let seen = seen.lock().unwrap_or_else(|e| e.into_inner());
assert_eq!(seen.len(), 2);
assert_eq!(seen[0].objective(), "起点");
assert_eq!(seen[0].expected_output(), Some("要点"));
assert_eq!(seen[0].allowed_tools(), &["calc".to_string()]);
assert_eq!(seen[1].objective(), "s:起点");
assert_eq!(seen[1].expected_output(), Some("要点"));
assert_eq!(seen[1].allowed_tools(), &["calc".to_string()]);
}
struct ReviewWorker {
calls: Arc<Mutex<Vec<AgentTask>>>,
first_try_good: bool,
}
#[async_trait]
impl Orchestrator for ReviewWorker {
type Input = AgentTask;
type Output = String;
async fn run_with_context(
&self,
task: Self::Input,
_ctx: &RunContext,
) -> Result<Self::Output, AgentError> {
self.calls
.lock()
.unwrap_or_else(|e| e.into_inner())
.push(task.clone());
if self.first_try_good || task.objective.contains("修订") {
Ok("good answer".to_string())
} else {
Ok("bad answer".to_string())
}
}
}
type ReviewWorkerPair = (
Arc<dyn Orchestrator<Input = AgentTask, Output = String>>,
Arc<Mutex<Vec<AgentTask>>>,
);
fn review_worker(first_try_good: bool) -> ReviewWorkerPair {
let calls = Arc::new(Mutex::new(Vec::new()));
let worker = Arc::new(ReviewWorker {
calls: calls.clone(),
first_try_good,
}) as Arc<dyn Orchestrator<Input = AgentTask, Output = String>>;
(worker, calls)
}
struct ReviewChecker;
#[async_trait]
impl Orchestrator for ReviewChecker {
type Input = String;
type Output = String;
async fn run_with_context(
&self,
input: Self::Input,
_ctx: &RunContext,
) -> Result<Self::Output, AgentError> {
if input.contains("good answer") {
Ok("<<<VERDICT>>>PASS<<<END_VERDICT>>>".to_string())
} else {
Ok(
"<<<VERDICT>>>FAIL<<<END_VERDICT>>>\n<<<FEEDBACK>>>请补充细节<<<END_FEEDBACK>>>"
.to_string(),
)
}
}
}
struct AlwaysFailReview;
#[async_trait]
impl Orchestrator for AlwaysFailReview {
type Input = String;
type Output = String;
async fn run_with_context(
&self,
_input: Self::Input,
_ctx: &RunContext,
) -> Result<Self::Output, AgentError> {
Ok(
"<<<VERDICT>>>FAIL<<<END_VERDICT>>>\n<<<FEEDBACK>>>还差得远<<<END_FEEDBACK>>>"
.to_string(),
)
}
}
#[tokio::test]
async fn test_review_passes_on_first_attempt() {
let (worker, calls) = review_worker(true);
let orch = ReviewOrchestrator::new(worker, Arc::new(ReviewChecker), 3);
let ctx = RunContext::new("r1");
let out = orch
.run_with_context(AgentTask::new("写报告"), &ctx)
.await
.unwrap();
assert_eq!(out, "good answer");
assert_eq!(
calls.lock().unwrap_or_else(|e| e.into_inner()).len(),
1,
"达标后不应重做"
);
}
#[tokio::test]
async fn test_review_redo_until_pass() {
let (worker, calls) = review_worker(false);
let orch = ReviewOrchestrator::new(worker, Arc::new(ReviewChecker), 3);
let ctx = RunContext::new("r2");
let task = AgentTask::new("写报告")
.with_expected_output("一页结论")
.with_allowed_tools(["calc"]);
let out = orch.run_with_context(task, &ctx).await.unwrap();
assert_eq!(out, "good answer");
let calls = calls.lock().unwrap_or_else(|e| e.into_inner());
assert_eq!(calls.len(), 2, "首轮不达标应重做一轮");
assert_eq!(calls[0].objective(), "写报告");
assert!(
calls[1].objective().contains("请补充细节"),
"第二轮目标应携带评审反馈, 实际: {}",
calls[1].objective()
);
assert_eq!(calls[1].expected_output(), Some("一页结论"));
assert_eq!(calls[1].allowed_tools(), &["calc".to_string()]);
}
#[tokio::test]
async fn test_review_exhausts_returns_error_by_default() {
let (worker, _) = review_worker(false);
let orch = ReviewOrchestrator::new(worker, Arc::new(AlwaysFailReview), 2);
let ctx = RunContext::new("r3");
let err = orch
.run_with_context(AgentTask::new("任务"), &ctx)
.await
.unwrap_err();
assert!(err.to_string().contains("未达标"), "{}", err);
}
#[tokio::test]
async fn test_review_keep_last_output_on_exhaustion() {
let (worker, _) = review_worker(true);
let orch =
ReviewOrchestrator::new(worker, Arc::new(AlwaysFailReview), 2).keep_last_output();
let ctx = RunContext::new("r4");
let out = orch
.run_with_context(AgentTask::new("任务"), &ctx)
.await
.unwrap();
assert_eq!(out, "good answer");
}
#[tokio::test]
async fn test_review_orchestrator_composes_in_pipeline() {
let (worker, _) = review_worker(false);
let review = ReviewOrchestrator::new(worker, Arc::new(ReviewChecker), 3);
let pipe = SequentialPipeline::new(vec![Arc::new(review), mock_orch("tail")]);
let ctx = RunContext::new("r5");
let out = pipe
.run_with_context(AgentTask::new("研究X"), &ctx)
.await
.unwrap();
assert_eq!(out, "tail:good answer:r5");
}
#[test]
fn test_parse_review_verdict_json() {
assert_eq!(
parse_review_verdict(r#"{"passed": true}"#),
Some(ReviewVerdict::pass())
);
assert_eq!(
parse_review_verdict(r#"{"passed": false, "feedback": "缺引用"}"#),
Some(ReviewVerdict::fail("缺引用"))
);
}
#[test]
fn test_parse_review_verdict_delimited() {
let v = parse_review_verdict(
"<<<VERDICT>>>FAIL<<<END_VERDICT>>>\n<<<FEEDBACK>>>请补充细节<<<END_FEEDBACK>>>",
)
.unwrap();
assert!(!v.passed);
assert_eq!(v.feedback, "请补充细节");
let p = parse_review_verdict("<<<VERDICT>>>PASS<<<END_VERDICT>>>").unwrap();
assert!(p.passed);
assert!(p.feedback.is_empty());
}
#[test]
fn test_parse_review_verdict_plain_text() {
let p = parse_review_verdict("PASS").unwrap();
assert!(p.passed);
let f = parse_review_verdict("FAIL: 引用不足").unwrap();
assert!(!f.passed);
assert_eq!(f.feedback, "引用不足");
}
#[test]
fn test_parse_review_verdict_invalid() {
assert!(parse_review_verdict("whatever").is_none());
}
}