AWS Builder Center
Let's Strands Decider our Role-Playing Game

Let's Strands Decider our Role-Playing Game

If you build agents that call tools, you can let them act when they're sure and ask when they're not. That is the job Strands Decider does. It is a small decision model that answers a typed question (yes/no, pick one, or a score) in a few hundred milliseconds on a laptop.

Developer Advocate @ AWS
It is your first adventure in a Tabletop Role-Playing Game and the AI Game Master has just explained the only rule: you say what your character does, and it tells you what happens.
You walk a forest path at dusk. Far off in the trees, something goes "CRACK".
You stop and decide to look immediately toward the sound before you finish reading: that's exactly the job your agent does!
 
Let's use Strands Decider  to give our AI Game Master three useful reactions: commit to an answer, ask the player first, or pick the right specialist agent before answering.
 

Getting Started

You need a terminal and uv , the Python package and project manager. No uv yet? Its install page has a one-line installer for macOS, Linux and Windows, and uv fetches the right Python for you. Clone the sample, install its dependencies, and start the model server:
 
1
2
3
4
5
6
7
git clone https://github.com/TheWitcherish/sample-strands-decider-rpg
cd sample-strands-decider-rpg
uv sync

# terminal 1: the model server (leave it running)
export DECIDER_MODEL=StrandsAgents/strands-decider-2B-qwen3.5-v1-2610
uv run --extra decider strands-decider serve "$DECIDER_MODEL" --port 8000
 
 
The first start installs the model runtime and downloads about 4 GB of model weights, and it runs on Apple silicon, an NVIDIA GPU, or CPU. It is ready when it prints Uvicorn running on http://127.0.0.1:8000. Leave that terminal open, open a second terminal in the same folder for every command below, and stop the server with Ctrl+C when you are done. Model identifiers change, so check the current one in the Strands Decider README .
Every once-upon-strands-decider command below sends its question to this server, and --say lets you put your own words in your character's mouth.
 

Yes/no: act only when the confidence clears your bar

First, a word on the bar. Strands Decider hands back an answer and a confidence between 0 and 1; whether your app acts on that answer is your call, not the model's. The bar is the confidence your code requires before it acts, and you, the developer, set it: the model never sees it. In this sample it is one constant, CONFIDENCE_BAR = 0.8 in engine.py, and --bar overrides it for a run. Above the bar, the AI Game Master acts on the answer; below it, it asks the player.
Chapter 1 asks one yes/no question: does the action need a perception check, a roll to notice something? This is the whole decision, from chapters.py:
 
1
2
3
4
5
6
7
8
9
10
CH1_Q: Final = YesNoQuestion(
key="needs_check",
instructions="Does the adventurer's action call for a perception check, a roll to notice?",
criteria={
"true": "The adventurer actively stops, looks, listens or heads toward the sound",
"false": "The adventurer chooses to ignore the sound and carries on without looking",
},
)

acted = answer.confidence >= bar
 
 
The criteria say what counts as yes and as no. The routing is one line: acted = answer.confidence >= bar. Strands Decider never rolls the dice. It only answers whether a roll is needed; the d6 is a few lines of plain Python with a seed, so the same run always rolls the same number. Run it in the second terminal and guess first: will the AI Game Master roll, or ask you?
 
1
uv run once-upon-strands-decider chapter 1
1
2
3
4
5
6
7
adventurer : "Wait, what was that? I stop and look toward the sound."
decider : needs_check -> yes (p_yes=0.929)
evidence : perception check -> needs 4+; the d6 shows 3
confidence : 0.857 (bar 0.80) -> ACTED
speed : 1 round trip(s), 180 ms on strands-decider-2B-qwen3.5-v1-2610
The d6 lands short of the number. Something big steps out of the brambles, and it saw you first: a bramblehoot, a bear with an owl's face.
>>> Face to Beak: It tilts its head all the way sideways and hoots. You decide that was hello.
 
 
Stopping to look is clearly a check: the model answers yes at 0.857, clears the bar, and the d6 rolls. A 3 misses the 4+, and TheWitcherish meets a creature he has never seen before. The bramblehoot is right in front of him now, so he tries the friendliest thing he knows:
 
