Skip to content

Add LaunchDarkly AI Configs integration sample for Strands#264

Open
sattensil wants to merge 4 commits into
strands-agents:mainfrom
sattensil:scarlett/launchdarkly-ai-configs
Open

Add LaunchDarkly AI Configs integration sample for Strands#264
sattensil wants to merge 4 commits into
strands-agents:mainfrom
sattensil:scarlett/launchdarkly-ai-configs

Conversation

@sattensil
Copy link
Copy Markdown

Summary

Adds a new sample under python/03-integrate/runtime-control/launchdarkly that drives a Strands agent entirely from LaunchDarkly AI Configs: model, instructions, parameters, tool list, and the multi-agent graph topology all live in LaunchDarkly.

The notebook is self-contained — it creates the project, AI Configs, variations, governed tool, default targeting, and an Agent Graph via the LaunchDarkly API on first run; re-running is idempotent.

Highlights:

  • create_strands_model maps an AIAgentConfig to the right Strands model class (OpenAIModel / AnthropicModel / BedrockModel) by reading config.provider.name with a Bedrock model-id fallback — no per-provider branching in the sample.
  • Three triage variations (OpenAI gpt-5, Anthropic Claude Sonnet 4.6, Bedrock Claude Sonnet 4.6) demonstrate provider swap via the LaunchDarkly UI.
  • The agent's tool list comes from config.model.parameters['tools'], resolved at runtime against a local TOOL_REGISTRY. Detaching a tool in LaunchDarkly takes effect on the next invocation with no code change.
  • Per-config tracking via tracker.track_metrics_of_async + an LDAIMetrics extractor fires duration / success / tokens atomically for async Strands invocations; track_tool_call fires from the @tool body.
  • execute_graph walks node.get_edges() at runtime, builds a Strands Agent generically per node from node.get_config(), and routes between agents based on edge.handoff.route values the LLM emits. Graph-level handoff success/failure + the final path are tracked via graph.create_tracker().
  • Routing protocol (JSON envelope, when to hand off) lives in each AI Config's instructions in LaunchDarkly; the builder only injects the runtime route list from edges. Adding a node + edge in the LaunchDarkly UI changes which agents fire on the next run.
  • Section 9 demos both branches: a status-only query terminates at the triage agent; an analysis query hands off to a strands-specialist-agent.

Test plan

  • pip install -r python/03-integrate/runtime-control/launchdarkly/requirements.txt
  • Create a .env with LAUNCHDARKLY_API_TOKEN, OPENAI_API_KEY, ANTHROPIC_API_KEY (per README)
  • Run the notebook top to bottom; verify section 1 creates the project, sections 2-4 create the variations + governed tool + default targeting, section 7 runs three multi-turn root-agent turns, section 8 creates the specialist + agent graph, section 9 runs both demo queries
  • Confirm the Agent Graph view in LaunchDarkly shows non-zero invocations on both nodes after section 9
  • Swap the default variation on strands-agent between gpt-5-agent, claude-sonnet-agent, bedrock-claude-agent in the LaunchDarkly UI and re-run section 9 — same code, different provider

sattensil added 4 commits May 13, 2026 17:11
A new sample under python/03-integrate/runtime-control/launchdarkly
demonstrating how to drive a Strands agent entirely from LaunchDarkly
AI Configs: model, instructions, parameters, tool list, and multi-agent
graph topology all live in LaunchDarkly. Provider, tools, and graph
shape can be changed from the LaunchDarkly UI with no code change.

The notebook is self-contained: it creates the LaunchDarkly project,
two AI Configs (triage + specialist), three triage variations (OpenAI
gpt-5, Anthropic Claude Sonnet 4.6, Bedrock Claude Sonnet 4.6), a
governed get_order_status tool attached per-variation, default
targeting, and an Agent Graph wiring triage -> specialist with handoff
metadata.

Highlights:

- create_strands_model maps an AIAgentConfig to the matching Strands
  model class (OpenAIModel / AnthropicModel / BedrockModel) by reading
  config.provider.name with a Bedrock model-id fallback.

- The agent's tool list comes from config.model.parameters['tools'],
  resolved against a local TOOL_REGISTRY at runtime. Detach a tool in
  the LaunchDarkly UI and the next invocation has no tools.

- Per-config tracking via config.create_tracker().track_metrics_of_async
  + an LDAIMetrics extractor fires duration / success / tokens atomically
  for async Strands invocations; tool-call counts come from the @tool body.

- execute_graph walks node.get_edges() at runtime, builds a Strands Agent
  per node generically from node.get_config(), and routes between agents
  based on edge.handoff.route values that the LLM selects from the valid
  set. Graph-level handoff success/failure + final path are tracked via
  graph.create_tracker().

- The routing protocol (JSON envelope, when to hand off) lives in each
  AI Config's instructions field in LaunchDarkly; the builder only
  injects the runtime list of available routes derived from edges.

- Section 9 demos both branches: a status-only query terminates at the
  triage agent; an analysis query hands off to the specialist.

Adding a node + edge in the LaunchDarkly UI changes which agents fire
on the next run with no code change — GRAPH_KEY is the only key the
dispatcher hardcodes.
If a prior cleanup cell closed the LDClient, re-running section 7 or
section 9 alone evaluated against a closed client and got stale 'last
known values' from the in-memory feature store — new nodes added in the
LD UI weren't visible because the cache predated them.

Both run_turn and execute_graph now check ldclient.get().is_initialized()
at entry. If not, they reinit via ldclient.set_config + rebuild the
LDAIClient/agent_config wrappers. Once initialized, the SDK streaming
connection keeps state live within ~1s of UI changes.
Two bugs kept the LDClient stuck on stale state across re-runs:
1. is_initialized() is a one-way latch — it stays True after close(),
   so the prior self-heal check skipped reinit and agent_graph served
   from the in-memory snapshot frozen at close() time.
2. The cleanup cell called close() at all. flush() is enough; kernel
   shutdown handles connection release.

Fix:
- Drop close() from the cleanup cell.
- Harden self-heal in run_turn + execute_graph to also probe the
  private _closed flag.
Brief operational tip pointing users at the self-heal behavior and the
'Restart Kernel + Run All' fallback after editing the graph in the LD UI.
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.

1 participant