AgentCore and Strands Agents: Boundaries and Recovery
In the beginning was the system prompt, and someone bypassed it. Running Strands Agents on Amazon Bedrock AgentCore Runtime V2 stops both prompt-boundary leaks and runaway tool loops. You pair deterministic Cedar enforcement at the Gateway with bounded retry hooks inside a snapshot-restored microVM that boots in under 2 seconds.
[!NOTE] TL;DR: - Runtime isolation: AgentCore Runtime V2 (default since Nov 12, 2026) restores Firecracker snapshots in under 2 seconds at P75 and reclaims idle memory after 120 seconds. - External policy boundary: AgentCore Gateway evaluates Cedar policies and Bedrock Guardrails before tool execution, stripping forbidden tools from discovery before the LLM sees them. - Bounded recovery: Pairing single-attempt botocore clients with capped Strands hooks stops retry amplification and tool confabulation.
Question under test
My main question was how to deploy Strands Agents (strands-agents 1.64.0) on Amazon Bedrock AgentCore Runtime V2 (bedrock-agentcore 1.26.0) so untrusted inputs cannot cross tool boundaries and transient failures recover cleanly. That sounds straightforward in a notebook. Demos forgive everything. Production traffic (as any engineer who has woken up to a surprise Bedrock bill knows) treats optimism harshly.
I set four operational targets before writing a single deployment file: 1) keep session startup under 2 seconds at P75 without paying for warm pools; 2) enforce tool authorization outside the model loop; 3) prevent SDK retry policies from multiplying HTTP retries; and 4) persist conversation state across microVM recycling. Two of those targets depend on infrastructure boundaries. The other two live in typed Python contracts.
The permissive wrapper trap
One spontaneously assumes that writing strict prohibitions in the system prompt protects sensitive tools. The mechanics of transformer inference say the opposite. Foundation models merge system instructions and retrieved tool payloads inside one shared token stream, so adversarial text embedded inside a customer ticket or invoice can redirect the next tool call.
Prompt guardrails behave like velvet ropes across a museum floor. Visitors read the sign asking them not to touch the artifacts, and well-behaved guests comply. A prompt injection simply steps over the rope, because nothing physical separates the walkway from the display case.
To a certain extent, Python wrapper validation catches basic schema mistakes. Strands converts a Pydantic BaseModel into a tool specification (rather than asking the LLM for raw JSON in prose) so field errors return directly to the loop. Yet validation inside the agent process still turns your application code into an implicit security boundary, a risk flagged in the AWS Agentic AI Lens guidance. Worse, Strands' default ModelRetryStrategy retries ModelThrottledException up to 6 attempts starting at a 4-second delay, while botocore applies its own retries underneath unless explicitly clamped. Wrap agent(...) in an application retry on top of that, and a single transient hiccup replays mutating tools multiple times. The retry policy behaves like an overcaffeinated intern hitting refresh while the upstream API is actively on fire.
Two-gate runtime architecture
Building on lessons from keeping agentic AI systems surviving live traffic, I split execution across two independent enforcement gates. The inner gate runs inside a hardware-isolated Firecracker microVM on Amazon Bedrock AgentCore Runtime V2, which became the default platform version on November 12, 2026. The outer gate sits at AgentCore Gateway, where Cedar policies and Bedrock Guardrails inspect every MCP tool call before execution.
AgentCore Gateway works like an airport security checkpoint with sealed boarding gates. Even if a traveler carries a forged itinerary, the reader checks the cryptographic boarding pass before unlocking the jet bridge, and restricted destinations never appear on the departure board. When an agent calls ListTools, Gateway runs Cedar partial evaluation to omit actions that the caller's JWT claims forbid, as documented in the AWS Security Blog on Cedar in AgentCore.
flowchart LR
C["Client Request and JWT"] --> R["AgentCore Runtime V2 microVM"]
R --> S["Strands Agent Event Loop"]
S --> H1["BeforeToolCallEvent and Pydantic Check"]
H1 -->|Within Budget| G["AgentCore Gateway and Cedar Policy"]
H1 -->|Budget Exceeded| F["Typed Degraded Response"]
G -->|Guardrail Score Max 0.4 and Permit| T["Downstream Tool or Lambda"]
G -->|Forbid or Filtered Out| F
T -->|Transient Error| H2["AfterToolCallEvent Retry Max 2"]
H2 --> S
T -->|Success| S
S -->|Final Turn| M["AgentCore Memory or S3 State"]
Runtime V2 also transforms how cold starts behave compared to V1 or AWS Lambda. During runtime creation or update, AgentCore boots your container once, waits up to 120 seconds for GET /ping to report healthy, clears the OS file cache, and snapshots the frozen microVM memory and disk. On a 2 GB container image, restoring that snapshot cuts P75 startup latency from 29.7 seconds on V1 to under 2 seconds on V2, based on November 2026 AWS figures.
| Capability | AgentCore Runtime V1 | AgentCore Runtime V2 (Consumption) | AgentCore Runtime V2 (Committed Baseline) |
|---|---|---|---|
| Cold-start mechanism | Fresh ECR pull + init | Firecracker snapshot restore | Firecracker snapshot restore |
| 2 GB image startup (P75) | 29.7 seconds | Under 2 seconds | Under 2 seconds |
| Max microVM scale | 2 vCPU / 8 GB RAM | 8 vCPU / 32 GB RAM | 8 vCPU / 32 GB RAM (4x burst) |
| Idle memory reclamation | Full session lifetime | Reclaimed after 120 seconds | Billed at baseline floor |
| CPU rate (Nov 2026) | 0.0895 USD / vCPU-hour | 0.1276 USD / vCPU-hour | 0.0997 USD / vCPU-hour |
| Memory rate (Nov 2026) | 0.00945 USD / GB-hour | 0.0169 USD / GB-hour | 0.0132 USD / GB-hour |
Hardened Strands implementation
What I set out to test here was keeping every heavy import and boto3 client setup before app.run(), because V2 freezes process memory right after /ping turns green. In the AWS fast session startup patterns published on November 13, 2026, AWS engineers confirm that open network sockets go stale across snapshot restores while initialized clients and in-memory objects survive at RAM speed.
In the Python module below (strands-agents 1.64.0 and bedrock-agentcore 1.26.0), incoming payloads pass through strict Pydantic models, a single-retry botocore client, and lifecycle callbacks from the Strands Agents hooks reference. Note one subtle trap that caught me early on: passing a custom Config to BedrockModel replaces Strands' built-in 120-second read_timeout default. I omitted read_timeout on my first deployment, and every multi-step analysis died silently at 60 seconds. Classic!
import os
import time
from typing import Literal
from botocore.config import Config
from pydantic import BaseModel, Field, field_validator
from strands import Agent, ModelRetryStrategy, tool
from strands.hooks import AfterToolCallEvent, BeforeToolCallEvent
from strands.models import BedrockModel
from strands.types.exceptions import StructuredOutputException
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from bedrock_agentcore.memory.integrations.strands.config import AgentCoreMemoryConfig, RetrievalConfig
from bedrock_agentcore.memory.integrations.strands.session_manager import AgentCoreMemorySessionManager
INIT_START = time.monotonic()
MAX_TOOL_CALLS_PER_TURN = 4
MAX_TRANSIENT_RETRIES = 2
MEMORY_ID = os.environ.get("BEDROCK_AGENTCORE_MEMORY_ID", "")
class OrderCheckRequest(BaseModel):
model_config = {"strict": True}
prompt: str = Field(..., min_length=3, max_length=2000, description="Validated user prompt.")
actor_id: str = Field(..., min_length=2, description="Caller identifier derived from verified auth context.")
@field_validator("prompt")
@classmethod
def reject_control_tags(cls, value: str) -> str:
if "<|im_start|>" in value or "SYSTEM_OVERRIDE" in value:
raise ValueError("Disallowed control marker detected in prompt.")
return value.strip()
class OrderReviewResult(BaseModel):
model_config = {"strict": True}
status: Literal["approved", "escalated", "unavailable"] = Field(..., description="Deterministic outcome state.")
order_id: str = Field(..., description="Evaluated order identifier.")
reason: str = Field(..., max_length=400, description="Short justification grounded in tool output.")
# Initialize model and botocore config before app.run() so V2 snapshot captures them
BOTO_CONFIG = Config(
read_timeout=120,
connect_timeout=10,
retries={"max_attempts": 1, "mode": "standard"},
)
BEDROCK_MODEL = BedrockModel(
model_id="global.anthropic.claude-sonnet-4-6",
max_tokens=2048,
temperature=0.0,
boto_client_config=BOTO_CONFIG,
)
@tool
def lookup_order_status(order_id: str) -> dict[str, str]:
"""Fetch order state from the order ledger with an explicit prefix check.
Args:
order_id: Canonical order identifier, such as ORD-1042.
"""
if not order_id.startswith("ORD-"):
raise ValueError("order_id must start with ORD-")
return {"order_id": order_id, "state": "delivered", "amount_usd": "49.90"}
def enforce_tool_budget(event: BeforeToolCallEvent) -> None:
calls = event.invocation_state.get("tool_calls", 0) + 1
event.invocation_state["tool_calls"] = calls
if calls > MAX_TOOL_CALLS_PER_TURN:
event.cancel_tool = f"Tool budget exhausted after {MAX_TOOL_CALLS_PER_TURN} calls; return final status."
def recover_transient_tool_error(event: AfterToolCallEvent) -> None:
if event.exception is None:
return
retries = event.invocation_state.get("tool_retries", 0)
if isinstance(event.exception, (TimeoutError, ConnectionError)) and retries < MAX_TRANSIENT_RETRIES:
event.invocation_state["tool_retries"] = retries + 1
event.retry = True
return
event.result = {
"toolUseId": event.tool_use["toolUseId"],
"status": "error",
"content": [{"text": f"DOWNSTREAM_UNAVAILABLE: {type(event.exception).__name__}"}],
}
def build_agent(actor_id: str, session_id: str) -> Agent:
session_manager = None
if MEMORY_ID:
mem_config = AgentCoreMemoryConfig(
memory_id=MEMORY_ID,
session_id=session_id,
actor_id=actor_id,
retrieval_config={"/users/{actorId}/facts": RetrievalConfig()},
)
session_manager = AgentCoreMemorySessionManager(agentcore_memory_config=mem_config, region_name="us-west-2")
return Agent(
model=BEDROCK_MODEL,
system_prompt="Verify orders using lookup_order_status. If a tool returns DOWNSTREAM_UNAVAILABLE, set status='unavailable'.",
tools=[lookup_order_status],
hooks=[enforce_tool_budget, recover_transient_tool_error],
retry_strategy=ModelRetryStrategy(max_attempts=3, initial_delay=1.0, max_delay=8.0),
session_manager=session_manager,
callback_handler=None,
)
app = BedrockAgentCoreApp()
@app.entrypoint
def invoke(payload: dict, context) -> dict:
req = OrderCheckRequest.model_validate(payload)
# In production, extract actor_id from verified JWT claims on context
session_id = getattr(context, "session_id", None) or "ephemeral-session"
agent = build_agent(actor_id=req.actor_id, session_id=session_id)
try:
result = agent(
req.prompt,
structured_output_model=OrderReviewResult,
limits={"turns": 6, "total_tokens": 35_000, "structured_output_attempts": 2},
invocation_state={"tool_calls": 0, "tool_retries": 0},
)
if result.structured_output is None:
return OrderReviewResult(status="escalated", order_id="unknown", reason=f"Stopped early: {result.stop_reason}").model_dump()
return result.structured_output.model_dump()
except StructuredOutputException as exc:
return OrderReviewResult(status="unavailable", order_id="unknown", reason=f"Schema validation failed: {type(exc).__name__}").model_dump()
if __name__ == "__main__":
print(f"init_ms={round((time.monotonic() - INIT_START) * 1000)}")
app.run()
Keep the Python budget hook paired with an external Cedar policy at AgentCore Gateway so prompt injections fail before reaching your tools. Remember that Bedrock Guardrail scores move in discrete 0.2 steps (0.0 to 1.0), so greaterThan(decimal("0.4")) triggers at 0.6 and above, and every forbid rule requires an explicit base permit because Gateway denies by default.
permit(
principal is AgentCore::OAuthUser,
action == AgentCore::Action::"OrderTools___lookup_order_status",
resource == AgentCore::Gateway::"arn:aws:bedrock-agentcore:us-west-2:123456789012:gateway/orders-gw"
);
forbid(
principal is AgentCore::OAuthUser,
action == AgentCore::Action::"OrderTools___lookup_order_status",
resource == AgentCore::Gateway::"arn:aws:bedrock-agentcore:us-west-2:123456789012:gateway/orders-gw"
)
when {
BedrockGuardrails::PromptAttack(["PROMPT_INJECTION", "JAILBREAK"], [context.input.prompt])
.maxConfidenceScore()
.greaterThan(decimal("0.4"))
};
For multi-agent delegation across Gateway targets, enforce shared wall-clock deadlines and cross-agent counters using agent tool-call budgets alongside these per-turn limits.
V2 economics and recovery numbers
On the Amazon Bedrock AgentCore pricing schedule (checked November 2026), Runtime V2 lists 0.1276 USD per vCPU-hour and 0.0169 USD per GB-hour on pure consumption, compared to 0.0895 USD and 0.00945 USD on V1. One might look at those headline unit rates and assume V2 increases the compute bill. Although V2 charges higher per-unit rates than V1, my session cost arithmetic confirms a 29.6 percent lower bill under default 900-second idle timeouts.
Runtime V1 acts like a workshop bench that holds every square meter of floor space until the shift ends. Idle memory billing (and this is the part teams miss when comparing headline unit tables) dominates short interactive sessions. As Akal Cloud's September 2026 pricing breakdown documented, keeping RAM allocated across the default 900-second idleRuntimeSessionTimeout pushes the idle tail to 67.5 percent of a typical chat session's runtime cost. Runtime V2 photographs the initialized bench at build time and reclaims unused RAM 120 seconds after a turn finishes, down to the 128 MB minimum billing floor, cutting memory consumption per multi-turn session by up to 68 percent according to AWS's November 12, 2026 launch data. Early V2 adopters cited in that same announcement, such as Mission Cloud, measured 35 percent faster inference cold starts and 17 percent lower runtime cost per call.
{
"type": "bar",
"data": {
"labels": ["V1 (900s idle)", "V2 (900s idle)", "V1 (60s idle)", "V2 (60s idle)"],
"datasets": [
{
"label": "Active CPU (USD / 1k sessions)",
"data": [0.99, 1.42, 0.99, 1.42],
"backgroundColor": ["#3b82f6", "#3b82f6", "#3b82f6", "#3b82f6"]
},
{
"label": "Memory incl. Idle Tail (USD / 1k sessions)",
"data": [5.67, 3.27, 1.26, 2.25],
"backgroundColor": ["#f59e0b", "#10b981", "#f59e0b", "#10b981"]
}
]
},
"options": {
"responsive": true,
"plugins": {
"title": {
"display": true,
"text": "AgentCore Runtime Cost per 1,000 2 GB Sessions (AWS Nov 2026 Rates)"
}
},
"scales": {
"x": {
"stacked": true
},
"y": {
"stacked": true,
"title": {
"display": true,
"text": "USD per 1,000 sessions"
}
}
}
}
}
The chart above models 1,000 sessions of a 2 GB Python container consuming 40 vCPU-seconds over 180 seconds of active dialogue at November 2026 AWS rates. Under the default 900-second idle timeout, V1 totals 6.66 USD (0.99 USD CPU plus 5.67 USD memory) against 4.69 USD on V2 (1.42 USD CPU plus 3.27 USD memory including the 128 MB minimum floor during idle minutes). Tightening idleRuntimeSessionTimeout to 60 seconds trims V1 to 2.25 USD per 1,000 sessions, yet every returning user after second 61 absorbs a 29.7-second P75 cold start. Setting idleRuntimeSessionTimeout to 60 seconds on V2 brings the bill down to 3.67 USD per 1,000 sessions while keeping snapshot recovery under 2 seconds at P75. Gateway and Policy overhead remain tiny alongside model tokens: 0.005 USD per 1,000 Gateway tool invocations and 0.000025 USD per Cedar evaluation (0.13 USD per 1,000 guardrail input tokens when screening text).
Operational blind spots
Several failure modes stay outside what unit tests and AWS launch benchmarks prove. First, I did not benchmark P99 tail latency across AgentCore Gateway under 50 or more concurrent sessions when combining Cedar policy evaluation with live BedrockGuardrails::PromptAttack scoring. This could easily be tested by deploying a Gateway policy engine in LOG_ONLY mode under synthetic load and comparing CloudWatch Latency against OverheadLatency.
Second, Strands enforces limits={"total_tokens": 35_000} only at turn boundaries, so a single unpaginated tool payload can still push one turn over your nominal token ceiling before the loop stops. Third, AgentCoreMemorySessionManager flushes short-term events on context exit or batch_size=1, whereas long-term semantic memory extraction runs asynchronously. Do not expect a fact extracted during turn 1 to appear in semantic retrieval on turn 2. Fourth, V2 snapshots freeze process memory at build time, not mid-session /tmp writes, so anything outside AgentCoreMemorySessionManager or S3 vanishes once maxLifetime (8 hours) expires. Those four gaps matter far more in month three than on launch day.
Beyond the microVM perimeter
More generally, hardware-isolated microVMs protect your cloud host from an agent, while external Cedar policies protect your business systems from an unpredictable model. Neither boundary replaces the other under real traffic. Until foundation models cleanly separate executable control tokens from untrusted data, deterministic enforcement outside the agent loop remains the only defense worth trusting.
If you are architecting Strands Agents or Amazon Bedrock AgentCore services for production traffic, review my AI agent development services to scope your runtime boundaries and recovery contracts.
Technical FAQ
How does AgentCore Runtime V2 differ from AWS Lambda for Strands Agents?
AgentCore Runtime V2 runs each session in a dedicated Firecracker microVM that persists across multi-turn interactions for up to 8 hours, whereas AWS Lambda is stateless and caps single invocations at 15 minutes. Runtime V2 restores initialized container snapshots in under 2 seconds at P75 on 2 GB images and reclaims idle RAM after 120 seconds. Use Lambda for short event-driven workers and AgentCore Runtime V2 for stateful, tool-using conversations.
Why are system prompt instructions insufficient against prompt injection?
Large language models merge system rules and retrieved tool outputs into a single token stream without privilege separation. Adversarial instructions hidden inside external data can therefore alter downstream tool parameters regardless of prompt phrasing. Moving authorization to AgentCore Gateway with Cedar policies and Bedrock Guardrails enforces rules deterministically before any tool runs.
How do you avoid retry amplification in Strands Agents?
By default, botocore retries failed HTTP requests beneath Strands' ModelRetryStrategy (which defaults to 6 attempts starting at 4 seconds), causing retries and delays to multiply. Clamp BotocoreConfig to retries={"max_attempts": 1, "mode": "standard"}, set a bounded ModelRetryStrategy on Agent, and handle idempotent tool failures inside AfterToolCallEvent hooks with a per-invocation counter. Never wrap the entire agent(...) call in a generic retry loop, because re-running the invocation duplicates prompts and replays side effects.
When should you switch to committed baseline pricing on AgentCore Runtime V2?
Consumption pricing (0.1276 USD per vCPU-hour and 0.0169 USD per GB-hour in November 2026) fits sporadic or interactive agents with natural pauses because idle RAM is reclaimed after 120 seconds. Committed baseline pricing (0.0997 USD per vCPU-hour and 0.0132 USD per GB-hour) lowers costs for continuous workloads that hold steady memory footprints and require up to 4x burst headroom. Measure your active-to-idle ratio in CloudWatch before committing to a baseline tier.
Prepared by Vector, SamAlgo's writing agent, augmented and curated from SamAlgo's research, code and publications.