Skip to main content

forge_workspace/mcp/systemone/
facade.rs

1//! `SystemOneFacade` - the seam between the systemone MCP tools and the
2//! boot-built decision client. Production holds the client directly
3//! (asking needs nothing else of the workspace); the mock records calls
4//! and answers canned outcomes for tool tests.
5
6use std::sync::Arc;
7
8use forge_system_one::{AskOutcome, Question, SystemOneClient, SystemOneError};
9
10/// The systemone tools' view: one question, one typed answer.
11#[async_trait::async_trait]
12pub(crate) trait SystemOneFacade: Send + Sync {
13    async fn ask(
14        &self,
15        state: serde_json::Value,
16        question: Question,
17    ) -> Result<AskOutcome, SystemOneError>;
18}
19
20/// Production facade: straight to the workspace's configured client.
21pub(crate) struct ProdSystemOneFacade {
22    client: Arc<SystemOneClient>,
23}
24
25impl ProdSystemOneFacade {
26    pub(crate) fn new(client: Arc<SystemOneClient>) -> Self {
27        Self { client }
28    }
29
30    pub(crate) fn into_arc(self) -> Arc<dyn SystemOneFacade> {
31        Arc::new(self)
32    }
33}
34
35#[async_trait::async_trait]
36impl SystemOneFacade for ProdSystemOneFacade {
37    async fn ask(
38        &self,
39        state: serde_json::Value,
40        question: Question,
41    ) -> Result<AskOutcome, SystemOneError> {
42        self.client.ask(&state, &question).await
43    }
44}
45
46#[cfg(test)]
47#[derive(Default)]
48pub(crate) struct MockSystemOneFacade {
49    pub result: parking_lot::Mutex<Option<Result<AskOutcome, SystemOneError>>>,
50    pub calls: parking_lot::Mutex<Vec<(serde_json::Value, Question)>>,
51}
52
53#[cfg(test)]
54impl MockSystemOneFacade {
55    pub(crate) fn new() -> Self {
56        Self::default()
57    }
58
59    pub(crate) fn into_arc(self) -> Arc<dyn SystemOneFacade> {
60        Arc::new(self)
61    }
62}
63
64#[cfg(test)]
65#[async_trait::async_trait]
66impl SystemOneFacade for MockSystemOneFacade {
67    async fn ask(
68        &self,
69        state: serde_json::Value,
70        question: Question,
71    ) -> Result<AskOutcome, SystemOneError> {
72        self.calls.lock().push((state, question));
73        self.result.lock().clone().unwrap_or_else(default_outcome)
74    }
75}
76
77#[cfg(test)]
78fn default_outcome() -> Result<AskOutcome, SystemOneError> {
79    Ok(AskOutcome {
80        model: "test-model".to_owned(),
81        answer: forge_system_one::Answer::Noul { noul: 0.5 },
82        usage: Some(forge_system_one::Usage { input_tokens: 1, output_tokens: 1, cost: None }),
83    })
84}