mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-04 07:20:10 +00:00
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.
144 lines
5.9 KiB
Python
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)
|
|
]
|