Fix the defects the agents page turned up - #21
Merged
Conversation
added 6 commits
September 6, 2026 21:10
OperationDeadlineConfig spells the field calls_caps while BudgetContext.create takes
call_caps. Pydantic ignores unknown keys, so OperationDeadlineConfig(call_caps={...})
produced a config with no caps at all and no complaint; the deadline factory then built
every context for that operation uncapped.
The field keeps its name -- it is what OperationDeadlineConfigProtocol requires and what
existing configuration and dumps use -- and now accepts both spellings on input.
The context stored the caller's dict, so ctx.call_caps handed back the very mapping it was built from. DeadlineContextFactory passes config.calls_caps straight through, which made the settings object -- APP-scoped, shared by every request -- writable through any context built from it: mutating ctx.call_caps changed the caps of every later request for that operation. The context now takes a copy. Mutating ctx.call_caps still changes that context's later calls; it no longer reaches anything else.
make install ran uv sync --group dev, which leaves Pydantic and Dishka out, so make test-unit -- the command CONTRIBUTING tells a contributor to run -- failed at collection on tests/unit/contrib with two ModuleNotFoundError. CI already syncs with --all-extras.
The configuration guide said timeout_for(cap=...) returns min(cap, remaining), which holds only while remaining is above min_timeout. The floor outranks the remaining budget and the cap, applied last, outranks the floor -- neither was written down anywhere outside the agents page. Behaviour is unchanged; the guides and the two docstrings now describe it.
The class docstring promised "DeadlineContextFactory and optionally per-request BudgetContext"; there is one provider on it and it returns the factory.
…sable budget Two things a reader could only find by opening the source: the models behind the settings extra are BaseModel and not BaseSettings, and DeadlineBudget.total_seconds reports the total minus the safety margin -- as does DeadlineExceededError.budget_seconds.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
AlexeyShalaev
added a commit
that referenced
this pull request
Sep 6, 2026
…xt (#22) The work landed in #21. Its squash subject lost the Conventional Commit prefix -- my mistake on the merge, not the author's -- so release-please skipped the merge and these fixes would never have reached a release. This commit carries the record. It changes no code: #21 is already on master. * OperationDeadlineConfig spells the field calls_caps while BudgetContext.create takes call_caps, and the model ignored the unknown key, so a configuration written the second way silently carried no caps at all. Both spellings are accepted now through AliasChoices, and serialisation is unchanged. * BudgetContext held the caps dict it was handed rather than a copy. Because the factory passes the settings object's own dict straight through, mutating one request's caps rewrote the application-scoped settings and every later context for that operation. * make install and the contributing guide now sync the extras CI uses, so make test-unit works on a fresh checkout instead of failing at collection.
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.
Seven findings from reading the source against the docs while
docs/agents.mdwas beingwritten. Each one was reproduced first; all seven reproduced. Nothing here is breaking:
no name, default or signature changed.
1.
OperationDeadlineConfig(call_caps=...)silently produced a config with no capsThe settings field is
calls_caps,BudgetContext.createtakescall_caps. Pydanticignores unknown keys, so the near-miss built a config whose caps were empty, and
DeadlineContextFactorythen handed every call of that operation the full remainingbudget, uncapped, with no error anywhere:
The field keeps its name — it is what
OperationDeadlineConfigProtocolrequires, whatexisting configuration files and
model_dump()use — and now carriesvalidation_alias=AliasChoices("calls_caps", "call_caps"), so both spellings are acceptedon input and both land in
calls_caps. Serialisation is unchanged.I did not reach for
extra="forbid": it would turn every unrelated key in a caller'sconfiguration into a hard error, which is a much wider change than this finding needs, and
it makes the mistake fail rather than work.
Tests:
test__operation_config__with_call_caps_spelling__populates_calls_capsandtest__operation_config__dump__keeps_calls_caps_as_the_field_name. Both fail on master.2.
BudgetContextstored the caller's caps dict, so a request could rewrite the settingsctx.call_capshanded back the very mapping the context was built from, andDeadlineContextFactory.create_for_operationpassesconfig.calls_capsstraight through.The settings object is APP-scoped and shared by every request, so one context could write
into it:
BudgetContext.__init__now takesdict(call_caps). Mutatingctx.call_capsstill changesthat context's later calls — that is documented and unchanged — but it no longer reaches the
settings, and mutating the dict you passed to
create()after the fact no longer changesthe context. That second half is a behaviour change, so
docs/agents.mdrule 13 changedwith it.
Tests: two in
tests/unit/test_context.py, one intests/unit/contrib/test_dishka.py, allthree failing on master.
3.
make test-unitfailed on a fresh checkoutmake installranuv sync --group dev, which leaves the optional extras out, so the twocontrib test modules failed at collection:
CI already syncs with
--all-extras;make installand theCONTRIBUTING.mdsetup blocknow do the same, with a line saying why.
4. The guides did not say which of the budget, the floor and the cap wins
docs/guide/configuration.mdsaidtimeout_for(cap=...)"returnsmin(cap, remaining)",which holds only while
remainingis abovemin_timeout. Both departures are real:I read this as the doc drifting, not the code: the floor over the budget is what the safety
margin exists to pay for, and a service-level cap that got widened to reach a floor would be
worse than one that does not. Behaviour is unchanged; the configuration guide now carries
the two lines of arithmetic and both consequences, the quickstart carries a sentence each,
and the
timeout_for/timeout_for_calldocstrings no longer claim a[min_timeout, cap]bound they do not enforce.
5.
DeadlineProvider's docstring promised aBudgetContextproviderIt said it "Provides DeadlineContextFactory and optionally per-request BudgetContext"; the
class has one
@provideand it returns the factory. The docstring now says what itprovides, what binding it needs from you, and why a context is not provided — it belongs to
one operation and its countdown starts when it is built.
6. Nothing said the settings models read no environment
BaseDeadlineSettingsis apydantic.BaseModel, not apydantic_settings.BaseSettings,despite the extra being called
settings;docs/guide/integrations.mdnever said so.Added a paragraph there. The example in that section also used
BaseModelandFieldwithout importing them — fixed in the same block.
7.
total_secondsreports the usable budget, and the quickstart read as if it were the totalDeadlineBudget(total_seconds=10.0, safety_margin=0.5).total_secondsis9.5, andDeadlineExceededError.budget_secondsis the same9.5. Correct, deliberate, and onlywritten down on the agents page. The quickstart's
safety_marginsection now says it.Verification
make test-integrationexits 5 —tests/integration/holds only an__init__.py, sonothing is selected and pytest reports "no tests ran". That is the state on master too; I
left it alone.