Floor 5: Party Formation (A2A)

Floor 5: Party Formation (A2A)

Files on this floor: 5_a2a_integration/agents/rules_agent/rules_agent.py, 5_a2a_integration/agents/character_agent/character_agent.py, 5_a2a_integration/agents/narrator_orchestrator/narrator_orchestrator.py, 5_a2a_integration/utils/create_knowledge_base.py, 5_a2a_integration/test/test.http

Cordula CORDULA’S LOG, FLOOR 5. The estuary floods twice a day. The dance floor is there, then it is not. This is where the System finally wanted to start the show, which meant rules, and nobody in the party had read them. We built a clerk to read the rule book, a registration desk to stamp our sheets, and a Narrator to run the game, each one doing one job and talking to the others. Barnaby ran the meeting. He was good at it. He delegated, he cited the handbook, he made the Narrator answer in a format the HUD could read. Then a Quarterly Close Accountant stepped onto the pickleweed and challenged us to a dance-off, and we found out whether any of it worked.

System AI System AI: “Floor 5. A party. How sweet. Three agents, four terminals, one orchestrator, and a very real chance you forget to start one of them. The audience is placing bets on which.”

Floor Objective

Assemble the full party: expose the Rules Agent and the Character Agent as remote agents with A2A, then build a Narrator orchestrator that talks to both, rolls dice through the MCP server from Floor 4 and returns its story as structured output.

What is A2A? Agent2Agent (A2A) is an open protocol for agents to talk to each other, whoever built them and whatever framework they run on. Each agent publishes an agent card (name, description, skills, URL) and accepts messages over HTTP; a client reads the card, sends a task and gets the answer back. Where MCP connects an agent to tools, A2A connects an agent to other agents. In Strands, A2AServer exposes any agent this way, and the vended tool make_a2a_client lets the Narrator call them.

Make sure you are at the root of Floor 5 in your terminal:

cd aiRPG-seattle-dungeon-crawl/5_a2a_integration

Cordula CORDULA: The estuary smelled like salt and cut grass. The tide table on a post said the dance floor would be dry for six hours, then gone. Six hours to stand up a rule clerk, a registration desk and a Narrator, and get them talking. I have run tighter timelines on a beach landing. I have not run one with an octopus in charge.

System Architecture

System AI System AI: “Four processes, four ports, one orchestrator. This is the floor where Crawlers lose track of a terminal and blame the framework. The framework is fine. Check your terminals.”

        Rules Agent             Character Agent          Dice Service
           (Port 8000)              (Port 8001)            (Port 8002)
                │                        │                      │
                └────────────────────────┼──────────────────────┘
                                         │
                              Narrator Orchestrator
                                   (Port 8009)

Build a system with three specialized agents that work together:

Part 1: Setting Up the Rules Agent

First, you need to create a Knowledge Base for the Rules Agent. The Crawl Handbook, the crawl’s own rule book, must be processed and stored in ChromaDB (a vector database) so the agent can query it for rules questions.

Creating the Knowledge Base

Item ITEM ACQUIRED: [The Crawl Handbook]

Eleven Markdown files of rules, written by the System and revised every time a Crawler found a loophole. Rarity: Free. The Rules Agent reads it so you never have to, then cites section names at you with visible satisfaction.

Warning SYSTEM WARNING: You must complete this setup before the Rules Agent can work.

Step 1: Read the Handbook

The handbook ships with this repository, in workshop/world/crawl-handbook/: one file per section, from ability scores and dance-offs to Safe Rooms and Ferry Tokens. Skim Dance-Offs and The Final Beat now: the Narrator will run both before this floor is over. Nothing to download.

Step 2: Build the Knowledge Base

Achievement NEW ACHIEVEMENT UNLOCKED!

[I READ THE MANUAL (A ROBOT DID)]

Reward: You received a [Bronze Box]. Inside: a chroma.sqlite3 file and a vector index that understands the rules better than you do.

Run the script to index the handbook. Important: run this from the utils folder.

cd utils
python create_knowledge_base.py
cd ..

What the script does. It reads every handbook file and keeps the section title from its first heading. Each file is cut into passages of about 1,000 characters that overlap by 200, so a rule split across two passages stays findable, and the section name is kept with each passage. ChromaDB then creates a local database in rules_knowledge_base/ (a SQLite file plus a vector index) and turns every passage into a vector with its default embedding model, all-MiniLM-L6-v2, downloaded on first run (about 80 MB, cached afterwards): two passages about the same thing end up with vectors close to each other. Later, when the Rules Agent calls its tool, the question is turned into a vector the same way and the three closest passages come back with their section names. No LLM is involved in that lookup, it is pure semantic search; the model only reads the passages afterwards to write its answer.

Verify that the knowledge base was created:

  • Check that the 5_a2a_integration/utils/rules_knowledge_base/ folder exists
  • Look for chroma.sqlite3 inside
  • You should see: “Knowledge base creation complete!”

Change a rule in the handbook and run the script again: the Rules Agent answers from the new text on its next question. That is the whole point of keeping rules in a knowledge base instead of a prompt.

you seeit meansfix
Crawl Handbook not foundthe script cannot find workshop/world/crawl-handbook/run it from 5_a2a_integration/utils, inside the repository
rules_knowledge_base/ appears somewhere elsethe database is created in the folder you run fromdelete the stray folder, cd utils, run it again
the download of the embedding model hangs or failsyour network blocks iton a corporate network, set HTTPS_PROXY and run again; the model is cached after one good run

Cordula CORDULA: Nobody in the party had read the rules. Barnaby claimed he had skimmed them, which is what officers say. So we fed the handbook to a machine that would read every page and remember where each rule lived. It cost us four minutes and an eighty-megabyte download. I have paid more for less on a range day.

Configure the Rules Agent

System AI System AI: “One agent per context_id. Callers never see each other’s history. The System finds this privacy endearing, and temporary.”

In agents/rules_agent/rules_agent.py, complete the create_agent function. Until now you created one Agent at module level and talked to it yourself. An agent keeps its conversation history in memory, so one instance means one conversation. An A2A server is different: several callers can talk to it at the same time, each with their own context_id, and they must not see each other’s history. So instead of an agent, you give A2AServer an agent factory: a function that builds a fresh Agent for each new context. A2AServer calls it the first time a context appears and reuses that agent for the rest of that conversation. The configuration is the same as before, it just lives inside a function now. See Conversation Isolation on the A2A page.

Step 1: Add the Tool

# TODO: Step 1 - Add the query_rules tool to the agent

Same as in Floors 3 and 4: the tool is what the agent can do, here look up a rule in the knowledge base.

Show me the code for Step 1
def create_agent(context_id: str) -> Agent:
    return Agent(
        model="us.amazon.nova-2-lite-v1:0",  # Amazon Nova 2 Lite, US cross-Region inference profile
        # TODO: Step 1 - Add the query_rules tool to the agent
        # TODO: Step 2 - Add the name "Rules Agent" to the agent
        # TODO: Step 3 - Add the description "..." to the agent
        tools=[query_rules],
        system_prompt="""...""",
        hooks=[NewlineAfterTurn()],
    )

The three TODOs of Steps 1 to 3 sit together inside Agent(...). The solution branch puts the three answers in a row under the third comment; one under each comment works the same way.

Step 2: Add the Name

# TODO: Step 2 - Add the name "Rules Agent" to the agent

Floors 1 to 4 never set a name, because nobody but you talked to the agent. Here other agents will: A2AServer publishes an agent card, a small JSON document served at /.well-known/agent-card.json that says who the agent is, what it can do and where to reach it (Fetching the Agent Card shows what a card contains). Here is the one your Rules Agent will serve once the three steps are done:

{
  "name": "Rules Agent",
  "description": "Rules lookup for the dungeon: fast answers from the Crawl Handbook, with the section they come from.",
  "url": "http://127.0.0.1:8000/",
  "version": "0.0.1",
  "skills": [
    {"id": "query_rules", "name": "query_rules", "description": "Look up a rule in the Crawl Handbook knowledge base. ..."}
  ],
  "capabilities": {"streaming": true},
  "defaultInputModes": ["text"],
  "defaultOutputModes": ["text"]
}

The skills are your tools: Strands fills them from the tool names and docstrings. The name at the top of the card is the one you set in this step. Without it, every agent of the party would be called “Strands Agents”.

Show me the code for Step 2
        # TODO: Step 2 - Add the name "Rules Agent" to the agent
        name="Rules Agent",

Step 3: Add the Description

# TODO: Step 3 - Add the description "Rules lookup for the dungeon: fast answers from the Crawl Handbook, with the section they come from." to the agent

