Improve inline autocomplete reliability, sanitization, and debug logging (#407)

* Enhance autocomplete functionality and logging. Increased debounce time for autocomplete suggestions and added minimum context character requirement. Improved inline suggestion handling with new cleanup logic for tab acceptance. Introduced a new logging option for autocomplete-only logs in CLI. Updated various components to support these changes, including sanitization and error handling in the autocomplete engine.

* Add autocomplete CLI adapter for improved argument handling

This commit introduces a new module, , which encapsulates the argument parsing and logging logic specific to the autocomplete namespace in the CLI. Key features include extraction of leading verbose flags, handling of the  flag, and improved help message printing. The existing CLI command handling has been refactored to utilize this new adapter, enhancing code organization and maintainability.

* Refactor inline completion sanitization and enhance context handling
This commit is contained in:
YellowSnnowmann
2026-04-07 09:11:42 -07:00
committed by GitHub
parent 000b40bf43
commit fb8987bcad
19 changed files with 943 additions and 185 deletions
+181
View File
@@ -0,0 +1,181 @@
//! Autocomplete-specific CLI adapter.
//!
//! Keeps autocomplete-only argument handling out of the generic core CLI.
use anyhow::Result;
use crate::core::logging::CliLogDefault;
use crate::openhuman::autocomplete::ops::{autocomplete_start_cli, AutocompleteStartCliOptions};
pub struct NamespacePreparse {
pub args: Vec<String>,
pub init_logging: Option<(bool, CliLogDefault)>,
}
/// Extract only *leading* global verbose flags so parameter values remain intact.
/// Returns `(verbose, remaining_args)`.
fn extract_leading_verbose_flags(args: &[String]) -> (bool, Vec<String>) {
let mut verbose = false;
let mut index = 0usize;
while index < args.len() {
match args[index].as_str() {
"-v" | "--verbose" => {
verbose = true;
index += 1;
}
_ => break,
}
}
(verbose, args[index..].to_vec())
}
pub fn preparse_namespace(namespace: &str, args: &[String]) -> NamespacePreparse {
if namespace != "autocomplete" {
return NamespacePreparse {
args: args.to_vec(),
init_logging: None,
};
}
let (verbose, remaining) = extract_leading_verbose_flags(args);
NamespacePreparse {
args: remaining,
init_logging: Some((verbose, CliLogDefault::AutocompleteOnly)),
}
}
pub fn parse_run_scope_flag(flag: &str) -> Option<CliLogDefault> {
if flag == "--autocomplete-logs" {
Some(CliLogDefault::AutocompleteOnly)
} else {
None
}
}
pub fn print_run_scope_help_line() {
println!(
" --autocomplete-logs When RUST_LOG is unset: stderr shows only inline-autocomplete logs"
);
}
pub fn maybe_print_namespace_help_footer(namespace: &str) {
if namespace == "autocomplete" {
println!(
"Logging: stderr is autocomplete-only by default (unless RUST_LOG is set); add -v for trace."
);
}
}
pub fn maybe_print_start_help(namespace: &str, function: &str) -> bool {
if namespace == "autocomplete" && function == "start" {
print_autocomplete_start_help();
true
} else {
false
}
}
pub fn maybe_handle_namespace_start(
namespace: &str,
function: &str,
args: &[String],
) -> Result<Option<serde_json::Value>> {
if namespace != "autocomplete" || function != "start" {
return Ok(None);
}
let cli_options = parse_autocomplete_start_cli_options(args)?;
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
let value = rt
.block_on(async { autocomplete_start_cli(cli_options).await })
.map_err(anyhow::Error::msg)?;
Ok(Some(value))
}
/// Parses CLI options specific to the `autocomplete start` command.
fn parse_autocomplete_start_cli_options(args: &[String]) -> Result<AutocompleteStartCliOptions> {
let mut debounce_ms: Option<u64> = None;
let mut serve = false;
let mut spawn = false;
let mut i = 0usize;
while i < args.len() {
match args[i].as_str() {
"--debounce-ms" => {
let raw = args
.get(i + 1)
.ok_or_else(|| anyhow::anyhow!("missing value for --debounce-ms"))?;
debounce_ms = Some(
raw.parse::<u64>()
.map_err(|e| anyhow::anyhow!("invalid --debounce-ms: {e}"))?,
);
i += 2;
}
"--serve" => {
serve = true;
i += 1;
}
"--spawn" => {
spawn = true;
i += 1;
}
other => return Err(anyhow::anyhow!("unknown autocomplete start arg: {other}")),
}
}
if serve && spawn {
return Err(anyhow::anyhow!(
"--serve and --spawn are mutually exclusive"
));
}
Ok(AutocompleteStartCliOptions {
debounce_ms,
serve,
spawn,
})
}
/// Prints help information for the `autocomplete start` command.
fn print_autocomplete_start_help() {
println!("Usage: openhuman autocomplete start [--debounce-ms <u64>] [--serve|--spawn]");
println!();
println!(" --debounce-ms <u64> Override debounce in milliseconds.");
println!(" --serve Run autocomplete loop in the current foreground process.");
println!(" --spawn Spawn autocomplete loop as a background process.");
}
#[cfg(test)]
mod tests {
use super::parse_autocomplete_start_cli_options;
#[test]
fn parse_autocomplete_start_cli_options_rejects_serve_and_spawn() {
let args = vec!["--serve".to_string(), "--spawn".to_string()];
let err = parse_autocomplete_start_cli_options(&args)
.expect_err("must reject mutually exclusive flags");
assert!(err.to_string().contains("mutually exclusive"));
}
#[test]
fn extract_leading_verbose_flags_preserves_param_like_values() {
let args = vec![
"-v".to_string(),
"set_style".to_string(),
"--style-instructions".to_string(),
"--verbose".to_string(),
];
let (verbose, remaining) = super::extract_leading_verbose_flags(&args);
assert!(verbose);
assert_eq!(
remaining,
vec![
"set_style".to_string(),
"--style-instructions".to_string(),
"--verbose".to_string()
]
);
}
}
+27 -85
View File
@@ -9,9 +9,10 @@ use serde_json::{Map, Value};
use std::collections::BTreeMap;
use crate::core::all;
use crate::core::autocomplete_cli_adapter;
use crate::core::jsonrpc::{default_state, invoke_method, parse_json_params};
use crate::core::logging::CliLogDefault;
use crate::core::{ControllerSchema, TypeSchema};
use crate::openhuman::autocomplete::ops::{autocomplete_start_cli, AutocompleteStartCliOptions};
/// The ASCII banner displayed when the CLI starts.
const CLI_BANNER: &str = r#"
@@ -77,6 +78,7 @@ fn run_server_command(args: &[String]) -> Result<()> {
let mut host: Option<String> = None;
let mut socketio_enabled = true;
let mut verbose = false;
let mut log_scope = CliLogDefault::Global;
let mut i = 0usize;
// Manual argument parsing loop for specific flags.
@@ -108,8 +110,13 @@ fn run_server_command(args: &[String]) -> Result<()> {
verbose = true;
i += 1;
}
other if autocomplete_cli_adapter::parse_run_scope_flag(other).is_some() => {
log_scope = autocomplete_cli_adapter::parse_run_scope_flag(other)
.unwrap_or(CliLogDefault::Global);
i += 1;
}
"-h" | "--help" => {
println!("Usage: openhuman run [--host <addr>] [--port <u16>] [--jsonrpc-only] [-v|--verbose]");
println!("Usage: openhuman run [--host <addr>] [--port <u16>] [--jsonrpc-only] [--autocomplete-logs] [-v|--verbose]");
println!();
println!(
" --host <addr> Bind address (default: 127.0.0.1 or OPENHUMAN_CORE_HOST)"
@@ -118,6 +125,7 @@ fn run_server_command(args: &[String]) -> Result<()> {
" --port <u16> Listen address port (default: 7788 or OPENHUMAN_CORE_PORT)"
);
println!(" --jsonrpc-only HTTP JSON-RPC only; disable Socket.IO");
autocomplete_cli_adapter::print_run_scope_help_line();
println!(" -v, --verbose Shorthand for RUST_LOG=debug when RUST_LOG is unset");
println!();
println!("Logging: set RUST_LOG (e.g. RUST_LOG=debug openhuman run). Default level is info.");
@@ -127,7 +135,7 @@ fn run_server_command(args: &[String]) -> Result<()> {
}
}
crate::core::logging::init_for_cli_run(verbose);
crate::core::logging::init_for_cli_run(verbose, log_scope);
// Initialize the Tokio runtime and start the server.
let rt = tokio::runtime::Builder::new_multi_thread()
@@ -250,7 +258,7 @@ fn run_voice_server_command(args: &[String]) -> Result<()> {
}
}
crate::core::logging::init_for_cli_run(verbose);
crate::core::logging::init_for_cli_run(verbose, CliLogDefault::Global);
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
@@ -305,6 +313,12 @@ fn run_namespace_command(
));
};
let preparsed = autocomplete_cli_adapter::preparse_namespace(namespace, args);
let args: &[String] = &preparsed.args;
if let Some((verbose, scope)) = preparsed.init_logging {
crate::core::logging::init_for_cli_run(verbose, scope);
}
if args.is_empty() || is_help(&args[0]) {
print_namespace_help(namespace, schemas);
return Ok(());
@@ -317,19 +331,15 @@ fn run_namespace_command(
));
};
// Special case for autocomplete start command which has its own CLI options.
if namespace == "autocomplete" && function == "start" {
if args.len() > 1 && is_help(&args[1]) {
print_autocomplete_start_help();
// Domain adapters can intercept specific namespace/function combinations.
if args.len() > 1 && is_help(&args[1]) {
if autocomplete_cli_adapter::maybe_print_start_help(namespace, function) {
return Ok(());
}
let cli_options = parse_autocomplete_start_cli_options(&args[1..])?;
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
let value = rt
.block_on(async { autocomplete_start_cli(cli_options).await })
.map_err(anyhow::Error::msg)?;
}
if let Some(value) =
autocomplete_cli_adapter::maybe_handle_namespace_start(namespace, function, &args[1..])?
{
println!("{}", serde_json::to_string_pretty(&value)?);
return Ok(());
}
@@ -355,64 +365,6 @@ fn run_namespace_command(
Ok(())
}
/// Parses CLI options specific to the `autocomplete start` command.
///
/// # Arguments
///
/// * `args` - CLI arguments for the autocomplete start command.
fn parse_autocomplete_start_cli_options(args: &[String]) -> Result<AutocompleteStartCliOptions> {
let mut debounce_ms: Option<u64> = None;
let mut serve = false;
let mut spawn = false;
let mut i = 0usize;
while i < args.len() {
match args[i].as_str() {
"--debounce-ms" => {
let raw = args
.get(i + 1)
.ok_or_else(|| anyhow::anyhow!("missing value for --debounce-ms"))?;
debounce_ms = Some(
raw.parse::<u64>()
.map_err(|e| anyhow::anyhow!("invalid --debounce-ms: {e}"))?,
);
i += 2;
}
"--serve" => {
serve = true;
i += 1;
}
"--spawn" => {
spawn = true;
i += 1;
}
other => return Err(anyhow::anyhow!("unknown autocomplete start arg: {other}")),
}
}
// Ensure the user doesn't try to both foreground and background the process.
if serve && spawn {
return Err(anyhow::anyhow!(
"--serve and --spawn are mutually exclusive"
));
}
Ok(AutocompleteStartCliOptions {
debounce_ms,
serve,
spawn,
})
}
/// Prints help information for the `autocomplete start` command.
fn print_autocomplete_start_help() {
println!("Usage: openhuman autocomplete start [--debounce-ms <u64>] [--serve|--spawn]");
println!();
println!(" --debounce-ms <u64> Override debounce in milliseconds.");
println!(" --serve Run autocomplete loop in the current foreground process.");
println!(" --spawn Spawn autocomplete loop as a background process.");
}
/// Parses command-line arguments into a JSON map based on a function's schema.
///
/// # Arguments
@@ -548,6 +500,7 @@ fn print_namespace_help(namespace: &str, schemas: &[ControllerSchema]) {
println!(" {} - {}", schema.function, schema.description);
}
println!("\nUse `openhuman {namespace} <function> --help` for parameters.");
autocomplete_cli_adapter::maybe_print_namespace_help_footer(namespace);
}
/// Prints detailed help for a specific function, including its parameters and description.
@@ -576,10 +529,7 @@ fn is_help(value: &str) -> bool {
#[cfg(test)]
mod tests {
use super::{
grouped_schemas, parse_autocomplete_start_cli_options, parse_function_params,
parse_input_value,
};
use super::{grouped_schemas, parse_function_params, parse_input_value};
use crate::core::{ControllerSchema, FieldSchema, TypeSchema};
#[test]
@@ -597,14 +547,6 @@ mod tests {
assert!(grouped.contains_key("local_ai"));
}
#[test]
fn parse_autocomplete_start_cli_options_rejects_serve_and_spawn() {
let args = vec!["--serve".to_string(), "--spawn".to_string()];
let err = parse_autocomplete_start_cli_options(&args)
.expect_err("must reject mutually exclusive flags");
assert!(err.to_string().contains("mutually exclusive"));
}
#[test]
fn parse_function_params_rejects_unknown_param() {
let schema = ControllerSchema {
+36 -4
View File
@@ -16,6 +16,15 @@ use tracing_subscriber::util::SubscriberInitExt;
static INIT: Once = Once::new();
/// Default `RUST_LOG` when it is unset: either global levels or only the inline autocomplete module tree.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CliLogDefault {
/// Typical server/CLI logging (`info`, or `debug` when `verbose`).
Global,
/// Silence other modules; only `openhuman_core::openhuman::autocomplete::*` emits logs.
AutocompleteOnly,
}
/// `14:32:01 <INFO> (jsonrpc) message…` — colors when stderr is a TTY.
struct CleanCliFormat;
@@ -77,16 +86,39 @@ fn short_target(target: &str) -> &str {
/// Initialize `tracing` + bridge the `log` crate so existing `log::info!` calls appear.
///
/// - If `RUST_LOG` is unset: uses `info`, or `debug` when `verbose` is true.
/// - If `RUST_LOG` is unset: uses [`CliLogDefault`] and `verbose` to pick a default filter string.
/// - Safe to call once; subsequent calls are ignored.
pub fn init_for_cli_run(verbose: bool) {
pub fn init_for_cli_run(verbose: bool, default_scope: CliLogDefault) {
INIT.call_once(|| {
if std::env::var_os("RUST_LOG").is_none() {
std::env::set_var("RUST_LOG", if verbose { "debug" } else { "info" });
let default = match default_scope {
CliLogDefault::Global => {
if verbose {
"debug".to_string()
} else {
"info".to_string()
}
}
CliLogDefault::AutocompleteOnly => {
let level = if verbose { "trace" } else { "debug" };
format!("off,openhuman_core::openhuman::autocomplete={level}")
}
};
std::env::set_var("RUST_LOG", default);
}
let filter = tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
tracing_subscriber::EnvFilter::new(if verbose { "debug" } else { "info" })
match default_scope {
CliLogDefault::Global => {
tracing_subscriber::EnvFilter::new(if verbose { "debug" } else { "info" })
}
CliLogDefault::AutocompleteOnly => {
let level = if verbose { "trace" } else { "debug" };
tracing_subscriber::EnvFilter::new(format!(
"off,openhuman_core::openhuman::autocomplete={level}"
))
}
}
});
let use_color = io::stderr().is_terminal();
+6 -6
View File
@@ -92,7 +92,7 @@ fn run_ingest(args: &[String]) -> Result<()> {
anyhow::anyhow!("missing file argument. Use a file path or '-' for stdin.")
})?;
crate::core::logging::init_for_cli_run(verbose);
crate::core::logging::init_for_cli_run(verbose, crate::core::logging::CliLogDefault::Global);
let content = read_input(&file_path)?;
let doc_key = key.unwrap_or_else(|| file_path.clone());
@@ -194,7 +194,7 @@ fn run_docs(args: &[String]) -> Result<()> {
}
}
crate::core::logging::init_for_cli_run(verbose);
crate::core::logging::init_for_cli_run(verbose, crate::core::logging::CliLogDefault::Global);
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
@@ -245,7 +245,7 @@ fn run_graph_query(args: &[String]) -> Result<()> {
}
}
crate::core::logging::init_for_cli_run(verbose);
crate::core::logging::init_for_cli_run(verbose, crate::core::logging::CliLogDefault::Global);
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
@@ -307,7 +307,7 @@ fn run_query(args: &[String]) -> Result<()> {
namespace.ok_or_else(|| anyhow::anyhow!("--namespace is required for query"))?;
let query = query.ok_or_else(|| anyhow::anyhow!("--query is required"))?;
crate::core::logging::init_for_cli_run(verbose);
crate::core::logging::init_for_cli_run(verbose, crate::core::logging::CliLogDefault::Global);
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
@@ -339,7 +339,7 @@ fn run_namespaces(args: &[String]) -> Result<()> {
}
}
crate::core::logging::init_for_cli_run(verbose);
crate::core::logging::init_for_cli_run(verbose, crate::core::logging::CliLogDefault::Global);
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
@@ -382,7 +382,7 @@ fn run_clear(args: &[String]) -> Result<()> {
let namespace =
namespace.ok_or_else(|| anyhow::anyhow!("--namespace is required for clear"))?;
crate::core::logging::init_for_cli_run(verbose);
crate::core::logging::init_for_cli_run(verbose, crate::core::logging::CliLogDefault::Global);
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
+1
View File
@@ -2,6 +2,7 @@
use serde::Serialize;
pub mod all;
pub mod autocomplete_cli_adapter;
pub mod cli;
pub mod dispatch;
pub mod jsonrpc;
+4 -1
View File
@@ -35,7 +35,10 @@ pub fn run_repl(args: &[String]) -> anyhow::Result<()> {
return Ok(());
}
crate::core::logging::init_for_cli_run(opts.verbose);
crate::core::logging::init_for_cli_run(
opts.verbose,
crate::core::logging::CliLogDefault::Global,
);
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
+9 -3
View File
@@ -160,7 +160,10 @@ fn run_server(args: &[String]) -> Result<()> {
return Ok(());
}
crate::core::logging::init_for_cli_run(opts.verbose);
crate::core::logging::init_for_cli_run(
opts.verbose,
crate::core::logging::CliLogDefault::Global,
);
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
@@ -342,7 +345,10 @@ fn run_start_session(args: &[String]) -> Result<()> {
}
let (opts, _) = parse_opts(args)?;
crate::core::logging::init_for_cli_run(opts.verbose);
crate::core::logging::init_for_cli_run(
opts.verbose,
crate::core::logging::CliLogDefault::Global,
);
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
@@ -622,7 +628,7 @@ fn init_quiet_logging(verbose: bool) {
if !verbose && std::env::var_os("RUST_LOG").is_none() {
std::env::set_var("RUST_LOG", "warn");
}
crate::core::logging::init_for_cli_run(verbose);
crate::core::logging::init_for_cli_run(verbose, crate::core::logging::CliLogDefault::Global);
}
fn is_help(value: &str) -> bool {
+5 -2
View File
@@ -166,7 +166,10 @@ fn run_skills_server(args: &[String]) -> Result<()> {
return Ok(());
}
crate::core::logging::init_for_cli_run(opts.verbose);
crate::core::logging::init_for_cli_run(
opts.verbose,
crate::core::logging::CliLogDefault::Global,
);
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
@@ -508,7 +511,7 @@ fn init_quiet_logging(verbose: bool) {
if !verbose && std::env::var_os("RUST_LOG").is_none() {
std::env::set_var("RUST_LOG", "warn");
}
crate::core::logging::init_for_cli_run(verbose);
crate::core::logging::init_for_cli_run(verbose, crate::core::logging::CliLogDefault::Global);
}
fn is_help(value: &str) -> bool {
+5 -2
View File
@@ -119,7 +119,10 @@ fn run_server(args: &[String]) -> Result<()> {
return Ok(());
}
crate::core::logging::init_for_cli_run(opts.verbose);
crate::core::logging::init_for_cli_run(
opts.verbose,
crate::core::logging::CliLogDefault::Global,
);
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
@@ -354,7 +357,7 @@ fn init_quiet_logging(verbose: bool) {
if !verbose && std::env::var_os("RUST_LOG").is_none() {
std::env::set_var("RUST_LOG", "warn");
}
crate::core::logging::init_for_cli_run(verbose);
crate::core::logging::init_for_cli_run(verbose, crate::core::logging::CliLogDefault::Global);
}
fn is_help(value: &str) -> bool {