Strands Agents TypeScript SDK

repository·main·Indexed 20 days ago

https://github.com/strands-agents/sdk-typescript

A model-driven TypeScript/JavaScript SDK for building and running AI agents. It features type-safe tool definitions, structured outputs via Zod, and support for both deterministic (Graph) and dynamic (Swarm) multi-agent orchestration. The SDK integrates with model providers like Amazon Bedrock and OpenAI, supports the Model Context Protocol (MCP), and includes vended tools such as a persistent Bash tool for shell command execution.

Tokens
56.8K
Snippets
182
Records
265
Agent score
71%

What's inside strands-agents-sdk-typescript

  1. Overview of available Strands Agents examples

    main

    The repository contains several specialized examples demonstrating different orchestration patterns and integrations:

    • first-agent: Demonstrates basic agent usage, including tool calling, the invoke method, and streaming response patterns.
    • graph: Shows multi-agent orchestration using graph structures (linear flows, fan-out patterns, and streaming).
    • swarm: Demonstrates swarm-based orchestration where agents drive handoffs between one another.
    • mcp: Shows integration with the Model Context Protocol (MCP) to connect with external tool servers.
    • agents-as-tools: Implements the pattern where an orchestrator agent delegates tasks to specialized agent-based tools.
    • browser-agent: A browser-based agent capable of DOM manipulation (supports OpenAI, Anthropic, and Bedrock).
    • telemetry: Demonstrates OpenTelemetry tracing integrated with Jaeger.
  2. Orchestrate multiple agents using the Swarm pattern

    main

    The Swarm pattern is used for dynamic, model-driven routing. Instead of a fixed graph, agents decide whether to produce a final response or hand off control to another agent. This makes the execution path flexible and determined by the LLM.

    import { Agent, BedrockModel, Swarm } from '@strands-agents/sdk'
    
    const model = new BedrockModel({ maxTokens: 1024 })
    
    const researcher = new Agent({
      model,
      id: 'researcher',
      description: 'Researches a topic and gathers key facts.',
      systemPrompt: 'Research the answer, then hand off to the writer.',
    })
    
    const writer = new Agent({
      model,
      id: 'writer',
      description: 'Writes a polished final answer.',
      systemPrompt: 'Write the final answer. Do not hand off.',
    })
    
    const swarm = new Swarm({
      nodes: [researcher, writer],
      start: 'researcher',
      maxSteps: 4,
    })
    
    const result = await swarm.invoke('What is the largest ocean?')
  3. How the strands-wasm architecture works

    main

    The system uses a WebAssembly (WASM) component architecture where the TypeScript SDK acts as the guest and Python acts as the host.

    1. The Guest (TS SDK): The TypeScript SDK is compiled into a WASM component (strands-agent.wasm). It handles the agent runtime, including the event loop, model provider HTTP calls (Bedrock, Anthropic, OpenAI, Gemini), tools, hooks, and streaming.
    2. The Host (Python): Python loads the WASM component via wasmtime-py and drives it.

    The WIT Contract (wit/agent.wit) defines the boundary:

    • Exports (TS $\rightarrow$ Python): The api interface allows Python to call into WASM for agent construction, streaming, and conversation management.
    • Imports (Python $\rightarrow$ TS): The WASM guest calls back into Python via tool-provider to execute Python-defined tools, and via host-log to route logs to Python's logging framework.
  4. Identify when to use peer dependencies

    main

    A dependency MUST be a peer dependency if it crosses an API boundary—specifically, if users are expected to construct or pass instances of that dependency into the SDK.

    For example, because users construct zod schemas and pass them into the tool function, zod is treated as a peer dependency.

    import { z } from 'zod'
    import { Agent, tool } from '@strands-agents/sdk'
    
    const calculator = tool({
      name: 'calculator',
      inputSchema: z.object({ value: z.number() }),
      callback: (input) => input.value * 2,
    })
    
    const agent = new Agent({ model, tools: [calculator] })
  5. How Bash Tool session persistence works

    main

    The Bash tool maintains a persistent session for each agent instance. This means that environment variables, functions, and working directory changes (using cd) persist across multiple tool calls within the same session. Each agent instance receives its own isolated bash session, which is automatically cleaned up when the agent is garbage collected.

    // Variables persist across commands in the same session
    res = await agent.invoke('run export "MY_VAR=hello"')
    res = await agent.invoke('run "echo $MY_VAR"')
    // res.lastMessage will show "hello"
  6. Follow naming conventions across WASM layers

    main

    Each layer in the WASM bridge uses a specific case convention. Adhering to these is critical for correct data mapping:

    LayerConventionExample
    WIT (wit/agent.wit)kebab-casewindow-size
    TS (strands-wasm/entry.ts)camelCasewindowSize
    Python (strands-py-wasm/)snake_casewindow_size

    Key Mapping Rules:

    • TS Layer: componentize-js automatically translates WIT kebab-case to JS camelCase. When accessing fields in entry.ts, use camelCase (e.g., cmConfig.windowSize maps to window-size).
    • Python Layer: wasmtime-py does not translate automatically. You must use kebab-case keys directly when building or reading WIT records in _wasm_host.py using _rec or getattr.
    # Building a WIT record in Python
    _rec(**{"window-size": 40, "should-truncate-results": True})
    
    # Reading a WIT record in Python
    getattr(rec, "window-size")
  7. Understand file ownership in the WASM bridge

    main

    When developing features that cross the WASM boundary, you must modify files across multiple layers. Use this mapping to identify which file owns which concern:

    FileOwnsWhen to modify
    wit/agent.witBoundary types and contractAdding new config fields, new WIT records, new resource methods, or new import/export interfaces
    strands-wasm/entry.tsConfig deserialization and TS SDK instantiationChanging how config is read from WIT and passed to TS SDK constructors, adding new createXxx() functions, or modifying stream event mapping
    strands-py-wasm/strands/_wasm_host.pyConfig serialization (Python → WIT) and WASM runtime managementAdding _build_xxx() serialization functions, modifying WasmAgent methods, or changing StreamEvent conversions
    strands-py-wasm/strands/agent/__init__.pyPython user-facing API and config extractionAdding/modifying constructor parameters or extracting config from Python class instances
    strands-py-wasm/strands/_conversions.pyData format conversions between layersModifying how StreamEvent dataclasses are converted to dicts, how TS messages are converted to Python format, or mapping lifecycle events

    Important: Do not edit these files manually:

    • strands-py-wasm/strands/_generated/types.py (Auto-generated from WIT)
    • strands-wasm/generated/ (Auto-generated WIT type bindings)
    • strands-wasm/build.js (Build pipeline)
    • strands-wasm/patches/getChunkedStream.js (WASI buffer reuse workaround)
  8. Choose between MockMessageModel and TestModelProvider for testing

    main

    When writing tests for the Strands SDK, select your model provider based on the level of granularity required:

    • MockMessageModel: Use this for agent loop tests and high-level flows. It is content-focused and designed to eliminate boilerplate by working with content blocks.
    • TestModelProvider: Use this for low-level event streaming tests where you need precise control over individual events in the stream.
  9. Understand Field to Getter/Setter Conversion Policy

    main

    The Strands TypeScript SDK may convert public mutable fields into properties with getter and setter methods during minor or patch releases. This change is not considered a breaking change because the syntax for accessing the property remains identical for the consumer.

    When this conversion occurs, the SDK may introduce:

    • Validation logic: Throwing errors if an invalid value is assigned.
    • Side effects: Such as logging, notifications, or internal state updates during assignment.
    • Computed values: Getters that return transformed or computed data.

    Your existing code using direct assignment (e.g., agent.model = newModel) or direct access (e.g., const m = agent.model) will continue to function without modification.

    // Before: Direct field access
    agent.model = newModel;
    const currentModel = agent.model;
    
    // After: Getter/setter (usage remains identical)
    agent.model = newModel;  // Calls setter
    const currentModel = agent.model;  // Calls getter
  10. Determine what to test in the SDK

    main

    Follow these rules to determine the scope of your testing:

    • Implementations: You MUST write tests for implementations (functions, classes, methods).
    • Interfaces: You SHOULD NOT write tests for interfaces, as the TypeScript compiler already enforces type correctness.
    • Complex Types: You SHOULD write Vitest type tests (*.test-d.ts) for complex types to ensure backwards compatibility.
  11. Decide where a new feature should run

    main

    Before implementing a feature, determine its execution context by answering these questions:

    1. Does it need to execute Python user code at runtime?

      • Yes: Requires a WIT import interface so the guest can call back to the host (see tool-provider in wit/agent.wit).
      • No: The feature runs entirely in the WASM guest.
    2. Is it configured at construction or invoked at runtime?

      • Construction: Use the Config holder pattern. The Python class stores config, which is serialized through WIT to the TS SDK.
      • Runtime: Requires WIT export methods on the agent or response-stream resource (see get-messages or set-messages).
    3. Is it a Plugin in the TS SDK?

      • Yes: Pass it via the appropriate Agent constructor field (e.g., conversationManager, plugins). The TS PluginRegistry calls initAgent() automatically.
      • No: Wire it directly in the AgentImpl constructor in entry.ts.
  12. Orchestrate multiple agents using the Graph pattern

    main

    The Graph pattern is used for deterministic execution. You define a set of agents as nodes and specify the execution order using edges. This allows for parallel execution and controlled workflows where downstream nodes run once dependencies are met.

    import { Agent, BedrockModel, Graph } from '@strands-agents/sdk'
    
    const model = new BedrockModel({ maxTokens: 1024 })
    
    const researcher = new Agent({
      model,
      id: 'researcher',
      systemPrompt: 'Research the topic and provide key facts.',
    })
    
    const writer = new Agent({
      model,
      id: 'writer',
      systemPrompt: 'Rewrite the research into a polished paragraph.',
    })
    
    const graph = new Graph({
      nodes: [researcher, writer],
      edges: [['researcher', 'writer']],
    })
    
    const result = await graph.invoke('What is the largest ocean?')