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
1 change: 1 addition & 0 deletions packages/claude-agents/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ That means the LaunchDarkly flag variation must have `provider.name == "Anthropi
| `src/launchdarkly_ai_claude_agents/native_graph.py` | `to_claude_agents()` native graph adapter |
| `src/launchdarkly_ai_claude_agents/builtins.py` | Pre-constructed `NativeTool` sentinels for Claude built-in tools |
| `src/launchdarkly_ai_claude_agents/__init__.py` | Package exports |
| `src/launchdarkly_ai_claude_agents/_version.py` | `PACKAGE_NAME` and `__version__`, sent with `$ld:ai:sdk:info` and `$ld:ai:sdk:usage` |

---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@
See https://launchdarkly.com/docs for usage.
"""

__version__ = "0.2.4" # x-release-please-version

from launchdarkly_ai_server import register_ai_sdk_package

from . import native_graph # noqa: F401
from ._version import PACKAGE_NAME, __version__
from .builtins import (
ClaudeBash,
ClaudeEdit,
Expand Down Expand Up @@ -50,4 +49,4 @@
"to_claude_agents",
]

register_ai_sdk_package("launchdarkly-ai-claude-agents", __version__)
register_ai_sdk_package(PACKAGE_NAME, __version__)
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""This package's identity, as sent with ``$ld:ai:sdk:info`` and ``$ld:ai:sdk:usage``."""

PACKAGE_NAME = "launchdarkly-ai-claude-agents"
__version__ = "0.2.4" # x-release-please-version
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@

from typing import Any

from launchdarkly_ai_claude_agents.handler import create_claude_agents_handler
from launchdarkly_ai_server import graph
from launchdarkly_ai_claude_agents.handler import _create_claude_agents_handler
from launchdarkly_ai_server import _graph, report_usage

from ._version import PACKAGE_NAME, __version__


def claude_graph(key: str, **options: Any) -> Any:
Expand All @@ -15,4 +17,5 @@ def claude_graph(key: str, **options: Any) -> Any:
Equivalent to ``graph(key, handlers=[create_claude_agents_handler()], **options)``.
Use the base ``graph()`` directly for multi-provider graphs.
"""
return graph(key, handlers=[create_claude_agents_handler()], **options)
report_usage("claude-agents.claudeGraph", PACKAGE_NAME, __version__)
return _graph(key, handlers=[_create_claude_agents_handler()], **options)
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,21 @@
ProviderHandler,
SpanMessage,
SpanMessagePart,
_config,
_create_handler,
compose_history,
config,
content_to_text,
create_handler,
end_span_once,
end_unfinished_spans,
parse_template,
report_usage,
set_conversation_id_if_absent,
set_input_content_attributes,
set_output_content_attributes,
set_tool_call_content_attributes,
)

from ._version import PACKAGE_NAME, __version__
from .spans import (
MCP_TOOL_PREFIX,
TOOL_MCP_NAME,
Expand Down Expand Up @@ -508,6 +510,12 @@ def create_claude_agents_handler(*, capture_content: bool = False) -> ProviderHa
Set *capture_content* to put prompts, model output, tool arguments and tool results on the
emitted spans. It defaults to off. See TELEMETRY-CONTRACT.md section 7.
"""
report_usage("claude-agents.createClaudeAgentsHandler", PACKAGE_NAME, __version__)
return _create_claude_agents_handler(capture_content=capture_content)


def _create_claude_agents_handler(*, capture_content: bool = False) -> ProviderHandler:
"""Non-reporting :func:`create_claude_agents_handler`, used by this package's wrappers."""

async def _call_impl(
config: AiConfigRep,
Expand Down Expand Up @@ -694,7 +702,7 @@ def _stream_impl(
capture_content=capture_content,
)

return create_handler(
return _create_handler(
("Anthropic", "agent"),
_call_impl, # type: ignore[arg-type]
_stream_impl, # type: ignore[arg-type]
Expand Down Expand Up @@ -886,10 +894,11 @@ def claude_agents(
**kwargs: Any,
) -> Any:
"""Convenience wrapper: creates a handler and calls config(...).invoke()."""
report_usage("claude-agents.claudeAgents", PACKAGE_NAME, __version__)
variables = kwargs.pop("variables", None)
capture_content = kwargs.pop("capture_content", False)
return config(
return _config(
key=config_key,
handler=create_claude_agents_handler(capture_content=capture_content),
handler=_create_claude_agents_handler(capture_content=capture_content),
**kwargs,
).invoke(user_input, context, variables=variables)
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@
NativeTool,
get_client,
make_track_data,
report_usage,
to_ld_context,
)

from ._version import PACKAGE_NAME, __version__

try:
from opentelemetry import trace
from opentelemetry.trace import StatusCode as SpanStatusCode
Expand Down Expand Up @@ -207,6 +210,7 @@ def to_claude_agents(

Returns an object with a ``.invoke(input, variables)`` coroutine.
"""
report_usage("claude-agents.toClaudeAgents", PACKAGE_NAME, __version__)
_opts = opts or {}

async def invoke(
Expand Down
8 changes: 4 additions & 4 deletions packages/claude-agents/tests/test_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@

class TestClaudeGraph:
def test_graph_called_with_correct_key(self) -> None:
with patch("launchdarkly_ai_claude_agents.graph.graph") as mock_graph:
with patch("launchdarkly_ai_claude_agents.graph._graph") as mock_graph:
mock_graph.return_value = MagicMock()
claude_graph("my-flag-key")
mock_graph.assert_called_once()
assert mock_graph.call_args[0][0] == "my-flag-key"

def test_handlers_pre_populated(self) -> None:
with patch("launchdarkly_ai_claude_agents.graph.graph") as mock_graph:
with patch("launchdarkly_ai_claude_agents.graph._graph") as mock_graph:
mock_graph.return_value = MagicMock()
claude_graph("key")
kw = mock_graph.call_args[1]
Expand All @@ -26,14 +26,14 @@ def test_handlers_pre_populated(self) -> None:

def test_user_supplied_options_forwarded(self) -> None:
ctx = {"kind": "user", "key": "u1"}
with patch("launchdarkly_ai_claude_agents.graph.graph") as mock_graph:
with patch("launchdarkly_ai_claude_agents.graph._graph") as mock_graph:
mock_graph.return_value = MagicMock()
claude_graph("key", context=ctx)
kw = mock_graph.call_args[1]
assert kw.get("context") == ctx

def test_user_cannot_override_handlers(self) -> None:
with patch("launchdarkly_ai_claude_agents.graph.graph") as mock_graph:
with patch("launchdarkly_ai_claude_agents.graph._graph") as mock_graph:
mock_graph.return_value = MagicMock()
claude_graph("key", extra_kwarg=42)
kw = mock_graph.call_args[1]
Expand Down
6 changes: 3 additions & 3 deletions packages/claude-agents/tests/test_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -1604,7 +1604,7 @@ def _fake_config(**_kwargs: Any) -> Any:
return instance

monkeypatch.setattr(
"launchdarkly_ai_claude_agents.handler.config", _fake_config
"launchdarkly_ai_claude_agents.handler._config", _fake_config
)
result = await claude_agents("cfg-key", "hi", {"key": "u1"})
assert result == {"output": "ok"}
Expand Down Expand Up @@ -1633,7 +1633,7 @@ def test_config_key_forwarded_as_key(self) -> None:
mock_config_fn = MagicMock(return_value=mock_config_instance)
mock_config_instance.invoke = MagicMock(return_value="result")

with patch.object(_handler_mod, "config", mock_config_fn):
with patch.object(_handler_mod, "_config", mock_config_fn):
from launchdarkly_ai_claude_agents.handler import claude_agents

ctx = {"kind": "user", "key": "u1"}
Expand All @@ -1656,7 +1656,7 @@ def test_callable_without_extra_kwargs(self) -> None:
mock_config_fn = MagicMock(return_value=mock_config_instance)
mock_config_instance.invoke = MagicMock(return_value="result")

with patch.object(_handler_mod, "config", mock_config_fn):
with patch.object(_handler_mod, "_config", mock_config_fn):
from launchdarkly_ai_claude_agents.handler import claude_agents

ctx = {"kind": "user", "key": "u1"}
Expand Down
149 changes: 149 additions & 0 deletions packages/claude-agents/tests/test_sdk_usage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
"""Tests for TESTING.md §3.27 — claude-agents."""

from types import SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

import launchdarkly_ai_server.lifecycle as lifecycle_module
from launchdarkly_ai_claude_agents import (
claude_agents,
claude_graph,
create_claude_agents_handler,
to_claude_agents,
)
from launchdarkly_ai_claude_agents._version import PACKAGE_NAME
from launchdarkly_ai_claude_agents._version import __version__ as PACKAGE_VERSION
from launchdarkly_ai_server import SDK_INFO_CONTEXT, __version__, init_client, shutdown

CONTEXT = {"kind": "user", "key": "user-1"}


def _fake() -> MagicMock:
client = MagicMock()
client.track = MagicMock()
client.variation = AsyncMock(return_value=None)
client.flush = AsyncMock()
client.close = AsyncMock()
return client


async def _settle(value: Any) -> None:
if hasattr(value, "__await__"):
try:
await value
except Exception:
return


@pytest.fixture
async def client() -> Any:
await shutdown()
installed = _fake()
with patch.object(lifecycle_module, "_setup_telemetry", return_value=None):
await init_client(client=installed)
installed.track.reset_mock()
yield installed
await shutdown()


@pytest.mark.parametrize(
("helper", "call"),
[
("claude-agents.claudeAgents", lambda: claude_agents("k", "q", CONTEXT)),
("claude-agents.createClaudeAgentsHandler", create_claude_agents_handler),
("claude-agents.claudeGraph", lambda: claude_graph("k")),
("claude-agents.toClaudeAgents", lambda: to_claude_agents(None)),
],
)
async def test_helper_sends_one_usage_event(
client: MagicMock, helper: str, call: Any
) -> None:
try:
await _settle(call())
except Exception:
pass
calls = [
entry
for entry in client.track.call_args_list
if entry.args[0] == "$ld:ai:sdk:usage"
]
assert [entry.args[2].get("helper") for entry in calls] == [helper]
assert calls[0].args[1] == SDK_INFO_CONTEXT
assert calls[0].args[2] == {
"aiSdkName": "launchdarkly-ai-server",
"aiSdkVersion": __version__,
"aiSdkLanguage": "python",
"helper": helper,
"helperPackageName": PACKAGE_NAME,
"helperPackageVersion": PACKAGE_VERSION,
}
assert calls[0].args[3] == 1


def _helpers(client: MagicMock) -> list[str]:
return [
entry.args[2].get("helper")
for entry in client.track.call_args_list
if entry.args[0] == "$ld:ai:sdk:usage"
]


async def _drain(stream: Any) -> None:
try:
async for _ in stream:
pass
except Exception:
return


async def test_graph_wrapper_invoke_and_stream_report_only_the_wrapper(
client: MagicMock,
) -> None:
"""The returned graph's methods are not separate ``client.graph.*`` calls."""
instance = claude_graph("k")
await _settle(instance.invoke("q", CONTEXT))
await _drain(instance.stream("q", CONTEXT))
assert _helpers(client) == ["claude-agents.claudeGraph"]


async def test_native_adapter_invoke_reports_only_the_adapter(
client: MagicMock,
) -> None:
async def _definition() -> Any:
return SimpleNamespace(enabled=False, key="k")

runner = to_claude_agents(_definition(), {"context": CONTEXT})
with pytest.raises(ValueError, match="disabled"):
await runner.invoke("q")
assert _helpers(client) == ["claude-agents.toClaudeAgents"]


async def test_held_helper_flushes_with_its_package() -> None:
"""A helper called before init keeps its package fields until the flush."""
await shutdown()
lifecycle_module._reset_for_testing()
try:
create_claude_agents_handler()
except Exception:
pass # The factory may need credentials; it reports first.
installed = _fake()
with patch.object(lifecycle_module, "_setup_telemetry", return_value=None):
await init_client(client=installed)
calls = [
entry.args[2]
for entry in installed.track.call_args_list
if entry.args[0] == "$ld:ai:sdk:usage"
]
assert calls == [
{
"aiSdkName": "launchdarkly-ai-server",
"aiSdkVersion": __version__,
"aiSdkLanguage": "python",
"helper": "claude-agents.createClaudeAgentsHandler",
"helperPackageName": PACKAGE_NAME,
"helperPackageVersion": PACKAGE_VERSION,
}
]
await shutdown()
1 change: 1 addition & 0 deletions packages/claude-messages/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ That means the LaunchDarkly flag variation must have `provider.name == "Anthropi
|---|---|
| `src/launchdarkly_ai_claude_messages/handler.py` | All implementation — message building, tool schema conversion, tool-use loop, telemetry |
| `src/launchdarkly_ai_claude_messages/__init__.py` | Package exports |
| `src/launchdarkly_ai_claude_messages/_version.py` | `PACKAGE_NAME` and `__version__`, sent with `$ld:ai:sdk:info` and `$ld:ai:sdk:usage` |

---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@
See https://launchdarkly.com/docs for usage.
"""

__version__ = "0.2.3" # x-release-please-version

from launchdarkly_ai_server import register_ai_sdk_package

from ._version import PACKAGE_NAME, __version__
from .handler import claude_messages, create_claude_messages_handler

__all__ = ["claude_messages", "create_claude_messages_handler"]

register_ai_sdk_package("launchdarkly-ai-claude-messages", __version__)
register_ai_sdk_package(PACKAGE_NAME, __version__)
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""This package's identity, as sent with ``$ld:ai:sdk:info`` and ``$ld:ai:sdk:usage``."""

PACKAGE_NAME = "launchdarkly-ai-claude-messages"
__version__ = "0.2.3" # x-release-please-version
Loading
Loading