Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,11 @@ agent.run("What are the key benefits of using a multi-agent system?")

### Autonomous Agent with `max_loops="auto"`

Setting `max_loops="auto"` lets the agent decide for itself when the task is complete — it keeps reasoning and acting until it reaches a stopping condition, rather than halting after a fixed number of iterations. This is the recommended mode for open-ended, multi-step tasks where the number of steps isn't known in advance.
Setting `max_loops="auto"` lets the agent decide for itself when the task is
complete instead of halting after one fixed loop count. Configurable run and
subtask ceilings still bound unattended execution. This is the recommended
mode for open-ended, multi-step tasks where the number of steps isn't known in
advance.

```python
from swarms import Agent
Expand All @@ -147,7 +151,10 @@ agent = Agent(
"execute each step thoroughly, and signal completion only when the full task is done."
),
model_name="gpt-5.4",
max_loops="auto", # Agent decides when it's done — no fixed iteration cap
max_loops="auto", # Agent decides when the work is complete
max_run_tokens=50_000, # Optional estimated text-token ceiling for this run
max_subtask_iterations=50, # Bound total subtask selections in the run
max_subtask_loops=12, # Bound the LLM turns spent on any one subtask
autosave=True,
verbose=True,
)
Expand All @@ -161,6 +168,11 @@ result = agent.run(
print(result)
```

`max_run_tokens` is checked before each autonomous model request and includes
locally estimated text from the system prompt, messages, tool schemas, and
responses. Provider-reported usage remains authoritative; image, audio, cache,
and hidden reasoning tokens are not included in the local estimate.

**When to use `max_loops="auto"`:**
- Open-ended research or analysis tasks
- Tasks that require iterative refinement (e.g., write → review → revise)
Expand Down
138 changes: 131 additions & 7 deletions swarms/agents/autonomous_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@
from swarms.prompts.handoffs_prompt import get_handoffs_prompt
from swarms.structs.autonomous_loop_utils import (
MAX_PLANNING_ATTEMPTS,
MAX_SUBTASK_ITERATIONS,
MAX_SUBTASK_LOOPS,
assign_task_tool,
cancel_sub_agent_tasks_tool,
check_sub_agent_status_tool,
Expand All @@ -54,6 +52,7 @@
from swarms.structs.transcript import Transcript
from swarms.utils.formatter import formatter
from swarms.utils.index import exists, format_data_structure
from swarms.utils.litellm_tokenizer import count_tokens


def _format_tool_error(function_name: str, error: Exception) -> str:
Expand All @@ -73,6 +72,10 @@ def _format_tool_error(function_name: str, error: Exception) -> str:
)


class AutonomousRunBudgetExceeded(RuntimeError):
"""Raised before an autonomous LLM call would exceed its run budget."""


