railtracks.prebuilt.tools

1from .memory import KeyValueMemoryToolSet
2from .todo import ToDoToolSet
3
4__all__ = [
5    "KeyValueMemoryToolSet",
6    "ToDoToolSet",
7]
class KeyValueMemoryToolSet(railtracks.prebuilt.tools._base.ToolSet):
 32class KeyValueMemoryToolSet(ToolSet):
 33    """Prebuilt key-value memory tools for an agent.
 34
 35    Gives an agent a persistent, exact-match scratch pad: save a fact under a
 36    key, read it back later, forget it, list everything, or search. State lives
 37    in the injected :class:`~railtracks.retrieval.stores.key_value.KeyValueStore`
 38    (defaults to an in-process :class:`InMemoryKeyValueStore`). Pass a store
 39    constructed with a ``snapshot_path`` for persistence across runs::
 40
 41        store = InMemoryKeyValueStore(snapshot_path="memory.json")
 42        toolset = KeyValueMemoryToolSet(store=store)
 43
 44    All memory in a toolset shares one namespace. To keep separate memories for
 45    different agents, give each its own ``KeyValueMemoryToolSet`` (and its own
 46    store).
 47
 48    Args:
 49        store: Backing key-value store. Defaults to a fresh, ephemeral
 50            ``InMemoryKeyValueStore``.
 51        search: Ranking algorithm used by ``search_memories``. Defaults to
 52            ``LexicalSearch()``. Pass a ``LexicalSearch(LexicalSearchConfig(...))``
 53            to tune the ranking weights, pass a
 54            ``SemanticSearch(embedding=...)`` for dense-vector ranking, or any
 55            other :class:`~railtracks.prebuilt.tools.memory.search.SearchAlgorithm`
 56            implementation to swap the algorithm entirely.
 57        on_change: Optional callback fired after every mutation, letting an
 58            outer system react (push to a UI, mirror to a database, log).
 59            Called as ``on_change(key, value)`` where ``value`` is the new
 60            value on a save and ``None`` on a forget. Exceptions raised by the
 61            callback are logged and swallowed so they never break a tool call.
 62    """
 63
 64    def __init__(
 65        self,
 66        store: KeyValueStore | None = None,
 67        search: SearchAlgorithm | None = None,
 68        on_change: Callable[[str, str | None], None] | None = None,
 69    ) -> None:
 70        self.store: KeyValueStore = store if store is not None else _default_store()
 71        self.search: SearchAlgorithm = (
 72            search if search is not None else _default_search()
 73        )
 74        self.on_change = on_change
 75
 76    def _notify(self, key: str, value: str | None) -> None:
 77        if self.on_change is None:
 78            return
 79        try:
 80            self.on_change(key, value)
 81        except Exception as e:
 82            logger.error(f"Error in on_change callback for key {key!r}: {e}")
 83
 84    async def remember(self, key: str, value: str) -> str:
 85        """Save a fact to memory under a key, for recall later.
 86
 87        If the key already holds a value it is overwritten, so use a stable,
 88        descriptive key (e.g. "user_timezone", "project_deadline") and re-call
 89        remember() to update a fact.
 90
 91        Args:
 92            key: Short, stable identifier for the fact (used to recall it).
 93            value: The fact to store, as a self-contained string.
 94
 95        Returns:
 96            A confirmation that the fact was stored.
 97        """
 98        await self.store.set(key, value)
 99        self._notify(key, value)
