diff --git a/EvoScientist/EvoScientist.py b/EvoScientist/EvoScientist.py
index b440141..b00b739 100644
--- a/EvoScientist/EvoScientist.py
+++ b/EvoScientist/EvoScientist.py
@@ -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,
},
)
diff --git a/EvoScientist/cli/interactive.py b/EvoScientist/cli/interactive.py
index 38feefd..f8cae4e 100644
--- a/EvoScientist/cli/interactive.py
+++ b/EvoScientist/cli/interactive.py
@@ -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
diff --git a/EvoScientist/cli/mcp_install_cmd.py b/EvoScientist/cli/mcp_install_cmd.py
index 3d4e060..a2706a9 100644
--- a/EvoScientist/cli/mcp_install_cmd.py
+++ b/EvoScientist/cli/mcp_install_cmd.py
@@ -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 ")
diff --git a/EvoScientist/cli/skills_cmd.py b/EvoScientist/cli/skills_cmd.py
index 90dcfa3..9f13602 100644
--- a/EvoScientist/cli/skills_cmd.py
+++ b/EvoScientist/cli/skills_cmd.py
@@ -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", "✓ ")
diff --git a/EvoScientist/commands/implementation/general.py b/EvoScientist/commands/implementation/general.py
index ca19270..42c6b93 100644
--- a/EvoScientist/commands/implementation/general.py
+++ b/EvoScientist/commands/implementation/general.py
@@ -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
diff --git a/EvoScientist/middleware/memory.py b/EvoScientist/middleware/memory.py
index a220891..9fe8582 100644
--- a/EvoScientist/middleware/memory.py
+++ b/EvoScientist/middleware/memory.py
@@ -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)
diff --git a/EvoScientist/paths.py b/EvoScientist/paths.py
index d7e54eb..82d3eab 100644
--- a/EvoScientist/paths.py
+++ b/EvoScientist/paths.py
@@ -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:
diff --git a/EvoScientist/stream/display.py b/EvoScientist/stream/display.py
index 56f47bb..ba9bf14 100644
--- a/EvoScientist/stream/display.py
+++ b/EvoScientist/stream/display.py
@@ -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" ")
- ).strip()
- if raw.upper() == other_letter:
- raw = pt_prompt(
- HTML(" ")
- ).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(" ")
- ).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"}
diff --git a/EvoScientist/stream/utils.py b/EvoScientist/stream/utils.py
index 526b899..5cd93d5 100644
--- a/EvoScientist/stream/utils.py
+++ b/EvoScientist/stream/utils.py
@@ -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
):
diff --git a/tests/test_ask_user.py b/tests/test_ask_user.py
index f5d1cb2..90050b2 100644
--- a/tests/test_ask_user.py
+++ b/tests/test_ask_user.py
@@ -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)
diff --git a/tests/test_paths.py b/tests/test_paths.py
index d044e62..29c0391 100644
--- a/tests/test_paths.py
+++ b/tests/test_paths.py
@@ -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()
diff --git a/tests/test_stream_utils.py b/tests/test_stream_utils.py
index e009033..16628fc 100644
--- a/tests/test_stream_utils.py
+++ b/tests/test_stream_utils.py
@@ -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)"
diff --git a/tests/test_tui_widgets.py b/tests/test_tui_widgets.py
index 15dc384..eff270a 100644
--- a/tests/test_tui_widgets.py
+++ b/tests/test_tui_widgets.py
@@ -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: