Skip to content
Open
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
13 changes: 12 additions & 1 deletion nerve/agent/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -967,6 +967,7 @@ async def _get_or_create_client(
self, session_id: str, source: str, model: str | None,
fork_from: str | None = None,
fork_last_turn_id: str | None = None,
effort_override: str | None = None,
) -> AgentClient:
"""Get an existing persistent client or create a new one.

Expand Down Expand Up @@ -1165,7 +1166,7 @@ async def _record_wakeup_cb(sid: str, tool_input: dict) -> Any:
session_id=session_id,
source=source,
model=requested_model,
effort=self._base_effort_for_source(
effort=effort_override or self._base_effort_for_source(
source, self.config.agent.effort,
self.config.agent.cron_effort,
),
Expand Down Expand Up @@ -2103,6 +2104,7 @@ async def run(
source: str = "web",
channel: str | None = None,
model: str | None = None,
effort_override: str | None = None,
internal: bool = False,
images: list[dict[str, Any]] | None = None,
image_refs: list[dict[str, Any]] | None = None,
Expand Down Expand Up @@ -2148,6 +2150,7 @@ async def run(
try:
return await self._run_inner(
session_id, user_message, source, channel, model,
effort_override=effort_override,
internal=internal, images=images,
image_refs=image_refs,
)
Expand Down Expand Up @@ -2191,6 +2194,7 @@ async def _run_inner(
source: str,
channel: str | None,
model: str | None,
effort_override: str | None = None,
internal: bool = False,
images: list[dict[str, Any]] | None = None,
image_refs: list[dict[str, Any]] | None = None,
Expand Down Expand Up @@ -2271,6 +2275,7 @@ async def _run_inner(
client = await self._get_or_create_client(
session_id, source, model, fork_from=fork_from,
fork_last_turn_id=fork_last_turn_id,
effort_override=effort_override,
)

# Check for deferred /stop that arrived while we were setting up
Expand Down Expand Up @@ -2380,6 +2385,7 @@ async def _run_inner(
await self._safe_disconnect(client)
client = await self._get_or_create_client(
session_id, source, model,
effort_override=effort_override,
)
continue # retry the query

Expand Down Expand Up @@ -2416,6 +2422,7 @@ async def _run_inner(
await self._safe_disconnect(client)
client = await self._get_or_create_client(
session_id, source, model,
effort_override=effort_override,
)
continue # retry query + response
break # success — exit retry loop
Expand Down Expand Up @@ -2994,6 +3001,7 @@ async def run_cron(
model: str | None = None,
run_id: str | None = None,
cache_ttl: str = "",
effort: str | None = None,
) -> str:
"""Run an agent turn for a cron job in an isolated session.

Expand All @@ -3014,6 +3022,7 @@ async def run_cron(
user_message=prompt,
source="cron",
model=model, # backend default_model(source) fills cron defaults
effort_override=effort,
)
finally:
await self._teardown_oneshot_client(session_id)
Expand All @@ -3025,6 +3034,7 @@ async def run_persistent_cron(
model: str | None = None,
session_id: str | None = None,
cache_ttl: str = "",
effort: str | None = None,
) -> str:
"""Run a persistent cron job that maintains context across runs.

Expand Down Expand Up @@ -3052,6 +3062,7 @@ async def run_persistent_cron(
user_message=prompt,
source="cron",
model=model, # backend default_model(source) fills cron defaults
effort_override=effort,
)
finally:
# The session is reused by the next run (until rotation), which
Expand Down
3 changes: 3 additions & 0 deletions nerve/cron/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class CronJob:
prompt_file: str = ""
description: str = ""
model: str = "" # Override model; empty = use config default
effort: str = "" # Override reasoning effort (low/medium/high/xhigh/max); empty = source default (cron_effort)
# Per-job prompt-cache TTL override: "5m", "1h" or "auto"; empty = use
# agent.cache_ttl from config. See nerve/agent/cache_policy.py.
cache_ttl: str = ""
Expand Down Expand Up @@ -121,6 +122,7 @@ def from_dict(cls, d: dict, base_dir: Path | None = None) -> CronJob:
prompt_file=d.get("prompt_file", ""),
description=d.get("description", ""),
model=d.get("model", ""),
effort=d.get("effort", ""),
cache_ttl=d.get("cache_ttl", ""),
session_mode=d.get("session_mode", "isolated"),
context_rotate_hours=int(d.get("context_rotate_hours", 24)),
Expand Down Expand Up @@ -183,6 +185,7 @@ def save_jobs(jobs: list[CronJob], jobs_file: Path) -> None:
"prompt_file": job.prompt_file,
"description": job.description,
"model": job.model,
"effort": job.effort,
"cache_ttl": job.cache_ttl,
"session_mode": job.session_mode,
"context_rotate_hours": job.context_rotate_hours,
Expand Down
3 changes: 3 additions & 0 deletions nerve/cron/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,7 @@ async def _run_job_inner(self, job: CronJob) -> None:

try:
model = job.model or self.config.agent.cron_model
effort = job.effort or None # per-job effort override; None = source default (cron_effort)
rotated = False
base_prompt = job.resolve_prompt()

Expand Down Expand Up @@ -665,6 +666,7 @@ async def _run_job_inner(self, job: CronJob) -> None:
model=model,
session_id=session_id,
cache_ttl=job.cache_ttl,
effort=effort,
)
else:
response = await self.engine.run_cron(
Expand All @@ -673,6 +675,7 @@ async def _run_job_inner(self, job: CronJob) -> None:
model=model,
run_id=run_id,
cache_ttl=job.cache_ttl,
effort=effort,
)

# Keep the tail of the response — for multi-message runs the
Expand Down
30 changes: 30 additions & 0 deletions tests/test_cron.py
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,36 @@ def test_from_dict_explicit_true(self):
assert job.lock is True


# ---------------------------------------------------------------------------
# CronJob.effort field (per-job reasoning-effort override)
# ---------------------------------------------------------------------------

class TestCronJobEffort:
def test_default_empty(self):
job = _make_job()
assert job.effort == ""

def test_from_dict_default(self):
job = CronJob.from_dict({"id": "x", "schedule": "1h", "prompt": "p"})
assert job.effort == ""

def test_from_dict_explicit(self):
job = CronJob.from_dict({
"id": "x", "schedule": "1h", "prompt": "p", "effort": "high",
})
assert job.effort == "high"

def test_round_trips_through_save_load(self, tmp_path):
from nerve.cron.jobs import load_jobs, save_jobs

job = CronJob.from_dict({
"id": "x", "schedule": "1h", "prompt": "p", "effort": "xhigh",
})
out = tmp_path / "out.yaml"
save_jobs([job], out)
assert load_jobs(out)[0].effort == "xhigh"


# ---------------------------------------------------------------------------
# Job lock (concurrent run serialization)
# ---------------------------------------------------------------------------
Expand Down
Loading