diff --git a/src/plyngent/agent/todo_stack.py b/src/plyngent/agent/todo_stack.py index 510c470..0428421 100644 --- a/src/plyngent/agent/todo_stack.py +++ b/src/plyngent/agent/todo_stack.py @@ -174,15 +174,19 @@ class TodoStack: return "\n".join(lines) def review_prompt(self) -> str: - return ( - "Todo stack review (internal control — not a human message).\n" - "This is a LIFO stack of **task groups** (not a queue of single tasks).\n" - "push([T1,T2]) creates one group of siblings; push([T1.1,T1.2]) pushes a " - "child group; pop removes the whole top group. Update items by id; " - "pop when a breakdown level is finished. Open work remains and you " - "did not call any todo_* tools this turn.\n\n" - f"Current stack:\n{self.render()}" - ) + """Compact, model-facing nag: open work still exists this turn.""" + open_items = self.open_items() + n_open = len(open_items) + n_groups = self.depth + lines = [ + f"[TODO OPEN] {n_open} open item(s) across {n_groups} group(s) " + f"(not empty). You did not call todo_* this turn.", + "Tools: todo_list | todo_push(titles=[...]) | todo_update | todo_pop | todo_clear", + "Rules: TOP group = current level; push=one sibling group; pop=remove whole TOP group.", + "Act on open work before ending (finish/pop TOP, or push child tasks). Stack:", + self.render(), + ] + return "\n".join(lines) def _existing_numeric_ids(self) -> list[int]: """Numeric suffixes of ids that look like ``tN`` (for counter reuse).""" diff --git a/tests/test_agent/test_todo_stack.py b/tests/test_agent/test_todo_stack.py index b5dcdd1..cbea337 100644 --- a/tests/test_agent/test_todo_stack.py +++ b/tests/test_agent/test_todo_stack.py @@ -152,11 +152,11 @@ async def test_todo_tools_and_persist(tmp_path: object) -> None: stack = TodoStack() set_todo_stack(stack, on_change=None) registry = ToolRegistry(list(TODO_TOOLS)) - out = await registry.execute("todo_push", '{"titles": "T1\\nT2"}') + out = await registry.execute("todo_push", '{"titles": ["T1", "T2"]}') assert "group" in out.lower() or "pushed" in out assert stack.depth == 1 assert [i.title for i in stack.groups[0].items] == ["T1", "T2"] - _ = await registry.execute("todo_push", '{"titles": "T1.1; T1.2"}') + out = await registry.execute("todo_push", '{"titles": ["T1", "T2"]}') assert stack.depth == 2 out3 = await registry.execute("todo_pop", "{}") assert "popped" in out3 @@ -193,7 +193,7 @@ class ScriptedClient: self.calls += 1 text = "ok" if self.calls > 1 else "done without todos" for msg in param.messages: - if isinstance(msg, DeveloperChatMessage) and "Todo stack review" in msg.content: + if isinstance(msg, DeveloperChatMessage) and "[TODO OPEN]" in msg.content: text = "reviewed stack" break return ChatCompletionResponse( @@ -232,7 +232,7 @@ 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 stack review" in m.content for m in agent.messages) - assert not any(isinstance(m, UserChatMessage) and "Todo stack review" 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)