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

# Getting started with Strands and AgentControl configs

This guide explains how to integrate [Strands Agents](https://strandsagents.com) with LaunchDarkly AgentControl. Using AgentControl configs with Strands lets you manage agent instructions, model configuration, and parameters outside of your application code.

This guide uses AgentControl's agent mode. Agent mode uses a single `instructions` string, which maps directly to Strands' `system_prompt`. To learn more, read [Agents in AgentControl](/docs/home/agentcontrol/agents).

<Tip>
  **New to AgentControl?**

  If you're new to AgentControl, start with the [Quickstart](/docs/home/agentcontrol/quickstart) and return to this guide when you are ready for a Strands-specific example.

  To learn more about AgentControl-specific SDKs, read [AI SDKs](/docs/sdk/ai). For Python-specific details, read the [Python AI SDK reference](/docs/sdk/ai/python).
</Tip>

<Info>
  **The Strands TypeScript SDK is in beta**

  The Strands TypeScript SDK is a pre-1.0 release candidate and only ships `BedrockModel` and `OpenAIModel`. It cannot run Anthropic-backed variations. If you want a single codebase that serves both OpenAI and Anthropic variations, use the Python SDK.

  The Node.js example in this guide uses an OpenAI-backed default variation so it works without targeting rules.
</Info>

## Prerequisites

To complete this guide, you must have the following prerequisites:

* A LaunchDarkly account, including:
  * A LaunchDarkly [SDK key](/docs/home/account/environment/keys#view-or-copy-sdk-credentials) for your environment.
  * A member role that allows [AgentControl actions](/docs/home/account/roles/role-actions#agentcontrol-config-actions). The LaunchDarkly project admin, maintainer, and developer project roles, as well as the admin and owner base roles, include this ability. To learn more about LaunchDarkly roles, read [Roles](/docs/home/account/roles).
* A Python 3.10+ or Node.js 20+ development environment.
* Strands Agents installed in your application.
* An API key for your chosen model provider.

## Concepts

Before you begin, review these key concepts.

### Strands agents

Strands provides a minimal, provider-agnostic framework for building tool-using agents. The `Agent` class accepts a `model`, a `system_prompt`, a list of `tools`, and an optional `conversation_manager`. It exposes `invoke_async` to run a single turn. The `SlidingWindowConversationManager` keeps the last N messages in memory so followup turns automatically reference earlier context without passing a thread or session ID.

### Agent mode AgentControl configs

Agent mode AgentControl configs use an `instructions` field instead of a messages array. This single instruction string serves as the system prompt for your agent. Agent mode is ideal for:

* Multi-step agent workflows
* Tool-using agents
* Persistent agent sessions

### The `agent_config` function

The `agent_config` function retrieves the AgentControl config variation for a given context. It returns an `AIAgentConfig` object that includes the customized instructions, model configuration, and a `tracker` property for recording metrics. Call this function each time you create an agent so LaunchDarkly can evaluate targeting and return the current configuration.

### Provider dispatch

Unlike LangChain, Strands does not currently have a first-party LaunchDarkly provider package. Each Strands model class is provider-specific and uses provider-specific names: `AnthropicModel` for Anthropic, `OpenAIModel` for OpenAI, and so on. To serve different providers from a single AgentControl config, dispatch on `agent_config.provider.name` and construct the matching Strands model class. This guide includes a `create_strands_model` helper that does this for you.

## Step 1: Install dependencies

Install the LaunchDarkly SDKs and Strands packages.

<CodeGroup>
  ```bash title="Python" lines wrap theme={null}
  pip install launchdarkly-server-sdk launchdarkly-server-sdk-ai strands-agents strands-agents-tools anthropic openai boto3 python-dotenv
  ```

  ```bash title="Node.js (server-side)" lines wrap theme={null}
  npm install @launchdarkly/node-server-sdk @launchdarkly/server-sdk-ai @strands-agents/sdk openai zod dotenv
  ```
</CodeGroup>

## Step 2: Create an AgentControl config in LaunchDarkly

Create an AgentControl config in agent mode to store your agent configuration. This guide creates two variations, one backed by OpenAI and one backed by Anthropic, to show you how Strands dispatches to different providers from the same AgentControl config key.

To create an AgentControl config:

1. In the left sidebar, click **Create** and select **Config**.
2. In the "Create config" dialog, select **Agent**.
3. Enter a name for your AgentControl config and set the key to `strands-agent`.
4. Click **Create**. The new AgentControl config appears.

<Frame caption="The &#x22;Create&#x22; menu options.">
  <img src="https://mintcdn.com/launchdarkly/-b7nPh0oyigf6iW7/images/auto/create-menu.auto.png?fit=max&auto=format&n=-b7nPh0oyigf6iW7&q=85&s=5b3fe40c4dd161b2972de2334ff19c0a" alt="The &#x22;Create&#x22; menu options." width="1180" height="440" data-path="images/auto/create-menu.auto.png" />
</Frame>

Then, create the first variation:

1. On the AgentControl config's **Variations** tab, replace "Untitled variation" with a variation name, such as "GPT-5 agent".
2. Click **Select a model** and choose the `gpt-5` OpenAI model.
3. Click **Parameters** and set `max_completion_tokens` to `2000`.
4. In the **Instructions** field, enter your agent's system prompt:

```lines wrap theme={null}
You are a helpful order-status assistant. Use the get_order_status tool to look up orders by their ID. Always explain your reasoning and summarize results clearly.
```

5. Click **Review and save**.

Add a second variation:

1. Click **Add variation** and name the new variation "Claude Sonnet agent".
2. Click **Select a model** and choose the `claude-sonnet-4` Anthropic model.
3. Click **Parameters** and set `max_tokens` to `2000`.
4. Use the same instructions as the first variation.
5. Click **Review and save**.

<Frame caption="A completed variation with model configuration and instructions.">
  <img src="https://mintcdn.com/launchdarkly/-b7nPh0oyigf6iW7/images/auto/ai-config-variation-complete.auto.png?fit=max&auto=format&n=-b7nPh0oyigf6iW7&q=85&s=e6b80ed68d6edb05d7f31ee72e6b4974" alt="A completed variation with model configuration and instructions." width="2070" height="638" data-path="images/auto/ai-config-variation-complete.auto.png" />
</Frame>

## Step 3: Set up targeting rules

Configure targeting rules to control which users receive which variation. Serve the "GPT-5 agent" variation as the default so the Node.js example runs without changes, and target specific users or segments to the "Claude Sonnet agent" variation.

To create the default rule:

1. Select the **Targeting** tab for your AgentControl config.
2. In the "Default rule" section, click **Edit**.
3. Configure the default rule to serve the "GPT-5 agent" variation.
4. Click **Review and save**.

<Frame caption="The default targeting rule configured to serve a variation.">
  <img src="https://mintcdn.com/launchdarkly/hutarVphEq2dY_zb/images/auto/guide-ai-config-model-config-update-default-targeting-rule.auto.png?fit=max&auto=format&n=hutarVphEq2dY_zb&q=85&s=b92bcc5a507600be0fd944fbd203113e" alt="The default targeting rule configured to serve a variation." width="2074" height="1138" data-path="images/auto/guide-ai-config-model-config-update-default-targeting-rule.auto.png" />
</Frame>

The AgentControl config is enabled by default. After you add the integration code to your application, LaunchDarkly serves the variation you configured to your users.

## Step 4: Integrate Strands with AgentControl configs

With the AgentControl config and targeting in place, integrate Strands with the LaunchDarkly AI SDK so your application fetches the current model, instructions, and parameters on every request instead of reading hardcoded values. Because Strands does not currently have a first-party LaunchDarkly provider package, the integration involves mapping the AgentControl config payload to the matching Strands model class yourself.

Complete these steps in order, since each depends on the previous one.

The integration involves these key steps:

1. Define the tools your agent can call using the Strands `@tool` decorator (Python) or `tool()` helper (Node.js).
2. Build a provider dispatcher that maps `agent_config.provider.name` to the matching Strands model class.
3. Initialize the LaunchDarkly base SDK client with your SDK key.
4. Initialize the LaunchDarkly AI client from the base client.
5. Get the agent config using `agent_config()` (Python) or `aiClient.agentConfig()` (Node.js).
6. Build a Strands `Agent` with a `SlidingWindowConversationManager` for short-term memory.
7. Invoke the agent and track metrics with the AgentControl config's tracker.

The following example defines a `get_order_status` tool that looks up a customer order by its ID. The tool handler returns the order status text your agent will summarize in its reply. In Python, the `@tool` decorator reads the function's type hints and docstring to generate the JSON schema Strands passes to the model. In Node.js, the `tool()` helper takes the name, description, and an explicit Zod input schema.

<CodeGroup>
  ```python title="Python" expandable lines wrap theme={null}
  from strands import tool


  @tool
  def get_order_status(order_id: str) -> str:
      """Look up the status of a customer order by order ID."""
      orders = {
          "ORD-123": "Shipped — arrives Thursday",
          "ORD-456": "Processing — estimated ship date: tomorrow",
          "ORD-789": "Delivered on Monday",
      }
      return orders.get(order_id, f"No order found with ID {order_id}")


  # Map tool keys (matching the LaunchDarkly tool keys) to local handlers. The agent
  # build step resolves the active tool list from `agent_config.tools` so detaching
  # `get_order_status` from the variation in LaunchDarkly takes effect on the next
  # agent invocation, with no code change.
  TOOL_REGISTRY = {"get_order_status": get_order_status}
  ```

  ```typescript title="Node.js" expandable lines wrap theme={null}
  import { tool } from '@strands-agents/sdk';
  import { z } from 'zod';
  import { type LDAIConfigTracker } from '@launchdarkly/server-sdk-ai';

  // Module-level tracker; reassigned per run inside `runTurn` so the tool callback
  // fires `trackToolCall` on the active execution. The SDK enforces at-most-once
  // tracking per tracker, so each turn needs its own `createTracker()`.
  let activeTracker: LDAIConfigTracker | null = null;

  const getOrderStatus = tool({
    name: 'get_order_status',
    description: 'Look up the status of a customer order by order ID',
    inputSchema: z.object({
      order_id: z.string().describe('The order ID to look up'),
    }),
    callback: ({ order_id }) => {
      activeTracker?.trackToolCall('get_order_status');
      const orders: Record<string, string> = {
        'ORD-123': 'Shipped — arrives Thursday',
        'ORD-456': 'Processing — estimated ship date: tomorrow',
        'ORD-789': 'Delivered on Monday',
      };
      return orders[order_id] || `No order found with ID ${order_id}`;
    },
  });

  // Map tool keys (matching the LaunchDarkly tool keys) to local handlers. The agent
  // build step resolves the active tool list from `agentConfig.model?.parameters?.tools`
  // so detaching `get_order_status` from the variation in LaunchDarkly takes effect on
  // the next agent invocation, with no code change.
  const TOOL_REGISTRY: Record<string, any> = { get_order_status: getOrderStatus };
  ```
</CodeGroup>

Build a provider dispatcher. Strands model classes are provider-specific, so read `agent_config.provider.name` and construct the matching class. LaunchDarkly surfaces attached tools via a flat `parameters.tools` shape in the variation payload. Drop that key before passing parameters through, because Strands receives tools from the `Agent` constructor.

<CodeGroup>
  ```python title="Python" expandable lines wrap theme={null}
  import os
  from strands.models.anthropic import AnthropicModel
  from strands.models.openai import OpenAIModel
  from strands.models.bedrock import BedrockModel


  def create_strands_model(agent_config):
      """Map an LDAIAgentConfig to the matching Strands model class by provider."""
      provider = (agent_config.provider.name if agent_config.provider else "").lower()
      model_id = agent_config.model.name
      params = dict(agent_config.model.to_dict().get("parameters") or {})
      # LaunchDarkly surfaces attached tools from `parameters.tools` in its own flat shape.
      # Drop the key here. Strands receives tools from the Agent constructor.
      params.pop("tools", None)

      is_bedrock = provider == "bedrock" or model_id.startswith(
          ("us.", "eu.", "apac.", "anthropic.", "amazon.", "meta.")
      )

      if is_bedrock:
          region = (
              params.pop("region_name", None)
              or os.environ.get("AWS_REGION")
              or "us-west-2"
          )
          known = {
              k: params.pop(k)
              for k in ("max_tokens", "temperature", "top_p", "stop_sequences")
              if k in params
          }
          if "max_tokens" not in known:
              known["max_tokens"] = 1024
          return BedrockModel(
              model_id=model_id,
              region_name=region,
              additional_request_fields=params or None,
              **known,
          )
      if provider == "anthropic":
          # AnthropicModel requires max_tokens as a kwarg, not in params.
          max_tokens = int(params.pop("max_tokens", None) or params.pop("maxTokens", None) or 1024)
          return AnthropicModel(model_id=model_id, max_tokens=max_tokens, params=params or None)
      if provider == "openai":
          # Pass parameters through unchanged. GPT-5 wants `max_completion_tokens`,
          # GPT-4o wants `max_tokens`. Keep that choice in the AgentControl config variation.
          return OpenAIModel(model_id=model_id, params=params)
      raise ValueError(f"Unsupported provider for Strands: {provider!r}")
  ```

  ```typescript title="Node.js" expandable lines wrap theme={null}
  import { AnthropicModel } from '@strands-agents/sdk/models/anthropic';
  import { OpenAIModel } from '@strands-agents/sdk/models/openai';
  import { type LDAIAgentConfig } from '@launchdarkly/server-sdk-ai';

  function createStrandsModel(agentConfig: LDAIAgentConfig) {
    const provider = agentConfig.provider?.name?.toLowerCase() ?? '';
    const modelId = agentConfig.model?.name ?? '';
    const params: Record<string, any> = { ...(agentConfig.model?.parameters ?? {}) };
    // LaunchDarkly surfaces attached tools from parameters.tools in its own flat shape.
    // Drop it. Strands receives tools from the Agent constructor.
    delete params.tools;

    if (provider === 'anthropic') {
      const maxTokens = Number(params.max_tokens ?? params.maxTokens ?? 1024);
      delete params.max_tokens;
      delete params.maxTokens;
      return new AnthropicModel({ modelId, maxTokens, params });
    }
    if (provider === 'openai') {
      // Pass params through unchanged. GPT-5 wants `max_completion_tokens`, GPT-4o wants `max_tokens`.
      return new OpenAIModel({ api: 'chat', modelId, params });
    }
    throw new Error(`Unsupported provider for Strands: ${provider}`);
  }
  ```
</CodeGroup>

Initialize the LaunchDarkly SDK and AI client, fetch the agent config, build the Strands model with `create_strands_model` (Python) or `createStrandsModel` (Node.js), and create the agent.

<CodeGroup>
  ```python title="Python" expandable lines wrap theme={null}
  import os
  import ldclient
  from ldclient import Context
  from ldclient.config import Config
  from ldai import LDAIClient
  from strands import Agent
  from strands.agent.conversation_manager.sliding_window_conversation_manager import (
      SlidingWindowConversationManager,
  )


  ldclient.set_config(Config(os.environ.get("LAUNCHDARKLY_SDK_KEY")))
  if not ldclient.get().is_initialized():
      raise RuntimeError("LaunchDarkly SDK failed to initialize")

  ai_client = LDAIClient(ldclient.get())

  context = Context.builder("user-123").kind("user").name("Sandy").build()

  # Pass a default for improved resiliency when the AgentControl config is unavailable
  # or LaunchDarkly is unreachable. Omit it to disable the default.
  # Example:
  #   from ldai import AIAgentConfigDefault
  #   default = AIAgentConfigDefault(
  #       enabled=True,
  #       model={"name": "gpt-5"},
  #       provider={"name": "openai"},
  #       instructions="You are a helpful assistant.",
  #   )
  #   agent_config = ai_client.agent_config("strands-agent", context, default)
  agent_config = ai_client.agent_config("strands-agent", context)
  if not agent_config.enabled:
      raise RuntimeError("Agent Config is disabled")

  model = create_strands_model(agent_config)

  # Resolve the agent's tool list from the LaunchDarkly variation. AIAgentConfig.tools
  # is the typed surface in SDK 0.20+ (a dict of name -> LDTool) — no more digging
  # through model.parameters.
  ld_tool_names = list(agent_config.tools or {})
  resolved_tools = [TOOL_REGISTRY[n] for n in ld_tool_names if n in TOOL_REGISTRY]

  # SlidingWindowConversationManager gives the agent short-term memory across turns.
  conversation_manager = SlidingWindowConversationManager(window_size=40)

  agent = Agent(
      name="order-assistant",
      model=model,
      system_prompt=agent_config.instructions,
      tools=resolved_tools,
      conversation_manager=conversation_manager,
  )
  ```

  ```typescript title="Node.js" expandable lines wrap theme={null}
  import { init, LDClient, LDContext } from '@launchdarkly/node-server-sdk';
  import { initAi, type LDAIClient, type LDAIAgentConfig } from '@launchdarkly/server-sdk-ai';
  import { Agent, SlidingWindowConversationManager } from '@strands-agents/sdk';

  const SDK_KEY = process.env.LAUNCHDARKLY_SDK_KEY;
  const CONFIG_KEY = 'strands-agent';

  const ldClient: LDClient = init(SDK_KEY);

  try {
    await ldClient.waitForInitialization({ timeout: 10 });
  } catch {
    console.error('LaunchDarkly SDK failed to initialize');
    return;
  }

  const aiClient: LDAIClient = initAi(ldClient);

  const context: LDContext = { kind: 'user', key: 'user-123', name: 'Sandy' };

  // Pass a default for improved resiliency when the AgentControl config is unavailable
  // or LaunchDarkly is unreachable. Omit it to disable the default.
  // Example:
  //   const fallback = {
  //     enabled: true,
  //     model: { name: 'gpt-5' },
  //     provider: { name: 'openai' },
  //     instructions: 'You are a helpful assistant.',
  //   };
  //   const agentConfig = await aiClient.agentConfig(CONFIG_KEY, context, fallback);
  const agentConfig: LDAIAgentConfig = await aiClient.agentConfig(CONFIG_KEY, context);

  if (!agentConfig.enabled || !agentConfig.instructions) {
    console.log('Agent Config is disabled — run the Python notebook first');
    await ldClient.close();
    return;
  }

  const model = createStrandsModel(agentConfig);

  // Resolve the agent's tool list from the active variation. LaunchDarkly returns
  // attached tools under `parameters.tools` in OpenAI function shape; we only need
  // the names to look up the local handlers from TOOL_REGISTRY.
  const ldToolParams = (agentConfig.model?.parameters as any)?.tools ?? [];
  const resolvedTools = ldToolParams
    .map((t: any) => TOOL_REGISTRY[t.name])
    .filter(Boolean);

  const agent = new Agent({
    model,
    systemPrompt: agentConfig.instructions,
    tools: resolvedTools,
    conversationManager: new SlidingWindowConversationManager({ windowSize: 40 }),
    printer: false,
  });
  ```
</CodeGroup>

Invoke the agent and track metrics. Each turn is one **execution** as far as the tracker is concerned: the SDK assigns a fresh `runId` per `create_tracker()` or `createTracker()` call and enforces at-most-once tracking for success, error, tokens, and duration. Build a new tracker inside `run_turn` for each invocation.

Strands returns an `AgentResult` whose `metrics.accumulated_usage` (Python) or `metrics.accumulatedUsage` (Node.js) aggregates token counts across every provider call in the turn, including any round trips to call tools. The Python example uses `tracker.track_metrics_of_async` with an extractor that returns an `LDAIMetrics` carrying token usage **and** the per-tool call list reconstructed from `metrics.tool_metrics`. The `@tool` handler stays a pure business function and the SDK fires one `track_tool_call` event per invocation when the turn completes. The Node.js example uses `tracker.trackMetricsOf` with a converter that returns the usage shape the tracker expects; tool calls are recorded from a `trackToolCall` call inside the tool callback, with the active tracker referenced via a module-level binding.

<CodeGroup>
  ```python title="Python" expandable lines wrap theme={null}
  from ldai.tracker import TokenUsage
  from ldai.providers import LDAIMetrics


  def strands_metrics_extractor(result):
      """Pull token usage and tool calls off a Strands AgentResult into an LDAIMetrics.

      Strands' EventLoopMetrics records every tool invocation in `tool_metrics`
      (keyed by tool name, with a per-tool `call_count`). Flattening that map into
      a list of tool keys lets the LaunchDarkly SDK fire one `track_tool_call`
      per invocation without the @tool body needing access to the tracker.
      """
      usage = getattr(result.metrics, "accumulated_usage", {}) or {}
      input_tokens = usage.get("inputTokens", 0)
      output_tokens = usage.get("outputTokens", 0)
      total = usage.get("totalTokens", 0) or (input_tokens + output_tokens)

      tool_calls = []
      for tool_name, tm in (result.metrics.tool_metrics or {}).items():
          tool_calls.extend([tool_name] * tm.call_count)

      return LDAIMetrics(
          success=True,
          tokens=TokenUsage(input=input_tokens, output=output_tokens, total=total) if total > 0 else None,
          tool_calls=tool_calls or None,
      )


  async def run_turn(agent, user_input):
      # Each tracker is one execution (its own runId, at-most-once for
      # success/error/tokens/duration/tool_call). Build a fresh tracker per turn.
      tracker = agent_config.create_tracker()
      try:
          result = await tracker.track_metrics_of_async(
              strands_metrics_extractor,
              lambda: agent.invoke_async(user_input),
          )
          print(f"Agent: {result.message['content'][0]['text']}")
      except Exception as e:
          print(f"Error: {e}")


  # Three turns on the same agent instance: first fires the tool, second reuses
  # conversation memory for a follow-up that reuses the tool, third summarizes
  # without calling any tool.
  await run_turn(agent, "What's the status of order ORD-123?")
  await run_turn(agent, "What about ORD-456?")
  await run_turn(agent, "Summarize both orders for me.")
  ```

  ```typescript title="Node.js" expandable lines wrap theme={null}
  import { type AgentResult } from '@strands-agents/sdk';

  function trackStrandsMetrics(result: AgentResult) {
    const usage = result.metrics?.accumulatedUsage;
    if (!usage) return { success: true };
    return {
      success: true,
      usage: {
        total: usage.totalTokens || usage.inputTokens + usage.outputTokens,
        input: usage.inputTokens,
        output: usage.outputTokens,
      },
    };
  }

  async function runTurn(input: string): Promise<void> {
    // Each tracker is one execution. Build a fresh tracker per turn
    // and publish it to `activeTracker` so the tool callback fires
    // `trackToolCall` on the same execution.
    activeTracker = agentConfig.createTracker();
    try {
      const result = await activeTracker.trackMetricsOf(
        trackStrandsMetrics,
        () => agent.invoke(input),
      );
      console.log(`Agent: ${result.toString()}`);
    } catch (error) {
      console.error(`Error: ${error}`);
    }
  }

  await runTurn("What's the status of order ORD-123?");
  await runTurn('What about ORD-456?');
  await runTurn('Summarize both orders for me.');
  ```
</CodeGroup>

The fallback argument to `agent_config` / `agentConfig` is optional. When omitted, LaunchDarkly returns a disabled config if the flag is off or the SDK is unreachable. Pass an explicit fallback to keep the agent running during outages.

## Complete example

Here is a complete working example that combines all the steps.

<Accordion title="Show full example code">
  <CodeGroup>
    ```python title="Python" expandable lines wrap theme={null}
    import asyncio
    import os
    import ldclient
    from ldclient import Context
    from ldclient.config import Config
    from ldai import LDAIClient
    from ldai.tracker import TokenUsage
    from ldai.providers import LDAIMetrics
    from strands import Agent, tool
    from strands.models.anthropic import AnthropicModel
    from strands.models.openai import OpenAIModel
    from strands.models.bedrock import BedrockModel
    from strands.agent.conversation_manager.sliding_window_conversation_manager import (
        SlidingWindowConversationManager,
    )
    from dotenv import load_dotenv

    load_dotenv()

    SDK_KEY = os.environ.get("LAUNCHDARKLY_SDK_KEY")
    AGENT_CONFIG_KEY = "strands-agent"

    @tool
    def get_order_status(order_id: str) -> str:
        """Look up the status of a customer order by order ID."""
        orders = {
            "ORD-123": "Shipped — arrives Thursday",
            "ORD-456": "Processing — estimated ship date: tomorrow",
            "ORD-789": "Delivered on Monday",
        }
        return orders.get(order_id, f"No order found with ID {order_id}")


    # Map tool keys (matching the LaunchDarkly tool keys) to local handlers. The agent
    # build step resolves the active tool list from `agent_config.tools` at runtime.
    TOOL_REGISTRY = {"get_order_status": get_order_status}


    def create_strands_model(agent_config):
        """Map an LDAIAgentConfig to the matching Strands model class by provider."""
        provider = (agent_config.provider.name if agent_config.provider else "").lower()
        model_id = agent_config.model.name
        params = dict(agent_config.model.to_dict().get("parameters") or {})
        # LaunchDarkly surfaces attached tools from `parameters.tools` in its own flat shape.
        # Drop the key here. Strands receives tools from the agent constructor.
        params.pop("tools", None)

        is_bedrock = provider == "bedrock" or model_id.startswith(
            ("us.", "eu.", "apac.", "anthropic.", "amazon.", "meta.")
        )

        if is_bedrock:
            region = (
                params.pop("region_name", None)
                or os.environ.get("AWS_REGION")
                or "us-west-2"
            )
            known = {
                k: params.pop(k)
                for k in ("max_tokens", "temperature", "top_p", "stop_sequences")
                if k in params
            }
            if "max_tokens" not in known:
                known["max_tokens"] = 1024
            return BedrockModel(
                model_id=model_id,
                region_name=region,
                additional_request_fields=params or None,
                **known,
            )
        if provider == "anthropic":
            # AnthropicModel requires max_tokens as a kwarg, not in params.
            max_tokens = int(params.pop("max_tokens", None) or params.pop("maxTokens", None) or 1024)
            return AnthropicModel(model_id=model_id, max_tokens=max_tokens, params=params or None)
        if provider == "openai":
            # Pass parameters through unchanged. GPT-5 wants `max_completion_tokens`,
            # GPT-4o wants `max_tokens`. Keep that choice in the LaunchDarkly variation.
            return OpenAIModel(model_id=model_id, params=params)
        raise ValueError(f"Unsupported provider for Strands: {provider!r}")


    def strands_metrics_extractor(result):
        """Pull token usage and tool calls off a Strands AgentResult into an LDAIMetrics."""
        usage = getattr(result.metrics, "accumulated_usage", {}) or {}
        input_tokens = usage.get("inputTokens", 0)
        output_tokens = usage.get("outputTokens", 0)
        total = usage.get("totalTokens", 0) or (input_tokens + output_tokens)

        tool_calls = []
        for tool_name, tm in (result.metrics.tool_metrics or {}).items():
            tool_calls.extend([tool_name] * tm.call_count)

        return LDAIMetrics(
            success=True,
            tokens=TokenUsage(input=input_tokens, output=output_tokens, total=total) if total > 0 else None,
            tool_calls=tool_calls or None,
        )


    async def async_main():
        ldclient.set_config(Config(SDK_KEY))
        if not ldclient.get().is_initialized():
            print("LaunchDarkly SDK failed to initialize")
            return

        ai_client = LDAIClient(ldclient.get())

        context = Context.builder("user-123").kind("user").name("Sandy").build()

        # Pass a default for improved resiliency when the AgentControl config is unavailable
        # or LaunchDarkly is unreachable. Omit it to disable the default.
        # Example:
        #   from ldai import AIAgentConfigDefault
        #   default = AIAgentConfigDefault(
        #       enabled=True,
        #       model={"name": "gpt-5"},
        #       provider={"name": "openai"},
        #       instructions="You are a helpful assistant.",
        #   )
        #   agent_config = ai_client.agent_config(AGENT_CONFIG_KEY, context, default)
        agent_config = ai_client.agent_config(AGENT_CONFIG_KEY, context)

        if not agent_config.enabled:
            print("Agent Config is disabled — run the notebook to set up the config")
            return

        model = create_strands_model(agent_config)

        # Resolve the agent's tool list from the LaunchDarkly variation. AIAgentConfig.tools
        # is the typed surface in SDK 0.20+ (a dict of name -> LDTool) — no more digging
        # through model.parameters.
        ld_tool_names = list(agent_config.tools or {})
        resolved_tools = [TOOL_REGISTRY[n] for n in ld_tool_names if n in TOOL_REGISTRY]

        # SlidingWindowConversationManager gives the agent short-term memory across turns.
        conversation_manager = SlidingWindowConversationManager(window_size=40)

        agent = Agent(
            name="order-assistant",
            model=model,
            system_prompt=agent_config.instructions,
            tools=resolved_tools,
            conversation_manager=conversation_manager,
        )

        async def run_turn(agent, user_input):
            # Fresh tracker per turn (each is one execution: own runId, at-most-once
            # for success/error/tokens/duration/tool_call). The extractor flattens
            # `result.metrics.tool_metrics` into the `tool_calls` field of LDAIMetrics,
            # which the SDK turns into one track_tool_call event per invocation.
            tracker = agent_config.create_tracker()
            try:
                result = await tracker.track_metrics_of_async(
                    strands_metrics_extractor,
                    lambda: agent.invoke_async(user_input),
                )
                print(f"Agent: {result.message['content'][0]['text']}")
            except Exception as e:
                print(f"Error: {e}")

        await run_turn(agent, "What's the status of order ORD-123?")
        await run_turn(agent, "What about ORD-456?")
        await run_turn(agent, "Summarize both orders for me.")

        # Always flush events before closing. Otherwise, trailing events are at risk of being
        # lost, in both short-lived scripts and long-running services.
        ldclient.get().flush()
        ldclient.get().close()


    def main():
        asyncio.run(async_main())


    if __name__ == "__main__":
        main()
    ```

    ```typescript title="Node.js" expandable lines wrap theme={null}
    import dotenv from 'dotenv';
    dotenv.config();
    import { init, LDClient, LDContext } from '@launchdarkly/node-server-sdk';
    import {
      initAi,
      type LDAIClient,
      type LDAIAgentConfig,
      type LDAIConfigTracker,
    } from '@launchdarkly/server-sdk-ai';
    import {
      Agent,
      tool,
      SlidingWindowConversationManager,
      type AgentResult,
    } from '@strands-agents/sdk';
    import { AnthropicModel } from '@strands-agents/sdk/models/anthropic';
    import { OpenAIModel } from '@strands-agents/sdk/models/openai';
    import { z } from 'zod';

    const SDK_KEY = process.env.LAUNCHDARKLY_SDK_KEY;
    const CONFIG_KEY = 'strands-agent';

    // Module-level tracker; reassigned per turn inside `runTurn`.
    let activeTracker: LDAIConfigTracker | null = null;

    const getOrderStatus = tool({
      name: 'get_order_status',
      description: 'Look up the status of a customer order by order ID',
      inputSchema: z.object({
        order_id: z.string().describe('The order ID to look up'),
      }),
      callback: ({ order_id }) => {
        activeTracker?.trackToolCall('get_order_status');
        const orders: Record<string, string> = {
          'ORD-123': 'Shipped — arrives Thursday',
          'ORD-456': 'Processing — estimated ship date: tomorrow',
          'ORD-789': 'Delivered on Monday',
        };
        return orders[order_id] || `No order found with ID ${order_id}`;
      },
    });

    // Map tool keys (matching the LaunchDarkly tool keys) to local handlers.
    const TOOL_REGISTRY: Record<string, any> = { get_order_status: getOrderStatus };

    function createStrandsModel(agentConfig: LDAIAgentConfig) {
      const provider = agentConfig.provider?.name?.toLowerCase() ?? '';
      const modelId = agentConfig.model?.name ?? '';
      const params: Record<string, any> = { ...(agentConfig.model?.parameters ?? {}) };
      // LaunchDarkly surfaces attached tools from parameters.tools in its own flat shape.
      // Drop it. Strands receives tools from the Agent constructor.
      delete params.tools;

      if (provider === 'anthropic') {
        const maxTokens = Number(params.max_tokens ?? params.maxTokens ?? 1024);
        delete params.max_tokens;
        delete params.maxTokens;
        return new AnthropicModel({ modelId, maxTokens, params });
      }
      if (provider === 'openai') {
        // Pass params through unchanged. GPT-5 wants `max_completion_tokens`, GPT-4o wants `max_tokens`.
        return new OpenAIModel({ api: 'chat', modelId, params });
      }
      throw new Error(`Unsupported provider for Strands: ${provider}`);
    }

    function trackStrandsMetrics(result: AgentResult) {
      const usage = result.metrics?.accumulatedUsage;
      if (!usage) return { success: true };
      return {
        success: true,
        usage: {
          total: usage.totalTokens || usage.inputTokens + usage.outputTokens,
          input: usage.inputTokens,
          output: usage.outputTokens,
        },
      };
    }

    async function main() {
      if (!SDK_KEY) {
        console.error('LAUNCHDARKLY_SDK_KEY not set');
        return;
      }

      const ldClient: LDClient = init(SDK_KEY);

      try {
        await ldClient.waitForInitialization({ timeout: 10 });
      } catch {
        console.error('LaunchDarkly SDK failed to initialize');
        return;
      }

      const aiClient: LDAIClient = initAi(ldClient);

      const context: LDContext = { kind: 'user', key: 'user-123', name: 'Sandy' };

      // Pass a default for improved resiliency when the AgentControl config is unavailable
      // or LaunchDarkly is unreachable. Omit it to disable the default.
      // Example:
      //   const fallback = {
      //     enabled: true,
      //     model: { name: 'gpt-5' },
      //     provider: { name: 'openai' },
      //     instructions: 'You are a helpful assistant.',
      //   };
      //   const agentConfig = await aiClient.agentConfig(CONFIG_KEY, context, fallback);
      const agentConfig: LDAIAgentConfig = await aiClient.agentConfig(CONFIG_KEY, context);

      if (!agentConfig.enabled || !agentConfig.instructions) {
        console.log('Agent Config is disabled — run the Python notebook first');
        await ldClient.close();
        return;
      }

      const model = createStrandsModel(agentConfig);

      // Resolve the agent's tool list from the active variation. LaunchDarkly returns
      // attached tools under `parameters.tools` in OpenAI function shape.
      const ldToolParams = (agentConfig.model?.parameters as any)?.tools ?? [];
      const resolvedTools = ldToolParams
        .map((t: any) => TOOL_REGISTRY[t.name])
        .filter(Boolean);

      const agent = new Agent({
        model,
        systemPrompt: agentConfig.instructions,
        tools: resolvedTools,
        conversationManager: new SlidingWindowConversationManager({ windowSize: 40 }),
        printer: false,
      });

      async function runTurn(input: string): Promise<void> {
        // Each tracker is one execution. Build a fresh tracker per turn
        // and publish it to `activeTracker` so the tool callback fires
        // `trackToolCall` on the same execution.
        activeTracker = agentConfig.createTracker();
        try {
          const result = await activeTracker.trackMetricsOf(
            trackStrandsMetrics,
            () => agent.invoke(input),
          );
          console.log(`Agent: ${result.toString()}`);
        } catch (error) {
          console.error(`Error: ${error}`);
        }
      }

      await runTurn("What's the status of order ORD-123?");
      await runTurn('What about ORD-456?');
      await runTurn('Summarize both orders for me.');

      // Always flush events before closing. Otherwise, trailing events are at risk of being
      // lost in both short-lived scripts and long-running services.
      await ldClient.flush();
      await ldClient.close();
    }

    main().catch((err) => {
      console.error('Fatal error:', err);
      process.exit(1);
    });
    ```
  </CodeGroup>
</Accordion>

## Step 5: Monitor results

View metrics for your AgentControl config in the LaunchDarkly UI.

To monitor results, navigate to your AgentControl config and click the **Monitoring** tab.

LaunchDarkly displays metrics including:

* Generation count
* Token usage (input, output, total)
* Time to generate
* Error rate

Use these metrics to compare agent performance across the OpenAI and Anthropic variations, identify cost differences, and make data-driven decisions about which configuration to use for different user segments. To learn more, read [Monitor AgentControl configs](/docs/home/agentcontrol/monitor).

To view aggregated metrics across all your AgentControl configs, navigate to **Insights** in the left sidebar under the **AI** section. The Insights overview page displays cost, latency, error rate, invocation counts, and model distribution across your organization. To learn more, read about [AI insights](/docs/home/agentcontrol/insights).

<Frame caption="The Insights overview page showing cost, latency, error rate, and invocation metrics for a Strands AgentControl config.">
  <img src="https://mintcdn.com/launchdarkly/Y_qcqLWSC5ccm6eB/images/__LD_UI_no_test/guide-ai-config-strands-insights.png?fit=max&auto=format&n=Y_qcqLWSC5ccm6eB&q=85&s=4e3ed13ff2267be0591bf312b30dc64b" alt="The Insights overview page showing cost, latency, error rate, and invocation metrics for a Strands AgentControl config." width="3382" height="1478" data-path="images/__LD_UI_no_test/guide-ai-config-strands-insights.png" />
</Frame>

## Comparing agent mode and completion mode

| Aspect | Agent Mode | Completion Mode |
| - | - | - |
| Config field | `instructions` (string) | `messages` (array) |
| SDK method | `agent_config()` | `completion_config()` |
| Default class | `AIAgentConfigDefault` | `AICompletionConfigDefault` |
| Use case | Multi-step workflows, tool use | Single-turn completions |

## Conclusion

In this guide, you learned how to integrate Strands Agents with LaunchDarkly AgentControl to manage agent configuration outside of your application code.

You can now:

* Change agent models and instructions without redeploying your application
* Swap between Anthropic and OpenAI-backed variations from a single AgentControl config key
* Target different agent configurations to different users based on context attributes
* Track and compare agent performance across variations
* Maintain multi-turn conversation memory with `SlidingWindowConversationManager`
* Govern tools centrally in LaunchDarkly and attach them to variations

To explore additional capabilities, read:

* [Run experiments with AgentControl](/docs/home/agentcontrol/experimentation) to compare agent variations using statistical analysis
* [Config targeting](/docs/home/agentcontrol/target) to serve different agents to different user segments
* [Agents in AgentControl](/docs/home/agentcontrol/agents) for a deeper look at agent mode

For more AgentControl examples, read the other [AgentControl guides](/docs/guides/agentcontrol) in this section.

<View title="Developer">
  <Note>
    **Want to know more? Start a trial.**

    Your 14-day trial begins as soon as you sign up. Get started in minutes using the in-app Quickstart. You'll discover how easy it is to release, monitor, and optimize your software.<br /><br />

    Want to try it out? <a href="https://app.launchdarkly.com/signup">Start a trial</a>.
  </Note>
</View>

<View title="Federal docs" />

<View title="EU docs" />
