Skip to content

fix(migrate): roll forward to the new AI SDK - #218

Open
sattensil wants to merge 5 commits into
mainfrom
scarlett/sdk-roll-forward/migrate
Open

sattensil wants to merge 5 commits into
mainfrom
scarlett/sdk-roll-forward/migrate

Conversation

@sattensil

@sattensil sattensil commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Rewrite the five-stage migration around config().invoke() with handler packages or a custom handler, keeping the original provider call as the fallback when invoke() raises RuntimeError.
  • Remove tracker wiring: the SDK records duration, tokens, success/error, and tool calls. Stage 4 becomes "Verify metrics" plus manual feedback events.
  • Placeholders are {{name}} with no spaces; ldctx → ldContext. Tools move to tool_handlers.
  • Rewrite every reference: audit checklist, fallback patterns, before/after examples, metrics patterns, agent-mode frameworks (LangGraph, custom StateGraph, CrewAI, Strands, ReAct), and agent graphs (graph(), resolve_graph, to_lang_graph).

Jira: DEVREL-1644

Testing

  • Not applicable
  • Manual (describe below)

Ran before/after Example 1 against a live config: the served path and the no-SDK-key fallback path both returned. Checked every API name, signature, and package against the installed SDKs; all links return 200.

Notes

  • Targets launchdarkly-ai-server 0.3.0 (Python) and @launchdarkly/ai-node 0.3.0 (Node.js).
  • Part of a set of six roll-forward PRs (migrate, built-in-metrics, projects, tools, online-evals, custom-metrics). Each regenerates skills.json, so later merges may need python3 scripts/generate_catalog.py re-run.
  • The matching notebook in launchdarkly-labs/agentcontrol-cookbooks mirrors this skill's code; that update lands separately.

Note

Overview
Rolls the AgentControl migrate skill forward to the current AI SDKs (launchdarkly-ai-server / @launchdarkly/ai-node 0.3.0+), bumping the skill to 0.2.0 in skills.json and rewriting the five-stage flow and all migrate reference docs.

The workflow now centers on config().invoke() with handler packages or custom handlers, tool_handlers for tools, and {{name}} placeholders (no spaces). Stage 4 is verify metrics instead of wiring create_tracker() / track_* — the SDK records duration, tokens, success/error, and tool calls automatically. Fallbacks are app-owned: keep the original hardcoded call and run it on RuntimeError, replacing *ConfigDefault passed into completion_config / agent_config.

Adds explicit guidance for migrating off the legacy AI SDK (ldai, completion_config, trackers), moves configs-targeting immediately after config creation, and refreshes LangGraph/CrewAI/Strands patterns, before/after examples, fallback docs, metrics mapping, and agent graphs (graph(), resolve_graph, to_lang_graph) without manual graph trackers.

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

Rewrite the migration around config().invoke() with handler packages or custom handlers, keeping the original call as the fallback. Remove tracker wiring (the SDK records metrics), switch placeholders to {{name}} without spaces, pass tools as tool_handlers, and update the agent-mode, agent-graph, fallback, audit, and before/after references for launchdarkly-ai-server 0.3.0 and @launchdarkly/ai-node 0.3.0.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 471a3a7. Configure here.

Comment thread skills/agentcontrol/migrate/SKILL.md Resolved
@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Skill eval results

Skill Before After Δ
agentcontrol/configs-create 100/100 (4/4) 75/100 (3/4) -25
agentcontrol/configs-update 80/100 (4/5) 80/100 (4/5) no change
agentcontrol/configs-variations 80/100 (4/5) 80/100 (4/5) no change
agentcontrol/tools 75/100 (3/4) 75/100 (3/4) no change
feature-flags/flag-and-release-change - 100/100 (4/4) new
feature-flags/flag-release - 100/100 (5/5) new
feature-flags/launchdarkly-flag-command - 100/100 (3/3) new
feature-flags/launchdarkly-flag-create 100/100 (3/3) 100/100 (4/4) no change
feature-flags/launchdarkly-flag-drift - 100/100 (4/4) new
feature-flags/should-flag-change - 100/100 (17/17) new
onboarding - 100/100 (4/4) new

