diff --git a/src/crates/core/src/agentic/agents/registry.rs b/src/crates/core/src/agentic/agents/registry.rs index c4c00392..da1f436d 100644 --- a/src/crates/core/src/agentic/agents/registry.rs +++ b/src/crates/core/src/agentic/agents/registry.rs @@ -980,7 +980,11 @@ impl AgentRegistry { description, tools, prompt, - if review { true } else { readonly.unwrap_or(old.readonly) }, + if review { + true + } else { + readonly.unwrap_or(old.readonly) + }, old.path.clone(), old.kind, ); diff --git a/src/crates/core/src/agentic/coordination/coordinator.rs b/src/crates/core/src/agentic/coordination/coordinator.rs index 69ad48e7..75f5c56b 100644 --- a/src/crates/core/src/agentic/coordination/coordinator.rs +++ b/src/crates/core/src/agentic/coordination/coordinator.rs @@ -3,11 +3,10 @@ //! Top-level component that integrates all subsystems and provides a unified interface use super::{scheduler::DialogSubmissionPolicy, turn_outcome::TurnOutcome}; -use crate::agentic::WorkspaceBinding; use crate::agentic::agents::get_agent_registry; use crate::agentic::core::{ - Message, MessageContent, ProcessingPhase, PromptEnvelope, Session, SessionConfig, SessionKind, - SessionState, SessionSummary, TurnStats, has_prompt_markup, + has_prompt_markup, Message, MessageContent, ProcessingPhase, PromptEnvelope, Session, + SessionConfig, SessionKind, SessionState, SessionSummary, TurnStats, }; use crate::agentic::events::{ AgenticEvent, EventPriority, EventQueue, EventRouter, EventSubscriber, @@ -17,8 +16,9 @@ use crate::agentic::fork::{ForkContextSnapshot, ForkExecutionRequest, ForkExecut use crate::agentic::image_analysis::ImageContextData; use crate::agentic::round_preempt::DialogRoundPreemptSource; use crate::agentic::session::SessionManager; -use crate::agentic::tools::ToolRuntimeRestrictions; use crate::agentic::tools::pipeline::{SubagentParentInfo, ToolPipeline}; +use crate::agentic::tools::ToolRuntimeRestrictions; +use crate::agentic::WorkspaceBinding; use crate::service::bootstrap::{ ensure_workspace_persona_files_for_prompt, is_workspace_bootstrap_pending, }; @@ -29,8 +29,8 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::sync::OnceLock; -use tokio::sync::{OwnedSemaphorePermit, RwLock, Semaphore, mpsc, watch}; -use tokio::time::{Duration, Instant, sleep}; +use tokio::sync::{mpsc, watch, OwnedSemaphorePermit, RwLock, Semaphore}; +use tokio::time::{sleep, Duration, Instant}; use tokio_util::sync::CancellationToken; const MANUAL_COMPACTION_COMMAND: &str = "/compact"; @@ -2231,8 +2231,8 @@ Update the persona files and delete BOOTSTRAP.md as soon as bootstrap is complet }; // Create dynamic deadline via watch channel so it can be adjusted at runtime. - let initial_deadline = timeout_seconds - .map(|seconds| Instant::now() + Duration::from_secs(seconds)); + let initial_deadline = + timeout_seconds.map(|seconds| Instant::now() + Duration::from_secs(seconds)); let (deadline_tx, mut deadline_rx) = watch::channel(initial_deadline); // Check cancel token (before creating session) diff --git a/src/crates/core/src/agentic/execution/stream_processor.rs b/src/crates/core/src/agentic/execution/stream_processor.rs index 2d9a3b2b..10dd0005 100644 --- a/src/crates/core/src/agentic/execution/stream_processor.rs +++ b/src/crates/core/src/agentic/execution/stream_processor.rs @@ -404,7 +404,8 @@ impl StreamProcessor { for tool_call in tool_calls { trace!( "Cleaning up tool: {} ({})", - tool_call.tool_name, tool_call.tool_id + tool_call.tool_name, + tool_call.tool_id ); let tool_event = if is_user_cancellation { diff --git a/src/crates/core/src/agentic/execution/types.rs b/src/crates/core/src/agentic/execution/types.rs index d0ba86e9..928e48ea 100644 --- a/src/crates/core/src/agentic/execution/types.rs +++ b/src/crates/core/src/agentic/execution/types.rs @@ -2,8 +2,8 @@ use crate::agentic::core::Message; use crate::agentic::round_preempt::DialogRoundPreemptSource; -use crate::agentic::tools::ToolRuntimeRestrictions; use crate::agentic::tools::pipeline::SubagentParentInfo; +use crate::agentic::tools::ToolRuntimeRestrictions; use crate::agentic::workspace::WorkspaceServices; use crate::agentic::WorkspaceBinding; use serde_json::Value; diff --git a/src/crates/core/src/agentic/tools/framework.rs b/src/crates/core/src/agentic/tools/framework.rs index 67a5bd8a..abeb3c64 100644 --- a/src/crates/core/src/agentic/tools/framework.rs +++ b/src/crates/core/src/agentic/tools/framework.rs @@ -96,7 +96,8 @@ impl ToolUseContext { } pub fn enforce_tool_runtime_restrictions(&self, tool_name: &str) -> BitFunResult<()> { - self.runtime_tool_restrictions.ensure_tool_allowed(tool_name) + self.runtime_tool_restrictions + .ensure_tool_allowed(tool_name) } pub fn enforce_path_operation( diff --git a/src/crates/core/src/agentic/tools/implementations/delete_file_tool.rs b/src/crates/core/src/agentic/tools/implementations/delete_file_tool.rs index 8ca40358..d28563a5 100644 --- a/src/crates/core/src/agentic/tools/implementations/delete_file_tool.rs +++ b/src/crates/core/src/agentic/tools/implementations/delete_file_tool.rs @@ -1,8 +1,8 @@ use crate::agentic::tools::framework::{ Tool, ToolRenderOptions, ToolResult, ToolUseContext, ValidationResult, }; -use crate::agentic::tools::ToolPathOperation; use crate::agentic::tools::workspace_paths::is_bitfun_runtime_uri; +use crate::agentic::tools::ToolPathOperation; use crate::util::errors::{BitFunError, BitFunResult}; use async_trait::async_trait; use log::debug; diff --git a/src/crates/core/src/agentic/tools/mod.rs b/src/crates/core/src/agentic/tools/mod.rs index 9fb29df8..c16791fc 100644 --- a/src/crates/core/src/agentic/tools/mod.rs +++ b/src/crates/core/src/agentic/tools/mod.rs @@ -10,8 +10,8 @@ pub mod image_context; pub mod implementations; pub mod input_validator; pub mod pipeline; -pub mod restrictions; pub mod registry; +pub mod restrictions; pub mod user_input_manager; pub mod workspace_paths; @@ -19,8 +19,8 @@ pub use framework::{Tool, ToolResult, ToolUseContext, ValidationResult}; pub use image_context::{ImageContextData, ImageContextProvider, ImageContextProviderRef}; pub use input_validator::InputValidator; pub use pipeline::*; -pub use restrictions::{ToolPathOperation, ToolPathPolicy, ToolRuntimeRestrictions}; pub use registry::{ create_tool_registry, get_all_registered_tool_names, get_all_registered_tools, get_all_tools, get_readonly_registered_tool_names, get_readonly_tools, }; +pub use restrictions::{ToolPathOperation, ToolPathPolicy, ToolRuntimeRestrictions}; diff --git a/src/crates/core/src/agentic/tools/restrictions.rs b/src/crates/core/src/agentic/tools/restrictions.rs index 63f1582d..a8bb66f1 100644 --- a/src/crates/core/src/agentic/tools/restrictions.rs +++ b/src/crates/core/src/agentic/tools/restrictions.rs @@ -195,10 +195,7 @@ mod tests { #[test] fn denied_tool_names_override_allow_list() { let restrictions = ToolRuntimeRestrictions { - allowed_tool_names: ["Write", "Edit"] - .into_iter() - .map(str::to_string) - .collect(), + allowed_tool_names: ["Write", "Edit"].into_iter().map(str::to_string).collect(), denied_tool_names: ["Write"].into_iter().map(str::to_string).collect(), path_policy: ToolPathPolicy::default(), }; @@ -221,7 +218,8 @@ mod tests { #[test] fn local_path_containment_handles_missing_children() { - let root = std::env::temp_dir().join(format!("bitfun-restrictions-{}", uuid::Uuid::new_v4())); + let root = + std::env::temp_dir().join(format!("bitfun-restrictions-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(root.join("allowed")).expect("create temp root"); let allowed_child = root.join("allowed").join("nested").join("file.txt"); diff --git a/src/crates/core/src/service/snapshot/snapshot_system.rs b/src/crates/core/src/service/snapshot/snapshot_system.rs index c4732b07..1bf2a8b8 100644 --- a/src/crates/core/src/service/snapshot/snapshot_system.rs +++ b/src/crates/core/src/service/snapshot/snapshot_system.rs @@ -364,7 +364,7 @@ impl FileSnapshotSystem { compressed_content: match optimized_content { OptimizedContent::Raw(data) => data, OptimizedContent::Compressed(data) => data, - OptimizedContent::Reference(_) => unreachable!(), + OptimizedContent::Reference(_) => Vec::new(), }, timestamp: SystemTime::now(), metadata, @@ -435,7 +435,8 @@ impl FileSnapshotSystem { fn optimize_content(&self, content: &[u8]) -> OptimizedContent { if self.dedup_enabled { let hash = self.calculate_content_hash(content); - if self.hash_to_path.contains_key(&hash) { + let content_path = self.get_content_path(&hash); + if self.hash_to_path.contains_key(&hash) && content_path.exists() { return OptimizedContent::Reference(hash); } } @@ -837,3 +838,60 @@ impl FileSnapshotSystem { self.get_baseline_snapshot_id(file_path).await.is_some() } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::service::workspace_runtime::{WorkspaceRuntimeContext, WorkspaceRuntimeTarget}; + + fn test_runtime_context() -> WorkspaceRuntimeContext { + let runtime_root = + std::env::temp_dir().join(format!("bitfun_snapshot_test_{}", Uuid::new_v4())); + WorkspaceRuntimeContext::new( + WorkspaceRuntimeTarget::LocalWorkspace { + workspace_root: runtime_root.join("workspace"), + }, + runtime_root, + ) + } + + fn create_runtime_dirs(context: &WorkspaceRuntimeContext) { + for directory in context.required_directories() { + fs::create_dir_all(directory).expect("create runtime directory"); + } + } + + #[tokio::test] + async fn create_snapshot_reuses_empty_baseline_content_without_panicking() { + let context = test_runtime_context(); + create_runtime_dirs(&context); + + let file_path = context.runtime_root.join("workspace").join("empty.txt"); + fs::create_dir_all(file_path.parent().expect("file has parent")).expect("create parent"); + + let mut snapshot_system = FileSnapshotSystem::new(context.clone()); + snapshot_system + .initialize() + .await + .expect("initialize snapshots"); + snapshot_system + .create_empty_baseline(&file_path) + .await + .expect("create empty baseline"); + + fs::write(&file_path, []).expect("write empty file"); + + let snapshot_id = snapshot_system + .create_snapshot(&file_path) + .await + .expect("create snapshot"); + let restored = snapshot_system + .restore_snapshot_content(&snapshot_id) + .await + .expect("restore snapshot content"); + + assert!(restored.is_empty()); + + fs::remove_dir_all(&context.runtime_root).expect("cleanup runtime root"); + } +} diff --git a/src/crates/core/tests/stream_processor_anthropic.rs b/src/crates/core/tests/stream_processor_anthropic.rs index 6de8f8f5..4f207f8d 100644 --- a/src/crates/core/tests/stream_processor_anthropic.rs +++ b/src/crates/core/tests/stream_processor_anthropic.rs @@ -84,10 +84,7 @@ async fn anthropic_extended_thinking_sse_produces_reasoning_and_text() { result.usage.as_ref().map(|usage| usage.total_token_count), Some(25) ); - assert_eq!( - result.thinking_signature.as_deref(), - Some("sig_abc123") - ); + assert_eq!(result.thinking_signature.as_deref(), Some("sig_abc123")); let thinking_chunks: Vec<(&str, bool)> = output .events