The description at the top of the card: this is what the orchestrator reads in Part 3 to decide which agent to ask. It is required: without one, the server stops with ValueError: A2A agent description cannot be None or empty. See Basic Server Setup for an agent with a name and a description.

Show me the code for Step 3
        # TODO: Step 3 - Add the description "..." to the agent
        description="Rules lookup for the dungeon: fast answers from the Crawl Handbook, with the section they come from.",

Step 4: Create the A2A Server

# TODO: Step 4 - Create an A2AServer with the create_agent factory on port 8000

A2AServer wraps your factory in an HTTP server that speaks the A2A protocol: it serves the agent card and builds one agent per context_id through create_agent. See Creating an A2A Server.

Show me the code for Step 4
# TODO: Step 4 - Create an A2AServer with the create_agent factory on port 8000
a2a_server = A2AServer(agent_factory=create_agent, port=8000)

Pass the function itself, create_agent, without calling it: the server calls it once per new context_id.

Step 5: Serve the Agent

# TODO: Step 5 - Start the A2A server

serve() starts the server and blocks until you stop it. It sits under if __name__ == "__main__": so that importing the module (the tests do) does not start a server.

Show me the code for Step 5
if __name__ == "__main__":
    # TODO: Step 5 - Start the A2A server
    a2a_server.serve()

Replace the pass that holds the place under the TODO.

Cordula CORDULA: The clerk we built had a name tag, a one-line job description and a window. Anyone could walk up, read the tag and ask a question, and nobody could read the questions anybody else had asked. Barnaby approved. “A proper civil service, Sergeant. Each petitioner gets a fresh clerk who has never heard of the last one.”

Step 6: Launch the Rules Agent

Open a new terminal in the floor folder (5_a2a_integration), activate the virtual environment (a new terminal does not keep the one from Floor 0: source ../.venv/bin/activate) and start the agent from its folder:

cd agents/rules_agent
python rules_agent.py

You should see Uvicorn running on http://127.0.0.1:8000. Open http://127.0.0.1:8000/.well-known/agent-card.json in your browser: this is the agent card, with the name and description you just wrote. Leave this terminal open, the orchestrator needs the agent in Part 4.

Check from another terminal: curl -s http://127.0.0.1:8000/.well-known/agent-card.json prints the card, starting with {"name":"Rules Agent" (the fields may come in another order).

you seeit meansfix
ValueError: A2A agent description cannot be None or emptyStep 3 is missingadd description=...
"name": "Strands Agents" in the cardStep 2 is missingadd name="Rules Agent"
address already in useanother process holds port 8000stop it; lsof -i :8000 on macOS and Linux
the agent starts, but its first answer reports a tool error about the collection crawl_handbookthe knowledge base was not built, or was built from the wrong folderrun Step 2 of “Creating the Knowledge Base” from utils
the script ends at once, with no Uvicorn lineserve() is missingStep 5

Part 2: Setting Up the Character Agent

System AI System AI: “The Character Agent is the party’s registration desk. Cordula and Barnaby get their sheets stamped, Groove and Mana computed, a starter kit and 10 Ferry Tokens each. The companions get no paperwork. Shelly has filed a complaint. The rain shell is not included. The rain shell is never included.”

The Character Agent manages the party’s character sheets. You play as a set party: Cordula Vance and Barnaby are the two Crawlers, and Shelly and Chonks ride along as companions, with no sheet of their own. A sheet holds a name, a species (see species), pronouns, a class, six ability scores and the two derived stats the code computes from them: Groove (10 + 2 x Constitution modifier + level) and Mana (5 + Intelligence modifier + Wisdom modifier). The registry starts empty: in Part 4 the Narrator registers the party with the scores from the cast. The agent comes with three pre-built tools:

  • create_character: creates new characters with stats
  • find_character_by_name: looks up characters by name
  • list_all_characters: lists all characters in the database

Configure the Character Agent

In agents/character_agent/character_agent.py, the factory create_agent already exists. Same six steps as for the Rules Agent, with this agent’s tools and card.

Step 1: Add the Tools

# TODO: Step 1 - Add the create_character, find_character_by_name and list_all_characters tools to the agent

Three tools this time; the agent picks the right one from their docstrings.

Show me the code for Step 1
        # TODO: Step 1 - Add the create_character, find_character_by_name and list_all_characters tools to the agent
        tools=[create_character, find_character_by_name, list_all_characters],

