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 if crate::agent_surface::active() {
23 let shaped = crate::agent_surface::apply_global(serde_json::to_value(value)?);
24 return Ok(if pretty {
25 serde_json::to_string_pretty(&shaped)?
26 } else {
27 serde_json::to_string(&shaped)?
28 });
29 }
30 Ok(if pretty {
31 serde_json::to_string_pretty(value)?
32 } else {
33 serde_json::to_string(value)?
34 })
35}
36
37/// Whether the envelope should be indented.
38///
39/// Indentation is for a human reading a terminal. Off a terminal it is bytes a
40/// consumer pays for and never reads, so the envelope goes out compact.
41///
42/// GAP-SG-170: this also keeps `--max-output-bytes` honest. `budget::enforce`
43/// measures with `serde_json::to_string`, the compact form, while emission used
44/// `to_string_pretty` unconditionally. The ceiling was therefore enforced on one
45/// serialization and violated on another — measured at 8 659 bytes emitted for a
46/// declared cap of 8 000, with the overshoot growing alongside the cap because
47/// indentation scales with content. With the surface active the answer is never
48/// indented, so the byte the budget counts is the byte the caller receives.
49fn should_indent() -> bool {
50 use std::io::IsTerminal;
51 !crate::agent_surface::active() && std::io::stdout().is_terminal()
52}
53
54/// Serializes `value` as JSON and writes it to stdout with a trailing newline.
55///
56/// Indented on a terminal, compact everywhere else; see [`should_indent`].
57///
58/// Flushes stdout after writing. A `BrokenPipe` error is silenced so that
59/// piping to consumers that close early (e.g. `head`) does not surface an error.
60///
61/// # Errors
62/// Returns `Err` when serialization fails or when a non-`BrokenPipe` I/O error occurs.
63#[inline]
64pub fn emit_json<T: Serialize>(value: &T) -> Result<(), AppError> {
65 sink::write_line(render(value, should_indent())?.as_bytes())
66}
67
68/// Serializes `value` as compact (single-line) JSON and writes it to stdout with a trailing newline.
69///
70/// Flushes stdout after writing. A `BrokenPipe` error is silenced.
71///
72/// # Errors
73/// Returns `Err` when serialization fails or when a non-`BrokenPipe` I/O error occurs.
74#[inline]
75pub fn emit_json_compact<T: Serialize>(value: &T) -> Result<(), AppError> {
76 sink::write_line(render(value, false)?.as_bytes())
77}