core/agent: strengthen todo prompts when stack not empty

Inject turn-start [TODO REMINDER] for non-empty stacks; rewrite end-of-turn
[TODO OPEN] copy so open items read as unfinished work. needs_review now
always true while open items remain (even if todo_* ran this turn); terminal-only
stacks still need review if untouched. Update tool docs and tests.
This commit is contained in:
2026-07-19 20:34:01 +08:00
parent a54fa534f3
commit e1e4df0f3f
6 changed files with 138 additions and 12 deletions
+56 -1
View File
@@ -103,13 +103,40 @@ def test_single_title_still_one_group() -> None:
def test_todo_stack_needs_review() -> None:
stack = TodoStack()
assert not stack.needs_review()
_ = stack.push("work")
item = stack.push("work")
stack.begin_turn()
# Open items always need review, even if todo_* was used this turn.
assert stack.needs_review()
stack.mark_touched()
assert stack.needs_review()
stack.update(item.id, status="done")
stack.begin_turn()
# Terminal-only stack: review only when untouched this turn.
assert stack.needs_review()
stack.mark_touched()
assert not stack.needs_review()
def test_todo_prompts_signal_undone_work() -> None:
stack = TodoStack()
item = stack.push("open work")
reminder = stack.turn_reminder_prompt()
assert "[TODO REMINDER]" in reminder
assert "Stack not empty" in reminder
assert "unfinished" in reminder.lower()
assert "open work" in reminder
review = stack.review_prompt()
assert "[TODO OPEN]" in review
assert "Stack not empty" in review
assert "undone" in review.lower() or "incomplete" in review.lower()
assert "open work" in review
stack.update(item.id, status="done")
terminal_review = stack.review_prompt()
assert "done/cancelled" in terminal_review or "Bookkeeping" in terminal_review
def test_legacy_flat_and_frames_migrate() -> None:
flat = TodoStack.from_raw(
{
@@ -232,7 +259,35 @@ async def test_loop_injects_todo_review_when_untouched() -> None:
async for _event in agent.run("do stuff"):
pass
assert client.calls >= 2
assert any(isinstance(m, DeveloperChatMessage) and "[TODO REMINDER]" in m.content for m in agent.messages)
assert any(isinstance(m, DeveloperChatMessage) and "[TODO OPEN]" in m.content for m in agent.messages)
assert not any(isinstance(m, UserChatMessage) and "[TODO OPEN]" in m.content for m in agent.messages)
finally:
set_todo_stack(None)
@pytest.mark.asyncio
async def test_loop_injects_todo_review_when_open_after_touch() -> None:
"""Open items still trigger end-of-turn review even if todo_* ran this turn."""
stack = TodoStack()
_ = stack.push("still open")
stack.begin_turn()
stack.mark_touched() # simulates todo_list earlier in the turn
assert stack.needs_review()
client = ScriptedClient()
agent = ChatAgent(
client, # type: ignore[arg-type]
model="m",
tools=ToolRegistry(list(TODO_TOOLS)),
stream=False,
todo_stack=stack,
)
set_todo_stack(stack)
try:
async for _event in agent.run("do stuff"):
pass
assert client.calls >= 2
assert any(isinstance(m, DeveloperChatMessage) and "[TODO OPEN]" in m.content for m in agent.messages)
finally:
set_todo_stack(None)