Merge PR #1054 to deliver agent response to user and fix HEARTBEAT_OK detection
fix(heartbeat): deliver agent response to user and fix HEARTBEAT_OK detection
This commit is contained in:
@@ -372,13 +372,21 @@ def gateway(
|
|||||||
session_key="heartbeat",
|
session_key="heartbeat",
|
||||||
channel=channel,
|
channel=channel,
|
||||||
chat_id=chat_id,
|
chat_id=chat_id,
|
||||||
# suppress: heartbeat should not push progress to external channels
|
on_progress=_silent, # suppress: heartbeat should not push progress to external channels
|
||||||
on_progress=_silent,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def on_heartbeat_notify(response: str) -> None:
|
||||||
|
"""Deliver a heartbeat response to the user's channel."""
|
||||||
|
from nanobot.bus.events import OutboundMessage
|
||||||
|
channel, chat_id = _pick_heartbeat_target()
|
||||||
|
if channel == "cli":
|
||||||
|
return # No external channel available to deliver to
|
||||||
|
await bus.publish_outbound(OutboundMessage(channel=channel, chat_id=chat_id, content=response))
|
||||||
|
|
||||||
heartbeat = HeartbeatService(
|
heartbeat = HeartbeatService(
|
||||||
workspace=config.workspace_path,
|
workspace=config.workspace_path,
|
||||||
on_heartbeat=on_heartbeat,
|
on_heartbeat=on_heartbeat,
|
||||||
|
on_notify=on_heartbeat_notify,
|
||||||
interval_s=30 * 60, # 30 minutes
|
interval_s=30 * 60, # 30 minutes
|
||||||
enabled=True
|
enabled=True
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,14 +9,15 @@ from loguru import logger
|
|||||||
# Default interval: 30 minutes
|
# Default interval: 30 minutes
|
||||||
DEFAULT_HEARTBEAT_INTERVAL_S = 30 * 60
|
DEFAULT_HEARTBEAT_INTERVAL_S = 30 * 60
|
||||||
|
|
||||||
# The prompt sent to agent during heartbeat
|
# Token the agent replies with when there is nothing to report
|
||||||
HEARTBEAT_PROMPT = """Read HEARTBEAT.md in your workspace (if it exists).
|
|
||||||
Follow any instructions or tasks listed there.
|
|
||||||
If nothing needs attention, reply with just: HEARTBEAT_OK"""
|
|
||||||
|
|
||||||
# Token that indicates "nothing to do"
|
|
||||||
HEARTBEAT_OK_TOKEN = "HEARTBEAT_OK"
|
HEARTBEAT_OK_TOKEN = "HEARTBEAT_OK"
|
||||||
|
|
||||||
|
# The prompt sent to agent during heartbeat
|
||||||
|
HEARTBEAT_PROMPT = (
|
||||||
|
"Read HEARTBEAT.md in your workspace and follow any instructions listed there. "
|
||||||
|
f"If nothing needs attention, reply with exactly: {HEARTBEAT_OK_TOKEN}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _is_heartbeat_empty(content: str | None) -> bool:
|
def _is_heartbeat_empty(content: str | None) -> bool:
|
||||||
"""Check if HEARTBEAT.md has no actionable content."""
|
"""Check if HEARTBEAT.md has no actionable content."""
|
||||||
@@ -39,19 +40,23 @@ class HeartbeatService:
|
|||||||
"""
|
"""
|
||||||
Periodic heartbeat service that wakes the agent to check for tasks.
|
Periodic heartbeat service that wakes the agent to check for tasks.
|
||||||
|
|
||||||
The agent reads HEARTBEAT.md from the workspace and executes any
|
The agent reads HEARTBEAT.md from the workspace and executes any tasks
|
||||||
tasks listed there. If nothing needs attention, it replies HEARTBEAT_OK.
|
listed there. If it has something to report, the response is forwarded
|
||||||
|
to the user via on_notify. If nothing needs attention, the agent replies
|
||||||
|
HEARTBEAT_OK and the response is silently dropped.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
workspace: Path,
|
workspace: Path,
|
||||||
on_heartbeat: Callable[[str], Coroutine[Any, Any, str]] | None = None,
|
on_heartbeat: Callable[[str], Coroutine[Any, Any, str]] | None = None,
|
||||||
|
on_notify: Callable[[str], Coroutine[Any, Any, None]] | None = None,
|
||||||
interval_s: int = DEFAULT_HEARTBEAT_INTERVAL_S,
|
interval_s: int = DEFAULT_HEARTBEAT_INTERVAL_S,
|
||||||
enabled: bool = True,
|
enabled: bool = True,
|
||||||
):
|
):
|
||||||
self.workspace = workspace
|
self.workspace = workspace
|
||||||
self.on_heartbeat = on_heartbeat
|
self.on_heartbeat = on_heartbeat
|
||||||
|
self.on_notify = on_notify
|
||||||
self.interval_s = interval_s
|
self.interval_s = interval_s
|
||||||
self.enabled = enabled
|
self.enabled = enabled
|
||||||
self._running = False
|
self._running = False
|
||||||
@@ -113,15 +118,14 @@ class HeartbeatService:
|
|||||||
if self.on_heartbeat:
|
if self.on_heartbeat:
|
||||||
try:
|
try:
|
||||||
response = await self.on_heartbeat(HEARTBEAT_PROMPT)
|
response = await self.on_heartbeat(HEARTBEAT_PROMPT)
|
||||||
|
if HEARTBEAT_OK_TOKEN in response.upper():
|
||||||
# Check if agent said "nothing to do"
|
logger.info("Heartbeat: OK (nothing to report)")
|
||||||
if HEARTBEAT_OK_TOKEN.replace("_", "") in response.upper().replace("_", ""):
|
|
||||||
logger.info("Heartbeat: OK (no action needed)")
|
|
||||||
else:
|
else:
|
||||||
logger.info("Heartbeat: completed task")
|
logger.info("Heartbeat: completed, delivering response")
|
||||||
|
if self.on_notify:
|
||||||
except Exception as e:
|
await self.on_notify(response)
|
||||||
logger.error("Heartbeat execution failed: {}", e)
|
except Exception:
|
||||||
|
logger.exception("Heartbeat execution failed")
|
||||||
|
|
||||||
async def trigger_now(self) -> str | None:
|
async def trigger_now(self) -> str | None:
|
||||||
"""Manually trigger a heartbeat."""
|
"""Manually trigger a heartbeat."""
|
||||||
|
|||||||
Reference in New Issue
Block a user