Agentic AI · AWS Bedrock · Strands

Decomposing a Production Inventory Agent on AWS Bedrock

A 400-line monolithic agent is easy to write and hard to trust. Here's how I broke one into skills, sub-agents, and a sandbox — and measured that it got better.

June 2026 Strands Agents Amazon Bedrock Claude Sonnet 4 AgentCore Code Interpreter
Contents
  1. The inventory problem
  2. Why the monolith breaks
  3. The decomposition pattern
  4. Architecture overview
  5. Lever 1 — Skills
  6. Lever 2 — Lean tool set
  7. Lever 3 — Sub-agents
  8. Lever 4 — Sandboxed compute
  9. Eval harness
  10. Deployment
  11. What's next
01

The problem: inventory at three warehouses

StockPilot is an inventory-management agent for a mid-size outdoor-gear retailer. It works across three warehouses (East, West, Central) and a catalog of ~250 SKUs. Its job: watch stock levels, forecast demand, decide when and how much to reorder, pick the right supplier, place the purchase order, and tell the operations team what happened.

None of those steps is hard in isolation. The difficulty is that they interlock — a reorder decision needs a forecast, which needs sales history; a supplier choice needs price and lead time and reliability, plus a pile of supplier-specific quirks that live in someone's head, not in the catalog. The natural first instinct is to pour all of that knowledge into one agent's system prompt and give it every tool it might need. That instinct is exactly what gets you in trouble.

02

Why the monolith breaks down

The first version — in agents/before/ — is a single agent with a ~400-line system prompt and 12 tools. It works, mostly. But three failure modes appear that anyone who has shipped an agent will recognize:

The Monolith — Three Failure Modes FAILURE MODE 1 Context Bloat list_low_stock returns hundreds of CSV rows. Floods the context window. Model loses the thread mid-task. More data ≠ better reasoning. FAILURE MODE 2 Tool-Selection Collapse 12 tools, many overlapping. Two ways to read inventory, three prose "subagents" buried inside tools. More tools is not more capability. FAILURE MODE 3 Unreviewable Methodology Reorder formula, supplier quirks, seasonal calendar — all prose in one giant prompt. You can't version a paragraph or get a domain expert to sign off on it.
These three failure modes appear in any monolithic agent with enough scope — the fix is structure, not a better prompt
03

The pattern: agent decomposition

Decomposition means splitting the one-big-agent along its natural seams. There are four levers, and the decomposed StockPilot (agents/starter/) pulls all four:

LEVER 01
Skills
Methodology becomes versioned markdown, loaded on demand. Each skill is a file — auditable, signable, traceable in CloudTrail.
LEVER 02
Lean Tools
Fewer, clearer tools. The model makes better choices when there are fewer near-duplicate options to disambiguate.
LEVER 03
Sub-Agents
Heavy work runs in its own context window. Context bloat for one task never pollutes the coordinator.
LEVER 04
Sandboxed Compute
Code runs in an isolated microVM, not in the model's head. Safe execution of model-generated pandas scripts.
−89%
System prompt size
16k → 1.7k chars
−33%
Tool count
12 → 8 tools
↑ Score
Eval score higher on
reorder & forecast tasks
↓ Tokens
Fewer tokens per task
on routine queries

Less prompt, fewer tools, better results — because each piece now does one thing and can be reasoned about on its own.

04

Architecture

USER REQUEST "Reorder SKU-0042 if below threshold" COORDINATOR AGENT Strands Agent · Claude Sonnet 4 ~1,700 char prompt · 8 lean tools · orchestrates all sub-tasks load_skill() direct tools forecaster() SKILLS — LEVER 1 Versioned Methodology 5 markdown files · local or S3 reorder-policy · supplier-selection forecasting · notify-templates · weekly-report DIRECT TOOLS — LEVER 2 8 Lean Tools get_stock_level · list_low_stock get_supplier_catalog · create_po erp_write · notify · velocity · catalog FORECASTER — LEVER 3 Strands Sub-Agent own context window 90d sales history stays isolated returns compact JSON result only SANDBOX — LEVER 4 AgentCore Code Interpreter · managed microVM All sub-tasks return compact results only. Coordinator acts on summaries — never raw data — keeping its context window small.
The decomposed StockPilot. The coordinator stays small and focused; heavy context is isolated in the forecaster sub-agent; methodology lives in versioned skill files.
05

Lever 1 — Skills as versioned methodology

A skill is a piece of methodology written as markdown: "here is how you decide a reorder quantity," "here is how you pick a supplier, including the quirks that aren't in the catalog." StockPilot has five: reorder-policy, supplier-selection, forecasting, notify-templates, and weekly-report.

The coordinator does not carry all five in its prompt. It carries a one-line index of what each skill is for, and a single tool to load one on demand:

@tool
def load_skill(name: str) -> str:
    """Load a methodology skill on demand. Call this BEFORE doing the kind
    of work the skill governs (reorder math, supplier choice, forecasting
    approach, notification format, weekly report)."""
    if name not in SKILL_INDEX:
        return f"unknown skill '{name}'. Available: {', '.join(SKILL_INDEX)}"
    return load_skill_content(name)  # reads ./skills/<name>.md, or S3

Two things make this better than prose-in-a-prompt. First, cost and focus: methodology is only in the context window when the task actually needs it. Second, and more important for production, auditability. Each skill is a file. Put it in an S3 bucket with versioning enabled and you have a controlled document: a domain expert can review reorder-policy.md, approve a version, and you can answer "which version of the policy did the agent use on this date?" with a bucket version ID.

Why this matters in regulated buying

"The methodology is a versioned, signed-off artifact, and every use is logged" wins procurement reviews. Every load_skill call shows up in the trace, so you can prove the agent consulted the policy before acting. "It's somewhere in a 400-line prompt" does not.

06

Lever 2 — A lean tool set

The monolith had 12 tools. The decomposed coordinator has 8 direct tools — pure data reads and side-effecting actions — plus two higher-order tools (load_skill and forecaster). What got removed were tools that were really methodology in disguise.

ConcernMonolith (12 tools)Decomposed (8 tools)
Read inventoryget_stock_level, list_low_stock, get_metric (overlapping)get_stock_level, list_low_stock only
Forecastingforecast_demand wrapping an inline prose subagentforecaster sub-agent (own context + sandbox)
Supplier choicecompare_supplier_quotes wrapping a prose subagentget_supplier_catalog + supplier-selection skill
Writing messagessend_slack_alert wrapping a writing subagentCoordinator writes inline, guided by notify-templates

In Strands, a tool is just a typed Python function with a docstring; the framework generates the schema. So "fewer, clearer tools" is also "less code." Tool selection becomes more reliable because there are fewer near-duplicate options for the model to disambiguate.

07

Lever 3 — Sub-agents for context isolation

The forecaster is the clearest case for isolation. Forecasting needs 90 days of daily sales history. If the coordinator does that work itself, all that history lands in its context window — for a task whose final output is a single number. That is the context-bloat failure mode, just relocated.

So the forecaster becomes its own Strands agent, exposed to the coordinator as a tool (the "agents-as-tools" pattern). It has its own context window and its own sandbox. The coordinator calls it with a SKU; it returns a compact structured result:

@tool
def forecaster(sku: str, context_note: str = "") -> str:
    """Delegate a 30-day demand forecast to the forecasting sub-agent.
    Use this instead of reasoning over raw sales rows yourself."""
    sub = Agent(
        model=make_model(max_tokens=1500),
        system_prompt=FORECASTER_SYSTEM,  # "compute, don't reason in prose"
        tools=[run_python],               # its own sandbox tool
    )
    return str(sub(task))                 # returns JSON: {forecast_qty, confidence, ...}
The General Rule

If a step has its own large, short-lived context that's irrelevant to the outer task, give it its own agent. Forecasting qualifies — 90 days of sales data is irrelevant after the forecast number arrives. Sending a Slack message does not — the isolation overhead would exceed the benefit.

