> ## Documentation Index
> Fetch the complete documentation index at: https://docs.waniwani.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Changelog

> Release notes for the Waniwani SDK, including breaking changes and deprecated APIs.

This page tracks API changes that require code updates, organized by the version that introduced them. Deprecations are safe to ignore short-term — the old shape keeps working until the removal version listed in the deprecation notice.

## Deprecations at a glance

| API                                                                                         | Status      | Since  | Removed in |
| ------------------------------------------------------------------------------------------- | ----------- | ------ | ---------- |
| `track.lead()` (alias of `track.leadQualified()`)                                           | Deprecated  | 0.15.1 | 0.16.0     |
| `.addNode(id, run, options?)` (positional)                                                  | Deprecated  | 0.12.0 | 0.13.0     |
| `createTool`, `createResource`, `registerTools` from `@waniwani/sdk/mcp`                    | Deprecated  | 0.12.0 | 0.14.0     |
| MCP-widget React host (`WidgetProvider`, `useToolOutput`, …) from `@waniwani/sdk/mcp/react` | Deprecated  | 0.12.0 | 0.14.0     |
| `toNextJsHandler` from `@waniwani/sdk/next-js`                                              | Deprecated  | 0.12.0 | 0.14.0     |
| `toExpressJsHandler` from `@waniwani/sdk/express-js`                                        | Deprecated  | 0.12.0 | 0.14.0     |
| Chat-server types from `@waniwani/sdk/chat/server`                                          | Deprecated  | 0.12.0 | 0.14.0     |
| `ChatCard` from `@waniwani/sdk/chat`                                                        | Deprecated  | 0.12.0 | 0.14.0     |
| `evals/*` (entire subtree)                                                                  | **Removed** | 0.12.0 | 0.12.0     |

<Tip>
  TypeScript will strike through deprecated signatures in your IDE. Hover for the replacement.
</Tip>

## Breaking changes at a glance

| Change                                                                            | Kind             | Version | Auto-fixable        |
| --------------------------------------------------------------------------------- | ---------------- | ------- | ------------------- |
| `track.lead()` → `track.leadQualified()`, event `"lead"` → `"lead_qualified"`     | Rename           | 0.15.0  | Yes (codemod below) |
| `addConditionalEdge(from, condition)` → `addConditionalEdge(from, to, condition)` | Signature change | 0.14.0  | Yes (codemod below) |

<Note>
  When you bump `@waniwani/sdk` across a minor version (`0.x` minors can break), scan this page for every breaking change between your old and new version and apply the documented migration. Each one below is a mechanical rewrite an agent or codemod can apply in a single pass, followed by `bun run typecheck && bun test`.
</Note>

***

## 0.15.0: `lead` becomes `lead_qualified`

The lead event is named `lead_qualified`, its helper is `track.leadQualified()`, and its properties are `LeadQualifiedProperties`: `externalId`, `email`, and `name` alongside `source`. The typed input is `RevenueLeadQualifiedInput` (`RevenueLeadInput` and `LeadProperties` no longer exist).

`lead_qualified` means the person met your qualification bar (finished the qualifying questions, requested a demo, matched your target profile). It belongs at the node where qualification completes, not at flow entry; see [Instrumentation](/sdk/tracking/instrumentation) for placement rules.

### Why

`lead` under-specified what the event represented, so integrations fired it anywhere from flow entry to CRM push and funnels were not comparable across projects. The name now states the semantic, and the new properties (`externalId` as your CRM record id, plus `email` and `name`) give the dashboard real dedup and join keys instead of an anonymous count.

### Before

```ts theme={null}
await client.track.lead({ source: "newsletter", externalUserId: "user_123" });

await client.track({ event: "lead", properties: { source: "newsletter" }, sessionId });
```

### After

```ts theme={null}
await client.track.leadQualified({
  externalId: "lead_abc123", // your CRM/lead record id, the strongest dedup key
  email: "jane@example.com",
  name: "Jane Doe",
  source: "newsletter",
  externalUserId: "user_123",
});

await client.track({ event: "lead_qualified", properties: { source: "newsletter" }, sessionId });
```

All new properties are optional; a bare `track.leadQualified({ source })` is valid.

### Migration (codemod)

1. Replace every `.track.lead(` call with `.track.leadQualified(`.
2. Replace `event: "lead"` with `event: "lead_qualified"` in generic `track()` calls (and `eventType: "lead"` in the legacy shape).
3. Replace type imports: `RevenueLeadInput` → `RevenueLeadQualifiedInput`, `LeadProperties` → `LeadQualifiedProperties`.
4. Where the call site has them available, enrich the event: pass your lead record id as `externalId`, plus `email` and `name`.
5. Run `bun run typecheck && bun test`.

