Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions cron/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,16 @@ def _normalize_job_record(job: Dict[str, Any]) -> Dict[str, Any]:
state = "scheduled" if normalized.get("enabled", True) else "paused"
normalized["state"] = state

try:
normalized["reasoning_effort"] = _normalize_reasoning_effort(
normalized.get("reasoning_effort")
)
except ValueError as exc:
logger.warning(
"Job '%s': invalid reasoning_effort in stored record: %s", job_id, exc
)
normalized["reasoning_effort"] = None

return normalized


Expand Down Expand Up @@ -975,6 +985,19 @@ def _normalize_job_optional_text(value: Any, *, strip_trailing_slash: bool = Fal
return text or None


def _normalize_reasoning_effort(value: Any) -> Optional[str]:
"""Normalize and validate an optional per-job reasoning override."""
if value is None or (isinstance(value, str) and not value.strip()):
return None
from hermes_constants import VALID_REASONING_EFFORTS, parse_reasoning_effort

parsed = parse_reasoning_effort(value)
if parsed is None:
valid = ", ".join(("none", *VALID_REASONING_EFFORTS))
raise ValueError(f"reasoning_effort must be one of: {valid}")
return "none" if not parsed["enabled"] else parsed["effort"]


def _compute_provider_model_snapshots(
*,
provider: Any,
Expand Down Expand Up @@ -1048,6 +1071,7 @@ def create_job(
workdir: Optional[str] = None,
no_agent: bool = False,
attach_to_session: Optional[bool] = None,
reasoning_effort: Optional[str] = None,
) -> Dict[str, Any]:
"""
Create a new cron job.
Expand Down Expand Up @@ -1124,6 +1148,7 @@ def create_job(
normalized_workdir = _normalize_workdir(workdir)
normalized_no_agent = bool(no_agent)
normalized_attach = attach_to_session if isinstance(attach_to_session, bool) else None
normalized_reasoning_effort = _normalize_reasoning_effort(reasoning_effort)

# no_agent jobs are meaningless without a script — the script IS the job.
# Surface this as a clear ValueError at create time so bad configs never
Expand Down Expand Up @@ -1213,6 +1238,7 @@ def create_job(
"origin": origin, # Tracks where job was created for "origin" delivery
"enabled_toolsets": normalized_toolsets,
"workdir": normalized_workdir,
"reasoning_effort": normalized_reasoning_effort,
}
# Only persist attach_to_session when explicitly set, so existing jobs and
# the common case stay byte-identical (absent key => fall back to the
Expand Down Expand Up @@ -1309,6 +1335,11 @@ def update_job(job_id: str, updates: Dict[str, Any]) -> Optional[Dict[str, Any]]
else:
updates["workdir"] = _normalize_workdir(_wd)

if "reasoning_effort" in updates:
updates["reasoning_effort"] = _normalize_reasoning_effort(
updates.get("reasoning_effort")
)

previous_inference_axes = _normalized_inference_axes(job)
updated = _apply_skill_fields({**job, **updates})
schedule_changed = "schedule" in updates
Expand Down
25 changes: 21 additions & 4 deletions cron/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2864,12 +2864,29 @@ def run_job(
except Exception:
pass

# Reasoning config from config.yaml (raw value — a YAML boolean False
# means thinking disabled, see parse_reasoning_effort)
# A valid per-job override wins; absent or invalid stored values safely
# fall back to config.yaml. Literal "none" explicitly disables reasoning.
from hermes_constants import parse_reasoning_effort
reasoning_config = parse_reasoning_effort(
_cfg.get("agent", {}).get("reasoning_effort", "")
reasoning_agent_cfg = _cfg.get("agent", {})
global_effort = (
reasoning_agent_cfg.get("reasoning_effort", "")
if isinstance(reasoning_agent_cfg, dict)
else ""
)
job_effort = job.get("reasoning_effort")
has_job_effort = job_effort is not None and job_effort != ""
reasoning_config = (
parse_reasoning_effort(job_effort)
if has_job_effort
else parse_reasoning_effort(global_effort)
)
if has_job_effort and reasoning_config is None:
logger.warning(
"Job '%s': invalid reasoning_effort %r; falling back to global agent.reasoning_effort",
job_id,
job_effort,
)
reasoning_config = parse_reasoning_effort(global_effort)

# Prefill messages from env or config.yaml. The top-level
# prefill_messages_file key is canonical; agent.prefill_messages_file is
Expand Down
19 changes: 19 additions & 0 deletions hermes_cli/cron.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ def cron_list(show_all: bool = False):
workdir = job.get("workdir")
if workdir:
print(f" Workdir: {workdir}")
if job.get("reasoning_effort") is not None:
print(f" Reasoning: {job['reasoning_effort']}")

# Execution history
last_status = job.get("last_status")
Expand Down Expand Up @@ -310,6 +312,7 @@ def cron_create(args):
script=getattr(args, "script", None),
workdir=getattr(args, "workdir", None),
no_agent=getattr(args, "no_agent", False) or None,
reasoning_effort=getattr(args, "reasoning_effort", None),
)
if not result.get("success"):
print(color(f"Failed to create job: {result.get('error', 'unknown error')}", Colors.RED))
Expand All @@ -326,6 +329,8 @@ def cron_create(args):
print(" Mode: no-agent (script stdout delivered directly)")
if job_data.get("workdir"):
print(f" Workdir: {job_data['workdir']}")
if job_data.get("reasoning_effort") is not None:
print(f" Reasoning: {job_data['reasoning_effort']}")
print(f" Next run: {result['next_run_at']}")
_warn_if_gateway_not_running()
return 0
Expand Down Expand Up @@ -361,6 +366,17 @@ def cron_edit(args):
if skill not in final_skills:
final_skills.append(skill)

if getattr(args, "clear_reasoning_effort", False) and getattr(
args, "reasoning_effort", None
):
print(color("Cannot combine --reasoning-effort with --clear-reasoning-effort", Colors.RED))
return 1
reasoning_effort = (
""
if getattr(args, "clear_reasoning_effort", False)
else getattr(args, "reasoning_effort", None)
)

result = _cron_api(
action="update",
job_id=args.job_id,
Expand All @@ -373,6 +389,7 @@ def cron_edit(args):
script=getattr(args, "script", None),
workdir=getattr(args, "workdir", None),
no_agent=getattr(args, "no_agent", None),
reasoning_effort=reasoning_effort,
)
if not result.get("success"):
print(color(f"Failed to update job: {result.get('error', 'unknown error')}", Colors.RED))
Expand All @@ -392,6 +409,8 @@ def cron_edit(args):
print(" Mode: no-agent (script stdout delivered directly)")
if updated.get("workdir"):
print(f" Workdir: {updated['workdir']}")
if updated.get("reasoning_effort") is not None:
print(f" Reasoning: {updated['reasoning_effort']}")
return 0


Expand Down
19 changes: 19 additions & 0 deletions hermes_cli/subcommands/cron.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@

from typing import Callable

from hermes_constants import VALID_REASONING_EFFORTS
from hermes_cli.subcommands._shared import add_accept_hooks_flag


_REASONING_EFFORT_CHOICES = ["none", *VALID_REASONING_EFFORTS]


def build_cron_parser(subparsers, *, cmd_cron: Callable) -> None:
"""Attach the ``cron`` subcommand (and its sub-actions) to ``subparsers``."""
cron_parser = subparsers.add_parser(
Expand Down Expand Up @@ -70,6 +74,11 @@ def build_cron_parser(subparsers, *, cmd_cron: Callable) -> None:
"--workdir",
help="Absolute path for the job to run from. Injects AGENTS.md / CLAUDE.md / .cursorrules from that directory and uses it as the cwd for terminal/file/code_exec tools. Omit to preserve old behaviour (no project context files).",
)
cron_create.add_argument(
"--reasoning-effort",
choices=_REASONING_EFFORT_CHOICES,
help="Per-job reasoning override; omit to inherit agent.reasoning_effort.",
)

# cron edit
cron_edit = cron_subparsers.add_parser(
Expand Down Expand Up @@ -134,6 +143,16 @@ def build_cron_parser(subparsers, *, cmd_cron: Callable) -> None:
"--workdir",
help="Absolute path for the job to run from (injects AGENTS.md etc. and sets terminal cwd). Pass empty string to clear.",
)
cron_edit.add_argument(
"--reasoning-effort",
choices=_REASONING_EFFORT_CHOICES,
help="Per-job reasoning override; 'none' explicitly disables reasoning.",
)
cron_edit.add_argument(
"--clear-reasoning-effort",
action="store_true",
help="Clear the override and inherit agent.reasoning_effort.",
)

# lifecycle actions
cron_pause = cron_subparsers.add_parser("pause", help="Pause a scheduled job")
Expand Down
Loading
Loading