sqlite_graphrag/output/envelope.rs
1//! Complete JSON envelopes — and the single point where the agent-native
2//! surface is applied.
3//!
4//! GAP-SG-142 put the reshaping here rather than in each subcommand precisely
5//! because there is exactly one function pair every payload passes through.
6//! One implementation covers the whole CLI and no command has to know the
7//! surface exists.
8//!
9//! The `active()` check keeps the fast path intact: with no shaping flag set,
10//! the value is serialized straight from its own `Serialize` impl and the
11//! envelope is byte-for-byte what it was before the surface existed.
12
13use super::sink;
14use crate::errors::AppError;
15use serde::Serialize;
16
17/// Serializes `value` and returns its JSON text, applying the agent-native
18/// surface when one is installed.
19///
20/// `pretty` selects indented output; compact is a single line.
21fn render<T: Serialize>(value: &T, pretty: bool) -> Result<String, AppError> {
22 // Two reasons to enter the layer, not one. A knob means the envelope is
23 // reshaped; a resolved target means it must be annotated even though no
24 // knob was set. GAP-SG-205: gating solely on `active()` is what hid the
25 // target on the default path, since a caller that sets no flag is exactly
26 // the caller the Explicit Target Designation rule is written for.
27 if crate::agent_surface::active() || crate::agent_surface::target::is_reportable() {
28 let shaped = crate::agent_surface::apply_global(serde_json::to_value(value)?)?;
29 return Ok(if pretty {
30 serde_json::to_string_pretty(&shaped)?
31 } else {
32 serde_json::to_string(&shaped)?
33 });
34 }
35 Ok(if pretty {
36 serde_json::to_string_pretty(value)?
37 } else {
38 serde_json::to_string(value)?
39 })
40}
41
42/// Whether the envelope should be indented.
43///
44/// Indentation is for a human reading a terminal. Off a terminal it is bytes a
45/// consumer pays for and never reads, so the envelope goes out compact.
46///
47/// GAP-SG-170: this also keeps `--max-output-bytes` honest. `budget::enforce`
48/// measures with `serde_json::to_string`, the compact form, while emission used
49/// `to_string_pretty` unconditionally. The ceiling was therefore enforced on one
50/// serialization and violated on another — measured at 8 659 bytes emitted for a
51/// declared cap of 8 000, with the overshoot growing alongside the cap because
52/// indentation scales with content. With the surface active the answer is never
53/// indented, so the byte the budget counts is the byte the caller receives.
54fn should_indent() -> bool {
55 use std::io::IsTerminal;
56 !crate::agent_surface::active() && std::io::stdout().is_terminal()
57}
58
59/// Serializes `value` as JSON and writes it to stdout with a trailing newline.
60///
61/// Indented on a terminal, compact everywhere else; see `should_indent`.
62///
63/// Flushes stdout after writing. A `BrokenPipe` error is silenced so that
64/// piping to consumers that close early (e.g. `head`) does not surface an error.
65///
66/// # Errors
67/// Returns `Err` when serialization fails or when a non-`BrokenPipe` I/O error occurs.
68#[inline]
69pub fn emit_json<T: Serialize>(value: &T) -> Result<(), AppError> {
70 sink::write_line(render(value, should_indent())?.as_bytes())
71}
72
73/// Serializes `value` as compact (single-line) JSON and writes it to stdout with a trailing newline.
74///
75/// Flushes stdout after writing. A `BrokenPipe` error is silenced.
76///
77/// # Errors
78/// Returns `Err` when serialization fails or when a non-`BrokenPipe` I/O error occurs.
79#[inline]
80pub fn emit_json_compact<T: Serialize>(value: &T) -> Result<(), AppError> {
81 sink::write_line(render(value, false)?.as_bytes())
82}