Skip to content

feat(build): build the AI SDK like the other LaunchDarkly JS SDKs so CommonJS apps can load it (2/4) - #77

Merged
tonytrinh3 merged 3 commits into
mainfrom
tonytrinh3/AIC-3415-dual-esm-cjs-output
Sep 23, 2026
Merged

tonytrinh3 merged 3 commits into
mainfrom
tonytrinh3/AIC-3415-dual-esm-cjs-output

Conversation

@tonytrinh3

@tonytrinh3 tonytrinh3 commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Plain version: The other LaunchDarkly JavaScript SDKs already ship in two module formats. This AI SDK did not. It shipped ESM only, so old CommonJS projects crashed when they loaded it. This PR gives the AI SDK the same two-format build, so those projects work.

What it means for the code (junior eng): The other JavaScript SDKs live in launchdarkly/js-core. They build with tsup and publish both an import condition and a require condition. The AI SDK was the outlier with no require condition. Now every @launchdarkly/ai-* package builds with tsup and emits dist/index.js (ESM) and dist/index.cjs (CommonJS). So require('@launchdarkly/ai-node') works. A Webpack bundle that keeps the packages external also works. Node no longer throws ERR_PACKAGE_PATH_NOT_EXPORTED.

Closes AIC-3415. This fixes the failure in AIC-3370.

Which AIC-3370 asks this PR answers

Plain version: This PR answers ask 1 in full. It also adds a few rows to the support table in the root README, which is part of ask 4. The other asks belong to other PRs.

What it means for the code (junior eng): Ask 1 wants a CommonJS-compatible conditional export. The evidence sits in this diff.

  • Nine packages now build both formats: client, ai-node, ai-otel, claude-agents, claude-messages, openai-agents, openai-messages, langchain-agents, and langchain-messages.
  • Each exports map gains a require branch:
".": {
  "import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
  "require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }
}
  • In integration-tests/commonjs-webpack/commonjs-webpack.test.ts, the externalized case flips from an expected failure to an expected success. The suite also adds a require() consumer and an import consumer that run from packed tarballs on Node 22 and Node 24.

Ask 4 wants a support matrix. PR #76 owns that matrix. This PR only updates its rows in the root README, because five load paths now work instead of three. It adds no new bundler and no new row.

Asks 2 and 4 belong to PR #76. Ask 3 belongs to PR #76 plus PR #79. Ask 5 belongs to PR #78.

Changes

  • This PR adopts the build that js-core already uses. 11 js-core packages build with tsup, and 23 of its 32 packages publish a require condition. The AI SDK published none, so it was the outlier.
  • Nine packages move from tsc to tsup-node. They share one config at tsup.config.base.ts.
  • Each exports map gets a require condition. It points to dist/index.cjs and dist/index.d.cts. The fields main, module, and types stay aligned for old tools.
  • The externalized test case from PR1 now expects success. The suite also adds require() and import consumers, tarball checks, and manifest checks.
  • The CJS output keeps optional dependencies behind a runtime import(). So users still choose to install OpenTelemetry, LangChain providers, and @launchdarkly/node-server-sdk.
  • The integration job in CI now runs on Node 22 and Node 24.
Implementation details

The build

Each package holds one line of config:

export { default } from '../../tsup.config.base';

The shared config is small:

// tsup.config.base.ts
export default defineConfig({
  entry: { index: 'src/index.ts' },
  format: ['esm', 'cjs'],
  splitting: false,
  sourcemap: true,
  clean: true,
  dts: true,
});

The scripts prepare, build, and build:watch call tsup-node.

Consistency with js-core

This PR adds no new pattern. It copies the current js-core build.

  • The config in packages/sdk/server-ai/tsup.config.ts matches the config here field for field: the same entry, the same two formats, splitting: false, sourcemaps, clean output, and declarations.
  • The manifest shape matches too. The field main points to the .cjs file. The field module points to the .js file. The exports map holds a require branch and an import branch, and each branch names its own declaration file.
  • Two cosmetic differences remain. js-core lists require before import. Our legacy fields omit the leading ./. Node matches conditions by name and not by order, so neither difference changes behavior.

Bundler coverage

CI proves one bundler: Webpack 5 with webpack-node-externals. That tool produced the incident, so it comes first. A require condition is the standard mechanism, so esbuild, Rollup, and Vite should now work too. I did not test them, so the README does not claim them.

Optional dependencies

esbuild keeps await import('...') of an external package as a real dynamic import in the CJS file. A static require would force users to install every optional package. The test suite reads each dist/index.cjs. It asserts the file contains import("@launchdarkly/node-server-sdk"). It asserts the file does not contain require("@launchdarkly/node-server-sdk").

TypeScript project layout

The declaration build reads files outside the old glob src/*.ts. That broke the composite project build with error TS6307. Each package tsconfig.json drops composite. A new file tsconfig.ref.json holds composite: true and include: ["src/**/*"]. The root tsconfig.json points at those files. The root build and clean scripts now call workspace scripts. The build runs @launchdarkly/ai-server first, because the handler packages read types from its dist folder.

Alternatives that I rejected

A second tsc pass plus a dist/package.json shim adds more moving parts. It also produces no .d.cts file. A custom esbuild wrapper repeats what tsup already does. js-core already uses tsup.

Scope

I changed no source imports and no runtime logic. The package @launchdarkly/ai-node stays a pure barrel. I updated the READMEs, because CommonJS is now supported.

Testing

These commands pass: yarn build, yarn typecheck, yarn code:check, yarn lint:pkg, and yarn test. I ran npm pack --dry-run for all nine packages. Each tarball holds dist/index.js, dist/index.cjs, dist/index.d.ts, and dist/index.d.cts. The command yarn test:integration passes 21 of 21 tests on Node 22.23.2 and on Node 24.16.0. That run covers the externalized case, the allowlisted case, and the require and import consumers.

Link to Devin session: https://app.devin.ai/sessions/c730a69bf9534f648dd5cb5f1db6a28b
Open in Devin Desktop: https://app.devin.ai/desktop/session/c730a69bf9534f648dd5cb5f1db6a28b?variant=devin
Requested by: @tonytrinh3


Note

Overview
Adds CommonJS publishing across the AI SDK so require(), Webpack externalized bundles, and plain CJS/ESM consumers resolve published packages instead of hitting ERR_PACKAGE_PATH_NOT_EXPORTED (AIC-3370).

All nine publishable workspaces switch from tsc to shared tsup (tsup.config.base.ts), emitting dist/index.js + dist/index.cjs and matching index.d.ts / index.d.cts. Each package.json gains a require branch in exports and sets main to the CJS file. Root build/clean run workspace scripts ( @launchdarkly/ai-server first ); TypeScript composite moves to tsconfig.ref.json per package.

Integration tests and CI flip the Webpack externalized case to expect success, add entrypoints.cjs / entrypoints.mjs, tarball/manifest checks, and assertions that optional peers stay on runtime import() in CJS. The CommonJS/Webpack job runs on Node 22 and 24. READMEs document dual-format support and updated load-path matrix.

Reviewed by Cursor Bugbot for commit 9bb4956. Bugbot is set up for automated code reviews on this repo. Configure here.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@devin-ai-integration

Copy link
Copy Markdown
Contributor

@cursor review

@devin-ai-integration devin-ai-integration Bot left a comment

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.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Devin Review

@devin-ai-integration devin-ai-integration Bot changed the title feat(build): dual ESM/CommonJS output via tsup feat(build): dual ESM/CommonJS output via tsup (2/4) Sep 22, 2026
…pack-compatibility-contract' into tonytrinh3/AIC-3415-dual-esm-cjs-output

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

# Conflicts:
#	README.md
@tonytrinh3
tonytrinh3 requested a review from atornsii September 22, 2026 14:08
@tonytrinh3
tonytrinh3 marked this pull request as ready for review September 22, 2026 14:11
@devin-ai-integration devin-ai-integration Bot changed the title feat(build): dual ESM/CommonJS output via tsup (2/4) feat(build): build the AI SDK like the other LaunchDarkly JS SDKs so CommonJS apps can load it (2/4) Sep 22, 2026
tonytrinh3 added a commit that referenced this pull request Sep 23, 2026
…ckages (1/4) (#76)

## Summary

This PR fixes nothing. It turns the customer's bug into a test that
fails on purpose, and writes down the workaround customers can use until
the real fix (PR2) ships.

Closes [AIC-3414](https://launchdarkly.atlassian.net/browse/AIC-3414).
Parent: [AIC-3370](https://launchdarkly.atlassian.net/browse/AIC-3370).
PR 1 of 4 — no module-format change yet.

### The bug, in plain terms

Our `@launchdarkly/ai-*` packages ship ESM-only: `"exports"` has an
`import` entry and no `require` entry. A customer Lambda that emits
CommonJS and bundles with Webpack + `webpack-node-externals` leaves our
packages external, and Webpack rewrites `await
import('@launchdarkly/ai-node')` into
`require('@launchdarkly/ai-node')`. Node finds no `require` entry and
throws:

```text
Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: No "exports" main defined in /var/task/node_modules/@launchdarkly/ai-node/package.json
```

That happens before any of our code runs, so no AI config is evaluated
and nothing reaches AI Insights. An app with a hardcoded fallback prompt
keeps returning plausible answers — which is why this went unnoticed.

### Why a test, when we already have a ticket

Our monorepo physically cannot reproduce this: internally the packages
are symlinked, so `require` resolves fine. The bug only exists in the
*published* package shape. So the test builds what npm would ship and
consumes it like a customer would — `yarn build` → `npm pack` → install
the tarballs into an empty project → Webpack-bundle a CommonJS Lambda
handler → run it.

It bundles that handler twice, differing only by one line of Webpack
config (`nodeExternals({ allowlist: [/^@launchdarkly\/ai-/] })`), and
asserts both outcomes:

```text
left external  → must fail with ERR_PACKAGE_PATH_NOT_EXPORTED / No "exports" main defined
allowlisted    → must load config, initClient, openaiMessages, NodeTracerProvider
```

The failing case is a real assertion, not a skip or `it.fails`. PR2
flips it from "must fail" to "must pass" — that flip is the proof the
fix works on real published artifacts, and the reason this PR lands
first. The handler only reports which symbols resolved: no SDK key, no
LaunchDarkly call, no model spend.

### Which AIC-3370 asks this PR answers

It answers asks 2, 3, and 4:

- **Ask 2 — an official Lambda recipe naming the exact allowlist.**
`packages/ai-node/README.md` gives the Serverless + Webpack config with
`allowlist: [/^@launchdarkly\/ai-/]`, and CI asserts that recipe works.
- **Ask 3 — a smoke test that packages and invokes a real CommonJS
Lambda, because a source-level import test cannot catch this.** The
harness installs `npm pack` tarballs into a separate project, so Node
reads the published `"exports"` map. PR4 (#79) runs the same shape
against the published npm versions in a real Lambda after release.
- **Ask 4 — a documented support matrix.** The root README covers
CommonJS source, CommonJS bundle output, native ESM, and Node 22, and
marks which rows CI proves. It names Webpack only; esbuild and Rollup
rows are not covered yet.

Asks 1 (a `require` condition) and 5 (an initialization diagnostic) are
PR2 (#77) and PR3 (#78).

### Also in this PR

- `yarn test:integration` plus a dedicated Node 22 CI job matching the
reported Lambda runtime; existing unit-test jobs untouched.
- Docs customers can use today: a Lambda + Serverless + Webpack recipe
in `packages/ai-node/README.md`, and a module-format support matrix in
the root README saying what works, what doesn't, and which rows CI
proves.
- `biome.json` excludes `**/.staging` and `**/dist-*` so generated
Webpack output isn't linted.

### Testing

`yarn test:integration` on Node 22.21.1 and Node 24 (2 passed), plus
`yarn workspace @launchdarkly/ai-node test`, `yarn code:check`, `yarn
lint:pkg`. The harness shells out to `yarn build` and `npm pack`, so
it's much slower than the unit suites — hence its own script and CI job.
Start review at `integration-tests/commonjs-webpack/README.md`, then
`harness.ts`, then the two assertions in `commonjs-webpack.test.ts`. No
UI or staging preview applies.

[AIC-3414]:
https://launchdarkly.atlassian.net/browse/AIC-3414?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
[AIC-3370]:
https://launchdarkly.atlassian.net/browse/AIC-3370?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ


Link to Devin session:
https://app.devin.ai/sessions/2798b5e37086465cb5a3af7c906a217b
Open in Devin Desktop:
https://app.devin.ai/desktop/session/2798b5e37086465cb5a3af7c906a217b?variant=devin
Requested by: @tonytrinh3
Base automatically changed from tonytrinh3/AIC-3414-commonjs-webpack-compatibility-contract to main September 23, 2026 15:33
@devin-ai-integration

Copy link
Copy Markdown
Contributor

Pre-merge verification on packed artifacts

Plain version: I built the packages the way npm would publish them. I installed them into two empty projects. One project is CommonJS. One project is ESM. Both loaded every package, with require and with import, on Node 22 and Node 24. Types work too.

One risk stays, and it does not block this PR. If one app loads the same package both ways, Node loads two copies of it. Two copies mean two clients and two telemetry setups. Every package that ships both formats has this behavior. It only affects an app that mixes require and import for the same package.

What it means for the code (junior eng): What I ran, against npm pack tarballs and not workspace links:

  • require() and await import() of all nine packages, from a CommonJS consumer and an ESM consumer, on Node 22 and Node 24. Every package resolved. @launchdarkly/ai-otel exports nothing by design.
  • tsc with module: node16, moduleResolution: node16, and skipLibCheck: false, from a .cts file and from a .mts file. No errors. Every tarball ships dist/index.js, dist/index.cjs, dist/index.d.ts, and dist/index.d.cts.
  • The optional dependencies stay behind a real import() in dist/index.cjs. tsup did not inline them, so the OTel and node-server-sdk discovery path still works from CommonJS.

The dual-copy case, measured in a CommonJS consumer:

require('@launchdarkly/ai-server').NativeTool === (await import('@launchdarkly/ai-server')).NativeTool;
// false

So instanceof NativeTool fails across the two copies, and initClient() state is not shared between them. The fix for a consumer is one load style per package. I can add that line to the README here, or open a follow-up ticket for it.

@tonytrinh3
tonytrinh3 merged commit 2cda755 into main Sep 23, 2026
10 checks passed
@tonytrinh3
tonytrinh3 deleted the tonytrinh3/AIC-3415-dual-esm-cjs-output branch September 23, 2026 16:10
@github-actions github-actions Bot mentioned this pull request Sep 23, 2026
tonytrinh3 added a commit that referenced this pull request Oct 8, 2026
… import [3/4] (#78)

## Summary

Read this first: this PR only changes error text. It fixes no load
failure. It adds no feature. It does not fix the original
[AIC-3370](https://launchdarkly.atlassian.net/browse/AIC-3370) crash,
because that crash happened before any SDK code ran. PR #77 fixed that
part.

|
|
|
||
||
||
||
||
||
||
||
|

**Plain version:** The SDK loads a few optional packages while it
starts. A bundler can break that load. The old error message said the
package is missing. That message was wrong, because the package is
installed. The import still fails. The message now states the real cause
and the fix.

**What it means for the code (junior eng):**
`packages/client/src/lifecycle.ts` loads optional packages with `await
import()`. A bundler can turn that call into `require()`. `require()`
cannot load an ESM-only package. ESM means ECMAScript Modules, the
`import`/`export` module format. Node then throws `ERR_REQUIRE_ESM` or a
similar error. Both catch blocks used to report "not installed" for
every error. Now a classifier reads the error first.

Closes [AIC-3416](https://launchdarkly.atlassian.net/browse/AIC-3416).
This PR is part of
[AIC-3370](https://launchdarkly.atlassian.net/browse/AIC-3370). This PR
is stacked on #77
([AIC-3415](https://launchdarkly.atlassian.net/browse/AIC-3415)).

- The new file `packages/client/src/import-diagnostics.ts` sorts a
failed import into three groups. The groups are ESM interoperability, a
missing package, and an unknown error.
- The ESM message names the package. It contains the phrase `ESM package
was externalized into CommonJS output`. It gives the fix `allowlist:
[/^@launchdarkly\/ai-/]`. It links the Lambda recipe in
`packages/ai-node/README.md`.
- Every new error keeps the original error in `cause`.
- An unknown error passes through without a new label. Before this PR,
the SDK called every failure "not installed".
- The telemetry path warns one time per process. `shutdownTelemetry()`
resets that state. The node SDK path still throws.

## Which AIC-3370 asks this PR answers

**Plain version:** This PR answers ask 5 only. Ask 5 wants an error
message that names the real cause. The message names the package. It
says the bundler externalized an ESM package. It gives the fix. It links
the Lambda recipe. It keeps the original error, so no detail is lost.

**What it means for the code (junior eng):**
`esmExternalizationDiagnostic` in
`packages/client/src/import-diagnostics.ts` builds the text. The text
contains `ESM package was externalized into CommonJS output`. It
contains `allowlist: [/^@launchdarkly\/ai-/]`. It links the recipe in
`packages/ai-node/README.md`. `describeImportFailure` sets `cause` to
the original error.
`packages/client/src/__tests__/import-diagnostics.test.ts` covers the
codes, the message forms, a real `MODULE_NOT_FOUND`, an unknown error,
the preserved `cause`, and the warn-once behavior.

The limit matters. The diagnostic covers the optional dynamic imports in
`packages/client/src/lifecycle.ts`. Those imports run after #77 fixes
root package resolution. The diagnostic cannot catch the original root
import failure. That failure happens in the Node resolver, before our
code runs.

This PR answers no other ask. Ask 1 is #77. Ask 2 is #76. Ask 4 is #76.
Ask 3 is #76 plus #79.

<details>
<summary>Implementation details</summary>

### The old behavior

Both optional imports discarded the error:

```ts
// setupTelemetry
} catch { console.warn('...OpenTelemetry SDK packages are not installed...'); return; }

// initBaseClient
} catch { throw new Error('...node-server-sdk is not installed...'); }
```

Both messages are wrong when the package is present. The bundler is the
real cause.

### The new behavior

```ts
// setupTelemetry
} catch (err) {
  if (isEsmInteropError(err)) {
    warnEsmExternalizationOnce(moduleNameFromError(err, 'an OpenTelemetry SDK package'), err);
    return;
  }
  // the peer dependency install message stays the same
}

// initBaseClient
} catch (err) {
  throw describeImportFailure('@launchdarkly/node-server-sdk', err, MISSING_NODE_SDK_MESSAGE);
}
```

`describeImportFailure(moduleName, error, missingDependencyMessage)`
returns one of three values. It returns the ESM message with `cause`. It
returns the install message with `cause`. It returns the original error
object.

### How the classifier decides

The classifier reads the error `code` first. It treats `ERR_REQUIRE_ESM`
and `ERR_PACKAGE_PATH_NOT_EXPORTED` as ESM interoperability errors. It
treats `MODULE_NOT_FOUND` and `ERR_MODULE_NOT_FOUND` as a missing
package.

The classifier then reads the message text. A bundled loader does not
always keep the `code` property. The message patterns are `require() of
ES Module`, `Must use import to load ES Module`, `No "exports" main
defined`, `is not defined by "exports"`, and `Cannot find
module/package`.

`moduleNameFromError` reads the package name from the quoted specifier.
It also reads the name from a `node_modules/<pkg>` path. The caller
passes a fallback name, so the message never shows an empty name.

### Scope limit

This PR does not catch the original failure from before PR2. That
failure happened in the package resolver, before any SDK code ran. PR
#77 removed that failure.

### One behavior change to review

The node SDK path used to report "not installed" for every failure. An
unrelated failure now propagates unchanged. An example is a `TypeError`
from inside the package itself.

### Alternatives that I rejected

I did not export the classifier from the public barrel. The diagnostic
is internal.

I did not keep the logic inside `lifecycle.ts`. A separate file is
easier to test. Vitest replaces a throwing import mock with its own
factory error. That made the lifecycle tests flaky.

### Testing

`packages/client/src/__tests__/import-diagnostics.test.ts` covers each
error code and each message form. It checks that a real
`MODULE_NOT_FOUND` keeps the install message. It checks that an unknown
error passes through by identity. It checks a non-error throw. It checks
`cause`. It checks module name extraction from a specifier, from a path,
and from the fallback. It checks the message content. It checks the
warn-once behavior and the reset.

I ran these commands from the repo root, and all passed: `yarn build`,
`yarn typecheck`, `yarn code:check`, `yarn lint:pkg`, `yarn test` (395
client tests, three runs to check for flakes), and `yarn
test:integration` (21 tests from the PR1 harness).

This PR changes no UI. There is no staging preview.

### Reviewer guide

1. Read `packages/client/src/import-diagnostics.ts` first. It holds the
classifier and the message text.
2. Read the two catch blocks in `packages/client/src/lifecycle.ts`. The
telemetry path still warns and continues. The node SDK path still
throws.
3. Read `packages/client/src/__tests__/import-diagnostics.test.ts`.

</details>

[AIC-3370]:
https://launchdarkly.atlassian.net/browse/AIC-3370?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
[AIC-3416]:
https://launchdarkly.atlassian.net/browse/AIC-3416?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ


Link to Devin session:
https://app.devin.ai/sessions/d378cea89d3841e19600978ae3c8f2ad
Open in Devin Desktop:
https://app.devin.ai/desktop/session/d378cea89d3841e19600978ae3c8f2ad?variant=devin
Requested by: @tonytrinh3

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Overview**
> Improves **error messaging only** when optional `await import()` calls
in client initialization fail because a bundler externalized ESM
packages into CommonJS (misleading “not installed” text before).
> 
> Adds **`import-diagnostics`** to classify failures: ESM interop
(`ERR_REQUIRE_ESM`, message patterns), genuine missing modules, or
pass-through for unrelated errors. ESM cases get a diagnostic naming the
package, an `allowlist: [/^@launchdarkly\/ai-/]` hint, and a link to the
Lambda/Webpack recipe; wrapped errors keep the original in **`cause`**.
> 
> **`lifecycle.ts`** now uses this in the OpenTelemetry setup catch
(one-time `console.warn` via `warnEsmExternalizationOnce`, reset on
`shutdownTelemetry`) and in the `@launchdarkly/node-server-sdk` import
catch (`describeImportFailure` still throws, but with the right
message). True `MODULE_NOT_FOUND` still shows install guidance; other
failures are no longer mislabeled as missing deps.
> 
> Vitest coverage in **`import-diagnostics.test.ts`** exercises
classification, module name extraction, diagnostics, and warn-once
behavior.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
dc16a78. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
andrewklatzke pushed a commit that referenced this pull request Oct 9, 2026
🤖 I have created a release *beep* *boop*
---


<details><summary>@launchdarkly/ai-claude-agents: 0.4.0</summary>

##
[0.4.0](https://github.com/launchdarkly/js-ai-sdk/compare/@launchdarkly/ai-claude-agents-0.3.0...@launchdarkly/ai-claude-agents-0.4.0)
(2026-10-09)


### Features

* **AIC-3211:** record each graph node as its own tracking event
([#91](#91))
([2df2923](2df2923))
* **AIC-3495:** record which public helpers an application calls
([#115](#115))
([aeb4019](aeb4019))
* **build:** build the AI SDK like the other LaunchDarkly JS SDKs so
CommonJS apps can load it (2/4)
([#77](#77))
([2cda755](2cda755))


### Bug Fixes

* **claude-agents:** forward only an allowlist of query() options from
model.parameters
([37bd8a7](37bd8a7))
* **deps:** depend on the @launchdarkly/ai-server release each package
needs ([#119](#119))
([9c2a56b](9c2a56b))
* **handlers:** camelize snake_case model.parameters for framework
handlers
([75c6287](75c6287))
* **langchain:** stop forwarding additionalModelRequestFields to
ChatBedrockConverse
([f6268fd](f6268fd))
* pass AI Config model parameters through to every provider handler
([#73](#73))
([993225f](993225f))


### Dependencies

* The following workspace dependencies were updated
  * dependencies
    * @launchdarkly/ai-server bumped from ^0.3.0 to ^0.4.0
</details>

<details><summary>@launchdarkly/ai-claude-messages: 0.4.0</summary>

##
[0.4.0](https://github.com/launchdarkly/js-ai-sdk/compare/@launchdarkly/ai-claude-messages-0.3.0...@launchdarkly/ai-claude-messages-0.4.0)
(2026-10-09)


### Features

* **AIC-3495:** record which public helpers an application calls
([#115](#115))
([aeb4019](aeb4019))
* **build:** build the AI SDK like the other LaunchDarkly JS SDKs so
CommonJS apps can load it (2/4)
([#77](#77))
([2cda755](2cda755))
* **claude-messages:** forward only Messages API keys from
model.parameters
([9a1819e](9a1819e))


### Bug Fixes

* **deps:** depend on the @launchdarkly/ai-server release each package
needs ([#119](#119))
([9c2a56b](9c2a56b))
* **handlers:** camelize snake_case model.parameters for framework
handlers
([75c6287](75c6287))
* **langchain:** stop forwarding additionalModelRequestFields to
ChatBedrockConverse
([f6268fd](f6268fd))
* **openai-messages:** only exclude model.parameters keys that would
break the handler
([39e342d](39e342d))
* pass AI Config model parameters through to every provider handler
([#73](#73))
([993225f](993225f))
* **SEC-9507:** authorize message-handler tools from the active
variation ([#90](#90))
([c16fe93](c16fe93))


### Dependencies

* The following workspace dependencies were updated
  * dependencies
    * @launchdarkly/ai-server bumped from ^0.3.0 to ^0.4.0
</details>

<details><summary>@launchdarkly/ai-langchain-agents: 0.4.0</summary>

##
[0.4.0](https://github.com/launchdarkly/js-ai-sdk/compare/@launchdarkly/ai-langchain-agents-0.3.0...@launchdarkly/ai-langchain-agents-0.4.0)
(2026-10-09)


### Features

* **AIC-3211:** record each graph node as its own tracking event
([#91](#91))
([2df2923](2df2923))
* **AIC-3495:** record which public helpers an application calls
([#115](#115))
([aeb4019](aeb4019))
* **build:** build the AI SDK like the other LaunchDarkly JS SDKs so
CommonJS apps can load it (2/4)
([#77](#77))
([2cda755](2cda755))


### Bug Fixes

* **deps:** depend on the @launchdarkly/ai-server release each package
needs ([#119](#119))
([9c2a56b](9c2a56b))
* **handlers:** camelize snake_case model.parameters for framework
handlers
([75c6287](75c6287))
* **langchain:** forward only a per-model-class allowlist of constructor
fields
([0990a51](0990a51))
* **langchain:** stop forwarding additionalModelRequestFields to
ChatBedrockConverse
([f6268fd](f6268fd))
* pass AI Config model parameters through to every provider handler
([#73](#73))
([993225f](993225f))


### Dependencies

* The following workspace dependencies were updated
  * dependencies
    * @launchdarkly/ai-server bumped from ^0.3.0 to ^0.4.0
</details>

<details><summary>@launchdarkly/ai-langchain-messages: 0.4.0</summary>

##
[0.4.0](https://github.com/launchdarkly/js-ai-sdk/compare/@launchdarkly/ai-langchain-messages-0.3.0...@launchdarkly/ai-langchain-messages-0.4.0)
(2026-10-09)


### Features

* **AIC-3495:** record which public helpers an application calls
([#115](#115))
([aeb4019](aeb4019))
* **build:** build the AI SDK like the other LaunchDarkly JS SDKs so
CommonJS apps can load it (2/4)
([#77](#77))
([2cda755](2cda755))


### Bug Fixes

* **deps:** depend on the @launchdarkly/ai-server release each package
needs ([#119](#119))
([9c2a56b](9c2a56b))
* **handlers:** camelize snake_case model.parameters for framework
handlers
([75c6287](75c6287))
* **langchain:** forward only a per-model-class allowlist of constructor
fields
([0990a51](0990a51))
* **langchain:** stop forwarding additionalModelRequestFields to
ChatBedrockConverse
([f6268fd](f6268fd))
* pass AI Config model parameters through to every provider handler
([#73](#73))
([993225f](993225f))
* **SEC-9507:** authorize message-handler tools from the active
variation ([#90](#90))
([c16fe93](c16fe93))


### Dependencies

* The following workspace dependencies were updated
  * dependencies
    * @launchdarkly/ai-server bumped from ^0.3.0 to ^0.4.0
</details>

<details><summary>@launchdarkly/ai-node: 0.3.0</summary>

##
[0.3.0](https://github.com/launchdarkly/js-ai-sdk/compare/@launchdarkly/ai-node-0.2.0...@launchdarkly/ai-node-0.3.0)
(2026-10-09)


### Experimental

* **skills:** add experimental Agent Skills under
`@launchdarkly/ai-server/experimental`, re-exported from
`@launchdarkly/ai-node/experimental`
([a242659](a242659))


### Features

* **build:** build the AI SDK like the other LaunchDarkly JS SDKs so
CommonJS apps can load it (2/4)
([#77](#77))
([2cda755](2cda755))
* **build:** dual ESM/CommonJS output via tsup
([d8e62e4](d8e62e4))
* **client:** `initClient(client, options)` accepts telemetry options
for a pre-initialized client
([a242659](a242659))
* **client:** warn about unrecognized `initClient` options instead of
ignoring them silently
([a242659](a242659))


### Bug Fixes

* **client:** `shutdownTelemetry()` releases the OpenTelemetry globals
the SDK registered, so a later `initClient` exports spans again
([a242659](a242659))
* **client:** a repeat `initClient(client)` returns the client already
set instead of replacing it
([a242659](a242659))
* **client:** an `initClient` that fails, or that `shutdown()`
interrupts, closes the client it built, and a failure is no longer
cached, so the next call retries
([a242659](a242659))
* **deps:** depend on the @launchdarkly/ai-server release each package
needs ([#119](#119))
([9c2a56b](9c2a56b))


### Dependencies

* The following workspace dependencies were updated
  * dependencies
    * @launchdarkly/ai-server bumped from ^0.3.0 to ^0.4.0
</details>

<details><summary>@launchdarkly/ai-openai-agents: 0.4.0</summary>

##
[0.4.0](https://github.com/launchdarkly/js-ai-sdk/compare/@launchdarkly/ai-openai-agents-0.3.0...@launchdarkly/ai-openai-agents-0.4.0)
(2026-10-09)


### Features

* **AIC-3210:** support streaming responses from agent graph nodes
([#84](#84))
([24671e8](24671e8))
* **AIC-3211:** record each graph node as its own tracking event
([#91](#91))
([2df2923](2df2923))
* **AIC-3495:** record which public helpers an application calls
([#115](#115))
([aeb4019](aeb4019))
* **build:** build the AI SDK like the other LaunchDarkly JS SDKs so
CommonJS apps can load it (2/4)
([#77](#77))
([2cda755](2cda755))


### Bug Fixes

* **deps:** depend on the @launchdarkly/ai-server release each package
needs ([#119](#119))
([9c2a56b](9c2a56b))
* **handlers:** camelize snake_case model.parameters for framework
handlers
([75c6287](75c6287))
* **langchain:** stop forwarding additionalModelRequestFields to
ChatBedrockConverse
([f6268fd](f6268fd))
* **openai-agents:** forward only an allowlist of ModelSettings fields
from model.parameters
([398e935](398e935))
* pass AI Config model parameters through to every provider handler
([#73](#73))
([993225f](993225f))
* **telemetry:** report OpenAI tool arguments as an object, not a JSON
string ([#68](#68))
([d614791](d614791))


### Dependencies

* The following workspace dependencies were updated
  * dependencies
    * @launchdarkly/ai-server bumped from ^0.3.0 to ^0.4.0
</details>

<details><summary>@launchdarkly/ai-openai-messages: 0.4.0</summary>

##
[0.4.0](https://github.com/launchdarkly/js-ai-sdk/compare/@launchdarkly/ai-openai-messages-0.3.0...@launchdarkly/ai-openai-messages-0.4.0)
(2026-10-09)


### Features

* **AIC-3495:** record which public helpers an application calls
([#115](#115))
([aeb4019](aeb4019))
* **build:** build the AI SDK like the other LaunchDarkly JS SDKs so
CommonJS apps can load it (2/4)
([#77](#77))
([2cda755](2cda755))
* **openai-messages:** forward only Responses API keys from
model.parameters
([5c51dc9](5c51dc9))


### Bug Fixes

* **deps:** depend on the @launchdarkly/ai-server release each package
needs ([#119](#119))
([9c2a56b](9c2a56b))
* **handlers:** camelize snake_case model.parameters for framework
handlers
([75c6287](75c6287))
* **langchain:** stop forwarding additionalModelRequestFields to
ChatBedrockConverse
([f6268fd](f6268fd))
* **openai-messages:** only exclude model.parameters keys that would
break the handler
([39e342d](39e342d))
* pass AI Config model parameters through to every provider handler
([#73](#73))
([993225f](993225f))
* **SEC-9507:** authorize message-handler tools from the active
variation ([#90](#90))
([c16fe93](c16fe93))
* **telemetry:** report OpenAI tool arguments as an object, not a JSON
string ([#68](#68))
([d614791](d614791))


### Dependencies

* The following workspace dependencies were updated
  * dependencies
    * @launchdarkly/ai-server bumped from ^0.3.0 to ^0.4.0
</details>

<details><summary>@launchdarkly/ai-otel: 0.2.0</summary>

##
[0.2.0](https://github.com/launchdarkly/js-ai-sdk/compare/@launchdarkly/ai-otel-0.1.1...@launchdarkly/ai-otel-0.2.0)
(2026-10-09)


### Features

* **build:** build the AI SDK like the other LaunchDarkly JS SDKs so
CommonJS apps can load it (2/4)
([#77](#77))
([2cda755](2cda755))
* **build:** dual ESM/CommonJS output via tsup
([d8e62e4](d8e62e4))
</details>

<details><summary>@launchdarkly/ai-server: 0.4.0</summary>

##
[0.4.0](https://github.com/launchdarkly/js-ai-sdk/compare/@launchdarkly/ai-server-0.3.0...@launchdarkly/ai-server-0.4.0)
(2026-10-09)


### Experimental

* **skills:** add experimental Agent Skills under
`@launchdarkly/ai-server/experimental`, re-exported from
`@launchdarkly/ai-node/experimental`
([a242659](a242659))


### Features

* **AIC-3210:** support streaming responses from agent graph nodes
([#84](#84))
([24671e8](24671e8))
* **AIC-3211:** record each graph node as its own tracking event
([#91](#91))
([2df2923](2df2923))
* **AIC-3416:** say the real reason when a bundler breaks an optional
import [3/4]
([#78](#78))
([7ee4d5a](7ee4d5a))
* **AIC-3495:** record which public helpers an application calls
([#115](#115))
([aeb4019](aeb4019))
* **build:** build the AI SDK like the other LaunchDarkly JS SDKs so
CommonJS apps can load it (2/4)
([#77](#77))
([2cda755](2cda755))
* **client:** `initClient(client, options)` accepts telemetry options
for a pre-initialized client
([a242659](a242659))
* **client:** restore pickForwardedModelParameters for closed provider
request types
([f7f85d0](f7f85d0))
* **client:** warn about unrecognized `initClient` options instead of
ignoring them silently
([a242659](a242659))


### Bug Fixes

* **client:** `shutdownTelemetry()` releases the OpenTelemetry globals
the SDK registered, so a later `initClient` exports spans again
([a242659](a242659))
* **client:** a repeat `initClient(client)` returns the client already
set instead of replacing it
([a242659](a242659))
* **client:** an `initClient` that fails, or that `shutdown()`
interrupts, closes the client it built, and a failure is no longer
cached, so the next call retries
([a242659](a242659))
* **handlers:** camelize snake_case model.parameters for framework
handlers
([75c6287](75c6287))
* **judges:** a judge's own config must not fail the run it grades
([#17](#17))
([6e85cac](6e85cac))
* pass AI Config model parameters through to every provider handler
([#73](#73))
([993225f](993225f))
* **vercel-messages:** map model.parameters onto AI SDK call settings
([d2a52ae](d2a52ae))
* **vercel:** tag vercelEvaluate spans with run identity
([#100](#100))
([d32bede](d32bede))
</details>

<details><summary>@launchdarkly/ai-vercel-agents: 0.2.0</summary>

##
[0.2.0](https://github.com/launchdarkly/js-ai-sdk/compare/@launchdarkly/ai-vercel-agents-0.1.0...@launchdarkly/ai-vercel-agents-0.2.0)
(2026-10-09)


### Features

* **AIC-3408:** add Vercel AI SDK messages and agents adapters
([#83](#83))
([7d08c9c](7d08c9c))
* **AIC-3495:** record which public helpers an application calls
([#115](#115))
([aeb4019](aeb4019))


### Bug Fixes

* **deps:** depend on the @launchdarkly/ai-server release each package
needs ([#119](#119))
([9c2a56b](9c2a56b))
* **langchain:** stop forwarding additionalModelRequestFields to
ChatBedrockConverse
([f6268fd](f6268fd))
* pass AI Config model parameters through to every provider handler
([#73](#73))
([993225f](993225f))
* **vercel-agents:** map model.parameters onto ToolLoopAgent call
settings
([4241d75](4241d75))
* **vercel:** never forward headers, providerOptions, maxRetries or
timeout from model.parameters
([0a616fb](0a616fb))
* **vercel:** send image content as AI SDK file parts
([#99](#99))
([2856abc](2856abc))


### Dependencies

* The following workspace dependencies were updated
  * dependencies
    * @launchdarkly/ai-server bumped from ^0.3.0 to ^0.4.0
</details>

<details><summary>@launchdarkly/ai-vercel-messages: 0.2.0</summary>

##
[0.2.0](https://github.com/launchdarkly/js-ai-sdk/compare/@launchdarkly/ai-vercel-messages-0.1.0...@launchdarkly/ai-vercel-messages-0.2.0)
(2026-10-09)


### Features

* **AIC-3408:** add Vercel AI SDK messages and agents adapters
([#83](#83))
([7d08c9c](7d08c9c))
* **AIC-3495:** record which public helpers an application calls
([#115](#115))
([aeb4019](aeb4019))


### Bug Fixes

* **deps:** depend on the @launchdarkly/ai-server release each package
needs ([#119](#119))
([9c2a56b](9c2a56b))
* **langchain:** stop forwarding additionalModelRequestFields to
ChatBedrockConverse
([f6268fd](f6268fd))
* pass AI Config model parameters through to every provider handler
([#73](#73))
([993225f](993225f))
* **vercel-messages:** map model.parameters onto AI SDK call settings
([d2a52ae](d2a52ae))
* **vercel:** never forward headers, providerOptions, maxRetries or
timeout from model.parameters
([0a616fb](0a616fb))
* **vercel:** send image content as AI SDK file parts
([#99](#99))
([2856abc](2856abc))
* **vercel:** tag vercelEvaluate spans with run identity
([#100](#100))
([d32bede](d32bede))


### Dependencies

* The following workspace dependencies were updated
  * dependencies
    * @launchdarkly/ai-server bumped from ^0.3.0 to ^0.4.0
</details>

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Overview**
> **Release Please monorepo cut** that bumps published versions in
`.release-please-manifest.json`, each package’s `package.json` /
`src/version.ts`, and adds matching **CHANGELOG** sections (including
first changelogs for the Vercel packages). Provider packages now depend
on **`@launchdarkly/ai-server` ^0.4.0**.
> 
> The documented release for **`@launchdarkly/ai-server` 0.4.0** (and
aligned provider bumps) includes **dual ESM + CommonJS builds**, **agent
graph node streaming**, **per-node tracking events** and **public-helper
usage telemetry**, **`initClient` / `shutdownTelemetry` lifecycle
fixes**, and **experimental Agent Skills** under
`@launchdarkly/ai-server/experimental`. Cross-cutting fixes in the notes
cover **AI Config `model.parameters` forwarding** (allowlists,
snake_case → camelCase), **message-handler tool authorization
(SEC-9507)**, and **Vercel adapter** parameter mapping and multimodal
content handling.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
4acc72c. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
tonytrinh3 added a commit that referenced this pull request Oct 21, 2026
…79)

## Summary

**Plain version:** We publish packages to npm. A customer installs them
and runs them on AWS Lambda.
This change adds a test that does the same thing right after each
release.
The test installs the published packages, runs them in a real AWS
Lambda, and then deletes that Lambda.
If the test fails, the packages are broken for customers, and we know in
minutes.

**What it means for the code (junior eng):** A canary is a small test
against production artifacts.
This canary closes
[AIC-3370](https://launchdarkly.atlassian.net/browse/AIC-3370).
The suite from PR1 packs the workspace with `npm pack`, so it tests the
tarball that we plan to ship.
This canary tests the artifact that npm actually serves, in the runtime
that customers use.

Closes [AIC-3417](https://launchdarkly.atlassian.net/browse/AIC-3417).
[AIC-3416](https://launchdarkly.atlassian.net/browse/AIC-3416) is
merged.
The base branch is `main` now. I merged `main` into this branch and
resolved the conflicts.

- **The AWS account and the OIDC role are not confirmed.** I found no
approved AWS account for this repository. I found no GitHub OIDC role
for this repository. The `id-token` permission here serves npm trusted
publishing only. The job reads `vars.AWS_LAMBDA_CANARY_ROLE_ARN`. GitHub
skips the job while that variable is empty. I added no credentials. The
README lists the trust policy and the permissions.
- `integration-tests/lambda-commonjs/` holds the fixture. It uses
Serverless 3, Webpack 5, and `webpack-node-externals` 3. The output is
`commonjs2`. The runtime is `nodejs22.x`. The fixture sets no externals
allowlist, so Node resolves the LaunchDarkly packages at runtime. That
path is the AIC-3370 failure path.
- `canary.mjs` has three commands. `wait` polls npm. `build` installs
the exact versions and packages the app. `assert` checks the payload
from the Lambda. The assert step requires the exact type of each export.
- `release-please.yml` gets a `release-canary` job. A new
`canary_versions` dispatch input runs the canary on demand. The cleanup
step runs under `if: always()`. The step fails the job when it cannot
delete the stack or the log group.
- The canary loads packages only. It uses no SDK key. It evaluates no
flag. It calls no model.
- The canary pins `@launchdarkly/ai-server` too. `@launchdarkly/ai-node`
depends on the core package with `*`, so npm would otherwise pick any
visible core version. A core-only release now also runs the canary.

## Which AIC-3370 asks this PR answers

**Plain version:** This PR answers ask 3. It runs the packages that npm
serves inside a real Lambda after each release.
It does not answer the other four asks. Other PRs in the stack answer
those.

**What it means for the code (junior eng):** This PR completes ask 3.
It installs exact published npm versions. It packages a CommonJS Lambda
with Webpack 5 and `webpack-node-externals` 3.
It deploys that Lambda on Node 22. It invokes the Lambda after each
release.

Ask 3 has two halves. #76 covers the packed-tarball half. This PR covers
the published-artifact half.

The real AWS path is gated on `vars.AWS_LAMBDA_CANARY_ROLE_ARN`.
No approved AWS account exists yet. No GitHub OIDC role exists yet.
So the deploy path, the invoke path, and the cleanup path are unverified
against real AWS.

This PR does not answer asks 1, 2, 4, or 5. #77 answers ask 1. #76
answers asks 2 and 4. #78 answers ask 5.


<details>
<summary>Implementation details</summary>

### Why this test exists

The suite in `integration-tests/commonjs-webpack` packs the workspace
with `npm pack`.
That suite cannot catch a publish-time regression.
Examples are a wrong `files` list, a stale `dist` folder, or a bad
exports map in the published tag.
That suite also does not run on Lambda.

The difference is real today. I invoked the generated bundle against the
published `0.2.0` artifacts.

```
$ node -e "require('./handler.js').handler()..."
ERR_PACKAGE_PATH_NOT_EXPORTED No "exports" main defined in node_modules/@launchdarkly/ai-node/package.json
```

That error is the AIC-3370 bug. The canary passes after we publish the
dual output from PR2.

### The driver

`canary.mjs` holds all of the logic. It takes one exact version for each
package.
You pass `--ai-node=0.3.0` and the other flags.
You can also set
`LD_AI_CANARY_VERSIONS='ai-node=0.3.0,openai-messages=0.3.0,ai-otel=0.2.0'`.

| Command | Behavior |
| --- | --- |
| `wait` | Calls `npm view <pkg>@<version> version` up to 30 times, 10
seconds apart. A new publish is not readable at once. |
| `build` | Copies `fixture/` into `.canary/app`. Runs `npm install
--save-exact`. Checks the tree with `npm ls`. Writes `versions.json`.
Runs `serverless package` when you pass `--package`. |
| `assert` | Reads the invoke payload and checks it. |

The driver checks exactness two times.
It first matches the input against
`/^\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?$/`.
It then compares the installed tree from `npm ls --depth=0 --json`
against the request.

The build writes `versions.json` into the app, and the handler returns
that file.
So the assert step checks the versions that the Lambda loaded.
It does not trust the versions that CI asked for.

The handler returns this shape. The assert step compares each capability
against `EXPECTED_CAPABILITIES`.

```json
{ "ok": true, "runtime": "v22.x", "versions": { }, "capabilities": { "config": "function", "initClient": "function", "shutdown": "function", "openaiMessages": "function", "aiOtel": "object" } }
```

### The workflow job

```yaml
release-canary:
  needs: [release-please, release-client, release-ai-node, release-ai-otel, release-openai-messages]
  if: always() && no failed or cancelled need && vars.AWS_LAMBDA_CANARY_ROLE_ARN != ''
      && (dispatch needs canary_versions, push needs a released canary package)
  permissions: { contents: read, id-token: write }
  env: { CANARY_STAGE: c${{ github.run_id }}-${{ github.run_attempt }} }
```

A push reads the versions from `.release-please-manifest.json`.
A manual run reads them from the `canary_versions` input.
Each run and attempt gets its own stage, so the function name never
collides.

The existing publish jobs do not change.
`manual-publish` now also requires an empty `canary_versions`.
So a canary-only manual run publishes nothing.

The cleanup step fails loudly. It ignores only a log group that AWS
already removed.

```bash
serverless remove --stage "$CANARY_STAGE" || status=1
aws logs delete-log-group --log-group-name "$log_group"   # ResourceNotFoundException is the only pass
exit "$status"
```

### Decisions

- The config schema of Serverless 3 does not know `nodejs22.x`. The
package step prints a warning. `configValidationMode: warn` keeps it a
warning. CloudFormation accepts the runtime. The canary reads
`process.version` and fails on any runtime that is not Node 22.
- `aws-actions/configure-aws-credentials` is pinned to a SHA for v4.3.1.
This matches the pinning style of this repository.
- `.canary/` is now in `.gitignore` and in the Biome ignore list.
Serverless writes generated CloudFormation JSON there, and `code:check`
fails on those files.

### Testing

- `yarn code:fix`, `yarn code:check`, `yarn lint:pkg`, `yarn build`,
`yarn typecheck`, and `yarn test` all pass.
- `canary.test.ts` adds 8 tests to `yarn test:integration`. The tests
use no network and no AWS account. They cover the happy path, a version
that is not exact, a version mismatch, a wrong runtime, an export that
did not resolve, an export with the wrong type, a missing export, and an
error payload from Lambda.
- The local dry run packaged the app against the published artifacts.
The archive holds the three external LaunchDarkly packages.
  ```
node canary.mjs build --ai-node=0.2.0 --openai-messages=0.2.0
--ai-otel=0.1.1 --package --stage=local
  ```
- I did not verify the deploy path, the invoke path, or the cleanup path
against AWS. There is no role to assume yet.

### Reviewer guide

1. Read `integration-tests/lambda-commonjs/README.md` for the AWS trust
policy, the permissions, and the owner.
2. Read the `release-canary` job in
`.github/workflows/release-please.yml`. Check the `if` condition, the
OIDC step, and the cleanup step.
3. Read `canary.mjs` for the version checks and the assert contract.
4. Read `fixture/`. Confirm that you want no externals allowlist in this
test.

</details>

[AIC-3370]:
https://launchdarkly.atlassian.net/browse/AIC-3370?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
[AIC-3417]:
https://launchdarkly.atlassian.net/browse/AIC-3417?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
[AIC-3416]:
https://launchdarkly.atlassian.net/browse/AIC-3416?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

Link to Devin session:
https://app.devin.ai/sessions/bc2f6d69a1b54d9a8ee7b3f1cdee2f8c
Open in Devin Desktop:
https://app.devin.ai/desktop/session/bc2f6d69a1b54d9a8ee7b3f1cdee2f8c?variant=devin
Requested by: @tonytrinh3

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Overview**
> Adds a **published-artifact release canary** that runs after relevant
npm publishes (or on demand) to catch CommonJS Lambda load failures like
AIC-3370.
> 
> The new `integration-tests/lambda-commonjs` harness uses `canary.mjs`
to poll npm for exact semver versions, stage a Serverless 3 + Webpack 5
+ `webpack-node-externals` app with **no externals allowlist**, and
validate an invoke payload (Node 22 runtime, export types, installed
versions). `canary.test.ts` covers assert/version guards offline; `yarn
test:integration` now includes it. Generated `.canary/` output is
gitignored and excluded from Biome.
> 
> **`release-please.yml`** gains a `release-canary` job (GitHub OIDC →
ephemeral `ld-ai-canary-*` deploy/invoke/cleanup) gated on
`AWS_LAMBDA_CANARY_ROLE_ARN` and
`AWS_LAMBDA_CANARY_PERMISSIONS_BOUNDARY`, plus optional
`canary_versions` on `workflow_dispatch` for canary-only runs. Manual
publish is skipped when `canary_versions` is set.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
11acd8d. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants