Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
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
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
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
366 changes: 359 additions & 7 deletions packages/client/README.md

Large diffs are not rendered by default.

501 changes: 491 additions & 10 deletions packages/client/agents.md

Large diffs are not rendered by default.

44 changes: 44 additions & 0 deletions packages/client/src/launchdarkly_ai_server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,23 @@
resolve_tools,
)
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 @@ -69,7 +84,12 @@
ProviderGraphResponse,
ProviderHandler,
ProviderResponse,
ReconcileAction,
ReconcileActionKind,
ReconcileReport,
Skill,
SkillOutcome,
SkillOutcomeReason,
SkillReference,
StreamChunkEvent,
StreamDoneEvent,
Expand Down Expand Up @@ -131,7 +151,11 @@
"ProviderGraphResponse",
"ProviderHandler",
"ProviderResponse",
"ReconcileAction",
"ReconcileActionKind",
"ReconcileReport",
"Skill",
"SkillOutcome",
"SkillReference",
"StreamChunkEvent",
"StreamDoneEvent",
Expand Down Expand Up @@ -208,4 +232,24 @@
"GraphInstance",
# skills
"skill_refs",
"get_skill",
"get_skill_result",
"get_skills",
"all_skills",
"write_skills",
"SkillStore",
"InMemorySkillStore",
# skills — the FDv2 delivery transport, and the eager re-reconcile it enables
"FDv2SkillStore",
"StoreDiagnostics",
"watch_skills",
"SkillWatcher",
# skills — the three closed-set unions a typed consumer needs to name
"ReconcileActionKind",
"OnUnavailable",
"SkillOutcomeReason",
# skills — on-disk constants, identical across languages
"SKILL_FILENAME",
"MANIFEST_FILENAME",
"MANIFEST_VERSION",
]
42 changes: 40 additions & 2 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 .types import InitClientOptions

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -130,13 +131,43 @@ 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']`` configures the store the Agent Skills accessors
read from. Absent by default, in which case they raise an actionable error.

This function is idempotent for the client singleton: a second call returns
the existing client without re-initializing, and every option is ignored —
**except** ``skillStore``, which is applied on every successful call. That
asymmetry is deliberate, and it is what lets a client that was lazily
auto-initialized, or initialized without a store, be given one afterwards.
A ``skillStore`` of ``None`` (or absent) never clears an already-configured
store; use ``shutdown()`` for that. The store is installed only once
initialization has succeeded: a call that raises leaves no global state
behind, so a failed init cannot leave the skill accessors working against a
store the application believes was never installed.

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

opts = options or {}

ld_client = await _resolve_client(opts, client)

# The single success point: every path that raises returns before here, so
# "installed only on success" is one statement rather than a copy per exit.
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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this new _resolve_client method? Or can we continue to use the init_client method as it was before and set the store at the appropriate point in the initialization?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skill store is different than the other things we set in that,

  1. We want to be able to add a store even if the SDK was initialized with no store or a different store, to support lazy initialization and then setting the store later when it's ready
  2. We only want to set the store if the SDK init has been successful, so that we don't try to use it when we can't

That's why _resolve_client was introduced, just to wrap all of the return paths and set the skill store once, instead of 3 or 4 times throughout.

"""
Returns the singleton client, initializing it on first call.

Split from ``init_client`` so that function has exactly one success point to
hang the ``skillStore`` carve-out on.
"""
global _client

# Idempotent — if already initialized, return the existing client
if _client is not None:
return _client
Expand Down Expand Up @@ -198,12 +229,18 @@ 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 (and telemetry emitter): after a
shutdown, re-pass ``skillStore`` to the next ``init_client`` if the skill
accessors should keep working.
"""
global _client, _tracer_provider

local_client = _client
local_provider = _tracer_provider

skills._clear_state()

# Null the singleton before any awaits so a second call is a no-op
_client = None
_tracer_provider = None
Expand Down Expand Up @@ -240,6 +277,7 @@ def _reset_for_testing() -> None:
global _client, _tracer_provider
_client = None
_tracer_provider = None
skills._clear_state()


async def inspect_config(
Expand Down
Loading
Loading