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
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,10 @@ Biome organises imports alphabetically within each group. Vitest's `vi.mock` cal

PRs are squash-merged with the PR title as the commit message, and release-please builds each package's changelog from those messages.

- **A change to an experimental feature uses that feature's scope:** `skills` (Agent Skills), `evaluations` (Evaluations from Code). `feat`, `fix` and `perf` commits with one of these scopes are listed under **Experimental** (`changelog-sections` in `release-please-config.json`), for example `feat(skills): watch skills for revocations`. Any other scope, such as `client`, lists the change as a core feature or fix.
- **Use exactly one scope.** `feat(skills,client):` matches no Experimental row, so it lands under Features.
- **A change to an experimental feature uses that feature's scope:** `evaluations` (Evaluations from Code). `feat`, `fix` and `perf` commits with that scope are listed under **Experimental** (`changelog-sections` in `release-please-config.json`), for example `feat(evaluations): score with a local judge`. Any other scope, such as `client` or `skills` (Agent Skills, now core), lists the change as a core feature or fix.
- **Use exactly one scope.** `feat(evaluations,client):` matches no Experimental row, so it lands under Features.
- **A PR that changes both core and experimental behaviour** should be split. If it can't be, add a `BEGIN_COMMIT_OVERRIDE` block to its description with one conventional-commit line per change. An override also corrects a merged PR's title, as long as the release PR hasn't merged yet.
- **A breaking experimental change** uses `!` while the package is 0.x: `feat(skills)!:` bumps the minor version, which is what an experimental break is allowed. After 1.0, `!` would force a major release. Use `feat(skills):` without `!` instead, and say the change is breaking in the title.
- **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`.
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ npm install @launchdarkly/ai-server @launchdarkly/vercel-server-sdk @launchdarkl

Then call `initClient(preInitializedClient)` with your platform's LD client before the first AI call.

> **The runtime must provide Node's built-in modules.** For Agent Skills, the `@launchdarkly/ai-server` package root imports `node:fs`, `node:fs/promises`, `node:path`, `node:os` and `node:crypto` when it loads, and every handler package imports that root. On a runtime that doesn't provide them, the import fails even if you never use Agent Skills. Restoring support for those runtimes is tracked in AIC-3674.

**With telemetry** (recommended for production) — traces export to the LaunchDarkly Observability dashboard:

```bash
Expand Down
35 changes: 0 additions & 35 deletions api-surface/ai-server.experimental.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,54 +7,19 @@
# + Name joining the public surface; may be exported or not

+ AIConfig
allSkills
createSkill
createSkillOutcome
createSkillReference
+ Criterion
+ DatasetRow
+ EvalRunResult
+ EvalTool
+ EvaluationsError
+ EvaluationsModule
FDv2Mode
FDv2SkillStore
FDv2SkillStoreOptions
+ GenerationConfig
+ GenerationMode
+ GenerationOverrides
getSkill
getSkillResult
getSkills
+ initEvaluations usage=experimental.initEvaluations
+ InitEvaluationsOptions
+ InlineDatasetRow
InMemorySkillStore
+ Judge
MANIFEST_FILENAME
MANIFEST_VERSION
OnUnavailable
RawSkillObject
ReconcileAction
ReconcileActionKind
ReconcileReport
+ RunOptions
+ RunSummary
+ Scorer
setSkillStore
Skill
SKILL_FILENAME
SkillOutcome
SkillOutcomeReason
SkillReference
skillRefs
SKILLS_DEFAULT_BASE_URI
SKILLS_DEFAULT_DEBOUNCE_MS
SKILLS_DEFAULT_STREAM_URI
SkillStore
SkillWatcher
StoreDiagnostics
watchSkills
WatchSkillsOptions
writeSkills
WriteSkillsOptions
35 changes: 35 additions & 0 deletions api-surface/ai-server.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

