diff options
| author | aichat-agent <agent@aichat.local> | 2026-10-01 14:58:06 +0000 |
|---|---|---|
| committer | aichat-agent <agent@aichat.local> | 2026-10-01 15:54:19 +0000 |
| commit | 83a2cd1ac24ca0feda34ae877aaf31f41b58b86c (patch) | |
| tree | cbc83678fa05fe6a82908c448a4e20eae673a9c7 | |
| parent | 82976d349ad97ac9aae0655ad631dace5e2a6385 (diff) | |
| download | aichat-83a2cd1ac24ca0feda34ae877aaf31f41b58b86c.tar.gz | |
feat: add built-in subquery tool with hierarchical query ids
* Add config options subquery and subquery_max_depth.
* Register an internal subquery function that spawns a fresh LLM query
with the current model configuration but without the full context.
The model selects context numerically (message_indexes) or by text
excerpt (excerpts).
* Subqueries can use tools, including further nested subqueries up to
subquery_max_depth.
* Queries and subqueries get hierarchical ids ([0], [0/1], [0/1/2])
that prefix tool-call prints and spinner messages.
| -rw-r--r-- | config.example.yaml | 2 | ||||
| -rw-r--r-- | src/client/common.rs | 30 | ||||
| -rw-r--r-- | src/config/input.rs | 41 | ||||
| -rw-r--r-- | src/config/mod.rs | 22 | ||||
| -rw-r--r-- | src/config/subquery.rs | 360 | ||||
| -rw-r--r-- | src/function.rs | 29 | ||||
| -rw-r--r-- | src/main.rs | 4 | ||||
| -rw-r--r-- | src/render/stream.rs | 10 | ||||
| -rw-r--r-- | src/repl/mod.rs | 4 | ||||
| -rw-r--r-- | src/utils/abort_signal.rs | 68 |
10 files changed, 560 insertions, 10 deletions
diff --git a/config.example.yaml b/config.example.yaml index 7c367d7..ae249b6 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -17,6 +17,8 @@ function_calling: true # Enables or disables function calling (Globall mapping_tools: # Alias for a tool or toolset fs: 'fs_cat,fs_ls,fs_mkdir,fs_rm,fs_write' use_tools: null # Which tools to use by default. (e.g. 'fs,web_search') +subquery: false # Enable the built-in subquery tool, letting the model spawn fresh context-limited LLM queries. +subquery_max_depth: 3 # Maximum nesting depth of subqueries (the main query counts as depth 1). # ---- prelude ---- repl_prelude: null # Set a default role or session for REPL mode (e.g. role:<name>, session:<name>, <session>:<role>) diff --git a/src/client/common.rs b/src/client/common.rs index 3b89d11..ed95291 100644 --- a/src/client/common.rs +++ b/src/client/common.rs @@ -403,6 +403,26 @@ pub async fn create_openai_compatible_client_config( Ok(Some((model, clients))) } +async fn eval_tool_calls_with_ctx( + input: &Input, + client: &dyn Client, + tool_calls: Vec<ToolCall>, +) -> Result<Vec<ToolResult>> { + let path = input.query_path(); + let subquery_ctx = crate::config::SubqueryContext::from_input(input); + let mut ctx = crate::function::EvalToolCallsContext { + subquery_ctx: Some(subquery_ctx), + }; + if path.is_empty() { + eval_tool_calls(client.global_config(), tool_calls, &mut ctx).await + } else { + crate::utils::with_query_scope(crate::utils::format_query_path(&path), async { + eval_tool_calls(client.global_config(), tool_calls, &mut ctx).await + }) + .await + } +} + pub async fn call_chat_completions( input: &Input, print: bool, @@ -432,7 +452,10 @@ pub async fn call_chat_completions( client.global_config().read().print_markdown(&text)?; } } - Ok((text, eval_tool_calls(client.global_config(), tool_calls)?)) + Ok(( + text, + eval_tool_calls_with_ctx(input, client, tool_calls).await?, + )) } Err(err) => Err(err), } @@ -463,7 +486,10 @@ pub async fn call_chat_completions_streaming( if !text.is_empty() && !text.ends_with('\n') { println!(); } - Ok((text, eval_tool_calls(client.global_config(), tool_calls)?)) + Ok(( + text, + eval_tool_calls_with_ctx(input, client, tool_calls).await?, + )) } Err(err) => { if !text.is_empty() { diff --git a/src/config/input.rs b/src/config/input.rs index d6d154b..71807f3 100644 --- a/src/config/input.rs +++ b/src/config/input.rs @@ -27,6 +27,13 @@ pub struct Input { medias: Vec<String>, data_urls: HashMap<String, String>, tool_calls: Option<MessageContentToolCalls>, + query_path: Vec<u64>, + subquery_data: Option<( + Vec<Message>, + Option<f64>, + Option<f64>, + Option<Vec<crate::function::FunctionDeclaration>>, + )>, role: Role, rag_name: Option<String>, with_session: bool, @@ -47,6 +54,8 @@ impl Input { medias: Default::default(), data_urls: Default::default(), tool_calls: None, + query_path: vec![], + subquery_data: None, role, rag_name: None, with_session, @@ -114,6 +123,8 @@ impl Input { medias, data_urls, tool_calls: Default::default(), + query_path: vec![], + subquery_data: None, role, rag_name: None, with_session, @@ -167,6 +178,25 @@ impl Input { self.config.read().stream && !self.role().model().no_stream() } + pub fn query_path(&self) -> Vec<u64> { + self.query_path.clone() + } + + pub fn set_query_path(&mut self, path: Vec<u64>) { + self.query_path = path; + } + + pub fn set_subquery( + &mut self, + messages: Vec<Message>, + temperature: Option<f64>, + top_p: Option<f64>, + functions: Option<Vec<crate::function::FunctionDeclaration>>, + ) { + self.subquery_data = Some((messages, temperature, top_p, functions)); + self.with_session = false; + } + pub fn continue_output(&self) -> Option<&str> { self.continue_output.as_deref() } @@ -235,6 +265,17 @@ impl Input { model: &Model, stream: bool, ) -> Result<ChatCompletionsData> { + if let Some((subquery_messages, subquery_temperature, subquery_top_p, subquery_functions)) = + &self.subquery_data + { + return Ok(crate::client::ChatCompletionsData { + messages: subquery_messages.clone(), + temperature: *subquery_temperature, + top_p: *subquery_top_p, + functions: subquery_functions.clone(), + stream, + }); + } let mut messages = self.build_messages()?; patch_messages(&mut messages, model); model.guard_max_input_tokens(&messages)?; diff --git a/src/config/mod.rs b/src/config/mod.rs index 10718df..693c9f5 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -2,6 +2,7 @@ mod agent; mod input; mod role; mod session; +mod subquery; pub use self::agent::{complete_agent_variables, list_agents, Agent, AgentVariables}; pub use self::input::Input; @@ -9,6 +10,7 @@ pub use self::role::{ Role, RoleLike, CODE_ROLE, CREATE_TITLE_ROLE, EXPLAIN_SHELL_ROLE, SHELL_ROLE, }; use self::session::Session; +pub use self::subquery::{eval_subquery, SubqueryContext}; use crate::client::{ create_client_config, list_client_types, list_models, ClientConfig, MessageContentToolCalls, @@ -117,6 +119,9 @@ pub struct Config { pub mapping_tools: IndexMap<String, String>, pub use_tools: Option<String>, + pub subquery: bool, + pub subquery_max_depth: usize, + pub repl_prelude: Option<String>, pub cmd_prelude: Option<String>, pub agent_prelude: Option<String>, @@ -193,6 +198,9 @@ impl Default for Config { mapping_tools: Default::default(), use_tools: None, + subquery: false, + subquery_max_depth: 3, + repl_prelude: None, cmd_prelude: None, agent_prelude: None, @@ -599,6 +607,8 @@ impl Config { ("rag_top_k", rag_top_k.to_string()), ("dry_run", self.dry_run.to_string()), ("function_calling", self.function_calling.to_string()), + ("subquery", self.subquery.to_string()), + ("subquery_max_depth", self.subquery_max_depth.to_string()), ("stream", self.stream.to_string()), ("save", self.save.to_string()), ("keybindings", self.keybindings.clone()), @@ -677,6 +687,14 @@ impl Config { } config.write().function_calling = value; } + "subquery" => { + let value = value.parse().with_context(|| "Invalid value")?; + config.write().subquery = value; + } + "subquery_max_depth" => { + let value = value.parse().with_context(|| "Invalid value")?; + config.write().subquery_max_depth = value; + } "stream" => { let value = value.parse().with_context(|| "Invalid value")?; config.write().stream = value; @@ -1710,6 +1728,10 @@ impl Config { ); functions = agent_functions; } + + if self.subquery { + functions.push(self::subquery::subquery_declaration()); + } }; if functions.is_empty() { None diff --git a/src/config/subquery.rs b/src/config/subquery.rs new file mode 100644 index 0000000..6811791 --- /dev/null +++ b/src/config/subquery.rs @@ -0,0 +1,360 @@ +use super::{GlobalConfig, Input, RoleLike}; + +use crate::client::{call_chat_completions, call_chat_completions_streaming, Message, MessageRole}; +use crate::function::ToolResult; +use crate::utils::{ + create_abort_signal, format_query_path, indent_text, next_query_id, with_query_scope, + AbortSignal, +}; + +use anyhow::{anyhow, bail, Result}; +use serde_json::{json, Value}; + +pub const SUBQUERY_TOOL_NAME: &str = "subquery"; + +pub fn subquery_declaration() -> crate::function::FunctionDeclaration { + crate::function::FunctionDeclaration { + name: SUBQUERY_TOOL_NAME.to_string(), + description: concat!( + "Delegate a fully self-contained sub task to a fresh LLM query that does NOT ", + "inherit the current conversation context. Only the context explicitly passed ", + "through the parameters (by message indexes and/or text excerpts) is visible to ", + "the subquery. The subquery itself can still use tools (including nested ", + "subqueries when the depth limit allows), and its final answer is returned to you." + ) + .to_string(), + parameters: crate::function::JsonSchema { + type_value: Some("object".to_string()), + description: None, + properties: Some( + [ + ( + "task", + crate::function::JsonSchema { + type_value: Some("string".to_string()), + description: Some( + "The task or question for the subquery. Must be fully self-contained; do not rely on the surrounding conversation." + .to_string(), + ), + properties: None, + items: None, + any_of: None, + enum_value: None, + default: None, + required: None, + }, + ), + ( + "message_indexes", + crate::function::JsonSchema { + type_value: Some("array".to_string()), + description: Some( + "0-based indexes of the current context window messages to pass to the subquery as context. Messages are numbered in the order they appear in the context window, starting at 0." + .to_string(), + ), + properties: None, + items: Some(Box::new(crate::function::JsonSchema { + type_value: Some("integer".to_string()), + description: None, + properties: None, + items: None, + any_of: None, + enum_value: None, + default: None, + required: None, + })), + any_of: None, + enum_value: None, + default: None, + required: None, + }, + ), + ( + "excerpts", + crate::function::JsonSchema { + type_value: Some("array".to_string()), + description: Some( + "Verbatim text excerpts copied from the current context window, provided to the subquery as extra context." + .to_string(), + ), + properties: None, + items: Some(Box::new(crate::function::JsonSchema { + type_value: Some("string".to_string()), + description: None, + properties: None, + items: None, + any_of: None, + enum_value: None, + default: None, + required: None, + })), + any_of: None, + enum_value: None, + default: None, + required: None, + }, + ), + ] + .into_iter() + .map(|(k, v)| (k.to_string(), v)) + .collect(), + ), + items: None, + any_of: None, + enum_value: None, + default: None, + required: Some(vec!["task".to_string()]), + }, + agent: false, + } +} + +pub struct SubqueryContext { + path: Vec<u64>, + context_messages: Vec<Message>, +} + +impl SubqueryContext { + pub fn from_input(input: &Input) -> Self { + let path = input.query_path(); + let context_messages = if path.is_empty() { + vec![] + } else { + match input.build_messages() { + Ok(v) => v, + Err(err) => { + debug!("Failed to build context messages for subquery: {err}"); + vec![] + } + } + }; + Self { + path, + context_messages, + } + } +} + +pub async fn eval_subquery( + config: &GlobalConfig, + ctx: &SubqueryContext, + arguments: Value, +) -> Result<Value> { + let parent_path = ctx.path.clone(); + if parent_path.is_empty() { + bail!("Invalid subquery call: missing parent query id"); + } + let max_depth = config.read().subquery_max_depth; + if parent_path.len() >= max_depth { + bail!("Subquery maximum depth ({max_depth}) exceeded"); + } + let (task, indexes, excerpts) = parse_subquery_args(&arguments)?; + let abort_signal = create_abort_signal(); + let sub_id = next_query_id(&parent_path); + let mut sub_path = parent_path; + sub_path.push(sub_id); + let result = with_query_scope(format_query_path(&sub_path), async { + run_subquery_inner(config, ctx, task, indexes, excerpts, sub_path, abort_signal).await + }) + .await; + match result { + Ok(text) => Ok(json!({ "output": text })), + Err(err) => Ok(json!({ "error": format!("{err:?}") })), + } +} + +async fn run_subquery_inner( + config: &GlobalConfig, + ctx: &SubqueryContext, + task: String, + indexes: Vec<usize>, + excerpts: Vec<String>, + sub_path: Vec<u64>, + abort_signal: AbortSignal, +) -> Result<String> { + let mut context_parts = vec![]; + if !indexes.is_empty() { + context_parts.push(format!( + "The following messages were numerically selected from the parent context window:\n\n{}", + numbered_context_text(&ctx.context_messages, &indexes) + )); + } + if !excerpts.is_empty() { + context_parts.push(format!( + "The following text excerpts were selected from the parent context window:\n\n{}", + excerpts.join("\n\n") + )); + } + let mut messages = vec![]; + if !context_parts.is_empty() { + messages.push(Message::new( + MessageRole::System, + crate::client::MessageContent::Text(context_parts.join("\n\n")), + )); + } + messages.push(Message::new( + MessageRole::User, + crate::client::MessageContent::Text(task), + )); + run_subquery(config, messages, sub_path, abort_signal).await +} + +fn parse_subquery_args(arguments: &Value) -> Result<(String, Vec<usize>, Vec<String>)> { + let arguments = match arguments { + Value::Object(map) => Value::Object(map.clone()), + Value::String(text) => serde_json::from_str(text) + .map_err(|_| anyhow!("Invalid arguments for '{SUBQUERY_TOOL_NAME}': {text}"))?, + _ => bail!("Invalid arguments for '{SUBQUERY_TOOL_NAME}': {arguments}"), + }; + let task = arguments["task"] + .as_str() + .map(|v| v.trim().to_string()) + .unwrap_or_default(); + if task.is_empty() { + bail!("The call '{SUBQUERY_TOOL_NAME}' requires a non-empty 'task' argument"); + } + let indexes = match &arguments["message_indexes"] { + Value::Array(items) => items + .iter() + .filter_map(|v| match v { + Value::String(s) => s.trim().parse::<usize>().ok(), + _ => v.as_u64().map(|v| v as usize), + }) + .collect(), + Value::Null => vec![], + other => { + bail!("The call '{SUBQUERY_TOOL_NAME}' has invalid 'message_indexes': {other}") + } + }; + let excerpts = match &arguments["excerpts"] { + Value::Array(items) => items + .iter() + .filter_map(|v| v.as_str().map(|v| v.to_string())) + .collect(), + Value::String(v) => vec![v.clone()], + Value::Null => vec![], + other => bail!("The call '{SUBQUERY_TOOL_NAME}' has invalid 'excerpts': {other}"), + }; + Ok((task, indexes, excerpts)) +} + +fn numbered_context_text(messages: &[Message], indexes: &[usize]) -> String { + let mut lines = vec![]; + for (i, message) in messages.iter().enumerate() { + if !indexes.contains(&i) { + continue; + } + lines.push(format!( + "# {i} {}:\n{}", + format!("{:?}", message.role).to_lowercase(), + indent_text(message.content.to_text(), 4).trim_start() + )); + } + if lines.is_empty() { + "(no matching messages found)".into() + } else { + lines.join("\n\n") + } +} + +async fn run_subquery( + config: &GlobalConfig, + mut messages: Vec<Message>, + path: Vec<u64>, + abort_signal: AbortSignal, +) -> Result<String> { + let client = crate::client::init_client(config, None)?; + let (temperature, top_p, functions, stream) = { + let temp_input = Input::from_str(config, "", None); + let (temperature, top_p) = (temp_input.role().temperature(), temp_input.role().top_p()); + let max_depth = config.read().subquery_max_depth; + let functions = + config + .read() + .select_functions(temp_input.role()) + .map(|mut declarations| { + if path.len() < max_depth { + declarations.push(subquery_declaration()); + } + declarations + }); + ( + temperature, + top_p, + functions, + config.read().stream && !temp_input.role().model().no_stream(), + ) + }; + let output; + let mut steps = 0; + loop { + steps += 1; + if steps > 8 { + bail!("Subquery exceeded the maximum number of tool-call steps"); + } + let input = build_subquery_input( + config, + messages.clone(), + temperature, + top_p, + functions.clone(), + path.clone(), + )?; + let (text, tool_results) = if stream { + call_chat_completions_streaming(&input, client.as_ref(), abort_signal.clone()).await? + } else { + call_chat_completions(&input, false, false, client.as_ref(), abort_signal.clone()) + .await? + }; + if tool_results.is_empty() { + output = text; + break; + } + messages.extend(tool_result_messages(&text, &tool_results)); + } + Ok(output) +} + +fn build_subquery_input( + config: &GlobalConfig, + messages: Vec<Message>, + temperature: Option<f64>, + top_p: Option<f64>, + functions: Option<Vec<crate::function::FunctionDeclaration>>, + path: Vec<u64>, +) -> Result<Input> { + let mut input = Input::from_str(config, "", None); + input.set_subquery(messages, temperature, top_p, functions); + input.set_query_path(path); + Ok(input) +} + +fn tool_result_messages(text: &str, tool_results: &[ToolResult]) -> Vec<Message> { + let mut messages = vec![]; + let text = text.trim(); + if !text.is_empty() { + messages.push(Message::new( + MessageRole::Assistant, + crate::client::MessageContent::Text(text.to_string()), + )); + } + let results: Vec<ToolResult> = tool_results + .iter() + .cloned() + .enumerate() + .map(|(i, mut result)| { + if result.call.id.is_none() { + result.call.id = Some(format!("subquery-call-{}", i + 1)); + } + result + }) + .collect(); + messages.push(Message::new( + MessageRole::Assistant, + crate::client::MessageContent::ToolCalls(crate::client::MessageContentToolCalls::new( + results, + String::new(), + )), + )); + messages +} diff --git a/src/function.rs b/src/function.rs index 5907b71..778330a 100644 --- a/src/function.rs +++ b/src/function.rs @@ -1,5 +1,5 @@ use crate::{ - config::{Agent, Config, GlobalConfig}, + config::{eval_subquery, Agent, Config, GlobalConfig, SubqueryContext}, utils::*, }; @@ -18,7 +18,15 @@ const PATH_SEP: &str = ";"; #[cfg(not(windows))] const PATH_SEP: &str = ":"; -pub fn eval_tool_calls(config: &GlobalConfig, mut calls: Vec<ToolCall>) -> Result<Vec<ToolResult>> { +pub struct EvalToolCallsContext { + pub subquery_ctx: Option<SubqueryContext>, +} + +pub async fn eval_tool_calls( + config: &GlobalConfig, + mut calls: Vec<ToolCall>, + ctx: &mut EvalToolCallsContext, +) -> Result<Vec<ToolResult>> { let mut output = vec![]; if calls.is_empty() { return Ok(output); @@ -29,7 +37,7 @@ pub fn eval_tool_calls(config: &GlobalConfig, mut calls: Vec<ToolCall>) -> Resul } let mut is_all_null = true; for call in calls { - let mut result = call.eval(config)?; + let mut result = call.eval(config, ctx).await?; if result.is_null() { result = json!("DONE"); } else { @@ -170,7 +178,14 @@ impl ToolCall { } } - pub fn eval(&self, config: &GlobalConfig) -> Result<Value> { + #[async_recursion::async_recursion] + pub async fn eval(&self, config: &GlobalConfig, ctx: &EvalToolCallsContext) -> Result<Value> { + if self.name == SUBQUERY_TOOL_NAME { + return match &ctx.subquery_ctx { + Some(sub_ctx) => eval_subquery(config, sub_ctx, self.arguments.clone()).await, + None => bail!("Invalid subquery call: missing parent query context"), + }; + } let (call_name, cmd_name, mut cmd_args, envs) = match &config.read().agent { Some(agent) => self.extract_call_config_from_agent(config, agent)?, None => self.extract_call_config_from_config(config)?, @@ -245,6 +260,8 @@ impl ToolCall { } } +const SUBQUERY_TOOL_NAME: &str = "subquery"; + pub fn run_llm_function( cmd_name: String, cmd_args: Vec<String>, @@ -274,6 +291,10 @@ pub fn run_llm_function( #[cfg(windows)] let cmd_name = polyfill_cmd_name(&cmd_name, &bin_dirs); if *IS_STDOUT_TERMINAL { + let prompt = match current_query_scope() { + Some(scope) => format!("{scope} {prompt}"), + None => prompt, + }; println!("{}", dimmed_text(&prompt)); } let exit_code = run_command(&cmd_name, &cmd_args, Some(envs)) diff --git a/src/main.rs b/src/main.rs index 369a282..b30d022 100644 --- a/src/main.rs +++ b/src/main.rs @@ -195,10 +195,12 @@ async fn run(config: GlobalConfig, cli: Cli, text: Option<String>) -> Result<()> #[async_recursion::async_recursion] async fn start_directive( config: &GlobalConfig, - input: Input, + mut input: Input, code_mode: bool, abort_signal: AbortSignal, ) -> Result<()> { + let root_id = crate::utils::next_query_id(&[]); + input.set_query_path(vec![root_id]); let client = input.create_client()?; let extract_code = !*IS_STDOUT_TERMINAL && code_mode; config.write().before_chat_completion(&input)?; diff --git a/src/render/stream.rs b/src/render/stream.rs index d6cadde..af69176 100644 --- a/src/render/stream.rs +++ b/src/render/stream.rs @@ -36,7 +36,10 @@ pub async fn raw_stream( mut rx: UnboundedReceiver<SseEvent>, abort_signal: &AbortSignal, ) -> Result<()> { - let mut spinner = Some(spawn_spinner("Generating")); + let mut spinner = Some(spawn_spinner(&match crate::utils::current_query_scope() { + Some(scope) => format!("{scope} Generating"), + None => "Generating".to_string(), + })); loop { if abort_signal.aborted() { @@ -75,7 +78,10 @@ async fn markdown_stream_inner( let columns = terminal::size()?.0; - let mut spinner = Some(spawn_spinner("Generating")); + let mut spinner = Some(spawn_spinner(&match crate::utils::current_query_scope() { + Some(scope) => format!("{scope} Generating"), + None => "Generating".to_string(), + })); 'outer: loop { if abort_signal.aborted() { diff --git a/src/repl/mod.rs b/src/repl/mod.rs index f8a51d1..85f4f9c 100644 --- a/src/repl/mod.rs +++ b/src/repl/mod.rs @@ -723,6 +723,10 @@ async fn ask( mut input: Input, with_embeddings: bool, ) -> Result<()> { + if input.query_path().is_empty() { + let root_id = crate::utils::next_query_id(&[]); + input.set_query_path(vec![root_id]); + } if input.is_empty() { return Ok(()); } diff --git a/src/utils/abort_signal.rs b/src/utils/abort_signal.rs index a13e5bb..47534ae 100644 --- a/src/utils/abort_signal.rs +++ b/src/utils/abort_signal.rs @@ -2,7 +2,7 @@ use anyhow::Result; use crossterm::event::{self, Event, KeyCode, KeyModifiers}; use std::{ sync::{ - atomic::{AtomicBool, Ordering}, + atomic::{AtomicBool, AtomicU64, Ordering}, Arc, }, time::Duration, @@ -10,6 +10,72 @@ use std::{ pub type AbortSignal = Arc<AbortSignalInner>; +#[derive(Debug, Default)] +pub struct QueryIdGen { + counter: AtomicU64, + parents: parking_lot::RwLock<std::collections::HashMap<String, u64>>, +} + +impl QueryIdGen { + pub fn root_id(&self) -> u64 { + self.counter.fetch_add(1, Ordering::SeqCst) + } + + pub fn child_id(&self, parent_path: &[u64]) -> u64 { + if parent_path.is_empty() { + return self.root_id(); + } + let key = parent_path + .iter() + .map(|v| v.to_string()) + .collect::<Vec<_>>() + .join("/"); + let mut parents = self.parents.write(); + let entry = parents.entry(key).or_insert(0); + let id = *entry; + *entry += 1; + id + } +} + +pub fn format_query_path(path: &[u64]) -> String { + if path.is_empty() { + return String::new(); + } + format!( + "[{}]", + path.iter() + .map(|v| v.to_string()) + .collect::<Vec<_>>() + .join("/") + ) +} + +tokio::task_local! { + static QUERY_SCOPE: String; + static QUERY_ID_GEN: QueryIdGen; +} + +pub fn current_query_scope() -> Option<String> { + QUERY_SCOPE + .try_with(|v| if v.is_empty() { None } else { Some(v.clone()) }) + .ok() + .flatten() +} + +pub async fn with_query_scope<F: std::future::Future>(scope: String, future: F) -> F::Output { + QUERY_SCOPE.scope(scope, future).await +} + +pub fn next_query_id(parent_path: &[u64]) -> u64 { + if parent_path.is_empty() { + return QUERY_ID_GEN.try_with(|g| g.root_id()).unwrap_or(0); + } + QUERY_ID_GEN + .try_with(|g| g.child_id(parent_path)) + .unwrap_or(0) +} + pub struct AbortSignalInner { ctrlc: AtomicBool, ctrld: AtomicBool, |
