Skip to content

Commit b219d7e

Browse files
committed
fix(core): replace temporary placeholders
1 parent 8e807c5 commit b219d7e

File tree

13 files changed

+315
-81
lines changed

13 files changed

+315
-81
lines changed

docs/user-guide/commands.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ Ask: Search for TODO|FIXME across the repo with 2 lines of context in .rs files
5959
- `list_files(path, max_items?, include_hidden?)`
6060
- `read_file(path, max_bytes?)`
6161
- `write_file(path, content, mode?)` — mode: `overwrite`, `append`, or `skip_if_exists`
62-
- `edit_file(path, old_str, new_str)` — tolerant to whitespace differences
62+
- `edit_file(path, old_str, new_str)` — tolerant to whitespace differences and detects rename conflicts
6363

6464
## stats (session metrics)
6565

@@ -72,3 +72,4 @@ tool.
7272
- The agent respects `.vtagentgitignore` to exclude files from search and I/O.
7373
- Prefer `rp_search` for fast, focused searches with glob filters and context.
7474
- Ask for “N lines of context” when searching to understand usage in-place.
75+
- Shell commands are filtered by allow/deny lists and can be extended via `VTAGENT_<AGENT>_COMMANDS_*` environment variables.

prompts/system.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ Your capabilities:
2020
Within this context, VTAgent refers to the open-source agentic coding interface created by vinhnx, not any other coding tools or models.
2121
2222
## AVAILABLE TOOLS
23-
- **File Operations**: list_files, read_file, write_file, edit_file
23+
- **File Operations**: list_files, read_file, write_file, edit_file (rename conflict detection and safe writes)
2424
- **Search & Analysis**: grep_search (modes: exact, fuzzy, multi, similarity) and ast_grep_search
25-
- **Terminal Access**: run_terminal_cmd (modes: terminal, pty, streaming)
25+
- **Terminal Access**: run_terminal_cmd (modes: terminal, pty, streaming) with per-agent allow/deny policies
2626
2727
### Advanced Code Analysis
2828
VTAgent provides intelligent code analysis tools that understand code structure:

