Files
openworker/coworker/memory/tools.py
T
Devika Verma ef59b0f39a Memory V1: remembered facts, your instructions, one screen
Coworkers remember durable things you tell them and use them in future sessions.
One Settings screen lists everything remembered - edit, delete, or stop new saves; standing instructions ride along.
Knowledge is session-stable, the save switch is per-message; sqlite gains a summary column via in-place migration.
2026-07-28 20:32:14 +05:30

144 lines
5.9 KiB
Python

"""Memory tools — the agent's explicit paths into memory.
`remember` saves a new fact; `memory_update` / `memory_forget` revise or retire one by
the [#id] shown in the known-memories block, so corrections replace stale facts instead
of piling up next to them. `memory_read` fetches full bodies by id — the retrieval half
of index mode (MEMORY-SPEC §7); registered always, harmless in full mode.
`on_saved` is the save-notice hook (spec §5.1): the manager passes a callback that pushes
a memory_saved event to the session's surface so it can render "I'll remember that — …
[Undo]" inline in the transcript. It fires for `memory_update` too — the
update-don't-duplicate rule means many saves arrive as edits to an existing memory, and
those were invisible (owner-hit 2026-07-28) — carrying the previous text so Undo can put
it back. Failures in the callback never fail the write.
"""
from __future__ import annotations
from typing import Callable, Optional
import aisuite as ai
from .base import MemoryItem, MemoryStore, Scope
_SCOPES = {s.value for s in Scope}
_META = dict(category="memory", risk_level="low", capabilities=["remember"])
def memory_tools(
store: MemoryStore,
*,
workspace: Optional[str],
on_saved: Optional[Callable[[MemoryItem, Optional[str]], None]] = None,
saving_enabled: Optional[Callable[[], bool]] = None,
) -> list:
"""The agent's memory tools.
`saving_enabled` is a LIVE callable checked on each write, so the Settings switch
applies to conversations already running — in BOTH directions (owner-hit
2026-07-28: off kept saving, then on kept refusing). The registry is fixed at
build, so the write tools are always registered and refuse when saving is off;
`memory_read` never gates (off = stop learning, not amnesia).
"""
def _saving_off() -> bool:
return saving_enabled is not None and not saving_enabled()
_OFF_ERROR = (
"Saving memories is turned off in the user's Settings (they can turn it back "
"on in Settings ▸ Memory). Nothing was saved — tell the user plainly instead "
"of implying you remembered it."
)
def _announce(item: MemoryItem, previous: Optional[str]) -> None:
"""Surface the write to the user (§5.1). Best-effort: the notice is never worth
failing a write that already succeeded."""
if on_saved is None:
return
try:
on_saved(item, previous)
except Exception:
pass
def remember(content: str, summary: str = "", scope: str = "workspace") -> dict:
"""Save a durable memory (a fact or preference) to recall in future sessions.
Check the known-memories list first: if one already covers this, use
memory_update instead of saving a near-duplicate.
Args:
content (str): The thing to remember, with the why.
summary (str): One-line gist (15 words max) shown in compact listings.
scope (str): "global" (facts about the user — applies everywhere) or
"workspace" (facts about this project only).
"""
if _saving_off():
return {"saved": False, "error": _OFF_ERROR}
chosen = Scope(scope) if scope in _SCOPES else Scope.WORKSPACE
if chosen is Scope.SESSION: # dead scope (spec §3): never save to it
chosen = Scope.WORKSPACE
item = store.add(
content,
scope=chosen,
summary=summary.strip() or None,
workspace=workspace if chosen is Scope.WORKSPACE else None,
)
_announce(item, None)
return {"id": item.id, "scope": item.scope.value, "saved": True}
def memory_read(memory_ids: list[int]) -> dict:
"""Read the full content of memories by id (use when the known-memories list
shows only a one-line summary and you need the details before acting).
Args:
memory_ids (list[int]): The [#id]s to fetch.
"""
found, missing = [], []
for mid in memory_ids:
item = store.get(int(mid))
if item is None:
missing.append(int(mid))
else:
found.append(
{"id": item.id, "scope": item.scope.value, "content": item.content}
)
result: dict = {"memories": found}
if missing:
result["missing"] = missing
return result
def memory_update(memory_id: int, content: str, summary: str = "") -> dict:
"""Rewrite an existing memory with corrected or refined content.
Args:
memory_id (int): The memory's id, from the [#id] in the known-memories list.
content (str): The full corrected memory text (replaces the old text).
summary (str): Corrected one-line gist (15 words max).
"""
if _saving_off():
return {"updated": False, "error": _OFF_ERROR}
# Captured BEFORE the write so the user's Undo can restore the old wording.
existing = store.get(memory_id)
previous = existing.content if existing is not None else None
item = store.update(memory_id, content, summary=summary.strip() or None)
if item is None:
return {"updated": False, "error": f"no memory with id {memory_id}"}
_announce(item, previous)
return {"updated": True, "id": item.id}
def memory_forget(memory_id: int) -> dict:
"""Delete a memory that turned out to be wrong or is no longer true.
Args:
memory_id (int): The memory's id, from the [#id] in the known-memories list.
"""
if _saving_off():
return {"deleted": False, "error": _OFF_ERROR}
if store.delete(memory_id):
return {"deleted": True, "id": memory_id}
return {"deleted": False, "error": f"no memory with id {memory_id}"}
return [
ai.tool(fn, metadata=ai.ToolMetadata(**_META))
for fn in (remember, memory_read, memory_update, memory_forget)
]