feat: implement Crosscheck Work experiment

Add the 'Crosscheck Work' experiment to the Agents settings. When enabled,
a reviewer sub-agent is spawned after the main agent finishes a turn (with
no pending tool calls). The reviewer critiques the output using a dedicated
system prompt focused on correctness, simplicity, and code quality. If the
reviewer does not respond with 'LGTM!', its feedback is injected as a
synthetic user query back to the main agent, which must address it. This
loop continues until the reviewer approves or max iterations is reached.

Components:
- Feature flag: CrosscheckWork (enabled in DOGFOOD_FLAGS)
- Settings: agents.experiments.crosscheck_enabled,
  agents.experiments.crosscheck_model_id,
  agents.experiments.crosscheck_max_iterations
- Settings UI: new 'Experiments' subpage under Agents
- Crosscheck module: app/src/ai/crosscheck/ with prompt, reviewer model
- Controller integration: hooks into AfterStreamFinished when no actions
  are queued, triggers reviewer, handles feedback injection
- Provider support: OpenAI-compatible and Bedrock direct invocation
- Safety: max iteration guard, error handling, reset on new user query
This commit is contained in:
Ryan Ward
2026-07-22 15:58:55 -05:00
parent ca2cf6f8d6
commit e5062ae432
9 changed files with 912 additions and 7 deletions
+92 -7
View File
@@ -92,13 +92,14 @@ use crate::settings::{
AgentModeCodingPermissionsType, AgentModeCommandExecutionDenylist,
AgentModeCommandExecutionPredicate, AgentModeQuerySuggestionsEnabled, BedrockAutoLogin,
BedrockEnabled, CanUseWarpCreditsForFallback, CodeSettings, CodebaseContextEnabled,
FileBasedMcpEnabled, GitOperationsAutogenEnabled, IncludeAgentCommandsInHistory, InputSettings,
IntelligentAutosuggestionsEnabled, LongRunningCommandSubmissionMode, MemoryEnabled,
NLDInTerminalEnabled, NaturalLanguageAutosuggestionsEnabled, OpenAIEnabled,
OrchestrationMessageDisplayMode, PromptSubmissionMode, RuleSuggestionsEnabled,
SharedBlockTitleGenerationEnabled, ShouldRenderCLIAgentToolbar,
ShouldRenderUseAgentToolbarForUserCommands, ShowAgentTips, ShowConversationHistory,
ShowHintText, ThinkingDisplayMode, VoiceInputEnabled, WarpDriveContextEnabled,
CrosscheckEnabled, FileBasedMcpEnabled, GitOperationsAutogenEnabled,
IncludeAgentCommandsInHistory, InputSettings, IntelligentAutosuggestionsEnabled,
LongRunningCommandSubmissionMode, MemoryEnabled, NLDInTerminalEnabled,
NaturalLanguageAutosuggestionsEnabled, OpenAIEnabled, OrchestrationMessageDisplayMode,
PromptSubmissionMode, RuleSuggestionsEnabled, SharedBlockTitleGenerationEnabled,
ShouldRenderCLIAgentToolbar, ShouldRenderUseAgentToolbarForUserCommands, ShowAgentTips,
ShowConversationHistory, ShowHintText, ThinkingDisplayMode, VoiceInputEnabled,
WarpDriveContextEnabled,
};
use crate::terminal::session_settings::{SessionSettings, SessionSettingsChangedEvent};
use crate::terminal::CLIAgent;
@@ -128,6 +129,8 @@ pub enum AISubpage {
Bedrock,
/// OpenAI-compatible (LiteLLM) provider configuration.
OpenAI,
/// Experimental features.
Experiments,
}
impl AISubpage {
@@ -139,6 +142,7 @@ impl AISubpage {
SettingsSection::ThirdPartyCLIAgents => Some(Self::ThirdPartyCLIAgents),
SettingsSection::Bedrock => Some(Self::Bedrock),
SettingsSection::OpenAI => Some(Self::OpenAI),
SettingsSection::Experiments => Some(Self::Experiments),
// AgentMCPServers renders the standalone MCPServers page, not an AI subpage.
_ => None,
}
@@ -2866,6 +2870,9 @@ impl AISettingsPageView {
let title: Option<&str> = None;
return (PageType::new_uncategorized(widgets, title), None);
}
Some(AISubpage::Experiments) => {
widgets.push(Box::new(ExperimentsWidget::default()));
}
}
// Subpage widgets render their own subheader-sized titles internally,
@@ -3656,6 +3663,7 @@ pub enum AISettingsPageAction {
agent: Option<CLIAgent>,
},
ToggleCloudAgentComputerUse,
ToggleCrosscheckEnabled,
}
impl From<&AISettingsPageAction> for LoginGatedFeature {
@@ -4356,6 +4364,12 @@ impl TypedActionView for AISettingsPageView {
});
ctx.notify();
}
AISettingsPageAction::ToggleCrosscheckEnabled => {
AISettings::handle(ctx).update(ctx, |settings, ctx| {
report_if_error!(settings.crosscheck_enabled.toggle_and_save_value(ctx));
});
ctx.notify();
}
AISettingsPageAction::ToggleBedrockEnabled => {
AISettings::handle(ctx).update(ctx, |settings, ctx| {
report_if_error!(settings.bedrock_enabled.toggle_and_save_value(ctx));
@@ -9790,6 +9804,77 @@ impl SettingsWidget for CustomModelRoutersWidget {
}
}
// ── Experiments widget ──────────────────────────────────────────────────────
#[derive(Default)]
struct ExperimentsWidget {
crosscheck_toggle: SwitchStateHandle,
}
impl SettingsWidget for ExperimentsWidget {
type View = AISettingsPageView;
fn search_terms(&self) -> &str {
"experiments crosscheck review reviewer sub-agent feedback loop lgtm"
}
fn should_render(&self, _app: &AppContext) -> bool {
FeatureFlag::CrosscheckWork.is_enabled()
}
fn render(
&self,
view: &Self::View,
appearance: &Appearance,
app: &AppContext,
) -> Box<dyn Element> {
let ai_settings = AISettings::as_ref(app);
let is_any_ai_enabled = ai_settings.is_any_ai_enabled(app);
let header = build_sub_header(
appearance,
"Experiments",
Some(styles::header_font_color(is_any_ai_enabled, app)),
)
.with_margin_bottom(HEADER_PADDING)
.finish();
let crosscheck_toggle = render_ai_setting_toggle::<CrosscheckEnabled>(
"Crosscheck Work",
AISettingsPageAction::ToggleCrosscheckEnabled,
*ai_settings.crosscheck_enabled,
is_any_ai_enabled,
self.crosscheck_toggle.clone(),
&view.local_only_icon_tooltip_states,
app,
);
let crosscheck_description = render_ai_setting_description(
"When enabled, a reviewer agent critiques the main agent's output after each turn. \
The feedback loop continues until the reviewer responds with \"LGTM!\" or the \
maximum iteration count is reached.",
is_any_ai_enabled,
app,
);
let model_description = render_ai_setting_description(
"Configure the reviewer model and max iterations in settings.toml under \
[agents.experiments].",
is_any_ai_enabled,
app,
);
let column = Flex::column()
.with_child(header)
.with_child(crosscheck_toggle)
.with_child(crosscheck_description)
.with_child(model_description)
.finish();
column
}
}
mod styles {
use galaxy_core::ui::appearance::Appearance;
use galaxy_core::ui::theme::Fill;
+4
View File
@@ -272,6 +272,7 @@ pub enum SettingsSection {
ThirdPartyCLIAgents,
Bedrock,
OpenAI,
Experiments,
/// Internal backing-page identifier for CodeSettingsPageView. Multiple subpages
/// (CodeIndexing, EditorAndCodeReview) share this single backing page,
/// so this variant is needed as the key in `settings_pages`.
@@ -306,6 +307,7 @@ impl Display for SettingsSection {
SettingsSection::ThirdPartyCLIAgents => write!(f, "Third party CLI agents"),
SettingsSection::Bedrock => write!(f, "AWS Bedrock"),
SettingsSection::OpenAI => write!(f, "OpenAI / LiteLLM"),
SettingsSection::Experiments => write!(f, "Experiments"),
SettingsSection::Warpify => write!(f, "Wormhole"),
SettingsSection::CodeIndexing => write!(f, "Indexing and projects"),
SettingsSection::EditorAndCodeReview => write!(f, "Editor and Code Review"),
@@ -363,6 +365,7 @@ impl SettingsSection {
Self::ThirdPartyCLIAgents,
Self::Bedrock,
Self::OpenAI,
Self::Experiments,
]
}
@@ -405,6 +408,7 @@ impl FromStr for SettingsSection {
"CloudEnvironments" | "Environments" => Ok(Self::CloudEnvironments),
"OzCloudAPIKeys" => Ok(Self::OzCloudAPIKeys),
"SharedBlocks" | "Shared Blocks" => Ok(Self::SharedBlocks),
"Experiments" => Ok(Self::Experiments),
_ => Err(()),
}
}