Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
140 commits
Select commit Hold shift + click to select a range
3c6cc27
feat(client): Agent Skills value types
XieX Aug 25, 2026
98c9853
docs(client): phrase skill vocabularies as cross-SDK, not Python-spec…
XieX Sep 16, 2026
11d86f9
feat(client): symlink-refusing filesystem primitives
XieX Aug 25, 2026
3db4993
docs(client): trim safe-fs comments and lower TEMP_NAME_ATTEMPTS
XieX Sep 16, 2026
358cfd4
feat(client): Agent Skills store seam, verification, and accessors
XieX Aug 25, 2026
8d4bad2
fix(client): do not reject skill content that begins with a BOM
XieX Sep 16, 2026
977f876
test(client): integrity verification and the accessor telemetry sweep
XieX Aug 25, 2026
8644e92
test(client): cover BOM-prefixed content, and drop Python from comments
XieX Sep 16, 2026
be42090
feat(client): writeSkills materialization and the abuse matrix
XieX Aug 25, 2026
9124525
fix(client): read the manifest through readRegularFile
XieX Sep 15, 2026
afbd8f0
docs(client): sweep skills-fs comments
XieX Sep 16, 2026
d0bbfd7
test(client): the rest of the writeSkills suite
XieX Aug 25, 2026
1c2c602
test(client): describe the manifest format without naming Python
XieX Sep 16, 2026
50721ca
docs(client): document Agent Skills
XieX Aug 25, 2026
c854554
docs(client): Agent Skills — close out the security review's SDK-side…
XieX Sep 4, 2026
6e2ea64
fix(client): a broken store answer is not an absent skill
XieX Sep 11, 2026
67cda6f
test(client): a broken store answer never deletes managed files
XieX Sep 11, 2026
6279f4a
test(client): Agent Skills — the root-swap races the pinned handle do…
pkaeding Sep 4, 2026
4c40603
test(client): let the root-swap races fail outright
pkaeding Sep 4, 2026
0479a61
fix(client): pin the skills root through /proc/self/fd on Linux
XieX Sep 4, 2026
19acd23
docs(client): sweep review-closeout comments
XieX Sep 16, 2026
6197fe3
feat(client): Agent Skills — the FDv2 delivery transport
XieX Sep 4, 2026
7b560b8
fix(client): Agent Skills — read the skill's version off the wire key
XieX Sep 11, 2026
9407ddc
feat(client): Agent Skills — name the payload a transfer completed
XieX Sep 11, 2026
3aa45a8
fix(client): Agent Skills — keep FDv2 delivery alive through the fail…
XieX Sep 11, 2026
d4430b9
feat(client): Agent Skills — removeListener, and a no-store message t…
XieX Sep 11, 2026
ede868a
docs(client): Agent Skills — describe the FDv2 wire contract as it ships
XieX Sep 11, 2026
4a5de2b
fix(client): four delivery defects from the Bugbot review
XieX Sep 15, 2026
44ef4e2
docs(client): Agent Skills — sweep delivery-transport comments for a …
XieX Sep 15, 2026
b73192e
docs(client): Agent Skills — finish the public-SDK comment sweep
XieX Sep 15, 2026
0a0e847
fix(client): Agent Skills — do not expire an unchanging environment
XieX Sep 15, 2026
a92ee61
fix(client): Agent Skills — resolve waitForSkills false when delivery…
XieX Sep 15, 2026
fba8cb8
fix(client): Agent Skills — clear the SSE data buffer on every block
XieX Sep 15, 2026
c4e9d0a
fix(client): Agent Skills — bound the retry loop a goodbye cannot escape
XieX Sep 15, 2026
65dbcae
fix(client): Agent Skills — report revocations and hashless payloads …
XieX Sep 15, 2026
3ebc7ac
fix(client): Agent Skills — tighten the store's listener and wait lif…
XieX Sep 15, 2026
7106c4d
fix(client): Agent Skills — stream from stream.launchdarkly.com by de…
XieX Sep 15, 2026
f13fc19
docs(client): Agent Skills — bring the transport contract notes up to…
XieX Sep 15, 2026
47cdc18
docs(client): sweep transport comments and correct hashlessObjects
XieX Sep 16, 2026
937131d
fix(client)!: Agent Skills — three contract fixes and the tests that …
XieX Sep 16, 2026
8c5dba4
fix(client)!: Agent Skills — the multi-version store model
XieX Sep 16, 2026
2d5712d
test(client): the rest of the writeSkills suite (#34)
XieX Sep 18, 2026
bfb40f6
docs(client): document Agent Skills
XieX Aug 25, 2026
8807c40
fix(client): stop reporting a withholding for a key that resolved
XieX Sep 18, 2026
432d75c
fix(client)!: Agent Skills — narrow the filesystem destructive and Do…
XieX Sep 16, 2026
f74c412
fix(client)!: Agent Skills — test integrity, and the two guards the t…
XieX Sep 16, 2026
49db83b
docs(client): regenerate the export block and correct the platform note
XieX Sep 17, 2026
7bfa88e
fix(client)!: Agent Skills — close the readiness and attribution prun…
XieX Sep 30, 2026
43ed093
test(client): pin the key-mismatch detection surface (failing)
XieX Sep 17, 2026
75df51f
fix(client): record the key mismatch on the log surface
XieX Sep 17, 2026
191fcdf
fix(client): Agent Skills — align the FDv2 protocol reader with the spec
XieX Sep 30, 2026
288e302
fix(client): keep the stale-selector repair out of the retry budget
XieX Sep 17, 2026
860b759
fix(client): pair the poll etag with the basis it was issued against
XieX Sep 18, 2026
bd5e4da
fix(client): Agent Skills — bound the SSE decoder per event
XieX Sep 30, 2026
37fccfd
fix(client): bound the transport per response, not per read
XieX Sep 18, 2026
ae858c9
fix(client): refuse cleartext base URIs and redirects in FDv2SkillStore
XieX Sep 22, 2026
6baa20b
fix(client): Agent Skills — watcher, safe-fs and BYOC robustness
XieX Sep 30, 2026
3b8dcd0
feat(client): declare ?kinds=agent-skill, and treat a 422 as "no skil…
XieX Sep 24, 2026
ec7fc2f
docs(client): say the 422's cause once, on the type that names it
XieX Sep 24, 2026
790ef3c
test(client): stop the hashless-reconcile case racing its own second …
XieX Sep 24, 2026
df337a8
fix(client): make HTTP 422 a fatal transport failure
XieX Sep 29, 2026
3eea7e0
Update skills-fdv2.ts
XieX Sep 29, 2026
ea02aec
Update agents.md
XieX Sep 29, 2026
cc86953
docs(client): correct what the kinds declaration made stale
XieX Sep 28, 2026
78659d8
docs(client): finish correcting what the kinds declaration made stale
XieX Sep 29, 2026
21c67e0
Update skills-fdv2.test.ts
XieX Sep 29, 2026
e707d0d
fix(client): record the version mismatch on the log surface
XieX Sep 28, 2026
068143f
docs(client): stop giving a shared connection as the reason for the skip
XieX Sep 28, 2026
91942c5
fix(client): shape-check served_version, matching the Python SDK
XieX Sep 30, 2026
9928c9d
fix(client): reject a non-finite writeSkills timeout
XieX Sep 30, 2026
9efb571
feat(client): Agent Skills — the FDv2 delivery transport (#50)
XieX Sep 30, 2026
32fc8f7
test(client): pin the content-hash rule to the service's own digests
XieX Sep 30, 2026
14a0b0e
docs(client): document Agent Skills (#35)
XieX Sep 30, 2026
6b750a8
test(client): pin the adoption comparison to the same content-hash di…
XieX Sep 30, 2026
6d5dbaa
Merge branch 'xie/agent-skills-feature-ac9ac7' into xie/skills-08-rev…
XieX Sep 30, 2026
b1d2a36
fix(client): Agent Skills — contract fixes, the multi-version store, …
XieX Oct 1, 2026
9151d4a
feat(client): declare ?kinds=agent-skill, and make a 422 a fatal tran…
XieX Oct 1, 2026
a9b9c01
fix(client): Agent Skills — watcher, safe-fs and BYOC robustness (#98)
XieX Oct 1, 2026
439f637
fix(client): Agent Skills — bound the SSE decoder, refuse cleartext U…
XieX Oct 1, 2026
b99e564
fix(client): Agent Skills — align the FDv2 protocol reader with the s…
XieX Oct 1, 2026
82d8dd7
fix(client)!: Agent Skills — close the readiness and attribution prun…
XieX Oct 1, 2026
40101a7
docs(client): Agent Skills — close out the security review's SDK-side…
XieX Oct 1, 2026
57c3b25
Merge branch 'main' into xie/agent-skills-feature-ac9ac7
XieX Oct 1, 2026
7bbecd7
fix(client): a 304 confirms a payload rather than establishing one
XieX Oct 1, 2026
43bfbc8
docs(client): trim the 304/etag comments to what a consumer needs
XieX Oct 2, 2026
8e497ed
fix(skills): install initClient's skillStore only once the client res…
XieX Oct 2, 2026
4cd0588
docs(client): tighten Agent Skills comments and docstrings
XieX Oct 2, 2026
86265f3
docs(client): trim the Agent Skills README and agents.md sections
XieX Oct 2, 2026
bfb6996
fix(skills): close four behaviour gaps with the Python SDK
XieX Oct 2, 2026
5cd5e53
fix(client): apply skill objects that follow a none intent
XieX Oct 2, 2026
c1b3af1
fix(client): retry recoverable skill-delivery failures indefinitely
XieX Oct 2, 2026
9f0b870
docs(client): scope on-disk revocation claims to the '*' form
XieX Oct 2, 2026
be65e93
fix(client): validate the skill store's backoff options
XieX Oct 2, 2026
fdc0d81
fix(client): reset the skill store's backoff only after a stream stay…
XieX Oct 2, 2026
232396d
fix(client): stop skill delivery on a response over the memory bound
XieX Oct 2, 2026
433cc02
fix(client): log a routine goodbye recycle at debug, not warn
XieX Oct 2, 2026
29711fe
docs(client): stop tying the failure counter to the backoff in comments
XieX Oct 2, 2026
1c8b75c
test(client): drop the zero-base backoff case the constructor now ref…
XieX Oct 5, 2026
76c300c
fix(skills): count only served skills in the getSkills withholding wa…
XieX Oct 5, 2026
2709c66
fix(client): apply changes after a none intent, and retry recoverable…
XieX Oct 5, 2026
6a7194a
Merge xie/agent-skills-feature-ac9ac7 into xie/skills-js-parity-gaps
XieX Oct 5, 2026
fdb2b54
Merge branch 'xie/agent-skills-feature-ac9ac7' into xie/skills-304-fi…
XieX Oct 5, 2026
44a3cf3
test(client): assert a declined foreign payload is not a commit
XieX Oct 5, 2026
5ae1109
fix(client): a 304 confirms a payload rather than establishing one (#…
XieX Oct 5, 2026
e071aef
Fixup
XieX Oct 5, 2026
7fc790a
fix(skills): close four behaviour gaps with the Python SDK (#107)
XieX Oct 5, 2026
7b2a51e
Merge branch 'xie/agent-skills-feature-ac9ac7' into xie/initclient-st…
XieX Oct 5, 2026
50aaa8d
fix(lifecycle): let a failed initClient be retried
XieX Oct 5, 2026
3aed185
fix(lifecycle): release the OTel globals this SDK registered, and onl…
XieX Oct 5, 2026
7951f2b
fix(client): make a repeat BYOC initClient return the existing client
XieX Oct 5, 2026
52c1d0f
fix(lifecycle): an init abandoned by shutdown() must not adopt its cl…
XieX Oct 5, 2026
f378cbb
docs(client): say a restarted store's waitForSkills waits again
XieX Oct 6, 2026
bfb5bbd
test(client): write the adoption hazard vector's é as an escape
XieX Oct 6, 2026
0f1ddf4
Merge xie/initclient-store-timing into xie/fix-byoc-telemetry-idempot…
XieX Oct 6, 2026
6720f6f
fix(lifecycle): build no provider for an init abandoned during teleme…
XieX Oct 6, 2026
ff01947
feat(client): publish Agent Skills from the experimental entry point
XieX Oct 6, 2026
1afc2e0
Merge xie/initclient-store-timing into xie/fix-byoc-telemetry-idempot…
XieX Oct 6, 2026
31c29fa
fix(client): keep experimental code out of the root and add ai-node/e…
XieX Oct 6, 2026
5f3779b
fix(client): warn generically about unrecognized initClient options
XieX Oct 6, 2026
865b2ec
refactor(lifecycle): leave skillStore timing to the experimental setter
XieX Oct 6, 2026
13408c1
fix(client): resolve /experimental types under node10 and address review
XieX Oct 6, 2026
800a4fc
refactor(client): prefix the Agent Skills defaults with SKILLS_
XieX Oct 6, 2026
42968ff
fix(client): validate skills in skillRefs, not in parseAiConfig
XieX Oct 7, 2026
d8e47b0
fix(client): validate skills in skillRefs, not in parseAiConfig (#117)
XieX Oct 7, 2026
f2f9f51
docs(client): stop the skills quickstart before writeSkills when no c…
XieX Oct 7, 2026
0a9baaf
test(client): pin the Agent Skills content-hash rule to the service's…
XieX Oct 7, 2026
568bd10
Merge remote-tracking branch 'origin/xie/initclient-store-timing' int…
XieX Oct 7, 2026
53a763f
feat(client): publish Agent Skills from the experimental entry point …
XieX Oct 7, 2026
d457477
docs(client): say a restarted store's waitForSkills waits again (#113)
XieX Oct 7, 2026
f407947
Merge remote-tracking branch 'origin/xie/agent-skills-feature-ac9ac7'…
XieX Oct 7, 2026
6575893
docs(client): drop shutdown()'s early return from agents.md
XieX Oct 7, 2026
38c321e
Merge remote-tracking branch 'origin/xie/initclient-store-timing' int…
XieX Oct 7, 2026
cf49bd7
fix(client): make a repeat BYOC initClient return the existing client…
XieX Oct 7, 2026
4de1cd7
fix(lifecycle): retry a failed initClient cleanly, and release the OT…
XieX Oct 7, 2026
bcca880
Merge branch 'main' into xie/agent-skills-feature-ac9ac7
XieX Oct 7, 2026
c27005c
test(client): add main's model-parameter helpers to the root surface
XieX Oct 7, 2026
93ded06
fix(skills): pre-release review must-fix findings (#120)
XieX Oct 8, 2026
90ead9d
feat(client)!: Agent Skills — take FDv2SkillStore time options in sec…
XieX Oct 9, 2026
f3bdfe3
Merge origin/main into xie/agent-skills-feature-ac9ac7
XieX Oct 9, 2026
960ecc3
fix(skills): set per-skill directory mode to 0755 through the pinned …
XieX Oct 9, 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
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@ jobs:
- name: Type check all packages
run: yarn typecheck

# Fails when a public name changes in either entry point of
# @launchdarkly/ai-server without the checked-in report under
# packages/client/etc/ being updated (yarn workspace @launchdarkly/ai-server api:update).
- name: Check client API reports
run: yarn workspace @launchdarkly/ai-server api:check

# ─── Test ───────────────────────────────────────────────────────────────────
test:
name: Tests
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,6 @@ spec.md
tmp/
integration-tests/**/.staging/

.claude/
.claude/
# API Extractor working files
packages/client/temp/
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,11 +164,11 @@ The client manages a singleton connection to LaunchDarkly and the associated tel
| Export | Description |
|---|---|
| `initClient(options?)` | Auto-discovers and initializes `@launchdarkly/node-server-sdk` (optional peer dep, loaded via dynamic import). Optional — the first AI API call triggers lazy init when `LD_SDK_KEY` is set. Accepts optional overrides for SDK key, base URIs, service name, environment, and OTLP endpoint. Returns `Promise<LDClientInterface>`. On every successful path, including the already-initialized path, flushes `$ld:ai:sdk:info` for any LaunchDarkly AI packages that have not yet reported. |
| `initClient(client)` | **BYOC overload** — accepts a pre-initialized `LDClientInterface` (e.g. from `@launchdarkly/vercel-server-sdk`). Stores it directly without calling the node SDK. Flushes pending `$ld:ai:sdk:info` events. |
| `initClient(client, options?)` | **BYOC overload** — accepts a pre-initialized `LDClientInterface` (e.g. from `@launchdarkly/vercel-server-sdk`). Stores it directly without calling the node SDK. Flushes pending `$ld:ai:sdk:info` events. The optional second argument is the same options bag as the other overload. Idempotent like that overload: once a client is set, a second call returns it and ignores both a different client and the new telemetry options — `shutdown()` first to swap clients. |
| `getClient()` | Returns the initialized `LDClientInterface`. Throws if initialization has not completed. |
| `shutdown()` | Flushes all pending events and telemetry, then closes the client. Must be called before the process exits. Clears sdk-info reporting so a later client reports again. |
| `waitForTelemetry()` | Waits for the OTel provider to be ready. Useful when spans must not be dropped at startup. |
| `shutdownTelemetry()` | Flushes and stops the OTel exporter independently of the LD client. |
| `shutdownTelemetry()` | Flushes and stops the OTel exporter independently of the LD client, and releases the OTel globals the SDK registered. |
| `registerAiSdkPackage(name, version)` | Records a LaunchDarkly AI package identity. Handler and convenience packages call this at import time. |

### Core Data Types
Expand Down
66 changes: 65 additions & 1 deletion integration-tests/commonjs-webpack/commonjs-webpack.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { beforeAll, describe, expect, it } from 'vitest';
import { bundle, createConsumer, invoke, packedFiles, repoRoot, runEntrypoint } from './harness.js';
import {
bundle,
consumerDir,
createConsumer,
invoke,
packedFiles,
RESOLUTION_MODES,
type ResolutionMode,
repoRoot,
runEntrypoint,
typecheck,
} from './harness.js';

const SETUP_TIMEOUT_MS = 15 * 60 * 1000;
const CASE_TIMEOUT_MS = 5 * 60 * 1000;
Expand Down Expand Up @@ -37,7 +48,12 @@ const OPTIONAL_DEPS: Record<string, string[]> = {
const RESOLVED_SYMBOLS = {
aiNodeConfig: 'function',
aiNodeInitClient: 'function',
aiNodeExperimentalGetSkill: 'function',
aiNodeRootGetSkill: 'undefined',
aiServerConfig: 'function',
// The experimental subpath resolves, and its names stay off the root.
aiServerExperimentalGetSkill: 'function',
aiServerRootGetSkill: 'undefined',
openaiMessages: 'function',
langchainMessages: 'function',
};
Expand Down Expand Up @@ -109,6 +125,31 @@ describe('CommonJS consumer bundled with Webpack + webpack-node-externals', () =
CASE_TIMEOUT_MS,
);

// `node10` ignores `exports`, so the experimental subpath's types resolve only through
// `typesVersions`; without it the import fails with TS2307.
it.each(Object.keys(RESOLUTION_MODES) as ResolutionMode[])(
'type-checks root and experimental imports under moduleResolution %s',
(mode) => {
const result = typecheck(mode);

expect(result.stderr).toBe('');
expect(result.status, result.stdout).toBe(0);
// A module tsc cannot resolve in the consumer is looked up again in every ancestor
// `node_modules`, and the consumer sits inside this repo, so a failed lookup would land on
// the workspace copy and pass. Every file loaded from the repo must be the consumer's own.
const loaded = result.stdout.split('\n').filter((file) => file.startsWith(repoRoot));
expect(loaded.filter((file) => !file.startsWith(`${consumerDir}/`))).toEqual([]);
for (const pkg of ['ai-server', 'ai-node']) {
const experimental = join(consumerDir, 'node_modules', '@launchdarkly', pkg, 'dist', 'experimental.d.');
expect(
loaded.some((file) => file.startsWith(experimental)),
pkg,
).toBe(true);
}
},
CASE_TIMEOUT_MS,
);

it.each([...Object.keys(OPTIONAL_DEPS)])('keeps %s optional dependencies behind a dynamic import in CJS', (pkg) => {
const cjs = readFileSync(join(repoRoot, 'packages', pkg, 'dist', 'index.cjs'), 'utf8');

Expand All @@ -129,6 +170,20 @@ describe('CommonJS consumer bundled with Webpack + webpack-node-externals', () =
expect.arrayContaining(['dist/index.js', 'dist/index.cjs', 'dist/index.d.ts', 'dist/index.d.cts']),
);
});

it.each([
'client',
'ai-node',
])('packs the experimental entry point in both runtime and declaration formats for %s', (workspace) => {
expect(packedFiles.get(workspace)).toEqual(
expect.arrayContaining([
'dist/experimental.js',
'dist/experimental.cjs',
'dist/experimental.d.ts',
'dist/experimental.d.cts',
]),
);
});
});

describe('package manifests', () => {
Expand All @@ -143,4 +198,13 @@ describe('package manifests', () => {
expect(manifest.module).toBe('dist/index.js');
expect(manifest.types).toBe('dist/index.d.ts');
});

it.each(['client', 'ai-node'])('%s declares the experimental subpath with both conditions', (pkg) => {
const manifest = JSON.parse(readFileSync(join(repoRoot, 'packages', pkg, 'package.json'), 'utf8'));

expect(manifest.exports['./experimental']).toEqual({
import: { types: './dist/experimental.d.ts', default: './dist/experimental.js' },
require: { types: './dist/experimental.d.cts', default: './dist/experimental.cjs' },
});
});
});
6 changes: 6 additions & 0 deletions integration-tests/commonjs-webpack/fixture/entrypoints.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

// Loads every installed package root through `require` and reports what resolved.
const aiNode = require('@launchdarkly/ai-node');
const aiNodeExperimental = require('@launchdarkly/ai-node/experimental');
const aiServer = require('@launchdarkly/ai-server');
const aiServerExperimental = require('@launchdarkly/ai-server/experimental');
const openaiMessages = require('@launchdarkly/ai-openai-messages');
const langchainMessages = require('@launchdarkly/ai-langchain-messages');

Expand All @@ -12,7 +14,11 @@ process.stdout.write(
JSON.stringify({
aiNodeConfig: typeof aiNode.config,
aiNodeInitClient: typeof aiNode.initClient,
aiNodeExperimentalGetSkill: typeof aiNodeExperimental.getSkill,
aiNodeRootGetSkill: typeof aiNode.getSkill,
aiServerConfig: typeof aiServer.config,
aiServerExperimentalGetSkill: typeof aiServerExperimental.getSkill,
aiServerRootGetSkill: typeof aiServer.getSkill,
openaiMessages: typeof openaiMessages.openaiMessages,
langchainMessages: typeof langchainMessages.langchainMessages,
}),
Expand Down
6 changes: 6 additions & 0 deletions integration-tests/commonjs-webpack/fixture/entrypoints.mjs
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
// ESM twin of entrypoints.cjs — same package roots, resolved through the `import` condition.
import * as aiNode from '@launchdarkly/ai-node';
import * as aiNodeExperimental from '@launchdarkly/ai-node/experimental';
import * as langchainMessages from '@launchdarkly/ai-langchain-messages';
import * as openaiMessages from '@launchdarkly/ai-openai-messages';
import * as aiServer from '@launchdarkly/ai-server';
import * as aiServerExperimental from '@launchdarkly/ai-server/experimental';

await import('@launchdarkly/ai-otel');

process.stdout.write(
JSON.stringify({
aiNodeConfig: typeof aiNode.config,
aiNodeInitClient: typeof aiNode.initClient,
aiNodeExperimentalGetSkill: typeof aiNodeExperimental.getSkill,
aiNodeRootGetSkill: typeof aiNode.getSkill,
aiServerConfig: typeof aiServer.config,
aiServerExperimentalGetSkill: typeof aiServerExperimental.getSkill,
aiServerRootGetSkill: typeof aiServer.getSkill,
openaiMessages: typeof openaiMessages.openaiMessages,
langchainMessages: typeof langchainMessages.langchainMessages,
}),
Expand Down
15 changes: 15 additions & 0 deletions integration-tests/commonjs-webpack/fixture/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// 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 * as server from '@launchdarkly/ai-server';
import { InMemorySkillStore, type SkillStore, setSkillStore } 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;

export const used = [config, initClient, server.config];
41 changes: 40 additions & 1 deletion integration-tests/commonjs-webpack/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ const WORKSPACES = ['client', 'ai-node', 'ai-otel', 'openai-messages', 'langchai
/** Pinned to the versions reported in AIC-3370. */
const BUNDLER_DEPS = ['webpack@5.104.1', 'webpack-cli@6.0.1', 'webpack-node-externals@3.0.0'];

/** Type-checks the consumer's imports (see `typecheck`). */
const TYPESCRIPT_DEP = 'typescript@5.9.3';

export type Variant = 'externalized' | 'allowlisted';

/** Workspace name -> paths inside the tarball `npm publish` would upload. */
Expand Down Expand Up @@ -92,7 +95,7 @@ export function createConsumer(): string {
);
cpSync(join(here, 'fixture'), consumerDir, { recursive: true });

runOrThrow('npm', ['install', '--no-audit', '--no-fund', ...tarballs, ...BUNDLER_DEPS], consumerDir);
runOrThrow('npm', ['install', '--no-audit', '--no-fund', ...tarballs, ...BUNDLER_DEPS, TYPESCRIPT_DEP], consumerDir);

return consumerDir;
}
Expand All @@ -107,6 +110,42 @@ export function invoke(variant: Variant): RunResult {
return run('node', ['invoke.cjs', `./dist-${variant}/handler.js`], consumerDir);
}

/** `moduleResolution` modes paired with the `module` setting each requires. */
export const RESOLUTION_MODES = {
node10: 'commonjs',
node16: 'node16',
bundler: 'esnext',
} as const;

export type ResolutionMode = keyof typeof RESOLUTION_MODES;

/**
* Type-checks `types.ts` against the installed tarballs' declarations. Runs the consumer's own
* `tsc` rather than `npx`, whose npm config warnings would land in stderr. `--listFiles` prints
* every file the program loaded, so a caller can check where each declaration came from.
*/
export function typecheck(mode: ResolutionMode): RunResult {
return run(
'node',
[
join('node_modules', 'typescript', 'bin', 'tsc'),
'--noEmit',
'--listFiles',
// Only the consumer's own `@types`, not those of every ancestor `node_modules`.
'--typeRoots',
join('node_modules', '@types'),
'--strict',
'--skipLibCheck',
'--module',
RESOLUTION_MODES[mode],
'--moduleResolution',
mode,
'types.ts',
],
consumerDir,
);
}

/** Runs an unbundled entry point against the installed tarballs. */
export function runEntrypoint(file: string): RunResult {
return run('node', [file], consumerDir);
Expand Down
13 changes: 13 additions & 0 deletions packages/ai-node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,19 @@ module.exports = {
};
```

## Experimental features

Experimental features are not exported from the package root. Import them from
`@launchdarkly/ai-node/experimental`, which re-exports `@launchdarkly/ai-server/experimental`, so
you need no direct dependency on `@launchdarkly/ai-server`. For example:

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

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

## Full API reference

See [`@launchdarkly/ai-server`](../client/README.md) — all exports are re-exported unchanged from this package.
3 changes: 2 additions & 1 deletion packages/ai-node/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +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 |

---

Expand All @@ -32,7 +33,7 @@ This package re-exports everything from `@launchdarkly/ai-server` and nothing el
export * from '@launchdarkly/ai-server';
```

Every symbol available from `@launchdarkly/ai-server` is available from `@launchdarkly/ai-node` under the same name. No additional symbols are added. When `@launchdarkly/ai-server` gains a new export, this package automatically picks it up.
Every symbol available from `@launchdarkly/ai-server` is available from `@launchdarkly/ai-node` under the same name, and every symbol from `@launchdarkly/ai-server/experimental` is available from `@launchdarkly/ai-node/experimental`. No additional symbols are added. When `@launchdarkly/ai-server` gains a new export, this package automatically picks it up.

---

Expand Down
17 changes: 17 additions & 0 deletions packages/ai-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@
"main": "dist/index.cjs",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"typesVersions": {
"*": {
"experimental": [
"dist/experimental.d.ts"
]
}
},
"exports": {
".": {
"import": {
Expand All @@ -37,6 +44,16 @@
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
},
"./experimental": {
"import": {
"types": "./dist/experimental.d.ts",
"default": "./dist/experimental.js"
},
"require": {
"types": "./dist/experimental.d.cts",
"default": "./dist/experimental.cjs"
}
}
},
"files": [
Expand Down
18 changes: 18 additions & 0 deletions packages/ai-node/src/__tests__/reexports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,21 @@ describe('@launchdarkly/ai-node re-exports', () => {
}
});
});

describe('@launchdarkly/ai-node/experimental re-exports', () => {
it('exports every named export from @launchdarkly/ai-server/experimental, as the same references', async () => {
const serverExperimental = await import('@launchdarkly/ai-server/experimental');
const nodeExperimental = await import('../experimental.js');

const serverKeys = Object.keys(serverExperimental);
expect(serverKeys.length).toBeGreaterThan(0);
for (const key of serverKeys) {
expect((nodeExperimental as any)[key], key).toBe((serverExperimental as any)[key]);
}
});

it('keeps experimental names off the package root', () => {
expect('getSkill' in node).toBe(false);
expect('setSkillStore' in node).toBe(false);
});
});
17 changes: 17 additions & 0 deletions packages/ai-node/src/experimental.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Experimental features of the LaunchDarkly AI SDK for Node.js.
*
* @launchdarkly/ai-node/experimental
*
* Re-exports `@launchdarkly/ai-server/experimental`, so a Node.js application
* that installs only this package can reach experimental features without a
* direct dependency on `@launchdarkly/ai-server` (required under strict
* package managers such as pnpm and Yarn PnP). Every name here may change in a
* minor release, and none is exported from the package root.
*/
import { registerAiSdkPackage } from '@launchdarkly/ai-server';
import { LD_AI_PACKAGE_NAME, LD_AI_PACKAGE_VERSION } from './version.js';

registerAiSdkPackage(LD_AI_PACKAGE_NAME, LD_AI_PACKAGE_VERSION);

export * from '@launchdarkly/ai-server/experimental';
11 changes: 10 additions & 1 deletion packages/ai-node/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@
export { default } from '../../tsup.config.base';
import { defineConfig } from 'tsup';
import base from '../../tsup.config.base';

// Two entry points, mirroring @launchdarkly/ai-server: the package root and
// `@launchdarkly/ai-node/experimental`. Both re-export from external packages,
// so there is nothing to share between them.
export default defineConfig({
...base,
entry: { index: 'src/index.ts', experimental: 'src/experimental.ts' },
});
Loading
Loading