Skip to main content

algocline_app/service/
run.rs

1use std::sync::Arc;
2
3use algocline_core::QueryId;
4use algocline_engine::{FeedResult, VariantPkg};
5
6use super::eval_store::splice_response_string;
7use super::resolve::{is_package_installed, make_require_code, resolve_code, QueryResponse};
8use super::transcript::write_transcript_log;
9use super::AppService;
10
11/// Splice `save_warning` into the JSON `result` when the optional
12/// warning is `Some(_)`. Returns the original string unchanged when
13/// there is no warning.
14fn splice_save_warning(result_json: &str, warning: Option<String>) -> String {
15    match warning {
16        Some(msg) => splice_response_string(result_json, "save_warning", &msg),
17        None => result_json.to_string(),
18    }
19}
20
21impl AppService {
22    /// Execute Lua code with optional JSON context.
23    ///
24    /// `project_root` — optional absolute path to the project root containing
25    /// `alc.lock`. Falls back to `ALC_PROJECT_ROOT` env or ancestor walk.
26    pub async fn run(
27        &self,
28        code: Option<String>,
29        code_file: Option<String>,
30        ctx: Option<serde_json::Value>,
31        project_root: Option<String>,
32    ) -> Result<String, String> {
33        let code = resolve_code(code, code_file)?;
34        let ctx = ctx.unwrap_or(serde_json::Value::Null);
35        let extra = self.resolve_extra_lib_paths(project_root.as_deref());
36        let variants = self.resolve_variant_pkgs(project_root.as_deref());
37        self.start_and_tick(code, ctx, None, extra, variants).await
38    }
39
40    /// Apply a built-in strategy to a task.
41    ///
42    /// If the requested package is not installed, automatically installs the
43    /// bundled package collection from GitHub before executing.
44    ///
45    /// `project_root` — optional absolute path to the project root containing
46    /// `alc.lock`. Falls back to `ALC_PROJECT_ROOT` env or ancestor walk.
47    pub async fn advice(
48        &self,
49        strategy: &str,
50        task: Option<String>,
51        opts: Option<serde_json::Value>,
52        project_root: Option<String>,
53    ) -> Result<String, String> {
54        // Auto-install bundled packages if the requested strategy is missing
55        let app_dir = self.log_config.app_dir();
56        if !is_package_installed(&app_dir, strategy) {
57            self.auto_install_bundled_packages().await?;
58            if !is_package_installed(&app_dir, strategy) {
59                return Err(format!(
60                    "Package '{strategy}' not found after installing bundled collection. \
61                     Use alc_pkg_install to install it manually."
62                ));
63            }
64        }
65
66        let code = make_require_code(strategy);
67
68        let mut ctx_map = match opts {
69            Some(serde_json::Value::Object(m)) => m,
70            _ => serde_json::Map::new(),
71        };
72        if let Some(task) = task {
73            ctx_map.insert("task".into(), serde_json::Value::String(task));
74        }
75        let ctx = serde_json::Value::Object(ctx_map);
76
77        let extra = self.resolve_extra_lib_paths(project_root.as_deref());
78        let variants = self.resolve_variant_pkgs(project_root.as_deref());
79        self.start_and_tick(code, ctx, Some(strategy), extra, variants)
80            .await
81    }
82
83    /// Continue a paused execution — batch feed.
84    pub async fn continue_batch(
85        &self,
86        session_id: &str,
87        responses: Vec<QueryResponse>,
88    ) -> Result<String, String> {
89        let mut last_result = None;
90        for qr in responses {
91            let qid = QueryId::parse(&qr.query_id);
92            let result = self
93                .registry
94                .feed_response(session_id, &qid, qr.response, qr.usage.as_ref())
95                .await
96                .map_err(|e| format!("Continue failed: {e}"))?;
97            last_result = Some(result);
98        }
99        let result = last_result.ok_or("Empty responses array")?;
100        self.maybe_log_transcript(&result, session_id);
101        let json = result.to_json(session_id).to_string();
102        let save_warning = self.maybe_save_eval(&result, session_id, &json);
103        Ok(splice_save_warning(&json, save_warning))
104    }
105
106    /// Continue a paused execution — single response (with optional query_id).
107    pub async fn continue_single(
108        &self,
109        session_id: &str,
110        response: String,
111        query_id: Option<&str>,
112        usage: Option<algocline_core::TokenUsage>,
113    ) -> Result<String, String> {
114        let query_id = match query_id {
115            Some(qid) => QueryId::parse(qid),
116            None => self
117                .registry
118                .resolve_sole_pending_id(session_id)
119                .await
120                .map_err(|e| format!("Continue failed: {e}"))?,
121        };
122
123        let result = self
124            .registry
125            .feed_response(session_id, &query_id, response, usage.as_ref())
126            .await
127            .map_err(|e| format!("Continue failed: {e}"))?;
128
129        self.maybe_log_transcript(&result, session_id);
130        let json = result.to_json(session_id).to_string();
131        let save_warning = self.maybe_save_eval(&result, session_id, &json);
132        Ok(splice_save_warning(&json, save_warning))
133    }
134
135    // ─── Internal ───────────────────────────────────────────────
136
137    pub(super) fn maybe_log_transcript(&self, result: &FeedResult, session_id: &str) {
138        if let FeedResult::Finished(exec_result) = result {
139            let strategy = self
140                .session_strategies
141                .lock()
142                .ok()
143                .and_then(|mut map| map.remove(session_id));
144            write_transcript_log(
145                &self.log_config,
146                session_id,
147                &exec_result.metrics,
148                strategy.as_deref(),
149            );
150        }
151    }
152
153    /// Persist eval result for a finished session, returning any storage
154    /// failure as `Some(msg)` so the caller can surface it on the wire
155    /// response. `None` covers both "not an eval session" and
156    /// "successfully saved" — they are indistinguishable to the caller
157    /// because both produce the same wire shape.
158    pub(super) fn maybe_save_eval(
159        &self,
160        result: &FeedResult,
161        session_id: &str,
162        result_json: &str,
163    ) -> Option<String> {
164        if !matches!(result, FeedResult::Finished(_)) {
165            return None;
166        }
167        let strategy = {
168            let mut map = self.eval_sessions.lock().unwrap_or_else(|e| e.into_inner());
169            map.remove(session_id)
170        };
171        strategy.and_then(|s| {
172            super::eval_store::save_eval_result(&self.log_config.app_dir(), &s, result_json).err()
173        })
174    }
175
176    pub(super) async fn start_and_tick(
177        &self,
178        code: String,
179        ctx: serde_json::Value,
180        strategy: Option<&str>,
181        extra_lib_paths: Vec<std::path::PathBuf>,
182        variant_pkgs: Vec<VariantPkg>,
183    ) -> Result<String, String> {
184        let scenarios_dir = self.log_config.app_dir().scenarios_dir();
185        let session = self
186            .executor
187            .start_session(
188                code,
189                ctx,
190                extra_lib_paths,
191                variant_pkgs,
192                Arc::clone(&self.state_store),
193                Arc::clone(&self.card_store),
194                scenarios_dir,
195            )
196            .await?;
197        let (session_id, result) = self
198            .registry
199            .start_execution(session)
200            .await
201            .map_err(|e| format!("Execution failed: {e}"))?;
202        if let Some(s) = strategy {
203            if let Ok(mut map) = self.session_strategies.lock() {
204                map.insert(session_id.clone(), s.to_string());
205            }
206        }
207        self.maybe_log_transcript(&result, &session_id);
208        Ok(result.to_json(&session_id).to_string())
209    }
210}