You've built agentic loops from scratch. You know the cycle: send messages to the model, check if it wants to call a tool, execute the tool, feed the result back, repeat until the model says "done." Strands implements exactly this pattern — but gives you lifecycle hooks, invocation limits, cancellation, conversation management, and observability for free.
This lesson maps what you already know to Strands' TypeScript API. By the end, you'll have a running agent with a custom tool, configured for Bedrock with Claude Sonnet 4.6, with explicit control over how many cycles it can run.
The Strands Agents SDK is available natively in two languages:
| Language | Status | Notes |
|---|---|---|
| Python | Mature (since May 2025) | Full feature set, 25M+ downloads. Community tools package with 30+ tools. |
| TypeScript | v1.0 (April 2026) | Full type safety, runs in Node.js and browser. 4 built-in vended tools. |
While the agent orchestration itself runs in Python or TypeScript, your tools can be written in any language via MCP (Model Context Protocol). An MCP server is a standalone process that exposes tools over stdio or HTTP — it can be written in Go, Rust, Java, C#, or anything else that speaks the protocol.
// Connect to an MCP server written in any language
import { Agent } from '@strands-agents/sdk'
import { McpClient, StdioClientTransport } from '@strands-agents/sdk/mcp'
// This could be a Go binary, a Rust server, a Java JAR — anything
const mcpClient = new McpClient({
transport: new StdioClientTransport({
command: './my-tool-server', // Your binary in any language
args: ['--mode', 'mcp'],
}),
})
const agent = new Agent({
tools: [mcpClient], // Tools auto-discovered from the MCP server
})
await agent.invoke('Use the tools from my custom server')
This means: orchestrate in TypeScript, but delegate heavy lifting to tools in whatever language suits the task. A Go binary for file system operations, a Rust process for data parsing, a Java service for legacy system integration — all accessible as MCP tools.
MCP supports three transports: stdio (spawn a local process), Streamable HTTP (call a remote server), and SSE (server-sent events). Use stdio for local tools, HTTP for remote services.
Both SDKs share the same architecture and concepts. We're using TypeScript throughout this course. The Python SDK has a larger community tools ecosystem, but the core features (agent loop, multi-agent, hooks, plugins, streaming, structured output) are at parity.
The vocabulary maps cleanly to what you've built by hand:
| Your hand-rolled loop | Strands equivalent |
|---|---|
| The outer while loop | Agent class + its internal loop |
| Tool definitions (JSON schema) | tool() function with Zod schemas |
| Checking stop_reason from API | result.stopReason on AgentResult |
| Conversation history array | Managed by ConversationManager |
| System prompt string | systemPrompt option on Agent |
| Model client (Anthropic SDK) | BedrockModel or string model ID |
Strands' agent loop follows this cycle on every invocation:
Key details that differ from a naive hand-rolled loop:
invoke() call. The loop checks limits at the top of each cycle.agent.cancel() or pass an AbortSignal. The loop checks for cancellation at four checkpoints: top of cycle, during streaming, before tool execution, and between sequential tool calls.Stop reasons in Strands are richer than the raw API. Beyond end_turn and tool_use, you get limitTurns, limitTotalTokens, limitOutputTokens, cancelled, contentFiltered, and guardrailIntervention. These let you build deterministic budgets around non-deterministic model behavior.
Let's build a minimal agent that does something concrete: counts words in text. This maps directly to the tool-use pattern you've built before, but through Strands' API.
mkdir strands-hello && cd strands-hello
npm init -y
npm pkg set type=module
npm install @strands-agents/sdk
Create agent.ts:
import { Agent, tool } from '@strands-agents/sdk'
import { z } from '@strands-agents/sdk/zod'
// Define a tool — Zod schema replaces hand-written JSON schema
const wordCounter = tool({
name: 'word_counter',
description: 'Count words in a given text',
inputSchema: z.object({
text: z.string().describe('The text to count words in'),
}),
callback: async (input) => {
const count = input.text.trim().split(/\s+/).length
return { result: `${count} words` }
},
})
// Create the agent — Bedrock + Claude Sonnet 4.6
const agent = new Agent({
systemPrompt: 'You are a helpful text analysis assistant.',
model: 'us.anthropic.claude-sonnet-4-6-20250725-v1:0',
tools: [wordCounter],
})
// Invoke with an explicit turn limit
const result = await agent.invoke(
'How many words are in: "The quick brown fox jumps over the lazy dog"',
{ limits: { turns: 3 } }
)
console.log('Stop reason:', result.stopReason)
console.log('Response:', result.message?.content)
npx tsx agent.ts
This assumes AWS credentials configured for Bedrock access (via environment variables, ~/.aws/credentials, or IAM role). The model ID us.anthropic.claude-sonnet-4-6-20250725-v1:0 must be enabled in your Bedrock console for the configured region.
Compare what you'd need to build yourself versus what Strands handles:
The limits option is your primary lever for determinism. In a consulting context where you're running agents against client data, unbounded loops are unacceptable. Three caps:
const result = await agent.invoke('Analyze this codebase', {
limits: {
turns: 5, // Max 5 reason→act cycles
outputTokens: 2000, // Cap model output per invocation
totalTokens: 10000, // Cap total (input + output) tokens
},
})
// Check why the loop stopped
switch (result.stopReason) {
case 'endTurn':
// Normal completion — model finished its response
break
case 'limitTurns':
// Hit the turn budget — model wanted to keep going
break
case 'limitTotalTokens':
// Token budget exhausted
break
case 'cancelled':
// External cancellation (AbortSignal or agent.cancel())
break
}
Always set limits.turns in production. A model that enters a retry loop on a broken tool can burn through your token budget in seconds. Start with turns: 10 as a reasonable ceiling for most single-task agents.
This is the TypeScript-native pattern — pass standard AbortSignal instances for timeout or user-driven cancellation:
// Timeout after 30 seconds
const result = await agent.invoke('Summarize this report', {
cancelSignal: AbortSignal.timeout(30_000),
})
// Or with a manual controller (e.g., user clicks "Stop")
const controller = new AbortController()
someButton.onclick = () => controller.abort()
const result2 = await agent.invoke('Long analysis task', {
cancelSignal: controller.signal,
})
Tools can also participate in cancellation by forwarding the signal:
const fetchTool = tool({
name: 'fetch_url',
description: 'Fetch content from a URL',
inputSchema: z.object({ url: z.string().url() }),
callback: async (input, context) => {
// Forward cancel signal to fetch
const res = await fetch(input.url, {
signal: context?.agent.cancelSignal,
})
return { result: await res.text() }
},
})
Think of Strands as the framework layer on top of the loop you already understand. It doesn't change the fundamental mechanic — it standardizes the interfaces around it:
The value proposition for your workflow: you stop writing loop plumbing and start writing tools and prompts. The framework handles the mechanics. When you need to customize behavior (hook into pre/post tool execution, manage conversation windows, add guardrails), you extend rather than rewrite.
cancelSignal option on invoke(). This composes with agent.cancel() — either can trigger cancellation independently.AbortSignal.timeout(ms) passed as the cancelSignal option. Strands uses AbortSignal.any() internally to compose it with its own controller.Agent Loop — Strands Agents Docs — The official deep dive on loop mechanics, stop reasons, cancellation checkpoints, limits, and concurrent invocations. ~10 min read.