### Compatibility

* `track.lead()` exists as a `@deprecated` alias since **0.15.1**: it emits a `lead_qualified` event and will be removed in **0.16.0**. On 0.15.0 exactly, the alias is absent and step 1 is required to compile.
* The transport normalizes the event name `"lead"` to `"lead_qualified"` at runtime (since 0.15.1), so already-compiled pre-0.15 callers keep producing valid events. The `EventType` union does not accept `"lead"`, so TypeScript call sites must migrate.

### Verifying

```bash theme={null}
grep -rn "track\.lead(\|\"lead\"\|'lead'" src/ app/ lib/  # should return no tracking call sites
bun run typecheck
bun test
```

***

## 0.14.0: `addConditionalEdge` declares its branch targets

`addConditionalEdge` now takes the reachable nodes explicitly as the second argument: `addConditionalEdge(from, to, condition)`. The condition's return type is constrained to `to`, so a branch can never route to an undeclared node, and the flow graph used for funnel analytics and Mermaid diagrams is correct by construction.

### Why

Before 0.14.0, graph introspection discovered a conditional edge's targets by stringifying the condition function and scanning its source for node names. That heuristic was fragile: it broke under minification, missed dynamically-computed targets, and produced false positives when a node id appeared as a comparison value or in a comment — so funnel analytics could show the wrong branches. Declaring `to` removes the guesswork entirely.

### Before

```ts theme={null}
.addConditionalEdge("analyze_email", (state) =>
  state.isCompanyEmail ? "done" : "ask_company",
)
```

### After

```ts theme={null}
.addConditionalEdge("analyze_email", ["done", "ask_company"], (state) =>
  state.isCompanyEmail ? "done" : "ask_company",
)
```

### Migration (codemod)

For each `.addConditionalEdge(from, condition)` call, insert a second argument: an array of every node the condition can return (including `END` if it returns `END`). The simplest reliable source for that array is the set of literal node names returned by the condition body.

1. Find every two-argument `.addConditionalEdge(` call.
2. Read the condition body and collect every node name (string literal or `END`) it can return.
3. Insert that list, deduplicated, as the new second argument — leaving the condition as the third.
4. Run `bun run typecheck`. The compiler now flags any return value missing from `to`, so any over- or under-declared list surfaces immediately. Fix by aligning `to` with what the condition actually returns.

```ts theme={null}
// Before
.addConditionalEdge("route_country", (state) => {
  if (state.country === "FR") return "fr_path";
  if (state.country === "DE") return "de_path";
  return "default_path";
})

// After
.addConditionalEdge(
  "route_country",
  ["fr_path", "de_path", "default_path"],
  (state) => {
    if (state.country === "FR") return "fr_path";
    if (state.country === "DE") return "de_path";
    return "default_path";
  },
)
```

### Verifying

```bash theme={null}
bun run typecheck   # condition return type is now checked against `to`
bun test
```

If typecheck reports `Type '"x"' is not assignable to type '"a" | "b"'`, the condition returns a node missing from `to` — add it. If it reports an unused-looking target, the condition never returns it — safe to leave (over-declaration is allowed) or trim.

***

## 0.12.0: Legacy reorganization

The MCP-widget-in-host stack (`createTool` / `createResource` / `WidgetProvider` and host bridge hooks) and the chat-server BFF adapters (`toNextJsHandler`, `toExpressJsHandler`, `createApiHandler`) were moved to dedicated `@waniwani/sdk/legacy*` entry points. New code should use `createFlow` from `@waniwani/sdk/mcp` and the direct-to-backend `ChatEmbed` from `@waniwani/sdk/chat`.

The **old entry points still re-export every symbol unchanged** — your imports keep working without edits. The legacy entry points exist so you can adopt the final import paths now and avoid the next major bump.

### Why

* **OSS-first framing.** `@waniwani/sdk/mcp` is now the OSS surface: `createFlow`, `StateGraph`, `KvStore`, `MemoryKvStore`. No API key required, no hosted dependency on the core path.
* **Smaller default bundle.** Splitting the legacy surface off lets host-only React hooks and the chat-server router stay out of the OSS bundle once the re-exports are dropped.
* **Sunset signal.** The chat widget will talk directly to `app.waniwani.ai` in a future release, removing the need for a self-hosted BFF. The Next.js / Express adapters become unnecessary.

### Migration matrix

Every row is a mechanical rewrite — no behavior changes, only the import path. An LLM or codemod can apply all of these in one pass.

| Symbol(s)                                                                                                                                                                                                                              | Old import                  | New import                                                                                                                                                                  |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `createTool`, `registerTools`, `ToolConfig`, `ToolHandler`, `ToolHandlerContext`, `RegisteredTool`, `ToolToolCallback`                                                                                                                 | `@waniwani/sdk/mcp`         | `@waniwani/sdk/legacy`                                                                                                                                                      |
| `createResource`, `ResourceConfig`, `RegisteredResource`, `WidgetCSP`                                                                                                                                                                  | `@waniwani/sdk/mcp`         | `@waniwani/sdk/legacy`                                                                                                                                                      |
| `detectPlatform`, `isMCPApps`, `isOpenAI`, `WidgetPlatform`                                                                                                                                                                            | `@waniwani/sdk/mcp`         | `@waniwani/sdk/legacy/react`                                                                                                                                                |
| `HostContext`, `ToolCallResult`, `ToolResult`, `UnifiedWidgetClient`                                                                                                                                                                   | `@waniwani/sdk/mcp`         | `@waniwani/sdk/legacy/react`                                                                                                                                                |
| `WidgetProvider`, `useWidgetClient`                                                                                                                                                                                                    | `@waniwani/sdk/mcp/react`   | `@waniwani/sdk/legacy/react`                                                                                                                                                |
| `useToolOutput`, `useToolResponseMetadata`, `useCallTool`, `useSendFollowUp`, `useFlowAction`, `useUpdateModelContext`                                                                                                                 | `@waniwani/sdk/mcp/react`   | `@waniwani/sdk/legacy/react`                                                                                                                                                |
| `useDisplayMode`, `useRequestDisplayMode`, `useTheme`, `useLocale`, `useSafeArea`, `useMaxHeight`, `useWidgetState`, `useOpenExternal`, `useIsChatGptApp`                                                                              | `@waniwani/sdk/mcp/react`   | `@waniwani/sdk/legacy/react`                                                                                                                                                |
| `InitializeNextJsInIframe`, `LoadingWidget`                                                                                                                                                                                            | `@waniwani/sdk/mcp/react`   | `@waniwani/sdk/legacy/react`                                                                                                                                                |
| `DevModeProvider`, `getMockState`, `initializeMockOpenAI`, `updateMockDisplayMode`, `updateMockGlobal`, `updateMockTheme`, `updateMockToolOutput`                                                                                      | `@waniwani/sdk/mcp/react`   | `@waniwani/sdk/legacy/react`                                                                                                                                                |
| `DeviceType`, `DisplayMode`, `SafeArea`, `SafeAreaInsets`, `Theme`, `UnknownObject`, `UserAgent` (widget host types)                                                                                                                   | `@waniwani/sdk/mcp/react`   | `@waniwani/sdk/legacy/react`                                                                                                                                                |
| `FlowActionResult`, `ModelContextContentBlock`, `ModelContextUpdate`, `SendFollowUpOptions`                                                                                                                                            | `@waniwani/sdk/mcp/react`   | `@waniwani/sdk/legacy/react`                                                                                                                                                |
| `toNextJsHandler`, `NextJsHandlerOptions`, `NextJsHandlerResult`                                                                                                                                                                       | `@waniwani/sdk/next-js`     | `@waniwani/sdk/legacy/next-js`                                                                                                                                              |
| `toExpressJsHandler`, `ExpressJsHandlerOptions`, `ExpressJsHandlerResult`, `ExpressLikeRequest`, `ExpressLikeResponse`, `ExpressMiddleware`, `ChatOptions`                                                                             | `@waniwani/sdk/express-js`  | `@waniwani/sdk/legacy/express-js`                                                                                                                                           |
| `ApiHandler`, `ApiHandlerOptions`, `BeforeRequestContext`, `BeforeRequestResult`, `ClientVisitorContext`, `VisitorMeta`, `WebSearchConfig`, `GeoLocation`, `extractGeoFromHeaders`, `WaniWaniError` (when imported from `chat/server`) | `@waniwani/sdk/chat/server` | *stays* — `@waniwani/sdk/chat/server` is a re-export shim around `legacy/chat/server/*`. No required rewrite. Will be removed alongside the chat-server adapters in 0.14.0. |
| `ChatCard`, `ChatCardProps`                                                                                                                                                                                                            | `@waniwani/sdk/chat`        | *no replacement* — migrate to `ChatEmbed` (see below)                                                                                                                       |

`useWaniwani`, `UseWaniwaniOptions`, and `WaniwaniWidget` stay on `@waniwani/sdk/mcp/react` — that hook is non-legacy.

### What stays the same

These remain on `@waniwani/sdk/mcp` and are the recommended surface for new code:

* `createFlow`, `StateGraph`, `START`, `END`, `redacted`, `createFlowTestHarness`, `AddNodeConfig`, all flow-related types
* `KvStore`, `MemoryKvStore`, `WaniwaniKvStore`
* `withWaniwani`, `WithWaniwaniOptions`
* `createTrackingRoute`, `TrackingRouteOptions`
* `ScopedWaniWaniClient`
* `McpServer`, `ZodRawShapeCompat` (shared MCP types)

And on `@waniwani/sdk`:

* `waniwani()`, `defineConfig`, `WaniWaniError`, all tracking helpers

### Codemod recipe (one-shot)

An agent updating a downstream repo can apply these substitutions in order. Each is a string-level rewrite of an `import … from "<path>"` statement — semantics are preserved end-to-end.

```bash theme={null}
# 1. Server-side MCP primitives → @waniwani/sdk/legacy
#    (createTool, createResource, registerTools, and their associated types)
#    Rewrite any import of { createTool | createResource | registerTools |
#    ToolConfig | ToolHandler | ToolHandlerContext | RegisteredTool |
#    ToolToolCallback | ResourceConfig | RegisteredResource | WidgetCSP }
#    from "@waniwani/sdk/mcp" → "@waniwani/sdk/legacy"

# 2. Widget host hooks/components/types → @waniwani/sdk/legacy/react
#    Rewrite any import of { WidgetProvider | useWidgetClient | useToolOutput |
#    useToolResponseMetadata | useCallTool | useSendFollowUp | useFlowAction |
#    useUpdateModelContext | useDisplayMode | useRequestDisplayMode | useTheme |
#    useLocale | useSafeArea | useMaxHeight | useWidgetState | useOpenExternal |
#    useIsChatGptApp | InitializeNextJsInIframe | LoadingWidget |
#    DevModeProvider | getMockState | initializeMockOpenAI |
#    updateMockDisplayMode | updateMockGlobal | updateMockTheme |
#    updateMockToolOutput | DeviceType | DisplayMode | SafeArea |
#    SafeAreaInsets | Theme | UnknownObject | UserAgent | FlowActionResult |
#    ModelContextContentBlock | ModelContextUpdate | SendFollowUpOptions |
#    HostContext | ToolCallResult | ToolResult | UnifiedWidgetClient |
#    detectPlatform | isMCPApps | isOpenAI | WidgetPlatform }
#    from "@waniwani/sdk/mcp/react" → "@waniwani/sdk/legacy/react"
#
#    Leave imports of { useWaniwani | UseWaniwaniOptions | WaniwaniWidget }
#    on "@waniwani/sdk/mcp/react" — those are non-legacy.

# 3. Next.js adapter → @waniwani/sdk/legacy/next-js
#    Rewrite "@waniwani/sdk/next-js" → "@waniwani/sdk/legacy/next-js"
#    (every export of the old path was moved; no symbol-level filtering needed)

# 4. Express adapter → @waniwani/sdk/legacy/express-js
#    Rewrite "@waniwani/sdk/express-js" → "@waniwani/sdk/legacy/express-js"

# 5. Chat-server types
#    "@waniwani/sdk/chat/server" still works as a re-export shim.
#    No required rewrite. If you want to drop the shim early, point the imports
#    at the legacy server-types module that backs your adapter.
```

If you split imports across files, the only ambiguous case is **step 1 vs step 3 (Express) when both come from the same root**. Mechanical rule: **the source path determines the destination, not the symbol name**. The same symbol does not appear in two unrelated source paths within this matrix.

### Verifying a one-shot migration

After substituting, the codebase should still typecheck and run unchanged. Run:

```bash theme={null}
bun run typecheck
bun run lint
bun test
```

If you see errors of the form `Module '"@waniwani/sdk/mcp"' has no exported member 'X'`, it means that symbol is being moved entirely out of the old path in a later major (0.14.0). Until then the old import keeps working — the matrix above tells you the final destination.

### Verifying a customer codebase has zero legacy imports

To audit a downstream repo for any remaining legacy imports:

```bash theme={null}
# Any import from a legacy path
grep -rn "@waniwani/sdk/legacy" src/ app/ pages/

# Legacy symbols still pulled through non-legacy paths.
# These greps only flag the moved symbols; OSS symbols (createFlow, KvStore,
# withWaniwani, useWaniwani, …) stay on the original paths and are not matched.

grep -rEn 'createTool|createResource|registerTools|ToolConfig|ToolHandler|ToolHandlerContext|RegisteredTool|ToolToolCallback|ResourceConfig|RegisteredResource|WidgetCSP|detectPlatform|isMCPApps|isOpenAI|WidgetPlatform|HostContext|ToolCallResult|ToolResult|UnifiedWidgetClient' src/ app/ pages/ \
  | grep "@waniwani/sdk/mcp"

grep -rEn 'WidgetProvider|useWidgetClient|useToolOutput|useToolResponseMetadata|useCallTool|useSendFollowUp|useFlowAction|useUpdateModelContext|useDisplayMode|useRequestDisplayMode|useTheme|useLocale|useSafeArea|useMaxHeight|useWidgetState|useOpenExternal|useIsChatGptApp|InitializeNextJsInIframe|LoadingWidget|DevModeProvider' src/ app/ pages/ \
  | grep "@waniwani/sdk/mcp/react"

grep -rEn 'from ["'\'']@waniwani/sdk/next-js["'\'']' src/ app/ pages/
grep -rEn 'from ["'\'']@waniwani/sdk/express-js["'\'']' src/ app/ pages/
```

The first command should produce hits (those are the new paths). The remaining commands should produce **either zero hits, or the same lines as before the migration** (meaning the back-compat shim is doing its job and you can defer the rewrite).

### `ChatCard` → `ChatEmbed`

`ChatCard` is still exported but marked `@deprecated`. It wraps `ChatEmbed` with always-visible card chrome (header, border, fixed dimensions) and assumes the Waniwani-hosted backend.

```tsx theme={null}
// Before
import { ChatCard } from "@waniwani/sdk/chat";

<ChatCard
  apiKey={process.env.NEXT_PUBLIC_WANIWANI_API_KEY}
  title="Assistant"
  width={500}
  height={600}
  welcomeMessage="Hi! How can I help?"
/>
```

```tsx theme={null}
// After — bring-your-own-backend, no card chrome
import { ChatEmbed } from "@waniwani/sdk/chat";

<ChatEmbed
  api="/api/my-chat-endpoint"
  welcome={{ title: "Assistant", description: "Hi! How can I help?" }}
/>
```

`ChatEmbed` does not fetch `/config` or `/tool` against the Waniwani host — you point `api` at your own endpoint and (optionally) supply `mcp.resourceEndpoint` if your backend serves widget resources.

### `evals/*` removed

The `src/evals/*` subtree (chat eval runner, scorers, reporter) was removed entirely. No re-export shim. If you imported `runChatEval`, `createScorer`, or any sibling from `@waniwani/sdk/evals`, the build will fail on upgrade — there is no equivalent in this release. Eval tooling has moved to the Waniwani platform and is no longer shipped in-SDK.

***

## 0.12.0: `addNode` object form

The positional `.addNode(id, run, options?)` signature is deprecated in favor of an options-bag form. Metadata sits at the top of the call where the eye lands, the handler is a named field, and future options can be added without widening a positional signature.

### Before

```ts theme={null}
flow
  .addNode("ask_family_details", ({ interrupt }) => {
    return interrupt({
      partnerName: { question: "What is your partner's name?" },
      numKids: { question: "How many children do you have?" },
    });
  }, { label: "Ask family details" });
```

### After

```ts theme={null}
flow.addNode({
  id: "ask_family_details",
  label: "Ask family details",
  run: ({ interrupt }) =>
    interrupt({
      partnerName: { question: "What is your partner's name?" },
      numKids: { question: "How many children do you have?" },
    }),
});
```

### What changed

* `id` replaces the first positional argument.
* `run` replaces the second positional argument (the handler).
* `label` and `hideFromFunnel` move from the third `options` argument onto the same config object.
* `label` is now optional. When omitted, it defaults to `id`. The positional form required `label` whenever you passed an `options` object.

### Compatibility

Both signatures are supported. The positional form is marked `@deprecated` and will be removed in **0.13.0**. You can mix the two within the same flow during the transition. There is no functional difference between the two shapes.

### Type export

A new `AddNodeConfig<TState, TName>` type is exported from `@waniwani/sdk/mcp` if you need to type a config object outside the call site.

```ts theme={null}
import type { AddNodeConfig } from "@waniwani/sdk/mcp";

const askEmail: AddNodeConfig<MyState, "ask_email"> = {
  id: "ask_email",
  run: ({ interrupt }) =>
    interrupt({ email: { question: "What's your email?" } }),
};

flow.addNode(askEmail);
```
