lc-testkit 0.17.0

Testing harness for langchainrust — record real LLM exchanges and replay them offline
Documentation
//! A5:agent 级离线录播——真 `FunctionCallingAgent` + 真 `Calculator` 工具 +
//! `ReplayProvider`,零网络跑通工具调用循环。
//!
//! 这是 lc-testkit 二期新能力的真实消费方:
//! - 工具录制(A2 `RecordingProvider::bind_tools`)录下请求侧 `tools`;
//! - 回放侧(A3 `ReplayProvider::bind_tools`)让 agent"绑了工具的循环"前提成立;
//! - 两轮 FIFO 回放把「请求工具调用 → 工具结果喂回 → 最终答案」放回,
//!   断言 ToolStart/ToolEnd 事件序列与最终答案落地。

use std::sync::Arc;

use futures_util::StreamExt;
use lc_agents::{AgentExecutor, AgentStreamEvent, FunctionCallingAgent};
use lc_core::language_models::LLMResult;
use lc_core::tools::ToolCall;
use lc_testkit::{RecordedExchange, ReplayProvider};
use lc_tools::Calculator;

#[tokio::test]
async fn agent_offline_replays_tool_call_loop() {
    // 1. 手写确定性录播(等价 fixture):
    //    - 第一轮:模型请求调用 calculator(响应带 tool_calls)
    //    - 第二轮:工具结果喂回后,模型给出最终答案
    let exchanges = vec![
        RecordedExchange {
            messages: vec![],
            response: LLMResult {
                content: String::new(),
                model: "replay".to_string(),
                tool_calls: Some(vec![ToolCall::builder("call_1")
                    .name("calculator")
                    .arguments("{\"expression\":\"2+2\"}".to_string())
                    .build()]),
                ..Default::default()
            },
            tools: None,
        },
        RecordedExchange {
            messages: vec![],
            response: LLMResult {
                content: "4".to_string(),
                model: "replay".to_string(),
                ..Default::default()
            },
            tools: None,
        },
    ];
    let replay = ReplayProvider::from_exchanges(exchanges);

    // 2. 真 agent(带真 Calculator 工具)+ 真 executor
    let agent = FunctionCallingAgent::new(replay, vec![Arc::new(Calculator::new())], None);
    let executor = AgentExecutor::new(Arc::new(agent), vec![Arc::new(Calculator::new())]);

    // 3. 流式执行:工具循环应发出 ToolStart → ToolEnd,最终以 FinalAnswer 收尾
    let mut stream = executor.stream("2+2=?".to_string());
    let mut events = Vec::new();
    while let Some(event) = stream.next().await {
        events.push(event.expect("回放 agent 执行不应失败"));
    }

    // 4. 断言:工具事件序列 + 最终答案来自第二轮回放
    assert!(events.len() >= 4, "事件数不足: {events:?}");
    assert!(
        matches!(&events[0], AgentStreamEvent::ToolStart { name, .. } if name == "calculator"),
        "第一个事件应为 calculator 的 ToolStart: {events:?}"
    );
    assert!(
        events
            .iter()
            .any(|e| matches!(e, AgentStreamEvent::ToolEnd { .. })),
        "应有 ToolEnd 事件: {events:?}"
    );
    match events.last().expect("有事件") {
        AgentStreamEvent::FinalAnswer { content } => {
            assert_eq!(content.trim(), "4", "最终答案应来自第二轮回放");
        }
        other => panic!("期望 FinalAnswer 终态,got {other:?}"),
    }
}