Simplify system prompt: remove coding style and parallel tool call sections

- Remove IMPORTANT FOR CODING section (~1,500 chars of coding guidelines)
- Remove <use_parallel_tool_calls> 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.
This commit is contained in:
Dhanji R. Prasanna
2026-01-11 06:35:18 +08:00
parent 33c1aba86e
commit 1090e30d6c
7 changed files with 245 additions and 52 deletions

1
Cargo.lock generated
View File

@@ -1425,7 +1425,6 @@ dependencies = [
"async-trait",
"base64 0.22.1",
"chrono",
"const_format",
"futures-util",
"g3-computer-control",
"g3-config",

View File

@@ -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]

View File

@@ -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 (Dont 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 aint 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 dont 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`:
### <Feature Name>
- `<file_path>` [<start>..<end>] - `<function_name>()`, `<StructName>`
## Patterns
### <Pattern Name>
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.
<use_parallel_tool_calls>
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>
"
)
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 youre doing is WRITING files, and you dont 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.";

View File

@@ -272,6 +272,22 @@ fn create_core_tools(exclude_research: bool) -> Vec<Tool> {
});
}
// 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"));

View File

@@ -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<W: UiWriter>(
// 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,

View File

@@ -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<W: UiWriter>(
tool_call: &ToolCall,
ctx: &mut ToolContext<'_, W>,
) -> Result<String> {
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, &timestamp, &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"));
}
}

View File

@@ -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;