Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ impl CoworkMode {
"WebSearch".to_string(),
"WebFetch".to_string(),
"ControlHub".to_string(),
// Recurring office work ("check these channels every 30
// minutes") is squarely this mode's job, and ControlHub's
// `wait` sends schedules here rather than pinning a turn open
// for the interval.
"Cron".to_string(),
"InitMiniApp".to_string(),
"FinalizeMiniApp".to_string(),
"PublishMiniApp".to_string(),
Expand Down Expand Up @@ -120,6 +125,15 @@ mod tests {
}
}

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

#[test]
fn cowork_mode_includes_miniapp_lifecycle_tools_in_defaults() {
let tools = CoworkMode::new().default_tools();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ impl DeepResearchMode {
"WriteStdin".to_string(),
"ExecControl".to_string(),
"ControlHub".to_string(),
// Standing research ("re-check these sources every morning")
// belongs on a schedule, and ControlHub's `wait` points here
// rather than at an hour-long turn.
"Cron".to_string(),
"TodoWrite".to_string(),
"AskUserQuestion".to_string(),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ impl TeamMode {
"AskUserQuestion".to_string(),
"Git".to_string(),
"ControlHub".to_string(),
// Every mode that carries ControlHub needs Cron: ControlHub's
// `wait` tells the agent to schedule long or repeating work
// here instead of holding the turn open.
"Cron".to_string(),
"GetFileDiff".to_string(),
],
}
Expand Down
4 changes: 4 additions & 0 deletions src/crates/assembly/core/src/agentic/agents/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@ pub fn shared_coding_mode_tools() -> Vec<String> {
"Git".to_string(),
"ReviewPlatform".to_string(),
"ControlHub".to_string(),
// Pairs with ControlHub: its `wait` sends anything repeating, or
// further out than an hour, to Cron rather than holding the turn open
// for the interval.
"Cron".to_string(),
"InitMiniApp".to_string(),
"FinalizeMiniApp".to_string(),
"PublishMiniApp".to_string(),
Expand Down
24 changes: 24 additions & 0 deletions src/crates/assembly/core/src/agentic/agents/registry/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,30 @@ fn every_builtin_primary_mode_defaults_to_the_thread_goal_lifecycle() {
}
}

#[test]
fn every_builtin_mode_with_control_hub_can_also_schedule_with_cron() {
// ControlHub's `wait` documentation tells the agent to schedule anything
// repeating — or further out than an hour — with Cron instead of holding
// the turn open. A mode that offers one without the other sends the agent
// after a tool that is not in its list; Cowork answered a "check every 30
// minutes" request with "I have no cron tool" for exactly this reason.
for spec in builtin_agent_specs()
.iter()
.filter(|spec| spec.category == AgentCategory::Mode)
{
let mode = (spec.factory)();
let default_tools = mode.default_tools();
if !default_tools.iter().any(|tool| tool == "ControlHub") {
continue;
}
assert!(
default_tools.iter().any(|tool| tool == "Cron"),
"builtin mode {} offers ControlHub but cannot schedule with Cron",
mode.id()
);
}
}

#[test]
fn non_deep_review_builtin_subagents_default_to_primary() {
for agent_type in [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ Use this tool via `{ domain, action, params }` for browser automation, terminal
* `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.
* `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.
* A `wait` carrying neither is rejected with `INVALID_PARAMS` — it never silently returns.
* `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.
* `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.
- Automation workflow: connect -> navigate -> snapshot (returns @e1, @e2 ... refs) -> click/fill with `{ "selector": "@e1" }` (the key `ref` is accepted too).
- 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`.

Expand Down