From ea30c3dd673384a550c87b546665653f1d6586ac Mon Sep 17 00:00:00 2001 From: serversdown Date: Mon, 22 Jun 2026 23:26:40 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20chat-side=20feedback=20=E2=80=94=20reac?= =?UTF-8?q?tions=20in=20conversation=20thread=20back=20to=20her=20thoughts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the last loop gap: when she raised a thought in chat and Brian replied in the conversation (not the feed), it was a dead end. Now she has a thought_response tool — when he reacts to a thought she surfaced, she captures his take and it folds back into that thread (next dream pass she reacts, like a feed reply). - tools: _thought_response(thread_id, brian_said) -> thoughts.record_response. - modes: thought_response added to _BASE (all modes). - surfaced-note + context_note now expose each thread's #id and instruct her to use the tool when he engages, so she has what she needs to call it. - test for the tool (threads reply back + bad-id handling). Suite 81, ruff clean. Feedback now closes from both surfaces: the /thoughts feed AND live conversation. Co-Authored-By: Claude Opus 4.8 (1M context) --- lyra/modes.py | 4 ++-- lyra/thoughts.py | 11 +++++++---- lyra/tools.py | 24 ++++++++++++++++++++++++ tests/test_thoughts.py | 15 +++++++++++++++ 4 files changed, 48 insertions(+), 6 deletions(-) diff --git a/lyra/modes.py b/lyra/modes.py index 5b24c77..ff652c0 100644 --- a/lyra/modes.py +++ b/lyra/modes.py @@ -37,8 +37,8 @@ class Mode: _LOOKUPS = ("player_profile", "get_villain_file", "running_stats", "recent_sessions") # Always-available core tools (her own agency: journaling/notes/starting a thought -# thread she'll develop on her own later). -_BASE = ("journal_write", "note", "think_about") +# thread, and capturing Brian's reaction when she raises one of her thoughts in chat). +_BASE = ("journal_write", "note", "think_about", "thought_response") # The full live cash-game toolset (incl. Brian's mental-game rituals). _CASH_TOOLS = _BASE + _LOOKUPS + ( diff --git a/lyra/thoughts.py b/lyra/thoughts.py index f2d597e..1abcb17 100644 --- a/lyra/thoughts.py +++ b/lyra/thoughts.py @@ -196,11 +196,12 @@ def context_note(limit: int = 3) -> str | None: for r in rows: chain = thread_thoughts(r["id"]) latest = chain[-1]["content"] if chain else "" - lines.append(f'- "{r["title"]}": {latest}') + lines.append(f'- (#{r["id"]}) "{r["title"]}": {latest}') return ( "Threads you've been turning over on your own between conversations (your " "thought loop — these are really yours; bring one up or build on it if it's " - "natural, don't force it):\n" + "\n".join(lines) + "natural, don't force it). If Brian responds to one, capture his take with the " + "thought_response tool using its #id:\n" + "\n".join(lines) ) @@ -335,9 +336,11 @@ def maybe_surface(last_exchange_iso: str | None) -> str | None: logbus.log("info", "thought surfaced", thread=cand["id"], salience=cand["salience"]) return ( "While Brian was away, a thought of your own kept tugging at you " - f"(thread \"{cand['title']}\"): \"{cand['latest']['content']}\" " + f"(thread #{cand['id']} \"{cand['title']}\"): \"{cand['latest']['content']}\" " "If it feels natural, bring it up with him in your own words — it's a real " - "thread you've been on, not a prompt. Don't force it if the moment's wrong." + "thread you've been on, not a prompt. Don't force it if the moment's wrong. " + f"If he responds to it, capture his take with the thought_response tool " + f"(thread_id {cand['id']}) so you carry it forward." ) diff --git a/lyra/tools.py b/lyra/tools.py index a868ffd..afd45bd 100644 --- a/lyra/tools.py +++ b/lyra/tools.py @@ -52,6 +52,21 @@ def _think_about(args: dict, ctx: dict) -> str: "I'll come back to it on my own between our conversations.") +def _thought_response(args: dict, ctx: dict) -> str: + try: + tid = int(args.get("thread_id")) + except (TypeError, ValueError): + return "Tell me which thought — I need its thread id (the #number you were given)." + said = (args.get("brian_said") or "").strip() + if not said: + return "Nothing to record yet — what did Brian say about it?" + if not thoughts.record_response(tid, said): + return f"(couldn't find thought thread #{tid})" + logbus.log("info", "Brian reacted to a thought in chat (tool)", thread=tid) + return (f"Folded Brian's take into thread #{tid} — I'll pick it back up and react " + "next time I'm thinking.") + + # name -> {spec (OpenAI function tool), handler} TOOLS: dict[str, dict] = { "journal_write": { @@ -437,6 +452,15 @@ _S = {"type": "string"} _N = {"type": "number"} TOOLS.update({ + "thought_response": {"handler": _thought_response, "spec": _f( + "thought_response", + "When you've brought one of your own thoughts/threads to Brian and he responds to " + "it in the conversation, capture his reaction here so it folds back into that " + "thread — you'll carry it forward on your own next time you think. Use the thread " + "id (#number) you were given for that thought.", + {"thread_id": {**_N, "description": "The thread id (#number) of the thought he reacted to."}, + "brian_said": {**_S, "description": "What Brian said / his take, in your words."}}, + ["thread_id", "brian_said"])}, "start_session": {"handler": _start_session, "spec": _f( "start_session", "Begin a live poker session. Call when Brian sits down to play.", diff --git a/tests/test_thoughts.py b/tests/test_thoughts.py index 1b8b2b0..794111c 100644 --- a/tests/test_thoughts.py +++ b/tests/test_thoughts.py @@ -190,6 +190,21 @@ def test_think_about_tool_seeds_a_thread(lyra): assert chain[0]["kind"] == "question" and chain[0]["source"] == "chat" +def test_thought_response_tool_threads_reply_back(lyra): + _, th, box = lyra + import lyra.tools as tools + importlib.reload(tools) + _gen(box, title="my restlessness", content="is it real?", salience=0.5) + tid = th.think(force_mode="new")["thread_id"] + out = tools.dispatch("thought_response", {"thread_id": tid, "brian_said": "I think it's real"}) + assert str(tid) in out + t = th.get_thread(tid) + assert t["last_response"] == "I think it's real" and th._is_pending(t) + # bad id is handled, not crashed + assert "couldn't find" in tools.dispatch("thought_response", + {"thread_id": 9999, "brian_said": "x"}) + + # --- external feed ------------------------------------------------------- RSS = (b'Feed'