feat(memory): migrate MEMORY.md to global path & enhance ask-user prompts (#161)

* feat(prompt): enhance user interaction with multiple-choice and free-text questions

* refactor(paths): rename MEMORY_DIR to MEMORIES_DIR for consistency

* style(tests): format code for better readability in test cases

* feat(prompt): add validation for 'other' option in user prompt

* feat(prompt): refactor validation logic for user prompts and add skip option

* feat(style): refactor to use shared _PICKER_STYLE from interactive module
This commit is contained in:
Xi Zhang
2026-04-16 16:33:45 +02:00
committed by GitHub
parent 7f522cb4fe
commit 210e8864f6
13 changed files with 221 additions and 118 deletions
+5 -6
View File
@@ -257,7 +257,7 @@ def _get_default_backend():
workspace_dir = str(_paths_mod.WORKSPACE_ROOT)
set_active_workspace(workspace_dir)
memory_dir = str(_paths_mod.MEMORY_DIR)
memory_dir = str(_paths_mod.MEMORIES_DIR)
user_skills_dir = str(_paths_mod.USER_SKILLS_DIR)
global_skills_dir = str(_paths_mod.GLOBAL_SKILLS_DIR)
@@ -279,7 +279,7 @@ def _get_default_backend():
default=ws_backend,
routes={
"/skills/": sk_backend,
"/memory/": mem_backend,
"/memories/": mem_backend,
},
)
@@ -296,7 +296,7 @@ def _get_default_middleware():
cfg = _ensure_config()
model = _ensure_chat_model()
memory_dir = str(_paths_mod.MEMORY_DIR)
memory_dir = str(_paths_mod.MEMORIES_DIR)
mw = [
create_context_editing_middleware(model),
ContextOverflowMapperMiddleware(),
@@ -395,7 +395,7 @@ def create_cli_agent(workspace_dir: str | None = None, checkpointer=None, config
workspace_dir = str(_paths.WORKSPACE_ROOT)
# Read paths dynamically so runtime set_workspace_root() changes are picked up
_mem_dir = str(_paths.MEMORY_DIR)
_mem_dir = str(_paths.MEMORIES_DIR)
_usr_skills_dir = str(_paths.USER_SKILLS_DIR)
_global_skills_dir = str(_paths.GLOBAL_SKILLS_DIR)
@@ -412,7 +412,6 @@ def create_cli_agent(workspace_dir: str | None = None, checkpointer=None, config
global_dir=_global_skills_dir,
secondary_dir=SKILLS_DIR,
)
# Memory always uses SHARED directory (not per-session) for cross-session persistence
mem_backend = FilesystemBackend(
root_dir=_mem_dir,
virtual_mode=True,
@@ -421,7 +420,7 @@ def create_cli_agent(workspace_dir: str | None = None, checkpointer=None, config
default=ws_backend,
routes={
"/skills/": sk_backend,
"/memory/": mem_backend,
"/memories/": mem_backend,
},
)
+1 -1
View File
@@ -294,7 +294,7 @@ def cmd_interactive(
from .. import paths
memory_dir = str(paths.MEMORY_DIR)
memory_dir = str(paths.MEMORIES_DIR)
from ..config.settings import get_config_dir
+1 -12
View File
@@ -9,7 +9,6 @@ from __future__ import annotations
from collections import Counter
import questionary
from prompt_toolkit.styles import Style as PtStyle
from questionary import Choice
from ..mcp.registry import (
@@ -22,17 +21,7 @@ from ..mcp.registry import (
install_mcp_servers,
)
from ..stream.display import console
_PICKER_STYLE = PtStyle.from_dict(
{
"questionmark": "#888888",
"question": "",
"pointer": "bold",
"highlighted": "bold",
"text": "#888888",
"answer": "bold",
}
)
from .interactive import _PICKER_STYLE
_INSTALLED_INDICATOR = ("fg:#4caf50", "\u2713 ")
+1 -12
View File
@@ -150,22 +150,11 @@ def _cmd_install_skills(args: str = "") -> None:
from collections import Counter
import questionary
from prompt_toolkit.styles import Style as PtStyle
from questionary import Choice
from ..paths import GLOBAL_SKILLS_DIR, USER_SKILLS_DIR
from ..tools.skills_manager import fetch_remote_skill_index, install_skill
_PICKER_STYLE = PtStyle.from_dict(
{
"questionmark": "#888888",
"question": "",
"pointer": "bold",
"highlighted": "bold",
"text": "#888888",
"answer": "bold",
}
)
from .interactive import _PICKER_STYLE
# Installed-item indicator style for disabled checkbox choices.
_INSTALLED_INDICATOR = ("fg:#4caf50", "✓ ")
@@ -49,7 +49,7 @@ class CurrentCommand(Command):
f"Workspace: {_shorten_path(ctx.workspace_dir)}",
style="dim",
)
memory_path = paths.MEMORY_DIR
memory_path = paths.MEMORIES_DIR
if memory_path:
from ...cli.agent import _shorten_path
+7 -7
View File
@@ -4,7 +4,7 @@ Automatically extracts and persists long-term memory (user profile, research
preferences, experiment conclusions) from conversations.
Two mechanisms:
1. **Injection** (every LLM call): Reads ``/memory/MEMORY.md`` and appends it
1. **Injection** (every LLM call): Reads ``/memories/MEMORY.md`` and appends it
to the system prompt so the agent always has context.
2. **Extraction** (threshold-triggered): When the conversation exceeds a
configurable message count, uses an LLM call to pull out structured facts
@@ -17,7 +17,7 @@ from EvoScientist.middleware import EvoMemoryMiddleware
middleware = EvoMemoryMiddleware(
backend=my_backend, # or backend factory
memory_path="/memory/MEMORY.md",
memory_path="/memories/MEMORY.md",
extraction_model=chat_model,
trigger=("messages", 20),
)
@@ -168,7 +168,7 @@ Use this to personalize your responses and avoid re-asking known information.
- An experiment completes with notable conclusions
**How to update memory:**
- If `/memory/MEMORY.md` does not exist yet, use `write_file` to create it
- If `/memories/MEMORY.md` does not exist yet, use `write_file` to create it
- If it already exists, use `edit_file` to update specific sections
- Use this markdown structure:
@@ -446,7 +446,7 @@ class EvoMemoryMiddleware(AgentMiddleware):
Args:
backend: Backend instance or factory for reading/writing memory files.
memory_path: Virtual path to MEMORY.md (default ``/memory/MEMORY.md``).
memory_path: Virtual path to MEMORY.md (default ``/memories/MEMORY.md``).
extraction_model: Chat model used for extraction (can be a cheap/fast
model like ``claude-haiku``). If ``None``, automatic extraction is
disabled and only prompt injection + manual ``edit_file`` works.
@@ -461,7 +461,7 @@ class EvoMemoryMiddleware(AgentMiddleware):
self,
*,
backend: BACKEND_TYPES,
memory_path: str = "/memory/MEMORY.md",
memory_path: str = "/memories/MEMORY.md",
extraction_model: BaseChatModel | None = None,
trigger: tuple[str, int] = ("messages", 20),
) -> None:
@@ -687,7 +687,7 @@ class EvoMemoryMiddleware(AgentMiddleware):
logger.debug("Failed to load memory during modify_request: %s", e)
# Use placeholder when memory file doesn't exist yet
if not memory_content:
memory_content = "(No memory saved yet. Create `/memory/MEMORY.md` when you learn important information.)"
memory_content = "(No memory saved yet. Create `/memories/MEMORY.md` when you learn important information.)"
from deepagents.middleware._utils import append_to_system_message
@@ -802,7 +802,7 @@ def create_memory_middleware(
"""
from deepagents.backends import FilesystemBackend
from ..paths import MEMORY_DIR as _DEFAULT_MEMORY_DIR
from ..paths import MEMORIES_DIR as _DEFAULT_MEMORY_DIR
if memory_dir is None:
memory_dir = str(_DEFAULT_MEMORY_DIR)
+30 -4
View File
@@ -22,7 +22,6 @@ def _env_path(key: str) -> Path | None:
WORKSPACE_ROOT = _env_path("EVOSCIENTIST_WORKSPACE_DIR") or Path.cwd()
RUNS_DIR = _env_path("EVOSCIENTIST_RUNS_DIR") or (WORKSPACE_ROOT / "runs")
MEMORY_DIR = _env_path("EVOSCIENTIST_MEMORY_DIR") or (WORKSPACE_ROOT / "memory")
USER_SKILLS_DIR = _env_path("EVOSCIENTIST_SKILLS_DIR") or (WORKSPACE_ROOT / "skills")
MEDIA_DIR = _env_path("EVOSCIENTIST_MEDIA_DIR") or (WORKSPACE_ROOT / "media")
@@ -33,9 +32,27 @@ def _global_skills_dir() -> Path:
return base / "evoscientist" / "skills"
def _global_memories_dir() -> Path:
xdg = os.environ.get("XDG_CONFIG_HOME")
base = Path(xdg) if xdg else Path.home() / ".config"
return base / "evoscientist" / "memories"
# Global skills: shared across all workspaces (~/.config/evoscientist/skills/)
GLOBAL_SKILLS_DIR: Path = _global_skills_dir()
# Global memories: shared across all workspaces (~/.config/evoscientist/memories/)
GLOBAL_MEMORIES_DIR: Path = _global_memories_dir()
# Memories dir: global by default, overridable via env var.
# Supports both new (EVOSCIENTIST_MEMORIES_DIR) and old (EVOSCIENTIST_MEMORY_DIR) env vars.
MEMORIES_DIR: Path = (
_env_path("EVOSCIENTIST_MEMORIES_DIR")
or _env_path("EVOSCIENTIST_MEMORY_DIR")
or GLOBAL_MEMORIES_DIR
)
MEMORY_DIR = MEMORIES_DIR # backward compat alias
def set_workspace_root(path: str | Path) -> None:
"""Update workspace root and re-derive dependent directories.
@@ -43,10 +60,14 @@ def set_workspace_root(path: str | Path) -> None:
Directories with an explicit environment-variable override keep their
env-var value; all others are re-derived from the new root.
Also resets ``_active_workspace`` to the new root as a safe default.
Note: MEMORIES_DIR is global (not workspace-scoped) but env var overrides
are re-evaluated here to support late-set environment variables.
"""
global \
WORKSPACE_ROOT, \
RUNS_DIR, \
MEMORIES_DIR, \
MEMORY_DIR, \
USER_SKILLS_DIR, \
MEDIA_DIR, \
@@ -54,7 +75,12 @@ def set_workspace_root(path: str | Path) -> None:
WORKSPACE_ROOT = Path(path).resolve()
_active_workspace = WORKSPACE_ROOT
RUNS_DIR = _env_path("EVOSCIENTIST_RUNS_DIR") or (WORKSPACE_ROOT / "runs")
MEMORY_DIR = _env_path("EVOSCIENTIST_MEMORY_DIR") or (WORKSPACE_ROOT / "memory")
MEMORIES_DIR = (
_env_path("EVOSCIENTIST_MEMORIES_DIR")
or _env_path("EVOSCIENTIST_MEMORY_DIR")
or GLOBAL_MEMORIES_DIR
)
MEMORY_DIR = MEMORIES_DIR
USER_SKILLS_DIR = _env_path("EVOSCIENTIST_SKILLS_DIR") or (
WORKSPACE_ROOT / "skills"
)
@@ -64,13 +90,13 @@ def set_workspace_root(path: str | Path) -> None:
def ensure_dirs() -> None:
"""Create runtime subdirectories if they do not exist.
Only memory is created eagerly — skills directories are created on demand
Only memories is created eagerly — skills directories are created on demand
by install_skill() when the user first installs a skill.
Does NOT create the workspace root itself — it should already exist
(either the user's cwd or a directory they specified).
"""
MEMORY_DIR.mkdir(parents=True, exist_ok=True)
MEMORIES_DIR.mkdir(parents=True, exist_ok=True)
def default_workspace_dir() -> Path:
+61 -35
View File
@@ -987,16 +987,20 @@ def _get_event_loop() -> asyncio.AbstractEventLoop:
def _resolve_ask_user_prompt(ask_user_data: dict) -> dict:
"""Interactive console Q&A for ask_user events.
Presents questions via ``prompt_toolkit.prompt()`` (not ``input()``)
for proper CJK IME support and styled prompts without cursor drift.
Presents multiple-choice questions with arrow-key navigation via
``questionary.select()`` and free-text questions via
``questionary.text()`` with required-field validation. Matches the
questionary style used throughout the rest of the CLI.
"""
from prompt_toolkit import prompt as pt_prompt # type: ignore[import-untyped]
from prompt_toolkit.formatted_text import HTML # type: ignore[import-untyped]
import questionary # type: ignore[import-untyped]
from ..cli.interactive import _PICKER_STYLE
questions = ask_user_data.get("questions", [])
if not questions:
return {"answers": [], "status": "answered"}
total = len(questions)
console.print()
console.print(
Panel(
@@ -1013,43 +1017,65 @@ def _resolve_ask_user_prompt(ask_user_data: dict) -> dict:
q_text = q.get("question", "")
q_type = q.get("type", "text")
required = q.get("required", True)
tag = " [dim](optional)[/dim]" if not required else ""
console.print(f" [bold]{i + 1}. {q_text}[/bold]{tag}")
optional_suffix = " (optional)" if not required else ""
prompt_text = f"({i + 1}/{total}) {q_text}{optional_suffix}"
def _make_validator(is_required: bool):
def _validate(v: str) -> bool | str:
if is_required and not v.strip():
return "This field is required."
return True
return _validate
if q_type == "multiple_choice":
choices = q.get("choices", [])
for j, choice in enumerate(choices):
label = choice.get("value", str(choice))
letter = chr(ord("A") + j)
console.print(Text(f" {letter}. {label}", style="dim"))
other_letter = chr(ord("A") + len(choices))
console.print(
Text(f" {other_letter}. Other (type your answer)", style="dim")
)
choice_labels = [c.get("value", str(c)) for c in choices]
skip_label = "Skip"
if not required:
choice_labels.append(skip_label)
other_label = "Other (type your answer)"
choice_labels.append(other_label)
selected = questionary.select(
prompt_text,
choices=choice_labels,
style=_PICKER_STYLE,
).ask()
if selected is None: # Ctrl+C
raise KeyboardInterrupt
if selected == skip_label:
answers.append("")
console.print()
continue
if selected == other_label:
selected = questionary.text(
"Your answer:",
validate=_make_validator(required),
style=_PICKER_STYLE,
).ask()
if selected is None:
raise KeyboardInterrupt
answers.append(selected)
letters = "/".join(chr(ord("A") + k) for k in range(len(choices) + 1))
raw = pt_prompt(
HTML(f" <b><style fg='#1565c0'>Choice [{letters}]:</style></b> ")
).strip()
if raw.upper() == other_letter:
raw = pt_prompt(
HTML(" <b><style fg='#42a5f5'>&gt; Your answer:</style></b> ")
).strip()
answers.append(raw)
elif len(raw) == 1 and raw.upper().isalpha():
idx = ord(raw.upper()) - ord("A")
if 0 <= idx < len(choices):
answers.append(choices[idx].get("value", raw))
else:
answers.append(raw)
else:
answers.append(raw)
else:
raw = pt_prompt(
HTML(" <b><style fg='#42a5f5'>&gt; Answer:</style></b> ")
).strip()
answers.append(raw)
answer = questionary.text(
prompt_text,
validate=_make_validator(required),
style=_PICKER_STYLE,
).ask()
if answer is None: # Ctrl+C
raise KeyboardInterrupt
answers.append(answer)
console.print()
except (EOFError, KeyboardInterrupt):
console.print("[dim] Cancelled.[/dim]")
return {"status": "cancelled"}
+3 -3
View File
@@ -114,9 +114,9 @@ def _tool_path_arg(args: dict | None) -> str:
def _is_memory_path(path: str) -> bool:
"""Return True when a virtual path targets the shared memory directory."""
"""Return True when a virtual path targets the global memories directory."""
normalized = (path or "").strip()
return normalized == "/memory" or normalized.startswith("/memory/")
return normalized == "/memories" or normalized.startswith("/memories/")
def format_tool_compact(name: str, args: dict | None) -> str:
@@ -250,7 +250,7 @@ def format_tool_compact_with_result(
if name_lower in ("write_file", "edit_file"):
if (
"/memory/" in result_content
"/memories/" in result_content
or "/MEMORY.md" in result_content
or "MEMORY.md" in result_content
):
+45 -7
View File
@@ -445,30 +445,37 @@ def test_auto_mode_disables_ask_user_middleware(
class TestRichCLIPrompt:
"""Test _resolve_ask_user_prompt with mocked prompt_toolkit.prompt()."""
_PT_PROMPT = "prompt_toolkit.prompt"
"""Test _resolve_ask_user_prompt with mocked questionary."""
def test_text_question_returns_answered(self):
from unittest.mock import MagicMock
from EvoScientist.stream.display import _resolve_ask_user_prompt
data = {
"questions": [{"question": "What dataset?", "type": "text"}],
"tool_call_id": "tc_1",
}
with patch(self._PT_PROMPT, side_effect=["CIFAR-10"]):
mock_text = MagicMock()
mock_text.return_value.ask.return_value = "CIFAR-10"
with patch("questionary.text", mock_text):
result = _resolve_ask_user_prompt(data)
assert result["status"] == "answered"
assert result["answers"] == ["CIFAR-10"]
def test_keyboard_interrupt_returns_cancelled(self):
from unittest.mock import MagicMock
from EvoScientist.stream.display import _resolve_ask_user_prompt
data = {
"questions": [{"question": "What?", "type": "text"}],
"tool_call_id": "tc_1",
}
with patch(self._PT_PROMPT, side_effect=KeyboardInterrupt):
# questionary returns None when user presses Ctrl+C
mock_text = MagicMock()
mock_text.return_value.ask.return_value = None
with patch("questionary.text", mock_text):
result = _resolve_ask_user_prompt(data)
assert result["status"] == "cancelled"
@@ -480,7 +487,9 @@ class TestRichCLIPrompt:
assert result["status"] == "answered"
assert result["answers"] == []
def test_multiple_choice_letter_mapping(self):
def test_multiple_choice_selection(self):
from unittest.mock import MagicMock
from EvoScientist.stream.display import _resolve_ask_user_prompt
data = {
@@ -493,11 +502,40 @@ class TestRichCLIPrompt:
],
"tool_call_id": "tc_1",
}
with patch(self._PT_PROMPT, side_effect=["B"]):
mock_select = MagicMock()
mock_select.return_value.ask.return_value = "ImageNet"
with patch("questionary.select", mock_select):
result = _resolve_ask_user_prompt(data)
assert result["status"] == "answered"
assert result["answers"] == ["ImageNet"]
def test_multiple_choice_other_option(self):
from unittest.mock import MagicMock
from EvoScientist.stream.display import _resolve_ask_user_prompt
data = {
"questions": [
{
"question": "Which?",
"type": "multiple_choice",
"choices": [{"value": "CIFAR-10"}, {"value": "ImageNet"}],
}
],
"tool_call_id": "tc_1",
}
mock_select = MagicMock()
mock_select.return_value.ask.return_value = "Other (type your answer)"
mock_text = MagicMock()
mock_text.return_value.ask.return_value = "custom dataset"
with (
patch("questionary.select", mock_select),
patch("questionary.text", mock_text),
):
result = _resolve_ask_user_prompt(data)
assert result["status"] == "answered"
assert result["answers"] == ["custom dataset"]
# ---------------------------------------------------------------------------
# TUI widget (basic construction)
+27 -12
View File
@@ -14,6 +14,7 @@ def _restore_paths():
orig = {
"WORKSPACE_ROOT": paths.WORKSPACE_ROOT,
"RUNS_DIR": paths.RUNS_DIR,
"MEMORIES_DIR": paths.MEMORIES_DIR,
"MEMORY_DIR": paths.MEMORY_DIR,
"USER_SKILLS_DIR": paths.USER_SKILLS_DIR,
"_active_workspace": paths._active_workspace,
@@ -21,6 +22,7 @@ def _restore_paths():
yield
paths.WORKSPACE_ROOT = orig["WORKSPACE_ROOT"]
paths.RUNS_DIR = orig["RUNS_DIR"]
paths.MEMORIES_DIR = orig["MEMORIES_DIR"]
paths.MEMORY_DIR = orig["MEMORY_DIR"]
paths.USER_SKILLS_DIR = orig["USER_SKILLS_DIR"]
paths._active_workspace = orig["_active_workspace"]
@@ -29,8 +31,13 @@ def _restore_paths():
class TestSetWorkspaceRoot:
"""Tests for set_workspace_root()."""
def test_updates_derived_dirs(self, tmp_path):
def test_updates_derived_dirs(self, tmp_path, monkeypatch):
"""set_workspace_root should update WORKSPACE_ROOT and all derived dirs."""
monkeypatch.delenv("EVOSCIENTIST_MEMORIES_DIR", raising=False)
monkeypatch.delenv("EVOSCIENTIST_MEMORY_DIR", raising=False)
monkeypatch.delenv("EVOSCIENTIST_RUNS_DIR", raising=False)
monkeypatch.delenv("EVOSCIENTIST_SKILLS_DIR", raising=False)
new_root = tmp_path / "my_workspace"
new_root.mkdir()
@@ -38,7 +45,8 @@ class TestSetWorkspaceRoot:
assert paths.WORKSPACE_ROOT == new_root.resolve()
assert paths.RUNS_DIR == new_root.resolve() / "runs"
assert paths.MEMORY_DIR == new_root.resolve() / "memory"
# MEMORIES_DIR is global — not derived from workspace root
assert paths.MEMORIES_DIR == paths.GLOBAL_MEMORIES_DIR
assert paths.USER_SKILLS_DIR == new_root.resolve() / "skills"
def test_resets_active_workspace(self, tmp_path):
@@ -60,7 +68,7 @@ class TestSetWorkspaceRoot:
custom_runs = tmp_path / "custom_runs"
env = {
"EVOSCIENTIST_MEMORY_DIR": str(custom_mem),
"EVOSCIENTIST_MEMORIES_DIR": str(custom_mem),
"EVOSCIENTIST_SKILLS_DIR": str(custom_skills),
"EVOSCIENTIST_RUNS_DIR": str(custom_runs),
}
@@ -75,8 +83,8 @@ class TestSetWorkspaceRoot:
assert paths.WORKSPACE_ROOT == new_root.resolve()
assert paths._active_workspace == new_root.resolve()
# Derived dirs should reflect the env overrides, not the new root
assert paths.MEMORY_DIR == custom_mem.expanduser()
# MEMORIES_DIR respects env override
assert paths.MEMORIES_DIR == custom_mem.expanduser()
assert paths.USER_SKILLS_DIR == custom_skills.expanduser()
assert paths.RUNS_DIR == custom_runs.expanduser()
@@ -94,14 +102,21 @@ class TestEnsureDirsUsesUpdatedPaths:
"""ensure_dirs should create dirs at the currently set paths."""
def test_ensure_dirs_uses_updated_paths(self, tmp_path):
"""After set_workspace_root, ensure_dirs creates dirs at new location."""
"""ensure_dirs creates the global memories dir (not workspace-local)."""
new_root = tmp_path / "workspace"
new_root.mkdir()
paths.set_workspace_root(new_root)
paths.ensure_dirs()
custom_mem = tmp_path / "memories"
assert (new_root / "memory").is_dir()
assert not (
new_root / "skills"
).exists() # skills created on demand by install_skill()
with mock.patch.dict(
os.environ, {"EVOSCIENTIST_MEMORIES_DIR": str(custom_mem)}
):
paths.set_workspace_root(new_root)
paths.ensure_dirs()
assert custom_mem.is_dir()
assert not (new_root / "memory").exists() # no longer workspace-local
assert not (new_root / "memories").exists()
assert not (
new_root / "skills"
).exists() # skills created on demand by install_skill()
+38 -17
View File
@@ -95,38 +95,51 @@ class TestFormatToolCompact:
result = format_tool_compact("edit_file", {"path": "f.py"})
assert result == "edit_file(f.py)"
# Memory file special display
def test_read_file_memory(self):
result = format_tool_compact("read_file", {"path": "/memory/MEMORY.md"})
# Global memory file special display (/memories/ = global)
def test_read_file_global_memory(self):
result = format_tool_compact("read_file", {"path": "/memories/MEMORY.md"})
assert result == "Reading memory"
def test_read_file_memory_file_path_alias(self):
result = format_tool_compact("read_file", {"file_path": "/memory/MEMORY.md"})
def test_read_file_global_memory_file_path_alias(self):
result = format_tool_compact("read_file", {"file_path": "/memories/MEMORY.md"})
assert result == "Reading memory"
def test_read_file_any_memory_file(self):
result = format_tool_compact("read_file", {"path": "/memory/history.md"})
def test_read_file_any_global_memory_file(self):
result = format_tool_compact("read_file", {"path": "/memories/history.md"})
assert result == "Reading memory"
def test_write_file_memory(self):
def test_write_file_global_memory(self):
result = format_tool_compact("write_file", {"path": "/MEMORY.md"})
assert result == "Updating memory"
# Also covers paths with /memory/ prefix
result2 = format_tool_compact("write_file", {"path": "/memory/MEMORY.md"})
# Also covers paths with /memories/ prefix
result2 = format_tool_compact("write_file", {"path": "/memories/MEMORY.md"})
assert result2 == "Updating memory"
def test_edit_file_memory(self):
result = format_tool_compact("edit_file", {"path": "/memory/MEMORY.md"})
def test_edit_file_global_memory(self):
result = format_tool_compact("edit_file", {"path": "/memories/MEMORY.md"})
assert result == "Updating memory"
def test_write_edit_any_memory_file(self):
write_result = format_tool_compact("write_file", {"path": "/memory/soul.md"})
def test_write_edit_any_global_memory_file(self):
write_result = format_tool_compact("write_file", {"path": "/memories/soul.md"})
edit_result = format_tool_compact(
"edit_file", {"path": "/memory/skills-context.md"}
"edit_file", {"path": "/memories/skills-context.md"}
)
assert write_result == "Updating memory"
assert edit_result == "Updating memory"
# Project-local /memory/ files show normal tool display
def test_read_file_project_memory(self):
result = format_tool_compact(
"read_file", {"path": "/memory/ideation-memory.md"}
)
assert result == "read_file(/memory/ideation-memory.md)"
def test_edit_file_project_memory(self):
result = format_tool_compact(
"edit_file", {"path": "/memory/experiment-memory.md"}
)
assert result == "edit_file(/memory/experiment-memory.md)"
def test_memory_display_inferred_from_result_when_args_sparse(self):
read_result = format_tool_compact_with_result(
"read_file",
@@ -138,17 +151,25 @@ class TestFormatToolCompact:
edit_result = format_tool_compact_with_result(
"edit_file",
{},
"Successfully replaced 1 instance(s) of the string in '/memory/MEMORY.md'",
"Successfully replaced 1 instance(s) of the string in '/memories/MEMORY.md'",
)
assert edit_result == "Updating memory"
write_result = format_tool_compact_with_result(
"write_file",
{},
"Wrote updated content to '/memory/history.md'",
"Wrote updated content to '/memories/history.md'",
)
assert write_result == "Updating memory"
def test_project_memory_result_not_special(self):
result = format_tool_compact_with_result(
"write_file",
{},
"Wrote updated content to '/memory/ideation-memory.md'",
)
assert result != "Updating memory"
def test_glob(self):
result = format_tool_compact("glob", {"pattern": "*.py"})
assert result == "glob(*.py)"
+1 -1
View File
@@ -206,7 +206,7 @@ class TestToolCallWidget(unittest.TestCase):
w = ToolCallWidget("edit_file", {}, "mem-1")
w._result_content = (
"Successfully replaced 1 instance(s) of the string in '/memory/MEMORY.md'"
"Successfully replaced 1 instance(s) of the string in '/memories/MEMORY.md'"
)
class _Header: