feat(memory): add workspace Dream prompt override
This commit is contained in:
+29
-6
@@ -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"])
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user