sqlite_graphrag/cli/globals.rs
1//! Global flag surface: the root `Cli` parser and its validation.
2//!
3//! Every flag that applies before a subcommand is chosen lives here, together
4//! with the cross-flag validation `main` runs before dispatching.
5
6use super::commands::Commands;
7use crate::backend_choice::{EmbeddingBackendChoice, LlmBackendChoice};
8use crate::i18n::{current, Language};
9use clap::Parser;
10
11/// Returns the maximum simultaneous invocations allowed by the CPU heuristic.
12fn max_concurrency_ceiling() -> usize {
13 std::thread::available_parallelism()
14 .map(|n| n.get() * 2)
15 .unwrap_or(8)
16}
17
18#[derive(Parser)]
19#[command(name = "sqlite-graphrag")]
20#[command(version)]
21#[command(about = "Local GraphRAG memory for LLMs in a single SQLite file")]
22#[command(arg_required_else_help = true)]
23#[command(after_help = "DATABASE PATH (GAP-SG-32):\n \
24 `--db` is a PER-SUBCOMMAND flag, so it must come AFTER the subcommand:\n \
25 sqlite-graphrag remember --db ./graphrag.sqlite --name mem --type note ...\n \
26 Placing it before the subcommand (e.g. `sqlite-graphrag --db x.sqlite remember`) is rejected.\n \
27 Prefer `--db` on every invocation (one-shot agents). Optional XDG defaults:\n \
28 `sqlite-graphrag config set db.path ./graphrag.sqlite`\n \
29 Product environment variables are not read at runtime; use flags + `config set/get`.")]
30/// CLI.
31pub struct Cli {
32 /// Maximum number of simultaneous CLI invocations allowed (default: 4).
33 ///
34 /// Caps the counting semaphore used for CLI concurrency slots. The value must
35 /// stay within [1, 2×nCPUs]. Values above the ceiling are rejected with exit 2.
36 #[arg(long, global = true, value_name = "N")]
37 pub max_concurrency: Option<usize>,
38
39 /// Wait up to SECONDS for a free concurrency slot before giving up (exit 75).
40 ///
41 /// Useful in retrying agent pipelines: the process polls every 500 ms until a
42 /// slot opens or the timeout expires. Default: 300s (5 minutes).
43 #[arg(long, global = true, value_name = "SECONDS")]
44 pub wait_lock: Option<u64>,
45
46 /// Skip the available-memory check before loading the model.
47 ///
48 /// Exclusive use in automated tests where real allocation does not occur.
49 #[arg(long, global = true, hide = true, default_value_t = false)]
50 pub skip_memory_guard: bool,
51
52 // `--strict-env-clear` lived here until v1.2.2. It controlled which
53 // environment variables an LLM subprocess inherited, and this release
54 // removed every LLM subprocess. The flag still parsed, still flowed into
55 // `RuntimeOverrides`, and was read by nothing — the inverse of the dead
56 // configuration channel this release also closed: there the text promised a
57 // channel the code ignored, here a flag promised an effect it could no
58 // longer have. Both are removed rather than documented.
59 /// Fail instead of degrading when the query embedding cannot be produced.
60 ///
61 /// `recall` and `hybrid-search` fall back to FTS5-only ranking when the
62 /// provider is unreachable, raise `vec_degraded` on the envelope, and exit
63 /// `0`. That is the right default for a human reading results, and the wrong
64 /// one for an agent that parses `.results` and never looks at the flags: it
65 /// silently receives a keyword search where it asked for a hybrid one.
66 ///
67 /// Under this flag a degraded read exits non-zero with the usual error
68 /// envelope, so the retry verdict travels with it. A degradation the caller
69 /// ASKED for with `--fallback-fts-only` is deliberate and never fails.
70 #[arg(long, global = true, default_value_t = false)]
71 pub fail_on_degraded: bool,
72
73 /// Language for human-facing stderr messages. Accepts `en` or `pt`.
74 ///
75 /// Without the flag, detection uses XDG `i18n.lang` then OS locale
76 /// (`LC_ALL`/`LC_MESSAGES`/`LANG`). JSON stdout stays deterministic and
77 /// identical across languages; only human-facing strings are affected.
78 #[arg(long, global = true, value_enum, value_name = "LANG")]
79 pub lang: Option<crate::i18n::Language>,
80
81 /// Time zone for `*_iso` fields in JSON output (for example `America/Sao_Paulo`).
82 ///
83 /// Accepts any IANA time zone name. Without the flag, it falls back to
84 /// XDG `display.tz`; if unset, UTC is used. Integer epoch fields
85 /// are not affected.
86 #[arg(long, global = true, value_name = "IANA")]
87 pub tz: Option<chrono_tz::Tz>,
88
89 /// Directory holding `config.toml`. Overrides the OS config directory.
90 ///
91 /// Precedence (G-T-XDG-04): this flag > OS default. It deliberately does
92 /// NOT consult a `config set` key, because the config file itself lives in
93 /// this directory and reading it to find itself would be circular.
94 /// Hidden: it exists for hermetic test isolation and sandboxed hosts.
95 #[arg(long, global = true, hide = true, value_name = "DIR")]
96 pub config_dir: Option<std::path::PathBuf>,
97
98 /// Directory for lock files, model files and other cache artifacts.
99 ///
100 /// Precedence (G-T-XDG-04): this flag > XDG `cache.dir` > OS default.
101 /// Hidden for the same reason as `--config-dir`.
102 #[arg(long, global = true, hide = true, value_name = "DIR")]
103 pub cache_dir: Option<std::path::PathBuf>,
104
105 /// Increase logging verbosity (-v=info, -vv=debug, -vvv=trace).
106 ///
107 /// Overrides XDG `log.level` when present. Logs are emitted
108 /// to stderr; JSON stdout is unaffected.
109 #[arg(short = 'v', long, global = true, action = clap::ArgAction::Count)]
110 pub verbose: u8,
111
112 /// Suppress non-error tracing on stderr (sets log level to `error`).
113 ///
114 /// Prefer this in pipelines that capture stdout JSON (`> out.json`).
115 /// Never combine stdout and stderr into the same file (`&>` / `2>&1`) —
116 /// that contaminates the JSON envelope (v1.1.05 Bug 2). Conflicts with
117 /// `-v` / `--verbose` only in spirit: quiet wins when both are present.
118 #[arg(short = 'q', long, global = true, default_value_t = false)]
119 pub quiet: bool,
120
121 // `--extraction-backend` lived here until v1.2.2, and was the most
122 // advertised dead flag in the crate: ten shipped documents described its
123 // four values while `src/` held exactly ONE mention of it — this very
124 // declaration. It never reached `RuntimeOverrides`, let alone a consumer.
125 // Its doc still described selecting between a headless `claude`/`codex`
126 // extractor and a `fastembed` pipeline, and BOTH were removed from the
127 // product. Found by asking who READS the field rather than how many times
128 // the identifier appears, which is the check `tests/inert_flag_guard.rs`
129 // now performs for the whole `Cli` struct.
130 /// Embedding dimensionality override (default 1024 since v1.2.0).
131 ///
132 /// Precedence: this flag > XDG `embedding.dim` >
133 /// the `dim` recorded in the database `schema_meta` > 1024. Existing
134 /// databases keep their recorded dimensionality automatically; use
135 /// this flag only to migrate a corpus to a new dimensionality
136 /// (followed by `enrich --operation re-embed`). Range: [8, 4096].
137 #[arg(long, global = true, value_name = "N", value_parser = clap::value_parser!(u64).range(8..=4096))]
138 pub embedding_dim: Option<u64>,
139
140 /// LLM backend for embedding. Accepts `openrouter` (OpenRouter REST)
141 /// or `none` (skips embedding; useful for tests). Prefer the flag;
142 /// optional XDG `llm.backend` via `config set`.
143 ///
144 /// Kept `Option` with no `default_value_t`, for the reason `--llm-fallback`
145 /// already documents below: a clap default makes the field always `Some`,
146 /// which silently swallows the XDG layer the doc promises. The default
147 /// lives in [`crate::runtime_config::llm_backend`] instead, so
148 /// flag > XDG > `open-router` actually resolves.
149 #[arg(long, global = true, value_enum)]
150 pub llm_backend: Option<LlmBackendChoice>,
151
152 /// v1.0.82 (GAP-003): model to invoke on the chosen backend.
153 /// Prefer the flag; optional XDG `llm.model`.
154 #[arg(long, global = true, value_name = "MODEL")]
155 pub llm_model: Option<String>,
156
157 /// Chain of LLM backends tried in order when the primary fails.
158 ///
159 /// Defaults to `none`. The default lives in the runtime registry rather
160 /// than in `default_value` here on purpose: a clap default makes the field
161 /// always `Some`, which silently swallows the XDG layer the doc promises —
162 /// `config set llm.fallback` would have been read by nothing. Leaving it
163 /// `None` when unset is what lets flag > XDG > constant actually resolve.
164 #[arg(long, global = true)]
165 pub llm_fallback: Option<String>,
166
167 /// v1.0.82 (GAP-005): persists with a NULL embedding when all
168 /// backends in the chain fail. The memory stays in `pending_embeddings`
169 /// for reprocessing via `embedding retry`. Prefer the flag; optional XDG
170 /// XDG `llm.skip_embedding_on_failure`.
171 #[arg(
172 long,
173 global = true,
174 default_value_t = false,
175 value_parser = clap::builder::BoolishValueParser::new(),
176 )]
177 pub skip_embedding_on_failure: bool,
178
179 // GAP-SG-204 (drive-by): the rendered text read "optional XDG XDG
180 // `llm.max_host_concurrency`" — a duplicated word from a mechanical edit —
181 // and described the ceiling as covering "LLM subprocesses", a thing v1.2.0
182 // removed. It bounds concurrent host SLOTS.
183 /// Host-wide ceiling of concurrent LLM slots. Default derived from `ncpus`.
184 ///
185 /// Prefer the flag; optional XDG `llm.max_host_concurrency`.
186 #[arg(long, global = true, value_name = "N")]
187 pub llm_max_host_concurrency: Option<u32>,
188
189 /// v1.0.82 (GAP-004): seconds to wait for a free LLM slot
190 /// before failing with exit 75. Default 30s. Prefer the flag; optional XDG
191 /// XDG `llm.slot_wait_secs`.
192 #[arg(long, global = true, value_name = "SECONDS")]
193 pub llm_slot_wait_secs: Option<u64>,
194
195 /// v1.0.82 (GAP-004): if set, fails immediately (exit 75)
196 /// when no LLM slot is free. Prefer the flag; optional XDG
197 /// XDG `llm.slot_no_wait`.
198 #[arg(
199 long,
200 global = true,
201 default_value_t = false,
202 value_parser = clap::builder::BoolishValueParser::new(),
203 )]
204 pub llm_slot_no_wait: bool,
205
206 /// Embedding backend selector.
207 ///
208 /// `openrouter` uses the REST API and requires a stored key. `auto` resolves
209 /// to the same path when a key is reachable and degrades to no embedding
210 /// when it is not. There is no subprocess backend: generation happens over
211 /// HTTP, in-process, one shot.
212 ///
213 /// Prefer the flag; optional XDG `config set embedding.backend`.
214 ///
215 /// Kept `Option` with no `default_value_t`: a clap default makes the field
216 /// always `Some` and the XDG layer promised right above would be read by
217 /// nothing. The default lives in
218 /// [`crate::runtime_config::embedding_backend`] instead.
219 #[arg(long, global = true, value_enum)]
220 pub embedding_backend: Option<EmbeddingBackendChoice>,
221
222 /// v1.0.93: embedding model for the OpenRouter API. Required when
223 /// `--embedding-backend openrouter`. Prefer the flag; optional XDG `embedding.model`.
224 #[arg(long, global = true, value_name = "MODEL")]
225 pub embedding_model: Option<String>,
226
227 /// OpenRouter API key for a single invocation.
228 ///
229 /// Prefer `config add-key --provider openrouter --from-stdin`, which stores
230 /// the key at rest under XDG with mode 0600 and keeps it out of both the
231 /// shell history and the process table. No environment variable supplies
232 /// this value: the product never reads one (G-T-XDG-04).
233 #[arg(long, global = true, value_name = "KEY", hide = true)]
234 pub openrouter_api_key: Option<String>,
235
236 /// Per-request budget, in seconds, for every OpenRouter call.
237 ///
238 /// Global because the deadline binds the EMBEDDING client too, and that
239 /// client is built once per process at startup. Declared only on `enrich`,
240 /// the flag reached the chat path and nothing else: `remember`, `ingest`,
241 /// `edit`, `restore` and `split-body` were pinned to the compiled default
242 /// with no way to widen it, and a slow provider turned into exit 11 with no
243 /// operator recourse. `enrich --openrouter-timeout <N>` keeps working
244 /// unchanged, because a clap global argument accepts being written at the
245 /// subcommand position.
246 ///
247 /// Kept optional so an EXPLICIT value is distinguishable from an omitted
248 /// one, which is what lets flag > XDG > constant resolve instead of the
249 /// flag always winning with a default nobody asked for.
250 #[arg(long, global = true, value_name = "SECONDS")]
251 pub openrouter_timeout: Option<u64>,
252
253 /// GAP-SG-142: keep only these keys in each result object (comma separated).
254 ///
255 /// Accepts dotted paths (`stats.total`). Keys missing from an element are
256 /// skipped rather than emitted as `null`, so a projection never invents
257 /// fields. Envelopes without a result array are projected themselves.
258 /// `--fields` is an accepted spelling of the same flag.
259 #[arg(
260 long,
261 visible_alias = "fields",
262 global = true,
263 value_name = "KEYS",
264 value_delimiter = ','
265 )]
266 pub select: Vec<String>,
267
268 /// GAP-SG-142: keep only result elements satisfying `EXPR`.
269 ///
270 /// Grammar: `key=value`, `key!=value`, `key~substring` (case-insensitive
271 /// containment). `==` is a synonym of `=`. Repeat the flag to conjoin
272 /// predicates with AND. A malformed expression fails fast with exit 2 so a
273 /// typo is never mistaken for an empty result set. Failure envelopes are
274 /// never filtered: `error: true` / `ok: false` always reaches the caller.
275 #[arg(long, global = true, value_name = "EXPR")]
276 pub filter: Vec<String>,
277
278 /// GAP-SG-201: declare what `--filter` is allowed to observe.
279 ///
280 /// Omitted, a predicate over a page the query already truncated is refused
281 /// with exit 2, because the answer would describe a set the predicate never
282 /// saw: `--filter type=skill list` reports 39 of 1892 memories, and the same
283 /// request with `--limit 50` reported 0. `page` accepts the narrower reading
284 /// and records it; `universe` states the requirement explicitly.
285 ///
286 /// Only paginated commands with a countable universe are ever refused. A
287 /// `-k` in `hybrid-search` or `recall` bounds a ranking rather than paging a
288 /// table, so the top-k IS the answer and filtering it is legitimate.
289 #[arg(long, global = true, value_enum, value_name = "SCOPE")]
290 pub filter_scope: Option<crate::agent_surface::universe::FilterScope>,
291
292 /// GAP-SG-202: accept a `--select` / `--filter` / `--sort` / `--dedupe-by`
293 /// key that this envelope carries nowhere.
294 ///
295 /// Without it such a key is refused with exit 2, because an unresolvable key
296 /// produces an empty answer indistinguishable from missing data — a typo
297 /// reads as "the memory does not exist". With it the pre-v1.2.6 behaviour is
298 /// restored for callers who genuinely probe a heterogeneous payload.
299 #[arg(long, global = true, default_value_t = false)]
300 pub allow_unknown_keys: bool,
301
302 /// GAP-SG-207: accept the ambient database target for a verb that changes
303 /// durable state.
304 ///
305 /// A mutating subcommand normally has to name its target with `--db`, and is
306 /// refused with exit 2 when it does not. This is the explicit dispensation
307 /// the Explicit Target Designation rule requires beside that requirement:
308 /// the inheritance still happens, but a human asked for it and the envelope
309 /// records that they did, so it is a decision rather than an accident.
310 #[arg(long, global = true, default_value_t = false)]
311 pub use_active: bool,
312
313 /// GAP-SG-142: emit at most N result elements.
314 ///
315 /// Distinct from the per-subcommand `--limit` and from `-k`, which bound
316 /// the *query*; this bounds only what is written to stdout, after
317 /// filtering. Precedence: this flag > XDG `agent_surface.max_items` > 0
318 /// (no cap).
319 ///
320 /// The name is not a stylistic choice and no `--limit` alias is offered:
321 /// eight subcommands (`related`, `pending`, `pending-embeddings`, `list`,
322 /// `export`, `embedding`, `graph entities`, `enrich`) already declare their
323 /// own `--limit`, and a global argument sharing that long flag would give
324 /// clap two definitions for one name inside those subcommands.
325 ///
326 /// Applies to EVERY array in the envelope, not only the primary one: an
327 /// agent asking for two nodes must not be handed sixty thousand edges
328 /// alongside them. `--select` stays on the primary array — see the module
329 /// documentation of [`crate::agent_surface`] for why projecting a
330 /// heterogeneous secondary array would erase it rather than shrink it.
331 #[arg(long, global = true, value_name = "N")]
332 pub max_items: Option<usize>,
333
334 /// GAP-SG-142: sort result elements ascending by this key (dotted path).
335 ///
336 /// Numbers compare numerically, everything else as text. Elements without
337 /// the key keep their relative order at the end of the list.
338 #[arg(long, global = true, value_name = "KEY")]
339 pub sort: Option<String>,
340
341 /// GAP-SG-142: drop later result elements repeating this key's value.
342 ///
343 /// Elements lacking the key are always kept, since they were never proven
344 /// duplicate.
345 #[arg(long, global = true, value_name = "KEY")]
346 pub dedupe_by: Option<String>,
347
348 /// GAP-SG-142: replace the payload with `{"count": N}`.
349 ///
350 /// `N` is the number of result elements left after `--filter`,
351 /// `--dedupe-by` and `--max-items`.
352 ///
353 /// GAP-SG-201, and this CHANGED the exit code in v1.2.8: over a paginated
354 /// command whose limit actually cut rows, this is now refused with exit `2`
355 /// rather than answering a page count that reads as the inventory. Declare
356 /// `--filter-scope page` to accept the narrower reading, and the
357 /// `count_scope` field of the agent-surface record then reports `page`
358 /// instead of `matched`. A top-k bound is never refused: the k IS the answer.
359 ///
360 /// GAP-SG-209: refused with exit `2` on `export` and `ingest`, which emit one
361 /// record per line. A count applied there ran once per line and answered
362 /// about a single record instead of the stream.
363 ///
364 /// GAP-SG-206: after a subcommand that actually persists, the knob is
365 /// SUPPRESSED rather than honoured, and the `count_only_suppressed` field of
366 /// the agent-surface record says so. Replacing a write envelope with a number
367 /// would discard `memory_id` and `entities_created` for an operation that
368 /// already happened and cannot be replayed.
369 #[arg(long, global = true, default_value_t = false)]
370 pub count_only: bool,
371
372 /// GAP-SG-142: shorten every string longer than N characters.
373 ///
374 /// Counts characters, never bytes, so a UTF-8 sequence is never split.
375 /// Truncation is recorded under `agent_surface.content_truncated` and
376 /// raises the top-level `truncated` flag. Precedence: this flag > XDG
377 /// `agent_surface.truncate_content` > 0 (disabled).
378 #[arg(long, global = true, value_name = "N")]
379 pub truncate_content: Option<usize>,
380
381 /// GAP-SG-142: cap the serialized envelope at N bytes.
382 ///
383 /// Enforced by dropping trailing result elements until the payload fits —
384 /// never by slicing the JSON text, which would not parse. What was dropped
385 /// is recorded under `agent_surface.output_truncated` / `dropped`.
386 /// Precedence: this flag > XDG `agent_surface.max_output_bytes` > 0
387 /// (no ceiling).
388 ///
389 /// GAP-SG-209: refused with exit `2` on `export` and `ingest`. A byte budget
390 /// spent once per emitted line is not a budget on the output.
391 #[arg(long, global = true, value_name = "N")]
392 pub max_output_bytes: Option<usize>,
393
394 /// Refuse to read stdin anywhere in this invocation.
395 ///
396 /// The refusal is declarative, not emergent: without the flag, a stdin path
397 /// only fails once the read is attempted (immediately on a TTY, after the
398 /// deadline otherwise). With it, `--body-stdin`, `--graph-stdin`,
399 /// `remember-batch` and every other stdin reader fail up front with exit 1
400 /// (`AppError::Validation`), even when a pipe is attached and would have
401 /// supplied data.
402 ///
403 /// Precedence: this flag > XDG `cli.no_input` > `false`.
404 // Until v1.2.4 the paragraph above promised exit 65, and seven
405 // operator-facing documents repeated it. No arm of `AppError::exit_code`
406 // returns 65, so an agent branching on it never matched this case. The
407 // correction lives in a plain comment, not a doc comment: clap renders doc
408 // comments as help text, and naming the wrong code — even to disown it —
409 // puts the number back in front of the reader `tests/no_input_exit_contract.rs`
410 // is there to keep it away from.
411 #[arg(long, global = true, default_value_t = false)]
412 pub no_input: bool,
413
414 /// Subcommand to execute.
415 #[command(subcommand)]
416 pub command: Option<Commands>,
417}
418
419impl Cli {
420 /// Validates concurrency flags and returns a localised descriptive error if invalid.
421 ///
422 /// Requires that `crate::i18n::init()` has already been called (happens before this
423 /// function in the `main` flow). In English it emits EN messages; in Portuguese it emits PT.
424 pub fn validate_flags(&self) -> Result<(), String> {
425 if let Some(n) = self.max_concurrency {
426 if n == 0 {
427 return Err(match current() {
428 Language::English => "--max-concurrency must be >= 1".to_string(),
429 Language::Portuguese => "--max-concurrency deve ser >= 1".to_string(),
430 });
431 }
432 let ceiling = max_concurrency_ceiling();
433 if n > ceiling {
434 return Err(match current() {
435 Language::English => format!(
436 "--max-concurrency {n} exceeds the ceiling of {ceiling} (2×nCPUs) on this system"
437 ),
438 Language::Portuguese => format!(
439 "--max-concurrency {n} excede o teto de {ceiling} (2×nCPUs) neste sistema"
440 ),
441 });
442 }
443 }
444 self.install_agent_surface()?;
445 self.install_write_policy();
446 // Installed here rather than at each call site so the guarantee holds
447 // for every stdin reader, present and future, from a single point.
448 crate::stdin_helper::install_no_input(crate::runtime_config::no_input(self.no_input));
449 Ok(())
450 }
451
452 /// GAP-SG-207: records whether this process may inherit its database target
453 /// from ambient configuration, for [`crate::paths::AppPaths::resolve`].
454 ///
455 /// Runs beside [`Self::install_agent_surface`] and for the same reason: this
456 /// is the one hook that sees the parsed command line before any handler
457 /// executes, which is the only window where refusing still means "nothing
458 /// happened" rather than "it happened somewhere you did not name".
459 ///
460 /// With no subcommand there is nothing to mutate, so the requirement is off.
461 /// That is not a permissive default — a bare invocation prints help and
462 /// resolves no path at all.
463 fn install_write_policy(&self) {
464 let requires_explicit_target = self.command.as_ref().is_some_and(Commands::persists);
465 crate::paths::install_write_policy(crate::paths::WritePolicy {
466 requires_explicit_target,
467 use_active: self.use_active,
468 });
469 }
470
471 /// GAP-SG-142: resolves the agent-native output surface and installs it
472 /// process-wide, so [`crate::output`] can reshape every envelope from a
473 /// single point.
474 ///
475 /// Runs from [`Self::validate_flags`] because that is the one bootstrap
476 /// hook already invoked after language and XDG initialisation and before
477 /// any subcommand dispatch — exactly the window where a malformed
478 /// `--filter` must abort with exit 2 rather than be mistaken for an empty
479 /// result set.
480 ///
481 /// # Errors
482 /// Returns the localized parse error of the first malformed `--filter`.
483 fn install_agent_surface(&self) -> Result<(), String> {
484 let mut filters = Vec::with_capacity(self.filter.len());
485 for raw in &self.filter {
486 filters.push(crate::agent_surface::filter::FilterExpr::parse(raw)?);
487 }
488 crate::agent_surface::init(crate::agent_surface::AgentSurface {
489 // Alias suppression is only correct for the subcommand that declared
490 // the alias, so the surface has to know which one emitted the
491 // envelope. Resolvable here at zero cost because `validate_flags`
492 // already runs on the parsed `Cli`, after dispatch is decided and
493 // before any command runs.
494 command: self
495 .command
496 .as_ref()
497 .and_then(Commands::agent_surface_slug)
498 .map(str::to_string),
499 mutates: self.command.as_ref().is_none_or(Commands::mutates),
500 allow_unknown_keys: self.allow_unknown_keys,
501 use_active: self.use_active,
502 filter_scope: self.filter_scope,
503 select: self.select.clone(),
504 filters,
505 sort: self.sort.clone(),
506 dedupe_by: self.dedupe_by.clone(),
507 max_items: crate::runtime_config::agent_surface_max_items(self.max_items),
508 count_only: self.count_only,
509 streamed: self.command.as_ref().is_some_and(Commands::streams),
510 writes_receipt: self.command.as_ref().is_some_and(Commands::persists),
511 truncate_content: crate::runtime_config::agent_surface_truncate_content(
512 self.truncate_content,
513 ),
514 max_output_bytes: crate::runtime_config::agent_surface_max_output_bytes(
515 self.max_output_bytes,
516 ),
517 });
518 Ok(())
519 }
520}