@@ -131,6 +131,8 @@ export async function runPlannerLoop(
131131 const failures : FailureLike [ ] = [ ] ;
132132 let terminalOnlyContinuations = 0 ;
133133 let requiredToolMisses = 0 ;
134+ let unavailableToolCallRetries = 0 ;
135+ let silentFailedFinishRecoveries = 0 ;
134136 const requireNonTerminalToolCall =
135137 params . requireNonTerminalToolCall === true &&
136138 hasExposedNonTerminalTool ( params . tools ) ;
@@ -218,27 +220,19 @@ export async function runPlannerLoop(
218220 ! hasExecutedNonTerminalTool ( trajectory )
219221 ) {
220222 requiredToolMisses ++ ;
221- if ( requiredToolMisses > config . maxRequiredToolMisses ) {
222- params . runtime . logger ?. warn ?.(
223- {
224- src : "planner-loop" ,
225- iteration,
226- misses : requiredToolMisses ,
227- max : config . maxRequiredToolMisses ,
228- messageToUser : plannerOutput . messageToUser ,
229- } ,
230- "required_tool_misses budget exhausted; honoring planner's terminal reply instead of throwing" ,
231- ) ;
232- } else {
233- handleRequiredToolPlannerMiss ( {
234- trajectory,
235- iteration,
236- plannerOutput,
237- reason : "no_tool_calls" ,
238- logger : params . runtime . logger ,
239- } ) ;
240- continue ;
241- }
223+ assertTrajectoryLimit ( {
224+ kind : "required_tool_misses" ,
225+ max : config . maxRequiredToolMisses ,
226+ observed : requiredToolMisses ,
227+ } ) ;
228+ handleRequiredToolPlannerMiss ( {
229+ trajectory,
230+ iteration,
231+ plannerOutput,
232+ reason : "no_tool_calls" ,
233+ logger : params . runtime . logger ,
234+ } ) ;
235+ continue ;
242236 }
243237 trajectory . steps . push ( {
244238 iteration,
@@ -326,27 +320,19 @@ export async function runPlannerLoop(
326320 ! hasExecutedNonTerminalTool ( trajectory )
327321 ) {
328322 requiredToolMisses ++ ;
329- if ( requiredToolMisses > config . maxRequiredToolMisses ) {
330- params . runtime . logger ?. warn ?.(
331- {
332- src : "planner-loop" ,
333- iteration,
334- misses : requiredToolMisses ,
335- max : config . maxRequiredToolMisses ,
336- messageToUser : plannerOutput . messageToUser ,
337- } ,
338- "required_tool_misses budget exhausted; honoring planner's terminal tool-call reply instead of throwing" ,
339- ) ;
340- } else {
341- handleRequiredToolPlannerMiss ( {
342- trajectory,
343- iteration,
344- plannerOutput,
345- reason : "terminal_only_tool_calls" ,
346- logger : params . runtime . logger ,
347- } ) ;
348- continue ;
349- }
323+ assertTrajectoryLimit ( {
324+ kind : "required_tool_misses" ,
325+ max : config . maxRequiredToolMisses ,
326+ observed : requiredToolMisses ,
327+ } ) ;
328+ handleRequiredToolPlannerMiss ( {
329+ trajectory,
330+ iteration,
331+ plannerOutput,
332+ reason : "terminal_only_tool_calls" ,
333+ logger : params . runtime . logger ,
334+ } ) ;
335+ continue ;
350336 }
351337 const finalMessage = terminalMessageFromToolCalls (
352338 plannerOutput . toolCalls ,
@@ -388,12 +374,43 @@ export async function runPlannerLoop(
388374 const nonTerminalCalls = plannerOutput . toolCalls
389375 . filter ( ( toolCall ) => ! isTerminalToolCall ( toolCall ) )
390376 . map ( ( toolCall , index ) => ensureToolCallId ( toolCall , iteration , index ) ) ;
391- trajectory . plannedQueue . push ( ...nonTerminalCalls ) ;
377+ const unavailable = splitUnavailableToolCalls (
378+ nonTerminalCalls ,
379+ params . tools ,
380+ ) ;
381+ if ( unavailable . invalid . length > 0 ) {
382+ params . runtime . logger ?. warn ?.(
383+ {
384+ iteration,
385+ invalidToolCalls : unavailable . invalid . map (
386+ ( toolCall ) => toolCall . name ,
387+ ) ,
388+ } ,
389+ "Planner called unavailable tools; retrying without executing them" ,
390+ ) ;
391+ trajectory . context = appendUnavailableToolCallEvent ( {
392+ context : trajectory . context ,
393+ iteration,
394+ invalidToolCalls : unavailable . invalid ,
395+ tools : params . tools ,
396+ } ) ;
397+ if ( unavailable . valid . length === 0 ) {
398+ unavailableToolCallRetries ++ ;
399+ assertTrajectoryLimit ( {
400+ kind : "unavailable_tool_calls" ,
401+ max : config . maxUnavailableToolCallRetries ,
402+ observed : unavailableToolCallRetries ,
403+ } ) ;
404+ continue ;
405+ }
406+ }
407+ const validNonTerminalCalls = unavailable . valid ;
408+ trajectory . plannedQueue . push ( ...validNonTerminalCalls ) ;
392409 trajectory . context = {
393410 ...trajectory . context ,
394411 plannedQueue : [
395412 ...( trajectory . context . plannedQueue ?? [ ] ) ,
396- ...nonTerminalCalls . map ( ( toolCall ) => ( {
413+ ...validNonTerminalCalls . map ( ( toolCall ) => ( {
397414 id : toolCall . id ,
398415 name : toolCall . name ,
399416 args : stringifyForModel ( toolCall . params ?? { } ) ,
@@ -402,7 +419,7 @@ export async function runPlannerLoop(
402419 } ) ) ,
403420 ] ,
404421 } ;
405- for ( const toolCall of nonTerminalCalls ) {
422+ for ( const toolCall of validNonTerminalCalls ) {
406423 trajectory . context = appendContextEvent ( trajectory . context , {
407424 id : `queue:${ toolCall . id ?? toolCall . name } :${ iteration } ` ,
408425 type : "planned_tool_call" ,
@@ -503,12 +520,32 @@ export async function runPlannerLoop(
503520 appendEvaluatorContextEvent ( trajectory , evaluator , iteration ) ;
504521
505522 if ( evaluator . decision === "FINISH" ) {
523+ if (
524+ shouldRecoverSilentFailedFinish ( {
525+ evaluator,
526+ trajectory,
527+ recoveryCount : silentFailedFinishRecoveries ,
528+ } )
529+ ) {
530+ silentFailedFinishRecoveries ++ ;
531+ trajectory . context = appendSilentFailedFinishRecoveryEvent ( {
532+ context : trajectory . context ,
533+ iteration,
534+ evaluator,
535+ trajectory,
536+ } ) ;
537+ continue ;
538+ }
506539 return {
507540 status : "finished" ,
508541 trajectory,
509542 evaluator,
510543 finalMessage : userSafeFinalMessage (
511- evaluator . messageToUser ?? latestToolResultText ( trajectory ) ,
544+ evaluator . messageToUser ??
545+ latestToolResultText ( trajectory ) ??
546+ ( evaluator . success === false
547+ ? failedToolFallbackMessage ( trajectory )
548+ : undefined ) ,
512549 trajectory ,
513550 ) ,
514551 } ;
@@ -1636,6 +1673,7 @@ async function executeQueuedToolCall(params: {
16361673 toolName : params . toolCall . name ,
16371674 success : result . success ,
16381675 error : result . error ,
1676+ repeatKey : toolFailureRepeatKey ( params . toolCall ) ,
16391677 } ;
16401678 if ( ! result . success || result . error != null ) {
16411679 params . failures . push ( failure ) ;
0 commit comments