100        return f"Remembered '{key}': {value}"
101
102    async def recall(self, key: str) -> str:
103        """Recall the value previously stored under a key.
104
105        Args:
106            key: The exact key the fact was stored under.
107
108        Returns:
109            The stored value, or a message saying nothing is stored under that
110            key. Use list_memories() if you are unsure of the exact key.
111        """
112        value = await self.store.get(key)
113        if value is None:
114            return f"No memory found under key '{key}'."
115        return value
116
117    async def forget(self, key: str) -> str:
118        """Delete the fact stored under a key.
119
120        Args:
121            key: The key to remove. Forgetting a key that does not exist is a
122                no-op and is reported as such.
123
124        Returns:
125            A confirmation describing what happened.
126        """
127        existed = await self.store.get(key) is not None
128        await self.store.delete(key)
129        self._notify(key, None)
130        if existed:
131            return f"Forgot '{key}'."
132        return f"Nothing was stored under '{key}'; nothing to forget."
133
134    async def list_memories(self) -> str:
135        """List every key and value currently held in memory.
136
137        Returns:
138            A newline-separated "key: value" listing, or a message saying
139            memory is empty.
140        """
141        items = await self.store.items()
142        if not items:
143            return "No memories stored."
144        return "\n".join(f"- {key}: {value}" for key, value in items.items())
145
146    async def list_keys(self) -> str:
147        """List just the keys currently held in memory, without their values.
148
149        Prefer this over list_memories() to see what is stored without pulling
150        every value into context; then recall(key) only the ones you need.
151
152        Returns:
153            A newline-separated list of keys, or a message saying memory is
154            empty.
155        """
156        keys = await self.store.keys()
157        if not keys:
158            return "No memories stored."
159        return "\n".join(f"- {key}" for key in keys)
160
161    async def search_memories(self, query: str) -> str:
162        """Search stored memories for relevance to a query across keys and values.
163
164        Use this when you remember roughly what a fact was about but not the
165        exact key. Ranking favors exact and substring key matches, but also
166        catches value hits, multi-word queries, and near-miss typos — so it
167        finds more than a plain substring search would.
168
169        Args:
170            query: Free-text search string.
171
172        Returns:
173            Matching "key: value" entries ranked by relevance, or a message
174            saying nothing matched.
175        """
176        items = await self.store.items()
177        hits = await self.search.search(items, query)
178        if not hits:
179            return f"No memories matched '{query}'."
180        return "\n".join(f"- {key}: {value}" for key, value, _score in hits)
181
182    @classmethod
183    def prompt(cls) -> str:
184        return (
185            "Use the memory tools to remember facts across the conversation. "
186            "Call remember(key, value) to save a fact under a short, stable, descriptive key; "
187            "re-calling remember() with the same key overwrites the old value. "
188            "Call recall(key) to read a fact back, and forget(key) to delete one. "
189            "If you are unsure of the exact key, call search_memories() to find related entries, "
190            "list_keys() to see what is stored without pulling in every value, "
191            "or list_memories() to see everything stored. "
192            "Save anything the user tells you that may be useful later (preferences, names, goals, "
193            "constraints), and recall before asking the user to repeat themselves."
194        )
195
196    def tool_set(self) -> list[RTFunction]:
197        functions = [
198            self.remember,
199            self.recall,
200            self.forget,
201            self.list_keys,
202            self.list_memories,
203            self.search_memories,
204        ]
205        return [rt.function_node(func) for func in functions]

Prebuilt key-value memory tools for an agent.

Gives an agent a persistent, exact-match scratch pad: save a fact under a key, read it back later, forget it, list everything, or search. State lives in the injected ~railtracks.retrieval.stores.key_value.KeyValueStore (defaults to an in-process InMemoryKeyValueStore). Pass a store constructed with a snapshot_path for persistence across runs::

store = InMemoryKeyValueStore(snapshot_path="memory.json")
toolset = KeyValueMemoryToolSet(store=store)

All memory in a toolset shares one namespace. To keep separate memories for different agents, give each its own KeyValueMemoryToolSet (and its own store).

Arguments:
  • store: Backing key-value store. Defaults to a fresh, ephemeral InMemoryKeyValueStore.
  • search: Ranking algorithm used by search_memories. Defaults to LexicalSearch(). Pass a LexicalSearch(LexicalSearchConfig(...)) to tune the ranking weights, pass a SemanticSearch(embedding=...) for dense-vector ranking, or any other ~railtracks.prebuilt.tools.memory.search.SearchAlgorithm implementation to swap the algorithm entirely.
  • on_change: Optional callback fired after every mutation, letting an outer system react (push to a UI, mirror to a database, log). Called as on_change(key, value) where value is the new value on a save and None on a forget. Exceptions raised by the callback are logged and swallowed so they never break a tool call.
