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
4 changes: 2 additions & 2 deletions packages/claude-agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,10 @@ asyncio.run(main())

```python
import os
from launchdarkly_ai_server import FDv2SkillStore, config, set_skill_store
from launchdarkly_ai_server import LaunchDarklySkillStore, config, set_skill_store
from launchdarkly_ai_claude_agents import create_claude_agents_skills_handler

store = FDv2SkillStore(os.environ["LD_SDK_KEY"]).start()
store = LaunchDarklySkillStore(os.environ["LD_SDK_KEY"]).start()
store.wait_for_skills(timeout=10)
set_skill_store(store)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,10 +155,10 @@ def create_claude_agents_skills_handler(

import os

from launchdarkly_ai_server import FDv2SkillStore, config, set_skill_store
from launchdarkly_ai_server import LaunchDarklySkillStore, config, set_skill_store
from launchdarkly_ai_claude_agents import create_claude_agents_skills_handler

store = FDv2SkillStore(os.environ["LD_SDK_KEY"]).start()
store = LaunchDarklySkillStore(os.environ["LD_SDK_KEY"]).start()
store.wait_for_skills(timeout=10)
set_skill_store(store)

Expand Down
22 changes: 11 additions & 11 deletions packages/client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ No code changes are required — `init_client()` detects the packages at runtime
| Variable | Required | Description |
|---|---|---|
| `LD_SDK_KEY` | Yes | LaunchDarkly server-side SDK key |
| `LD_BASE_URI` | No | Override the LaunchDarkly polling base URI (e.g. for staging). `FDv2SkillStore` reads it too when given no URI option |
| `LD_STREAM_URI` | No | Override the streaming URI. `FDv2SkillStore` reads it too when given no URI option |
| `LD_BASE_URI` | No | Override the LaunchDarkly polling base URI (e.g. for staging). `LaunchDarklySkillStore` reads it too when given no URI option |
| `LD_STREAM_URI` | No | Override the streaming URI. `LaunchDarklySkillStore` reads it too when given no URI option |
| `LD_EVENTS_URI` | No | Override the events URI |
| `LD_SERVICE_NAME` | No | OTel `service.name` resource attribute (default: `python-sdk`) |
| `LD_ENVIRONMENT` | No | `deployment.environment` resource attribute attached to telemetry |
Expand Down Expand Up @@ -670,8 +670,8 @@ worth alerting on too.
verification passes, and the usual cause is a bug in a custom `SkillStore` adapter (a stale
cache entry, a colliding key, a wrong index lookup) rather than tampering, so neither inflates
LaunchDarkly's integrity counter. Both still write this record, so a rule on
`ld.skills.integrity_failure` catches them. If `FDv2SkillStore` is your only store, treat them
like `hash_mismatch`; behind a custom adapter, suspect the adapter first.
`ld.skills.integrity_failure` catches them. If `LaunchDarklySkillStore` is your only store,
treat them like `hash_mismatch`; behind a custom adapter, suspect the adapter first.

`get_skill` returns `None` for a `version_mismatch` like any other failure, so this record is
the only place it is visible unless you use `get_skill_result`, which reports it as the
Expand Down Expand Up @@ -725,16 +725,16 @@ be resolved and log a count at WARN. Use `get_skill_result` per key when you nee
#### Receiving skills from LaunchDarkly

`InMemorySkillStore` is for tests and bring-your-own-content. In production, skill content
arrives through `FDv2SkillStore`, which uses LaunchDarkly's SDK-facing FDv2 delivery channel
arrives through `LaunchDarklySkillStore`, which uses LaunchDarkly's SDK-facing FDv2 delivery channel
(the `GET /sdk/poll` and `GET /sdk/stream` endpoints the base SDK's FDv2 data source uses),
authenticated with the environment's server-side SDK key.

```python
import os

from launchdarkly_ai_server import FDv2SkillStore, set_skill_store, watch_skills
from launchdarkly_ai_server import LaunchDarklySkillStore, set_skill_store, watch_skills

