perspt_sdk/
exploration.rs1use serde::{Deserialize, Serialize};
12
13use crate::capability::{ActorId, Capability, EffectKind};
14use crate::routing::ModelRoute;
15
16#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
18pub struct ExplorationBudget {
19 pub max_files: u32,
20 pub max_tokens: u64,
21 pub max_tool_calls: u32,
22 pub max_wall_clock_secs: u64,
23 pub max_parallel_workers: u32,
24}
25
26impl Default for ExplorationBudget {
27 fn default() -> Self {
28 Self {
29 max_files: 500,
30 max_tokens: 50_000,
31 max_tool_calls: 200,
32 max_wall_clock_secs: 120,
33 max_parallel_workers: 8,
34 }
35 }
36}
37
38#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
40pub struct ExplorationUsage {
41 pub files: u32,
42 pub tokens: u64,
43 pub tool_calls: u32,
44 pub wall_clock_secs: u64,
45}
46
47impl ExplorationBudget {
48 pub fn admits(&self, usage: &ExplorationUsage) -> bool {
50 usage.files <= self.max_files
51 && usage.tokens <= self.max_tokens
52 && usage.tool_calls <= self.max_tool_calls
53 && usage.wall_clock_secs <= self.max_wall_clock_secs
54 }
55}
56
57#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
59pub struct ProjectMap {
60 pub languages: Vec<String>,
61 pub package_roots: Vec<String>,
62 pub build_systems: Vec<String>,
63 pub entry_points: Vec<String>,
64 pub risk_hotspots: Vec<String>,
65}
66
67#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
69pub struct GraphHint {
70 pub goal: String,
71 pub suggested_outputs: Vec<String>,
72 pub rationale: String,
73}
74
75#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
77pub struct ExplorationReport {
78 pub report_id: String,
79 pub model_route: Option<ModelRoute>,
80 pub project_map: ProjectMap,
81 pub graph_hints: Vec<GraphHint>,
82 pub verifier_recommendations: Vec<String>,
83 pub input_witnesses: Vec<String>,
85 pub deterministically_backed: bool,
88}
89
90impl ExplorationReport {
91 pub fn new(project_map: ProjectMap) -> Self {
92 Self {
93 report_id: uuid::Uuid::new_v4().to_string(),
94 model_route: None,
95 project_map,
96 graph_hints: Vec::new(),
97 verifier_recommendations: Vec::new(),
98 input_witnesses: Vec::new(),
99 deterministically_backed: false,
100 }
101 }
102
103 pub fn is_barrier_eligible(&self) -> bool {
106 self.deterministically_backed
107 }
108}
109
110pub fn exploration_capability(actor: ActorId) -> Capability {
113 Capability::new(
114 actor,
115 vec![
116 EffectKind::ReadFile,
117 EffectKind::Search,
118 EffectKind::List,
119 EffectKind::LspQuery,
120 EffectKind::GitRead,
121 ],
122 )
123 .with_paths(vec!["*"])
124}
125
126pub fn is_read_only_capability(cap: &Capability) -> bool {
128 cap.effects.iter().all(|e| e.is_read_only())
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn exploration_capability_is_read_only() {
137 let cap = exploration_capability(ActorId::new("explorer"));
138 assert!(is_read_only_capability(&cap));
139 assert!(!cap.grants(EffectKind::WriteArtifact));
140 assert!(!cap.grants(EffectKind::ApplyPatch));
141 assert!(!cap.grants(EffectKind::MutateDependencies));
142 }
143
144 #[test]
145 fn budget_admits_within_limits_and_rejects_overflow() {
146 let budget = ExplorationBudget::default();
147 let ok = ExplorationUsage {
148 files: 10,
149 tokens: 1000,
150 tool_calls: 5,
151 wall_clock_secs: 10,
152 };
153 assert!(budget.admits(&ok));
154 let over = ExplorationUsage { files: 9999, ..ok };
155 assert!(!budget.admits(&over));
156 }
157
158 #[test]
159 fn model_summary_alone_is_not_a_barrier() {
160 let report = ExplorationReport::new(ProjectMap::default());
161 assert!(!report.is_barrier_eligible());
162 }
163
164 #[test]
165 fn deterministically_backed_report_is_barrier_eligible() {
166 let mut report = ExplorationReport::new(ProjectMap::default());
167 report.deterministically_backed = true;
168 assert!(report.is_barrier_eligible());
169 }
170}