KeyValueMemoryToolSet( store: railtracks.retrieval.stores.key_value.protocol.KeyValueStore | None = None, search: railtracks.prebuilt.tools.memory.search.protocol.SearchAlgorithm | None = None, on_change: Optional[Callable[[str, str | None], NoneType]] = None)
64    def __init__(
65        self,
66        store: KeyValueStore | None = None,
67        search: SearchAlgorithm | None = None,
68        on_change: Callable[[str, str | None], None] | None = None,
69    ) -> None:
70        self.store: KeyValueStore = store if store is not None else _default_store()
71        self.search: SearchAlgorithm = (
72            search if search is not None else _default_search()
73        )
74        self.on_change = on_change
store: railtracks.retrieval.stores.key_value.protocol.KeyValueStore
search: railtracks.prebuilt.tools.memory.search.protocol.SearchAlgorithm
on_change
async def remember(self, key: str, value: str) -> str:
 84    async def remember(self, key: str, value: str) -> str:
 85        """Save a fact to memory under a key, for recall later.
 86
 87        If the key already holds a value it is overwritten, so use a stable,
 88        descriptive key (e.g. "user_timezone", "project_deadline") and re-call
 89        remember() to update a fact.
 90
 91        Args:
 92            key: Short, stable identifier for the fact (used to recall it).
 93            value: The fact to store, as a self-contained string.
 94
 95        Returns:
 96            A confirmation that the fact was stored.
 97        """
 98        await self.store.set(key, value)
 99        self._notify(key, value)
100        return f"Remembered '{key}': {value}"

Save a fact to memory under a key, for recall later.

If the key already holds a value it is overwritten, so use a stable, descriptive key (e.g. "user_timezone", "project_deadline") and re-call remember() to update a fact.

Arguments:
  • key: Short, stable identifier for the fact (used to recall it).
  • value: The fact to store, as a self-contained string.
Returns:

A confirmation that the fact was stored.

async def recall(self, key: str) -> str:
102    async def recall(self, key: str) -> str:
103        """Recall the value previously stored under a key.
104
105        Args:
106            key: The exact key the fact was stored under.
107
108        Returns:
109            The stored value, or a message saying nothing is stored under that
110            key. Use list_memories() if you are unsure of the exact key.
111        """
112        value = await self.store.get(key)
113        if value is None:
114            return f"No memory found under key '{key}'."
115        return value

Recall the value previously stored under a key.

Arguments:
  • key: The exact key the fact was stored under.
Returns:

The stored value, or a message saying nothing is stored under that key. Use list_memories() if you are unsure of the exact key.

async def forget(self, key: str) -> str:
117    async def forget(self, key: str) -> str:
118        """Delete the fact stored under a key.
119
120        Args:
121            key: The key to remove. Forgetting a key that does not exist is a
122                no-op and is reported as such.
123
124        Returns:
125            A confirmation describing what happened.
126        """
127        existed = await self.store.get(key) is not None
128        await self.store.delete(key)
129        self._notify(key, None)
130        if existed:
131            return f"Forgot '{key}'."
132        return f"Nothing was stored under '{key}'; nothing to forget."

Delete the fact stored under a key.

Arguments:
  • key: The key to remove. Forgetting a key that does not exist is a no-op and is reported as such.
Returns:

A confirmation describing what happened.

async def list_memories(self) -> str:
134    async def list_memories(self) -> str:
135        """List every key and value currently held in memory.
136
137        Returns:
138            A newline-separated "key: value" listing, or a message saying
139            memory is empty.
140        """
141        items = await self.store.items()
142        if not items:
143            return "No memories stored."
144        return "\n".join(f"- {key}: {value}" for key, value in items.items())

List every key and value currently held in memory.

Returns:

A newline-separated "key: value" listing, or a message saying memory is empty.

async def list_keys(self) -> str:
146    async def list_keys(self) -> str:
147        """List just the keys currently held in memory, without their values.
148
149        Prefer this over list_memories() to see what is stored without pulling
150        every value into context; then recall(key) only the ones you need.
151
152        Returns:
153            A newline-separated list of keys, or a message saying memory is
154            empty.
155        """
156        keys = await self.store.keys()
157        if not keys:
158            return "No memories stored."
159        return "\n".join(f"- {key}" for key in keys)

List just the keys currently held in memory, without their values.

Prefer this over list_memories() to see what is stored without pulling every value into context; then recall(key) only the ones you need.

Returns:

A newline-separated list of keys, or a message saying memory is empty.

async def search_memories(self, query: str) -> str:
161    async def search_memories(self, query: str) -> str:
162        """Search stored memories for relevance to a query across keys and values.
163
164        Use this when you remember roughly what a fact was about but not the
165        exact key. Ranking favors exact and substring key matches, but also
166        catches value hits, multi-word queries, and near-miss typos — so it
167        finds more than a plain substring search would.
168
169        Args:
170            query: Free-text search string.
171
172        Returns:
173            Matching "key: value" entries ranked by relevance, or a message
174            saying nothing matched.
175        """
176        items = await self.store.items()
177        hits = await self.search.search(items, query)
178        if not hits:
179            return f"No memories matched '{query}'."
180        return "\n".join(f"- {key}: {value}" for key, value, _score in hits)

