Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,13 @@ pnpm exec tsx examples/claudeAgents.ts
- `Turn.messages` stores input messages, while `Turn.output_messages` stores
the terminal agent response. `Turn.record(messages=..., output_messages=...)`
replaces the two lists independently.
- Mypy requires explicit `return None` paths in functions annotated with
`T | None`; bare `return` and implicit fallthrough trigger return-value
errors.
- Python `Turn` spans are same-process `invoke_agent` operations and use OTel's
default `SpanKind.INTERNAL`. Do not set `gen_ai.provider.name` on a `Turn`;
set it on child `LLM`/`chat` spans, 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 to
`invoke_agent_attributes()`, and `log_turn()` must accept both fields.
Expand All @@ -457,6 +464,15 @@ pnpm exec tsx examples/claudeAgents.ts
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`, and `Tool` handles; it must not create raw
OTel spans or call the low-level GenAI attribute builders itself. Use
`set_attributes()` only for semantic fields the typed handles do not expose.
The legacy calls-based path remains separate.
- Stream adapters can create child handles when work starts, then enter them
with a normal `with` when the completion message arrives. Preserve logical
timing with `LLM.started_at` and an explicit `Tool.started_at` instead of
keeping contexts open with `ExitStack`.

### Credential-shaped fields in call inputs and attributes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@
from opentelemetry.sdk.trace import TracerProvider as SDKTracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
from opentelemetry.trace import StatusCode
from opentelemetry.trace import SpanKind, StatusCode

from tests.integrations.claude_agent_sdk.conftest import ReplayTransport, load_cassette
from weave.conversation import agent_name_override
from weave.integrations.claude_agent_sdk.otel_integration import (
get_claude_agent_sdk_otel_patcher,
)
from weave.trace.settings import override_settings


@pytest.fixture
Expand Down Expand Up @@ -55,6 +56,16 @@ def patch_claude_agent_sdk_otel() -> Generator[None]:
patcher.undo_patch()


@pytest.fixture(autouse=True)
def disable_capture_info() -> Generator[None]:
"""Keep exact span payload assertions independent of host metadata."""
with override_settings(
capture_client_info=False,
capture_system_info=False,
):
yield


# --- helpers ----------------------------------------------------------------


Expand All @@ -65,7 +76,7 @@ def get_attrs(span: Any) -> dict[str, Any]:
def check_integration_and_strip(attrs: dict[str, Any]) -> dict[str, Any]:
"""Assert + remove the flattened integration.* provenance keys.

The agent OTel processor stamps integration provenance on every span; pop it
Conversation attributes stamp integration provenance on every span; pop it
here so the exact-shape assertions below stay focused on the GenAI semconv keys.
"""
assert attrs["integration.name"] == "claude_agent_sdk"
Expand Down Expand Up @@ -116,6 +127,7 @@ async def run_query(cassette: str, prompt: str) -> None:
async def test_simple_text_query_otel(otel_spans: InMemorySpanExporter) -> None:
await run_query("simple_text_response", "What is 2+2?")
spans = otel_spans.get_finished_spans()
assert {span.instrumentation_scope.name for span in spans} == {"weave.conversation"}

agent_spans = get_spans_by_op(spans, "invoke_agent")
chat_spans = get_spans_by_op(spans, "chat")
Expand All @@ -126,10 +138,10 @@ async def test_simple_text_query_otel(otel_spans: InMemorySpanExporter) -> None:
# glance and any spec drift (added/removed/renamed keys) fails the test.
agent_span = agent_spans[0]
assert agent_span.name == "invoke_agent claude_agent_sdk"
assert agent_span.kind == SpanKind.INTERNAL
assert check_integration_and_strip(get_attrs(agent_span)) == {
"gen_ai.operation.name": "invoke_agent",
"gen_ai.agent.name": "claude_agent_sdk",
"gen_ai.provider.name": "anthropic",
"gen_ai.conversation.id": "s-abc123",
"gen_ai.request.model": "claude-sonnet-4-6",
"gen_ai.input.messages": (
Expand Down Expand Up @@ -358,8 +370,7 @@ async def test_custom_agent_name_query_otel(otel_spans: InMemorySpanExporter) ->
assert agent_span.name == "invoke_agent research_agent"
attrs = check_integration_and_strip(get_attrs(agent_span))
assert attrs["gen_ai.agent.name"] == "research_agent"
# Only the agent name is overridden — the rest of the GenAI shape is intact.
assert attrs["gen_ai.provider.name"] == "anthropic"
# Only the agent name is overridden — the model remains intact.
assert attrs["gen_ai.request.model"] == "claude-sonnet-4-6"


Expand Down
Loading
Loading