Skip to content

fix(flow): treat agent exceptions as failed steps so retry/fallback engage - #98

Open
XuQuanxin04 wants to merge 1 commit into
wanxingai:mainfrom
XuQuanxin04:fix/flow-agent-exception-retry
Open

fix(flow): treat agent exceptions as failed steps so retry/fallback engage#98
XuQuanxin04 wants to merge 1 commit into
wanxingai:mainfrom
XuQuanxin04:fix/flow-agent-exception-retry

Conversation

@XuQuanxin04

Copy link
Copy Markdown

Summary

  • _run_step called _call_agent without catching exceptions. Real agents typically raise on API/network/rate-limit failures (rather than returning a RunResult with error set), and _call_agent re-raises those — directly when no timeout is set, or via future.result() when one is. The exception therefore escaped the retry loop on the first attempt:
    • max_retry never retried a raising agent (the workflow crashed on attempt 1 instead of trying again),
    • fallback_agent was never consulted for a raising agent,
    • with no fallback, flow.run() raised instead of marking the step failed and skipping its dependents.
  • Fix: wrap both the primary and fallback agent calls so a raised exception is normalized into a failed step result using the existing [LA-FLOW-...] error convention. The retry loop and fallback path then engage exactly as they do for errors returned in a RunResult, and the flow fails gracefully with downstream steps skipped.

Reproduction (before this patch)

class Flaky:
    def __init__(self): self.calls = 0
    def run(self, query, **kw):
        self.calls += 1
        if self.calls < 3:
            raise RuntimeError("transient API error")
        return "recovered"

flow = LightFlow().step("flaky", agent=Flaky(), max_retry=3)
flow.run("go")   # raised RuntimeError after 1 attempt; never retried

Compatibility

  • Existing agent.run("hello") behavior is unchanged for agents that return normally.
  • Existing stream=True behavior is unchanged.
  • Behavior change is strictly a fix: agents that raise no longer crash the orchestration; they are retried / fall back / mark the step failed, consistent with how RunResult(error=...) failures are already handled. KeyboardInterrupt/SystemExit (not subclasses of Exception) still propagate.

Tests

  • python -m compileall -q LightAgent
  • PYTHONPATH=. python -m pytest -q tests/test_lightflow.py — 17 passed
  • Added three regression tests:
    • test_lightflow_retries_when_agent_raises — a transiently-raising agent is retried and succeeds.
    • test_lightflow_fallback_runs_when_agent_always_raises — a persistently-raising agent falls back.
    • test_lightflow_marks_step_failed_when_agent_always_raises_without_fallback — the step is failed, the dependent step is skipped, and run() returns a failed result instead of raising.

Notes

No API changes. The pre-existing retry/fallback tests only covered errors delivered via RunResult(error=...); this covers the raised-exception path, which is the common failure mode for real LLM integrations.

…ngage

_run_step called _call_agent without catching exceptions. Real agents
typically raise on API/network/rate-limit failures rather than returning
a RunResult with an error set, and _call_agent re-raises those (either
directly or via future.result()). The exception therefore escaped the
retry loop on the first attempt: max_retry never retried a raising agent,
fallback_agent was never consulted, and flow.run() crashed instead of
marking the step failed and skipping dependents.

Wrap both the primary and fallback agent calls so a raised exception is
normalized into a failed step result (using the existing [LA-FLOW-...]
error convention), allowing the retry loop and fallback path to engage,
and letting the flow fail gracefully with downstream steps skipped.

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant