@@ -105,89 +105,79 @@ impl BeamSearch {
105105
106106 merged
107107 }
108- }
109-
110- impl Default for BeamSearch {
111- fn default ( ) -> Self {
112- Self :: new ( )
113- }
114- }
115108
116- #[ async_trait]
117- impl SearchTree for BeamSearch {
118- async fn search (
109+ /// Core beam search logic parameterized by start node.
110+ ///
111+ /// This is the shared implementation used by both `search` (starts from root)
112+ /// and `search_from` (starts from an arbitrary node).
113+ async fn search_impl (
119114 & self ,
120115 tree : & DocumentTree ,
121116 context : & RetrievalContext ,
122117 config : & SearchConfig ,
123118 pilot : Option < & dyn Pilot > ,
119+ start_node : NodeId ,
124120 ) -> SearchResult {
125121 let mut result = SearchResult :: default ( ) ;
126122 let beam_width = config. beam_width . min ( self . beam_width ) ;
127123 let mut visited: HashSet < NodeId > = HashSet :: new ( ) ;
128124
129- println ! ( "[DEBUG] BeamSearch: query='{}', beam_width={}, min_score={:.2}" ,
130- context. query, beam_width, config. min_score) ;
125+ // Mark start_node as visited so we don't go back up
126+ visited. insert ( start_node) ;
127+
128+ debug ! (
129+ "BeamSearch: query='{}', start_node={:?}, beam_width={}, min_score={:.2}" ,
130+ context. query, start_node, beam_width, config. min_score
131+ ) ;
131132
132133 // Track Pilot interventions
133134 let mut pilot_interventions = 0 ;
134135
135- // Initialize with root 's children
136- let root_children = tree. children ( tree . root ( ) ) ;
137- println ! ( "[DEBUG] Root has {} children" , root_children . len( ) ) ;
136+ // Initialize with start_node 's children
137+ let start_children = tree. children ( start_node ) ;
138+ debug ! ( "Start node has {} children" , start_children . len( ) ) ;
138139
139140 // Check if Pilot wants to guide the start
140141 let initial_candidates = if let Some ( p) = pilot {
141- println ! ( "[DEBUG] BeamSearch: Pilot is available, name={}, guide_at_start={}" ,
142- p. name( ) , p. config( ) . guide_at_start) ;
142+ debug ! (
143+ "BeamSearch: Pilot is available, name={}, guide_at_start={}" ,
144+ p. name( ) ,
145+ p. config( ) . guide_at_start
146+ ) ;
143147 if p. config ( ) . guide_at_start {
144- println ! ( "[DEBUG] BeamSearch: Calling pilot.guide_start()..." ) ;
145148 if let Some ( guidance) = p. guide_start ( tree, & context. query ) . await {
146149 debug ! (
147150 "Pilot provided start guidance with confidence {}" ,
148151 guidance. confidence
149152 ) ;
150153 pilot_interventions += 1 ;
151- println ! ( "[DEBUG] BeamSearch: Pilot returned guidance! confidence={:.2}, candidates={}" ,
152- guidance. confidence, guidance. ranked_candidates. len( ) ) ;
153154
154- // Use Pilot's ranked order if available
155155 if guidance. has_candidates ( ) {
156156 self . merge_with_pilot_decision (
157157 tree,
158- & root_children ,
158+ & start_children ,
159159 & guidance,
160160 & context. query ,
161161 )
162162 } else {
163- println ! ( "[DEBUG] BeamSearch: Guidance has no candidates, using algorithm scoring" ) ;
164- self . score_candidates_with_query ( tree, & root_children, & context. query )
163+ self . score_candidates_with_query ( tree, & start_children, & context. query )
165164 }
166165 } else {
167- println ! ( "[DEBUG] BeamSearch: pilot.guide_start() returned None" ) ;
168- self . score_candidates_with_query ( tree, & root_children, & context. query )
166+ self . score_candidates_with_query ( tree, & start_children, & context. query )
169167 }
170168 } else {
171- println ! ( "[DEBUG] BeamSearch: guide_at_start=false, skipping Pilot" ) ;
172- self . score_candidates_with_query ( tree, & root_children, & context. query )
169+ self . score_candidates_with_query ( tree, & start_children, & context. query )
173170 }
174171 } else {
175- println ! ( "[DEBUG] BeamSearch: No Pilot available" ) ;
176- self . score_candidates_with_query ( tree, & root_children, & context. query )
172+ self . score_candidates_with_query ( tree, & start_children, & context. query )
177173 } ;
178174
179175 let mut current_beam: Vec < SearchPath > = initial_candidates
180176 . into_iter ( )
181177 . map ( |( node_id, score) | SearchPath :: from_node ( node_id, score) )
182178 . collect ( ) ;
183179
184- // Debug: show initial scores
185- println ! ( "[DEBUG] Initial {} candidates after scoring" , current_beam. len( ) ) ;
186- for ( i, path) in current_beam. iter ( ) . enumerate ( ) . take ( 5 ) {
187- if let Some ( node) = tree. get ( path. leaf . unwrap_or ( tree. root ( ) ) ) {
188- println ! ( "[DEBUG] Initial {}: score={:.3}, title='{}'" , i, path. score, node. title) ;
189- }
190- }
180+ debug ! ( "Initial {} candidates after scoring" , current_beam. len( ) ) ;
191181
192182 // Keep top beam_width
193183 current_beam. truncate ( beam_width) ;
@@ -207,7 +197,6 @@ impl SearchTree for BeamSearch {
207197
208198 // Check if this is a leaf node
209199 if tree. is_leaf ( leaf_id) {
210- // Add to final results
211200 if path. score >= config. min_score {
212201 result. paths . push ( path. clone ( ) ) ;
213202 }
@@ -220,7 +209,6 @@ impl SearchTree for BeamSearch {
220209
221210 // ========== Pilot Intervention Point ==========
222211 let scored_children = if let Some ( p) = pilot {
223- // Build search state for Pilot
224212 let state = SearchState :: new (
225213 tree,
226214 & context. query ,
@@ -229,14 +217,12 @@ impl SearchTree for BeamSearch {
229217 & visited,
230218 ) ;
231219
232- // Check if Pilot wants to intervene
233220 if p. should_intervene ( & state) {
234221 trace ! (
235222 "Pilot intervening at fork with {} candidates" ,
236223 children. len( )
237224 ) ;
238225
239- println ! ( "[DEBUG] BEAM SEARCH: Pilot intervening at decision point" ) ;
240226 match p. decide ( & state) . await {
241227 decision => {
242228 pilot_interventions += 1 ;
@@ -246,7 +232,6 @@ impl SearchTree for BeamSearch {
246232 std:: mem:: discriminant( & decision. direction)
247233 ) ;
248234
249- // Merge algorithm scores with Pilot decision
250235 self . merge_with_pilot_decision (
251236 tree,
252237 & children,
@@ -256,19 +241,16 @@ impl SearchTree for BeamSearch {
256241 }
257242 }
258243 } else {
259- // No intervention, use algorithm scoring
260244 self . score_candidates_with_query ( tree, & children, & context. query )
261245 }
262246 } else {
263- // No Pilot, use algorithm scoring
264247 self . score_candidates_with_query ( tree, & children, & context. query )
265248 } ;
266249 // ==============================================
267250
268251 for ( child_id, child_score) in scored_children. into_iter ( ) . take ( beam_width) {
269252 let new_path = path. extend ( child_id, child_score) ;
270253
271- // Record trace
272254 let child_node = tree. get ( child_id) ;
273255 result. trace . push ( NavigationStep {
274256 node_id : format ! ( "{:?}" , child_id) ,
@@ -296,7 +278,6 @@ impl SearchTree for BeamSearch {
296278
297279 current_beam = next_beam;
298280
299- // Check if we have enough results
300281 if result. paths . len ( ) >= config. top_k {
301282 break ;
302283 }
@@ -312,9 +293,8 @@ impl SearchTree for BeamSearch {
312293 // Fallback: if no results found, add best candidates regardless of score
313294 if result. paths . is_empty ( ) && config. min_score > 0.0 {
314295 debug ! ( "No results above min_score, adding best candidates as fallback" ) ;
315- // Re-score initial candidates and take top-k
316296 let all_candidates =
317- self . score_candidates_with_query ( tree, & tree. children ( tree . root ( ) ) , & context. query ) ;
297+ self . score_candidates_with_query ( tree, & tree. children ( start_node ) , & context. query ) ;
318298 for ( node_id, score) in all_candidates. into_iter ( ) . take ( config. top_k ) {
319299 result. paths . push ( SearchPath :: from_node ( node_id, score) ) ;
320300 }
@@ -328,11 +308,40 @@ impl SearchTree for BeamSearch {
328308 } ) ;
329309 result. paths . truncate ( config. top_k ) ;
330310
331- // Record Pilot interventions
332311 result. pilot_interventions = pilot_interventions;
333312
334313 result
335314 }
315+ }
316+
317+ impl Default for BeamSearch {
318+ fn default ( ) -> Self {
319+ Self :: new ( )
320+ }
321+ }
322+
323+ #[ async_trait]
324+ impl SearchTree for BeamSearch {
325+ async fn search (
326+ & self ,
327+ tree : & DocumentTree ,
328+ context : & RetrievalContext ,
329+ config : & SearchConfig ,
330+ pilot : Option < & dyn Pilot > ,
331+ ) -> SearchResult {
332+ self . search_impl ( tree, context, config, pilot, tree. root ( ) ) . await
333+ }
334+
335+ async fn search_from (
336+ & self ,
337+ tree : & DocumentTree ,
338+ context : & RetrievalContext ,
339+ config : & SearchConfig ,
340+ pilot : Option < & dyn Pilot > ,
341+ start_node : NodeId ,
342+ ) -> SearchResult {
343+ self . search_impl ( tree, context, config, pilot, start_node) . await
344+ }
336345
337346 fn name ( & self ) -> & ' static str {
338347 "beam"
0 commit comments