The Strands Agent Loop

Lesson 1 · Strands Agents (TypeScript) · ~12 minutes

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.

Language Support

The Strands Agents SDK is available natively in two languages:

LanguageStatusNotes
PythonMature (since May 2025)Full feature set, 25M+ downloads. Community tools package with 30+ tools.
TypeScriptv1.0 (April 2026)Full type safety, runs in Node.js and browser. 4 built-in vended tools.

Using tools written in other languages

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 transports

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.

What Strands Calls Things

The vocabulary maps cleanly to what you've built by hand:

Your hand-rolled loopStrands equivalent
The outer while loopAgent class + its internal loop
Tool definitions (JSON schema)tool() function with Zod schemas
Checking stop_reason from APIresult.stopReason on AgentResult
Conversation history arrayManaged by ConversationManager
System prompt stringsystemPrompt option on Agent
Model client (Anthropic SDK)BedrockModel or string model ID

The Loop Internals

Strands' agent loop follows this cycle on every invocation:

Input ──▶ ┌──────────────────────────────────────────┐ │ 1. Send messages to model │ │ 2. Model responds with text OR tool_use │ │ 3. If tool_use: execute, append result │ │ 4. Loop back to step 1 │ │ 5. If end_turn: return AgentResult │ └──────────────────────────────────────────┘ ──▶ Response

Key details that differ from a naive hand-rolled loop:

Key Insight

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.

Your First Strands Agent (TypeScript)

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.

Project setup

mkdir strands-hello && cd strands-hello
npm init -y
npm pkg set type=module
npm install @strands-agents/sdk

The agent

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)

Run it

npx tsx agent.ts
Credentials required

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.

What You Get For Free

Compare what you'd need to build yourself versus what Strands handles:

Hand-rolled

Strands gives you

Controlling the Loop

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
}
Production pattern

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.

Cancellation with AbortSignal

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() }
  },
})

The Mental Model

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:

┌─────────────────────────────────────────────────────────┐ │ Your Application │ ├─────────────────────────────────────────────────────────┤ │ Agent │ Tools (Zod) │ Hooks │ Plugins │ ├─────────────────────────────────────────────────────────┤ │ Strands Agent Loop │ │ (cycle management, limits, cancellation, error recovery)│ ├─────────────────────────────────────────────────────────┤ │ Model Provider (BedrockModel) │ │ (streaming, token counting, retries) │ ├─────────────────────────────────────────────────────────┤ │ AWS Bedrock / Claude Sonnet 4.6 │ └─────────────────────────────────────────────────────────┘

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.

When a Strands agent hits its turn limit, what happens to pending tool calls?
Correct. Limits fire at the top of each iteration — tools from the previous turn always run to completion before the check. The agent's message history stays valid and reinvokable.
Not quite. The limit check happens at the start of a new cycle, meaning any tools requested in the previous turn have already executed. This keeps the conversation history in a valid state.
What's the TypeScript-native way to cancel a Strands agent after a timeout?
Correct. Strands accepts standard AbortSignal instances via the cancelSignal option on invoke(). This composes with agent.cancel() — either can trigger cancellation independently.
The idiomatic TypeScript approach is AbortSignal.timeout(ms) passed as the cancelSignal option. Strands uses AbortSignal.any() internally to compose it with its own controller.
If a tool throws an error during execution, what does Strands do?
Correct. Strands wraps tool failures as error results rather than exceptions. This gives the model a chance to recover — try a different tool, adjust parameters, or inform the user. The loop continues.
Strands doesn't crash the loop on tool errors. It wraps the failure as an error tool result, letting the model see what went wrong and decide how to proceed. This is the same pattern you'd build into a robust hand-rolled loop.
📖 Primary Source

Agent Loop — Strands Agents Docs — The official deep dive on loop mechanics, stop reasons, cancellation checkpoints, limits, and concurrent invocations. ~10 min read.

💬 Questions? Ask me anything that's unclear. I can explain how Strands differs from your Go implementation, dive deeper into any stop reason, or help you set up your first real project.
Next →