store = FDv2SkillStore(os.environ["LD_SDK_KEY"]).start()
store = LaunchDarklySkillStore(os.environ["LD_SDK_KEY"]).start()
if not store.wait_for_skills(timeout=10):
# No payload arrived. Reconciling now would find an empty store; see below.
print(f"skill delivery has not answered yet: {store.failed or 'still waiting'}")
Expand All @@ -752,9 +752,9 @@ finally:
**A reconcile that runs before delivery answers does not prune.** A store still waiting for its
first payload looks the same as an environment with no skills, and `write_skills("*")` would
otherwise treat that as every skill revoked and delete the files from a previous run.
`FDv2SkillStore` reports readiness through the optional `is_initialized()`; until it is true, a
reconcile reports the retrieval unavailable (`report.ok` is `False`, and the error says why)
and leaves the disk alone.
`LaunchDarklySkillStore` reports readiness through the optional `is_initialized()`; until it
is true, a reconcile reports the retrieval unavailable (`report.ok` is `False`, and the error
says why) and leaves the disk alone.

**A 422 means this connection will never be assigned a skill payload, and delivery stops.**
Every request declares the payload it wants (`kinds=agent-skill`), and LaunchDarkly answers
Expand Down Expand Up @@ -914,7 +914,7 @@ that skips verification.
| `write_skills(skills, root, *, prune=True, timeout=10.0, on_unavailable="keep")` | Materialize skills under `root`, returning a `ReconcileReport`. `skills` is a sequence of `Skill` / `SkillReference` / key strings, `"*"`, or a `SkillSource`, which is read once; a source that raises or answers anything malformed is an unavailable retrieval. `prune` removes formerly-managed skills no longer requested. `on_unavailable="raise"` raises instead of reporting when content cannot be retrieved. Raises `ValueError` for an unusable root, a negative or non-finite `timeout`, or an unrecognised `on_unavailable`. `timeout=0` is valid and makes every skill report an error. **Performs synchronous filesystem I/O — see the note below.** |
| `SkillStore` | The structural interface content arrives through: `get_object(kind, key, version=None)`, `all_objects(kind)`, optional `is_initialized()`, `add_listener(kind, fn)` / `remove_listener(kind, fn)`. A store without `is_initialized()` is treated as initialized. Both shipped stores deliver only the skill kind, so `add_listener` on any other kind raises. |
| `InMemorySkillStore(objects=None)` | A dict-backed store with `put(raw)`, for local development and testing. Holds several versions of a key. |
| `FDv2SkillStore(sdk_key, *, base_uri=…, stream_uri=…, mode="stream", …)` | The delivery transport: a store fed by LaunchDarkly over the SDK-facing FDv2 channel. `start()`, `wait_for_skills(timeout)`, `is_initialized()`, `close()`, `diagnostics`, `failed`; also a context manager. `close()` is **final** — `start()` afterwards raises. `poll_interval`, `read_timeout`, `initial_backoff` and `max_backoff` must be positive and finite, and `initial_backoff` may not exceed `max_backoff`. **Server-side only.** See *Receiving skills from LaunchDarkly* above. |
| `LaunchDarklySkillStore(sdk_key, *, base_uri=…, stream_uri=…, mode="stream", …)` | The delivery transport: a store fed by LaunchDarkly over the SDK-facing FDv2 channel. `start()`, `wait_for_skills(timeout)`, `is_initialized()`, `close()`, `diagnostics`, `failed`; also a context manager. `close()` is **final** — `start()` afterwards raises. `poll_interval`, `read_timeout`, `initial_backoff` and `max_backoff` must be positive and finite, and `initial_backoff` may not exceed `max_backoff`. **Server-side only.** See *Receiving skills from LaunchDarkly* above. |
| `watch_skills(skills, root, *, debounce=0.5, on_reconcile=None, …)` | `write_skills` plus a re-reconcile on every delivery change, and, for a `SkillSource`, on every change to one of its `config_keys`, so revocation takes effect once changes have been quiet for `debounce` rather than at the next restart. `watcher.notify()` schedules a reconcile through the same debounce. Returns `(initial report, SkillWatcher)`; close the watcher when done. `debounce` is in **seconds**, non-negative and finite. `on_reconcile` receives each *subsequent* report. One watcher per root. |
| `StoreDiagnostics` | What the transport has seen: `payloads_transferred`, `skill_objects_received`, `objects_ignored`, `objects_revoked`, `payloads_ignored`, `hashless_objects`, `connection_failures`, `last_error`. |

