Skip to content

Commit 4ffff74

Browse files
committed
feat:implemented todo list
1 parent fb852bd commit 4ffff74

3 files changed

Lines changed: 177 additions & 22 deletions

File tree

lambda_agent/agent.py

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -64,19 +64,20 @@ def __init__(self):
6464
"and try to fix it in a fast loop. First, take a moment to fully understand the error. "
6565
"Investigate the specific context (e.g., read the file, check the directory) to figure "
6666
"out why it failed before trying a new command.\n\n"
67-
"## Scratchpad\n"
68-
"You have a persistent scratchpad file (.agent/scratchpad.md) available "
69-
"in the working directory. Use it for complex or multi-step tasks:\n"
70-
"1. **Planning**: Before starting a large task, use write_scratchpad to "
71-
"outline your plan with sections like '## Plan', '## Implementation Steps', "
72-
"'## Open Questions'.\n"
73-
"2. **Progress tracking**: As you complete steps, use update_scratchpad to "
74-
"log your progress under a '## Progress' section.\n"
75-
"3. **Context persistence**: If a task spans many turns, read_scratchpad "
76-
"at the start of each turn to recall your plan.\n"
77-
"4. **Cleanup**: Use clear_scratchpad when a task is fully complete.\n"
78-
"The scratchpad is stored in a hidden .agent/ directory — it is for your "
79-
"internal use only and is not shown to the user.\n\n"
67+
"## MANDATORY PLANNING WORKFLOW\n"
68+
"To prevent hallucination and infinite loops, you MUST follow this strict workflow "
69+
"for EVERY task (unless it is a trivial single-step question):\n"
70+
"1. **Plan First**: Before executing ANY file writes or system commands, you MUST "
71+
"use the write_todo tool to create a step-by-step task list and implementation plan.\n"
72+
"2. **Implement**: Execute your tools to fulfill the plan. After each major step, "
73+
"use update_todo to check off the step (e.g., mark as done) or log progress.\n"
74+
"3. **Notes (Optional)**: If you need to write down discoveries, architectural ideas, "
75+
"or free-form observations during the prompt, you may use write_scratchpad and "
76+
"update_scratchpad to maintain a separate context file for notes.\n"
77+
"4. **Complete**: When the task is fully tested and complete, use clear_todo. Then call finish_task to return a final message to the user and stop the agent loop.\n"
78+
"You are strictly forbidden from writing code or running modifying commands before "
79+
"you have written a plan to the todo list. "
80+
"The todo list is at .agent/todo.md and the scratchpad is at .agent/scratchpad.md.\n\n"
8081
"## Sub-Agents\n"
8182
"You can spawn lightweight sub-agents using dispatch_subagent to perform "
8283
"independent, parallelizable work. Sub-agents run in separate threads "
@@ -169,17 +170,8 @@ def chat(self, user_input: str) -> tuple[str, TokenUsage]:
169170
response = self.chat_session.send_message(payload)
170171
turn_usage = turn_usage + self._accumulate(response)
171172

172-
max_tool_iterations = 10
173-
iterations = 0
174-
175173
# The loop will continue as long as Gemini decides to call tools
176174
while True:
177-
iterations += 1
178-
if iterations > max_tool_iterations:
179-
error_msg = f"Error: Maximum tool call limit ({max_tool_iterations}) reached to prevent infinite loops."
180-
self.transcript.log("assistant", error_msg)
181-
return error_msg, turn_usage
182-
183175
try:
184176
# 1. Check if the model returned a function_call
185177
tool_calls = response.function_calls if response.function_calls else []
@@ -204,6 +196,10 @@ def chat(self, user_input: str) -> tuple[str, TokenUsage]:
204196
"write_scratchpad",
205197
"update_scratchpad",
206198
"clear_scratchpad",
199+
"read_todo",
200+
"write_todo",
201+
"update_todo",
202+
"clear_todo",
207203
}
208204
if function_name not in _HIDDEN_TOOLS:
209205
# Sub-agent dispatches get a distinct green style
@@ -251,6 +247,10 @@ def chat(self, user_input: str) -> tuple[str, TokenUsage]:
251247
meta={"tool": function_name},
252248
)
253249