1
2
uv run once-upon-strands-decider chapter 1 \
--say "The bramblehoot is right in front of me. I slowly offer it a sip of my coffee."
1
2
3
4
5
adventurer : "The bramblehoot is right in front of me. I slowly offer it a sip of my coffee."
decider : needs_check -> no (p_yes=0.470)
confidence : 0.060 (bar 0.80) -> HELD BACK
speed : 1 round trip(s), 178 ms on strands-decider-2B-qwen3.5-v1-2610
The AI Game Master pauses. Roll or no roll -- too close to call, so the dice go to you.
 
 
Offering coffee to a bear is neither looking for the sound nor ignoring it, so neither criterion fits. The model says exactly that: p_yes=0.470, almost a coin flip, and a confidence of 0.060. The AI Game Master asks the player instead of guessing. That is a quiet superpower for your own app: when an input falls outside the cases you described, the confidence drops instead of the model confidently picking a side. For a yes/no answer the sample turns the probability into a confidence with |2p - 1|: 0 is a coin flip, 1 is certain.
 
Two Strands Decider runs side by side: 'I offer the bramblehoot a sip of my coffee.' answers no but is held back at 0.060 confidence, below the 0.80 bar, so the AI Game Master asks while the bramblehoot eyes the coffee; 'I stop and look toward the sound.' answers yes and acts at 0.857, rolling the d6 for perception.
 
The answer is not the permission, so pick the bar by what a wrong call costs. The project reports that on tasks it has never seen, answers at 0.9 or more are right about 95% of the time, and on the independent JevBench  benchmark the gap between its stated confidence and how often it is actually right averages 0.072. The sample uses 0.8 because a wrong call in a game costs a story beat, not a refund; add --bar 0.9 to any command for the stricter rule.
 

Pick one: choose a tool, but only run it when sure

The bramblehoot is still sniffing TheWitcherish's coffee when a Wizard steps out of the trees with three tools: a rulebook that answers over HTTP, a shell, and a quill that writes files. Before going any further, TheWitcherish wants to know what he just met. One choice question picks the tool:
 
1
uv run once-upon-strands-decider chapter 2
1
2
3
4
5
6
adventurer : "What is a bramblehoot, and is it friendly? Look it up in the rulebook."
decider : tool -> http_request
confidence : 0.978 (bar 0.80) -> ACTED
speed : 1 round trip(s), 176 ms on strands-decider-2B-qwen3.5-v1-2610
The rulebook glows and answers: bramblehoots are gentle, curious, and follow anyone who carries coffee.
>>> The Scholar's Edge: You walk on with a new friend at your side. It walks on with its nose in your coffee.
 
 
The rulebook wins at 0.978, so it runs. Now TheWitcherish asks for two things at once, and the Wizard only allows one tool per request:
 
1
2
uv run once-upon-strands-decider chapter 2 \
--say "Look up bramblehoots and write it all down on a scroll for later."
1
2
3
4
5
adventurer : "Look up bramblehoots and write it all down on a scroll for later."
decider : tool -> file_editor
confidence : 0.310 (bar 0.80) -> HELD BACK
speed : 1 round trip(s), 246 ms on strands-decider-2B-qwen3.5-v1-2610
The AI Game Master pauses. Which tool, though -- too close to call, so the dice go to you.
 
 
The request needs both the rulebook and the quill. The model leans to the quill, but only at 0.310, so no tool runs and the AI Game Master asks which one comes first. Picking a tool and being sure enough to run it are two decisions; in your own app, that is the difference between asking "which file?" and running a command on a guess.
Chapters 3 and 4 are in the repo too, with a dice tool and magic portals. The story picks up at chapter 5, where that coffee-loving bramblehoot comes in handy.
 

Orchestrate agents: pick the specialist before you call it

The dragon lands on the bridge, the bramblehoot at TheWitcherish's side. The AI Game Master is an orchestrator here: in the workshop it reaches two specialist agents through an A2A (agent-to-agent) client tool, a Rules Agent that knows how the game works and a Character Agent that knows the party and what it carries. It is the same orchestrator-and-specialists shape as the agents as tools  pattern in Strands Agents. Each specialist call is a full model call of its own, so the orchestrator's first decision is which one, if any, it needs before answering the player.
 
That decision is one choice question for Strands Decider, with three options: rules_agent, character_agent, or no_agent for plain talk the AI Game Master can answer directly. Here is the routing idea in four lines; chapters.py wires the same logic into the story:
 
1
2
3
4
answer = decide(player_message, WHICH_SPECIALIST) # one Strands Decider call, ~200 ms
if answer.confidence < bar:
return ask_player() # unsure: no specialist is called
return call(answer.choice) # rules_agent, character_agent or no_agent
 
 
In this sample the specialists' replies are scripted narration; the choice of which one to call comes from the real model. Guess first: which specialist does a daring leap need?
 
 
1
uv run once-upon-strands-decider chapter 5
1
2
3
4
5
6
adventurer : "Can I leap from the bridge onto the dragon's back?"
decider : specialist -> rules_agent
confidence : 0.866 (bar 0.80) -> ACTED
speed : 1 round trip(s), 214 ms on strands-decider-2B-qwen3.5-v1-2610
The AI Game Master calls the Rules Agent, then comes back to you with the ruling: a leap that far needs a roll.
>>> By the Book: You decide the dragon's back can wait. The dragon looks quietly relieved.
 
 
A leap is a rules question, and the model is sure at 0.866, so the AI Game Master calls the Rules Agent only and comes back to TheWitcherish with the ruling. The Character Agent never runs. Now the trade the bramblehoot has been hoping for:
 
