feat(memory): add workspace Dream prompt override

This commit is contained in:
chengyongru
2026-07-03 00:41:51 +08:00
committed by Xubin Ren
parent 5af22042ec
commit f38fd7d5d3
22 changed files with 295 additions and 10 deletions
+29 -6
View File
@@ -481,13 +481,39 @@ class MemoryStore:
def get_latest_cursor(self) -> int:
return max(self._next_cursor() - 1, 0)
@property
def dream_prompt_file(self) -> Path:
return self.workspace / "prompts" / "dream.md"
def has_dream_prompt_override(self) -> bool:
with suppress(OSError):
return self.dream_prompt_file.is_file() and bool(
self.dream_prompt_file.read_text(encoding="utf-8").strip()
)
return False
@staticmethod
def default_dream_prompt() -> str:
from nanobot.agent.skills import BUILTIN_SKILLS_DIR
return render_template(
"agent/dream.md",
strip=True,
skill_creator_path=str(BUILTIN_SKILLS_DIR / "skill-creator" / "SKILL.md"),
)
def _dream_template(self) -> str:
with suppress(OSError):
text = self.dream_prompt_file.read_text(encoding="utf-8")
if text.strip():
return text.rstrip()
return self.default_dream_prompt()
def build_dream_prompt(self, *, max_entries: int = 20) -> tuple[str, int] | None:
"""Build the Dream prompt with unprocessed history context.
Returns ``(prompt, last_cursor)`` or ``None`` if nothing to process.
"""
from nanobot.agent.skills import BUILTIN_SKILLS_DIR
last_cursor = self.get_last_dream_cursor()
entries = self.read_unprocessed_history(since_cursor=last_cursor)
if not entries:
@@ -498,10 +524,7 @@ class MemoryStore:
f"[{e['timestamp']}] {truncate_text(e['content'], 500)}"
for e in batch
)
skill_creator_path = str(BUILTIN_SKILLS_DIR / "skill-creator" / "SKILL.md")
template = render_template(
"agent/dream.md", strip=True, skill_creator_path=skill_creator_path,
)
template = self._dream_template()
prompt = f"{template}\n\n## Conversation History\n{history_text}"
return (prompt, batch[-1]["cursor"])
+6 -1
View File
@@ -418,6 +418,7 @@ class TelegramChannel(BaseChannel):
BotCommand("dream", "Run Dream memory consolidation now"),
BotCommand("dream_log", "Show the latest Dream memory change"),
BotCommand("dream_restore", "Restore Dream memory to an earlier version"),
BotCommand("dream_prompt", "Show or initialize the Dream prompt override"),
BotCommand("help", "Show available commands"),
]
@@ -477,6 +478,8 @@ class TelegramChannel(BaseChannel):
return content.replace("/dream_log", "/dream-log", 1)
if content == "/dream_restore" or content.startswith("/dream_restore "):
return content.replace("/dream_restore", "/dream-restore", 1)
if content == "/dream_prompt" or content.startswith("/dream_prompt "):
return content.replace("/dream_prompt", "/dream-prompt", 1)
return content
async def start(self) -> None:
@@ -523,7 +526,9 @@ class TelegramChannel(BaseChannel):
)
self._app.add_handler(
MessageHandler(
filters.Regex(r"^/(dream-log|dream_log|dream-restore|dream_restore)(?:@\w+)?(?:\s+.*)?$"),
filters.Regex(
r"^/(dream-log|dream_log|dream-restore|dream_restore|dream-prompt|dream_prompt)(?:@\w+)?(?:\s+.*)?$"
),
self._forward_command,
)
)
+61 -2
View File
@@ -106,6 +106,13 @@ BUILTIN_COMMAND_SPECS: tuple[BuiltinCommandSpec, ...] = (
"Revert memory to a previous Dream snapshot.",
"undo-2",
),
BuiltinCommandSpec(
"/dream-prompt",
"Dream memory",
"Tell Dream how to organize this workspace's memory.",
"file-text",
"[init]",
),
BuiltinCommandSpec(
"/skill",
"List skills",
@@ -408,6 +415,49 @@ async def cmd_dream(ctx: CommandContext) -> OutboundMessage:
)
async def cmd_dream_prompt(ctx: CommandContext) -> OutboundMessage:
"""Show or set up the workspace Dream memory instructions."""
store = ctx.loop.context.memory
path = store.dream_prompt_file
args = ctx.args.strip().lower()
if args == "init":
if path.exists():
content = (
f"Dream memory instructions already exist at `{path}`.\n\n"
"Edit that file, or delete/empty it to return to nanobot's default."
)
else:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(store.default_dream_prompt() + "\n", encoding="utf-8")
content = (
f"Created Dream memory instructions at `{path}`.\n\n"
"Edit that file to teach Dream how to organize memory. "
"Delete or empty it to return to nanobot's default."
)
elif args:
content = "Usage: /dream-prompt [init]"
elif store.has_dream_prompt_override():
content = (
"Dream memory instructions: custom for this workspace\n\n"
f"- Path: `{path}`\n"
"- Delete or empty this file to return to nanobot's default."
)
else:
content = (
"Dream memory instructions: nanobot default\n\n"
f"- Editable file: `{path}`\n"
"- Run `/dream-prompt init` to create an editable copy."
)
return OutboundMessage(
channel=ctx.msg.channel,
chat_id=ctx.msg.chat_id,
content=content,
metadata={**dict(ctx.msg.metadata or {}), "render_as": "text"},
)
def _format_dream_no_input_message() -> str:
return "\n".join([
"Dream has no conversation history to process yet.",
@@ -422,6 +472,7 @@ def _format_dream_no_input_message() -> str:
"- Enable `agents.defaults.idleCompactAfterMinutes` so completed chats become Dream input automatically.",
"- Compact the current chat into memory once that manual action is available.",
"- If you expected history to exist, check whether `memory/history.jsonl` has new entries after the Dream cursor.",
"- Use `/dream-prompt` to see or change how Dream organizes memory.",
])
@@ -508,7 +559,10 @@ async def cmd_dream_log(ctx: CommandContext) -> OutboundMessage:
if not git.is_initialized():
if store.get_last_dream_cursor() == 0:
msg = "Dream has not run yet. Run `/dream`, or wait for the next scheduled Dream cycle."
msg = (
"Dream has not run yet. Run `/dream`, or wait for the next scheduled Dream cycle.\n\n"
"Use `/dream-prompt` to see or change how Dream organizes memory."
)
else:
msg = "Dream history is not available because memory versioning is not initialized."
return OutboundMessage(
@@ -539,7 +593,10 @@ async def cmd_dream_log(ctx: CommandContext) -> OutboundMessage:
commit, diff = result
content = _format_dream_log_content(commit, diff)
else:
content = "Dream memory has no saved versions yet."
content = (
"Dream memory has no saved versions yet.\n\n"
"Use `/dream-prompt` to see or change how Dream organizes memory."
)
return OutboundMessage(
channel=ctx.msg.channel, chat_id=ctx.msg.chat_id,
@@ -821,6 +878,8 @@ def register_builtin_commands(router: CommandRouter) -> None:
router.prefix("/dream-log ", cmd_dream_log)
router.exact("/dream-restore", cmd_dream_restore)
router.prefix("/dream-restore ", cmd_dream_restore)
router.exact("/dream-prompt", cmd_dream_prompt)
router.prefix("/dream-prompt ", cmd_dream_prompt)
router.exact("/skill", cmd_skill)
router.exact("/help", cmd_help)
router.exact("/pairing", cmd_pairing)
+11
View File
@@ -0,0 +1,11 @@
# Dream Memory Instructions
This folder is for plain-language instructions that tell Dream how to organize memory in this workspace.
Most users do not need to edit anything here. To guide Dream differently for this workspace, run:
```text
/dream-prompt init
```
That creates `prompts/dream.md`. Edit it in plain Markdown. Delete or empty it to return to nanobot's default memory behavior.
+1
View File
@@ -741,6 +741,7 @@ def sync_workspace_templates(workspace: Path, silent: bool = False) -> list[str]
if item.name.endswith(".md") and not item.name.startswith("."):
_write(item, workspace / item.name)
_write(tpl / "memory" / "MEMORY.md", workspace / "memory" / "MEMORY.md")
_write(tpl / "prompts" / "README.md", workspace / "prompts" / "README.md")
_write(None, workspace / "memory" / "history.jsonl")
(workspace / "skills").mkdir(exist_ok=True)