fix(sdk-typescript): honor AGENTFIELD_LOG_STDOUT, and document the per-SDK logging knobs - #1020
Merged
Merged
Conversation
docs/api/AGENT_NODE_LOGS.md has promised AGENTFIELD_LOG_STDOUT SDK-agnostically since it was written, and the Python (`_stdout_mirror_enabled`) and Go (`executionLogStdoutEnabled`) SDKs both honor it. The TypeScript ExecutionLogger did not: `mirrorToStdout` defaulted to `true` and nothing ever read the environment, so a TypeScript node had no way to turn the structured stdout mirror off (#985). `mirrorToStdout` becomes tri-state (`boolean | undefined`). An explicit option still wins in both directions; when it is absent the flag is resolved from the environment. The resolution happens per emit rather than in the constructor because `Agent` builds one shared ExecutionLogger at construction time, so a snapshot would freeze the flag for the whole process lifetime — Python and Go both re-read it on every record. The accepted falsy spellings (`0`/`false`/`no`/`off`, case-insensitive, whitespace trimmed) move into a new internal `utils/envFlags` helper that `processLogs.ts` now shares, so the list cannot drift between modules or against the other SDKs. The helper guards `process` with `typeof process !== 'undefined'`, matching the guard ExecutionLogger already uses for `process.stdout`, so the class stays usable outside Node. The default is unchanged: unset, empty, or any unrecognised value keeps the mirror on, so a typo cannot silently drop log output, and nothing in the repo sets this variable. Serialization now happens inside the mirror branch — with the mirror off the JSON envelope is never built, which is the cost the flag exists to avoid (the transport is handed the object, not the string). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`node_logs.max_line_bytes()` had no direct test, yet its parsing differs from the Go and TypeScript SDKs in ways the environment-variable reference is about to describe: Python clamps every integer below 256 up to 256 (including zero and negatives) where Go and TypeScript reject those values and fall back to 16384, and Python's `int(raw, 10)` rejects `512abc` where TypeScript's `parseInt` prefix-parses it to 512. Table-driven so the documented matrix and the code cannot drift apart. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ine-cap claims AGENTFIELD_LOG_STDOUT was documented under "Python SDK agents" even though the Go SDK reads it too (and now the TypeScript SDK does). It moves to "Structured logging (SDKs)" with an explicit reader list; a pointer stays in the Python section so a reader scanning only their own section does not lose it. Two consequences that were previously undocumented are stated so they are not later filed as regressions: a record with no execution id is skipped by control-plane dispatch in all three SDKs and is therefore dropped entirely when the mirror is off, and because the node-log ring is fed by captured stdout, disabling the mirror also empties structured records out of GET /agentfield/v1/logs. The AGENTFIELD_LOG_MAX_LINE_BYTES entry claimed "minimum: 256" and that "the Go and TypeScript SDKs treat invalid values as unset". The first is misleading and the second is false. Python clamps sub-256 integers up to 256; Go and TypeScript reject them upward to the 16384 default, so `=100` yields a cap 64x larger than requested. Python and Go reject non-integers outright while TypeScript's parseInt prefix-parses (`512abc` -> 512). The Python clamp also governs both Python log paths — the stdout/stderr tee behind /agentfield/v1/logs and the structured-mirror elision budget — not just the mirror. Every number here is pinned by the new test_node_logs.py table. Each SDK README gains a short Logging section carrying that SDK's own numbers. The Python one uses absolute github.com URLs because that file is the PyPI long_description, where relative links render dead. No code behaviour changes in this commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
Performance
⚠ Regression 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. |
Member
|
okay this was needed ! thanks mang |
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
docs/api/AGENT_NODE_LOGS.mdhas always describedAGENTFIELD_LOG_STDOUTas an SDK-agnostic way to turn the structured stdout mirror off, and the Python (_stdout_mirror_enabled) and Go (executionLogStdoutEnabled) SDKs both honor it — the TypeScriptExecutionLoggernever read it, so a TypeScript node had no way to opt out. This teaches the TypeScript logger the same variable and fixes the environment-variable reference, which filedAGENTFIELD_LOG_STDOUTunder "Python SDK agents" and made two claims aboutAGENTFIELD_LOG_MAX_LINE_BYTESthat are wrong.The default does not change: unset, empty, or any unrecognised value keeps the mirror on, and nothing in this repo sets the variable, so no compose file, manifest, desktop bundled node, or
aftemplate goes quiet on upgrade.Why
Refs #985. That issue is about the Python structured-log path more broadly (the stdout dump on the event loop, the
logging.StreamHandlerRLock, and a request for an async-friendly queue); this PR only closes the cross-SDK half of it — the opt-out that Python and Go already have, missing in TypeScript — plus the documentation that pointed people at a knob one SDK did not implement. The locking and event-loop concerns in #985 are untouched, so this isRefs, notFixes.Changes
fix(sdk-typescript)—mirrorToStdoutbecomes tri-state (boolean | undefined). An explicit constructor option still wins in both directions; when absent the flag resolves fromAGENTFIELD_LOG_STDOUTper emit, not in the constructor, becauseAgentbuilds one sharedExecutionLoggerand a snapshot would freeze the flag for the process lifetime (Python and Go both re-read per record). The accepted falsy spellings move into a new internalsrc/utils/envFlags.tsthatprocessLogs.tsnow shares, so the0/false/no/offlist cannot drift between modules. The helper guardsprocesswithtypeof process !== 'undefined', matching the guardExecutionLoggeralready used one line later forprocess.stdout. Serialization moved inside the mirror branch, so with the mirror off the JSON envelope is never built — the transport is handed the object, not the string.test(sdk-python)—node_logs.max_line_bytes()had no direct test even though its parsing is what the docs half now describes. Added a table pinning the clamp (100/0/-5→ 256), the default (abc→ 16384), the512abcrejection that TypeScript'sparseIntwould prefix-parse, and the pass-through (512→ 512).docs(logging)—AGENTFIELD_LOG_STDOUTmoves to "Structured logging (SDKs)" with an explicit reader list and a pointer left behind in the Python section. TheAGENTFIELD_LOG_MAX_LINE_BYTESentry drops the misleadingminimum: 256and the false claim that "the Go and TypeScript SDKs treat invalid values as unset". Two consequences that were previously undocumented are now stated so they are not later filed as regressions. Each SDK README gains a short Logging section carrying that SDK's own numbers.Validation contract
Behaviour, and the test that covers it (all in
sdk/typescript/tests/execution_logger.test.tsunless noted):0/false/no/offkeeps the mirror on — identical to todaykeeps stdout mirroring enabled for AGENTFIELD_LOG_STDOUT=%j(undefined,'',true,1,yes,on,ture)false/0/no/off, any case, surrounding whitespace ignored, writes no structured linedisables stdout mirroring for AGENTFIELD_LOG_STDOUT=%j without disabling transport(false,FALSE," False ",0,no,off)mirrorToStdoutoverrides the environment in both directionslets mirrorToStdout=$option override AGENTFIELD_LOG_STDOUT=$envre-reads AGENTFIELD_LOG_STDOUT between emits on the same loggerAgent's own construction path, the only path real users hithonors AGENTFIELD_LOG_STDOUT through the Agent construction pathprocessaccess is guarded, so the class stays usable off Nodedoes not assume process exists when resolving stdout mirroring(stubsprocesstoundefined)transport.emitis still called oncedoes not serialize the record when the mirror is disabled(atoJSONspy that must not fire)mirrorToStdout: falseexplicitly keep passing unchangednpm test, unchanged assertions0/false/no/offlist is shared, not duplicatedsrc/utils/envFlags.tsis the single definition;processLogs.tslogsEnabled()now calls it and its existing tests still pass=100— Python 256, Go 16384, TypeScript 16384sdk/python/tests/test_node_logs.py::test_max_line_bytes_pins_python_parsingpins the Python column; Go and TypeScript are read off their existing codeGET /agentfield/v1/logsdocs/ENVIRONMENT_VARIABLES.mdanddocs/api/AGENT_NODE_LOGS.md— pre-existing behaviour in all three SDKs, documented so it is not later filed as a regressionAGENTFIELD_LOG_STDOUTis filed cross-SDK with a pointer left in the Python section; theminimum: 256preamble is reworded; the Python clamp is described as governing both Python log paths;docs/api/AGENT_NODE_LOGS.mdis updated for the 256 floor and the reader list.mdfilesHow it was tested
Rebased onto
origin/main(b458f9c3, v0.1.138-rc.2) and re-ran the literal CI steps for every surface this touches:cd sdk/typescript && npm ci --no-audit --no-fund— passcd sdk/typescript && npm run lint— passcd sdk/typescript && npm test— passcd sdk/python && ruff check .(ruff 0.15.22) — passcd sdk/python && ./scripts/run_pytest.sh -q— passcd sdk/go && go mod tidy && git diff --exit-code go.mod go.sum,go build ./...,gofmt -l,go test -count=1 ./...— pass (no.gofiles change here; the Go surface is in scope only becausesdk/go/README.mdis touched)Every added line in
src/utils/envFlags.tsand in the changedExecutionLoggerbranch is exercised by the tests listed above, so patch coverage on the touched lines is full for the code half; the rest of the diff is Markdown and tests.No live control plane was started — the change is SDK-local and every path is reachable from unit tests. Each number in the docs commit was read back out of the code rather than carried over from the previous prose:
sdk/python/agentfield/node_logs.pymax_line_bytes()—max(256, int(raw, 10)),ValueError→ 16384sdk/go/agent/process_logs.goprocessLogsMaxLineBytes()—Atoierror orn < 256→ 16384sdk/typescript/src/agent/processLogs.tsmaxLineBytes()—parseInt(raw, 10), kept only when finite and>= 256, else 16384Notes / follow-ups
tests/execution_logger_methods.test.ts:97and:157,tests/execution_logger.test.ts:111) construct loggers without an explicitmirrorToStdoutand assert a line was written. They used to be env-independent because the default was a hardcodedtrue; they now resolve through the environment, so a developer withAGENTFIELD_LOG_STDOUT=falseexported in their shell — plausible while working on exactly this feature — would see three confusing failures. CI cannot go red from this (the workflow does not set the variable). The fix is abeforeEach(() => vi.stubEnv('AGENTFIELD_LOG_STDOUT', undefined))in both describe blocks; theafterEach(vi.unstubAllEnvs())added here already restores it, but it only undoes vitest's own stubs, not an inherited value.sdk/typescript/README.mdlinks the environment reference with a relative path whilesdk/python/README.mduses an absolutegithub.comURL. The Python one is deliberate — that file is the PyPI long_description, where relative links render dead. npm rewrites relative links usingrepository.url+directory, both of whichsdk/typescript/package.jsonsets, so the relative form should resolve there too; worth making all three consistent at some point.AGENTFIELD_LOG_MAX_LINE_BYTESwording says "Python and Go reject non-integers, while TypeScript prefix-parses them". Strictly, TypeScript only prefix-parses values that start with digits —abcgivesNaNand falls back to 16384 like the others. The parenthetical example (512abc→512) carries the real distinction.sdk/python/tests/test_node_logs.pyputs the new parametrized test above the file's first section banner. Cosmetic only.RLockcontention and event-loop blocking described in [Python SDK] Structured logs, event loop and Postgres instance #985, and any change to the Python emit path. Also unchanged is the divergence itself — Python clamping to 256 where Go and TypeScript reject upward to 16384 is now documented rather than unified, since aligning them would change behaviour for existing deployments.🤖 Generated with Claude Code