Search stored memories for relevance to a query across keys and values.

Use this when you remember roughly what a fact was about but not the exact key. Ranking favors exact and substring key matches, but also catches value hits, multi-word queries, and near-miss typos — so it finds more than a plain substring search would.

Arguments:
  • query: Free-text search string.
Returns:

Matching "key: value" entries ranked by relevance, or a message saying nothing matched.

@classmethod
def prompt(cls) -> str:
182    @classmethod
183    def prompt(cls) -> str:
184        return (
185            "Use the memory tools to remember facts across the conversation. "
186            "Call remember(key, value) to save a fact under a short, stable, descriptive key; "
187            "re-calling remember() with the same key overwrites the old value. "
188            "Call recall(key) to read a fact back, and forget(key) to delete one. "
189            "If you are unsure of the exact key, call search_memories() to find related entries, "
190            "list_keys() to see what is stored without pulling in every value, "
191            "or list_memories() to see everything stored. "
192            "Save anything the user tells you that may be useful later (preferences, names, goals, "
193            "constraints), and recall before asking the user to repeat themselves."
194        )

Mutilple short sentances guiding the agent

def tool_set(self) -> list[railtracks.built_nodes.function.base.RTFunction]:
196    def tool_set(self) -> list[RTFunction]:
197        functions = [
198            self.remember,
199            self.recall,
200            self.forget,
201            self.list_keys,
202            self.list_memories,
203            self.search_memories,
204        ]
205        return [rt.function_node(func) for func in functions]
class ToDoToolSet(railtracks.prebuilt.tools._base.ToolSet):
 49class ToDoToolSet(ToolSet):
 50    def __init__(self, callback: Callable[[str, str, State], None] | None = None):
 51        """Create an empty toolset with an optional post-add callback.
 52
 53        Args:
 54            callback: Invoked after each successful add() with (short_description, description, state).
 55                      Exceptions are logged and swallowed; the todo is always committed regardless.
 56        """
 57        self.todos: list[ToDo] = []
 58        self._lock = asyncio.Lock()
 59
 60        if callback is None:
 61
 62            def default_add_callback(
 63                short_description: str, description: str, state: State
 64            ):
 65                pass
 66
 67            callback = default_add_callback
 68
 69        self.add_callback = callback
 70
 71    async def add(
 72        self, short_description: str, description: str, state: State = State.NOT_STARTED
 73    ):
 74        """Add a new todo to this toolset instance.
 75
 76        Args:
 77            short_description: Brief, unique label for the todo (used as its identifier in listings).
 78            description: Full details of what needs to be done.
 79            state: Initial state of the todo. Defaults to NOT_STARTED.
 80
 81        Raises:
 82            ValueError: If a todo with the same short_description or description already exists.
 83        """
 84        async with self._lock:
 85            to_do = ToDo(
 86                id=len(self.todos) + 1,
 87                short_description=short_description,
 88                description=description,
 89                state=state,
 90            )
 91
 92            validity_check = self.check_if_valid(self.todos, to_do)
 93            if validity_check is not None:
 94                raise ValueError(validity_check)
 95
 96            self.todos.append(to_do)
 97
 98        # Callback fires after the todo is committed; kept outside the lock so
 99        # user-provided callbacks cannot cause a deadlock.