Expand Down
10 changes: 5 additions & 5 deletions packages/client/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ No other `launchdarkly-ai-*` package may define or duplicate these. They import
| `src/launchdarkly_ai_server/types_validation.py` | `parse_ai_config` — validates flag variation shape; `is_valid_skill_key` / `is_valid_skill_version` / `skill_key_rejection_reason` (the canonical key-grammar explanation every layer quotes) |
| `src/launchdarkly_ai_server/skills.py` | Agent Skills, retrieval half — `skill_refs`, `get_skill`/`get_skills`/`all_skills`, `InMemorySkillStore`, and the store/telemetry injection points `_set_store` / `_set_emitter_for_testing` |
| `src/launchdarkly_ai_server/skills_core.py` | Shared skills internals — the `SkillStore` seam, module state, the telemetry seam and its three recorders, integrity verification, and store resolution. Imported by both `skills.py` and the materialization layer; imports neither |
| `src/launchdarkly_ai_server/skills_fdv2.py` | Agent Skills, delivery transport — the FDv2 protocol, the wire-key/`version` translation, the held object set, and `FDv2SkillStore`. Sits **below** the store interface; imports `skills_core` only, and nothing imports it |
| `src/launchdarkly_ai_server/skills_fdv2.py` | Agent Skills, delivery transport — the FDv2 protocol, the wire-key/`version` translation, the held object set, and `LaunchDarklySkillStore`. Sits **below** the store interface; imports `skills_core` only, and nothing imports it |
| `src/launchdarkly_ai_server/skills_watch.py` | Agent Skills, eager re-reconcile — `watch_skills` / `SkillWatcher`, wiring the store's change listener, and for a `SkillSource` the base client's `flag_tracker` listener, to `write_skills`. Sits **above** `skills_fs` and modifies none of it |
| `src/launchdarkly_ai_server/skills_source.py` | Agent Skills, config-scoped requested set — `config_skills(key, context)`, which evaluates one AI Config per read and classifies every failure as a raised read, never `[]`. With `skills_watch`, the only skills module that imports `lifecycle`, and it takes only `get_client` |
| `src/launchdarkly_ai_server/skills_fs.py` | Agent Skills, materialization half — `write_skills`, request resolution, the manifest format and on-disk filenames, per-skill reconcile, and pruning |
Expand Down Expand Up @@ -82,7 +82,7 @@ from launchdarkly_ai_server.experimental.evaluations import init_evaluations