class AutonomousAgentLoop:
"""
Plan-execute-summarize loop used when ``max_loops="auto"``.
Expand Down Expand Up @@ -127,6 +130,119 @@ def _map_batch_results(
formatter=format_data_structure,
)

@staticmethod
def _stringify_for_token_count(value: Any) -> str:
"""Return a stable text representation for local token estimation."""
if isinstance(value, str):
return value
return json.dumps(value, sort_keys=True, default=str)

def _estimate_request_tokens(
self,
task: Any = None,
messages: Optional[List[Dict[str, Any]]] = None,
) -> int:
"""Estimate request text, system prompt, and tool-schema tokens."""
request_parts = [self.agent.system_prompt]
if task is not None:
request_parts.append(
self._stringify_for_token_count(task)
)
if messages:
request_parts.append(
self._stringify_for_token_count(messages)
)
if self.agent.tools_list_dictionary:
request_parts.append(
self._stringify_for_token_count(
self.agent.tools_list_dictionary
)
)
return count_tokens(
"\n".join(part for part in request_parts if part),
model=self.agent.model_name,
)

def _call_llm_with_budget(
self, task: Any = None, *args, **kwargs
) -> Any:
"""Call the LLM while enforcing and recording the autonomous text budget."""
prompt_tokens = self._estimate_request_tokens(
task=task, messages=kwargs.get("messages")
)
token_budget = self.agent.max_run_tokens

if token_budget is not None:
remaining = (
token_budget - self.agent.autonomous_run_token_count
)
output_allowance = remaining - prompt_tokens
if output_allowance < 1:
self.agent.autonomous_budget_exhausted = True
raise AutonomousRunBudgetExceeded

requested_max_tokens = kwargs.get(
"max_tokens", self.agent.max_tokens
)
if (
requested_max_tokens is None
or requested_max_tokens > output_allowance
):
kwargs["max_tokens"] = output_allowance

# Count before dispatch: a provider can accept and bill a request even
# if response processing later fails locally.
self.agent.autonomous_run_token_count += prompt_tokens
self.agent.autonomous_run_call_count += 1
response = self.agent.call_llm(task=task, *args, **kwargs)
self.agent.autonomous_run_token_count += count_tokens(
self._stringify_for_token_count(response),
model=self.agent.model_name,
)

if (
token_budget is not None
and self.agent.autonomous_run_token_count >= token_budget
):
self.agent.autonomous_budget_exhausted = True
return response

def _usage_report(self) -> str:
"""Format the locally estimated autonomous-run usage."""
budget = self.agent.max_run_tokens
budget_text = (
f" / {budget} configured" if budget is not None else ""
)
return (
"\n\nAutonomous Run Usage:\n"
f"- LLM calls completed: {self.agent.autonomous_run_call_count}\n"
"- Estimated text tokens (requests, tools, and responses): "
f"{self.agent.autonomous_run_token_count}{budget_text}\n"
"- Provider-reported billing remains authoritative; image, audio, "
"cache, and hidden reasoning tokens are not included in this local estimate."
)

def _budget_exhausted_summary(self) -> str:
"""Return a deterministic summary without another LLM request."""
summary = (
"Task Execution Summary\n\n"
"The autonomous run stopped before another LLM request because "
"the configured max_run_tokens budget would have been exceeded."
)
if self.agent.autonomous_subtasks:
summary += "\n\nSubtask Breakdown:\n"
for subtask in self.agent.autonomous_subtasks:
summary += (
f"- {subtask.get('step_id', 'unknown')}: "
f"{subtask.get('status', 'unknown')} - "
f"{subtask.get('description', '')}\n"
)
summary += self._usage_report()
self.agent.short_memory.add(
role=self.agent.agent_name, content=summary
)
return summary

def _run_autonomous_loop(
self,
task: Optional[Union[str, Any]] = None,
Expand Down Expand Up @@ -208,6 +324,9 @@ def _run_autonomous_loop(
self.agent.subtask_status = {}
self.agent.plan_created = False
self.agent.think_call_count = 0
self.agent.autonomous_run_token_count = 0
self.agent.autonomous_run_call_count = 0
self.agent.autonomous_budget_exhausted = False

self._say_user(task)

Expand Down Expand Up @@ -383,7 +502,7 @@ def _run_autonomous_loop(
):
planning_attempts += 1
try:
response = self.agent.call_llm(
response = self._call_llm_with_budget(
task=None,
img=img,
current_loop=0,
Expand Down Expand Up @@ -502,6 +621,8 @@ def _run_autonomous_loop(
plan_created = True
break

except AutonomousRunBudgetExceeded:
return self._budget_exhausted_summary()
except Exception as e:
if self.agent.verbose:
logger.error(
Expand Down Expand Up @@ -565,7 +686,7 @@ def _run_autonomous_loop(
title="Autonomous Loop: Execution Phase",
)

max_subtask_iterations = MAX_SUBTASK_ITERATIONS
max_subtask_iterations = self.agent.max_subtask_iterations
total_iterations = 0

while not self._all_subtasks_complete():
Expand Down Expand Up @@ -611,7 +732,7 @@ def _run_autonomous_loop(

# Subtask execution loop: thinking -> tool actions -> observation
subtask_iterations = 0
max_subtask_loops = MAX_SUBTASK_LOOPS
max_subtask_loops = self.agent.max_subtask_loops
subtask_done = False

# Counts CONSECUTIVE think calls across this subtask's
Expand All @@ -637,7 +758,7 @@ def _run_autonomous_loop(
subtask_iterations += 1

try:
response = self.agent.call_llm(
response = self._call_llm_with_budget(
task=None,
img=img,
current_loop=subtask_iterations,
Expand Down Expand Up @@ -1108,6 +1229,8 @@ def _run_autonomous_loop(
# before firing again on the very next iteration.
self.agent.think_call_count = 0

except AutonomousRunBudgetExceeded:
return self._budget_exhausted_summary()
except Exception as e:
if self.agent.verbose:
logger.error(
Expand Down Expand Up @@ -1164,7 +1287,8 @@ def _run_autonomous_loop(
)

return self.agent._generate_final_summary(
streaming_callback=streaming_callback
streaming_callback=streaming_callback,
messages=self._transcript.messages,
)

except Exception as error:
Expand Down
59 changes: 52 additions & 7 deletions swarms/structs/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@
)
from swarms.agents.ape_agent import auto_generate_prompt
from swarms.agents.context_compressor import ContextCompressor
from swarms.agents.autonomous_loop import AutonomousAgentLoop
from swarms.agents.autonomous_loop import (
AutonomousAgentLoop,
AutonomousRunBudgetExceeded,
)
from swarms.agents.llm_manager import LLMManager
from swarms.agents.skills_manager import SkillsManager
from swarms.prompts.agent_system_prompts import AGENT_SYSTEM_PROMPT_3
Expand Down Expand Up @@ -206,6 +209,10 @@ class Agent:
"create_sub_agent", "assign_task".
Defaults to "all" (all tools enabled). Pass a list of tool names to restrict tools, or "all"
for unrestricted access. Use this to control which tools the agent can use during autonomous execution.
max_run_tokens (int): Optional estimated text-token budget for one autonomous run.
Checked before every autonomous LLM call. Provider billing remains authoritative.
max_subtask_iterations (int): Maximum outer autonomous execution iterations.
max_subtask_loops (int): Maximum LLM iterations for one autonomous subtask.
prompt_caching (bool): Enable provider-side prompt caching. When True, ephemeral
cache_control breakpoints are added to the stable prefix of each request (system
prompt, tools, and the last message) so it is cached and re-billed at a discount.
Expand Down Expand Up @@ -406,6 +413,9 @@ def __init__(
selected_tools: Optional[Union[str, List[str]]] = "all",
context_compression: bool = True,
persistent_memory: bool = False,
max_run_tokens: Optional[int] = None,
max_subtask_iterations: int = 100,
max_subtask_loops: int = 20,
*args,
**kwargs,
):
Expand All @@ -415,6 +425,25 @@ def __init__(
self.selected_tools = selected_tools
self.llm = llm
self.max_loops = max_loops
for limit_name, limit_value in (
("max_run_tokens", max_run_tokens),
("max_subtask_iterations", max_subtask_iterations),
("max_subtask_loops", max_subtask_loops),
):
if limit_value is not None and (
isinstance(limit_value, bool)
or not isinstance(limit_value, int)
or limit_value < 1
):
raise ValueError(
f"{limit_name} must be a positive integer"
)
self.max_run_tokens = max_run_tokens
self.max_subtask_iterations = max_subtask_iterations
self.max_subtask_loops = max_subtask_loops
self.autonomous_run_token_count = 0
self.autonomous_run_call_count = 0
self.autonomous_budget_exhausted = False
self.stopping_condition = stopping_condition
self.loop_interval = loop_interval
self.retry_attempts = retry_attempts
Expand Down Expand Up @@ -2040,11 +2069,18 @@ def _generate_final_summary(
"task": self.short_memory.return_history_as_string()
}

response = self.call_llm(
current_loop=0,
streaming_callback=streaming_callback,
**call_kwargs,
)
if self.max_loops == "auto":
response = self.autonomous_loop._call_llm_with_budget(
current_loop=0,
streaming_callback=streaming_callback,
**call_kwargs,
)
else:
response = self.call_llm(
current_loop=0,
streaming_callback=streaming_callback,
**call_kwargs,
)

response = self.parse_llm_output(response)

Expand Down Expand Up @@ -2085,7 +2121,10 @@ def _generate_final_summary(
title="Task Completion Summary",
)

return result
return (
result
+ self.autonomous_loop._usage_report()
)

# If complete_task wasn't called, generate summary manually
comprehensive_summary = f"""Task Execution Summary
Expand All @@ -2107,6 +2146,10 @@ def _generate_final_summary(
)

comprehensive_summary += f"\nFinal Response:\n{response}"
if self.max_loops == "auto":
comprehensive_summary += (
self.autonomous_loop._usage_report()
)

self.short_memory.add(
role=self.agent_name, content=comprehensive_summary
Expand All @@ -2122,6 +2165,8 @@ def _generate_final_summary(
self.short_memory, type=self.output_type
)

except AutonomousRunBudgetExceeded:
return self.autonomous_loop._budget_exhausted_summary()
except Exception as e:
if self.verbose:
logger.error(f"Error generating final summary: {e}")
Expand Down
Loading