Repository navigation
LCORE-4593: redact the query, replayed turns and summaries of compacted-mode requests - #2997
Open
max-svistunov wants to merge 3 commits into
Open
max-svistunov wants to merge 3 commits into
max-svistunov wants to merge 3 commits into
Conversation
Contributor
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 59 minutes. View limit details
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. Comment |
The redaction shield rewrites the user prompt as an agent capability, so it only reaches text that goes through the agent. Compacted mode builds the model input and stores the turn outside the agent and needs the same rules as a plain function. request_redactor() returns that function for a request: the rules of the redaction shields the request selects, applied in configuration order, which is the order the capabilities run in. It returns None when the selection holds no redaction rule and raises the 404 of get_shields_for_request for an unknown shield name. TextRedactor is the type of the returned function. Nothing calls it yet, so no behaviour changes. Unit tests cover the None cases, the default selection, the rule order and the 404.
With a redaction shield and compaction enabled, the query of a compacted turn reached the model and the conversation store unredacted. The redaction capability rewrites pydantic-ai's messages, but in compacted mode the request input is the explicit list built by apply_compaction, sent as an extra_body override that replaces the message-derived input, and the endpoints store CompactionResult.original_input themselves. Both were built from the raw query. apply_compaction and apply_compaction_blocking take the redaction of the request (request_redactor) and apply it to the new query, so the explicit input and original_input carry the redacted text. /v1/query, /v1/streaming_query and A2A pass it; /v1/responses does not, its redaction shield blocks instead of rewriting. A request that is not compacted is returned untouched, and without a selected redaction shield nothing changes. Two side effects. On /v1/query and /v1/streaming_query the agent prompt of a compacted turn is taken from the explicit input, so it is now the redacted query and the other shields judge the text the model gets. An unknown shield name fails before compaction instead of after it. Tested at unit level (explicit input, the override built from it and original_input, with the shield selected and deselected) and with integration tests on the three entry points: model input and stored turn of a compacted conversation, per-request shield_ids, and the unchanged request of a conversation that is not compacted. The integration tests of the compacted turn fail without the fix.
Conversations hold user turns that were stored unredacted: compacted turns written before the previous commit, and turns of requests that deselected the shield. Compacted mode replayed them as stored, handed them to the summarizer as stored, and replayed the summaries made from them. apply_compaction now applies the redaction of the request to the user turns and the summaries it puts into the explicit input, and to the user turns it hands to the summarizer. Summaries are covered because they are sent as user messages. Assistant turns are left as stored, as the redaction capability leaves them. Nothing stored is rewritten, and the recursive fold still sends the stored summaries. The replayed turns are redacted only after the summary boundary is taken: the first kept turn is found among the stored items by identity, and a redacted copy would move the boundary past a turn that is still verbatim. Unit tests cover the explicit input (summary and user turns redacted, assistant and system turns untouched), the items handed to the summarizer, and the boundary on a conversation with an older marker among the kept turns. The shields guide says what is redacted in compacted mode and what is not.
max-svistunov
force-pushed
the
lcore-4593-pii-redaction-compacted-mode
branch
from
October 29, 2026 17:33
66cc834 to
ad38391
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
LCORE-4593. With a
redactionshield and compaction enabled, the query of a compacted turn reached the model and the conversation store as typed, on/v1/query,/v1/streaming_queryand A2A. Later turns replayed and summarized it that way. The caller did not see it, because the answer is still redacted on its way out. Compaction is off by default.Cause. The redaction shield is an agent capability that rewrites the prompt in pydantic-ai's messages. In compacted mode the model input is the explicit list
apply_compactionbuilds (summaries, recent turns, new query), and the stored turn comes fromCompactionResult.original_input. Both used the query as it arrived. The summarizer is a direct client call, so it got the stored turns as they were.Fix.
request_redactor()(src/utils/shields.py) turns the redaction shields a request selects into one function, orNoneif there are no rules.apply_compaction/apply_compaction_blockingtake it asredactand apply it to the new query, the replayed user turns and summaries, and the user turns sent to the summarizer. Replayed turns are redacted only after the summary boundary is found, because the kept turns are found by identity; a test pins that./v1/queryand/v1/streaming_querypass the request'sshield_ids; A2A uses every configured shield.docs/user_doc/shields_guide.mdsays what compacted mode redacts and what it does not.Without a redaction shield selected,
redactisNoneand requests are built and stored as on main. Requests that are not compacted are not changed.Size: +535/-4 in three commits (source 105, tests 414, docs 16).
Not covered.
GET /v1/conversations/{id}returns them as stored./v1/responsesgets noredact, because its redaction shield blocks instead of rewriting.Side effects. In a compacted turn, question validity and Granite Guardian now judge the redacted query (read in the code, not tested). An unknown
shield_idsname is now rejected before compaction instead of after it. A broad rule also rewrites model-written text in replayed summaries, and its replacement must not match a rule itself (documented).Related work. Built on current main, which already has #2796, #2867 and #2868. #2866 (LCORE-3789) does not conflict with this PR. The pydantic-ai compaction (LCORE-4691) replaces these call sites and has to carry the fix over.
Type of change
pyproject.toml+uv.lock]requirements.*.txtfor Konflux]Tools used to create PR
Identify any AI code assistants used in this PR (for transparency and review context)
Related Tickets & Documents
Checklist before requesting a review
Testing
openai/gpt-4o-mini, aggressive compaction (context_windows: 2000,threshold_ratio: 0.1,token_floor: 100,buffer_turns: 1), aredactionshield (e-mail ->[REDACTED_EMAIL],\b\d{3}-\d{2}-\d{4}\b->[REDACTED_NUMBER]), and a recording proxy in front of the provider. Send three turns with PII; turn 3 is the first compacted one. Expected: the provider request and the stored item are redacted on every turn. Recorded on/v1/query(/v1/streaming_queryand A2Amessage/sendgave the same result):With
"shield_ids": []the query is sent and stored as typed.The live runs were made before the rebase onto current main. Since then the A2A call site moved into
_compact_a2a_request(#2796), and the compaction code takes its context window fromresolve_context_window(#2993). The integration tests cover all three entry points on the rebased code. Not run live: server mode,/v1/responses, A2A streaming, attachments/RAG. No e2e config has both shields and compaction.