fix(heartbeat): prevent internal reasoning leaks and finalization fallback in delivery
Three failure modes addressed:
1. Model reflects HEARTBEAT.md instructions back as output instead of
executing them ("HEARTBEAT.md has active tasks listed...")
2. Model narrates decision logic ("Best judgment call: stay quiet")
3. Model produces empty output for silence, runner treats it as failure,
finalization retry generates "couldn't produce a final answer" which
gets delivered to the user
Changes:
- Add _is_deliverable() pre-filter in HeartbeatService._tick() that catches
finalization fallback messages and leaked reasoning patterns before they
reach the evaluator
- Wrap Phase 2 task input with a delivery-awareness preamble telling the
model its output goes directly to the user's messaging app
- Add meta-reasoning suppression criterion to evaluator template
No changes to agent/loop.py, runner.py, providers, or config schema.
This commit is contained in:
@@ -792,6 +792,14 @@ def _run_gateway(
|
||||
return "cli", "direct"
|
||||
|
||||
# Create heartbeat service
|
||||
heartbeat_preamble = (
|
||||
"[Your response will be delivered directly to the user's messaging app. "
|
||||
"Output ONLY the final user-facing message. Never reference internal "
|
||||
"files (HEARTBEAT.md, AWARENESS.md, etc.), your instructions, or your "
|
||||
"decision process. If nothing needs reporting, respond with just "
|
||||
"'All clear.' and nothing else.]\n\n"
|
||||
)
|
||||
|
||||
async def on_heartbeat_execute(tasks: str) -> str:
|
||||
"""Phase 2: execute heartbeat tasks through the full agent loop."""
|
||||
channel, chat_id = _pick_heartbeat_target()
|
||||
@@ -800,7 +808,7 @@ def _run_gateway(
|
||||
pass
|
||||
|
||||
resp = await agent.process_direct(
|
||||
tasks,
|
||||
heartbeat_preamble + tasks,
|
||||
session_key="heartbeat",
|
||||
channel=channel,
|
||||
chat_id=chat_id,
|
||||
|
||||
Reference in New Issue
Block a user