Skip to content

Commit caeb9e8

Browse files
hyperpolymathclaude
andcommitted
feat(integration): wire MetaController into AgentCore + server
AgentCore: - Add meta_controller: Arc<MetaController> field; pass through new() - process_goal() now calls meta_controller.plan() instead of router.select() - run_preconditions() executes coprocessor ops before the prover fires; outcomes are injected into ProofState::metadata under "coprocessor.<aspect>" keys so backends and tactic suggesters see pre-computed witnesses - record_outcome() feeds every QED/failure back to the StatisticsTracker so Bayesian routing converges toward what works for each (prover, domain) - Router retained for backward-compat reflection path; MetaController is the primary planning path going forward Server: - AppState gains meta_controller: Arc<MetaController>; constructed once at server startup so the Bayesian stats accumulate across all requests - POST /api/agent/plan endpoint: accepts { aspects, preferred_prover, candidates, default_timeout_ms }, returns the MetaController's Plan (chosen prover, coprocessor preconditions, Bayesian timeout estimate, rationale). Clients can inspect the plan before submitting a full proof. 872 tests green, 0 clippy errors. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 60d2e75 commit caeb9e8

2 files changed

Lines changed: 183 additions & 26 deletions

File tree

src/rust/agent/mod.rs

Lines changed: 69 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use anyhow::Result;
1212
use serde::{Deserialize, Serialize};
1313
use std::collections::VecDeque;
14+
use std::sync::Arc;
1415
use tokio::sync::RwLock;
1516
use tracing::{debug, info, warn};
1617

@@ -24,6 +25,7 @@ pub mod meta_controller; // Coordinates prover × coprocessor × goal-aspect
2425
pub mod planner;
2526
pub mod router;
2627

28+
use meta_controller::{MetaController, PlanOutcome};
2729
use memory::ProofMemory;
2830
use planner::Planner;
2931
use router::ProverRouter;
@@ -121,6 +123,12 @@ pub struct AgentCore {
121123
/// Planner (decomposes goals into sub-goals)
122124
planner: Box<dyn Planner>,
123125

126+
/// MetaController — aspect-aware (prover × coprocessor) planner with
127+
/// Pareto-frontier ranking and Bayesian timeout estimation. Supersedes
128+
/// the raw ProverRouter for goal dispatch; the router is retained for
129+
/// the backward-compat reflection path (`reflect_on_success/failure`).
130+
meta_controller: Arc<MetaController>,
131+
124132
/// Prover router (selects best prover for a goal)
125133
router: ProverRouter,
126134

@@ -167,10 +175,16 @@ impl Default for AgentConfig {
167175
}
168176

169177
impl AgentCore {
170-
/// Create a new agent core
178+
/// Create a new agent core.
179+
///
180+
/// `meta_controller` is the shared (prover × coprocessor) planner. Pass
181+
/// `Arc::new(MetaController::new())` for a standalone controller, or share
182+
/// one that already has a `StatisticsTracker` plugged in (via
183+
/// `MetaController::with_stats`) so outcomes feed a persistent learning loop.
171184
pub fn new(
172185
memory: Box<dyn ProofMemory>,
173186
planner: Box<dyn Planner>,
187+
meta_controller: Arc<MetaController>,
174188
router: ProverRouter,
175189
provers: Vec<Box<dyn ProverBackend>>,
176190
config: AgentConfig,
@@ -179,6 +193,7 @@ impl AgentCore {
179193
goal_queue: RwLock::new(VecDeque::new()),
180194
memory,
181195
planner,
196+
meta_controller,
182197
router,
183198
provers,
184199
config,
@@ -323,47 +338,69 @@ impl AgentCore {
323338
return Ok(GoalResult::Decomposed { sub_goals });
324339
}
325340

326-
// Step 3: Select prover
327-
let prover_kind = goal
328-
.preferred_prover
329-
.unwrap_or_else(|| self.router.select(&goal));
341+
// Step 3: Plan — MetaController selects the prover and queues coprocessor
342+
// preconditions whose results will be available to the prover as proof
343+
// context (ProofState::metadata). Falls back gracefully to Z3 when no
344+
// aspect routing matches or no stats exist.
345+
let plan = self.meta_controller.plan(&goal).await;
346+
let precondition_outcomes = self
347+
.meta_controller
348+
.run_preconditions(&plan)
349+
.await
350+
.unwrap_or_default();
351+
352+
// Use the MetaController's chosen prover, falling back to the
353+
// router if that prover is not in our local pool.
354+
let prover_kind = if self.provers.iter().any(|p| p.kind() == plan.prover) {
355+
plan.prover
356+
} else {
357+
self.router.select(&goal)
358+
};
330359

331360
let prover = self
332361
.provers
333362
.iter()
334363
.find(|p| p.kind() == prover_kind)
335364
.ok_or_else(|| anyhow::anyhow!("Prover {:?} not available", prover_kind))?;
336365

337-
// Step 4: Get tactic suggestions (neural guidance)
366+
// Step 4: Seed ProofState metadata with coprocessor witness data so
367+
// the prover backend and tactic suggester can see pre-computed facts
368+
// (e.g. factorizations, Gröbner bases, crypto verifications).
369+
// Keys follow the convention "coprocessor.<aspect>".
370+
let mut metadata: std::collections::HashMap<String, serde_json::Value> = Default::default();
371+
for (pre, outcome) in plan
372+
.coprocessor_preconditions
373+
.iter()
374+
.zip(precondition_outcomes.iter())
375+
{
376+
if let Ok(v) = serde_json::to_value(outcome) {
377+
metadata.insert(format!("coprocessor.{}", pre.aspect), v);
378+
}
379+
}
380+
381+
// Step 5: Get tactic suggestions (neural guidance), using the
382+
// context-enriched initial state so the suggestion model sees the
383+
// coprocessor witnesses.
384+
let initial_state = ProofState {
385+
goals: vec![goal.goal.clone()],
386+
context: Default::default(),
387+
proof_script: vec![],
388+
metadata: metadata.clone(),
389+
};
338390
let tactics = if self.config.neural_enabled {
339-
prover
340-
.suggest_tactics(
341-
&ProofState {
342-
goals: vec![goal.goal.clone()],
343-
context: Default::default(),
344-
proof_script: vec![],
345-
metadata: Default::default(),
346-
},
347-
5,
348-
)
349-
.await?
391+
prover.suggest_tactics(&initial_state, 5).await?
350392
} else {
351-
vec![Tactic::Assumption] // Fallback
393+
vec![Tactic::Assumption]
352394
};
353395

354-
// Step 5: Try tactics against the chosen backend.
396+
// Step 6: Try tactics against the chosen backend.
355397
//
356398
// Each candidate is applied via the `ProverBackend::apply_tactic`
357399
// FFI and the resulting state either yields the full proof (QED),
358400
// advances the state (Success — we fall through to next tactic,
359401
// keeping this step-thin), or errors out.
360402
let start = std::time::Instant::now();
361-
let mut current_state = ProofState {
362-
goals: vec![goal.goal.clone()],
363-
context: Default::default(),
364-
proof_script: vec![],
365-
metadata: Default::default(),
366-
};
403+
let mut current_state = initial_state;
367404

368405
for tactic in tactics {
369406
debug!("Trying tactic {:?} for goal {}", tactic, goal.goal.id);
@@ -372,6 +409,9 @@ impl AgentCore {
372409
Ok(TacticResult::QED) => {
373410
current_state.proof_script.push(tactic.clone());
374411
let time_ms = start.elapsed().as_millis() as u64;
412+
self.meta_controller
413+
.record_outcome(&plan, &goal, PlanOutcome::Success, time_ms)
414+
.await;
375415
return Ok(GoalResult::Proved {
376416
proof: current_state,
377417
prover: prover_kind,
@@ -391,6 +431,10 @@ impl AgentCore {
391431
}
392432
}
393433

434+
let elapsed_ms = start.elapsed().as_millis() as u64;
435+
self.meta_controller
436+
.record_outcome(&plan, &goal, PlanOutcome::Failure, elapsed_ms)
437+
.await;
394438
Ok(GoalResult::Failed {
395439
reason: "No tactic succeeded".to_string(),
396440
prover: Some(prover_kind),

src/rust/server.rs

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ use axum::{
1414
Json, Router,
1515
};
1616
use colored::Colorize;
17-
use echidna::core::{ProofState, Tactic, TacticResult};
17+
use echidna::agent::meta_controller::{MetaController, Plan};
18+
use echidna::agent::AgenticGoal;
19+
use echidna::core::{Goal, ProofState, Tactic, TacticResult, Term};
1820
use echidna::dispatch::ProverDispatcher;
1921
use echidna::{ProverBackend, ProverConfig, ProverKind};
2022
use reqwest::Client;
@@ -37,6 +39,11 @@ struct AppState {
3739
ml_client: Client,
3840
/// Julia ML API base URL
3941
ml_api_url: String,
42+
/// MetaController — shared (prover × coprocessor) planner with persistent
43+
/// StatisticsTracker. Survives request boundaries so the Bayesian routing
44+
/// state accumulates across the server's lifetime. All handlers that
45+
/// perform goal-aware dispatch share this single instance.
46+
meta_controller: Arc<MetaController>,
4047
}
4148

4249
/// A proof session
@@ -74,10 +81,13 @@ pub async fn start_server(port: u16, host: String, enable_cors: bool) -> Result<
7481
},
7582
}
7683

84+
let meta_controller = Arc::new(MetaController::new());
85+
7786
let state = AppState {
7887
sessions: Arc::new(RwLock::new(HashMap::new())),
7988
ml_client,
8089
ml_api_url,
90+
meta_controller,
8191
};
8292

8393
// Build router
@@ -99,6 +109,7 @@ pub async fn start_server(port: u16, host: String, enable_cors: bool) -> Result<
99109
.route("/api/session/:id/apply", post(apply_tactic_handler))
100110
.route("/api/session/:id/tree", get(get_proof_tree))
101111
// Additional UI-specific endpoints
112+
.route("/api/agent/plan", post(agent_plan_handler))
102113
.route("/api/aspect-tags", get(get_aspect_tags))
103114
.route("/api/tactics/suggest", post(suggest_tactics_ui))
104115
.route("/api/theorems/search", get(search_theorems_ui))
@@ -157,6 +168,99 @@ pub async fn start_server(port: u16, host: String, enable_cors: bool) -> Result<
157168
Ok(())
158169
}
159170

171+
// ========== Agent Planning ==========
172+
173+
/// Request to `POST /api/agent/plan`.
174+
///
175+
/// Asks the MetaController to plan a proof attempt: which prover to use,
176+
/// which coprocessor preconditions to run first, and the Bayesian-estimated
177+
/// timeout. The response is a `Plan` that callers can inspect before
178+
/// deciding whether to submit a full `/api/verify` or `/api/prove` request.
179+
///
180+
/// `aspects` drive the coprocessor routing (e.g. `"arithmetic.factorisation"`
181+
/// triggers a Math precondition; `"algebra.groebner.basis"` triggers Singular).
182+
/// `preferred_prover` and `candidates` are both optional — omitting both lets
183+
/// the Pareto ranker pick freely from Z3 as the default.
184+
#[derive(Debug, Deserialize)]
185+
struct AgentPlanRequest {
186+
/// Goal aspect tags (e.g. `["arithmetic.factorisation", "crypto.hash.sha256"]`).
187+
#[serde(default)]
188+
aspects: Vec<String>,
189+
190+
/// Client's preferred prover, if any. Overridden by Pareto ranking when
191+
/// `candidates` is non-empty.
192+
preferred_prover: Option<ProverKind>,
193+
194+
/// Candidate provers to rank. When empty, `preferred_prover` (or Z3)
195+
/// is selected directly without Pareto scoring.
196+
#[serde(default)]
197+
candidates: Vec<ProverKind>,
198+
199+
/// Default timeout hint (ms) used when no historical data is available.
200+
#[serde(default = "default_timeout_ms")]
201+
default_timeout_ms: u64,
202+
}
203+
204+
fn default_timeout_ms() -> u64 {
205+
30_000
206+
}
207+
208+
/// `POST /api/agent/plan` — return a MetaController plan for the given goal aspects.
209+
///
210+
/// Does not execute any provers or coprocessors; it only plans. Callers use
211+
/// the returned `coprocessor_preconditions` to understand what computational
212+
/// preparation the system recommends before submitting the proof content.
213+
async fn agent_plan_handler(
214+
State(state): State<AppState>,
215+
Json(req): Json<AgentPlanRequest>,
216+
) -> impl IntoResponse {
217+
// Build a minimal AgenticGoal from the request metadata. The goal target
218+
// is a placeholder (`Term::Const("?")`) because planning doesn't inspect
219+
// the proof content — only the aspects and prover preferences matter here.
220+
use echidna::agent::Priority;
221+
let goal = AgenticGoal {
222+
goal: Goal {
223+
id: uuid::Uuid::new_v4().to_string(),
224+
target: Term::Const("?".to_string()),
225+
hypotheses: vec![],
226+
},
227+
priority: Priority::Medium,
228+
attempts: 0,
229+
max_attempts: 3,
230+
preferred_prover: req.preferred_prover,
231+
aspects: req.aspects,
232+
parent: None,
233+
};
234+
235+
let plan: Plan = if req.candidates.is_empty() {
236+
state.meta_controller.plan(&goal).await
237+
} else {
238+
use echidna::verification::confidence::TrustLevel;
239+
state
240+
.meta_controller
241+
.plan_with_pareto(
242+
&goal,
243+
&req.candidates,
244+
req.default_timeout_ms,
245+
TrustLevel::Level2,
246+
)
247+
.await
248+
};
249+
250+
Json(json!({
251+
"prover": format!("{:?}", plan.prover),
252+
"estimated_timeout_ms": plan.estimated_timeout_ms,
253+
"rationale": plan.rationale,
254+
"coprocessor_preconditions": plan.coprocessor_preconditions
255+
.iter()
256+
.map(|p| json!({
257+
"kind": format!("{:?}", p.kind),
258+
"aspect": p.aspect,
259+
}))
260+
.collect::<Vec<_>>(),
261+
}))
262+
}
263+
160264
// ========== Groove Discovery ==========
161265

162266
/// Groove capability manifest for ECHIDNA.
@@ -691,6 +795,15 @@ async fn search_handler(
691795
}
692796
}
693797

798+
// Cross-prover layer (see main.rs::search_command for rationale).
799+
// Folds VeriSimDB matches into the same response so REST clients
800+
// see results from backends without native search commands too.
801+
let verisim_url = std::env::var("VERISIM_URL")
802+
.unwrap_or_else(|_| "http://localhost:8080".to_string());
803+
if let Ok(cross) = echidna::vcl_ut::cross_prover_search_names(&verisim_url, pattern, 50).await {
804+
all_results.extend(cross);
805+
}
806+
694807
let count = all_results.len();
695808
Ok(Json(SearchResponse {
696809
results: all_results,

0 commit comments

Comments
 (0)