Repository navigation
fix(sdk/python): let AsyncConfig env flags fall back to the class default - #1100
Merged
ZeroPoint95 merged 1 commit intoOct 14, 2026
Conversation
…ault The two boolean parsers added in Agent-Field#1082 each bake in a polarity: _env_flag_default_on returns True for anything outside the falsey vocabulary and _env_flag_default_off returns False for anything outside the truthy one. That is correct for AsyncConfig's own field defaults, but it makes the fallback independent of the class -- a subclass that changes a default gets the parser's answer for an unrecognised string instead of its own: @DataClass class CachingOff(AsyncConfig): enable_result_caching: bool = False # AGENTFIELD_ASYNC_ENABLE_RESULT_CACHING=maybe CachingOff.from_environment().enable_result_caching # True, should be False Add one polarity-free parser, _env_flag, that raises ValueError outside the vocabulary, and use it for all five flags. get_env_var already catches (ValueError, TypeError) and returns the default_value it was handed, which from_environment() passes as config.<field> -- so the fallback now comes from the class, exactly as the float and int converters in the same function already do. This is the second half of Agent-Field#1089; the remaining strict parsers in logger.py, agent.py and agent_server.py are handled separately in Agent-Field#1090. It also matches the Go control plane, which has always parsed this way: config.applyBoolEnv leaves *target untouched and logs a warning when strconv.ParseBool rejects the string, and the handler tests pin parseBool(" YeS ") -> true, parseBool("0") -> false, parseBool("maybe") -> error. _env_flag_default_on and _env_flag_default_off are kept, now expressed in terms of _env_flag, for call sites that know their default statically -- Agent-Field#1090 imports _env_flag_default_off into logger.py for the default-off AGENTFIELD_LOG_* flags, so removing them would break an open PR. A test pins both wrappers' contract. AsyncConfig's own observable behaviour is unchanged: with no AGENTFIELD_ASYNC_* variable set the converters are never reached, and digesting every field of from_environment() on both trees gives the same SHA-256 (5960894a0c0bd6c817a09800eee26998e09625f7dfb23cd3d75ce1ee9088eb90). For a set variable, every value in the vocabulary parses as before and every value outside it still yields the field default -- the default just now comes from the class. Fixes the AsyncConfig half of Agent-Field#1089.
remote-controlled-man
requested review from
a team and
AbirAbbas
as code owners
October 8, 2026 02:14
Contributor
Performance
✓ No regressions detected |
Contributor
📊 Coverage gateThresholds from
✅ Gate passedNo surface regressed past the allowed threshold and the aggregate stayed above the floor. |
Contributor
📐 Patch coverage gateThreshold: 80% on lines this PR touches vs
✅ Patch gate passedEvery surface whose lines were touched by this PR has patch coverage at or above the threshold. |
ZeroPoint95
approved these changes
Oct 14, 2026
Contributor
|
Thank you @remote-controlled-man for the contribution! Approved and will merge soon. |
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
Takes the
AsyncConfighalf of #1089 — the point raised there about #1082. The other half (theremaining strict parsers in
logger.py,agent.pyandagent_server.py) is #1090, and this PR doesnot touch those files.
#1082 replaced five
lambda x: x.lower() == "true"converters with two polarity-aware helpers. Eachone hard-codes the answer for "unrecognised string":
_env_flag_default_onreturnsTrue,_env_flag_default_offreturnsFalse. That is right forAsyncConfig's own field defaults, but itmakes the fallback independent of the class, so a subclass that changes a default gets the parser's
answer instead of its own:
Subclassing is a supported pattern here, not a hypothetical:
AsyncConfigis a plain dataclass and bothpublic constructors take one (
agent.py:846,client.py:182), andtest_client_keeps_explicit_async_configalready pins that a caller-supplied instance is used as-is.The fallback machinery is already correct —
get_env_varcatches(ValueError, TypeError)and returnsthe
default_valueit was handed, whichfrom_environment()passes asconfig.<field>, i.e. theclass's real default. The boolean converters simply never reach it, because a membership test cannot
fail. The float and int converters in the same function do reach it.
So this adds one polarity-free parser and points all five flags at it:
An unrecognised value now raises,
get_env_varcatches it, and the default comes from the class.This also matches the Go control plane, which has always parsed this way:
config.applyBoolEnv(
control-plane/internal/config/config.go:1128) leaves*targetuntouched and logswarning: invalid %s=%q: %v; keeping %twhenstrconv.ParseBoolrejects the string, and the handlertests pin
parseBool(" YeS ")→ true,parseBool("0")→ false,parseBool("maybe")→ error.Whitespace-tolerant, vocabulary-based, error on unknown. Go additionally logs a warning; Python's
float and int converters fall back silently today, so this matches the local convention rather than
adding logging — happy to add a warning too if you'd prefer the Go behaviour exactly.
AsyncConfig's own observable behaviour does not change. With noAGENTFIELD_ASYNC_*set,get_env_varreturns the default before consulting any converter, so the new code is never reached;digesting every field of
from_environment()on both trees gives the same SHA-256(
5960894a0c0bd6c817a09800eee26998e09625f7dfb23cd3d75ce1ee9088eb90). Vocabulary values parse exactlyas before, and out-of-vocabulary values still yield the field default for
AsyncConfigitself — thedefault just now comes from the class, which is what fixes the subclass case.
One thing to decide as a reviewer
_env_flag_default_onand_env_flag_default_offare kept, re-expressed in terms of_env_flag,because #1090 contains
from .async_config import _env_flag_default_offinlogger.pyand removing itwould break an open PR. A test pins both wrappers' contract.
The trade-off: after this change
_env_flag_default_onhas no caller anywhere in the repository (it wasonly used by the four call sites this PR rewrites). I kept it because it is the matching half of a
two-day-old pair whose sibling #1090 depends on, and deleting one while keeping the other seemed like
arbitrary churn. If you would rather drop it, that is a one-line change and I am happy to.
Type of change
Test plan
19 tests added to
sdk/python/tests/test_async_config.py. On unmodifiedmainthey give 9 failed,92 passed, distributed exactly as the defect predicts:
enable_event_streamforced on), each getting the parser's baked-in answer instead of the class's.test_every_flag_falls_back_to_its_own_class_default— all five flags × three unparseable values× three classes, of which only the two fields whose subclass default contradicts the baked-in polarity
fail. The other nine pass on
maintoo, so the failures are surgical rather than blanket.test_env_flag_rejects_values_outside_the_vocabulary(" ON "→ True,"Off"→ False,maybe/``/2/`truthy` → `ValueError`).test_polarity_wrappers_keep_their_contract_for_external_callerspasses onmainas well as afterthe fix — that is deliberate, it is the guard for fix(python-sdk): use SDK-wide truthy/falsy vocabulary for remaining strict env flags #1090's import.
All 82 tests already in the file (from #1082 and before) pass unchanged. The test subclasses are
decorated
@dataclass; without that the inherited__init__keeps the base defaults and the scenariodoes not reproduce at all.
Commands run locally (Windows, CPython 3.13.12 in an isolated venv,
pip install -e ".[dev]"plusruff==0.15.22at the versionsdk-python.ymlpins):cd sdk/python && python -m pytest tests/test_async_config.py --no-cov→ 101 passedcd sdk/python && python -m pytest --ignore=tests/test_agent_mesh.py --no-cov→9 failed, 2487 passed, 6 skipped, against 9 failed, 2468 passed, 6 skipped on unmodified
main.2487 = 2468 + 19, and the sorted set of failing test ids is byte-identical tomain, sonothing regressed. Those 9 are pre-existing Windows-host failures —
'gbk' codec can't decodefrompathlib.read_text, andb'inline\r' != b'inline\n'CRLF assertions — that pass on CI'subuntu-latest.tests/test_agent_mesh.pyis excluded because it hangs on this host at ~8%,verified to hang identically on unmodified
main.cd sdk/python && python -m mypy --config-file mypy.ini agentfield/→ Success: no issues found in81 source files
cd sdk/python && python -m ruff check .→ All checks passedcd sdk/python && python -m ruff format --diff agentfield/async_config.py tests/test_async_config.py→ 2 files already formatted
Test coverage
coverage-baseline.json— not applicable; no source lines wereremoved that carried coverage, and no surface percentage can move (see below).
./scripts/coverage-summary.shlocally because it measures all five surfaces and this machine hasno Go, Bun, or Docker engine. Leaving this for CI rather than claiming it.
Heads-up so the gate output is not misread:
agentfield.async_configis not in the--covwhitelistin
pyproject.toml's pytestaddopts(which lists nine modules), andscripts/coverage-surface.shmeasures
sdk-pythonby running barepytest, so this file never reachessdk-python-coverage.xml.patch-coverage-gate.shrunsdiff-coveragainst that XML and can only count lines the report knowsabout, so this PR will report
sdk-python | 0 | — | ➖ no changes. #1078 shows the mechanism — ittouched 2 executable lines in
rate_limiter.py(whitelisted) plus 48 test lines and the gate reportedexactly
2 | 100.00%.So the gate reporting "no changes" here is not evidence that the new code is untested.
_env_flagisexercised directly and through
from_environment()for all five flags across truthy, falsey, padded,unparseable and unset values, and both wrappers are pinned. Adding
--cov=agentfield.async_configtoaddoptswould let the gate see this module — I leftpyproject.tomlalone to keep this to one seam,and can send that separately if you want it.
Checklist
docs/CONTRIBUTING.mdanddocs/DEVELOPMENT.md.conventional-commits; the commit is not signed, as I have no signing key configured. Flagging it
rather than ticking the box.
AsyncConfighalf) and fix(sdk/python): parse AGENTFIELD_ASYNC boolean flags with the repo's env vocabulary #1082 (the PR itfollows from). Not using a closing keyword, because fix(python-sdk): use SDK-wide truthy/falsy vocabulary for remaining strict env flags #1090 also fixes part of Python SDK: remaining env flags with strict boolean parsing #1089 and should not be
pre-empted.
Related issues / PRs
AsyncConfighalf of; the maintainer's description of the fix("one parser that raises
ValueErroron unknown values, lettingget_env_var's existing fallbackreturn the class's real default") is what is implemented here.
AsyncConfig.logger.py,agent.py,agent_server.py). No file overlap withthis PR, but it imports
_env_flag_default_offfromasync_config, which is why that helper is kept.Whichever merges second should be a clean rebase.