Only suites whose source actually changed since their last recorded score were re-run. Soft-failing while we stabilise the baseline.

Raise a dedicated exception at the ReAct step limit so the RuntimeError fallback doesn't swallow it, accept sync or async tool functions in the StateGraph example, and drop tools from parameters in the LangChain factory example.
@sattensil
sattensil requested a review from jeffdupont October 23, 2026 20:17

@jeffdupont jeffdupont left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Its core fallback, except RuntimeError, also catches failures from the SDK's own handlers after they have already run tools. Its install step leaves out launchdarkly-server-sdk, so every request silently takes the fallback.


skills/agentcontrol/migrate/references/phase-1-analysis-checklist.md:143 — launchdarkly-ai-server doesn't bundle the server SDK. On PyPI its only required dependency is opentelemetry-api, and none of the handler packages pull it in either. Without launchdarkly-server-sdk, init_client() raises RuntimeError("LaunchDarkly server SDK not installed...") (python-ai-sdk lifecycle.py:271-280). In this skill that error lands in the except RuntimeError fallback, so a migrated app silently runs the hardcoded path on every request and the Monitoring tab stays empty. Suggest: launchdarkly-ai-server + launchdarkly-server-sdk (required peer). Same fix for the install list at SKILL.md:160. The ai-node row is correct as written.


skills/agentcontrol/migrate/SKILL.md:406 — "Catch RuntimeError specifically; provider errors are different exception types" doesn't hold for the handler packages this skill recommends. They raise RuntimeError themselves after the run has started:

  • Tool loop exceeded the maximum number of steps (10) in openai-messages, claude-messages and langchain-messages
  • Claude agent run ended with ... in claude-agents

The SDK re-raises these unchanged. So an agent that hits the step cap after calling a side-effecting tool falls into this fallback, which re-runs the hardcoded agent and calls the tool again. The AgentStepLimitError fix in 71ce24e covers only the custom-loop example.

Smallest fix, on the error path only: inspect_config never raises, so re-raise when the config was actually served:

except RuntimeError:
    if (await inspect_config("chat-assistant", context))["enabled"]:
        raise
    return answer_hardcoded(user_input)

The same pattern applies at SKILL.md:24, SKILL.md:234, references/fallback-defaults-pattern.md:9,42, references/before-after-examples.md:75,221 and references/agent-graph-reference.md (the graph except (RuntimeError, ValueError)). For Node.js (fallback-defaults-pattern.md:60, before-after-examples.md:155), "anything that isn't a provider error" also catches Error('Tool loop exceeded...') and tool-handler errors. The same inspectConfig check works there.


