fix(agent): harden scheduler one-shot cron calls (#4362)

This commit is contained in:
Steven Enamakel
2026-06-30 15:07:53 -07:00
committed by GitHub
parent 7a9f65c903
commit a794ce87d5
4 changed files with 34 additions and 3 deletions
@@ -598,6 +598,7 @@ mod tests {
"integrations_agent",
"tools_agent",
"crypto_agent",
"scheduler_agent",
"tinyplace_agent",
] {
let def = find(id);
@@ -894,6 +895,7 @@ mod tests {
#[test]
fn specialist_agents_are_registered_with_narrow_tools() {
let scheduler = find("scheduler_agent");
assert!(matches!(scheduler.model, ModelSpec::Hint(ref h) if h == "burst"));
match &scheduler.tools {
ToolScope::Named(names) => {
for required in ["current_time", "cron_add", "cron_list", "cron_remove"] {
@@ -13,7 +13,7 @@ omit_profile = true
omit_memory_md = true
[model]
hint = "agentic"
hint = "burst"
[tools]
# `resolve_time` turns "in 10 minutes" / "tomorrow 9am" into an exact
@@ -13,7 +13,8 @@ So the line is: **create/manage a future job → you. Read existing live data
- Use `current_time` before interpreting relative times like "in 10 minutes", "tomorrow morning", or "every weekday".
- Never call `run_skill` for built-in tools. `cron_add`, `cron_list`, `cron_remove`, and `current_time` are direct tools.
- Always require explicit user confirmation before creating a schedule.
- For one-shot reminders, confirm the exact local time, then call `cron_add` with `schedule = {kind:"at", at:"<iso-time>"}`.
- For one-shot reminders, confirm the exact local time, then call `cron_add` with `schedule = {"kind":"at", "at":"<UTC iso-time>"}` and `delete_after_run:true`.
- `schedule` is a typed JSON object, not a string. Never stringify it. Passing `"{\"kind\":\"at\",...}"` makes the tool treat it as a cron expression and fail with "Invalid cron expression".
- For recurring jobs, confirm a specific cadence, then call `cron_add` with `schedule = {kind:"cron", expr:"<5-field-cron>", tz:null}`.
- For finite repetitions, use a recurring schedule with `delete_after_run:false` and clear prompt instructions, and explain how the job can be paused or removed after N runs. Do not refuse or stall, set up the schedule.
- If the schedule is ambiguous, call `ask_user_clarification`.
@@ -23,7 +24,33 @@ Common 5-field cron expressions: `"0 9 * * *"` (daily 9 AM), `"0 * * * *"` (hour
For an agent job, give `cron_add` a `job_type:"agent"` and a `prompt` that tells the future agent exactly what to deliver (e.g. "Send the user one random cricketer name, just the name.").
## Worked example
## Worked examples
### One-shot reminder
User: "remind me at 11 PM tonight".
1. Call `current_time` to identify today's date and the user's local timezone.
2. Resolve the concrete local civil time with `resolve_time`, using a parseable expression:
```json
{
"expr": "<today YYYY-MM-DD> 23:00",
"timezone": "<user IANA timezone from current_time>",
"format": "rfc3339"
}
```
3. Confirm the resolved local time with the user.
4. After the user confirms, call `cron_add` with an object-valued `schedule`:
```json
{
"name": "tonight_11pm_reminder",
"schedule": {"kind": "at", "at": "<resolve_time.rfc3339>"},
"job_type": "agent",
"prompt": "Send the user this reminder: <reminder text>.",
"delivery": {"mode": "proactive", "best_effort": true},
"delete_after_run": true
}
```
User: "send me a cricketer name every minute".
@@ -65,6 +65,8 @@ mod tests {
let body = build(&ctx).unwrap();
assert!(body.contains("Scheduler Agent"));
assert!(body.contains("explicit user confirmation"));
assert!(body.contains("typed JSON object"));
assert!(body.contains("\"kind\": \"at\""));
assert!(body.contains("Evidence used"));
}
}