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(".")).with_backend(backend)
47    }
48
49    #[tokio::test]
50    async fn preflight_check_ok_when_backend_responds() {
51        let agent = agent_with_backend(Box::new(MockBackend::with_text("pong")));
52        assert!(agent.preflight_check().await.is_ok());
53    }
54
55    #[tokio::test]
56    async fn preflight_check_ok_when_backend_returns_empty_text() {
57        let agent = agent_with_backend(Box::new(MockBackend::with_text("")));
58        assert!(agent.preflight_check().await.is_ok());
59    }
60
61    #[tokio::test]
62    async fn preflight_check_errors_when_backend_fails() {
63        let agent = agent_with_backend(Box::new(AlwaysFailBackend));
64        let err = agent
65            .preflight_check()
66            .await
67            .expect_err("failing backend must bubble out");
68        assert!(
69            matches!(err, crate::PawanError::Llm(_)),
70            "expected Llm error variant, got {err:?}"
71        );
72    }
73
74    #[tokio::test]
75    async fn preflight_check_error_message_mentions_unreachable() {
76        let agent = agent_with_backend(Box::new(AlwaysFailBackend));
77        let err = agent.preflight_check().await.unwrap_err();
78        let msg = err.to_string();
79        assert!(
80            msg.contains("Model unreachable"),
81            "error should be wrapped with 'Model unreachable', got: {msg}"
82        );
83    }
84
85    #[tokio::test]
86    async fn preflight_check_does_not_mutate_agent_history() {
87        let agent = agent_with_backend(Box::new(MockBackend::with_text("pong")));
88        let before = agent.history().len();
89        agent.preflight_check().await.unwrap();
90        assert_eq!(
91            agent.history().len(),
92            before,
93            "preflight must not persist the ping message into agent history"
94        );
95    }
96}