skills/agentcontrol/migrate/SKILL.md:267 — On launchdarkly-ai-server 0.4.0, a list works too: handler_key() normalizes list and tuple tags (python-ai-sdk #152). Only 0.3.0 needed a tuple. Every handler package on PyPI requires launchdarkly-ai-server>=0.3.1, so anyone who installs a handler today gets 0.4.0. Suggest dropping this sentence, keeping the tuple in the example, and bumping the compatibility line to 0.4.0+.


skills/agentcontrol/migrate/SKILL.md:421 — This contradicts the caveat at :241. On 0.2.5, openai-messages forwards no parameters and claude-messages forwards only max_tokens, so neither can crash on max_search_results. Only the LangChain handler or a custom handler passes raw params through. python-ai-sdk #107 (merged, unreleased) and js-ai-sdk #118 change every handler to forward a fixed allowlist and drop other keys. The advice to use model.custom stays right, but the reason should be "handlers won't pass it to your code, and custom/LangChain handlers would send it to the provider." Suggest tying the :241 caveat to versions: "openai-messages ≤ 0.2.5 / claude-messages ≤ 0.2.5; fixed by python-ai-sdk #107 in the next release." The same rewording applies at agent-mode-frameworks.md:86, phase-1-analysis-checklist.md:108 and before-after-examples.md:88.

Comment thread skills/agentcontrol/migrate/SKILL.md Outdated
messages = [
{**m, "content": parse_template(m["content"], variables)} for m in ld_config.get("messages", [])
]
response = openai_client.chat.completions.create(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Should fix. call_openai is async, but it calls the sync openai_client.chat.completions.create(...). Every request holds the event loop for the whole model call, so concurrent requests in an async server run one at a time. before-after-examples.md:59 already uses AsyncOpenAI with await. Use the same here: response = await async_openai.chat.completions.create(...).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 31e10a3. The example creates an AsyncOpenAI client and awaits the call.

3. **Build a `Context` per request.** The context carries targeting inputs — user ID, plan tier, region, whatever the rollout is keyed on. Reuse the same context the app already uses for feature flag evaluation if one exists.
4. **Always check `config.enabled`.** Even a successful `completion_config` call can return a disabled config (if the variation is turned off in LaunchDarkly). The disabled path should not call the provider.
5. **Do not cache the config object across requests.** Call `completion_config` / `agent_config` inside the request handler so LaunchDarkly can re-evaluate targeting per call. One `LDAIClient` instance, many `completion_config` calls.
1. **The request handler's signature and return shape stay the same.** Only where the model, prompt, and parameters come from changes, plus the `try` / `except RuntimeError` around `invoke()`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Should fix. Rule 1 says the request handler's signature stays the same, and SKILL.md:92 says the same. But every example turns a sync answer(user_question) into async def answer(user_id, user_question) (:18 → :71, and SKILL.md:210 → :230). Every existing caller breaks: a sync caller now gets a coroutine back, and every caller has to supply user_id. An agent following the rule won't update the callers. Suggest saying it plainly: "The call site becomes async and needs a context source (user id); update every caller, or keep a sync wrapper with asyncio.run for scripts."

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 31e10a3. Core principle 2 and rule 1 now say the call site becomes async and needs a context source, and to update every caller or keep an asyncio.run() wrapper for scripts.

Comment thread skills/agentcontrol/migrate/SKILL.md Outdated
tracker.track_judge_result(result)
result = await config(key="chat-assistant", handler=handlers, skip_judges=True).invoke(user_input, context)
for task in result.judge_tasks or []:
asyncio.create_task(run_judge_worker(task))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Should fix. The event loop keeps only a weak reference to tasks, so a fire-and-forget create_task(run_judge_worker(task)) can be garbage-collected before it finishes, and that judge score is lost. The asyncio docs carry this warning for create_task. Smallest fix: keep the tasks in a set, as in the docs: t = asyncio.create_task(...); _bg.add(t); t.add_done_callback(_bg.discard). Or point at the online-evals worker instead of inlining this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 31e10a3. The tasks go into a set with add_done_callback(discard), as in the asyncio docs.

Comment thread skills/agentcontrol/migrate/SKILL.md Outdated
4. **Wire feedback tracking if the app has thumbs-up/down UI.** Both SDKs expose `trackFeedback` with a `{kind}` argument.
1. **Check the Monitoring tab.** Hit the wrapped endpoint in staging, then open the config in LaunchDarkly → **Monitoring**. Duration, token, and generation counts should appear within 1–2 minutes. Force an error (bad provider key) and confirm the error count increments; the SDK records the error and re-raises.
2. **If tokens are missing**, the handler isn't returning usage. Custom handlers must return `{"output": ..., "usage": {"input": n, "output": n}}`. For streaming, enable usage on the final chunk (`stream_options={"include_usage": True}` for OpenAI).
3. **Wire feedback if the app has thumbs-up/down UI.** The SDK doesn't record feedback. Store the result's `track_data` with the message and send the event when the user responds:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nice to have. Neither SDK's stream() done event carries track_data today: Python yields response, usage and judge_results (client.py:272-277), and JS yields response, usage and judgeResults. A streaming app with thumbs-up/down has nothing to attribute feedback with. Worth one sentence saying so, since the coverage table sends streaming apps to stream().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added in 31e10a3. The feedback step now says stream() doesn't return track_data yet.

…judges

Use AsyncOpenAI in the custom handler, say plainly that call sites become async and need a context source, keep references to background judge tasks, note that stream() has no track_data, and set custom knobs with the MCP variation tool instead of curl.
sattensil added a commit that referenced this pull request Oct 26, 2026
#218 and #219 rewrite these files and already use the new tool names, so renaming them here only conflicts.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants