diff --git a/nanobot/agent/memory.py b/nanobot/agent/memory.py index 04d988ee5..8980a4baa 100644 --- a/nanobot/agent/memory.py +++ b/nanobot/agent/memory.py @@ -582,14 +582,53 @@ class Dream: def _build_tools(self) -> ToolRegistry: """Build a minimal tool registry for the Dream agent.""" - from nanobot.agent.tools.filesystem import EditFileTool, ReadFileTool + from nanobot.agent.skills import BUILTIN_SKILLS_DIR + from nanobot.agent.tools.filesystem import EditFileTool, ReadFileTool, WriteFileTool tools = ToolRegistry() workspace = self.store.workspace - tools.register(ReadFileTool(workspace=workspace, allowed_dir=workspace)) + # Allow reading builtin skills for reference during skill creation + extra_read = [BUILTIN_SKILLS_DIR] if BUILTIN_SKILLS_DIR.exists() else None + tools.register(ReadFileTool( + workspace=workspace, + allowed_dir=workspace, + extra_allowed_dirs=extra_read, + )) tools.register(EditFileTool(workspace=workspace, allowed_dir=workspace)) + # write_file scoped to skills/ directory for skill creation + skills_dir = workspace / "skills" + skills_dir.mkdir(parents=True, exist_ok=True) + tools.register(WriteFileTool(workspace=skills_dir, allowed_dir=skills_dir)) return tools + # -- skill listing -------------------------------------------------------- + + def _list_existing_skills(self) -> list[str]: + """List existing skills as 'name — description' for dedup context.""" + import re as _re + + from nanobot.agent.skills import BUILTIN_SKILLS_DIR + + _DESC_RE = _re.compile(r"^description:\s*(.+)$", _re.MULTILINE | _re.IGNORECASE) + entries: dict[str, str] = {} + for base in (self.store.workspace / "skills", BUILTIN_SKILLS_DIR): + if not base.exists(): + continue + for d in base.iterdir(): + if not d.is_dir(): + continue + skill_md = d / "SKILL.md" + if not skill_md.exists(): + continue + # Prefer workspace skills over builtin (same name) + if d.name in entries and base == BUILTIN_SKILLS_DIR: + continue + content = skill_md.read_text(encoding="utf-8")[:500] + m = _DESC_RE.search(content) + desc = m.group(1).strip() if m else "(no description)" + entries[d.name] = desc + return [f"{name} — {desc}" for name, desc in sorted(entries.items())] + # -- main entry ---------------------------------------------------------- async def run(self) -> bool: @@ -615,6 +654,7 @@ class Dream: current_memory = self.store.read_memory() or "(empty)" current_soul = self.store.read_soul() or "(empty)" current_user = self.store.read_user() or "(empty)" + file_context = ( f"## Current Date\n{current_date}\n\n" f"## Current MEMORY.md ({len(current_memory)} chars)\n{current_memory}\n\n" @@ -622,7 +662,7 @@ class Dream: f"## Current USER.md ({len(current_user)} chars)\n{current_user}" ) - # Phase 1: Analyze + # Phase 1: Analyze (no skills list — dedup is Phase 2's job) phase1_prompt = ( f"## Conversation History\n{history_text}\n\n{file_context}" ) @@ -647,7 +687,14 @@ class Dream: return False # Phase 2: Delegate to AgentRunner with read_file / edit_file - phase2_prompt = f"## Analysis Result\n{analysis}\n\n{file_context}" + existing_skills = self._list_existing_skills() + skills_section = "" + if existing_skills: + skills_section = ( + "\n\n## Existing Skills\n" + + "\n".join(f"- {s}" for s in existing_skills) + ) + phase2_prompt = f"## Analysis Result\n{analysis}\n\n{file_context}{skills_section}" tools = self._tools messages: list[dict[str, Any]] = [ diff --git a/nanobot/templates/agent/dream_phase1.md b/nanobot/templates/agent/dream_phase1.md index 596958e36..3cc19b186 100644 --- a/nanobot/templates/agent/dream_phase1.md +++ b/nanobot/templates/agent/dream_phase1.md @@ -3,6 +3,7 @@ Compare conversation history against current memory files. Also scan memory file Output one line per finding: [FILE] atomic fact (not already in memory) [FILE-REMOVE] reason for removal +[SKILL] kebab-case-name: one-line description of the reusable pattern Files: USER (identity, preferences), SOUL (bot behavior, tone), MEMORY (knowledge, project context) @@ -18,6 +19,12 @@ Staleness — flag for [FILE-REMOVE]: - Detailed incident info after 14 days — reduce to one-line summary - Superseded: approaches replaced by newer solutions, deprecated dependencies +Skill discovery — flag [SKILL] when ALL of these are true: +- A specific, repeatable workflow appeared 2+ times in the conversation history +- It involves clear steps (not vague preferences like "likes concise answers") +- It is substantial enough to warrant its own instruction set (not trivial like "read a file") +- Do not worry about duplicates — the next phase will check against existing skills + Do not add: current weather, transient status, temporary errors, conversational filler. [SKIP] if nothing needs updating. diff --git a/nanobot/templates/agent/dream_phase2.md b/nanobot/templates/agent/dream_phase2.md index 49c8020da..450be7096 100644 --- a/nanobot/templates/agent/dream_phase2.md +++ b/nanobot/templates/agent/dream_phase2.md @@ -1,11 +1,13 @@ Update memory files based on the analysis below. - [FILE] entries: add the described content to the appropriate file - [FILE-REMOVE] entries: delete the corresponding content from memory files +- [SKILL] entries: create a new skill under skills//SKILL.md using write_file ## File paths (relative to workspace root) - SOUL.md - USER.md - memory/MEMORY.md +- skills//SKILL.md (for [SKILL] entries only) Do NOT guess paths. @@ -17,6 +19,17 @@ Do NOT guess paths. - Surgical edits only — never rewrite entire files - If nothing to update, stop without calling tools +## Skill creation rules (for [SKILL] entries) +- Use write_file to create skills//SKILL.md +- Before writing, read_file skills/skill-creator/SKILL.md for format reference (frontmatter structure, naming conventions, quality standards) +- **Dedup check**: read existing skills listed below to verify the new skill is not functionally redundant. Skip creation if an existing skill already covers the same workflow. +- Include YAML frontmatter with name and description fields +- Keep SKILL.md under 2000 words — concise and actionable +- Include: when to use, steps, output format, at least one example +- Do NOT overwrite existing skills — skip if the skill directory already exists +- Reference specific tools the agent has access to (read_file, write_file, exec, web_search, etc.) +- Skills are instruction sets, not code — do not include implementation code + ## Quality - Every line must carry standalone value - Concise bullets under clear headers