Compare commits
17 Commits
micn/testi
...
jochen_reo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
467e300ec2 | ||
|
|
f501751bdf | ||
|
|
a96a15d1fc | ||
|
|
24dc7ad642 | ||
|
|
a097c3abef | ||
|
|
34e55050b3 | ||
|
|
551a577ee1 | ||
|
|
84718223bc | ||
|
|
28a83d2dcf | ||
|
|
0ce905dc74 | ||
|
|
9f0d5add1e | ||
|
|
be6c6bfca4 | ||
|
|
94a41c5c34 | ||
|
|
09dbad2d68 | ||
|
|
ffbf410b17 | ||
|
|
c6f3f12b71 | ||
|
|
e556f06b15 |
73
.github/workflows/ci.yml
vendored
73
.github/workflows/ci.yml
vendored
@@ -1,73 +0,0 @@
|
|||||||
name: CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- os: ubuntu-latest
|
|
||||||
arch: x86_64
|
|
||||||
- os: ubuntu-latest
|
|
||||||
arch: aarch64
|
|
||||||
- os: macos-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install Rust
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
|
|
||||||
- name: Set up QEMU (for aarch64 on Linux)
|
|
||||||
if: matrix.arch == 'aarch64' && runner.os == 'Linux'
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
|
|
||||||
- name: Cache cargo
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/registry
|
|
||||||
~/.cargo/git
|
|
||||||
target
|
|
||||||
key: ${{ runner.os }}-${{ matrix.arch || 'x86_64' }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
|
|
||||||
- name: Install system dependencies (Ubuntu)
|
|
||||||
if: runner.os == 'Linux' && matrix.arch != 'aarch64'
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libx11-dev libxdo-dev libxcb-shape0-dev libxcb-xfixes0-dev libxtst-dev
|
|
||||||
|
|
||||||
- name: Build and test (Linux aarch64)
|
|
||||||
if: matrix.arch == 'aarch64' && runner.os == 'Linux'
|
|
||||||
uses: uraimo/run-on-arch-action@v2
|
|
||||||
with:
|
|
||||||
arch: aarch64
|
|
||||||
distro: ubuntu22.04
|
|
||||||
install: |
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y curl build-essential libx11-dev libxdo-dev libxcb-shape0-dev libxcb-xfixes0-dev libxtst-dev
|
|
||||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
|
||||||
run: |
|
|
||||||
. $HOME/.cargo/env
|
|
||||||
cargo build --workspace --exclude g3-computer-control
|
|
||||||
cargo test --workspace --exclude g3-computer-control --lib --tests
|
|
||||||
|
|
||||||
- name: Build (Linux x86_64)
|
|
||||||
if: matrix.arch != 'aarch64' && runner.os == 'Linux'
|
|
||||||
run: cargo build --workspace --exclude g3-computer-control
|
|
||||||
|
|
||||||
- name: Run tests (Linux x86_64)
|
|
||||||
if: matrix.arch != 'aarch64' && runner.os == 'Linux'
|
|
||||||
run: cargo test --workspace --exclude g3-computer-control --lib --tests
|
|
||||||
|
|
||||||
- name: Build (macOS)
|
|
||||||
if: runner.os == 'macOS'
|
|
||||||
run: cargo build --workspace
|
|
||||||
|
|
||||||
- name: Run tests (macOS)
|
|
||||||
if: runner.os == 'macOS'
|
|
||||||
run: cargo test --workspace --lib --tests
|
|
||||||
9
Cargo.lock
generated
9
Cargo.lock
generated
@@ -1365,11 +1365,13 @@ dependencies = [
|
|||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
"g3-config",
|
"g3-config",
|
||||||
"g3-core",
|
"g3-core",
|
||||||
|
"hex",
|
||||||
"indicatif",
|
"indicatif",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"rustyline",
|
"rustyline",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sha2",
|
||||||
"termimad",
|
"termimad",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
@@ -1409,6 +1411,7 @@ dependencies = [
|
|||||||
"config",
|
"config",
|
||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"shellexpand",
|
"shellexpand",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
@@ -1652,6 +1655,12 @@ version = "0.5.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hex"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "home"
|
name = "home"
|
||||||
version = "0.5.9"
|
version = "0.5.9"
|
||||||
|
|||||||
@@ -34,3 +34,4 @@ temperature = 0.3 # Slightly higher temperature for more creative implementatio
|
|||||||
fallback_default_max_tokens = 8192
|
fallback_default_max_tokens = 8192
|
||||||
enable_streaming = true
|
enable_streaming = true
|
||||||
timeout_seconds = 60
|
timeout_seconds = 60
|
||||||
|
allow_multiple_tool_calls = true # Enable multiple tool calls, will usually only work with Anthropic
|
||||||
@@ -57,6 +57,7 @@ timeout_seconds = 60
|
|||||||
# Retry configuration for recoverable errors (timeouts, rate limits, etc.)
|
# Retry configuration for recoverable errors (timeouts, rate limits, etc.)
|
||||||
max_retry_attempts = 3 # Default mode retry attempts
|
max_retry_attempts = 3 # Default mode retry attempts
|
||||||
autonomous_max_retry_attempts = 6 # Autonomous mode retry attempts (higher for long-running tasks)
|
autonomous_max_retry_attempts = 6 # Autonomous mode retry attempts (higher for long-running tasks)
|
||||||
|
allow_multiple_tool_calls = true # Enable multiple tool calls
|
||||||
|
|
||||||
[computer_control]
|
[computer_control]
|
||||||
enabled = false # Set to true to enable computer control (requires OS permissions)
|
enabled = false # Set to true to enable computer control (requires OS permissions)
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ serde_json = { workspace = true }
|
|||||||
rustyline = "17.0.1"
|
rustyline = "17.0.1"
|
||||||
dirs = "5.0"
|
dirs = "5.0"
|
||||||
tokio-util = "0.7"
|
tokio-util = "0.7"
|
||||||
|
sha2 = "0.10"
|
||||||
|
hex = "0.4"
|
||||||
indicatif = "0.17"
|
indicatif = "0.17"
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
crossterm = "0.29.0"
|
crossterm = "0.29.0"
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ use rustyline::error::ReadlineError;
|
|||||||
use rustyline::DefaultEditor;
|
use rustyline::DefaultEditor;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
|
||||||
@@ -1660,6 +1661,17 @@ async fn run_autonomous(
|
|||||||
} else {
|
} else {
|
||||||
output.print("📋 Requirements loaded from requirements.md");
|
output.print("📋 Requirements loaded from requirements.md");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate SHA256 of requirements
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(requirements.as_bytes());
|
||||||
|
let requirements_sha = hex::encode(hasher.finalize());
|
||||||
|
|
||||||
|
output.print(&format!("🔒 Requirements SHA256: {}", requirements_sha));
|
||||||
|
|
||||||
|
// Pass SHA to agent for staleness checking
|
||||||
|
agent.set_requirements_sha(requirements_sha.clone());
|
||||||
|
|
||||||
output.print("🔄 Starting coach-player feedback loop...");
|
output.print("🔄 Starting coach-player feedback loop...");
|
||||||
|
|
||||||
// Check if implementation files already exist
|
// Check if implementation files already exist
|
||||||
@@ -1692,8 +1704,8 @@ async fn run_autonomous(
|
|||||||
// Player mode: implement requirements (with coach feedback if available)
|
// Player mode: implement requirements (with coach feedback if available)
|
||||||
let player_prompt = if coach_feedback.is_empty() {
|
let player_prompt = if coach_feedback.is_empty() {
|
||||||
format!(
|
format!(
|
||||||
"You are G3 in implementation mode. Read and implement the following requirements:\n\n{}\n\nImplement this step by step, creating all necessary files and code.",
|
"You are G3 in implementation mode. Read and implement the following requirements:\n\n{}\n\nRequirements SHA256: {}\n\nImplement this step by step, creating all necessary files and code.",
|
||||||
requirements
|
requirements, requirements_sha
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
format!(
|
format!(
|
||||||
|
|||||||
@@ -91,4 +91,18 @@ impl UiWriter for MachineUiWriter {
|
|||||||
fn wants_full_output(&self) -> bool {
|
fn wants_full_output(&self) -> bool {
|
||||||
true // Machine mode wants complete, untruncated output
|
true // Machine mode wants complete, untruncated output
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn prompt_user_yes_no(&self, message: &str) -> bool {
|
||||||
|
// In machine mode, we can't interactively prompt, so we log the request and return true
|
||||||
|
// to allow automation to proceed.
|
||||||
|
println!("PROMPT_USER_YES_NO: {}", message);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt_user_choice(&self, message: &str, options: &[&str]) -> usize {
|
||||||
|
println!("PROMPT_USER_CHOICE: {}", message);
|
||||||
|
println!("OPTIONS: {:?}", options);
|
||||||
|
// Default to first option (index 0) for automation
|
||||||
|
0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -343,5 +343,40 @@ impl UiWriter for ConsoleUiWriter {
|
|||||||
fn flush(&self) {
|
fn flush(&self) {
|
||||||
let _ = io::stdout().flush();
|
let _ = io::stdout().flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn prompt_user_yes_no(&self, message: &str) -> bool {
|
||||||
|
print!("{} [y/N] ", message);
|
||||||
|
let _ = io::stdout().flush();
|
||||||
|
|
||||||
|
let mut input = String::new();
|
||||||
|
if io::stdin().read_line(&mut input).is_ok() {
|
||||||
|
let trimmed = input.trim().to_lowercase();
|
||||||
|
trimmed == "y" || trimmed == "yes"
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt_user_choice(&self, message: &str, options: &[&str]) -> usize {
|
||||||
|
println!("{} ", message);
|
||||||
|
for (i, option) in options.iter().enumerate() {
|
||||||
|
println!(" [{}] {}", i + 1, option);
|
||||||
|
}
|
||||||
|
print!("Select an option (1-{}): ", options.len());
|
||||||
|
let _ = io::stdout().flush();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let mut input = String::new();
|
||||||
|
if io::stdin().read_line(&mut input).is_ok() {
|
||||||
|
if let Ok(choice) = input.trim().parse::<usize>() {
|
||||||
|
if choice > 0 && choice <= options.len() {
|
||||||
|
return choice - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
print!("Invalid choice. Please select (1-{}): ", options.len());
|
||||||
|
let _ = io::stdout().flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,11 +36,20 @@ fn main() {
|
|||||||
// Copy the dylib to the output directory so it can be found at runtime
|
// Copy the dylib to the output directory so it can be found at runtime
|
||||||
let target_dir = manifest_dir.parent().unwrap().parent().unwrap().join("target");
|
let target_dir = manifest_dir.parent().unwrap().parent().unwrap().join("target");
|
||||||
let profile = env::var("PROFILE").unwrap_or_else(|_| "debug".to_string());
|
let profile = env::var("PROFILE").unwrap_or_else(|_| "debug".to_string());
|
||||||
let output_dir = target_dir.join(&profile);
|
|
||||||
|
// Determine the actual target directory (could be llvm-cov-target or regular target)
|
||||||
|
let target_dir_name = env::var("CARGO_TARGET_DIR")
|
||||||
|
.unwrap_or_else(|_| target_dir.to_string_lossy().to_string());
|
||||||
|
let actual_target_dir = PathBuf::from(&target_dir_name);
|
||||||
|
let output_dir = actual_target_dir.join(&profile);
|
||||||
|
|
||||||
let dylib_src = lib_path.join("libVisionBridge.dylib");
|
let dylib_src = lib_path.join("libVisionBridge.dylib");
|
||||||
let dylib_dst = output_dir.join("libVisionBridge.dylib");
|
let dylib_dst = output_dir.join("libVisionBridge.dylib");
|
||||||
|
|
||||||
|
// Create output directory if it doesn't exist
|
||||||
|
std::fs::create_dir_all(&output_dir)
|
||||||
|
.expect(&format!("Failed to create output directory {}", output_dir.display()));
|
||||||
|
|
||||||
std::fs::copy(&dylib_src, &dylib_dst)
|
std::fs::copy(&dylib_src, &dylib_dst)
|
||||||
.expect(&format!("Failed to copy dylib from {} to {}", dylib_src.display(), dylib_dst.display()));
|
.expect(&format!("Failed to copy dylib from {} to {}", dylib_src.display(), dylib_dst.display()));
|
||||||
|
|
||||||
|
|||||||
@@ -15,3 +15,4 @@ dirs = "5.0"
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.8"
|
tempfile = "3.8"
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
|||||||
@@ -70,10 +70,17 @@ pub struct AgentConfig {
|
|||||||
pub max_context_length: Option<u32>,
|
pub max_context_length: Option<u32>,
|
||||||
pub fallback_default_max_tokens: usize,
|
pub fallback_default_max_tokens: usize,
|
||||||
pub enable_streaming: bool,
|
pub enable_streaming: bool,
|
||||||
|
pub allow_multiple_tool_calls: bool,
|
||||||
pub timeout_seconds: u64,
|
pub timeout_seconds: u64,
|
||||||
pub auto_compact: bool,
|
pub auto_compact: bool,
|
||||||
pub max_retry_attempts: u32,
|
pub max_retry_attempts: u32,
|
||||||
pub autonomous_max_retry_attempts: u32,
|
pub autonomous_max_retry_attempts: u32,
|
||||||
|
#[serde(default = "default_check_todo_staleness")]
|
||||||
|
pub check_todo_staleness: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_check_todo_staleness() -> bool {
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -145,10 +152,12 @@ impl Default for Config {
|
|||||||
max_context_length: None,
|
max_context_length: None,
|
||||||
fallback_default_max_tokens: 8192,
|
fallback_default_max_tokens: 8192,
|
||||||
enable_streaming: true,
|
enable_streaming: true,
|
||||||
|
allow_multiple_tool_calls: false,
|
||||||
timeout_seconds: 60,
|
timeout_seconds: 60,
|
||||||
auto_compact: true,
|
auto_compact: true,
|
||||||
max_retry_attempts: 3,
|
max_retry_attempts: 3,
|
||||||
autonomous_max_retry_attempts: 6,
|
autonomous_max_retry_attempts: 6,
|
||||||
|
check_todo_staleness: true,
|
||||||
},
|
},
|
||||||
computer_control: ComputerControlConfig::default(),
|
computer_control: ComputerControlConfig::default(),
|
||||||
webdriver: WebDriverConfig::default(),
|
webdriver: WebDriverConfig::default(),
|
||||||
@@ -265,10 +274,12 @@ impl Config {
|
|||||||
max_context_length: None,
|
max_context_length: None,
|
||||||
fallback_default_max_tokens: 8192,
|
fallback_default_max_tokens: 8192,
|
||||||
enable_streaming: true,
|
enable_streaming: true,
|
||||||
|
allow_multiple_tool_calls: false,
|
||||||
timeout_seconds: 60,
|
timeout_seconds: 60,
|
||||||
auto_compact: true,
|
auto_compact: true,
|
||||||
max_retry_attempts: 3,
|
max_retry_attempts: 3,
|
||||||
autonomous_max_retry_attempts: 6,
|
autonomous_max_retry_attempts: 6,
|
||||||
|
check_todo_staleness: true,
|
||||||
},
|
},
|
||||||
computer_control: ComputerControlConfig::default(),
|
computer_control: ComputerControlConfig::default(),
|
||||||
webdriver: WebDriverConfig::default(),
|
webdriver: WebDriverConfig::default(),
|
||||||
|
|||||||
40
crates/g3-config/tests/test_multiple_tool_calls.rs
Normal file
40
crates/g3-config/tests/test_multiple_tool_calls.rs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod test_multiple_tool_calls {
|
||||||
|
use g3_config::{Config, AgentConfig};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_config_has_multiple_tool_calls_field() {
|
||||||
|
let config = Config::default();
|
||||||
|
|
||||||
|
// Test that the field exists and defaults to false
|
||||||
|
assert_eq!(config.agent.allow_multiple_tool_calls, false);
|
||||||
|
|
||||||
|
// Test that we can create a config with the field set to true
|
||||||
|
let mut custom_config = Config::default();
|
||||||
|
custom_config.agent.allow_multiple_tool_calls = true;
|
||||||
|
assert_eq!(custom_config.agent.allow_multiple_tool_calls, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_agent_config_serialization() {
|
||||||
|
let agent_config = AgentConfig {
|
||||||
|
max_context_length: Some(100000),
|
||||||
|
fallback_default_max_tokens: 8192,
|
||||||
|
enable_streaming: true,
|
||||||
|
allow_multiple_tool_calls: true,
|
||||||
|
timeout_seconds: 60,
|
||||||
|
auto_compact: true,
|
||||||
|
max_retry_attempts: 3,
|
||||||
|
autonomous_max_retry_attempts: 6,
|
||||||
|
check_todo_staleness: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test serialization
|
||||||
|
let json = serde_json::to_string(&agent_config).unwrap();
|
||||||
|
assert!(json.contains("\"allow_multiple_tool_calls\":true"));
|
||||||
|
|
||||||
|
// Test deserialization
|
||||||
|
let deserialized: AgentConfig = serde_json::from_str(&json).unwrap();
|
||||||
|
assert_eq!(deserialized.allow_multiple_tool_calls, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,9 @@ authors = ["G3 Team"]
|
|||||||
description = "Web console for monitoring and managing g3 instances"
|
description = "Web console for monitoring and managing g3 instances"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "g3-console"
|
name = "g3-console"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|||||||
5
crates/g3-console/src/lib.rs
Normal file
5
crates/g3-console/src/lib.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod api;
|
||||||
|
pub mod logs;
|
||||||
|
pub mod models;
|
||||||
|
pub mod process;
|
||||||
|
pub mod launch;
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
mod api;
|
use g3_console::api;
|
||||||
mod logs;
|
use g3_console::process;
|
||||||
mod models;
|
use g3_console::launch;
|
||||||
mod process;
|
|
||||||
mod launch;
|
|
||||||
|
|
||||||
use api::control::{kill_instance, launch_instance, restart_instance};
|
use api::control::{kill_instance, launch_instance, restart_instance};
|
||||||
use api::instances::{get_instance, get_file_content, list_instances};
|
use api::instances::{get_instance, get_file_content, list_instances};
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ pub async fn another_async(x: i32) -> Result<(), ()> {
|
|||||||
println!("{}\n", "=".repeat(80));
|
println!("{}\n", "=".repeat(80));
|
||||||
|
|
||||||
let mut parser = Parser::new();
|
let mut parser = Parser::new();
|
||||||
let language: Language = tree_sitter_rust::language().into();
|
let language: Language = tree_sitter_rust::LANGUAGE.into();
|
||||||
parser.set_language(&language)?;
|
parser.set_language(&language)?;
|
||||||
|
|
||||||
let tree = parser.parse(source_code, None).unwrap();
|
let tree = parser.parse(source_code, None).unwrap();
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class MyClass:
|
|||||||
println!("{}\n", "=".repeat(80));
|
println!("{}\n", "=".repeat(80));
|
||||||
|
|
||||||
let mut parser = Parser::new();
|
let mut parser = Parser::new();
|
||||||
let language: Language = tree_sitter_python::language().into();
|
let language: Language = tree_sitter_python::LANGUAGE.into();
|
||||||
parser.set_language(&language)?;
|
parser.set_language(&language)?;
|
||||||
|
|
||||||
let tree = parser.parse(source_code, None).unwrap();
|
let tree = parser.parse(source_code, None).unwrap();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
//! Test Python async query
|
//! Test Python async query
|
||||||
|
|
||||||
use tree_sitter::{Parser, Query, QueryCursor, Language};
|
use tree_sitter::{Parser, Query, QueryCursor, Language};
|
||||||
|
use streaming_iterator::StreamingIterator;
|
||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
fn main() -> anyhow::Result<()> {
|
||||||
let source_code = r#"
|
let source_code = r#"
|
||||||
@@ -12,7 +13,7 @@ async def async_function():
|
|||||||
"#;
|
"#;
|
||||||
|
|
||||||
let mut parser = Parser::new();
|
let mut parser = Parser::new();
|
||||||
let language: Language = tree_sitter_python::language().into();
|
let language: Language = tree_sitter_python::LANGUAGE.into();
|
||||||
parser.set_language(&language)?;
|
parser.set_language(&language)?;
|
||||||
|
|
||||||
let tree = parser.parse(source_code, None).unwrap();
|
let tree = parser.parse(source_code, None).unwrap();
|
||||||
|
|||||||
@@ -27,14 +27,18 @@ use g3_computer_control::WebDriverController;
|
|||||||
use g3_config::Config;
|
use g3_config::Config;
|
||||||
use g3_execution::CodeExecutor;
|
use g3_execution::CodeExecutor;
|
||||||
use g3_providers::{CacheControl, CompletionRequest, Message, MessageRole, ProviderRegistry, Tool};
|
use g3_providers::{CacheControl, CompletionRequest, Message, MessageRole, ProviderRegistry, Tool};
|
||||||
|
use chrono::Local;
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
use std::fs::OpenOptions;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::sync::{Mutex, OnceLock};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
use prompts::{SYSTEM_PROMPT_FOR_NON_NATIVE_TOOL_USE, SYSTEM_PROMPT_FOR_NATIVE_TOOL_USE};
|
use prompts::{SYSTEM_PROMPT_FOR_NON_NATIVE_TOOL_USE, get_system_prompt_for_native};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ToolCall {
|
pub struct ToolCall {
|
||||||
@@ -755,6 +759,7 @@ pub struct Agent<W: UiWriter> {
|
|||||||
macax_controller:
|
macax_controller:
|
||||||
std::sync::Arc<tokio::sync::RwLock<Option<g3_computer_control::MacAxController>>>,
|
std::sync::Arc<tokio::sync::RwLock<Option<g3_computer_control::MacAxController>>>,
|
||||||
tool_call_count: usize,
|
tool_call_count: usize,
|
||||||
|
requirements_sha: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<W: UiWriter> Agent<W> {
|
impl<W: UiWriter> Agent<W> {
|
||||||
@@ -958,7 +963,7 @@ impl<W: UiWriter> Agent<W> {
|
|||||||
|
|
||||||
let system_prompt = if provider_has_native_tool_calling {
|
let system_prompt = if provider_has_native_tool_calling {
|
||||||
// For native tool calling providers, use a more explicit system prompt
|
// For native tool calling providers, use a more explicit system prompt
|
||||||
SYSTEM_PROMPT_FOR_NATIVE_TOOL_USE.to_string()
|
get_system_prompt_for_native(config.agent.allow_multiple_tool_calls)
|
||||||
} else {
|
} else {
|
||||||
// For non-native providers (embedded models), use JSON format instructions
|
// For non-native providers (embedded models), use JSON format instructions
|
||||||
SYSTEM_PROMPT_FOR_NON_NATIVE_TOOL_USE.to_string()
|
SYSTEM_PROMPT_FOR_NON_NATIVE_TOOL_USE.to_string()
|
||||||
@@ -1026,6 +1031,7 @@ impl<W: UiWriter> Agent<W> {
|
|||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
tool_call_count: 0,
|
tool_call_count: 0,
|
||||||
|
requirements_sha: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1214,6 +1220,63 @@ impl<W: UiWriter> Agent<W> {
|
|||||||
Ok(context_length)
|
Ok(context_length)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn tool_log_handle() -> Option<&'static Mutex<std::fs::File>> {
|
||||||
|
static TOOL_LOG: OnceLock<Option<Mutex<std::fs::File>>> = OnceLock::new();
|
||||||
|
|
||||||
|
TOOL_LOG
|
||||||
|
.get_or_init(|| {
|
||||||
|
if let Err(e) = std::fs::create_dir_all("logs") {
|
||||||
|
error!("Failed to create logs directory for tool log: {}", e);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ts = Local::now().format("%Y%m%d_%H%M%S").to_string();
|
||||||
|
let path = format!("logs/tool_calls_{}.log", ts);
|
||||||
|
|
||||||
|
match OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.append(true)
|
||||||
|
.open(&path)
|
||||||
|
{
|
||||||
|
Ok(file) => Some(Mutex::new(file)),
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to open tool log file {}: {}", path, e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log_tool_call(&self, tool_call: &ToolCall, response: &str) {
|
||||||
|
if let Some(handle) = Self::tool_log_handle() {
|
||||||
|
let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
|
||||||
|
let args_str = serde_json::to_string(&tool_call.args)
|
||||||
|
.unwrap_or_else(|_| "<unserializable>".to_string());
|
||||||
|
|
||||||
|
fn sanitize(s: &str) -> String {
|
||||||
|
s.replace('\n', "\\n")
|
||||||
|
}
|
||||||
|
fn truncate(s: &str, limit: usize) -> String {
|
||||||
|
s.chars().take(limit).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
let args_snippet = truncate(&sanitize(&args_str), 80);
|
||||||
|
let response_snippet = truncate(&sanitize(response), 80);
|
||||||
|
|
||||||
|
let tool_field = format!("{:<15}", tool_call.tool);
|
||||||
|
let line = format!(
|
||||||
|
"{} {} {} 🟩 {}\n",
|
||||||
|
timestamp, tool_field, args_snippet, response_snippet
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Ok(mut file) = handle.lock() {
|
||||||
|
let _ = file.write_all(line.as_bytes());
|
||||||
|
let _ = file.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_provider_info(&self) -> Result<(String, String)> {
|
pub fn get_provider_info(&self) -> Result<(String, String)> {
|
||||||
let provider = self.providers.get(None)?;
|
let provider = self.providers.get(None)?;
|
||||||
Ok((provider.name().to_string(), provider.model().to_string()))
|
Ok((provider.name().to_string(), provider.model().to_string()))
|
||||||
@@ -1918,6 +1981,10 @@ impl<W: UiWriter> Agent<W> {
|
|||||||
&self.config
|
&self.config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_requirements_sha(&mut self, sha: String) {
|
||||||
|
self.requirements_sha = Some(sha);
|
||||||
|
}
|
||||||
|
|
||||||
async fn stream_completion(
|
async fn stream_completion(
|
||||||
&mut self,
|
&mut self,
|
||||||
request: CompletionRequest,
|
request: CompletionRequest,
|
||||||
@@ -2091,6 +2158,15 @@ impl<W: UiWriter> Agent<W> {
|
|||||||
"required": ["content"]
|
"required": ["content"]
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
Tool {
|
||||||
|
name: "code_coverage".to_string(),
|
||||||
|
description: "Generate a code coverage report for the entire workspace using cargo llvm-cov. This runs all tests with coverage instrumentation and returns a summary of coverage statistics. Requires llvm-tools-preview and cargo-llvm-cov to be installed (they will be auto-installed if missing).".to_string(),
|
||||||
|
input_schema: json!({
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
"required": []
|
||||||
|
}),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Add code_search tool
|
// Add code_search tool
|
||||||
@@ -2729,8 +2805,12 @@ impl<W: UiWriter> Agent<W> {
|
|||||||
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
|
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
let provider = self.providers.get(None)?;
|
// Get provider info for logging, then drop it to avoid borrow issues
|
||||||
debug!("Got provider: {}", provider.name());
|
let (provider_name, provider_model) = {
|
||||||
|
let provider = self.providers.get(None)?;
|
||||||
|
(provider.name().to_string(), provider.model().to_string())
|
||||||
|
};
|
||||||
|
debug!("Got provider: {}", provider_name);
|
||||||
|
|
||||||
// Create error context for detailed logging
|
// Create error context for detailed logging
|
||||||
let last_prompt = request
|
let last_prompt = request
|
||||||
@@ -2743,8 +2823,8 @@ impl<W: UiWriter> Agent<W> {
|
|||||||
|
|
||||||
let error_context = ErrorContext::new(
|
let error_context = ErrorContext::new(
|
||||||
"stream_completion".to_string(),
|
"stream_completion".to_string(),
|
||||||
provider.name().to_string(),
|
provider_name.clone(),
|
||||||
provider.model().to_string(),
|
provider_model.clone(),
|
||||||
last_prompt,
|
last_prompt,
|
||||||
self.session_id.clone(),
|
self.session_id.clone(),
|
||||||
self.context_window.used_tokens,
|
self.context_window.used_tokens,
|
||||||
@@ -2757,8 +2837,8 @@ impl<W: UiWriter> Agent<W> {
|
|||||||
|
|
||||||
// Log initial request details
|
// Log initial request details
|
||||||
debug!("Starting stream with provider={}, model={}, messages={}, tools={}, max_tokens={:?}",
|
debug!("Starting stream with provider={}, model={}, messages={}, tools={}, max_tokens={:?}",
|
||||||
provider.name(),
|
provider_name,
|
||||||
provider.model(),
|
provider_model,
|
||||||
request.messages.len(),
|
request.messages.len(),
|
||||||
request.tools.is_some(),
|
request.tools.is_some(),
|
||||||
request.max_tokens
|
request.max_tokens
|
||||||
@@ -2848,10 +2928,125 @@ impl<W: UiWriter> Agent<W> {
|
|||||||
// Process chunk with the new parser
|
// Process chunk with the new parser
|
||||||
let completed_tools = parser.process_chunk(&chunk);
|
let completed_tools = parser.process_chunk(&chunk);
|
||||||
|
|
||||||
// Handle completed tool calls
|
// Handle completed tool calls - process all if multiple calls enabled
|
||||||
if let Some(tool_call) = completed_tools.into_iter().next() {
|
let tools_to_process: Vec<ToolCall> = if self.config.agent.allow_multiple_tool_calls {
|
||||||
|
completed_tools
|
||||||
|
} else {
|
||||||
|
// Original behavior - only take the first tool
|
||||||
|
completed_tools.into_iter().take(1).collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to check if two tool calls are duplicates
|
||||||
|
let are_duplicates = |tc1: &ToolCall, tc2: &ToolCall| -> bool {
|
||||||
|
tc1.tool == tc2.tool && tc1.args == tc2.args
|
||||||
|
};
|
||||||
|
|
||||||
|
// De-duplicate tool calls and track duplicates
|
||||||
|
let mut seen_in_chunk: Vec<ToolCall> = Vec::new();
|
||||||
|
let mut deduplicated_tools: Vec<(ToolCall, Option<String>)> = Vec::new();
|
||||||
|
|
||||||
|
for tool_call in tools_to_process {
|
||||||
|
let mut duplicate_type = None;
|
||||||
|
|
||||||
|
// Check for duplicates in current chunk
|
||||||
|
if seen_in_chunk.iter().any(|tc| are_duplicates(tc, &tool_call)) {
|
||||||
|
duplicate_type = Some("DUP IN CHUNK".to_string());
|
||||||
|
} else {
|
||||||
|
// Check for duplicate against previous message in history
|
||||||
|
// Look at the last assistant message that contains tool calls
|
||||||
|
let mut found_in_prev = false;
|
||||||
|
for msg in self.context_window.conversation_history.iter().rev() {
|
||||||
|
if matches!(msg.role, MessageRole::Assistant) {
|
||||||
|
// Try to parse tool calls from the message content
|
||||||
|
if msg.content.contains(r#"\"tool\""#) {
|
||||||
|
// Simple JSON extraction for tool calls
|
||||||
|
let content = &msg.content;
|
||||||
|
let mut start_idx = 0;
|
||||||
|
while let Some(tool_start) = content[start_idx..].find(r#"{\"tool\""#) {
|
||||||
|
let tool_start = start_idx + tool_start;
|
||||||
|
// Find the end of this JSON object
|
||||||
|
let mut brace_count = 0;
|
||||||
|
let mut in_string = false;
|
||||||
|
let mut escape_next = false;
|
||||||
|
let mut end_idx = tool_start;
|
||||||
|
|
||||||
|
for (i, ch) in content[tool_start..].char_indices() {
|
||||||
|
if escape_next {
|
||||||
|
escape_next = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ch == '\\' && in_string {
|
||||||
|
escape_next = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ch == '"' && !escape_next {
|
||||||
|
in_string = !in_string;
|
||||||
|
}
|
||||||
|
if !in_string {
|
||||||
|
if ch == '{' {
|
||||||
|
brace_count += 1;
|
||||||
|
} else if ch == '}' {
|
||||||
|
brace_count -= 1;
|
||||||
|
if brace_count == 0 {
|
||||||
|
end_idx = tool_start + i + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if end_idx > tool_start {
|
||||||
|
let tool_json = &content[tool_start..end_idx];
|
||||||
|
if let Ok(prev_tool) = serde_json::from_str::<ToolCall>(tool_json) {
|
||||||
|
if are_duplicates(&prev_tool, &tool_call) {
|
||||||
|
found_in_prev = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
start_idx = end_idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Only check the most recent assistant message
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if found_in_prev {
|
||||||
|
duplicate_type = Some("DUP IN MSG".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to seen list if not a duplicate in chunk
|
||||||
|
if duplicate_type.as_ref().map_or(true, |s| s != "DUP IN CHUNK") {
|
||||||
|
seen_in_chunk.push(tool_call.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
deduplicated_tools.push((tool_call, duplicate_type));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process each tool call
|
||||||
|
for (tool_call, duplicate_type) in deduplicated_tools {
|
||||||
debug!("Processing completed tool call: {:?}", tool_call);
|
debug!("Processing completed tool call: {:?}", tool_call);
|
||||||
|
|
||||||
|
// If it's a duplicate, log it and return a warning
|
||||||
|
if let Some(dup_type) = &duplicate_type {
|
||||||
|
// Log the duplicate with red prefix
|
||||||
|
let prefixed_tool_name = format!("🟥 {} {}", tool_call.tool, dup_type);
|
||||||
|
let warning_msg = format!(
|
||||||
|
"⚠️ Duplicate tool call detected ({}): Skipping execution of {} with args {}",
|
||||||
|
dup_type,
|
||||||
|
tool_call.tool,
|
||||||
|
serde_json::to_string(&tool_call.args).unwrap_or_else(|_| "<unserializable>".to_string())
|
||||||
|
);
|
||||||
|
|
||||||
|
// Log to tool log with red prefix
|
||||||
|
let mut modified_tool_call = tool_call.clone();
|
||||||
|
modified_tool_call.tool = prefixed_tool_name;
|
||||||
|
self.log_tool_call(&modified_tool_call, &warning_msg);
|
||||||
|
continue; // Skip execution of duplicate
|
||||||
|
}
|
||||||
|
|
||||||
// Check if we should auto-compact at 90% BEFORE executing the tool
|
// Check if we should auto-compact at 90% BEFORE executing the tool
|
||||||
// We need to do this before any borrows of self
|
// We need to do this before any borrows of self
|
||||||
if self.auto_compact && self.context_window.percentage_used() >= 90.0 {
|
if self.auto_compact && self.context_window.percentage_used() >= 90.0 {
|
||||||
@@ -3140,7 +3335,16 @@ impl<W: UiWriter> Agent<W> {
|
|||||||
current_response.clear();
|
current_response.clear();
|
||||||
// Reset response_started flag for next iteration
|
// Reset response_started flag for next iteration
|
||||||
response_started = false;
|
response_started = false;
|
||||||
break; // Break out of current stream to start a new one
|
|
||||||
|
// For single tool mode, break immediately
|
||||||
|
if !self.config.agent.allow_multiple_tool_calls {
|
||||||
|
break; // Break out of current stream to start a new one
|
||||||
|
}
|
||||||
|
} // End of for loop processing each tool call
|
||||||
|
|
||||||
|
// If we processed any tools in multiple mode, break out to start new stream
|
||||||
|
if tool_executed && self.config.agent.allow_multiple_tool_calls {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no tool calls were completed, continue streaming normally
|
// If no tool calls were completed, continue streaming normally
|
||||||
@@ -3223,8 +3427,8 @@ impl<W: UiWriter> Agent<W> {
|
|||||||
error!("Iteration: {}/{}", iteration_count, MAX_ITERATIONS);
|
error!("Iteration: {}/{}", iteration_count, MAX_ITERATIONS);
|
||||||
error!(
|
error!(
|
||||||
"Provider: {} (model: {})",
|
"Provider: {} (model: {})",
|
||||||
provider.name(),
|
provider_name,
|
||||||
provider.model()
|
provider_model
|
||||||
);
|
);
|
||||||
error!("Chunks received: {}", chunks_received);
|
error!("Chunks received: {}", chunks_received);
|
||||||
error!("Parser state:");
|
error!("Parser state:");
|
||||||
@@ -3504,6 +3708,16 @@ impl<W: UiWriter> Agent<W> {
|
|||||||
// Increment tool call count
|
// Increment tool call count
|
||||||
self.tool_call_count += 1;
|
self.tool_call_count += 1;
|
||||||
|
|
||||||
|
let result = self.execute_tool_inner(tool_call).await;
|
||||||
|
let log_str = match &result {
|
||||||
|
Ok(s) => s.clone(),
|
||||||
|
Err(e) => format!("ERROR: {}", e),
|
||||||
|
};
|
||||||
|
self.log_tool_call(tool_call, &log_str);
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute_tool_inner(&mut self, tool_call: &ToolCall) -> Result<String> {
|
||||||
debug!("=== EXECUTING TOOL ===");
|
debug!("=== EXECUTING TOOL ===");
|
||||||
debug!("Tool name: {}", tool_call.tool);
|
debug!("Tool name: {}", tool_call.tool);
|
||||||
debug!("Tool args (raw): {:?}", tool_call.args);
|
debug!("Tool args (raw): {:?}", tool_call.args);
|
||||||
@@ -4051,6 +4265,56 @@ impl<W: UiWriter> Agent<W> {
|
|||||||
let mut todo = self.todo_content.write().await;
|
let mut todo = self.todo_content.write().await;
|
||||||
*todo = content.clone();
|
*todo = content.clone();
|
||||||
|
|
||||||
|
// Check for staleness if enabled and we have a requirements SHA
|
||||||
|
if self.config.agent.check_todo_staleness {
|
||||||
|
if let Some(req_sha) = &self.requirements_sha {
|
||||||
|
// Parse the first line for the SHA header
|
||||||
|
if let Some(first_line) = content.lines().next() {
|
||||||
|
if first_line.starts_with("{{Based on the requirements file with SHA256:") {
|
||||||
|
let parts: Vec<&str> = first_line.split("SHA256:").collect();
|
||||||
|
if parts.len() > 1 {
|
||||||
|
let todo_sha = parts[1].trim().trim_end_matches("}}").trim();
|
||||||
|
if todo_sha != req_sha {
|
||||||
|
let warning = format!(
|
||||||
|
"⚠️ TODO list is stale! It was generated from a different requirements file.\nExpected SHA: {}\nFound SHA: {}",
|
||||||
|
req_sha, todo_sha
|
||||||
|
);
|
||||||
|
self.ui_writer.print_context_status(&warning);
|
||||||
|
|
||||||
|
// Beep 6 times
|
||||||
|
print!("\x07\x07\x07\x07\x07\x07");
|
||||||
|
let _ = std::io::stdout().flush();
|
||||||
|
|
||||||
|
let options = ["Ignore and Continue", "Mark as Stale", "Quit Application"];
|
||||||
|
let choice = self.ui_writer.prompt_user_choice("Requirements have changed! What would you like to do?", &options);
|
||||||
|
|
||||||
|
match choice {
|
||||||
|
0 => {
|
||||||
|
// Ignore and Continue
|
||||||
|
self.ui_writer.print_context_status("⚠️ Ignoring staleness warning.");
|
||||||
|
}
|
||||||
|
1 => {
|
||||||
|
// Mark as Stale
|
||||||
|
// We return a message to the agent so it knows to regenerate/fix it.
|
||||||
|
return Ok("⚠️ TODO list is stale (requirements changed). Please regenerate the TODO list to match the new requirements.".to_string());
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
// Quit Application
|
||||||
|
self.ui_writer.print_context_status("❌ Quitting application as requested.");
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Header missing, but we have a SHA. Warn the user?
|
||||||
|
// For now, maybe just proceed... assuming it's an old TODO.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if content.trim().is_empty() {
|
if content.trim().is_empty() {
|
||||||
Ok("📝 TODO list is empty".to_string())
|
Ok("📝 TODO list is empty".to_string())
|
||||||
} else {
|
} else {
|
||||||
@@ -4097,6 +4361,46 @@ impl<W: UiWriter> Agent<W> {
|
|||||||
Ok("❌ Missing content argument".to_string())
|
Ok("❌ Missing content argument".to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"code_coverage" => {
|
||||||
|
debug!("Processing code_coverage tool call");
|
||||||
|
self.ui_writer.print_context_status("🔍 Generating code coverage report...");
|
||||||
|
|
||||||
|
// Ensure coverage tools are installed
|
||||||
|
match g3_execution::ensure_coverage_tools_installed() {
|
||||||
|
Ok(already_installed) => {
|
||||||
|
if !already_installed {
|
||||||
|
self.ui_writer.print_context_status("✅ Coverage tools installed successfully");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return Ok(format!("❌ Failed to install coverage tools: {}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run cargo llvm-cov --workspace
|
||||||
|
let output = std::process::Command::new("cargo")
|
||||||
|
.args(&["llvm-cov", "--workspace"])
|
||||||
|
.current_dir(std::env::current_dir()?)
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
|
||||||
|
// Combine output
|
||||||
|
let mut result = String::from("✅ Code coverage report generated successfully\n\n");
|
||||||
|
result.push_str("## Coverage Summary\n");
|
||||||
|
result.push_str(&stdout);
|
||||||
|
if !stderr.is_empty() {
|
||||||
|
result.push_str("\n## Warnings\n");
|
||||||
|
result.push_str(&stderr);
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
|
} else {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
Ok(format!("❌ Failed to generate coverage report:\n{}", stderr))
|
||||||
|
}
|
||||||
|
}
|
||||||
"webdriver_start" => {
|
"webdriver_start" => {
|
||||||
debug!("Processing webdriver_start tool call");
|
debug!("Processing webdriver_start tool call");
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,10 @@ Every multi-step task follows this pattern:
|
|||||||
|
|
||||||
Note: todo_write replaces the entire todo.g3.md file, so always read first to preserve content. TODO lists persist across g3 sessions in the workspace directory.
|
Note: todo_write replaces the entire todo.g3.md file, so always read first to preserve content. TODO lists persist across g3 sessions in the workspace directory.
|
||||||
|
|
||||||
|
IMPORTANT: If you are provided with a SHA256 hash of the requirements file, you MUST include it as the very first line of the todo.g3.md file in the following format:
|
||||||
|
`{{Based on the requirements file with SHA256: <SHA>}}`
|
||||||
|
This ensures the TODO list is tracked against the specific version of requirements it was generated from.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
**Example 1: Feature Implementation**
|
**Example 1: Feature Implementation**
|
||||||
@@ -185,7 +189,25 @@ Do not explain what you're going to do - just do it by calling the tools.
|
|||||||
";
|
";
|
||||||
|
|
||||||
pub const SYSTEM_PROMPT_FOR_NATIVE_TOOL_USE: &'static str =
|
pub const SYSTEM_PROMPT_FOR_NATIVE_TOOL_USE: &'static str =
|
||||||
concatcp!(CODING_STYLE, SYSTEM_NATIVE_TOOL_CALLS);
|
concatcp!(SYSTEM_NATIVE_TOOL_CALLS, CODING_STYLE);
|
||||||
|
|
||||||
|
/// Generate system prompt based on whether multiple tool calls are allowed
|
||||||
|
pub fn get_system_prompt_for_native(allow_multiple: bool) -> String {
|
||||||
|
if allow_multiple {
|
||||||
|
// Replace the "ONE tool" instruction with multiple tools instruction
|
||||||
|
let base = SYSTEM_PROMPT_FOR_NATIVE_TOOL_USE.to_string();
|
||||||
|
base.replace(
|
||||||
|
"2. Call the appropriate tool with the required parameters",
|
||||||
|
"2. Call the appropriate tool(s) with the required parameters - you may call multiple tools in parallel when appropriate.
|
||||||
|
<use_parallel_tool_calls>
|
||||||
|
For maximum efficiency, whenever you perform multiple independent operations, invoke all relevant tools simultaneously rather than sequentially. Prioritize calling tools in parallel whenever possible. For example, when reading 3 files, run 3 tool calls in parallel to read all 3 files into context at the same time. When running multiple read-only commands like `ls` or `list_dir`, always run all of the commands in parallel. Err on the side of maximizing parallel tool calls rather than running too many tools sequentially.
|
||||||
|
</use_parallel_tool_calls>
|
||||||
|
"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
SYSTEM_PROMPT_FOR_NATIVE_TOOL_USE.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const SYSTEM_NON_NATIVE_TOOL_USE: &'static str =
|
const SYSTEM_NON_NATIVE_TOOL_USE: &'static str =
|
||||||
"You are G3, a general-purpose AI agent. Your goal is to analyze and solve problems by writing code.
|
"You are G3, a general-purpose AI agent. Your goal is to analyze and solve problems by writing code.
|
||||||
@@ -285,6 +307,10 @@ Every multi-step task follows this pattern:
|
|||||||
|
|
||||||
Note: todo_write replaces the entire list, so always read first to preserve content.
|
Note: todo_write replaces the entire list, so always read first to preserve content.
|
||||||
|
|
||||||
|
IMPORTANT: If you are provided with a SHA256 hash of the requirements file, you MUST include it as the very first line of the todo.g3.md file in the following format:
|
||||||
|
`{{Based on the requirements file with SHA256: <SHA>}}`
|
||||||
|
This ensures the TODO list is tracked against the specific version of requirements it was generated from.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
**Example 1: Feature Implementation**
|
**Example 1: Feature Implementation**
|
||||||
@@ -345,4 +371,4 @@ If you can complete it with 1-2 tool calls, skip TODO.
|
|||||||
";
|
";
|
||||||
|
|
||||||
pub const SYSTEM_PROMPT_FOR_NON_NATIVE_TOOL_USE: &'static str =
|
pub const SYSTEM_PROMPT_FOR_NON_NATIVE_TOOL_USE: &'static str =
|
||||||
concatcp!(CODING_STYLE, SYSTEM_NON_NATIVE_TOOL_USE);
|
concatcp!(SYSTEM_NON_NATIVE_TOOL_USE, CODING_STYLE);
|
||||||
|
|||||||
@@ -56,6 +56,13 @@ pub trait UiWriter: Send + Sync {
|
|||||||
/// Returns true if this UI writer wants full, untruncated output
|
/// Returns true if this UI writer wants full, untruncated output
|
||||||
/// Default is false (truncate for human readability)
|
/// Default is false (truncate for human readability)
|
||||||
fn wants_full_output(&self) -> bool { false }
|
fn wants_full_output(&self) -> bool { false }
|
||||||
|
|
||||||
|
/// Prompt the user for a yes/no confirmation
|
||||||
|
fn prompt_user_yes_no(&self, message: &str) -> bool;
|
||||||
|
|
||||||
|
/// Prompt the user to choose from a list of options
|
||||||
|
/// Returns the index of the selected option
|
||||||
|
fn prompt_user_choice(&self, message: &str, options: &[&str]) -> usize;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A no-op implementation for when UI output is not needed
|
/// A no-op implementation for when UI output is not needed
|
||||||
@@ -80,4 +87,6 @@ impl UiWriter for NullUiWriter {
|
|||||||
fn notify_sse_received(&self) {}
|
fn notify_sse_received(&self) {}
|
||||||
fn flush(&self) {}
|
fn flush(&self) {}
|
||||||
fn wants_full_output(&self) -> bool { false }
|
fn wants_full_output(&self) -> bool { false }
|
||||||
|
fn prompt_user_yes_no(&self, _message: &str) -> bool { true }
|
||||||
|
fn prompt_user_choice(&self, _message: &str, _options: &[&str]) -> usize { 0 }
|
||||||
}
|
}
|
||||||
@@ -551,6 +551,7 @@ async fn test_cpp_search() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[ignore]
|
||||||
async fn test_kotlin_search() {
|
async fn test_kotlin_search() {
|
||||||
let request = CodeSearchRequest {
|
let request = CodeSearchRequest {
|
||||||
searches: vec![SearchSpec {
|
searches: vec![SearchSpec {
|
||||||
|
|||||||
193
crates/g3-core/tests/todo_staleness_test.rs
Normal file
193
crates/g3-core/tests/todo_staleness_test.rs
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
use g3_core::{Agent, ToolCall};
|
||||||
|
use g3_core::ui_writer::UiWriter;
|
||||||
|
use g3_config::Config;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use tempfile::TempDir;
|
||||||
|
use serial_test::serial;
|
||||||
|
|
||||||
|
// Mock UI Writer for testing
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct MockUiWriter {
|
||||||
|
output: Arc<Mutex<Vec<String>>>,
|
||||||
|
prompt_responses: Arc<Mutex<Vec<bool>>>,
|
||||||
|
choice_responses: Arc<Mutex<Vec<usize>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MockUiWriter {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
output: Arc::new(Mutex::new(Vec::new())),
|
||||||
|
prompt_responses: Arc::new(Mutex::new(Vec::new())),
|
||||||
|
choice_responses: Arc::new(Mutex::new(Vec::new())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_prompt_response(&self, response: bool) {
|
||||||
|
self.prompt_responses.lock().unwrap().push(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_choice_response(&self, response: usize) {
|
||||||
|
self.choice_responses.lock().unwrap().push(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_output(&self) -> Vec<String> {
|
||||||
|
self.output.lock().unwrap().clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UiWriter for MockUiWriter {
|
||||||
|
fn print(&self, message: &str) {
|
||||||
|
self.output.lock().unwrap().push(message.to_string());
|
||||||
|
}
|
||||||
|
fn println(&self, message: &str) {
|
||||||
|
self.output.lock().unwrap().push(message.to_string());
|
||||||
|
}
|
||||||
|
fn print_inline(&self, message: &str) {
|
||||||
|
self.output.lock().unwrap().push(message.to_string());
|
||||||
|
}
|
||||||
|
fn print_system_prompt(&self, _prompt: &str) {}
|
||||||
|
fn print_context_status(&self, message: &str) {
|
||||||
|
self.output.lock().unwrap().push(format!("STATUS: {}", message));
|
||||||
|
}
|
||||||
|
fn print_context_thinning(&self, _message: &str) {}
|
||||||
|
fn print_tool_header(&self, _tool_name: &str) {}
|
||||||
|
fn print_tool_arg(&self, _key: &str, _value: &str) {}
|
||||||
|
fn print_tool_output_header(&self) {}
|
||||||
|
fn update_tool_output_line(&self, _line: &str) {}
|
||||||
|
fn print_tool_output_line(&self, _line: &str) {}
|
||||||
|
fn print_tool_output_summary(&self, _hidden_count: usize) {}
|
||||||
|
fn print_tool_timing(&self, _duration_str: &str) {}
|
||||||
|
fn print_agent_prompt(&self) {}
|
||||||
|
fn print_agent_response(&self, _content: &str) {}
|
||||||
|
fn notify_sse_received(&self) {}
|
||||||
|
fn flush(&self) {}
|
||||||
|
fn wants_full_output(&self) -> bool { false }
|
||||||
|
fn prompt_user_yes_no(&self, message: &str) -> bool {
|
||||||
|
self.output.lock().unwrap().push(format!("PROMPT: {}", message));
|
||||||
|
self.prompt_responses.lock().unwrap().pop().unwrap_or(true)
|
||||||
|
}
|
||||||
|
fn prompt_user_choice(&self, message: &str, options: &[&str]) -> usize {
|
||||||
|
self.output.lock().unwrap().push(format!("CHOICE: {} Options: {:?}", message, options));
|
||||||
|
self.choice_responses.lock().unwrap().pop().unwrap_or(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn test_todo_staleness_check_matching_sha() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let todo_path = temp_dir.path().join("todo.g3.md");
|
||||||
|
std::env::set_current_dir(&temp_dir).unwrap();
|
||||||
|
|
||||||
|
let sha = "abc123hash";
|
||||||
|
let content = format!("{{{{Based on the requirements file with SHA256: {}}}}}\n- [ ] Task 1", sha);
|
||||||
|
std::fs::write(&todo_path, content).unwrap();
|
||||||
|
|
||||||
|
let mut config = Config::default();
|
||||||
|
config.agent.check_todo_staleness = true;
|
||||||
|
|
||||||
|
let ui_writer = MockUiWriter::new();
|
||||||
|
let mut agent = Agent::new_autonomous(config, ui_writer).await.unwrap();
|
||||||
|
agent.set_requirements_sha(sha.to_string());
|
||||||
|
|
||||||
|
let tool_call = ToolCall {
|
||||||
|
tool: "todo_read".to_string(),
|
||||||
|
args: serde_json::json!({}),
|
||||||
|
};
|
||||||
|
let result = agent.execute_tool(&tool_call).await.unwrap();
|
||||||
|
|
||||||
|
assert!(result.contains("📝 TODO list:"));
|
||||||
|
assert!(!result.contains("⚠️ TODO list is stale"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn test_todo_staleness_check_mismatch_sha_ignore() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let todo_path = temp_dir.path().join("todo.g3.md");
|
||||||
|
std::env::set_current_dir(&temp_dir).unwrap();
|
||||||
|
|
||||||
|
let sha_file = "old_sha";
|
||||||
|
let sha_req = "new_sha";
|
||||||
|
let content = format!("{{{{Based on the requirements file with SHA256: {}}}}}\n- [ ] Task 1", sha_file);
|
||||||
|
std::fs::write(&todo_path, content).unwrap();
|
||||||
|
|
||||||
|
let mut config = Config::default();
|
||||||
|
config.agent.check_todo_staleness = true;
|
||||||
|
|
||||||
|
let ui_writer = MockUiWriter::new();
|
||||||
|
ui_writer.set_choice_response(0); // Ignore
|
||||||
|
|
||||||
|
let mut agent = Agent::new_autonomous(config, ui_writer).await.unwrap();
|
||||||
|
agent.set_requirements_sha(sha_req.to_string());
|
||||||
|
|
||||||
|
let tool_call = ToolCall {
|
||||||
|
tool: "todo_read".to_string(),
|
||||||
|
args: serde_json::json!({}),
|
||||||
|
};
|
||||||
|
let result = agent.execute_tool(&tool_call).await.unwrap();
|
||||||
|
|
||||||
|
assert!(result.contains("📝 TODO list:"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn test_todo_staleness_check_mismatch_sha_mark_stale() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let todo_path = temp_dir.path().join("todo.g3.md");
|
||||||
|
std::env::set_current_dir(&temp_dir).unwrap();
|
||||||
|
|
||||||
|
let sha_file = "old_sha";
|
||||||
|
let sha_req = "new_sha";
|
||||||
|
let content = format!("{{{{Based on the requirements file with SHA256: {}}}}}\n- [ ] Task 1", sha_file);
|
||||||
|
std::fs::write(&todo_path, content).unwrap();
|
||||||
|
|
||||||
|
let mut config = Config::default();
|
||||||
|
config.agent.check_todo_staleness = true;
|
||||||
|
|
||||||
|
let ui_writer = MockUiWriter::new();
|
||||||
|
ui_writer.set_choice_response(1); // Mark as Stale
|
||||||
|
|
||||||
|
let mut agent = Agent::new_autonomous(config, ui_writer).await.unwrap();
|
||||||
|
agent.set_requirements_sha(sha_req.to_string());
|
||||||
|
|
||||||
|
let tool_call = ToolCall {
|
||||||
|
tool: "todo_read".to_string(),
|
||||||
|
args: serde_json::json!({}),
|
||||||
|
};
|
||||||
|
let result = agent.execute_tool(&tool_call).await.unwrap();
|
||||||
|
|
||||||
|
assert!(result.contains("⚠️ TODO list is stale"));
|
||||||
|
assert!(result.contains("Please regenerate"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: We cannot easily test "Quit" (index 2) because it calls std::process::exit(0)
|
||||||
|
// which would kill the test runner. We skip that test case here.
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn test_todo_staleness_check_disabled() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let todo_path = temp_dir.path().join("todo.g3.md");
|
||||||
|
std::env::set_current_dir(&temp_dir).unwrap();
|
||||||
|
|
||||||
|
let sha_file = "old_sha";
|
||||||
|
let sha_req = "new_sha";
|
||||||
|
let content = format!("{{{{Based on the requirements file with SHA256: {}}}}}\n- [ ] Task 1", sha_file);
|
||||||
|
std::fs::write(&todo_path, content).unwrap();
|
||||||
|
|
||||||
|
let mut config = Config::default();
|
||||||
|
config.agent.check_todo_staleness = false;
|
||||||
|
|
||||||
|
let ui_writer = MockUiWriter::new();
|
||||||
|
let mut agent = Agent::new_autonomous(config, ui_writer).await.unwrap();
|
||||||
|
agent.set_requirements_sha(sha_req.to_string());
|
||||||
|
|
||||||
|
let tool_call = ToolCall {
|
||||||
|
tool: "todo_read".to_string(),
|
||||||
|
args: serde_json::json!({}),
|
||||||
|
};
|
||||||
|
let result = agent.execute_tool(&tool_call).await.unwrap();
|
||||||
|
|
||||||
|
assert!(result.contains("📝 TODO list:"));
|
||||||
|
}
|
||||||
13
crates/g3-execution/examples/setup_coverage_tools.rs
Normal file
13
crates/g3-execution/examples/setup_coverage_tools.rs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
use g3_execution::ensure_coverage_tools_installed;
|
||||||
|
|
||||||
|
fn main() -> anyhow::Result<()> {
|
||||||
|
// Ensure coverage tools are installed
|
||||||
|
let already_installed = ensure_coverage_tools_installed()?;
|
||||||
|
|
||||||
|
if already_installed {
|
||||||
|
println!("All coverage tools are already installed!");
|
||||||
|
} else {
|
||||||
|
println!("Coverage tools have been installed successfully!");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -330,3 +330,87 @@ impl CodeExecutor {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if rustup component llvm-tools-preview is installed
|
||||||
|
pub fn is_llvm_tools_installed() -> Result<bool> {
|
||||||
|
let output = Command::new("rustup")
|
||||||
|
.args(&["component", "list", "--installed"])
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
let installed = String::from_utf8_lossy(&output.stdout)
|
||||||
|
.lines()
|
||||||
|
.any(|line| line.trim() == "llvm-tools-preview" || line.starts_with("llvm-tools"));
|
||||||
|
|
||||||
|
Ok(installed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if cargo-llvm-cov is installed
|
||||||
|
pub fn is_cargo_llvm_cov_installed() -> Result<bool> {
|
||||||
|
let output = Command::new("cargo")
|
||||||
|
.args(&["--list"])
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
let installed = String::from_utf8_lossy(&output.stdout)
|
||||||
|
.lines()
|
||||||
|
.any(|line| line.trim().starts_with("llvm-cov"));
|
||||||
|
|
||||||
|
Ok(installed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Install llvm-tools-preview via rustup
|
||||||
|
pub fn install_llvm_tools() -> Result<()> {
|
||||||
|
info!("Installing llvm-tools-preview...");
|
||||||
|
let output = Command::new("rustup")
|
||||||
|
.args(&["component", "add", "llvm-tools-preview"])
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
anyhow::bail!("Failed to install llvm-tools-preview: {}", stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("✅ llvm-tools-preview installed successfully");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Install cargo-llvm-cov via cargo install
|
||||||
|
pub fn install_cargo_llvm_cov() -> Result<()> {
|
||||||
|
info!("Installing cargo-llvm-cov... (this may take a few minutes)");
|
||||||
|
let output = Command::new("cargo")
|
||||||
|
.args(&["install", "cargo-llvm-cov"])
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
anyhow::bail!("Failed to install cargo-llvm-cov: {}", stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("✅ cargo-llvm-cov installed successfully");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure both llvm-tools-preview and cargo-llvm-cov are installed
|
||||||
|
/// Returns Ok(true) if tools were already installed, Ok(false) if they were installed by this function
|
||||||
|
pub fn ensure_coverage_tools_installed() -> Result<bool> {
|
||||||
|
let mut already_installed = true;
|
||||||
|
|
||||||
|
// Check and install llvm-tools-preview
|
||||||
|
if !is_llvm_tools_installed()? {
|
||||||
|
info!("llvm-tools-preview not found, installing...");
|
||||||
|
install_llvm_tools()?;
|
||||||
|
already_installed = false;
|
||||||
|
} else {
|
||||||
|
info!("✅ llvm-tools-preview is already installed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check and install cargo-llvm-cov
|
||||||
|
if !is_cargo_llvm_cov_installed()? {
|
||||||
|
info!("cargo-llvm-cov not found, installing...");
|
||||||
|
install_cargo_llvm_cov()?;
|
||||||
|
already_installed = false;
|
||||||
|
} else {
|
||||||
|
info!("✅ cargo-llvm-cov is already installed");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(already_installed)
|
||||||
|
}
|
||||||
|
|||||||
70
tail_tool_logs.sh
Executable file
70
tail_tool_logs.sh
Executable file
@@ -0,0 +1,70 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Useful tool for tailing tool_calls files. It picks up whatever the latest is and does tail -f
|
||||||
|
|
||||||
|
if [[ -n "$G3_WORKSPACE" ]]; then
|
||||||
|
TARGET_DIR="$G3_WORKSPACE/logs"
|
||||||
|
else
|
||||||
|
TARGET_DIR="$HOME/tmp/workspace/logs"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -d "$TARGET_DIR" ]]; then
|
||||||
|
echo "Error: Directory '$TARGET_DIR' does not exist."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$TARGET_DIR" || exit 1
|
||||||
|
|
||||||
|
echo "Monitoring directory '$TARGET_DIR' for newest 'tool_calls*' file..."
|
||||||
|
|
||||||
|
|
||||||
|
# Variables to keep track of the current state
|
||||||
|
CURRENT_PID=""
|
||||||
|
CURRENT_FILE=""
|
||||||
|
|
||||||
|
# Cleanup function: Kill the background tail process when this script is stopped (Ctrl+C)
|
||||||
|
cleanup() {
|
||||||
|
echo ""
|
||||||
|
echo "Stopping monitor..."
|
||||||
|
if [[ -n "$CURRENT_PID" ]]; then
|
||||||
|
kill "$CURRENT_PID" 2>/dev/null
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Register the cleanup function for SIGINT (Ctrl+C) and SIGTERM
|
||||||
|
trap cleanup SIGINT SIGTERM
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
# Find the newest file matching the pattern using ls -t (sort by time)
|
||||||
|
# 2>/dev/null suppresses errors if no files are found
|
||||||
|
NEWEST_FILE=$(ls -t tool_calls* 2>/dev/null | head -n 1)
|
||||||
|
|
||||||
|
# If a file was found AND it is different from the one we are currently watching
|
||||||
|
if [[ -n "$NEWEST_FILE" && "$NEWEST_FILE" != "$CURRENT_FILE" ]]; then
|
||||||
|
|
||||||
|
# If we were already watching a file, kill the old tail process
|
||||||
|
if [[ -n "$CURRENT_PID" ]]; then
|
||||||
|
kill "$CURRENT_PID" 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"
|
||||||
|
echo ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"
|
||||||
|
echo ">>> Switched to new file: $NEWEST_FILE"
|
||||||
|
echo ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"
|
||||||
|
echo ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"
|
||||||
|
|
||||||
|
# Start tail in the background (&)
|
||||||
|
tail -f "$NEWEST_FILE" &
|
||||||
|
|
||||||
|
# Capture the Process ID ($!) of the tail command we just launched
|
||||||
|
CURRENT_PID=$!
|
||||||
|
|
||||||
|
# Update the tracker variable
|
||||||
|
CURRENT_FILE="$NEWEST_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Wait 1 second before checking again
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
Reference in New Issue
Block a user