mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-05-06 01:36:17 +00:00
fix: suppress intermediate progress output in cron jobs
Cron jobs now pass on_progress=_silent to process_direct, matching the heartbeat pattern. Previously, tool hints and streaming deltas were published to the user channel via bus during execution, but the final response could be rejected by evaluate_response — leaving users with confusing partial output and no conclusion. Closes #3319
This commit is contained in:
parent
8eddacf2f8
commit
79821a571f
@ -726,12 +726,17 @@ def _run_gateway(
|
|||||||
cron_token = None
|
cron_token = None
|
||||||
if isinstance(cron_tool, CronTool):
|
if isinstance(cron_tool, CronTool):
|
||||||
cron_token = cron_tool.set_cron_context(True)
|
cron_token = cron_tool.set_cron_context(True)
|
||||||
|
|
||||||
|
async def _silent(*_args, **_kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
resp = await agent.process_direct(
|
resp = await agent.process_direct(
|
||||||
reminder_note,
|
reminder_note,
|
||||||
session_key=f"cron:{job.id}",
|
session_key=f"cron:{job.id}",
|
||||||
channel=job.payload.channel or "cli",
|
channel=job.payload.channel or "cli",
|
||||||
chat_id=job.payload.to or "direct",
|
chat_id=job.payload.to or "direct",
|
||||||
|
on_progress=_silent,
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
if isinstance(cron_tool, CronTool) and cron_token is not None:
|
if isinstance(cron_tool, CronTool) and cron_token is not None:
|
||||||
|
|||||||
@ -1032,6 +1032,97 @@ def test_gateway_cron_evaluator_receives_scheduled_reminder_context(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_gateway_cron_job_suppresses_intermediate_progress(
|
||||||
|
monkeypatch, tmp_path: Path
|
||||||
|
) -> None:
|
||||||
|
"""Cron jobs must pass on_progress=_silent to process_direct so that
|
||||||
|
tool hints and streaming deltas are never leaked to the user channel
|
||||||
|
before evaluate_response decides whether to deliver."""
|
||||||
|
config_file = tmp_path / "instance" / "config.json"
|
||||||
|
config_file.parent.mkdir(parents=True)
|
||||||
|
config_file.write_text("{}")
|
||||||
|
|
||||||
|
config = Config()
|
||||||
|
config.agents.defaults.workspace = str(tmp_path / "config-workspace")
|
||||||
|
bus = MagicMock()
|
||||||
|
bus.publish_outbound = AsyncMock()
|
||||||
|
seen: dict[str, object] = {}
|
||||||
|
|
||||||
|
monkeypatch.setattr("nanobot.config.loader.set_config_path", lambda _path: None)
|
||||||
|
monkeypatch.setattr("nanobot.config.loader.load_config", lambda _path=None: config)
|
||||||
|
monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None)
|
||||||
|
monkeypatch.setattr("nanobot.cli.commands._make_provider", lambda _config: object())
|
||||||
|
monkeypatch.setattr("nanobot.bus.queue.MessageBus", lambda: bus)
|
||||||
|
monkeypatch.setattr("nanobot.session.manager.SessionManager", lambda _workspace: object())
|
||||||
|
|
||||||
|
class _FakeCron:
|
||||||
|
def __init__(self, _store_path: Path) -> None:
|
||||||
|
self.on_job = None
|
||||||
|
seen["cron"] = self
|
||||||
|
|
||||||
|
class _FakeAgentLoop:
|
||||||
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
|
self.model = "test-model"
|
||||||
|
self.tools = {}
|
||||||
|
|
||||||
|
async def process_direct(self, *_args, on_progress=None, **_kwargs):
|
||||||
|
seen["on_progress"] = on_progress
|
||||||
|
return OutboundMessage(
|
||||||
|
channel="telegram",
|
||||||
|
chat_id="user-1",
|
||||||
|
content="Done.",
|
||||||
|
)
|
||||||
|
|
||||||
|
async def close_mcp(self) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def run(self) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
class _StopAfterCronSetup:
|
||||||
|
def __init__(self, *_args, **_kwargs) -> None:
|
||||||
|
raise _StopGatewayError("stop")
|
||||||
|
|
||||||
|
async def _always_reject(*_args, **_kwargs) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
monkeypatch.setattr("nanobot.cron.service.CronService", _FakeCron)
|
||||||
|
monkeypatch.setattr("nanobot.agent.loop.AgentLoop", _FakeAgentLoop)
|
||||||
|
monkeypatch.setattr("nanobot.channels.manager.ChannelManager", _StopAfterCronSetup)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"nanobot.utils.evaluator.evaluate_response",
|
||||||
|
_always_reject,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = runner.invoke(app, ["gateway", "--config", str(config_file)])
|
||||||
|
assert isinstance(result.exception, _StopGatewayError)
|
||||||
|
|
||||||
|
cron = seen["cron"]
|
||||||
|
job = CronJob(
|
||||||
|
id="cron-silent-test",
|
||||||
|
name="test-silent",
|
||||||
|
payload=CronPayload(
|
||||||
|
message="Run something.",
|
||||||
|
deliver=True,
|
||||||
|
channel="telegram",
|
||||||
|
to="user-1",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
response = asyncio.run(cron.on_job(job))
|
||||||
|
|
||||||
|
assert response == "Done."
|
||||||
|
# on_progress must be a callable (the _silent noop), not None and not bus_progress
|
||||||
|
assert seen["on_progress"] is not None
|
||||||
|
assert callable(seen["on_progress"])
|
||||||
|
# Verify it actually swallows calls (no side effects)
|
||||||
|
asyncio.run(seen["on_progress"]("tool_hint", "🔧 $ echo test"))
|
||||||
|
# Nothing published to bus since evaluator rejected
|
||||||
|
bus.publish_outbound.assert_not_awaited()
|
||||||
|
|
||||||
|
|
||||||
def test_gateway_workspace_override_does_not_migrate_legacy_cron(
|
def test_gateway_workspace_override_does_not_migrate_legacy_cron(
|
||||||
monkeypatch, tmp_path: Path
|
monkeypatch, tmp_path: Path
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user