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 state —
src/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.
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 theON_RETRYhook; 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
ON_RETRYhook fires:src/praisonai-agents/praisonaiagents/agent/chat_mixin.py:4471-4481(sync) and:4552-4562(async) —HookEvent.ON_RETRY.retry-afterviasrc/praisonai-agents/praisonaiagents/llm/retry_utils.py(calculate_backoff_with_retry_after).src/praisonai-agents/praisonaiagents/streaming/events.py:26-38listsREQUEST_START, HEADERS_RECEIVED, FIRST_TOKEN, DELTA_*, TOOL_*, LAST_TOKEN, STREAM_END, ERROR, STREAM_UNAVAILABLE— noRETRY/WAITING.ON_RETRY, so nothing is printed during the backoff sleep; the terminal appears hung.Desired behaviour
StreamEventType.RETRY) carrying attempt number, max attempts, delay and reason.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
praisonaiCLI renderer)ON_RETRY, but the baseline UX should ship in the wrapper.streaming/events.py— add theRETRYevent type and emit it whereON_RETRYfires (chat_mixin.py:4471/:4552).Proposed approach
StreamEventType.RETRY(withattempt,max_attempts,delay,reason) instreaming/events.pyand emit it alongside the existingON_RETRYhook inchat_mixin.py(guarded byhas_callbacks, so it stays zero-overhead when no consumer is attached).actions/verboserenderer), render a transient status line forRETRYevents; suppress undersilent/json.Resolution sketch
streaming/events.py: extend the enum +StreamEventfields.chat_mixin.py:4471/:4552: after firingON_RETRY,emitter.emit(StreamEvent(type=RETRY, …)).RETRY→ print/refresh a status line; clear on the next token /STREAM_END.RETRYevent with the correct attempt/delay; CLI test that the status line renders inactions/verbosebut not injson; 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
ON_RETRYemission tochat_mixin.py:4471-4481/:4552-4562and confirmedstreaming/events.py:26-38has no retry state.retry-after(llm/retry_utils.py) but is not surfaced to the terminal.