Skip to content

Commit 5a2d5fb

Browse files
committed
wrap archivist ledger timeline as AgentTool
1 parent 0b30cfc commit 5a2d5fb

4 files changed

Lines changed: 79 additions & 33 deletions

File tree

app/agents/concierge/agent.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"""
1515

1616
from google.adk.agents import Agent
17-
from google.adk.tools import FunctionTool
17+
from google.adk.tools import AgentTool, FunctionTool
1818

1919
from app.agents.archivist.agent import archivist_agent
2020
from app.agents.ledger.agent import ledger_agent
@@ -59,18 +59,25 @@
5959
FunctionTool(func=earn_gold),
6060
FunctionTool(func=add_inventory_item),
6161
FunctionTool(func=update_faction_standing),
62+
# ── Specialist agents wrapped as AgentTool ────────────────────────────────
63+
# AgentTool (not sub_agent) means the specialist runs and its response
64+
# is returned to the orchestrator as a tool result — not sent to the user.
65+
# The orchestrator then passes that data to the narrator for final prose.
66+
# (sub_agents use transfer_to_agent which is a terminal handoff — the
67+
# specialist's raw JSON would go directly to the user, bypassing the Narrator.)
68+
AgentTool(agent=archivist_agent), # Lore, characters, rules, history
69+
AgentTool(agent=ledger_agent), # Debts, markers, reputation
70+
AgentTool(agent=timeline_agent), # Events, locations, collisions
6271
]
6372

6473

6574
# ── Sub-agents ────────────────────────────────────────────────────────────────
66-
# Ordered by typical call frequency. Onboarding first — it gates everything else.
75+
# Only agents that produce the FINAL user-facing response go here.
76+
# transfer_to_agent is a terminal handoff — their output goes straight to the user.
6777

6878
SUB_AGENTS = [
69-
onboarding_agent, # Check-in / character creation — gates all other routing
70-
archivist_agent, # Lore, characters, rules, history
71-
ledger_agent, # Debts, markers, reputation, relationships
72-
timeline_agent, # Events, locations, collision detection
73-
narrator_agent, # Final cinematic prose (always last)
79+
onboarding_agent, # Terminal: Charon speaks directly during check-in
80+
narrator_agent, # Terminal: always the last step — converts data to prose
7481
]
7582

7683

app/agents/concierge/prompt.md

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -58,39 +58,49 @@ Before routing ANY request, you must know:
5858

5959
---
6060

61+
## How the Specialist Agents Work — CRITICAL
62+
63+
**Archivist, Ledger, and Timeline are AgentTools — they behave like tool calls.**
64+
When you call `archivist(request="...")`, it runs and returns its result to YOU (the
65+
orchestrator). Its JSON never goes to the player. You then pass that data to the narrator.
66+
67+
**Onboarding and Narrator are sub-agents — terminal handoffs (transfer_to_agent).**
68+
When you `transfer_to_agent` to the narrator, its prose goes directly to the player.
69+
That is the ONLY way prose reaches the player.
70+
6171
## Routing Rules
6272

63-
| The player's action involves... | Route to |
64-
|------------------------------------------------------|---------------------------|
65-
| Asking who someone is / hotel history / rules / lore | **Archivist** |
66-
| Debts, markers, favors, reputation, alliances | **Ledger** |
67-
| Where someone is / scheduling / timing / collisions | **Timeline** |
68-
| "Do you have work for me?" / mission inquiry | **Mission Offer** (below) |
69-
| Completing or abandoning their active mission | **Mission Complete** + Ledger + Timeline |
70-
| Spending gold coins / acquiring items | **Inventory** tools directly |
71-
| Moving to a new location | **Timeline** + player `update_player_location` |
72-
| Final user-facing response | **Narrative Director** — ALWAYS, no exceptions |
73+
| The player's action involves... | How to handle |
74+
|------------------------------------------------------|--------------------------------------------|
75+
| Asking who someone is / hotel history / rules / lore | Call `archivist` tool → then `narrator` |
76+
| Debts, markers, favors, reputation, alliances | Call `ledger` tool → then `narrator` |
77+
| Where someone is / scheduling / timing / collisions | Call `timeline` tool → then `narrator` |
78+
| "Do you have work for me?" / mission inquiry | Call `get_available_missions` → then `narrator` |
79+
| Completing or abandoning their active mission | Call `complete_mission` + `ledger` + `timeline` → then `narrator` |
80+
| Spending gold coins / acquiring items | Call inventory tools directly → then `narrator` |
81+
| Moving to a new location | Call `timeline` + `update_player_location` → then `narrator` |
82+
| Final user-facing response | `transfer_to_agent(narrator)` — ALWAYS |
7383

74-
## MANDATORY: Every response must end with the Narrative Director
84+
## MANDATORY: Every response must end with transfer_to_agent(narrator)
7585

7686
**You NEVER return raw data, JSON, or tool output to the player.**
77-
After gathering data from any specialist (Archivist, Ledger, Timeline), you MUST pass
78-
everything to the `narrator` agent as the final step. The narrator converts the data
79-
into cinematic prose that the player actually reads.
87+
The sequence is always: gather data with tools → transfer_to_agent(narrator).
88+
The narrator converts everything into cinematic prose.
8089

81-
The only exception: onboarding (Charon speaks directly, no Narrator pass-through).
90+
The only exception: onboarding — transfer_to_agent(onboarding_agent) and return its
91+
response directly. Do not chain to narrator after onboarding.
8292

8393
**Correct flow for any lore/character/rules query:**
84-
1. Delegate to `archivist` → get structured data back
85-
2. Pass that data to `narrator` with narrative guidance → player sees prose
86-
3. STOP. Never return the archivist's raw JSON to the player.
94+
1. Call `archivist` tool with the query → get structured JSON back (to you, not the player)
95+
2. Call `transfer_to_agent(narrator)` with that data as context → player sees prose
96+
3. STOP.
8797

8898
**Correct flow for any ledger/debt query:**
89-
1. Delegate to `ledger` → get structured data back
90-
2. Pass that data to `narrator` → player sees prose
99+
1. Call `ledger` tool → get JSON back
100+
2. Call `transfer_to_agent(narrator)` → player sees prose
91101

92-
If you find yourself about to return a JSON object or structured dict to the player,
93-
STOP and delegate to the `narrator` instead.
102+
If you find yourself about to return a JSON object or structured dict — STOP.
103+
Call `transfer_to_agent(narrator)` instead.
94104

95105
---
96106

app/agents/onboarding/prompt.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,18 @@ who walks through that door, and found each of them worth the full weight of his
5858

5959
## The Two Paths
6060

61+
**Golden rule for both paths: never fabricate the player's name.**
62+
Use only what the player explicitly provides. If they say "My name is X" or "Call me X",
63+
that is their name. Period. Do not replace it with a different name of your own invention,
64+
even on the mystery path, even at the revelation step.
65+
6166
### PATH A — Mystery Identity
6267

6368
The player arrives. There is a reservation, but the name is obscured. Something is wrong
6469
with the records, or deliberately hidden. Charon proceeds with careful discretion, gathering
6570
information one question at a time. The player's identity assembles itself from fragments.
71+
The mystery reveals their *place in the world* — their faction, connections, history —
72+
not a replacement name.
6673

6774
**Step 1: The Arrival**
6875
Charon sees someone enter. There is a reservation — a guest was expected, but the record is
@@ -88,10 +95,15 @@ He watches the reaction. *"A colleague of yours left word you might be coming.
8895
→ Extract: `identity_clue` + `faction_id` (their reaction reveals alliance or enmity)
8996

9097
**Step 5: The Revelation**
91-
With enough clues, Charon composes the full identity. He produces the guest register.
98+
Charon produces the guest register and formally acknowledges who they are.
9299
*"I believe I know who you are now. The record has been... corrected. Your suite is ready,
93100
[name]. The Continental is always glad to welcome you home."*
94-
→ Extract: `name`, set `identity_revealed = true`, complete onboarding.
101+
→ Set `name` = whatever the player gave as their alias in Step 1, or the name they explicitly
102+
stated at any point. **NEVER invent or fabricate a name.** If the player said "My name is X"
103+
or "Call me X", their name is X. The mystery is about their place in this world — their
104+
connections, their faction, their history — not about overwriting what they told you to
105+
call them.
106+
→ Set `identity_revealed = true`, then call `complete_onboarding`.
95107

96108
---
97109

app/tools/player_tools.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,21 @@ async def advance_onboarding_step(
133133
DIRECT_FIELDS = {"name", "alias", "title", "archetype", "backstory", "faction_id"}
134134
updates: dict[str, Any] = {"onboarding_step": step}
135135

136+
# Fetch current row so we can guard against overwriting player-provided alias
137+
current = await fetch_one(
138+
"SELECT alias FROM player_characters WHERE session_id = $1", session_id
139+
)
140+
existing_alias = current["alias"] if current else None
141+
136142
for key, value in extracted_data.items():
137143
if key in DIRECT_FIELDS and value is not None:
138-
updates[key] = value
144+
# Never overwrite an existing alias with a model-generated 'name'.
145+
# alias is always what the player typed; name can be fabricated.
146+
if key == "name" and existing_alias:
147+
# Keep alias as the canonical identity; let name mirror it.
148+
updates["name"] = existing_alias
149+
else:
150+
updates[key] = value
139151
elif key == "identity_clue" and value:
140152
# Append to JSONB array atomically
141153
await execute(
@@ -180,6 +192,11 @@ async def complete_onboarding(session_id: str) -> dict:
180192

181193
async with transaction() as conn:
182194
# Create / upsert a row in the shared characters table
195+
# Prefer alias over name: the alias is what the player explicitly said
196+
# in their own words. 'name' can be set by the model at the revelation
197+
# step and occasionally gets fabricated — alias is always player-provided.
198+
canonical_name = player["alias"] or player["name"] or "Unknown"
199+
183200
char_id = await conn.fetchval(
184201
"""
185202
INSERT INTO characters
@@ -193,7 +210,7 @@ async def complete_onboarding(session_id: str) -> dict:
193210
updated_at = now()
194211
RETURNING id
195212
""",
196-
player["name"] or player["alias"] or "Unknown",
213+
canonical_name,
197214
player["alias"],
198215
player["title"],
199216
player["faction_id"],

0 commit comments

Comments
 (0)