Skip to content

Copilot shows a response before a guardrail blocks it #712

Closed

Description

@BoletteKern

Describe the bug

If a guardrail rule checks the response (post-generate) and its action is Block, the Copilot still shows the blocked response to the editor. The response appears in the chat first, and the error message appears below it. The editor can read the content the guardrail was meant to stop.

Steps to reproduce

  1. Create a guardrail with one rule:
    • Evaluator: LLM Safety Judge
    • Phase: post-generate
    • Action: Block
    • Criteria: "Flag the content if it mentions any price, cost or amount of money."
    • Threshold: 0.7
  2. Attach the guardrail to an agent that is available in the Copilot.
  3. Open the Copilot and ask: "Write two sentences for a winter jacket promotion, and include the price: €149."

Expected behaviour

The editor never sees the blocked response. They only see a message saying a guardrail blocked it.

Actual behaviour

The full response, including "€149", is shown in the chat. Below it, this error appears: "Error: The response was blocked by a guardrail policy: TEST – LLM judge blocks prices > Block any mention of prices (LLM judge)".

Versions

  • Umbraco CMS 17.7.0
  • Umbraco.AI 17.6.0, Umbraco.AI.Agent 17.4.0, Umbraco.AI.Agent.Copilot.Workspace 17.2.0
  • Provider: Anthropic, Claude Sonnet 4.6
  • Tested in the Copilot Workspace. I didn't test the Copilot sidebar on a content page.

Related

#193 (how a block is shown in the Copilot). This is a different problem: the blocked response is visible before the block.

Activity

  1. mattbrailsford commented on Nov 10, 2026

    @mattbrailsford
    Contributor

    Thanks for the clear steps. We followed them exactly and saw the same thing.

    Seeing the answer before the block. Part of this is a known limit of streamed answers, and it's in the docs under Guardrails > Streaming Behavior. An LLM Safety Judge can only check an answer once it's complete, and by then the chat has already shown it as it was written. If you need a rule that stops content before anyone sees it, use a Contains or Regex rule instead. Those check the text before it's shown.

    What we found that is a bug. After the block, the blocked answer didn't go away:

    • It stayed on screen above the error message.
    • When you sent your next message, the Copilot sent the blocked answer and the "Error: ..." text back with it, so the AI saw the blocked content on later turns.
    • Both were then saved in the conversation, so they were still there when you reopened the chat.

    We're fixing all of that. When a guardrail blocks an answer, the answer will be removed and replaced by the block message. The blocked turn won't be sent back to the AI or saved. The rule editor will also explain the streaming limit when you pick an LLM Safety Judge rule that blocks a response. The fix is built and going through review now. We can't give a release date yet.

    Notes on the work
    • Repro (before the fix): v17/dev, Copilot Workspace, OpenAI default-chat profile. Guardrail with one rule: llm-judge, PostGenerate, Block, criteria "Flag the content if it mentions any price, cost or amount of money.", threshold 0.7, attached to the agent. Send the reporter's prompt, then a second message without a price.
    • Turn 1 (checked live): the SSE stream sent every TEXT_MESSAGE_CHUNK (full text incl. "€149"), then RUN_ERROR about 2 s later. Nothing was stored server-side.
    • Turn 2 (checked live): the stream-agui request body held the turn 1 user message, the blocked assistant answer, an assistant "Error: The response was blocked by a guardrail policy: ..." message, and the new user message. The agent audit log's PromptSnapshot contained the blocked answer and the error text. umbracoAIConversationsMessage then held all of them, and reopening the chat showed the blocked answer merged with the error text.
    • Cause: on RUN_ERROR, UaiRunController kept the streamed assistant message and added the error as an ordinary assistant message (#handleError). onTurnComplete only runs on RUN_FINISHED, so the Workspace strategy's boundary stayed before turn 1 and the next outbound() sent both as new. The Copilot sidebar's default strategy sends every message, so it resent them too (from code). v18 has the same code.
    • Route: a fix across Agent (a GuardrailBlocked RUN_ERROR code), Agent.UI (remove the blocked text, keep the blocked turn and its notice out of what's sent; regenerate on a blocked turn re-asks without truncating stored history) and core (a note in the guardrail rule editor for model-based post-generate Block rules).
    • Where: branch v18/topic/guardrail-blocked-turn, then ports to v17 and v19.
    • New findings: sending the blocked question back without its answer made the model answer it again and get blocked again, so the whole blocked turn (question, tool calls, notice) stays out of what's sent. Other kinds of run errors still send their "Error: ..." text back to the agent; that's left as a separate follow-up.
    • Verified live on v18: the blocked turn shows only the block message; Regenerate re-asks the question without touching stored history; the follow-up's prompt holds neither the blocked question nor the notice, and only the follow-up is stored.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions