From 1090e30d6c706e6e735eab525de03c70ec48e7ac Mon Sep 17 00:00:00 2001 From: "Dhanji R. Prasanna" Date: Sun, 11 Jan 2026 06:35:18 +0800 Subject: [PATCH] Simplify system prompt: remove coding style and parallel tool call sections - Remove IMPORTANT FOR CODING section (~1,500 chars of coding guidelines) - Remove block (~500 chars) - Remove unused const_format dependency from g3-core - Simplify get_system_prompt_for_native() to just return base prompt - Response Guidelines now cleanly ends the static prompt Prompt reduced from ~8,500 to ~6,500 characters. --- Cargo.lock | 1 - crates/g3-core/Cargo.toml | 1 - crates/g3-core/src/prompts.rs | 79 +++++------ crates/g3-core/src/tool_definitions.rs | 31 ++++- crates/g3-core/src/tool_dispatch.rs | 5 +- crates/g3-core/src/tools/memory.rs | 178 +++++++++++++++++++++++++ crates/g3-core/src/tools/mod.rs | 2 + 7 files changed, 245 insertions(+), 52 deletions(-) create mode 100644 crates/g3-core/src/tools/memory.rs diff --git a/Cargo.lock b/Cargo.lock index afdc6a2..610cb1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1425,7 +1425,6 @@ dependencies = [ "async-trait", "base64 0.22.1", "chrono", - "const_format", "futures-util", "g3-computer-control", "g3-config", diff --git a/crates/g3-core/Cargo.toml b/crates/g3-core/Cargo.toml index a755ff6..ed812c3 100644 --- a/crates/g3-core/Cargo.toml +++ b/crates/g3-core/Cargo.toml @@ -43,7 +43,6 @@ tree-sitter-scheme = "0.24" streaming-iterator = "0.1" walkdir = "2.4" -const_format = "0.2" base64 = "0.22.1" [dev-dependencies] diff --git a/crates/g3-core/src/prompts.rs b/crates/g3-core/src/prompts.rs index 40cefa5..802b67b 100644 --- a/crates/g3-core/src/prompts.rs +++ b/crates/g3-core/src/prompts.rs @@ -1,29 +1,3 @@ -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”. -"; - 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. @@ -126,6 +100,39 @@ IMPORTANT: If the user asks you to just respond with text (like \"just say hello Do not explain what you're going to do - just do it by calling the tools. +# Project Memory + +Project memory (if available) is automatically loaded at startup alongside README.md and AGENTS.md. It contains feature locations, patterns, and entry points discovered in previous sessions. + +**IMPORTANT**: After completing a task where you discovered code locations, call the **`remember`** tool to save them. This helps avoid re-discovering the same code in future sessions. + +## Memory Format + +Use this format when calling `remember`: + +### +- `` [..] - `()`, `` + +## Patterns + +### +1. Step one +2. Step two + +## When to Remember + +Call `remember` at the end of your turn IF you discovered: +- A feature's location (file + char range + function/struct names) +- A useful pattern or workflow +- An entry point for a subsystem + +Do NOT save duplicates - check the Project Memory section (loaded at startup) to see what's already known. + +## Example + +After discovering where WebDriver tools live: + +{\"tool\": \"remember\", \"args\": {\"notes\": \"## Features\\n\\n### WebDriver Browser Automation\\n- crates/g3-core/src/tools/webdriver.rs [0..21750] - execute_webdriver_start(), execute_webdriver_navigate(), WebDriverSession\"}} # Response Guidelines @@ -134,21 +141,11 @@ Do not explain what you're going to do - just do it by calling the tools. - Use quick and clever humor when appropriate. "; -pub const SYSTEM_PROMPT_FOR_NATIVE_TOOL_USE: &'static str = - concatcp!(SYSTEM_NATIVE_TOOL_CALLS, CODING_STYLE); +pub const SYSTEM_PROMPT_FOR_NATIVE_TOOL_USE: &'static str = SYSTEM_NATIVE_TOOL_CALLS; /// Generate system prompt based on whether multiple tool calls are allowed pub fn get_system_prompt_for_native() -> String { - // Always allow multiple tool calls - they are processed sequentially after stream ends - 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. - - 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. - -" - ) + SYSTEM_PROMPT_FOR_NATIVE_TOOL_USE.to_string() } const SYSTEM_NON_NATIVE_TOOL_USE: &'static str = @@ -229,7 +226,7 @@ Short description for providers without native calling specs: 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. +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] @@ -318,15 +315,13 @@ Skip TODO tools for simple single-step tasks: 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); +pub const SYSTEM_PROMPT_FOR_NON_NATIVE_TOOL_USE: &'static str = SYSTEM_NON_NATIVE_TOOL_USE; /// The G3 identity line that gets replaced in agent mode const G3_IDENTITY_LINE: &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."; diff --git a/crates/g3-core/src/tool_definitions.rs b/crates/g3-core/src/tool_definitions.rs index 3b1770f..f1ae6df 100644 --- a/crates/g3-core/src/tool_definitions.rs +++ b/crates/g3-core/src/tool_definitions.rs @@ -272,6 +272,22 @@ fn create_core_tools(exclude_research: bool) -> Vec { }); } + // Project memory tool (memory is auto-loaded at startup, only remember is needed) + tools.push(Tool { + name: "remember".to_string(), + description: "Update the project memory with new discoveries. Call this at the END of your turn (before your summary) if you discovered something worth noting. Provide your notes in markdown format - they will be merged with existing memory.".to_string(), + input_schema: json!({ + "type": "object", + "properties": { + "notes": { + "type": "string", + "description": "New discoveries to add to memory in markdown format. Use the format:\n### Feature Name\n- `file/path.rs` [start..end] - `function_name()`, `StructName`\n\nDo not include content already in memory." + } + }, + "required": ["notes"] + }), + }); + tools } @@ -477,8 +493,9 @@ mod tests { let tools = create_core_tools(false); // Should have the core tools: shell, background_process, read_file, read_image, // write_file, str_replace, take_screenshot, - // todo_read, todo_write, code_coverage, code_search, research (12 total) - assert_eq!(tools.len(), 12); + // todo_read, todo_write, code_coverage, code_search, research, remember + // (13 total - memory is auto-loaded, only remember tool needed) + assert_eq!(tools.len(), 13); } #[test] @@ -492,15 +509,15 @@ mod tests { fn test_create_tool_definitions_core_only() { let config = ToolConfig::default(); let tools = create_tool_definitions(config); - assert_eq!(tools.len(), 12); + assert_eq!(tools.len(), 13); } #[test] fn test_create_tool_definitions_all_enabled() { let config = ToolConfig::new(true, true); let tools = create_tool_definitions(config); - // 12 core + 15 webdriver = 27 - assert_eq!(tools.len(), 27); + // 13 core + 15 webdriver = 28 + assert_eq!(tools.len(), 28); } #[test] @@ -518,8 +535,8 @@ mod tests { let tools_with_research = create_core_tools(false); let tools_without_research = create_core_tools(true); - assert_eq!(tools_with_research.len(), 12); - assert_eq!(tools_without_research.len(), 11); + assert_eq!(tools_with_research.len(), 13); + assert_eq!(tools_without_research.len(), 12); assert!(tools_with_research.iter().any(|t| t.name == "research")); assert!(!tools_without_research.iter().any(|t| t.name == "research")); diff --git a/crates/g3-core/src/tool_dispatch.rs b/crates/g3-core/src/tool_dispatch.rs index 02db63f..df33d66 100644 --- a/crates/g3-core/src/tool_dispatch.rs +++ b/crates/g3-core/src/tool_dispatch.rs @@ -7,7 +7,7 @@ use anyhow::Result; use tracing::{debug, warn}; use crate::tools::executor::ToolContext; -use crate::tools::{file_ops, misc, research, shell, todo, webdriver}; +use crate::tools::{file_ops, memory, misc, research, shell, todo, webdriver}; use crate::ui_writer::UiWriter; use crate::ToolCall; @@ -44,6 +44,9 @@ pub async fn dispatch_tool( // Research tool "research" => research::execute_research(tool_call, ctx).await, + // Project memory tools + "remember" => memory::execute_remember(tool_call, ctx).await, + // WebDriver tools "webdriver_start" => webdriver::execute_webdriver_start(tool_call, ctx).await, "webdriver_navigate" => webdriver::execute_webdriver_navigate(tool_call, ctx).await, diff --git a/crates/g3-core/src/tools/memory.rs b/crates/g3-core/src/tools/memory.rs new file mode 100644 index 0000000..c13263c --- /dev/null +++ b/crates/g3-core/src/tools/memory.rs @@ -0,0 +1,178 @@ +//! Project memory tool: remember. +//! +//! These tools provide a persistent "working memory" for the project, +//! storing feature locations, patterns, and entry points discovered +//! during g3 sessions. + +use anyhow::Result; +use chrono::Utc; +use std::path::PathBuf; + +use crate::ui_writer::UiWriter; +use crate::ToolCall; + +use super::executor::ToolContext; + +/// Get the path to the memory file. +/// Memory is stored at `.g3/memory.md` in the working directory. +fn get_memory_path(working_dir: Option<&str>) -> PathBuf { + let base = working_dir + .map(PathBuf::from) + .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))); + base.join(".g3").join("memory.md") +} + +/// Format the file size in a human-readable way. +fn format_size(chars: usize) -> String { + if chars < 1000 { + format!("{} chars", chars) + } else { + format!("{:.1}k chars", chars as f64 / 1000.0) + } +} + +/// Execute the remember tool. +/// Merges new notes with existing memory and saves to file. +pub async fn execute_remember( + tool_call: &ToolCall, + ctx: &mut ToolContext<'_, W>, +) -> Result { + let notes = tool_call + .args + .get("notes") + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow::anyhow!("Missing required 'notes' parameter"))?; + + let memory_path = get_memory_path(ctx.working_dir); + + // Ensure .g3 directory exists + if let Some(parent) = memory_path.parent() { + std::fs::create_dir_all(parent)?; + } + + // Read existing memory or create new + let existing = if memory_path.exists() { + std::fs::read_to_string(&memory_path)? + } else { + String::new() + }; + + // Merge notes with existing memory + let updated = merge_memory(&existing, notes); + + // Add/update header with timestamp and size + let timestamp = Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string(); + let size = format_size(updated.len()); + let final_content = update_header(&updated, ×tamp, &size); + + // Write back + std::fs::write(&memory_path, &final_content)?; + + ctx.ui_writer + .println(&format!("💾 Memory updated ({})", format_size(final_content.len()))); + + Ok(format!("Memory updated. Size: {}", format_size(final_content.len()))) +} + +/// Merge new notes into existing memory. +/// Appends new notes to the appropriate sections or creates new sections. +fn merge_memory(existing: &str, new_notes: &str) -> String { + if existing.is_empty() { + // Start fresh with just the notes + return new_notes.trim().to_string(); + } + + // Simple merge strategy: append new notes to the end + // The LLM is responsible for providing well-formatted notes + // and avoiding duplicates (as instructed in the prompt) + let existing_trimmed = existing.trim(); + let new_trimmed = new_notes.trim(); + + // Remove the header line if present (we'll re-add it) + let existing_body = remove_header(existing_trimmed); + + format!("{}\n\n{}", existing_body.trim(), new_trimmed) +} + +/// Remove the header line (# Project Memory and > Updated: ...) from content. +fn remove_header(content: &str) -> String { + let mut lines: Vec<&str> = content.lines().collect(); + + // Remove "# Project Memory" if first line + if !lines.is_empty() && lines[0].starts_with("# Project Memory") { + lines.remove(0); + } + + // Remove "> Updated: ..." line if present at start + if !lines.is_empty() && lines[0].starts_with("> Updated:") { + lines.remove(0); + } + + // Remove leading empty lines + while !lines.is_empty() && lines[0].trim().is_empty() { + lines.remove(0); + } + + lines.join("\n") +} + +/// Update or add the header with timestamp and size. +fn update_header(content: &str, timestamp: &str, size: &str) -> String { + let body = remove_header(content); + format!( + "# Project Memory\n> Updated: {} | Size: {}\n\n{}", + timestamp, + size, + body.trim() + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_format_size() { + assert_eq!(format_size(500), "500 chars"); + assert_eq!(format_size(999), "999 chars"); + assert_eq!(format_size(1000), "1.0k chars"); + assert_eq!(format_size(2500), "2.5k chars"); + assert_eq!(format_size(10000), "10.0k chars"); + } + + #[test] + fn test_merge_memory_empty() { + let result = merge_memory("", "### New Feature\n- `file.rs` [0..100] - `func()`"); + assert_eq!(result, "### New Feature\n- `file.rs` [0..100] - `func()`"); + } + + #[test] + fn test_merge_memory_append() { + let existing = "# Project Memory\n> Updated: 2025-01-10 | Size: 1k\n\n### Feature A\n- `a.rs` [0..50]"; + let new_notes = "### Feature B\n- `b.rs` [0..100]"; + let result = merge_memory(existing, new_notes); + + assert!(result.contains("### Feature A")); + assert!(result.contains("### Feature B")); + assert!(!result.contains("# Project Memory")); // Header removed for re-adding + } + + #[test] + fn test_remove_header() { + let content = "# Project Memory\n> Updated: 2025-01-10 | Size: 1k\n\n### Feature\n- details"; + let result = remove_header(content); + assert!(!result.contains("# Project Memory")); + assert!(!result.contains("> Updated:")); + assert!(result.contains("### Feature")); + } + + #[test] + fn test_update_header() { + let content = "### Feature\n- details"; + let result = update_header(content, "2025-01-10T12:00:00Z", "500 chars"); + + assert!(result.starts_with("# Project Memory")); + assert!(result.contains("> Updated: 2025-01-10T12:00:00Z | Size: 500 chars")); + assert!(result.contains("### Feature")); + } +} diff --git a/crates/g3-core/src/tools/mod.rs b/crates/g3-core/src/tools/mod.rs index a6b31a0..f7f95cd 100644 --- a/crates/g3-core/src/tools/mod.rs +++ b/crates/g3-core/src/tools/mod.rs @@ -8,9 +8,11 @@ //! - `webdriver` - Browser automation via WebDriver //! - `misc` - Other tools (screenshots, code search, etc.) //! - `research` - Web research via scout agent +//! - `memory` - Project memory (read_memory, remember) pub mod executor; pub mod file_ops; +pub mod memory; pub mod misc; pub mod research; pub mod shell;