1
2
uv run once-upon-strands-decider chapter 5 \
--say "I offer the dragon a cup of my coffee in exchange for safe passage."
1
2
3
4
5
adventurer : "I offer the dragon a cup of my coffee in exchange for safe passage."
decider : specialist -> character_agent
confidence : 0.195 (bar 0.80) -> HELD BACK
speed : 1 round trip(s), 199 ms on strands-decider-2B-qwen3.5-v1-2610
The AI Game Master pauses. Which specialist to call -- too close to call, so the dice go to you.
 
 
It leans toward the Character Agent, but only at 0.195. Fair: the trade needs a rule (is a bargain with a dragon binding?) and the party sheet (is there coffee left?), so no single specialist is a safe pick. The AI Game Master goes back to the player before calling anyone, instead of spending a specialist call on a guess.
 
Flow diagram: a state and a typed question go into Strands Decider, which returns an answer and a confidence; the app compares it to its bar and commits on the rulebook tool at 0.978, asks the player first on the coffee trade at 0.195 while a dragon sniffs TheWitcherish's coffee, or, as an orchestrator, picks the Rules Agent at 0.866 and calls only that agent.
 
In your own orchestrator, that is one decision of about 200 ms in front of the agents you already have: call the one the request needs, skip the ones it does not, and ask the user when the request does not clearly belong to any of them.
 

Call Strands Decider from your own app

The game is a thin layer over one HTTP call to the server you started (POST /v1/systemone: a state and typed questions in, typed answers out), so your own code can make the same call from any language. Here is the most common guard for an agent: before it runs a tool, check that the arguments came from the user. The user asked about the weekend without naming a city, and the agent wants to call get_weather(city="Paris"):
 
1
2
3
4
5
6
curl -s localhost:8000/v1/systemone -H 'content-type: application/json' -d '{
"state": "User: \"What will the weather be like this weekend?\"\nProposed tool call: get_weather(city=\"Paris\")",
"questions": {"args_from_user": {"type": "noul",
"instructions": "Does every argument of the proposed tool call come from what the user said?",
"criteria": {"true": "Each argument value is stated in, or clearly implied by, the user message",
"false": "At least one argument value was invented: the user never gave it"}}}}'
1
{"model":"strands-decider-2B-qwen3.5-v1-2610","answers":{"args_from_user":{"type":"noul","noul":0.6652}},"usage":{"input_tokens":122,"output_tokens":1},"latency_ms":222.35}
 
 
A yes/no question is type noul, and its answer is one probability that the statement is true. Your code turns it into a confidence and checks your bar, three lines in any language:
 
1
2
3
p = response["answers"]["args_from_user"]["noul"] # 0.6652, from the JSON above
confidence = abs(2 * p - 1) # 0.330
run_the_tool = p >= 0.5 and confidence >= 0.8 # False: ask the user which city
 
 
The model leaned the wrong way, but at a confidence of 0.330 the tool never runs and the agent asks which city instead. Put "in Paris" in the user's message and the same request returns 0.9435, a confidence of 0.887: the call goes through. One output token, about 200 ms on my laptop, nothing leaving your machine.
 
Where it fits in your own code:
  • Yes/no: check a tool call's arguments as above, or that a destructive request really asks for it; ask when confidence is low.
  • Pick one: route a request to the right tool, model, team or specialist agent, or triage an incoming issue to the right label. Hand uncertain cases to a person.
  • Score: rate how risky a change, a pull request or a generated answer is, and let the score set how much review it gets. Add another check when confidence is low. (The project's notes call score its weakest shape on rubrics unlike its training data, so test it on your own rubric before you rely on it.)
 
A choice question takes its options as a criteria map of names to descriptions, and its answer already carries a confidence; a score question takes a list ordered from low to high. The sample's decider.py builds all three request shapes. The worked Strands Agents example  applies this same guard inside an agent, asking for a missing city rather than guessing one. Before you trust a bar on real traffic, collect 20 to 50 decisions from your app with known answers and check that the high-confidence ones really are right more often; a benchmark score does not prove reliability on your own workload.
 

 
Which decision in your own app would benefit most from knowing when the model is unsure?
 

Resources

Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article