feat: she can suggest + switch modes (set_mode tool + mode awareness)
"She suggests, you confirm" — instead of brittle keyword→mode mapping, she's given awareness of her modes + the ability to switch, and her judgment decides when to offer (the model reads "should I drive to Cleveland?" vs "should I fold the river" far better than a lexicon could). - tools: set_mode(mode) — switches the session's mode; in _BASE (all modes). - mind: a per-turn mode-menu note listing her modes + "offer a switch when the work clearly shifts; on his yes, call set_mode; don't nag." - Sticky mode stays manual otherwise; Poker still auto-engages on session start. - test: set_mode switches + rejects unknown. Suite 97 green, ruff clean. Note: server-side switch takes effect next turn; the UI badge syncs on next mode load (cosmetic lag). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -74,6 +74,20 @@ def _inner_life_note() -> Message | None:
|
|||||||
return {"role": "system", "content": "\n\n".join(parts)}
|
return {"role": "system", "content": "\n\n".join(parts)}
|
||||||
|
|
||||||
|
|
||||||
|
def _mode_menu_note(current: modes.Mode | None) -> str:
|
||||||
|
"""Tell her the modes she can switch to + when to offer it. She judges the fit
|
||||||
|
(the model reads context far better than a keyword would)."""
|
||||||
|
menu = ", ".join(f"{m.label} ({k})" for k, m in modes.MODES.items())
|
||||||
|
cur = current.label if current else "Talk"
|
||||||
|
return (
|
||||||
|
f"Your modes: {menu}. You're in {cur} right now. If Brian is clearly doing a "
|
||||||
|
"different kind of work than your current mode — weighing a real decision while "
|
||||||
|
"you're in Talk, digging into engineering, reviewing poker away from the table — "
|
||||||
|
"briefly OFFER to switch (one short line). If he says yes, call set_mode with the "
|
||||||
|
"mode key. Don't offer every turn or nag; only when it genuinely fits and serves him."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _now_note() -> Message:
|
def _now_note() -> Message:
|
||||||
"""Current wall-clock time + how long since Brian last said anything."""
|
"""Current wall-clock time + how long since Brian last said anything."""
|
||||||
line = f"The current date and time is {clock.stamp()}."
|
line = f"The current date and time is {clock.stamp()}."
|
||||||
@@ -109,6 +123,10 @@ def build_messages(session_id: str, user_msg: str,
|
|||||||
if mode and mode.card:
|
if mode and mode.card:
|
||||||
messages.append({"role": "system", "content": mode.card})
|
messages.append({"role": "system", "content": mode.card})
|
||||||
|
|
||||||
|
# Mode awareness: she can offer to switch when the work clearly shifts (she decides
|
||||||
|
# when — better than a keyword guess). One line, on his yes she calls set_mode.
|
||||||
|
messages.append({"role": "system", "content": _mode_menu_note(mode)})
|
||||||
|
|
||||||
# Live ritual state (e.g. Alligator Blood ON) — dynamic, rides with the card.
|
# Live ritual state (e.g. Alligator Blood ON) — dynamic, rides with the card.
|
||||||
state_note = _mode_state_note(mode)
|
state_note = _mode_state_note(mode)
|
||||||
if state_note:
|
if state_note:
|
||||||
|
|||||||
+1
-1
@@ -42,7 +42,7 @@ _LOOKUPS = ("player_profile", "get_villain_file", "running_stats", "recent_sessi
|
|||||||
|
|
||||||
# Always-available core tools (her own agency: journaling/notes/starting a thought
|
# Always-available core tools (her own agency: journaling/notes/starting a thought
|
||||||
# thread, and capturing Brian's reaction when she raises one of her thoughts in chat).
|
# thread, and capturing Brian's reaction when she raises one of her thoughts in chat).
|
||||||
_BASE = ("journal_write", "note", "think_about", "thought_response")
|
_BASE = ("journal_write", "note", "think_about", "thought_response", "set_mode")
|
||||||
|
|
||||||
# The full live cash-game toolset (incl. Brian's mental-game rituals).
|
# The full live cash-game toolset (incl. Brian's mental-game rituals).
|
||||||
_CASH_TOOLS = _BASE + _LOOKUPS + (
|
_CASH_TOOLS = _BASE + _LOOKUPS + (
|
||||||
|
|||||||
@@ -52,6 +52,20 @@ def _think_about(args: dict, ctx: dict) -> str:
|
|||||||
"I'll come back to it on my own between our conversations.")
|
"I'll come back to it on my own between our conversations.")
|
||||||
|
|
||||||
|
|
||||||
|
def _set_mode(args: dict, ctx: dict) -> str:
|
||||||
|
from lyra import modes
|
||||||
|
key = (args.get("mode") or "").strip().lower()
|
||||||
|
m = modes.MODES.get(key)
|
||||||
|
if not m:
|
||||||
|
return f"(unknown mode '{key}'; valid: {', '.join(modes.MODES)})"
|
||||||
|
sid = ctx.get("session_id")
|
||||||
|
if not sid:
|
||||||
|
return "(no session to switch)"
|
||||||
|
memory.set_session_mode(sid, key)
|
||||||
|
logbus.log("info", "mode switch (tool)", session=sid, mode=key)
|
||||||
|
return f"Switched to {m.label} mode."
|
||||||
|
|
||||||
|
|
||||||
def _thought_response(args: dict, ctx: dict) -> str:
|
def _thought_response(args: dict, ctx: dict) -> str:
|
||||||
try:
|
try:
|
||||||
tid = int(args.get("thread_id"))
|
tid = int(args.get("thread_id"))
|
||||||
@@ -452,6 +466,12 @@ _S = {"type": "string"}
|
|||||||
_N = {"type": "number"}
|
_N = {"type": "number"}
|
||||||
|
|
||||||
TOOLS.update({
|
TOOLS.update({
|
||||||
|
"set_mode": {"handler": _set_mode, "spec": _f(
|
||||||
|
"set_mode",
|
||||||
|
"Switch your conversation mode when the work clearly shifts and Brian's agreed to it. "
|
||||||
|
"Offer first ('want me in Decide for this?'), then call this on his yes.",
|
||||||
|
{"mode": {**_S, "description": "Mode key: conversation | poker_cash | build | explore | study | decide"}},
|
||||||
|
["mode"])},
|
||||||
"thought_response": {"handler": _thought_response, "spec": _f(
|
"thought_response": {"handler": _thought_response, "spec": _f(
|
||||||
"thought_response",
|
"thought_response",
|
||||||
"When you've brought one of your own thoughts/threads to Brian and he responds to "
|
"When you've brought one of your own thoughts/threads to Brian and he responds to "
|
||||||
|
|||||||
@@ -47,6 +47,16 @@ def test_every_mode_tool_exists(lyra):
|
|||||||
assert set(mode.tools) <= set(tools.TOOLS), f"{mode.key} references unknown tools"
|
assert set(mode.tools) <= set(tools.TOOLS), f"{mode.key} references unknown tools"
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_mode_tool_switches_session(lyra):
|
||||||
|
memory, _, _, tools = lyra
|
||||||
|
memory.ensure_session("s1")
|
||||||
|
out = tools.dispatch("set_mode", {"mode": "decide"}, {"session_id": "s1"})
|
||||||
|
assert "Decide" in out and memory.get_session_mode("s1") == "decide"
|
||||||
|
# unknown mode is handled, session unchanged
|
||||||
|
assert "unknown" in tools.dispatch("set_mode", {"mode": "nope"}, {"session_id": "s1"}).lower()
|
||||||
|
assert memory.get_session_mode("s1") == "decide"
|
||||||
|
|
||||||
|
|
||||||
def test_work_modes_present_and_gated(lyra):
|
def test_work_modes_present_and_gated(lyra):
|
||||||
_, _, modes, tools = lyra
|
_, _, modes, tools = lyra
|
||||||
# the full set Brian chose
|
# the full set Brian chose
|
||||||
|
|||||||
Reference in New Issue
Block a user