1use serde_json::Value;
7
8use crate::core::context_compiler::CompileCandidate;
9use crate::core::context_field::{
10 ContextField, ContextItemId, ContextKind, ContextState, FieldSignals, TokenBudget, ViewCosts,
11 ViewKind,
12};
13use crate::core::context_ledger::ContextLedger;
14use crate::core::context_policies::PolicySet;
15
16const DEFAULT_BUDGET: usize = 12_000;
17
18pub fn handle(
19 args: Option<&serde_json::Map<String, Value>>,
20 ledger: &ContextLedger,
21 policies: &PolicySet,
22) -> String {
23 let task = get_str(args, "task").unwrap_or_else(|| "general".to_string());
24 let budget_tokens: usize = args
25 .and_then(|a| a.get("budget"))
26 .and_then(serde_json::Value::as_u64)
27 .map_or(DEFAULT_BUDGET, |b| b as usize);
28 let profile = get_str(args, "profile").unwrap_or_else(|| "balanced".to_string());
29
30 let field = ContextField::new();
31 let budget = TokenBudget {
32 total: budget_tokens,
33 used: 0,
34 };
35 let temperature = budget.temperature();
36
37 let intent_route = crate::core::intent_router::route_v1(&task);
38 let intent_mode = &intent_route.decision.effective_read_mode;
39
40 let mut plan_items: Vec<PlanItem> = Vec::new();
41 let mut total_estimated = 0usize;
42
43 for entry in &ledger.entries {
44 let path = &entry.path;
45 let seen_before = true;
46
47 let effective_state = policies.effective_state(
48 path,
49 entry.state.unwrap_or(ContextState::Included),
50 seen_before,
51 entry.original_tokens,
52 );
53 if effective_state == ContextState::Excluded {
54 plan_items.push(PlanItem {
55 path: path.clone(),
56 recommended_view: "excluded".to_string(),
57 estimated_tokens: 0,
58 phi: 0.0,
59 state: "excluded".to_string(),
60 reason: "policy".to_string(),
61 });
62 continue;
63 }
64
65 let phi = entry.phi.unwrap_or_else(|| {
66 let signals = FieldSignals {
67 relevance: if task != "general" && path.contains(&task) {
68 0.8
69 } else {
70 0.3
71 },
72 ..Default::default()
73 };
74 field.compute_phi(&signals)
75 });
76
77 let view_costs = entry
78 .view_costs
79 .clone()
80 .unwrap_or_else(|| ViewCosts::from_full_tokens(entry.original_tokens));
81
82 let recommended_view = policies
83 .recommended_view(path, seen_before, entry.original_tokens)
84 .unwrap_or_else(|| {
85 if intent_mode != "auto" && intent_mode != "reference" {
86 ViewKind::parse(intent_mode)
87 } else {
88 field.select_view(&view_costs, temperature)
89 }
90 });
91
92 let estimated_tokens = view_costs.get(&recommended_view);
93 total_estimated += estimated_tokens;
94
95 plan_items.push(PlanItem {
96 path: path.clone(),
97 recommended_view: recommended_view.as_str().to_string(),
98 estimated_tokens,
99 phi,
100 state: format!("{effective_state:?}"),
101 reason: format!("profile:{profile}"),
102 });
103 }
104
105 let loaded_paths: Vec<String> = ledger.entries.iter().map(|e| e.path.clone()).collect();
106 let (target_files, keywords) = crate::core::task_relevance::parse_task_hints(&task);
107 let classification = crate::core::intent_engine::classify(&task);
108 let structured = crate::core::intent_engine::StructuredIntent {
109 task_type: classification.task_type,
110 confidence: classification.confidence,
111 targets: target_files,
112 keywords,
113 scope: crate::core::intent_engine::IntentScope::MultiFile,
114 language_hint: None,
115 urgency: 0.5,
116 action_verb: None,
117 };
118 let deficit = crate::core::context_deficit::detect_deficit(ledger, &structured, &loaded_paths);
119
120 for suggestion in &deficit.suggested_files {
121 if !plan_items.iter().any(|p| p.path == suggestion.path) {
122 plan_items.push(PlanItem {
123 path: suggestion.path.clone(),
124 recommended_view: suggestion.recommended_mode.clone(),
125 estimated_tokens: suggestion.estimated_tokens,
126 phi: 0.5,
127 state: "suggested".to_string(),
128 reason: format!("{:?}", suggestion.reason),
129 });
130 total_estimated += suggestion.estimated_tokens;
131 }
132 }
133
134 plan_items.sort_by(|a, b| {
135 b.phi
136 .partial_cmp(&a.phi)
137 .unwrap_or(std::cmp::Ordering::Equal)
138 });
139
140 format_plan(&task, budget_tokens, total_estimated, &plan_items, &profile)
141}
142
143pub fn plan_to_candidates(ledger: &ContextLedger, policies: &PolicySet) -> Vec<CompileCandidate> {
145 let field = ContextField::new();
146 let mut candidates = Vec::new();
147
148 for entry in &ledger.entries {
149 let path = &entry.path;
150 let seen_before = true;
151 let effective_state = policies.effective_state(
152 path,
153 entry.state.unwrap_or(ContextState::Included),
154 seen_before,
155 entry.original_tokens,
156 );
157
158 let phi = entry.phi.unwrap_or_else(|| {
159 let signals = FieldSignals {
160 relevance: 0.3,
161 ..Default::default()
162 };
163 field.compute_phi(&signals)
164 });
165
166 let view_costs = entry
167 .view_costs
168 .clone()
169 .unwrap_or_else(|| ViewCosts::from_full_tokens(entry.original_tokens));
170
171 let item_id = entry
172 .id
173 .clone()
174 .unwrap_or_else(|| ContextItemId::from_file(path));
175
176 candidates.push(CompileCandidate {
177 id: item_id,
178 kind: entry.kind.unwrap_or(ContextKind::File),
179 path: path.clone(),
180 state: effective_state,
181 phi,
182 view_costs: view_costs.clone(),
183 selected_view: entry.active_view.unwrap_or(ViewKind::Full),
184 selected_tokens: entry.sent_tokens,
185 pinned: effective_state == ContextState::Pinned,
186 });
187 }
188
189 candidates
190}
191
192#[derive(Debug)]
193struct PlanItem {
194 path: String,
195 recommended_view: String,
196 estimated_tokens: usize,
197 phi: f64,
198 state: String,
199 reason: String,
200}
201
202fn format_plan(
203 task: &str,
204 budget: usize,
205 estimated: usize,
206 items: &[PlanItem],
207 profile: &str,
208) -> String {
209 let utilization = if budget > 0 {
210 estimated as f64 / budget as f64 * 100.0
211 } else {
212 0.0
213 };
214
215 let mut out = String::new();
216 out.push_str(&format!("[ctx_plan] task=\"{task}\" profile={profile}\n"));
217 out.push_str(&format!(
218 "Budget: {estimated}/{budget} tokens ({utilization:.1}% estimated)\n\n"
219 ));
220
221 let included: Vec<_> = items.iter().filter(|i| i.state != "excluded").collect();
222 let excluded: Vec<_> = items.iter().filter(|i| i.state == "excluded").collect();
223
224 if !included.is_empty() {
225 out.push_str("Planned items:\n");
226 for item in &included {
227 out.push_str(&format!(
228 " {} {} {}t phi={:.2} [{}]\n",
229 item.path, item.recommended_view, item.estimated_tokens, item.phi, item.state
230 ));
231 }
232 }
233
234 if !excluded.is_empty() {
235 out.push_str(&format!("\nExcluded ({}):\n", excluded.len()));
236 for item in &excluded {
237 out.push_str(&format!(" {} — {}\n", item.path, item.reason));
238 }
239 }
240
241 if utilization > 90.0 {
242 out.push_str(
243 "\nWARNING: Estimated tokens exceed 90% of budget. Consider stricter views.\n",
244 );
245 }
246
247 out
248}
249
250fn get_str(args: Option<&serde_json::Map<String, Value>>, key: &str) -> Option<String> {
251 args?
252 .get(key)?
253 .as_str()
254 .map(std::string::ToString::to_string)
255}