Step 2: Add the Name

# TODO: Step 2 - Add the name "Character Agent" to the agent

First field of this agent’s card, as in Part 1.

Show me the code for Step 2
        # TODO: Step 2 - Add the name "Character Agent" to the agent
        name="Character Agent",

Step 3: Add the Description

# TODO: Step 3 - Add the description "Crawler registration for the dungeon: creates characters from the ability scores it is given, stores them, finds and lists them." to the agent

Second field of the card, what the orchestrator will read to pick this agent.

Show me the code for Step 3
        # TODO: Step 3 - Add the description "..." to the agent
        description="Crawler registration for the dungeon: creates characters from the ability scores it is given, stores them, finds and lists them.",

As in Part 1, the three answers can sit in a row under the third TODO, which is where the solution branch puts them.

Step 4: Create the A2A Server

# TODO: Step 4 - Create an A2AServer with the create_agent factory on port 8001

Port 8001: each agent needs its own port.

Show me the code for Step 4
# TODO: Step 4 - Create an A2AServer with the create_agent factory on port 8001
a2a_server = A2AServer(agent_factory=create_agent, port=8001)

Step 5: Serve the Agent

# TODO: Step 5 - Start the A2A server

Same as in Part 1.

Show me the code for Step 5
if __name__ == "__main__":
    # TODO: Step 5 - Start the A2A server
    a2a_server.serve()

Step 6: Launch the Character Agent

Open a second terminal (the Rules Agent keeps running in the first one), activate the virtual environment again and start the agent:

cd agents/character_agent
python character_agent.py

You should see Uvicorn running on http://127.0.0.1:8001, and its agent card at http://127.0.0.1:8001/.well-known/agent-card.json. Leave this terminal open too.

Check: curl -s http://127.0.0.1:8001/.well-known/agent-card.json prints a card named Character Agent with three skills, one per tool. The roadblocks are the Rules Agent’s, with port 8001.

Cordula CORDULA: The registration desk does the math itself. You hand it six numbers and it stamps out Groove and Mana, and no model gets to round in your favor. Shelly filed a complaint about not getting a sheet. Chonks did not file anything. Chonks was stuck to the desk.

Part 3: Setting Up the Narrator Orchestrator

The Narrator Orchestrator coordinates all agents and serves as the main API endpoint. It integrates:

  • The Rules Agent (via A2A)
  • The Character Agent (via A2A)
  • The dice service (the MCP server from Floor 4)

Configure the Narrator Agent

System AI System AI: “This is where I come in. The Narrator orchestrator is the System’s body in your terminal. Hand it dice, hand it allies, hand it a schema, then stand back.”

The given code at the top of the file builds NARRATOR_MODEL, the model the Narrator runs on:

NARRATOR_MODEL = BedrockModel(model_id="us.amazon.nova-2-lite-v1:0")

Nova 2 Lite is the strongest generally available Nova model. It can also think before it answers (extended thinking), but a full Narrator turn, with its A2A calls, measured about 9 seconds with thinking off and 24 to 91 seconds at medium. For a game, off wins. The comment above NARRATOR_MODEL shows the one argument that turns thinking on if you want deeper plans and can wait for them.

What a Narrator Turn Costs

A Narrator turn is not one model call. The Narrator calls the model, the model asks for tools, the tool results go back in, and the loop runs again until the answer is ready: four to six calls for a dance-off round. Every call resends the system prompt, the tool specifications and the conversation so far, so input tokens pile up. The Rules Agent and the Character Agent run their own model calls on top.

At the Amazon Nova 2 Lite prices from Floor 1 (about $0.30 per million input tokens and $2.50 per million output tokens):

what runsrough tokensrough cost
one Narrator turn, thinking off15,000 in, 1,500 out0.45 cents + 0.38 cents: under one cent
the same turn, if thinking adds 2,000 tokens to each of 5 model calls15,000 in, 11,500 out0.45 cents + 2.9 cents: about 3.3 cents
the four curl commands of Part 4, thinking offabout 60,000 in, 6,000 outabout 3.3 cents
building the knowledge basenonefree: ChromaDB embeds the handbook on your machine

