Metadata-Version: 2.5
Name: observe-instrumentation-genai-claude-agent-sdk
Version: 0.1.0
Summary: Claude Agent SDK instrumentation for Observe, emitting OpenTelemetry GenAI semantic conventions.
Project-URL: Homepage, https://github.com/snowflakedb/observe-otel-genai-python/tree/main/instrumentation/opentelemetry-instrumentation-genai-claude-agent-sdk
Project-URL: Repository, https://github.com/snowflakedb/observe-otel-genai-python
Author: Snowflake Inc.
License-Expression: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: 3.15
Requires-Python: >=3.11
Requires-Dist: observe-util-genai<1,>=0.1.0
Requires-Dist: opentelemetry-api~=1.45
Requires-Dist: opentelemetry-instrumentation<1,>=0.64b0
Requires-Dist: opentelemetry-semantic-conventions<1,>=0.64b0
Requires-Dist: wrapt<3.0.0,>=1.17.0
Provides-Extra: instruments
Requires-Dist: claude-agent-sdk<0.3,>=0.2.140; extra == 'instruments'
Description-Content-Type: text/x-rst

Observe Claude Agent SDK Instrumentation
========================================

This library traces LLM requests made through the
`Claude Agent SDK <https://github.com/anthropics/claude-agent-sdk-python>`_ and
emits `OpenTelemetry GenAI semantic conventions
<https://opentelemetry.io/docs/specs/semconv/gen-ai/>`_.

Don't install it next to the OpenTelemetry project's
``opentelemetry-instrumentation-genai-claude-agent-sdk``: both instrument the
same SDK, so every call would be recorded twice.

Installation
------------

::

    pip install observe-instrumentation-genai-claude-agent-sdk

If you don't have a Claude Agent SDK application yet, try our `examples <examples>`_
which only need a valid Anthropic API key.

Check out the `zero-code example <examples/zero-code>`_ for a quick start.

Usage
-----

This section describes how to set up Claude Agent SDK instrumentation if you're setting OpenTelemetry up manually.
Check out the `manual example <examples/manual>`_ for more details.

.. code-block:: python

    from observeinc.instrumentation.genai.claude_agent_sdk import ClaudeAgentSDKInstrumentor

    # Instrument Claude Agent SDK
    ClaudeAgentSDKInstrumentor().instrument()

    # Use Claude Agent SDK as normal
    import anyio
    from claude_agent_sdk import ClaudeAgentOptions, AgentDefinition, AssistantMessage, TextBlock, query

    async def main():
        options = ClaudeAgentOptions(
            agents={
                "assistant": AgentDefinition(
                    description="A helpful assistant",
                    prompt="You are a helpful assistant.",
                    tools=["Read"],
                    model="sonnet",
                ),
            },
            include_partial_messages=True,
        )

        async for message in query(
            prompt="Hello, Claude!",
            options=options,
        ):
            if isinstance(message, AssistantMessage):
                for block in message.content:
                    if isinstance(block, TextBlock):
                        print(block.text)

    anyio.run(main)

Call ``instrument()`` before ``from claude_agent_sdk import query``. The
instrumentation replaces ``query`` on the ``claude_agent_sdk`` module, so a
``query`` imported earlier still points at the original function and its calls
are not traced. Calls to ``claude_agent_sdk.query(...)`` and ``ClaudeSDKClient`` are
traced regardless of import order. This only matters for manual setup:
``opentelemetry-instrument`` instruments before your code runs.

With ``ClaudeSDKClient``, each turn of a connection gets its own
``invoke_agent`` span, however many ``receive_response()`` calls read it. The
span opens under whatever span was active when you called ``query()`` for that
turn. A turn that starts background work stays open until that work finishes or
the next turn begins, so its span can include the time you spent before sending
the next prompt.

Configuration
-------------

Capture Message Content
***********************

By default, prompts and completions are not captured. To capture message content, set the
environment variable ``OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT`` to one of
``NO_CONTENT``, ``SPAN_ONLY``, ``EVENT_ONLY``, ``SPAN_AND_EVENT``, or ``ALLOW_LIST``:

::

    export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=SPAN_AND_EVENT

``ALLOW_LIST`` captures no content, like ``NO_CONTENT``, and keeps only a fixed list of
attributes on everything recorded, as described in the ``observe-util-genai`` README. It drops
``claude_agent_sdk.usage.is_partial``, so under it you cannot tell which token counts are undercounts.

