- When you learn something new about the codebase or introduce a new concept, update this file (
AGENTS.md) to reflect the new knowledge. This is YOUR FILE! It should grow and evolve with you. - If there is something that doesn't make sense architecturally, devex-wise, or product-wise, please update the
Requests to Humanssection below. - Always follow the established coding patterns and conventions in the codebase.
- Document any significant architectural decisions or changes.
- Prefer no code comments; when needed, use one line for a non-obvious invariant.
IMPORTANT: Always place imports at the top of Python files.
- All imports must be at the module level (top of the file), not inside functions or methods.
- The only exceptions are:
- Circular import avoidance (must be documented with a comment explaining why)
- Optional dependencies that may not be installed (must be wrapped in try/except)
- TYPE_CHECKING imports for type hints only
- Note: Imports inside functions are not caught by linting. Agents must self-enforce this rule.
❌ Never do this:
def my_function():
import re # BAD: import inside function
return re.match(...)✅ Always do this:
import re # GOOD: import at top of file
def my_function():
return re.match(...)This project uses uv for dependency management. Dependencies are organized into dependency-groups (not extras) in pyproject.toml.
Quick reference:
- Run tests:
uv run --group test python -m pytest <path> -v - Run linting:
uvx ruff check <path> - Run lint with auto-fix:
uvx ruff check --fix <path>
Common pitfalls:
- Do NOT use bare
python -m pytest— thepythonon PATH may be from a uv cache, not the project.venv. Always useuv run. - Do NOT use
--extra test—testis a dependency-group, not an optional-dependency. Use--group test. ruffis not installed in any project dependency group. Useuvx ruffto run it.- Ruff now enforces
PLWrules.PLW0602,PLW0603,PLW1641, andPLW3201are handled with spot-level inline# noqaon specific lines (not global/per-file ignore). Prefer fixing code first; if intentional, suppress only the exact line. - Be careful with
PLW1514autofixes on serialization-sensitive code (weave/type_handlers/Content/content.py,weave/type_handlers/Audio/audio.py) and mocked file I/O (weave/trace_server/costs/update_costs.py): addingencoding=changed behavior/tests, so these files are explicitly ignored for that rule.
uv build resolves the unconstrained hatchling build requirement in an
isolated environment. Keep the pinned pypa/gh-action-pypi-publish action new
enough to validate the Core Metadata version emitted by current Hatchling; for
example, Hatchling 1.32 emits Metadata 2.5, which requires the action's Twine 7
and Packaging 26 stack rather than Twine 6.1 and Packaging 25.
- Your machine should be setup for you automatically via
bin/codex_setup.sh - If you encounter any setup issues:
- Check the setup script for potential problems
- Update
bin/codex_setup.shwith necessary fixes - Document any manual steps required in this section
Important: For OpenAI Codex agents (most likely you!), your environment does not have internet access. If you need something setup beforehand, this is where you need to do it.
weave/- Core implementationweave/- Python package implementationweave/trace_server- Backend server implementation
The trace server preserves explicit Azure connection strings and account keys
for backward compatibility. When neither is configured, it uses
DefaultAzureCredential, including AKS workload identity. Read-only export
links use an account-key SAS for explicit credentials and a user-delegation SAS
for workload identity.
weave/trace_server/model_providers/model_providers.json and weave/trace_server/costs/cost_checkpoint.json are generated. Never edit them by hand — regenerate with make update_model_providers / make update_costs (see weave/Makefile).
Generate implicit cost created_at values in UTC because insertion interprets naive checkpoint timestamps as UTC.
Note: the scripts read modelsBegin.json/modelsFinal.json, which are symlinks into wandb/core and only resolve when this repo is checked out as the submodule inside wandb/core (services/weave-trace/weave-python/weave-public).
weave/vendor/weave_server_sdk/ is the generated Weave Trace API client, copied
in from wandb/core rather than depended on because it is not published to PyPI.
Never edit it by hand — regenerate it in core, then re-run
scripts/vendor_weave_server_sdk.py with --sdk-output and --core. See
weave/vendor/README.md. The Node SDK copy lives at
sdks/node/src/vendor/weave-server-sdk/ and is refreshed the same way with
scripts/vendor_node_weave_server_sdk.py. See sdks/node/src/vendor/README.md.
Persisted AgentDashboard objects intentionally use a closed, discriminated
schema. Supported panel variants and their configuration fields must be added
to builtin_object_classes/agent_dashboard.py; do not replace panel settings
with an untyped dictionary. After changing the model, run
make synchronize-base-object-schemas from the repository root so the Python
schema and the dependent Core frontend types stay aligned.
Custom Runtime registration is a desired-state facade over the existing
Provider and ProviderModel built-in objects. Keep ProviderModel.provider
as the Provider digest and preserve custom::<provider>::<model> selectors;
those representations are consumed by inference and the current UI.
Inference caches resolved custom-provider configuration per replica, so updates
may remain stale for up to 60 seconds.
When trace-server request/response models or route schemas change, regenerate
the TypeScript client in wandb/core and re-vendor it with
scripts/vendor_node_weave_server_sdk.py. See sdks/node/src/vendor/README.md.
Evaluation result rows merge agent span links from two sources: legacy
weave.genai_span_ref call attributes and OTel spans whose promoted
eval_run_id plus eval_predict_and_score_call_id columns identify the
trial. Keep the promoted-column hydration best-effort so eval results remain
available during rolling deploys.
weave.invoking_span ({trace_id, span_id}, hex) points the other way, from a
call to the OTel span that was current when it started — an agent's
execute_tool span in the case it is built for, but any ambient instrumentation
qualifies. create_call writes it on every call started inside that span, not
just the outermost one: skipping when the parent call already carries it marks
every other level, because the check reads the parent's recorded state and the
parent may itself have skipped. A reader must treat a missing key and a pair
that matches no span as normal — the write does not check that the span reaches
our backend. Spans opened by trace_server.tracing are the one exclusion, and
they are recognised through WEAVE_SERVER_SPAN_KEY on the context rather than
off the span object, which under the OTel→DD bridge is not an SDK span.
WeaveClient.createCall in the Node SDK writes the same key with the same
meaning, but sees fewer spans. That SDK registers no OTel context manager and
never calls startActiveSpan, so its own spans never become current and
getActiveSpan returns only instrumentation the user installed themselves.
Linking the execute_tool spans the openai-agents and google_adk
integrations emit has to read those integrations' own state instead — the
agents SDK keeps its current span in its own AsyncLocalStorage, and the ADK
plugin keeps a map of open tool spans.
If sdks/node/node_modules is missing, run pnpm install --frozen-lockfile in sdks/node first. Do not use npm install; this SDK is pinned to pnpm.
- Testing is managed by
noxwith multiple shards for different Python versions - Each shard represents specific package configurations
- Never create a
_FakeServer, stub, or mockTraceServerInterfaceto test server-side logic. Use the existingclientfixture (givesclient.server+client.project_id) or thetrace_serverfixture, which run against a real ClickHouse backend. Build inputs with the real APIs (obj_create,table_create, etc.). Mock only external services we don't own.
Assert on the full value, not that a fragment appears somewhere inside a
stringified payload. Substring (in) and membership checks pass on accidental
matches and let the rest of the payload drift undetected, so prefer them only
when membership in a collection is genuinely the contract under test.
❌ Never do this:
assert "short memo" in msg # substring of a serialized blob
assert "error" in str(response)
assert "note" in feedback.payload # key-presence instead of value✅ Always do this:
assert feedback.payload == {"note": "short memo", "emoji": "👍"} # whole object
assert feedback.payload["note"] == "short memo" # exact fieldIf you only care about one field, pin that field with ==; if you care about
the shape, compare the whole dict/object. The same rule applies to SQL: assert
the complete query string, never assert "WHERE x" in sql.
- Integration tests replay provider traffic from VCR cassettes while the weave
client concurrently talks to ClickHouse over localhost HTTP. vcrpy is not
safe for that concurrency out of the box: its urllib3 passthrough wraps real
sends in
force_reset(), which briefly unpatches httpx/httpcore globally and lets provider calls escape an active cassette to the live API (seen as real OpenAI 429s in CI).tests/integrations/conftest.pycarries two autouse fixtures that prevent this — read their docstrings before touching VCR config, and keeptests/integrations/langchain/test_vcr_isolation.pygreen. - Corollary: a real provider 4xx during a
record_mode="none"cassette means VCR's patches were absent at request time (an isolation bug), not that the cassette failed to match — matching failures raiseCannotOverwriteExistingCassetteExceptioninstead.
Focus on these primary test shards:
tests-3.12(shard='trace')- Core tracing functionalitytests-3.12(shard='flow')- Higher level work"flow" objectstests-3.12(shard='trace_server')- Server implementationtests-3.12(shard='trace_server_bindings')- Server bindings
The Claude Agent SDK shard combines ClickHouse-backed calls tests with
no-server OTel tests. PR CI runs tests-3.10(shard='claude_agent_sdk') once
without a marker filter so both tracing paths contribute coverage.
Calls-based tests must include the integration's intentional text and thinking
child calls in exact operation-set assertions.
Every other shard, flow included, is run by CI with -m "trace_server", and
that filter comes from the workflow rather than from noxfile.py. A test that
uses no server fixture therefore never runs in CI unless it carries
@pytest.mark.trace_server itself.
IMPORTANT: Any test depending on the client fixture runs against ClickHouse, the only trace-server backend. Locally, the test fixtures auto-start a ClickHouse Docker container if one isn't already running, so Docker must be available. Pass --clickhouse-process=true to use a local clickhouse-server binary instead of Docker.
- Run all tests in a specific shard:
nox --no-install -e "tests-3.12(shard='trace')" - Run a specific test by appending
-- [test]like so:nox --no-install -e "tests-3.12(shard='trace')" -- tests/trace/test_client_trace.py::test_simple_op - Run linting:
nox --no-install -e lint(Note: This will modify files)
Important: Since you don't have internet access, you must run nox with --no-install. We have pre-installed the requirements on the above shards.
Test paths must be relative to the repository root, not the tests/ directory.
Examples:
- ✅ CORRECT:
-- tests/trace/test_dataset.py::test_basic_dataset_lifecycle - ❌ WRONG:
-- trace/test_dataset.py::test_basic_dataset_lifecycle
The --trace-server flag selects the backend: clickhouse (default) or
fake (in-memory).
Fake / in-memory (Fastest for Development):
nox --no-install -e "tests-3.12(shard='trace')" -- tests/trace/test_client_trace.py::test_simple_op --trace-server=fakeThe fake backend (weave/trace_server/in_memory_trace_server.py,
InMemoryTraceServer) is a pure-Python, dict-backed drop-in replacement that
lives parallel to the ClickHouse implementation. It replicates ClickHouse
(the production backend) at the interface level: JSON_VALUE string typing for
dynamic fields, to*OrNull cast rules, NULLS-LAST ordering, DateTime64
comparisons, and computed summary fields. Tests that assert ClickHouse
internals (SQL, table routing/residence, insert batching, bucket file
storage) are gated with client_is_clickhouse and skip on the fake. No
files, no SQL, no Docker.
ClickHouse (the real backend):
nox --no-install -e "tests-3.12(shard='trace')" -- tests/trace/test_client_trace.py::test_simple_opClickHouse is the only trace-server backend (--trace-server defaults to
clickhouse):
nox --no-install -e "tests-3.12(shard='trace')" -- tests/trace/test_client_trace.py::test_simple_opNote: ClickHouse tests require Docker to be running (the fixtures start a
container automatically), or a local clickhouse-server binary with
--clickhouse-process=true. When neither is available, use the in-memory
fake with --trace-server=fake.
The --remote-http-trace-server flag controls which remote HTTP trace server implementation is used for testing trace server bindings:
RemoteHTTPTraceServer (Default):
nox --no-install -e "tests-3.12(shard='trace_server_bindings')" -- tests/trace_server_bindings/test_trace_server_bindings.py --remote-http-trace-server=remoteStainlessRemoteHTTPTraceServer:
nox --no-install -e "tests-3.12(shard='trace_server_bindings')" -- tests/trace_server_bindings/test_trace_server_bindings.py --remote-http-trace-server=stainlessImportant Notes:
- The
--remote-http-trace-serverflag is for trace server binding implementation (RemoteHTTPTraceServer vs StainlessRemoteHTTPTraceServer) - Both implementations share the same test file; the flag determines which server class is used
Color Flag Conflicts:
If you encounter an error like Can not specify both --no-color and --force-color, this is due to conflicting environment variables. Unset them before running nox:
unset NO_COLOR FORCE_COLOR && nox --no-install -e "tests-3.12(shard='trace')" -- tests/trace/test_dataset.py::test_basic_dataset_lifecyclePrek stashing behavior:
nox --no-install -e lint runs prek, and prek stashes unstaged changes before running hooks. If you need to validate a fix to one file (for example with --mypy-only), stage that file first or run the checker directly, otherwise hooks may run against older content.
Markdown serialization and MTSAAS env:
weave/type_handlers/Markdown/markdown.py only stores large markdown payloads in markup.md when is_mtsaas() is true. If local WANDB_BASE_URL/WF_TRACE_SERVER_URL differs from CI defaults, test_serialization_correctness[markdown] may fail locally with inline markup differences. For CI-like behavior, set:
WF_TRACE_SERVER_URL=https://trace.wandb.ai nox --no-install -e "tests-3.12(shard='trace')" -- tests/trace/data_serialization/test_serialization_correctness.py::test_serialization_correctness[markdown]If you encounter import errors or missing modules, reinstall the test shard environment:
nox --install-only -e "tests-3.12(shard='trace')"Then run your tests with --no-install as usual.
The langchain integration tests work fully on macOS including chromadb/vector store tests.
Running LangChain Tests:
nox --no-install -e "tests-3.12(shard='langchain')" -- tests/integrations/langchain/The Node SDK (sdks/node) is a pnpm project — it ships a pnpm-lock.yaml
and pins "packageManager": "pnpm@10.8.1" in package.json. Do not run
npm i: npm's resolver crashes trying to dedupe pnpm's symlink node_modules
(TypeError: Cannot read properties of null (reading 'matches')).
cd sdks/node
pnpm install
pnpm test
To run an example (e.g. the Claude Agent SDK demo), dist/ must be built first
(import 'weave' self-resolves via the package exports to dist/index.mjs);
pnpm install builds it via the prepare script. Then:
pnpm exec tsx examples/claudeAgents.ts
Pure ESM auto-instrumentation requires the weave/instrument loader. For
one-off tsx live validation scripts where that loader is not installed, call
wrapClaudeAgentSdk() and consume the returned module view so tracing is
deterministic.
sdks/node/src/integrations/integrationMetadata.tsremains shared:asAttributes()supplies nested provenance to Weave-call integrations, whileasOtelAttributes()supplies canonicalweave.integration.nameandweave.integration.versionidentity to OTel integrations and preserves flattenedweave.integration.meta.*provenance. OTel scalar metadata stays typed; non-scalar values are stringified.
- A batch result is a discriminated union and only its
succeededvariant carries amessage; a batch ends when every request has succeeded, errored, been canceled or expired. Readmessagewithout checking the discriminator and the reducer storesundefined, which the usage summarizer then throws on, out of the stream iterator'sfinally. patchAnthropicMessagesCreatewrapsMessages.prototype.createin aProxy, so property reads still reach the mock behind it and a test can reconfigure it through the patched prototype.patchBatchApiassignsop()'s plain wrapper toBatchescreate,retrieveandresultsinstead, so for those a test reconfigures the mock it captured before patching, or reaches it through__wrappedFunction.
- Anthropic reports
input_tokensas fresh, uncached prompt only and bills the rest throughcache_read_input_tokensandcache_creation_input_tokens. Weave's cost math subtracts those two from the prompt total, so the usage summary has to carry an inclusiveinput_tokens.totalInputTokens()inintegrations/anthropicUsage.tsis where that sum lives, for this integration and for the Claude Agent SDK OTel tracer; it mirrors the Pythontotal_input_tokens(). - Only the summary is normalized. The provider's own
usageobject stays untouched, so the response the caller receives keeps Anthropic's own numbers. - Streaming
message_deltausage is cumulative, and every field exceptoutput_tokensis nullable. Skip the null ones, as the vendor's own client does, and overwrite rather than add — a delta carries the running total, not an increment.
client.get()rebuilds aWeaveImagefrom aPIL.Image.Imagepayload and aWeaveAudiofrom awave.Wave_readone, downloading the stored file fromref.projectId, not the client's project.- It also rebuilds a
Datefrom adatetime.datetimepayload, which stores no file: Python writes the ISO string inline into the record'sval. ADateis a UTC instant with millisecond resolution, so the round trip keeps the moment but loses the original offset and truncates Python's microseconds. It reads whole-minute offsets only, so a timestamp from before its zone adopted one comes back as anInvalid Date(Africa/Monroviaran on-00:44:30until 1972). wave.Wave_readis what this SDK writes, and what Python wrote until May 2025. Both store oneaudio.wavand no metadata file.- Python now records every
Audioandwave.Wave_readobject asweave.type_handlers.Audio.audio.Audiowith an extra_metadata.json, becauseregister()inweave/type_handlers/Audio/audio.pyputs theAudioserializer first and itsis_audio_instancepredicate also acceptswave.Wave_read.
- The existing
Turn.record({messages: [...]})path records input messages; Turn stores these separately from terminaloutputMessages. - Record a terminal agent result with
Turn.record({outputMessages: [...]});Turn.end()serializes it onto theinvoke_agentspan asgen_ai.output.messages.
Conversation,Turn,LLM,Tool, andSubAgentemit canonicalinvoke_agent,chat, andexecute_toolspans through/agents/otel/v1/traces.- These spans go through the
BasicTracerProviderthatgenai/provider.tsbuilds, never the OTel global registry. A processor that must see them belongs in that provider'sspanProcessors, which is built lazily on the first span and rebuilt on a project switch — registering once frominit()misses both. - At a span's attribute limit OTel JS drops the incoming attribute, where
Python's
BoundedAttributesevicts the oldest, so the two SDKs keep different halves of an overflowing set. The eval linker writes the pair eval results match on before its display-only attributes for that reason, and the op linker sits after it in the Node provider's array for the same reason. Registering the two linkers in opposite order is how both SDKs end up keeping the same half. - For callback or generator integrations, create
Conversation,Turn, andLLMin shortrunIsolated()scopes, then retain and pass explicit handles.ToolandSubAgentdo not use ambient state. - Keep response models on child
chatspans; usesetAttributes()for fields without typedrecord()methods. - Every TypeScript GenAI span handle supports
recordError(error)to mark a failure without ending the span; terminal failures can useend({error}). The SDK deriveserror.typefromerror.name. AfterrecordError(), close withend()without passing the same error again. TurnandSubAgentare logical in-processinvoke_agentspans: emitSpanKind.INTERNALand do not setgen_ai.provider.name; provider identity belongs on child model spans.- Record
invoke_agentinputs and outputs throughTurn.record()orSubAgent.record(). Keepgen_ai.tool.*attributes onexecute_toolspans, not agent spans. - The Claude Agent SDK integration keeps each
Toolopen until its matchingtool_result. Toolaccepts JSON-compatible arguments and results. It records strings as-is and serializes structured values for OTel attributes. PreferTool.end({result, error}); mutableTool.resultremains only for backward compatibility.- Agent span list queries omit heavy message, tool-payload, and raw-span fields;
use an
AgentSpansQueryReqwithinclude_details=Truewhen validating stored span details. - Parallel/background Claude Agent SDK
Agentcalls can emit anasync_launchedorremote_launchedtool result before forwarded child messages, then finish via atask_notification. Keep oneSubAgentkeyed by tool-use ID across turn boundaries until that notification arrives. - Record every background
task_notificationstatus on itsSubAgentasclaude_agent_sdk.task.statussocompleted,failed, andstoppedremain distinguishable independently of standarderror.typemetadata. - Route a forwarded assistant message to an already-open
SubAgentbefore creating aTurn; background messages can arrive after the root result and must not create an empty root turn or consume the next pending input. - For synchronous
Agent/Taskcompletion, read terminal output from the structuredSDKUserMessage.tool_use_result.content; use the model-facingtool_resultblock content only as a compatibility fallback. Background completion output continues to come fromtask_notification.summary. - Read a subagent's lifetime from the launch request (
run_in_background, orisolation: 'remote', which is always backgrounded) rather than inferring it from the result status — an unrecognized status must not downgrade it. - Surviving a turn boundary is transitive: a background
SubAgent's openTools and nestedSubAgents survive with it. Closing a descendant early also drops it from the open map, so the next forwarded message re-creates it under the wrongTurn. - Buffer a subagent's terminal state (and the timestamp it was observed) instead
of ending the span on the spot:
SpanBaseonly records an error throughend(), and the span must close after its children. Pass the timestamp asendTimeso the deferral doesn't inflate the duration. - For streamed sessions, queue observed user inputs in FIFO order and close one
Turnfor each matching SDKresult. - Treat
Agentand legacyTasktool calls as subagents keyed by tool-call ID, and preserve the caller'sforwardSubagentTextoption.
- Tap
AsyncIterable<SDKUserMessage>prompts without consuming or cloning them; map text and base64/URL images to GenAI text, blob, and URI parts. - Each user-to-result cycle is one root turn. Resumed queries share
session_id, and long-lived streamed inputs queue turns in FIFO order. - Treat
blob.contentas media/ref data exposed throughcontent_refs, not prose. - Run the example from
sdks/node, use repository-relative fixtures, keep text and image sessions separate, and set bothtoolsandallowedTools.
- Title format:
<type>(<scope>): <description>, where<type>is one ofchore,feat,fix,perf,refactor,revert,style,security,test. A scope is required (requireScope: true) and CI validates it (.github/workflows/pr.yaml). - Pick the scope by which SDK/area the change touches:
weave_ts— required for ALL TypeScript / Node SDK changes (anything undersdks/node/). Any PR that modifies the TS SDK must be marked(weave_ts), e.g.fix(weave_ts): ...,feat(weave_ts): ...,chore(weave_ts): ....weave— the Python SDK and trace server (the default forweave/…changes), e.g.fix(weave): ...,feat(weave): ...,chore(weave): ....- Other valid scopes (see
pr.yamlfor the authoritative list):ui,app,dev,deps,inference.
- If a single PR spans both the Python and TS SDKs, prefer splitting it; if that isn't practical, scope it to the SDK that carries the primary change and call out the other in the PR body.
- Provide detailed PR summaries including:
- Purpose of changes
- Testing performed
- Any breaking changes
- Related issues/PRs
- Run lint
- Ensure all tests pass
- Update documentation if needed
- Check for any breaking changes
- Prefer native
${{ secrets.GITHUB_TOKEN }}for repository-local workflow operations that only need the current repo. - Use
actions/create-github-app-token@v3withvars.WANDBOT_3000_APP_IDandsecrets.WANDBOT_3000_PRIVATE_KEYfor cross-repository access or bot pushes that must behave like app-authenticated writes. - Do not introduce new GitHub PAT secrets in workflows unless there is no viable
GITHUB_TOKENor GitHub App alternative.
- Python code follows standard module organization
- TypeScript/React components are organized by feature
- Shared utilities should be placed in appropriate common directories
- Use appropriate error types from
weave.errors - Include meaningful error messages
- Add error handling tests
- Built-in and API-key-authenticated custom providers use LiteLLM.
- Custom runtimes without an API key use the OpenAI client directly so configured headers and unauthenticated endpoints do not receive a bearer header.
- OpenAI Responses and Chat Completions report
cache_write_tokensunderusage.input_tokens_detailsandusage.prompt_tokens_details, respectively. Normalize either field when present instead of parsing model names. The standard cost registry supplies each model'scache_creation_input_token_cost. Becauseinput_tokensandprompt_tokensincludecached_tokensandcache_write_tokens, an unmappedcache_write_tokensvalue is billed at the Input rate instead of the rate for cache writes. - The OpenAI CI shard runs with
-m trace_server; usage-normalization coverage must therefore come from client-backed pricing cases, not only standalone unit tests intests/integrations/openai/test_openai_sdk.py.
- Since autopatching was removed from
weave.init(), integration tests must explicitly patch their integrations - Add a fixture with
autouse=Trueat the top of each integration test file to enable patching - Example pattern:
@pytest.fixture(autouse=True) def patch_integration() -> Generator[None, None, None]: patcher = get_integration_patcher() patcher.attempt_patch() yield patcher.undo_patch()
- Some integrations (like instructor) may need to patch multiple libraries
Turn.messagesstores input messages, whileTurn.output_messagesstores the terminal agent response.Turn.record(messages=..., output_messages=...)replaces the two lists independently.SubAgentowns its input/output messages and originating tool-call arguments/result. Integrations populate them throughSubAgent.record()so_build_attrs()appliesinclude_contentand PII redaction; do not write those content attributes directly withset_attributes().Tool,LLM,SubAgent, andTurnexposerecord_error(error)to mark a failure without ending the span. It deriveserror.typefrom the exception; callend()separately. Context managers do both for escaping exceptions.- Mypy requires explicit
return Nonepaths in functions annotated withT | None; barereturnand implicit fallthrough trigger return-value errors. - Python
Turnspans are same-processinvoke_agentoperations and use OTel's defaultSpanKind.INTERNAL. Do not setgen_ai.provider.nameon aTurn; set it on childLLM/chatspans, because one turn can use multiple providers. - Keep streaming and batch paths aligned:
Turn._build_attrs()must apply content gating and PII redaction to both lists before passing them toinvoke_agent_attributes(), andlog_turn()must accept both fields. - The Python Claude Agent SDK integration taps async prompt iterables, queues
user inputs in FIFO order, and closes one
Turnfor eachResultMessage. - Keep its output adapters split by SDK contract: string
query()and eachClaudeSDKClient.receive_response()use the linear single-turn tracer, while only standalonequery(AsyncIterable)uses multi-result queue/lookahead logic. - Standalone async-iterable queries can emit bootstrap
system/initbefore the first prompt is consumed. Defer only that init for trace attribution; forward other output received without a pending query-triggering input without attaching it to the next turn, especially late background-task notifications. Correctly completing background subagents across results requires separate conversation-scoped task ownership. - Name the corresponding test cases "string prompt" and "async-iterable prompt" rather than sync/async: both SDK entry points are asynchronous. Keep one-input success and pre/post-result failure assertions paired across them.
- SDK output can fail before the first message or between completed turns. Keep
the submitted/pending input observable by ending its
Turnthrough the exception path so the span records the error before the exception propagates. - The Agents conversation tree renders recognized GenAI operations. Generic
OTel spans with an empty
gen_ai.operation.nameremain stored and queryable throughagent_spans_query, but are omitted from that visual tree; use a semantic marker span when a UI-visible trace regression is required. - For spans with
ERRORstatus, missingerror_typeandstatus_messagefall back to the latest OTelexceptionevent. Expliciterror.typeand status-message values take precedence, and handled exception events on non-error spans remain raw-only. - Content spans with
ERRORstatus emit a chat message even without text or reasoning so the conversation timeline retains the failure. Successful empty tool-calling steps remain omitted. - Use the Trace tree view for parentage comparisons. The default flamegraph
collapses overlapping siblings into synthetic groups, which can make flat
and nested traces look deceptively similar; give live-example spans realistic
duration and verify their stored parent IDs with
agent_spans_query.
- Anthropic reports Claude Agent SDK
input_tokensas fresh, uncached input only. Weave usage requires an inclusive prompt total, withcache_read_input_tokensandcache_creation_input_tokensrepresented as subsets ofinput_tokens, because cost and cache-hit-rate rollups use that convention. - Both Python tracing paths must normalize aggregate result usage through
weave/integrations/claude_agent_sdk/usage.py. Keep the SDK's yieldedResultMessageand the calls-based root output unchanged; normalize the calls-based usage summary and the OTel span attributes consumed by Weave rollups. - Regression coverage must exercise both the calls-based and OTel integrations with nonzero cache-read and cache-creation counts.
- The OTel-selected Claude Agent SDK path composes the Python GenAI
Conversation,Turn,LLM,Tool, andSubAgenthandles; it must not create raw OTel spans or call the low-level GenAI attribute builders itself. Useset_attributes()only for semantic fields the typed handles do not expose. The legacy calls-based path remains separate. - Tap Python
AsyncIterable[dict]prompts without consuming or cloning them. Map Claude text and base64/URL image blocks to GenAI text, blob, and URI parts; media payloads remain data forcontent_refs, not chat prose. - Treat
Agentand legacyTasktool calls as subagents keyed by tool-use ID. Route nested assistant messages and tools throughparent_tool_use_id. Synchronous calls close on their matching tool result; background launch acknowledgements stay open untiltask_notification. Dispatch task events throughSystemMessage.subtypeanddata, maptask_idto tool-use ID fromtask_started/task_progress, and do not import typed task messages that are absent from older supported Claude Agent SDK versions. - Start those subagents with
SubAgent.start(set_current=False), never__enter__. Parallel delegations close in completion order, andend()detaches throughContextVar.reset, so an out-of-LIFO close leaves the ambient OTel context pointing at an ended span — user code running between streamed messages would nest under it. Children nest either way, becausestart_llm/start_tool/start_subagentthread an explicit parent. - Stream adapters can create child handles when work starts, then enter them
with a normal
withwhen the completion message arrives. Preserve logical timing withLLM.started_atand an explicitTool.started_atinstead of keeping contexts open withExitStack.
SensitiveDataPolicyis a closedofforpii-v1enum. Unknown values fail instead of falling back to a policy.redact_pii_valuewalks supported trace values copy-on-write, combines credential-key replacement with PII replacement, and never scans dictionary keys. Clean subtrees retain their identity.- Complete Weave refs, valid base64 data URLs, and valid standalone base64 payloads pass through unchanged. Malformed lookalikes remain eligible for scanning.
- The three call converters in
clickhouse/schema_converters.pyrunredact_sensitive_keysoverinputsandattributesbefore extracting refs, so a field whose name matches the policy inweave/trace_server/credential_redaction.pyis stored with its string value replaced.in_memory_trace_server.pyapplies the same function, so both backends read back the same thing. - The same converters redact
otel_dump. It is a raw copy of the client's span, so it carries the attribute values thatattributes_dumpholds a second time, and it is a public field on the insert schemas, so the OTel route is not its only producer. - The walk is copy-on-write and replaces only non-empty strings. Both properties are load-bearing — see the module docstring — so keep them if you touch it.
GenAIOTelExportReq.sensitive_data_policyis an internal, omittable, non-nullable field. Omission resolves tooff.- An authorized server route may set
pii-v1from the owning organization's policy. Never populate the field from caller-controlled OTLP content or a process environment fallback. - Agent OTel ingest parses and redacts every span (each shared resource once)
before credential redaction, blob stripping, derived-column extraction, or
insertion starts for any of them. A redaction failure rejects the whole
request before any Content file write or insert; a value nested too deeply
to scan is rejected as
RequestTooLarge(HTTP 413). - PII redaction covers span and event names, all four attribute containers,
and the status message. Names feed the
span_name,operation_name, andagent_namegrouping columns, so a PII-bearing name changes grouping and agent identity; redaction wins that trade-off. IDs, trace state, and timestamps are structural and remain unchanged. pii-v1scans strings and never decodes payloads: non-string leaves, including raw bytes (stored base64-encoded in dumps), pass through unchanged, like the preserved data URLs and standalone base64.
- The agents OTel ingest calls
redact_credentials_from_spanbeforestrip_inline_blobs_from_span, and outside the file-storage guard around it. Both are load-bearing — see that function's docstring — so keep the order and the placement if you touch it. - It redacts all four attribute containers a span carries: its own, its resource, its events and its links. The output columns derive from those too, so on this path a credential-shaped name inside a structured output is replaced as well.
- The completions path builds a span row without a parsed OTel span, so it never
reaches that hook.
build_completion_spanredacts the request once and every column that can carry a client string reads that copy; the provider response is left as the provider sent it, because a response is generated content.
- Update relevant docstrings for Python code
- Add JSDoc comments for TypeScript code
- Update this file when introducing new patterns or concepts
Weave provides automatic implicit patching for all supported integrations using an import hook mechanism:
- Automatic Patching: Libraries are automatically patched regardless of when they are imported
- Import Hook: An import hook intercepts library imports and applies patches automatically
- Explicit Patching: Optional manual patching is still available for fine-grained control
Example:
# Automatic patching - works regardless of import order!
# Option 1: Import before weave.init()
import openai
import weave
weave.init('my-project') # OpenAI is automatically patched!
# Option 2: Import after weave.init()
import weave
weave.init('my-project')
import anthropic # Automatically patched via import hook!
# Option 3: Explicit patching (optional)
import weave
weave.init('my-project')
weave.patch_openai() # Manually patch if neededAll integrations have corresponding patch functions for explicit control: patch_openai(), patch_anthropic(), patch_mistral(), etc.
The import hook uses Python's sys.meta_path to intercept imports and automatically apply patches when supported libraries are imported. This ensures seamless integration tracking without requiring users to manage import order or make explicit patch calls.
If you prefer explicit control over which integrations are patched, you can disable implicit patching:
# Via settings parameter
weave.init('my-project', settings={'implicitly_patch_integrations': False})
# Via environment variable
export WEAVE_IMPLICITLY_PATCH_INTEGRATIONS=falseWhen disabled, you must explicitly call patch functions like weave.patch_openai() to enable tracing for integrations.
This section contains a list of questions, clarifications, or tasks that LLM agents wish to have humans complete. If there is something that doesn't make sense architecturally, devex-wise, or product-wise, please update this file and the humans will take care of it. Think of this as the reverse-task assignment - a place where you can communicate back to us.
- Add TypeScript testing guidelines
- Add
output_messagesto PythonTurn.record()for parity with TypeScriptTurn.record({outputMessages: ...}); the lower-level Pythoninvoke_agent_attributes()builder already supports agent output. - Repair the existing
pnpm run typecheck:examplesfailures caused by OpenAI type drift inexamples/agent.ts,classesWithOps.ts,imageGeneration.ts,quickstart*.ts, andstreamFunctionCalls.ts. - Isolate Node GenAI test exit hooks: a full in-band Jest run can register
more than ten
beforeExitlisteners and flush queued test calls to the real trace server after teardown when developer W&B credentials are present, turning an otherwise-green run into a post-test failure. - ...