mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-07 12:47:55 -10:00
fix: expose max-budget on praisonai loop and pass YAML execution block through (fixes #6206)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
032e36891b
commit
91f0cc7727
5 files changed
+128
-2
No files matched your search
@@ -957,7 +957,8 @@ def _validate_feature_config(feature: str, config: Dict[str, Any]) -> List[str]:
|
||||
"guardrails": {"enabled", "max_retries", "validator"},
|
||||
"web": {"enabled", "search", "fetch", "provider"},
|
||||
"output": {"enabled", "verbose", "markdown", "stream", "metrics", "preset"},
|
||||
"execution": {"enabled", "max_iter", "max_rpm", "max_execution_time", "preset"},
|
||||
"execution": {"enabled", "max_iter", "max_rpm", "max_execution_time", "preset",
|
||||
"max_budget", "on_budget_exceeded", "durable", "journal_path", "resume_run_id"},
|
||||
"caching": {"enabled", "prompt_caching", "ttl"},
|
||||
"autonomy": {"enabled", "max_steps", "approval_required"},
|
||||
"context": {"enabled", "max_tokens", "strategy"},
|
||||
|
||||
@@ -591,7 +591,11 @@ class YAMLWorkflowParser:
|
||||
# so we don't override the Agent's own caching default.
|
||||
if cache is False:
|
||||
agent_kwargs['caching'] = False
|
||||
_execution: Dict[str, Any] = {}
|
||||
# An agent-level `execution:` mapping passes straight through to
|
||||
# ExecutionConfig (e.g. max_budget, on_budget_exceeded, durable,
|
||||
# journal_path, resume_run_id); flat max_iter/max_execution_time win.
|
||||
_yaml_execution = config.get('execution')
|
||||
_execution: Dict[str, Any] = dict(_yaml_execution) if isinstance(_yaml_execution, dict) else {}
|
||||
if max_iter is not None:
|
||||
_execution['max_iter'] = max_iter
|
||||
if max_execution_time is not None:
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
"""An agent's YAML `execution:` block reaches ExecutionConfig (Issue #6206)."""
|
||||
from praisonaiagents.workflows.yaml_parser import YAMLWorkflowParser
|
||||
|
||||
|
||||
def test_execution_block_passes_budget_and_durable_keys():
|
||||
agent = YAMLWorkflowParser()._create_agent("worker", {
|
||||
"role": "Worker",
|
||||
"execution": {
|
||||
"max_budget": 1.5,
|
||||
"on_budget_exceeded": "warn",
|
||||
"durable": True,
|
||||
"resume_run_id": "run-123",
|
||||
},
|
||||
"max_iter": 7,
|
||||
})
|
||||
assert agent._max_budget == 1.5
|
||||
assert agent._on_budget_exceeded == "warn"
|
||||
assert agent.execution.durable is True
|
||||
assert agent.execution.resume_run_id == "run-123"
|
||||
assert agent.execution.max_iter == 7
|
||||
|
||||
|
||||
def test_flat_max_iter_overrides_execution_block():
|
||||
agent = YAMLWorkflowParser()._create_agent("worker", {
|
||||
"execution": {"max_iter": 3},
|
||||
"max_iter": 9,
|
||||
})
|
||||
assert agent.execution.max_iter == 9
|
||||
|
||||
|
||||
def test_no_execution_block_keeps_defaults():
|
||||
agent = YAMLWorkflowParser()._create_agent("worker", {"role": "Worker"})
|
||||
assert not agent._max_budget
|
||||
@@ -31,6 +31,8 @@ def loop_main(
|
||||
observe: bool = typer.Option(False, "--observe", "-o", help="Enable observability event logging"),
|
||||
recovery: bool = typer.Option(True, "--recovery/--no-recovery", help="Enable graduated doom loop recovery (default: on)"),
|
||||
level: Optional[str] = typer.Option(None, "--level", "-l", help="Autonomy level: suggest, auto_edit, full_auto"),
|
||||
max_budget: Optional[float] = typer.Option(None, "--max-budget", help="Hard USD spend cap for the whole run"),
|
||||
on_budget_exceeded: str = typer.Option("stop", "--on-budget-exceeded", help="When --max-budget is hit: stop or warn"),
|
||||
):
|
||||
"""Run an agent in an autonomous loop.
|
||||
|
||||
@@ -66,6 +68,8 @@ def loop_main(
|
||||
observe=observe,
|
||||
recovery=recovery,
|
||||
level=level,
|
||||
max_budget=max_budget,
|
||||
on_budget_exceeded=on_budget_exceeded,
|
||||
)
|
||||
|
||||
|
||||
@@ -80,8 +84,13 @@ def _run_loop(
|
||||
observe: bool = False,
|
||||
recovery: bool = True,
|
||||
level: Optional[str] = None,
|
||||
max_budget: Optional[float] = None,
|
||||
on_budget_exceeded: str = "stop",
|
||||
):
|
||||
"""Execute the autonomous loop."""
|
||||
if on_budget_exceeded not in ("stop", "warn"):
|
||||
typer.echo("Error: --on-budget-exceeded must be 'stop' or 'warn'", err=True)
|
||||
raise typer.Exit(2)
|
||||
try:
|
||||
from praisonaiagents import Agent
|
||||
except ImportError:
|
||||
@@ -117,6 +126,13 @@ def _run_loop(
|
||||
|
||||
if not verbose:
|
||||
agent_kwargs["output"] = "silent"
|
||||
|
||||
# Only forward a budget when the user set one (None = no cap, zero overhead).
|
||||
if max_budget is not None:
|
||||
agent_kwargs["execution"] = {
|
||||
"max_budget": max_budget,
|
||||
"on_budget_exceeded": on_budget_exceeded,
|
||||
}
|
||||
|
||||
# Add completion promise instructions if provided
|
||||
if completion_promise:
|
||||
@@ -138,6 +154,8 @@ def _run_loop(
|
||||
typer.echo(" Context clearing: enabled")
|
||||
if timeout:
|
||||
typer.echo(f" Timeout: {timeout}s")
|
||||
if max_budget is not None:
|
||||
typer.echo(f" Max budget: ${max_budget} ({on_budget_exceeded})")
|
||||
typer.echo("")
|
||||
|
||||
# Run autonomous loop using unified API
|
||||
@@ -200,6 +218,8 @@ OPTIONS:
|
||||
-p, --completion-promise STR Promise text to signal completion
|
||||
-c, --clear-context Clear chat history between iterations
|
||||
-t, --timeout FLOAT Timeout in seconds
|
||||
--max-budget FLOAT Hard USD spend cap for the whole run
|
||||
--on-budget-exceeded STR stop (default) or warn when the cap is hit
|
||||
-m, --model STR LLM model to use
|
||||
-v, --verbose Show verbose output
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
"""`praisonai loop --max-budget` reaches the agent's ExecutionConfig (Issue #6206)."""
|
||||
import pytest
|
||||
|
||||
pytest.importorskip("typer")
|
||||
|
||||
import typer
|
||||
|
||||
from praisonai_code.cli.commands import loop as loop_module
|
||||
|
||||
|
||||
class _Result:
|
||||
success = True
|
||||
completion_reason = "promise"
|
||||
iterations = 1
|
||||
duration_seconds = 0.1
|
||||
started_at = None
|
||||
output = "done"
|
||||
error = None
|
||||
|
||||
|
||||
def _capture(monkeypatch):
|
||||
captured = {}
|
||||
|
||||
class FakeAgent:
|
||||
def __init__(self, **kwargs):
|
||||
captured["agent_kwargs"] = kwargs
|
||||
|
||||
def start(self, prompt, **kwargs):
|
||||
return _Result()
|
||||
|
||||
import praisonaiagents
|
||||
monkeypatch.setattr(praisonaiagents, "Agent", FakeAgent)
|
||||
return captured
|
||||
|
||||
|
||||
def _run(**extra):
|
||||
loop_module._run_loop(
|
||||
prompt="build it", max_iterations=2, completion_promise=None,
|
||||
clear_context=False, timeout=None, model=None, verbose=False, **extra,
|
||||
)
|
||||
|
||||
|
||||
def test_max_budget_forwarded_to_execution(monkeypatch):
|
||||
captured = _capture(monkeypatch)
|
||||
_run(max_budget=0.5, on_budget_exceeded="warn")
|
||||
assert captured["agent_kwargs"]["execution"] == {
|
||||
"max_budget": 0.5, "on_budget_exceeded": "warn",
|
||||
}
|
||||
|
||||
|
||||
def test_no_budget_leaves_execution_unset(monkeypatch):
|
||||
captured = _capture(monkeypatch)
|
||||
_run()
|
||||
assert "execution" not in captured["agent_kwargs"]
|
||||
|
||||
|
||||
def test_invalid_on_budget_exceeded_rejected(monkeypatch):
|
||||
_capture(monkeypatch)
|
||||
with pytest.raises(typer.Exit):
|
||||
_run(max_budget=1.0, on_budget_exceeded="explode")
|
||||
|
||||
|
||||
def test_budget_dict_builds_real_execution_config():
|
||||
pytest.importorskip("praisonaiagents")
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="a", execution={"max_budget": 0.25, "on_budget_exceeded": "stop"})
|
||||
assert agent._max_budget == 0.25
|
||||
Reference in new issue
Block a user