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:
Dhanji R. Prasanna
2025-12-25 18:33:03 +11:00
parent d9c58576a1
commit 64f27c0abc
3 changed files with 33 additions and 33 deletions

View File

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

View File

@@ -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"));
} }
} }

View File

@@ -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\"}}