Skip to content

LCORE-3582: fix compacted-mode 500s in the agent pipeline - #2451

Open
max-svistunov wants to merge 1 commit into
lightspeed-core:mainfrom
max-svistunov:lcore-3582-compacted-mode-input
Open

LCORE-3582: fix compacted-mode 500s in the agent pipeline#2451
max-svistunov wants to merge 1 commit into
lightspeed-core:mainfrom
max-svistunov:lcore-3582-compacted-mode-input

Conversation

@max-svistunov

Copy link
Copy Markdown
Contributor

Description

Fix LCORE-3582: once a conversation compacted (LCORE-1572), every subsequent request on it failed with HTTP 500 on both /v1/query and /v1/streaming_query, permanently bricking the conversation. In compacted mode CompactionResult.params.input is an explicit item list (summaries + recent turns + new query) with the conversation parameter omitted, but the pydantic-ai agent pipeline did prompt = cast(str, responses_params.input) and handed the list to agent.run(), which dies client-side before any request reaches Llama Stack. The A2A executor had a quieter variant: it passes raw user text as the prompt, so compacted A2A turns silently lost all conversation context.

The fix routes the explicit input through the existing params→model-settings seam:

  • _model_settings_from_responses_params (src/pydantic_ai_lightspeed/llamastack/_model.py): in compacted mode the dumped input list is added to extra_body, which the OpenAI SDK merges into the request body with precedence — the wire request matches what the non-agent /v1/responses path sends. This also fixes the A2A context loss with no a2a.py changes.
  • OgxResponsesModel._prepare_compacted_input (applied in request() and request_stream()): drops the override once a ModelResponse exists in the message history, so client-side tool-loop iterations keep pydantic-ai's mapped messages (which carry tool results).
  • New agent_prompt_text() (src/utils/conversation_compaction.py) replaces cast(str, ...) at all four call sites: returns string input unchanged, else the text of the trailing message item.
  • The blocked-moderation path in retrieve_agent_response now skips append_turn_items_to_conversation in compacted mode (mirrors the streaming path), preventing summary/history duplication into the conversation.
  • map_agent_inference_error now logs the original exception with traceback at error level — previously these failures produced a generic 500 with nothing in the logs.

Known limitation (noted on the Jira): image attachments are not folded into the overridden explicit input, so a compacted turn with images sends the text-only explicit list (before this fix, compaction+images hard-failed with 500).

Type of change

  • Refactor
  • New feature
  • Bug fix
  • CVE fix
  • Optimization
  • Documentation Update
  • Configuration Update
  • Bump-up service version
  • Bump-up dependent library
  • Bump-up library or tool used for development (does not change the final image)
  • CI configuration change
  • Konflux configuration change
  • Unit tests improvement
  • Integration tests improvement
  • End to end tests improvement
  • Benchmarks improvement

Tools used to create PR

Identify any AI code assistants used in this PR (for transparency and review context)

  • Assisted-by: Claude Opus 4.8
  • Generated by: Claude Opus 4.8

Related Tickets & Documents

  • Related Issue # LCORE-3582
  • Closes # LCORE-3582

Checklist before requesting a review

  • I have performed a self-review of my code.
  • PR has passed all pre-merge test jobs.
  • If it is a core feature, I have added thorough tests.

Testing

  1. Start the local stack with compaction configured to trigger aggressively:

    compaction:
      enabled: true
      threshold_ratio: 0.0
      token_floor: 0
      buffer_turns: 0
    inference:
      default_provider: openai
      default_model: gpt-4o-mini
      context_windows:
        openai/gpt-4o-mini: 128000

    plus a sqlite conversation_cache, and set OTEL_ANONYMIZATION_SECRET in the environment.

  2. Send a first /v1/query (new conversation), then follow-up queries with the returned conversation_id.
    Expected: every turn returns 200 (turn 2+ previously returned the generic 500).
    Actual (verified live against llama-stack 0.6.0):

    • Turn 2 (triggers summarization): 200, answer returned.
    • Turn 3 (marker-exists path): asked "What was my very first question in this conversation about?" → "Your very first question in this conversation was about asking for a one-sentence definition of Kubernetes." — the summary context demonstrably reaches the model.
  3. Connect to /v1/streaming_query on the compacted conversation.
    Expected: compaction event, token stream, end event (previously an error event with status 500).
    Actual:

    start -> {"conversation_id": "556a...", ...}
    compaction -> {"status": "started", ...}
    end -> {"referenced_documents": [], "truncated": null, "input_tokens": 184, "output_tokens": 3}
    ANSWER: OpenShift.   (contextual follow-up answered correctly)
    
  4. Run the tests specific to this change:

    uv run pytest tests/unit/pydantic_ai_lightspeed/llamastack/test_model.py \
      tests/unit/utils/test_conversation_compaction.py tests/unit/utils/agents/ -q
    

    Result: 163 passed.

  5. Full suites:

    uv run make test-unit          # 3225 passed, 1 skipped, coverage 90.35%
    uv run python -m pytest tests/integration --ignore=tests/integration/container_lifecycle -q
                                   # 260 passed, 1 xfailed (container_lifecycle needs a container runtime)
    