Reasoning tokens are billed as output tokens, the expensive kind, and that is the second reason thinking stays off: the first is the wait. We measured the time, not the reasoning tokens, so the second row is an illustration, not a measurement. Measure your own: add print(response.metrics.accumulated_usage) under print(response.structured_output) in ask_agent, and every request prints its inputTokens and outputTokens.

Step 1: Create MCP Client for Dice Rolling

# TODO: Step 1 - Create an MCPClient with the url "http://localhost:8002/mcp"

Same client as in Floor 4: an MCPClient with url="http://localhost:8002/mcp". See Streamable HTTP on the MCP Transports page; as before, the client goes straight into tools=[...], no with block (managed integration).

Show me the code for Step 1
# TODO: Step 1 - Create an MCPClient with the url "http://localhost:8002/mcp"
mcp_client = MCPClient(url="http://localhost:8002/mcp")

MCPClient is already imported at the top of the file. Name it mcp_client: Step 3 uses that name.

Step 2: Create the A2A Client Tool

# TODO: Step 2 - Create the A2A client tool with make_a2a_client and the allowed agent endpoints

make_a2a_client builds a single tool, a2a_client, that the Narrator uses to talk to remote agents: with operation="discover" it fetches an agent’s card (name, description, skills), and with operation="send_message" it sends a request to that agent’s endpoint. Pass it allowed_endpoints, the list of agent URLs it may reach ("http://127.0.0.1:8000" for the Rules Agent, "http://127.0.0.1:8001" for the Character Agent): any other endpoint is rejected before a connection is even made. See the A2A Client vended tool page.

Show me the code for Step 2
try:
    # TODO: Step 2 - Create the A2A client tool with make_a2a_client and the allowed agent endpoints
    a2a_client = make_a2a_client(allowed_endpoints=[
        "http://127.0.0.1:8000",  # Rules Agent
        "http://127.0.0.1:8001",  # Character Agent
    ])

The TODO sits inside a try: block, so indent your lines to match it.

Step 3: Add the Tools

# TODO: Step 3 - Add the mcp_client and a2a_client tools to the agent

The Narrator gets two tools: mcp_client, which brings in roll_dice from the dice server, and a2a_client, which lets it discover and message the Rules Agent and the Character Agent. Pass the MCP client itself, as in Floor 4: Strands lists its tools when the agent starts. See Give your agent tools.

Show me the code for Step 3
        # TODO: Step 3 - Add the mcp_client and a2a_client tools to the agent
        # TODO: Step 4 - Force the response to use the StoryOutput model
        tools=[mcp_client, a2a_client],

Step 4: Force Structured Output

Item ITEM ACQUIRED: [StoryOutput]

A Pydantic model that forces the System to answer in JSON. Rarity: Rare. The System resents it. The web interface depends on it. The System resents that too.

By default, an agent replies with free-form text. Structured output makes the model return a typed object that matches a schema you define as a Pydantic model; Strands validates it and hands it back as clean JSON. See Request structured output.

# TODO: Step 4 - Force the response to use the StoryOutput model

We have coded StoryOutput for you, a little further up in the file: the schema of one Narrator turn, with response (the narration), actions_suggestions (what the player could do next), details (which tools and agents were used) and dice_rolls (each roll with its type, result and reason). Here is an example:

{
  "response": "Tempo: Driving. The Quarterly Close Accountant loosens their tie and calls Footwork like it is an audit finding. Cordula answers with a 13 plus 1 for Dexterity: 14 against their 11. Round one to the kayak patrol.",
  "actions_suggestions": ["Let Barnaby drop an Ink Cloud before the next call", "Ask the accountant about their weekend", "Challenge them to a second dance-off, double or nothing, during quarter close"],
  "details": "roll_dice through the dice server, twice; Rules Agent for the tempo bonus",
  "dice_rolls": [{"dice_type": "1d20", "result": 13, "reason": "Footwork Move Call, round 1, Cordula +1 Dexterity"},
                 {"dice_type": "1d20", "result": 9, "reason": "Footwork Move Call, round 1, accountant +0 Dexterity +2 Driving tempo"}]
}

We need this shape because the web interface you will play with in Floor 6 renders those fields separately: parsing free-form prose would be fragile, whereas a guaranteed JSON shape keeps the UI reliable.

Show me the code for Step 4
        # TODO: Step 3 - Add the mcp_client and a2a_client tools to the agent
        # TODO: Step 4 - Force the response to use the StoryOutput model
        tools=[mcp_client, a2a_client],
        structured_output_model=StoryOutput,
    )

