fix(cli): surface continued-failure reason and wire serve ui-gateway --agents

workflows.py: a None-output step with on_error=continue now carries its failure_reason in the step result, so the final error reports the real reason instead of the generic 'step(s) failed' fallback. serve.py: serve ui-gateway --agents now loads the agents YAML into a parsed list and forwards agents=[...] (the gateway host takes a list via configure_host, not an agents_file path); the previous signature check always rejected the option. Fails loudly (exit 2) on a missing/malformed file.

Co-authored-by: Mervin Praison <[email protected]>
This commit is contained in:
praisonai-triage-agent[bot]andMervin Praison committed 2026-09-08 08:39:01 +00:00
1 parent 3b3b82d9af
commit 69769cbdd8
3 files changed
+91 -16

No files matched your search

@@ -1818,13 +1818,19 @@ class AgentFlow:
except Exception as e:
logger.error(f"Failed to save output to file: {e}")
# Store result
results.append({
# Store result. A step that failed under on_error="continue" reaches
# here (the on_error="stop" branch above already returned); carry its
# failure_reason through so the final result surfaces *why* it failed
# rather than the generic "step(s) failed: <name>" fallback.
step_record = {
"step": step.name,
"output": output,
"status": self.step_statuses.get(step.name, "completed"),
"retries": retry_count
})
}
if step_failed and failure_reason:
step_record["error"] = failure_reason
results.append(step_record)
previous_output = output
if verbose:
@@ -101,6 +101,39 @@ class TestAgentStepProducingNothingFailsTheRun:
assert "error" not in result
class TestContinuedFailureKeepsItsReason:
"""A None-output step with on_error='continue' must still carry its reason.
The run continues past the failed step, so it takes the general result path
rather than the on_error='stop' branch. That path used to omit the step's
error, so the final result fell back to the generic 'step(s) failed: <name>'
text instead of the computed 'agent produced no output' diagnostic.
"""
def test_continued_none_step_reports_its_reason_not_the_generic_fallback(self):
first = Task(
name="first", agent=_NullAgent(), action="do it", max_retries=0
)
first.on_error = "continue"
wf = Workflow(
name="continue-flow",
steps=[
first,
Task(name="second", agent=_RealAgent(), action="Write a haiku", max_retries=0),
],
)
result = wf.start("go")
assert result["status"] == "failed"
failed = [s for s in result["steps"] if s.get("status") == "failed"]
assert failed and failed[0]["step"] == "first"
assert "produced no output" in (failed[0].get("error") or ""), (
"a continued failure lost its diagnostic reason"
)
assert "produced no output" in result.get("error", ""), (
"the final result must surface the real reason, not the fallback"
)
class TestHandlerStepsAreNotJudgedThisWay:
"""A custom handler returning None is legitimate -- leave it alone."""
@@ -429,6 +429,51 @@ def serve_ui(
raise typer.Exit(4)
def _load_gateway_agents(agents_file: str, output):
"""Load an agents YAML file into a list of ``Agent`` objects.
The integrated gateway host forwards ``agents`` (a parsed list) to
``configure_host``; it does not accept a file path. Parse the YAML here and
fail loudly (exit 2) on a missing/malformed/empty file rather than silently
starting an empty gateway.
"""
import os
import yaml
if not os.path.exists(agents_file):
output.print_error(f"Agent file not found: {agents_file}")
raise typer.Exit(2)
try:
with open(agents_file, "r") as f:
config = yaml.safe_load(f)
except (OSError, yaml.YAMLError) as e:
output.print_error(f"Could not read agent file {agents_file}: {e}")
raise typer.Exit(2)
agents_cfg = config.get("agents") if isinstance(config, dict) else None
if not isinstance(agents_cfg, list) or not agents_cfg:
output.print_error(f"Agent file {agents_file} has no 'agents' list")
raise typer.Exit(2)
from praisonaiagents import Agent
agents = []
for entry in agents_cfg:
if not isinstance(entry, dict):
output.print_error(
f"Agent file {agents_file} contains a non-mapping agent entry"
)
raise typer.Exit(2)
agents.append(
Agent(
name=entry.get("name", "agent"),
instructions=entry.get("instructions", ""),
llm=entry.get("llm"),
)
)
return agents
@app.command("ui-gateway")
def serve_ui_gateway(
host: str = typer.Option("127.0.0.1", "--host", "-h", help="Host to bind to"),
@@ -455,20 +500,11 @@ def serve_ui_gateway(
run_integrated_gateway = getattr(_mod, "run_integrated_gateway")
kwargs = dict(host=host, port=port, title=title, style=style)
# `--agents` was declared (and used in this command's own docstring
# example) but never forwarded. Pass it only when supplied, and only
# when the gateway host accepts it, so an older praisonai-bot still
# starts instead of raising TypeError.
# example) but never forwarded. The gateway host forwards **kwargs to
# ``configure_host``, which accepts a parsed ``agents`` list (not a
# file path), so load the YAML here and pass ``agents=[...]``.
if agents_file:
import inspect
if "agents_file" in inspect.signature(run_integrated_gateway).parameters:
kwargs["agents_file"] = agents_file
else:
output.print_error(
"--agents is not supported by the installed praisonai-bot "
"gateway host; upgrade praisonai-bot to use it."
)
raise typer.Exit(2)
kwargs["agents"] = _load_gateway_agents(agents_file, output)
run_integrated_gateway(**kwargs)
except ImportError as e:
output.print_error(f"UI-Gateway module not available: {e}")