Once a conversation compacted, every subsequent request on it failed
with HTTP 500 on both /v1/query and /v1/streaming_query, permanently
bricking the conversation. Root cause: in compacted mode (LCORE-1572)
CompactionResult.params.input is an explicit item list (summaries +
recent verbatim turns + new query) with the conversation parameter
omitted, but the pydantic-ai agent pipeline did
prompt = cast(str, responses_params.input) and handed the list to
agent.run(), which dies client-side before any request reaches Llama
Stack. The A2A executor had a quieter variant of the same gap: it
passes the raw user text as the prompt, so compacted A2A turns silently
lost all conversation context.

The fix makes the explicit input reach the wire through the existing
params-to-model-settings seam:

- _model_settings_from_responses_params (pydantic_ai_lightspeed/
  llamastack/_model.py): when omit_conversation is set and input is an
  item list, the dumped list is added to extra_body. The OpenAI SDK
  merges extra_body into the request body with precedence
  (_merge_mappings: "the second mapping takes precedence"), so the
  explicit list replaces the prompt-derived input — the request body
  matches what the non-agent /v1/responses path sends. This also fixes
  the A2A context loss with no a2a.py changes, since build_agent
  already routes params through this seam.
- OgxResponsesModel gains _prepare_compacted_input, applied in both
  request() and request_stream() after the conversation-continuation
  trim: once a ModelResponse exists in the message history (a
  client-side tool-loop continuation), the input override is dropped so
  pydantic-ai's mapped messages — which carry the tool results — win.
- New agent_prompt_text() helper (utils/conversation_compaction.py)
  replaces the cast(str, ...) at all four call sites (non-streaming and
  streaming, plain and multimodal): returns input unchanged when it is
  a string, else the text of the trailing message item of the explicit
  list. The prompt still drives capabilities and multimodal input
  construction; the wire input comes from the override.
- The blocked-moderation path in retrieve_agent_response now skips
  append_turn_items_to_conversation when omit_conversation is set,
  mirroring the streaming path — appending the full explicit list would
  duplicate summaries and history into the conversation.
- map_agent_inference_error now logs the original exception at error
  level with the traceback before mapping. Previously the mapped
  HTTPException discarded it and callers raised without logging, so
  these failures produced a generic 500 with nothing in the logs —
  which is what made this bug expensive to diagnose.

Verified live against llama-stack 0.6.0: on a compacted conversation,
turn after turn returns 200 on both endpoints (previously 500), the
streaming path emits compaction/token/end events, and the model
correctly answers questions about pre-compaction turns, proving the
summary context reaches the model. Known limitation: image attachments
are not folded into the overridden explicit input, so a compacted turn
with images sends the text-only list (compaction+images previously hard
-failed; noted in LCORE-3582).

Unit tests cover the extra_body override (present in compacted mode,
absent otherwise), the tool-loop guard, agent_prompt_text, the prompt
threading through both retrieve paths, the moderation-append guard, and
the error logging.
@coderabbitai

coderabbitai Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@max-svistunov, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 32 minutes

Limit details: You’ve used all 1 included review currently available under your plan.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6bad4dce-1ab2-48b6-9c31-b2066c774d60

📥 Commits

Reviewing files that changed from the base of the PR and between f9e5343 and 1850069.

📒 Files selected for processing (9)
  • src/pydantic_ai_lightspeed/llamastack/_model.py
  • src/utils/agents/error_handler.py
  • src/utils/agents/query.py
  • src/utils/agents/streaming.py
  • src/utils/conversation_compaction.py
  • tests/unit/pydantic_ai_lightspeed/llamastack/test_model.py
  • tests/unit/utils/agents/test_query.py
  • tests/unit/utils/agents/test_streaming.py
  • tests/unit/utils/test_conversation_compaction.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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