Pass the class, StoryOutput, not an instance of it. The /inquire route already reads response.structured_output.

Cordula CORDULA: Barnaby ran the meeting from my shoulders. He asked the clerk for the rule, the desk for the sheet, and the dice for the roll, and he did not do anyone’s job for them. Then he made the Narrator fill out a form for every answer, four boxes, every time. “If it is not in the box, Sergeant, it did not happen.” I have known colonels who could not manage that.

Step 5: Launch the Narrator

The Narrator rolls its dice through the MCP server from Floor 4, so that server has to run too. If it is still running from Floor 4, skip this. Otherwise open a third terminal in the floor folder, activate the virtual environment and start it:

cd ../4_mcp_integration
python dice_roll_mcp_server.py

Then open a fourth terminal for the Narrator itself:

cd agents/narrator_orchestrator
python narrator_orchestrator.py

You should see Uvicorn running on http://127.0.0.1:8009. Four terminals are now running: the Rules Agent, the Character Agent, the dice server and the Narrator.

Check: the Narrator prints its agent, then the Uvicorn line, with no Error occurred: line between them. The file catches any error raised while it builds the agent and prints it, then starts the server anyway, so read the output above the Uvicorn line.

you seeit meansfix
Error occurred: ...MCPClientInitializationError... above the Uvicorn linethe dice server is not runningstart it (above), then restart the Narrator
NameError: name 'agent' is not defined on the first requestbuilding the agent failed at startupscroll up to the Error occurred: line and fix that first
TypeError about structured_output_modelan SDK older than this crawl’suv sync

Part 4: Test the System

Floor boss FLOOR BOSS: Four curl commands: a rules question, registering the party, a lookup on the party leader, and a dance-off against the Quarterly Close Accountant. Survive all four and the party is formed.

At this point four terminals should be running:

TerminalProcessEndpoint
1Rules Agent, python rules_agent.pyhttp://127.0.0.1:8000
2Character Agent, python character_agent.pyhttp://127.0.0.1:8001
3Dice server (Floor 4), python dice_roll_mcp_server.pyhttp://127.0.0.1:8002/mcp
4Narrator, python narrator_orchestrator.pyhttp://127.0.0.1:8009

If one is missing, go back to the launch step of its part.

The Narrator is a FastAPI application listening on port 8009. Each curl command below sends a POST request to its /inquire endpoint with a JSON body holding your question, exactly what a web page or any other client would do. While the request runs, the four terminals show how the Narrator treats it: which agent it asks over A2A, when it rolls the dice over MCP, and what each of them answers. Open a fifth terminal for the commands themselves (no virtual environment needed, curl is a system command) and run them there (or, if you have a REST client, use the test/test.http file we prepared for these tests):

# Query the rules
curl -X POST http://0.0.0.0:8009/inquire \
  -H "Content-Type: application/json" \
  -d '{"question": "How does the Final Beat work?"}'
# Register the party (the Narrator passes the cast scores, the Character Agent computes the rest)
curl -X POST http://0.0.0.0:8009/inquire \
  -H "Content-Type: application/json" \
  -d '{"question": "Register the party with the System: Cordula Vance and Barnaby."}'
# Look up the party leader
curl -X POST http://0.0.0.0:8009/inquire \
  -H "Content-Type: application/json" \
  -d '{"question": "What are Barnaby'\''s Dexterity and Mana?"}'
# Start a dance-off
curl -X POST http://0.0.0.0:8009/inquire \
  -H "Content-Type: application/json" \
  -d '{"question": "Cordula challenges the Quarterly Close Accountant to a dance-off at the Pier 59 collapse. Run round one."}'

Watch the four terminals while a request runs. In the Narrator terminal the tool calls appear in order: a2a_client discovering the agent cards, then send_message to the Rules Agent or the Character Agent, or roll_dice for the dice. The agent you asked logs the question in its own terminal, and the dice server prints a DICE ROLL line for every roll: none while the party registers (the Narrator passes the scores from the cast), two 1d20 lines for each dance-off round. The Character Agent terminal shows two New Crawler registered lines, and the sheets it returns carry Groove 17 and Mana 8 for Cordula, Groove 11 and Mana 11 for Barnaby: computed by the code, not the model. The answer you get back is JSON shaped like StoryOutput: response, actions_suggestions, details and dice_rolls. Barnaby’s lookup should come back with Dexterity 18 and Mana 11: the Narrator read it from the Character Agent instead of inventing it.

