Skip to content

Commit 121542e

Browse files
authored
feat: cron agents can suppress delivery with [SILENT] response (NousResearch#1833)
feat: cron agents can suppress delivery with [SILENT] response
2 parents e101b55 + 4b62109 commit 121542e

3 files changed

Lines changed: 123 additions & 3 deletions

File tree

cron/scheduler.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@
3737

3838
from cron.jobs import get_due_jobs, mark_job_run, save_job_output
3939

40+
# Sentinel: when a cron agent has nothing new to report, it can start its
41+
# response with this marker to suppress delivery. Output is still saved
42+
# locally for audit.
43+
SILENT_MARKER = "[SILENT]"
44+
4045
# Resolve Hermes home directory (respects HERMES_HOME override)
4146
_hermes_home = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
4247

@@ -180,6 +185,17 @@ def _build_job_prompt(job: dict) -> str:
180185
"""Build the effective prompt for a cron job, optionally loading one or more skills first."""
181186
prompt = job.get("prompt", "")
182187
skills = job.get("skills")
188+
189+
# Always prepend [SILENT] guidance so the cron agent can suppress
190+
# delivery when it has nothing new or noteworthy to report.
191+
silent_hint = (
192+
"[SYSTEM: If you have nothing new or noteworthy to report, respond "
193+
"with exactly \"[SILENT]\" (optionally followed by a brief internal "
194+
"note). This suppresses delivery to the user while still saving "
195+
"output locally. Only use [SILENT] when there are genuinely no "
196+
"changes worth reporting.]\n\n"
197+
)
198+
prompt = silent_hint + prompt
183199
if skills is None:
184200
legacy = job.get("skill")
185201
skills = [legacy] if legacy else []
@@ -480,9 +496,16 @@ def tick(verbose: bool = True) -> int:
480496
if verbose:
481497
logger.info("Output saved to: %s", output_file)
482498

483-
# Deliver the final response to the origin/target chat
499+
# Deliver the final response to the origin/target chat.
500+
# If the agent responded with [SILENT], skip delivery (but
501+
# output is already saved above). Failed jobs always deliver.
484502
deliver_content = final_response if success else f"⚠️ Cron job '{job.get('name', job['id'])}' failed:\n{error}"
485-
if deliver_content:
503+
should_deliver = bool(deliver_content)
504+
if should_deliver and success and deliver_content.strip().upper().startswith(SILENT_MARKER):
505+
logger.info("Job '%s': agent returned %s — skipping delivery", job["id"], SILENT_MARKER)
506+
should_deliver = False
507+
508+
if should_deliver:
486509
try:
487510
_deliver_result(job, deliver_content)
488511
except Exception as de:

hermes_cli/commands.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ class CommandDef:
104104
subcommands=("list", "add", "create", "edit", "pause", "resume", "run", "remove")),
105105
CommandDef("reload-mcp", "Reload MCP servers from config", "Tools & Skills",
106106
aliases=("reload_mcp",)),
107+
CommandDef("browser", "Connect browser tools to your live Chrome via CDP", "Tools & Skills",
108+
cli_only=True, args_hint="[connect|disconnect|status]",
109+
subcommands=("connect", "disconnect", "status")),
107110
CommandDef("plugins", "List installed plugins and their status",
108111
"Tools & Skills", cli_only=True),
109112

tests/cron/test_scheduler.py

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import pytest
99

10-
from cron.scheduler import _resolve_origin, _resolve_delivery_target, _deliver_result, run_job
10+
from cron.scheduler import _resolve_origin, _resolve_delivery_target, _deliver_result, run_job, SILENT_MARKER
1111

1212

1313
class TestResolveOrigin:
@@ -449,3 +449,97 @@ def _skill_view(name):
449449
assert "Instructions for blogwatcher." in prompt_arg
450450
assert "Instructions for find-nearby." in prompt_arg
451451
assert "Combine the results." in prompt_arg
452+
453+
454+
class TestSilentDelivery:
455+
"""Verify that [SILENT] responses suppress delivery while still saving output."""
456+
457+
def _make_job(self):
458+
return {
459+
"id": "monitor-job",
460+
"name": "monitor",
461+
"deliver": "origin",
462+
"origin": {"platform": "telegram", "chat_id": "123"},
463+
}
464+
465+
def test_normal_response_delivers(self):
466+
with patch("cron.scheduler.get_due_jobs", return_value=[self._make_job()]), \
467+
patch("cron.scheduler.run_job", return_value=(True, "# output", "Results here", None)), \
468+
patch("cron.scheduler.save_job_output", return_value="/tmp/out.md"), \
469+
patch("cron.scheduler._deliver_result") as deliver_mock, \
470+
patch("cron.scheduler.mark_job_run"):
471+
from cron.scheduler import tick
472+
tick(verbose=False)
473+
deliver_mock.assert_called_once()
474+
475+
def test_silent_response_suppresses_delivery(self, caplog):
476+
with patch("cron.scheduler.get_due_jobs", return_value=[self._make_job()]), \
477+
patch("cron.scheduler.run_job", return_value=(True, "# output", "[SILENT]", None)), \
478+
patch("cron.scheduler.save_job_output", return_value="/tmp/out.md"), \
479+
patch("cron.scheduler._deliver_result") as deliver_mock, \
480+
patch("cron.scheduler.mark_job_run"):
481+
from cron.scheduler import tick
482+
with caplog.at_level(logging.INFO, logger="cron.scheduler"):
483+
tick(verbose=False)
484+
deliver_mock.assert_not_called()
485+
assert any(SILENT_MARKER in r.message for r in caplog.records)
486+
487+
def test_silent_with_note_suppresses_delivery(self):
488+
with patch("cron.scheduler.get_due_jobs", return_value=[self._make_job()]), \
489+
patch("cron.scheduler.run_job", return_value=(True, "# output", "[SILENT] No changes detected", None)), \
490+
patch("cron.scheduler.save_job_output", return_value="/tmp/out.md"), \
491+
patch("cron.scheduler._deliver_result") as deliver_mock, \
492+
patch("cron.scheduler.mark_job_run"):
493+
from cron.scheduler import tick
494+
tick(verbose=False)
495+
deliver_mock.assert_not_called()
496+
497+
def test_silent_is_case_insensitive(self):
498+
with patch("cron.scheduler.get_due_jobs", return_value=[self._make_job()]), \
499+
patch("cron.scheduler.run_job", return_value=(True, "# output", "[silent] nothing new", None)), \
500+
patch("cron.scheduler.save_job_output", return_value="/tmp/out.md"), \
501+
patch("cron.scheduler._deliver_result") as deliver_mock, \
502+
patch("cron.scheduler.mark_job_run"):
503+
from cron.scheduler import tick
504+
tick(verbose=False)
505+
deliver_mock.assert_not_called()
506+
507+
def test_failed_job_always_delivers(self):
508+
"""Failed jobs deliver regardless of [SILENT] in output."""
509+
with patch("cron.scheduler.get_due_jobs", return_value=[self._make_job()]), \
510+
patch("cron.scheduler.run_job", return_value=(False, "# output", "", "some error")), \
511+
patch("cron.scheduler.save_job_output", return_value="/tmp/out.md"), \
512+
patch("cron.scheduler._deliver_result") as deliver_mock, \
513+
patch("cron.scheduler.mark_job_run"):
514+
from cron.scheduler import tick
515+
tick(verbose=False)
516+
deliver_mock.assert_called_once()
517+
518+
def test_output_saved_even_when_delivery_suppressed(self):
519+
with patch("cron.scheduler.get_due_jobs", return_value=[self._make_job()]), \
520+
patch("cron.scheduler.run_job", return_value=(True, "# full output", "[SILENT]", None)), \
521+
patch("cron.scheduler.save_job_output") as save_mock, \
522+
patch("cron.scheduler._deliver_result") as deliver_mock, \
523+
patch("cron.scheduler.mark_job_run"):
524+
save_mock.return_value = "/tmp/out.md"
525+
from cron.scheduler import tick
526+
tick(verbose=False)
527+
save_mock.assert_called_once_with("monitor-job", "# full output")
528+
deliver_mock.assert_not_called()
529+
530+
531+
class TestBuildJobPromptSilentHint:
532+
"""Verify _build_job_prompt always injects [SILENT] guidance."""
533+
534+
def test_hint_always_present(self):
535+
from cron.scheduler import _build_job_prompt
536+
job = {"prompt": "Check for updates"}
537+
result = _build_job_prompt(job)
538+
assert "[SILENT]" in result
539+
assert "Check for updates" in result
540+
541+
def test_hint_present_even_without_prompt(self):
542+
from cron.scheduler import _build_job_prompt
543+
job = {"prompt": ""}
544+
result = _build_job_prompt(job)
545+
assert "[SILENT]" in result

0 commit comments

Comments
 (0)