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
11 changes: 10 additions & 1 deletion core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,17 @@ class AppConfig:
providers: Dict[str, Any]
default_provider: str

# The installed location of the harness (parent of core/), not the caller's
# CWD. `motion` can be pointed at any workspace directory, so config lookup
# must not depend on where it happens to be invoked from.
_REPO_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


class ConfigManager:
CONFIG_PATHS = ["config.yml", "config.example.yml"]
CONFIG_PATHS = [
os.path.join(_REPO_DIR, "config.yml"),
os.path.join(_REPO_DIR, "config.example.yml"),
]

def __init__(self, config_path: str = ""):
if config_path:
Expand Down
44 changes: 39 additions & 5 deletions core/learning.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
import hashlib
import os
import asyncio
from typing import List, Dict, Any, Optional
from dataclasses import dataclass
from datetime import datetime
from core.providers import ModelConfig, ProviderFactory
from memory.db import MemoryDB, MemoryChunk
from memory.db import MemoryDB, MemoryChunk, EMBEDDING_DIM


def _fallback_embedding(text: str, dim: int = EMBEDDING_DIM) -> List[float]:
"""Deterministic, always-non-zero embedding for when no embedding
provider is available. Mirrors MotionAgent.get_embedding's fallback so
behavior is consistent across the codebase."""
h = hashlib.sha256(text.encode()).digest()
raw = [float(b) / 255.0 for b in h]
vec = (raw * ((dim // len(raw)) + 1))[:dim]
norm = sum(v * v for v in vec) ** 0.5 or 1.0
return [v / norm for v in vec]

@dataclass
class Trajectory:
Expand All @@ -14,15 +26,26 @@ class Trajectory:
final_result: str
success: bool

# The installed location of the harness (parent of core/), not the caller's
# CWD - auto-synthesized skills accumulate here regardless of which project
# directory `motion` is currently pointed at.
_REPO_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


class SkillSynthesizer:
"""
The 'Crystallization' engine.
Turns successful tool-call trajectories into reusable .md skills.
"""
def __init__(self, model_config: ModelConfig, db: MemoryDB, skills_dir: str = "skills"):
def __init__(self, model_config: ModelConfig, db: MemoryDB, skills_dir: Optional[str] = None, embedding_provider=None):
self.provider = ProviderFactory.get_provider(model_config)
self.db = db
self.skills_dir = skills_dir
self.skills_dir = skills_dir or os.path.join(_REPO_DIR, "skills")
# Used to compute a real embedding for synthesized skills so they can
# actually participate in semantic recall. Expected to expose an
# async get_embedding(text) -> list[float] (e.g. a MotionAgent
# instance, which already has a safe hash-based fallback built in).
self.embedding_provider = embedding_provider

if not os.path.exists(self.skills_dir):
os.makedirs(self.skills_dir)
Expand Down Expand Up @@ -60,10 +83,21 @@ async def synthesize(self, trajectory: Trajectory) -> Optional[str]:
with open(file_path, "w", encoding="utf-8") as f:
f.write(skill_content)

# Also index the skill in the MemoryDB for semantic recall
# Also index the skill in the MemoryDB for semantic recall. A
# zero-vector embedding is never valid here: cosine
# similarity/distance is undefined for a zero-norm vector, which
# crashes semantic search rather than just being unhelpful.
embedding = None
if self.embedding_provider is not None:
try:
embedding = await self.embedding_provider.get_embedding(skill_content)
except Exception:
embedding = None
if not embedding:
embedding = _fallback_embedding(skill_content)
self.db.add_memory(MemoryChunk(
content=skill_content,
embedding=[0.0] * 128, # Embedding would be generated by a real provider
embedding=embedding,
metadata={"file": file_path, "type": "SKILL"},
mem_type="DOC"
))
Expand Down
8 changes: 5 additions & 3 deletions core/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import logging

from core.providers import ModelConfig, ProviderFactory
from main import MotionAgent
from main import MotionAgent, REPO_DIR

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -189,7 +189,7 @@ async def _execute_task(self, request: TaskRequest, model_override: Optional[Mod

try:
config = model_override or self.default_config
agent = MotionAgent(config, memory_path=f"memory_{request.task_id}.db")
agent = MotionAgent(config, memory_path=os.path.join(REPO_DIR, f"memory_{request.task_id}.db"))

# Record user turn
task.conversation.append({"role": "user", "content": request.prompt})
Expand All @@ -208,6 +208,8 @@ async def on_stream_chunk(chunk: str) -> None:
request.prompt,
target="user",
on_stream_chunk=on_stream_chunk,
workspace=self.workspace_path,
agent_mode="build",
)

# Fallback for non-streaming providers
Expand All @@ -233,7 +235,7 @@ async def on_stream_chunk(chunk: str) -> None:
except Exception:
pass
try:
db_path = f"memory_{request.task_id}.db"
db_path = os.path.join(REPO_DIR, f"memory_{request.task_id}.db")
if os.path.exists(db_path):
os.remove(db_path)
except Exception:
Expand Down
Loading