Compare commits
29 Commits
micn/conso
...
jochen_reo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
467e300ec2 | ||
|
|
f501751bdf | ||
|
|
a96a15d1fc | ||
|
|
24dc7ad642 | ||
|
|
a097c3abef | ||
|
|
34e55050b3 | ||
|
|
551a577ee1 | ||
|
|
84718223bc | ||
|
|
28a83d2dcf | ||
|
|
0ce905dc74 | ||
|
|
9f0d5add1e | ||
|
|
be6c6bfca4 | ||
|
|
94a41c5c34 | ||
|
|
09dbad2d68 | ||
|
|
ffbf410b17 | ||
|
|
c6f3f12b71 | ||
|
|
14c8d066c9 | ||
|
|
e556f06b15 | ||
|
|
b6e226df67 | ||
|
|
5b46922047 | ||
|
|
1069664e16 | ||
|
|
725f54b99b | ||
|
|
325aab6b0e | ||
|
|
3f21bdc7b2 | ||
|
|
9bffd8b1bf | ||
|
|
bfee8040e9 | ||
|
|
a150ba6a55 | ||
|
|
296bf5a449 | ||
|
|
7f73b664a3 |
36
Cargo.lock
generated
36
Cargo.lock
generated
@@ -576,6 +576,26 @@ dependencies = [
|
|||||||
"tiny-keccak",
|
"tiny-keccak",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const_format"
|
||||||
|
version = "0.2.35"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad"
|
||||||
|
dependencies = [
|
||||||
|
"const_format_proc_macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const_format_proc_macros"
|
||||||
|
version = "0.2.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-xid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "convert_case"
|
name = "convert_case"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@@ -1345,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",
|
||||||
@@ -1389,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",
|
||||||
@@ -1427,6 +1450,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"const_format",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"g3-computer-control",
|
"g3-computer-control",
|
||||||
"g3-config",
|
"g3-config",
|
||||||
@@ -1631,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"
|
||||||
@@ -4090,6 +4120,12 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-xid"
|
||||||
|
version = "0.2.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unsafe-libyaml"
|
name = "unsafe-libyaml"
|
||||||
version = "0.2.11"
|
version = "0.2.11"
|
||||||
|
|||||||
@@ -11,14 +11,27 @@ model = "databricks-claude-sonnet-4"
|
|||||||
max_tokens = 4096
|
max_tokens = 4096
|
||||||
temperature = 0.1
|
temperature = 0.1
|
||||||
use_oauth = true
|
use_oauth = true
|
||||||
|
# cache_config = "ephemeral" # Optional: Enable prompt caching for Claude models
|
||||||
|
# Options: "ephemeral", "5minute", "1hour"
|
||||||
|
# Reduces costs and latency for repeated prompts. Uses Anthropic's prompt caching with different TTLs.
|
||||||
|
# The cache control will be automatically applied to:
|
||||||
|
# - The system prompt at the start of each session
|
||||||
|
# - Assistant responses after every 10 tool calls
|
||||||
|
# - 5minute costs $3/mtok, more details below
|
||||||
|
# https://docs.claude.com/en/docs/build-with-claude/prompt-caching#pricing
|
||||||
|
|
||||||
[providers.anthropic]
|
[providers.anthropic]
|
||||||
api_key = "your-anthropic-api-key"
|
api_key = "your-anthropic-api-key"
|
||||||
model = "claude-3-haiku-20240307" # Using a faster model for player
|
model = "claude-sonnet-4-5"
|
||||||
max_tokens = 4096
|
max_tokens = 4096
|
||||||
temperature = 0.3 # Slightly higher temperature for more creative implementations
|
temperature = 0.3 # Slightly higher temperature for more creative implementations
|
||||||
|
# cache_config = "ephemeral" # Optional: Enable prompt caching
|
||||||
|
# Options: "ephemeral", "5minute", "1hour"
|
||||||
|
# Reduces costs and latency for repeated prompts. Uses Anthropic's prompt caching with different TTLs.
|
||||||
|
# enable_1m_context = true # optional, more expensive
|
||||||
|
|
||||||
[agent]
|
[agent]
|
||||||
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
|
||||||
@@ -15,6 +15,17 @@ max_tokens = 4096 # Per-request output limit (how many tokens the model can gen
|
|||||||
temperature = 0.1
|
temperature = 0.1
|
||||||
use_oauth = true
|
use_oauth = true
|
||||||
|
|
||||||
|
[providers.anthropic]
|
||||||
|
api_key = "your-anthropic-api-key"
|
||||||
|
model = "claude-sonnet-4-5"
|
||||||
|
max_tokens = 4096
|
||||||
|
temperature = 0.3 # Slightly higher temperature for more creative implementations
|
||||||
|
# cache_config = "ephemeral" # Optional: Enable prompt caching
|
||||||
|
# Options: "ephemeral", "5minute", "1hour"
|
||||||
|
# Reduces costs and latency for repeated prompts. Uses Anthropic's prompt caching with different TTLs.
|
||||||
|
# enable_1m_context = true # optional, more expensive
|
||||||
|
|
||||||
|
|
||||||
# Multiple OpenAI-compatible providers can be configured with custom names
|
# Multiple OpenAI-compatible providers can be configured with custom names
|
||||||
# Each provider gets its own section under [providers.openai_compatible.<name>]
|
# Each provider gets its own section under [providers.openai_compatible.<name>]
|
||||||
# [providers.openai_compatible.openrouter]
|
# [providers.openai_compatible.openrouter]
|
||||||
@@ -46,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
|
||||||
@@ -1686,11 +1698,14 @@ async fn run_autonomous(
|
|||||||
turn, max_turns
|
turn, max_turns
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// Surface provider info for player agent
|
||||||
|
agent.print_provider_banner("Player");
|
||||||
|
|
||||||
// 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!(
|
||||||
@@ -1879,6 +1894,9 @@ async fn run_autonomous(
|
|||||||
let mut coach_agent =
|
let mut coach_agent =
|
||||||
Agent::new_autonomous_with_readme_and_quiet(coach_config, ui_writer, None, quiet).await?;
|
Agent::new_autonomous_with_readme_and_quiet(coach_config, ui_writer, None, quiet).await?;
|
||||||
|
|
||||||
|
// Surface provider info for coach agent
|
||||||
|
coach_agent.print_provider_banner("Coach");
|
||||||
|
|
||||||
// Ensure coach agent is also in the workspace directory
|
// Ensure coach agent is also in the workspace directory
|
||||||
project.enter_workspace()?;
|
project.enter_workspace()?;
|
||||||
|
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ pub struct AnthropicConfig {
|
|||||||
pub model: String,
|
pub model: String,
|
||||||
pub max_tokens: Option<u32>,
|
pub max_tokens: Option<u32>,
|
||||||
pub temperature: Option<f32>,
|
pub temperature: Option<f32>,
|
||||||
|
pub cache_config: Option<String>, // "ephemeral", "5minute", "1hour", or None to disable
|
||||||
|
pub enable_1m_context: Option<bool>, // Enable 1m context window (costs extra)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -68,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)]
|
||||||
@@ -143,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(),
|
||||||
@@ -263,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};
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ tree-sitter-scheme = "0.24"
|
|||||||
streaming-iterator = "0.1"
|
streaming-iterator = "0.1"
|
||||||
walkdir = "2.4"
|
walkdir = "2.4"
|
||||||
|
|
||||||
|
const_format = "0.2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.8"
|
tempfile = "3.8"
|
||||||
serial_test = "3.0"
|
serial_test = "3.0"
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
374
crates/g3-core/src/prompts.rs
Normal file
374
crates/g3-core/src/prompts.rs
Normal file
@@ -0,0 +1,374 @@
|
|||||||
|
use const_format::concatcp;
|
||||||
|
const CODING_STYLE: &'static str = "# IMPORTANT FOR CODING:
|
||||||
|
It is very important that you adhere to these principles when writing code. I will use a code quality tool to assess the code you have generated.
|
||||||
|
|
||||||
|
### Most important for coding: Specific guideline for code design:
|
||||||
|
|
||||||
|
- Functions and methods should be short - at most 80 lines, ideally under 40.
|
||||||
|
- Classes should be modular and composable. They should not have more than 20 methods.
|
||||||
|
- Do not write deeply nested (above 6 levels deep) ‘if’, ‘match’ or ‘case’ statements, rather refactor into separate logical sections or functions.
|
||||||
|
- Code should be written such that it is maintainable and testable.
|
||||||
|
- For Rust code write *ALL* test code into a ‘tests’ directory that is a peer to the ‘src’ of each crate, and is for testing code in that crate.
|
||||||
|
- For Python code write *ALL* test code into a top level ‘tests’ directory.
|
||||||
|
- Each non-trivial function should have test coverage. DO NOT WRITE TESTS FOR INDIVIDUAL FUNCTIONS / METHODS / CLASSES unless they are large and important. Instead write something
|
||||||
|
at a higher level of abstraction, closer to an integration test.
|
||||||
|
- Write tests in separate files, where the filename should match the main implementation and adding a “_test” suffix.
|
||||||
|
|
||||||
|
### Important for coding: General guidelines for code design:
|
||||||
|
|
||||||
|
Keep the code as simple as possible, with few if any external dependencies.
|
||||||
|
DRY (Don’t repeat yourself) - each small piece code may only occur exactly once in the entire system.
|
||||||
|
KISS (Keep it simple, stupid!) - keep each small piece of software simple and unnecessary complexity should be avoided.
|
||||||
|
YAGNI (You ain’t gonna need it) - Always implement things when you actually need them never implements things before you need them.
|
||||||
|
|
||||||
|
Use Descriptive Names for Code Elements. - As a rule of thumb, use more descriptive names for larger scopes. e.g., name a loop counter variable “i” is good when the scope of the loop is a single line. But don’t name some class field or method parameter “i”.
|
||||||
|
|
||||||
|
When modifying an existing code base, do not unnecessarily refactor or modify code that is not directly relevant to the current coding task. It is fine to do so if new code calls/is called by the new functionality, or you prevent code duplication when new functionality is added.
|
||||||
|
If possible constrain the side-effects on other pieces of code if possible, this is part of the principle of modularity.
|
||||||
|
|
||||||
|
### Important for coding: General advice on designing algorithms:
|
||||||
|
|
||||||
|
If possible, consider the \"Gang of Four\" design patterns when writing code.
|
||||||
|
|
||||||
|
The Gang of Four (GOF) patterns are set of 23 common software design patterns introduced in the book
|
||||||
|
\"Design Patterns: Elements of Reusable Object-Oriented Software\".
|
||||||
|
|
||||||
|
These patterns categorize into three main groups:
|
||||||
|
|
||||||
|
1. Creational Patterns
|
||||||
|
2. Structural Patterns
|
||||||
|
3. Behavioral Patterns
|
||||||
|
|
||||||
|
These patterns provide solutions to common design problems and help make software systems more modular, flexible and maintainable. Consider using these patterns in your code design.";
|
||||||
|
|
||||||
|
const SYSTEM_NATIVE_TOOL_CALLS: &'static str =
|
||||||
|
"You are G3, an AI programming agent of the same skill level as a seasoned engineer at a major technology company. You analyze given tasks and write code to achieve goals.
|
||||||
|
|
||||||
|
You have access to tools. When you need to accomplish a task, you MUST use the appropriate tool. Do not just describe what you would do - actually use the tools.
|
||||||
|
|
||||||
|
IMPORTANT: You must call tools to achieve goals. When you receive a request:
|
||||||
|
1. Analyze and identify what needs to be done
|
||||||
|
2. Call the appropriate tool with the required parameters
|
||||||
|
3. Continue or complete the task based on the result
|
||||||
|
4. If you repeatedly try something and it fails, try a different approach
|
||||||
|
5. Call the final_output tool with a detailed summary when done.
|
||||||
|
|
||||||
|
For shell commands: Use the shell tool with the exact command needed. Avoid commands that produce a large amount of output, and consider piping those outputs to files. Example: If asked to list files, immediately call the shell tool with command parameter \"ls\".
|
||||||
|
If you create temporary files for verification, place these in a subdir named 'tmp'. Do NOT pollute the current dir.
|
||||||
|
|
||||||
|
# Task Management with TODO Tools
|
||||||
|
|
||||||
|
**REQUIRED for multi-step tasks.** Use TODO tools when your task involves ANY of:
|
||||||
|
- Multiple files to create/modify (2+)
|
||||||
|
- Multiple distinct steps (3+)
|
||||||
|
- Dependencies between steps
|
||||||
|
- Testing or verification needed
|
||||||
|
- Uncertainty about approach
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
Every multi-step task follows this pattern:
|
||||||
|
1. **Start**: Call todo_read, then todo_write to create your plan
|
||||||
|
2. **During**: Execute steps, then todo_read and todo_write to mark progress
|
||||||
|
3. **End**: Call todo_read to verify all items complete
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
**Example 1: Feature Implementation**
|
||||||
|
User asks: \"Add user authentication with tests\"
|
||||||
|
|
||||||
|
First action:
|
||||||
|
{\"tool\": \"todo_read\", \"args\": {}}
|
||||||
|
|
||||||
|
Then create plan:
|
||||||
|
{\"tool\": \"todo_write\", \"args\": {\"content\": \"- [ ] Add user authentication\\n - [ ] Create User struct\\n - [ ] Add login endpoint\\n - [ ] Add password hashing\\n - [ ] Write unit tests\\n - [ ] Write integration tests\"}}
|
||||||
|
|
||||||
|
After completing User struct:
|
||||||
|
{\"tool\": \"todo_read\", \"args\": {}}
|
||||||
|
{\"tool\": \"todo_write\", \"args\": {\"content\": \"- [ ] Add user authentication\\n - [x] Create User struct\\n - [ ] Add login endpoint\\n - [ ] Add password hashing\\n - [ ] Write unit tests\\n - [ ] Write integration tests\"}}
|
||||||
|
|
||||||
|
**Example 2: Bug Fix**
|
||||||
|
User asks: \"Fix the memory leak in cache module\"
|
||||||
|
|
||||||
|
{\"tool\": \"todo_read\", \"args\": {}}
|
||||||
|
{\"tool\": \"todo_write\", \"args\": {\"content\": \"- [ ] Fix memory leak\\n - [ ] Review cache.rs\\n - [ ] Check for unclosed resources\\n - [ ] Add drop implementation\\n - [ ] Write test to verify fix\"}}
|
||||||
|
|
||||||
|
**Example 3: Refactoring**
|
||||||
|
User asks: \"Refactor database layer to use async/await\"
|
||||||
|
|
||||||
|
{\"tool\": \"todo_read\", \"args\": {}}
|
||||||
|
{\"tool\": \"todo_write\", \"args\": {\"content\": \"- [ ] Refactor to async\\n - [ ] Update function signatures\\n - [ ] Replace blocking calls\\n - [ ] Update all callers\\n - [ ] Update tests\"}}
|
||||||
|
|
||||||
|
## Format
|
||||||
|
|
||||||
|
Use markdown checkboxes:
|
||||||
|
- \"- [ ]\" for incomplete tasks
|
||||||
|
- \"- [x]\" for completed tasks
|
||||||
|
- Indent with 2 spaces for subtasks
|
||||||
|
|
||||||
|
Keep items short, specific, and action-oriented.
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
✓ Prevents missed steps
|
||||||
|
✓ Makes progress visible
|
||||||
|
✓ Helps recover from interruptions
|
||||||
|
✓ Creates better summaries
|
||||||
|
|
||||||
|
## When NOT to Use
|
||||||
|
|
||||||
|
Skip TODO tools for simple single-step tasks:
|
||||||
|
- \"List files\" → just use shell
|
||||||
|
- \"Read config.json\" → just use read_file
|
||||||
|
- \"Search for functions\" → just use code_search
|
||||||
|
|
||||||
|
If you can complete it with 1-2 tool calls, skip TODO.
|
||||||
|
|
||||||
|
# Code Search Guidelines
|
||||||
|
|
||||||
|
IMPORTANT: When searching for code constructs (functions, classes, methods, structs, etc.), ALWAYS use `code_search` instead of shell grep/rg.
|
||||||
|
If you create temporary files for verification, place these in a subdir named 'tmp'. Do NOT pollute the current dir.
|
||||||
|
|
||||||
|
# Code Search Guidelines
|
||||||
|
|
||||||
|
IMPORTANT: When searching for code constructs (functions, classes, methods, structs, etc.), ALWAYS use `code_search` instead of shell grep/rg.
|
||||||
|
It's syntax-aware and finds actual code, not comments or strings. Only use shell grep for:
|
||||||
|
- Searching non-code files (logs, markdown, text)
|
||||||
|
- Simple string searches across all file types
|
||||||
|
- When you need regex for text content (not code structure)
|
||||||
|
|
||||||
|
Common code_search query patterns:
|
||||||
|
|
||||||
|
**Rust:**
|
||||||
|
- All functions: {\"tool\": \"code_search\", \"args\": {\"searches\": [{\"name\": \"functions\", \"query\": \"(function_item name: (identifier) @name)\", \"language\": \"rust\"}]}}
|
||||||
|
- Async functions: {\"tool\": \"code_search\", \"args\": {\"searches\": [{\"name\": \"async_fns\", \"query\": \"(function_item (function_modifiers) name: (identifier) @name)\", \"language\": \"rust\"}]}}
|
||||||
|
- Structs: {\"tool\": \"code_search\", \"args\": {\"searches\": [{\"name\": \"structs\", \"query\": \"(struct_item name: (type_identifier) @name)\", \"language\": \"rust\"}]}}
|
||||||
|
- Enums: {\"tool\": \"code_search\", \"args\": {\"searches\": [{\"name\": \"enums\", \"query\": \"(enum_item name: (type_identifier) @name)\", \"language\": \"rust\"}]}}
|
||||||
|
- Impl blocks: {\"tool\": \"code_search\", \"args\": {\"searches\": [{\"name\": \"impls\", \"query\": \"(impl_item type: (type_identifier) @name)\", \"language\": \"rust\"}]}}
|
||||||
|
|
||||||
|
**Python:**
|
||||||
|
- Functions: {\"tool\": \"code_search\", \"args\": {\"searches\": [{\"name\": \"functions\", \"query\": \"(function_definition name: (identifier) @name)\", \"language\": \"python\"}]}}
|
||||||
|
- Classes: {\"tool\": \"code_search\", \"args\": {\"searches\": [{\"name\": \"classes\", \"query\": \"(class_definition name: (identifier) @name)\", \"language\": \"python\"}]}}
|
||||||
|
|
||||||
|
**JavaScript/TypeScript:**
|
||||||
|
- Functions: {\"tool\": \"code_search\", \"args\": {\"searches\": [{\"name\": \"functions\", \"query\": \"(function_declaration name: (identifier) @name)\", \"language\": \"javascript\"}]}}
|
||||||
|
- Classes: {\"tool\": \"code_search\", \"args\": {\"searches\": [{\"name\": \"classes\", \"query\": \"(class_declaration name: (identifier) @name)\", \"language\": \"javascript\"}]}}
|
||||||
|
- Arrow functions: {\"tool\": \"code_search\", \"args\": {\"searches\": [{\"name\": \"arrow_fns\", \"query\": \"(arrow_function) @fn\", \"language\": \"javascript\"}]}}
|
||||||
|
|
||||||
|
**Go:**
|
||||||
|
- Functions: {\"tool\": \"code_search\", \"args\": {\"searches\": [{\"name\": \"functions\", \"query\": \"(function_declaration name: (identifier) @name)\", \"language\": \"go\"}]}}
|
||||||
|
- Methods: {\"tool\": \"code_search\", \"args\": {\"searches\": [{\"name\": \"methods\", \"query\": \"(method_declaration name: (field_identifier) @name)\", \"language\": \"go\"}]}}
|
||||||
|
|
||||||
|
**Java/C++:**
|
||||||
|
- Classes: {\"tool\": \"code_search\", \"args\": {\"searches\": [{\"name\": \"classes\", \"query\": \"(class_declaration name: (identifier) @name)\", \"language\": \"java\"}]}}
|
||||||
|
- Methods: {\"tool\": \"code_search\", \"args\": {\"searches\": [{\"name\": \"methods\", \"query\": \"(method_declaration name: (identifier) @name)\", \"language\": \"java\"}]}}
|
||||||
|
|
||||||
|
**Advanced features:**
|
||||||
|
- Multiple searches: {\"tool\": \"code_search\", \"args\": {\"searches\": [{\"name\": \"funcs\", \"query\": \"(function_item name: (identifier) @name)\", \"language\": \"rust\"}, {\"name\": \"structs\", \"query\": \"(struct_item name: (type_identifier) @name)\", \"language\": \"rust\"}]}}
|
||||||
|
- With context: {\"tool\": \"code_search\", \"args\": {\"searches\": [{\"name\": \"funcs\", \"query\": \"(function_item name: (identifier) @name)\", \"language\": \"rust\", \"context_lines\": 3}]}}
|
||||||
|
- Specific paths: {\"tool\": \"code_search\", \"args\": {\"searches\": [{\"name\": \"funcs\", \"query\": \"(function_item name: (identifier) @name)\", \"language\": \"rust\", \"paths\": [\"src/core\"]}]}}
|
||||||
|
|
||||||
|
|
||||||
|
IMPORTANT: If the user asks you to just respond with text (like \"just say hello\" or \"tell me about X\"), do NOT use tools. Simply respond with the requested text directly. Only use tools when you need to execute commands or complete tasks that require action.
|
||||||
|
|
||||||
|
When taking screenshots of specific windows (like \"my Safari window\" or \"my terminal\"), ALWAYS use list_windows first to identify the correct window ID, then use take_screenshot with the window_id parameter.
|
||||||
|
|
||||||
|
Do not explain what you're going to do - just do it by calling the tools.
|
||||||
|
|
||||||
|
|
||||||
|
# Response Guidelines
|
||||||
|
|
||||||
|
- Use Markdown formatting for all responses except tool calls.
|
||||||
|
- Whenever taking actions, use the pronoun 'I'
|
||||||
|
";
|
||||||
|
|
||||||
|
pub const SYSTEM_PROMPT_FOR_NATIVE_TOOL_USE: &'static str =
|
||||||
|
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 =
|
||||||
|
"You are G3, a general-purpose AI agent. Your goal is to analyze and solve problems by writing code.
|
||||||
|
|
||||||
|
You have access to tools. When you need to accomplish a task, you MUST use the appropriate tool. Do not just describe what you would do - actually use the tools.
|
||||||
|
|
||||||
|
# Tool Call Format
|
||||||
|
|
||||||
|
When you need to execute a tool, write ONLY the JSON tool call on a new line:
|
||||||
|
|
||||||
|
{\"tool\": \"tool_name\", \"args\": {\"param\": \"value\"}
|
||||||
|
|
||||||
|
The tool will execute immediately and you'll receive the result (success or error) to continue with.
|
||||||
|
|
||||||
|
# Available Tools
|
||||||
|
|
||||||
|
Short description for providers without native calling specs:
|
||||||
|
|
||||||
|
- **shell**: Execute shell commands
|
||||||
|
- Format: {\"tool\": \"shell\", \"args\": {\"command\": \"your_command_here\"}
|
||||||
|
- Example: {\"tool\": \"shell\", \"args\": {\"command\": \"ls ~/Downloads\"}
|
||||||
|
|
||||||
|
- **read_file**: Read the contents of a file (supports partial reads via start/end)
|
||||||
|
- Format: {\"tool\": \"read_file\", \"args\": {\"file_path\": \"path/to/file\", \"start\": 0, \"end\": 100}
|
||||||
|
- Example: {\"tool\": \"read_file\", \"args\": {\"file_path\": \"src/main.rs\"}
|
||||||
|
- Example (partial): {\"tool\": \"read_file\", \"args\": {\"file_path\": \"large.log\", \"start\": 0, \"end\": 1000}
|
||||||
|
|
||||||
|
- **write_file**: Write content to a file (creates or overwrites)
|
||||||
|
- Format: {\"tool\": \"write_file\", \"args\": {\"file_path\": \"path/to/file\", \"content\": \"file content\"}
|
||||||
|
- Example: {\"tool\": \"write_file\", \"args\": {\"file_path\": \"src/lib.rs\", \"content\": \"pub fn hello() {}\"}
|
||||||
|
|
||||||
|
- **str_replace**: Replace text in a file using a diff
|
||||||
|
- Format: {\"tool\": \"str_replace\", \"args\": {\"file_path\": \"path/to/file\", \"diff\": \"--- old\\n-old text\\n+++ new\\n+new text\"}
|
||||||
|
- Example: {\"tool\": \"str_replace\", \"args\": {\"file_path\": \"src/main.rs\", \"diff\": \"--- old\\n-old_code();\\n+++ new\\n+new_code();\"}
|
||||||
|
|
||||||
|
- **final_output**: Signal task completion with a detailed summary of work done in markdown format
|
||||||
|
- Format: {\"tool\": \"final_output\", \"args\": {\"summary\": \"what_was_accomplished\"}
|
||||||
|
|
||||||
|
- **todo_read**: Read the entire TODO list from todo.g3.md file in workspace directory
|
||||||
|
- Format: {\"tool\": \"todo_read\", \"args\": {}}
|
||||||
|
- Example: {\"tool\": \"todo_read\", \"args\": {}}
|
||||||
|
|
||||||
|
- **todo_write**: Write or overwrite the entire todo.g3.md file (WARNING: overwrites completely, always read first)
|
||||||
|
- Format: {\"tool\": \"todo_write\", \"args\": {\"content\": \"- [ ] Task 1\\n- [ ] Task 2\"}}
|
||||||
|
- Example: {\"tool\": \"todo_write\", \"args\": {\"content\": \"- [ ] Implement feature\\n - [ ] Write tests\\n - [ ] Run tests\"}}
|
||||||
|
|
||||||
|
- **code_search**: Syntax-aware code search using tree-sitter. Supports Rust, Python, JavaScript, TypeScript.
|
||||||
|
- Format: {\"tool\": \"code_search\", \"args\": {\"searches\": [{\"name\": \"label\", \"query\": \"tree-sitter query\", \"language\": \"rust|python|javascript|typescript\", \"paths\": [\"src/\"], \"context_lines\": 0}]}}
|
||||||
|
- Find functions: {\"tool\": \"code_search\", \"args\": {\"searches\": [{\"name\": \"find_functions\", \"query\": \"(function_item name: (identifier) @name)\", \"language\": \"rust\", \"paths\": [\"src/\"]}]}}
|
||||||
|
- Find async functions: {\"tool\": \"code_search\", \"args\": {\"searches\": [{\"name\": \"find_async\", \"query\": \"(function_item (function_modifiers) name: (identifier) @name)\", \"language\": \"rust\"}]}}
|
||||||
|
- Find structs: {\"tool\": \"code_search\", \"args\": {\"searches\": [{\"name\": \"structs\", \"query\": \"(struct_item name: (type_identifier) @name)\", \"language\": \"rust\"}]}}
|
||||||
|
- Multiple searches: {\"tool\": \"code_search\", \"args\": {\"searches\": [{\"name\": \"funcs\", \"query\": \"(function_item name: (identifier) @name)\", \"language\": \"rust\"}, {\"name\": \"structs\", \"query\": \"(struct_item name: (type_identifier) @name)\", \"language\": \"rust\"}]}}
|
||||||
|
- With context lines: {\"tool\": \"code_search\", \"args\": {\"searches\": [{\"name\": \"funcs\", \"query\": \"(function_item name: (identifier) @name)\", \"language\": \"rust\", \"context_lines\": 3}]}}
|
||||||
|
- \"context\": 3 (show surrounding lines),
|
||||||
|
- \"json_style\": \"stream\" (for large results)
|
||||||
|
|
||||||
|
# Instructions
|
||||||
|
|
||||||
|
1. Analyze the request and break down into smaller tasks if appropriate
|
||||||
|
2. Execute ONE tool at a time. An exception exists for when you're writing files. See below.
|
||||||
|
3. STOP when the original request was satisfied
|
||||||
|
4. Call the final_output tool when done
|
||||||
|
|
||||||
|
For reading files, prioritize use of code_search tool use with multiple search requests per call instead of read_file, if it makes sense.
|
||||||
|
|
||||||
|
Exception to using ONE tool at a time:
|
||||||
|
If all you’re doing is WRITING files, and you don’t need to do anything else between each step.
|
||||||
|
You can issue MULTIPLE write_file tool calls in a request, however you may ONLY make a SINGLE write_file call for any file in that request.
|
||||||
|
For example you may call:
|
||||||
|
[START OF REQUEST]
|
||||||
|
write_file(\"helper.rs\", \"...\")
|
||||||
|
write_file(\"file2.txt\", \"...\")
|
||||||
|
[DONE]
|
||||||
|
|
||||||
|
But NOT:
|
||||||
|
[START OF REQUEST]
|
||||||
|
write_file(\"helper.rs\", \"...\")
|
||||||
|
write_file(\"file2.txt\", \"...\")
|
||||||
|
write_file(\"helper.rs\", \"...\")
|
||||||
|
[DONE]
|
||||||
|
|
||||||
|
# Task Management with TODO Tools
|
||||||
|
|
||||||
|
**REQUIRED for multi-step tasks.** Use TODO tools when your task involves ANY of:
|
||||||
|
- Multiple files to create/modify (2+)
|
||||||
|
- Multiple distinct steps (3+)
|
||||||
|
- Dependencies between steps
|
||||||
|
- Testing or verification needed
|
||||||
|
- Uncertainty about approach
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
Every multi-step task follows this pattern:
|
||||||
|
1. **Start**: Call todo_read, then todo_write to create your plan
|
||||||
|
2. **During**: Execute steps, then todo_read and todo_write to mark progress
|
||||||
|
3. **End**: Call todo_read to verify all items complete
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
**Example 1: Feature Implementation**
|
||||||
|
User asks: \"Add user authentication with tests\"
|
||||||
|
|
||||||
|
First action:
|
||||||
|
{\"tool\": \"todo_read\", \"args\": {}}
|
||||||
|
|
||||||
|
Then create plan:
|
||||||
|
{\"tool\": \"todo_write\", \"args\": {\"content\": \"- [ ] Add user authentication\\n - [ ] Create User struct\\n - [ ] Add login endpoint\\n - [ ] Add password hashing\\n - [ ] Write unit tests\\n - [ ] Write integration tests\"}}
|
||||||
|
|
||||||
|
After completing User struct:
|
||||||
|
{\"tool\": \"todo_read\", \"args\": {}}
|
||||||
|
{\"tool\": \"todo_write\", \"args\": {\"content\": \"- [ ] Add user authentication\\n - [x] Create User struct\\n - [ ] Add login endpoint\\n - [ ] Add password hashing\\n - [ ] Write unit tests\\n - [ ] Write integration tests\"}}
|
||||||
|
|
||||||
|
**Example 2: Bug Fix**
|
||||||
|
User asks: \"Fix the memory leak in cache module\"
|
||||||
|
|
||||||
|
{\"tool\": \"todo_read\", \"args\": {}}
|
||||||
|
{\"tool\": \"todo_write\", \"args\": {\"content\": \"- [ ] Fix memory leak\\n - [ ] Review cache.rs\\n - [ ] Check for unclosed resources\\n - [ ] Add drop implementation\\n - [ ] Write test to verify fix\"}}
|
||||||
|
|
||||||
|
**Example 3: Refactoring**
|
||||||
|
User asks: \"Refactor database layer to use async/await\"
|
||||||
|
|
||||||
|
{\"tool\": \"todo_read\", \"args\": {}}
|
||||||
|
{\"tool\": \"todo_write\", \"args\": {\"content\": \"- [ ] Refactor to async\\n - [ ] Update function signatures\\n - [ ] Replace blocking calls\\n - [ ] Update all callers\\n - [ ] Update tests\"}}
|
||||||
|
|
||||||
|
## Format
|
||||||
|
|
||||||
|
Use markdown checkboxes:
|
||||||
|
- \"- [ ]\" for incomplete tasks
|
||||||
|
- \"- [x]\" for completed tasks
|
||||||
|
- Indent with 2 spaces for subtasks
|
||||||
|
|
||||||
|
Keep items short, specific, and action-oriented.
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
✓ Prevents missed steps
|
||||||
|
✓ Makes progress visible
|
||||||
|
✓ Helps recover from interruptions
|
||||||
|
✓ Creates better summaries
|
||||||
|
|
||||||
|
## When NOT to Use
|
||||||
|
|
||||||
|
Skip TODO tools for simple single-step tasks:
|
||||||
|
- \"List files\" → just use shell
|
||||||
|
- \"Read config.json\" → just use read_file
|
||||||
|
- \"Search for functions\" → just use code_search
|
||||||
|
|
||||||
|
If you can complete it with 1-2 tool calls, skip TODO.
|
||||||
|
|
||||||
|
|
||||||
|
# Response Guidelines
|
||||||
|
|
||||||
|
- Use Markdown formatting for all responses except tool calls.
|
||||||
|
- Whenever taking actions, use the pronoun 'I'
|
||||||
|
";
|
||||||
|
|
||||||
|
pub const SYSTEM_PROMPT_FOR_NON_NATIVE_TOOL_USE: &'static str =
|
||||||
|
concatcp!(SYSTEM_NON_NATIVE_TOOL_USE, CODING_STYLE);
|
||||||
@@ -6,14 +6,10 @@ use std::sync::Arc;
|
|||||||
fn test_task_result_basic_functionality() {
|
fn test_task_result_basic_functionality() {
|
||||||
// Create a context window with some messages
|
// Create a context window with some messages
|
||||||
let mut context = ContextWindow::new(10000);
|
let mut context = ContextWindow::new(10000);
|
||||||
context.add_message(Message {
|
context.add_message(Message::new(MessageRole::User, "Test message 1".to_string())
|
||||||
role: MessageRole::User,
|
);
|
||||||
content: "Test message 1".to_string(),
|
context.add_message(Message::new(MessageRole::Assistant, "Response 1".to_string())
|
||||||
});
|
);
|
||||||
context.add_message(Message {
|
|
||||||
role: MessageRole::Assistant,
|
|
||||||
content: "Response 1".to_string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create a TaskResult
|
// Create a TaskResult
|
||||||
let response = "This is the response\n\nFinal output block".to_string();
|
let response = "This is the response\n\nFinal output block".to_string();
|
||||||
@@ -100,10 +96,7 @@ fn test_context_window_preservation() {
|
|||||||
|
|
||||||
// Add some messages
|
// Add some messages
|
||||||
for i in 0..5 {
|
for i in 0..5 {
|
||||||
context.add_message(Message {
|
context.add_message(Message::new(if i % 2 == 0 { MessageRole::User } else { MessageRole::Assistant }, format!("Message {}", i)));
|
||||||
role: if i % 2 == 0 { MessageRole::User } else { MessageRole::Assistant },
|
|
||||||
content: format!("Message {}", i),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create TaskResult
|
// Create TaskResult
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -46,10 +46,10 @@ fn test_thin_context_basic() {
|
|||||||
// Add some messages to the first third
|
// Add some messages to the first third
|
||||||
for i in 0..9 {
|
for i in 0..9 {
|
||||||
if i % 2 == 0 {
|
if i % 2 == 0 {
|
||||||
context.add_message(Message {
|
context.add_message(Message::new(
|
||||||
role: MessageRole::Assistant,
|
MessageRole::Assistant,
|
||||||
content: format!("Assistant message {}", i),
|
format!("Assistant message {}", i),
|
||||||
});
|
));
|
||||||
} else {
|
} else {
|
||||||
// Add tool results with varying sizes
|
// Add tool results with varying sizes
|
||||||
let content = if i == 1 {
|
let content = if i == 1 {
|
||||||
@@ -63,10 +63,10 @@ fn test_thin_context_basic() {
|
|||||||
format!("Tool result: small result {}", i)
|
format!("Tool result: small result {}", i)
|
||||||
};
|
};
|
||||||
|
|
||||||
context.add_message(Message {
|
context.add_message(Message::new(
|
||||||
role: MessageRole::User,
|
MessageRole::User,
|
||||||
content,
|
content,
|
||||||
});
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,10 +98,10 @@ fn test_thin_write_file_tool_calls() {
|
|||||||
let mut context = ContextWindow::new(10000);
|
let mut context = ContextWindow::new(10000);
|
||||||
|
|
||||||
// Add some messages including a write_file tool call with large content
|
// Add some messages including a write_file tool call with large content
|
||||||
context.add_message(Message {
|
context.add_message(Message::new(
|
||||||
role: MessageRole::User,
|
MessageRole::User,
|
||||||
content: "Please create a large file".to_string(),
|
"Please create a large file".to_string(),
|
||||||
});
|
));
|
||||||
|
|
||||||
// Add an assistant message with a write_file tool call containing large content
|
// Add an assistant message with a write_file tool call containing large content
|
||||||
let large_content = "x".repeat(1500);
|
let large_content = "x".repeat(1500);
|
||||||
@@ -109,22 +109,22 @@ fn test_thin_write_file_tool_calls() {
|
|||||||
r#"{{"tool": "write_file", "args": {{"file_path": "test.txt", "content": "{}"}}}}"#,
|
r#"{{"tool": "write_file", "args": {{"file_path": "test.txt", "content": "{}"}}}}"#,
|
||||||
large_content
|
large_content
|
||||||
);
|
);
|
||||||
context.add_message(Message {
|
context.add_message(Message::new(
|
||||||
role: MessageRole::Assistant,
|
MessageRole::Assistant,
|
||||||
content: format!("I'll create that file.\n\n{}", tool_call_json),
|
format!("I'll create that file.\n\n{}", tool_call_json),
|
||||||
});
|
));
|
||||||
|
|
||||||
context.add_message(Message {
|
context.add_message(Message::new(
|
||||||
role: MessageRole::User,
|
MessageRole::User,
|
||||||
content: "Tool result: ✅ Successfully wrote 1500 lines".to_string(),
|
"Tool result: ✅ Successfully wrote 1500 lines".to_string(),
|
||||||
});
|
));
|
||||||
|
|
||||||
// Add more messages to ensure we have enough for "first third" logic
|
// Add more messages to ensure we have enough for "first third" logic
|
||||||
for i in 0..6 {
|
for i in 0..6 {
|
||||||
context.add_message(Message {
|
context.add_message(Message::new(
|
||||||
role: MessageRole::Assistant,
|
MessageRole::Assistant,
|
||||||
content: format!("Response {}", i),
|
format!("Response {}", i),
|
||||||
});
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger thinning at 50%
|
// Trigger thinning at 50%
|
||||||
@@ -154,10 +154,10 @@ fn test_thin_str_replace_tool_calls() {
|
|||||||
let mut context = ContextWindow::new(10000);
|
let mut context = ContextWindow::new(10000);
|
||||||
|
|
||||||
// Add some messages including a str_replace tool call with large diff
|
// Add some messages including a str_replace tool call with large diff
|
||||||
context.add_message(Message {
|
context.add_message(Message::new(
|
||||||
role: MessageRole::User,
|
MessageRole::User,
|
||||||
content: "Please update the file".to_string(),
|
"Please update the file".to_string(),
|
||||||
});
|
));
|
||||||
|
|
||||||
// Add an assistant message with a str_replace tool call containing large diff
|
// Add an assistant message with a str_replace tool call containing large diff
|
||||||
let large_diff = format!("--- old\n{}\n+++ new\n{}", "-old line\n".repeat(100), "+new line\n".repeat(100));
|
let large_diff = format!("--- old\n{}\n+++ new\n{}", "-old line\n".repeat(100), "+new line\n".repeat(100));
|
||||||
@@ -165,22 +165,22 @@ fn test_thin_str_replace_tool_calls() {
|
|||||||
r#"{{"tool": "str_replace", "args": {{"file_path": "test.txt", "diff": "{}"}}}}"#,
|
r#"{{"tool": "str_replace", "args": {{"file_path": "test.txt", "diff": "{}"}}}}"#,
|
||||||
large_diff.replace('\n', "\\n")
|
large_diff.replace('\n', "\\n")
|
||||||
);
|
);
|
||||||
context.add_message(Message {
|
context.add_message(Message::new(
|
||||||
role: MessageRole::Assistant,
|
MessageRole::Assistant,
|
||||||
content: format!("I'll update that file.\n\n{}", tool_call_json),
|
format!("I'll update that file.\n\n{}", tool_call_json),
|
||||||
});
|
));
|
||||||
|
|
||||||
context.add_message(Message {
|
context.add_message(Message::new(
|
||||||
role: MessageRole::User,
|
MessageRole::User,
|
||||||
content: "Tool result: ✅ applied unified diff".to_string(),
|
"Tool result: ✅ applied unified diff".to_string(),
|
||||||
});
|
));
|
||||||
|
|
||||||
// Add more messages to ensure we have enough for "first third" logic
|
// Add more messages to ensure we have enough for "first third" logic
|
||||||
for i in 0..6 {
|
for i in 0..6 {
|
||||||
context.add_message(Message {
|
context.add_message(Message::new(
|
||||||
role: MessageRole::Assistant,
|
MessageRole::Assistant,
|
||||||
content: format!("Response {}", i),
|
format!("Response {}", i),
|
||||||
});
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger thinning at 50%
|
// Trigger thinning at 50%
|
||||||
@@ -212,10 +212,10 @@ fn test_thin_context_no_large_results() {
|
|||||||
|
|
||||||
// Add only small messages
|
// Add only small messages
|
||||||
for i in 0..9 {
|
for i in 0..9 {
|
||||||
context.add_message(Message {
|
context.add_message(Message::new(
|
||||||
role: MessageRole::User,
|
MessageRole::User,
|
||||||
content: format!("Tool result: small {}", i),
|
format!("Tool result: small {}", i),
|
||||||
});
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
context.used_tokens = 5000;
|
context.used_tokens = 5000;
|
||||||
@@ -244,7 +244,7 @@ fn test_thin_context_only_affects_first_third() {
|
|||||||
MessageRole::Assistant
|
MessageRole::Assistant
|
||||||
};
|
};
|
||||||
|
|
||||||
context.add_message(Message { role, content });
|
context.add_message(Message::new(role, content));
|
||||||
}
|
}
|
||||||
|
|
||||||
context.used_tokens = 5000;
|
context.used_tokens = 5000;
|
||||||
|
|||||||
@@ -8,27 +8,18 @@ fn test_todo_read_results_not_thinned() {
|
|||||||
let mut context = ContextWindow::new(10000);
|
let mut context = ContextWindow::new(10000);
|
||||||
|
|
||||||
// Add a todo_read tool call
|
// Add a todo_read tool call
|
||||||
context.add_message(Message {
|
context.add_message(Message::new(MessageRole::Assistant, r#"{"tool": "todo_read", "args": {}}"#.to_string()));
|
||||||
role: MessageRole::Assistant,
|
|
||||||
content: r#"{"tool": "todo_read", "args": {}}"#.to_string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add a large TODO result (> 500 chars)
|
// Add a large TODO result (> 500 chars)
|
||||||
let large_todo_result = format!(
|
let large_todo_result = format!(
|
||||||
"Tool result: 📝 TODO list:\n{}",
|
"Tool result: 📝 TODO list:\n{}",
|
||||||
"- [ ] Task with long description\n".repeat(50)
|
"- [ ] Task with long description\n".repeat(50)
|
||||||
);
|
);
|
||||||
context.add_message(Message {
|
context.add_message(Message::new(MessageRole::User, large_todo_result.clone()));
|
||||||
role: MessageRole::User,
|
|
||||||
content: large_todo_result.clone(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add more messages to ensure we have enough for "first third" logic
|
// Add more messages to ensure we have enough for "first third" logic
|
||||||
for i in 0..6 {
|
for i in 0..6 {
|
||||||
context.add_message(Message {
|
context.add_message(Message::new(MessageRole::Assistant, format!("Response {}", i)))
|
||||||
role: MessageRole::Assistant,
|
|
||||||
content: format!("Response {}", i),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger thinning at 50%
|
// Trigger thinning at 50%
|
||||||
@@ -65,27 +56,18 @@ fn test_todo_write_results_not_thinned() {
|
|||||||
|
|
||||||
// Add a todo_write tool call
|
// Add a todo_write tool call
|
||||||
let large_content = "- [ ] Task\n".repeat(100);
|
let large_content = "- [ ] Task\n".repeat(100);
|
||||||
context.add_message(Message {
|
context.add_message(Message::new(MessageRole::Assistant, format!(r#"{{"tool": "todo_write", "args": {{"content": "{}"}}}}"#, large_content)));
|
||||||
role: MessageRole::Assistant,
|
|
||||||
content: format!(r#"{{"tool": "todo_write", "args": {{"content": "{}"}}}}"#, large_content),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add a large TODO write result
|
// Add a large TODO write result
|
||||||
let large_todo_result = format!(
|
let large_todo_result = format!(
|
||||||
"Tool result: ✅ TODO list updated ({} chars) and saved to todo.g3.md",
|
"Tool result: ✅ TODO list updated ({} chars) and saved to todo.g3.md",
|
||||||
large_content.len()
|
large_content.len()
|
||||||
);
|
);
|
||||||
context.add_message(Message {
|
context.add_message(Message::new(MessageRole::User, large_todo_result.clone()));
|
||||||
role: MessageRole::User,
|
|
||||||
content: large_todo_result.clone(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add more messages
|
// Add more messages
|
||||||
for i in 0..6 {
|
for i in 0..6 {
|
||||||
context.add_message(Message {
|
context.add_message(Message::new(MessageRole::Assistant, format!("Response {}", i)))
|
||||||
role: MessageRole::Assistant,
|
|
||||||
content: format!("Response {}", i),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger thinning at 50%
|
// Trigger thinning at 50%
|
||||||
@@ -119,24 +101,15 @@ fn test_non_todo_results_still_thinned() {
|
|||||||
let mut context = ContextWindow::new(10000);
|
let mut context = ContextWindow::new(10000);
|
||||||
|
|
||||||
// Add a non-TODO tool call (e.g., read_file)
|
// Add a non-TODO tool call (e.g., read_file)
|
||||||
context.add_message(Message {
|
context.add_message(Message::new(MessageRole::Assistant, r#"{"tool": "read_file", "args": {"file_path": "test.txt"}}"#.to_string()));
|
||||||
role: MessageRole::Assistant,
|
|
||||||
content: r#"{"tool": "read_file", "args": {"file_path": "test.txt"}}"#.to_string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add a large read_file result (> 500 chars)
|
// Add a large read_file result (> 500 chars)
|
||||||
let large_result = format!("Tool result: {}", "x".repeat(1500));
|
let large_result = format!("Tool result: {}", "x".repeat(1500));
|
||||||
context.add_message(Message {
|
context.add_message(Message::new(MessageRole::User, large_result));
|
||||||
role: MessageRole::User,
|
|
||||||
content: large_result,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add more messages
|
// Add more messages
|
||||||
for i in 0..6 {
|
for i in 0..6 {
|
||||||
context.add_message(Message {
|
context.add_message(Message::new(MessageRole::Assistant, format!("Response {}", i)))
|
||||||
role: MessageRole::Assistant,
|
|
||||||
content: format!("Response {}", i),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger thinning at 50%
|
// Trigger thinning at 50%
|
||||||
@@ -172,27 +145,18 @@ fn test_todo_read_with_spaces_in_tool_name() {
|
|||||||
let mut context = ContextWindow::new(10000);
|
let mut context = ContextWindow::new(10000);
|
||||||
|
|
||||||
// Add a todo_read tool call with spaces (JSON formatting variation)
|
// Add a todo_read tool call with spaces (JSON formatting variation)
|
||||||
context.add_message(Message {
|
context.add_message(Message::new(MessageRole::Assistant, r#"{"tool": "todo_read", "args": {}}"#.to_string()));
|
||||||
role: MessageRole::Assistant,
|
|
||||||
content: r#"{"tool": "todo_read", "args": {}}"#.to_string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add a large TODO result
|
// Add a large TODO result
|
||||||
let large_todo_result = format!(
|
let large_todo_result = format!(
|
||||||
"Tool result: 📝 TODO list:\n{}",
|
"Tool result: 📝 TODO list:\n{}",
|
||||||
"- [ ] Task\n".repeat(50)
|
"- [ ] Task\n".repeat(50)
|
||||||
);
|
);
|
||||||
context.add_message(Message {
|
context.add_message(Message::new(MessageRole::User, large_todo_result.clone()));
|
||||||
role: MessageRole::User,
|
|
||||||
content: large_todo_result.clone(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add more messages
|
// Add more messages
|
||||||
for i in 0..6 {
|
for i in 0..6 {
|
||||||
context.add_message(Message {
|
context.add_message(Message::new(MessageRole::Assistant, format!("Response {}", i)))
|
||||||
role: MessageRole::Assistant,
|
|
||||||
content: format!("Response {}", i),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger thinning
|
// Trigger thinning
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ fn get_todo_path(temp_dir: &TempDir) -> PathBuf {
|
|||||||
#[serial]
|
#[serial]
|
||||||
async fn test_todo_write_creates_file() {
|
async fn test_todo_write_creates_file() {
|
||||||
let temp_dir = TempDir::new().unwrap();
|
let temp_dir = TempDir::new().unwrap();
|
||||||
let agent = create_test_agent_in_dir(&temp_dir).await;
|
let mut agent = create_test_agent_in_dir(&temp_dir).await;
|
||||||
let todo_path = get_todo_path(&temp_dir);
|
let todo_path = get_todo_path(&temp_dir);
|
||||||
|
|
||||||
// Initially, todo.g3.md should not exist
|
// Initially, todo.g3.md should not exist
|
||||||
@@ -67,7 +67,7 @@ async fn test_todo_read_from_file() {
|
|||||||
fs::write(&todo_path, test_content).unwrap();
|
fs::write(&todo_path, test_content).unwrap();
|
||||||
|
|
||||||
// Create agent (should load from file)
|
// Create agent (should load from file)
|
||||||
let agent = create_test_agent_in_dir(&temp_dir).await;
|
let mut agent = create_test_agent_in_dir(&temp_dir).await;
|
||||||
|
|
||||||
// Create a tool call to read TODO
|
// Create a tool call to read TODO
|
||||||
let tool_call = g3_core::ToolCall {
|
let tool_call = g3_core::ToolCall {
|
||||||
@@ -88,7 +88,7 @@ async fn test_todo_read_from_file() {
|
|||||||
#[serial]
|
#[serial]
|
||||||
async fn test_todo_read_empty_file() {
|
async fn test_todo_read_empty_file() {
|
||||||
let temp_dir = TempDir::new().unwrap();
|
let temp_dir = TempDir::new().unwrap();
|
||||||
let agent = create_test_agent_in_dir(&temp_dir).await;
|
let mut agent = create_test_agent_in_dir(&temp_dir).await;
|
||||||
|
|
||||||
// Create a tool call to read TODO (file doesn't exist)
|
// Create a tool call to read TODO (file doesn't exist)
|
||||||
let tool_call = g3_core::ToolCall {
|
let tool_call = g3_core::ToolCall {
|
||||||
@@ -111,7 +111,7 @@ async fn test_todo_persistence_across_agents() {
|
|||||||
|
|
||||||
// Agent 1: Write TODO
|
// Agent 1: Write TODO
|
||||||
{
|
{
|
||||||
let agent = create_test_agent_in_dir(&temp_dir).await;
|
let mut agent = create_test_agent_in_dir(&temp_dir).await;
|
||||||
let tool_call = g3_core::ToolCall {
|
let tool_call = g3_core::ToolCall {
|
||||||
tool: "todo_write".to_string(),
|
tool: "todo_write".to_string(),
|
||||||
args: serde_json::json!({
|
args: serde_json::json!({
|
||||||
@@ -126,7 +126,7 @@ async fn test_todo_persistence_across_agents() {
|
|||||||
|
|
||||||
// Agent 2: Read TODO (new agent instance)
|
// Agent 2: Read TODO (new agent instance)
|
||||||
{
|
{
|
||||||
let agent = create_test_agent_in_dir(&temp_dir).await;
|
let mut agent = create_test_agent_in_dir(&temp_dir).await;
|
||||||
let tool_call = g3_core::ToolCall {
|
let tool_call = g3_core::ToolCall {
|
||||||
tool: "todo_read".to_string(),
|
tool: "todo_read".to_string(),
|
||||||
args: serde_json::json!({}),
|
args: serde_json::json!({}),
|
||||||
@@ -143,7 +143,7 @@ async fn test_todo_persistence_across_agents() {
|
|||||||
#[serial]
|
#[serial]
|
||||||
async fn test_todo_update_preserves_file() {
|
async fn test_todo_update_preserves_file() {
|
||||||
let temp_dir = TempDir::new().unwrap();
|
let temp_dir = TempDir::new().unwrap();
|
||||||
let agent = create_test_agent_in_dir(&temp_dir).await;
|
let mut agent = create_test_agent_in_dir(&temp_dir).await;
|
||||||
let todo_path = get_todo_path(&temp_dir);
|
let todo_path = get_todo_path(&temp_dir);
|
||||||
|
|
||||||
// Write initial TODO
|
// Write initial TODO
|
||||||
@@ -173,7 +173,7 @@ async fn test_todo_update_preserves_file() {
|
|||||||
#[serial]
|
#[serial]
|
||||||
async fn test_todo_handles_large_content() {
|
async fn test_todo_handles_large_content() {
|
||||||
let temp_dir = TempDir::new().unwrap();
|
let temp_dir = TempDir::new().unwrap();
|
||||||
let agent = create_test_agent_in_dir(&temp_dir).await;
|
let mut agent = create_test_agent_in_dir(&temp_dir).await;
|
||||||
let todo_path = get_todo_path(&temp_dir);
|
let todo_path = get_todo_path(&temp_dir);
|
||||||
|
|
||||||
// Create a large TODO (but under the 50k limit)
|
// Create a large TODO (but under the 50k limit)
|
||||||
@@ -202,7 +202,7 @@ async fn test_todo_handles_large_content() {
|
|||||||
#[serial]
|
#[serial]
|
||||||
async fn test_todo_respects_size_limit() {
|
async fn test_todo_respects_size_limit() {
|
||||||
let temp_dir = TempDir::new().unwrap();
|
let temp_dir = TempDir::new().unwrap();
|
||||||
let agent = create_test_agent_in_dir(&temp_dir).await;
|
let mut agent = create_test_agent_in_dir(&temp_dir).await;
|
||||||
|
|
||||||
// Create content that exceeds the default 50k limit
|
// Create content that exceeds the default 50k limit
|
||||||
let huge_content = "x".repeat(60_000);
|
let huge_content = "x".repeat(60_000);
|
||||||
@@ -232,7 +232,7 @@ async fn test_todo_agent_initialization_loads_file() {
|
|||||||
fs::write(&todo_path, initial_content).unwrap();
|
fs::write(&todo_path, initial_content).unwrap();
|
||||||
|
|
||||||
// Create agent - should load the file during initialization
|
// Create agent - should load the file during initialization
|
||||||
let agent = create_test_agent_in_dir(&temp_dir).await;
|
let mut agent = create_test_agent_in_dir(&temp_dir).await;
|
||||||
|
|
||||||
// Read TODO - should return the pre-existing content
|
// Read TODO - should return the pre-existing content
|
||||||
let tool_call = g3_core::ToolCall {
|
let tool_call = g3_core::ToolCall {
|
||||||
@@ -248,7 +248,7 @@ async fn test_todo_agent_initialization_loads_file() {
|
|||||||
#[serial]
|
#[serial]
|
||||||
async fn test_todo_handles_unicode_content() {
|
async fn test_todo_handles_unicode_content() {
|
||||||
let temp_dir = TempDir::new().unwrap();
|
let temp_dir = TempDir::new().unwrap();
|
||||||
let agent = create_test_agent_in_dir(&temp_dir).await;
|
let mut agent = create_test_agent_in_dir(&temp_dir).await;
|
||||||
let todo_path = get_todo_path(&temp_dir);
|
let todo_path = get_todo_path(&temp_dir);
|
||||||
|
|
||||||
// Create TODO with unicode characters
|
// Create TODO with unicode characters
|
||||||
@@ -283,7 +283,7 @@ async fn test_todo_handles_unicode_content() {
|
|||||||
#[serial]
|
#[serial]
|
||||||
async fn test_todo_empty_content_creates_empty_file() {
|
async fn test_todo_empty_content_creates_empty_file() {
|
||||||
let temp_dir = TempDir::new().unwrap();
|
let temp_dir = TempDir::new().unwrap();
|
||||||
let agent = create_test_agent_in_dir(&temp_dir).await;
|
let mut agent = create_test_agent_in_dir(&temp_dir).await;
|
||||||
let todo_path = get_todo_path(&temp_dir);
|
let todo_path = get_todo_path(&temp_dir);
|
||||||
|
|
||||||
// Write empty TODO
|
// Write empty TODO
|
||||||
@@ -306,7 +306,7 @@ async fn test_todo_empty_content_creates_empty_file() {
|
|||||||
#[serial]
|
#[serial]
|
||||||
async fn test_todo_whitespace_only_content() {
|
async fn test_todo_whitespace_only_content() {
|
||||||
let temp_dir = TempDir::new().unwrap();
|
let temp_dir = TempDir::new().unwrap();
|
||||||
let agent = create_test_agent_in_dir(&temp_dir).await;
|
let mut agent = create_test_agent_in_dir(&temp_dir).await;
|
||||||
|
|
||||||
// Write whitespace-only TODO
|
// Write whitespace-only TODO
|
||||||
let tool_call = g3_core::ToolCall {
|
let tool_call = g3_core::ToolCall {
|
||||||
|
|||||||
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,22 +21,18 @@
|
|||||||
//! // Create the provider with your API key
|
//! // Create the provider with your API key
|
||||||
//! let provider = AnthropicProvider::new(
|
//! let provider = AnthropicProvider::new(
|
||||||
//! "your-api-key".to_string(),
|
//! "your-api-key".to_string(),
|
||||||
//! Some("claude-3-5-sonnet-20241022".to_string()), // Optional: defaults to claude-3-5-sonnet-20241022
|
//! Some("claude-3-5-sonnet-20241022".to_string()),
|
||||||
//! Some(4096), // Optional: max tokens
|
//! Some(4096),
|
||||||
//! Some(0.1), // Optional: temperature
|
//! Some(0.1),
|
||||||
|
//! None, // cache_config
|
||||||
|
//! None, // enable_1m_context
|
||||||
//! )?;
|
//! )?;
|
||||||
//!
|
//!
|
||||||
//! // Create a completion request
|
//! // Create a completion request
|
||||||
//! let request = CompletionRequest {
|
//! let request = CompletionRequest {
|
||||||
//! messages: vec![
|
//! messages: vec![
|
||||||
//! Message {
|
//! Message::new(MessageRole::System, "You are a helpful assistant.".to_string()),
|
||||||
//! role: MessageRole::System,
|
//! Message::new(MessageRole::User, "Hello! How are you?".to_string()),
|
||||||
//! content: "You are a helpful assistant.".to_string(),
|
|
||||||
//! },
|
|
||||||
//! Message {
|
|
||||||
//! role: MessageRole::User,
|
|
||||||
//! content: "Hello! How are you?".to_string(),
|
|
||||||
//! },
|
|
||||||
//! ],
|
//! ],
|
||||||
//! max_tokens: Some(1000),
|
//! max_tokens: Some(1000),
|
||||||
//! temperature: Some(0.7),
|
//! temperature: Some(0.7),
|
||||||
@@ -62,15 +58,16 @@
|
|||||||
//! async fn main() -> anyhow::Result<()> {
|
//! async fn main() -> anyhow::Result<()> {
|
||||||
//! let provider = AnthropicProvider::new(
|
//! let provider = AnthropicProvider::new(
|
||||||
//! "your-api-key".to_string(),
|
//! "your-api-key".to_string(),
|
||||||
//! None, None, None,
|
//! None,
|
||||||
|
//! None,
|
||||||
|
//! None,
|
||||||
|
//! None, // cache_config
|
||||||
|
//! None, // enable_1m_context
|
||||||
//! )?;
|
//! )?;
|
||||||
//!
|
//!
|
||||||
//! let request = CompletionRequest {
|
//! let request = CompletionRequest {
|
||||||
//! messages: vec![
|
//! messages: vec![
|
||||||
//! Message {
|
//! Message::new(MessageRole::User, "Write a short story about a robot.".to_string()),
|
||||||
//! role: MessageRole::User,
|
|
||||||
//! content: "Write a short story about a robot.".to_string(),
|
|
||||||
//! },
|
|
||||||
//! ],
|
//! ],
|
||||||
//! max_tokens: Some(1000),
|
//! max_tokens: Some(1000),
|
||||||
//! temperature: Some(0.7),
|
//! temperature: Some(0.7),
|
||||||
@@ -123,6 +120,8 @@ pub struct AnthropicProvider {
|
|||||||
model: String,
|
model: String,
|
||||||
max_tokens: u32,
|
max_tokens: u32,
|
||||||
temperature: f32,
|
temperature: f32,
|
||||||
|
cache_config: Option<String>,
|
||||||
|
enable_1m_context: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AnthropicProvider {
|
impl AnthropicProvider {
|
||||||
@@ -131,6 +130,8 @@ impl AnthropicProvider {
|
|||||||
model: Option<String>,
|
model: Option<String>,
|
||||||
max_tokens: Option<u32>,
|
max_tokens: Option<u32>,
|
||||||
temperature: Option<f32>,
|
temperature: Option<f32>,
|
||||||
|
cache_config: Option<String>,
|
||||||
|
enable_1m_context: Option<bool>,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let client = Client::builder()
|
let client = Client::builder()
|
||||||
.timeout(Duration::from_secs(300))
|
.timeout(Duration::from_secs(300))
|
||||||
@@ -147,6 +148,8 @@ impl AnthropicProvider {
|
|||||||
model,
|
model,
|
||||||
max_tokens: max_tokens.unwrap_or(4096),
|
max_tokens: max_tokens.unwrap_or(4096),
|
||||||
temperature: temperature.unwrap_or(0.1),
|
temperature: temperature.unwrap_or(0.1),
|
||||||
|
cache_config,
|
||||||
|
enable_1m_context: enable_1m_context.unwrap_or(false),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,9 +159,12 @@ impl AnthropicProvider {
|
|||||||
.post(ANTHROPIC_API_URL)
|
.post(ANTHROPIC_API_URL)
|
||||||
.header("x-api-key", &self.api_key)
|
.header("x-api-key", &self.api_key)
|
||||||
.header("anthropic-version", ANTHROPIC_VERSION)
|
.header("anthropic-version", ANTHROPIC_VERSION)
|
||||||
// Anthropic beta 1m context window. Enable if needed. It costs extra, so check first.
|
|
||||||
// .header("anthropic-beta", "context-1m-2025-08-07")
|
|
||||||
.header("content-type", "application/json");
|
.header("content-type", "application/json");
|
||||||
|
|
||||||
|
if self.enable_1m_context {
|
||||||
|
builder = builder.header("anthropic-beta", "context-1m-2025-08-07");
|
||||||
|
}
|
||||||
|
|
||||||
if streaming {
|
if streaming {
|
||||||
builder = builder.header("accept", "text/event-stream");
|
builder = builder.header("accept", "text/event-stream");
|
||||||
}
|
}
|
||||||
@@ -166,6 +172,11 @@ impl AnthropicProvider {
|
|||||||
builder
|
builder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn convert_cache_control(cache_control: &crate::CacheControl) -> crate::CacheControl {
|
||||||
|
// Anthropic uses the same format, so just clone it
|
||||||
|
cache_control.clone()
|
||||||
|
}
|
||||||
|
|
||||||
fn convert_tools(&self, tools: &[Tool]) -> Vec<AnthropicTool> {
|
fn convert_tools(&self, tools: &[Tool]) -> Vec<AnthropicTool> {
|
||||||
tools
|
tools
|
||||||
.iter()
|
.iter()
|
||||||
@@ -214,6 +225,8 @@ impl AnthropicProvider {
|
|||||||
role: "user".to_string(),
|
role: "user".to_string(),
|
||||||
content: vec![AnthropicContent::Text {
|
content: vec![AnthropicContent::Text {
|
||||||
text: message.content.clone(),
|
text: message.content.clone(),
|
||||||
|
cache_control: message.cache_control.as_ref()
|
||||||
|
.map(Self::convert_cache_control),
|
||||||
}],
|
}],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -222,6 +235,8 @@ impl AnthropicProvider {
|
|||||||
role: "assistant".to_string(),
|
role: "assistant".to_string(),
|
||||||
content: vec![AnthropicContent::Text {
|
content: vec![AnthropicContent::Text {
|
||||||
text: message.content.clone(),
|
text: message.content.clone(),
|
||||||
|
cache_control: message.cache_control.as_ref()
|
||||||
|
.map(Self::convert_cache_control),
|
||||||
}],
|
}],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -564,7 +579,7 @@ impl LLMProvider for AnthropicProvider {
|
|||||||
.content
|
.content
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|c| match c {
|
.filter_map(|c| match c {
|
||||||
AnthropicContent::Text { text } => Some(text.as_str()),
|
AnthropicContent::Text { text, .. } => Some(text.as_str()),
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
@@ -658,6 +673,11 @@ impl LLMProvider for AnthropicProvider {
|
|||||||
// Claude models support native tool calling
|
// Claude models support native tool calling
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn supports_cache_control(&self) -> bool {
|
||||||
|
// Anthropic supports cache control
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Anthropic API request/response structures
|
// Anthropic API request/response structures
|
||||||
@@ -701,7 +721,11 @@ struct AnthropicMessage {
|
|||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
enum AnthropicContent {
|
enum AnthropicContent {
|
||||||
#[serde(rename = "text")]
|
#[serde(rename = "text")]
|
||||||
Text { text: String },
|
Text {
|
||||||
|
text: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
cache_control: Option<crate::CacheControl>,
|
||||||
|
},
|
||||||
#[serde(rename = "tool_use")]
|
#[serde(rename = "tool_use")]
|
||||||
ToolUse {
|
ToolUse {
|
||||||
id: String,
|
id: String,
|
||||||
@@ -771,21 +795,14 @@ mod tests {
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
).unwrap();
|
).unwrap();
|
||||||
|
|
||||||
let messages = vec![
|
let messages = vec![
|
||||||
Message {
|
Message::new(MessageRole::System, "You are a helpful assistant.".to_string()),
|
||||||
role: MessageRole::System,
|
Message::new(MessageRole::User, "Hello!".to_string()),
|
||||||
content: "You are a helpful assistant.".to_string(),
|
Message::new(MessageRole::Assistant, "Hi there!".to_string()),
|
||||||
},
|
|
||||||
Message {
|
|
||||||
role: MessageRole::User,
|
|
||||||
content: "Hello!".to_string(),
|
|
||||||
},
|
|
||||||
Message {
|
|
||||||
role: MessageRole::Assistant,
|
|
||||||
content: "Hi there!".to_string(),
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
let (system, anthropic_messages) = provider.convert_messages(&messages).unwrap();
|
let (system, anthropic_messages) = provider.convert_messages(&messages).unwrap();
|
||||||
@@ -803,14 +820,11 @@ mod tests {
|
|||||||
Some("claude-3-haiku-20240307".to_string()),
|
Some("claude-3-haiku-20240307".to_string()),
|
||||||
Some(1000),
|
Some(1000),
|
||||||
Some(0.5),
|
Some(0.5),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
).unwrap();
|
).unwrap();
|
||||||
|
|
||||||
let messages = vec![
|
let messages = vec![Message::new(MessageRole::User, "Test message".to_string())];
|
||||||
Message {
|
|
||||||
role: MessageRole::User,
|
|
||||||
content: "Test message".to_string(),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
let request_body = provider
|
let request_body = provider
|
||||||
.create_request_body(&messages, None, false, 1000, 0.5)
|
.create_request_body(&messages, None, false, 1000, 0.5)
|
||||||
@@ -831,6 +845,8 @@ mod tests {
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
).unwrap();
|
).unwrap();
|
||||||
|
|
||||||
let tools = vec![
|
let tools = vec![
|
||||||
@@ -859,4 +875,48 @@ mod tests {
|
|||||||
assert!(anthropic_tools[0].input_schema.required.is_some());
|
assert!(anthropic_tools[0].input_schema.required.is_some());
|
||||||
assert_eq!(anthropic_tools[0].input_schema.required.as_ref().unwrap()[0], "location");
|
assert_eq!(anthropic_tools[0].input_schema.required.as_ref().unwrap()[0], "location");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cache_control_serialization() {
|
||||||
|
let provider = AnthropicProvider::new(
|
||||||
|
"test-key".to_string(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
|
// Test message WITHOUT cache_control
|
||||||
|
let messages_without = vec![Message::new(MessageRole::User, "Hello".to_string())];
|
||||||
|
let (_, anthropic_messages_without) = provider.convert_messages(&messages_without).unwrap();
|
||||||
|
let json_without = serde_json::to_string(&anthropic_messages_without).unwrap();
|
||||||
|
|
||||||
|
println!("Anthropic JSON without cache_control: {}", json_without);
|
||||||
|
// Check if cache_control appears in the JSON
|
||||||
|
if json_without.contains("cache_control") {
|
||||||
|
println!("WARNING: JSON contains 'cache_control' field when not configured!");
|
||||||
|
assert!(!json_without.contains("\"cache_control\":null"),
|
||||||
|
"JSON should not contain 'cache_control: null'");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test message WITH cache_control
|
||||||
|
let messages_with = vec![Message::with_cache_control(
|
||||||
|
MessageRole::User,
|
||||||
|
"Hello".to_string(),
|
||||||
|
crate::CacheControl::ephemeral(),
|
||||||
|
)];
|
||||||
|
let (_, anthropic_messages_with) = provider.convert_messages(&messages_with).unwrap();
|
||||||
|
let json_with = serde_json::to_string(&anthropic_messages_with).unwrap();
|
||||||
|
|
||||||
|
println!("Anthropic JSON with cache_control: {}", json_with);
|
||||||
|
assert!(json_with.contains("cache_control"),
|
||||||
|
"JSON should contain 'cache_control' field when configured");
|
||||||
|
assert!(json_with.contains("ephemeral"),
|
||||||
|
"JSON should contain 'ephemeral' type");
|
||||||
|
|
||||||
|
// The key assertion: when cache_control is None, it should not appear in JSON
|
||||||
|
assert!(!json_without.contains("cache_control") || !json_without.contains("null"),
|
||||||
|
"JSON should not contain 'cache_control' field or null values when not configured");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,10 +39,7 @@
|
|||||||
//! // Create a completion request
|
//! // Create a completion request
|
||||||
//! let request = CompletionRequest {
|
//! let request = CompletionRequest {
|
||||||
//! messages: vec![
|
//! messages: vec![
|
||||||
//! Message {
|
//! Message::new(MessageRole::User, "Hello! How are you?".to_string()),
|
||||||
//! role: MessageRole::User,
|
|
||||||
//! content: "Hello! How are you?".to_string(),
|
|
||||||
//! },
|
|
||||||
//! ],
|
//! ],
|
||||||
//! max_tokens: Some(1000),
|
//! max_tokens: Some(1000),
|
||||||
//! temperature: Some(0.7),
|
//! temperature: Some(0.7),
|
||||||
@@ -251,9 +248,12 @@ impl DatabricksProvider {
|
|||||||
MessageRole::Assistant => "assistant",
|
MessageRole::Assistant => "assistant",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Always use simple string format (Databricks doesn't support cache_control)
|
||||||
|
let content = serde_json::Value::String(message.content.clone());
|
||||||
|
|
||||||
databricks_messages.push(DatabricksMessage {
|
databricks_messages.push(DatabricksMessage {
|
||||||
role: role.to_string(),
|
role: role.to_string(),
|
||||||
content: Some(message.content.clone()),
|
content: Some(content),
|
||||||
tool_calls: None, // Only used in responses, not requests
|
tool_calls: None, // Only used in responses, not requests
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -864,8 +864,22 @@ impl LLMProvider for DatabricksProvider {
|
|||||||
let content = databricks_response
|
let content = databricks_response
|
||||||
.choices
|
.choices
|
||||||
.first()
|
.first()
|
||||||
.and_then(|choice| choice.message.content.as_ref())
|
.and_then(|choice| {
|
||||||
.cloned()
|
choice.message.content.as_ref().map(|c| {
|
||||||
|
// Handle both string and array formats
|
||||||
|
if let Some(s) = c.as_str() {
|
||||||
|
s.to_string()
|
||||||
|
} else if let Some(arr) = c.as_array() {
|
||||||
|
// Extract text from content blocks
|
||||||
|
arr.iter()
|
||||||
|
.filter_map(|block| block.get("text").and_then(|t| t.as_str()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("")
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
// Check if there are tool calls in the response
|
// Check if there are tool calls in the response
|
||||||
@@ -1037,6 +1051,10 @@ impl LLMProvider for DatabricksProvider {
|
|||||||
// This includes Claude, Llama, DBRX, and most other models on the platform
|
// This includes Claude, Llama, DBRX, and most other models on the platform
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn supports_cache_control(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Databricks API request/response structures
|
// Databricks API request/response structures
|
||||||
@@ -1067,7 +1085,8 @@ struct DatabricksFunction {
|
|||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct DatabricksMessage {
|
struct DatabricksMessage {
|
||||||
role: String,
|
role: String,
|
||||||
content: Option<String>, // Make content optional since tool calls might not have content
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
content: Option<serde_json::Value>, // Can be string or array of content blocks
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
tool_calls: Option<Vec<DatabricksToolCall>>, // Add tool_calls field for responses
|
tool_calls: Option<Vec<DatabricksToolCall>>, // Add tool_calls field for responses
|
||||||
}
|
}
|
||||||
@@ -1154,18 +1173,9 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let messages = vec![
|
let messages = vec![
|
||||||
Message {
|
Message::new(MessageRole::System, "You are a helpful assistant.".to_string()),
|
||||||
role: MessageRole::System,
|
Message::new(MessageRole::User, "Hello!".to_string()),
|
||||||
content: "You are a helpful assistant.".to_string(),
|
Message::new(MessageRole::Assistant, "Hi there!".to_string()),
|
||||||
},
|
|
||||||
Message {
|
|
||||||
role: MessageRole::User,
|
|
||||||
content: "Hello!".to_string(),
|
|
||||||
},
|
|
||||||
Message {
|
|
||||||
role: MessageRole::Assistant,
|
|
||||||
content: "Hi there!".to_string(),
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
let databricks_messages = provider.convert_messages(&messages).unwrap();
|
let databricks_messages = provider.convert_messages(&messages).unwrap();
|
||||||
@@ -1187,10 +1197,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let messages = vec![Message {
|
let messages = vec![Message::new(MessageRole::User, "Test message".to_string())];
|
||||||
role: MessageRole::User,
|
|
||||||
content: "Test message".to_string(),
|
|
||||||
}];
|
|
||||||
|
|
||||||
let request_body = provider
|
let request_body = provider
|
||||||
.create_request_body(&messages, None, false, 1000, 0.5)
|
.create_request_body(&messages, None, false, 1000, 0.5)
|
||||||
@@ -1273,4 +1280,62 @@ mod tests {
|
|||||||
assert!(llama_provider.has_native_tool_calling());
|
assert!(llama_provider.has_native_tool_calling());
|
||||||
assert!(dbrx_provider.has_native_tool_calling());
|
assert!(dbrx_provider.has_native_tool_calling());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cache_control_serialization() {
|
||||||
|
let provider = DatabricksProvider::from_token(
|
||||||
|
"https://test.databricks.com".to_string(),
|
||||||
|
"test-token".to_string(),
|
||||||
|
"databricks-claude-sonnet-4".to_string(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Test message WITHOUT cache_control
|
||||||
|
let messages_without = vec![Message::new(MessageRole::User, "Hello".to_string())];
|
||||||
|
let databricks_messages_without = provider.convert_messages(&messages_without).unwrap();
|
||||||
|
let json_without = serde_json::to_string(&databricks_messages_without).unwrap();
|
||||||
|
|
||||||
|
println!("JSON without cache_control: {}", json_without);
|
||||||
|
assert!(!json_without.contains("cache_control"),
|
||||||
|
"JSON should not contain 'cache_control' field when not configured");
|
||||||
|
|
||||||
|
// Test message WITH cache_control - should still NOT include it (Databricks doesn't support it)
|
||||||
|
let messages_with = vec![Message::with_cache_control(
|
||||||
|
MessageRole::User,
|
||||||
|
"Hello".to_string(),
|
||||||
|
crate::CacheControl::ephemeral(),
|
||||||
|
)];
|
||||||
|
let databricks_messages_with = provider.convert_messages(&messages_with).unwrap();
|
||||||
|
let json_with = serde_json::to_string(&databricks_messages_with).unwrap();
|
||||||
|
|
||||||
|
println!("JSON with cache_control: {}", json_with);
|
||||||
|
assert!(!json_with.contains("cache_control"),
|
||||||
|
"JSON should NOT contain 'cache_control' field - Databricks doesn't support it");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_databricks_does_not_support_cache_control() {
|
||||||
|
let claude_provider = DatabricksProvider::from_token(
|
||||||
|
"https://test.databricks.com".to_string(),
|
||||||
|
"test-token".to_string(),
|
||||||
|
"databricks-claude-sonnet-4".to_string(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let llama_provider = DatabricksProvider::from_token(
|
||||||
|
"https://test.databricks.com".to_string(),
|
||||||
|
"test-token".to_string(),
|
||||||
|
"databricks-meta-llama-3-3-70b-instruct".to_string(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(!claude_provider.supports_cache_control(), "Databricks should not support cache_control even for Claude models");
|
||||||
|
assert!(!llama_provider.supports_cache_control(), "Databricks should not support cache_control for Llama models");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ pub trait LLMProvider: Send + Sync {
|
|||||||
fn has_native_tool_calling(&self) -> bool {
|
fn has_native_tool_calling(&self) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if the provider supports cache control
|
||||||
|
fn supports_cache_control(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -32,10 +37,40 @@ pub struct CompletionRequest {
|
|||||||
pub tools: Option<Vec<Tool>>,
|
pub tools: Option<Vec<Tool>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct CacheControl {
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub cache_type: CacheType,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub ttl: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum CacheType {
|
||||||
|
Ephemeral,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CacheControl {
|
||||||
|
pub fn ephemeral() -> Self {
|
||||||
|
Self { cache_type: CacheType::Ephemeral, ttl: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn five_minute() -> Self {
|
||||||
|
Self { cache_type: CacheType::Ephemeral, ttl: Some("5m".to_string()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn one_hour() -> Self {
|
||||||
|
Self { cache_type: CacheType::Ephemeral, ttl: Some("1h".to_string()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Message {
|
pub struct Message {
|
||||||
pub role: MessageRole,
|
pub role: MessageRole,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub cache_control: Option<CacheControl>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -95,6 +130,45 @@ pub use databricks::DatabricksProvider;
|
|||||||
pub use embedded::EmbeddedProvider;
|
pub use embedded::EmbeddedProvider;
|
||||||
pub use openai::OpenAIProvider;
|
pub use openai::OpenAIProvider;
|
||||||
|
|
||||||
|
impl Message {
|
||||||
|
/// Create a new message with optional cache control
|
||||||
|
pub fn new(role: MessageRole, content: String) -> Self {
|
||||||
|
Self {
|
||||||
|
role,
|
||||||
|
content,
|
||||||
|
cache_control: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new message with cache control
|
||||||
|
pub fn with_cache_control(role: MessageRole, content: String, cache_control: CacheControl) -> Self {
|
||||||
|
Self {
|
||||||
|
role,
|
||||||
|
content,
|
||||||
|
cache_control: Some(cache_control),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a message with cache control, with provider validation
|
||||||
|
pub fn with_cache_control_validated(
|
||||||
|
role: MessageRole,
|
||||||
|
content: String,
|
||||||
|
cache_control: CacheControl,
|
||||||
|
provider: &dyn LLMProvider
|
||||||
|
) -> Self {
|
||||||
|
if !provider.supports_cache_control() {
|
||||||
|
tracing::warn!(
|
||||||
|
"Cache control requested for provider '{}' which does not support it. \
|
||||||
|
Cache control is only supported by Anthropic and Anthropic via Databricks.",
|
||||||
|
provider.name()
|
||||||
|
);
|
||||||
|
return Self::new(role, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::with_cache_control(role, content, cache_control)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Provider registry for managing multiple LLM providers
|
/// Provider registry for managing multiple LLM providers
|
||||||
pub struct ProviderRegistry {
|
pub struct ProviderRegistry {
|
||||||
providers: HashMap<String, Box<dyn LLMProvider>>,
|
providers: HashMap<String, Box<dyn LLMProvider>>,
|
||||||
@@ -144,3 +218,68 @@ impl Default for ProviderRegistry {
|
|||||||
Self::new()
|
Self::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_message_serialization_without_cache_control() {
|
||||||
|
let msg = Message::new(MessageRole::User, "Hello".to_string());
|
||||||
|
let json = serde_json::to_string(&msg).unwrap();
|
||||||
|
|
||||||
|
println!("Message JSON without cache_control: {}", json);
|
||||||
|
assert!(!json.contains("cache_control"),
|
||||||
|
"JSON should not contain 'cache_control' field when not configured");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_message_serialization_with_cache_control() {
|
||||||
|
let msg = Message::with_cache_control(
|
||||||
|
MessageRole::User,
|
||||||
|
"Hello".to_string(),
|
||||||
|
CacheControl::ephemeral(),
|
||||||
|
);
|
||||||
|
let json = serde_json::to_string(&msg).unwrap();
|
||||||
|
|
||||||
|
println!("Message JSON with cache_control: {}", json);
|
||||||
|
assert!(json.contains("cache_control"),
|
||||||
|
"JSON should contain 'cache_control' field when configured");
|
||||||
|
assert!(json.contains("ephemeral"),
|
||||||
|
"JSON should contain 'ephemeral' value");
|
||||||
|
assert!(json.contains("\"type\":"),
|
||||||
|
"JSON should contain 'type' field in cache_control");
|
||||||
|
assert!(!json.contains("null"),
|
||||||
|
"JSON should not contain null values");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cache_control_five_minute_serialization() {
|
||||||
|
let msg = Message::with_cache_control(
|
||||||
|
MessageRole::User,
|
||||||
|
"Hello".to_string(),
|
||||||
|
CacheControl::five_minute(),
|
||||||
|
);
|
||||||
|
let json = serde_json::to_string(&msg).unwrap();
|
||||||
|
|
||||||
|
println!("Message JSON with 5-minute cache_control: {}", json);
|
||||||
|
assert!(json.contains("cache_control"), "JSON should contain 'cache_control' field");
|
||||||
|
assert!(json.contains("ephemeral"), "JSON should contain 'ephemeral' type");
|
||||||
|
assert!(json.contains("\"ttl\":\"5m\""), "JSON should contain ttl field with 5m value");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cache_control_one_hour_serialization() {
|
||||||
|
let msg = Message::with_cache_control(
|
||||||
|
MessageRole::User,
|
||||||
|
"Hello".to_string(),
|
||||||
|
CacheControl::one_hour(),
|
||||||
|
);
|
||||||
|
let json = serde_json::to_string(&msg).unwrap();
|
||||||
|
|
||||||
|
println!("Message JSON with 1-hour cache_control: {}", json);
|
||||||
|
assert!(json.contains("cache_control"), "JSON should contain 'cache_control' field");
|
||||||
|
assert!(json.contains("ephemeral"), "JSON should contain 'ephemeral' type");
|
||||||
|
assert!(json.contains("\"ttl\":\"1h\""), "JSON should contain ttl field with 1h value");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
131
crates/g3-providers/tests/cache_control_error_regression_test.rs
Normal file
131
crates/g3-providers/tests/cache_control_error_regression_test.rs
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
//! Regression test for cache_control serialization bug
|
||||||
|
//!
|
||||||
|
//! This test verifies that cache_control is NOT serialized in the wrong format.
|
||||||
|
//! The bug was that it serialized as:
|
||||||
|
//! - `system.0.cache_control.ephemeral.ttl` (WRONG)
|
||||||
|
//!
|
||||||
|
//! It should serialize as:
|
||||||
|
//! - `"cache_control": {"type": "ephemeral"}` for ephemeral
|
||||||
|
//! - `"cache_control": {"type": "ephemeral", "ttl": "5m"}` for 5minute
|
||||||
|
//! - `"cache_control": {"type": "ephemeral", "ttl": "1h"}` for 1hour
|
||||||
|
|
||||||
|
use g3_providers::{CacheControl, Message, MessageRole};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_no_wrong_serialization_format() {
|
||||||
|
// Test ephemeral
|
||||||
|
let msg = Message::with_cache_control(
|
||||||
|
MessageRole::System,
|
||||||
|
"Test".to_string(),
|
||||||
|
CacheControl::ephemeral(),
|
||||||
|
);
|
||||||
|
let json = serde_json::to_string(&msg).unwrap();
|
||||||
|
|
||||||
|
println!("Ephemeral message JSON: {}", json);
|
||||||
|
|
||||||
|
// Should NOT contain the wrong format
|
||||||
|
assert!(!json.contains("system.0.cache_control"),
|
||||||
|
"JSON should not contain 'system.0.cache_control' path");
|
||||||
|
assert!(!json.contains("cache_control.ephemeral"),
|
||||||
|
"JSON should not contain 'cache_control.ephemeral' path");
|
||||||
|
|
||||||
|
// Should contain the correct format
|
||||||
|
assert!(json.contains(r#""cache_control":{"type":"ephemeral"}"#),
|
||||||
|
"JSON should contain correct cache_control format");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_five_minute_no_wrong_format() {
|
||||||
|
let msg = Message::with_cache_control(
|
||||||
|
MessageRole::System,
|
||||||
|
"Test".to_string(),
|
||||||
|
CacheControl::five_minute(),
|
||||||
|
);
|
||||||
|
let json = serde_json::to_string(&msg).unwrap();
|
||||||
|
|
||||||
|
println!("5-minute message JSON: {}", json);
|
||||||
|
|
||||||
|
// Should NOT contain the wrong format
|
||||||
|
assert!(!json.contains("system.0.cache_control"),
|
||||||
|
"JSON should not contain 'system.0.cache_control' path");
|
||||||
|
assert!(!json.contains("cache_control.ephemeral.ttl"),
|
||||||
|
"JSON should not contain 'cache_control.ephemeral.ttl' path");
|
||||||
|
|
||||||
|
// Should contain the correct format with ttl as a direct field
|
||||||
|
assert!(json.contains(r#""type":"ephemeral""#),
|
||||||
|
"JSON should contain type field");
|
||||||
|
assert!(json.contains(r#""ttl":"5m""#),
|
||||||
|
"JSON should contain ttl field with value 5m");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_one_hour_no_wrong_format() {
|
||||||
|
let msg = Message::with_cache_control(
|
||||||
|
MessageRole::System,
|
||||||
|
"Test".to_string(),
|
||||||
|
CacheControl::one_hour(),
|
||||||
|
);
|
||||||
|
let json = serde_json::to_string(&msg).unwrap();
|
||||||
|
|
||||||
|
println!("1-hour message JSON: {}", json);
|
||||||
|
|
||||||
|
// Should NOT contain the wrong format
|
||||||
|
assert!(!json.contains("system.0.cache_control"),
|
||||||
|
"JSON should not contain 'system.0.cache_control' path");
|
||||||
|
assert!(!json.contains("cache_control.ephemeral.ttl"),
|
||||||
|
"JSON should not contain 'cache_control.ephemeral.ttl' path");
|
||||||
|
|
||||||
|
// Should contain the correct format with ttl as a direct field
|
||||||
|
assert!(json.contains(r#""type":"ephemeral""#),
|
||||||
|
"JSON should contain type field");
|
||||||
|
assert!(json.contains(r#""ttl":"1h""#),
|
||||||
|
"JSON should contain ttl field with value 1h");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cache_control_structure_is_flat() {
|
||||||
|
// Verify that the cache_control object has a flat structure
|
||||||
|
// with 'type' and optional 'ttl' at the same level
|
||||||
|
|
||||||
|
let cache_control = CacheControl::five_minute();
|
||||||
|
let json_value = serde_json::to_value(&cache_control).unwrap();
|
||||||
|
|
||||||
|
println!("Cache control as JSON value: {}", serde_json::to_string_pretty(&json_value).unwrap());
|
||||||
|
|
||||||
|
let obj = json_value.as_object().expect("Should be an object");
|
||||||
|
|
||||||
|
// Should have exactly 2 keys at the top level
|
||||||
|
assert_eq!(obj.len(), 2, "Cache control should have exactly 2 top-level fields");
|
||||||
|
|
||||||
|
// Both 'type' and 'ttl' should be at the same level
|
||||||
|
assert!(obj.contains_key("type"), "Should have 'type' field");
|
||||||
|
assert!(obj.contains_key("ttl"), "Should have 'ttl' field");
|
||||||
|
|
||||||
|
// 'type' should be a string, not an object
|
||||||
|
assert!(obj["type"].is_string(), "'type' should be a string value");
|
||||||
|
|
||||||
|
// 'ttl' should be a string, not nested
|
||||||
|
assert!(obj["ttl"].is_string(), "'ttl' should be a string value");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ephemeral_cache_control_structure() {
|
||||||
|
let cache_control = CacheControl::ephemeral();
|
||||||
|
let json_value = serde_json::to_value(&cache_control).unwrap();
|
||||||
|
|
||||||
|
println!("Ephemeral cache control as JSON value: {}", serde_json::to_string_pretty(&json_value).unwrap());
|
||||||
|
|
||||||
|
let obj = json_value.as_object().expect("Should be an object");
|
||||||
|
|
||||||
|
// Should have exactly 1 key (only 'type', no 'ttl')
|
||||||
|
assert_eq!(obj.len(), 1, "Ephemeral cache control should have exactly 1 top-level field");
|
||||||
|
|
||||||
|
// Should have 'type' field
|
||||||
|
assert!(obj.contains_key("type"), "Should have 'type' field");
|
||||||
|
|
||||||
|
// Should NOT have 'ttl' field
|
||||||
|
assert!(!obj.contains_key("ttl"), "Ephemeral should not have 'ttl' field");
|
||||||
|
|
||||||
|
// 'type' should be a string with value "ephemeral"
|
||||||
|
assert_eq!(obj["type"].as_str().unwrap(), "ephemeral");
|
||||||
|
}
|
||||||
164
crates/g3-providers/tests/cache_control_integration_test.rs
Normal file
164
crates/g3-providers/tests/cache_control_integration_test.rs
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
//! Integration tests for cache_control feature
|
||||||
|
//!
|
||||||
|
//! These tests verify that cache_control is correctly serialized in messages
|
||||||
|
//! for both Anthropic and Databricks providers.
|
||||||
|
|
||||||
|
use g3_providers::{CacheControl, Message, MessageRole};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ephemeral_cache_control_serialization() {
|
||||||
|
let cache_control = CacheControl::ephemeral();
|
||||||
|
let json = serde_json::to_value(&cache_control).unwrap();
|
||||||
|
|
||||||
|
println!("Ephemeral cache_control JSON: {}", serde_json::to_string(&json).unwrap());
|
||||||
|
|
||||||
|
assert_eq!(json, json!({
|
||||||
|
"type": "ephemeral"
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Verify no ttl field is present
|
||||||
|
assert!(!json.as_object().unwrap().contains_key("ttl"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_five_minute_cache_control_serialization() {
|
||||||
|
let cache_control = CacheControl::five_minute();
|
||||||
|
let json = serde_json::to_value(&cache_control).unwrap();
|
||||||
|
|
||||||
|
println!("5-minute cache_control JSON: {}", serde_json::to_string(&json).unwrap());
|
||||||
|
|
||||||
|
assert_eq!(json, json!({
|
||||||
|
"type": "ephemeral",
|
||||||
|
"ttl": "5m"
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_one_hour_cache_control_serialization() {
|
||||||
|
let cache_control = CacheControl::one_hour();
|
||||||
|
let json = serde_json::to_value(&cache_control).unwrap();
|
||||||
|
|
||||||
|
println!("1-hour cache_control JSON: {}", serde_json::to_string(&json).unwrap());
|
||||||
|
|
||||||
|
assert_eq!(json, json!({
|
||||||
|
"type": "ephemeral",
|
||||||
|
"ttl": "1h"
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_message_with_ephemeral_cache_control() {
|
||||||
|
let msg = Message::with_cache_control(
|
||||||
|
MessageRole::System,
|
||||||
|
"System prompt".to_string(),
|
||||||
|
CacheControl::ephemeral(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let json = serde_json::to_value(&msg).unwrap();
|
||||||
|
println!("Message with ephemeral cache_control: {}", serde_json::to_string(&json).unwrap());
|
||||||
|
|
||||||
|
let cache_control = json.get("cache_control").expect("cache_control field should exist");
|
||||||
|
assert_eq!(cache_control.get("type").unwrap(), "ephemeral");
|
||||||
|
assert!(!cache_control.as_object().unwrap().contains_key("ttl"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_message_with_five_minute_cache_control() {
|
||||||
|
let msg = Message::with_cache_control(
|
||||||
|
MessageRole::System,
|
||||||
|
"System prompt".to_string(),
|
||||||
|
CacheControl::five_minute(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let json = serde_json::to_value(&msg).unwrap();
|
||||||
|
println!("Message with 5-minute cache_control: {}", serde_json::to_string(&json).unwrap());
|
||||||
|
|
||||||
|
let cache_control = json.get("cache_control").expect("cache_control field should exist");
|
||||||
|
assert_eq!(cache_control.get("type").unwrap(), "ephemeral");
|
||||||
|
assert_eq!(cache_control.get("ttl").unwrap(), "5m");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_message_with_one_hour_cache_control() {
|
||||||
|
let msg = Message::with_cache_control(
|
||||||
|
MessageRole::System,
|
||||||
|
"System prompt".to_string(),
|
||||||
|
CacheControl::one_hour(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let json = serde_json::to_value(&msg).unwrap();
|
||||||
|
println!("Message with 1-hour cache_control: {}", serde_json::to_string(&json).unwrap());
|
||||||
|
|
||||||
|
let cache_control = json.get("cache_control").expect("cache_control field should exist");
|
||||||
|
assert_eq!(cache_control.get("type").unwrap(), "ephemeral");
|
||||||
|
assert_eq!(cache_control.get("ttl").unwrap(), "1h");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_message_without_cache_control() {
|
||||||
|
let msg = Message::new(MessageRole::User, "Hello".to_string());
|
||||||
|
|
||||||
|
let json = serde_json::to_value(&msg).unwrap();
|
||||||
|
println!("Message without cache_control: {}", serde_json::to_string(&json).unwrap());
|
||||||
|
|
||||||
|
// cache_control field should not be present when not set
|
||||||
|
assert!(!json.as_object().unwrap().contains_key("cache_control"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cache_control_json_format_ephemeral() {
|
||||||
|
let cache_control = CacheControl::ephemeral();
|
||||||
|
let json_str = serde_json::to_string(&cache_control).unwrap();
|
||||||
|
|
||||||
|
println!("Ephemeral JSON string: {}", json_str);
|
||||||
|
|
||||||
|
// Verify exact JSON format
|
||||||
|
assert_eq!(json_str, r#"{"type":"ephemeral"}"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cache_control_json_format_five_minute() {
|
||||||
|
let cache_control = CacheControl::five_minute();
|
||||||
|
let json_str = serde_json::to_string(&cache_control).unwrap();
|
||||||
|
|
||||||
|
println!("5-minute JSON string: {}", json_str);
|
||||||
|
|
||||||
|
// Verify exact JSON format
|
||||||
|
assert_eq!(json_str, r#"{"type":"ephemeral","ttl":"5m"}"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cache_control_json_format_one_hour() {
|
||||||
|
let cache_control = CacheControl::one_hour();
|
||||||
|
let json_str = serde_json::to_string(&cache_control).unwrap();
|
||||||
|
|
||||||
|
println!("1-hour JSON string: {}", json_str);
|
||||||
|
|
||||||
|
// Verify exact JSON format
|
||||||
|
assert_eq!(json_str, r#"{"type":"ephemeral","ttl":"1h"}"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialization_ephemeral() {
|
||||||
|
let json_str = r#"{"type":"ephemeral"}"#;
|
||||||
|
let cache_control: CacheControl = serde_json::from_str(json_str).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(cache_control.ttl, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialization_five_minute() {
|
||||||
|
let json_str = r#"{"type":"ephemeral","ttl":"5m"}"#;
|
||||||
|
let cache_control: CacheControl = serde_json::from_str(json_str).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(cache_control.ttl, Some("5m".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialization_one_hour() {
|
||||||
|
let json_str = r#"{"type":"ephemeral","ttl":"1h"}"#;
|
||||||
|
let cache_control: CacheControl = serde_json::from_str(json_str).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(cache_control.ttl, Some("1h".to_string()));
|
||||||
|
}
|
||||||
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