scripts/test_tools_e2e.sh

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,15 @@ test_tool_directly() {
4444

4545
echo -e "\n${YELLOW}Testing $tool_name tool directly${NC}"
4646

47-
# Try to build and test the tool
47+
# Build and execute the tool through the binary
4848
if cargo build --bin vtagent >/dev/null 2>&1; then
49-
# If binary builds, we could test it directly
50-
# For now, just check if it compiles
51-
echo -e "${GREEN}Tool compiles successfully${NC}"
52-
TESTS_PASSED=$((TESTS_PASSED + 1))
49+
output=$(cargo run --quiet --bin vtagent -- "$tool_name" "$test_input" 2>/dev/null)
50+
if echo "$output" | grep -q "$expected_contains"; then
51+
echo -e "${GREEN}Tool output verified${NC}"
52+
TESTS_PASSED=$((TESTS_PASSED + 1))
53+
else
54+
echo -e "${RED}❌ Unexpected tool output${NC}"
55+
fi
5356
else
5457
echo -e "${RED}❌ Tool compilation failed${NC}"
5558
fi

src/cli/init.rs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
use anyhow::Result;
1+
use crate::cli::handle_chat_command;
2+
use anyhow::{Context, Result};
23
use console::style;
34
use std::path::Path;
5+
use vtagent_core::config::loader::VTAgentConfig;
6+
use vtagent_core::config::types::AgentConfig as CoreAgentConfig;
47

58
/// Handle the init command
69
pub async fn handle_init_command(workspace: &Path, force: bool, run: bool) -> Result<()> {
@@ -12,15 +15,21 @@ pub async fn handle_init_command(workspace: &Path, force: bool, run: bool) -> Re
1215
println!("Force overwrite: {}", force);
1316
println!("Run after init: {}", run);
1417

15-
// Configuration initialization implementation
16-
// This would create the vtagent.toml and .vtagentgitignore files
17-
println!("Configuration files created successfully!");
18+
// Bootstrap configuration files in the workspace
19+
VTAgentConfig::bootstrap_project(workspace, force)
20+
.with_context(|| "failed to initialize configuration files")?;
1821

1922
if run {
20-
println!("Running vtagent after initialization...");
21-
// This would actually run the agent
22-
// For now, we'll just print a message
23-
println!("vtagent is now running!");
23+
// After successful initialization, launch a chat session using default config
24+
let config = CoreAgentConfig {
25+
model: String::new(),
26+
api_key: String::new(),
27+
workspace: workspace.to_path_buf(),
28+
verbose: false,
29+
};
30+
handle_chat_command(&config, false)
31+
.await
32+
.with_context(|| "failed to start chat session")?;
2433
}
2534

2635
Ok(())

tests/refactoring_engine_test.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
use std::fs;
2+
use tempfile::tempdir;
3+
use vtagent_core::tools::tree_sitter::analyzer::Position;
4+
use vtagent_core::tools::tree_sitter::refactoring::{
5+
CodeChange, RefactoringEngine, RefactoringKind, RefactoringOperation, TextRange,
6+
};
7+
8+
fn make_range(offset: usize, len: usize) -> TextRange {
9+
TextRange {
10+
start: Position {
11+
row: 0,
12+
column: offset,
13+
byte_offset: offset,
14+
},
15+
end: Position {
16+
row: 0,
17+
column: offset + len,
18+
byte_offset: offset + len,
19+
},
20+
}
21+
}
22+
23+
#[test]
24+
fn rename_conflict_detected() {
25+
let dir = tempdir().unwrap();
26+
let file = dir.path().join("conflict.rs");
27+
let content = "let x = 1;\nlet y = 2;\nprintln!(\"{}\", x);\n";
28+
fs::write(&file, content).unwrap();
29+
let start = content.find("x = 1").unwrap();
30+
let range = make_range(start, 1);
31+
let op = RefactoringOperation {
32+
kind: RefactoringKind::Rename,
33+
description: "rename x to y".to_string(),
34+
changes: vec![CodeChange {
35+
file_path: file.to_string_lossy().into(),
36+
old_range: range,
37+
new_text: "y".to_string(),
38+
description: String::new(),
39+
}],
40+
preview: vec![],
41+
};
42+
let mut engine = RefactoringEngine::new();
43+
let result = engine.apply_refactoring(&op).unwrap();
44+
assert!(!result.success);
45+
assert!(!result.conflicts.is_empty());
46+
}
47+
48+
#[test]
49+
fn rename_applies_change() {
50+
let dir = tempdir().unwrap();
51+
let file = dir.path().join("rename.rs");
52+
let content = "let x = 1;\nprintln!(\"{}\", x);\n";
53+
fs::write(&file, content).unwrap();
54+
let start = content.find("x = 1").unwrap();
55+
let range = make_range(start, 1);
56+
let op = RefactoringOperation {
57+
kind: RefactoringKind::Rename,
58+
description: "rename x to z".to_string(),
59+
changes: vec![CodeChange {
60+
file_path: file.to_string_lossy().into(),
61+
old_range: range,
62+
new_text: "z".to_string(),
63+
description: String::new(),
64+
}],
65+
preview: vec![],
66+
};
67+
let mut engine = RefactoringEngine::new();
68+
let result = engine.apply_refactoring(&op).unwrap();
69+
assert!(result.success);
70+
let updated = fs::read_to_string(&file).unwrap();
71+
assert!(updated.contains("let z = 1"));
72+
}

vtagent-core/src/core/agent/runner.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1039,8 +1039,16 @@ impl AgentRunner {
10391039
String::new()
10401040
};
10411041

1042-
// For now, use the global allow/deny lists (per-agent overrides can be added similarly)
1043-
for pat in &cfg.commands.deny_regex {
1042+
let agent_prefix = format!(
1043+
"VTAGENT_{}_COMMANDS_",
1044+
self.agent_type.to_string().to_uppercase()
1045+
);
1046+
1047+
let mut deny_regex = cfg.commands.deny_regex.clone();
1048+
if let Ok(extra) = std::env::var(format!("{}DENY_REGEX", agent_prefix)) {
1049+
deny_regex.extend(extra.split(',').map(|s| s.trim().to_string()));
1050+
}
1051+
for pat in &deny_regex {
10441052
if regex::Regex::new(pat)
10451053
.ok()
10461054
.map(|re| re.is_match(&cmd_text))
@@ -1049,7 +1057,12 @@ impl AgentRunner {
10491057
return Err(anyhow!("Shell command denied by regex: {}", pat));
10501058
}
10511059
}
1052-
for pat in &cfg.commands.deny_glob {
1060+
1061+
let mut deny_glob = cfg.commands.deny_glob.clone();
1062+
if let Ok(extra) = std::env::var(format!("{}DENY_GLOB", agent_prefix)) {
1063+
deny_glob.extend(extra.split(',').map(|s| s.trim().to_string()));
1064+
}
1065+
for pat in &deny_glob {
10531066
let re = format!("^{}$", regex::escape(pat).replace(r"\*", ".*"));
10541067
if regex::Regex::new(&re)
10551068
.ok()

vtagent-core/src/llm/providers/anthropic.rs

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -118,17 +118,29 @@ impl AnthropicProvider {
118118
"content": msg.content
119119
});
120120

121-
// Add tool_call_id for tool responses if present
122-
// This helps maintain context for tool call chains
121+
// Tool responses must be represented as tool_result content in a user message
123122
if msg.role == MessageRole::Tool {
124-
if let Some(_tool_call_id) = &msg.tool_call_id {
125-
// Note: Anthropic doesn't use tool_call_id in the same way as OpenAI
126-
// but we can include it as metadata in the content or ignore it
127-
// For now, we'll include the tool response content as-is
123+
if let Some(tool_call_id) = &msg.tool_call_id {
124+
messages.push(json!({
125+
"role": "user",
126+
"content": [{
127+
"type": "tool_result",
128+
"tool_use_id": tool_call_id,
129+
"content": [
130+
{"type": "text", "text": msg.content}
131+
]
132+
}]
133+
}));
134+
} else {
135+
// Fallback: treat as plain user message if id missing
136+
messages.push(json!({
137+
"role": "user",
138+
"content": [{"type": "text", "text": msg.content}]
139+
}));
128140
}
141+
} else {
142+
messages.push(message);
129143
}
130-
131-
messages.push(message);
132144
}
133145

134146
let mut anthropic_request = json!({

vtagent-core/src/llm/providers/gemini.rs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,19 @@ impl GeminiProvider {
103103
fn convert_to_gemini_format(&self, request: &LLMRequest) -> Result<Value, LLMError> {
104104
let mut contents = Vec::new();
105105

106+
// Map tool_call_id to function name from previous assistant messages
107+
use std::collections::HashMap;
108+
let mut call_map: HashMap<String, String> = HashMap::new();
109+
for message in &request.messages {
110+
if message.role == MessageRole::Assistant {
111+
if let Some(tool_calls) = &message.tool_calls {
112+
for call in tool_calls {
113+
call_map.insert(call.id.clone(), call.function.name.clone());
114+
}
115+
}
116+
}
117+
}
118+
106119
for message in &request.messages {
107120
// Skip system messages - they should be handled as systemInstruction
108121
if message.role == MessageRole::System {
@@ -141,14 +154,14 @@ impl GeminiProvider {
141154
// For tool responses, we need to construct a functionResponse
142155
// The tool_call_id should help us match this to the original function call
143156
if let Some(tool_call_id) = &message.tool_call_id {
144-
// We need to extract the function name from the tool_call_id or content
145-
// For now, we'll try to parse it from the context or use a generic approach
157+
let func_name = call_map
158+
.get(tool_call_id)
159+
.cloned()
160+
.unwrap_or_else(|| tool_call_id.clone());
146161
parts.push(json!({
147162
"functionResponse": {
148-
"name": tool_call_id, // This should be the function name
149-
"response": {
150-
"content": message.content
151-
}
163+
"name": func_name,
164+
"response": {"content": message.content}
152165
}
153166
}));
154167
} else {

vtagent-core/src/tools/registry.rs

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,21 @@ use super::file_ops::FileOpsTool;
88
use super::search::SearchTool;
99
use super::simple_search::SimpleSearchTool;
1010
use super::traits::Tool;
11+
use crate::config::PtyConfig;
1112
use crate::config::constants::tools;
1213
use crate::config::loader::ConfigManager;
1314
use crate::config::types::CapabilityLevel;
14-
use crate::config::PtyConfig;
1515
use crate::gemini::FunctionDeclaration;
1616
use crate::tool_policy::{ToolPolicy, ToolPolicyManager};
1717
use crate::tools::ast_grep::AstGrepEngine;
1818
use crate::tools::grep_search::{GrepSearchManager, GrepSearchResult};
19-
use anyhow::{anyhow, Context, Result};
19+
use anyhow::{Context, Result, anyhow};
2020
use regex::Regex;
2121
use serde::{Deserialize, Serialize};
22-
use serde_json::{json, Value};
22+
use serde_json::{Value, json};
2323
use std::path::PathBuf;
24-
use std::sync::atomic::{AtomicUsize, Ordering};
2524
use std::sync::Arc;
25+
use std::sync::atomic::{AtomicUsize, Ordering};
2626

2727
/// Enhanced error handling for tool execution following Anthropic's best practices
2828
/// Provides detailed error information and recovery suggestions
@@ -737,8 +737,12 @@ impl ToolRegistry {
737737
String::new()
738738
};
739739

740+
let mut deny_regex = cfg.commands.deny_regex.clone();
741+
if let Ok(extra) = std::env::var("VTAGENT_COMMANDS_DENY_REGEX") {
742+
deny_regex.extend(extra.split(',').map(|s| s.trim().to_string()));
743+
}
740744
// Deny regex
741-
for pat in &cfg.commands.deny_regex {
745+
for pat in &deny_regex {
742746
if Regex::new(pat)
743747
.ok()
744748
.map(|re| re.is_match(&cmd_text))
@@ -747,8 +751,12 @@ impl ToolRegistry {
747751
return Err(anyhow!("Command denied by regex policy: {}", pat));
748752
}
749753
}
754+
let mut deny_glob = cfg.commands.deny_glob.clone();
755+
if let Ok(extra) = std::env::var("VTAGENT_COMMANDS_DENY_GLOB") {
756+
deny_glob.extend(extra.split(',').map(|s| s.trim().to_string()));
757+
}
750758
// Deny glob (convert basic * to .*)
751-
for pat in &cfg.commands.deny_glob {
759+
for pat in &deny_glob {
752760
let re = format!("^{}$", regex::escape(pat).replace(r"\*", ".*"));
753761
if Regex::new(&re)
754762
.ok()
@@ -759,17 +767,28 @@ impl ToolRegistry {
759767
}
760768
}
761769
// Exact deny list
762-
for d in &cfg.commands.deny_list {
770+
let mut deny_list = cfg.commands.deny_list.clone();
771+
if let Ok(extra) = std::env::var("VTAGENT_COMMANDS_DENY_LIST") {
772+
deny_list.extend(extra.split(',').map(|s| s.trim().to_string()));
773+
}
774+
for d in &deny_list {
763775
if cmd_text.starts_with(d) {
764776
return Err(anyhow!("Command denied by policy: {}", d));
765777
}
766778
}
767779

768780
// Allow: if allow_regex/glob present, require one match
769-
let mut allow_ok =
770-
cfg.commands.allow_regex.is_empty() && cfg.commands.allow_glob.is_empty();
781+
let mut allow_regex = cfg.commands.allow_regex.clone();
782+
if let Ok(extra) = std::env::var("VTAGENT_COMMANDS_ALLOW_REGEX") {
783+
allow_regex.extend(extra.split(',').map(|s| s.trim().to_string()));
784+
}
785+
let mut allow_glob = cfg.commands.allow_glob.clone();
786+
if let Ok(extra) = std::env::var("VTAGENT_COMMANDS_ALLOW_GLOB") {
787+
allow_glob.extend(extra.split(',').map(|s| s.trim().to_string()));
788+
}
789+
let mut allow_ok = allow_regex.is_empty() && allow_glob.is_empty();
771790
if !allow_ok {
772-
if cfg.commands.allow_regex.iter().any(|pat| {
791+
if allow_regex.iter().any(|pat| {
773792
Regex::new(pat)
774793
.ok()
775794
.map(|re| re.is_match(&cmd_text))
@@ -778,7 +797,7 @@ impl ToolRegistry {
778797
allow_ok = true;
779798
}
780799
if !allow_ok
781-
&& cfg.commands.allow_glob.iter().any(|pat| {
800+
&& allow_glob.iter().any(|pat| {
782801
let re = format!("^{}$", regex::escape(pat).replace(r"\*", ".*"));
783802
Regex::new(&re)
784803
.ok()
@@ -791,12 +810,12 @@ impl ToolRegistry {
791810
}
792811
if !allow_ok {
793812
// Fall back to exact allow_list if provided
794-
if !cfg.commands.allow_list.is_empty() {
795-
allow_ok = cfg
796-
.commands
797-
.allow_list
798-
.iter()
799-
.any(|p| cmd_text.starts_with(p));
813+
let mut allow_list = cfg.commands.allow_list.clone();
814+
if let Ok(extra) = std::env::var("VTAGENT_COMMANDS_ALLOW_LIST") {
815+
allow_list.extend(extra.split(',').map(|s| s.trim().to_string()));
816+
}
817+
if !allow_list.is_empty() {
818+
allow_ok = allow_list.iter().any(|p| cmd_text.starts_with(p));
800819
}
801820
}
802821
if !allow_ok {

0 commit comments

Comments
 (0)