mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-13 15:50:02 +00:00
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.
This commit is contained in:
+100
-11
@@ -23,7 +23,13 @@ from .connectors import (
|
||||
)
|
||||
from .engine import Approver, TurnEngine
|
||||
from .environment import environment_context
|
||||
from .memory import MemoryStore, Scope, format_memories, memory_tools
|
||||
from .memory import (
|
||||
MemoryStore,
|
||||
Scope,
|
||||
format_user_rules,
|
||||
memory_tools,
|
||||
render_memory_block,
|
||||
)
|
||||
from .permissions import Mode, PermissionEngine
|
||||
from .project import load_agents_md
|
||||
from .roots import RootDir, normalize_roots, render_context
|
||||
@@ -57,21 +63,50 @@ in which files, how you'll verify) — don't describe edits as if you were makin
|
||||
the plan is approved, this same session switches to execution and you implement it; if
|
||||
rejected, revise the plan using the feedback."""
|
||||
|
||||
# When-to-remember rules, injected only when a memory store is wired. Without these,
|
||||
# models either never call `remember` or save noise the repo already records.
|
||||
# When-to-remember rules (MEMORY-SPEC §4.2), injected only when a memory store is wired.
|
||||
# Without these, models either never call `remember` or save noise the repo already
|
||||
# records. The conservative bias is deliberate: a wrong memory feels broken and creepy at
|
||||
# once; a missing one merely means the user repeats themselves.
|
||||
_MEMORY_GUIDANCE = """\
|
||||
Memory:
|
||||
- You have persistent memory across sessions. Use `remember` for durable facts: the user's \
|
||||
corrections and stated preferences (include the why), and project context you couldn't \
|
||||
rederive from the code. Don't save what the repo already records (code structure, git \
|
||||
history, AGENTS.md) or details that only matter to the current task. Use absolute dates, \
|
||||
never "yesterday".
|
||||
rederive from the code. Scope by what the fact is about: facts about the user -> "global"; \
|
||||
facts about the current work -> "workspace". Always pass a one-line summary (15 words max) \
|
||||
alongside the full content.
|
||||
- Save conservatively — a wrong memory costs more than a missing one. Save only clearly \
|
||||
durable facts ("from now on", "always", "in all my chats"). Ambiguous one-off phrasing \
|
||||
("I prefer simple talking"): apply it now, don't save it. But when the user explicitly \
|
||||
asks you to remember something, always save it.
|
||||
- Sensitive topics (health, finances, relationships, beliefs): never save silently. Ask \
|
||||
first — "Want me to remember this for next time?" — and save only on a yes.
|
||||
- When you save, say so in one short plain sentence in your visible reply ("I'll remember \
|
||||
that you prefer short replies."). And the first time a remembered fact shapes your \
|
||||
behavior in a session, note it in one quiet line ("Keeping this short since you prefer \
|
||||
simple replies.") — first use only, not every message.
|
||||
- Don't save what the repo already records (code structure, git history, AGENTS.md) or \
|
||||
details that only matter to the current task. Use absolute dates, never "yesterday".
|
||||
- Before saving, check the known-memories list: if an entry already covers it, revise that \
|
||||
entry with `memory_update` instead of adding a near-duplicate; retire wrong or obsolete \
|
||||
entries with `memory_forget`.
|
||||
- Memories reflect when they were written. If one names a file, flag, or URL, verify it \
|
||||
still exists before relying on it."""
|
||||
|
||||
# Injected INSTEAD of the memory guidance when the user turned memory off (§4.3).
|
||||
# Off means "stop LEARNING", not "forget what you know": already-saved memories stay
|
||||
# injected and usable; only the write tools are gone. Without this notice the model
|
||||
# bluffs — asked to "remember" with no remember tool, it narrated a fake save through
|
||||
# its todo list ("I'll remember that your favorite color is blue"), observed live
|
||||
# 2026-07-28. Honesty needs the model to KNOW saving is off, not just lack the tools.
|
||||
_MEMORY_OFF_NOTICE = """\
|
||||
Saving new memories is turned off in this user's Settings. What you already know about \
|
||||
them (the known-memories list, if any) is still true and you should keep using it — but \
|
||||
you have no way to save, change, or delete anything, and nothing new from this \
|
||||
conversation will carry over to future ones. If the user asks you to remember something \
|
||||
new, state both halves plainly: you'll keep it in mind for the rest of this conversation, \
|
||||
but it won't be saved once the conversation ends — they can turn saving back on in \
|
||||
Settings ▸ Memory. Never imply you saved, noted, or will remember anything new."""
|
||||
|
||||
# UX-015 (§33): the GUI interleaves these status lines with humanized tool rows inside a
|
||||
# collapsed "turn" — they're what the user reads while the agent works. Universal (appended
|
||||
# for every persona); models that ignore it degrade gracefully to a turn with no narration.
|
||||
@@ -118,6 +153,21 @@ def build_engine(
|
||||
max_iterations: Optional[int] = None,
|
||||
model_settings: Optional[dict[str, Any]] = None,
|
||||
memory_store: Optional[MemoryStore] = None,
|
||||
# MEMORY-SPEC §5.1: called with the MemoryItem right after `remember` persists it —
|
||||
# the manager uses this to push the memory_saved event that powers the save toast.
|
||||
on_memory_saved: Optional[Any] = None,
|
||||
# MEMORY-SPEC §6: the user's standing rules (Settings textarea). Injected verbatim
|
||||
# above auto memories; independent of the memory on/off switch. No tool writes it.
|
||||
# A CALLABLE is read per turn (the server passes one so a Settings edit reaches
|
||||
# conversations already open); a plain string is a fixed value for CLI/tests.
|
||||
user_rules: Optional[Any] = None,
|
||||
# True when the user turned memory OFF in Settings (vs. memory simply not wired):
|
||||
# injects the honesty notice so the model says so instead of faking a save.
|
||||
memory_off: bool = False,
|
||||
# LIVE saving switch, consulted per write so turning memory off applies to
|
||||
# conversations already running (the registry is fixed at build, so the tool stays
|
||||
# and refuses). Same pattern as the skills menu's live filter.
|
||||
memory_saving_enabled: Optional[Any] = None,
|
||||
messages: Optional[list[dict[str, Any]]] = None,
|
||||
extra_tools: Optional[list[Any]] = None,
|
||||
secrets: Optional[SecretStore] = None,
|
||||
@@ -247,15 +297,46 @@ def build_engine(
|
||||
if conventions:
|
||||
instructions = f"{instructions}\n\n{conventions}"
|
||||
|
||||
# The user's own standing instructions, read once here: like the memories below,
|
||||
# they're session-stable knowledge. Edits apply to NEW conversations (the Settings
|
||||
# copy says exactly that), never mid-conversation.
|
||||
rules_block = format_user_rules(
|
||||
(user_rules() if callable(user_rules) else user_rules) or ""
|
||||
)
|
||||
if rules_block:
|
||||
instructions = f"{instructions}\n\n{rules_block}"
|
||||
|
||||
# The live saving switch. The callable (server) beats the build-time flag (CLI/tests):
|
||||
# the setting can flip EITHER WAY mid-conversation, so nothing about it may be baked
|
||||
# into the fixed registry or the static instructions (owner-hit 2026-07-28, both
|
||||
# directions: off kept saving, then on kept claiming it was off).
|
||||
def _saving_enabled() -> bool:
|
||||
if memory_saving_enabled is not None:
|
||||
return bool(memory_saving_enabled())
|
||||
return not memory_off
|
||||
|
||||
if memory_store is not None:
|
||||
# Always the full toolset: the registry is fixed at build, so a session born
|
||||
# while saving was off must still be able to save the moment it's turned on.
|
||||
# Enforcement is the tools' own live check, not their absence.
|
||||
registry.register_all(
|
||||
memory_tools(memory_store, workspace=str(ws) if ws else None)
|
||||
memory_tools(
|
||||
memory_store,
|
||||
workspace=str(ws) if ws else None,
|
||||
on_saved=on_memory_saved,
|
||||
saving_enabled=_saving_enabled,
|
||||
)
|
||||
)
|
||||
instructions = f"{instructions}\n\n{_MEMORY_GUIDANCE}"
|
||||
# What the coworker KNOWS is fixed at session start (MEMORY-SPEC §7.1): a
|
||||
# conversation's knowledge must not shift underfoot — a fact it referenced ten
|
||||
# turns ago cannot silently vanish — and the system prompt is the cached prefix,
|
||||
# so the facts are processed once instead of re-sent every turn. Deletions reach
|
||||
# NEW conversations; the UI says so rather than pretending otherwise.
|
||||
remembered = memory_store.list(scope=Scope.GLOBAL)
|
||||
if ws is not None:
|
||||
remembered += memory_store.list(scope=Scope.WORKSPACE, workspace=str(ws))
|
||||
block = format_memories(remembered)
|
||||
block = render_memory_block(remembered)
|
||||
if block:
|
||||
instructions = f"{instructions}\n\n{block}"
|
||||
|
||||
@@ -285,9 +366,12 @@ def build_engine(
|
||||
registry.register(propose_plan_tool())
|
||||
|
||||
# Per-turn ephemeral context, appended to the latest user message since mid-thread system
|
||||
# messages aren't reliable across providers. Two producers: the plan-mode reminder (mode can
|
||||
# flip mid-session, so it's checked each turn, not baked into the instructions) and the live
|
||||
# directory list (orphan Cowork can gain folders mid-session; Cowork/MyHelper only).
|
||||
# messages aren't reliable across providers. Three producers: the plan-mode reminder (mode can
|
||||
# flip mid-session, so it's checked each turn, not baked into the instructions), the live
|
||||
# directory list (orphan Cowork can gain folders mid-session; Cowork/MyHelper only), and the
|
||||
# memory-SAVING notice (same reason as plan mode — the switch flips either way mid-chat).
|
||||
# Note what is NOT here: the memories and the user's rules. Those are knowledge, fixed at
|
||||
# session start (§7.1).
|
||||
roots_context = (
|
||||
(lambda: render_context(root_list))
|
||||
if root_list and agent.family == "knowledge"
|
||||
@@ -300,6 +384,11 @@ def build_engine(
|
||||
parts.append(_PLAN_MODE_CONTEXT)
|
||||
elif permissions.mode is Mode.DISCUSS:
|
||||
parts.append(_DISCUSS_MODE_CONTEXT)
|
||||
# Only the SAVING switch is per-turn (§4.3): it governs an action, not
|
||||
# knowledge, so it must bite the moment the user flips it. What the coworker
|
||||
# knows stays fixed for the session — see the instructions built above.
|
||||
if memory_store is not None and not _saving_enabled():
|
||||
parts.append(_MEMORY_OFF_NOTICE)
|
||||
if roots_context is not None:
|
||||
ctx = roots_context()
|
||||
if ctx:
|
||||
|
||||
Reference in New Issue
Block a user