Repository navigation
feat(agents): queue only sandbox work; per-user message limits - #12175
Merged
Merged
Conversation
…ssion close
Owner decision of 2026-10-14 ("queue only the pipeline, not every chat
message"): a /messages turn now starts at once. Only a plot_pipeline call
(after its readiness check) or a run_data_code call takes the run queue's
slot and the turn's start in the rate window, mid-turn, through the ledger's
run_slot (TurnSlot.take in agents/main.py). The route runs the ADK invocation
in its own task and multiplexes its events with the queue positions, so the
stream still sends status{step:"queued"} while the tool waits. The request
deadline pauses during the wait and starts again in full. A full queue or an
expired wait fails the turn with capacity and aborts the run; the slot is held
until the turn ends, so a second sandbox run in a turn books nothing more.
RunQueue loses the unrated entries and rate(); RequestLedger.rate_start
becomes run_slot.
Chat turns are bounded instead by per-user message limits counted in the
usage book (AGENT_USER_MESSAGES_PER_MINUTE 10, AGENT_USER_MESSAGES_PER_DAY
200), checked before the scope judge and answered with the fixed rate_limited
or daily_limit refusal, and a session closes at its third out_of_scope or
attack refusal (AGENT_SESSION_REFUSALS; needs_data never counts), counted
from the session's withheld messages: that refusal and every later turn
answer session_closed. The page records the three new refusal codes as their
own guardrail reasons.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
End-to-end timeout caps conflict with the new deadline behavior, and attack-closed sessions can return the wrong refusal.
3 open findings
What changed in this PR
This PR moves queueing from whole chat turns to sandbox work and adds per-user message and session-refusal limits.
Changes:
- Queues only plot and data-code sandbox operations.
- Adds message rate limits and session closure after repeated scope refusals.
- Updates analytics, documentation, and tests for the new behavior.
| File | Description |
|---|---|
agents/main.py |
Implements turn slots, deadlines, and pre-judge limits. |
agents/anyplot/run_queue.py |
Simplifies queue entries to sandbox runs. |
agents/anyplot/pipeline.py |
Acquires a slot before pipeline execution. |
agents/anyplot/tools/session.py |
Queues data-code sandbox runs. |
agents/anyplot/plugins/ledger.py |
Adds per-user message accounting. |
agents/anyplot/plugins/scope_guard.py |
Counts refusals and closes sessions. |
agents/anyplot/settings.py |
Adds message and refusal settings. |
agents/anyplot/prompts/refusals.yaml |
Adds localized refusal messages. |
agents/stream.py |
Documents updated stream behavior. |
agents/README.md |
Documents new queue and limits. |
app/src/hooks/useAgentSession.ts |
Tracks new guardrail reasons. |
app/src/hooks/useAgentSession.test.ts |
Tests new refusal analytics. |
docs/concepts/agent-network.md |
Updates queue architecture and bounds. |
docs/reference/api.md |
Updates SSE and endpoint contracts. |
docs/reference/plausible.md |
Documents new analytics reasons. |
changelog.d/queue-pipeline-only.md |
Records the user-visible changes. |
tests/unit/agents/runtime/test_turn_limits.py |
Tests message and refusal limits. |
tests/unit/agents/runtime/test_run_queue.py |
Removes obsolete unrated-run tests. |
tests/unit/agents/runtime/test_run_queue_flow.py |
Tests mid-turn sandbox queueing. |
tests/unit/agents/runtime/test_policy.py |
Tests localized refusal coverage. |
tests/unit/agents/runtime/test_data_code_tool.py |
Tests data-code slot acquisition. |
🧠 Review effort: Balanced
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…osed session first Copilot round 1 on #12175: - The request deadline resumed in full after a queue wait, so a turn could run 540 s + 600 s queued + 540 s, past the BFF's 1,190 s turn cap. It now resumes with the time it had left. - A session closed by three `attack` refusals also hits the strike lockout, which answered `budget` before the `session_closed` check. The closure is checked first now. - docs/reference/api.md names `attack` as a closing refusal too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M
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.


Summary
/messagesturn now starts at once. The ADK run goes into its own task, and the stream reads an outbox that carries the ADK events and the queue positions.plot_pipelineandrun_data_codetake the turn's run slot throughTurnSlot.take(ledger.run_slot, which replaces feat(agents): generate data from code in the sandbox (run_data_code) #12172'srate_start). The slot books the one start per turn in the rate window and is held until the turn ends.error{code:"capacity"}.rated = …gate andRunQueue.rateare removed.ready→message→done, with no wait, even while another user's pipeline holds the slot.ready→status{queued}(only when it must wait) → the pipeline steps →plot→message→done.AGENT_USER_MESSAGES_PER_MINUTE(10) andAGENT_USER_MESSAGES_PER_DAY(200) are checked before the scope judge, so a flood costs no model call.rate_limitedanddaily_limit, in all 11 refusal languages.AGENT_SESSION_REFUSALS).out_of_scopeplaceholders:attackstores the same placeholder, andneeds_datahas its own and never counts.session_closed, and every later message is refused before the judge.attackstrike lockout is unchanged.agent_guardrail_blockreason.Notes for the owner
session_closedtext asks the user to reload.capacitystill counts against the daily pipeline runs, as before.Plan
N/A
Test plan
pytest tests/unit tests/integration: 10,506 passed. The newtest_turn_limits.pycovers:needs_datanot counted.api core agents) and the changelog check pass.