Publish harness and TUI open-source
initial sync from the monorepo
This commit is contained in:
commit
c68e39f604
2734 changed files with 1437016 additions and 0 deletions
6
crates/codegen/xai-grok-shell/src/tools/bridge.rs
Normal file
6
crates/codegen/xai-grok-shell/src/tools/bridge.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
//! ToolBridge: re-exported from `xai-grok-tools`.
|
||||
//!
|
||||
//! The bridge implementation now lives in `xai_grok_tools::bridge`.
|
||||
//! This module re-exports everything for backward compatibility.
|
||||
|
||||
pub use xai_grok_tools::bridge::{ToolBridge, ToolBridgeResult};
|
||||
732
crates/codegen/xai-grok-shell/src/tools/config.rs
Normal file
732
crates/codegen/xai-grok-shell/src/tools/config.rs
Normal file
|
|
@ -0,0 +1,732 @@
|
|||
use crate::models;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use xai_grok_sampler::SamplerConfig;
|
||||
use xai_grok_tools::implementations::grok_build;
|
||||
use xai_grok_tools::registry::types::ToolConfig;
|
||||
|
||||
/// Production grok-build foreground command-timeout ceiling (seconds). The
|
||||
/// tool-server binary defaults to a 5-minute foreground ceiling
|
||||
/// (`DEFAULT_MAX_TIMEOUT_MS`); production opts *up* to 10h by sending this
|
||||
/// explicitly (overridable via config.toml). Bounds only foreground commands —
|
||||
/// background tasks are always unbounded.
|
||||
pub const PRODUCTION_MAX_TIMEOUT_SECS: f64 = 36_000.0; // 10 hours
|
||||
|
||||
/// User configurable settings for the built-in bash tool (`[toolset.bash]`).
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct BashToolConfig {
|
||||
pub timeout_secs: Option<f64>,
|
||||
/// Foreground ceiling for model-provided command timeouts (seconds). When
|
||||
/// `None`, `to_bash_params_json` emits the production default
|
||||
/// ([`PRODUCTION_MAX_TIMEOUT_SECS`], 10h), opting up from the tool-server
|
||||
/// binary's 5-minute built-in default. Set it lower to cap foreground
|
||||
/// commands further. Bounds foreground only; background tasks are always
|
||||
/// unbounded. Tools run in-process so this is always honored — no server
|
||||
/// version gate needed.
|
||||
pub max_timeout_secs: Option<f64>,
|
||||
pub output_byte_limit: Option<usize>,
|
||||
pub cmd_prefix: Option<String>,
|
||||
/// Whether to auto-background a command when it times out (default: `true`).
|
||||
pub auto_background_on_timeout: Option<bool>,
|
||||
/// Max FG block before auto-bg when `auto_background_on_timeout` is on
|
||||
/// (milliseconds). `None` → server default 15s; `Some(0)` → no short budget
|
||||
/// (auto-bg only at model/default timeout).
|
||||
pub foreground_block_budget_ms: Option<u64>,
|
||||
/// Whether to allow a background `&` operator in foreground commands
|
||||
/// (default: `true`). Resolution: config.toml (this) > remote settings > `true`.
|
||||
pub allow_background_operator: Option<bool>,
|
||||
/// Declared so the unknown-key scan accepts `[toolset.bash] persistent_shell`;
|
||||
/// the effective value is resolved (layered) by `resolve_persistent_local_shell`.
|
||||
pub persistent_shell: Option<bool>,
|
||||
}
|
||||
|
||||
impl BashToolConfig {
|
||||
/// Build the JSON params map for the bash tool.
|
||||
///
|
||||
/// `remote_auto_bg` is the remote settings fallback for `auto_background_on_timeout`
|
||||
/// and `remote_allow_background_operator` for `allow_background_operator`.
|
||||
/// Resolution: local config.toml > remote fallback > `true`.
|
||||
pub fn to_bash_params_json(
|
||||
&self,
|
||||
remote_auto_bg: Option<bool>,
|
||||
remote_allow_background_operator: Option<bool>,
|
||||
) -> serde_json::Map<String, serde_json::Value> {
|
||||
let mut map = serde_json::Map::new();
|
||||
if let Some(t) = self.timeout_secs {
|
||||
map.insert("timeout_secs".into(), t.into());
|
||||
}
|
||||
// The tool-server binary defaults the foreground ceiling to 5 min;
|
||||
// production grok-build opts up to 10h by sending it explicitly
|
||||
// (overridable via config.toml). Foreground-only; background stays unbounded.
|
||||
let max_timeout_secs = self.max_timeout_secs.unwrap_or(PRODUCTION_MAX_TIMEOUT_SECS);
|
||||
map.insert("max_timeout_secs".into(), max_timeout_secs.into());
|
||||
if let Some(limit) = self.output_byte_limit {
|
||||
map.insert("output_byte_limit".into(), limit.into());
|
||||
}
|
||||
if let Some(ref p) = self.cmd_prefix {
|
||||
map.insert("cmd_prefix".into(), p.clone().into());
|
||||
}
|
||||
let auto_bg = self
|
||||
.auto_background_on_timeout
|
||||
.or(remote_auto_bg)
|
||||
.unwrap_or(true);
|
||||
map.insert("auto_background_on_timeout".into(), auto_bg.into());
|
||||
if let Some(ms) = self.foreground_block_budget_ms {
|
||||
map.insert("foreground_block_budget_ms".into(), ms.into());
|
||||
}
|
||||
let allow_bg_op = self
|
||||
.allow_background_operator
|
||||
.or(remote_allow_background_operator)
|
||||
.unwrap_or(true);
|
||||
map.insert("allow_background_operator".into(), allow_bg_op.into());
|
||||
map
|
||||
}
|
||||
}
|
||||
|
||||
/// User configurable settings for the ask_user_question tool
|
||||
/// (`[toolset.ask_user_question]`).
|
||||
///
|
||||
/// Consumed out-of-band by
|
||||
/// `crate::util::config::resolve_ask_user_question_params_from_disk`, which
|
||||
/// reads the raw config layers so the documented precedence (requirements >
|
||||
/// env > user > managed > remote) holds — this struct exists so the keys are
|
||||
/// recognized in `config.toml` and round-trip through `AgentConfig`.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct AskUserQuestionToolConfig {
|
||||
/// Whether the questionnaire timeout is armed (default: `true`).
|
||||
/// `false` waits forever for answers.
|
||||
pub timeout_enabled: Option<bool>,
|
||||
/// Wait budget in seconds when the timer is armed (positive integer;
|
||||
/// default: 1800 / 30 minutes).
|
||||
pub timeout_secs: Option<u64>,
|
||||
}
|
||||
|
||||
/// User configurable settings for the web_fetch tool (`[toolset.web_fetch]`).
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct WebFetchToolConfig {
|
||||
/// Egress proxy endpoint. When set, all HTTP requests are routed through
|
||||
/// this URL. Resolution: TOML > `GROK_WEB_FETCH_PROXY` env > remote settings > None.
|
||||
pub proxy_endpoint: Option<String>,
|
||||
/// Domains the tool is allowed to fetch. When set, overrides the built-in
|
||||
/// default allowlist. An explicit empty list blocks all fetches.
|
||||
/// Resolution: TOML > remote settings > built-in defaults.
|
||||
pub allowed_domains: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl WebFetchToolConfig {
|
||||
/// Resolve `WebFetchParams` by merging TOML > env > remote settings layers.
|
||||
///
|
||||
/// `remote_proxy` and `remote_domains` are the remote settings fallback values
|
||||
/// from `RemoteSettings`. `context_window` comes from the session's
|
||||
/// SamplingConfig (model-provided).
|
||||
pub fn resolve_params(
|
||||
&self,
|
||||
remote_proxy: Option<&str>,
|
||||
remote_domains: Option<&[String]>,
|
||||
context_window_tokens: Option<u64>,
|
||||
) -> xai_grok_tools::implementations::grok_build::web_fetch::WebFetchParams {
|
||||
use crate::agent::config::env_string;
|
||||
|
||||
let proxy_endpoint = self
|
||||
.proxy_endpoint
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.or_else(|| env_string("GROK_WEB_FETCH_PROXY"))
|
||||
.or_else(|| remote_proxy.map(|s| s.to_owned()));
|
||||
|
||||
let allowed_domains = self
|
||||
.allowed_domains
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.or_else(|| remote_domains.map(|d| d.to_vec()));
|
||||
|
||||
xai_grok_tools::implementations::grok_build::web_fetch::WebFetchParams {
|
||||
proxy_endpoint,
|
||||
allowed_domains,
|
||||
context_window_tokens,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Top-level toolset configuration for the shell layer.
|
||||
///
|
||||
/// This is the *shell-side* config that holds sampling-level settings
|
||||
/// (e.g., web search API key from the sampling client). It is distinct
|
||||
/// from `xai_grok_tools::registry::types::ToolsetConfig` which holds
|
||||
/// tool-implementation-level config (bash limits, web search mode).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct ShellToolsetConfig {
|
||||
pub bash: BashToolConfig,
|
||||
pub web_search: SamplerConfig,
|
||||
/// Web fetch tool parameters (`[toolset.web_fetch]`).
|
||||
#[serde(default)]
|
||||
pub web_fetch: WebFetchToolConfig,
|
||||
/// Ask-user-question tool parameters (`[toolset.ask_user_question]`).
|
||||
#[serde(default)]
|
||||
pub ask_user_question: AskUserQuestionToolConfig,
|
||||
/// Which file-operation toolset to use: `"standard"` (default) or `"hashline"`.
|
||||
#[serde(default)]
|
||||
pub file_toolset: FileToolset,
|
||||
/// Hashline scheme parameters. Only used when `file_toolset = "hashline"`.
|
||||
#[serde(default)]
|
||||
pub hashline: HashlineSchemeConfig,
|
||||
}
|
||||
|
||||
impl Default for ShellToolsetConfig {
|
||||
fn default() -> Self {
|
||||
Self::new(None, None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Web-search-specific sampling overrides applied on top of a base `SamplerConfig`.
|
||||
pub(crate) fn web_search_sampling_config(base: SamplerConfig) -> SamplerConfig {
|
||||
let model = if base.model.is_empty() {
|
||||
models::default_web_search_model().to_string()
|
||||
} else {
|
||||
base.model.clone()
|
||||
};
|
||||
SamplerConfig {
|
||||
model,
|
||||
max_completion_tokens: Some(8192),
|
||||
temperature: Some(0.1),
|
||||
top_p: Some(0.95),
|
||||
force_http1: false,
|
||||
max_retries: None,
|
||||
..base
|
||||
}
|
||||
}
|
||||
|
||||
impl ShellToolsetConfig {
|
||||
/// Optionally layers sampling credentials onto the web search config.
|
||||
pub fn new(base: Option<Self>, sampling_config: Option<SamplerConfig>) -> Self {
|
||||
let default_base = SamplerConfig {
|
||||
api_key: None,
|
||||
base_url: "https://api.x.ai/v1".to_string(),
|
||||
model: String::new(),
|
||||
max_completion_tokens: None,
|
||||
temperature: None,
|
||||
top_p: None,
|
||||
api_backend: Default::default(),
|
||||
auth_scheme: Default::default(),
|
||||
extra_headers: indexmap::IndexMap::new(),
|
||||
context_window: 256_000,
|
||||
client_version: None,
|
||||
reasoning_effort: None,
|
||||
force_http1: false,
|
||||
max_retries: None,
|
||||
stream_tool_calls: false,
|
||||
idle_timeout_secs: None,
|
||||
client_identifier: None,
|
||||
deployment_id: None,
|
||||
user_id: None,
|
||||
origin_client: None,
|
||||
// Default base for the in-process web-search tool config.
|
||||
// Real `SamplerConfig`s (e.g. from `sampling_config_for_model`)
|
||||
// overwrite this entire struct via the `..base` pattern in
|
||||
// `web_search_sampling_config`, so leaving the callback
|
||||
// `None` here is fine -- it is only the placeholder for the
|
||||
// "no base provided" path. The live attribution
|
||||
// wiring lives at the production SamplerConfig sites in
|
||||
// agent/config.rs and acp_session.rs.
|
||||
attribution_callback: None,
|
||||
bearer_resolver: None,
|
||||
supports_backend_search: false,
|
||||
compactions_remaining: None,
|
||||
compaction_at_tokens: None,
|
||||
doom_loop_recovery: None,
|
||||
header_injector: None,
|
||||
};
|
||||
let mut toolset = base.unwrap_or_else(|| Self {
|
||||
bash: BashToolConfig::default(),
|
||||
web_search: web_search_sampling_config(default_base),
|
||||
web_fetch: WebFetchToolConfig::default(),
|
||||
ask_user_question: AskUserQuestionToolConfig::default(),
|
||||
file_toolset: FileToolset::default(),
|
||||
hashline: HashlineSchemeConfig::default(),
|
||||
});
|
||||
if let Some(sc) = sampling_config {
|
||||
toolset.web_search = web_search_sampling_config(sc);
|
||||
}
|
||||
toolset
|
||||
}
|
||||
|
||||
/// Returns true if web search is enabled based on config.
|
||||
pub fn web_search_enabled(&self) -> bool {
|
||||
self.web_search.api_key.is_some()
|
||||
}
|
||||
|
||||
/// Resolve the effective file toolset. Local config takes precedence;
|
||||
/// remote `/v1/settings` is used as fallback when local is the default.
|
||||
pub fn resolve_file_toolset(
|
||||
&self,
|
||||
remote: Option<&crate::util::config::RemoteSettings>,
|
||||
) -> FileToolset {
|
||||
if self.file_toolset != FileToolset::Standard {
|
||||
return self.file_toolset;
|
||||
}
|
||||
if let Some(remote) = remote
|
||||
&& remote.file_toolset.as_deref() == Some("hashline")
|
||||
{
|
||||
return FileToolset::Hashline;
|
||||
}
|
||||
self.file_toolset
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// File toolset selection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Configuration for hashline anchor scheme parameters.
|
||||
///
|
||||
/// Configurable in `config.toml` under `[toolset.hashline]`:
|
||||
/// ```toml
|
||||
/// [toolset]
|
||||
/// file_toolset = "hashline"
|
||||
///
|
||||
/// [toolset.hashline]
|
||||
/// scheme = "chunk" # "chunk" (default) or "content_only"
|
||||
/// hash_len = 3 # anchor hash length (1-4, default 3)
|
||||
/// chunk_size = 8 # chunk size for chunk scheme (default 8)
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct HashlineSchemeConfig {
|
||||
/// Active scheme: `"chunk"` (default) or `"content_only"`.
|
||||
pub scheme: String,
|
||||
/// Anchor hash length in characters (1-4).
|
||||
pub hash_len: usize,
|
||||
/// Chunk size for the chunk scheme.
|
||||
pub chunk_size: usize,
|
||||
}
|
||||
|
||||
impl Default for HashlineSchemeConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
scheme: "chunk".to_owned(),
|
||||
hash_len: 3,
|
||||
chunk_size: 8,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HashlineSchemeConfig {
|
||||
/// Validate the config. Returns an error message if invalid.
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
match self.scheme.as_str() {
|
||||
"chunk" | "content_only" => {}
|
||||
other => {
|
||||
return Err(format!(
|
||||
"unknown hashline scheme \"{other}\": expected \"chunk\" or \"content_only\""
|
||||
));
|
||||
}
|
||||
}
|
||||
if self.hash_len == 0 || self.hash_len > 4 {
|
||||
return Err(format!(
|
||||
"hashline hash_len must be 1..=4, got {}",
|
||||
self.hash_len
|
||||
));
|
||||
}
|
||||
if self.scheme == "chunk" && self.chunk_size == 0 {
|
||||
return Err("hashline chunk_size must be > 0".to_owned());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Which set of read/edit/search tools to use for file operations.
|
||||
///
|
||||
/// Selects between the standard `GrokBuild` toolset (`read_file`,
|
||||
/// `search_replace`, `grep`) and the anchor-based `GrokBuildHashline`
|
||||
/// toolset (`hashline_read`, `hashline_edit`, `hashline_grep`).
|
||||
/// The two are mutually exclusive.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum FileToolset {
|
||||
/// Standard toolset: read_file, search_replace, grep.
|
||||
#[default]
|
||||
Standard,
|
||||
/// Hashline toolset: hashline_read, hashline_edit, hashline_grep.
|
||||
Hashline,
|
||||
}
|
||||
|
||||
impl FileToolset {
|
||||
/// Return the `ToolConfig` entries for the read/edit/search tools
|
||||
/// belonging to this toolset. For hashline, the scheme params are
|
||||
/// threaded into each tool's params.
|
||||
/// Returns an error if hashline config is invalid (upfront validation).
|
||||
pub fn tool_configs(
|
||||
self,
|
||||
hashline_config: &HashlineSchemeConfig,
|
||||
) -> Result<Vec<ToolConfig>, String> {
|
||||
match self {
|
||||
Self::Standard => Ok(vec![
|
||||
ToolConfig::for_tool::<grok_build::ReadFileTool>(),
|
||||
ToolConfig::for_tool::<grok_build::SearchReplaceTool>(),
|
||||
ToolConfig::for_tool::<grok_build::GrepTool>(),
|
||||
]),
|
||||
Self::Hashline => {
|
||||
hashline_config.validate()?;
|
||||
let params_json = serde_json::json!({
|
||||
"scheme": hashline_config.scheme,
|
||||
"hash_len": hashline_config.hash_len,
|
||||
"chunk_size": hashline_config.chunk_size,
|
||||
});
|
||||
let params_map = match params_json {
|
||||
serde_json::Value::Object(m) => Some(m),
|
||||
_ => None,
|
||||
};
|
||||
Ok(vec![
|
||||
ToolConfig {
|
||||
id: "GrokBuildHashline:hashline_read".to_owned(),
|
||||
params: params_map.clone(),
|
||||
name_override: None,
|
||||
params_name_overrides: None,
|
||||
description_override: None,
|
||||
behavior_version: None,
|
||||
kind: None,
|
||||
},
|
||||
ToolConfig {
|
||||
id: "GrokBuildHashline:hashline_edit".to_owned(),
|
||||
params: params_map.clone(),
|
||||
name_override: None,
|
||||
params_name_overrides: None,
|
||||
description_override: None,
|
||||
behavior_version: None,
|
||||
kind: None,
|
||||
},
|
||||
ToolConfig {
|
||||
id: "GrokBuildHashline:hashline_grep".to_owned(),
|
||||
params: params_map,
|
||||
name_override: None,
|
||||
params_name_overrides: None,
|
||||
description_override: None,
|
||||
behavior_version: None,
|
||||
kind: None,
|
||||
},
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn file_toolset_default_is_standard() {
|
||||
assert_eq!(FileToolset::default(), FileToolset::Standard);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standard_toolset_configs() {
|
||||
let configs = FileToolset::Standard
|
||||
.tool_configs(&HashlineSchemeConfig::default())
|
||||
.unwrap();
|
||||
assert_eq!(configs.len(), 3);
|
||||
let ids: Vec<&str> = configs.iter().map(|c| c.id.as_str()).collect();
|
||||
assert!(ids.contains(&"GrokBuild:read_file"));
|
||||
assert!(ids.contains(&"GrokBuild:search_replace"));
|
||||
assert!(ids.contains(&"GrokBuild:grep"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hashline_toolset_configs() {
|
||||
let configs = FileToolset::Hashline
|
||||
.tool_configs(&HashlineSchemeConfig::default())
|
||||
.unwrap();
|
||||
assert_eq!(configs.len(), 3);
|
||||
let ids: Vec<&str> = configs.iter().map(|c| c.id.as_str()).collect();
|
||||
assert!(ids.contains(&"GrokBuildHashline:hashline_read"));
|
||||
assert!(ids.contains(&"GrokBuildHashline:hashline_edit"));
|
||||
assert!(ids.contains(&"GrokBuildHashline:hashline_grep"));
|
||||
}
|
||||
|
||||
/// Plan/explore omit `search_replace` by contract ("no Write/Edit/
|
||||
/// MultiEdit"); the hashline override must not hand it back as
|
||||
/// `hashline_edit`.
|
||||
#[test]
|
||||
fn file_toolset_override_never_grants_edit_to_read_only_toolsets() {
|
||||
let file_tools = FileToolset::Hashline
|
||||
.tool_configs(&HashlineSchemeConfig::default())
|
||||
.unwrap();
|
||||
|
||||
for mut def in [
|
||||
xai_grok_agent::config::AgentDefinition::plan(),
|
||||
xai_grok_agent::config::AgentDefinition::explore(),
|
||||
] {
|
||||
let name = def.name.clone();
|
||||
assert!(
|
||||
!def.tool_config
|
||||
.tools
|
||||
.iter()
|
||||
.any(|t| t.id == "GrokBuild:search_replace"),
|
||||
"{name}: fixture must be read-only before the override"
|
||||
);
|
||||
def.override_file_tools(file_tools.clone());
|
||||
let ids: Vec<&str> = def
|
||||
.tool_config
|
||||
.tools
|
||||
.iter()
|
||||
.map(|t| t.id.as_str())
|
||||
.collect();
|
||||
// The swap engages (read moves to hashline)...
|
||||
assert!(
|
||||
ids.contains(&"GrokBuildHashline:hashline_read"),
|
||||
"{name}: {ids:?}"
|
||||
);
|
||||
assert!(!ids.contains(&"GrokBuild:read_file"), "{name}: {ids:?}");
|
||||
// ...but never grants the edit slot.
|
||||
assert!(
|
||||
!ids.contains(&"GrokBuildHashline:hashline_edit"),
|
||||
"{name}: override granted an edit tool to a no-edit toolset: {ids:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hashline_configs_carry_scheme_params() {
|
||||
let cfg = HashlineSchemeConfig {
|
||||
scheme: "content_only".to_owned(),
|
||||
hash_len: 2,
|
||||
chunk_size: 16,
|
||||
};
|
||||
let configs = FileToolset::Hashline.tool_configs(&cfg).unwrap();
|
||||
for tc in &configs {
|
||||
let params = tc
|
||||
.params
|
||||
.as_ref()
|
||||
.expect("hashline tools should have params");
|
||||
assert_eq!(
|
||||
params.get("scheme").and_then(|v| v.as_str()),
|
||||
Some("content_only")
|
||||
);
|
||||
assert_eq!(params.get("hash_len").and_then(|v| v.as_u64()), Some(2));
|
||||
assert_eq!(params.get("chunk_size").and_then(|v| v.as_u64()), Some(16));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_toolset_deserializes_from_string() {
|
||||
let standard: FileToolset = serde_json::from_str("\"standard\"").unwrap();
|
||||
assert_eq!(standard, FileToolset::Standard);
|
||||
let hashline: FileToolset = serde_json::from_str("\"hashline\"").unwrap();
|
||||
assert_eq!(hashline, FileToolset::Hashline);
|
||||
}
|
||||
|
||||
// -- resolve_file_toolset precedence tests ---
|
||||
|
||||
#[test]
|
||||
fn resolve_local_hashline_wins_over_remote() {
|
||||
let cfg = ShellToolsetConfig {
|
||||
file_toolset: FileToolset::Hashline,
|
||||
..ShellToolsetConfig::default()
|
||||
};
|
||||
// Local says hashline — remote is irrelevant.
|
||||
assert_eq!(cfg.resolve_file_toolset(None), FileToolset::Hashline,);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_remote_used_when_local_default() {
|
||||
let cfg = ShellToolsetConfig::default(); // Standard (default)
|
||||
let remote = crate::util::config::RemoteSettings {
|
||||
file_toolset: Some("hashline".to_owned()),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(
|
||||
cfg.resolve_file_toolset(Some(&remote)),
|
||||
FileToolset::Hashline,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_default_when_no_remote() {
|
||||
let cfg = ShellToolsetConfig::default();
|
||||
assert_eq!(cfg.resolve_file_toolset(None), FileToolset::Standard,);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_hashline_config_rejected_upfront() {
|
||||
let bad = HashlineSchemeConfig {
|
||||
scheme: "bogus".to_owned(),
|
||||
hash_len: 3,
|
||||
chunk_size: 8,
|
||||
};
|
||||
let err = FileToolset::Hashline.tool_configs(&bad);
|
||||
assert!(err.is_err(), "unknown scheme should be rejected upfront");
|
||||
assert!(err.unwrap_err().contains("unknown"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_hash_len_rejected_upfront() {
|
||||
let bad = HashlineSchemeConfig {
|
||||
scheme: "chunk".to_owned(),
|
||||
hash_len: 0,
|
||||
chunk_size: 8,
|
||||
};
|
||||
assert!(FileToolset::Hashline.tool_configs(&bad).is_err());
|
||||
}
|
||||
|
||||
// -- resolve_params precedence tests ---
|
||||
|
||||
#[test]
|
||||
fn resolve_params_toml_wins_over_remote() {
|
||||
let local = WebFetchToolConfig {
|
||||
proxy_endpoint: Some("https://toml-proxy.example.com".to_owned()),
|
||||
allowed_domains: Some(vec!["toml.example.com".to_owned()]),
|
||||
};
|
||||
let params = local.resolve_params(
|
||||
Some("https://remote-proxy.example.com"),
|
||||
Some(&["remote.example.com".to_owned()]),
|
||||
None,
|
||||
);
|
||||
assert_eq!(
|
||||
params.proxy_endpoint.as_deref(),
|
||||
Some("https://toml-proxy.example.com")
|
||||
);
|
||||
assert_eq!(
|
||||
params.allowed_domains,
|
||||
Some(vec!["toml.example.com".to_owned()])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_params_remote_used_when_toml_absent() {
|
||||
let local = WebFetchToolConfig::default();
|
||||
let params = local.resolve_params(
|
||||
Some("https://remote-proxy.example.com"),
|
||||
Some(&["remote.example.com".to_owned()]),
|
||||
None,
|
||||
);
|
||||
assert_eq!(
|
||||
params.proxy_endpoint.as_deref(),
|
||||
Some("https://remote-proxy.example.com")
|
||||
);
|
||||
assert_eq!(
|
||||
params.allowed_domains,
|
||||
Some(vec!["remote.example.com".to_owned()])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_params_defaults_when_nothing_set() {
|
||||
let local = WebFetchToolConfig::default();
|
||||
let params = local.resolve_params(None, None, None);
|
||||
assert!(params.proxy_endpoint.is_none());
|
||||
assert!(params.allowed_domains.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_params_toml_empty_domains_blocks_all() {
|
||||
let local = WebFetchToolConfig {
|
||||
proxy_endpoint: None,
|
||||
allowed_domains: Some(vec![]),
|
||||
};
|
||||
let params = local.resolve_params(None, Some(&["remote.example.com".to_owned()]), None);
|
||||
assert_eq!(params.allowed_domains, Some(vec![]));
|
||||
}
|
||||
|
||||
// -- to_bash_params_json allow_background_operator precedence (local > remote > true) --
|
||||
|
||||
fn allow_bg_op(map: &serde_json::Map<String, serde_json::Value>) -> Option<bool> {
|
||||
map.get("allow_background_operator")
|
||||
.and_then(|v| v.as_bool())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allow_background_operator_local_wins_over_remote() {
|
||||
let local = BashToolConfig {
|
||||
allow_background_operator: Some(false),
|
||||
..BashToolConfig::default()
|
||||
};
|
||||
assert_eq!(
|
||||
allow_bg_op(&local.to_bash_params_json(None, Some(true))),
|
||||
Some(false)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allow_background_operator_remote_used_when_local_absent() {
|
||||
let local = BashToolConfig::default();
|
||||
assert_eq!(
|
||||
allow_bg_op(&local.to_bash_params_json(None, Some(false))),
|
||||
Some(false)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allow_background_operator_defaults_true_when_unset() {
|
||||
let local = BashToolConfig::default();
|
||||
assert_eq!(
|
||||
allow_bg_op(&local.to_bash_params_json(None, None)),
|
||||
Some(true)
|
||||
);
|
||||
}
|
||||
|
||||
// -- max_timeout_secs: production sets the 10h foreground ceiling explicitly
|
||||
// (also the binary default); overridable via config.toml --
|
||||
|
||||
fn max_timeout(map: &serde_json::Map<String, serde_json::Value>) -> Option<f64> {
|
||||
map.get("max_timeout_secs").and_then(|v| v.as_f64())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_timeout_secs_defaults_to_production_10h() {
|
||||
let local = BashToolConfig::default();
|
||||
assert_eq!(
|
||||
max_timeout(&local.to_bash_params_json(None, None)),
|
||||
Some(PRODUCTION_MAX_TIMEOUT_SECS),
|
||||
"production grok-build must set the 10h foreground ceiling"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_timeout_secs_local_override_wins() {
|
||||
let local = BashToolConfig {
|
||||
max_timeout_secs: Some(300.0),
|
||||
..BashToolConfig::default()
|
||||
};
|
||||
assert_eq!(
|
||||
max_timeout(&local.to_bash_params_json(None, None)),
|
||||
Some(300.0),
|
||||
);
|
||||
}
|
||||
|
||||
// -- foreground_block_budget_ms: only emitted when set (server defaults to 15s) --
|
||||
|
||||
fn fg_budget(map: &serde_json::Map<String, serde_json::Value>) -> Option<u64> {
|
||||
map.get("foreground_block_budget_ms")
|
||||
.and_then(|v| v.as_u64())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn foreground_block_budget_ms_omitted_by_default() {
|
||||
let local = BashToolConfig::default();
|
||||
assert!(
|
||||
fg_budget(&local.to_bash_params_json(None, None)).is_none(),
|
||||
"unset budget must not be sent (server keeps 15s default)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn foreground_block_budget_ms_local_override_emitted() {
|
||||
let local = BashToolConfig {
|
||||
foreground_block_budget_ms: Some(0),
|
||||
..BashToolConfig::default()
|
||||
};
|
||||
assert_eq!(fg_budget(&local.to_bash_params_json(None, None)), Some(0),);
|
||||
let local = BashToolConfig {
|
||||
foreground_block_budget_ms: Some(30_000),
|
||||
..BashToolConfig::default()
|
||||
};
|
||||
assert_eq!(
|
||||
fg_budget(&local.to_bash_params_json(None, None)),
|
||||
Some(30_000),
|
||||
);
|
||||
}
|
||||
}
|
||||
22
crates/codegen/xai-grok-shell/src/tools/mod.rs
Normal file
22
crates/codegen/xai-grok-shell/src/tools/mod.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
//! Tool infrastructure for xai-grok-shell.
|
||||
//!
|
||||
//! All tool execution goes through `xai-grok-tools` via the `ToolBridge`.
|
||||
//! Types (ToolOutput, ToolInput, TodoState, etc.) come from `xai-grok-tools` directly.
|
||||
|
||||
pub mod bridge;
|
||||
pub mod config;
|
||||
pub mod notification_bridge;
|
||||
pub mod retry;
|
||||
pub mod todo;
|
||||
pub mod tool_context;
|
||||
|
||||
pub use self::{
|
||||
config::{BashToolConfig, FileToolset, ShellToolsetConfig},
|
||||
retry::{RetryConfig, execute_with_retry},
|
||||
tool_context::ToolContext,
|
||||
};
|
||||
|
||||
// Re-export key types from xai-grok-tools for convenience
|
||||
pub use self::todo::{TodoId, TodoItem, TodoPriority, TodoStatus};
|
||||
pub use xai_grok_tools::types::output::ToolOutput;
|
||||
pub use xai_grok_tools::types::{MCPToolInput, ToolInput};
|
||||
2146
crates/codegen/xai-grok-shell/src/tools/notification_bridge.rs
Normal file
2146
crates/codegen/xai-grok-shell/src/tools/notification_bridge.rs
Normal file
File diff suppressed because it is too large
Load diff
26
crates/codegen/xai-grok-shell/src/tools/retry.rs
Normal file
26
crates/codegen/xai-grok-shell/src/tools/retry.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
//! Retry utilities — re-exported from `xai-grok-tools`.
|
||||
//!
|
||||
//! The canonical implementation now lives in `xai_grok_tools::retry`.
|
||||
//! This module re-exports with backward-compatible aliases.
|
||||
|
||||
pub use xai_grok_tools::retry::BackoffConfig as RetryConfig;
|
||||
pub use xai_grok_tools::retry::{BackoffConfig, execute_with_backoff};
|
||||
|
||||
use std::future::Future;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Backward-compatible wrapper around `execute_with_backoff` that uses
|
||||
/// `anyhow::Error` as the error type (matching the old signature).
|
||||
pub async fn execute_with_retry<T, E, EFut, R, RFut>(
|
||||
config: &RetryConfig,
|
||||
execute: E,
|
||||
on_retry: R,
|
||||
) -> Result<T, anyhow::Error>
|
||||
where
|
||||
E: FnMut() -> EFut,
|
||||
EFut: Future<Output = Result<T, anyhow::Error>>,
|
||||
R: FnMut(u32, u32, Duration) -> RFut,
|
||||
RFut: Future<Output = ()>,
|
||||
{
|
||||
execute_with_backoff(config, execute, on_retry).await
|
||||
}
|
||||
81
crates/codegen/xai-grok-shell/src/tools/todo.rs
Normal file
81
crates/codegen/xai-grok-shell/src/tools/todo.rs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
//! Todo types — re-exported from `xai-grok-tools` with ACP conversion helpers.
|
||||
//!
|
||||
//! Types are canonical in `xai-grok-tools`. This module adds ACP ↔ TodoItem
|
||||
//! conversions since `xai-grok-tools` is protocol-agnostic.
|
||||
|
||||
pub use xai_grok_tools::implementations::grok_build::todo::TodoId;
|
||||
pub use xai_grok_tools::implementations::grok_build::todo::TodoItem;
|
||||
pub use xai_grok_tools::implementations::grok_build::todo::TodoPriority;
|
||||
pub use xai_grok_tools::implementations::grok_build::todo::TodoState;
|
||||
pub use xai_grok_tools::implementations::grok_build::todo::TodoStatus;
|
||||
|
||||
use agent_client_protocol as acp;
|
||||
|
||||
/// Convert an ACP `PlanEntry` to a `TodoItem`.
|
||||
///
|
||||
/// Handles the cancelled state: ACP has no `Cancelled` status, so cancelled
|
||||
/// items are stored as `Completed` with `{"cancelled": true}` in meta.
|
||||
pub fn todo_item_from_plan_entry(entry: acp::PlanEntry) -> TodoItem {
|
||||
let status = match entry.status {
|
||||
acp::PlanEntryStatus::Pending => TodoStatus::Pending,
|
||||
acp::PlanEntryStatus::InProgress => TodoStatus::InProgress,
|
||||
acp::PlanEntryStatus::Completed => {
|
||||
// Check if this is actually a cancelled item
|
||||
if entry
|
||||
.meta
|
||||
.as_ref()
|
||||
.and_then(|m| m.get("cancelled"))
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
TodoStatus::Cancelled
|
||||
} else {
|
||||
TodoStatus::Completed
|
||||
}
|
||||
}
|
||||
// TODO(acp-0.10): `PlanEntryStatus` is #[non_exhaustive].
|
||||
_ => TodoStatus::Pending,
|
||||
};
|
||||
TodoItem {
|
||||
content: entry.content,
|
||||
priority: match entry.priority {
|
||||
acp::PlanEntryPriority::High => TodoPriority::High,
|
||||
acp::PlanEntryPriority::Medium => TodoPriority::Medium,
|
||||
acp::PlanEntryPriority::Low => TodoPriority::Low,
|
||||
// TODO(acp-0.10): `PlanEntryPriority` is #[non_exhaustive].
|
||||
_ => TodoPriority::Medium,
|
||||
},
|
||||
status,
|
||||
meta: entry.meta.map(serde_json::Value::Object),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a `TodoItem` to an ACP `PlanEntry`.
|
||||
///
|
||||
/// Cancelled items become `Completed` with `{"cancelled": true}` in meta.
|
||||
pub fn plan_entry_from_todo_item(item: TodoItem) -> acp::PlanEntry {
|
||||
let status = match item.status {
|
||||
TodoStatus::Pending => acp::PlanEntryStatus::Pending,
|
||||
TodoStatus::InProgress => acp::PlanEntryStatus::InProgress,
|
||||
TodoStatus::Completed => acp::PlanEntryStatus::Completed,
|
||||
TodoStatus::Cancelled => acp::PlanEntryStatus::Completed,
|
||||
};
|
||||
let mut meta = item.meta;
|
||||
if item.status == TodoStatus::Cancelled {
|
||||
let mut m = meta.unwrap_or_else(|| serde_json::json!({}));
|
||||
if let Some(obj) = m.as_object_mut() {
|
||||
obj.insert("cancelled".into(), true.into());
|
||||
}
|
||||
meta = Some(m);
|
||||
}
|
||||
acp::PlanEntry::new(
|
||||
item.content,
|
||||
match item.priority {
|
||||
TodoPriority::High => acp::PlanEntryPriority::High,
|
||||
TodoPriority::Medium => acp::PlanEntryPriority::Medium,
|
||||
TodoPriority::Low => acp::PlanEntryPriority::Low,
|
||||
},
|
||||
status,
|
||||
)
|
||||
.meta(meta.and_then(|v| v.as_object().cloned()))
|
||||
}
|
||||
249
crates/codegen/xai-grok-shell/src/tools/tool_context.rs
Normal file
249
crates/codegen/xai-grok-shell/src/tools/tool_context.rs
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
//! Session context — legacy name "ToolContext".
|
||||
//!
|
||||
//! This struct holds session-level state (cwd, gateway, filesystem, hunk tracker, etc.)
|
||||
//! that the session actor needs for non-tool operations (ACP communication, git, rewind, etc.).
|
||||
//!
|
||||
//! Tool execution goes through the ToolBridge, which has its own SessionContext from
|
||||
//! xai-grok-tools. This struct is NOT used for tool execution — it's session infrastructure.
|
||||
//!
|
||||
//! Note: Could be renamed to `SessionConfig` or flattened onto `SessionActor` in a future PR.
|
||||
use crate::terminal::AsyncTerminalRunner;
|
||||
use agent_client_protocol as acp;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use xai_acp_lib::AcpAgentGatewaySender as GatewaySender;
|
||||
use xai_grok_paths::AbsPathBuf;
|
||||
use xai_grok_workspace::file_system::{AsyncFileSystem, AsyncFsWrapper};
|
||||
use xai_grok_workspace::session::file_state::FileStateHandle;
|
||||
use xai_hunk_tracker::HunkTrackerHandle;
|
||||
/// RAII marker: the turn is blocked inside an interruptible wait. Increments
|
||||
/// [`ToolContext::blocking_wait_depth`] for its lifetime; `Drop` decrements
|
||||
/// (a cancelled turn can't leak the count).
|
||||
pub(crate) struct BlockingWaitGuard(Arc<std::sync::atomic::AtomicUsize>);
|
||||
impl BlockingWaitGuard {
|
||||
pub(crate) fn enter(depth: Arc<std::sync::atomic::AtomicUsize>) -> Self {
|
||||
depth.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||
Self(depth)
|
||||
}
|
||||
}
|
||||
impl Drop for BlockingWaitGuard {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.0.fetch_update(
|
||||
std::sync::atomic::Ordering::SeqCst,
|
||||
std::sync::atomic::Ordering::SeqCst,
|
||||
|depth| Some(depth.saturating_sub(1)),
|
||||
);
|
||||
}
|
||||
}
|
||||
/// Session-level context. NOT used for tool execution (bridge handles that).
|
||||
/// Holds ACP gateway, cwd, hunk tracker, etc. for session infrastructure.
|
||||
#[derive(Clone)]
|
||||
pub struct ToolContext {
|
||||
pub gateway: Option<GatewaySender>,
|
||||
pub session_id: Option<acp::SessionId>,
|
||||
pub fs: AsyncFsWrapper,
|
||||
pub terminal: Arc<dyn AsyncTerminalRunner>,
|
||||
pub cwd: AbsPathBuf,
|
||||
pub file_state_handle: Option<FileStateHandle>,
|
||||
pub session_env: Arc<HashMap<String, String>>,
|
||||
pub hunk_tracker_handle: HunkTrackerHandle,
|
||||
/// Whether hunk tracking is active for this session. `false` when the
|
||||
/// resolved mode is `off`/`disabled` — `hunk_tracker_handle` is then a
|
||||
/// `noop()` and the fs-notify loop skips forwarding to avoid per-event cost.
|
||||
pub hunk_tracking_enabled: bool,
|
||||
pub prompt_index: Arc<tokio::sync::Mutex<usize>>,
|
||||
/// Current subagent nesting depth for this session.
|
||||
/// Top-level sessions start at 0; child sessions are parent_depth + 1.
|
||||
pub subagent_depth: u32,
|
||||
/// Unified subagent event sender — carries spawn, query, cancel,
|
||||
/// list-active, completions, and outstanding messages to the coordinator.
|
||||
/// `None` if subagent support is not enabled.
|
||||
pub subagent_event_tx: Option<
|
||||
tokio::sync::mpsc::UnboundedSender<
|
||||
xai_grok_tools::implementations::grok_build::task::types::SubagentEvent,
|
||||
>,
|
||||
>,
|
||||
/// Shared LSP runtime — cloned cheaply (Arc) from parent to child.
|
||||
/// Same pattern as `fs` and `terminal`.
|
||||
pub lsp: Option<Arc<dyn xai_grok_tools::implementations::lsp::LspBackend>>,
|
||||
/// LSP server names snapshot from session creation (not updated mid-session).
|
||||
pub lsp_server_names: Vec<String>,
|
||||
/// Shared turn-active flag — set `true` at turn start, `false` at turn end.
|
||||
/// Used by the between-turn completion drain in `handle_prompt`.
|
||||
pub is_turn_active: Option<Arc<std::sync::atomic::AtomicBool>>,
|
||||
/// Shared buffer for mid-turn monitor event notifications.
|
||||
/// Events pushed here are drained by the session turn loop
|
||||
/// (`inject_pending_monitor_events`) and surfaced as ONE hidden
|
||||
/// synthetic user message before the next sampling step.
|
||||
pub monitor_event_buffer:
|
||||
Option<xai_grok_tools::implementations::grok_build::task::types::MonitorEventBuffer>,
|
||||
/// Shared set of IDs delivered via auto-wake synthetic prompts.
|
||||
/// Used by `TaskCompletionReminder` to suppress duplicate reminders.
|
||||
pub auto_wake_delivered:
|
||||
Option<xai_grok_tools::reminders::task_completion::AutoWakeDeliveredIds>,
|
||||
/// Channel for requesting trace uploads for synthetic auto-wake turns.
|
||||
pub(crate) synthetic_trace_tx:
|
||||
Option<tokio::sync::mpsc::UnboundedSender<crate::upload::turn::SyntheticTurnTraceRequest>>,
|
||||
/// Shared slot for the synthetic trace channel. Populated by
|
||||
/// `start_subagent_coordinator` after the notification bridge is spawned.
|
||||
/// The notification bridge reads from this slot on each completion event.
|
||||
pub(crate) synthetic_trace_tx_shared: Option<
|
||||
std::sync::Arc<
|
||||
std::sync::Mutex<
|
||||
Option<
|
||||
tokio::sync::mpsc::UnboundedSender<
|
||||
crate::upload::turn::SyntheticTurnTraceRequest,
|
||||
>,
|
||||
>,
|
||||
>,
|
||||
>,
|
||||
>,
|
||||
/// Resolved name of the `BackgroundTaskAction` tool in the current toolset.
|
||||
/// Used by auto-wake to format completion messages with the correct tool name.
|
||||
pub task_output_tool_name: String,
|
||||
/// Whether auto-wake is enabled. When `false`, background task and subagent
|
||||
/// completions fall back to the idle-gated notification drain.
|
||||
pub auto_wake_enabled: bool,
|
||||
/// When set, bash + subagent auto-wake synthetic prompts are suppressed.
|
||||
/// Shared `Arc` written at one chokepoint — see
|
||||
/// `SessionActor::set_goal_loop_active_resource` for the rationale.
|
||||
pub goal_loop_active_gate: Arc<std::sync::atomic::AtomicBool>,
|
||||
/// Count of interruptible blocking waits the running turn is parked in (via
|
||||
/// [`BlockingWaitGuard`]). `queue_input` reads it: a prompt arriving while
|
||||
/// non-zero takes the send-now path.
|
||||
pub blocking_wait_depth: Arc<std::sync::atomic::AtomicUsize>,
|
||||
}
|
||||
impl ToolContext {
|
||||
pub fn new(
|
||||
cwd: AbsPathBuf,
|
||||
gateway: Option<GatewaySender>,
|
||||
session_id: Option<acp::SessionId>,
|
||||
fs: Arc<dyn AsyncFileSystem>,
|
||||
terminal: Arc<dyn AsyncTerminalRunner>,
|
||||
hunk_tracker_handle: HunkTrackerHandle,
|
||||
) -> Self {
|
||||
let session_env = xai_grok_workspace::envrc::load_envrc_or_empty_when_trusted(
|
||||
cwd.as_path(),
|
||||
crate::agent::folder_trust::project_scope_allowed(cwd.as_path()),
|
||||
);
|
||||
Self {
|
||||
gateway,
|
||||
session_id,
|
||||
fs: AsyncFsWrapper::new(fs),
|
||||
terminal,
|
||||
cwd,
|
||||
file_state_handle: None,
|
||||
session_env: Arc::new(session_env),
|
||||
hunk_tracker_handle,
|
||||
hunk_tracking_enabled: true,
|
||||
prompt_index: Arc::new(tokio::sync::Mutex::new(0)),
|
||||
subagent_depth: 0,
|
||||
subagent_event_tx: None,
|
||||
lsp: None,
|
||||
lsp_server_names: Vec::new(),
|
||||
is_turn_active: None,
|
||||
monitor_event_buffer: None,
|
||||
auto_wake_delivered: None,
|
||||
synthetic_trace_tx: None,
|
||||
synthetic_trace_tx_shared: None,
|
||||
task_output_tool_name:
|
||||
xai_grok_tools::reminders::task_completion::DEFAULT_TASK_OUTPUT_TOOL.to_string(),
|
||||
auto_wake_enabled: true,
|
||||
goal_loop_active_gate: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
blocking_wait_depth: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
|
||||
}
|
||||
}
|
||||
pub fn with_preloaded_env(
|
||||
cwd: AbsPathBuf,
|
||||
gateway: Option<GatewaySender>,
|
||||
session_id: Option<acp::SessionId>,
|
||||
fs: Arc<dyn AsyncFileSystem>,
|
||||
terminal: Arc<dyn AsyncTerminalRunner>,
|
||||
hunk_tracker_handle: HunkTrackerHandle,
|
||||
session_env: HashMap<String, String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
gateway,
|
||||
session_id,
|
||||
fs: AsyncFsWrapper::new(fs),
|
||||
terminal,
|
||||
cwd,
|
||||
file_state_handle: None,
|
||||
session_env: Arc::new(session_env),
|
||||
hunk_tracker_handle,
|
||||
hunk_tracking_enabled: true,
|
||||
prompt_index: Arc::new(tokio::sync::Mutex::new(0)),
|
||||
subagent_depth: 0,
|
||||
subagent_event_tx: None,
|
||||
lsp: None,
|
||||
lsp_server_names: Vec::new(),
|
||||
is_turn_active: None,
|
||||
monitor_event_buffer: None,
|
||||
auto_wake_delivered: None,
|
||||
synthetic_trace_tx: None,
|
||||
synthetic_trace_tx_shared: None,
|
||||
task_output_tool_name:
|
||||
xai_grok_tools::reminders::task_completion::DEFAULT_TASK_OUTPUT_TOOL.to_string(),
|
||||
auto_wake_enabled: true,
|
||||
goal_loop_active_gate: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
blocking_wait_depth: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
|
||||
}
|
||||
}
|
||||
pub fn with_file_state_handle(mut self, handle: FileStateHandle) -> Self {
|
||||
self.file_state_handle = Some(handle);
|
||||
self
|
||||
}
|
||||
pub fn with_prompt_index(mut self, prompt_index: Arc<tokio::sync::Mutex<usize>>) -> Self {
|
||||
self.prompt_index = prompt_index;
|
||||
self
|
||||
}
|
||||
/// Set whether hunk tracking is active. `false` pairs with a `noop()`
|
||||
/// `hunk_tracker_handle` so the fs-notify loop skips the per-event forward.
|
||||
pub fn with_hunk_tracking_enabled(mut self, enabled: bool) -> Self {
|
||||
self.hunk_tracking_enabled = enabled;
|
||||
self
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{terminal::AsyncTerminalRunner, tools::ToolContext};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use xai_grok_paths::AbsPathBuf;
|
||||
use xai_grok_workspace::file_system::{AsyncFileSystem, AsyncFsWrapper};
|
||||
use xai_hunk_tracker::HunkTrackerHandle;
|
||||
impl ToolContext {
|
||||
pub fn new_local_context(
|
||||
cwd: AbsPathBuf,
|
||||
fs: Arc<dyn AsyncFileSystem>,
|
||||
terminal: Arc<dyn AsyncTerminalRunner>,
|
||||
) -> Self {
|
||||
Self {
|
||||
gateway: None,
|
||||
session_id: None,
|
||||
fs: AsyncFsWrapper::new(fs),
|
||||
terminal,
|
||||
cwd,
|
||||
file_state_handle: None,
|
||||
session_env: Arc::new(HashMap::new()),
|
||||
hunk_tracker_handle: HunkTrackerHandle::noop(),
|
||||
hunk_tracking_enabled: true,
|
||||
prompt_index: Arc::new(tokio::sync::Mutex::new(0)),
|
||||
subagent_depth: 0,
|
||||
subagent_event_tx: None,
|
||||
lsp: None,
|
||||
lsp_server_names: Vec::new(),
|
||||
is_turn_active: None,
|
||||
monitor_event_buffer: None,
|
||||
auto_wake_delivered: None,
|
||||
synthetic_trace_tx: None,
|
||||
synthetic_trace_tx_shared: None,
|
||||
task_output_tool_name:
|
||||
xai_grok_tools::reminders::task_completion::DEFAULT_TASK_OUTPUT_TOOL.to_string(),
|
||||
auto_wake_enabled: true,
|
||||
goal_loop_active_gate: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
blocking_wait_depth: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue