fix(cron): restore reasoning_effort on cronjob() for the CLI lane — model dispatch still drops it

The CLI (hermes cron create/edit) routes through cronjob(); removing the
parameter outright broke that lane (CI slices 6/9). The parameter is back
on the function, but CRONJOB_SCHEMA and the registry handler still omit
it — same pattern as the intentional model/provider/base_url omission.
New test proves a hallucinated reasoning_effort arg through the model
dispatch is dropped.
This commit is contained in:
Teknium
2026-08-20 19:33:40 -07:00
parent 991af03f4c
commit 43c6dace56
2 changed files with 45 additions and 5 deletions

View File

@@ -191,13 +191,42 @@ class TestCronjobToolReasoningEffort:
listed = json.loads(cronjob(action="list"))["jobs"][0]
assert "reasoning_effort" not in listed
def _tool_handler(self):
import tools.cronjob_tools as mod
return mod.registry._tools["cronjob"].handler
def test_schema_does_not_expose_reasoning_effort(self):
"""Policy pin: the model-facing tool schema must NOT offer the
"""Policy pin: the model-facing surface must NOT offer the
reasoning_effort knob. Models never choose model config; the CLI is
the only mutation surface for this field."""
the only mutation surface for this field. The cronjob() function
keeps the parameter for the CLI lane (hermes_cli/cron.py), but the
tool schema and the registry dispatch drop it — same pattern as
model/provider/base_url."""
import inspect
from tools.cronjob_tools import CRONJOB_SCHEMA, cronjob
import tools.cronjob_tools as mod
assert "reasoning_effort" not in CRONJOB_SCHEMA["parameters"]["properties"]
assert "reasoning_effort" not in inspect.signature(cronjob).parameters
assert "reasoning_effort" not in mod.CRONJOB_SCHEMA["parameters"]["properties"]
# The registry handler lambda must not forward the agent's args to
# the parameter (mirrors the intentional model/provider omission).
source = inspect.getsource(self._tool_handler())
assert 'args.get("reasoning_effort")' not in source
def test_tool_dispatch_drops_reasoning_effort_arg(self, tmp_cron_dir):
"""Even if a model hallucinates the argument, dispatch ignores it:
the created job must carry NO pin."""
import json
out = json.loads(
self._tool_handler()(
{
"action": "create",
"prompt": "daily digest",
"schedule": "every 1h",
"reasoning_effort": "max",
}
)
)
assert out["success"] is True
assert load_jobs()[0].get("reasoning_effort") is None

View File

@@ -1213,6 +1213,7 @@ def cronjob(
attach_to_session: Optional[bool] = None,
monitor_script: Optional[str] = None,
monitor_url: Optional[str] = None,
reasoning_effort: Optional[str] = None,
task_id: str = None,
session_id: Optional[str] = None,
) -> str:
@@ -1313,6 +1314,12 @@ def cronjob(
attach_to_session=attach_to_session,
monitor_script=_normalize_optional_job_value(monitor_script),
monitor_url=_normalize_optional_job_value(monitor_url),
# reasoning_effort reaches here from the CLI
# (hermes cron create --reasoning-effort) ONLY — it is
# deliberately absent from CRONJOB_SCHEMA and the model
# dispatch below: models do not make model-config
# decisions (standing policy).
reasoning_effort=reasoning_effort,
)
except CronSchedulerRegistrationError as exc:
_partial = exc.to_dict()
@@ -1495,6 +1502,10 @@ def cronjob(
updates["provider"] = _normalize_optional_job_value(provider)
if base_url is not None:
updates["base_url"] = _normalize_optional_job_value(base_url, strip_trailing_slash=True)
if reasoning_effort is not None:
# CLI-only lane (see create above): update_job validates
# against the canonical grammar; empty string clears the pin.
updates["reasoning_effort"] = reasoning_effort
# Re-validate the EFFECTIVE provider/base_url on EVERY update, not
# only when this update supplies provider/base_url. A job persisted
# before this guard (or written directly to the jobs store) may