100        try:
101            self.add_callback(short_description, description, state)
102        except Exception as e:
103            logger.error(f"Error in callback for todo: {e}")
104
105    @classmethod
106    def check_if_valid(cls, todos: list[ToDo], todo_to_add: ToDo) -> str | None:
107        """Return an error message if todo_to_add conflicts with existing todos, else None.
108
109        Args:
110            todos: The current list of todos to validate against.
111            todo_to_add: The candidate todo whose short_description and description are checked.
112        """
113        if todo_to_add.short_description in [todo.short_description for todo in todos]:
114            return f"Todo with short description '{todo_to_add.short_description}' already exists. Please provide a unique short description."
115
116        if todo_to_add.description in [todo.description for todo in todos]:
117            return f"Todo with description '{todo_to_add.description}' already exists. Please provide a unique description."
118
119        return None
120
121    def _get_all_todos(self) -> list[ToDo]:
122        return self.todos
123
124    async def get_all_todos(self) -> list[str]:
125        """Return complete_print() strings for all active (non-NO_LONGER_PLANNED) todos."""
126        async with self._lock:
127            return [
128                todo.complete_print()
129                for todo in self._get_all_todos()
130                if todo.state != State.NO_LONGER_PLANNED
131            ]
132
133    async def get_completed_todos(self) -> list[str]:
134        """Return complete_print() strings for todos in COMPLETED state."""
135        async with self._lock:
136            return [
137                todo.complete_print()
138                for todo in self._get_all_todos()
139                if todo.state == State.COMPLETED
140            ]
141
142    async def get_not_started_todos(self) -> list[str]:
143        """Return complete_print() strings for todos in NOT_STARTED state."""
144        async with self._lock:
145            return [
146                todo.complete_print()
147                for todo in self._get_all_todos()
148                if todo.state == State.NOT_STARTED
149            ]
150
151    async def get_incomplete_todos(self) -> list[str]:
152        """Return complete_print() strings for todos in NOT_STARTED, IN_PROGRESS, or FAILED state."""
153        incomplete_states = {State.NOT_STARTED, State.IN_PROGRESS, State.FAILED}
154        async with self._lock:
155            return [
156                todo.complete_print()
157                for todo in self._get_all_todos()
158                if todo.state in incomplete_states
159            ]
160
161    async def get_failed_todos(self) -> list[str]:
162        """Return complete_print() strings for todos in FAILED state."""
163        async with self._lock:
164            return [
165                todo.complete_print()
166                for todo in self._get_all_todos()
167                if todo.state == State.FAILED
168            ]
169
170    async def _find_and_update(self, todo_id: int, new_state: State) -> str:
171        async with self._lock:
172            for todo in self._get_all_todos():
173                if todo.id == todo_id:
174                    todo.update_state(new_state)
175                    return todo.complete_print()
176        raise ValueError(f"Todo with identifier '{todo_id}' not found.")
177
178    async def complete_todo_by_id(self, todo_id: int):
179        """Mark a todo as COMPLETED; raises ValueError if not found.
180
181        Args:
182            todo_id: The integer id of the todo to complete.
183        """
184        return "Successfully completed todo:\n" + await self._find_and_update(
185            todo_id, State.COMPLETED
186        )
187
188    async def start_todo_by_id(self, todo_id: int):
189        """Mark a todo as IN_PROGRESS; raises ValueError if not found.
190
191        Args:
192            todo_id: The integer id of the todo to start.
193        """
194        return "Successfully started todo:\n" + await self._find_and_update(
195            todo_id, State.IN_PROGRESS
196        )
197
198    async def fail_todo_by_id(self, todo_id: int):
199        """Mark a todo as FAILED; raises ValueError if not found.
200
201        Args:
202            todo_id: The integer id of the todo to fail.
203        """
204        return "Successfully marked todo as failed:\n" + await self._find_and_update(
205            todo_id, State.FAILED
206        )
207
208    async def no_longer_plan_todo_by_id(self, todo_id: int):
209        """Mark a todo as NO_LONGER_PLANNED; raises ValueError if not found.
210
211        Args:
212            todo_id: The integer id of the todo to deprioritize.
213        """
214        return (
215            "Successfully marked todo as no longer planned:\n"
216            + await self._find_and_update(todo_id, State.NO_LONGER_PLANNED)
217        )
218
219    async def make_all_no_longer_planned(self):
220        """Mark all NOT_STARTED and IN_PROGRESS todos as NO_LONGER_PLANNED; leaves COMPLETED and FAILED unchanged."""
221        affected = 0
222        async with self._lock:
223            for todo in self._get_all_todos():
224                if todo.state in {State.NOT_STARTED, State.IN_PROGRESS}:
225                    todo.update_state(State.NO_LONGER_PLANNED)
226                    affected += 1
227        return f"Marked {affected} todo(s) as no longer planned."
228
229    async def update_todo_by_id(self, todo_id: int, new_state: State):
230        """Transition a todo to an arbitrary state; raises ValueError if not found.
231
232        Args:
233            todo_id: The integer id of the todo to update.
234            new_state: The State to transition the todo to.
235        """
236        return "Successfully updated todo:\n" + await self._find_and_update(
237            todo_id, new_state
238        )
239
240    async def pretty_dashboard(self) -> str:
241        """Return a human-readable dashboard of active todos, or 'No todos found.'"""
242        async with self._lock:
243            lines = [
244                t.simplified_print()
245                for t in self._get_all_todos()
246                if t.state != State.NO_LONGER_PLANNED
247            ]
248        if not lines:
249            return "No todos found."
250        return "To-Dos\n" + "\n".join(lines)
251
252    @classmethod
253    def prompt(cls) -> str:
254        """Return the system prompt instructing an LLM how to use this toolset."""
255        return (
256            "Use the todo tools to plan and track your work. "
257            "Begin by calling add() for every task before starting any of them. "
258            "Call start_todo_by_id() when you begin a task and complete_todo_by_id() when it is done. "
259            "If a task cannot be completed, call fail_todo_by_id() instead. "
260            "If a planned task is no longer relevant, call no_longer_plan_todo_by_id() to remove it from active views. "
261            "To abandon the entire current plan, call make_all_no_longer_planned() — this leaves completed and failed todos unchanged. "
262            "Use update_todo_by_id() if a task needs a state change outside of the helpers above. "
263            "Retrieve identifiers via get_all_todos() before calling any id-based method. "
264            "Each todo requires a unique short_description and description."
265        )
266
267    def tool_set(self) -> list[RTFunction]:
268        """Return the list of RTFunction nodes for all public tools in this toolset."""
269        functions = [
270            self.add,
271            self.complete_todo_by_id,
272            self.start_todo_by_id,
273            self.fail_todo_by_id,
274            self.no_longer_plan_todo_by_id,
275            self.make_all_no_longer_planned,
276            self.update_todo_by_id,
277            self.get_all_todos,
278            self.get_completed_todos,
279            self.get_not_started_todos,
280            self.get_incomplete_todos,
281            self.get_failed_todos,
282        ]
283
284        return [rt.function_node(func) for func in functions]

Helper class that provides a standard way to create an ABC using inheritance.

ToDoToolSet( callback: Optional[Callable[[str, str, railtracks.prebuilt.tools.todo.todos.State], NoneType]] = None)
50    def __init__(self, callback: Callable[[str, str, State], None] | None = None):
51        """Create an empty toolset with an optional post-add callback.
52
53        Args:
54            callback: Invoked after each successful add() with (short_description, description, state).
55                      Exceptions are logged and swallowed; the todo is always committed regardless.
56        """
57        self.todos: list[ToDo] = []
58        self._lock = asyncio.Lock()
59
60        if callback is None:
61
62            def default_add_callback(
63                short_description: str, description: str, state: State
64            ):
65                pass
66
67            callback = default_add_callback
68
69        self.add_callback = callback

Create an empty toolset with an optional post-add callback.

Arguments:
  • callback: Invoked after each successful add() with (short_description, description, state). Exceptions are logged and swallowed; the todo is always committed regardless.
todos: list[railtracks.prebuilt.tools.todo.todos.ToDo]
add_callback
async def add( self, short_description: str, description: str, state: railtracks.prebuilt.tools.todo.todos.State = <State.NOT_STARTED: 'not_started'>):
 71    async def add(
 72        self, short_description: str, description: str, state: State = State.NOT_STARTED
 73    ):
 74        """Add a new todo to this toolset instance.
 75
 76        Args:
 77            short_description: Brief, unique label for the todo (used as its identifier in listings).
 78            description: Full details of what needs to be done.
 79            state: Initial state of the todo. Defaults to NOT_STARTED.
 80
 81        Raises:
 82            ValueError: If a todo with the same short_description or description already exists.
 83        """
 84        async with self._lock:
 85            to_do = ToDo(
 86                id=len(self.todos) + 1,
 87                short_description=short_description,
 88                description=description,
 89                state=state,
 90            )
 91
 92            validity_check = self.check_if_valid(self.todos, to_do)
 93            if validity_check is not None:
 94                raise ValueError(validity_check)
 95
 96            self.todos.append(to_do)
 97
 98        # Callback fires after the todo is committed; kept outside the lock so
 99        # user-provided callbacks cannot cause a deadlock.
100        try:
101            self.add_callback(short_description, description, state)
102        except Exception as e:
103            logger.error(f"Error in callback for todo: {e}")

Add a new todo to this toolset instance.

Arguments:
  • short_description: Brief, unique label for the todo (used as its identifier in listings).
  • description: Full details of what needs to be done.
  • state: Initial state of the todo. Defaults to NOT_STARTED.
Raises:
  • ValueError: If a todo with the same short_description or description already exists.
@classmethod
def check_if_valid( cls, todos: list[railtracks.prebuilt.tools.todo.todos.ToDo], todo_to_add: railtracks.prebuilt.tools.todo.todos.ToDo) -> str | None:
105    @classmethod
106    def check_if_valid(cls, todos: list[ToDo], todo_to_add: ToDo) -> str | None:
107        """Return an error message if todo_to_add conflicts with existing todos, else None.
108
109        Args:
110            todos: The current list of todos to validate against.
111            todo_to_add: The candidate todo whose short_description and description are checked.
112        """
113        if todo_to_add.short_description in [todo.short_description for todo in todos]:
114            return f"Todo with short description '{todo_to_add.short_description}' already exists. Please provide a unique short description."
115
116        if todo_to_add.description in [todo.description for todo in todos]:
117            return f"Todo with description '{todo_to_add.description}' already exists. Please provide a unique description."
118
119        return None

Return an error message if todo_to_add conflicts with existing todos, else None.

Arguments:
  • todos: The current list of todos to validate against.
  • todo_to_add: The candidate todo whose short_description and description are checked.
async def get_all_todos(self) -> list[str]:
124    async def get_all_todos(self) -> list[str]:
125        """Return complete_print() strings for all active (non-NO_LONGER_PLANNED) todos."""
126        async with self._lock:
127            return [
128                todo.complete_print()
129                for todo in self._get_all_todos()
130                if todo.state != State.NO_LONGER_PLANNED
131            ]

Return complete_print() strings for all active (non-NO_LONGER_PLANNED) todos.

async def get_completed_todos(self) -> list[str]:
133    async def get_completed_todos(self) -> list[str]:
134        """Return complete_print() strings for todos in COMPLETED state."""
135        async with self._lock:
136            return [
137                todo.complete_print()
138                for todo in self._get_all_todos()
139                if todo.state == State.COMPLETED
140            ]

Return complete_print() strings for todos in COMPLETED state.

async def get_not_started_todos(self) -> list[str]:
142    async def get_not_started_todos(self) -> list[str]:
143        """Return complete_print() strings for todos in NOT_STARTED state."""
144        async with self._lock:
145            return [
146                todo.complete_print()
147                for todo in self._get_all_todos()
148                if todo.state == State.NOT_STARTED
149            ]

Return complete_print() strings for todos in NOT_STARTED state.

async def get_incomplete_todos(self) -> list[str]:
151    async def get_incomplete_todos(self) -> list[str]:
152        """Return complete_print() strings for todos in NOT_STARTED, IN_PROGRESS, or FAILED state."""
153        incomplete_states = {State.NOT_STARTED, State.IN_PROGRESS, State.FAILED}
154        async with self._lock:
155            return [
156                todo.complete_print()
157                for todo in self._get_all_todos()
158                if todo.state in incomplete_states
159            ]

Return complete_print() strings for todos in NOT_STARTED, IN_PROGRESS, or FAILED state.

async def get_failed_todos(self) -> list[str]:
161    async def get_failed_todos(self) -> list[str]:
162        """Return complete_print() strings for todos in FAILED state."""
163        async with self._lock:
164            return [
165                todo.complete_print()
166                for todo in self._get_all_todos()
167                if todo.state == State.FAILED
168            ]

Return complete_print() strings for todos in FAILED state.

async def complete_todo_by_id(self, todo_id: int):
178    async def complete_todo_by_id(self, todo_id: int):
179        """Mark a todo as COMPLETED; raises ValueError if not found.
180
181        Args:
182            todo_id: The integer id of the todo to complete.
183        """
184        return "Successfully completed todo:\n" + await self._find_and_update(
185            todo_id, State.COMPLETED
186        )

Mark a todo as COMPLETED; raises ValueError if not found.

Arguments:
  • todo_id: The integer id of the todo to complete.
async def start_todo_by_id(self, todo_id: int):
188    async def start_todo_by_id(self, todo_id: int):
189        """Mark a todo as IN_PROGRESS; raises ValueError if not found.
190
191        Args:
192            todo_id: The integer id of the todo to start.
193        """
194        return "Successfully started todo:\n" + await self._find_and_update(
195            todo_id, State.IN_PROGRESS
196        )

Mark a todo as IN_PROGRESS; raises ValueError if not found.

Arguments:
  • todo_id: The integer id of the todo to start.
async def fail_todo_by_id(self, todo_id: int):
198    async def fail_todo_by_id(self, todo_id: int):
199        """Mark a todo as FAILED; raises ValueError if not found.
200
201        Args:
202            todo_id: The integer id of the todo to fail.
203        """
204        return "Successfully marked todo as failed:\n" + await self._find_and_update(
205            todo_id, State.FAILED
206        )

Mark a todo as FAILED; raises ValueError if not found.

Arguments:
  • todo_id: The integer id of the todo to fail.
async def no_longer_plan_todo_by_id(self, todo_id: int):
208    async def no_longer_plan_todo_by_id(self, todo_id: int):
209        """Mark a todo as NO_LONGER_PLANNED; raises ValueError if not found.
210
211        Args:
212            todo_id: The integer id of the todo to deprioritize.
213        """
214        return (
215            "Successfully marked todo as no longer planned:\n"
216            + await self._find_and_update(todo_id, State.NO_LONGER_PLANNED)
217        )

Mark a todo as NO_LONGER_PLANNED; raises ValueError if not found.

Arguments:
  • todo_id: The integer id of the todo to deprioritize.
async def make_all_no_longer_planned(self):
219    async def make_all_no_longer_planned(self):
220        """Mark all NOT_STARTED and IN_PROGRESS todos as NO_LONGER_PLANNED; leaves COMPLETED and FAILED unchanged."""
221        affected = 0
222        async with self._lock:
223            for todo in self._get_all_todos():
224                if todo.state in {State.NOT_STARTED, State.IN_PROGRESS}:
225                    todo.update_state(State.NO_LONGER_PLANNED)
226                    affected += 1
227        return f"Marked {affected} todo(s) as no longer planned."

Mark all NOT_STARTED and IN_PROGRESS todos as NO_LONGER_PLANNED; leaves COMPLETED and FAILED unchanged.

async def update_todo_by_id( self, todo_id: int, new_state: railtracks.prebuilt.tools.todo.todos.State):
229    async def update_todo_by_id(self, todo_id: int, new_state: State):
230        """Transition a todo to an arbitrary state; raises ValueError if not found.
231
232        Args:
233            todo_id: The integer id of the todo to update.
234            new_state: The State to transition the todo to.
235        """
236        return "Successfully updated todo:\n" + await self._find_and_update(
237            todo_id, new_state
238        )

Transition a todo to an arbitrary state; raises ValueError if not found.

Arguments:
  • todo_id: The integer id of the todo to update.
  • new_state: The State to transition the todo to.
async def pretty_dashboard(self) -> str:
240    async def pretty_dashboard(self) -> str:
241        """Return a human-readable dashboard of active todos, or 'No todos found.'"""
242        async with self._lock:
243            lines = [
244                t.simplified_print()
245                for t in self._get_all_todos()
246                if t.state != State.NO_LONGER_PLANNED
247            ]
248        if not lines:
249            return "No todos found."
250        return "To-Dos\n" + "\n".join(lines)

Return a human-readable dashboard of active todos, or 'No todos found.'

@classmethod
def prompt(cls) -> str:
252    @classmethod
253    def prompt(cls) -> str:
254        """Return the system prompt instructing an LLM how to use this toolset."""
255        return (
256            "Use the todo tools to plan and track your work. "
257            "Begin by calling add() for every task before starting any of them. "
258            "Call start_todo_by_id() when you begin a task and complete_todo_by_id() when it is done. "
259            "If a task cannot be completed, call fail_todo_by_id() instead. "
260            "If a planned task is no longer relevant, call no_longer_plan_todo_by_id() to remove it from active views. "
261            "To abandon the entire current plan, call make_all_no_longer_planned() — this leaves completed and failed todos unchanged. "
262            "Use update_todo_by_id() if a task needs a state change outside of the helpers above. "
263            "Retrieve identifiers via get_all_todos() before calling any id-based method. "
264            "Each todo requires a unique short_description and description."
265        )

Return the system prompt instructing an LLM how to use this toolset.

def tool_set(self) -> list[railtracks.built_nodes.function.base.RTFunction]:
267    def tool_set(self) -> list[RTFunction]:
268        """Return the list of RTFunction nodes for all public tools in this toolset."""
269        functions = [
270            self.add,
271            self.complete_todo_by_id,
272            self.start_todo_by_id,
273            self.fail_todo_by_id,
274            self.no_longer_plan_todo_by_id,
275            self.make_all_no_longer_planned,
276            self.update_todo_by_id,
277            self.get_all_todos,
278            self.get_completed_todos,
279            self.get_not_started_todos,
280            self.get_incomplete_todos,
281            self.get_failed_todos,
282        ]
283
284        return [rt.function_node(func) for func in functions]

Return the list of RTFunction nodes for all public tools in this toolset.