Skip to main content

lean_ctx/core/dashboard_pro/
wire.rs

1// SPDX-License-Identifier: Apache-2.0
2//! The engine's own copy of the `leanctx.dashboard-pro/1` wire shapes it
3//! exchanges with the licensed runtime. Field names match the runtime's
4//! contract one to one; the engine never links the runtime crate.
5
6use std::collections::BTreeMap;
7
8use serde::{Deserialize, Serialize};
9use serde_json::{Map, Value};
10
11pub(crate) const SCHEMA: &str = "leanctx.dashboard-pro/1";
12
13/// One computation request.
14#[derive(Debug, Clone, Default, Serialize, Deserialize)]
15pub(crate) struct Request {
16    pub(crate) schema: String,
17    pub(crate) op: String,
18    pub(crate) now_utc: String,
19    pub(crate) project: Project,
20    pub(crate) query: Map<String, Value>,
21    pub(crate) body: Value,
22    pub(crate) inputs: Map<String, Value>,
23}
24
25#[derive(Debug, Clone, Default, Serialize, Deserialize)]
26pub(crate) struct Project {
27    pub(crate) root_hash: String,
28    pub(crate) labels: Vec<ProjectLabel>,
29    pub(crate) repo_hashes: Vec<String>,
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub(crate) name: Option<String>,
32}
33
34#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
35pub(crate) struct ProjectLabel {
36    pub(crate) hash: String,
37    pub(crate) name: String,
38}
39
40/// The runtime's answer.
41#[derive(Debug, Clone, Default, Deserialize)]
42pub(crate) struct Response {
43    pub(crate) status: u16,
44    #[serde(default)]
45    pub(crate) body: Option<Value>,
46    #[serde(default)]
47    pub(crate) file: Option<FileOut>,
48    /// Kept untyped until each plan is checked: an unknown effect is a 409,
49    /// not an unreadable answer.
50    #[serde(default)]
51    pub(crate) plans: Option<Vec<Value>>,
52    #[serde(default)]
53    pub(crate) error: Option<ErrorOut>,
54}
55
56#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
57pub(crate) struct FileOut {
58    pub(crate) name: String,
59    pub(crate) mime: String,
60    pub(crate) bytes_b64: String,
61}
62
63#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
64pub(crate) struct ErrorOut {
65    pub(crate) code: String,
66    pub(crate) message: String,
67}
68
69/// A write the engine validates, executes and reads back.
70#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
71#[serde(tag = "effect", deny_unknown_fields)]
72pub(crate) enum Plan {
73    #[serde(rename = "alerts.settings")]
74    AlertsSettings { settings: AlertSettings },
75    #[serde(rename = "alerts.notify")]
76    AlertsNotify {
77        at: String,
78        text: String,
79        counts: BTreeMap<String, i64>,
80        test: bool,
81        desktop: bool,
82        webhook: String,
83    },
84    #[serde(rename = "profiles.put")]
85    ProfilesPut {
86        name: String,
87        values: Map<String, Value>,
88        saved_at: String,
89    },
90    #[serde(rename = "profiles.delete")]
91    ProfilesDelete { name: String },
92    #[serde(rename = "config.set")]
93    ConfigSet {
94        key: String,
95        wire: String,
96        expected: Value,
97        weakening: bool,
98    },
99    #[serde(rename = "memory.snapshot")]
100    MemorySnapshot { reason: String },
101    #[serde(rename = "memory.restore")]
102    MemoryRestore {
103        id: String,
104        missing: Vec<String>,
105        changed: Vec<String>,
106    },
107    #[serde(rename = "memory.forget")]
108    MemoryForget { refs: Vec<String>, reason: String },
109    #[serde(rename = "memory.consolidate")]
110    MemoryConsolidate { run: bool },
111    #[serde(rename = "context.overlay")]
112    ContextOverlay { action: String, path: String },
113    #[serde(rename = "adaptive.record")]
114    AdaptiveRecord { item: AppliedItem },
115    #[serde(rename = "adaptive.dismiss")]
116    AdaptiveDismiss { path: String },
117    #[serde(rename = "ledger.export")]
118    LedgerExport {
119        format: String,
120        file_name: String,
121        mime: String,
122        filters: LedgerFilters,
123        project_column: ProjectColumn,
124    },
125}
126
127impl Plan {
128    pub(crate) fn effect(&self) -> &'static str {
129        match self {
130            Self::AlertsSettings { .. } => "alerts.settings",
131            Self::AlertsNotify { .. } => "alerts.notify",
132            Self::ProfilesPut { .. } => "profiles.put",
133            Self::ProfilesDelete { .. } => "profiles.delete",
134            Self::ConfigSet { .. } => "config.set",
135            Self::MemorySnapshot { .. } => "memory.snapshot",
136            Self::MemoryRestore { .. } => "memory.restore",
137            Self::MemoryForget { .. } => "memory.forget",
138            Self::MemoryConsolidate { .. } => "memory.consolidate",
139            Self::ContextOverlay { .. } => "context.overlay",
140            Self::AdaptiveRecord { .. } => "adaptive.record",
141            Self::AdaptiveDismiss { .. } => "adaptive.dismiss",
142            Self::LedgerExport { .. } => "ledger.export",
143        }
144    }
145}
146
147/// The Ledger page's checked filters.
148#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
149pub(crate) struct LedgerFilters {
150    pub(crate) scope: Option<Vec<String>>,
151    pub(crate) repos: Option<RepoSelection>,
152    pub(crate) proxy: bool,
153    pub(crate) tool: String,
154    pub(crate) model: String,
155    pub(crate) text: String,
156    pub(crate) from: String,
157    pub(crate) to: String,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
161#[serde(tag = "kind", rename_all = "snake_case")]
162pub(crate) enum RepoSelection {
163    Only { hashes: Vec<String> },
164    Except { hashes: Vec<String> },
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
168#[serde(tag = "kind", rename_all = "snake_case")]
169pub(crate) enum ProjectColumn {
170    Fixed { name: String },
171    Labels { labels: Vec<ProjectLabel> },
172}
173
174/// Python-style tolerant readers of engine files.
175pub(crate) mod lenient {
176    use serde::{Deserialize, Deserializer};
177    use serde_json::Value;
178
179    pub(crate) fn text_of(value: &Value) -> Option<String> {
180        match value {
181            Value::Null => None,
182            Value::String(text) => Some(text.clone()),
183            Value::Bool(flag) => Some(if *flag { "True" } else { "False" }.to_string()),
184            Value::Number(number) => Some(number.to_string()),
185            other => Some(other.to_string()),
186        }
187    }
188
189    pub(crate) fn int_of(value: &Value) -> Option<i64> {
190        match value {
191            Value::Number(number) => number
192                .as_i64()
193                .or_else(|| {
194                    number
195                        .as_u64()
196                        .map(|n| i64::try_from(n).unwrap_or(i64::MAX))
197                })
198                .or_else(|| number.as_f64().map(|f| f.trunc() as i64)),
199            Value::Bool(flag) => Some(i64::from(*flag)),
200            Value::String(text) => text.trim().parse().ok(),
201            _ => None,
202        }
203    }
204
205    pub(crate) fn float_of(value: &Value) -> Option<f64> {
206        match value {
207            Value::Number(number) => number.as_f64(),
208            Value::Bool(flag) => Some(f64::from(u8::from(*flag))),
209            Value::String(text) => text.trim().parse().ok(),
210            _ => None,
211        }
212    }
213
214    pub(crate) fn opt_str<'de, D: Deserializer<'de>>(de: D) -> Result<Option<String>, D::Error> {
215        Ok(text_of(&Value::deserialize(de)?))
216    }
217
218    pub(crate) fn opt_i64<'de, D: Deserializer<'de>>(de: D) -> Result<Option<i64>, D::Error> {
219        Ok(int_of(&Value::deserialize(de)?))
220    }
221
222    pub(crate) fn opt_f64<'de, D: Deserializer<'de>>(de: D) -> Result<Option<f64>, D::Error> {
223        Ok(float_of(&Value::deserialize(de)?))
224    }
225}
226
227/// One line of the signed savings ledger, as far as the bridge reads it.
228#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
229pub struct LedgerEntry {
230    #[serde(default, deserialize_with = "lenient::opt_str")]
231    pub ts: Option<String>,
232    #[serde(default, deserialize_with = "lenient::opt_str")]
233    pub(crate) tool: Option<String>,
234    #[serde(default, deserialize_with = "lenient::opt_str")]
235    pub(crate) mechanism: Option<String>,
236    #[serde(default, deserialize_with = "lenient::opt_str")]
237    pub(crate) model_id: Option<String>,
238    #[serde(default, deserialize_with = "lenient::opt_str")]
239    pub(crate) version: Option<String>,
240    #[serde(default, deserialize_with = "lenient::opt_i64")]
241    pub baseline_tokens: Option<i64>,
242    #[serde(default, deserialize_with = "lenient::opt_i64")]
243    pub actual_tokens: Option<i64>,
244    #[serde(default, deserialize_with = "lenient::opt_i64")]
245    pub saved_tokens: Option<i64>,
246    #[serde(default, deserialize_with = "lenient::opt_i64")]
247    pub bounce_adjustment: Option<i64>,
248    #[serde(default, deserialize_with = "lenient::opt_f64")]
249    pub(crate) saved_usd: Option<f64>,
250    #[serde(default, deserialize_with = "lenient::opt_f64")]
251    pub(crate) unit_price_per_m_usd: Option<f64>,
252    #[serde(default, deserialize_with = "lenient::opt_i64")]
253    pub(crate) lines_added: Option<i64>,
254    #[serde(default, deserialize_with = "lenient::opt_i64")]
255    pub(crate) lines_removed: Option<i64>,
256    #[serde(default, deserialize_with = "lenient::opt_i64")]
257    pub(crate) loc_added: Option<i64>,
258    #[serde(default, deserialize_with = "lenient::opt_i64")]
259    pub(crate) loc_removed: Option<i64>,
260    #[serde(default, deserialize_with = "lenient::opt_str")]
261    pub measurement_method: Option<String>,
262    #[serde(default, deserialize_with = "lenient::opt_str")]
263    pub evidence_class: Option<String>,
264    /// Confidence `[0, 1]` of the savings measurement (engine metrics K7 range).
265    #[serde(
266        default,
267        deserialize_with = "lenient::opt_f64",
268        skip_serializing_if = "Option::is_none"
269    )]
270    pub confidence: Option<f64>,
271    #[serde(default, deserialize_with = "lenient::opt_str")]
272    pub(crate) attribution_group: Option<String>,
273    #[serde(default, deserialize_with = "lenient::opt_str")]
274    pub repo_hash: Option<String>,
275    #[serde(default, deserialize_with = "lenient::opt_str")]
276    pub(crate) prev_hash: Option<String>,
277    #[serde(default, deserialize_with = "lenient::opt_str")]
278    pub(crate) entry_hash: Option<String>,
279}
280
281#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
282pub(crate) struct LedgerPage {
283    pub(crate) available: bool,
284    pub(crate) total: i64,
285    pub(crate) offset: i64,
286    pub(crate) limit: i64,
287    pub(crate) entries: Vec<LedgerEntry>,
288    pub(crate) from: String,
289    pub(crate) to: String,
290}
291
292#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
293pub(crate) struct LedgerFacets {
294    pub(crate) entries: i64,
295    pub(crate) first: String,
296    pub(crate) last: String,
297    pub(crate) proxied: i64,
298    pub(crate) models: Vec<ModelCount>,
299    pub(crate) tools: Vec<ToolCount>,
300    pub(crate) repos: Vec<RepoCount>,
301}
302
303#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
304pub(crate) struct ModelCount {
305    pub(crate) model: String,
306    pub(crate) entries: i64,
307}
308
309#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
310pub(crate) struct ToolCount {
311    pub(crate) tool: String,
312    pub(crate) entries: i64,
313}
314
315#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
316pub(crate) struct RepoCount {
317    pub(crate) repo: String,
318    pub(crate) entries: i64,
319}
320
321#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
322pub(crate) struct LedgerTotals {
323    pub(crate) tools: BTreeMap<String, ToolTotals>,
324    pub(crate) days: Vec<DayTotals>,
325}
326
327#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
328pub(crate) struct ToolTotals {
329    pub(crate) calls: i64,
330    pub(crate) saved: i64,
331    pub(crate) baseline: i64,
332    pub(crate) usd: f64,
333    pub(crate) corrected: i64,
334    pub(crate) lines_added: i64,
335    pub(crate) lines_removed: i64,
336}
337
338#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
339pub(crate) struct DayTotals {
340    pub(crate) day: String,
341    pub(crate) calls: i64,
342    pub(crate) saved: i64,
343    pub(crate) corrected: i64,
344    pub(crate) usd: f64,
345    pub(crate) cache_usd: f64,
346}
347
348/// `[reductions, saved, usd]`.
349pub(crate) type GroupRow = (i64, i64, f64);
350
351#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
352pub(crate) struct ReportBucket {
353    pub(crate) entries: i64,
354    pub(crate) reductions: i64,
355    pub(crate) saved: i64,
356    pub(crate) baseline: i64,
357    pub(crate) actual: i64,
358    pub(crate) usd: f64,
359    pub(crate) would_usd: f64,
360    pub(crate) blended_usd: f64,
361    pub(crate) blended_saved: i64,
362    pub(crate) stand_in_usd: f64,
363    pub(crate) corrections: i64,
364    pub(crate) corrected: i64,
365    pub(crate) corrected_usd: f64,
366    pub(crate) cache_entries: i64,
367    pub(crate) edits: i64,
368    pub(crate) tools: BTreeMap<String, GroupRow>,
369    pub(crate) models: BTreeMap<String, GroupRow>,
370    pub(crate) mechanisms: BTreeMap<String, GroupRow>,
371}
372
373pub(crate) type LedgerDays = BTreeMap<String, BTreeMap<String, ReportBucket>>;
374
375#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
376pub(crate) struct Chain {
377    pub(crate) lines: i64,
378    pub(crate) broken: Option<i64>,
379    pub(crate) spans: BTreeMap<String, (i64, i64, String, String)>,
380}
381
382#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
383pub(crate) struct GuardEvent {
384    pub(crate) at: String,
385    pub(crate) kind: String,
386    pub(crate) count: i64,
387    pub(crate) tool: String,
388}
389
390#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
391pub(crate) struct EgressRow {
392    pub(crate) mtime: i64,
393    pub(crate) provider: String,
394    pub(crate) model: String,
395    pub(crate) locality: String,
396    pub(crate) outcome: String,
397    pub(crate) original: i64,
398    pub(crate) delivered: i64,
399    pub(crate) redactions: i64,
400    pub(crate) blocked: i64,
401    pub(crate) injection: i64,
402    pub(crate) incomplete: i64,
403    pub(crate) mode: String,
404}
405
406#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
407pub(crate) struct AlertSettings {
408    pub(crate) enabled: bool,
409    pub(crate) kinds: Vec<String>,
410    pub(crate) desktop: bool,
411    pub(crate) webhook: String,
412}
413
414#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
415pub(crate) struct AlertRecord {
416    pub(crate) at: String,
417    pub(crate) text: String,
418    pub(crate) channels: Vec<String>,
419    pub(crate) test: bool,
420}
421
422#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
423pub(crate) struct Profile {
424    pub(crate) name: String,
425    #[serde(default)]
426    pub(crate) values: Map<String, Value>,
427    #[serde(default)]
428    pub(crate) saved_at: String,
429}
430
431#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
432pub(crate) struct ProxyDays {
433    pub(crate) first: String,
434    pub(crate) days: Vec<ProxyDay>,
435    pub(crate) kept: ProxyKept,
436}
437
438#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
439pub(crate) struct ProxyDay {
440    pub(crate) day: String,
441    pub(crate) models: BTreeMap<String, ProxyModelDay>,
442}
443
444#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
445pub(crate) struct ProxyModelDay {
446    pub(crate) prompts: i64,
447    pub(crate) cached_tokens: i64,
448    pub(crate) discount_usd: f64,
449}
450
451#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
452pub(crate) struct ProxyKept {
453    pub(crate) saved: i64,
454    pub(crate) corrected: i64,
455    pub(crate) usd: f64,
456    pub(crate) reductions: i64,
457    pub(crate) corrections: i64,
458}
459
460#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
461pub(crate) struct HostUsageFile {
462    pub(crate) project: String,
463    pub(crate) host: String,
464    pub(crate) days: BTreeMap<String, BTreeMap<String, HostCounts>>,
465}
466
467#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
468pub struct HostCounts {
469    pub requests: i64,
470    pub input: i64,
471    pub cache_read: i64,
472    pub cache_write: i64,
473    pub output: i64,
474}
475
476#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
477pub(crate) struct SnapshotMeta {
478    pub(crate) id: String,
479    pub(crate) created_at: Option<Value>,
480    pub(crate) facts: Option<Value>,
481    pub(crate) reason: Option<Value>,
482}
483
484#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
485pub(crate) struct AdaptiveState {
486    #[serde(default)]
487    pub(crate) applied: Vec<AppliedItem>,
488    #[serde(default)]
489    pub(crate) dismissed: Vec<String>,
490}
491
492#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
493pub(crate) struct AppliedItem {
494    pub(crate) path: String,
495    pub(crate) action: String,
496    #[serde(default)]
497    pub(crate) applied_at: String,
498    #[serde(default)]
499    pub(crate) baseline: Baseline,
500}
501
502#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
503pub(crate) struct Baseline {
504    #[serde(default)]
505    pub(crate) reads: i64,
506    #[serde(default)]
507    pub(crate) original: i64,
508    #[serde(default)]
509    pub(crate) saved: i64,
510    #[serde(default)]
511    pub(crate) ratio: Option<f64>,
512}
513
514#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
515pub(crate) struct HeatmapEntry {
516    #[serde(default, deserialize_with = "lenient::opt_i64")]
517    pub(crate) access_count: Option<i64>,
518    #[serde(default, deserialize_with = "lenient::opt_i64")]
519    pub(crate) total_original_tokens: Option<i64>,
520    #[serde(default, deserialize_with = "lenient::opt_i64")]
521    pub(crate) total_tokens_saved: Option<i64>,
522    #[serde(default, deserialize_with = "lenient::opt_str")]
523    pub(crate) last_access: Option<String>,
524}