Skip to main content

talon_core/query/
output.rs

1//! Query tool output types.
2
3use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6
7use crate::contracts::{ContainerPath, VaultPath};
8
9use super::related::RelationKind;
10
11/// Source snippet used to synthesize an ask answer.
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
13#[serde(rename_all = "camelCase")]
14pub struct AskSource {
15    /// Vault-relative path.
16    pub vault_path: VaultPath,
17    /// Display title.
18    pub title: String,
19    /// Snippet supplied to the answer model.
20    pub snippet: String,
21    /// Search score after rerank and scope weighting.
22    pub score: f64,
23}
24
25/// Verbose `talon ask` stage diagnostics.
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "camelCase")]
28pub struct AskDiagnostics {
29    /// Chat-completions endpoint used for planning and synthesis.
30    pub endpoint: String,
31    /// Ask model used for planning and synthesis.
32    pub model: String,
33    /// Query-planning stage diagnostics.
34    pub planning: AskLlmStageDiagnostics,
35    /// Search stage diagnostics.
36    pub search: AskSearchDiagnostics,
37    /// Synthesis stage diagnostics.
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub synthesis: Option<AskLlmStageDiagnostics>,
40}
41
42/// Verbose diagnostics for an LLM call.
43#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
44#[serde(rename_all = "camelCase")]
45pub struct AskLlmStageDiagnostics {
46    /// Stage duration in milliseconds.
47    pub duration_ms: u64,
48    /// Visible content returned by the chat model.
49    pub content: String,
50}
51
52/// Verbose diagnostics for the retrieval stage.
53#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
54#[serde(rename_all = "camelCase")]
55pub struct AskSearchDiagnostics {
56    /// Stage duration in milliseconds.
57    pub duration_ms: u64,
58    /// Total search results before final truncation.
59    pub total: u32,
60}
61
62/// Vault-grounded natural-language answer response.
63#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
64#[serde(rename_all = "camelCase")]
65pub struct AskResponse {
66    /// Vault root (absolute container path).
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub vault: Option<ContainerPath>,
69    /// User question.
70    pub question: String,
71    /// Synthesized answer.
72    pub answer: String,
73    /// Search queries planned for retrieval.
74    pub queries: Vec<String>,
75    /// Ranked source snippets used for synthesis.
76    pub sources: Vec<AskSource>,
77    /// Verbose stage diagnostics.
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub diagnostics: Option<AskDiagnostics>,
80}
81
82/// Read result.
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
84#[serde(rename_all = "camelCase")]
85pub struct ReadResult {
86    /// Whether the note was found.
87    pub found: bool,
88    /// Vault-relative path.
89    pub vault_path: VaultPath,
90    /// Optional note title.
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub title: Option<String>,
93    /// Optional note content.
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub content: Option<String>,
96    /// Section metadata when the read target included an Obsidian heading.
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub section: Option<ReadSection>,
99    /// Outgoing links.
100    #[serde(default)]
101    pub links: Vec<String>,
102    /// Backlinks.
103    #[serde(default)]
104    pub backlinks: Vec<String>,
105    /// Tags.
106    #[serde(default)]
107    pub tags: Vec<String>,
108    /// Aliases.
109    #[serde(default)]
110    pub aliases: Vec<String>,
111}
112
113/// Metadata for a heading-scoped read.
114#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
115#[serde(rename_all = "camelCase")]
116pub struct ReadSection {
117    /// Heading text as written in the note.
118    pub heading: String,
119    /// 1-based line number where the section starts in the note body.
120    pub from_line: u32,
121    /// 1-based inclusive line number where the section ends in the note body.
122    pub to_line: u32,
123    /// Obsidian wikilink reference for the resolved section.
124    pub obsidian_ref: String,
125}
126
127/// Read response.
128#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
129#[serde(rename_all = "camelCase")]
130pub struct ReadResponse {
131    /// Vault root (absolute container path).
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub vault: Option<ContainerPath>,
134    /// Read results.
135    pub results: Vec<ReadResult>,
136}
137
138impl ReadResponse {
139    /// Builds a stub read response for CLI scaffolding.
140    #[must_use]
141    pub const fn stub() -> Self {
142        Self {
143            vault: None,
144            results: Vec::new(),
145        }
146    }
147}
148
149/// The recall sections bundled together.
150#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
151#[serde(rename_all = "camelCase")]
152pub struct VaultRecall {
153    /// Top search results (hybrid pipeline output).
154    pub active_notes: Vec<NoteExcerpt>,
155    /// Notes reachable via link graph from `active_notes`.
156    pub linked_context: Vec<LinkedNote>,
157}
158
159/// A note excerpt returned in `recall.active_notes`.
160#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
161#[serde(rename_all = "camelCase")]
162pub struct NoteExcerpt {
163    /// Vault-relative path.
164    pub vault_path: VaultPath,
165    /// Display title.
166    pub title: String,
167    /// Result snippet (with heading breadcrumb when available).
168    pub snippet: String,
169    /// Hybrid retrieval score (post-rerank, post-scope-multiplier).
170    pub score: f64,
171    /// 1-based rank within `active_notes`.
172    pub rank: u32,
173    /// Last modified date in "YYYY-MM-DD" format, empty when unavailable.
174    pub mtime: String,
175}
176
177/// A note reachable via the link graph returned in `recall.linked_context`.
178#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
179#[serde(rename_all = "camelCase")]
180pub struct LinkedNote {
181    /// Vault-relative path.
182    pub vault_path: VaultPath,
183    /// Display title.
184    pub title: String,
185    /// Raw link text from the highest-scoring source note.
186    pub link_text: String,
187    /// Direction (Outgoing takes precedence over Backlink when mixed).
188    pub relation: RelationKind,
189    /// Number of graph hops from source notes (currently always 1).
190    pub hops: u8,
191    /// Sum of scores from all active notes that link here, weighted
192    /// by source score. Higher = cited by more or higher-scoring active notes.
193    pub aggregated_score: f64,
194    /// Active notes that contributed this link: `(vault_path, score)` pairs.
195    /// Used by the MCP suppression layer to recompute `aggregated_score`
196    /// after filtering out suppressed source notes.
197    pub source_notes: Vec<(VaultPath, f64)>,
198}
199
200/// Vault-native context recall response.
201#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
202#[serde(rename_all = "camelCase")]
203pub struct RecallResponse {
204    /// Vault root (absolute container path).
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub vault: Option<ContainerPath>,
207    /// The five recall sections, or null when skipped by confidence gate.
208    pub vault_recall: Option<VaultRecall>,
209    /// Calibrated evidence quality score in [0, 1].
210    pub evidence_score: f64,
211    /// Estimated tokens used in the payload (≤ `budget_tokens` within ±2%).
212    pub tokens_used: u32,
213    /// Paths suppressed by `--exclude` before budget allocation.
214    pub excluded: Vec<String>,
215    /// Paths retrieved but dropped during greedy budget trimming.
216    pub excluded_by_budget: Vec<String>,
217    /// True when `evidence_score` < `min_confidence` or zero results returned.
218    pub skipped: bool,
219    /// Optional recall profiling diagnostics.
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub diagnostics: Option<RecallDiagnostics>,
222}
223
224/// Recall pipeline profiling diagnostics.
225#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
226#[serde(rename_all = "camelCase")]
227pub struct RecallDiagnostics {
228    /// Full input estimate before distillation.
229    pub input_tokens: usize,
230    /// Main query estimate after distillation/compaction.
231    pub query_tokens: usize,
232    /// Number of retrieval queries sent to the hybrid pipeline.
233    pub query_count: usize,
234    /// Number of weighted phrases extracted locally.
235    pub phrase_count: usize,
236    /// Prompt-view tokens sent to the query distiller, after safety margins.
237    #[serde(skip_serializing_if = "Option::is_none")]
238    pub distillation_input_tokens: Option<usize>,
239    /// Whether the query distiller LLM call ran.
240    pub distillation_ran: bool,
241    /// Query distiller wall time.
242    pub distillation_ms: Option<u64>,
243    /// Whether the query distiller returned a usable compact query.
244    pub distillation_succeeded: bool,
245    /// Why recall fell back to deterministic query compaction.
246    #[serde(skip_serializing_if = "Option::is_none")]
247    pub distillation_fallback_reason: Option<String>,
248    /// Retrieval/fusion/rerank wall time.
249    pub retrieval_ms: u64,
250    /// Number of embedding batches attempted by hybrid retrieval.
251    pub embed_batches: u32,
252    /// Candidates submitted to the reranker, when rerank ran.
253    pub rerank_candidates: Option<u32>,
254    /// Rerank wall time when the hybrid pipeline reported it.
255    pub rerank_ms: Option<u64>,
256    /// Full recall wall time.
257    pub total_ms: u64,
258}
259
260/// A single frontmatter entry.
261#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
262#[serde(rename_all = "camelCase")]
263pub struct MetaEntry {
264    /// Vault-relative path.
265    pub path: VaultPath,
266    /// Frontmatter key-value pairs.
267    pub frontmatter: BTreeMap<String, serde_json::Value>,
268    /// Resolved scope name, if applicable.
269    #[serde(skip_serializing_if = "Option::is_none")]
270    pub scope: Option<String>,
271    /// File modification time as RFC 3339 / ISO 8601.
272    #[serde(skip_serializing_if = "Option::is_none")]
273    pub mtime: Option<String>,
274}
275
276/// Frontmatter query response.
277#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
278#[serde(rename_all = "camelCase")]
279pub struct MetaResponse {
280    /// Vault root (absolute container path).
281    #[serde(skip_serializing_if = "Option::is_none")]
282    pub vault: Option<ContainerPath>,
283    /// Frontmatter entries.
284    pub entries: Vec<MetaEntry>,
285    /// Tag counts, if requested.
286    #[serde(skip_serializing_if = "Option::is_none")]
287    pub tag_counts: Option<BTreeMap<String, u32>>,
288}
289
290/// A change entry (added or modified).
291#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
292#[serde(rename_all = "camelCase")]
293pub struct ChangeEntry {
294    /// Vault-relative path.
295    pub path: VaultPath,
296    /// When this file was last indexed (RFC 3339 / ISO 8601).
297    pub indexed_at: String,
298}
299
300/// A tombstone entry (deleted file).
301#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
302#[serde(rename_all = "camelCase")]
303pub struct TombstoneEntry {
304    /// Vault-relative path.
305    pub path: VaultPath,
306    /// When the file was detected as deleted (RFC 3339 / ISO 8601).
307    pub deleted_at: String,
308}
309
310/// Change feed response.
311#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
312#[serde(rename_all = "camelCase")]
313pub struct ChangesResponse {
314    /// Vault root (absolute container path).
315    #[serde(skip_serializing_if = "Option::is_none")]
316    pub vault: Option<ContainerPath>,
317    /// Files newly indexed since the timestamp.
318    pub added: Vec<ChangeEntry>,
319    /// Files re-indexed since the timestamp.
320    pub modified: Vec<ChangeEntry>,
321    /// Files deleted (from tombstones).
322    pub deleted: Vec<TombstoneEntry>,
323}