feat: move TODO lists to session-scoped directories
TODO lists are now stored in .g3/sessions/<session_id>/todo.g3.md instead of the workspace root. This prevents different g3 sessions from accidentally picking up or overwriting each other's TODOs. Changes: - Add get_session_todo_path() function in paths.rs - Update todo_read/todo_write handlers to use session-specific paths - Remove TODO loading at Agent initialization (sessions start fresh) - Update prompts to reflect session-scoped behavior Fallback behavior preserved for planner mode (G3_TODO_PATH env var).
This commit is contained in:
@@ -51,7 +51,7 @@ pub use paths::{
|
|||||||
G3_WORKSPACE_PATH_ENV, ensure_session_dir, get_context_summary_file, get_g3_dir, get_logs_dir,
|
G3_WORKSPACE_PATH_ENV, ensure_session_dir, get_context_summary_file, get_g3_dir, get_logs_dir,
|
||||||
get_session_file, get_session_logs_dir, get_thinned_dir, logs_dir,
|
get_session_file, get_session_logs_dir, get_thinned_dir, logs_dir,
|
||||||
};
|
};
|
||||||
use paths::get_todo_path;
|
use paths::{get_todo_path, get_session_todo_path};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ToolCall {
|
pub struct ToolCall {
|
||||||
@@ -1114,23 +1114,9 @@ impl<W: UiWriter> Agent<W> {
|
|||||||
context_window.add_message(readme_message);
|
context_window.add_message(readme_message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load existing TODO list if present (after system prompt and README)
|
// NOTE: TODO lists are now session-scoped and stored in .g3/sessions/<session_id>/todo.g3.md
|
||||||
let todo_path = get_todo_path();
|
// We don't load any TODO at initialization since we don't have a session_id yet.
|
||||||
let initial_todo_content = if todo_path.exists() {
|
// The agent will use todo_read to load the TODO once a session is established.
|
||||||
std::fs::read_to_string(&todo_path).ok()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(ref todo_content) = initial_todo_content {
|
|
||||||
if !todo_content.trim().is_empty() {
|
|
||||||
let todo_message = Message::new(
|
|
||||||
MessageRole::System,
|
|
||||||
format!("📋 Existing TODO list (from todo.g3.md):\n\n{}", todo_content),
|
|
||||||
);
|
|
||||||
context_window.add_message(todo_message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize computer controller if enabled
|
// Initialize computer controller if enabled
|
||||||
let computer_controller = if config.computer_control.enabled {
|
let computer_controller = if config.computer_control.enabled {
|
||||||
@@ -1160,11 +1146,8 @@ impl<W: UiWriter> Agent<W> {
|
|||||||
session_id: None,
|
session_id: None,
|
||||||
tool_call_metrics: Vec::new(),
|
tool_call_metrics: Vec::new(),
|
||||||
ui_writer,
|
ui_writer,
|
||||||
todo_content: std::sync::Arc::new(tokio::sync::RwLock::new({
|
// TODO content starts empty - session-scoped TODOs are loaded via todo_read
|
||||||
// Initialize from TODO.md file if it exists
|
todo_content: std::sync::Arc::new(tokio::sync::RwLock::new(String::new())),
|
||||||
let todo_path = get_todo_path();
|
|
||||||
std::fs::read_to_string(&todo_path).unwrap_or_default()
|
|
||||||
})),
|
|
||||||
is_autonomous,
|
is_autonomous,
|
||||||
quiet,
|
quiet,
|
||||||
computer_controller,
|
computer_controller,
|
||||||
@@ -2946,7 +2929,7 @@ impl<W: UiWriter> Agent<W> {
|
|||||||
},
|
},
|
||||||
Tool {
|
Tool {
|
||||||
name: "todo_read".to_string(),
|
name: "todo_read".to_string(),
|
||||||
description: "Read your current TODO list from todo.g3.md file in the workspace directory. Shows what tasks are planned and their status. Call this at the start of multi-step tasks to check for existing plans, and during execution to review progress before updating. TODO lists persist across g3 sessions.".to_string(),
|
description: "Read your current TODO list from todo.g3.md file in the session directory. Shows what tasks are planned and their status. Call this at the start of multi-step tasks to check for existing plans, and during execution to review progress before updating. TODO lists are scoped to the current session.".to_string(),
|
||||||
input_schema: json!({
|
input_schema: json!({
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {},
|
"properties": {},
|
||||||
@@ -2955,7 +2938,7 @@ impl<W: UiWriter> Agent<W> {
|
|||||||
},
|
},
|
||||||
Tool {
|
Tool {
|
||||||
name: "todo_write".to_string(),
|
name: "todo_write".to_string(),
|
||||||
description: "Create or update your TODO list in todo.g3.md file with a complete task plan. Use markdown checkboxes: - [ ] for pending, - [x] for complete. This tool replaces the entire file content, so always call todo_read first to preserve existing content. Essential for multi-step tasks. Changes persist across g3 sessions.".to_string(),
|
description: "Create or update your TODO list in todo.g3.md file with a complete task plan. Use markdown checkboxes: - [ ] for pending, - [x] for complete. This tool replaces the entire file content, so always call todo_read first to preserve existing content. Essential for multi-step tasks. TODO lists are scoped to the current session.".to_string(),
|
||||||
input_schema: json!({
|
input_schema: json!({
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -5331,8 +5314,13 @@ impl<W: UiWriter> Agent<W> {
|
|||||||
}
|
}
|
||||||
"todo_read" => {
|
"todo_read" => {
|
||||||
debug!("Processing todo_read tool call");
|
debug!("Processing todo_read tool call");
|
||||||
// Read from todo.g3.md file (uses G3_TODO_PATH env var if set, else current dir)
|
// Read from session-specific todo.g3.md if we have a session, else fall back to workspace
|
||||||
let todo_path = get_todo_path();
|
let todo_path = if let Some(ref session_id) = self.session_id {
|
||||||
|
let _ = ensure_session_dir(session_id); // Ensure dir exists
|
||||||
|
get_session_todo_path(session_id)
|
||||||
|
} else {
|
||||||
|
get_todo_path()
|
||||||
|
};
|
||||||
|
|
||||||
if !todo_path.exists() {
|
if !todo_path.exists() {
|
||||||
// Also update in-memory content to stay in sync
|
// Also update in-memory content to stay in sync
|
||||||
@@ -5447,7 +5435,12 @@ impl<W: UiWriter> Agent<W> {
|
|||||||
// If all todos are complete, delete the file instead of writing
|
// If all todos are complete, delete the file instead of writing
|
||||||
// EXCEPT in planner mode (G3_TODO_PATH is set) - preserve for rename to completed_todo_*.md
|
// EXCEPT in planner mode (G3_TODO_PATH is set) - preserve for rename to completed_todo_*.md
|
||||||
let in_planner_mode = std::env::var("G3_TODO_PATH").is_ok();
|
let in_planner_mode = std::env::var("G3_TODO_PATH").is_ok();
|
||||||
let todo_path = get_todo_path();
|
let todo_path = if let Some(ref session_id) = self.session_id {
|
||||||
|
let _ = ensure_session_dir(session_id); // Ensure dir exists
|
||||||
|
get_session_todo_path(session_id)
|
||||||
|
} else {
|
||||||
|
get_todo_path()
|
||||||
|
};
|
||||||
|
|
||||||
if !in_planner_mode && !has_incomplete && (content_str.contains("- [x]") || content_str.contains("- [X]")) {
|
if !in_planner_mode && !has_incomplete && (content_str.contains("- [x]") || content_str.contains("- [X]")) {
|
||||||
if todo_path.exists() {
|
if todo_path.exists() {
|
||||||
@@ -5469,8 +5462,6 @@ impl<W: UiWriter> Agent<W> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write to todo.g3.md file (uses G3_TODO_PATH env var if set, else current dir)
|
|
||||||
|
|
||||||
match std::fs::write(&todo_path, content_str) {
|
match std::fs::write(&todo_path, content_str) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// Also update in-memory content to stay in sync
|
// Also update in-memory content to stay in sync
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ pub fn get_todo_path() -> PathBuf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the path to the todo.g3.md file for a specific session.
|
||||||
|
/// Returns .g3/sessions/<session_id>/todo.g3.md
|
||||||
|
pub fn get_session_todo_path(session_id: &str) -> PathBuf {
|
||||||
|
get_session_logs_dir(session_id).join("todo.g3.md")
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the path to the logs directory.
|
/// Get the path to the logs directory.
|
||||||
///
|
///
|
||||||
/// Checks for G3_WORKSPACE_PATH environment variable first (used by planning mode),
|
/// Checks for G3_WORKSPACE_PATH environment variable first (used by planning mode),
|
||||||
@@ -110,15 +116,18 @@ mod tests {
|
|||||||
let thinned_dir = get_thinned_dir(session_id);
|
let thinned_dir = get_thinned_dir(session_id);
|
||||||
let session_file = get_session_file(session_id);
|
let session_file = get_session_file(session_id);
|
||||||
let summary_file = get_context_summary_file(session_id);
|
let summary_file = get_context_summary_file(session_id);
|
||||||
|
let todo_file = get_session_todo_path(session_id);
|
||||||
|
|
||||||
// All paths should be under the session directory
|
// All paths should be under the session directory
|
||||||
assert!(thinned_dir.starts_with(&session_dir));
|
assert!(thinned_dir.starts_with(&session_dir));
|
||||||
assert!(session_file.starts_with(&session_dir));
|
assert!(session_file.starts_with(&session_dir));
|
||||||
assert!(summary_file.starts_with(&session_dir));
|
assert!(summary_file.starts_with(&session_dir));
|
||||||
|
assert!(todo_file.starts_with(&session_dir));
|
||||||
|
|
||||||
// Check expected filenames
|
// Check expected filenames
|
||||||
assert!(thinned_dir.ends_with("thinned"));
|
assert!(thinned_dir.ends_with("thinned"));
|
||||||
assert!(session_file.ends_with("session.json"));
|
assert!(session_file.ends_with("session.json"));
|
||||||
assert!(summary_file.ends_with("context_summary.txt"));
|
assert!(summary_file.ends_with("context_summary.txt"));
|
||||||
|
assert!(todo_file.ends_with("todo.g3.md"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ Every multi-step task follows this pattern:
|
|||||||
2. **During**: Execute steps, then todo_read and todo_write to mark progress
|
2. **During**: Execute steps, then todo_read and todo_write to mark progress
|
||||||
3. **End**: Call todo_read to verify all items complete
|
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.
|
Note: todo_write replaces the entire todo.g3.md file, so always read first to preserve content. TODO lists are scoped to the current session and stored in the session 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:
|
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>}}`
|
`{{Based on the requirements file with SHA256: <SHA>}}`
|
||||||
@@ -270,11 +270,11 @@ Short description for providers without native calling specs:
|
|||||||
- **final_output**: Signal task completion with a detailed summary of work done in markdown format
|
- **final_output**: Signal task completion with a detailed summary of work done in markdown format
|
||||||
- Format: {\"tool\": \"final_output\", \"args\": {\"summary\": \"what_was_accomplished\"}
|
- Format: {\"tool\": \"final_output\", \"args\": {\"summary\": \"what_was_accomplished\"}
|
||||||
|
|
||||||
- **todo_read**: Read the entire TODO list from todo.g3.md file in workspace directory
|
- **todo_read**: Read the current session's TODO list from todo.g3.md (session-scoped)
|
||||||
- Format: {\"tool\": \"todo_read\", \"args\": {}}
|
- Format: {\"tool\": \"todo_read\", \"args\": {}}
|
||||||
- Example: {\"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)
|
- **todo_write**: Write or overwrite the session's todo.g3.md file (WARNING: overwrites completely, always read first)
|
||||||
- Format: {\"tool\": \"todo_write\", \"args\": {\"content\": \"- [ ] Task 1\\n- [ ] Task 2\"}}
|
- 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\"}}
|
- Example: {\"tool\": \"todo_write\", \"args\": {\"content\": \"- [ ] Implement feature\\n - [ ] Write tests\\n - [ ] Run tests\"}}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user