Skip to main content

algocline_core/
engine_api.rs

1use async_trait::async_trait;
2
3// ─── Parameter types (transport-independent) ─────────────────────
4
5/// A single query response in a batch feed.
6#[derive(Debug)]
7pub struct QueryResponse {
8    /// Query ID (e.g. "q-0", "q-1").
9    pub query_id: String,
10    /// The host LLM's response for this query.
11    pub response: String,
12    /// Token usage reported by the host for this query.
13    pub usage: Option<crate::TokenUsage>,
14}
15
16// ─── Engine API trait ────────────────────────────────────────────
17
18/// Transport-independent API for the algocline engine.
19///
20/// Abstracts the full public surface of AppService so that callers
21/// (MCP handler, future daemon client, etc.) can operate through
22/// `Arc<dyn EngineApi>` without depending on the concrete implementation.
23///
24/// All methods are async to support both local (in-process) and remote
25/// (socket/HTTP) implementations uniformly.
26#[async_trait]
27pub trait EngineApi: Send + Sync {
28    // ─── Core execution ──────────────────────────────────────
29
30    /// Execute Lua code with optional JSON context.
31    async fn run(
32        &self,
33        code: Option<String>,
34        code_file: Option<String>,
35        ctx: Option<serde_json::Value>,
36        project_root: Option<String>,
37    ) -> Result<String, String>;
38
39    /// Apply an installed strategy package. Task is optional.
40    async fn advice(
41        &self,
42        strategy: &str,
43        task: Option<String>,
44        opts: Option<serde_json::Value>,
45        project_root: Option<String>,
46    ) -> Result<String, String>;
47
48    /// Continue a paused execution — single response (with optional query_id).
49    async fn continue_single(
50        &self,
51        session_id: &str,
52        response: String,
53        query_id: Option<&str>,
54        usage: Option<crate::TokenUsage>,
55    ) -> Result<String, String>;
56
57    /// Continue a paused execution — batch feed.
58    async fn continue_batch(
59        &self,
60        session_id: &str,
61        responses: Vec<QueryResponse>,
62    ) -> Result<String, String>;
63
64    // ─── Session status ──────────────────────────────────────
65
66    /// Query active session status.
67    ///
68    /// `pending_filter` is a free-form JSON value forwarded from MCP
69    /// callers, decoded inside the app layer into either a preset name
70    /// (`"meta"` / `"preview"` / `"full"`) or a custom field-filter
71    /// object. `None` keeps the legacy count-only snapshot.
72    async fn status(
73        &self,
74        session_id: Option<&str>,
75        pending_filter: Option<serde_json::Value>,
76    ) -> Result<String, String>;
77
78    // ─── Evaluation ──────────────────────────────────────────
79
80    /// Run an evalframe evaluation suite.
81    ///
82    /// `auto_card`: when true, emit an immutable Card
83    /// (`~/.algocline/cards/{strategy}/{card_id}.toml`) summarizing the run.
84    async fn eval(
85        &self,
86        scenario: Option<String>,
87        scenario_file: Option<String>,
88        scenario_name: Option<String>,
89        strategy: &str,
90        strategy_opts: Option<serde_json::Value>,
91        auto_card: bool,
92    ) -> Result<String, String>;
93
94    /// List eval history, optionally filtered by strategy.
95    async fn eval_history(&self, strategy: Option<&str>, limit: usize) -> Result<String, String>;
96
97    /// View a specific eval result by ID.
98    async fn eval_detail(&self, eval_id: &str) -> Result<String, String>;
99
100    /// Compare two eval results with statistical significance testing.
101    async fn eval_compare(&self, eval_id_a: &str, eval_id_b: &str) -> Result<String, String>;
102
103    // ─── Scenarios ───────────────────────────────────────────
104
105    /// List available scenarios.
106    async fn scenario_list(&self) -> Result<String, String>;
107
108    /// Show the content of a named scenario.
109    async fn scenario_show(&self, name: &str) -> Result<String, String>;
110
111    /// Install scenarios from a Git URL or local path.
112    async fn scenario_install(&self, url: String) -> Result<String, String>;
113
114    // ─── Packages ────────────────────────────────────────────
115
116    /// Link a local directory as a project-local package (symlink to cache).
117    ///
118    /// Scope selection:
119    /// - `scope = None` or `Some("global")` — symlink into `~/.algocline/packages/`
120    ///   (visible to all projects).
121    /// - `scope = Some("variant")` — record the path in `alc.local.toml`
122    ///   at the project root (worktree-scoped override, git-ignored). No
123    ///   symlink is created.
124    /// - Any other value → `Err("invalid scope: ...")`.
125    ///
126    /// `project_root` is only consulted when `scope = Some("variant")`.
127    /// If `None`, falls back to `ALC_PROJECT_ROOT` env or ancestor walk
128    /// from cwd.
129    async fn pkg_link(
130        &self,
131        path: String,
132        name: Option<String>,
133        force: Option<bool>,
134        scope: Option<String>,
135        project_root: Option<String>,
136    ) -> Result<String, String>;
137
138    /// List installed packages with metadata.
139    ///
140    /// When `project_root` is provided, project-local packages from `alc.toml`/`alc.lock`
141    /// are included with `scope: "project"`. Global packages carry `scope: "global"`.
142    ///
143    /// Mirrors the list-tool knob contract used by [`Self::hub_search`]
144    /// (plan.md §4.1). Parameters are individual JSON-primitive
145    /// `Option<T>` values so the `algocline-core` crate stays free of
146    /// `algocline-app`-internal types; the impl folds them into its
147    /// `pub(crate) ListOpts` struct.
148    ///
149    /// - `limit` is `Option<i32>` at this layer (MCP/JSON boundary).
150    ///   The impl clamps negative values to 0 and casts to `usize`.
151    ///   `Some(0)` (and thus clamped negatives) means **no limit**
152    ///   (return all entries — empty-means-all idiom); `None` falls
153    ///   back to the tool's default cap.
154    /// - `filter` is a free-form JSON object; it is `Deserialize`d into
155    ///   a `HashMap<String, Value>` inside the app layer. Non-object
156    ///   values are logged via `tracing::warn` and treated as no filter.
157    /// - `fields` / `verbose` drive projection on each entry of the
158    ///   `packages` array; `fields` wins when both are supplied.
159    /// - Top-level keys (`packages`, `search_paths`, `project_root`,
160    ///   `lockfile_path`) are never projected away.
161    #[allow(clippy::too_many_arguments)]
162    async fn pkg_list(
163        &self,
164        project_root: Option<String>,
165        limit: Option<i32>,
166        sort: Option<String>,
167        filter: Option<serde_json::Value>,
168        fields: Option<Vec<String>>,
169        verbose: Option<String>,
170    ) -> Result<String, String>;
171
172    /// Install a package from a Git URL or local path.
173    async fn pkg_install(&self, url: String, name: Option<String>) -> Result<String, String>;
174
175    /// Remove a symlinked package from `~/.algocline/packages/`.
176    ///
177    /// Only removes symlinks; for installed (copied) packages, use `pkg_remove`.
178    async fn pkg_unlink(&self, name: String) -> Result<String, String>;
179
180    /// Remove a package declaration from `alc.toml` and `alc.lock`.
181    ///
182    /// Requires an `alc.toml` to be found (via `project_root` or ancestor walk).
183    /// Does NOT delete physical files from `~/.algocline/packages/`.
184    async fn pkg_remove(
185        &self,
186        name: &str,
187        project_root: Option<String>,
188        version: Option<String>,
189    ) -> Result<String, String>;
190
191    /// Heal broken package state by reinstalling entries whose installed
192    /// directory is missing. Other broken kinds (dangling symlink,
193    /// declared-path missing) are surfaced as `unrepairable` with a
194    /// suggested remediation.
195    async fn pkg_repair(
196        &self,
197        name: Option<String>,
198        project_root: Option<String>,
199    ) -> Result<String, String>;
200
201    // ─── Logging ─────────────────────────────────────────────
202
203    /// Append a note to a session's log file.
204    async fn add_note(
205        &self,
206        session_id: &str,
207        content: &str,
208        title: Option<&str>,
209    ) -> Result<String, String>;
210
211    /// View session logs.
212    async fn log_view(
213        &self,
214        session_id: Option<&str>,
215        limit: Option<usize>,
216        max_chars: Option<usize>,
217    ) -> Result<String, String>;
218
219    /// Aggregate stats across all logged sessions.
220    async fn stats(
221        &self,
222        strategy_filter: Option<&str>,
223        days: Option<u64>,
224    ) -> Result<String, String>;
225
226    // ─── Project lifecycle ────────────────────────────────────
227
228    /// Initialize `alc.toml` in the given project root.
229    ///
230    /// Creates a minimal `alc.toml` (`[packages]` section only).
231    /// Fails if `alc.toml` already exists (no overwrite).
232    async fn init(&self, project_root: Option<String>) -> Result<String, String>;
233
234    /// Re-resolve all `alc.toml` entries and rewrite `alc.lock`.
235    ///
236    /// Requires an `alc.toml` to be present. Returns resolved count and errors.
237    async fn update(&self, project_root: Option<String>) -> Result<String, String>;
238
239    /// Migrate a legacy `alc.lock` to `alc.toml` + new `alc.lock` format.
240    ///
241    /// Detects legacy format via `linked_at` / `local_dir` fields.
242    /// Backs up the old lock file as `alc.lock.bak`.
243    async fn migrate(&self, project_root: Option<String>) -> Result<String, String>;
244
245    // ─── Cards ───────────────────────────────────────────────
246
247    /// List Card summaries, optionally filtered by pkg.
248    async fn card_list(&self, pkg: Option<String>) -> Result<String, String>;
249
250    /// Fetch a full Card by id.
251    async fn card_get(&self, card_id: &str) -> Result<String, String>;
252
253    /// Filter/sort Cards using the Prisma-style `where` DSL.
254    ///
255    /// - `pkg`: restricts filesystem scan to a single pkg subdir (I/O hint).
256    /// - `where_`: nested-object predicate (see `card::parse_where`).
257    /// - `order_by`: array of dotted-path sort keys; `-` prefix = desc.
258    /// - `limit` / `offset`: pagination.
259    async fn card_find(
260        &self,
261        pkg: Option<String>,
262        where_: Option<serde_json::Value>,
263        order_by: Option<serde_json::Value>,
264        limit: Option<usize>,
265        offset: Option<usize>,
266    ) -> Result<String, String>;
267
268    /// List aliases, optionally filtered by pkg.
269    async fn card_alias_list(&self, pkg: Option<String>) -> Result<String, String>;
270
271    /// Resolve an alias name to its bound Card and return the full Card JSON.
272    async fn card_get_by_alias(&self, name: &str) -> Result<String, String>;
273
274    /// Bind (or rebind) an alias to a Card.
275    async fn card_alias_set(
276        &self,
277        name: &str,
278        card_id: &str,
279        pkg: Option<String>,
280        note: Option<String>,
281    ) -> Result<String, String>;
282
283    /// Append new top-level fields to an existing Card (additive-only).
284    async fn card_append(&self, card_id: &str, fields: serde_json::Value)
285        -> Result<String, String>;
286
287    /// Install Cards from a Card Collection repo (Git URL or local path).
288    async fn card_install(&self, url: String) -> Result<String, String>;
289
290    /// Read per-case samples from a Card's sidecar JSONL file.
291    ///
292    /// `where_` applies the same Prisma-style DSL used by `card_find`
293    /// to each sample row; offset/limit page the post-filter stream.
294    async fn card_samples(
295        &self,
296        card_id: &str,
297        offset: Option<usize>,
298        limit: Option<usize>,
299        where_: Option<serde_json::Value>,
300    ) -> Result<String, String>;
301
302    /// Walk a Card's lineage tree via `metadata.prior_card_id`.
303    ///
304    /// - `direction`: `"up"` | `"down"` | `"both"` (default `"up"`).
305    /// - `depth`: max traversal depth (default 10).
306    /// - `include_stats`: include each node's `[stats]` section.
307    /// - `relation_filter`: optional list of accepted `prior_relation` values.
308    async fn card_lineage(
309        &self,
310        card_id: &str,
311        direction: Option<String>,
312        depth: Option<usize>,
313        include_stats: Option<bool>,
314        relation_filter: Option<Vec<String>>,
315    ) -> Result<String, String>;
316
317    /// Backfill one subscriber (`sink` URI) with all cards from the
318    /// primary store. Drift-safe: cards already present on the sink are
319    /// skipped (never overwritten). Returns a `SinkBackfillReport`
320    /// serialized as a JSON string.
321    async fn card_sink_backfill(&self, _sink: String, _dry_run: bool) -> Result<String, String> {
322        Err("card_sink_backfill: not implemented by this EngineApi impl".into())
323    }
324
325    // ─── Hub ─────────────────────────────────────────────────
326
327    /// Rebuild hub index from a packages directory.
328    ///
329    /// When `source_dir` is provided, scans that directory directly
330    /// (pure metadata, no manifest).  When omitted, scans `~/.algocline/packages/`.
331    async fn hub_reindex(
332        &self,
333        output_path: Option<String>,
334        source_dir: Option<String>,
335    ) -> Result<String, String>;
336
337    /// Show detailed information for a single package.
338    async fn hub_info(&self, pkg: String) -> Result<String, String>;
339
340    /// Search packages across remote index + local install state.
341    ///
342    /// This trait method mirrors the MCP `alc_hub_search` tool. Parameters
343    /// are deliberately individual JSON-primitive `Option<T>` values
344    /// (rather than an aggregate struct) so that the `algocline-core` crate
345    /// stays free of `algocline-app`-internal types (see plan.md §4.1).
346    /// The `algocline-app` side of the impl folds these into its
347    /// `pub(crate) ListOpts` struct.
348    ///
349    /// - `limit` is `Option<i32>` at this layer (MCP/JSON boundary). The
350    ///   impl casts to `usize` internally.
351    /// - `filter` is a free-form JSON object; it is `Deserialize`d into
352    ///   a `HashMap<String, Value>` inside the app layer.
353    /// - `fields` / `verbose` drive projection; `fields` wins when both
354    ///   are supplied.
355    #[allow(clippy::too_many_arguments)]
356    async fn hub_search(
357        &self,
358        query: Option<String>,
359        category: Option<String>,
360        installed_only: Option<bool>,
361        limit: Option<i32>,
362        sort: Option<String>,
363        filter: Option<serde_json::Value>,
364        fields: Option<Vec<String>>,
365        verbose: Option<String>,
366    ) -> Result<String, String>;
367
368    // ─── Diagnostics ─────────────────────────────────────────
369
370    /// Show server configuration and diagnostic info.
371    async fn info(&self) -> String;
372}