@@ -80,6 +80,30 @@ impl std::fmt::Display for LoopError {
8080
8181impl std:: error:: Error for LoopError { }
8282
83+ /// Build a `StormBreaker` from `LoopConfig`, merging custom
84+ /// mutating/exempt tool name lists with the built-in defaults.
85+ fn storm_for_config ( config : & LoopConfig ) -> StormBreaker {
86+ let has_custom = config. storm_mutating_tools . is_some ( ) || config. storm_exempt_tools . is_some ( ) ;
87+ if !has_custom {
88+ return StormBreaker :: default ( ) ;
89+ }
90+ let mutating: Option < Box < dyn Fn ( & super :: tools:: ToolCall ) -> bool + Send + Sync > > =
91+ config. storm_mutating_tools . as_ref ( ) . map ( |extras| {
92+ let extra_set: std:: collections:: HashSet < String > = extras. iter ( ) . cloned ( ) . collect ( ) ;
93+ Box :: new ( move |c : & super :: tools:: ToolCall | {
94+ super :: storm:: default_mutating ( c) || extra_set. contains ( & c. name )
95+ } ) as Box < dyn Fn ( & super :: tools:: ToolCall ) -> bool + Send + Sync >
96+ } ) ;
97+ let exempt: Option < Box < dyn Fn ( & super :: tools:: ToolCall ) -> bool + Send + Sync > > =
98+ config. storm_exempt_tools . as_ref ( ) . map ( |extras| {
99+ let extra_set: std:: collections:: HashSet < String > = extras. iter ( ) . cloned ( ) . collect ( ) ;
100+ Box :: new ( move |c : & super :: tools:: ToolCall | {
101+ super :: storm:: default_exempt ( c) || extra_set. contains ( & c. name )
102+ } ) as Box < dyn Fn ( & super :: tools:: ToolCall ) -> bool + Send + Sync >
103+ } ) ;
104+ StormBreaker :: new ( 6 , 3 , mutating, exempt)
105+ }
106+
83107/// Public entry point: start a new run from one or more prompt
84108/// messages. Faithful port of pi `runAgentLoop` (agent-loop.ts:95).
85109///
@@ -133,7 +157,7 @@ pub async fn run_agent_loop(
133157/// - otherwise → emit agent_start + turn_start, enter loop with
134158/// newMessages = [] (does NOT re-emit user-message events)
135159pub async fn run_agent_loop_continue (
136- context : Context ,
160+ mut context : Context ,
137161 config : LoopConfig ,
138162 signal : AbortSignal ,
139163 emit : & mpsc:: Sender < LoopEvent > ,
@@ -143,10 +167,7 @@ pub async fn run_agent_loop_continue(
143167 if context. messages . is_empty ( ) {
144168 return Err ( LoopError :: NoMessages ) ;
145169 }
146- // Pi lines 131-133: last-message role check. Phase 4 reads
147- // the role string from the placeholder `Vec<Value>` shape
148- // since that's what context.messages carries. Phase ??? may
149- // substitute typed messages.
170+ // Pi lines 131-133: last-message role check.
150171 let last_role = context
151172 . messages
152173 . last ( )
@@ -157,6 +178,15 @@ pub async fn run_agent_loop_continue(
157178 return Err ( LoopError :: CannotContinueFromAssistant ) ;
158179 }
159180
181+ // Heal messages before entering the loop (shrink oversized tool
182+ // results, drop unpaired tool calls). Matches the healing done
183+ // in spawn_loop_runner for the session-restore path.
184+ let heal_result =
185+ super :: heal:: heal_loaded_messages ( & context. messages , super :: heal:: DEFAULT_MAX_RESULT_CHARS ) ;
186+ if heal_result. healed_count > 0 {
187+ context. messages = heal_result. messages ;
188+ }
189+
160190 // Pi lines 135-139: newMessages = []; emit agent_start +
161191 // turn_start; enter loop.
162192 let new_messages: Vec < LoopMessage > = Vec :: new ( ) ;
@@ -192,7 +222,7 @@ pub async fn run_loop(
192222 // Storm breaker: tracks (tool_name, args) repeats to detect
193223 // stuck-in-a-loop behavior. Reset each new user turn.
194224 // Port of Reasonix `repair/index.ts:38-46` + `loop.ts:621`.
195- let mut storm = StormBreaker :: default ( ) ;
225+ let mut storm = storm_for_config ( & config ) ;
196226
197227 // Inflight set: authoritative running-id tracker.
198228 // UI cards consult `inflight.has(call_id)` to derive spinner state.
@@ -415,9 +445,10 @@ pub async fn run_loop(
415445 has_more_tool_calls = false ;
416446 }
417447
418- // Dispatch surviving calls.
448+ // Dispatch surviving calls through the unified dispatch.
449+ // `execute_tool_calls` takes pre-extracted tool calls.
419450 if !surviving_calls. is_empty ( ) {
420- let batch = execute_tool_calls_with (
451+ let batch = super :: tools :: execute_tool_calls (
421452 & current_context,
422453 & assistant_msg,
423454 & surviving_calls,
@@ -588,53 +619,6 @@ fn extract_tool_calls_from(msg: &AssistantMessage) -> Vec<super::tools::ToolCall
588619 super :: tools:: extract_tool_calls ( msg)
589620}
590621
591- /// Dispatch pre-filtered tool calls (after storm breaker has
592- /// removed suppressed calls). Mirrors the dispatch logic in
593- /// `execute_tool_calls` but takes the calls directly instead of
594- /// extracting from the assistant message.
595- async fn execute_tool_calls_with (
596- context : & Context ,
597- assistant_message : & AssistantMessage ,
598- tool_calls : & [ super :: tools:: ToolCall ] ,
599- config : & LoopConfig ,
600- signal : & AbortSignal ,
601- emit : & mpsc:: Sender < LoopEvent > ,
602- inflight : & InflightSet ,
603- ) -> super :: tools:: ExecutedToolCallBatch {
604- use super :: ExecutedToolCallBatch ;
605- let has_sequential = tool_calls. iter ( ) . any ( |tc| {
606- context
607- . tools
608- . iter ( )
609- . find ( |t| t. name ( ) == tc. name )
610- . and_then ( |t| t. execution_mode ( ) )
611- == Some ( super :: ToolExecutionMode :: Sequential )
612- } ) ;
613- if config. tool_execution == super :: ToolExecutionMode :: Sequential || has_sequential {
614- super :: tools:: execute_tool_calls_sequential (
615- context,
616- assistant_message,
617- tool_calls,
618- config,
619- signal,
620- emit,
621- inflight,
622- )
623- . await
624- } else {
625- super :: tools:: execute_tool_calls_parallel (
626- context,
627- assistant_message,
628- tool_calls,
629- config,
630- signal,
631- emit,
632- inflight,
633- )
634- . await
635- }
636- }
637-
638622/// Convert a `LoopMessage` to the placeholder `Value` shape used
639623/// in `Context.messages`. Mirrors `serialize_assistant` from
640624/// stream.rs but covers every variant.
@@ -755,6 +739,8 @@ mod tests {
755739 provider_name : None ,
756740 model_name : None ,
757741 compact_model : None ,
742+ storm_mutating_tools : None ,
743+ storm_exempt_tools : None ,
758744 }
759745 }
760746
0 commit comments