Skip to content

Commit 79a8504

Browse files
committed
fix(agents): give every ControlHub mode the Cron tool
Asked to sweep a set of channels every 30 minutes, a Cowork-mode agent replied that it had no cron tool and fell back to chaining long `wait` calls. It was telling the truth: `Cron` was in Claw's tool list and nowhere else, so in Cowork, Team, DeepResearch, and the four shared coding modes the agent could not see it at all. Deferred exposure is not the cause — deferred tools are advertised by name — the tool simply was not in those modes' `default_tools`. ControlHub's `wait` documentation now tells the agent to schedule repeating work with Cron instead of holding the turn open, and ControlHub ships in all of those modes, so the guidance pointed at a tool the agent did not have. - Add `Cron` to Cowork, Team, DeepResearch, and `shared_coding_mode_tools` (agentic / debug / multitask / plan), so it is available everywhere ControlHub is. - Cover the pairing with an invariant test over the built-in modes; it caught the shared coding baseline, which the first pass had missed. - Say in ControlHub's description what to do if Cron is genuinely absent, rather than leaving a chain of long waits as the silent fallback. Scheduling stays behind the normal permission gate — Cron is not read-only and emits a `custom_tool` intent — so this widens what the agent can propose, not what it can do unattended.
1 parent 9f8b560 commit 79a8504

6 files changed

Lines changed: 51 additions & 1 deletion

File tree

‎src/crates/assembly/core/src/agentic/agents/definitions/modes/cowork.rs‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ impl CoworkMode {
5656
"WebSearch".to_string(),
5757
"WebFetch".to_string(),
5858
"ControlHub".to_string(),
59+
// Recurring office work ("check these channels every 30
60+
// minutes") is squarely this mode's job, and ControlHub's
61+
// `wait` sends schedules here rather than pinning a turn open
62+
// for the interval.
63+
"Cron".to_string(),
5964
"InitMiniApp".to_string(),
6065
"FinalizeMiniApp".to_string(),
6166
"PublishMiniApp".to_string(),
@@ -120,6 +125,15 @@ mod tests {
120125
}
121126
}
122127

128+
#[test]
129+
fn cowork_mode_can_schedule_recurring_work() {
130+
// Asked to sweep a set of channels every 30 minutes, this mode used to
131+
// reply that it had no cron tool — accurately, because Cron was not in
132+
// its list — and fall back to chaining long waits.
133+
let tools = CoworkMode::new().default_tools();
134+
assert!(tools.contains(&"Cron".to_string()));
135+
}
136+
123137
#[test]
124138
fn cowork_mode_includes_miniapp_lifecycle_tools_in_defaults() {
125139
let tools = CoworkMode::new().default_tools();

‎src/crates/assembly/core/src/agentic/agents/definitions/modes/deep_research.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ impl DeepResearchMode {
3939
"WriteStdin".to_string(),
4040
"ExecControl".to_string(),
4141
"ControlHub".to_string(),
42+
// Standing research ("re-check these sources every morning")
43+
// belongs on a schedule, and ControlHub's `wait` points here
44+
// rather than at an hour-long turn.
45+
"Cron".to_string(),
4246
"TodoWrite".to_string(),
4347
"AskUserQuestion".to_string(),
4448
],

‎src/crates/assembly/core/src/agentic/agents/definitions/modes/team.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ impl TeamMode {
4444
"AskUserQuestion".to_string(),
4545
"Git".to_string(),
4646
"ControlHub".to_string(),
47+
// Every mode that carries ControlHub needs Cron: ControlHub's
48+
// `wait` tells the agent to schedule long or repeating work
49+
// here instead of holding the turn open.
50+
"Cron".to_string(),
4751
"GetFileDiff".to_string(),
4852
],
4953
}

‎src/crates/assembly/core/src/agentic/agents/mod.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ pub fn shared_coding_mode_tools() -> Vec<String> {
145145
"Git".to_string(),
146146
"ReviewPlatform".to_string(),
147147
"ControlHub".to_string(),
148+
// Pairs with ControlHub: its `wait` sends anything repeating, or
149+
// further out than an hour, to Cron rather than holding the turn open
150+
// for the interval.
151+
"Cron".to_string(),
148152
"InitMiniApp".to_string(),
149153
"FinalizeMiniApp".to_string(),
150154
"PublishMiniApp".to_string(),

‎src/crates/assembly/core/src/agentic/agents/registry/tests.rs‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,30 @@ fn every_builtin_primary_mode_defaults_to_the_thread_goal_lifecycle() {
340340
}
341341
}
342342

343+
#[test]
344+
fn every_builtin_mode_with_control_hub_can_also_schedule_with_cron() {
345+
// ControlHub's `wait` documentation tells the agent to schedule anything
346+
// repeating — or further out than an hour — with Cron instead of holding
347+
// the turn open. A mode that offers one without the other sends the agent
348+
// after a tool that is not in its list; Cowork answered a "check every 30
349+
// minutes" request with "I have no cron tool" for exactly this reason.
350+
for spec in builtin_agent_specs()
351+
.iter()
352+
.filter(|spec| spec.category == AgentCategory::Mode)
353+
{
354+
let mode = (spec.factory)();
355+
let default_tools = mode.default_tools();
356+
if !default_tools.iter().any(|tool| tool == "ControlHub") {
357+
continue;
358+
}
359+
assert!(
360+
default_tools.iter().any(|tool| tool == "Cron"),
361+
"builtin mode {} offers ControlHub but cannot schedule with Cron",
362+
mode.id()
363+
);
364+
}
365+
}
366+
343367
#[test]
344368
fn non_deep_review_builtin_subagents_default_to_primary() {
345369
for agent_type in [

‎src/crates/assembly/core/src/agentic/tools/implementations/control_hub_tool.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ Use this tool via `{ domain, action, params }` for browser automation, terminal
271271
* `wait { duration_ms }` — pause for a fixed time, up to 60 minutes (`ms` and `seconds` are accepted spellings). This is the action to use when you must idle between rounds of work, e.g. `{ "duration_ms": 1800000 }` to resume in 30 minutes. It needs no browser session, and the result reports the `ms` actually waited, so check that figure before assuming the full pause happened.
272272
* `wait { condition, timeout_ms? }` — wait on the page instead: 'load' | 'domcontentloaded' | 'networkidle' | a CSS/@ref selector, bounded by `timeout_ms` (default 15s). Requires a connected session. When a `condition` is present it always wins, and any duration you pass becomes its timeout rather than a separate sleep.
273273
* A `wait` carrying neither is rejected with `INVALID_PARAMS` — it never silently returns.
274-
* `wait` holds the turn open for its whole duration, so it suits a one-off pause, not a schedule. For work that should repeat ("produce another round every 30 minutes") or resume more than an hour out, create a job with the `Cron` tool instead — it ends the turn and re-invokes you when the job fires, rather than idling with the context loaded.
274+
* `wait` holds the turn open for its whole duration, so it suits a one-off pause, not a schedule. For work that should repeat ("produce another round every 30 minutes") or resume more than an hour out, create a job with the `Cron` tool instead — it ends the turn and re-invokes you when the job fires, rather than idling with the context loaded. Every built-in mode that has ControlHub also has `Cron`; if it is genuinely absent from your tool list, say so rather than substituting a chain of long `wait` calls.
275275
- Automation workflow: connect -> navigate -> snapshot (returns @e1, @e2 ... refs) -> click/fill with `{ "selector": "@e1" }` (the key `ref` is accepted too).
276276
- Take a fresh snapshot after any DOM mutation; a stale `@eN` ref returns `error.code = STALE_REF`, while a selector that matches nothing returns `NOT_FOUND`.
277277

0 commit comments

Comments
 (0)