250+
if function_name == "finish_task":
251+
# End the loop immediately if the task is finished
252+
return str(tool_result), turn_usage
253+
254254
# Format the result back into Gemini's expected Response format
255255
tool_responses.append(
256256
types.Part.from_function_response(

lambda_agent/todo.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
"""
2+
Todo Module
3+
===========
4+
Provides tools for the agent to maintain a persistent, human-readable todo
5+
file (.agent/todo.md) in the user's working directory.
6+
7+
The todo list lets the agent plan complex tasks and track progress, while
8+
the scratchpad is used for free-form notes and discoveries.
9+
"""
10+
11+
import os
12+
13+
AGENT_DIR = ".agent"
14+
TODO_FILE = os.path.join(AGENT_DIR, "todo.md")
15+
16+
_HEADER_TEMPLATE = """\
17+
<!-- This file is managed by the Lambda coding agent. -->
18+
<!-- Feel free to read it, but edits may be overwritten by the agent. -->
19+
20+
# Lambda Task List
21+
22+
## To Do
23+
"""
24+
25+
26+
def _ensure_todo() -> str:
27+
"""Return the absolute path to the todo list, creating it if it doesn't exist."""
28+
agent_dir = os.path.abspath(AGENT_DIR)
29+
os.makedirs(agent_dir, exist_ok=True)
30+
path = os.path.abspath(TODO_FILE)
31+
if not os.path.exists(path):
32+
with open(path, "w", encoding="utf-8") as f:
33+
f.write(_HEADER_TEMPLATE)
34+
return path
35+
36+
37+
def read_todo() -> str:
38+
"""Reads the full contents of the Lambda todo file (.agent/todo.md).
39+
40+
Use this to recall your current task list and implementation plan.
41+
"""
42+
path = _ensure_todo()
43+
try:
44+
with open(path, "r", encoding="utf-8") as f:
45+
return f.read()
46+
except Exception as e:
47+
return f"Error reading todo list: {e}"
48+
49+
50+
def write_todo(content: str) -> str:
51+
"""Overwrites the entire Lambda todo file with the provided content.
52+
53+
Use this when you need to replace the todo list with a fresh task list.
54+
For incremental updates, prefer update_todo.
55+
56+
Args:
57+
content: The full markdown content to write to the todo list.
58+
"""
59+
path = _ensure_todo()
60+
try:
61+
with open(path, "w", encoding="utf-8") as f:
62+
f.write(_HEADER_TEMPLATE + content)
63+
return f"Todo list written successfully → {path}"
64+
except Exception as e:
65+
return f"Error writing todo list: {e}"
66+
67+
68+
def update_todo(note: str, section: str = "To Do") -> str:
69+
"""Appends an item to a specific section in the todo list.
70+
71+
This is ideal for checking off steps or adding new sub-tasks.
72+
73+
Args:
74+
note: The text to append (supports markdown, e.g. '- [ ] Task').
75+
section: The section heading to append under (e.g. 'To Do', 'In Progress', 'Done').
76+
"""
77+
path = _ensure_todo()
78+
try:
79+
with open(path, "r", encoding="utf-8") as f:
80+
existing = f.read()
81+
82+
entry = f"\n{note}"
83+
84+
section_heading = f"## {section}"
85+
if section_heading in existing:
86+
# Append under the existing section
87+
parts = existing.split(section_heading, 1)
88+
# Find the next section heading (##) or end of file
89+
rest = parts[1]
90+
next_section = rest.find("\n## ")
91+
if next_section == -1:
92+
# No next section — just append at the end
93+
updated = existing + entry
94+
else:
95+
# Insert before the next section
96+
insert_pos = len(parts[0]) + len(section_heading) + next_section
97+
updated = existing[:insert_pos] + entry + "\n" + existing[insert_pos:]
98+
else:
99+
# Create the section at the end
100+
updated = existing.rstrip() + f"\n\n{section_heading}\n{entry}\n"
101+
102+
with open(path, "w", encoding="utf-8") as f:
103+
f.write(updated)
104+
105+
return f"Todo list updated (section: {section}) → {path}"
106+
except Exception as e:
107+
return f"Error updating todo list: {e}"
108+
109+
110+
def clear_todo() -> str:
111+
"""Clears the todo list, resetting it to a blank state.
112+
113+
Use this when a major task is fully complete and the task list is no longer needed.
114+
"""
115+
path = _ensure_todo()
116+
try:
117+
with open(path, "w", encoding="utf-8") as f:
118+
f.write(_HEADER_TEMPLATE)
119+
return f"Todo list cleared → {path}"
120+
except Exception as e:
121+
return f"Error clearing todo list: {e}"
122+
123+
124+
# Tool registrations for the agent
125+
TODO_EXECUTORS = {
126+
"read_todo": read_todo,
127+
"write_todo": write_todo,
128+
"update_todo": update_todo,
129+
"clear_todo": clear_todo,
130+
}
131+
132+
TODO_FUNCTIONS = [
133+
read_todo,
134+
write_todo,
135+
update_todo,
136+
clear_todo,
137+
]

lambda_agent/tools.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from rich.console import Console
99

1010
from .scratchpad import SCRATCHPAD_EXECUTORS, SCRATCHPAD_FUNCTIONS
11+
from .todo import TODO_EXECUTORS, TODO_FUNCTIONS
1112
from .subagent import SUBAGENT_EXECUTORS, SUBAGENT_FUNCTIONS
1213

1314
# Use the same console as the rest of the app if available; else create one
@@ -99,6 +100,7 @@ def get_workspace_summary() -> str:
99100
".cursorrules",
100101
".agentrules",
101102
".agent/scratchpad.md",
103+
".agent/todo.md",
102104
"pyproject.toml",
103105
"package.json",
104106
]
@@ -195,14 +197,28 @@ def ask_user(question: str) -> str:
195197
return f"Error asking user: {str(e)}"
196198

197199

200+
def finish_task(message: str) -> str:
201+
"""Explicitly mark a task as fully complete and return the final message to the user.
202+
203+
Call this tool when you have completed all steps in your todo list and are ready to stop.
204+
This will immediately exit your execution loop.
205+
206+
Args:
207+
message: The final message summarizing what was accomplished to present to the user.
208+
"""
209+
return message
210+
211+
198212
# A dictionary mapping tool names to Python functions for dynamic execution
199213
TOOL_EXECUTORS = {
200214
"read_file": read_file,
201215
"write_file": write_file,
202216
"run_command": run_command,
203217
"search_repo": search_repo,
204218
"ask_user": ask_user,
219+
"finish_task": finish_task,
205220
**SCRATCHPAD_EXECUTORS,
221+
**TODO_EXECUTORS,
206222
**SUBAGENT_EXECUTORS,
207223
}
208224

@@ -213,6 +229,8 @@ def ask_user(question: str) -> str:
213229
run_command,
214230
search_repo,
215231
ask_user,
232+
finish_task,
216233
*SCRATCHPAD_FUNCTIONS,
234+
*TODO_FUNCTIONS,
217235
*SUBAGENT_FUNCTIONS,
218236
]

0 commit comments

Comments
 (0)