@@ -74,15 +74,13 @@ def _format_tool_error(function_name: str, error: Exception) -> str:
7474 )
7575
7676
77- # Tools the loop itself depends on. These are never deferred - an agent that
78- # has to search for its own `subtask_done` cannot finish a subtask.
79- # How many tools a plan may pre-load in one go. Enough to cover a typical plan
80- # without pulling in the whole catalog and undoing the saving.
77+ # Enough for a typical plan without pulling in the whole catalog.
8178PREWARM_TOOL_LIMIT = 8
8279
8380# Pre-warm matches must score at least this fraction of the best match.
8481PREWARM_MIN_SCORE_RATIO = 0.6
8582
83+ # Never deferred: searching for subtask_done would stall the loop.
8684ALWAYS_LOADED_TOOLS = frozenset (
8785 {
8886 "create_plan" ,
@@ -110,14 +108,9 @@ class AutonomousAgentLoop:
110108
111109 def __init__ (self , agent : Any ):
112110 self .agent = agent
113- # The real conversation body sent to the model: user turns, assistant
114- # turns carrying `tool_calls`, and `{"role": "tool", ...}` results.
115- # `short_memory` is kept in sync alongside it because persistence,
116- # output formatting and the final summary all read from there.
111+ # The real body sent to the model; short_memory mirrors it.
117112 self ._transcript = Transcript ()
118- # The handoff block this loop last appended to the agent's
119- # system_prompt. Held so a later run can remove it before appending the
120- # current one, instead of stacking a fresh copy every run.
113+ # Removed before the next append, so runs do not stack copies.
121114 self ._applied_handoff_block : Optional [str ] = None
122115
123116 def _say_user (self , content : str , mirror : bool = True ) -> None :
@@ -225,8 +218,7 @@ def _run_autonomous_loop(
225218 """
226219 try :
227220
228- # Reset autonomous loop state. The transcript is cleared before
229- # the task is seeded, or the opening turn would be discarded.
221+ # Cleared before seeding, or the opening turn is lost.
230222 self ._transcript = Transcript ()
231223 self .agent .autonomous_subtasks = []
232224 self .agent .current_subtask_index = 0
@@ -259,14 +251,7 @@ def _run_autonomous_loop(
259251 f"Filtered to { len (planning_tools )} tools: { [t .get ('function' , {}).get ('name' , '' ) for t in planning_tools ]} "
260252 )
261253
262- # The `think` tool is opt-in via Agent(think_tool=True). It costs a
263- # full round-trip to produce reasoning the model could emit inline
264- # alongside its actions, so it is off unless asked for.
265- #
266- # This replaces an earlier `thinking_tokens is not None` check.
267- # `thinking_tokens` defaults to 1024 rather than None, so that test
268- # was always true and silently stripped `think` for every agent -
269- # the intent was to drop it only when extended thinking was on.
254+ # Opt-in; the old thinking_tokens check was always true.
270255 if not getattr (self .agent , "think_tool" , False ):
271256 planning_tools = [
272257 t
@@ -284,9 +269,7 @@ def _run_autonomous_loop(
284269 if self .agent .tools_list_dictionary is None :
285270 self .agent .tools_list_dictionary = []
286271
287- # With dynamic_tools, only the control tools are always present.
288- # The rest of the loop's tools - file, shell, grep, sub-agents -
289- # go into the catalog and are loaded on demand via tool_search.
272+ # Only control tools stay; the rest load via tool_search.
290273 if self .agent .dynamic_tools :
291274 control = [
292275 t
@@ -328,15 +311,7 @@ def _run_autonomous_loop(
328311 self .agent .tools_list_dictionary .append (tool )
329312 existing_tool_names .add (tool_name )
330313
331- # Add handoff prompt to system prompt.
332- #
333- # This runs on every run(), so a bare append accumulated a
334- # fresh copy each time and grew the prompt without bound on a
335- # reused agent. The block this loop last applied is remembered
336- # and removed before the current one goes on, which keeps the
337- # prompt the same size across runs while still refreshing it
338- # when the registry changes — a plain "already present" guard
339- # would pin the first registry's text forever.
314+ # Removed first, so a changed roster cannot go stale.
340315 agent_registry = self .agent ._get_agent_registry ()
341316 if agent_registry :
342317 handoff_prompt = get_handoffs_prompt (
@@ -355,8 +330,7 @@ def _run_autonomous_loop(
355330 )
356331 )
357332
358- # Only append when it is not already there, so a prompt
359- # that carries the text for another reason is left alone.
333+ # Left alone if present for another reason.
360334 if handoff_block not in self .agent .system_prompt :
361335 self .agent .system_prompt += handoff_block
362336
@@ -558,8 +532,7 @@ def _run_autonomous_loop(
558532 plan_created = True
559533 break
560534
561- # Every tool_call in the assistant turn must be answered
562- # before the next request, whether or not the plan landed.
535+ # Answer every tool_call before the next request.
563536 self ._flush_tool_results (
564537 planning_calls , planning_results
565538 )
@@ -582,8 +555,7 @@ def _run_autonomous_loop(
582555 "Failed to create plan after maximum attempts"
583556 )
584557
585- # Integrate user tools after planning phase. With dynamic_tools
586- # they are already in the catalog, reachable through tool_search.
558+ # Already in the catalog when dynamic_tools is on.
587559 if (
588560 exists (self .agent .tools )
589561 and not self .agent .dynamic_tools
@@ -685,15 +657,10 @@ def _run_autonomous_loop(
685657 max_subtask_loops = MAX_SUBTASK_LOOPS
686658 subtask_done = False
687659
688- # Counts CONSECUTIVE think calls across this subtask's
689- # iterations. It is reset here, once per subtask, and again by
690- # any non-think tool call below - not once per iteration, which
691- # would only ever catch repeats inside a single response.
660+ # Consecutive across the subtask, not one response.
692661 self .agent .think_call_count = 0
693662
694- # Add the execution prompt ONCE before the inner loop so the model
695- # doesn't see duplicate copies of it on subsequent iterations and
696- # mistakenly conclude "this task has been run before."
663+ # Once only, or the model reads duplicates as a rerun.
697664 execution_prompt = get_execution_prompt (
698665 subtask_id ,
699666 subtask_desc ,
@@ -726,17 +693,14 @@ def _run_autonomous_loop(
726693 content = response ,
727694 )
728695
729- # Record the model's turn, then answer every tool call
730- # it made before the next request goes out.
696+ # Answer every call before the next request.
731697 turn_calls = self ._record_assistant (response )
732698 turn_results : Dict [str , Any ] = {}
733699
734700 # Handle tool calls
735701 if isinstance (response , list ):
736702 regular_tool_calls = []
737- # complete_task sets this instead of returning
738- # mid-loop, so tool calls batched after it are not
739- # dropped.
703+ # Set, not returned, so later calls run.
740704 task_complete = False
741705
742706 for tool_call in response :
@@ -760,9 +724,7 @@ def _run_autonomous_loop(
760724 json .JSONDecodeError ,
761725 TypeError ,
762726 ) as parse_error :
763- # Report the malformed payload back to
764- # the model instead of aborting the
765- # whole iteration on one bad call.
727+ # Report back, do not abort.
766728 self .agent .short_memory .add (
767729 role = "Tool Executor" ,
768730 content = _format_tool_error (
@@ -781,9 +743,7 @@ def _run_autonomous_loop(
781743 function_name
782744 in planning_tool_handlers
783745 ):
784- # Set when the handler raises, so a
785- # failed call is not mistaken for a
786- # completed subtask or a finished task.
746+ # A raise is not a completion.
787747 tool_failed = False
788748
789749 # Special handling for handoff_task tool
@@ -816,9 +776,7 @@ def _run_autonomous_loop(
816776 tool_error ,
817777 )
818778 else :
819- # Only pre-visualize tools that won't be shown again
820- # with their result (subtask_done / complete_task are
821- # visualized post-execution so skip the pre call).
779+ # Shown post-execution.
822780 if function_name not in (
823781 "subtask_done" ,
824782 "complete_task" ,
@@ -852,9 +810,7 @@ def _run_autonomous_loop(
852810 tool_call .get ("id" , "" )
853811 ] = result
854812
855- # Any tool that is not `think` breaks
856- # the streak, which is what makes the
857- # limit a *consecutive* one.
813+ # Non-think breaks the streak.
858814 if function_name != "think" :
859815 self .agent .think_call_count = (
860816 0
@@ -880,8 +836,7 @@ def _run_autonomous_loop(
880836 result ,
881837 )
882838
883- # Check if subtask is done. A failed
884- # handler does not complete anything.
839+ # A failure completes nothing.
885840 if (
886841 function_name
887842 == "subtask_done"
@@ -910,9 +865,7 @@ def _run_autonomous_loop(
910865 title = f"Subtask { status .title ()} : { subtask_id } " ,
911866 )
912867
913- # Check if main task is complete. The
914- # return is deferred until every tool
915- # call in this response has run.
868+ # Deferred until calls finish.
916869 if (
917870 function_name
918871 == "complete_task"
@@ -925,11 +878,7 @@ def _run_autonomous_loop(
925878 tool_call
926879 )
927880
928- # Handle all regular tools together
929- # MCP tools are served by the MCP manager, not by
930- # tool_struct, which resolves against self.tools
931- # only. Split them out first or every MCP call
932- # raises ToolNotFoundError.
881+ # MCP resolves elsewhere; split first.
933882 if regular_tool_calls :
934883 (
935884 mcp_calls ,
@@ -1178,9 +1127,7 @@ def _run_autonomous_loop(
11781127 logger .warning (
11791128 f"Too many consecutive think calls ({ self .agent .think_call_count } ). Forcing action."
11801129 )
1181- # Force action. The nudge goes into the real
1182- # transcript, not just short_memory, or the model
1183- # never sees it on the next request.
1130+ # Into the transcript, or it is unseen.
11841131 nudge = (
11851132 "You have called `think` "
11861133 f"{ self .agent .think_call_count } times in a row "
@@ -1193,18 +1140,15 @@ def _run_autonomous_loop(
11931140 )
11941141 self ._transcript .append_user (nudge )
11951142
1196- # Reset so the nudge gets a fair chance to work
1197- # before firing again on the very next iteration.
1143+ # Give the nudge a chance before refiring.
11981144 self .agent .think_call_count = 0
11991145
12001146 except Exception as e :
12011147 if self .agent .verbose :
12021148 logger .error (
12031149 f"Error in subtask execution loop: { e } "
12041150 )
1205- # Record the failure in the conversation. Without this
1206- # the next iteration rebuilds an identical prompt and
1207- # the model repeats whatever just failed.
1151+ # Without this the next prompt is identical.
12081152 self .agent .short_memory .add (
12091153 role = "Tool Executor" ,
12101154 content = (
@@ -1215,13 +1159,7 @@ def _run_autonomous_loop(
12151159 )
12161160
12171161 if not subtask_done :
1218- # A subtask that burned its whole iteration budget without
1219- # finishing is recorded as failed, not left pending. Left
1220- # pending it stays eligible, so the outer loop re-selects it
1221- # and re-runs the same doomed budget up to
1222- # MAX_SUBTASK_ITERATIONS times - 100 x 20 = 2000 LLM calls
1223- # for one stuck subtask. Failing it terminates the run,
1224- # cascades `skipped` to its dependents, and reports honestly.
1162+ # Failed, not pending; pending would re-run it.
12251163 reason = (
12261164 f"Exhausted its { max_subtask_loops } -iteration budget "
12271165 "without completing."
@@ -1345,17 +1283,13 @@ def _create_plan_tool(
13451283 incoming : Dict [str , Dict [str , Any ]] = {}
13461284 incoming_order : List [str ] = []
13471285 known_step_ids = {step .get ("step_id" , "" ) for step in steps }
1348- # A revision may reference work that already finished but is not being
1349- # restated, so those ids stay valid dependency targets.
1286+ # Finished work stays a valid dependency target.
13501287 known_step_ids |= set (existing )
13511288
13521289 for step in steps :
13531290 step_id = step .get ("step_id" , "" )
13541291
1355- # step_id values in `dependencies` are model-generated free text.
1356- # A typo or hallucinated id used to satisfy the dependency check
1357- # silently; now it is dropped, with a warning, so the plan stays
1358- # runnable instead of deadlocking on a reference to nothing.
1292+ # Model-generated ids; drop bad ones, do not deadlock.
13591293 declared = step .get ("dependencies" , []) or []
13601294 dependencies = [
13611295 dep
@@ -1381,8 +1315,7 @@ def _create_plan_tool(
13811315 }
13821316 incoming_order .append (step_id )
13831317
1384- # Merge, preserving the order the plan already had and appending
1385- # genuinely new work at the end.
1318+ # Keep existing order, append new work at the end.
13861319 merged : List [Dict [str , Any ]] = []
13871320 added , updated , removed , retained = [], [], [], []
13881321
@@ -1446,8 +1379,7 @@ def _create_plan_tool(
14461379 )
14471380 return message
14481381
1449- # A revision reports what changed, not the whole plan, so the model
1450- # can see the effect of its edit.
1382+ # Reports the change, not the whole plan.
14511383 diff_parts = []
14521384 if added :
14531385 diff_parts .append (f"added { added } " )
@@ -1484,8 +1416,7 @@ def _mcp_tool_names(self) -> set:
14841416 if not getattr (agent , "mcp_enabled" , False ):
14851417 return set ()
14861418
1487- # Prefer the cache the dynamic loader already populated, so this does
1488- # not add a network call per turn.
1419+ # Use the loader's cache to avoid a call per turn.
14891420 schemas = getattr (agent , "_mcp_schemas_cache" , None )
14901421 if schemas is None :
14911422 try :
@@ -1582,10 +1513,7 @@ def _prewarm_tools_from_plan(
15821513 agent ._tool_search_tool (
15831514 query = query ,
15841515 max_results = PREWARM_TOOL_LIMIT ,
1585- # Speculative, so it demands stronger relevance than an explicit
1586- # search. A whole plan as the query contains enough common words
1587- # ("task", "data", "current") to give unrelated tools a nonzero
1588- # score, which would load the catalog and undo the saving.
1516+ # Speculative, so relevance must beat an explicit search.
15891517 min_score_ratio = PREWARM_MIN_SCORE_RATIO ,
15901518 )
15911519 return [
@@ -1791,17 +1719,11 @@ def _get_next_executable_subtask(
17911719 for dep in dependencies
17921720 ]
17931721
1794- # Only an actually completed dependency satisfies. A failed one
1795- # cannot produce the output its dependents were planned around,
1796- # and an unknown id (None) means the reference is unresolvable --
1797- # neither should unblock execution.
1722+ # Only completed unblocks; failed and unknown do not.
17981723 if all (status == "completed" for status in statuses ):
17991724 return subtask
18001725
1801- # Dependencies that failed or were skipped can never complete, so
1802- # this subtask is unreachable. Mark it skipped rather than leaving
1803- # it pending forever, so the run terminates and the final summary
1804- # can report what was not attempted.
1726+ # Unreachable: skip so the run can terminate.
18051727 blockers = [
18061728 dep
18071729 for dep , status in zip (dependencies , statuses )
0 commit comments