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}