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:
MervinPraisonandClaude Opus 5.5 committed 2026-11-03 08:58:57 +00:00
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