1
0

feat: add thought_search tool for querying Timmy's thinking history (#260)

Co-authored-by: Kimi Agent <kimi@timmy.local>
Co-committed-by: Kimi Agent <kimi@timmy.local>
This commit is contained in:
2026-03-15 19:35:58 -04:00
committed by hermes
parent 80aba0bf6d
commit bcbdc7d7cb
3 changed files with 203 additions and 6 deletions

View File

@@ -833,6 +833,115 @@ def test_thinking_chain_api_404(client):
assert response.status_code == 404
# ---------------------------------------------------------------------------
# Thought search
# ---------------------------------------------------------------------------
def test_search_thoughts_basic(tmp_path):
"""search_thoughts should find thoughts by content substring."""
from timmy import thinking
engine = _make_engine(tmp_path)
engine._store_thought("I wonder about sovereignty and freedom.", "existential")
engine._store_thought("The swarm is performing well today.", "swarm")
engine._store_thought("True sovereignty comes from local execution.", "sovereignty")
with patch.object(thinking, "thinking_engine", engine):
result = thinking.search_thoughts("sovereignty")
assert "Found 2 thought(s)" in result
assert "sovereignty" in result.lower()
def test_search_thoughts_with_seed_type(tmp_path):
"""search_thoughts should filter by seed_type when provided."""
from timmy import thinking
engine = _make_engine(tmp_path)
engine._store_thought("I wonder about sovereignty and freedom.", "existential")
engine._store_thought("True sovereignty comes from local execution.", "sovereignty")
with patch.object(thinking, "thinking_engine", engine):
result = thinking.search_thoughts("sovereignty", seed_type="sovereignty")
assert "Found 1 thought(s)" in result
assert '[seed_type="sovereignty"]' in result
def test_search_thoughts_no_matches(tmp_path):
"""search_thoughts should return helpful message when no matches found."""
from timmy import thinking
engine = _make_engine(tmp_path)
engine._store_thought("A thought about memory.", "memory")
with patch.object(thinking, "thinking_engine", engine):
result = thinking.search_thoughts("xyz_nonexistent")
assert "No thoughts found" in result
def test_search_thoughts_limit(tmp_path):
"""search_thoughts should respect the limit parameter."""
from timmy import thinking
engine = _make_engine(tmp_path)
for i in range(5):
engine._store_thought(f"Sovereignty thought number {i}.", "sovereignty")
with patch.object(thinking, "thinking_engine", engine):
result = thinking.search_thoughts("sovereignty", limit=3)
assert "Found 3 thought(s)" in result
def test_search_thoughts_limit_bounds(tmp_path):
"""search_thoughts should clamp limit to valid bounds."""
from timmy import thinking
engine = _make_engine(tmp_path)
engine._store_thought("A test thought.", "freeform")
with patch.object(thinking, "thinking_engine", engine):
# These should not raise errors - just clamp internally
result_low = thinking.search_thoughts("test", limit=0)
result_high = thinking.search_thoughts("test", limit=100)
# Both should execute (may return no results, but shouldn't crash)
assert isinstance(result_low, str)
assert isinstance(result_high, str)
def test_search_thoughts_case_insensitive(tmp_path):
"""search_thoughts should be case-insensitive (SQLite LIKE is case-insensitive)."""
from timmy import thinking
engine = _make_engine(tmp_path)
engine._store_thought("The SWARM is active today.", "swarm")
with patch.object(thinking, "thinking_engine", engine):
result_lower = thinking.search_thoughts("swarm")
result_upper = thinking.search_thoughts("SWARM")
result_mixed = thinking.search_thoughts("Swarm")
assert "Found 1 thought(s)" in result_lower
assert "Found 1 thought(s)" in result_upper
assert "Found 1 thought(s)" in result_mixed
def test_search_thoughts_returns_formatted_output(tmp_path):
"""search_thoughts should return formatted output with timestamps and seed types."""
from timmy import thinking
engine = _make_engine(tmp_path)
engine._store_thought("A memorable thought about existence.", "existential")
with patch.object(thinking, "thinking_engine", engine):
result = thinking.search_thoughts("memorable")
# Should contain timestamp-like content (year in 2026)
assert "2026-" in result or "2025-" in result
# Should contain seed type
assert "existential" in result
# Should contain the thought content
assert "memorable thought" in result
# ---------------------------------------------------------------------------
# _call_agent uses skip_mcp=True (#72)
# ---------------------------------------------------------------------------
@@ -884,8 +993,7 @@ async def test_call_agent_strips_think_tags(tmp_path):
mock_agent = AsyncMock()
mock_run = AsyncMock()
mock_run.content = (
"<think>Let me reason about this carefully...</think>"
"The actual thought content."
"<think>Let me reason about this carefully...</think>The actual thought content."
)
mock_agent.arun.return_value = mock_run
@@ -903,10 +1011,7 @@ async def test_call_agent_strips_multiline_think_tags(tmp_path):
mock_agent = AsyncMock()
mock_run = AsyncMock()
mock_run.content = (
"<think>\nStep 1: analyze\nStep 2: synthesize\n</think>\n"
"Clean output here."
)
mock_run.content = "<think>\nStep 1: analyze\nStep 2: synthesize\n</think>\nClean output here."
mock_agent.arun.return_value = mock_run
with patch("timmy.agent.create_timmy", return_value=mock_agent):