-
-
Teach it once. It handles the next hundred decisions, and zero wrong automatic actions on 24 past cases before a person switched it on.
-
Six demo cases at the returns dock. Cases an approved policy covers resolve themselves and never appear as work.
-
One decision card: what happened, what it costs, and what the agent recommends. Written to be read in thirty seconds.
-
Every line of evidence names the tool that produced it. Nothing on this card is unattributed.
-
The agent proposes the boundaries the answer should live inside, and says plainly what it could not verify.
-
The only two buttons. Approving records the decision and asks for a policy; it activates nothing.
-
The proposed policy: nine allowlisted fields, an action set fixed by the system, and exactly what changes against the active version.
-
The gate: 24 labeled historical cases replayed. Eleven automated, thirteen escalated, zero wrong. One wrong blocks activation.
-
The next case, handled without a person. Work order raised, disposition set, both writes read back and verified.
-
Same kit, same cheap accessory, but the serial does not match. A hard guardrail runs before any policy is consulted.
-
Policy versions: what each one allows, who activated it and when, and how it differs from the version before.
-
What the agent handled, what it escalated, and what it cost, counted from the product's own records.
-
Every event, tool call, approval and action in an append-only, hash-chained log the database refuses to edit.
-
One agent may propose. Deterministic code decides, acts and records. A person teaches the rule.
Inspiration
Automation handles the cases someone already wrote down. Everything else becomes an interruption.
Talk to anyone running operations at a small company and you find the same shape: a person who gets asked the same kind of question twenty times a week, answers it in thirty seconds, and never gets the hour it would take to turn that judgment into a written policy, let alone into automation. The knowledge stays in their head. The interruptions keep coming. And the moment you suggest automating it, the honest objection lands: how would I know it isn't going to do something stupid on the one case that's different?
That objection is the real product requirement. Not "can an agent do this", but "can a person hand over judgment and still be able to sleep".
What it does
OneDecision watches one narrow class of exception (a returned high-value camera kit that came back missing an accessory) and closes the learning loop exactly once.
- An exception arrives as an event, not as a chat prompt.
- A Strands agent investigates with tools: pulls the case, reconciles the bill of materials against what physically arrived, prices the missing part, and checks for an approved policy.
- No policy covers it, so it produces one compact decision card: the evidence and the tool that produced each line, a recommendation, what it is genuinely unsure about, the boundaries the decision should live inside, and why a person has to decide.
- The supervisor clicks Approve and Teach.
- The agent proposes a tightly bounded policy in a constrained language. It is inert.
- Deterministic code replays the candidate against 24 labeled historical cases and reports exactly what it would have automated, escalated, and got wrong.
- The supervisor explicitly activates the version.
- The next matching case resolves by itself: work order raised, disposition set, both writes read back and verified, exception closed against the policy version.
- A case that looks almost identical but has a serial mismatch still escalates.
- Every step is in an append-only, hash-chained audit log.
- A dashboard shows the history and usage behind it: outcomes, the share handled automatically, human decisions, agent runs, tool calls by tool, and model tokens when the provider reports them, all counted from the product's own records.
Each model call takes tens of seconds, so the page shows the run while it runs: every phase and tool call, with real timings, instead of a page that looks frozen.
How we built it
Python 3.11+ · Strands Agents SDK · FastAPI · Pydantic · PostgreSQL, with SQLite as the zero-setup demo backend · server-rendered HTML. One process, no build step, no client framework.
There is exactly one Strands agent. It is invoked three times along the path: investigate, produce the decision card, propose the policy, each with the same tools and a task-specific prompt. Extra agents would have added moving parts, not capability.
The interesting engineering is the line between reasoning and acting:
- The agent has no state-changing tools at all. Every tool is read-only.
execute_approved_policy,verify_action, and policy activation are ordinary Python functions the model has no way to call. - Policies are data, not code. A policy is a Pydantic structure with an allowlist of
nine fields, eight operators, typed values, hard numeric ceilings, and three fixed
actions. There is no
eval, no generated SQL, no generated Python, and no natural-language condition anywhere. - The agent doesn't even choose the actions. The proposal schema has no field into which a model could write "issue a refund". The action set is assembled from the allowlist server-side. That whole class of failure is a missing field, not a check.
- Facts come from the systems, not the model. The agent's report is reconciled against independently derived facts; any disagreement escalates. A hallucination, or a prompt injection buried in an inspector's note, can only make the system more conservative.
- Hard guardrails outrank policies and run before any policy is consulted. A policy can only ever narrow automation.
- Replay gates activation. One false automatic action against history blocks it.
- A human can revise the boundary, and the diff says which way it moved. The supervisor can tighten the agent's proposal or loosen it within the guardrails; the diff labels every change narrower or wider, and a revision is replayed from scratch before it can be activated. Building this surfaced a real flaw: the coverage floor in the replay gate was refusing revisions that automated less than the agent proposed. The system was declining to let a person be more careful. The floor now applies to proposals and to widening revisions only. Zero false automatic actions is never waived.
- The audit log is append-only and hash-chained;
UPDATEandDELETEare rejected by database triggers, and a test drops the triggers, tampers with a row, and asserts the chain notices.
The model provider sits behind an adapter: Bedrock, the Anthropic API, or a deterministic
implementation of the Strands Model interface that drives the unmodified agent loop
with no credentials and no network. That last one is why a judge can run the entire
product (real agent, real tool calls, real typed output) with make setup && make seed
&& make run and no AWS account. The same agent is deployed to Amazon Bedrock AgentCore
Runtime through a thin entrypoint (agentcore_main.py) and the AgentCore CLI's CDK
project, so the runtime is a transport detail rather than a second implementation.
Beyond the demo domain
The fair question about any vertical demo: is this just a returns app?
It is not, and the repository can show it rather than argue it. Counting non-blank Python lines, the domain packs and their adapters are about 850 lines against about 5,550 in the rest of the app, which holds the constrained policy language, the deterministic engine, the replay gate, the activation gate, idempotent execution, read-back verification, the hash-chained audit log, the policy diff, and both database backends. The agent's tools and prompts, the templates, and a few policy defaults still speak returns. What knows about cameras lives in one domain pack, which supplies six things: the fact record a policy may test and how it is derived, the guardrails, the fixed action set with its caps, an executor and a verifier, a labeled corpus to replay against, and the hard ceilings.
So we shipped a second domain. ap.invoice_variance governs an accounts-payable
exception: an invoice that does not match its purchase order. Same story, different room: an AP clerk sees the same $42 freight variance twenty times a week, approves it in thirty
seconds, and never writes it down.
| Returns | Accounts payable | |
|---|---|---|
| The recurring question | a kit came back missing its lens cap, what do I do? | this invoice is $42 over the PO, do I pay it? |
| Hard ceilings | $50 replacement, 1 component | $250 variance, 5% of the PO |
| Fixed actions | hold for parts, raise a work order, close | post a variance adjustment, release for payment, close |
| Guardrails | serial mismatch, new damage, safety-critical part, incomplete evidence | duplicate invoice, vendor on hold, no PO, receipt mismatch, tax mismatch, missing approver |
| Replay of the taught policy | 11 automated, 13 escalated, 0 wrong | 12 automated, 12 escalated, 0 wrong |
The second domain reuses every guarantee. Making room for it reshaped the policy schema and
orchestrator once, deleting 56 lines from the orchestrator and adding 17. A pack costs about 480 lines of Python plus its fixtures and tests.
tests/test_domain_ap_invoices.py, 17 tests touching no returns code, covers fact
derivation, every guardrail boundary, a policy language that refuses a field from another
domain, a replay across 24 labeled invoices, activation refused without a passing replay
and without a token, execution that pays once when run twice, verification that reads the
ledger back, and the audit chain intact.
Honest limits: the web screens and the agent's tools and prompts are still returns-shaped, so the AP domain runs through the governance path and its tests rather than the agent loop and the UI. And a domain has to reduce its judgment to allowlisted fields. Where a decision genuinely turns on free-text nuance, there is nothing to replay and nothing to bound, and this system will not automate it.
Challenges
Making "the model can't do that" true rather than asserted. The first design had the agent propose actions and validation reject bad ones. That's a check, and checks have bugs. Moving the action set out of the proposal schema entirely turned it into a type error instead.
Deciding what a near-match should do. Once a policy exists, a case that nearly matches it is the most dangerous case in the system. It is exactly where a system that wants to be helpful widens its own boundary. OneDecision escalates it and names the condition that failed. Only an exception family with no approved policy at all produces a fresh decision card.
Being honest about AWS. The build environment's AWS credentials turned out to be
invalid (InvalidClientTokenId). Rather than write deployment instructions from memory,
the AgentCore SDK was installed and introspected, the entrypoint was built and its routes
confirmed, and nothing went into docs/deployment-agentcore.md until it had actually been run.
The hosted-model path was then run live with Claude Opus 5, first on the Anthropic API and
then on Amazon Bedrock through Opus 5's cross-Region inference profile. The smoke test
passes on both, with real tool calls and typed output. Each live run found something the
offline suite could not: two bugs in how Anthropic requests were built, and on Bedrock, a
concurrency bug where the model requested several tools at once and they collided on the
shared database connection. The agent escalated rather than act on the bad data, which is
the design working, and tools now run one at a time. All three fixes have regression tests.
The opt-in integration tests then passed on Bedrock, 3 of 3, after one more fix: a shared
test fixture had been forcing the offline provider, so those tests had always skipped.
Finally, the agent was deployed to AgentCore Runtime with the AgentCore CLI and invoked
live: it investigates a case on Bedrock and returns a decision card from AWS. It has since
been redeployed from current main, runtime version 2, and re-invoked, so the deployed
code is the code in the repository rather than a snapshot of an earlier week.
A schema the model had to guess, and what it cost. One Bedrock run of the demo used
169,000 tokens, and 115,000 of them were a single step: proposing the policy. The cause was
not the model. The agent proposes conditions and a spend cap (the action set is assembled
server-side), but the tool that dry-runs a candidate demanded the full stored policy,
actions and all, and its docstring never said so. So the model discovered a second,
undocumented schema by trial and error: in one take, 17 of 24 replay calls were schema
errors, one of them probing with an action literally typed probe_invalid. Making the
tool accept the shape the agent already returns, and saying so in the prompt along with two
validator rules it kept tripping over, took that step from 15 model cycles and 115K tokens
to 4 cycles, 2 replays and 19K, measured on the live golden path, not estimated. Bedrock
prompt caching is now on, which moved most remaining input to cache reads, and the
dashboard shows cached tokens separately so its own arithmetic still adds up.
An agent that described the product wrongly. A decision card on Bedrock told the supervisor that approving would "also activate a standing policy". It does not: approval records a decision and asks for a candidate, and only a person's separate activation, after a passing replay, makes a policy live. The prompt had said actions happen "after a human has approved it", which reads as approve-then-activate. That is a product claim appearing on screen, so the fix was the prompt, and a test now fails if any prompt stops saying that approval never activates a policy.
Accomplishments
- A working end-to-end loop where a human teaches an agent once and the boundary actually holds afterwards.
- Zero false automatic actions, zero prohibited actions, and zero duplicate actions across 24 evaluation cases, measured by a harness that counts from the database, not asserted.
- 199 hermetic tests on a clean clone, 401 runs across PostgreSQL and SQLite together in about forty seconds, including prompt injection inside case notes, audit tampering, model timeouts, tool outages, and duplicate events. CI runs the whole suite, the smoke test, the golden path, and the safety gate on every push, and fails the build if the PostgreSQL half silently skipped.
- A second domain on the same machinery: accounts-payable invoice variance, 17 tests, proven on both backends, on the same governance code as returns.
- Verified from a clean clone: a fresh
git clone, no AWS account, no credentials, no.env: setup, seed, tests, smoke, demo and eval, then the whole teach-once loop over HTTP, ending with the audit chain intact. - The same Strands agent runs live on Claude Opus 5 through both the Anthropic API and Amazon Bedrock, deployed on AgentCore Runtime, and fully offline with no credentials.
- It runs unattended. A one-shot EventBridge Scheduler schedule invoked the deployed runtime directly, with no Lambda and nobody watching, and the agent did its forty-five seconds of work and then the schedule deleted itself.
- A replay gate that shows a supervisor what a proposed policy would have done to their own history before they trust it.
What we learned
The hard part of a professional agent is not capability, it is authority. An agent that can do the work is a weekend. An agent a manager will actually let act unattended needs a boundary that is enforced somewhere the model cannot reach, and evidence about that boundary that a non-engineer can read in thirty seconds.
The second lesson was cheaper to learn and easier to repeat: when an agent burns tokens, look at the seams before the prompt. Every expensive loop in this build came from the model reconciling two descriptions of the same thing: a tool that wanted one shape while the schema returned another. Fixing that seam took the step from 115,000 tokens to 19,000.
What's next
PostgreSQL behind the AgentCore deployment, so taught policies persist across sessions rather than living in a per-session database. The screens and the agent's tools and prompts for the second domain, which today runs through the governance path and its tests rather than the UI. Policy expiry with periodic re-replay against newer history, so a boundary taught in March has to re-earn its place in September. Event-driven triggers (a queue or an event bus feeding the same gated path) now that a scheduled invocation has shown the runtime will do the work with nobody watching. And the opt-in integration tests against the Anthropic API.
Built With
- amazon-bedrock
- amazon-bedrock-agentcore
- amazon-eventbridge
- aws-cdk
- claude
- fastapi
- github-actions
- jinja
- postgresql
- pydantic
- python
- sqlite
- strands-agents
Log in or sign up for Devpost to join the conversation.