Keeping Strands agents honest in a household money app A developer built Hestia, a household money app for the AWS Agents for Humans hackathon (Everyday Agents track), using two Strands Agents 1.53.0 agents that call Claude Haiku 4.5 on Amazon Bedrock. The review agent reads household records through four read-only Python tools and writes a briefing, while the reading agent converts pasted text into proposed records, with a local guard_narrative check withholding any briefing that overstates refunds, invents deadlines, or cites amounts not present in tool outputs. Many household money leaks are small and quiet. A washing machine breaks a few months before its guarantee runs out, and the repair gets paid without anyone asking the seller. A free trial turns into a monthly charge. A card payment has no receipt by the time someone needs one. None of this is hard to spot. It is just easy to miss. An agent sounds like a good fit: read the records, point at what needs a decision. The catch is that a language model writing about guarantees and money can sound certain about things nobody checked, like a refund being owed or a deadline that does not exist. Hestia, built for the AWS Agents for Humans hackathon Everyday Agents track , tries a narrow version: the model reads and points, plain Python works out the dates and amounts, and the household decides. Here is how its two Strands agents are built and what stops them from overstating. Both agents use Strands Agents 1.53.0 and call Claude Haiku 4.5 on Amazon Bedrock through the EU inference profile. Both run inside the same Lambda function, the one that handles POST routes, and neither has a tool that writes a record, prepares a notice or sends anything. The review agent reads a household through four tools and writes a short briefing. The reading agent has no tools and turns pasted text into proposed records. Both work inside a stored demo copy of a fictional household whose access lasts 30 minutes. Each review builds a fresh Agent with a BedrockModel temperature=0.2 , streaming=False , 700 output tokens , the tools and a system prompt, then calls it once, asking it to use every tool once and write the briefing. The tools are plain Python closures over the loaded private copy and the review date: | Tool | Reads | |---|---| | review repair evidence appliance id | one appliance, its seller and any saved case | | audit subscriptions | trials, price changes and duplicates | | check receipts and utilities | missing receipts and bills above baseline | | read case timeline | saved case status, next step and recent events | Strands builds each schema from the signature and docstring, so wrapping is one line. From src/hestia/agents/household agent.py : php def tool functions state: dict str, Any , today: date - dict str, Callable ..., str : ... def audit subscriptions - str: """Inspect recorded recurring charges for trial end dates, price changes and overlaps. Amounts are recorded monthly charges, not measured waste or savings. """ ... def strands tools state: dict str, Any , today: date - list Any : """Wrap the workspace callables as Strands tools schemas come from signatures and docs .""" from strands import tool return tool func for func in tool functions state, today .values The sentence the model reads to pick a tool is the sentence a reviewer reads in the source. And because each tool closes over one private copy, no tool takes an argument that could reach another household. Tool outputs are clipped to 1600 characters. After the run, the trace is rebuilt by pairing toolUse and toolResult blocks in agent.messages , and token usage comes from result.metrics.accumulated usage . Both are stored with the briefing. The system prompt says: use the tools, never state or imply entitlement to a refund, repair or amount, never invent deadlines, use only amounts and dates from tool outputs, do not draft the notice, and write under 180 words in three sections What I checked, Decisions waiting for you, Suggested next step . A prompt is a request, not a control. So the code checks the answer. guard narrative runs locally on the finished briefing. Any reason it returns withholds the whole briefing. It fires when the text: An empty reply is withheld too. The amount check matters most. Tools write "EUR 185.00", a bare "13.99" or a count of cents, so the guard strips ISO dates from the tool outputs a date must not lend its digits to an invented figure , normalises every number and also reads whole numbers as cents. From the tests: assert ha.guard narrative "A €1,399.00 fee requires review.", "1399 minor units" == assert ha.guard narrative "A €14.00 fee requires review.", outputs 14 only appears in dates assert ha.guard narrative "A €777.77 fee requires review.", outputs A withheld briefing is labelled as withheld, and the tool trace still shows, because the tool outputs come from the tools, not from the model's text. The guard does not patch text; a guard that rewrote model output would be a second author nobody reviewed. Its limits: it is a local pattern check, not Amazon Bedrock Guardrails. It does not check dates or amounts without a currency, and a wording its patterns miss would pass. The 180 word ceiling is only an instruction; the code enforces 3200 characters. When the model is not available, the review route runs the same tools directly the repair tool once for each appliance with a recorded repair and returns their outputs with no narrative and a visible reason. It answers HTTP 200 either way. Reasons decided before any model call are model not configured , session cap , daily cap and budget unconfirmed . Once the run starts, they are model timeout and model error: