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
4 changes: 2 additions & 2 deletions assets/orchestrate/codex.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ The fleet table below is the authoritative allow list — user-configured profil

## Tools

- `dispatch {provider, model?, effort?, access?, title, brief, cwd?, archive_on_complete?, result_max_chars?}` → new child thread, `brief` is its first message, returns `thread_id`. Visible in the user's sidebar. `model` + `effort` must name an enabled profile from the fleet table exactly (omit both → the provider's first enabled profile). `access`: `read_only` for reviews/investigation (no file changes; anything beyond pauses for approval, routed per Settings → Orchestrate → child approvals), `workspace_write` for implementation with auto-approved workspace edits, `full` (default). Completed children auto-archive after their result is delivered (default per Settings → Orchestrate); `archive_on_complete: false` keeps one visible, failed children always stay, and `send` to an archived child revives it. `result_max_chars` caps inline RESULT text (omit → 1200 characters; 0 → no limit — set 0 when the full report will be needed, instead of fetching it afterwards).
- `dispatch {provider, model?, effort?, access?, title, brief, cwd?, archive_on_complete?, result_max_chars?, fast?}` → new child thread, `brief` is its first message, returns `thread_id`. Visible in the user's sidebar. `model` + `effort` must name an enabled profile from the fleet table exactly (omit both → the provider's first enabled profile). `access`: `read_only` for reviews/investigation (no file changes; anything beyond pauses for approval, routed per Settings → Orchestrate → child approvals), `workspace_write` for implementation with auto-approved workspace edits, `full` (default). Completed children auto-archive after their result is delivered (default per Settings → Orchestrate); `archive_on_complete: false` keeps one visible, failed children always stay, and `send` to an archived child revives it. `result_max_chars` caps inline RESULT text (omit → 1200 characters; 0 → no limit — set 0 when the full report will be needed, instead of fetching it afterwards). `fast` overrides the profile's fast-mode setting for this child (true/false); pass it only when the user explicitly asks for fast mode on or off.
- `status {thread_id?}` → running/completed/failed + output tail + token usage. No `thread_id` = all children.
- `send {thread_id, message}` → follow-up to a child that still has useful context (fix instructions, mid-course corrections, one focused retry). Injected into the child's live turn when one is running, otherwise sent as its next turn — the response says which.
- `send {thread_id, message, fast?}` → follow-up to a child that still has useful context (fix instructions, mid-course corrections, one focused retry). Injected into the child's live turn when one is running, otherwise sent as its next turn — the response says which. `fast: true|false` switches the child's fast mode from its next turn; a running turn keeps its speed, so to speed up work in progress `cancel` first, then `send` with `fast` set (the child resumes its transcript on a fresh process). Only on the user's explicit instruction.
- `result {thread_id}` → full final message of a completed child, with token usage.
- `cancel {thread_id}` → stop a child.
- `archive {thread_ids}` → batch-archive children; reversible, and shuts down running children. Rarely needed: completed children auto-archive — use for failed children you will not retry and children kept with `archive_on_complete: false`.
Expand Down
4 changes: 2 additions & 2 deletions assets/orchestrate/fable.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ The fleet table below is the authoritative allow list — user-configured profil

## Tools