- addCachedTokensToInput
AiConfigRep
+ allSkills usage=client.allSkills
- anyMultimodal
- buildJudgeTasks usage=client.buildJudgeTasks
- camelizeModelParameters
Expand All @@ -29,8 +30,17 @@ ConversationIdSpanProcessor
createHandler usage=client.createHandler
- createHandlerInternal # internal: plumbing for the handler packages; moves to the internal entry point
- createRunUsage
+ createSkill
+ createSkillOutcome
+ createSkillReference
- endSpanOnce
+ FDv2Mode
+ FDv2SkillStore
+ FDv2SkillStoreOptions
getClient
+ getSkill usage=client.getSkill
+ getSkillResult usage=client.getSkillResult
+ getSkills usage=client.getSkills
globalRegistry
graph usage=client.graph.invoke,client.graph.stream
GraphArgs
Expand All @@ -51,6 +61,7 @@ HandlerStreamEvent
ImageContentBlock
initClient
+ InitClientOptions
+ InMemorySkillStore
+ InputTokenDetails
inspectConfig usage=client.inspectConfig
- inspectConfigInternal # internal: plumbing for the handler packages; moves to the internal entry point
Expand All @@ -73,19 +84,26 @@ LDVariationMeta
- makeGraphTrackData
- makeNodeTrackData
- makeRunTrackData
+ MANIFEST_FILENAME
+ MANIFEST_VERSION
Message
MessageContent
- NATIVE_TOOL_KEY
NativeTool
- normalizeModelParameters
- omitModelStamps
+ OnUnavailable
- parseJSONWithPossibleFences
parseTemplate
- pickForwardedModelParameters
ProviderGraphResponse
ProviderHandler
ProviderResponse
- ProviderSetupFn
+ RawSkillObject
+ ReconcileAction
+ ReconcileActionKind
+ ReconcileReport
- registerAiSdkPackage
Registry
- RegistryInput # internal: alias of Registry; ConfigArgs.registry and GraphOptions.registry take Registry
Expand All @@ -101,14 +119,27 @@ runJudge usage=client.runJudge
- setLdSpanAttributes
- setModelIdentityAttributes
- setOutputContentAttributes
+ setSkillStore
- setToolCallContentAttributes
- setToolDefinitionAttributes
- setUsageSpanAttributes
shutdown
- shutdownTelemetry # internal: not needed to init, configure, execute or read results
+ Skill
+ SKILL_FILENAME
+ SkillOutcome
+ SkillOutcomeReason
+ SkillReference
+ skillRefs usage=client.skillRefs
+ SKILLS_DEFAULT_BASE_URI
+ SKILLS_DEFAULT_DEBOUNCE_MS
+ SKILLS_DEFAULT_STREAM_URI
+ SkillStore
+ SkillWatcher
- SpanMessage
- SpanMessagePart
- SpanUsage
+ StoreDiagnostics
StreamEvent
+ StreamHandlerFn
TextContentBlock
Expand All @@ -122,4 +153,8 @@ ToolHandlerFn
TrackData
- TraverseVisitor
- waitForTelemetry # internal: not needed to init, configure, execute or read results
+ watchSkills usage=client.watchSkills
+ WatchSkillsOptions
withConversationId
+ writeSkills usage=client.writeSkills
+ WriteSkillsOptions
12 changes: 7 additions & 5 deletions integration-tests/commonjs-webpack/commonjs-webpack.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,14 @@ const OPTIONAL_DEPS: Record<string, string[]> = {
const RESOLVED_SYMBOLS = {
aiNodeConfig: 'function',
aiNodeInitClient: 'function',
aiNodeExperimentalGetSkill: 'function',
aiNodeRootGetSkill: 'undefined',
aiNodeExperimentalInitEvaluations: 'function',
aiNodeExperimentalGetSkill: 'undefined',
aiNodeRootGetSkill: 'function',
aiServerConfig: 'function',
// The experimental subpath resolves, and its names stay off the root.
aiServerExperimentalGetSkill: 'function',
aiServerRootGetSkill: 'undefined',
// The experimental subpath resolves; Agent Skills is on the root, not the subpath.
aiServerExperimentalInitEvaluations: 'function',
aiServerExperimentalGetSkill: 'undefined',
aiServerRootGetSkill: 'function',
openaiMessages: 'function',
langchainMessages: 'function',
};
Expand Down
2 changes: 2 additions & 0 deletions integration-tests/commonjs-webpack/fixture/entrypoints.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ process.stdout.write(
JSON.stringify({
aiNodeConfig: typeof aiNode.config,
aiNodeInitClient: typeof aiNode.initClient,
aiNodeExperimentalInitEvaluations: typeof aiNodeExperimental.initEvaluations,
aiNodeExperimentalGetSkill: typeof aiNodeExperimental.getSkill,
aiNodeRootGetSkill: typeof aiNode.getSkill,
aiServerConfig: typeof aiServer.config,
aiServerExperimentalInitEvaluations: typeof aiServerExperimental.initEvaluations,
aiServerExperimentalGetSkill: typeof aiServerExperimental.getSkill,
aiServerRootGetSkill: typeof aiServer.getSkill,
openaiMessages: typeof openaiMessages.openaiMessages,
Expand Down
2 changes: 2 additions & 0 deletions integration-tests/commonjs-webpack/fixture/entrypoints.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ process.stdout.write(
JSON.stringify({
aiNodeConfig: typeof aiNode.config,
aiNodeInitClient: typeof aiNode.initClient,
aiNodeExperimentalInitEvaluations: typeof aiNodeExperimental.initEvaluations,
aiNodeExperimentalGetSkill: typeof aiNodeExperimental.getSkill,
aiNodeRootGetSkill: typeof aiNode.getSkill,
aiServerConfig: typeof aiServer.config,
aiServerExperimentalInitEvaluations: typeof aiServerExperimental.initEvaluations,
aiServerExperimentalGetSkill: typeof aiServerExperimental.getSkill,
aiServerRootGetSkill: typeof aiServer.getSkill,
openaiMessages: typeof openaiMessages.openaiMessages,
Expand Down
23 changes: 17 additions & 6 deletions integration-tests/commonjs-webpack/fixture/types.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
// Type-checked by the integration test under each `moduleResolution` a consumer may use,
// including `node` (node10), which ignores `exports` and needs `typesVersions` for a subpath.
import { config, initClient } from '@launchdarkly/ai-node';
import { setSkillStore as setNodeSkillStore } from '@launchdarkly/ai-node/experimental';
import { config, initClient, setSkillStore as setNodeSkillStore } from '@launchdarkly/ai-node';
import * as nodeExperimental from '@launchdarkly/ai-node/experimental';
import * as server from '@launchdarkly/ai-server';
import { InMemorySkillStore, type SkillStore, setSkillStore } from '@launchdarkly/ai-server/experimental';
import { InMemorySkillStore, type SkillStore, setSkillStore } from '@launchdarkly/ai-server';
import * as serverExperimental from '@launchdarkly/ai-server/experimental';

const store: SkillStore = new InMemorySkillStore();
setSkillStore(store);
setNodeSkillStore(store);

// @ts-expect-error Agent Skills is not exported from the package root.
export const rootGetSkill = server.getSkill;
// @ts-expect-error Agent Skills is not exported from the experimental subpath.
Comment thread
jeffdupont marked this conversation as resolved.
export const experimentalGetSkill = nodeExperimental.getSkill;

export const used = [config, initClient, server.config];
// @ts-expect-error Nor from the server's experimental subpath, so a stale experimental.d.ts / .d.cts
// (or typesVersions) that still declares it fails here.
export const serverExperimentalGetSkill = serverExperimental.getSkill;

export const used = [
config,
initClient,
server.config,
serverExperimental.initEvaluations,
nodeExperimental.initEvaluations,
];
9 changes: 6 additions & 3 deletions packages/ai-node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,14 @@ Experimental features are not exported from the package root. Import them from
you need no direct dependency on `@launchdarkly/ai-server`. For example:

```ts
import { InMemorySkillStore, setSkillStore } from '@launchdarkly/ai-node/experimental';
import { initEvaluations } from '@launchdarkly/ai-node/experimental';
```

Names there may change in a minor release. See the
[Agent Skills guide](../client/README.md#agent-skills-experimental).
Names there may change in a minor release.

Agent Skills is no longer experimental: import it from the package root, for example
`import { InMemorySkillStore, setSkillStore } from '@launchdarkly/ai-node';`. See the
[Agent Skills guide](../client/README.md#agent-skills).

## Full API reference

Expand Down
2 changes: 1 addition & 1 deletion packages/ai-node/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ For edge runtimes (Vercel, Cloudflare Workers, Deno, etc.) use `@launchdarkly/ai
| File | Responsibility |
|---|---|
| `src/index.ts` | Single `export * from '@launchdarkly/ai-server'` — the entire public barrel |
| `src/experimental.ts` | Single `export * from '@launchdarkly/ai-server/experimental'` — published as `@launchdarkly/ai-node/experimental`, so Node.js users reach experimental features (Agent Skills) without a direct `@launchdarkly/ai-server` dependency |
| `src/experimental.ts` | Single `export * from '@launchdarkly/ai-server/experimental'` — published as `@launchdarkly/ai-node/experimental`, so Node.js users reach experimental features (Evaluations from Code) without a direct `@launchdarkly/ai-server` dependency |

---

Expand Down
11 changes: 9 additions & 2 deletions packages/ai-node/src/__tests__/reexports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,14 @@ describe('@launchdarkly/ai-node/experimental re-exports', () => {
});

it('keeps experimental names off the package root', () => {
expect('getSkill' in node).toBe(false);
expect('setSkillStore' in node).toBe(false);
expect('initEvaluations' in node).toBe(false);
});

it('exports Agent Skills from the package root only', async () => {
const nodeExperimental = await import('../experimental.js');
for (const key of ['getSkill', 'setSkillStore', 'InMemorySkillStore', 'FDv2SkillStore'] as const) {
expect((node as any)[key], key).toBe((server as any)[key]);
expect(key in nodeExperimental, key).toBe(false);
}
});
});
16 changes: 9 additions & 7 deletions packages/client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ const client = await initClient({
// Or skip initClient() and let the first model/graph call initialize lazily.

// Edge / custom runtime path — pass an already-initialized client.
// @launchdarkly/node-server-sdk is NOT required in this case.
// @launchdarkly/node-server-sdk is NOT required in this case. The runtime must still
// provide Node's built-in modules (node:fs, node:path, node:os, node:crypto): the
// package root imports them for Agent Skills when it loads.
// import { init } from '@launchdarkly/vercel-server-sdk';
// const vercelClient = init(clientSideId, edgeConfigClient);
// await initClient(vercelClient);
Expand Down Expand Up @@ -234,9 +236,9 @@ if (!result.enabled) {

---

### Agent Skills (experimental)
### Agent Skills

> **Experimental.** Agent Skills is published from the `@launchdarkly/ai-server/experimental` entry point and is not exported from the package root. Its names may change in a minor release, so pin the version you test against and read the changelog before upgrading. Import every name in this section from `@launchdarkly/ai-server/experimental` (or `@launchdarkly/ai-node/experimental` if you install `@launchdarkly/ai-node`).
Import every name in this section from `@launchdarkly/ai-server` (or `@launchdarkly/ai-node` if you install `@launchdarkly/ai-node`).

Agent Skills are versioned `SKILL.md` documents managed in LaunchDarkly and attached to AI Config variations by reference. This package tells you which skills a config references, retrieves their content, and writes them to `<root>/<key>/SKILL.md`, where agent runtimes such as the Claude Agent SDK discover them.

Expand All @@ -253,7 +255,7 @@ import {
setSkillStore,
skillRefs,
writeSkills,
} from '@launchdarkly/ai-server/experimental';
} from '@launchdarkly/ai-server';

// A store serves wire-shaped raw objects. `contentHash` is sha256, lowercase
// hex, over the verbatim UTF-8 bytes of `content`. Content that does not hash
Expand Down Expand Up @@ -381,7 +383,7 @@ On macOS and Windows, write permission on the managed root **and its ancestors**
`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 (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.

```ts
import { FDv2SkillStore, setSkillStore, watchSkills } from '@launchdarkly/ai-server/experimental';
import { FDv2SkillStore, setSkillStore, watchSkills } from '@launchdarkly/ai-server';

const store = new FDv2SkillStore(process.env.LD_SDK_KEY!).start();
if (!(await store.waitForSkills({ timeoutSeconds: 10 }))) {
Expand Down Expand Up @@ -463,7 +465,7 @@ The line is a `[LaunchDarkly] ` prefix, the event name, a space, and one JSON ob

**The record is written regardless of telemetry configuration.** It is not sampled, batched, or dependent on a LaunchDarkly connection. If you send LaunchDarkly nothing, this record is your complete detection surface for tampered or malformed skill content.

**Changes to `ld.skills.integrity_failure` are always announced.** Agent Skills is experimental, so like every name in this section the event name and its fields may change in a minor release. Any rename or removal gets a changelog entry under **Experimental**, so check the changelog before upgrading if you alert on this record. Once Agent Skills is promoted out of experimental, they change only in a major release.
**Changes to `ld.skills.integrity_failure` are always announced.** The event name and its fields are public API like every name in this section: a rename or removal is a breaking change with a changelog entry, so check the changelog before upgrading if you alert on this record.

| Field | Always present | Value |
|---|---|---|
Expand Down Expand Up @@ -507,7 +509,7 @@ These ten tokens are the whole vocabulary. The Python SDK emits the same ten for
The log record is for operators; `getSkillResult` is for your application. It runs the same retrieval and verification as `getSkill`, but reports which of five outcomes happened instead of collapsing them all to `null`.

```ts
import { getSkillResult } from '@launchdarkly/ai-server/experimental';
import { getSkillResult } from '@launchdarkly/ai-server';

const outcome = await getSkillResult('pdf-extraction', { version: 2 });

Expand Down
Loading
Loading