From 7dd3a028483402f6c91f86a13ab6f2600cf7cf23 Mon Sep 17 00:00:00 2001 From: kimi Date: Sat, 21 Mar 2026 10:07:17 -0400 Subject: [PATCH] feat: add produce_thought() to stream thinking to Matrix - Add produce_thought() function in src/infrastructure/presence.py - Returns dict with type='thought', agent_id, data={text, thought_id, chain_id}, ts - Text is truncated to 500 chars - Add comprehensive tests in tests/unit/test_presence.py Fixes #672 --- src/infrastructure/presence.py | 51 +++++++++++++++++++ tests/unit/test_presence.py | 90 +++++++++++++++++++++++++++++++++- 2 files changed, 140 insertions(+), 1 deletion(-) diff --git a/src/infrastructure/presence.py b/src/infrastructure/presence.py index 82c5e359..25e312cb 100644 --- a/src/infrastructure/presence.py +++ b/src/infrastructure/presence.py @@ -65,6 +65,57 @@ def produce_bark(agent_id: str, text: str, reply_to: str = None, style: str = "s } +def produce_thought( + agent_id: str, thought_text: str, thought_id: int, chain_id: str = None +) -> dict: + """Format a thinking engine thought as a Matrix thought message. + + Thoughts appear as subtle floating text in the 3D world, streaming from + Timmy's thinking engine (/thinking/api). This function wraps thoughts in + Matrix protocol format. + + Parameters + ---------- + agent_id: + Unique identifier for the agent (e.g. ``"timmy"``). + thought_text: + The thought text to display. Truncated to 500 characters. + thought_id: + Unique identifier for this thought (sequence number). + chain_id: + Optional chain identifier grouping related thoughts. + + Returns + ------- + dict + Thought message with keys ``type``, ``agent_id``, ``data`` (containing + ``text``, ``thought_id``, ``chain_id``), and ``ts``. + + Examples + -------- + >>> produce_thought("timmy", "Considering the options...", 42, "chain-123") + { + "type": "thought", + "agent_id": "timmy", + "data": {"text": "Considering the options...", "thought_id": 42, "chain_id": "chain-123"}, + "ts": 1742529600, + } + """ + # Truncate text to 500 characters (thoughts can be longer than barks) + truncated_text = thought_text[:500] if thought_text else "" + + return { + "type": "thought", + "agent_id": agent_id, + "data": { + "text": truncated_text, + "thought_id": thought_id, + "chain_id": chain_id, + }, + "ts": int(time.time()), + } + + def serialize_presence(presence: dict) -> dict: """Transform an ADR-023 presence dict into the world-state API shape. diff --git a/tests/unit/test_presence.py b/tests/unit/test_presence.py index 4e308de9..1c4eb7ca 100644 --- a/tests/unit/test_presence.py +++ b/tests/unit/test_presence.py @@ -4,7 +4,12 @@ from unittest.mock import patch import pytest -from infrastructure.presence import produce_agent_state, produce_bark, serialize_presence +from infrastructure.presence import ( + produce_agent_state, + produce_bark, + produce_thought, + serialize_presence, +) class TestSerializePresence: @@ -269,3 +274,86 @@ class TestProduceBark: assert result["data"]["text"] == "Running test suite..." assert result["data"]["reply_to"] == "parent-msg-456" assert result["data"]["style"] == "thought" + + +class TestProduceThought: + """Tests for produce_thought() — Matrix thought message producer.""" + + @patch("infrastructure.presence.time") + def test_full_message_structure(self, mock_time): + """Returns dict with type, agent_id, data, and ts keys.""" + mock_time.time.return_value = 1742529600 + result = produce_thought("timmy", "Considering the options...", 42) + + assert result["type"] == "thought" + assert result["agent_id"] == "timmy" + assert result["ts"] == 1742529600 + assert isinstance(result["data"], dict) + + def test_data_fields(self): + """data dict contains text, thought_id, and chain_id.""" + result = produce_thought("timmy", "Considering...", 42, chain_id="chain-123") + data = result["data"] + + assert data["text"] == "Considering..." + assert data["thought_id"] == 42 + assert data["chain_id"] == "chain-123" + + def test_default_chain_id_is_none(self): + """When chain_id is not provided, defaults to None.""" + result = produce_thought("timmy", "Thinking...", 1) + assert result["data"]["chain_id"] is None + + def test_text_truncated_to_500_chars(self): + """Text longer than 500 chars is truncated.""" + long_text = "A" * 600 + result = produce_thought("timmy", long_text, 1) + assert len(result["data"]["text"]) == 500 + assert result["data"]["text"] == "A" * 500 + + def test_text_exactly_500_chars_not_truncated(self): + """Text exactly 500 chars is not truncated.""" + text = "B" * 500 + result = produce_thought("timmy", text, 1) + assert result["data"]["text"] == text + + def test_text_shorter_than_500_not_padded(self): + """Text shorter than 500 chars is not padded.""" + result = produce_thought("timmy", "Short thought", 1) + assert result["data"]["text"] == "Short thought" + + def test_empty_text_handled(self): + """Empty text is handled gracefully.""" + result = produce_thought("timmy", "", 1) + assert result["data"]["text"] == "" + + def test_ts_is_unix_timestamp(self): + """ts should be an integer Unix timestamp.""" + result = produce_thought("timmy", "Hello!", 1) + assert isinstance(result["ts"], int) + assert result["ts"] > 0 + + def test_agent_id_passed_through(self): + """agent_id appears in the top-level message.""" + result = produce_thought("spark", "Hello!", 1) + assert result["agent_id"] == "spark" + + def test_thought_id_passed_through(self): + """thought_id appears in the data.""" + result = produce_thought("timmy", "Hello!", 999) + assert result["data"]["thought_id"] == 999 + + def test_with_all_parameters(self): + """Full parameter set produces expected output.""" + result = produce_thought( + agent_id="timmy", + thought_text="Analyzing the situation...", + thought_id=42, + chain_id="chain-abc", + ) + + assert result["type"] == "thought" + assert result["agent_id"] == "timmy" + assert result["data"]["text"] == "Analyzing the situation..." + assert result["data"]["thought_id"] == 42 + assert result["data"]["chain_id"] == "chain-abc" -- 2.43.0