- `dispatch {provider, model?, effort?, access?, title, brief, cwd?, archive_on_complete?, result_max_chars?}` → creates a child thread, sends `brief` as its first message, returns `thread_id`. The child appears in the user's sidebar; they can watch it live. `model` + `effort` must name an enabled profile exactly as listed in the fleet table (omit both to get the provider's first enabled profile). `access` gates what the child may do: `read_only` for reviews and investigation (the child cannot change files; anything beyond that pauses for approval, routed per Settings → Orchestrate → child approvals), `workspace_write` for implementation with edits auto-approved inside the workspace, `full` (default) for no prompts. Completed children are auto-archived after their result reaches you (default per Settings → Orchestrate) — pass `archive_on_complete: false` to keep one around; failed children stay visible, and `send` to an archived child revives it. `result_max_chars` sets the inline RESULT cap (omit for 1200 characters; 0 for no limit — set 0 when you know you will need the full report, rather than fetching it afterwards).
- `dispatch {provider, model?, effort?, access?, title, brief, cwd?, archive_on_complete?, result_max_chars?, fast?}` → creates a child thread, sends `brief` as its first message, returns `thread_id`. The child appears in the user's sidebar; they can watch it live. `model` + `effort` must name an enabled profile exactly as listed in the fleet table (omit both to get the provider's first enabled profile). `access` gates what the child may do: `read_only` for reviews and investigation (the child cannot change files; anything beyond that pauses for approval, routed per Settings → Orchestrate → child approvals), `workspace_write` for implementation with edits auto-approved inside the workspace, `full` (default) for no prompts. Completed children are auto-archived after their result reaches you (default per Settings → Orchestrate) — pass `archive_on_complete: false` to keep one around; failed children stay visible, and `send` to an archived child revives it. `result_max_chars` sets the inline RESULT cap (omit for 1200 characters; 0 for no limit — set 0 when you know you will need the full report, rather than fetching it afterwards). `fast` overrides the profile's fast-mode setting for this child (true or false); pass it only when the user explicitly asks for fast mode on or off.
- `status {thread_id?}` → running/completed/failed, an output tail, and token usage. Omit `thread_id` for all your children.
- `send {thread_id, message}` → follow-up message to a child (feedback, a mid-course correction, one focused retry). Steered into the child's live turn immediately when one is running, otherwise sent as its next turn — the response says which. Prefer this over dispatching a fresh child when the child's context is useful.
- `send {thread_id, message, fast?}` → follow-up message to a child (feedback, a mid-course correction, one focused retry). Steered into the child's live turn immediately when one is running, otherwise sent as its next turn — the response says which. Prefer this over dispatching a fresh child when the child's context is useful. `fast: true|false` switches the child's fast mode from its next turn on; a turn already running keeps its speed, so to speed up work in progress `cancel` first, then `send` with `fast` set and the child resumes its transcript on a fresh process. Use it only when the user explicitly asks.
- `result {thread_id}` → the completed child's full final message plus token usage.
- `cancel {thread_id}` → stop a child.
- `archive {thread_ids}` → batch-archive children; reversible, and shuts down any still running. Rarely needed: completed children auto-archive — use this for failed children you will not retry and children kept with `archive_on_complete: false`.
Expand Down
4 changes: 2 additions & 2 deletions assets/orchestrate/generic.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ The fleet table below is the authoritative allow list — user-configured profil

## Tools

