mod agent;
mod provider;
use super::{EXTENSION_FACTORIES, ExtensionFactory, PRIORITY_DEBUG, command_arg};
use crate::{
core::extensions::{
Extension, ExtensionCommand, ExtensionHook, ExtensionMode, ExtensionTool, RichSpan,
SubcommandDef,
},
modes::interactive::{
app::{App, MsgLevel},
handlers::register_slash_command,
},
};
use serde_json::Value;
use std::sync::Arc;
const EXT: &str = "debug";
const CMD: &str = "debug";
const SUBCOMMANDS: &[SubcommandDef] = &[
SubcommandDef {
name: "info",
description: "Show agent runtime status (provider, model, session)",
},
SubcommandDef {
name: "provider",
description: "Capture raw provider stream events ([on|off|dump])",
},
SubcommandDef {
name: "system-prompt",
description: "Print the current system prompt",
},
];
#[linkme::distributed_slice(EXTENSION_FACTORIES)]
static DEBUG_FACTORY: ExtensionFactory = ExtensionFactory {
priority: PRIORITY_DEBUG,
make: || -> Arc<dyn Extension> { Arc::new(Debug) },
};
pub struct Debug;
impl Extension for Debug {
fn name(&self) -> &str {
EXT
}
fn description(&self) -> &str {
"Debug helpers: agent status, raw provider stream events, system prompt"
}
fn default_enabled(&self) -> bool {
false
}
fn modes(&self) -> Vec<ExtensionMode> {
vec![ExtensionMode::Dev]
}
fn tools(&self) -> Vec<ExtensionTool> {
Vec::new()
}
fn commands(&self) -> Vec<ExtensionCommand> {
vec![ExtensionCommand {
name: CMD.to_string(),
description: "/debug <info|provider|system-prompt> - debug helpers".to_string(),
busy_safe: true, subcommands: SUBCOMMANDS.to_vec(),
}]
}
fn hooks(&self) -> Vec<ExtensionHook> {
vec![
ExtensionHook::AgentEvent,
ExtensionHook::ProviderStreamEvent,
]
}
fn on_registered(&self) {
register_slash_command(EXT, CMD, command_debug);
}
fn provider_stream_event(&self, event: &Value) {
provider::on_stream_event(event);
}
fn on_agent_event(&self, event: &Value) {
provider::on_agent_event(event);
}
fn render_custom_message(&self, custom_type: &str, data: &Value) -> Option<Vec<RichSpan>> {
provider::render_custom_message(custom_type, data)
}
}
fn command_debug(st: &mut App, raw: &str) -> bool {
let args = command_arg(raw);
let (sub, rest) = match args.split_once(char::is_whitespace) {
Some((sub, rest)) => (sub, rest.trim()),
None => (args, ""),
};
match sub.to_lowercase().as_str() {
"info" => agent::info(st),
"provider" => provider::command(st, rest),
"system-prompt" => agent::system_prompt(st),
other => st.push_msg(usage(other), MsgLevel::Warning),
}
false
}
fn usage(arg: &str) -> String {
let head = "Usage: /debug <info|provider|system-prompt>";
if arg.is_empty() {
head.to_string()
} else {
format!("{head} (got `{arg}`)")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
core::extensions::{
ExtensionMode, command_provider, register_extension, set_extension_enabled,
set_extension_mode, unregister_extension,
},
modes::interactive::{
agent_actor::{AgentCommand, WorkerHandle, channels},
app::sys_spans_text,
},
test_support::{AUTH_TEST_LOCK, AgentDirGuard},
};
fn lock() -> std::sync::MutexGuard<'static, ()> {
AUTH_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
fn app_with_channel() -> (App, tokio::sync::mpsc::UnboundedReceiver<AgentCommand>) {
let (tx, rx) = channels();
let mut st = App::new();
st.worker = WorkerHandle::new(tx);
(st, rx)
}
pub(super) fn register_enabled() {
let _ = unregister_extension(EXT);
crate::core::settings_manager::write_disabled_extensions(&[]).ok();
set_extension_mode(ExtensionMode::All);
register_extension(Debug);
set_extension_enabled(EXT, true);
provider::reset_for_test();
}
pub(super) fn cleanup() {
provider::reset_for_test();
let _ = unregister_extension(EXT);
}
#[test]
fn registering_extension_exposes_command() {
let _g = lock();
let _ad = AgentDirGuard::temp();
register_enabled();
assert_eq!(command_provider(CMD).as_deref(), Some(EXT));
cleanup();
}
#[test]
fn subcommands_match_parser() {
let names: Vec<&str> = SUBCOMMANDS.iter().map(|s| s.name).collect();
assert_eq!(names, vec!["info", "provider", "system-prompt"]);
let cmds = Debug.commands();
assert_eq!(cmds[0].subcommands, SUBCOMMANDS.to_vec());
for name in names {
let mut st = App::new();
command_debug(&mut st, &format!("debug {name}"));
assert!(
!st.system_messages
.iter()
.any(|(_, m)| sys_spans_text(&m.spans).contains("Usage:")),
"子命令 `{name}` 应被解析器识别"
);
}
}
#[test]
fn info_and_system_prompt_ask_the_worker() {
let _g = lock();
let (mut st, mut rx) = app_with_channel();
command_debug(&mut st, "debug info");
assert!(
matches!(rx.try_recv(), Ok(AgentCommand::DebugInfo)),
"info 应下发 DebugInfo 给 worker"
);
let (mut st, mut rx) = app_with_channel();
command_debug(&mut st, "debug system-prompt");
assert!(
matches!(rx.try_recv(), Ok(AgentCommand::SystemPrompt)),
"system-prompt 应下发 SystemPrompt 给 worker"
);
}
#[test]
fn missing_or_unknown_subcommand_reports_usage() {
let _g = lock();
let mut st = App::new();
assert!(!command_debug(&mut st, "debug"));
assert!(st.system_messages.iter().any(|(_, m)| {
let text = sys_spans_text(&m.spans);
text.contains("Usage:") && !text.contains("got")
}));
let mut st = App::new();
command_debug(&mut st, "debug bogus");
assert!(
st.system_messages
.iter()
.any(|(_, m)| { sys_spans_text(&m.spans).contains("(got `bogus`)") })
);
}
#[test]
fn provider_subcommand_arguments_reach_the_provider_module() {
let _g = lock();
provider::reset_for_test();
let mut st = App::new();
command_debug(&mut st, "debug provider");
assert!(provider::enabled(), "无参 `provider` 应切换为开启");
let mut st = App::new();
command_debug(&mut st, "debug provider off");
assert!(!provider::enabled(), "`provider off` 应关闭捕获");
let mut st = App::new();
command_debug(&mut st, "debug provider bogus");
assert!(!provider::enabled());
assert!(
st.system_messages
.iter()
.any(|(_, m)| { sys_spans_text(&m.spans).contains("Usage: /debug provider") })
);
cleanup();
}
}