Any other value is rejected with a warning and falls back to ``NO_CONTENT``.

Naming the Agent
****************

The root ``invoke_agent`` span is named ``claude-agent-sdk``. To name it after
the calling application instead:

.. code-block:: python

    ClaudeAgentSDKInstrumentor().instrument(agent_name="support-triage")

Subagents declared in ``options.agents`` do not name it. They are delegation
targets, and each gets its own nested ``invoke_agent`` span when the Agent tool
reaches it.

Claude Agent SDK Options
************************

Two ``ClaudeAgentOptions`` fields change how much of a run the instrumentation
can see. Neither is set on your behalf, because both change what your own
message loop receives.

``include_partial_messages=True`` makes the CLI forward stream events, which is
what the ``chat`` spans need to report a call's final token counts and stop
reason. Without it, those spans carry the mid-stream usage snapshot instead —
an undercount, marked on the span so a consumer can tell the difference — and
no ``gen_ai.client.operation.time_to_first_chunk`` or
``gen_ai.client.operation.time_per_output_chunk`` timings are recorded. The
token counts on the root ``invoke_agent`` span are accurate either way. Turning
it on adds ``StreamEvent`` messages to the stream your loop iterates.

A subagent's calls get no stream events even with the option on, so every one
of them reports the usage it started with and is marked as a snapshot.

``forward_subagent_text=True`` makes the CLI forward the text and thinking
blocks of a subagent spawned through the Agent tool. By default it forwards
only that subagent's ``tool_use`` and ``tool_result`` blocks, which is enough
for a progress heartbeat: the delegation still gets its own nested
``invoke_agent`` span, but the content recorded on it is thin, and empty if the
subagent never called a tool. Either way the forwarded messages arrive as
``AssistantMessage`` / ``UserMessage`` objects whose ``parent_tool_use_id`` is
the spawning Agent ``tool_use`` id, so turning the option on adds the
subagent's own text to your loop.

Token Usage
-----------

Each ``invoke_agent`` span reports only the tokens of its own agent's calls. A
subagent's calls count on the subagent's span, not on the span of the agent that
delegated to it, so adding up the ``invoke_agent`` spans of a run counts each
call once. Don't also add the ``chat`` spans: they cover the same calls, so each
call would be counted twice. With ``ClaudeSDKClient`` each turn gets its own root
``invoke_agent`` span; ``gen_ai.conversation.id`` groups the turns.

That sum falls short of what the run used in three cases, described below: a
subagent's output, calls the CLI makes for itself, and a turn that ends without
a result, whose root span then reports no tokens.

Agent spans carry only the two totals, ``gen_ai.usage.input_tokens`` (cache
reads and writes included) and ``gen_ai.usage.output_tokens``. The cache and
reasoning breakdowns are only on the ``chat`` spans, because the GenAI
conventions define them for model calls, not for agent spans. To get them for a
whole run, add them up from the ``chat`` spans.

- **The root agent** takes its counts from the ``usage`` field of each
  ``ResultMessage``, which counts the root agent's own calls since the previous
  result, not its subagents'. They are accurate whether or not stream events are
  on, and for resumed sessions too.
- **A subagent** gets no total of its own from the CLI, so its span adds up
  its ``chat`` spans. Input counts are accurate; output counts are not. The
  CLI forwards no stream events for a subagent, so each of its calls reports
  the near-zero output figure from when the call started. The subagent span
  and each of its ``chat`` spans carry ``claude_agent_sdk.usage.is_partial``
  set to ``true``.

Calls the CLI makes for itself, such as the one that gives a new session its
title, are counted on no span. Only the session-wide
``ResultMessage.model_usage`` includes them.

A turn that ends without a result, because the stream failed or the run was
closed part-way, leaves its root span without token counts. Its ``chat`` spans
still carry theirs.

No span reports cost. ``ResultMessage.total_cost_usd`` covers the whole session
and every agent in it, so it fits no single span. Read it in your own code if
you need it.


References
----------

* `OpenTelemetry Project <https://opentelemetry.io/>`_
* `OpenTelemetry GenAI semantic conventions <https://opentelemetry.io/docs/specs/semconv/gen-ai/>`_
* `Claude Agent SDK (Python) <https://github.com/anthropics/claude-agent-sdk-python>`_
* `Anthropic Documentation <https://docs.anthropic.com/>`_