- `dispatch {provider, model?, effort?, access?, title, brief, cwd?, archive_on_complete?, result_max_chars?}` → creates a child thread and sends `brief` as its first message; returns `thread_id`. The child is visible in the user's sidebar. `model` + `effort` must name an enabled profile from the fleet table exactly (omit both for the provider's first enabled profile). `access`: `read_only` for reviews/investigation (no file changes; anything beyond pauses for approval, routed per Settings → Orchestrate → child approvals), `workspace_write` for implementation with auto-approved workspace edits, `full` (default). Completed children auto-archive after their result is delivered (default per Settings → Orchestrate); set `archive_on_complete: false` to keep one visible, failed children always stay, and `send` to an archived child revives it. `result_max_chars` caps inline RESULT text (omit for the 1200-character default; 0 means no limit — set 0 when the full report will be needed, instead of fetching it afterwards).
- `dispatch {provider, model?, effort?, access?, title, brief, cwd?, archive_on_complete?, result_max_chars?, fast?}` → creates a child thread and sends `brief` as its first message; returns `thread_id`. The child is visible in the user's sidebar. `model` + `effort` must name an enabled profile from the fleet table exactly (omit both for the provider's first enabled profile). `access`: `read_only` for reviews/investigation (no file changes; anything beyond pauses for approval, routed per Settings → Orchestrate → child approvals), `workspace_write` for implementation with auto-approved workspace edits, `full` (default). Completed children auto-archive after their result is delivered (default per Settings → Orchestrate); set `archive_on_complete: false` to keep one visible, failed children always stay, and `send` to an archived child revives it. `result_max_chars` caps inline RESULT text (omit for the 1200-character default; 0 means no limit — set 0 when the full report will be needed, instead of fetching it afterwards). `fast` overrides the profile's fast-mode setting for this child (true/false); pass it only when the user explicitly asks for fast mode on or off.
- `status {thread_id?}` → running/completed/failed plus the latest output tail and token usage. Omit `thread_id` for all children.
- `send {thread_id, message}` → follow-up to a child with useful context (feedback, mid-course corrections, one focused retry). Delivered into the child's live turn when one is running, otherwise sent as its next turn — the response says which.
- `send {thread_id, message, fast?}` → follow-up to a child with useful context (feedback, mid-course corrections, one focused retry). Delivered into the child's live turn when one is running, otherwise sent as its next turn — the response says which. `fast: true|false` switches the child's fast mode from its next turn; a running turn keeps its speed, so to speed up work in progress `cancel` first, then `send` with `fast` set (the child resumes its transcript on a fresh process). Only on the user's explicit instruction.
- `result {thread_id}` → completed child's full final message, with token usage.
- `cancel {thread_id}` → stop a child.
- `archive {thread_ids}` → batch-archive children; reversible, and shuts down running children. Rarely needed: completed children auto-archive — use for failed children you will not retry and children kept with `archive_on_complete: false`.
Expand Down
4 changes: 4 additions & 0 deletions crates/orchestrate-mcp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ pub enum OrchestrateOp {
worktree: Option<bool>,
archive_on_complete: Option<bool>,
result_max_chars: Option<u32>,
/// Per-dispatch override of the profile's fast-mode setting.
fast: Option<bool>,
},
Status {
parent_id: String,
Expand All @@ -28,6 +30,8 @@ pub enum OrchestrateOp {
parent_id: String,
thread_id: String,
message: String,
/// Switch the child's fast mode before delivering the message.
fast: Option<bool>,
},
Result {
parent_id: String,
Expand Down
14 changes: 13 additions & 1 deletion crates/orchestrate-mcp/src/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ struct DispatchParams {
description = "Character cap for the inline result text in the completion callback (default 1200; 0 = unlimited). Raise it or pass 0 when you will need the full report anyway — cheaper than a follow-up result call."
)]
result_max_chars: Option<u32>,
#[serde(default)]
#[schemars(
description = "Override the child profile's fast-mode setting for this dispatch (true = on, false = off). Pass it only when the user explicitly asked for fast mode on or off; otherwise omit it and the profile decides. Ignored by providers without a fast mode."
)]
fast: Option<bool>,
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
struct StatusParams {
Expand All @@ -52,6 +57,11 @@ struct StatusParams {
struct SendParams {
thread_id: String,
message: String,
#[serde(default)]
#[schemars(
description = "Switch the child's fast mode (true = on, false = off) before delivering this message. Takes effect from the child's next turn: a turn already running keeps its speed, so to speed up work in progress cancel the child first, then send with fast set — it resumes its transcript on a fresh process. Pass it only when the user explicitly asks; omit it to leave the setting alone."
)]
fast: Option<bool>,
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
struct ThreadParams {
Expand Down Expand Up @@ -87,7 +97,7 @@ impl OrchestrateTools {
}

#[tool(
description = "Dispatch a brief to a new child tcode thread and return its thread id. profile is the provider-profile id from the fleet table, required when the entry names one. access is one of read_only (review/investigation: read-only actions run without prompts; anything that mutates pauses for user approval), workspace_write (edits auto-approved inside the workspace), or full (default; no approval prompts). worktree optionally isolates the child in tcode/<thread-id> and overrides the Orchestrate setting; the response identifies the path and branch or explains fallback. Completed children are auto-archived after their result is delivered unless archive_on_complete: false; failed children stay visible for retries."
description = "Dispatch a brief to a new child tcode thread and return its thread id. profile is the provider-profile id from the fleet table, required when the entry names one. access is one of read_only (review/investigation: read-only actions run without prompts; anything that mutates pauses for user approval), workspace_write (edits auto-approved inside the workspace), or full (default; no approval prompts). worktree optionally isolates the child in tcode/<thread-id> and overrides the Orchestrate setting; the response identifies the path and branch or explains fallback. Completed children are auto-archived after their result is delivered unless archive_on_complete: false; failed children stay visible for retries. fast overrides the profile's fast-mode setting for this child; use it only on the user's explicit instruction."
)]
async fn dispatch(
&self,
Expand All @@ -107,6 +117,7 @@ impl OrchestrateTools {
worktree: p.worktree,
archive_on_complete: p.archive_on_complete,
result_max_chars: p.result_max_chars,
fast: p.fast,
})
.await)
}
Expand Down Expand Up @@ -136,6 +147,7 @@ impl OrchestrateTools {
parent_id: self.parent_id.clone(),
thread_id: p.thread_id,
message: p.message,
fast: p.fast,
})
.await)
}
Expand Down
44 changes: 40 additions & 4 deletions crates/runtime/src/app/orchestrate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,24 @@ impl AppState {
Ok(id)
}

/// Switch a child's fast mode and persist it. Fast mode is a launch-time
/// option, so a live child restarts before its next turn (see
/// `options_changed_while_live`); a turn already running is unaffected.
fn set_child_fast(&mut self, thread_id: &str, fast: bool, cx: &mut HostCx) {
let Some(mut meta) = self
.resident(thread_id)
.map(|child| child.meta.clone())
.or_else(|| self.find_meta(thread_id))
else {
return;
};
apply_fast_selection(&mut meta.option_selections, meta.provider, fast);
if let Some(child) = self.resident_mut(thread_id) {
child.meta.option_selections = meta.option_selections.clone();
}
self.persist_meta(&meta, cx);
}

/// Resolve one MCP operation on the host owner thread.
pub(crate) fn handle_orchestrate_op(
&mut self,
Expand Down Expand Up @@ -334,6 +352,7 @@ impl AppState {
worktree,
archive_on_complete,
result_max_chars,
fast: fast_override,
} => {
let resolved = (|| {
let (provider, model, effort, fast, profile_id) = resolve_orchestrate_dispatch(
Expand All @@ -349,6 +368,9 @@ impl AppState {
return Err(format!("unknown profile: {id}"));
}
let approval_mode = resolve_dispatch_access(access.as_deref())?;
// The profile's fast setting is the default; a dispatch may
// override it either way on the user's explicit instruction.
let fast = fast_override.unwrap_or(fast);
Ok((provider, model, effort, fast, profile_id, approval_mode))
})();
let (provider, model, effort, fast, profile_id, approval_mode) = match resolved {
Expand Down Expand Up @@ -462,12 +484,16 @@ impl AppState {
parent_id,
thread_id,
message,
fast,
} => {
let result = (|| {
let archived = self
.require_child(&parent_id, &thread_id)?
.archived_at
.is_some();
if let Some(fast) = fast {
self.set_child_fast(&thread_id, fast, cx);
}
// A follow-up starts a new piece of work: a result reported
// before it must not be delivered as the answer to it.
self.child_reported_results.remove(&thread_id);
Expand Down Expand Up @@ -1128,7 +1154,7 @@ pub(super) fn render_orchestrate_configuration(
text.push_str(identity);
}
text.push_str(
"\n\n### Allowed child models\n\nProfiles pin the effort they dispatch at. A dispatch must name `model` and `effort` exactly as listed; both may be omitted, in which case tcode picks the first enabled profile for the provider. When an entry names a `profile`, pass it exactly as listed. The definitions below are user-configured routing guidance.\n",
"\n\n### Allowed child models\n\nProfiles pin the effort they dispatch at. A dispatch must name `model` and `effort` exactly as listed; both may be omitted, in which case tcode picks the first enabled profile for the provider. When an entry names a `profile`, pass it exactly as listed. A profile marked `fast mode` dispatches with the provider's fast mode; pass `fast: true|false` on a dispatch (or on a `send`, for a child that already exists) to override that only when the user explicitly asks. The definitions below are user-configured routing guidance.\n",
);
if !settings.child_models.iter().any(|child| child.enabled) {
text.push_str("No child models are enabled. Work without dispatching until the user enables one in Settings → Orchestrate.");
Expand Down Expand Up @@ -1347,13 +1373,23 @@ pub(super) fn build_child_meta(
value: serde_json::Value::String(effort),
});
}
if fast && let Some((id, value)) = fast_selection(provider) {
meta.option_selections.push(OptionSelection {
apply_fast_selection(&mut meta.option_selections, provider, fast);
meta
}

/// Set or clear the provider's fast-mode selection in `selections`. Other
/// selections (a Codex `flex` tier, say) are left alone.
fn apply_fast_selection(selections: &mut Vec<OptionSelection>, provider: ProviderKind, fast: bool) {
let Some((id, value)) = fast_selection(provider) else {
return;
};
selections.retain(|selection| !(selection.id == id && selection.value == value));
if fast {
selections.push(OptionSelection {
id: id.into(),
value,
});
}
meta
}

/// The option selection that turns on a provider's fast mode: Claude's
Expand Down
Loading