Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 24 additions & 8 deletions evolve_server/core/skill_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import hashlib
import json
import logging
from copy import deepcopy
from datetime import datetime, timezone
from typing import Any, Optional

Expand Down Expand Up @@ -106,6 +107,8 @@ def record_update(
skill_name: str,
content_sha: str,
action: str = "create",
*,
bundle_record: Optional[dict[str, Any]] = None,
) -> int:
"""Record a content-changing update. Returns the new version number.

Expand All @@ -120,15 +123,28 @@ def record_update(
new_version = entry.get("version", 0) + 1
entry["version"] = new_version
entry["content_sha"] = content_sha
if isinstance(bundle_record, dict):
for key in ("format", "entrypoint", "tree_sha256"):
if bundle_record.get(key):
entry[key] = bundle_record[key]
files = bundle_record.get("files")
if isinstance(files, list):
entry["files"] = deepcopy(files)
history: list = entry.setdefault("history", [])
history.append(
{
"version": new_version,
"content_sha": content_sha,
"timestamp": datetime.now(timezone.utc).isoformat(),
"action": action,
}
)
history_entry: dict[str, Any] = {
"version": new_version,
"content_sha": content_sha,
"timestamp": datetime.now(timezone.utc).isoformat(),
"action": action,
}
if isinstance(bundle_record, dict):
for key in ("format", "entrypoint", "tree_sha256"):
if bundle_record.get(key):
history_entry[key] = bundle_record[key]
files = bundle_record.get("files")
if isinstance(files, list):
history_entry["files"] = deepcopy(files)
history.append(history_entry)
if len(history) > 20:
entry["history"] = history[-20:]

Expand Down
17 changes: 13 additions & 4 deletions evolve_server/engines/EVOLVE_AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ workspace/
├── skills/ ← input+output: current skill library
│ └── <skill-name>/
│ ├── SKILL.md ← current version (refreshed from storage each round)
│ ├── references/ ← optional reference docs / prompts / notes
│ ├── scripts/ ← optional helper scripts / tooling
│ ├── assets/ ← optional templates / binaries / other assets
│ └── history/ ← persistent across rounds only in `--no-fresh` mode
│ ├── v1.md ← previous SKILL.md snapshot
│ ├── v1_evidence.md
Expand All @@ -30,10 +33,10 @@ workspace/
2. **Analyze** the sessions: identify patterns, failures, successes, and
which skills (if any) were referenced.
3. **Decide** what actions to take for each skill or pattern.
4. **Execute** by writing new or updated `SKILL.md` files in `skills/`.
4. **Execute** by writing new or updated skill bundles in `skills/`.

Work through these steps autonomously. Use your file-reading and writing
tools to inspect session data and produce skill files.
tools to inspect session data and produce skill bundles.

**File access boundary**: All your file operations MUST stay within this
workspace directory. The workspace contains copies of all data you need —
Expand Down Expand Up @@ -151,10 +154,16 @@ No action needed. Use when:
## Step 4: Execute — Write Skill Files

### For improve_skill / optimize_description:
Edit the existing `skills/<name>/SKILL.md` file in place.
Edit the existing `skills/<name>/` bundle in place. `SKILL.md` remains the
entrypoint, but you may also update supporting files such as
`references/`, `scripts/`, `assets/`, and `history/` when the evidence
shows the skill needs them.

### For create_skill:
Create a new directory `skills/<new-name>/SKILL.md`.
Create a new directory `skills/<new-name>/SKILL.md`. If the skill needs
supporting resources, you may also create additional files under
`references/`, `scripts/`, `assets/`, or other subdirectories inside the
same skill folder.

### SKILL.md Format

Expand Down
78 changes: 63 additions & 15 deletions evolve_server/engines/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@
from typing import Any

from skillclaw.object_store import build_object_store
from skillclaw.skill_bundle import (
bundle_entrypoint_bytes,
bundle_file_records,
bundle_tree_sha256,
)

from ..core.config import EvolveServerConfig
from ..core.constants import SLUG_RE
Expand All @@ -30,11 +35,13 @@
from ..storage.mock_bucket import LocalBucket
from ..storage.oss_helpers import (
delete_session_keys,
fetch_skill_content,
fetch_skill_bundle,
list_object_keys,
list_session_keys,
load_manifest,
read_json_object,
save_manifest,
save_version_bundle,
)
from .agent_workspace import AgentWorkspace
from .agents_md import load_agents_md
Expand Down Expand Up @@ -272,39 +279,75 @@ async def _drain_sessions(self) -> tuple[list[dict], list[str]]:
def _load_remote_skills(self) -> dict[str, dict[str, Any]]:
return load_manifest(self._bucket, self._prefix)

def _fetch_all_skills(self, manifest: dict[str, dict]) -> dict[str, str]:
"""Fetch SKILL.md content for all skills in the manifest."""
skills: dict[str, str] = {}
for name in manifest:
content = fetch_skill_content(self._bucket, self._prefix, name)
if content:
skills[name] = content
def _fetch_all_skills(self, manifest: dict[str, dict]) -> dict[str, dict[str, bytes]]:
"""Fetch full bundle content for all skills in the manifest."""
skills: dict[str, dict[str, bytes]] = {}
for name, record in manifest.items():
bundle = fetch_skill_bundle(self._bucket, self._prefix, name, record)
if bundle:
skills[name] = bundle
return skills

# ================================================================= #
# Upload evolved skills #
# ================================================================= #

def _upload_skill(self, skill: dict, action: str = "create") -> None:
def _upload_skill(
self,
skill: dict,
bundle_files: dict[str, bytes],
action: str = "create",
) -> None:
name = skill.get("name", "")
if not name:
return

skill_id = self._id_registry.get_or_create(name)
md_content = build_skill_md(skill)
if "SKILL.md" not in bundle_files:
bundle_files = {**bundle_files, "SKILL.md": build_skill_md(skill).encode("utf-8")}
md_bytes = bundle_entrypoint_bytes(bundle_files)
object_key = f"{self._prefix}skills/{name}/SKILL.md"

self._bucket.put_object(object_key, md_content.encode("utf-8"))

content_sha = hashlib.sha256(md_content.encode("utf-8")).hexdigest()
version = self._id_registry.record_update(name, content_sha, action=action)
self._bucket.put_object(object_key, md_bytes)
keep_bundle_keys: set[str] = set()
for rel_path, data in sorted(bundle_files.items()):
if rel_path == "SKILL.md":
continue
key = f"{self._prefix}skills/{name}/files/{rel_path}"
keep_bundle_keys.add(key)
self._bucket.put_object(key, data)

for key in list_object_keys(self._bucket, f"{self._prefix}skills/{name}/files/"):
if key not in keep_bundle_keys:
self._bucket.delete_object(key)

content_sha = hashlib.sha256(md_bytes).hexdigest()
tree_sha = bundle_tree_sha256(bundle_files)
bundle_record = {
"format": "bundle_v1",
"entrypoint": "SKILL.md",
"tree_sha256": tree_sha,
"files": bundle_file_records(bundle_files),
}
version = self._id_registry.record_update(
name,
content_sha,
action=action,
bundle_record=bundle_record,
)
save_version_bundle(self._bucket, self._prefix, name, version, bundle_files)

manifest = self._load_remote_skills()
manifest[name] = {
**manifest.get(name, {}),
"name": name,
"skill_id": skill_id,
"version": version,
"sha256": content_sha,
"tree_sha256": tree_sha,
"format": "bundle_v1",
"entrypoint": "SKILL.md",
"files": bundle_record["files"],
"uploaded_by": "evolve_server",
"uploaded_at": datetime.now(timezone.utc).isoformat(),
"description": skill.get("description", ""),
Expand Down Expand Up @@ -412,7 +455,12 @@ async def run_once(self) -> dict:
skill["name"] = name

try:
await self._call_storage(self._upload_skill, skill, action)
await self._call_storage(
self._upload_skill,
skill,
change.get("bundle_files", {}),
action,
)
skills_evolved += 1
evolution_records.append(
{
Expand Down
55 changes: 41 additions & 14 deletions evolve_server/engines/agent_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

Handles preparing the local workspace directory that OpenClaw operates on,
snapshotting skill state before agent execution, and collecting changes
(new / modified SKILL.md files) after the agent finishes.
(new / modified skill bundles) after the agent finishes.

Key design note on OpenClaw bootstrap integration:
OpenClaw's ``ensureAgentWorkspace()`` creates template bootstrap files
Expand All @@ -15,13 +15,19 @@

from __future__ import annotations

import hashlib
import json
import logging
import shutil
from pathlib import Path
from typing import Any

from skillclaw.skill_bundle import (
bundle_entrypoint_text,
bundle_tree_sha256,
read_skill_bundle,
write_skill_bundle,
)

from ..core.utils import parse_skill_content

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -56,6 +62,9 @@
├── skills/ ← current skill library (read + write)
│ └── <name>/
│ ├── SKILL.md
│ ├── references/
│ ├── scripts/
│ ├── assets/
│ └── history/
├── manifest.json ← skill manifest (read-only)
└── skill_registry.json
Expand All @@ -65,7 +74,9 @@

- **All file operations** stay within this workspace directory.
- Do NOT modify `sessions/`, `manifest.json`, or `skill_registry.json`.
- Write changes only to `skills/<name>/SKILL.md` (and `history/`).
- Write changes only inside `skills/<name>/` bundles.
- You may inspect and edit `SKILL.md`, `references/`, `scripts/`, `assets/`,
`history/`, and other supporting files that belong to a skill.
- If there are no actionable patterns, make no changes — that is fine.

## Memory
Expand Down Expand Up @@ -108,7 +119,7 @@ def reset(self) -> None:
def prepare(
self,
sessions: list[dict],
existing_skills: dict[str, str],
existing_skills: dict[str, str | dict[str, bytes | bytearray | str]],
manifest: dict[str, dict],
agents_md: str,
skill_registry_info: dict[str, Any] | None = None,
Expand All @@ -120,7 +131,8 @@ def prepare(
sessions:
Raw session dicts drained from storage.
existing_skills:
``{skill_name: SKILL.md content}`` for all current skills.
``{skill_name: bundle}`` for all current skills, where bundle is
either a raw ``SKILL.md`` string or ``{rel_path: bytes}``.
manifest:
Current manifest dict ``{skill_name: metadata}``.
agents_md:
Expand Down Expand Up @@ -152,10 +164,15 @@ def prepare(

# Write existing skills
self.skills_dir.mkdir(parents=True, exist_ok=True)
for path in sorted(self.skills_dir.iterdir()):
if path.is_dir() and path.name not in existing_skills:
shutil.rmtree(path)
for name, content in existing_skills.items():
skill_dir = self.skills_dir / name
skill_dir.mkdir(parents=True, exist_ok=True)
(skill_dir / "SKILL.md").write_text(content, encoding="utf-8")
if isinstance(content, dict):
write_skill_bundle(skill_dir, content, clean=True)
else:
write_skill_bundle(skill_dir, {"SKILL.md": content}, clean=True)

# Write manifest
manifest_path = self.root / "manifest.json"
Expand Down Expand Up @@ -209,17 +226,16 @@ def prepare(
)

def snapshot_skills(self) -> dict[str, str]:
"""Return ``{skill_name: sha256_of_content}`` for all skills in the workspace."""
"""Return ``{skill_name: tree_sha256}`` for all skills in the workspace."""
snapshot: dict[str, str] = {}
if not self.skills_dir.exists():
return snapshot
for skill_dir in sorted(self.skills_dir.iterdir()):
if not skill_dir.is_dir():
continue
skill_md = skill_dir / "SKILL.md"
if skill_md.is_file():
content = skill_md.read_bytes()
snapshot[skill_dir.name] = hashlib.sha256(content).hexdigest()
bundle = read_skill_bundle(skill_dir)
if "SKILL.md" in bundle:
snapshot[skill_dir.name] = bundle_tree_sha256(bundle)
return snapshot

def collect_changes(
Expand All @@ -233,6 +249,8 @@ def collect_changes(
- ``action``: ``"create"`` or ``"improve"``
- ``skill``: parsed skill dict (name, description, content, ...)
- ``raw_md``: the raw SKILL.md text
- ``bundle_files``: full bundle contents ``{rel_path: bytes}``
- ``tree_sha256``: directory-level fingerprint
"""
after_snapshot = self.snapshot_skills()
changes: list[dict[str, Any]] = []
Expand All @@ -242,8 +260,15 @@ def collect_changes(
if before_sha == after_sha:
continue

skill_md_path = self.skills_dir / name / "SKILL.md"
raw_md = skill_md_path.read_text(encoding="utf-8")
bundle_files = read_skill_bundle(self.skills_dir / name)
if "SKILL.md" not in bundle_files:
logger.warning(
"[AgentWorkspace] changed skill '%s' is missing SKILL.md; skipping",
name,
)
continue

raw_md = bundle_entrypoint_text(bundle_files)
parsed = parse_skill_content(name, raw_md)

action = "create" if before_sha is None else "improve"
Expand All @@ -253,6 +278,8 @@ def collect_changes(
"action": action,
"skill": parsed,
"raw_md": raw_md,
"bundle_files": bundle_files,
"tree_sha256": after_sha,
}
)
logger.info(
Expand Down
Loading
Loading