grok-build-upstream-mirror/crates/codegen/xai-grok-tools-api/src/slash_commands.rs
grokkybara[bot] 02d9359435 Synced from monorepo
Synced from monorepo

Changes:
- Workspace task snapshots only list incomplete backgrounded tasks
- Quiet auth, LSP, and config warnings in the shell
- Fix observability attributes for warm store errors, restore setup, remote tools, and preview denials
- Fail closed when soak metrics are missing
- Run plan-mode exit last in mixed tool batches
- Allow /loop to store prompts that can terminate the loop
- Make subagent maximum nesting depth configurable
- Security: apply sandbox profile to the leader process that executes tools

Source-Revision: 1adcd1f477870e4a97bacbd6be78c8a3bfbac46d
2026-07-27 17:54:34 +00:00

309 lines
15 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Canonical slash-command wording (`/loop`, `/imagine`, `/imagine-video`, `/goal`),
//! shared by every front-end (Grok Build shell/pager and other hosts) so
//! expansions cannot drift.
/// Canonical tool name advertised by the scheduler create tool. Gating code
/// (shell `CommandAvailability`, pager `required_tools`, host command lists)
/// keys `/loop` availability on this name.
pub const SCHEDULER_CREATE_TOOL_NAME: &str = "scheduler_create";
/// Usage hint shown when `/loop` is invoked with no arguments.
pub fn loop_usage_message() -> &'static str {
"Usage: /loop [interval] <prompt>\n\
Example: /loop 30m check deploy status\n\
Example: /loop check deploy status every hour\n\n\
Tell me how often it should run (e.g. 30m, 1 hour, every 2 days)."
}
/// Where a scheduled fire runs, which decides what the stored prompt can rely on.
///
/// Resolved from `[scheduler] background_loops` (env, config, managed policy and
/// remote settings all feed it), so `/loop` describes the runtime the user
/// actually has rather than hedging across both.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoopFireMode {
/// Each fire runs in a detached background subagent that cannot see this
/// conversation. The default.
Detached,
/// Each fire runs as a turn in this conversation, where earlier results from
/// the same task may still be visible.
InSession,
}
/// Build the model instruction that `/loop` expands into for `args`.
///
/// The model, not brittle host parsing, turns the request into the
/// `scheduler_create` interval, accepting every natural phrasing and erroring
/// on bad input rather than silently defaulting. See [`loop_usage_message`].
///
/// Only the framing differs by `mode`; the stop condition and length guidance
/// are identical, because both hold wherever the fire runs.
pub fn loop_schedule_instruction(args: &str, mode: LoopFireMode) -> String {
let fire_context = match mode {
LoopFireMode::Detached => {
"Each fire runs in a detached background subagent, not in this conversation,\n\
so the prompt you store must stand on its own.\n\n\
## Writing a prompt that survives a fresh fire\n\
- Inline the state a fire needs: paths, job/PR/branch ids, the command that checks\n\
status, and what \"healthy\" looks like. A fire cannot see this conversation, and\n\
a long-running task restarts from a short summary every few iterations.\n\
- Only a short status comes back here, so say what that status must contain."
}
LoopFireMode::InSession => {
"Each fire arrives as a new turn in this conversation, and earlier results from\n\
the same task may still be above it. The stored prompt is re-sent verbatim every\n\
time, so write a standing order rather than a one-off request.\n\n\
## Writing a prompt that reads well on every fire\n\
- Name the state that must not be guessed: paths, job/PR/branch ids, the command\n\
that checks status, and what \"healthy\" looks like. This conversation is\n\
compacted as it grows, so do not rely on details staying visible.\n\
- Earlier fires may be above you: continue from them instead of restarting."
}
};
format!(
"# /loop -- schedule a recurring prompt\n\n\
Turn the input below into a scheduler_create call. {fire_context}\n\
- Say what one fire does and when it bails: \"if still pending, report one line and\n\
stop.\" A fire must not poll inline.\n\
- Give it a stop condition and an exit: \"when <condition> holds, report it and call\n\
scheduler_delete <task_id>.\" Without that the loop runs until it expires.\n\
- Keep it short and concrete -- the stored prompt is re-sent on every fire.\n\n\
## Deriving the interval\n\
Convert the user's cadence -- however phrased, at either end of the request -- into a\n\
compact `<number><unit>` string (`s`/`m`/`h`/`d`); the remaining text is the prompt.\n\
The minimum is 60 seconds and shorter values are raised, so say so when it applies.\n\
If no cadence is given, ask the user how often it should run -- never invent one.\n\n\
## Action\n\
Schedule from what the user already gave you \u{2014} do not explore the workspace or run\n\
checks before scheduling; the first fire does that.\n\
1. Call scheduler_create with the interval, the prompt, and fire_immediately: true.\n\
If the interval is rejected, fix the string rather than guessing.\n\
2. Confirm what's scheduled, the cadence, its stop condition, that it auto-expires\n\
after 7 days, and the task_id to cancel with scheduler_delete.\n\
3. Do NOT execute the prompt inline. The scheduler fires it immediately.\n\n\
## Wrong tool for the job\n\
- \"Tell me when X finishes\" -> a background command or watch tool that wakes you on\n\
the event, not a recurring loop that re-checks on a timer.\n\
- \"Do X once in N minutes\" -> background `sleep <secs> && <command>`; scheduling is\n\
recurring-only.\n\n\
## Changing an existing loop\n\
Call scheduler_create with its task_id and only the changed fields; do not\n\
delete and recreate. If later work changes what a loop should do, update its\n\
prompt the same way.\n\n\
## Input\n\
{args}"
)
}
/// Canonical name of the image generation tool; gates `/imagine`.
pub const IMAGE_GEN_TOOL_NAME: &str = "image_gen";
/// Advertised name of the /imagine command.
pub const IMAGINE_COMMAND_NAME: &str = "imagine";
/// Canonical name of the image-to-video tool; gates `/imagine-video`.
pub const IMAGE_TO_VIDEO_TOOL_NAME: &str = "image_to_video";
/// Advertised name of the /imagine-video command.
pub const IMAGINE_VIDEO_COMMAND_NAME: &str = "imagine-video";
/// Usage hint shown when `/imagine` is invoked with no arguments.
pub fn imagine_usage_message() -> &'static str {
"Usage: /imagine <description>\n\
Provide a text description to generate an image."
}
/// Build the model instruction that `/imagine` expands into for `prompt`.
pub fn imagine_instruction(prompt: &str) -> String {
format!(
"Call the image_gen tool immediately, passing the user's prompt below \
verbatim — do not rewrite, embellish, or expand it. \
After the tool completes, briefly acknowledge and mention \
where the image was saved.\n\n\
Prompt: {prompt}"
)
}
/// Usage hint shown when `/imagine-video` is invoked with no arguments.
pub fn imagine_video_usage_message() -> &'static str {
"Usage: /imagine-video <description>\n\
Provide a text description to generate a video."
}
/// Build the model instruction that `/imagine-video` expands into for `prompt`.
pub fn imagine_video_instruction(prompt: &str) -> String {
format!(
"{IMAGINE_VIDEO_SKILL}\n\n\
User prompt: {prompt}"
)
}
/// Video workflow guidance injected by `/imagine-video`.
const IMAGINE_VIDEO_SKILL: &str = "\
# Imagine Video
Video starts from an image — there is no text-to-video tool. \
Default to `image_to_video`; use `reference_to_video` only when the user \
explicitly asks for it or a shot genuinely needs multiple reference images.
## Default: single clip
Unless the user asks for a long video, multiple scenes, or a multi-shot sequence, \
generate **one** video:
1. Create a source image with `image_gen` that stages the first frame \
(composition, subject, lighting).
2. Call `image_to_video` with that image and a short prompt describing the motion \
or camera move (12 sentences, present tense).
3. After the tool completes, mention the saved file path so the user can find it.
## Longer / multi-shot videos
When the user requests a longer video, multiple scenes, or a narrative sequence:
1. **Plan the story as shots** — break the idea into distinct shots, one beat each.
2. **Favor frequent, short shots** — prefer more 6s clips over fewer long ones; more cuts keep it dynamic.
3. **Create each shot's source image** with `image_gen` (or `image_edit` to combine references), keeping characters and settings consistent across shots.
4. **Animate each shot with `image_to_video`** — the source image becomes frame 1.
5. **Assemble with FFmpeg** using stream copy (`ffmpeg -f concat ... -c copy` — never re-encode). \
Keep every shot at the same resolution and frame rate so the concat works. \
After assembly, mention the final output path.
## Shot guidance
- **Prompt-craft:** one short, vivid moment in present tense with a clear camera movement, in 12 sentences.
- **Minimal but interesting:** one clear subject, one simple motion or camera move per shot. Avoid complex multi-action animation; make the shot compelling through composition, lighting, and a strong moment.
- **Complex source image?** Intricate frames (busy geometry, fine detail, heavy reflections) warp when animated. Keep the subject fixed and move only the camera (slow push-in, orbit, or parallax), or break into simpler shots. For new shots, generate a simpler, animation-friendly base image rather than animating a busy one.
- **`image_to_video` animates from frame 1** — stage the first frame with `image_gen`/`image_edit` before animating.
- **Aspect ratio:** set it on the source image (`image_gen` `aspect_ratio`); don't re-crop an existing video.
- **Duration:** 6s or 10s only (prefer 6s); round to the nearest.
- **Real people:** reference-first — drive the video from a verified reference image; never animate a named person without one.
- Don't loop the same clip unless asked.";
pub const UPDATE_GOAL_TOOL_NAME: &str = "update_goal";
pub const WORKFLOW_TOOL_NAME: &str = "workflow";
pub const GOAL_COMMAND_NAME: &str = "goal";
/// Bare subcommand tokens reserved for goal lifecycle control rather than
/// being treated as an objective, matching the shell's /goal grammar.
pub const GOAL_RESERVED_SUBCOMMANDS: &[&str] = &["status", "pause", "resume", "clear", "edit"];
pub fn goal_usage_message() -> &'static str {
"Usage: /goal <objective>\n\
Set an objective to work toward until it is complete."
}
pub fn goal_instruction(objective: &str) -> String {
format!(
"# /goal -- pursue an objective\n\n\
A goal has been set: {objective}\n\n\
Work directly on this goal and carry it as far as you can. Deliver \
everything the user asked for yourself: no follow-up questions, no \
manual steps left for the user. If the conversation continues, keep \
pursuing the goal until it is complete.\n\n\
TRACKING: break the objective into concrete steps and track them \
(use your todo tool if one is available), marking each done as you \
finish it.\n\n\
VERIFY AS YOU GO: test each change on the real path before moving on. \
A completion claim must be backed by evidence produced in this \
session, not assumptions.\n\n\
Call update_goal(completed: true, message: \"summary\") ONLY when the \
goal is fully achieved. Call update_goal(blocked_reason: \"reason\") \
only when truly stuck after 3+ consecutive failed attempts at the \
same problem. Call update_goal(message: \"status note\") to log \
progress along the way. If update_goal returns an error, continue \
working the goal and report status in your reply instead.\n\n\
Start now."
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn imagine_instruction_carries_prompt_verbatim() {
let text = imagine_instruction("a golden sunset");
assert!(text.contains("a golden sunset"));
assert!(text.contains("image_gen"));
assert!(text.contains("verbatim"));
}
#[test]
fn imagine_video_instruction_carries_prompt_and_workflow() {
let text = imagine_video_instruction("a cat playing piano");
assert!(text.contains("a cat playing piano"));
assert!(text.contains("image_to_video"));
assert!(text.contains("FFmpeg"));
}
#[test]
fn instruction_carries_args_and_contract_tokens() {
for mode in [LoopFireMode::Detached, LoopFireMode::InSession] {
let text = loop_schedule_instruction("every 30 minutes do x", mode);
assert!(text.contains("every 30 minutes do x"), "{mode:?}");
assert!(text.contains("<number><unit>"), "{mode:?}");
assert!(text.contains("ask the user how often"), "{mode:?}");
assert!(
!text.contains("10m"),
"no host-side default interval: {mode:?}"
);
assert!(
!text.contains("recurring:"),
"the retired one-shot flag must not be referenced: {mode:?}"
);
assert!(
text.contains("task_id"),
"must teach in-place updates via task_id: {mode:?}"
);
assert!(
text.contains("delete and recreate"),
"must steer away from delete+recreate: {mode:?}"
);
assert!(
text.contains("scheduler_delete <task_id>"),
"every mode must authorize the fire to end the task: {mode:?}"
);
}
}
#[test]
fn each_fire_mode_describes_its_own_runtime() {
let detached = loop_schedule_instruction("5m check ci", LoopFireMode::Detached);
let in_session = loop_schedule_instruction("5m check ci", LoopFireMode::InSession);
assert!(detached.contains("cannot see this conversation"));
assert!(!detached.contains("arrives as a new turn in this conversation"));
assert!(in_session.contains("arrives as a new turn in this conversation"));
assert!(!in_session.contains("cannot see this conversation"));
// The two levers the A/B showed carry the behavior are mode-independent.
for text in [&detached, &in_session] {
assert!(text.contains("report it and call"));
assert!(text.contains("Keep it short and concrete"));
}
}
#[test]
fn goal_instruction_carries_objective_and_contract_tokens() {
let text = goal_instruction("ship the widget");
assert!(text.contains("ship the widget"));
assert!(text.contains("update_goal(completed: true"));
assert!(text.contains("blocked_reason"));
assert!(text.contains("If update_goal returns an error"));
assert!(
!text.contains("system-reminder"),
"expansions ride as user messages and must not claim reminder authority"
);
assert!(goal_usage_message().contains("Usage: /goal"));
}
#[test]
fn usage_message_has_no_default_claim() {
assert!(loop_usage_message().contains("Usage: /loop"));
assert!(!loop_usage_message().contains("10m"));
}
}