Skip to content

Surface retry/backoff status in the CLI so rate-limit waits aren't mistaken for a hang #3311

Description

@MervinPraison

Summary

When a model call hits a rate limit or a transient error, the runtime backs off and retries — correctly honouring retry-after — but the wait is silent at the terminal, so a multi-second (or multi-attempt) backoff is indistinguishable from a frozen CLI. The retry signal already exists internally as the ON_RETRY hook; what is missing is a first-class streaming event and a default CLI renderer that shows a live "retrying in Ns (attempt k/N)" status. This is a small, CLI-first observability win that makes production runs legible.

Current behaviour

  • Retry/backoff is computed and honoured, and an ON_RETRY hook fires:
    • src/praisonai-agents/praisonaiagents/agent/chat_mixin.py:4471-4481 (sync) and :4552-4562 (async) — HookEvent.ON_RETRY.
    • Backoff honours retry-after via src/praisonai-agents/praisonaiagents/llm/retry_utils.py (calculate_backoff_with_retry_after).
  • But the streaming event taxonomy has no retry statesrc/praisonai-agents/praisonaiagents/streaming/events.py:26-38 lists REQUEST_START, HEADERS_RECEIVED, FIRST_TOKEN, DELTA_*, TOOL_*, LAST_TOKEN, STREAM_END, ERROR, STREAM_UNAVAILABLE — no RETRY / WAITING.
  • The CLI has no default consumer of ON_RETRY, so nothing is printed during the backoff sleep; the terminal appears hung.

Desired behaviour

  • Emit a structured retry lifecycle event (e.g. StreamEventType.RETRY) carrying attempt number, max attempts, delay and reason.
  • Have the CLI's default output renderer show a concise, self-updating line — e.g. Rate limited — retrying in 8s (attempt 2/4)… — that clears on success. Silent/JSON output modes stay silent (the event remains available programmatically).

Layer placement

  • Primary layer: wrapper (praisonai CLI renderer)
  • Why not core: the value is the CLI display; core needs only a tiny hook — emit the event. The retry logic itself already lives in core and is correct.
  • Why not wrapper: n/a — the rendering belongs here, in the CLI output layer that already consumes stream events.
  • Why not tools: unrelated to agent-callable tools.
  • Why not plugins: a default, always-on CLI status is not an optional lifecycle policy; a plugin could also consume ON_RETRY, but the baseline UX should ship in the wrapper.
  • Secondary touch (optional): core streaming/events.py — add the RETRY event type and emit it where ON_RETRY fires (chat_mixin.py:4471 / :4552).
  • 3-way surface (CLI + YAML + Python): partial — the event/hook is already available to Python and YAML-configured runs; the new piece is the CLI renderer.

Proposed approach

  • Core: add StreamEventType.RETRY (with attempt, max_attempts, delay, reason) in streaming/events.py and emit it alongside the existing ON_RETRY hook in chat_mixin.py (guarded by has_callbacks, so it stays zero-overhead when no consumer is attached).
  • Wrapper: in the CLI output handler (the default actions / verbose renderer), render a transient status line for RETRY events; suppress under silent / json.

Resolution sketch

  • streaming/events.py: extend the enum + StreamEvent fields.
  • chat_mixin.py:4471 / :4552: after firing ON_RETRY, emitter.emit(StreamEvent(type=RETRY, …)).
  • CLI renderer: handle RETRY → print/refresh a status line; clear on the next token / STREAM_END.
  • Tests: unit test that a simulated rate-limit emits a RETRY event with the correct attempt/delay; CLI test that the status line renders in actions/verbose but not in json; the mandatory real agentic test stays green.

Severity

Low–Medium — no correctness impact; a production-legibility improvement that removes a common "is it stuck?" confusion during backoffs.

Validation

  • Traced ON_RETRY emission to chat_mixin.py:4471-4481 / :4552-4562 and confirmed streaming/events.py:26-38 has no retry state.
  • Confirmed backoff honours retry-after (llm/retry_utils.py) but is not surfaced to the terminal.
  • Confirmed no existing open issue covers this.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingclaudeAuto-trigger Claude analysis

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions