feat(memory): add workspace Dream prompt override
This commit is contained in:
@@ -15,6 +15,8 @@ These commands work inside chat channels and interactive agent sessions:
|
|||||||
| `/dream-log <sha>` | Show a specific Dream memory change |
|
| `/dream-log <sha>` | Show a specific Dream memory change |
|
||||||
| `/dream-restore` | List recent Dream memory versions |
|
| `/dream-restore` | List recent Dream memory versions |
|
||||||
| `/dream-restore <sha>` | Restore memory to the state before a specific change |
|
| `/dream-restore <sha>` | Restore memory to the state before a specific change |
|
||||||
|
| `/dream-prompt` | Show how Dream is being guided for memory |
|
||||||
|
| `/dream-prompt init` | Create an editable Dream memory guide at `prompts/dream.md` |
|
||||||
| `/skill` | List enabled skills and their descriptions |
|
| `/skill` | List enabled skills and their descriptions |
|
||||||
| `/trigger` | Show local trigger usage |
|
| `/trigger` | Show local trigger usage |
|
||||||
| `/trigger <name>` | Create a named local trigger for the current chat/session |
|
| `/trigger <name>` | Create a named local trigger for the current chat/session |
|
||||||
|
|||||||
@@ -64,6 +64,9 @@ This is why nanobot's memory is not just archival. It is interpretive.
|
|||||||
workspace/
|
workspace/
|
||||||
├── SOUL.md # The bot's long-term voice and communication style
|
├── SOUL.md # The bot's long-term voice and communication style
|
||||||
├── USER.md # Stable knowledge about the user
|
├── USER.md # Stable knowledge about the user
|
||||||
|
├── prompts/
|
||||||
|
│ ├── README.md # Notes for memory guidance files
|
||||||
|
│ └── dream.md # Optional instructions for how Dream organizes memory
|
||||||
└── memory/
|
└── memory/
|
||||||
├── MEMORY.md # Project facts, decisions, and durable context
|
├── MEMORY.md # Project facts, decisions, and durable context
|
||||||
├── history.jsonl # Append-only history summaries
|
├── history.jsonl # Append-only history summaries
|
||||||
@@ -120,6 +123,8 @@ Memory is not hidden behind the curtain. Users can inspect and guide it.
|
|||||||
| `/dream-log <sha>` | Show a specific Dream change |
|
| `/dream-log <sha>` | Show a specific Dream change |
|
||||||
| `/dream-restore` | List recent Dream memory versions |
|
| `/dream-restore` | List recent Dream memory versions |
|
||||||
| `/dream-restore <sha>` | Restore memory to the state before a specific change |
|
| `/dream-restore <sha>` | Restore memory to the state before a specific change |
|
||||||
|
| `/dream-prompt` | Show how Dream is being guided for memory |
|
||||||
|
| `/dream-prompt init` | Create an editable Dream memory guide at `prompts/dream.md` |
|
||||||
|
|
||||||
These commands exist for a reason: automatic memory is powerful, but users should always retain the right to inspect, understand, and restore it.
|
These commands exist for a reason: automatic memory is powerful, but users should always retain the right to inspect, understand, and restore it.
|
||||||
|
|
||||||
@@ -135,6 +140,28 @@ This gives memory a history of its own:
|
|||||||
|
|
||||||
That turns memory from a silent mutation into an auditable process.
|
That turns memory from a silent mutation into an auditable process.
|
||||||
|
|
||||||
|
## Guiding Dream
|
||||||
|
|
||||||
|
Dream decides what to keep, update, or forget using nanobot's built-in memory instructions. Most users can leave this alone.
|
||||||
|
|
||||||
|
If one workspace needs a different memory style, create an editable guide:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/dream-prompt init
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates:
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace/prompts/dream.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit that file in plain Markdown. When it has content, Dream follows it for this workspace before reading the latest conversation history. You do not need to paste history into the file; Dream adds the current `## Conversation History` block automatically.
|
||||||
|
|
||||||
|
To return to nanobot's default behavior, delete `prompts/dream.md` or leave it empty.
|
||||||
|
|
||||||
|
Each workspace has its own guide. Changing this file does not affect other nanobot workspaces.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Dream is configured under `agents.defaults.dream`:
|
Dream is configured under `agents.defaults.dream`:
|
||||||
|
|||||||
+29
-6
@@ -481,13 +481,39 @@ class MemoryStore:
|
|||||||
def get_latest_cursor(self) -> int:
|
def get_latest_cursor(self) -> int:
|
||||||
return max(self._next_cursor() - 1, 0)
|
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:
|
def build_dream_prompt(self, *, max_entries: int = 20) -> tuple[str, int] | None:
|
||||||
"""Build the Dream prompt with unprocessed history context.
|
"""Build the Dream prompt with unprocessed history context.
|
||||||
|
|
||||||
Returns ``(prompt, last_cursor)`` or ``None`` if nothing to process.
|
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()
|
last_cursor = self.get_last_dream_cursor()
|
||||||
entries = self.read_unprocessed_history(since_cursor=last_cursor)
|
entries = self.read_unprocessed_history(since_cursor=last_cursor)
|
||||||
if not entries:
|
if not entries:
|
||||||
@@ -498,10 +524,7 @@ class MemoryStore:
|
|||||||
f"[{e['timestamp']}] {truncate_text(e['content'], 500)}"
|
f"[{e['timestamp']}] {truncate_text(e['content'], 500)}"
|
||||||
for e in batch
|
for e in batch
|
||||||
)
|
)
|
||||||
skill_creator_path = str(BUILTIN_SKILLS_DIR / "skill-creator" / "SKILL.md")
|
template = self._dream_template()
|
||||||
template = render_template(
|
|
||||||
"agent/dream.md", strip=True, skill_creator_path=skill_creator_path,
|
|
||||||
)
|
|
||||||
prompt = f"{template}\n\n## Conversation History\n{history_text}"
|
prompt = f"{template}\n\n## Conversation History\n{history_text}"
|
||||||
return (prompt, batch[-1]["cursor"])
|
return (prompt, batch[-1]["cursor"])
|
||||||
|
|
||||||
|
|||||||
@@ -418,6 +418,7 @@ class TelegramChannel(BaseChannel):
|
|||||||
BotCommand("dream", "Run Dream memory consolidation now"),
|
BotCommand("dream", "Run Dream memory consolidation now"),
|
||||||
BotCommand("dream_log", "Show the latest Dream memory change"),
|
BotCommand("dream_log", "Show the latest Dream memory change"),
|
||||||
BotCommand("dream_restore", "Restore Dream memory to an earlier version"),
|
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"),
|
BotCommand("help", "Show available commands"),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -477,6 +478,8 @@ class TelegramChannel(BaseChannel):
|
|||||||
return content.replace("/dream_log", "/dream-log", 1)
|
return content.replace("/dream_log", "/dream-log", 1)
|
||||||
if content == "/dream_restore" or content.startswith("/dream_restore "):
|
if content == "/dream_restore" or content.startswith("/dream_restore "):
|
||||||
return content.replace("/dream_restore", "/dream-restore", 1)
|
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
|
return content
|
||||||
|
|
||||||
async def start(self) -> None:
|
async def start(self) -> None:
|
||||||
@@ -523,7 +526,9 @@ class TelegramChannel(BaseChannel):
|
|||||||
)
|
)
|
||||||
self._app.add_handler(
|
self._app.add_handler(
|
||||||
MessageHandler(
|
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,
|
self._forward_command,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -106,6 +106,13 @@ BUILTIN_COMMAND_SPECS: tuple[BuiltinCommandSpec, ...] = (
|
|||||||
"Revert memory to a previous Dream snapshot.",
|
"Revert memory to a previous Dream snapshot.",
|
||||||
"undo-2",
|
"undo-2",
|
||||||
),
|
),
|
||||||
|
BuiltinCommandSpec(
|
||||||
|
"/dream-prompt",
|
||||||
|
"Dream memory",
|
||||||
|
"Tell Dream how to organize this workspace's memory.",
|
||||||
|
"file-text",
|
||||||
|
"[init]",
|
||||||
|
),
|
||||||
BuiltinCommandSpec(
|
BuiltinCommandSpec(
|
||||||
"/skill",
|
"/skill",
|
||||||
"List skills",
|
"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:
|
def _format_dream_no_input_message() -> str:
|
||||||
return "\n".join([
|
return "\n".join([
|
||||||
"Dream has no conversation history to process yet.",
|
"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.",
|
"- Enable `agents.defaults.idleCompactAfterMinutes` so completed chats become Dream input automatically.",
|
||||||
"- Compact the current chat into memory once that manual action is available.",
|
"- 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.",
|
"- 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 not git.is_initialized():
|
||||||
if store.get_last_dream_cursor() == 0:
|
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:
|
else:
|
||||||
msg = "Dream history is not available because memory versioning is not initialized."
|
msg = "Dream history is not available because memory versioning is not initialized."
|
||||||
return OutboundMessage(
|
return OutboundMessage(
|
||||||
@@ -539,7 +593,10 @@ async def cmd_dream_log(ctx: CommandContext) -> OutboundMessage:
|
|||||||
commit, diff = result
|
commit, diff = result
|
||||||
content = _format_dream_log_content(commit, diff)
|
content = _format_dream_log_content(commit, diff)
|
||||||
else:
|
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(
|
return OutboundMessage(
|
||||||
channel=ctx.msg.channel, chat_id=ctx.msg.chat_id,
|
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.prefix("/dream-log ", cmd_dream_log)
|
||||||
router.exact("/dream-restore", cmd_dream_restore)
|
router.exact("/dream-restore", cmd_dream_restore)
|
||||||
router.prefix("/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("/skill", cmd_skill)
|
||||||
router.exact("/help", cmd_help)
|
router.exact("/help", cmd_help)
|
||||||
router.exact("/pairing", cmd_pairing)
|
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("."):
|
if item.name.endswith(".md") and not item.name.startswith("."):
|
||||||
_write(item, workspace / item.name)
|
_write(item, workspace / item.name)
|
||||||
_write(tpl / "memory" / "MEMORY.md", workspace / "memory" / "MEMORY.md")
|
_write(tpl / "memory" / "MEMORY.md", workspace / "memory" / "MEMORY.md")
|
||||||
|
_write(tpl / "prompts" / "README.md", workspace / "prompts" / "README.md")
|
||||||
_write(None, workspace / "memory" / "history.jsonl")
|
_write(None, workspace / "memory" / "history.jsonl")
|
||||||
(workspace / "skills").mkdir(exist_ok=True)
|
(workspace / "skills").mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,34 @@ class TestBuildDreamPrompt:
|
|||||||
prompt, _ = result
|
prompt, _ = result
|
||||||
assert "skill-creator" in prompt
|
assert "skill-creator" in prompt
|
||||||
|
|
||||||
|
def test_workspace_dream_prompt_overrides_default(self, store):
|
||||||
|
store.dream_prompt_file.parent.mkdir(parents=True)
|
||||||
|
store.dream_prompt_file.write_text(
|
||||||
|
"Custom Dream prompt.",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
store.append_history("keep this fact")
|
||||||
|
|
||||||
|
result = store.build_dream_prompt()
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
prompt, _ = result
|
||||||
|
assert prompt.startswith("Custom Dream prompt.")
|
||||||
|
assert "memory consolidation engine" not in prompt
|
||||||
|
assert "## Conversation History" in prompt
|
||||||
|
assert "keep this fact" in prompt
|
||||||
|
|
||||||
|
def test_empty_workspace_dream_prompt_uses_default(self, store):
|
||||||
|
store.dream_prompt_file.parent.mkdir(parents=True)
|
||||||
|
store.dream_prompt_file.write_text(" \n", encoding="utf-8")
|
||||||
|
store.append_history("test")
|
||||||
|
|
||||||
|
result = store.build_dream_prompt()
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
prompt, _ = result
|
||||||
|
assert "memory consolidation engine" in prompt
|
||||||
|
|
||||||
def test_truncates_long_entries(self, store):
|
def test_truncates_long_entries(self, store):
|
||||||
long_content = "x" * 2000
|
long_content = "x" * 2000
|
||||||
store.append_history(long_content)
|
store.append_history(long_content)
|
||||||
|
|||||||
@@ -375,6 +375,15 @@ class TestSyncWorkspaceTemplates:
|
|||||||
|
|
||||||
assert (workspace / "memory").exists() or (workspace / "skills").exists()
|
assert (workspace / "memory").exists() or (workspace / "skills").exists()
|
||||||
|
|
||||||
|
def test_creates_prompt_readme_without_dream_override(self, tmp_path):
|
||||||
|
workspace = tmp_path / "workspace"
|
||||||
|
|
||||||
|
added = sync_workspace_templates(workspace, silent=True)
|
||||||
|
|
||||||
|
assert "prompts/README.md" in {path.replace("\\", "/") for path in added}
|
||||||
|
assert (workspace / "prompts" / "README.md").exists()
|
||||||
|
assert not (workspace / "prompts" / "dream.md").exists()
|
||||||
|
|
||||||
def test_returns_list_of_added_files(self, tmp_path):
|
def test_returns_list_of_added_files(self, tmp_path):
|
||||||
"""Should return list of relative paths for added files."""
|
"""Should return list of relative paths for added files."""
|
||||||
workspace = tmp_path / "workspace"
|
workspace = tmp_path / "workspace"
|
||||||
|
|||||||
@@ -280,6 +280,7 @@ async def test_start_creates_separate_pools_with_proxy(monkeypatch) -> None:
|
|||||||
assert any(cmd.command == "dream" for cmd in app.bot.commands)
|
assert any(cmd.command == "dream" for cmd in app.bot.commands)
|
||||||
assert any(cmd.command == "dream_log" for cmd in app.bot.commands)
|
assert any(cmd.command == "dream_log" for cmd in app.bot.commands)
|
||||||
assert any(cmd.command == "dream_restore" for cmd in app.bot.commands)
|
assert any(cmd.command == "dream_restore" for cmd in app.bot.commands)
|
||||||
|
assert any(cmd.command == "dream_prompt" for cmd in app.bot.commands)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -1570,6 +1571,14 @@ async def test_forward_command_normalizes_telegram_safe_dream_aliases() -> None:
|
|||||||
assert len(handled) == 1
|
assert len(handled) == 1
|
||||||
assert handled[0]["content"] == "/dream-restore deadbeef"
|
assert handled[0]["content"] == "/dream-restore deadbeef"
|
||||||
|
|
||||||
|
handled.clear()
|
||||||
|
update = _make_telegram_update(text="/dream_prompt@nanobot_test init", reply_to_message=None)
|
||||||
|
|
||||||
|
await channel._forward_command(update, None)
|
||||||
|
|
||||||
|
assert len(handled) == 1
|
||||||
|
assert handled[0]["content"] == "/dream-prompt init"
|
||||||
|
|
||||||
|
|
||||||
def test_telegram_bus_slash_command_regex_matches_agent_loop_commands() -> None:
|
def test_telegram_bus_slash_command_regex_matches_agent_loop_commands() -> None:
|
||||||
"""Bus-routed slash commands must match the Telegram handler regex (see builtin router)."""
|
"""Bus-routed slash commands must match the Telegram handler regex (see builtin router)."""
|
||||||
@@ -1588,6 +1597,7 @@ def test_telegram_bus_slash_command_regex_matches_agent_loop_commands() -> None:
|
|||||||
assert pat.fullmatch("/trigger@nanobot_bot CI summary")
|
assert pat.fullmatch("/trigger@nanobot_bot CI summary")
|
||||||
assert pat.fullmatch("/dream-log deadbeef") is None
|
assert pat.fullmatch("/dream-log deadbeef") is None
|
||||||
assert pat.fullmatch("/dream-restore deadbeef") is None
|
assert pat.fullmatch("/dream-restore deadbeef") is None
|
||||||
|
assert pat.fullmatch("/dream-prompt init") is None
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -1608,6 +1618,7 @@ async def test_on_help_includes_restart_command() -> None:
|
|||||||
assert "/skill" in help_text
|
assert "/skill" in help_text
|
||||||
assert "/dream" in help_text
|
assert "/dream" in help_text
|
||||||
assert "/dream-log" in help_text
|
assert "/dream-log" in help_text
|
||||||
|
assert "/dream-prompt" in help_text
|
||||||
assert "/goal" in help_text
|
assert "/goal" in help_text
|
||||||
assert "/trigger" in help_text
|
assert "/trigger" in help_text
|
||||||
assert "/pairing" in help_text
|
assert "/pairing" in help_text
|
||||||
|
|||||||
@@ -5,8 +5,16 @@ from types import SimpleNamespace
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from nanobot.agent.memory import MemoryStore
|
||||||
from nanobot.bus.events import InboundMessage, OutboundMessage
|
from nanobot.bus.events import InboundMessage, OutboundMessage
|
||||||
from nanobot.command.builtin import cmd_dream, cmd_dream_log, cmd_dream_restore
|
from nanobot.command.builtin import (
|
||||||
|
build_help_text,
|
||||||
|
builtin_command_palette,
|
||||||
|
cmd_dream,
|
||||||
|
cmd_dream_log,
|
||||||
|
cmd_dream_prompt,
|
||||||
|
cmd_dream_restore,
|
||||||
|
)
|
||||||
from nanobot.command.router import CommandContext
|
from nanobot.command.router import CommandContext
|
||||||
from nanobot.utils.gitstore import CommitInfo
|
from nanobot.utils.gitstore import CommitInfo
|
||||||
|
|
||||||
@@ -94,6 +102,12 @@ def _make_dream_ctx(tmp_path) -> tuple[CommandContext, _FakeBus]:
|
|||||||
return ctx, bus
|
return ctx, bus
|
||||||
|
|
||||||
|
|
||||||
|
def _make_dream_prompt_ctx(tmp_path, raw: str = "/dream-prompt", args: str = "") -> CommandContext:
|
||||||
|
msg = InboundMessage(channel="cli", sender_id="u1", chat_id="direct", content=raw)
|
||||||
|
loop = SimpleNamespace(context=SimpleNamespace(memory=MemoryStore(tmp_path)))
|
||||||
|
return CommandContext(msg=msg, session=None, key=msg.session_key, raw=raw, args=args, loop=loop)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_dream_no_history_explains_how_to_create_input(tmp_path) -> None:
|
async def test_dream_no_history_explains_how_to_create_input(tmp_path) -> None:
|
||||||
ctx, bus = _make_dream_ctx(tmp_path)
|
ctx, bus = _make_dream_ctx(tmp_path)
|
||||||
@@ -109,6 +123,7 @@ async def test_dream_no_history_explains_how_to_create_input(tmp_path) -> None:
|
|||||||
assert "idle auto-compact" in content
|
assert "idle auto-compact" in content
|
||||||
assert "Dream cursor" in content
|
assert "Dream cursor" in content
|
||||||
assert "agents.defaults.idleCompactAfterMinutes" in content
|
assert "agents.defaults.idleCompactAfterMinutes" in content
|
||||||
|
assert "/dream-prompt" in content
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -185,6 +200,61 @@ async def test_dream_log_before_first_run_is_clear() -> None:
|
|||||||
|
|
||||||
assert "Dream has not run yet." in out.content
|
assert "Dream has not run yet." in out.content
|
||||||
assert "Run `/dream`" in out.content
|
assert "Run `/dream`" in out.content
|
||||||
|
assert "/dream-prompt" in out.content
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_dream_log_without_saved_versions_mentions_prompt_command() -> None:
|
||||||
|
git = _FakeGit(initialized=True, commits=[])
|
||||||
|
|
||||||
|
out = await cmd_dream_log(_make_ctx("/dream-log", git))
|
||||||
|
|
||||||
|
assert "Dream memory has no saved versions yet." in out.content
|
||||||
|
assert "/dream-prompt" in out.content
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_dream_prompt_reports_default_prompt(tmp_path) -> None:
|
||||||
|
out = await cmd_dream_prompt(_make_dream_prompt_ctx(tmp_path))
|
||||||
|
|
||||||
|
assert "Dream memory instructions: nanobot default" in out.content
|
||||||
|
assert "prompts" in out.content
|
||||||
|
assert "dream.md" in out.content
|
||||||
|
assert "/dream-prompt init" in out.content
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_dream_prompt_init_copies_default_prompt(tmp_path) -> None:
|
||||||
|
ctx = _make_dream_prompt_ctx(tmp_path, "/dream-prompt init", "init")
|
||||||
|
|
||||||
|
out = await cmd_dream_prompt(ctx)
|
||||||
|
|
||||||
|
prompt_file = tmp_path / "prompts" / "dream.md"
|
||||||
|
assert "Created Dream memory instructions" in out.content
|
||||||
|
assert prompt_file.read_text(encoding="utf-8") == MemoryStore.default_dream_prompt() + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_dream_prompt_init_does_not_overwrite_existing_prompt(tmp_path) -> None:
|
||||||
|
prompt_file = tmp_path / "prompts" / "dream.md"
|
||||||
|
prompt_file.parent.mkdir()
|
||||||
|
prompt_file.write_text("custom", encoding="utf-8")
|
||||||
|
ctx = _make_dream_prompt_ctx(tmp_path, "/dream-prompt init", "init")
|
||||||
|
|
||||||
|
out = await cmd_dream_prompt(ctx)
|
||||||
|
|
||||||
|
assert "already exist" in out.content
|
||||||
|
assert prompt_file.read_text(encoding="utf-8") == "custom"
|
||||||
|
|
||||||
|
|
||||||
|
def test_dream_prompt_command_in_help_and_palette() -> None:
|
||||||
|
palette = builtin_command_palette()
|
||||||
|
|
||||||
|
assert any(
|
||||||
|
item["command"] == "/dream-prompt" and item["arg_hint"] == "[init]"
|
||||||
|
for item in palette
|
||||||
|
)
|
||||||
|
assert "/dream-prompt [init]" in build_help_text()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
@@ -26,12 +26,14 @@ class TestIsDispatchableCommand:
|
|||||||
assert router.is_dispatchable_command("/dream")
|
assert router.is_dispatchable_command("/dream")
|
||||||
assert router.is_dispatchable_command("/dream-log")
|
assert router.is_dispatchable_command("/dream-log")
|
||||||
assert router.is_dispatchable_command("/dream-restore")
|
assert router.is_dispatchable_command("/dream-restore")
|
||||||
|
assert router.is_dispatchable_command("/dream-prompt")
|
||||||
assert router.is_dispatchable_command("/goal")
|
assert router.is_dispatchable_command("/goal")
|
||||||
assert router.is_dispatchable_command("/pairing")
|
assert router.is_dispatchable_command("/pairing")
|
||||||
|
|
||||||
def test_prefix_commands_match(self, router: CommandRouter) -> None:
|
def test_prefix_commands_match(self, router: CommandRouter) -> None:
|
||||||
assert router.is_dispatchable_command("/dream-log abc123")
|
assert router.is_dispatchable_command("/dream-log abc123")
|
||||||
assert router.is_dispatchable_command("/dream-restore def456")
|
assert router.is_dispatchable_command("/dream-restore def456")
|
||||||
|
assert router.is_dispatchable_command("/dream-prompt init")
|
||||||
assert router.is_dispatchable_command("/model fast")
|
assert router.is_dispatchable_command("/model fast")
|
||||||
assert router.is_dispatchable_command("/goal migrate the database")
|
assert router.is_dispatchable_command("/goal migrate the database")
|
||||||
assert router.is_dispatchable_command("/pairing list")
|
assert router.is_dispatchable_command("/pairing list")
|
||||||
|
|||||||
@@ -921,6 +921,10 @@
|
|||||||
"title": "Restore memory",
|
"title": "Restore memory",
|
||||||
"description": "Revert memory to a previous Dream snapshot."
|
"description": "Revert memory to a previous Dream snapshot."
|
||||||
},
|
},
|
||||||
|
"dream_prompt": {
|
||||||
|
"title": "Dream memory",
|
||||||
|
"description": "Tell Dream how to organize this workspace's memory."
|
||||||
|
},
|
||||||
"goal": {
|
"goal": {
|
||||||
"title": "Long-running goal",
|
"title": "Long-running goal",
|
||||||
"description": "Tell the agent to treat this as a sustained multi-step goal."
|
"description": "Tell the agent to treat this as a sustained multi-step goal."
|
||||||
|
|||||||
@@ -911,6 +911,10 @@
|
|||||||
"title": "Restaurar memoria",
|
"title": "Restaurar memoria",
|
||||||
"description": "Revierte la memoria a una instantánea Dream anterior."
|
"description": "Revierte la memoria a una instantánea Dream anterior."
|
||||||
},
|
},
|
||||||
|
"dream_prompt": {
|
||||||
|
"title": "Memoria de Dream",
|
||||||
|
"description": "Indica a Dream cómo organizar la memoria de este espacio de trabajo."
|
||||||
|
},
|
||||||
"goal": {
|
"goal": {
|
||||||
"title": "Objetivo a largo plazo",
|
"title": "Objetivo a largo plazo",
|
||||||
"description": "Indica al agente que trate esto como un objetivo sostenido en varios pasos."
|
"description": "Indica al agente que trate esto como un objetivo sostenido en varios pasos."
|
||||||
|
|||||||
@@ -911,6 +911,10 @@
|
|||||||
"title": "Restaurer la mémoire",
|
"title": "Restaurer la mémoire",
|
||||||
"description": "Revenir à un instantané Dream précédent."
|
"description": "Revenir à un instantané Dream précédent."
|
||||||
},
|
},
|
||||||
|
"dream_prompt": {
|
||||||
|
"title": "Mémoire Dream",
|
||||||
|
"description": "Indiquez à Dream comment organiser la mémoire de cet espace de travail."
|
||||||
|
},
|
||||||
"goal": {
|
"goal": {
|
||||||
"title": "Objectif long terme",
|
"title": "Objectif long terme",
|
||||||
"description": "Demandez à l’agent de traiter ceci comme un objectif multi‑étapes durable."
|
"description": "Demandez à l’agent de traiter ceci comme un objectif multi‑étapes durable."
|
||||||
|
|||||||
@@ -911,6 +911,10 @@
|
|||||||
"title": "Pulihkan memori",
|
"title": "Pulihkan memori",
|
||||||
"description": "Kembalikan memori ke snapshot Dream sebelumnya."
|
"description": "Kembalikan memori ke snapshot Dream sebelumnya."
|
||||||
},
|
},
|
||||||
|
"dream_prompt": {
|
||||||
|
"title": "Memori Dream",
|
||||||
|
"description": "Atur cara Dream menyusun memori workspace ini."
|
||||||
|
},
|
||||||
"goal": {
|
"goal": {
|
||||||
"title": "Tujuan jangka panjang",
|
"title": "Tujuan jangka panjang",
|
||||||
"description": "Instruksikan agen memperlakukan ini sebagai tujuan multi-langkah yang berkelanjutan."
|
"description": "Instruksikan agen memperlakukan ini sebagai tujuan multi-langkah yang berkelanjutan."
|
||||||
|
|||||||
@@ -911,6 +911,10 @@
|
|||||||
"title": "メモリを復元",
|
"title": "メモリを復元",
|
||||||
"description": "以前の Dream スナップショットへメモリを戻します。"
|
"description": "以前の Dream スナップショットへメモリを戻します。"
|
||||||
},
|
},
|
||||||
|
"dream_prompt": {
|
||||||
|
"title": "Dream の記憶",
|
||||||
|
"description": "このワークスペースの記憶を Dream がどう整理するかを設定します。"
|
||||||
|
},
|
||||||
"goal": {
|
"goal": {
|
||||||
"title": "長期目標",
|
"title": "長期目標",
|
||||||
"description": "持続的な複数ステップの目標として扱うようエージェントに伝えます。"
|
"description": "持続的な複数ステップの目標として扱うようエージェントに伝えます。"
|
||||||
|
|||||||
@@ -911,6 +911,10 @@
|
|||||||
"title": "메모리 복원",
|
"title": "메모리 복원",
|
||||||
"description": "이전 Dream 스냅샷으로 메모리를 되돌립니다."
|
"description": "이전 Dream 스냅샷으로 메모리를 되돌립니다."
|
||||||
},
|
},
|
||||||
|
"dream_prompt": {
|
||||||
|
"title": "Dream 메모리",
|
||||||
|
"description": "Dream이 이 워크스페이스의 메모리를 정리하는 방식을 설정합니다."
|
||||||
|
},
|
||||||
"goal": {
|
"goal": {
|
||||||
"title": "장기 목표",
|
"title": "장기 목표",
|
||||||
"description": "에이전트에게 지속적인 다단계 목표로 처리하도록 지시합니다."
|
"description": "에이전트에게 지속적인 다단계 목표로 처리하도록 지시합니다."
|
||||||
|
|||||||
@@ -911,6 +911,10 @@
|
|||||||
"title": "Khôi phục bộ nhớ",
|
"title": "Khôi phục bộ nhớ",
|
||||||
"description": "Đưa bộ nhớ về một snapshot Dream trước đó."
|
"description": "Đưa bộ nhớ về một snapshot Dream trước đó."
|
||||||
},
|
},
|
||||||
|
"dream_prompt": {
|
||||||
|
"title": "Bộ nhớ Dream",
|
||||||
|
"description": "Cho Dream biết cách sắp xếp bộ nhớ của workspace này."
|
||||||
|
},
|
||||||
"goal": {
|
"goal": {
|
||||||
"title": "Mục tiêu dài hạn",
|
"title": "Mục tiêu dài hạn",
|
||||||
"description": "Yêu cầu agent xử lý đây là mục tiêu nhiều bước kéo dài."
|
"description": "Yêu cầu agent xử lý đây là mục tiêu nhiều bước kéo dài."
|
||||||
|
|||||||
@@ -920,6 +920,10 @@
|
|||||||
"title": "恢复记忆",
|
"title": "恢复记忆",
|
||||||
"description": "将记忆恢复到之前的 Dream 快照。"
|
"description": "将记忆恢复到之前的 Dream 快照。"
|
||||||
},
|
},
|
||||||
|
"dream_prompt": {
|
||||||
|
"title": "Dream 记忆",
|
||||||
|
"description": "设置 Dream 如何整理当前工作区的记忆。"
|
||||||
|
},
|
||||||
"goal": {
|
"goal": {
|
||||||
"title": "长期目标",
|
"title": "长期目标",
|
||||||
"description": "让助手把当前请求当作需要多步骤持续推进的目标。"
|
"description": "让助手把当前请求当作需要多步骤持续推进的目标。"
|
||||||
|
|||||||
@@ -911,6 +911,10 @@
|
|||||||
"title": "恢復記憶",
|
"title": "恢復記憶",
|
||||||
"description": "將記憶恢復到之前的 Dream 快照。"
|
"description": "將記憶恢復到之前的 Dream 快照。"
|
||||||
},
|
},
|
||||||
|
"dream_prompt": {
|
||||||
|
"title": "Dream 記憶",
|
||||||
|
"description": "設定 Dream 如何整理目前工作區的記憶。"
|
||||||
|
},
|
||||||
"goal": {
|
"goal": {
|
||||||
"title": "長期目標",
|
"title": "長期目標",
|
||||||
"description": "請助理把這則請求當成需要多步驟持續推進的目標。"
|
"description": "請助理把這則請求當成需要多步驟持續推進的目標。"
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const SLASH_COMMAND_KEYS = [
|
|||||||
"dream",
|
"dream",
|
||||||
"dream_log",
|
"dream_log",
|
||||||
"dream_restore",
|
"dream_restore",
|
||||||
|
"dream_prompt",
|
||||||
"goal",
|
"goal",
|
||||||
"trigger",
|
"trigger",
|
||||||
"help",
|
"help",
|
||||||
|
|||||||
Reference in New Issue
Block a user