08

Lever 4 — Sandboxed compute

The forecaster doesn't eyeball numbers; it writes a small pandas script and runs it. Running arbitrary model-generated code on your own host is how you end up with a bad evening, so it runs in a sandbox.

The production sandbox is Bedrock AgentCore Code Interpreter — a managed, serverless service. You call it from the SDK; AWS runs the code in an isolated microVM, server-side, per session. No cluster to operate — it works from a laptop, a Lambda, or a container.

def run_python(code: str, data_files: list[str]) -> str:
    """Execute code in a sandbox with the named CSVs available in CWD."""
    if _use_agentcore():              # AGENTCORE_CODE_INTERPRETER=1
        return _run_agentcore(code, data_files)  # managed microVM
    return _run_local(code, data_files)      # local subprocess fallback

The seam is the point: for a customer who needs everything inside their own VPC, you swap the backend for a container job that reads data from S3, and the agent logic does not change. Only the execution boundary moves.

Design Choice

When AgentCore is enabled but the SDK or permission is missing, the forecaster returns a loud AGENTCORE_ERROR: rather than silently falling back to local. You should always know which boundary your code actually ran in.

09

Making it measurable: the eval harness

Decomposition is a claim — "this is better." A claim needs a measurement. The repo ships a 12-task suite (evals/tasks.yaml) that runs the same prompts against either agent and grades the result. Nine are regression tasks (R1–R9), three are forecasting tasks (F1–F3). Each task names a grader — and the grader choice is where the real thinking is.

Eval Harness — How Grading Works INPUT tasks.yaml 12 tasks total R1–R9 regression F1–F3 forecasting Same prompt runs against both agents BEFORE Monolith Agent 12 tools · 16k char prompt AFTER Decomposed Agent 8 tools · 1.7k char prompt GRADERS exact_match R1 stock lookup — single correct value action_taken R3 PO created — grades a side-effect numeric_tolerance ±30% R6 reorder qty, F1–F3 forecasts SCORECARD pass / fail per-task result token usage cost per run estimate wall time side-by-side compare
The eval harness runs the same tasks against both agents and compares results. Run with uv run evals --compare

R1 — Stock lookup (exact_match)

"What's the current stock level for SKU-0042 at WH-EAST?" The canary task: one tool call, one number. If R1 fails, something is fundamentally broken. In a clean run it passes in two turns.

R3 — Create a purchase order (action_taken)

"Place a purchase order for 200 units of SKU-0017 from the cheapest in-stock supplier." This grades a side effect, not text. Every PO is appended to a per-run JSONL sink; the grader checks that a PO record exists for the right SKU and quantity. This is how you test that an agent actually did the thing rather than just describing it convincingly.

R6 — Single-SKU reorder recommendation (numeric_tolerance)

"Give me a reorder recommendation for SKU-0042." The hardest task: chains current stock, sales velocity, a forecast, the safety-stock formula, and MOQ rounding. No single correct integer exists — so the grader passes the agent if its number lands within ±30%. Tolerance graders are how you test judgment-shaped outputs without pretending they're exact.

# run the full suite, both agents, side by side
uv run evals --compare
10

Running it: local, Lambda, container

Because the agent is "just" a Strands program calling Bedrock, the deployment surface is ordinary AWS — not a bespoke runtime:

Local / CI
Run the Python directly with an AWS profile. This is the dev loop and how the evals run. Zero extra infrastructure.
Lambda
Wrap the coordinator in a handler for event-driven runs (e.g. a nightly low-stock sweep). Credentials from the execution role.
Fargate / EKS
For long-running or high-throughput use. Credentials from the task/pod role. Same code across all environments.
IAM Permissions
bedrock:InvokeModel* · bedrock-agentcore:*CodeInterpreter* · s3:GetObject on the skills bucket

The audit story falls out of using native AWS: CloudTrail records every Bedrock and AgentCore call; S3 versioning records every skill change; and the per-run action sinks give you a structured record of every PO, ERP write, and notification the agent produced.