# Agent Skills
from launchdarkly_ai_server import (
set_skill_store, SkillStore, InMemorySkillStore, FDv2SkillStore, StoreDiagnostics,
set_skill_store, SkillStore, InMemorySkillStore, LaunchDarklySkillStore, StoreDiagnostics,
skill_refs, get_skill, get_skill_result, get_skills, all_skills, write_skills,
watch_skills, SkillWatcher, SkillSource, config_skills,
Skill, SkillReference, SkillOutcome, ReconcileAction, ReconcileReport,
Expand Down Expand Up @@ -270,7 +270,7 @@ user's files.

### The delivery transport, and the one field that will bite you

`FDv2SkillStore` speaks LaunchDarkly's SDK-facing FDv2 channel (`GET /sdk/poll`,
`LaunchDarklySkillStore` speaks LaunchDarkly's SDK-facing FDv2 channel (`GET /sdk/poll`,
`GET /sdk/stream`, server-side SDK key in `Authorization`, `kinds` + `basis` params,
`If-None-Match`/304). It sits below the store interface and produces raw objects in the
shape `skills_core.SkillStore` documents; **nothing above that interface knows it exists**.
Expand Down Expand Up @@ -571,7 +571,7 @@ Store data is **untrusted input**; the transport is not part of the trust bounda
directory skips its unlink, drops the manifest entry, and reports `removed` while the
revoked skill stays on disk.
- **Every numeric option rejects non-finite values, not just negative ones.**
`write_skills`'s `timeout`, `watch_skills`'s `debounce`, and `FDv2SkillStore`'s
`write_skills`'s `timeout`, `watch_skills`'s `debounce`, and `LaunchDarklySkillStore`'s
`poll_interval` and `read_timeout` share one rule. `nan` and `inf` both pass a `< 0` check
and then mean opposite things downstream: an unbounded vs. an already-expired deadline, a
zero vs. a never-closing coalescing window, a poll loop that spins vs. one that stops while
Expand Down Expand Up @@ -819,7 +819,7 @@ create false confidence. The operator's verification steps are in the README.
`timeout` is a monotonic deadline, checked before each retrieval, each write, and each prune;
only the final manifest rewrite runs past it, so files already written are never orphaned.
Bounded retries are **not** implemented at this layer; retry policy belongs to the delivery
transport (`FDv2SkillStore`'s capped backoff). Why:
transport (`LaunchDarklySkillStore`'s capped backoff). Why:

1. **There is nothing transient to retry.** `SkillStore.get_object` is a synchronous
in-process read of already-delivered data, modelled on the LaunchDarkly data-store API. A
Expand Down
29 changes: 15 additions & 14 deletions packages/client/api-report/launchdarkly_ai_server.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,7 @@ execute_and_track(*, config_key: str, config: AiConfigRep, meta: VariationMeta,
ExecuteStreamDoneEvent = dict[str, Any]
ExecuteStreamEvent = dict[str, Any]
extract_variation(config_key: str, context: dict[str, Any]) -> dict[str, Any] [async]
FDv2SkillStore [class]
__init__(sdk_key: str, *, base_uri: str | None = None, stream_uri: str | None = None, mode: Mode = 'stream', poll_interval: float = 30.0, read_timeout: float | None = None, initial_backoff: float = 1.0, max_backoff: float = 30.0, _requester: Any = None)
__enter__() -> FDv2SkillStore
__exit__(*_exc: Any) -> None
add_listener(kind: str, fn: Callable[[dict[str, Any]], Any]) -> None
all_objects(kind: str) -> dict[str, dict[str, Any]]
close(timeout: float = 5.0) -> None
diagnostics: StoreDiagnostics [property]
failed: str | None [property]
get_object(kind: str, key: str, version: int | None = None) -> dict[str, Any] | None
is_initialized() -> bool
remove_listener(kind: str, fn: Callable[[dict[str, Any]], Any]) -> None
start() -> FDv2SkillStore
wait_for_skills(timeout: float = 10.0) -> bool
FDv2SkillStore = LaunchDarklySkillStore
get_client() -> Any
get_skill(key: str, *, version: int | None = None) -> Skill | None [async]
get_skill_result(key: str, *, version: int | None = None) -> SkillOutcome [async]
Expand Down Expand Up @@ -139,6 +126,20 @@ lang_chain_content_text(content: Any) -> str
lang_chain_finish_reasons(source: Any) -> list[str] | None
lang_chain_span_messages(messages: list[Any]) -> tuple[str | None, list[SpanMessage]]
lang_chain_span_usage(usage: dict[str, Any] | None) -> SpanUsage | None
LaunchDarklySkillStore [class]
__init__(sdk_key: str, *, base_uri: str | None = None, stream_uri: str | None = None, mode: Mode = 'stream', poll_interval: float = 30.0, read_timeout: float | None = None, initial_backoff: float = 1.0, max_backoff: float = 30.0, _requester: Any = None)
__enter__() -> LaunchDarklySkillStore
__exit__(*_exc: Any) -> None
add_listener(kind: str, fn: Callable[[dict[str, Any]], Any]) -> None
all_objects(kind: str) -> dict[str, dict[str, Any]]
close(timeout: float = 5.0) -> None
diagnostics: StoreDiagnostics [property]
failed: str | None [property]
get_object(kind: str, key: str, version: int | None = None) -> dict[str, Any] | None
is_initialized() -> bool
remove_listener(kind: str, fn: Callable[[dict[str, Any]], Any]) -> None
start() -> LaunchDarklySkillStore
wait_for_skills(timeout: float = 10.0) -> bool
LDClientInterface [class]
close() -> None [async]
flush() -> None [async]
Expand Down
6 changes: 5 additions & 1 deletion packages/client/src/launchdarkly_ai_server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
)
from .skills import _skill_refs as _skill_refs
from .skills_core import SkillStore
from .skills_fdv2 import FDv2SkillStore, StoreDiagnostics
from .skills_fdv2 import LaunchDarklySkillStore, StoreDiagnostics
Comment thread
XieX marked this conversation as resolved.
Resolved
from .skills_fs import (
MANIFEST_FILENAME,
MANIFEST_VERSION,
Expand Down Expand Up @@ -149,6 +149,9 @@
)
from .utils import _create_handler as _create_handler

# Deprecated: the 0.5.0 name. Removed in the next major release.
FDv2SkillStore = LaunchDarklySkillStore

Check warning on line 153 in packages/client/src/launchdarkly_ai_server/__init__.py

View workflow job for this annotation

GitHub Actions / API compatibility

FDv2SkillStore

Public object points to a different kind of object: `class` -> `attribute`

Check warning on line 153 in packages/client/src/launchdarkly_ai_server/__init__.py

View workflow job for this annotation

GitHub Actions / API compatibility

FDv2SkillStore

Public object points to a different kind of object: `class` -> `attribute`

Check warning on line 153 in packages/client/src/launchdarkly_ai_server/__init__.py

View workflow job for this annotation

GitHub Actions / API compatibility

FDv2SkillStore

Public object points to a different kind of object: `class` -> `attribute`

Check warning on line 153 in packages/client/src/launchdarkly_ai_server/__init__.py

View workflow job for this annotation

GitHub Actions / API compatibility

FDv2SkillStore

Public object points to a different kind of object: `class` -> `attribute`

__all__ = [ # noqa: RUF022
# types
"NATIVE_TOOL_KEY",
Expand Down Expand Up @@ -271,6 +274,7 @@
"SkillStore",
"InMemorySkillStore",
# Agent Skills: LaunchDarkly delivery store and on-change re-reconcile
"LaunchDarklySkillStore",
"FDv2SkillStore",
"StoreDiagnostics",
"watch_skills",
Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/launchdarkly_ai_server/skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def set_skill_store(store: SkillStore | None) -> None:
replacement.

Args:
store: ``FDv2SkillStore`` to receive skills from LaunchDarkly, or
store: ``LaunchDarklySkillStore`` to receive skills from LaunchDarkly, or
``InMemorySkillStore`` for local development and tests.

Raises:
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 @@ -92,10 +92,10 @@
INTEGRITY_REASON_CODES: frozenset[str] = frozenset(get_args(IntegrityReasonCode))
"""``IntegrityReasonCode`` as a runtime set, derived rather than restated."""

NO_STORE_MESSAGE = (

Check warning on line 95 in packages/client/src/launchdarkly_ai_server/skills_core.py

View workflow job for this annotation

GitHub Actions / API compatibility

NO_STORE_MESSAGE

Attribute value was changed: `'No skill store is configured, so skill content cannot be retrieved. Configure one with set_skill_store(store) from launchdarkly_ai_server — FDv2SkillStore receives content from LaunchDarkly, and InMemorySkillStore is available for local development and testing.'` -> `'No skill store is configured, so skill content cannot be retrieved. Configure one with set_skill_store(store) from launchdarkly_ai_server — LaunchDarklySkillStore receives content from LaunchDarkly, and InMemorySkillStore is available for local development and testing.'`

Check warning on line 95 in packages/client/src/launchdarkly_ai_server/skills_core.py

View workflow job for this annotation

GitHub Actions / API compatibility

NO_STORE_MESSAGE

Attribute value was changed: `'No skill store is configured, so skill content cannot be retrieved. Configure one with set_skill_store(store) from launchdarkly_ai_server — FDv2SkillStore receives content from LaunchDarkly, and InMemorySkillStore is available for local development and testing.'` -> `'No skill store is configured, so skill content cannot be retrieved. Configure one with set_skill_store(store) from launchdarkly_ai_server — LaunchDarklySkillStore receives content from LaunchDarkly, and InMemorySkillStore is available for local development and testing.'`

Check warning on line 95 in packages/client/src/launchdarkly_ai_server/skills_core.py

View workflow job for this annotation

GitHub Actions / API compatibility

NO_STORE_MESSAGE

Attribute value was changed: `'No skill store is configured, so skill content cannot be retrieved. Configure one with set_skill_store(store) from launchdarkly_ai_server — FDv2SkillStore receives content from LaunchDarkly, and InMemorySkillStore is available for local development and testing.'` -> `'No skill store is configured, so skill content cannot be retrieved. Configure one with set_skill_store(store) from launchdarkly_ai_server — LaunchDarklySkillStore receives content from LaunchDarkly, and InMemorySkillStore is available for local development and testing.'`

Check warning on line 95 in packages/client/src/launchdarkly_ai_server/skills_core.py

View workflow job for this annotation

GitHub Actions / API compatibility

NO_STORE_MESSAGE

Attribute value was changed: `'No skill store is configured, so skill content cannot be retrieved. Configure one with set_skill_store(store) from launchdarkly_ai_server — FDv2SkillStore receives content from LaunchDarkly, and InMemorySkillStore is available for local development and testing.'` -> `'No skill store is configured, so skill content cannot be retrieved. Configure one with set_skill_store(store) from launchdarkly_ai_server — LaunchDarklySkillStore receives content from LaunchDarkly, and InMemorySkillStore is available for local development and testing.'`
"No skill store is configured, so skill content cannot be retrieved. Configure "
"one with set_skill_store(store) from launchdarkly_ai_server "
"— FDv2SkillStore receives content from LaunchDarkly, and InMemorySkillStore is "
"— LaunchDarklySkillStore receives content from LaunchDarkly, and InMemorySkillStore is "
"available for local development and testing."
)
"""What the accessors report when no store is configured. Callers match on
Expand Down
Loading
Loading