Rebase onto upstream (a4d95fd) #12

Closed
wylab wants to merge 144 commits from rebase-onto-upstream into main
2 changed files with 117 additions and 15 deletions
Showing only changes of commit d00ee13c47 - Show all commits
+16 -15
View File
@@ -416,21 +416,7 @@ class AgentLoop:
preview = final_content[:120] + "..." if len(final_content) > 120 else final_content
logger.info(f"Response to {msg.channel}:{msg.sender_id}: {preview}")
# Append final assistant response to messages so it's captured in the tool chain slice
messages = self.context.add_assistant_message(
messages, final_content, None,
reasoning_content=final_reasoning,
)
# Save to session: user message + full tool chain (tool_use, tool_results, thinking, final reply)
# Store current_message (not msg.content) so the time prefix is preserved
# and cache keys match on subsequent turns
session.add_message("user", current_message)
for chain_msg in messages[turn_start:]:
session.add_raw_message(chain_msg)
self.sessions.save(session)
# Check for suppress mode
# Check for suppress mode BEFORE adding to session
suppress_output = msg.metadata.get("suppress_output", False) if msg.metadata else False
if suppress_output:
@@ -442,6 +428,21 @@ class AgentLoop:
final_content_for_session = final_content
outbound_metadata = msg.metadata or {}
# Append final assistant response to messages so it's captured in the tool chain slice
# Use the prefixed version for session storage
messages = self.context.add_assistant_message(
messages, final_content_for_session, None,
reasoning_content=final_reasoning,
)
# Save to session: user message + full tool chain (tool_use, tool_results, thinking, final reply)
# Store current_message (not msg.content) so the time prefix is preserved
# and cache keys match on subsequent turns
session.add_message("user", current_message)
for chain_msg in messages[turn_start:]:
session.add_raw_message(chain_msg)
self.sessions.save(session)
return OutboundMessage(
channel=msg.channel,
chat_id=msg.chat_id,
+101
View File
@@ -0,0 +1,101 @@
# tests/test_idle_heartbeat_integration.py
import pytest
import asyncio
from pathlib import Path
from datetime import datetime, timedelta
from nanobot.agent.loop import AgentLoop
from nanobot.bus.queue import MessageBus
from nanobot.heartbeat.service import HeartbeatService
from nanobot.session.manager import SessionManager
from nanobot.providers.base import LLMProvider, LLMResponse
from unittest.mock import AsyncMock, MagicMock
@pytest.mark.asyncio
async def test_idle_heartbeat_end_to_end():
"""
Integration test: heartbeat triggers when idle, runs in main session,
output is suppressed, session contains [HIDDEN] content.
"""
workspace = Path("/tmp/test-integration")
workspace.mkdir(exist_ok=True)
# Create HEARTBEAT.md with content
heartbeat_file = workspace / "HEARTBEAT.md"
heartbeat_file.write_text("# Test Task\n- Check something")
# Create mock provider
provider = MagicMock(spec=LLMProvider)
provider.chat = AsyncMock(return_value=LLMResponse(
content="Heartbeat executed successfully",
tool_calls=[] # has_tool_calls is a property, not a parameter
))
provider.get_default_model = MagicMock(return_value="test-model")
provider.thinking_budget = 0
# Create components
bus = MessageBus()
sessions = SessionManager(workspace)
loop = AgentLoop(
bus=bus,
provider=provider,
workspace=workspace,
session_manager=sessions
)
# Create session with old user message
session = sessions.get_or_create("telegram:239824268")
old_timestamp = (datetime.now() - timedelta(minutes=31)).isoformat()
session.messages.append({
"role": "user",
"content": "Old user message",
"timestamp": old_timestamp
})
sessions.save(session)
# Create heartbeat callback
async def on_heartbeat(prompt: str, metadata=None):
return await loop.process_direct(
prompt,
session_key="telegram:239824268",
channel="telegram",
chat_id="239824268",
metadata=metadata,
)
# Create heartbeat service
heartbeat = HeartbeatService(
workspace=workspace,
on_heartbeat=on_heartbeat,
interval_s=1,
enabled=True,
session_manager=sessions,
target_session_key="telegram:239824268",
idle_threshold_s=30 * 60,
)
# Trigger heartbeat
await heartbeat._tick()
# Reload session from disk
sessions._cache.clear() # Clear cache to force reload
session = sessions.get_or_create("telegram:239824268")
# Verify:
# 1. Session has new messages
assert len(session.messages) > 1
# 2. Find the heartbeat response (assistant message)
heartbeat_messages = [
m for m in session.messages
if m.get("role") == "assistant" and "[HIDDEN]" in m.get("content", "")
]
assert len(heartbeat_messages) > 0, "No [HIDDEN] heartbeat message found"
# 3. Verify content is prefixed with [HIDDEN]
heartbeat_msg = heartbeat_messages[0]
assert heartbeat_msg["content"].startswith("[HIDDEN]")
assert "Heartbeat executed successfully" in heartbeat_msg["content"]
# Cleanup
heartbeat_file.unlink()