Skip to main content

algocline_app/service/
status.rs

1use algocline_engine::PendingFilter;
2
3use super::AppService;
4
5impl AppService {
6    /// Snapshot of all active sessions for external observation.
7    ///
8    /// Returns JSON with session status, metrics, progress, and strategy name.
9    /// Only includes sessions currently held in the registry (paused, awaiting
10    /// host LLM responses). Completed sessions are not listed here — use
11    /// `alc_log_view` for historical data.
12    ///
13    /// `pending_filter` resolution:
14    /// - `None`  → legacy snapshot (count-only `pending_queries: N`).
15    /// - `Some(String)` → preset name: `"meta"` / `"preview"` / `"full"`.
16    ///   Unknown names return `Err` (typo protection).
17    /// - `Some(Object)` → free-form `PendingFilter` custom filter.
18    /// - Any other JSON shape → `Err`.
19    ///
20    /// The `preview` preset reads the char limit from
21    /// [`AppConfig::prompt_preview_chars`] (env `ALC_PROMPT_PREVIEW_CHARS`).
22    /// A custom filter that specifies its own `prompt: { mode: "preview",
23    /// chars: N }` wins outright — the per-request value is not clamped
24    /// by the env setting.
25    pub async fn status(
26        &self,
27        session_id: Option<&str>,
28        pending_filter: Option<serde_json::Value>,
29    ) -> Result<String, String> {
30        let filter = self.resolve_pending_filter(pending_filter)?;
31        let snapshots = self.registry.list_snapshots(filter.as_ref()).await;
32
33        // If a specific session requested, return just that one
34        if let Some(sid) = session_id {
35            return match snapshots.get(sid) {
36                Some(snapshot) => {
37                    let mut result = snapshot.clone();
38                    // Enrich with strategy name
39                    if let Ok(strategies) = self.session_strategies.lock() {
40                        if let Some(name) = strategies.get(sid) {
41                            result["strategy"] = serde_json::json!(name);
42                        }
43                    }
44                    result["session_id"] = serde_json::json!(sid);
45                    serde_json::to_string_pretty(&result).map_err(|e| e.to_string())
46                }
47                None => Err(format!("session '{sid}' not found (may have completed)")),
48            };
49        }
50
51        // List all active sessions
52        if snapshots.is_empty() {
53            return Ok(serde_json::json!({
54                "active_sessions": 0,
55                "sessions": [],
56            })
57            .to_string());
58        }
59
60        let strategies = self.session_strategies.lock().ok();
61        let sessions: Vec<serde_json::Value> = snapshots
62            .into_iter()
63            .map(|(id, mut snapshot)| {
64                if let Some(ref strats) = strategies {
65                    if let Some(name) = strats.get(&id) {
66                        snapshot["strategy"] = serde_json::json!(name);
67                    }
68                }
69                snapshot["session_id"] = serde_json::json!(id);
70                snapshot
71            })
72            .collect();
73
74        let result = serde_json::json!({
75            "active_sessions": sessions.len(),
76            "sessions": sessions,
77        });
78
79        serde_json::to_string_pretty(&result).map_err(|e| e.to_string())
80    }
81
82    /// Decode the incoming `pending_filter` JSON value into an optional
83    /// `PendingFilter`. Preset strings read the per-request char count
84    /// from this service's `AppConfig`; custom objects use the values
85    /// declared by the caller.
86    fn resolve_pending_filter(
87        &self,
88        raw: Option<serde_json::Value>,
89    ) -> Result<Option<PendingFilter>, String> {
90        let Some(value) = raw else {
91            return Ok(None);
92        };
93        match value {
94            serde_json::Value::String(name) => PendingFilter::from_preset_with(
95                &name,
96                self.log_config.prompt_preview_chars,
97            )
98            .map(Some)
99            .ok_or_else(|| {
100                format!(
101                    "unknown pending_filter preset '{name}' (valid: \"meta\" | \"preview\" | \"full\")"
102                )
103            }),
104            serde_json::Value::Object(_) => serde_json::from_value::<PendingFilter>(value)
105                .map(Some)
106                .map_err(|e| format!("invalid pending_filter object: {e}")),
107            other => Err(format!(
108                "pending_filter must be a preset name (string) or filter object, got {}",
109                match other {
110                    serde_json::Value::Null => "null",
111                    serde_json::Value::Bool(_) => "bool",
112                    serde_json::Value::Number(_) => "number",
113                    serde_json::Value::Array(_) => "array",
114                    _ => "unknown",
115                }
116            )),
117        }
118    }
119}