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
2 changes: 0 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,6 @@ PRs are squash-merged with the PR title as the commit message, and release-pleas
- **A breaking experimental change** uses `!` while the package is 0.x: `feat(evaluations)!:` bumps the minor version, which is what an experimental break is allowed. After 1.0, `!` would force a major release. Use `feat(evaluations):` without `!` instead, and say the change is breaking in the title.
- **When a feature is promoted to core,** remove its rows from `changelog-sections`.

The rule is specified in `TESTING.md` §0.3 in `launchdarkly/ai-sdks-monorepo`.

## Package Hierarchy

The monorepo is organized into three tiers plus a convenience barrel. Dependencies only flow **downward** — never sideways between packages in the same tier, and never upward.
Expand Down
1 change: 1 addition & 0 deletions packages/ai/src/launchdarkly_ai_python/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
__version__ = "0.4.0" # x-release-please-version

from launchdarkly_ai_server import * # noqa: F403
from launchdarkly_ai_server import __all__ as __all__
from launchdarkly_ai_server import register_ai_sdk_package

register_ai_sdk_package("launchdarkly-ai-python", __version__)
9 changes: 7 additions & 2 deletions packages/ai/tests/test_reexports.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""
Tests for §5 launchdarkly-ai-python re-export barrel.
Reference: TESTING.md §5
Tests for the launchdarkly-ai-python re-export barrel.
"""

import importlib
Expand All @@ -16,6 +15,12 @@ def test_all_named_exports_from_server_are_reexported(self) -> None:
f"launchdarkly_ai_python is missing re-export: {name!r}"
)

def test_all_matches_server(self) -> None:
server = importlib.import_module("launchdarkly_ai_server")
barrel = importlib.import_module("launchdarkly_ai_python")

assert barrel.__all__ == server.__all__

def test_reexported_values_are_identical_references(self) -> None:
server = importlib.import_module("launchdarkly_ai_server")
barrel = importlib.import_module("launchdarkly_ai_python")
Expand Down
4 changes: 2 additions & 2 deletions packages/claude-agents/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ The CLI also writes its own `AssistantMessage`, with `model` set to the literal
an API call fails and after a response stops at `max_output_tokens`. It is not a model response:
it gets no `chat` span and never joins the conversation a later span reports.

**Important:** a bare `return` inside `async for` abandons the generator. Python's asyncio finalizer will try to `aclose()` it later and may raise `RuntimeError: aclose(): asynchronous generator is already running` if the generator is still suspended inside a real SDK `await`. Always use `break` + explicit `aclose()`. See **Appendix A.4** in `TESTING.md`.
**Important:** a bare `return` inside `async for` abandons the generator. Python's asyncio finalizer will try to `aclose()` it later and may raise `RuntimeError: aclose(): asynchronous generator is already running` if the generator is still suspended inside a real SDK `await`. Always use `break` + explicit `aclose()`.

### 4. Telemetry

Expand Down Expand Up @@ -236,6 +236,6 @@ Span names and attributes are described in [Implementation Details → Telemetry
## Common Pitfalls

- **MCP tool name prefix**: tools registered in the MCP server are accessible as `mcp__tool-mcp__<name>`. The `allowed_tools` list must use this prefix; omitting it will cause the agent to not invoke any tools.
- **Async generator teardown**: always use `break` inside `async for` (not `return`) and wrap the loop in `try/finally: await gen.aclose()`. See TESTING.md Appendix A.4.
- **Async generator teardown**: always use `break` inside `async for` (not `return`) and wrap the loop in `try/finally: await gen.aclose()`.
- **`message.usage` shape**: the raw usage dict from the `claude_agent_sdk` is passed through directly to `parse_usage`. It accepts `input_tokens`/`output_tokens` which the SDK provides.
- **Native tools** (`ClaudeWebSearch`, etc.) are registered in `tool_handlers` as `NativeTool` sentinel instances (not callables). The handler's `_partition_tools` function separates them from user-defined tools — native tools go into `native_tool_names` for the agent's built-in access, user tools go through the MCP server.
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@
#: environment, working directory, file access, permissions, settings files, plugins, sandbox,
#: session state) stays under the application's control, never a config's.
#:
#: This is the cross-SDK list for Claude Agents (TESTING.md section 1.12), shared with the native
#: This is the cross-SDK list for Claude Agents, shared with the native
#: graph through :func:`_options_parameters`.
#:
#: The SDK offers no ``temperature``/``top_p``/``top_k``/``max_tokens``/``stop_sequences``/
Expand Down
3 changes: 1 addition & 2 deletions packages/claude-agents/tests/test_builtins.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""
Tests for §4 Claude Agents built-ins (builtins.py).
Reference: TESTING.md §4
Tests for the Claude Agents built-ins (builtins.py).
"""

from launchdarkly_ai_claude_agents.builtins import (
Expand Down
3 changes: 1 addition & 2 deletions packages/claude-agents/tests/test_graph.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""
Tests for §2.1 graph convenience wrapper (claude_graph).
Reference: TESTING.md §2.1
Tests for the graph convenience wrapper (claude_graph).
"""

from collections.abc import AsyncIterator
Expand Down
10 changes: 5 additions & 5 deletions packages/claude-agents/tests/test_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@ def test_native_tool_aliases_map_ld_key_to_provider_name(self) -> None:


# ---------------------------------------------------------------------------
# §1.3 Tool conversion — restored from the pre-rewrite file (not telemetry).
# Tool conversion — restored from the pre-rewrite file (not telemetry).
# ``_partition_tools`` kept its 3-tuple return, so these run byte-for-byte.
# ---------------------------------------------------------------------------

Expand Down Expand Up @@ -444,7 +444,7 @@ def test_empty_tools_no_tools_sent(self) -> None:


# ---------------------------------------------------------------------------
# §1.4 Tool execution loop (via _build_tool_mcp) — restored from the pre-rewrite
# Tool execution loop (via _build_tool_mcp) — restored from the pre-rewrite
# file. ``_build_tool_mcp`` kept its lazy ``importlib.import_module`` pattern
# (native_graph.py depends on it), so the old SDK-mocking approach still works
# unmodified for this one.
Expand Down Expand Up @@ -2028,7 +2028,7 @@ async def test_forwards_cwd_and_setting_sources_to_query(


# ---------------------------------------------------------------------------
# §1.2 Path C — None user_input must not produce None prompt.
# None user_input must not produce None prompt.
# Restored from the pre-rewrite file (not telemetry). Adapted from the old
# ``_patch_query``/mocked-module approach to ``monkeypatch.setattr(handler_mod, "query", ...)``,
# since ``query`` is now resolved once at import time rather than through
Expand All @@ -2037,7 +2037,7 @@ async def test_forwards_cwd_and_setting_sources_to_query(


class TestNoneUserInput:
"""TESTING.md §1.2 Path C: When user_input is None, the prompt passed to
"""When user_input is None, the prompt passed to
the provider must be '' (empty string), not None."""

async def test_none_user_input_instructions_path_prompt_is_empty_string(
Expand Down Expand Up @@ -2250,7 +2250,7 @@ async def _query(**kwargs: Any) -> AsyncIterator[Any]:

@pytest.mark.parametrize("path", _PATHS)
class TestHostOptionsAndBuiltinToolSet:
"""TESTING.md §2.x Claude Agents Handler — host options and built-in tool set.
"""Claude Agents Handler — host options and built-in tool set.

``cwd`` and ``setting_sources`` configure the CLI process, so only the application sets them.
``setting_sources`` not given sends ``[]``: the Agent SDK's own ``None`` loads every settings
Expand Down
5 changes: 2 additions & 3 deletions packages/claude-agents/tests/test_native_graph.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""
Tests for §2.2 native graph adapter (to_claude_agents) plus Anthropic-specific specs.
Reference: TESTING.md §2.2, §2.x (Anthropic)
Tests for the native graph adapter (to_claude_agents) plus Anthropic-specific specs.
"""

from __future__ import annotations
Expand Down Expand Up @@ -147,7 +146,7 @@ async def _query(**kwargs: Any) -> AsyncIterator[Any]:


# ---------------------------------------------------------------------------
# §2.2 Generic topology
# Generic topology
# ---------------------------------------------------------------------------


Expand Down
2 changes: 1 addition & 1 deletion packages/claude-agents/tests/test_sdk_usage.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Tests for TESTING.md §3.27 — claude-agents."""
"""Tests for SDK helper usage events — claude-agents."""

import hashlib
from pathlib import Path
Expand Down
2 changes: 1 addition & 1 deletion packages/claude-agents/tests/test_skills.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
Tests for TESTING.md §2.x Claude Agents Skills Integration (A.18).
Tests for the Claude Agents Skills integration.

``query()`` is replaced with a fake async generator that records the options it was given and can
look at the filesystem while the run is in progress. The skill store is a real
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
#: ``TestMessagesStreamAcceptsExactlyTheseKeys`` in this package's tests assert this
#: classification stays exhaustive as the SDK's own signatures change.
#:
#: This is the cross-SDK list for Claude Messages (TESTING.md section 1.12). A top-level
#: This is the cross-SDK list for Claude Messages. A top-level
#: ``effort`` is also accepted and moved into ``output_config.effort`` before this list applies
#: (see :func:`_rename_effort_to_output_config`), ``output_config`` keeps only its ``effort`` (see
#: :func:`_rebuild_output_config`), and ``max_tokens`` defaults to 1024. The same list
Expand Down
31 changes: 15 additions & 16 deletions packages/claude-messages/tests/test_handler.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""
Tests for launchdarkly-ai-claude-messages handler.
Covers §1.1–1.9 (generic handler tests).
Reference: TESTING.md §1
Covers the generic handler tests.
"""

from __future__ import annotations
Expand Down Expand Up @@ -104,7 +103,7 @@ def mock_anthropic(mocker):


# ---------------------------------------------------------------------------
# §1.1 Factory function and metadata
# Factory function and metadata
# ---------------------------------------------------------------------------


Expand Down Expand Up @@ -138,7 +137,7 @@ def test_multiple_calls_return_independent_instances(


# ---------------------------------------------------------------------------
# §1.2 Prompt construction
# Prompt construction
# ---------------------------------------------------------------------------


Expand Down Expand Up @@ -294,7 +293,7 @@ async def test_path_c_both_instructions_and_messages_messages_wins(


# ---------------------------------------------------------------------------
# §1.3 Tool conversion
# Tool conversion
# ---------------------------------------------------------------------------


Expand Down Expand Up @@ -373,7 +372,7 @@ async def test_custom_parameters_passthrough(


# ---------------------------------------------------------------------------
# §1.4 Tool execution loop
# Tool execution loop
# ---------------------------------------------------------------------------


Expand Down Expand Up @@ -740,7 +739,7 @@ async def test_multiple_consecutive_tool_calls(


# ---------------------------------------------------------------------------
# §1.5 Telemetry
# Telemetry
# ---------------------------------------------------------------------------
# Span recording
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -1308,7 +1307,7 @@ async def test_still_writes_the_legacy_content_events_when_enabled(


# ---------------------------------------------------------------------------
# §1.6 Error handling
# Error handling
# ---------------------------------------------------------------------------


Expand Down Expand Up @@ -1422,7 +1421,7 @@ async def test_rethrows_error(self, mock_anthropic: MagicMock) -> None:


# ---------------------------------------------------------------------------
# §1.9 Structured output (outputFormat)
# Structured output (outputFormat)
# ---------------------------------------------------------------------------


Expand Down Expand Up @@ -1476,7 +1475,7 @@ async def test_output_format_with_messages_system_appended(


# ---------------------------------------------------------------------------
# §1.7 Convenience export
# Convenience export
# ---------------------------------------------------------------------------


Expand Down Expand Up @@ -1524,7 +1523,7 @@ def test_callable_without_extra_kwargs(self) -> None:


# ---------------------------------------------------------------------------
# §1.8 Streaming
# Streaming
# ---------------------------------------------------------------------------


Expand Down Expand Up @@ -1844,12 +1843,12 @@ async def _bad_ctx() -> AsyncGenerator[Any, None]:


# ---------------------------------------------------------------------------
# §1.2 Path C — None user_input must not produce None content
# None user_input must not produce None content
# ---------------------------------------------------------------------------


class TestNoneUserInput:
"""TESTING.md §1.2 Path C: When user_input is None, the user-role message
"""When user_input is None, the user-role message
content sent to the provider must be '' not None."""

async def test_none_user_input_instructions_path_no_none_content(
Expand Down Expand Up @@ -1880,12 +1879,12 @@ async def _capture(**kwargs: Any) -> Any:


# ---------------------------------------------------------------------------
# §1.10 MAX_STEPS cap
# MAX_STEPS cap
# ---------------------------------------------------------------------------


class TestMaxStepsCap:
"""TESTING.md §1.10: The tool loop must break with an error after MAX_STEPS (5) iterations."""
"""The tool loop must break with an error after MAX_STEPS (5) iterations."""

def _tool_use_response(self, id: str = "tu1") -> MagicMock:
return _anthropic_response(
Expand Down Expand Up @@ -1973,7 +1972,7 @@ async def _iter() -> AsyncGenerator:


# ---------------------------------------------------------------------------
# §1.5 Streaming telemetry (Appendix A.5 — do not patch _HAS_OTEL=False)
# Streaming telemetry (do not patch _HAS_OTEL=False)
# ---------------------------------------------------------------------------


Expand Down
2 changes: 1 addition & 1 deletion packages/claude-messages/tests/test_sdk_usage.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Tests for TESTING.md §3.27 — claude-messages."""
"""Tests for SDK helper usage events — claude-messages."""

from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
Expand Down
4 changes: 2 additions & 2 deletions packages/client/src/launchdarkly_ai_server/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,8 +337,8 @@ def _make_handoff_fn(t: str) -> Callable[..., str]:
def _fn(*a: Any, **kw: Any) -> str:
if not chosen:
chosen.append(t)
# Both routing strings are pinned byte for byte in TESTING.md §3.15a, which
# gives the reasons: "recorded" keeps a node with a real tool from skipping
# Both routing strings are pinned byte for byte across SDKs, for two
# reasons: "recorded" keeps a node with a real tool from skipping
# it, and asking for no "final response" keeps a router from answering for
# its target.
return (
Expand Down
4 changes: 2 additions & 2 deletions packages/client/src/launchdarkly_ai_server/history.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""Shared conversation-history composition and multimodal content helpers.

Mirrors the TypeScript ``history`` module so every handler composes runtime
``history`` the same way (TESTING.md §1.11) and maps LaunchDarkly-canonical
content blocks to each provider's native shape (Appendix A.7).
``history`` the same way and maps LaunchDarkly-canonical
content blocks to each provider's native shape.

History messages are plain dicts: ``{"role": ..., "content": ...}`` where
``content`` is either a string or a list of content-block dicts:
Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/launchdarkly_ai_server/lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -615,7 +615,7 @@ async def extract_variation(
f"Variation '{config_key}' returned None (flag may be disabled)."
)

# Only an explicit ``_ldMeta.enabled`` counts as enabled (spec §3.10, as in JS and Go).
# Only an explicit ``_ldMeta.enabled`` counts as enabled (as in JS and Go).
ld_meta = (raw.get("_ldMeta") or {}) if isinstance(raw, dict) else {}
if not isinstance(ld_meta, dict) or not ld_meta.get("enabled"):
raise RuntimeError(f"Variation '{config_key}' is disabled.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
sense across providers (``temperature``, ``top_p``, ``max_tokens``, ``tool_choice``, ...). No
single provider SDK accepts all of them, and each handler package classifies every key its own
provider entry point accepts into exactly one written-down list: forwarded (model and run settings
only, the cross-SDK list for that handler in TESTING.md section 1.12), handler-owned (set by the
only, the cross-SDK list for that handler), handler-owned (set by the
call site itself), or excluded (accepted by the provider but never forwarded: credentials, endpoints
and connection settings, raw request injection such as extra headers or ``model_kwargs``, remote
tools, host-process settings, data location and retention, server-side state, identity and
Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/launchdarkly_ai_server/skills_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ def clear_state() -> None:


# Registered on load, so core's ``shutdown()`` clears this state without
# importing any Agent Skills module (TESTING.md §0.3).
# importing any Agent Skills module.
register_shutdown_hook("Agent Skills", clear_state)


Expand Down
Loading
Loading