diff --git a/reme/config/cli.yaml b/reme/config/cli.yaml
index 7d348d45..a6914170 100644
--- a/reme/config/cli.yaml
+++ b/reme/config/cli.yaml
@@ -8,12 +8,20 @@ metadata:
vector_weight: 0.7
candidate_multiplier: 2
-llms:
+as_llms:
default:
backend: openai
- # model_name: qwen3-235b-a22b-thinking-2507
model_name: qwen3.5-plus
- request_interval: 1
+
+as_llm_formatters:
+ default:
+ backend: openai
+
+as_token_counters:
+ default:
+ backend: hf
+ pretrained_model_name_or_path: Qwen/Qwen3-Coder-30B-A3B-Instruct
+ use_mirror: true
embedding_models:
default:
@@ -41,11 +49,3 @@ file_watchers:
recursive: false
scan_on_start: true
-token_counters:
- default:
- backend: base
-
- hf:
- backend: hf
- model_name: Qwen/Qwen3-Coder-30B-A3B-Instruct
- use_mirror: true
diff --git a/reme/core/__init__.py b/reme/core/__init__.py
index 053755cc..726a3455 100644
--- a/reme/core/__init__.py
+++ b/reme/core/__init__.py
@@ -2,6 +2,7 @@
from . import as_llm
from . import as_llm_formatter
+from . import as_token_counter
from . import embedding
from . import enumeration
from . import file_store
@@ -25,6 +26,7 @@ __all__ = [
# Submodules
"as_llm",
"as_llm_formatter",
+ "as_token_counter",
"embedding",
"enumeration",
"file_watcher",
diff --git a/reme/core/application.py b/reme/core/application.py
index 46f4a934..85a33c4d 100644
--- a/reme/core/application.py
+++ b/reme/core/application.py
@@ -172,6 +172,13 @@ class Application:
config_dict = config.model_dump(exclude={"backend"})
self.service_context.as_llm_formatters[name] = R.as_llm_formatters[config.backend](**config_dict)
+ for name, config in self.service_config.as_token_counters.items():
+ if config.backend not in R.as_token_counters:
+ logger.warning(f"Token counter backend {config.backend} is not supported.")
+ else:
+ config_dict = config.model_dump(exclude={"backend"})
+ self.service_context.as_token_counters[name] = R.as_token_counters[config.backend](**config_dict)
+
for name, config in self.service_config.llms.items():
if config.backend not in R.llms:
logger.warning(f"LLM backend {config.backend} is not supported.")
diff --git a/reme/core/as_token_counter/__init__.py b/reme/core/as_token_counter/__init__.py
new file mode 100644
index 00000000..51b3bd29
--- /dev/null
+++ b/reme/core/as_token_counter/__init__.py
@@ -0,0 +1,9 @@
+"""Module for registering AgentScope token counters."""
+
+from agentscope.token import OpenAITokenCounter
+from agentscope.token import HuggingFaceTokenCounter
+
+from ..registry_factory import R
+
+R.as_token_counters.register("openai")(OpenAITokenCounter)
+R.as_token_counters.register("hf")(HuggingFaceTokenCounter)
diff --git a/reme/core/op/base_op.py b/reme/core/op/base_op.py
index 86cfa3be..f25da289 100644
--- a/reme/core/op/base_op.py
+++ b/reme/core/op/base_op.py
@@ -9,6 +9,7 @@ from typing import Callable, Optional, Any
from agentscope.formatter import FormatterBase
from agentscope.model import ChatModelBase
+from agentscope.token import TokenCounterBase
from loguru import logger
from tqdm import tqdm
@@ -46,6 +47,7 @@ class BaseOp(metaclass=ABCMeta):
prompt_path: str = "",
as_llm: str | ChatModelBase = "default",
as_llm_formatter: str | FormatterBase = "default",
+ as_token_counter: str | TokenCounterBase = "default",
llm: str | BaseLLM = "default",
embedding_model: str | BaseEmbeddingModel = "default",
vector_store: str | BaseVectorStore = "default",
@@ -70,6 +72,7 @@ class BaseOp(metaclass=ABCMeta):
self._as_llm = as_llm
self._as_llm_formatter = as_llm_formatter
+ self._as_token_counter = as_token_counter
self._llm = llm
self._embedding_model = embedding_model
self._vector_store = vector_store
@@ -149,6 +152,13 @@ class BaseOp(metaclass=ABCMeta):
self._as_llm_formatter = self.service_context.as_llm_formatters[self._as_llm_formatter]
return self._as_llm_formatter
+ @property
+ def as_token_counter(self) -> TokenCounterBase:
+ """Get the token counter instance from ServiceContext."""
+ if isinstance(self._as_token_counter, str):
+ self._as_token_counter = self.service_context.as_token_counters[self._as_token_counter]
+ return self._as_token_counter
+
@property
def llm(self) -> BaseLLM:
"""Get the LLM instance from ServiceContext."""
diff --git a/reme/core/registry_factory.py b/reme/core/registry_factory.py
index f54ad3c1..921f27a8 100644
--- a/reme/core/registry_factory.py
+++ b/reme/core/registry_factory.py
@@ -36,6 +36,7 @@ class RegistryFactory:
self.llms = Registry()
self.as_llms = Registry()
self.as_llm_formatters = Registry()
+ self.as_token_counters = Registry()
self.embedding_models = Registry()
self.vector_stores = Registry()
self.file_stores = Registry()
diff --git a/reme/core/schema/service_config.py b/reme/core/schema/service_config.py
index 5e4cd212..758944b2 100644
--- a/reme/core/schema/service_config.py
+++ b/reme/core/schema/service_config.py
@@ -130,6 +130,7 @@ class ServiceConfig(BasicConfig):
flows: dict[str, FlowConfig] = Field(default_factory=dict)
as_llms: dict[str, BasicConfig] = Field(default_factory=dict)
as_llm_formatters: dict[str, BasicConfig] = Field(default_factory=dict)
+ as_token_counters: dict[str, BasicConfig] = Field(default_factory=dict)
llms: dict[str, LLMConfig] = Field(default_factory=dict)
embedding_models: dict[str, EmbeddingModelConfig] = Field(default_factory=dict)
vector_stores: dict[str, VectorStoreConfig] = Field(default_factory=dict)
diff --git a/reme/core/service_context.py b/reme/core/service_context.py
index 92d0d566..4ab83ff7 100644
--- a/reme/core/service_context.py
+++ b/reme/core/service_context.py
@@ -13,6 +13,7 @@ from .utils import load_env, PydanticConfigParser
if TYPE_CHECKING:
from agentscope.model import ChatModelBase
from agentscope.formatter import FormatterBase
+ from agentscope.token import TokenCounterBase
from .llm import BaseLLM
from .embedding import BaseEmbeddingModel
from .vector_store import BaseVectorStore
@@ -40,6 +41,7 @@ class ServiceContext(BaseDict):
log_to_console: bool = True,
default_as_llm_config: dict | None = None,
default_as_llm_formatter_config: dict | None = None,
+ default_as_token_counter_config: dict | None = None,
default_llm_config: dict | None = None,
default_embedding_model_config: dict | None = None,
default_vector_store_config: dict | None = None,
@@ -72,6 +74,8 @@ class ServiceContext(BaseDict):
self._update_section_config(kwargs, "as_llms", **default_as_llm_config)
if default_as_llm_formatter_config:
self._update_section_config(kwargs, "as_llm_formatters", **default_as_llm_formatter_config)
+ if default_as_token_counter_config:
+ self._update_section_config(kwargs, "as_token_counters", **default_as_token_counter_config)
if default_llm_config:
self._update_section_config(kwargs, "llms", **default_llm_config)
if default_embedding_model_config:
@@ -100,6 +104,7 @@ class ServiceContext(BaseDict):
self.thread_pool: ThreadPoolExecutor | None = None
self.as_llms: dict[str, "ChatModelBase"] = {}
self.as_llm_formatters: dict[str, "FormatterBase"] = {}
+ self.as_token_counters: dict[str, "TokenCounterBase"] = {}
self.llms: dict[str, "BaseLLM"] = {}
self.embedding_models: dict[str, "BaseEmbeddingModel"] = {}
self.token_counters: dict[str, "BaseTokenCounter"] = {}
diff --git a/reme/core/utils/llm_utils.py b/reme/core/utils/llm_utils.py
index 6ec2cd23..e828c24e 100644
--- a/reme/core/utils/llm_utils.py
+++ b/reme/core/utils/llm_utils.py
@@ -3,10 +3,60 @@
import json
import re
+from agentscope.message import Msg
from loguru import logger
from ..enumeration import Role
-from ..schema import Message, Trajectory, MemoryNode
+from ..schema import Message, Trajectory, MemoryNode, ToolCall
+
+
+def convert_as_msg_to_message(msg) -> Message:
+ """Convert an agentscope Msg object to the project's Message type."""
+ role_str = getattr(msg, "role", "user")
+ role = (
+ Role(role_str.lower())
+ if isinstance(role_str, str) and role_str.lower() in [r.value for r in Role]
+ else Role.USER
+ )
+
+ content_blocks = msg.get_content_blocks()
+ content = ""
+ reasoning_content = ""
+ tool_calls = []
+ tool_call_id = ""
+
+ for block in content_blocks:
+ block_type = block["type"]
+ if block_type == "thinking":
+ reasoning_content = block["thinking"]
+ elif block_type == "tool_use":
+ try:
+ tool_calls.append(
+ ToolCall(
+ id=block["id"],
+ name=block["name"],
+ arguments=json.dumps(block["input"], ensure_ascii=False),
+ ),
+ )
+ except (json.JSONDecodeError, TypeError):
+ pass
+ elif block_type == "tool_result":
+ role = Role.TOOL
+ tool_call_id = block["id"]
+ content = block["output"][0]["text"]
+ else:
+ content = block[block_type]
+
+ return Message(
+ name=getattr(msg, "name", None),
+ role=role,
+ content=content,
+ reasoning_content=reasoning_content,
+ tool_calls=tool_calls,
+ tool_call_id=tool_call_id,
+ time_created=getattr(msg, "timestamp", "") or "",
+ metadata=getattr(msg, "metadata", {}) or {},
+ )
def format_messages(
@@ -24,6 +74,8 @@ def format_messages(
for i, message in enumerate(messages):
if isinstance(message, dict):
message = Message(**message)
+ if isinstance(message, Msg):
+ message = convert_as_msg_to_message(message)
if not enable_system and message.role is Role.SYSTEM:
continue
diff --git a/reme/core/utils/std_logger.py b/reme/core/utils/std_logger.py
index dbdf1908..bfa6b32a 100644
--- a/reme/core/utils/std_logger.py
+++ b/reme/core/utils/std_logger.py
@@ -47,6 +47,7 @@ def get_loggerv2(
log_file_prefix: str = "reme",
rotation: str = "midnight",
retention_days: int = 7,
+ force_update: bool = False,
) -> logging.Logger:
"""Get a configured logger instance.
@@ -59,12 +60,13 @@ def get_loggerv2(
log_file_prefix: Prefix for log file names (e.g., 'reme' -> 'reme_2024-01-01.log').
rotation: Log rotation time, defaults to midnight.
retention_days: Number of days to retain log files.
+ force_update: Whether to force update the logger configuration even if it already exists.
Returns:
Configured Logger instance.
"""
- # Return existing logger if already created
- if name in _loggers:
+ # Return existing logger if already created and not force updating
+ if name in _loggers and not force_update:
return _loggers[name]
# Create new logger without using root logger
diff --git a/reme/memory/file_based/components/__init__.py b/reme/memory/file_based/components/__init__.py
index 42ab2f6b..86574790 100644
--- a/reme/memory/file_based/components/__init__.py
+++ b/reme/memory/file_based/components/__init__.py
@@ -4,10 +4,12 @@ from .compactor import Compactor
from .context_checker import ContextChecker
from .summarizer import Summarizer
from .tool_result_compactor import ToolResultCompactor
+from .cli import CliAgent
__all__ = [
"Compactor",
"Summarizer",
"ContextChecker",
"ToolResultCompactor",
+ "CliAgent",
]
diff --git a/reme/memory/file_based/components/cli.py b/reme/memory/file_based/components/cli.py
new file mode 100644
index 00000000..bc88bd56
--- /dev/null
+++ b/reme/memory/file_based/components/cli.py
@@ -0,0 +1,303 @@
+"""CLI component for interactive chat using agentscope-based memory tools."""
+
+import asyncio
+from datetime import datetime
+from pathlib import Path
+
+from agentscope.agent import ReActAgent
+from agentscope.message import Msg, TextBlock
+from agentscope.tool import Toolkit, ToolResponse
+from agentscope.pipeline import stream_printing_messages
+from loguru import logger
+
+from ....core.op import BaseOp
+from ....core.utils import format_messages
+from .compactor import Compactor
+from .context_checker import ContextChecker
+from .summarizer import Summarizer
+from ..tools import FileIO, MemorySearch
+
+
+class CliAgent(BaseOp):
+ """CLI agent for interactive chat with memory management."""
+
+ def __init__(
+ self,
+ working_dir: str,
+ vector_weight: float = 0.7,
+ candidate_multiplier: float = 3.0,
+ context_window_tokens: int = 128000,
+ reserve_tokens: int = 36000,
+ keep_recent_tokens: int = 20000,
+ language: str = "zh",
+ **kwargs,
+ ):
+ super().__init__(**kwargs)
+ self.working_dir: str = working_dir
+ Path(self.working_dir).mkdir(parents=True, exist_ok=True)
+ self.vector_weight: float = vector_weight
+ self.candidate_multiplier: float = candidate_multiplier
+ self.context_window_tokens: int = context_window_tokens
+ self.reserve_tokens: int = reserve_tokens
+ self.keep_recent_tokens: int = keep_recent_tokens
+ self.language: str = language
+
+ # Initialize message history
+ self.messages: list[Msg] = []
+ self.previous_summary: str = ""
+ self.summary_tasks: list[asyncio.Task] = []
+
+ def add_summary_task(self, messages: list[Msg]):
+ """Add summary task to queue."""
+ remaining_tasks = []
+ for task in self.summary_tasks:
+ if task.done():
+ exc = task.exception()
+ if exc is not None:
+ logger.exception(f"Summary task failed: {exc}")
+ else:
+ result = task.result()
+ logger.info(f"Summary task completed: {result}")
+ else:
+ remaining_tasks.append(task)
+ self.summary_tasks = remaining_tasks
+
+ # Create a toolkit for the summarizer
+ toolkit = self._create_file_toolkit()
+
+ # Create summarizer instance
+ memory_path = Path(self.working_dir) / "memory"
+ summarizer = Summarizer(
+ working_dir=self.working_dir,
+ memory_dir=str(memory_path),
+ memory_compact_threshold=int(self.context_window_tokens * 0.7),
+ token_counter=self.as_token_counter,
+ toolkit=toolkit,
+ as_llm=self.as_llm,
+ as_llm_formatter=self.as_llm_formatter,
+ language=self.language if self.language == "zh" else "",
+ console_enabled=False, # We disable the terminal printing to avoid messy outputs
+ )
+
+ # Create summary task
+ summary_task = asyncio.create_task(
+ summarizer.call(
+ messages=messages,
+ service_context=self.service_context,
+ ),
+ )
+ self.summary_tasks.append(summary_task)
+
+ def _create_file_toolkit(self):
+ """Create a toolkit with file operations."""
+
+ toolkit = Toolkit()
+ file_io = FileIO(working_dir=self.working_dir)
+ toolkit.register_tool_function(file_io.read)
+ toolkit.register_tool_function(file_io.write)
+ toolkit.register_tool_function(file_io.edit)
+
+ return toolkit
+
+ async def new(self) -> str:
+ """Reset conversation history using summary."""
+ if not self.messages:
+ self.messages.clear()
+ self.previous_summary = ""
+ return "No history to reset."
+
+ self.add_summary_task(self.messages)
+
+ self.messages.clear()
+ self.previous_summary = ""
+ return "History saved to memory files and reset."
+
+ async def context_check(self) -> dict:
+ """Check if messages exceed token limits."""
+ # Create context checker
+ checker = ContextChecker(
+ memory_compact_threshold=self.context_window_tokens - self.reserve_tokens,
+ memory_compact_reserve=self.reserve_tokens,
+ token_counter=self.as_token_counter,
+ )
+
+ return await checker.call(
+ messages=self.messages,
+ service_context=self.service_context,
+ )
+
+ async def compact(self, force_compact: bool = False) -> str:
+ """Compact history then reset."""
+ if not self.messages:
+ return "No history to compact."
+
+ # Check and find cut point
+ messages_to_compact, messages_to_keep, _ = await self.context_check()
+ tokens_before = len(self.messages)
+
+ if force_compact:
+ messages_to_summarize = self.messages
+ left_messages = []
+ elif not messages_to_compact:
+ return "History is within token limits, no compaction needed."
+ else:
+ messages_to_summarize = messages_to_compact
+ left_messages = messages_to_keep
+
+ # Create compactor
+ compactor = Compactor(
+ memory_compact_threshold=self.context_window_tokens - self.reserve_tokens,
+ token_counter=self.as_token_counter,
+ as_llm=self.as_llm,
+ as_llm_formatter=self.as_llm_formatter,
+ language=self.language if self.language == "zh" else "",
+ console_enabled=False, # We disable the terminal printing to avoid messy outputs
+ )
+
+ summary_content = await compactor.call(
+ messages=messages_to_summarize,
+ previous_summary=self.previous_summary,
+ service_context=self.service_context,
+ )
+
+ self.add_summary_task(messages=messages_to_summarize)
+
+ # Assemble final messages
+ self.messages = left_messages
+ self.previous_summary = summary_content
+
+ return f"History compacted from {tokens_before} messages."
+
+ def format_history(self) -> str:
+ """Format history messages."""
+ return format_messages(
+ messages=self.messages,
+ add_index=False,
+ add_reasoning=False,
+ strip_markdown_headers=False,
+ )
+
+ async def _build_messages(self, query: str) -> list[Msg]:
+ """Build system prompt message."""
+ current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S %A")
+
+ # Create system prompt
+ system_prompt = self.prompt_format(
+ "system_prompt",
+ workspace_dir=self.working_dir,
+ current_time=current_time,
+ has_previous_summary=bool(self.previous_summary),
+ previous_summary=self.previous_summary or "",
+ )
+
+ logger.info(f"[{self.__class__.__name__}] system_prompt: {system_prompt}")
+
+ # Build message list
+ messages = [Msg(name="system", role="system", content=system_prompt)]
+ messages.extend(self.messages)
+ messages.append(Msg(name="user", role="user", content=query))
+
+ return messages
+
+ async def memory_search(self, query: str, max_results: int = 5, min_score: float = 0.1) -> str:
+ """
+ Mandatory recall step: semantically search MEMORY.md + memory/*.md (and optional session transcripts)
+ before answering questions about prior work, decisions, dates, people, preferences, or todos;
+ returns top snippets with path + lines.
+
+ Args:
+ query: The semantic search query to find relevant memory snippets
+ max_results: Maximum number of search results to return (optional), default is 5
+ min_score: Minimum similarity score threshold for results (optional), default is 0.1
+
+ Returns:
+ Search results as formatted string
+ """
+ search_tool = MemorySearch(
+ vector_weight=self.vector_weight,
+ candidate_multiplier=self.candidate_multiplier,
+ )
+ search_result = await search_tool.call(
+ query=query,
+ max_results=max_results,
+ min_score=min_score,
+ service_context=self.service_context,
+ )
+ return ToolResponse(
+ content=[
+ TextBlock(
+ type="text",
+ text=search_result,
+ ),
+ ],
+ )
+
+ async def execute(self):
+ """Execute the agent."""
+ _ = await self.compact(force_compact=False)
+
+ # Build messages for the agent
+ query = self.context.query
+ messages = await self._build_messages(query)
+
+ toolkit = self._create_file_toolkit()
+ # Register memory search tool
+ toolkit.register_tool_function(self.memory_search)
+
+ # Create the ReAct agent
+ agent = ReActAgent(
+ name="reme_cli_agent",
+ model=self.as_llm,
+ sys_prompt=messages[0].content, # System prompt
+ formatter=self.as_llm_formatter,
+ toolkit=toolkit,
+ )
+
+ # We disable the terminal printing to avoid messy outputs
+ agent.set_console_output_enabled(False)
+
+ self.messages = messages[1:] # remove the first SYSTEM message
+
+ # Stream processing state
+ in_thinking = False
+ in_answer = False
+
+ # obtain the printing messages from the agent in a streaming way
+ last_text_content = ""
+ last_think_content = ""
+ async for msg, last in stream_printing_messages(
+ agents=[agent],
+ coroutine_task=agent(self.messages),
+ ):
+ # print(msg, last)
+ content_blocks = msg.get_content_blocks()
+ for block in content_blocks:
+ if block["type"] == "thinking":
+ if not in_thinking and len(block["thinking"]) > len(last_think_content):
+ print("\033[90m\nThinking: ", end="", flush=True)
+ in_thinking = True
+ print(block["thinking"][len(last_think_content) :], end="", flush=True)
+ last_think_content = block["thinking"]
+ elif block["type"] == "text":
+ if in_thinking:
+ print("\033[0m") # reset color after thinking
+ in_thinking = False
+ if not in_answer:
+ print("\nRemy: ", end="", flush=True)
+ in_answer = True
+ print(block["text"][len(last_text_content) :], end="", flush=True)
+ last_text_content = block["text"]
+ elif block["type"] == "tool_use":
+ if in_thinking:
+ print("\033[0m") # reset color after thinking
+ in_thinking = False
+ if last:
+ print(f"\033[36m -> Executing Tool: name={block['name']}, input={block['input']}\033[0m")
+ elif block["type"] == "tool_result":
+ if last:
+ last_think_content = "" # reset for further thinking
+ print(f"\033[36m -> Tool Result for `{block['name']}`: {block['output'][0]['text']}\033[0m")
+ else:
+ print(f"Unknown block type: {block['type']}")
+ if last:
+ self.messages.append(msg)
diff --git a/reme/memory/file_based/components/cli.yaml b/reme/memory/file_based/components/cli.yaml
new file mode 100644
index 00000000..29dbfb25
--- /dev/null
+++ b/reme/memory/file_based/components/cli.yaml
@@ -0,0 +1,95 @@
+system_prompt: |
+ You are a personal assistant named Remy.
+
+ ## Working Directory
+ {workspace_dir}
+
+ ## Current Time
+ {current_time}
+
+ ## Tools
+ - `read` Read file contents
+ - `write` Write file contents
+ - `edit` Edit file contents
+ - `memory_search` Search your memories via vector store
+
+ **Don't give up easily** — if a tool doesn't return what you expect, try a different angle or approach.
+
+ ## Memory System
+ You are spun up fresh at the start of every session. These files are how you maintain continuity:
+ - **Long-term memory:** `MEMORY.md` — when you pick up a lesson or catch yourself making a mistake, feel free to **read, edit, and update** MEMORY.md
+ - **Daily notes:** `memory/YYYY-MM-DD.md` — jot things down often. When the user says "remember this," or whenever you feel something is worth noting or adding as a todo, feel free to **read, edit, and update** `memory/YYYY-MM-DD.md`
+ - **Read before you write** — always use `read` to check existing content before updating with `edit` or `write`
+
+ ### Memory Retrieval
+ 1. Start with `memory_search` — if nothing comes up, try rephrasing from a different angle
+ 2. To review a specific daily note (`memory/YYYY-MM-DD.md`), use `read`
+
+ ## Response Style 😊
+ - Keep it short and natural — talk like a friend, not a manual
+ - Use emoji sparingly for warmth — no more than 1–2 per reply
+ - For quick confirmations (yes/no, got it), an emoji is fine (👍, ✅, 🤔)
+ - When explaining or performing actions, lead with substance over flair
+
+ ## 🛡️ Safety
+ - Never run destructive commands without asking first
+ - Prefer `trash` over `rm` — recoverable beats permanent
+ - When in doubt, ask
+
+ ## Continuous Improvement
+ This is just a starting point. When you spot useful patterns or lessons during your conversations, note them in `MEMORY.md`. Do not modify system-level config files.
+
+ [has_previous_summary]## Previous Conversation Summary
+ [has_previous_summary]
+ [has_previous_summary]{previous_summary}
+ [has_previous_summary]
+ [has_previous_summary]
+ [has_previous_summary]The above is a summary of our earlier conversation. Use it as context to maintain continuity.
+
+system_prompt_zh: |
+ 你是一个名叫 Remy 的个人助手。
+
+ ## 工作目录
+ {workspace_dir}
+
+ ## 当前时间
+ {current_time}
+
+ ## 工具集合
+ - `read` 读取文件内容
+ - `write` 写入文件内容
+ - `edit` 编辑文件内容
+ - `memory_search` 通过向量库检索你的记忆
+
+ **不要轻易放弃**:如果工具执行结果不符合预期,可以从不同的维度进行不同的尝试。
+
+ ## 记忆系统
+ 每次新会话开始时,你都会被重新唤醒。以下文件是你保持连续性的关键:
+ - **长期记忆:** `MEMORY.md`:当你学到经验,或者当你犯了错误,可以**自由地阅读、编辑和更新** MEMORY.md
+ - **每日笔记:** `memory/YYYY-MM-DD.md`:要勤记笔记,当用户说"记住这个",或者你觉得要记笔记/todo,可以**自由地阅读、编辑和更新** `memory/YYYY-MM-DD.md`
+ - **写入前先读取** — 务必先用 `read` 读取已有内容,再用 `edit` 或 `write` 更新文件
+
+ ### 记忆检索策略
+ 1. 优先使用`memory_search`检索记忆,没有搜索结果可以从不同角度多次尝试
+ 2. 如果你需要阅读每日笔记 `memory/YYYY-MM-DD.md`,可以使用`read`
+
+ ## 回应风格 😊
+ - 保持简洁自然,像朋友对话一样
+ - 适当使用 emoji 增加亲和力,但不要过度 — 每条回复最多 1-2 个
+ - 简单确认类场景(是/否、收到)可以用 emoji 快速回应(👍, ✅, 🤔)
+ - 涉及操作或解释时,优先给出有实质内容的文字回复
+
+ ## 🛡️ 安全规则
+ - 不要在没有询问的情况下运行破坏性命令
+ - 优先使用 `trash` 而不是 `rm`(可恢复比永久删除更好)
+ - 有疑问时,先询问
+
+ ## 持续改进
+ 这只是一个起点。当你在与用户的交互中发现有用的经验或模式,可以记录到 `MEMORY.md` 中。但不要修改系统级配置文件。
+
+ [has_previous_summary]## 之前的对话摘要
+ [has_previous_summary]
+ [has_previous_summary]{previous_summary}
+ [has_previous_summary]
+ [has_previous_summary]
+ [has_previous_summary]以上是我们之前对话的摘要。使用它作为上下文以保持连续性。
diff --git a/reme/memory/file_based/components/compactor.py b/reme/memory/file_based/components/compactor.py
index e7b37b30..fbe505ad 100644
--- a/reme/memory/file_based/components/compactor.py
+++ b/reme/memory/file_based/components/compactor.py
@@ -3,12 +3,10 @@
from agentscope.agent import ReActAgent
from agentscope.message import Msg
from agentscope.token import HuggingFaceTokenCounter
+from loguru import logger
from ..utils import AsMsgHandler
from ....core.op import BaseOp
-from ....core.utils import get_std_logger
-
-logger = get_std_logger()
class Compactor(BaseOp):
@@ -18,12 +16,14 @@ class Compactor(BaseOp):
self,
memory_compact_threshold: int,
token_counter: HuggingFaceTokenCounter,
+ console_enabled: bool = True,
**kwargs,
):
super().__init__(**kwargs)
self.memory_compact_threshold: int = memory_compact_threshold
self.msg_handler = AsMsgHandler(token_counter=token_counter)
+ self.console_enabled: bool = console_enabled
async def execute(self):
messages: list[Msg] = self.context.get("messages", [])
@@ -50,6 +50,7 @@ class Compactor(BaseOp):
sys_prompt=self.get_prompt("system_prompt"),
formatter=self.as_llm_formatter,
)
+ agent.set_console_output_enabled(self.console_enabled)
if previous_summary:
prefix: str = self.get_prompt("update_user_message_prefix")
diff --git a/reme/memory/file_based/components/summarizer.py b/reme/memory/file_based/components/summarizer.py
index d4e057be..83441149 100644
--- a/reme/memory/file_based/components/summarizer.py
+++ b/reme/memory/file_based/components/summarizer.py
@@ -6,12 +6,10 @@ from agentscope.agent import ReActAgent
from agentscope.message import Msg
from agentscope.token import HuggingFaceTokenCounter
from agentscope.tool import Toolkit
+from loguru import logger
from ..utils import AsMsgHandler
from ....core.op import BaseOp
-from ....core.utils import get_std_logger
-
-logger = get_std_logger()
class Summarizer(BaseOp):
@@ -24,6 +22,7 @@ class Summarizer(BaseOp):
memory_compact_threshold: int,
token_counter: HuggingFaceTokenCounter,
toolkit: Toolkit,
+ console_enabled: bool = True,
**kwargs,
):
super().__init__(**kwargs)
@@ -33,6 +32,7 @@ class Summarizer(BaseOp):
self.msg_handler = AsMsgHandler(token_counter=token_counter)
self.toolkit: Toolkit = toolkit
+ self.console_enabled: bool = console_enabled
async def execute(self):
messages: list[Msg] = self.context.get("messages", [])
@@ -59,6 +59,7 @@ class Summarizer(BaseOp):
formatter=self.as_llm_formatter,
toolkit=self.toolkit,
)
+ agent.set_console_output_enabled(self.console_enabled)
user_message: str = f"\n{history_formatted_str}\n\n" + self.prompt_format(
"user_message",
diff --git a/reme/reme_cli.py b/reme/reme_cli.py
new file mode 100644
index 00000000..5d086532
--- /dev/null
+++ b/reme/reme_cli.py
@@ -0,0 +1,192 @@
+"""ReMe File System"""
+
+import asyncio
+import sys
+from pathlib import Path
+
+from prompt_toolkit import PromptSession
+
+from .config import ReMeConfigParser
+from .core import Application
+
+from .core.utils import play_horse_easter_egg
+from .memory.file_based.components import CliAgent
+
+
+class ReMeCli(Application):
+ """ReMe Cli"""
+
+ def __init__(
+ self,
+ *args,
+ working_dir: str = ".reme",
+ config_path: str = "cli",
+ enable_logo: bool = True,
+ log_to_console: bool = True,
+ llm_api_key: str | None = None,
+ llm_base_url: str | None = None,
+ embedding_api_key: str | None = None,
+ embedding_base_url: str | None = None,
+ default_as_llm_config: dict | None = None,
+ default_embedding_model_config: dict | None = None,
+ default_file_store_config: dict | None = None,
+ default_token_counter_config: dict | None = None,
+ default_file_watcher_config: dict | None = None,
+ context_window_tokens: int = 128000,
+ reserve_tokens: int = 36000,
+ keep_recent_tokens: int = 20000,
+ vector_weight: float = 0.7,
+ candidate_multiplier: float = 3.0,
+ **kwargs,
+ ):
+ """Initialize ReMe with config."""
+ working_path = Path(working_dir)
+ working_path.mkdir(parents=True, exist_ok=True)
+ memory_path = working_path / "memory"
+ memory_path.mkdir(parents=True, exist_ok=True)
+ self.working_dir: str = str(working_path.absolute())
+
+ default_file_watcher_config = default_file_watcher_config or {}
+ if not default_file_watcher_config.get("watch_paths", None):
+ default_file_watcher_config["watch_paths"] = [
+ str(working_path / "MEMORY.md"),
+ str(working_path / "memory.md"),
+ str(memory_path),
+ ]
+ super().__init__(
+ *args,
+ llm_api_key=llm_api_key,
+ llm_base_url=llm_base_url,
+ embedding_api_key=embedding_api_key,
+ embedding_base_url=embedding_base_url,
+ working_dir=working_dir,
+ config_path=config_path,
+ enable_logo=enable_logo,
+ log_to_console=log_to_console,
+ parser=ReMeConfigParser,
+ default_as_llm_config=default_as_llm_config,
+ default_embedding_model_config=default_embedding_model_config,
+ default_file_store_config=default_file_store_config,
+ default_token_counter_config=default_token_counter_config,
+ default_file_watcher_config=default_file_watcher_config,
+ **kwargs,
+ )
+
+ self.service_config.metadata.setdefault("context_window_tokens", context_window_tokens)
+ self.service_config.metadata.setdefault("reserve_tokens", reserve_tokens)
+ self.service_config.metadata.setdefault("keep_recent_tokens", keep_recent_tokens)
+ self.service_config.metadata.setdefault("vector_weight", vector_weight)
+ self.service_config.metadata.setdefault("candidate_multiplier", candidate_multiplier)
+
+ self.commands = {
+ "/new": "Create a new conversation.",
+ "/compact": "Compact messages into a summary.",
+ "/exit": "Exit the application.",
+ "/clear": "Clear the history.",
+ "/help": "Show help.",
+ "/horse": "A surprise.",
+ }
+
+ async def chat_with_remy(self, **kwargs):
+ """Interactive CLI chat with Remy using simple streaming output."""
+ language = self.service_config.language
+ print(f"ReMe language={language or 'default'}")
+
+ cli_agent = CliAgent(
+ vector_weight=self.service_config.metadata["vector_weight"],
+ candidate_multiplier=self.service_config.metadata["candidate_multiplier"],
+ context_window_tokens=self.service_config.metadata["context_window_tokens"],
+ reserve_tokens=self.service_config.metadata["reserve_tokens"],
+ keep_recent_tokens=self.service_config.metadata["keep_recent_tokens"],
+ working_dir=self.working_dir,
+ language=language,
+ **kwargs,
+ )
+ session = PromptSession()
+
+ # Print welcome banner
+ print("\n========================================")
+ print(" Welcome to Remy Chat!")
+ print("========================================\n")
+
+ while True:
+ try:
+ # Get user input (async)
+ user_input = await session.prompt_async("You: ")
+ user_input = user_input.strip()
+ if not user_input:
+ continue
+
+ # Handle commands
+ if user_input == "/exit":
+ break
+
+ if user_input == "/new":
+ result = await cli_agent.new()
+ print(f"{result}\nConversation reset\n")
+ continue
+
+ if user_input == "/compact":
+ result = await cli_agent.compact(force_compact=True)
+ print(f"{result}\nHistory compacted.\n")
+ continue
+
+ if user_input == "/history":
+ result = cli_agent.format_history()
+ print(f"Formated History:\n{result}\n")
+ continue
+
+ if user_input == "/clear":
+ cli_agent.messages.clear()
+ print("History cleared.\n")
+ continue
+
+ if user_input == "/help":
+ print("\nCommands:")
+ for command, description in self.commands.items():
+ print(f" {command}: {description}")
+ continue
+
+ if user_input == "/horse":
+ play_horse_easter_egg()
+ continue
+
+ try:
+ await cli_agent.call(
+ query=user_input,
+ service_context=self.service_context,
+ )
+ except Exception as e:
+ print(f"\nStream error: {e}")
+
+ # End current streaming line
+ print("\n")
+ print("----------------------------------------\n")
+
+ except EOFError:
+ break
+ except KeyboardInterrupt:
+ print("\nInterrupted.")
+ break
+ except Exception as e:
+ print(f"Error: {e}")
+ import traceback
+
+ traceback.print_exc()
+
+ print("\nGoodbye!\n")
+
+
+async def async_main():
+ """Main function for testing the ReMeFs CLI."""
+ async with ReMeCli(*sys.argv[1:], log_to_console=False) as reme:
+ await reme.chat_with_remy()
+
+
+def main():
+ """Main function for testing the ReMeFs CLI."""
+ asyncio.run(async_main())
+
+
+if __name__ == "__main__":
+ main()