No — the user's raw message is actually the smallest piece of what gets sent. On every single call, the code layer assembles a full context window made up of five things:
All five get concatenated (in a specific structured format — usually a list of role-tagged messages) into one prompt, sent as a single API call. The LLM then reasons over the whole thing at once and produces either a text reply or a structured tool call.
Amazon Bedrock's InvokeModel / Converse API. Conversation history often lives in DynamoDB keyed by session ID. Retrieved memory comes from a Bedrock Knowledge Base (backed by OpenSearch Serverless or Aurora pgvector as the vector store) via a semantic search call before the main LLM invocation. Bedrock Agents actually manage the tool-schema injection and the loop for you if you don't want to hand-roll it.get_order_status(order_id), a summary of the last 9 messages, a memory snippet pulled from long-term storage ("this customer previously said they're a Prime member"), and finally your literal question — all in one request.It doesn't remember anything on its own — every single API call is stateless from the model's point of view. What looks like memory is actually the code layer re-sending the relevant history every single time. As a conversation grows, sending the full transcript gets expensive and eventually exceeds the model's context window, so real systems use a few techniques:
DynamoDB (fast, cheap, per-session), summarize into a Bedrock Knowledge Base entry when the session ends, and only pull that summary back in on the customer's next visit — rather than replaying a 40-message transcript from three weeks ago.Because the LLM can only reason over what's actually in front of it — if the assembled context is missing the right memory chunk, includes an irrelevant one, or the tool schema is vague, the model's decision quality drops, even though the model itself hasn't changed at all. This is why teams spend a lot of effort on "context engineering" — deciding exactly what to include, in what order, and how much of it — as a separate discipline from prompt writing.
Every tool is registered with a name, a plain-English description of what it does and when to use it, and a strict parameter schema (usually JSON Schema). All of these schemas are sent to the LLM as part of the context window on every call — the model doesn't have them memorized, they're re-declared every time. The LLM then does semantic matching: it reads the user's intent, compares it against every available tool's description, and picks the closest match. It outputs this decision not as free text but as a structured object — a tool name plus a set of arguments — because the model has been specifically trained (fine-tuned) to produce that format when it decides an action is needed instead of a direct answer.
The accuracy of this step depends almost entirely on how well the tools are described. Two vague, overlapping descriptions will genuinely confuse the model — this isn't a hypothetical, it's one of the most common real-world agent bugs.
Bedrock Agents, you define an "action group" per tool with an OpenAPI schema — Bedrock handles matching the user's utterance to the right action group and extracting parameters, then invokes the corresponding Lambda function. If you're building this yourself instead of using Bedrock Agents, you'd pass the tool list in the toolConfig of the Converse API call and parse the returned toolUse block.get_weather(city) and check_account_balance(account_id) — "what's it like outside in Boston" is unambiguous and maps cleanly to the first tool; the model isn't guessing, it's pattern-matching intent to description.MCP (Model Context Protocol) standardizes how a tool exposes itself to any LLM-based agent — instead of every company writing a bespoke integration for every pair of (agent framework, tool), a tool built as an MCP server can be plugged into any MCP-compatible agent with no custom glue code. It defines a consistent way to list available tools, describe their schemas, and invoke them, over a standard transport.
The practical benefit shows up at scale: if you have 5 internal tools and 3 different agent frameworks across your company, without a standard you'd write 15 integrations; with MCP, you write 5 MCP servers and every framework can use all of them.
The code layer should never pass LLM output straight through to execution. Before running anything, it validates the returned arguments against the tool's declared schema — right types, required fields present, values within allowed ranges. If validation fails, the code layer doesn't crash the whole request; it typically returns a structured error back into the conversation ("invalid parameter: date must be in the future") so the LLM can self-correct and retry with better arguments on the next turn.
book_flight(date="2020-01-01") for a future trip — a schema/business-rule check rejects it before hitting the airline API, and the error is fed back so the model retries with a corrected date instead of the booking silently failing downstream.Prompt injection is malicious text — from a user, an uploaded document, or a webpage the agent reads — crafted to look like an instruction and override the agent's real behavior. It's dangerous specifically because the model can't cryptographically tell the difference between "the developer's trusted system prompt" and "text that merely looks like an instruction" — both arrive as tokens in the same context window.
A layered defense in practice looks like:
Amazon Bedrock Guardrails lets you configure content filters and denied topics that apply to both input and output of a Bedrock model call — you'd apply a guardrail policy before the request reaches the LLM and again on its output. For anything fetched from the open web (a page the agent reads), treat it exactly like an untrusted upload — run it through the same guardrail/classifier pass before it's added to context.Least privilege means each tool's underlying credentials are scoped to exactly the actions it needs — nothing broader "just in case." Concretely this means writing a dedicated IAM role per tool (or per tool category) with a policy that allows only specific actions on specific resources, rather than reusing one broad application role for every tool the agent has access to. It also means separating read tools from write/delete tools at the credential level, so a compromised or confused agent calling a "read" tool physically cannot cause a destructive side effect.
get_weather Lambda gets an IAM role with permission only to call the specific external weather API secret from Secrets Manager — nothing else. A delete_ticket Lambda gets its own separate role, scoped only to that one DynamoDB table, and only the DeleteItem action, not *. This way, even if the LLM is somehow tricked into calling the wrong tool, the tool itself is physically incapable of exceeding its narrow permission boundary.The approval request needs to show the human reviewer enough context to make a real decision quickly — what action, on what resource, with what parameters, and why the agent decided to take it — not just a bare "approve/deny" button. You'd also want to set a default-deny timeout (if nobody responds in time, the action doesn't happen automatically), and log every approval/rejection so you can later measure whether approvals are being rubber-stamped (which tells you the tool shouldn't have needed approval in the first place, or the risk framing is wrong).
Step Functions with a "wait for callback" (waitForTaskToken) state — the workflow pauses, sends a notification via SNS or posts to a Slack channel through Lambda, and only resumes the actual tool execution once a human calls back with an approve/deny token.Data poisoning is an attacker getting false or malicious content written into long-term memory so that it's later retrieved and treated as trusted fact in a future conversation — effectively a delayed prompt injection that persists across sessions. The defense has to happen at write time, not just read time, because by the time you're reading poisoned data back, the damage window has already opened.
Bedrock Knowledge Base, don't let arbitrary conversation text sync into it directly — route writes through a Lambda that runs a guardrail/classification check and tags the source, then only that Lambda's role has PutObject/ingestion permission on the underlying S3 data source that feeds the knowledge base.This is genuinely tricky because embeddings aren't human-readable text you can grep for — you need to track, at write time, which raw records produced which vector IDs, so a deletion request can be translated into "delete these specific vector IDs" rather than trying to reverse-engineer embeddings later. In practice this means maintaining a mapping table (user ID → memory/vector IDs) alongside the vector store itself.
DynamoDB table mapping user_id → [vector_id, vector_id, ...] updated every time the Memory Extraction step writes a new embedding. A deletion request becomes: look up the user's vector IDs, call delete on those specific IDs in OpenSearch Serverless/Aurora pgvector, and remove the DynamoDB mapping row — all as one deletion workflow, ideally orchestrated by a Step Functions state machine so it's auditable and can't partially fail silently.This is the classic cache invalidation problem, and an agent system actually has two very different kinds of cache to think about, each needing a different staleness strategy:
Beyond TTL, there are three complementary invalidation strategies:
You also want to be honest about what's cacheable at all — anything that must be correct in real time (payments, account balances, safety-critical data) should bypass the cache entirely rather than trying to tune a TTL tightly enough to feel safe.
Amazon ElastiCache (Redis) in front of tool calls, with per-tool TTLs configured based on data volatility — a short TTL (60–300s) for anything hitting a live external API, a long TTL (hours) for static reference data. For event-driven invalidation, an upstream service publishes a change event to EventBridge, which triggers a small Lambda that deletes the matching Redis key immediately, instead of waiting for the TTL. For the LLM response cache specifically, some teams use Bedrock Prompt Caching (caches repeated prefixes like the system prompt and tool schemas across calls) combined with an application-level Redis cache keyed on a hash of the semantic request, with a source-content version baked into the key.Simple exact-match caching (hash the literal input string, look up the exact same string) only helps when users ask the identical thing verbatim, which is rare in natural language — "what are your hours" and "when are you open" are different strings but the same underlying request. For a meaningful cache hit rate, many teams use semantic caching instead: embed the incoming request, and check the cache for any previous request whose embedding is close enough (above a similarity threshold) to reuse its cached answer, rather than requiring an exact string match.
The tradeoff is risk of a false-positive hit — two questions that are similar in wording but actually need different answers ("what's the return policy for electronics" vs "for clothing"). This is managed by setting a conservative similarity threshold and by scoping the cache to a specific tool or intent category rather than caching globally.
OpenSearch Serverless vector index used for RAG — before calling the LLM, do a nearest-neighbor lookup against previously answered (and cached) queries; only fall through to the actual model call on a cache miss.Token cost is the most visible line item, but it multiplies with every loop iteration — a 4-step agent task can involve 4+ separate model calls for a single user request, not one. Beyond that, real cost centers include: vector store storage and query costs (embeddings at scale aren't free), the per-invocation cost of whatever compute runs your tools (Lambda invocations, container time), data transfer, and — often overlooked — the human review time for any approval-gated actions, which is a real operational cost even though it's not a cloud bill line item.
AWS Cost Explorer tagged by service, plus custom CloudWatch metrics emitted from your agent code for tokens-per-request and tool-calls-per-request, so you can see cost per completed task, not just total monthly spend — which is the number that actually tells you if a change made things cheaper or more expensive per unit of value delivered.Put hard ceilings in the code layer, not just monitoring after the fact: a maximum number of loop iterations per task, a maximum token budget per request, per-user rate limits, and a circuit breaker that pauses a specific workflow if its cost in a time window crosses a threshold. Monitoring and alerting are necessary but not sufficient on their own — by the time a human sees an alert, a loop can have already burned through a large budget in seconds.
AWS Budgets with an SNS action can auto-trigger a Lambda that disables a Bedrock model access grant or flips a feature flag in AppConfig to pause the agent, rather than just emailing someone.Latency compounds from every sequential round trip: each LLM reasoning step and each tool call is a network hop, and a multi-step task chains several of them one after another. The fix isn't "make the model faster" (though smaller models do help) — it's restructuring the workflow:
Step Functions using a Parallel state for independent branches (e.g. flight search + hotel search run in the same state, not sequential states), and use Bedrock's streaming response API so the front end renders tokens as they arrive instead of waiting for the full completion.Keep each agent instance stateless — no per-instance in-memory session data — so any instance behind the load balancer can handle any request, and back all instances with one shared, external state store. This lets you scale horizontally by adding more identical instances rather than needing bigger ones, and it means a crashed instance doesn't lose anyone's session.
ECS Fargate or as Lambda functions behind an Application Load Balancer / API Gateway, with session state in DynamoDB (which scales independently and handles concurrent access safely) and long-term memory in a shared OpenSearch Serverless collection — no instance holds state that another instance can't also access.The vector store itself. LLM capacity and compute scale fairly linearly by adding more provisioned throughput or more instances, but a poorly indexed or unsharded vector database can become the slow point once you have millions of stored embeddings being searched per request — and it's the kind of bottleneck that doesn't show up until you're well past initial load testing with a small dataset.
OpenSearch Serverless auto-scales the underlying compute for vector search, which is why many teams choose it over self-managing a vector database on EC2 — but you still need to think about index sharding strategy and metadata filtering to keep query latency flat as the memory store grows.First, distinguish transient failures (network blip, momentary rate limit) from real failures (the API is genuinely down, or your request is malformed). For transient failures, retry with exponential backoff and jitter — wait a bit longer each retry, with some randomness so many concurrent requests don't all retry at the exact same moment and hammer the recovering service simultaneously. Cap retries at 2-3 attempts; beyond that you're just adding latency for a call that's not going to succeed. If a dependency keeps failing across many requests, a circuit breaker should trip and stop sending traffic to it for a cooldown window, falling straight to a fallback path — this protects both your system's latency budget and the struggling downstream service.
Just as important: idempotency. If a tool call times out, you often don't know if it actually succeeded on the other end before the timeout. Retrying a non-idempotent action (like a payment) could double-execute it. The fix is to require an idempotency key on any mutating tool call, so retrying the same logical request is safe even if the first attempt's response was lost.
ElastiCache/DynamoDB and check that state before attempting a call, or use Step Functions' built-in retry/catch configuration per state. For idempotency, DynamoDB conditional writes keyed on a client-generated idempotency token prevent a payment Lambda from processing the same charge twice on retry.You need distributed tracing tied together by one correlation ID per user request/session, capturing: the exact prompt sent to the LLM (including which memory chunks were retrieved and which tool schemas were included), the model's raw response, every tool call with its parameters and result, and every memory read/write — each with a timestamp. Without this, you're guessing; with it, you can replay the entire decision chain and pinpoint exactly which input caused the wrong output.
AWS X-Ray for distributed tracing across Lambda/ECS calls, with a custom trace ID propagated through every hop, and CloudWatch Logs Insights to query structured logs (log the full prompt/response pairs as JSON) filtered by that trace ID to reconstruct the whole chain for one incident.Leading indicators, not just "is it up": tool-call error rate trending up, average steps-per-task creeping up (a sign of looping or confusion), retrieval relevance scores dropping, human-rejected-approval rate rising, and p95/p99 latency (not just average — tail latency is what users actually feel). Set alerting thresholds on the trend, not just a static value, since a slow creep is often the real warning sign, not a sudden spike.
CloudWatch metrics per request (tool_error_count, steps_taken, latency_ms) and set CloudWatch Alarms on anomaly-detection bands rather than fixed thresholds, so a gradual degradation trips an alert even if no single request looks obviously broken.Three concrete benefits: accuracy — a sub-agent with 5 relevant tools and a focused prompt makes far fewer tool-selection mistakes than one agent choosing among 40 tools across unrelated domains; security — least privilege is naturally easier to enforce per sub-agent, since a payments sub-agent's blast radius is contained to payment tools; and ownership — different teams can independently build, test, and deploy their own sub-agent without touching a shared monolith prompt that everyone is afraid to change.
The cost is coordination complexity and extra latency (each delegation is another round trip), so it's a genuine tradeoff, not a strict upgrade over a single agent.
Amazon Bedrock supports multi-agent collaboration where a "supervisor" agent routes to specialized "collaborator" agents, each with its own action groups (tools) and knowledge base — this maps almost directly onto the orchestrator/sub-agent pattern.Sub-agents can produce results that individually look correct but conflict with each other (a scheduling mismatch between two independently-booked services), the orchestrator can misroute a task to the wrong specialist, and debugging gets harder because a wrong final answer could originate from the orchestrator's routing decision, a sub-agent's tool call, or the merge step — you need tracing that spans across all of them with a shared correlation ID, otherwise a bug can hide at the "seam" between two agents that neither team notices in isolation.
Build a golden evaluation set — a representative sample of real tasks with known-correct tool choices and outcomes — and run the agent against it automatically on every change, scoring tool-selection accuracy, task completion rate, safety-policy violations, latency, and cost. Treat this exactly like a regression suite for traditional software: if a prompt or tool-schema change drops the eval score, you catch it before it reaches production traffic, not after a customer complains.
Beyond the automated eval set, adversarial testing matters specifically for agents: deliberately try prompt-injection-style inputs, ambiguous requests, and edge-case tool failures against the eval set to make sure the guardrails you built actually hold up, rather than just testing the happy path.
Bedrock has model evaluation tooling for scoring outputs against a labeled dataset; teams often pair this with a custom harness that also replays full agent traces (including tool calls) rather than scoring the LLM in isolation, since a correct model response with a wrong tool call is still a failed task.Stage it: ship to a small percentage of traffic first (say 5%), compare key metrics against the previous version side-by-side (completion rate, error rate, cost, safety flags), and only ramp up once you're confident there's no regression. Keep a fast, one-step rollback path — if you can't revert within minutes, the staged rollout doesn't actually protect you.
API Gateway or Lambda aliases with traffic shifting (CodeDeploy canary deployments) to send a small percentage of live traffic to the new agent version, monitored by the same CloudWatch metrics used for ongoing health checks, before shifting 100%.