diff --git a/src/config/navigation.yml b/src/config/navigation.yml
index 0485cac61..d528e2079 100644
--- a/src/config/navigation.yml
+++ b/src/config/navigation.yml
@@ -119,6 +119,7 @@ sidebar:
- docs/user-guide/concepts/bidirectional-streaming/interruption
- docs/user-guide/concepts/bidirectional-streaming/hooks
- docs/user-guide/concepts/bidirectional-streaming/session-management
+ - docs/user-guide/concepts/local-development
- label: Experimental
items:
- docs/user-guide/concepts/experimental/agent-config
diff --git a/src/content/docs/user-guide/concepts/local-development.mdx b/src/content/docs/user-guide/concepts/local-development.mdx
new file mode 100644
index 000000000..c1a256082
--- /dev/null
+++ b/src/content/docs/user-guide/concepts/local-development.mdx
@@ -0,0 +1,401 @@
+---
+title: Local Development & Mock Mode
+sidebar:
+ label: "Local Development"
+---
+
+When developing agents with the Strands Agents SDK, you don't always need real API credentials. Mock model providers let you build, iterate, and test agent logic locally — without calling Bedrock, OpenAI, or any other remote service.
+
+This guide covers how to create mock models for local development and testing, simulate common scenarios like tool calls and errors, and optionally use a local LLM backend for more realistic testing.
+
+## Creating a Mock Model
+
+A mock model implements the same `Model` interface as any real provider but returns pre-configured responses instead of calling an API. This is the simplest way to develop and test agent logic offline.
+
+
+
+
+```python
+from typing import Any, AsyncIterable, Optional
+from typing_extensions import Unpack
+
+from strands.models import Model
+from strands.types.content import Messages
+from strands.types.streaming import StreamEvent
+from strands.types.tools import ToolSpec
+
+
+class MockModel(Model):
+ """A simple mock model for local development and testing."""
+
+ class ModelConfig:
+ model_id: str = "mock-model"
+
+ def __init__(self, responses: list[str] | None = None) -> None:
+ self.responses = responses or ["Hello! I am a mock response."]
+ self.call_index = 0
+ self.config = {"model_id": "mock-model"}
+
+ def update_config(self, **kwargs: Any) -> None:
+ self.config.update(kwargs)
+
+ def get_config(self) -> dict:
+ return self.config
+
+ async def stream(
+ self,
+ messages: Messages,
+ tool_specs: Optional[list[ToolSpec]] = None,
+ system_prompt: Optional[str] = None,
+ **kwargs: Any,
+ ) -> AsyncIterable[StreamEvent]:
+ text = self.responses[self.call_index % len(self.responses)]
+ self.call_index += 1
+
+ yield {"messageStart": {"role": "assistant"}}
+ yield {"contentBlockStart": {"start": {}}}
+ yield {"contentBlockDelta": {"delta": {"text": text}}}
+ yield {"contentBlockStop": {}}
+ yield {"messageStop": {"stopReason": "end_turn"}}
+```
+
+
+
+```typescript
+--8<-- "user-guide/concepts/local-development.ts:basic_mock_model"
+```
+
+
+
+### Using the Mock Model
+
+Pass the mock model to an agent just like any real provider:
+
+
+
+
+```python
+from strands import Agent
+
+mock_model = MockModel(responses=["The capital of France is Paris."])
+agent = Agent(model=mock_model)
+
+response = agent("What is the capital of France?")
+print(response)
+```
+
+
+
+```typescript
+--8<-- "user-guide/concepts/local-development.ts:use_mock_model"
+```
+
+
+
+## Environment Variable Swap Pattern
+
+A common pattern is to swap between mock and real models using an environment variable. This lets you develop locally without changing any application code — just set `MOCK_MODE=true`:
+
+
+
+
+```python
+import os
+from strands import Agent
+from strands.models.bedrock import BedrockModel
+
+
+def create_model():
+ if os.environ.get("MOCK_MODE") == "true":
+ return MockModel(responses=["This is a mock response for local development."])
+ return BedrockModel(model_id="anthropic.claude-sonnet-4-20250514")
+
+
+model = create_model()
+agent = Agent(model=model)
+```
+
+Run locally with mock mode:
+
+```bash
+MOCK_MODE=true python my_agent.py
+```
+
+
+
+```typescript
+--8<-- "user-guide/concepts/local-development.ts:env_swap_pattern"
+```
+
+Run locally with mock mode:
+
+```bash
+MOCK_MODE=true npx tsx my_agent.ts
+```
+
+
+
+## Simulating Tool Calls
+
+To test agent tool-use loops, your mock model needs to return tool use events on the first call and text events after the tool result is returned:
+
+
+
+
+```python
+class MockToolModel(Model):
+ """A mock model that simulates tool calls."""
+
+ def __init__(self) -> None:
+ self.config = {"model_id": "mock-tool-model"}
+
+ def update_config(self, **kwargs: Any) -> None:
+ self.config.update(kwargs)
+
+ def get_config(self) -> dict:
+ return self.config
+
+ async def stream(
+ self,
+ messages: Messages,
+ tool_specs: Optional[list[ToolSpec]] = None,
+ system_prompt: Optional[str] = None,
+ **kwargs: Any,
+ ) -> AsyncIterable[StreamEvent]:
+ # Check if the last message contains a tool result
+ last_message = messages[-1] if messages else None
+ has_tool_result = last_message and any(
+ "toolResult" in block for block in last_message.get("content", [])
+ )
+
+ yield {"messageStart": {"role": "assistant"}}
+
+ if has_tool_result:
+ # Respond with final text after tool execution
+ yield {"contentBlockStart": {"start": {}}}
+ yield {
+ "contentBlockDelta": {
+ "delta": {"text": "The weather in Seattle is 72°F and sunny."}
+ }
+ }
+ yield {"contentBlockStop": {}}
+ yield {"messageStop": {"stopReason": "end_turn"}}
+ else:
+ # Simulate a tool call
+ yield {
+ "contentBlockStart": {
+ "start": {
+ "toolUseId": "tool_001",
+ "name": "get_weather",
+ }
+ }
+ }
+ yield {
+ "contentBlockDelta": {
+ "delta": {"toolUse": {"input": '{"location": "Seattle"}'}}
+ }
+ }
+ yield {"contentBlockStop": {}}
+ yield {"messageStop": {"stopReason": "tool_use"}}
+```
+
+
+
+```typescript
+--8<-- "user-guide/concepts/local-development.ts:mock_tool_calls"
+```
+
+
+
+## Simulating Errors
+
+Testing error handling is critical. Create a mock model that throws errors to verify your agent handles failures gracefully:
+
+
+
+
+```python
+class MockErrorModel(Model):
+ """A mock model that simulates errors for testing error handling."""
+
+ def __init__(self, should_fail: bool = True) -> None:
+ self.should_fail = should_fail
+ self.config = {"model_id": "mock-error-model"}
+
+ def update_config(self, **kwargs: Any) -> None:
+ self.config.update(kwargs)
+
+ def get_config(self) -> dict:
+ return self.config
+
+ async def stream(
+ self,
+ messages: Messages,
+ tool_specs: Optional[list[ToolSpec]] = None,
+ system_prompt: Optional[str] = None,
+ **kwargs: Any,
+ ) -> AsyncIterable[StreamEvent]:
+ if self.should_fail:
+ raise RuntimeError("Simulated API failure: rate limit exceeded")
+
+ yield {"messageStart": {"role": "assistant"}}
+ yield {"contentBlockStart": {"start": {}}}
+ yield {"contentBlockDelta": {"delta": {"text": "Success!"}}}
+ yield {"contentBlockStop": {}}
+ yield {"messageStop": {"stopReason": "end_turn"}}
+```
+
+
+
+```typescript
+--8<-- "user-guide/concepts/local-development.ts:mock_errors"
+```
+
+
+
+## Simulating Multi-Turn Conversations
+
+For testing multi-turn conversation flows, create a mock model with scripted responses for each turn:
+
+
+
+
+```python
+class MockMultiTurnModel(Model):
+ """A mock model with pre-scripted multi-turn responses."""
+
+ def __init__(self, turns: list[str]) -> None:
+ self.turns = turns
+ self.turn_index = 0
+ self.config = {"model_id": "mock-multi-turn-model"}
+
+ def update_config(self, **kwargs: Any) -> None:
+ self.config.update(kwargs)
+
+ def get_config(self) -> dict:
+ return self.config
+
+ async def stream(
+ self,
+ messages: Messages,
+ tool_specs: Optional[list[ToolSpec]] = None,
+ system_prompt: Optional[str] = None,
+ **kwargs: Any,
+ ) -> AsyncIterable[StreamEvent]:
+ if self.turn_index >= len(self.turns):
+ raise RuntimeError("All scripted turns have been consumed")
+
+ text = self.turns[self.turn_index]
+ self.turn_index += 1
+
+ yield {"messageStart": {"role": "assistant"}}
+ yield {"contentBlockStart": {"start": {}}}
+ yield {"contentBlockDelta": {"delta": {"text": text}}}
+ yield {"contentBlockStop": {}}
+ yield {"messageStop": {"stopReason": "end_turn"}}
+
+
+# Usage
+multi_turn_model = MockMultiTurnModel(turns=[
+ "Hi! How can I help you?",
+ "Sure, I can help with that.",
+ "Here is the result you asked for.",
+])
+agent = Agent(model=multi_turn_model)
+
+agent("Hello")
+agent("Can you help me?")
+agent("Show me the result")
+```
+
+
+
+```typescript
+--8<-- "user-guide/concepts/local-development.ts:mock_multi_turn"
+```
+
+
+
+## Using a Local LLM Backend
+
+For more realistic local development, you can run a local LLM instead of using mock responses. Strands Agents has built-in support for popular local LLM servers.
+
+### Ollama
+
+[Ollama](https://ollama.ai) lets you run open-source models locally. Strands provides a native `OllamaModel` provider:
+
+
+
+
+```bash
+# Install Ollama and pull a model
+brew install ollama # macOS
+ollama pull llama3.2
+
+# Install the Strands Ollama extra
+pip install 'strands-agents[ollama]'
+```
+
+```python
+from strands import Agent
+from strands.models.ollama import OllamaModel
+
+model = OllamaModel(
+ model_id="llama3.2",
+ host="http://localhost:11434",
+)
+
+agent = Agent(model=model)
+response = agent("Explain quantum computing in simple terms")
+```
+
+For complete Ollama setup and configuration, see the [Ollama model provider](./model-providers/ollama.md) documentation.
+
+
+
+Ollama support in the TypeScript SDK is not yet available as a built-in provider. You can use Ollama through the [Custom Model Provider](./model-providers/custom_model_provider.md) interface, or use the `MOCK_MODE` pattern above for local development.
+
+
+
+### llama.cpp
+
+[llama.cpp](https://github.com/ggerganov/llama.cpp) provides a lightweight C++ inference engine. Strands supports it natively:
+
+
+
+
+```bash
+# Install the Strands llama.cpp extra
+pip install 'strands-agents[llamacpp]'
+```
+
+```python
+from strands import Agent
+from strands.models.llamacpp import LlamaCppModel
+
+model = LlamaCppModel(
+ host="http://localhost:8080",
+ model_id="local-model",
+)
+
+agent = Agent(model=model)
+response = agent("What are the benefits of local LLM development?")
+```
+
+For complete llama.cpp setup instructions, see the [llama.cpp model provider](./model-providers/llamacpp.md) documentation.
+
+
+
+llama.cpp support in the TypeScript SDK is not yet available as a built-in provider. You can use llama.cpp through the [Custom Model Provider](./model-providers/custom_model_provider.md) interface to connect to the llama.cpp HTTP server.
+
+
+
+## Choosing Your Approach
+
+| Approach | Best for | Trade-offs |
+|---|---|---|
+| **Mock model** | Unit tests, CI/CD, offline development | Fast and deterministic, but no real LLM behavior |
+| **Env var swap** | Switching between local and production | Zero code changes, easy to integrate |
+| **Ollama / llama.cpp** | Integration testing, realistic local dev | Real LLM responses, but requires local setup and GPU |
+
+For most development workflows, start with a mock model for fast iteration and testing, then validate with a local LLM or real API before deploying.
diff --git a/src/content/docs/user-guide/concepts/local-development.ts b/src/content/docs/user-guide/concepts/local-development.ts
new file mode 100644
index 000000000..ce3584696
--- /dev/null
+++ b/src/content/docs/user-guide/concepts/local-development.ts
@@ -0,0 +1,245 @@
+/**
+ * TypeScript examples for the local development & mock mode guide.
+ * Demonstrates how to build a MockModel for testing without real API credentials.
+ */
+
+import { Agent } from '@strands-agents/sdk'
+import type {
+ Model,
+ BaseModelConfig,
+ Message,
+ ModelStreamEvent,
+ StreamOptions,
+ StopReason,
+} from '@strands-agents/sdk'
+
+// --8<-- [start:basic_mock_model]
+import { Model as BaseModel } from '@strands-agents/sdk'
+import type {
+ BaseModelConfig as Config,
+ Message as Msg,
+ ModelStreamEvent as StreamEvent,
+ StreamOptions as Opts,
+} from '@strands-agents/sdk'
+
+/**
+ * A simple mock model provider for local development and testing.
+ * Returns pre-configured responses without making any API calls.
+ */
+class MockModel extends BaseModel {
+ private responses: string[]
+ private callIndex = 0
+ private config: Config
+
+ constructor(responses: string[] = ['Hello! I am a mock response.']) {
+ super()
+ this.responses = responses
+ this.config = { modelId: 'mock-model' }
+ }
+
+ updateConfig(config: Config): void {
+ this.config = { ...this.config, ...config }
+ }
+
+ getConfig(): Config {
+ return this.config
+ }
+
+ async *stream(_messages: Msg[], _options?: Opts): AsyncGenerator {
+ // Cycle through responses, wrapping around if exhausted
+ const text = this.responses[this.callIndex % this.responses.length]!
+ this.callIndex++
+
+ // Yield the standard stream event sequence
+ yield { type: 'modelMessageStartEvent', role: 'assistant' }
+ yield { type: 'modelContentBlockStartEvent' }
+ yield {
+ type: 'modelContentBlockDeltaEvent',
+ delta: { type: 'textDelta', text },
+ }
+ yield { type: 'modelContentBlockStopEvent' }
+ yield { type: 'modelMessageStopEvent', stopReason: 'endTurn' }
+ }
+}
+// --8<-- [end:basic_mock_model]
+
+// --8<-- [start:use_mock_model]
+const mockModel = new MockModel(['The capital of France is Paris.'])
+const agent = new Agent({ model: mockModel })
+
+const response = await agent.invoke('What is the capital of France?')
+console.log(response)
+// --8<-- [end:use_mock_model]
+
+// --8<-- [start:env_swap_pattern]
+import { BedrockModel } from '@strands-agents/sdk'
+
+function createModel() {
+ if (process.env.MOCK_MODE === 'true') {
+ return new MockModel(['This is a mock response for local development.'])
+ }
+ return new BedrockModel({
+ modelId: 'anthropic.claude-sonnet-4-20250514',
+ })
+}
+
+const model = createModel()
+const agentWithSwap = new Agent({ model })
+// --8<-- [end:env_swap_pattern]
+
+// --8<-- [start:mock_tool_calls]
+/**
+ * A mock model that simulates tool calls.
+ * Useful for testing agent tool-use loops without real API credentials.
+ */
+class MockToolModel extends BaseModel {
+ private config: Config
+
+ constructor() {
+ super()
+ this.config = { modelId: 'mock-tool-model' }
+ }
+
+ updateConfig(config: Config): void {
+ this.config = { ...this.config, ...config }
+ }
+
+ getConfig(): Config {
+ return this.config
+ }
+
+ async *stream(messages: Msg[], _options?: Opts): AsyncGenerator {
+ // Check if the last message contains a tool result — if so, respond with text
+ const lastMessage = messages[messages.length - 1]
+ const hasToolResult = lastMessage?.content.some(
+ (block) => block.type === 'toolResultBlock'
+ )
+
+ yield { type: 'modelMessageStartEvent', role: 'assistant' }
+
+ if (hasToolResult) {
+ // Second turn: respond with final text after tool execution
+ yield { type: 'modelContentBlockStartEvent' }
+ yield {
+ type: 'modelContentBlockDeltaEvent',
+ delta: { type: 'textDelta', text: 'The weather in Seattle is 72°F and sunny.' },
+ }
+ yield { type: 'modelContentBlockStopEvent' }
+ yield { type: 'modelMessageStopEvent', stopReason: 'endTurn' }
+ } else {
+ // First turn: simulate a tool call
+ yield {
+ type: 'modelContentBlockStartEvent',
+ start: {
+ type: 'toolUseStart',
+ name: 'get_weather',
+ toolUseId: 'tool_001',
+ },
+ }
+ yield {
+ type: 'modelContentBlockDeltaEvent',
+ delta: {
+ type: 'toolUseInputDelta',
+ input: JSON.stringify({ location: 'Seattle' }),
+ },
+ }
+ yield { type: 'modelContentBlockStopEvent' }
+ yield { type: 'modelMessageStopEvent', stopReason: 'toolUse' }
+ }
+ }
+}
+// --8<-- [end:mock_tool_calls]
+
+// --8<-- [start:mock_errors]
+/**
+ * A mock model that simulates errors for testing error handling.
+ */
+class MockErrorModel extends BaseModel {
+ private shouldFail: boolean
+ private config: Config
+
+ constructor(shouldFail = true) {
+ super()
+ this.shouldFail = shouldFail
+ this.config = { modelId: 'mock-error-model' }
+ }
+
+ updateConfig(config: Config): void {
+ this.config = { ...this.config, ...config }
+ }
+
+ getConfig(): Config {
+ return this.config
+ }
+
+ async *stream(_messages: Msg[], _options?: Opts): AsyncGenerator {
+ if (this.shouldFail) {
+ throw new Error('Simulated API failure: rate limit exceeded')
+ }
+
+ yield { type: 'modelMessageStartEvent', role: 'assistant' }
+ yield { type: 'modelContentBlockStartEvent' }
+ yield {
+ type: 'modelContentBlockDeltaEvent',
+ delta: { type: 'textDelta', text: 'Success!' },
+ }
+ yield { type: 'modelContentBlockStopEvent' }
+ yield { type: 'modelMessageStopEvent', stopReason: 'endTurn' }
+ }
+}
+// --8<-- [end:mock_errors]
+
+// --8<-- [start:mock_multi_turn]
+/**
+ * A mock model with pre-scripted multi-turn conversation responses.
+ */
+class MockMultiTurnModel extends BaseModel {
+ private turns: string[]
+ private turnIndex = 0
+ private config: Config
+
+ constructor(turns: string[]) {
+ super()
+ this.turns = turns
+ this.config = { modelId: 'mock-multi-turn-model' }
+ }
+
+ updateConfig(config: Config): void {
+ this.config = { ...this.config, ...config }
+ }
+
+ getConfig(): Config {
+ return this.config
+ }
+
+ async *stream(_messages: Msg[], _options?: Opts): AsyncGenerator {
+ if (this.turnIndex >= this.turns.length) {
+ throw new Error('All scripted turns have been consumed')
+ }
+
+ const text = this.turns[this.turnIndex]!
+ this.turnIndex++
+
+ yield { type: 'modelMessageStartEvent', role: 'assistant' }
+ yield { type: 'modelContentBlockStartEvent' }
+ yield {
+ type: 'modelContentBlockDeltaEvent',
+ delta: { type: 'textDelta', text },
+ }
+ yield { type: 'modelContentBlockStopEvent' }
+ yield { type: 'modelMessageStopEvent', stopReason: 'endTurn' }
+ }
+}
+
+// Usage
+const multiTurnModel = new MockMultiTurnModel([
+ 'Hi! How can I help you?',
+ 'Sure, I can help with that.',
+ 'Here is the result you asked for.',
+])
+const multiTurnAgent = new Agent({ model: multiTurnModel })
+
+await multiTurnAgent.invoke('Hello')
+await multiTurnAgent.invoke('Can you help me?')
+await multiTurnAgent.invoke('Show me the result')
+// --8<-- [end:mock_multi_turn]