feat(config): implement verbosity settings with normal and concise modes (#3052)

* feat(config): implement verbosity settings with normal and concise modes

* fix(config): wrap unsafe env calls in tests and fix clippy/fmt errors for CI

* perf(config): avoid verbosity prompt allocations
This commit is contained in:
cyq
2026-06-13 01:53:29 +08:00
committed by GitHub
parent 5be5cd5a79
commit b63287e653
13 changed files with 187 additions and 1 deletions
+26
View File
@@ -92,6 +92,12 @@ struct Cli {
model: Option<String>,
#[arg(long = "output-mode")]
output_mode: Option<String>,
#[arg(
long = "verbosity",
value_name = "LEVEL",
help = "Controls transcript and output verbosity (normal, concise)"
)]
verbosity: Option<String>,
#[arg(long = "log-level")]
log_level: Option<String>,
#[arg(long)]
@@ -517,6 +523,7 @@ fn run() -> Result<()> {
approval_policy: cli.approval_policy.clone(),
sandbox_mode: cli.sandbox_mode.clone(),
yolo: Some(cli.yolo),
verbosity: cli.verbosity.clone(),
};
let command = cli.command.take();
@@ -1674,6 +1681,14 @@ fn build_tui_command(
passthrough: Vec<String>,
) -> Result<Command> {
let tui = locate_sibling_tui_binary()?;
let mut verbosity = resolved_runtime.verbosity.clone();
if verbosity.is_none()
&& passthrough
.iter()
.any(|arg| matches!(arg.as_str(), "exec" | "swebench" | "eval"))
{
verbosity = Some("concise".to_string());
}
let mut cmd = Command::new(&tui);
if let Some(config) = cli.config.as_ref() {
@@ -1753,6 +1768,9 @@ fn build_tui_command(
if let Some(output_mode) = cli.output_mode.as_ref() {
cmd.env("DEEPSEEK_OUTPUT_MODE", output_mode);
}
if let Some(v) = verbosity.as_ref() {
cmd.env("DEEPSEEK_VERBOSITY", v);
}
if let Some(log_level) = cli.log_level.as_ref() {
cmd.env("DEEPSEEK_LOG_LEVEL", log_level);
}
@@ -3124,6 +3142,7 @@ mod tests {
approval_policy: None,
sandbox_mode: None,
yolo: None,
verbosity: None,
http_headers: std::collections::BTreeMap::new(),
};
@@ -3182,6 +3201,7 @@ mod tests {
approval_policy: None,
sandbox_mode: None,
yolo: None,
verbosity: None,
http_headers: std::collections::BTreeMap::new(),
};
@@ -3221,6 +3241,7 @@ mod tests {
approval_policy: None,
sandbox_mode: None,
yolo: None,
verbosity: None,
http_headers: std::collections::BTreeMap::new(),
};
@@ -3260,6 +3281,7 @@ mod tests {
approval_policy: None,
sandbox_mode: None,
yolo: None,
verbosity: None,
http_headers: resolved_headers,
};
@@ -3316,6 +3338,7 @@ mod tests {
approval_policy: None,
sandbox_mode: None,
yolo: None,
verbosity: None,
http_headers: std::collections::BTreeMap::new(),
};
@@ -3381,6 +3404,7 @@ mod tests {
approval_policy: None,
sandbox_mode: None,
yolo: None,
verbosity: None,
http_headers: std::collections::BTreeMap::new(),
};
@@ -3447,6 +3471,7 @@ mod tests {
approval_policy: None,
sandbox_mode: None,
yolo: None,
verbosity: None,
http_headers: std::collections::BTreeMap::new(),
};
@@ -3543,6 +3568,7 @@ mod tests {
approval_policy: None,
sandbox_mode: None,
yolo: None,
verbosity: None,
http_headers: std::collections::BTreeMap::new(),
};
+54
View File
@@ -565,6 +565,7 @@ pub struct ConfigToml {
pub model: Option<String>,
pub auth_mode: Option<String>,
pub output_mode: Option<String>,
pub verbosity: Option<String>,
pub log_level: Option<String>,
pub telemetry: Option<bool>,
pub approval_policy: Option<String>,
@@ -1077,6 +1078,9 @@ impl ConfigToml {
if project.output_mode.is_some() {
self.output_mode = project.output_mode;
}
if project.verbosity.is_some() {
self.verbosity = project.verbosity;
}
if project.log_level.is_some() {
self.log_level = project.log_level;
}
@@ -1147,6 +1151,7 @@ impl ConfigToml {
"model" => self.model.clone(),
"auth.mode" => self.auth_mode.clone(),
"output_mode" => self.output_mode.clone(),
"verbosity" => self.verbosity.clone(),
"log_level" => self.log_level.clone(),
"telemetry" => self.telemetry.map(|v| v.to_string()),
"approval_policy" => self.approval_policy.clone(),
@@ -1295,6 +1300,7 @@ impl ConfigToml {
"model" => self.model = Some(value.to_string()),
"auth.mode" => self.auth_mode = Some(value.to_string()),
"output_mode" => self.output_mode = Some(value.to_string()),
"verbosity" => self.verbosity = Some(value.to_string()),
"log_level" => self.log_level = Some(value.to_string()),
"telemetry" => {
self.telemetry = Some(parse_bool(value)?);
@@ -1548,6 +1554,7 @@ impl ConfigToml {
"model" => self.model = None,
"auth.mode" => self.auth_mode = None,
"output_mode" => self.output_mode = None,
"verbosity" => self.verbosity = None,
"log_level" => self.log_level = None,
"telemetry" => self.telemetry = None,
"approval_policy" => self.approval_policy = None,
@@ -1686,6 +1693,9 @@ impl ConfigToml {
if let Some(v) = self.output_mode.as_ref() {
out.insert("output_mode".to_string(), v.clone());
}
if let Some(v) = self.verbosity.as_ref() {
out.insert("verbosity".to_string(), v.clone());
}
if let Some(v) = self.log_level.as_ref() {
out.insert("log_level".to_string(), v.clone());
}
@@ -2150,6 +2160,11 @@ impl ConfigToml {
.or_else(|| env.sandbox_mode.clone())
.or_else(|| self.sandbox_mode.clone());
let yolo = cli.yolo.or(env.yolo);
let verbosity = cli
.verbosity
.clone()
.or_else(|| env.verbosity.clone())
.or_else(|| self.verbosity.clone());
ResolvedRuntimeOptions {
provider,
@@ -2165,6 +2180,7 @@ impl ConfigToml {
approval_policy,
sandbox_mode,
yolo,
verbosity,
http_headers,
}
}
@@ -2770,6 +2786,7 @@ pub struct CliRuntimeOverrides {
pub approval_policy: Option<String>,
pub sandbox_mode: Option<String>,
pub yolo: Option<bool>,
pub verbosity: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -2807,6 +2824,7 @@ pub struct ResolvedRuntimeOptions {
pub approval_policy: Option<String>,
pub sandbox_mode: Option<String>,
pub yolo: Option<bool>,
pub verbosity: Option<String>,
pub http_headers: BTreeMap<String, String>,
}
@@ -3237,6 +3255,7 @@ struct EnvRuntimeOverrides {
approval_policy: Option<String>,
sandbox_mode: Option<String>,
yolo: Option<bool>,
verbosity: Option<String>,
http_headers: Option<BTreeMap<String, String>>,
deepseek_base_url: Option<String>,
nvidia_base_url: Option<String>,
@@ -3311,6 +3330,9 @@ impl EnvRuntimeOverrides {
arcee_model: std::env::var("ARCEE_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
verbosity: std::env::var("CODEWHALE_VERBOSITY")
.or_else(|_| std::env::var("DEEPSEEK_VERBOSITY"))
.ok(),
output_mode: std::env::var("DEEPSEEK_OUTPUT_MODE").ok(),
auth_mode: std::env::var("DEEPSEEK_AUTH_MODE").ok(),
log_level: std::env::var("DEEPSEEK_LOG_LEVEL").ok(),
@@ -6761,4 +6783,36 @@ unknown_policy = "surprise"
assert!(err.to_string().contains("unknown_policy"));
}
#[test]
fn test_verbosity_resolution() {
let _lock = env_lock();
// Test TOML parsing
let toml_str = r#"
verbosity = "concise"
"#;
let config: ConfigToml = toml::from_str(toml_str).unwrap();
assert_eq!(config.verbosity, Some("concise".to_string()));
// Test Env overrides
let _env = EnvGuard::without_deepseek_runtime_overrides();
unsafe {
std::env::set_var("CODEWHALE_VERBOSITY", "normal");
}
let env_overrides = EnvRuntimeOverrides::load();
assert_eq!(env_overrides.verbosity, Some("normal".to_string()));
unsafe {
std::env::remove_var("CODEWHALE_VERBOSITY");
}
// Test fallback to DEEPSEEK_VERBOSITY
unsafe {
std::env::set_var("DEEPSEEK_VERBOSITY", "concise");
}
let env_overrides = EnvRuntimeOverrides::load();
assert_eq!(env_overrides.verbosity, Some("concise".to_string()));
unsafe {
std::env::remove_var("DEEPSEEK_VERBOSITY");
}
}
}
+19 -1
View File
@@ -1638,6 +1638,7 @@ pub struct Config {
pub approval_policy: Option<String>,
pub sandbox_mode: Option<String>,
pub yolo: Option<bool>,
pub verbosity: Option<String>,
/// External sandbox backend: `"none"` or `"opensandbox"`.
/// When set, exec_shell routes commands through the backend's HTTP API
/// instead of spawning a local process.
@@ -2217,6 +2218,12 @@ impl Config {
);
}
}
if let Some(v) = self.verbosity.as_deref() {
let normalized = v.trim().to_ascii_lowercase();
if !matches!(normalized.as_str(), "normal" | "concise") {
anyhow::bail!("Invalid verbosity '{v}': expected normal or concise.");
}
}
if let Some(mode) = self.sandbox_mode.as_deref() {
let normalized = mode.trim().to_ascii_lowercase();
if !matches!(
@@ -4058,6 +4065,11 @@ fn apply_env_overrides(config: &mut Config) {
if let Ok(value) = std::env::var("DEEPSEEK_YOLO") {
config.yolo = Some(value == "1" || value.eq_ignore_ascii_case("true"));
}
if let Ok(value) =
std::env::var("CODEWHALE_VERBOSITY").or_else(|_| std::env::var("DEEPSEEK_VERBOSITY"))
{
config.verbosity = Some(value);
}
if let Ok(value) = std::env::var("DEEPSEEK_SANDBOX_BACKEND") {
config.sandbox_backend = Some(value);
}
@@ -4660,6 +4672,7 @@ fn merge_config(base: Config, override_cfg: Config) -> Config {
allow_shell: override_cfg.allow_shell.or(base.allow_shell),
prompt_suggestion: override_cfg.prompt_suggestion.or(base.prompt_suggestion),
yolo: override_cfg.yolo.or(base.yolo),
verbosity: override_cfg.verbosity.or(base.verbosity),
approval_policy: override_cfg.approval_policy.or(base.approval_policy),
sandbox_mode: override_cfg.sandbox_mode.or(base.sandbox_mode),
sandbox_backend: override_cfg.sandbox_backend.or(base.sandbox_backend),
@@ -4790,7 +4803,12 @@ fn warn_on_misplaced_top_level_keys(raw: &str) -> Option<String> {
// Sections CodeWhale does not recognize but users nest settings under.
const UNKNOWN_SECTIONS: &[&str] = &["general", "sandbox"];
// Keys that are only ever read from the top level of the config.
const TOP_LEVEL_KEYS: &[&str] = &["allow_shell", "sandbox_mode", "approval_policy"];
const TOP_LEVEL_KEYS: &[&str] = &[
"allow_shell",
"sandbox_mode",
"approval_policy",
"verbosity",
];
let mut hits: Vec<String> = Vec::new();
for section in UNKNOWN_SECTIONS {
+9
View File
@@ -274,6 +274,7 @@ pub struct EngineConfig {
/// Whether user-visible transcript rendering shows thinking blocks.
/// Prompt assembly uses this to avoid localizing hidden reasoning.
pub show_thinking: bool,
pub verbosity: Option<String>,
/// Maximum number of assistant steps before stopping.
pub max_steps: u32,
/// Maximum number of concurrently active subagents.
@@ -430,6 +431,7 @@ impl Default for EngineConfig {
),
tools_always_load: HashSet::new(),
prefer_bwrap: false,
verbosity: None,
tools: None,
}
}
@@ -700,6 +702,7 @@ impl Engine {
translation_enabled: config.translation_enabled,
model_id: &config.model,
show_thinking: config.show_thinking,
verbosity: config.verbosity.as_deref(),
},
);
let stable_prompt = Some(system_prompt);
@@ -1104,6 +1107,7 @@ impl Engine {
show_thinking,
allowed_tools,
hook_executor,
verbosity,
} => {
self.handle_send_message(
content,
@@ -1121,6 +1125,7 @@ impl Engine {
show_thinking,
allowed_tools,
hook_executor,
verbosity,
)
.await;
}
@@ -1378,6 +1383,7 @@ impl Engine {
self.config.show_thinking,
self.config.allowed_tools.clone(),
self.config.hook_executor.clone(),
self.config.verbosity.clone(),
)
.await;
}
@@ -1532,6 +1538,7 @@ impl Engine {
show_thinking: bool,
allowed_tools: Option<Vec<String>>,
hook_executor: Option<std::sync::Arc<crate::hooks::HookExecutor>>,
verbosity: Option<String>,
) {
// Reset cancel token for fresh turn (in case previous was cancelled)
self.reset_cancel_token();
@@ -1660,6 +1667,7 @@ impl Engine {
self.config.trust_mode = trust_mode;
self.config.translation_enabled = translation_enabled;
self.config.show_thinking = show_thinking;
self.config.verbosity = verbosity;
// Refresh stable prompt context. Current mode is carried by the
// request-time runtime prompt projection.
@@ -2448,6 +2456,7 @@ impl Engine {
translation_enabled: self.config.translation_enabled,
model_id: &self.config.model,
show_thinking: self.config.show_thinking,
verbosity: self.config.verbosity.as_deref(),
},
);
let mut stable_prompt =
+1
View File
@@ -41,6 +41,7 @@ pub enum Op {
/// Hook executor for control-plane hooks.
/// `ToolCallBefore` hooks may deny a tool call with exit code 2.
hook_executor: Option<std::sync::Arc<crate::hooks::HookExecutor>>,
verbosity: Option<String>,
},
/// Execute a user-submitted composer shell command (`! <command>`) without
+2
View File
@@ -5903,6 +5903,7 @@ async fn run_exec_agent(
search_base_url: config.search.as_ref().and_then(|s| s.base_url.clone()),
tools_always_load: config.tools_always_load(),
tools: config.tools.clone(),
verbosity: config.verbosity.clone(),
};
let engine_handle = spawn_engine(engine_config, config);
@@ -5969,6 +5970,7 @@ async fn run_exec_agent(
.and_then(crate::tui::approval::ApprovalMode::from_config_value)
.unwrap_or_default()
},
verbosity: config.verbosity.clone(),
})
.await?;
+63
View File
@@ -38,6 +38,7 @@ pub struct PromptSessionContext<'a> {
/// When false, the prompt should not spend localization pressure on
/// `reasoning_content` the user will never see.
pub show_thinking: bool,
pub verbosity: Option<&'a str>,
}
impl Default for PromptSessionContext<'_> {
@@ -50,6 +51,7 @@ impl Default for PromptSessionContext<'_> {
translation_enabled: false,
model_id: "codewhale",
show_thinking: true,
verbosity: None,
}
}
}
@@ -92,6 +94,18 @@ so any English prose in your response will block their decision-making."
)
}
fn concise_output_discipline_instruction() -> &'static str {
"\
## Concise Output Discipline
To minimize token usage and optimize speed:
- Output only direct, actionable code, technical steps, or final answers.
- Eliminate all conversational filler, fluff, introductions, transitions, or summarizing conclusions.
- Do NOT explain what you are about to do or what you have just completed.
- Do NOT provide conversational status updates before or after running tools.
- Keep explanations and comments extremely brief and technical, explaining only non-obvious reasoning."
}
fn translation_target_language_for_tag(locale_tag: &str) -> &'static str {
let normalized = locale_tag.trim().to_ascii_lowercase();
if normalized.starts_with("ja") {
@@ -1035,6 +1049,7 @@ pub fn system_prompt_for_mode_with_context_and_skills(
translation_enabled: false,
model_id: "codewhale",
show_thinking: true,
verbosity: None,
},
)
}
@@ -1117,6 +1132,13 @@ pub fn system_prompt_for_mode_with_context_skills_session_and_approval(
);
}
if session_context.verbosity == Some("concise") {
full_prompt = format!(
"{full_prompt}\n\n{}",
concise_output_discipline_instruction()
);
}
// 3. Skills block. #432: walks every candidate workspace
// skills directory (`.agents/skills`, `skills`,
// `.opencode/skills`, `.claude/skills`, `.cursor/skills`) plus global
@@ -1909,6 +1931,7 @@ mod tests {
translation_enabled: false,
model_id: "codewhale",
show_thinking: true,
verbosity: None,
},
) {
SystemPrompt::Text(text) => text,
@@ -1978,6 +2001,7 @@ mod tests {
translation_enabled: false,
model_id: "codewhale",
show_thinking: true,
verbosity: None,
},
) {
SystemPrompt::Text(text) => text,
@@ -2020,6 +2044,7 @@ mod tests {
translation_enabled: false,
model_id: "codewhale",
show_thinking: false,
verbosity: None,
},
) {
SystemPrompt::Text(text) => text,
@@ -2072,6 +2097,7 @@ mod tests {
translation_enabled: false,
model_id: "codewhale",
show_thinking: true,
verbosity: None,
},
) {
SystemPrompt::Text(text) => text,
@@ -2175,6 +2201,7 @@ mod tests {
translation_enabled: false,
model_id: "codewhale",
show_thinking: true,
verbosity: None,
},
) {
SystemPrompt::Text(text) => text,
@@ -2211,6 +2238,7 @@ mod tests {
translation_enabled: false,
model_id: "codewhale",
show_thinking: true,
verbosity: None,
},
) {
SystemPrompt::Text(text) => text,
@@ -2239,6 +2267,7 @@ mod tests {
translation_enabled: false,
model_id: "codewhale",
show_thinking: true,
verbosity: None,
},
) {
SystemPrompt::Text(text) => text,
@@ -2296,6 +2325,7 @@ mod tests {
translation_enabled: false,
model_id: "codewhale",
show_thinking: true,
verbosity: None,
},
) {
SystemPrompt::Text(text) => text,
@@ -2324,6 +2354,7 @@ mod tests {
translation_enabled: false,
model_id: "codewhale",
show_thinking: true,
verbosity: None,
},
) {
SystemPrompt::Text(text) => text,
@@ -2530,6 +2561,7 @@ mod tests {
translation_enabled: false,
model_id: "codewhale",
show_thinking: true,
verbosity: None,
},
) {
SystemPrompt::Text(text) => text,
@@ -2564,6 +2596,7 @@ mod tests {
translation_enabled: false,
model_id: "codewhale",
show_thinking: true,
verbosity: None,
},
) {
SystemPrompt::Text(text) => text,
@@ -3088,4 +3121,34 @@ mod tests {
"instructions block must annotate its source path"
);
}
#[test]
fn verbosity_concise_appends_discipline_block() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path();
let prompt = match super::system_prompt_for_mode_with_context_skills_session_and_approval(
workspace,
None,
None,
None,
PromptSessionContext {
user_memory_block: None,
goal_objective: None,
project_context_pack_enabled: false,
locale_tag: "en",
translation_enabled: false,
model_id: "codewhale",
show_thinking: true,
verbosity: Some("concise"),
},
) {
SystemPrompt::Text(text) => text,
SystemPrompt::Blocks(_) => panic!("expected text system prompt"),
};
assert!(
prompt.contains("## Concise Output Discipline"),
"Concise Output Discipline should be appended"
);
}
}
+2
View File
@@ -29,3 +29,5 @@ Long sessions accumulate context. To stay fast:
- Suggest `/compact` or Ctrl+L when context nears 60% during sustained work — the compaction relay preserves open blockers
- Use `note` for decisions you'll need across compaction boundaries
- A 3-turn session that fans out to sub-agents finishes faster AND stays responsive longer than a 15-turn sequential grind
Do NOT explain, announce, or mention to the user that you are running in Agent mode or how the approval policy works. Act silently on this mode instruction.
+2
View File
@@ -15,3 +15,5 @@ can't change it. Shell and code execution are unavailable.
Use this mode to build a thorough plan. Spawn read-only sub-agents for parallel investigation.
After `update_plan` presents the plan, wait for the user's next action instead of continuing to
tool around in Plan mode.
Do NOT explain, announce, or mention to the user that you are running in Plan mode, or describe the transition. Act silently on this mode instruction.
+2
View File
@@ -9,3 +9,5 @@ Even with auto-approval, use `checklist_write` for work that has several concret
visible and trackable in the sidebar. Keep simple commands and focused edits direct.
For multi-step initiatives, keep `checklist_write` current. Add `update_plan` only when a high-level strategy
would help and do not duplicate the checklist there.
Do NOT announce or mention to the user that you are running in YOLO mode. Act silently on this mode instruction.
+2
View File
@@ -1708,6 +1708,7 @@ impl RuntimeThreadManager {
} else {
crate::tui::approval::ApprovalMode::Suggest
},
verbosity: self.config.verbosity.clone(),
})
.await
.map_err(|e| anyhow!("Failed to start turn: {e}"))?;
@@ -2093,6 +2094,7 @@ impl RuntimeThreadManager {
search_base_url: self.config.search.as_ref().and_then(|s| s.base_url.clone()),
tools_always_load: self.config.tools_always_load(),
tools: self.config.tools.clone(),
verbosity: self.config.verbosity.clone(),
};
let engine = spawn_engine(engine_cfg, &self.config);
+2
View File
@@ -1438,6 +1438,7 @@ pub struct App {
pub compact_threshold: usize,
pub max_input_history: usize,
pub allow_shell: bool,
pub verbosity: Option<String>,
pub max_subagents: usize,
/// Per-SSE-chunk idle timeout for streamed turns, in seconds.
pub stream_chunk_timeout_secs: u64,
@@ -2203,6 +2204,7 @@ impl App {
compact_threshold,
max_input_history,
allow_shell,
verbosity: config.verbosity.clone(),
max_subagents,
stream_chunk_timeout_secs: config.stream_chunk_timeout_secs(),
subagent_cache: Vec::new(),
+3
View File
@@ -873,6 +873,7 @@ fn build_engine_config(app: &App, config: &Config) -> EngineConfig {
project_context_pack_enabled: config.project_context_pack_enabled(),
translation_enabled: app.translation_enabled,
show_thinking: app.show_thinking,
verbosity: app.verbosity.clone(),
// Effectively unlimited. V4 has a 1M context window and the user
// wants the model running until it's actually done. The previous cap
// of 100 hit the ceiling on long multi-step plans (wide refactors,
@@ -5516,6 +5517,7 @@ async fn dispatch_user_message(
translation_enabled: app.translation_enabled,
model_id: &app.model,
show_thinking: app.show_thinking,
verbosity: app.verbosity.as_deref(),
},
),
);
@@ -5618,6 +5620,7 @@ async fn dispatch_user_message(
show_thinking: app.show_thinking,
allowed_tools: app.active_allowed_tools.clone(),
hook_executor: app.runtime_services.hook_executor.clone(),
verbosity: app.verbosity.clone(),
})
.await
{