Skip to main content

pawan/agent/
preflight.rs

1use super::{Message, Role, ToolDefinition};
2
3impl super::PawanAgent {
4    /// Pre-flight health check: verify the LLM backend is reachable before starting work.
5    /// Sends a minimal "ping" message. Returns Ok(()) if the model responds.
6    pub async fn preflight_check(&self) -> crate::Result<()> {
7        let test = vec![Message {
8            role: Role::User,
9            content: "ping".into(),
10            tool_calls: vec![],
11            tool_result: None,
12        }];
13        let tools: Vec<ToolDefinition> = vec![];
14        match self.backend.generate(&test, &tools, None).await {
15            Ok(_) => Ok(()),
16            Err(e) => Err(crate::PawanError::Llm(format!("Model unreachable: {}", e))),
17        }
18    }
19}
20
21#[cfg(test)]
22mod tests {
23    use super::*;
24    use crate::agent::backend::mock::MockBackend;
25    use crate::agent::backend::LlmBackend;
26    use crate::agent::{LLMResponse, PawanAgent, TokenCallback};
27    use crate::config::PawanConfig;
28    use async_trait::async_trait;
29    use std::path::PathBuf;
30
31    struct AlwaysFailBackend;
32
33    #[async_trait]
34    impl LlmBackend for AlwaysFailBackend {
35        async fn generate(
36            &self,
37            _messages: &[Message],
38            _tools: &[ToolDefinition],
39            _on_token: Option<&TokenCallback>,
40        ) -> crate::Result<LLMResponse> {
41            Err(crate::PawanError::Llm("connection refused".into()))
42        }
43    }
44
45    fn agent_with_backend(backend: Box<dyn LlmBackend>) -> PawanAgent {
46        PawanAgent::new(PawanConfig::default(), PathBuf::from("."))
47            .with_backend(backend)
48    }
49
50    #[tokio::test]
51    async fn preflight_check_ok_when_backend_responds() {
52        let agent = agent_with_backend(Box::new(MockBackend::with_text("pong")));
53        assert!(agent.preflight_check().await.is_ok());
54    }
55
56    #[tokio::test]
57    async fn preflight_check_ok_when_backend_returns_empty_text() {
58        let agent = agent_with_backend(Box::new(MockBackend::with_text("")));
59        assert!(agent.preflight_check().await.is_ok());
60    }
61
62    #[tokio::test]
63    async fn preflight_check_errors_when_backend_fails() {
64        let agent = agent_with_backend(Box::new(AlwaysFailBackend));
65        let err = agent
66            .preflight_check()
67            .await
68            .expect_err("failing backend must bubble out");
69        assert!(
70            matches!(err, crate::PawanError::Llm(_)),
71            "expected Llm error variant, got {err:?}"
72        );
73    }
74
75    #[tokio::test]
76    async fn preflight_check_error_message_mentions_unreachable() {
77        let agent = agent_with_backend(Box::new(AlwaysFailBackend));
78        let err = agent.preflight_check().await.unwrap_err();
79        let msg = err.to_string();
80        assert!(
81            msg.contains("Model unreachable"),
82            "error should be wrapped with 'Model unreachable', got: {msg}"
83        );
84    }
85
86    #[tokio::test]
87    async fn preflight_check_does_not_mutate_agent_history() {
88        let agent = agent_with_backend(Box::new(MockBackend::with_text("pong")));
89        let before = agent.history().len();
90        agent.preflight_check().await.unwrap();
91        assert_eq!(
92            agent.history().len(),
93            before,
94            "preflight must not persist the ping message into agent history"
95        );
96    }
97}