you seeit meansfix
{"error": "Internal server error"}the Narrator failed; its terminal says whyread the Error occurred: line in terminal 4
the Narrator says a call to an agent failedthat agent is not running, or runs on another portcheck terminals 1 and 2 against the table above
curl: (7) Failed to connectthe Narrator is not runningPart 3, Step 5
Groove or Mana differ from 17 and 8 for Cordulathe scores the Narrator passed are not the cast’slook at the New Crawler registered line in terminal 2; restore characters.json (see Cleanup) and register again

Cordula CORDULA: The Quarterly Close Accountant stepped onto the pickleweed in a fleece vest and called Footwork like it was an audit finding. The Narrator set the tempo, asked the dice twice, and read both totals out loud with the modifiers shown. I took round one on a drill cadence. The accountant took it well and said they had a close to get back to, and I believed them. Barnaby, very quietly, through the collar: “Registered, Sergeant. They wrote my name down as party leader.” He sounded like he had been waiting a long time to hear it.

System AI SYSTEM DESIGN, LAYER 5: RULES, SHEETS AND THE CORE LOOP. This floor is where the game becomes a game. The rules live in a knowledge base the Rules Agent searches, so the Narrator cites them instead of improvising. The character sheet lives with the Character Agent, which computes Groove and Mana from the ability scores so no model can hand a Crawler extra stamina. The dance-off is the core loop: a Move Call, two rolls, a winner, Groove lost, a Final Beat when it is level. Every stat has a job in it, and nobody gets hurt. StoryOutput is the notification format the HUD will render on Floor 6. See The System and the Crawl Handbook.

Side Passage: The Paper Trail (optional)

Cordula CORDULA: The accountant left in a hurry and left a binder on the pickleweed: an audit report, of us, as a PDF. Our clerk only read Markdown.

The Paper Trail, a sidequest: read the accountant’s audit report with pypdf and add it to the Rules Agent’s knowledge base, so the Rules Agent cites it by page like any handbook section.

Boss Battle: The Developer Advocate Swarm (optional)

Cordula CORDULA: The Developer Advocate Swarm followed us down from the Locks, with better lighting this time. Four of them, one mic, and nobody deciding who went next.

The Developer Advocate Swarm: keynote, live demo, livestream and swag table pass the mic themselves, as a Strands swarm. The party wins by out-dancing whoever holds it. Its opposite, a graph whose order your code draws, waits on Floor 8: The Quarterly Earnings Call.

More Side Passages

Cordula CORDULA: Three more errands on the estuary: Barnaby wants to make his own calls, the accountant wants a typed answer, and somebody has to staple the receipts.

  • Barnaby Takes the Call, sidequest: an A2AAgent instead of the a2a_client tool, with every A2A event streamed
  • Read the Room, Typed, dance-off: the tempo and each Move Call picked as typed values by a decision model, against the Quarterly Close Accountant
  • Subcontract the Paperwork, sidequest: a subagent tool that hires a one-off clerk on a pinned Amazon Nova 2 Lite

Floor Clear

Floor cleared, Crawler. You’ve built a complete multi-agent Narrator system with agent orchestration.

Loot you’re keeping:

  • Setting up a ChromaDB knowledge base from the Crawl Handbook for the Rules Agent
  • Building agent factories for per-caller isolation (the agent_factory pattern)
  • Setting up A2A servers to expose your agents
  • Creating an A2A client tool so the orchestrator can discover and call other agents
  • Orchestrating specialized agents by combining A2A and MCP tools
  • Forcing structured JSON output with a response model

Stairwell Stairwell: Head to Floor 6 where you’ll test your Narrator with a web interface.

Achievement NEW ACHIEVEMENT UNLOCKED!

[FOUR TERMINALS OPEN, ZERO FRIENDS]

Reward: You received a [Gold Box]. Inside: a Rules Agent that cites handbook sections, a Character Agent that remembers Barnaby’s Dexterity to the point, and an orchestrator that just watched an accountant lose a Footwork call.

System AI System AI: “You built a multi-agent system and it returned typed JSON. I am, against my better judgment, impressed. Floor 6 lets you watch it play in public.”

View this file on GitHub commit 4018746, edited in the workshop repository only