mirror of
https://github.com/NCBM/plyngent.git
synced 2026-07-23 05:55:16 +08:00
core/agent: reuse todo ids after clear/pop; todo_push titles as array
This commit is contained in:
@@ -184,6 +184,20 @@ class TodoStack:
|
|||||||
f"Current stack:\n{self.render()}"
|
f"Current stack:\n{self.render()}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _existing_numeric_ids(self) -> list[int]:
|
||||||
|
"""Numeric suffixes of ids that look like ``tN`` (for counter reuse)."""
|
||||||
|
return [
|
||||||
|
int(item.id[1:])
|
||||||
|
for group in self._data.groups
|
||||||
|
for item in group.items
|
||||||
|
if item.id.startswith("t") and item.id[1:].isdigit()
|
||||||
|
]
|
||||||
|
|
||||||
|
def _sync_next_id(self) -> None:
|
||||||
|
"""Set ``next_id`` to one past the highest live id (or 1 if empty)."""
|
||||||
|
nums = self._existing_numeric_ids()
|
||||||
|
self._data.next_id = max(nums) + 1 if nums else 1
|
||||||
|
|
||||||
def _alloc(self, title: str, *, notes: str, status: TodoStatus) -> TodoItem:
|
def _alloc(self, title: str, *, notes: str, status: TodoStatus) -> TodoItem:
|
||||||
item_id = f"t{self._data.next_id}"
|
item_id = f"t{self._data.next_id}"
|
||||||
self._data.next_id += 1
|
self._data.next_id += 1
|
||||||
@@ -202,6 +216,8 @@ class TodoStack:
|
|||||||
msg = "at least one non-empty title is required"
|
msg = "at least one non-empty title is required"
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
note = notes.strip()
|
note = notes.strip()
|
||||||
|
# Rebase counter on live ids so clear/pop do not leave a high watermark.
|
||||||
|
self._sync_next_id()
|
||||||
items = [self._alloc(title, notes=note, status=status) for title in cleaned]
|
items = [self._alloc(title, notes=note, status=status) for title in cleaned]
|
||||||
group = TodoGroup(items=items)
|
group = TodoGroup(items=items)
|
||||||
self._data.groups.append(group)
|
self._data.groups.append(group)
|
||||||
@@ -218,12 +234,14 @@ class TodoStack:
|
|||||||
if not self._data.groups:
|
if not self._data.groups:
|
||||||
return None
|
return None
|
||||||
group = self._data.groups.pop()
|
group = self._data.groups.pop()
|
||||||
|
self._sync_next_id()
|
||||||
self.mark_touched()
|
self.mark_touched()
|
||||||
return group
|
return group
|
||||||
|
|
||||||
def clear(self) -> int:
|
def clear(self) -> int:
|
||||||
n = sum(len(g.items) for g in self._data.groups)
|
n = sum(len(g.items) for g in self._data.groups)
|
||||||
self._data.groups.clear()
|
self._data.groups.clear()
|
||||||
|
self._sync_next_id()
|
||||||
if n:
|
if n:
|
||||||
self.mark_touched()
|
self.mark_touched()
|
||||||
return n
|
return n
|
||||||
|
|||||||
@@ -970,11 +970,11 @@ def todos_cmd( # noqa: C901, PLR0911, PLR0912, PLR0915
|
|||||||
if act == "push":
|
if act == "push":
|
||||||
raw = " ".join(rest).strip()
|
raw = " ".join(rest).strip()
|
||||||
if not raw:
|
if not raw:
|
||||||
click.echo("error: usage: /todos push <title> | /todos push T1; T2")
|
click.echo('error: usage: /todos push <title> | /todos push ["T1","T2"] | T1; T2')
|
||||||
return
|
return
|
||||||
titles = parse_push_titles(raw)
|
titles = parse_push_titles(raw)
|
||||||
if not titles:
|
if not titles:
|
||||||
click.echo("error: no titles to push")
|
click.echo("error: no titles to push (use a JSON array or ; / newlines)")
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
group = stack.push_group(titles)
|
group = stack.push_group(titles)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from __future__ import annotations
|
|||||||
from typing import TYPE_CHECKING, cast
|
from typing import TYPE_CHECKING, cast
|
||||||
|
|
||||||
from plyngent.agent import tool
|
from plyngent.agent import tool
|
||||||
from plyngent.agent.todo_stack import parse_push_titles
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
@@ -53,17 +52,17 @@ def todo_list() -> str:
|
|||||||
|
|
||||||
|
|
||||||
@tool(name="todo_push")
|
@tool(name="todo_push")
|
||||||
def todo_push(titles: str, notes: str = "") -> str:
|
def todo_push(titles: list[str], notes: str = "") -> str:
|
||||||
"""Push **one task group** (siblings) onto the stack — not one level per title.
|
"""Push **one task group** (siblings) onto the stack — not one level per title.
|
||||||
|
|
||||||
``titles``: one title, newlines, ``;``, or JSON array. All listed titles
|
``titles``: JSON array of strings (tool schema type ``array``). All entries
|
||||||
become members of a single new TOP group. Example: ``T1\\nT2`` pushes one
|
become members of a single new TOP group. Example: ``[\"T1\", \"T2\"]`` pushes
|
||||||
group {T1, T2}; a later ``T1.1\\nT1.2`` pushes a child group above it.
|
one group {T1, T2}; a later ``[\"T1.1\", \"T1.2\"]`` pushes a child group above it.
|
||||||
"""
|
"""
|
||||||
stack = _require_stack()
|
stack = _require_stack()
|
||||||
parsed = parse_push_titles(titles)
|
parsed = [t.strip() for t in titles if t and t.strip()]
|
||||||
if not parsed:
|
if not parsed:
|
||||||
return "error: titles must contain at least one non-empty title"
|
return "error: titles must be a non-empty array of strings"
|
||||||
try:
|
try:
|
||||||
group = stack.push_group(parsed, notes=notes)
|
group = stack.push_group(parsed, notes=notes)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
|
|||||||
@@ -30,6 +30,26 @@ def test_parse_push_titles() -> None:
|
|||||||
assert parse_push_titles('["A", "B"]') == ["A", "B"]
|
assert parse_push_titles('["A", "B"]') == ["A", "B"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_ids_reuse_after_clear() -> None:
|
||||||
|
stack = TodoStack()
|
||||||
|
g = stack.push_group(["A", "B"])
|
||||||
|
assert [i.id for i in g.items] == ["t1", "t2"]
|
||||||
|
assert stack.clear() == 2
|
||||||
|
g2 = stack.push_group(["C"])
|
||||||
|
assert [i.id for i in g2.items] == ["t1"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_ids_reuse_after_pop() -> None:
|
||||||
|
stack = TodoStack()
|
||||||
|
_ = stack.push_group(["A", "B"]) # t1, t2
|
||||||
|
g2 = stack.push_group(["C"]) # t3
|
||||||
|
assert g2.items[0].id == "t3"
|
||||||
|
_ = stack.pop() # drop t3
|
||||||
|
g3 = stack.push_group(["D"])
|
||||||
|
# Highest live id is t2 → next is t3 again
|
||||||
|
assert g3.items[0].id == "t3"
|
||||||
|
|
||||||
|
|
||||||
def test_push_is_group_not_per_task_stack() -> None:
|
def test_push_is_group_not_per_task_stack() -> None:
|
||||||
"""Multi-title push is one group; pop removes the whole group."""
|
"""Multi-title push is one group; pop removes the whole group."""
|
||||||
stack = TodoStack()
|
stack = TodoStack()
|
||||||
|
|||||||
Reference in New Issue
Block a user