Add todo tool for agent task planning and management

Single `todo` tool that reads (no params) or writes (provide todos array
with merge flag). In-memory TodoStore on AIAgent, no system prompt
mutation, behavioral guidance in tool description only. State re-injected
after context compression events. Gateway sessions hydrate from
conversation history. Added to all platform toolsets.

Also wired into RL agent_loop.py with per-run TodoStore and fixed
browser_snapshot user_task passthrough from first user message.
This commit is contained in:
teknium1 2026-02-17 17:02:33 -08:00
parent d0f82e6dcc
commit e184f5ab3a
7 changed files with 1334 additions and 38 deletions

View file

@ -1237,6 +1237,10 @@ class AIAgent:
# Track conversation messages for session logging
self._session_messages: List[Dict[str, Any]] = []
# In-memory todo list for task planning (one per agent/session)
from tools.todo_tool import TodoStore
self._todo_store = TodoStore()
# Initialize context compressor for automatic context management
# Compresses conversation when approaching model's context limit
# Configuration via environment variables (can be set in .env or cli-config.yaml)
@ -1961,6 +1965,37 @@ class AIAgent:
"""Clear any pending interrupt request."""
self._interrupt_requested = False
self._interrupt_message = None
def _hydrate_todo_store(self, history: List[Dict[str, Any]]) -> None:
"""
Recover todo state from conversation history.
The gateway creates a fresh AIAgent per message, so the in-memory
TodoStore is empty. We scan the history for the most recent todo
tool response and replay it to reconstruct the state.
"""
# Walk history backwards to find the most recent todo tool response
last_todo_response = None
for msg in reversed(history):
if msg.get("role") != "tool":
continue
content = msg.get("content", "")
# Quick check: todo responses contain "todos" key
if '"todos"' not in content:
continue
try:
data = json.loads(content)
if "todos" in data and isinstance(data["todos"], list):
last_todo_response = data["todos"]
break
except (json.JSONDecodeError, TypeError):
continue
if last_todo_response:
# Replay the items into the store (replace mode)
self._todo_store.write(last_todo_response, merge=False)
if not self.quiet_mode:
print(f"{self.log_prefix}📋 Restored {len(last_todo_response)} todo item(s) from history")
_set_terminal_interrupt(False)
@property
@ -1999,6 +2034,12 @@ class AIAgent:
# Initialize conversation
messages = conversation_history or []
# Hydrate todo store from conversation history (gateway creates a fresh
# AIAgent per message, so the in-memory store is empty -- we need to
# recover the todo state from the most recent todo tool response in history)
if conversation_history and not self._todo_store.has_items():
self._hydrate_todo_store(conversation_history)
# Inject prefill messages at the start of conversation (before user's actual prompt)
# This is used for few-shot priming, e.g., a greeting exchange to set response style
if self.prefill_messages and not conversation_history:
@ -2419,7 +2460,10 @@ class AIAgent:
messages = self.context_compressor.compress(messages, current_tokens=approx_tokens)
if len(messages) < original_len:
# Compression was possible, retry
# Compression was possible -- re-inject todo state
todo_snapshot = self._todo_store.format_for_injection()
if todo_snapshot:
messages.append({"role": "user", "content": todo_snapshot})
print(f"{self.log_prefix} 🗜️ Compressed {original_len}{len(messages)} messages, retrying...")
continue # Retry with compressed messages
else:
@ -2670,8 +2714,17 @@ class AIAgent:
tool_start_time = time.time()
# Execute the tool - with animated spinner in quiet mode
if self.quiet_mode:
# Todo tool -- handle directly (needs agent's TodoStore instance)
if function_name == "todo":
from tools.todo_tool import todo_tool as _todo_tool
function_result = _todo_tool(
todos=function_args.get("todos"),
merge=function_args.get("merge", False),
store=self._todo_store,
)
tool_duration = time.time() - tool_start_time
# Execute other tools - with animated spinner in quiet mode
elif self.quiet_mode:
# Tool-specific spinner animations
tool_spinners = {
'web_search': ('arrows', ['🔍', '🌐', '📡', '🔎']),
@ -2748,6 +2801,10 @@ class AIAgent:
messages,
current_tokens=self.context_compressor.last_prompt_tokens
)
# Re-inject todo state after compression (cache already invalidated)
todo_snapshot = self._todo_store.format_for_injection()
if todo_snapshot:
messages.append({"role": "user", "content": todo_snapshot})
# Save session log incrementally (so progress is visible even if interrupted)
self._session_messages = messages