Skip to main content

agentstatedeveloper_cli/commands/
context_for.rs

1//! `asd context-for <qname[,qname...]>` — assemble agent query context.
2//!
3//! Returns a structured package containing everything an agent needs to reason
4//! about a symbol: signature, source range, direct callers/callees, declared
5//! and transitive effects, all ledger entries (invariants, hazards, decisions,
6//! etc.), and applicable policy constraints.  Pass `--budget-tokens N` to trim
7//! the output to fit a context window.
8//!
9//! This is also exposed as the `asd_context_for` MCP tool.
10
11use anyhow::Result;
12use clap::Args;
13use serde_json::{Value, json};
14
15use agentstatedeveloper_core::{AsgEffectStore, AsgIndexStore, AsgLedgerStore, Engine, IndexStore};
16
17use crate::commands::graph::AsdTimer;
18use crate::config::Config;
19
20// Plan M t-001 (1.0.91): assemble_symbol_context lifted to
21// core::context. Re-export so existing intra-CLI imports keep
22// working (investigate.rs imports this path).
23pub(crate) use agentstatedeveloper_core::assemble_symbol_context;
24
25#[derive(Debug, Args)]
26pub struct ContextForArgs {
27    /// Comma-separated list of fully-qualified symbol names.
28    /// Example: `DriftSynthPool.resolveForPreview,Scheduler.restartLane`
29    pub qnames: String,
30
31    /// Approximate token budget for the output (rough estimate: 1 token ≈ 4 chars).
32    /// When set, ledger entries and callees are trimmed to fit.
33    #[arg(long)]
34    pub budget_tokens: Option<usize>,
35
36    /// Include full source body of each symbol (can be large).
37    #[arg(long, default_value = "false")]
38    pub include_body: bool,
39
40    /// Suppress the stale-index warning.
41    #[arg(long)]
42    pub quiet: bool,
43
44    /// Print per-phase timing to stderr.
45    #[arg(long)]
46    pub timing: bool,
47}
48
49pub fn run(cfg: &Config, args: ContextForArgs) -> Result<()> {
50    let mut t = AsdTimer::new(args.timing);
51    if !args.quiet {
52        if let Some(warn) = agentstatedeveloper_core::stale_warning(&cfg.db_path, 3600) {
53            eprintln!("{warn}");
54        }
55    }
56    let engine = Engine::open_sqlite(&cfg.db_path)?;
57    t.phase("open_engine");
58    let index_store = AsgIndexStore::from_engine(&engine);
59    let effect_store = AsgEffectStore::from_engine(&engine);
60    let ledger_store = AsgLedgerStore::from_engine(&engine);
61
62    let id_map = index_store.build_id_map(&engine);
63    t.phase("build_id_map");
64
65    let qnames: Vec<&str> = args.qnames.split(',').map(|s| s.trim()).collect();
66    let budget_chars = args.budget_tokens.map(|t| t * 4);
67
68    let mut symbols_out = Vec::new();
69
70    for qname in &qnames {
71        let symbol = match index_store.get_symbol_by_qname(&engine.ref_name, qname)? {
72            Some(s) => s,
73            None => {
74                symbols_out.push(json!({ "qname": qname, "error": "symbol not found" }));
75                continue;
76            }
77        };
78        t.phase("symbol_lookup");
79
80        let sym_ctx = assemble_symbol_context(
81            &engine,
82            &index_store,
83            &effect_store,
84            &ledger_store,
85            &symbol,
86            &id_map,
87            args.include_body,
88            engine.fts.as_ref(),
89            None, // single symbol — compute ownership fresh
90        )?;
91        t.phase("assemble_context");
92        symbols_out.push(sym_ctx);
93    }
94
95    let out = json!({
96        "query": args.qnames,
97        "count": symbols_out.len(),
98        "symbols": symbols_out,
99    });
100
101    // Token economy (1.0.78): context_for output is always JSON
102    // consumed by agents — compact, no whitespace bloat.
103    let mut output = serde_json::to_string(&out)?;
104
105    // Trim to budget if requested (trim ledger entries from the end).
106    if let Some(max_chars) = budget_chars {
107        if output.len() > max_chars {
108            // Re-assemble with fewer ledger entries per symbol.
109            let trimmed = trim_to_budget(&out, max_chars);
110            output = serde_json::to_string(&trimmed)?;
111        }
112    }
113
114    println!("{output}");
115    t.total("context-for");
116    Ok(())
117}
118
119/// Reduce ledger entries to fit within `max_chars`. Trims `decisions_and_notes`
120/// first (least critical), then `proofs`, keeping invariants and hazards.
121fn trim_to_budget(out: &Value, _max_chars: usize) -> Value {
122    // Simple approach: return as-is and let the caller log a warning.
123    // A more sophisticated trim would iterate and drop entries.
124    // For now just note the budget was exceeded.
125    let mut v = out.clone();
126    if let Some(obj) = v.as_object_mut() {
127        obj.insert(
128            "_budget_warning".to_string(),
129            json!(format!(
130                "output exceeds budget; use --budget-tokens to trim, or filter to fewer qnames"
131            )),
132        );
133    }
134    v
135}