Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
124 commits
Select commit Hold shift + click to select a range
47a2a4a
feat(client): Agent Skills — reference discovery
XieX Aug 25, 2026
27ef12f
feat(client): Agent Skills — retrieval through an injectable store
XieX Aug 25, 2026
1afb690
feat(client): a documented log record for skill integrity failures
XieX Aug 28, 2026
8ad49f2
feat(client): descriptor-pinned filesystem primitives
XieX Aug 25, 2026
f1aad0a
test(client): Agent Skills — the filesystem abuse matrix
XieX Aug 25, 2026
568729d
feat(client): Agent Skills — materialize onto disk under a manifest
XieX Aug 25, 2026
c88d5bf
test(client): assert rename containment through the branching helper
XieX Aug 26, 2026
49c2741
feat(client): Agent Skills — self-healing reconciles, and keys no fil…
XieX Aug 30, 2026
eda4ae1
feat(client): Agent Skills — a distinguishable outcome for integrity …
XieX Aug 31, 2026
6a0e6aa
docs(client): Agent Skills — close out the security review's SDK-side…
XieX Sep 4, 2026
3bdbfab
test(client): Agent Skills — the root-swap races the descriptor walk …
pkaeding Sep 4, 2026
559c985
test(client): let the root-swap races fail outright
pkaeding Sep 4, 2026
dd5db37
fix(client): pin the skills root for the whole reconcile
XieX Sep 4, 2026
5040942
docs(client): deny the agent write access to the root's ancestors
XieX Sep 4, 2026
dd2d55c
feat(client): Agent Skills — re-reconcile on delivery with watch_skills
XieX Sep 10, 2026
0f797c0
feat(client): Agent Skills — the FDv2 delivery protocol, without the …
XieX Sep 10, 2026
e2b54fd
feat(client): an unheld version pin is a miss, not an integrity failure
XieX Sep 11, 2026
5817d89
fix(client): a broken store answer is not an absent skill
XieX Sep 11, 2026
29ad3fe
fix(client): attach the skill watcher's listener before its first rec…
XieX Sep 11, 2026
9571dca
chore: merge split/skills-retrieval into split/skills-safe-fs
XieX Sep 11, 2026
a6aae0a
chore: merge split/skills-safe-fs into split/skills-materialization
XieX Sep 11, 2026
4c6d965
test(client): a broken store answer never deletes managed files
XieX Sep 11, 2026
1cf5235
feat(client): Agent Skills — name the payload a transfer completed
XieX Sep 11, 2026
efc4ca7
fix(client): Agent Skills — read the skill's version off the wire key
XieX Sep 11, 2026
b6a25f9
feat(client): Agent Skills — the FDv2 delivery transport
XieX Sep 10, 2026
c285ff5
fix(client): retry an FDv2 stream that dies mid-read
XieX Sep 11, 2026
daf04a6
fix(client): raise the skill content cap to 10 MiB
XieX Sep 11, 2026
b7ee71a
fix(client): keep FDv2 delivery alive on an idle stream, and shut dow…
XieX Sep 14, 2026
88c225e
fix(client): log a recycled FDv2 stream at debug rather than warning
XieX Sep 14, 2026
d2386c8
feat(client): Agent Skills — the FDv2 delivery transport (#83)
XieX Sep 15, 2026
96576aa
feat(client): Agent Skills — the FDv2 delivery protocol, without the …
XieX Sep 15, 2026
09d7b60
feat(client): Agent Skills — re-reconcile on delivery with watch_skil…
XieX Sep 15, 2026
51f06a6
docs(client): Agent Skills — close out the security review's SDK-side…
XieX Sep 15, 2026
7b59680
feat(client): Agent Skills — a distinguishable outcome for integrity …
XieX Sep 15, 2026
b62571e
feat(client): Agent Skills — self-healing reconciles, and keys no fil…
XieX Sep 15, 2026
ba215ef
test(client): Agent Skills — the filesystem abuse matrix (5/5) (#54)
XieX Sep 15, 2026
71a2531
feat(client): Agent Skills — materialize onto disk under a manifest (…
XieX Sep 15, 2026
dad1d30
feat(client): descriptor-pinned filesystem primitives (3/5) (#52)
XieX Sep 15, 2026
68153ee
feat(client): Agent Skills — retrieval through an injectable store (2…
XieX Sep 15, 2026
d3a8bc1
fix(client): read the skills manifest through the pinned root descriptor
XieX Sep 15, 2026
e1356a5
fix(client): pin the skills root for the whole reconcile (SEC-8985 ro…
XieX Sep 15, 2026
b7ad16f
fix(client): give the key-mismatch resolution its outcome reason
XieX Sep 15, 2026
fe13c23
Merge remote-tracking branch 'origin/main' into split/skills-references
XieX Sep 15, 2026
0902f29
test(client): derive the oversize fixture from the content cap
XieX Sep 15, 2026
9252fb0
fix(client): resolve a missed pin as absent, not as an integrity failure
XieX Sep 15, 2026
ad53fa0
fix(client): probe for os.fchmod instead of assuming it
XieX Sep 16, 2026
7c070d0
fix(client): close two paths that let the reconcile prune what it sho…
XieX Sep 16, 2026
098455f
fix(client): stop reporting a withholding for a key that resolved
XieX Sep 15, 2026
5a896ce
fix(client): do not prune when the store has not received its initial…
XieX Sep 15, 2026
b0ac0e3
fix(client): bound the manifest read, and two smaller reconcile fixes
XieX Sep 15, 2026
a6eeae9
docs(client): sweep the skills comments for a public SDK audience
XieX Sep 15, 2026
e40fbb9
docs(client): move the threat model into agents.md, leave invariants …
XieX Sep 15, 2026
4bc12c0
test(client): make the no-track assertions order-independent
XieX Sep 15, 2026
d30d4b0
fix(client): require https and refuse redirects in the skills transport
XieX Sep 16, 2026
6c08ffe
fix(client): bound transport reads in memory before verification sees…
XieX Sep 16, 2026
45b549c
fix(client): pin the decision reads under the skills root to descriptors
XieX Sep 16, 2026
5e6313c
fix(client): start a replacement thread when a restart races the give-up
XieX Sep 16, 2026
6ac1577
fix(client): point FDv2 streaming at the streaming host
XieX Sep 17, 2026
c69d880
fix(client): floor Retry-After, and count only revocations that landed
XieX Sep 17, 2026
86fd04c
fix(client): make close() final, and reclassify HTTP 400 and 404
XieX Sep 17, 2026
2f59b9a
fix(client): refuse a skill-store listener on a kind that never fires
XieX Sep 17, 2026
daeedec
fix(client): reject a non-finite watch_skills debounce, and test the …
XieX Sep 17, 2026
4a204f8
fix(client): fail the config parse on an explicit skills: null
XieX Sep 17, 2026
030e8c3
fix(client): check content size before encoding it
XieX Sep 17, 2026
4092de7
docs(client): correct the stale cross-language parity claim, and the …
XieX Sep 17, 2026
a6c2af0
fix(client): publish the revocations an xfer-full states by omission
XieX Sep 17, 2026
204e287
Simplify comment
XieX Sep 17, 2026
c2d482a
fix(client): keep the stale-selector repair out of the retry budget
XieX Sep 17, 2026
0bdbdd3
test(client): pin the key-mismatch detection surface (failing)
XieX Sep 17, 2026
3a9df62
fix(client): record the key mismatch on the log surface
XieX Sep 17, 2026
a4fc1eb
fix(client): keep the resume point on the payload skills arrive on
XieX Sep 18, 2026
a30750a
fix(client): pair the poll etag with the basis it was issued against
XieX Sep 18, 2026
b9b05de
Merge base xie/python-agent-skills-review-fixes into the etag fix
XieX Sep 18, 2026
1ce4e53
docs(client): state the revocation bound without the watcher
XieX Sep 22, 2026
a81479c
feat(client): declare ?kinds=agent-skill, and treat a 422 as "no skil…
XieX Sep 24, 2026
b4f29a8
docs(client): say the 422's cause once, on the type that names it
XieX Sep 24, 2026
6776390
fix(client): publish the revocations an xfer-full states by omission …
XieX Sep 24, 2026
43ff5c0
fix(client): Agent Skills — transport, lifecycle and spec fixes from …
XieX Sep 24, 2026
91c9e20
docs(client): correct what the kinds declaration made stale, and trim…
XieX Sep 28, 2026
affb82c
docs(client): say what the 422 is kept off, and list the field that w…
XieX Sep 28, 2026
698c7b3
fix(client): record the version mismatch on the log surface
XieX Sep 28, 2026
bdedb33
fix(client): treat HTTP 422 as a fatal transport failure
XieX Sep 29, 2026
1119479
Update skills_fdv2.py
XieX Sep 29, 2026
50260aa
Update agents.md
XieX Sep 29, 2026
a359b3b
Update README.md
XieX Sep 29, 2026
a6ca2cc
fix(client): treat HTTP 422 as a fatal transport failure (#117)
XieX Sep 29, 2026
8e9537f
fix(client): restore the 422 message the web edit truncated
XieX Sep 29, 2026
9c5bc38
docs(client): say the version mismatch and the kinds declaration once
XieX Sep 29, 2026
666e79f
fix(client): name the recovery the 422 actually has
XieX Sep 29, 2026
32af7e6
fix(client): name one cause in the 422 message, and refer the rest
XieX Sep 29, 2026
260856b
fix(client): point the give-up log line at start(), not a restart
XieX Sep 29, 2026
64ada1e
feat(client): declare ?kinds=agent-skill, and treat a 422 as "no skil…
XieX Sep 29, 2026
fca58a3
Merge origin/main into xie/agent-skills
XieX Sep 29, 2026
7825d8d
chore(client): list ReconcileActionKind in __all__ once
XieX Sep 30, 2026
062862a
docs(client): sweep the skills comments and messages for an outside r…
XieX Sep 30, 2026
e1af563
test(client): pin seven Agent Skills behaviours the suite left unguarded
XieX Sep 30, 2026
f100d0c
test(client): pin the content-hash rule to the service's own digests
XieX Sep 30, 2026
2ed7dc2
fix(client): reject a non-finite write_skills timeout and poll_interval
XieX Sep 30, 2026
d6b0d77
test(client): pin the adoption comparison to the same content-hash di…
XieX Sep 30, 2026
d5ee277
fix(client): release the global OTel tracer provider on shutdown
XieX Oct 1, 2026
b34b45f
fix(client): a 304 confirms a payload rather than establishing one
XieX Oct 1, 2026
8f7d660
fix(client): only release the tracer provider this SDK actually regis…
XieX Oct 1, 2026
d1e89e6
fix(client): only adopt a client once telemetry setup has succeeded
XieX Oct 1, 2026
346a552
docs(client): trim the 304/etag comments to what a consumer needs
XieX Oct 2, 2026
744199f
docs(client): tighten Agent Skills comments and docstrings
XieX Oct 2, 2026
c547be2
docs(client): trim the Agent Skills README and agents.md sections
XieX Oct 2, 2026
f6bb491
fix(skills): apply changes after a none intent, and retry recoverable…
XieX Oct 2, 2026
a06237c
fix(skills): bound the retry loop now that failures retry indefinitely
XieX Oct 2, 2026
daaf208
Merge branch 'xie/agent-skills' into xie/skills-304-first-payload
XieX Oct 5, 2026
ebb6c1e
Merge branch 'xie/agent-skills' into xie/fix-otel-globals-on-shutdown
XieX Oct 5, 2026
bc403df
fix(client): a 304 confirms a payload rather than establishing one (#…
XieX Oct 5, 2026
770a004
fix(client): count only served skills in the get_skills withholding w…
XieX Oct 5, 2026
cc31b69
Merge branch 'xie/agent-skills' into xie/skills-fdv2-transport-fixes
XieX Oct 5, 2026
6888317
fix(skills): apply changes after a none intent, and retry recoverable…
XieX Oct 5, 2026
5d8fdc0
fix(client): count only served skills in the get_skills withholding w…
XieX Oct 5, 2026
9c22ba9
fix(client): release the global OTel tracer provider on shutdown (#126)
XieX Oct 5, 2026
4f672f6
test(client): assert a declined foreign payload is not a commit
XieX Oct 5, 2026
e43d9f7
test(client): assert a declined foreign payload is not a commit (#138)
XieX Oct 5, 2026
62fe384
fix(client): reset connection_failures when a store that gave up is s…
XieX Oct 5, 2026
920c974
test(client): assert a failed init_client can be retried
XieX Oct 5, 2026
542233b
test(client): assert a failed init_client can be retried (#140)
XieX Oct 5, 2026
1609855
test(client): pin the adoption comparison to the same content-hash di…
XieX Oct 6, 2026
a068cbc
docs(client): say a restarted store's wait_for_skills waits again
XieX Oct 6, 2026
805178f
fix(client): reset connection_failures when a store that gave up is s…
XieX Oct 6, 2026
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
458 changes: 458 additions & 0 deletions packages/client/README.md

Large diffs are not rendered by default.

673 changes: 671 additions & 2 deletions packages/client/agents.md

Large diffs are not rendered by default.

52 changes: 52 additions & 0 deletions packages/client/src/launchdarkly_ai_server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,24 @@
resolve_tools,
)
from .sdk_info import SDK_INFO_CONTEXT, SDK_INFO_EVENT, register_ai_sdk_package
from .skills import (
InMemorySkillStore,
all_skills,
get_skill,
get_skill_result,
get_skills,
skill_refs,
)
from .skills_core import SkillStore
from .skills_fdv2 import FDv2SkillStore, StoreDiagnostics
from .skills_fs import (
MANIFEST_FILENAME,
MANIFEST_VERSION,
SKILL_FILENAME,
OnUnavailable,
write_skills,
)
from .skills_watch import SkillWatcher, watch_skills
from .tracking import execute_and_stream, execute_and_track, wrap_tool_handlers
from .types import (
NATIVE_TOOL_KEY,
Expand Down Expand Up @@ -93,6 +111,13 @@
ProviderGraphResponse,
ProviderHandler,
ProviderResponse,
ReconcileAction,
ReconcileActionKind,
ReconcileReport,
Skill,
SkillOutcome,
SkillOutcomeReason,
SkillReference,
StreamChunkEvent,
StreamDoneEvent,
StreamEvent,
Expand Down Expand Up @@ -156,6 +181,11 @@
"ProviderGraphResponse",
"ProviderHandler",
"ProviderResponse",
"ReconcileAction",
"ReconcileReport",
"Skill",
"SkillOutcome",
"SkillReference",
"StreamChunkEvent",
"StreamDoneEvent",
"StreamEvent",
Expand Down Expand Up @@ -254,6 +284,28 @@
"graph",
"resolve_graph",
"GraphInstance",
# skills
"skill_refs",
"get_skill",
"get_skill_result",
"get_skills",
"all_skills",
"write_skills",
"SkillStore",
"InMemorySkillStore",
# skills — LaunchDarkly delivery store and on-change re-reconcile
"FDv2SkillStore",
"StoreDiagnostics",
"watch_skills",
"SkillWatcher",
# skills — literal types for typed consumers
"ReconcileActionKind",
"OnUnavailable",
"SkillOutcomeReason",
# skills — on-disk filenames and manifest version
"SKILL_FILENAME",
"MANIFEST_FILENAME",
"MANIFEST_VERSION",
]

register_ai_sdk_package("launchdarkly-ai-server", __version__)
124 changes: 117 additions & 7 deletions packages/client/src/launchdarkly_ai_server/lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import os
from typing import Any

from . import skills
from .sdk_info import flush_ai_sdk_info, reset_ai_sdk_info
from .types import InitClientOptions
from .utils import model_stamps_from_meta
Expand All @@ -23,6 +24,11 @@ def _env(name: str) -> str | None:

_client: Any = None
_tracer_provider: Any = None
# True only when *this* SDK's call to trace.set_tracer_provider actually took
# effect. "We built a provider" is not the same as "we own the global": the set
# is once-guarded, so when another library registered first ours is refused and
# the global stays theirs. Only the owner may release it on shutdown.
_owns_otel_globals: bool = False


def get_client() -> Any:
Expand All @@ -48,7 +54,7 @@ def _setup_telemetry(sdk_key: str, options: InitClientOptions | None = None) ->
- Registers W3C trace context and baggage propagators.
- Configures GZIP compression on the OTLP exporter.
"""
global _tracer_provider
global _tracer_provider, _owns_otel_globals

opts = options or {}

Expand Down Expand Up @@ -112,6 +118,18 @@ def _setup_telemetry(sdk_key: str, options: InitClientOptions | None = None) ->
pass

trace.set_tracer_provider(provider)
# The set is refused, with a warning from OTel, when another library got
# there first. Record whether it actually took: shutdown must not release
# a global it never owned, and the caller's telemetry options are moot if
# someone else's provider is the one handing out tracers.
_owns_otel_globals = trace.get_tracer_provider() is provider
if not _owns_otel_globals:
logger.warning(
"An OpenTelemetry tracer provider was already registered by "
"something else in this process, so LaunchDarkly's telemetry "
"configuration is not in effect; spans will go wherever that "
"provider sends them."
)
_tracer_provider = provider
return provider

Expand All @@ -132,22 +150,47 @@ async def init_client(

- Pass *client* directly (BYOC) to skip the LaunchDarkly Python SDK path.
- Otherwise, reads ``LD_SDK_KEY`` from env or ``options['sdkKey']``.
- ``options['skillStore']`` sets the store the Agent Skills accessors read
from. Without one, the accessors raise ``RuntimeError``.

Idempotent: later calls return the existing client and ignore every option
**except** ``skillStore``, which is applied on every successful call, so you
can add a store after initialization. A ``None`` store never clears the
current one (use ``shutdown()``), and a call that raises installs nothing.

Returns the initialized ``LDClientInterface`` instance.
"""
global _client

opts = options or {}

ld_client = await _resolve_client(opts, client)

# Reached only on success, so a failed init installs no store.
skill_store = opts.get("skillStore")
if skill_store is not None:
skills._set_store(skill_store)
return ld_client


async def _resolve_client(opts: InitClientOptions, client: Any) -> Any:
"""
Returns the singleton client, initializing it on first call.
"""
global _client

# Idempotent — if already initialized, return the existing client
if _client is not None:
flush_ai_sdk_info(_client)
return _client

# BYOC path — pre-initialized client
if client is not None:
_client = client
# Adopted only after telemetry setup succeeds. ``_client`` is the
# idempotency guard above, so assigning it first meant a setup that
# raised (a malformed OTEL_EXPORTER_OTLP_TIMEOUT does) left it set: the
# next call returned it as a silent success with no telemetry, hiding
# the config error. The caller owns this client, so it is not closed.
_setup_telemetry(opts.get("sdkKey", "byoc"), opts)
_client = client
flush_ai_sdk_info(_client)
return _client

Expand Down Expand Up @@ -193,32 +236,92 @@ async def init_client(
# start_wait caps the blocking init time; matches the TS SDK's 10 s timeout.
ld_client = client_cls(ld_config, start_wait=10)

# As on the BYOC path: only a fully set-up client becomes the singleton. We
# built this one, so close it on failure — it holds a streaming connection
# that would otherwise outlive the attempt.
try:
_setup_telemetry(sdk_key, opts)
except Exception:
try:
close_result = ld_client.close()
if inspect.isawaitable(close_result):
await close_result
except Exception:
pass
raise
_client = ld_client
_setup_telemetry(sdk_key, opts)
flush_ai_sdk_info(_client)
return _client


def _release_otel_globals() -> None:
"""
Releases the process-global tracer provider that ``_setup_telemetry``
installed, so a later ``init_client`` can install its own.

``trace.set_tracer_provider`` is guarded by a ``Once``: a second call logs
"Overriding of current TracerProvider is not allowed" and keeps the provider
already in place. Without this, an init/shutdown/init cycle would leave every
span routed to the provider that was already shut down, and export nothing.

opentelemetry-python exposes no public API to unset it, so this reaches for
the module globals — both the slot and the ``Once`` that guards it, since
clearing the slot alone leaves the guard tripped and the next set a no-op.

Callers must gate this on ``_owns_otel_globals``. Having built a provider is
not enough: when another library registered first, our set was refused and
the global is still theirs, so releasing it here would tear down the host
application's tracing and leave the global a no-op proxy.

The global text map propagator needs no equivalent: ``set_global_textmap``
is a plain assignment with no ``Once``, so the next setup overwrites it.
"""
try:
from opentelemetry import trace
from opentelemetry.util._once import Once

trace._TRACER_PROVIDER = None
trace._TRACER_PROVIDER_SET_ONCE = Once()
except Exception: # pragma: no cover - defensive, OTel absent or restructured
logger.debug("Could not release the global OTel tracer provider", exc_info=True)


async def shutdown() -> None:
"""
Shuts down the singleton client. Idempotent — safe to call multiple times
even if the client was never initialized or already shut down.

Also clears the configured skill store; pass ``skillStore`` again to the
next ``init_client`` to keep using the skill accessors.

When telemetry was running, this also releases the process-global tracer
provider so a later ``init_client`` can install its own — see
``_release_otel_globals``.
"""
global _client, _tracer_provider
global _client, _tracer_provider, _owns_otel_globals

local_client = _client
local_provider = _tracer_provider
owned_globals = _owns_otel_globals

skills._clear_state()

# Null the singleton before any awaits so a second call is a no-op
_client = None
_tracer_provider = None
_owns_otel_globals = False
reset_ai_sdk_info()

if local_provider is not None:
# Shut the provider down either way — we built it, and it owns an
# exporter and a batch timer — but only release the global registration
# when it was ours to take.
try:
local_provider.shutdown()
except Exception:
pass
if owned_globals:
_release_otel_globals()

if local_client is not None:
try:
Expand All @@ -243,9 +346,16 @@ def _set_client_for_testing(c: Any) -> None:

def _reset_for_testing() -> None:
"""Test helper — clear all singleton state."""
global _client, _tracer_provider
global _client, _tracer_provider, _owns_otel_globals
owned_globals = _owns_otel_globals
_client = None
_tracer_provider = None
_owns_otel_globals = False
skills._clear_state()
# Mirrors shutdown(): without this a suite that inits more than once leaves
# every later span on the first test's provider.
if owned_globals:
_release_otel_globals()


async def inspect_config(
Expand Down
Loading
Loading