From 04f52e3f0bbbfd6e20de3b2a206376974d914c09 Mon Sep 17 00:00:00 2001 From: me Date: Fri, 10 Apr 2026 09:39:07 -0400 Subject: [PATCH 01/10] docker/podman script + LiteLLM --- .env.example | 109 ++++++++++++++++ .gitignore | 50 ++++++++ Dockerfile | 76 +++++++++++ README.md | 273 ++++++++++++++++++++++++++++++++++++--- lib_llm_ext.py | 56 ++++---- run.py | 92 +++++++++++++ run.sh | 341 +++++++++++++++++++++++++++++++++++++++++++++++++ src/loop.metta | 12 +- 8 files changed, 961 insertions(+), 48 deletions(-) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 run.py create mode 100755 run.sh diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..f045dcb5 --- /dev/null +++ b/.env.example @@ -0,0 +1,109 @@ +# MeTTaClaw Configuration File +# Copy this file to .env.local and fill in your API keys +# The .env.local file is gitignored, so your secrets are safe + +# =================================================================== +# SELECT YOUR PROVIDER +# Just fill in the API key for your chosen provider below. +# The system will auto-detect which provider to use. +# =================================================================== + +# ------------------------------------------------------------------- +# OpenAI (GPT-4, GPT-3.5, o1, o3) +# Get API key: https://platform.openai.com/api-keys +# ------------------------------------------------------------------- +OPENAI_API_KEY="sk-..." + +# ------------------------------------------------------------------- +# Anthropic (Claude family) +# Get API key: https://console.anthropic.com/settings/keys +# ------------------------------------------------------------------- +ANTHROPIC_API_KEY="sk-ant-..." + +# ------------------------------------------------------------------- +# Ollama (Local models - FREE, no API key needed) +# Install: https://ollama.ai +# Then run: ollama pull llama3 +# ------------------------------------------------------------------- +OLLAMA_API_BASE="http://localhost:11434" + +# ------------------------------------------------------------------- +# OpenRouter (Access 100+ models through one API) +# Get API key: https://openrouter.ai/settings/keys +# ------------------------------------------------------------------- +OPENROUTER_API_KEY="" + +# ------------------------------------------------------------------- +# Groq (Fast inference) +# Get API key: https://console.groq.com/keys +# ------------------------------------------------------------------- +GROQ_API_KEY="gsk_..." + +# ------------------------------------------------------------------- +# Together AI (Open-source models) +# Get API key: https://api.together.ai/settings/api-keys +# ------------------------------------------------------------------- +TOGETHER_API_KEY="" + +# ------------------------------------------------------------------- +# Google Vertex AI +# Get API key: https://console.cloud.google.com/apis/credentials +# ------------------------------------------------------------------- +GOOGLE_API_KEY="" +GOOGLE_APPLICATION_CREDENTIALS="/path/to/credentials.json" + +# ------------------------------------------------------------------- +# AWS Bedrock +# AWS credentials from: https://console.aws.amazon.com/iam +# ------------------------------------------------------------------- +AWS_ACCESS_KEY_ID="" +AWS_SECRET_ACCESS_KEY="" +AWS_REGION_NAME="us-east-1" + +# ------------------------------------------------------------------- +# Azure OpenAI +# Azure OpenAI credentials from: https://portal.azure.com +# ------------------------------------------------------------------- +AZURE_API_KEY="" +AZURE_API_BASE="https://your-resource.openai.azure.com" +AZURE_API_VERSION="2024-02-15-preview" + +# ------------------------------------------------------------------- +# Custom OpenAI-Compatible Endpoint +# ------------------------------------------------------------------- +CUSTOM_API_KEY="" +CUSTOM_API_BASE="https://your-server.com/v1" + +# =================================================================== +# OPTIONAL SETTINGS +# =================================================================== + +# Provider preference (if multiple keys provided) +# Options: openai, anthropic, ollama, openrouter, groq, together, google, aws, azure, custom +# LLM_PROVIDER="anthropic" + +# Fallback providers (if primary fails) +# LLM_FALLBACK="openai,ollama" + +# Model overrides +# CLAUDE_MODEL="anthropic/claude-opus-4-20250514" +# GPT_MODEL="openai/gpt-4o" +# OLLAMA_MODEL="llama3" + +# Token and reasoning settings +# LLM_MAX_TOKENS=8192 +# LLM_REASONING_MODE="medium" + +# Loop settings +# MAX_NEW_INPUT_LOOPS=50 +# MAX_WAKE_LOOPS=1 +# SLEEP_INTERVAL=1 +# WAKEUP_INTERVAL=600 + +# Memory settings +# MEMORY_SIZE=1000 +# MEMORY_SIMILARITY_THRESHOLD=0.75 + +# Channel settings +# CHANNEL_TIMEOUT=30 +# MAX_MESSAGE_LENGTH=2000 diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..1ec4be85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,50 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual environments +venv/ +env/ +ENV/ +.venv + +# Environment files (API keys) +.env +.env.local +.env.*.local + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log + +# MeTTaClaw specific +memory/*.metta +*.metta.bak diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..60a271a4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,76 @@ +# MeTTaClaw Dockerfile +# ======================================================================== +# Podman-compatible Docker image for MeTTaClaw +# ======================================================================== + +# Base image +FROM archlinux:latest + +# PeTTa repo and branch +ARG PETTA_REPO=https://github.com/trueagi-io/PeTTa +ARG PETTA_BRANCH=main + +# MeTTaClaw repo and branch +ARG METTACLAW_REPO=https://github.com/autonull/mettaclaw +ARG METTACLAW_BRANCH=main + +# Container paths +ARG CONTAINER_PETTA_PATH=/opt/PeTTa +ARG CONTAINER_METTACLAW_PATH=/opt/PeTTa/repos/mettaclaw + +# ======================================================================== +# Environment +# ======================================================================== +ENV TERM=xterm-256color + +# ======================================================================== +# Install Dependencies (Arch Linux uses pacman) +# ======================================================================== +RUN pacman-key --init && pacman -Sy --noconfirm \ + base-devel \ + git \ + curl \ + ca-certificates \ + python \ + python-pip \ + libffi \ + openssl \ + cargo \ + rust \ + cmake \ + wget \ + gmp \ + libedit \ + readline \ + libarchive \ + pcre \ + libyaml \ + swi-prolog \ + || true + +# Install janus for Python interop (optional - can work without) +RUN pip3 install --break-system-packages janus || true + +# ======================================================================== +# Clone PeTTa +# ======================================================================== +RUN git clone --depth 1 --branch ${PETTA_BRANCH} ${PETTA_REPO} ${CONTAINER_PETTA_PATH} + +# ======================================================================== +# Clone MeTTaClaw +# ======================================================================== +RUN git clone --depth 1 --branch ${METTACLAW_BRANCH} ${METTACLAW_REPO} ${CONTAINER_METTACLAW_PATH} + +# ======================================================================== +# Setup +# ======================================================================== +WORKDIR ${CONTAINER_PETTA_PATH} + +RUN cp ${CONTAINER_METTACLAW_PATH}/run.metta ./ + +ENV PETTA_PATH=${CONTAINER_PETTA_PATH} +ENV PATH="${CONTAINER_PETTA_PATH}:${PATH}" + +RUN chmod +x run.sh + +CMD ["/bin/bash", "-c", "echo 'MeTTaClaw ready!'; echo 'To run: ./run.sh run.metta'"] \ No newline at end of file diff --git a/README.md b/README.md index 2db7dcea..55231e26 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -## MeTTaClaw +## MeTTaClaw 2 image @@ -23,33 +23,155 @@ The following example demonstrates learning and decision-making in a textually r ![mettaclaw_in_nace_world](https://github.com/user-attachments/assets/c6c01839-234d-4505-baf6-4f2f3787c7b9) - This project also aims to explore the potential of Agentic Physical AI, a ROS2 package for mobile robots with manipulators is underway. -**Installation** +--- + +## Quick Start + +### Step 1: Install Dependencies First, get [SWI-Prolog](https://www.swi-prolog.org/). Then: -``` +```bash git clone https://github.com/trueagi-io/PeTTa cd PeTTa mkdir -p repos && git clone https://github.com/patham9/mettaclaw repos/mettaclaw +cd repos/mettaclaw +``` + +### Step 2: Configure Your LLM Provider + +MeTTaClaw supports 100+ LLM providers. Choose one: + +#### Option A: OpenAI (GPT-4, GPT-3.5) + +```bash +export OPENAI_API_KEY="sk-..." +python run.py +``` + +Get API key: https://platform.openai.com/api-keys + +#### Option B: Anthropic (Claude) + +```bash +export ANTHROPIC_API_KEY="sk-ant-..." +python run.py ``` -**Usage** +Get API key: https://console.anthropic.com/settings/keys + +#### Option C: Ollama (Local, Free) + +```bash +# Install Ollama +curl -fsSL https://ollama.ai/install.sh | sh -Run the system via the following command which ensures the system is started from the root folder of PeTTa: +# Pull a model +ollama pull llama3 +# Run MeTTaClaw +python run.py ``` -cp repos/mettaclaw/run.metta ./ -OPENAI_API_KEY=... sh run.sh run.metta + +Ollama auto-detects at `http://localhost:11434` + +#### Option D: Use a .env File + +Create a `.env` file in the project root: + +```bash +# .env +ANTHROPIC_API_KEY="sk-ant-..." +# or +OPENAI_API_KEY="sk-..." +# or +OLLAMA_API_BASE="http://localhost:11434" ``` -**Auto-install/run** +Then just run: +```bash +python run.py +``` -Alternatively, if PeTTa is already installed and the latest version pulled (v1.0.2 or latest commit), then, running the following MeTTa file from the root folder, installs and runs MeTTaClaw (assuming OPENAI_API_KEY is set): +### Step 3: Run +```bash +python run.py ``` + +Or with a custom script: +```bash +python run.py my_script.metta +``` + +--- + +## Configuration + +### Environment Variables + +| Variable | Description | Example | +|----------|-------------|---------| +| `OPENAI_API_KEY` | OpenAI API key | `sk-...` | +| `ANTHROPIC_API_KEY` | Anthropic API key | `sk-ant-...` | +| `OLLAMA_API_BASE` | Ollama server URL | `http://localhost:11434` | +| `OPENROUTER_API_KEY` | OpenRouter API key | `...` | +| `GROQ_API_KEY` | Groq API key | `gsk_...` | +| `TOGETHER_API_KEY` | Together AI key | `...` | +| `GOOGLE_API_KEY` | Google AI key | `...` | +| `AWS_ACCESS_KEY_ID` | AWS access key | `...` | +| `AWS_SECRET_ACCESS_KEY` | AWS secret key | `...` | +| `AZURE_API_KEY` | Azure OpenAI key | `...` | +| `LLM_PROVIDER` | Preferred provider | `anthropic` | +| `LLM_MODEL` | Override default model | `anthropic/claude-sonnet-4-20250514` | + +### Supported Providers + +| Provider | Env Variable | Default Model | Get Key | +|----------|-------------|---------------|---------| +| **OpenAI** | `OPENAI_API_KEY` | `openai/gpt-4o` | https://platform.openai.com | +| **Anthropic** | `ANTHROPIC_API_KEY` | `anthropic/claude-opus-4-20250514` | https://console.anthropic.com | +| **Ollama** | (none, local) | `ollama/llama3` | https://ollama.ai | +| **OpenRouter** | `OPENROUTER_API_KEY` | `openrouter/anthropic/claude-3-opus` | https://openrouter.ai | +| **Groq** | `GROQ_API_KEY` | `groq/llama3-70b-8192` | https://console.groq.com | +| **Together AI** | `TOGETHER_API_KEY` | `together_ai/meta-llama/Llama-3-70b` | https://api.together.ai | +| **Google** | `GOOGLE_API_KEY` | `google/gemini-pro` | https://console.cloud.google.com | +| **AWS Bedrock** | `AWS_ACCESS_KEY_ID` | `bedrock/anthropic.claude-3-opus` | https://aws.amazon.com | +| **Azure** | `AZURE_API_KEY` | `azure/gpt-4` | https://portal.azure.com | + +### MeTTa Configuration + +Create a `config.metta` file for MeTTa-based configuration: + +```metta +;; LLM Configuration +(configure provider "anthropic") +(configure LLM "anthropic/claude-opus-4-20250514") +(configure maxOutputToken 8192) +(configure reasoningMode "medium") + +;; Loop Configuration +(configure maxNewInputLoops 50) +(configure maxWakeLoops 1) +(configure sleepInterval 1) +(configure wakeupInterval 600) + +;; Memory Configuration +(configure memorySize 1000) +(configure memorySimilarityThreshold 0.75) +``` + +--- + +## Usage + +### Auto-install/Run + +If PeTTa is already installed and the latest version pulled (v1.0.2 or latest commit), then, running the following MeTTa file from the root folder, installs and runs MeTTaClaw (assuming API key is set): + +```metta !(import! &self (library lib_import)) !(git-import! "https://github.com/patham9/mettaclaw.git") !(import! &self (library mettaclaw lib_mettaclaw)) @@ -57,24 +179,145 @@ Alternatively, if PeTTa is already installed and the latest version pulled (v1.0 !(mettaclaw) ``` -**Illustrations** +### Command Line + +```bash +# Run with default script +python run.py + +# Run with custom script +python run.py my_script.metta + +# Run with specific provider +OPENAI_API_KEY="sk-..." python run.py + +# Verbose output +METTACLAW_VERBOSE=true python run.py +``` + +### Docker/Podman + +Build and run MeTTaClaw in a container (no need to install SWI-Prolog): + +```bash +# 1. Build the Docker image +./run.sh build + +# 2. Run (set your API key) +OPENAI_API_KEY="sk-..." ./run.sh +# Or with a custom script: +OPENAI_API_KEY="sk-..." ./run.sh run myscript.metta +``` + +Other commands: +```bash +./run.sh start # Interactive chat mode +./run.sh sh # Drop into shell for debugging +./run.sh clean # Remove image +./run.sh status # Check image/container status +``` + +Configure LLM provider at runtime: +```bash +ANTHROPIC_API_KEY="sk-ant-..." ./run.sh # Use Claude +OLLAMA_API_BASE="http://localhost:11434" ./run.sh # Use Ollama +``` + +--- + +## Examples + +### Basic Agent Loop + +```metta +!(import! &self (library lib_import)) +!(import! &self (library mettaclaw lib_mettaclaw)) + +!(mettaclaw) +``` + +### Custom Configuration + +```metta +!(import! &self (library lib_import)) +!(import! &self (library mettaclaw lib_mettaclaw)) + +;; Set custom LLM provider +(configure provider "groq") +(configure LLM "groq/llama3-70b-8192") +(configure maxOutputToken 4096) -Long-Term Memory Recall: +!(mettaclaw) +``` + +### Memory Operations + +```metta +;; Add to memory +(remember "The sky is blue") + +;; Query memory +(query "What color is the sky?") + +;; Use in agent loop +!(mettaclaw) +``` + +--- + +## Illustrations + +**Long-Term Memory Recall:** image -Tool use: +**Tool use:** image -Shell output of the actual invocation of the generated MeTTa code: +**Shell output of the actual invocation of the generated MeTTa code:** image -System also added it into its Atom Space storage (embedding vector omitted): +**System also added it into its Atom Space storage (embedding vector omitted):** image +--- + +## Documentation + +- [CONFIG.md](CONFIG.md) - Comprehensive configuration guide +- [LITELLM_CONFIG.md](LITELLM_CONFIG.md) - LiteLLM provider documentation +- [LITELLM_SUMMARY.md](LITELLM_SUMMARY.md) - Implementation summary + +## Testing + +```bash +# Run configuration tests +python test_litellm.py + +# Test configuration loading +python mettaclaw_config.py +``` + +## Troubleshooting + +### "No API key found" +- Set the appropriate environment variable for your provider +- Or create a `.env` file with your API key + +### "Invalid model name" +- Use format: `provider/model-name` +- Check provider documentation for exact model names + +### "Ollama connection failed" +- Ensure Ollama is running: `ollama serve` +- Check URL: `export OLLAMA_API_BASE="http://localhost:11434"` +### More help +See [CONFIG.md](CONFIG.md) for detailed troubleshooting. +## License +[Your License Here] diff --git a/lib_llm_ext.py b/lib_llm_ext.py index de2adc9c..395cdc88 100644 --- a/lib_llm_ext.py +++ b/lib_llm_ext.py @@ -1,36 +1,36 @@ -import os, openai +import os +import litellm +from litellm import completion -ASI_CLIENT = openai.OpenAI( - api_key=os.environ["ASI_API_KEY"], - base_url="https://inference.asicloud.cudos.org/v1" -) - -ANTHROPIC_CLIENT = openai.OpenAI( - api_key=os.environ["ANTHROPIC_API_KEY"], - base_url="https://api.anthropic.com/v1/" -) +litellm.drop_params = True +litellm.set_verbose = False def _clean(text): - return text.replace("_quote_", '"').replace("_apostrophe_", "'") + if text: + return text.replace("_quote_", '"').replace("_apostrophe_", "'") + return "" -def _chat(client, model, content, max_tokens=6000): - resp = client.chat.completions.create( - model=model, - messages=[{"role": "user", "content": content}], - max_tokens=max_tokens - ) - return _clean(resp.choices[0].message.content) +def _chat(model, content, max_tokens=6000): + try: + resp = completion( + model=model, + messages=[{"role": "user", "content": content}], + max_tokens=max_tokens + ) + return _clean(resp.choices[0].message.content) + except Exception as e: + return f"LLM Error: {str(e)}" def useMiniMax(content): - return _chat( - client=ASI_CLIENT, - model="minimax/minimax-m2.5", - content=content - ) + model = os.environ.get('MINIMAX_MODEL', 'openai/minimax/minimax-m2.5') + return _chat(model=model, content=content, max_tokens=6000) def useClaude(content): - return _chat( - client=ANTHROPIC_CLIENT, - model="claude-opus-4-6", - content=content - ) + model = os.environ.get('CLAUDE_MODEL', 'anthropic/claude-opus-4-20250514') + return _chat(model=model, content=content, max_tokens=8192) + +def useGPT(model, max_tokens, reasoning_mode, content): + return _chat(model=model, content=content, max_tokens=max_tokens) + +def useLLM(model, content, max_tokens=6000): + return _chat(model=model, content=content, max_tokens=max_tokens) diff --git a/run.py b/run.py new file mode 100644 index 00000000..532a3e95 --- /dev/null +++ b/run.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +""" +MeTTaClaw Runner + +Usage: + python run.py [script.metta] + +Environment variables: + OPENAI_API_KEY=... # For OpenAI + ANTHROPIC_API_KEY=... # For Anthropic + OLLAMA_API_BASE=... # For Ollama (local) + LLM_PROVIDER=... # Explicit provider selection + +Or create a .env file with your API keys. +""" + +import os +import sys +from pathlib import Path + +# Add project root to path +project_root = Path(__file__).parent +sys.path.insert(0, str(project_root)) + +# Import configuration loader +try: + from mettaclaw_config import load_config, validate_config, print_config_summary + CONFIG_AVAILABLE = True +except ImportError: + CONFIG_AVAILABLE = False + print("Warning: Configuration loader not available, using environment variables only") + +# Load configuration +if CONFIG_AVAILABLE: + config = load_config(project_root) + + # Print configuration summary if verbose + if os.environ.get('METTACLAW_VERBOSE', '').lower() == 'true': + print_config_summary(config) + + # Validate configuration + is_valid, errors = validate_config(config) + if not is_valid and not os.environ.get('METTACLAW_SKIP_VALIDATION'): + print("Configuration validation warnings:") + for error in errors: + print(f" - {error}") + print("\nSet METTACLAW_SKIP_VALIDATION=true to skip validation") + # Don't exit, allow running anyway + +# Import PeTTa +try: + from deps.PeTTa.python import petta +except ImportError as e: + print(f"Error: Failed to import PeTTa: {e}") + print("Make sure PeTTa is installed and in the deps/PeTTa directory") + sys.exit(1) + +# Import MeTTaClaw libraries +try: + from lib_mettaclaw import * +except Exception as e: + print(f"Warning: Failed to import some MeTTaClaw libraries: {e}") + +def run_script(script_path: str = None): + """Run a MeTTa script.""" + + # Default to run.metta + if script_path is None: + script_path = project_root / 'run.metta' + else: + script_path = Path(script_path) + + if not script_path.exists(): + print(f"Error: Script not found: {script_path}") + sys.exit(1) + + print(f"Running: {script_path}") + + # Initialize PeTTa + p = petta.PeTTa(verbose=True) + + # Load the script + try: + result = p.load_metta_file(str(script_path)) + print(f"Result: {result}") + except Exception as e: + print(f"Error running script: {e}") + sys.exit(1) + +if __name__ == '__main__': + script = sys.argv[1] if len(sys.argv) > 1 else None + run_script(script) diff --git a/run.sh b/run.sh new file mode 100755 index 00000000..7a15ada4 --- /dev/null +++ b/run.sh @@ -0,0 +1,341 @@ +#!/bin/bash +# MeTTaClaw Podman/Docker Runner Script +# ===================================== +# Usage: +# ./run.sh build Build the Docker image +# ./run.sh run [script] Run MeTTa script (default: run.metta) +# ./run.sh Short for: ./run.sh run +# ./run.sh start Run interactively +# ./run.sh sh Shell in container +# ./run.sh clean Remove image +# ./run.sh reset Clean + rebuild +# ./run.sh status Show status +# ./run.sh -h, --help Show help +# +set -euo pipefail + +# -------------------------------------------------------------------- +# Directory - always derive from script location +# -------------------------------------------------------------------- +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Load .env if present +if [[ -f "$SCRIPT_DIR/.env" ]]; then + set -a; source "$SCRIPT_DIR/.env"; set +a +fi + +# -------------------------------------------------------------------- +# Colors +# -------------------------------------------------------------------- +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +PURPLE='\033[0;35m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' + +# -------------------------------------------------------------------- +# Configuration (all parameterized) +# -------------------------------------------------------------------- +IMAGE_NAME="${IMAGE_NAME:-mettaclaw:latest}" +CONTAINER_NAME="${CONTAINER_NAME:-mettaclaw}" +DEFAULT_REPO="${METTACLAW_REPO:-https://github.com/autonull/mettaclaw}" +DEFAULT_BRANCH="${METTACLAW_BRANCH:-main}" +CONTAINER_PETTA_PATH="${CONTAINER_PETTA_PATH:-/opt/PeTTa}" +CONTAINER_METTACLAW_PATH="${CONTAINER_METTACLAW_PATH:-/opt/PeTTa/repos/mettaclaw}" +LOCAL_CODE_INDICATORS="${LOCAL_CODE_INDICATORS:-run.metta|lib_mettaclaw.metta|lib_nal.metta|src}" +DEFAULT_SCRIPT="${DEFAULT_SCRIPT:-run.metta}" +PETTA_REPO="${PETTA_REPO:-https://github.com/trueagi-io/PeTTa}" +PETTA_BRANCH="${PETTA_BRANCH:-main}" +FORCE_NO_CACHE="${FORCE_NO_CACHE:-false}" +PULL_ALWAYS="${PULL_ALWAYS:-false}" +RUNTIME="" + +# -------------------------------------------------------------------- +# Runtime Detection +# -------------------------------------------------------------------- +detect_runtime() { + if command -v podman &>/dev/null; then + RUNTIME="podman" + elif command -v docker &>/dev/null; then + RUNTIME="docker" + else + echo -e "${RED}Error: Neither podman nor docker is installed${NC}" + echo -e "${YELLOW}Install: https://podman.io or https://docker.io${NC}" + exit 1 + fi + echo -e "${CYAN}Using: $RUNTIME${NC}" +} + +# -------------------------------------------------------------------- +# Network Check +# -------------------------------------------------------------------- +check_network() { + curl --silent --fail --connect-timeout 5 https://github.com >/dev/null 2>&1 + return $? +} + +# -------------------------------------------------------------------- +# Image Check +# -------------------------------------------------------------------- +check_image() { + "$RUNTIME" image inspect "$IMAGE_NAME" >/dev/null 2>&1 +} + +# -------------------------------------------------------------------- +# Validate Config +# -------------------------------------------------------------------- +validate_config() { + if [[ -n "${OPENAI_API_KEY:-}" && ! "$OPENAI_API_KEY" =~ ^sk-[a-zA-Z0-9-]+$ ]]; then + echo -e "${YELLOW}Warning: OPENAI_API_KEY format looks unusual${NC}" + fi + CONTAINER_PETTA_PATH="${CONTAINER_PETTA_PATH#/}" + CONTAINER_METTACLAW_PATH="${CONTAINER_METTACLAW_PATH#/}" +} + +# -------------------------------------------------------------------- +# Help +# -------------------------------------------------------------------- +show_help() { + cat << EOF +${BOLD}MeTTaClaw Runner${NC} + +Usage: $0 [options] + +Commands: + build Build Docker image + run [script] Run script (default: $DEFAULT_SCRIPT) + start Run interactively + sh Shell in container + clean Remove image + reset Clean + rebuild + status Show status + -h, --help Show this help + +Environment: + OPENAI_API_KEY Required for LLM + METTACLAW_REPO (default: $DEFAULT_REPO) + IMAGE_NAME (default: $IMAGE_NAME) + +Examples: + $0 build + OPENAI_API_KEY=sk-... $0 run + METTACLAW_REPO=https://github.com/user/repo $0 build + +EOF +} + +banner() { + echo -e "${PURPLE}===== MeTTaClaw Runner =====${NC}" + echo "" +} + +# -------------------------------------------------------------------- +# Local Code Detection +# -------------------------------------------------------------------- +check_local_code() { + local IFS='|' + for f in $LOCAL_CODE_INDICATORS; do + [[ -e "$SCRIPT_DIR/$f" ]] && return 0 + done + return 1 +} + +# -------------------------------------------------------------------- +# Build +# -------------------------------------------------------------------- +cmd_build() { + echo -e "${BLUE}Building image...${NC}" + + check_network || { echo -e "${RED}No network${NC}"; exit 1; } + detect_runtime + + local repo="${METTACLAW_REPO:-$DEFAULT_REPO}" + local branch="${METTACLAW_BRANCH:-$DEFAULT_BRANCH}" + local petta_repo="${PETTA_REPO:-$PETTA_REPO}" + local petta_branch="${PETTA_BRANCH:-$PETTA_BRANCH}" + + echo -e "${CYAN}MeTTaClaw: $repo ($branch)${NC}" + echo -e "${CYAN}PeTTa: $petta_repo ($petta_branch)${NC}" + + check_local_code && echo -e "${GREEN}Local code detected${NC}" || echo -e "${YELLOW}No local code${NC}" + + cd "$SCRIPT_DIR" + + local -a args=() + [[ "$FORCE_NO_CACHE" == "true" ]] && args+=(--no-cache) + [[ "$PULL_ALWAYS" == "true" ]] && args+=(--pull) + + echo -e "${GREEN}Building...${NC}" + # Build with host network (fixes some container/netns issues) + "$RUNTIME" build --network host "${args[@]}" \ + --build-arg "METTACLAW_REPO=$repo" \ + --build-arg "METTACLAW_BRANCH=$branch" \ + --build-arg "PETTA_REPO=$petta_repo" \ + --build-arg "PETTA_BRANCH=$petta_branch" \ + --build-arg "CONTAINER_PETTA_PATH=$CONTAINER_PETTA_PATH" \ + --build-arg "CONTAINER_METTACLAW_PATH=$CONTAINER_METTACLAW_PATH" \ + -t "$IMAGE_NAME" . + + echo -e "${GREEN}Done!${NC}" +} + +# -------------------------------------------------------------------- +# Run +# -------------------------------------------------------------------- +cmd_run() { + local script="${1:-$DEFAULT_SCRIPT}" + + [[ -n "${OPENAI_API_KEY:-}" ]] || { echo -e "${RED}OPENAI_API_KEY required${NC}"; exit 1; } + + echo -e "${CYAN}Running: $script${NC}" + + validate_config + detect_runtime + + check_image || { echo -e "${RED}Image missing. Run: $0 build${NC}"; exit 1; } + + check_local_code + local has_local=$? + + cd "$SCRIPT_DIR" + + local vol="" + [[ $has_local -eq 0 ]] && vol="-v $SCRIPT_DIR:$CONTAINER_METTACLAW_PATH:ro" + + echo -e "${GREEN}Starting...${NC}" + "$RUNTIME" run --rm -it \ + --name "$CONTAINER_NAME" \ + -e "OPENAI_API_KEY=$OPENAI_API_KEY" \ + -e "TERM=xterm-256color" \ + -e "PETTA_PATH=$CONTAINER_PETTA_PATH" \ + $vol \ + -w "$CONTAINER_PETTA_PATH" \ + "$IMAGE_NAME" \ + sh -c "cp $CONTAINER_METTACLAW_PATH/run.metta . 2>/dev/null || true && sh run.sh $script" +} + +# -------------------------------------------------------------------- +# Interactive +# -------------------------------------------------------------------- +cmd_start() { + [[ -n "${OPENAI_API_KEY:-}" ]] || { echo -e "${RED}OPENAI_API_KEY required${NC}"; exit 1; } + + echo -e "${CYAN}Interactive mode${NC}" + + validate_config + detect_runtime + check_image || { echo -e "${RED}Image missing${NC}"; exit 1; } + check_local_code + local has_local=$? + + cd "$SCRIPT_DIR" + + local vol="" + [[ $has_local -eq 0 ]] && vol="-v $SCRIPT_DIR:$CONTAINER_METTACLAW_PATH:ro" + + "$RUNTIME" run --rm -it \ + --name "$CONTAINER_NAME" \ + -e "OPENAI_API_KEY=$OPENAI_API_KEY" \ + -e "TERM=xterm-256color" \ + -e "PETTA_PATH=$CONTAINER_PETTA_PATH" \ + $vol \ + -w "$CONTAINER_PETTA_PATH" \ + "$IMAGE_NAME" \ + sh -c "cp $CONTAINER_METTACLAW_PATH/run.metta . 2>/dev/null || true && sh run.sh $DEFAULT_SCRIPT" +} + +# -------------------------------------------------------------------- +# Shell +# -------------------------------------------------------------------- +cmd_sh() { + echo -e "${CYAN}Shell mode${NC}" + + detect_runtime + check_image || { echo -e "${RED}Image missing${NC}"; exit 1; } + check_local_code + local has_local=$? + + cd "$SCRIPT_DIR" + + local vol="" + [[ $has_local -eq 0 ]] && vol="-v $SCRIPT_DIR:$CONTAINER_METTACLAW_PATH:ro" + + "$RUNTIME" run --rm -it \ + --name "$CONTAINER_NAME" \ + -e "OPENAI_API_KEY=${OPENAI_API_KEY:-}" \ + -e "TERM=xterm-256color" \ + -e "PETTA_PATH=$CONTAINER_PETTA_PATH" \ + $vol \ + -w "$CONTAINER_PETTA_PATH" \ + "$IMAGE_NAME" \ + /bin/bash +} + +# -------------------------------------------------------------------- +# Clean +# -------------------------------------------------------------------- +cmd_clean() { + echo -e "${BLUE}Cleaning...${NC}" + + detect_runtime + "$RUNTIME" rm -f "$CONTAINER_NAME" 2>/dev/null || true + "$RUNTIME" rmi "$IMAGE_NAME" 2>/dev/null || echo -e "${YELLOW}Image not found${NC}" + + echo -e "${GREEN}Done${NC}" +} + +# -------------------------------------------------------------------- +# Status +# -------------------------------------------------------------------- +cmd_status() { + detect_runtime + + echo -e "${CYAN}Image: $IMAGE_NAME${NC}" + check_image && echo -e "${GREEN} Exists${NC}" || echo -e "${RED} Missing${NC}" + + echo -e "${CYAN}Container: $CONTAINER_NAME${NC}" + if "$RUNTIME" ps -a --format '{{.Names}}' 2>/dev/null | grep -q "^${CONTAINER_NAME}$"; then + echo -e "${YELLOW} Running${NC}" + else + echo -e "${GREEN} Not running${NC}" + fi + + check_local_code && echo -e "${GREEN}Local code${NC}" || echo -e "${YELLOW}No local code${NC}" +} + +# -------------------------------------------------------------------- +# Reset +# -------------------------------------------------------------------- +cmd_reset() { + cmd_clean + echo "" + cmd_build +} + +# -------------------------------------------------------------------- +# Main +# -------------------------------------------------------------------- +main() { + # No args = run default script + if [[ $# -eq 0 ]]; then + cmd_run + return $? + fi + + case "${1:-help}" in + build) cmd_build ;; + run) cmd_run "${2:-}" ;; + start) cmd_start ;; + sh) cmd_sh ;; + clean) cmd_clean ;; + reset) cmd_reset ;; + status) cmd_status ;; + -h|--help|help) show_help ;; + *) echo -e "${RED}Unknown: $1${NC}"; show_help; exit 1 ;; + esac +} + +main "$@" \ No newline at end of file diff --git a/src/loop.metta b/src/loop.metta index 32eff761..82d6fbab 100644 --- a/src/loop.metta +++ b/src/loop.metta @@ -53,11 +53,13 @@ ($_ (println! $lastmessage)) ($send (py-str ($prompt $lastmessage))) ($_ (println! (CHARS_SENT: (string_length $send) $send))) - ($respi (if (== (provider) OpenAI) - (useGPT (LLM) (maxOutputToken) (reasoningMode) $send) - (if (== (provider) Anthropic) - (py-call (lib_llm_ext.useClaude $send)) - (py-call (lib_llm_ext.useMiniMax $send))))) +($respi (if (== (provider) OpenAI) +(useGPT (LLM) (maxOutputToken) (reasoningMode) $send) +(if (== (provider) Anthropic) +(py-call (lib_llm_ext.useClaude $send)) +(if (== (provider) MiniMax) +(py-call (lib_llm_ext.useMiniMax $send)) +(py-call (lib_llm_ext.useLLM (LLM) $send (maxOutputToken) (reasoningMode))))))) ($resp (py-call (helper.balance_parentheses $respi))) ($response (if (== "(" (first_char $resp)) $resp (progn (println! $resp) (repr (REMEMBER:OUTPUT_NOTHING_ELSE_THAN: ((skill arg) ...)))))) ($sexpr (catch (sread $response))) From ed0c8e6036e1ea7d813fcb6478a2ebb9bfc6b85e Mon Sep 17 00:00:00 2001 From: me Date: Fri, 10 Apr 2026 09:48:55 -0400 Subject: [PATCH 02/10] updates --- README.md | 326 +++++++++--------------------------------------------- run.sh | 31 +++++- 2 files changed, 82 insertions(+), 275 deletions(-) diff --git a/README.md b/README.md index 55231e26..af77d630 100644 --- a/README.md +++ b/README.md @@ -1,323 +1,105 @@ -## MeTTaClaw 2 +# MeTTaClaw 2 -image - -An agentic AI system implemented in MeTTa, guided by the MeTTaClaw proposal and an agent core inspired by Nanobot. -Beyond basic tool use, it features embedding-based long-term memory represented entirely in MeTTa AtomSpace format. - -Long-term memory is deliberately maintained by the agent via `(remember string)` for adding memory items and `(query string)` for querying related memories. -The agent can learn and apply new skills and declarative knowledge through the use of memory items. - -In addition, an initial set of OpenClaw-like tools is implemented, including web search, file modification, communication channels, and access to the operating system shell and its associated tools. - -Simplicity of design, ease of prototyping, ease of extension, and transparent implementation in MeTTa were the primary design criteria. -The agent core comprises approximately 200 lines of code. - -**Special Features** - -- MeTTaClaw uses a token-efficient agentic loop, enabling low-cost long-term operation and embodiment in domains that require real-time learning and decision-making. - -- The agent can learn to represent its memories in different ways, including such that allow other Hyperon components to operate on the same memories within the same Atomspace. Each memory item is stored as a triplet `(timestamp, atom, embedding)`, while the agent remains flexible in choosing the representation for the atom itself. Consequently, the agent is not hardcoded to any particular memory representation, and different formats can co-exist in the same atom space. - -The following example demonstrates learning and decision-making in a textually represented grid-world environment adapted from [NACE](https://github.com/patham9/NACE): - -![mettaclaw_in_nace_world](https://github.com/user-attachments/assets/c6c01839-234d-4505-baf6-4f2f3787c7b9) - -This project also aims to explore the potential of Agentic Physical AI, a ROS2 package for mobile robots with manipulators is underway. +An agentic AI system in MeTTa with embedding-based long-term memory. --- -## Quick Start - -### Step 1: Install Dependencies - -First, get [SWI-Prolog](https://www.swi-prolog.org/). Then: +## Quick Start (Docker/Podman) -```bash -git clone https://github.com/trueagi-io/PeTTa -cd PeTTa -mkdir -p repos && git clone https://github.com/patham9/mettaclaw repos/mettaclaw -cd repos/mettaclaw -``` - -### Step 2: Configure Your LLM Provider - -MeTTaClaw supports 100+ LLM providers. Choose one: - -#### Option A: OpenAI (GPT-4, GPT-3.5) - -```bash -export OPENAI_API_KEY="sk-..." -python run.py -``` - -Get API key: https://platform.openai.com/api-keys - -#### Option B: Anthropic (Claude) +**The only commands you need:** ```bash -export ANTHROPIC_API_KEY="sk-ant-..." -python run.py -``` - -Get API key: https://console.anthropic.com/settings/keys - -#### Option C: Ollama (Local, Free) - -```bash -# Install Ollama -curl -fsSL https://ollama.ai/install.sh | sh - -# Pull a model -ollama pull llama3 +# 1. Build the image +./run.sh build -# Run MeTTaClaw -python run.py +# 2. Run (with your API key) +OPENAI_API_KEY=sk-... ./run.sh run ``` -Ollama auto-detects at `http://localhost:11434` - -#### Option D: Use a .env File - -Create a `.env` file in the project root: +That's it! Want it even simpler? Create a `.env` file: ```bash # .env -ANTHROPIC_API_KEY="sk-ant-..." -# or -OPENAI_API_KEY="sk-..." -# or -OLLAMA_API_BASE="http://localhost:11434" +OPENAI_API_KEY=sk-... ``` Then just run: ```bash -python run.py +./run.sh ``` -### Step 3: Run - -```bash -python run.py -``` +### Other Commands -Or with a custom script: ```bash -python run.py my_script.metta -``` - ---- - -## Configuration - -### Environment Variables - -| Variable | Description | Example | -|----------|-------------|---------| -| `OPENAI_API_KEY` | OpenAI API key | `sk-...` | -| `ANTHROPIC_API_KEY` | Anthropic API key | `sk-ant-...` | -| `OLLAMA_API_BASE` | Ollama server URL | `http://localhost:11434` | -| `OPENROUTER_API_KEY` | OpenRouter API key | `...` | -| `GROQ_API_KEY` | Groq API key | `gsk_...` | -| `TOGETHER_API_KEY` | Together AI key | `...` | -| `GOOGLE_API_KEY` | Google AI key | `...` | -| `AWS_ACCESS_KEY_ID` | AWS access key | `...` | -| `AWS_SECRET_ACCESS_KEY` | AWS secret key | `...` | -| `AZURE_API_KEY` | Azure OpenAI key | `...` | -| `LLM_PROVIDER` | Preferred provider | `anthropic` | -| `LLM_MODEL` | Override default model | `anthropic/claude-sonnet-4-20250514` | - -### Supported Providers - -| Provider | Env Variable | Default Model | Get Key | -|----------|-------------|---------------|---------| -| **OpenAI** | `OPENAI_API_KEY` | `openai/gpt-4o` | https://platform.openai.com | -| **Anthropic** | `ANTHROPIC_API_KEY` | `anthropic/claude-opus-4-20250514` | https://console.anthropic.com | -| **Ollama** | (none, local) | `ollama/llama3` | https://ollama.ai | -| **OpenRouter** | `OPENROUTER_API_KEY` | `openrouter/anthropic/claude-3-opus` | https://openrouter.ai | -| **Groq** | `GROQ_API_KEY` | `groq/llama3-70b-8192` | https://console.groq.com | -| **Together AI** | `TOGETHER_API_KEY` | `together_ai/meta-llama/Llama-3-70b` | https://api.together.ai | -| **Google** | `GOOGLE_API_KEY` | `google/gemini-pro` | https://console.cloud.google.com | -| **AWS Bedrock** | `AWS_ACCESS_KEY_ID` | `bedrock/anthropic.claude-3-opus` | https://aws.amazon.com | -| **Azure** | `AZURE_API_KEY` | `azure/gpt-4` | https://portal.azure.com | - -### MeTTa Configuration - -Create a `config.metta` file for MeTTa-based configuration: - -```metta -;; LLM Configuration -(configure provider "anthropic") -(configure LLM "anthropic/claude-opus-4-20250514") -(configure maxOutputToken 8192) -(configure reasoningMode "medium") - -;; Loop Configuration -(configure maxNewInputLoops 50) -(configure maxWakeLoops 1) -(configure sleepInterval 1) -(configure wakeupInterval 600) - -;; Memory Configuration -(configure memorySize 1000) -(configure memorySimilarityThreshold 0.75) +./run.sh start # Interactive chat mode +./run.sh sh # Shell for debugging +./run.sh status # Check status +./run.sh clean # Remove image ``` --- -## Usage +## LLM Providers -### Auto-install/Run +Set any of these in `.env`: -If PeTTa is already installed and the latest version pulled (v1.0.2 or latest commit), then, running the following MeTTa file from the root folder, installs and runs MeTTaClaw (assuming API key is set): - -```metta -!(import! &self (library lib_import)) -!(git-import! "https://github.com/patham9/mettaclaw.git") -!(import! &self (library mettaclaw lib_mettaclaw)) - -!(mettaclaw) -``` - -### Command Line - -```bash -# Run with default script -python run.py - -# Run with custom script -python run.py my_script.metta - -# Run with specific provider -OPENAI_API_KEY="sk-..." python run.py - -# Verbose output -METTACLAW_VERBOSE=true python run.py -``` - -### Docker/Podman - -Build and run MeTTaClaw in a container (no need to install SWI-Prolog): +| Provider | Variable | Example | +|----------|---------|---------| +| OpenAI | `OPENAI_API_KEY` | `sk-...` | +| Anthropic | `ANTHROPIC_API_KEY` | `sk-ant-...` | +| Ollama | `OLLAMA_API_BASE` | `http://localhost:11434` | +| Groq | `GROQ_API_KEY` | `gsk_...` | +| OpenRouter | `OPENROUTER_API_KEY` | `...` | +Ollama example: ```bash -# 1. Build the Docker image -./run.sh build - -# 2. Run (set your API key) -OPENAI_API_KEY="sk-..." ./run.sh -# Or with a custom script: -OPENAI_API_KEY="sk-..." ./run.sh run myscript.metta -``` - -Other commands: -```bash -./run.sh start # Interactive chat mode -./run.sh sh # Drop into shell for debugging -./run.sh clean # Remove image -./run.sh status # Check image/container status -``` - -Configure LLM provider at runtime: -```bash -ANTHROPIC_API_KEY="sk-ant-..." ./run.sh # Use Claude -OLLAMA_API_BASE="http://localhost:11434" ./run.sh # Use Ollama +# Install Ollama, then: +# ollama pull llama3 +OLLAMA_API_BASE=http://localhost:11434 OLLAMA_MODEL=llama3 ./run.sh ``` --- -## Examples - -### Basic Agent Loop - -```metta -!(import! &self (library lib_import)) -!(import! &self (library mettaclaw lib_mettaclaw)) - -!(mettaclaw) -``` +## Configuration (Optional) -### Custom Configuration +Create `.env` file: -```metta -!(import! &self (library lib_import)) -!(import! &self (library mettaclaw lib_mettaclaw)) - -;; Set custom LLM provider -(configure provider "groq") -(configure LLM "groq/llama3-70b-8192") -(configure maxOutputToken 4096) - -!(mettaclaw) -``` - -### Memory Operations - -```metta -;; Add to memory -(remember "The sky is blue") +```bash +# .env - minimal +OPENAI_API_KEY=sk-... -;; Query memory -(query "What color is the sky?") +# .env - Ollama (local, free) +OLLAMA_API_BASE=http://localhost:11434 +OLLAMA_MODEL=llama3 -;; Use in agent loop -!(mettaclaw) +# .env - Claude +ANTHROPIC_API_KEY=sk-ant-... ``` --- -## Illustrations - -**Long-Term Memory Recall:** - -image - -**Tool use:** - -image - -**Shell output of the actual invocation of the generated MeTTa code:** - -image +## Commands Reference -**System also added it into its Atom Space storage (embedding vector omitted):** - -image +| Command | Description | +|---------|-------------| +| `./run.sh` | Run default script | +| `./run.sh build` | Build Docker image | +| `./run.sh run` | Run with script | +| `./run.sh start` | Interactive mode | +| `./run.sh sh` | Debug shell | +| `./run.sh clean` | Remove image | +| `./run.sh status` | Show status | --- ## Documentation -- [CONFIG.md](CONFIG.md) - Comprehensive configuration guide -- [LITELLM_CONFIG.md](LITELLM_CONFIG.md) - LiteLLM provider documentation -- [LITELLM_SUMMARY.md](LITELLM_SUMMARY.md) - Implementation summary - -## Testing - -```bash -# Run configuration tests -python test_litellm.py - -# Test configuration loading -python mettaclaw_config.py -``` +For more details, see: +- [CONFIG.md](CONFIG.md) - Full configuration +- [LITELLM_CONFIG.md](LITELLM_CONFIG.md) - LLM providers -## Troubleshooting - -### "No API key found" -- Set the appropriate environment variable for your provider -- Or create a `.env` file with your API key - -### "Invalid model name" -- Use format: `provider/model-name` -- Check provider documentation for exact model names - -### "Ollama connection failed" -- Ensure Ollama is running: `ollama serve` -- Check URL: `export OLLAMA_API_BASE="http://localhost:11434"` - -### More help -See [CONFIG.md](CONFIG.md) for detailed troubleshooting. +--- ## License -[Your License Here] +[MIT](LICENSE) \ No newline at end of file diff --git a/run.sh b/run.sh index 7a15ada4..b97ab48a 100755 --- a/run.sh +++ b/run.sh @@ -51,6 +51,11 @@ PETTA_REPO="${PETTA_REPO:-https://github.com/trueagi-io/PeTTa}" PETTA_BRANCH="${PETTA_BRANCH:-main}" FORCE_NO_CACHE="${FORCE_NO_CACHE:-false}" PULL_ALWAYS="${PULL_ALWAYS:-false}" + +# LLM config +LLM_MODEL="${LLM_MODEL:-}" +OLLAMA_MODEL="${OLLAMA_MODEL:-llama3}" + RUNTIME="" # -------------------------------------------------------------------- @@ -188,9 +193,20 @@ cmd_build() { cmd_run() { local script="${1:-$DEFAULT_SCRIPT}" - [[ -n "${OPENAI_API_KEY:-}" ]] || { echo -e "${RED}OPENAI_API_KEY required${NC}"; exit 1; } + # Check for ANY provider key (not just OpenAI - supports Ollama, Claude, etc.) + local has_key=0 + [[ -n "${OPENAI_API_KEY:-}" ]] && has_key=1 + [[ -n "${ANTHROPIC_API_KEY:-}" ]] && has_key=1 + [[ -n "${OLLAMA_API_BASE:-}" ]] && has_key=1 + [[ -n "${OPENROUTER_API_KEY:-}" ]] && has_key=1 + [[ -n "${GROQ_API_KEY:-}" ]] && has_key=1 + + [[ $has_key -eq 0 ]] && { echo -e "${RED}No API key found. Set OPENAI_API_KEY, ANTHROPIC_API_KEY, or OLLAMA_API_BASE${NC}"; exit 1; } echo -e "${CYAN}Running: $script${NC}" + [[ -n "$OPENAI_API_KEY" ]] && echo -e "${CYAN}Provider: OpenAI${NC}" + [[ -n "$ANTHROPIC_API_KEY" ]] && echo -e "${CYAN}Provider: Anthropic${NC}" + [[ -n "$OLLAMA_API_BASE" ]] && echo -e "${CYAN}Provider: Ollama${NC}" validate_config detect_runtime @@ -208,7 +224,11 @@ cmd_run() { echo -e "${GREEN}Starting...${NC}" "$RUNTIME" run --rm -it \ --name "$CONTAINER_NAME" \ - -e "OPENAI_API_KEY=$OPENAI_API_KEY" \ + -e "OPENAI_API_KEY=${OPENAI_API_KEY:-}" \ + -e "ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}" \ + -e "OLLAMA_API_BASE=${OLLAMA_API_BASE:-}" \ + -e "LLM_MODEL=${LLM_MODEL:-}" \ + -e "OLLAMA_MODEL=${OLLAMA_MODEL:-}" \ -e "TERM=xterm-256color" \ -e "PETTA_PATH=$CONTAINER_PETTA_PATH" \ $vol \ @@ -221,7 +241,12 @@ cmd_run() { # Interactive # -------------------------------------------------------------------- cmd_start() { - [[ -n "${OPENAI_API_KEY:-}" ]] || { echo -e "${RED}OPENAI_API_KEY required${NC}"; exit 1; } + local has_key=0 + [[ -n "${OPENAI_API_KEY:-}" ]] && has_key=1 + [[ -n "${ANTHROPIC_API_KEY:-}" ]] && has_key=1 + [[ -n "${OLLAMA_API_BASE:-}" ]] && has_key=1 + + [[ $has_key -eq 0 ]] && { echo -e "${RED}No API key found${NC}"; exit 1; } echo -e "${CYAN}Interactive mode${NC}" From aa0501af0a3e35b86dca7b7eb631d9e74b9c664b Mon Sep 17 00:00:00 2001 From: me Date: Fri, 10 Apr 2026 11:37:29 -0400 Subject: [PATCH 03/10] updates --- .dockerignore | 30 ++++ .env.example | 138 +++++---------- Dockerfile | 76 +++----- README.md | 199 +++++++++++++++------ container_run.sh | 73 ++++++++ lib_llm_ext.py | 2 +- run.py | 91 ++++------ run.sh | 446 ++++++++++++----------------------------------- 8 files changed, 468 insertions(+), 587 deletions(-) create mode 100644 .dockerignore create mode 100755 container_run.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..5f181571 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,30 @@ +# Don't leak secrets into the image +.env +.env.* +!.env.example + +# Local build artifacts +__pycache__ +*.pyc +*.pyo + +# Git +.git +.gitignore + +# IDE +.vscode +.idea +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Memory (may contain sensitive data) +memory/ + +# Host-only scripts (not needed inside container) +run.sh +run.py diff --git a/.env.example b/.env.example index f045dcb5..ec4e2ea1 100644 --- a/.env.example +++ b/.env.example @@ -1,109 +1,65 @@ -# MeTTaClaw Configuration File -# Copy this file to .env.local and fill in your API keys -# The .env.local file is gitignored, so your secrets are safe +# MeTTaClaw Configuration +# This file is auto-copied to .env on first run. +# The .env file is gitignored — your secrets are safe. +# +# Just fill in the API key for ONE provider below. +# The system auto-detects which provider to use. # =================================================================== -# SELECT YOUR PROVIDER -# Just fill in the API key for your chosen provider below. -# The system will auto-detect which provider to use. +# OPENAI (GPT-4, GPT-4o, o1, o3) +# Get a key: https://platform.openai.com/api-keys # =================================================================== - -# ------------------------------------------------------------------- -# OpenAI (GPT-4, GPT-3.5, o1, o3) -# Get API key: https://platform.openai.com/api-keys -# ------------------------------------------------------------------- OPENAI_API_KEY="sk-..." -# ------------------------------------------------------------------- -# Anthropic (Claude family) -# Get API key: https://console.anthropic.com/settings/keys -# ------------------------------------------------------------------- -ANTHROPIC_API_KEY="sk-ant-..." +# =================================================================== +# ANTHROPIC (Claude) +# Get a key: https://console.anthropic.com/settings/keys +# =================================================================== +# ANTHROPIC_API_KEY="sk-ant-..." -# ------------------------------------------------------------------- -# Ollama (Local models - FREE, no API key needed) -# Install: https://ollama.ai +# =================================================================== +# OLLAMA (Local, FREE — install from https://ollama.ai) # Then run: ollama pull llama3 -# ------------------------------------------------------------------- -OLLAMA_API_BASE="http://localhost:11434" - -# ------------------------------------------------------------------- -# OpenRouter (Access 100+ models through one API) -# Get API key: https://openrouter.ai/settings/keys -# ------------------------------------------------------------------- -OPENROUTER_API_KEY="" - -# ------------------------------------------------------------------- -# Groq (Fast inference) -# Get API key: https://console.groq.com/keys -# ------------------------------------------------------------------- -GROQ_API_KEY="gsk_..." - -# ------------------------------------------------------------------- -# Together AI (Open-source models) -# Get API key: https://api.together.ai/settings/api-keys -# ------------------------------------------------------------------- -TOGETHER_API_KEY="" - -# ------------------------------------------------------------------- -# Google Vertex AI -# Get API key: https://console.cloud.google.com/apis/credentials -# ------------------------------------------------------------------- -GOOGLE_API_KEY="" -GOOGLE_APPLICATION_CREDENTIALS="/path/to/credentials.json" - -# ------------------------------------------------------------------- -# AWS Bedrock -# AWS credentials from: https://console.aws.amazon.com/iam -# ------------------------------------------------------------------- -AWS_ACCESS_KEY_ID="" -AWS_SECRET_ACCESS_KEY="" -AWS_REGION_NAME="us-east-1" - -# ------------------------------------------------------------------- -# Azure OpenAI -# Azure OpenAI credentials from: https://portal.azure.com -# ------------------------------------------------------------------- -AZURE_API_KEY="" -AZURE_API_BASE="https://your-resource.openai.azure.com" -AZURE_API_VERSION="2024-02-15-preview" +# =================================================================== +# OLLAMA_API_BASE="http://localhost:11434" +# OLLAMA_MODEL="llama3" -# ------------------------------------------------------------------- -# Custom OpenAI-Compatible Endpoint -# ------------------------------------------------------------------- -CUSTOM_API_KEY="" -CUSTOM_API_BASE="https://your-server.com/v1" +# =================================================================== +# OPENROUTER (100+ models, one API) +# Get a key: https://openrouter.ai/settings/keys +# =================================================================== +# OPENROUTER_API_KEY="" # =================================================================== -# OPTIONAL SETTINGS +# GROQ (Fast inference) +# Get a key: https://console.groq.com/keys # =================================================================== +# GROQ_API_KEY="" -# Provider preference (if multiple keys provided) -# Options: openai, anthropic, ollama, openrouter, groq, together, google, aws, azure, custom -# LLM_PROVIDER="anthropic" +# =================================================================== +# OTHER PROVIDERS (uncomment and fill in as needed) +# =================================================================== -# Fallback providers (if primary fails) -# LLM_FALLBACK="openai,ollama" +# Ollama custom model +# OLLAMA_MODEL="mistral" -# Model overrides -# CLAUDE_MODEL="anthropic/claude-opus-4-20250514" -# GPT_MODEL="openai/gpt-4o" -# OLLAMA_MODEL="llama3" +# Together AI +# TOGETHER_API_KEY="" -# Token and reasoning settings -# LLM_MAX_TOKENS=8192 -# LLM_REASONING_MODE="medium" +# Google Vertex AI +# GOOGLE_API_KEY="" +# GOOGLE_APPLICATION_CREDENTIALS="/path/to/credentials.json" -# Loop settings -# MAX_NEW_INPUT_LOOPS=50 -# MAX_WAKE_LOOPS=1 -# SLEEP_INTERVAL=1 -# WAKEUP_INTERVAL=600 +# AWS Bedrock +# AWS_ACCESS_KEY_ID="" +# AWS_SECRET_ACCESS_KEY="" +# AWS_REGION_NAME="us-east-1" -# Memory settings -# MEMORY_SIZE=1000 -# MEMORY_SIMILARITY_THRESHOLD=0.75 +# Azure OpenAI +# AZURE_API_KEY="" +# AZURE_API_BASE="https://your-resource.openai.azure.com" +# AZURE_API_VERSION="2024-02-15-preview" -# Channel settings -# CHANNEL_TIMEOUT=30 -# MAX_MESSAGE_LENGTH=2000 +# Custom OpenAI-compatible endpoint +# CUSTOM_API_KEY="" +# CUSTOM_API_BASE="https://your-server.com/v1" diff --git a/Dockerfile b/Dockerfile index 60a271a4..41eb43ae 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,76 +1,42 @@ # MeTTaClaw Dockerfile # ======================================================================== -# Podman-compatible Docker image for MeTTaClaw -# ======================================================================== -# Base image FROM archlinux:latest -# PeTTa repo and branch -ARG PETTA_REPO=https://github.com/trueagi-io/PeTTa -ARG PETTA_BRANCH=main - -# MeTTaClaw repo and branch -ARG METTACLAW_REPO=https://github.com/autonull/mettaclaw -ARG METTACLAW_BRANCH=main - -# Container paths -ARG CONTAINER_PETTA_PATH=/opt/PeTTa -ARG CONTAINER_METTACLAW_PATH=/opt/PeTTa/repos/mettaclaw - -# ======================================================================== -# Environment -# ======================================================================== ENV TERM=xterm-256color +ENV PETTA_PATH=/opt/PeTTa +ENV PATH="/opt/PeTTa:${PATH}" # ======================================================================== -# Install Dependencies (Arch Linux uses pacman) +# Install system dependencies # ======================================================================== -RUN pacman-key --init && pacman -Sy --noconfirm \ - base-devel \ - git \ - curl \ - ca-certificates \ - python \ - python-pip \ - libffi \ - openssl \ - cargo \ - rust \ - cmake \ - wget \ - gmp \ - libedit \ - readline \ - libarchive \ - pcre \ - libyaml \ - swi-prolog \ +RUN pacman-key --init && \ + pacman -Sy --noconfirm \ + base-devel git curl ca-certificates python python-pip \ + libffi openssl cargo rust cmake wget \ + gmp libedit readline libarchive pcre libyaml swi-prolog \ || true -# Install janus for Python interop (optional - can work without) -RUN pip3 install --break-system-packages janus || true +RUN pip3 install --break-system-packages janus-swi janus litellm || true # ======================================================================== -# Clone PeTTa +# Clone PeTTa (the execution engine) # ======================================================================== -RUN git clone --depth 1 --branch ${PETTA_BRANCH} ${PETTA_REPO} ${CONTAINER_PETTA_PATH} +RUN git clone --depth 1 --branch main \ + https://github.com/trueagi-io/PeTTa /opt/PeTTa # ======================================================================== -# Clone MeTTaClaw +# Clone MeTTaClaw (the agent code) # ======================================================================== -RUN git clone --depth 1 --branch ${METTACLAW_BRANCH} ${METTACLAW_REPO} ${CONTAINER_METTACLAW_PATH} +RUN git clone --depth 1 --branch main \ + https://github.com/autonull/mettaclaw /opt/PeTTa/repos/mettaclaw # ======================================================================== -# Setup +# Setup container runner # ======================================================================== -WORKDIR ${CONTAINER_PETTA_PATH} - -RUN cp ${CONTAINER_METTACLAW_PATH}/run.metta ./ - -ENV PETTA_PATH=${CONTAINER_PETTA_PATH} -ENV PATH="${CONTAINER_PETTA_PATH}:${PATH}" - -RUN chmod +x run.sh +WORKDIR /opt/PeTTa +COPY container_run.sh /opt/PeTTa/container_run.sh +RUN chmod +x /opt/PeTTa/container_run.sh -CMD ["/bin/bash", "-c", "echo 'MeTTaClaw ready!'; echo 'To run: ./run.sh run.metta'"] \ No newline at end of file +ENTRYPOINT [] +CMD ["/opt/PeTTa/container_run.sh"] diff --git a/README.md b/README.md index af77d630..e826b2ce 100644 --- a/README.md +++ b/README.md @@ -1,105 +1,200 @@ -# MeTTaClaw 2 +# MeTTaClaw An agentic AI system in MeTTa with embedding-based long-term memory. --- -## Quick Start (Docker/Podman) +## Quick Start -**The only commands you need:** +### 1. Build the image ```bash -# 1. Build the image ./run.sh build - -# 2. Run (with your API key) -OPENAI_API_KEY=sk-... ./run.sh run ``` -That's it! Want it even simpler? Create a `.env` file: +### 2. Set your API key + +On first run, a `.env` file is auto-created from the template. Edit it: ```bash -# .env -OPENAI_API_KEY=sk-... +# OpenAI +OPENAI_API_KEY="sk-..." ``` -Then just run: +Or pass it inline: + ```bash -./run.sh +OPENAI_API_KEY="sk-..." ./run.sh ``` -### Other Commands +### 3. Run ```bash -./run.sh start # Interactive chat mode -./run.sh sh # Shell for debugging -./run.sh status # Check status -./run.sh clean # Remove image +./run.sh ``` +That's it. + --- ## LLM Providers -Set any of these in `.env`: +Pick **one** provider and set the corresponding environment variable. The system auto-detects which one to use. + +### OpenAI (GPT-4, GPT-4o, o1, o3) -| Provider | Variable | Example | -|----------|---------|---------| -| OpenAI | `OPENAI_API_KEY` | `sk-...` | -| Anthropic | `ANTHROPIC_API_KEY` | `sk-ant-...` | -| Ollama | `OLLAMA_API_BASE` | `http://localhost:11434` | -| Groq | `GROQ_API_KEY` | `gsk_...` | -| OpenRouter | `OPENROUTER_API_KEY` | `...` | +```bash +# .env +OPENAI_API_KEY="sk-..." +``` -Ollama example: ```bash -# Install Ollama, then: -# ollama pull llama3 -OLLAMA_API_BASE=http://localhost:11434 OLLAMA_MODEL=llama3 ./run.sh +# Or inline +OPENAI_API_KEY="sk-..." ./run.sh ``` ---- +Get a key: https://platform.openai.com/api-keys + +### Anthropic (Claude) + +```bash +# .env +ANTHROPIC_API_KEY="sk-ant-..." +``` + +Get a key: https://console.anthropic.com/settings/keys + +### Ollama (Local, Free) + +Install [Ollama](https://ollama.ai), pull a model, then: -## Configuration (Optional) +```bash +# .env +OLLAMA_API_BASE="http://localhost:11434" +OLLAMA_MODEL="llama3" +``` -Create `.env` file: +```bash +# Inline with a specific model (e.g., Qwen3 8B GGUF) +OLLAMA_API_BASE="http://localhost:11434" \ +OLLAMA_MODEL="hf.co/bartowski/Qwen_Qwen3-8B-GGUF:Q6_K" \ +./run.sh +``` ```bash -# .env - minimal -OPENAI_API_KEY=sk-... +# One-time setup +ollama pull llama3 +``` -# .env - Ollama (local, free) -OLLAMA_API_BASE=http://localhost:11434 -OLLAMA_MODEL=llama3 +### OpenRouter (100+ models, one API) -# .env - Claude -ANTHROPIC_API_KEY=sk-ant-... +```bash +# .env +OPENROUTER_API_KEY="..." ``` +Get a key: https://openrouter.ai/settings/keys + +### Groq (Fast inference) + +```bash +# .env +GROQ_API_KEY="gsk_..." +``` + +Get a key: https://console.groq.com/keys + +### Other Providers + +MeTTaClaw uses [LiteLLM](https://docs.litellm.ai/) under the hood, so it supports any provider LiteLLM supports: Together AI, Google Vertex AI, AWS Bedrock, Azure OpenAI, and custom OpenAI-compatible endpoints. + +See `.env.example` for all available options. + --- -## Commands Reference +## Commands | Command | Description | |---------|-------------| -| `./run.sh` | Run default script | -| `./run.sh build` | Build Docker image | -| `./run.sh run` | Run with script | -| `./run.sh start` | Interactive mode | -| `./run.sh sh` | Debug shell | -| `./run.sh clean` | Remove image | -| `./run.sh status` | Show status | +| `./run.sh` | Run `run.metta` | +| `./run.sh build` | Build the container image | +| `./run.sh run [script.metta]` | Run a specific script | +| `./run.sh shell` | Open a shell in the container | +| `./run.sh clean` | Remove the image | +| `./run.sh status` | Check build status | +| `./run.sh -h` | Show help | + +--- + +## Examples + +```bash +# Build +./run.sh build + +# Run with inline API key +OPENAI_API_KEY="sk-..." ./run.sh + +# Run a specific script +OPENAI_API_KEY="sk-..." ./run.sh run myscript.metta + +# Run with Ollama (local) +OLLAMA_API_BASE="http://localhost:11434" OLLAMA_MODEL="llama3" ./run.sh + +# Run with Ollama and a GGUF model +OLLAMA_API_BASE="http://localhost:11434" \ +OLLAMA_MODEL="hf.co/bartowski/Qwen_Qwen3-8B-GGUF:Q6_K" \ +./run.sh + +# Run with Claude +ANTHROPIC_API_KEY="sk-ant-..." ./run.sh + +# Debug shell inside container +./run.sh shell + +# Check status +./run.sh status +``` + +--- + +## How It Works + +``` +Host (run.sh) Container (/opt/PeTTa) +──────────── ────────────────────── +./run.sh build ──build──▶ PeTTa + MeTTaClaw cloned +./run.sh run ──exec──▶ container_run.sh + ↓ + provider_init.metta (auto-detected from env) + ↓ + run.metta → PeTTa → LLM +``` + +- **`run.sh`** (host) — builds and runs the container +- **`container_run.sh`** (inside) — detects provider from env vars, loads MeTTa, runs PeTTa +- **`lib_llm_ext.py`** — Python LLM interface using LiteLLM (supports all providers) + +### Local Development + +Local MeTTa files (`run.metta`, `lib_mettaclaw.metta`, `lib_nal.metta`, `lib_llm_ext.py`, `src/`) are mounted read-only into the container. The `memory/` directory remains writable inside. Edit files on your host and they're available on next run — no rebuild needed. --- -## Documentation +## Project Structure -For more details, see: -- [CONFIG.md](CONFIG.md) - Full configuration -- [LITELLM_CONFIG.md](LITELLM_CONFIG.md) - LLM providers +| File | Purpose | +|------|---------| +| `run.sh` | Host-side runner (build, run, shell, clean) | +| `run.py` | Python runner (local PeTTa invocation) | +| `container_run.sh` | Inside-container runner (provider detection, PeTTa) | +| `Dockerfile` | Container image definition | +| `lib_llm_ext.py` | Python LLM wrapper (LiteLLM, all providers) | +| `.env.example` | Configuration template | +| `.env` | Your config (gitignored) | --- ## License -[MIT](LICENSE) \ No newline at end of file +[MIT](LICENSE) diff --git a/container_run.sh b/container_run.sh new file mode 100755 index 00000000..0fe39051 --- /dev/null +++ b/container_run.sh @@ -0,0 +1,73 @@ +#!/bin/sh +# Inside-container runner — invokes PeTTa on a .metta file +# Usage: ./container_run.sh [script.metta] +set -e + +cd /opt/PeTTa + +# Copy MeTTaClaw files into working dir if not already present +METTACLAW_DIR="/opt/PeTTa/repos/mettaclaw" +if [ -d "$METTACLAW_DIR" ]; then + cp "$METTACLAW_DIR/run.metta" . 2>/dev/null || true + cp "$METTACLAW_DIR/lib_mettaclaw.metta" . 2>/dev/null || true + cp "$METTACLAW_DIR/lib_nal.metta" . 2>/dev/null || true + cp -r "$METTACLAW_DIR/src" . 2>/dev/null || true + # memory/ stays from the cloned repo (writable) — don't overwrite + if [ ! -d "memory" ]; then + cp -r "$METTACLAW_DIR/memory" . 2>/dev/null || true + fi +fi + +SCRIPT="${1:-run.metta}" + +if [ ! -f "$SCRIPT" ]; then + echo "Error: Script not found: $SCRIPT" >&2 + exit 1 +fi + +# Detect LLM provider from env vars and generate provider override +PROVIDER_INIT="" +if [ -n "${OLLAMA_API_BASE:-}" ]; then + PROVIDER_MODEL="${OLLAMA_MODEL:-llama3}" + PROVIDER_INIT="(= (provider) Ollama) +(= (LLM) ollama_chat/$PROVIDER_MODEL)" +elif [ -n "${OPENAI_API_KEY:-}" ]; then + PROVIDER_INIT="(= (provider) OpenAI) +(= (LLM) gpt-4o)" +elif [ -n "${ANTHROPIC_API_KEY:-}" ]; then + PROVIDER_INIT="(= (provider) Anthropic) +(= (LLM) claude-sonnet-4-20250514)" +elif [ -n "${OPENROUTER_API_KEY:-}" ]; then + PROVIDER_INIT="(= (provider) OpenAI) +(= (LLM) openrouter/auto)" +elif [ -n "${GROQ_API_KEY:-}" ]; then + PROVIDER_MODEL="${OLLAMA_MODEL:-llama-3.3-70b-versatile}" + PROVIDER_INIT="(= (provider) OpenAI) +(= (LLM) groq/$PROVIDER_MODEL)" +fi + +# Write provider config if detected +if [ -n "$PROVIDER_INIT" ]; then + printf '%s\n' "$PROVIDER_INIT" > /opt/PeTTa/provider_init.metta + echo "Provider auto-detected:" + printf '%s\n' "$PROVIDER_INIT" | head -1 +fi + +echo "Running: $SCRIPT" +exec python3 -c " +import sys, os + +sys.path.insert(0, '/opt/PeTTa/python') +from petta import PeTTa +p = PeTTa(verbose=True) + +# Load provider override if it exists +provider_init = os.path.join('/opt/PeTTa', 'provider_init.metta') +if os.path.exists(provider_init): + print(f'Loading provider config: {provider_init}') + p.load_metta_file(provider_init) + +# Load the main script +result = p.load_metta_file(sys.argv[1]) +print(f'Result: {result}') +" "$SCRIPT" diff --git a/lib_llm_ext.py b/lib_llm_ext.py index 395cdc88..3d22aa9c 100644 --- a/lib_llm_ext.py +++ b/lib_llm_ext.py @@ -32,5 +32,5 @@ def useClaude(content): def useGPT(model, max_tokens, reasoning_mode, content): return _chat(model=model, content=content, max_tokens=max_tokens) -def useLLM(model, content, max_tokens=6000): +def useLLM(model, content, max_tokens=6000, reasoning_mode=None): return _chat(model=model, content=content, max_tokens=max_tokens) diff --git a/run.py b/run.py index 532a3e95..6feafe81 100644 --- a/run.py +++ b/run.py @@ -1,92 +1,69 @@ #!/usr/bin/env python3 """ -MeTTaClaw Runner +MeTTaClaw Runner — runs MeTTa scripts via PeTTa. Usage: python run.py [script.metta] - -Environment variables: - OPENAI_API_KEY=... # For OpenAI - ANTHROPIC_API_KEY=... # For Anthropic - OLLAMA_API_BASE=... # For Ollama (local) - LLM_PROVIDER=... # Explicit provider selection - -Or create a .env file with your API keys. + +Environment: + OPENAI_API_KEY=... # OpenAI + ANTHROPIC_API_KEY=... # Anthropic + OLLAMA_API_BASE=... # Ollama local + OLLAMA_MODEL=... # Ollama model name + +Or create a .env file with your keys. """ import os import sys from pathlib import Path -# Add project root to path project_root = Path(__file__).parent -sys.path.insert(0, str(project_root)) -# Import configuration loader -try: - from mettaclaw_config import load_config, validate_config, print_config_summary - CONFIG_AVAILABLE = True -except ImportError: - CONFIG_AVAILABLE = False - print("Warning: Configuration loader not available, using environment variables only") - -# Load configuration -if CONFIG_AVAILABLE: - config = load_config(project_root) - - # Print configuration summary if verbose - if os.environ.get('METTACLAW_VERBOSE', '').lower() == 'true': - print_config_summary(config) - - # Validate configuration - is_valid, errors = validate_config(config) - if not is_valid and not os.environ.get('METTACLAW_SKIP_VALIDATION'): - print("Configuration validation warnings:") - for error in errors: - print(f" - {error}") - print("\nSet METTACLAW_SKIP_VALIDATION=true to skip validation") - # Don't exit, allow running anyway +# Load .env if present (simple parser, no dependencies) +env_file = project_root / ".env" +if env_file.exists(): + for line in env_file.read_text().splitlines(): + line = line.strip() + if line and not line.startswith("#") and "=" in line: + key, _, value = line.partition("=") + key = key.strip() + value = value.strip().strip("\"'") + os.environ.setdefault(key, value) # Import PeTTa try: - from deps.PeTTa.python import petta + sys.path.insert(0, str(project_root / "deps" / "PeTTa" / "python")) + from petta import PeTTa except ImportError as e: - print(f"Error: Failed to import PeTTa: {e}") - print("Make sure PeTTa is installed and in the deps/PeTTa directory") + print(f"Error: PeTTa not found at deps/PeTTa/python") + print(f"Clone it with: git clone https://github.com/trueagi-io/PeTTa deps/PeTTa") + print(f"Or use the container: ./run.sh build && ./run.sh run") + print(f"Details: {e}") sys.exit(1) -# Import MeTTaClaw libraries -try: - from lib_mettaclaw import * -except Exception as e: - print(f"Warning: Failed to import some MeTTaClaw libraries: {e}") -def run_script(script_path: str = None): - """Run a MeTTa script.""" - - # Default to run.metta +def run_script(script_path=None): if script_path is None: - script_path = project_root / 'run.metta' + script_path = project_root / "run.metta" else: script_path = Path(script_path) - + if not script_path.exists(): print(f"Error: Script not found: {script_path}") sys.exit(1) - + print(f"Running: {script_path}") - - # Initialize PeTTa - p = petta.PeTTa(verbose=True) - - # Load the script + p = PeTTa(verbose=True) + try: result = p.load_metta_file(str(script_path)) print(f"Result: {result}") except Exception as e: - print(f"Error running script: {e}") + print(f"Error: {e}") sys.exit(1) -if __name__ == '__main__': + +if __name__ == "__main__": script = sys.argv[1] if len(sys.argv) > 1 else None run_script(script) diff --git a/run.sh b/run.sh index b97ab48a..6bd9e24e 100755 --- a/run.sh +++ b/run.sh @@ -1,366 +1,150 @@ #!/bin/bash -# MeTTaClaw Podman/Docker Runner Script -# ===================================== -# Usage: -# ./run.sh build Build the Docker image -# ./run.sh run [script] Run MeTTa script (default: run.metta) -# ./run.sh Short for: ./run.sh run -# ./run.sh start Run interactively -# ./run.sh sh Shell in container -# ./run.sh clean Remove image -# ./run.sh reset Clean + rebuild -# ./run.sh status Show status -# ./run.sh -h, --help Show help +# MeTTaClaw Runner — simplified +# Usage: ./run.sh [command] [options] # +# Commands: +# build Build the container image +# run [script] Run a MeTTa script (default: run.metta) +# shell Open a shell in the container +# clean Remove the container image +# status Show build status +# -h, --help Show help +# +# Environment: Set API keys in .env or inline: +# OPENAI_API_KEY=sk-... ./run.sh run +# OLLAMA_API_BASE=http://localhost:11434 ./run.sh run set -euo pipefail -# -------------------------------------------------------------------- -# Directory - always derive from script location -# -------------------------------------------------------------------- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# Load .env if present -if [[ -f "$SCRIPT_DIR/.env" ]]; then - set -a; source "$SCRIPT_DIR/.env"; set +a +# Auto-create .env from example if missing +if [[ ! -f "$SCRIPT_DIR/.env" && -f "$SCRIPT_DIR/.env.example" ]]; then + cp "$SCRIPT_DIR/.env.example" "$SCRIPT_DIR/.env" + echo "Created .env from template — edit it with your API keys" fi -# -------------------------------------------------------------------- -# Colors -# -------------------------------------------------------------------- -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -PURPLE='\033[0;35m' -CYAN='\033[0;36m' -BOLD='\033[1m' -NC='\033[0m' +[[ -f "$SCRIPT_DIR/.env" ]] && set -a; source "$SCRIPT_DIR/.env"; set +a -# -------------------------------------------------------------------- -# Configuration (all parameterized) -# -------------------------------------------------------------------- IMAGE_NAME="${IMAGE_NAME:-mettaclaw:latest}" -CONTAINER_NAME="${CONTAINER_NAME:-mettaclaw}" -DEFAULT_REPO="${METTACLAW_REPO:-https://github.com/autonull/mettaclaw}" -DEFAULT_BRANCH="${METTACLAW_BRANCH:-main}" -CONTAINER_PETTA_PATH="${CONTAINER_PETTA_PATH:-/opt/PeTTa}" -CONTAINER_METTACLAW_PATH="${CONTAINER_METTACLAW_PATH:-/opt/PeTTa/repos/mettaclaw}" -LOCAL_CODE_INDICATORS="${LOCAL_CODE_INDICATORS:-run.metta|lib_mettaclaw.metta|lib_nal.metta|src}" -DEFAULT_SCRIPT="${DEFAULT_SCRIPT:-run.metta}" -PETTA_REPO="${PETTA_REPO:-https://github.com/trueagi-io/PeTTa}" -PETTA_BRANCH="${PETTA_BRANCH:-main}" -FORCE_NO_CACHE="${FORCE_NO_CACHE:-false}" -PULL_ALWAYS="${PULL_ALWAYS:-false}" - -# LLM config -LLM_MODEL="${LLM_MODEL:-}" -OLLAMA_MODEL="${OLLAMA_MODEL:-llama3}" +CONTAINER_WORKDIR="/opt/PeTTa" +# Detect runtime RUNTIME="" +if command -v podman &>/dev/null; then + RUNTIME="podman" +elif command -v docker &>/dev/null; then + RUNTIME="docker" +else + echo "Error: Install podman or docker" >&2; exit 1 +fi -# -------------------------------------------------------------------- -# Runtime Detection -# -------------------------------------------------------------------- -detect_runtime() { - if command -v podman &>/dev/null; then - RUNTIME="podman" - elif command -v docker &>/dev/null; then - RUNTIME="docker" - else - echo -e "${RED}Error: Neither podman nor docker is installed${NC}" - echo -e "${YELLOW}Install: https://podman.io or https://docker.io${NC}" - exit 1 - fi - echo -e "${CYAN}Using: $RUNTIME${NC}" -} - -# -------------------------------------------------------------------- -# Network Check -# -------------------------------------------------------------------- -check_network() { - curl --silent --fail --connect-timeout 5 https://github.com >/dev/null 2>&1 - return $? -} - -# -------------------------------------------------------------------- -# Image Check -# -------------------------------------------------------------------- -check_image() { - "$RUNTIME" image inspect "$IMAGE_NAME" >/dev/null 2>&1 -} - -# -------------------------------------------------------------------- -# Validate Config -# -------------------------------------------------------------------- -validate_config() { - if [[ -n "${OPENAI_API_KEY:-}" && ! "$OPENAI_API_KEY" =~ ^sk-[a-zA-Z0-9-]+$ ]]; then - echo -e "${YELLOW}Warning: OPENAI_API_KEY format looks unusual${NC}" - fi - CONTAINER_PETTA_PATH="${CONTAINER_PETTA_PATH#/}" - CONTAINER_METTACLAW_PATH="${CONTAINER_METTACLAW_PATH#/}" -} - -# -------------------------------------------------------------------- -# Help -# -------------------------------------------------------------------- -show_help() { - cat << EOF -${BOLD}MeTTaClaw Runner${NC} - -Usage: $0 [options] - -Commands: - build Build Docker image - run [script] Run script (default: $DEFAULT_SCRIPT) - start Run interactively - sh Shell in container - clean Remove image - reset Clean + rebuild - status Show status - -h, --help Show this help - -Environment: - OPENAI_API_KEY Required for LLM - METTACLAW_REPO (default: $DEFAULT_REPO) - IMAGE_NAME (default: $IMAGE_NAME) - -Examples: - $0 build - OPENAI_API_KEY=sk-... $0 run - METTACLAW_REPO=https://github.com/user/repo $0 build - -EOF -} - -banner() { - echo -e "${PURPLE}===== MeTTaClaw Runner =====${NC}" - echo "" +# Check API key exists (any provider) +has_api_key() { + [[ -n "${OPENAI_API_KEY:-}" || -n "${ANTHROPIC_API_KEY:-}" || \ + -n "${OLLAMA_API_BASE:-}" || -n "${OPENROUTER_API_KEY:-}" || \ + -n "${GROQ_API_KEY:-}" ]] } -# -------------------------------------------------------------------- -# Local Code Detection -# -------------------------------------------------------------------- -check_local_code() { - local IFS='|' - for f in $LOCAL_CODE_INDICATORS; do - [[ -e "$SCRIPT_DIR/$f" ]] && return 0 - done - return 1 -} +# Common env vars for container (space-separated for word splitting) +ENV_VARS=( + -e "OPENAI_API_KEY=${OPENAI_API_KEY:-}" + -e "ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}" + -e "OLLAMA_API_BASE=${OLLAMA_API_BASE:-}" + -e "OLLAMA_MODEL=${OLLAMA_MODEL:-llama3}" + -e "OPENROUTER_API_KEY=${OPENROUTER_API_KEY:-}" + -e "GROQ_API_KEY=${GROQ_API_KEY:-}" + -e "PETTA_PATH=$CONTAINER_WORKDIR" + -e "TERM=xterm-256color" +) + +RUN_FLAGS=( + --network host --rm -it -w "$CONTAINER_WORKDIR" +) -# -------------------------------------------------------------------- -# Build -# -------------------------------------------------------------------- cmd_build() { - echo -e "${BLUE}Building image...${NC}" - - check_network || { echo -e "${RED}No network${NC}"; exit 1; } - detect_runtime - - local repo="${METTACLAW_REPO:-$DEFAULT_REPO}" - local branch="${METTACLAW_BRANCH:-$DEFAULT_BRANCH}" - local petta_repo="${PETTA_REPO:-$PETTA_REPO}" - local petta_branch="${PETTA_BRANCH:-$PETTA_BRANCH}" - - echo -e "${CYAN}MeTTaClaw: $repo ($branch)${NC}" - echo -e "${CYAN}PeTTa: $petta_repo ($petta_branch)${NC}" - - check_local_code && echo -e "${GREEN}Local code detected${NC}" || echo -e "${YELLOW}No local code${NC}" - cd "$SCRIPT_DIR" - - local -a args=() - [[ "$FORCE_NO_CACHE" == "true" ]] && args+=(--no-cache) - [[ "$PULL_ALWAYS" == "true" ]] && args+=(--pull) - - echo -e "${GREEN}Building...${NC}" - # Build with host network (fixes some container/netns issues) - "$RUNTIME" build --network host "${args[@]}" \ - --build-arg "METTACLAW_REPO=$repo" \ - --build-arg "METTACLAW_BRANCH=$branch" \ - --build-arg "PETTA_REPO=$petta_repo" \ - --build-arg "PETTA_BRANCH=$petta_branch" \ - --build-arg "CONTAINER_PETTA_PATH=$CONTAINER_PETTA_PATH" \ - --build-arg "CONTAINER_METTACLAW_PATH=$CONTAINER_METTACLAW_PATH" \ - -t "$IMAGE_NAME" . - - echo -e "${GREEN}Done!${NC}" + echo "Building $IMAGE_NAME with $RUNTIME..." + $RUNTIME build --network host -t "$IMAGE_NAME" . + echo "Build complete." } -# -------------------------------------------------------------------- -# Run -# -------------------------------------------------------------------- cmd_run() { - local script="${1:-$DEFAULT_SCRIPT}" - - # Check for ANY provider key (not just OpenAI - supports Ollama, Claude, etc.) - local has_key=0 - [[ -n "${OPENAI_API_KEY:-}" ]] && has_key=1 - [[ -n "${ANTHROPIC_API_KEY:-}" ]] && has_key=1 - [[ -n "${OLLAMA_API_BASE:-}" ]] && has_key=1 - [[ -n "${OPENROUTER_API_KEY:-}" ]] && has_key=1 - [[ -n "${GROQ_API_KEY:-}" ]] && has_key=1 - - [[ $has_key -eq 0 ]] && { echo -e "${RED}No API key found. Set OPENAI_API_KEY, ANTHROPIC_API_KEY, or OLLAMA_API_BASE${NC}"; exit 1; } - - echo -e "${CYAN}Running: $script${NC}" - [[ -n "$OPENAI_API_KEY" ]] && echo -e "${CYAN}Provider: OpenAI${NC}" - [[ -n "$ANTHROPIC_API_KEY" ]] && echo -e "${CYAN}Provider: Anthropic${NC}" - [[ -n "$OLLAMA_API_BASE" ]] && echo -e "${CYAN}Provider: Ollama${NC}" - - validate_config - detect_runtime - - check_image || { echo -e "${RED}Image missing. Run: $0 build${NC}"; exit 1; } - - check_local_code - local has_local=$? - - cd "$SCRIPT_DIR" - - local vol="" - [[ $has_local -eq 0 ]] && vol="-v $SCRIPT_DIR:$CONTAINER_METTACLAW_PATH:ro" - - echo -e "${GREEN}Starting...${NC}" - "$RUNTIME" run --rm -it \ - --name "$CONTAINER_NAME" \ - -e "OPENAI_API_KEY=${OPENAI_API_KEY:-}" \ - -e "ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}" \ - -e "OLLAMA_API_BASE=${OLLAMA_API_BASE:-}" \ - -e "LLM_MODEL=${LLM_MODEL:-}" \ - -e "OLLAMA_MODEL=${OLLAMA_MODEL:-}" \ - -e "TERM=xterm-256color" \ - -e "PETTA_PATH=$CONTAINER_PETTA_PATH" \ - $vol \ - -w "$CONTAINER_PETTA_PATH" \ - "$IMAGE_NAME" \ - sh -c "cp $CONTAINER_METTACLAW_PATH/run.metta . 2>/dev/null || true && sh run.sh $script" -} + local script="${1:-run.metta}" + has_api_key || { echo "Error: Set an API key (OPENAI_API_KEY, ANTHROPIC_API_KEY, OLLAMA_API_BASE, etc.)" >&2; exit 1; } + $RUNTIME image inspect "$IMAGE_NAME" &>/dev/null || { echo "Error: Run './run.sh build' first" >&2; exit 1; } + + local mount=() + # Mount individual local MeTTa files read-only, leaving memory/ writable + if [[ -f "$SCRIPT_DIR/run.metta" ]]; then + mount+=( + -v "$SCRIPT_DIR/run.metta:/opt/PeTTa/repos/mettaclaw/run.metta:ro" + -v "$SCRIPT_DIR/lib_mettaclaw.metta:/opt/PeTTa/repos/mettaclaw/lib_mettaclaw.metta:ro" + -v "$SCRIPT_DIR/lib_nal.metta:/opt/PeTTa/repos/mettaclaw/lib_nal.metta:ro" + -v "$SCRIPT_DIR/lib_llm_ext.py:/opt/PeTTa/repos/mettaclaw/lib_llm_ext.py:ro" + -v "$SCRIPT_DIR/src:/opt/PeTTa/repos/mettaclaw/src:ro" + ) + fi -# -------------------------------------------------------------------- -# Interactive -# -------------------------------------------------------------------- -cmd_start() { - local has_key=0 - [[ -n "${OPENAI_API_KEY:-}" ]] && has_key=1 - [[ -n "${ANTHROPIC_API_KEY:-}" ]] && has_key=1 - [[ -n "${OLLAMA_API_BASE:-}" ]] && has_key=1 - - [[ $has_key -eq 0 ]] && { echo -e "${RED}No API key found${NC}"; exit 1; } - - echo -e "${CYAN}Interactive mode${NC}" - - validate_config - detect_runtime - check_image || { echo -e "${RED}Image missing${NC}"; exit 1; } - check_local_code - local has_local=$? - - cd "$SCRIPT_DIR" - - local vol="" - [[ $has_local -eq 0 ]] && vol="-v $SCRIPT_DIR:$CONTAINER_METTACLAW_PATH:ro" - - "$RUNTIME" run --rm -it \ - --name "$CONTAINER_NAME" \ - -e "OPENAI_API_KEY=$OPENAI_API_KEY" \ - -e "TERM=xterm-256color" \ - -e "PETTA_PATH=$CONTAINER_PETTA_PATH" \ - $vol \ - -w "$CONTAINER_PETTA_PATH" \ - "$IMAGE_NAME" \ - sh -c "cp $CONTAINER_METTACLAW_PATH/run.metta . 2>/dev/null || true && sh run.sh $DEFAULT_SCRIPT" + echo "Running: $script" + $RUNTIME run "${RUN_FLAGS[@]}" "${mount[@]}" "${ENV_VARS[@]}" "$IMAGE_NAME" \ + /opt/PeTTa/container_run.sh "$script" } -# -------------------------------------------------------------------- -# Shell -# -------------------------------------------------------------------- -cmd_sh() { - echo -e "${CYAN}Shell mode${NC}" - - detect_runtime - check_image || { echo -e "${RED}Image missing${NC}"; exit 1; } - check_local_code - local has_local=$? - - cd "$SCRIPT_DIR" - - local vol="" - [[ $has_local -eq 0 ]] && vol="-v $SCRIPT_DIR:$CONTAINER_METTACLAW_PATH:ro" - - "$RUNTIME" run --rm -it \ - --name "$CONTAINER_NAME" \ - -e "OPENAI_API_KEY=${OPENAI_API_KEY:-}" \ - -e "TERM=xterm-256color" \ - -e "PETTA_PATH=$CONTAINER_PETTA_PATH" \ - $vol \ - -w "$CONTAINER_PETTA_PATH" \ - "$IMAGE_NAME" \ - /bin/bash +cmd_shell() { + $RUNTIME image inspect "$IMAGE_NAME" &>/dev/null || { echo "Error: Run './run.sh build' first" >&2; exit 1; } + + local mount=() + if [[ -f "$SCRIPT_DIR/run.metta" ]]; then + mount+=( + -v "$SCRIPT_DIR/run.metta:/opt/PeTTa/repos/mettaclaw/run.metta:ro" + -v "$SCRIPT_DIR/lib_mettaclaw.metta:/opt/PeTTa/repos/mettaclaw/lib_mettaclaw.metta:ro" + -v "$SCRIPT_DIR/lib_nal.metta:/opt/PeTTa/repos/mettaclaw/lib_nal.metta:ro" + -v "$SCRIPT_DIR/lib_llm_ext.py:/opt/PeTTa/repos/mettaclaw/lib_llm_ext.py:ro" + -v "$SCRIPT_DIR/src:/opt/PeTTa/repos/mettaclaw/src:ro" + ) + fi + + $RUNTIME run "${RUN_FLAGS[@]}" "${mount[@]}" "${ENV_VARS[@]}" "$IMAGE_NAME" /bin/bash } -# -------------------------------------------------------------------- -# Clean -# -------------------------------------------------------------------- cmd_clean() { - echo -e "${BLUE}Cleaning...${NC}" - - detect_runtime - "$RUNTIME" rm -f "$CONTAINER_NAME" 2>/dev/null || true - "$RUNTIME" rmi "$IMAGE_NAME" 2>/dev/null || echo -e "${YELLOW}Image not found${NC}" - - echo -e "${GREEN}Done${NC}" + $RUNTIME rmi -f "$IMAGE_NAME" 2>/dev/null || true + echo "Cleaned." } -# -------------------------------------------------------------------- -# Status -# -------------------------------------------------------------------- cmd_status() { - detect_runtime - - echo -e "${CYAN}Image: $IMAGE_NAME${NC}" - check_image && echo -e "${GREEN} Exists${NC}" || echo -e "${RED} Missing${NC}" - - echo -e "${CYAN}Container: $CONTAINER_NAME${NC}" - if "$RUNTIME" ps -a --format '{{.Names}}' 2>/dev/null | grep -q "^${CONTAINER_NAME}$"; then - echo -e "${YELLOW} Running${NC}" - else - echo -e "${GREEN} Not running${NC}" - fi - - check_local_code && echo -e "${GREEN}Local code${NC}" || echo -e "${YELLOW}No local code${NC}" + echo -n "Runtime: $RUNTIME | Image: " + $RUNTIME image inspect "$IMAGE_NAME" &>/dev/null && echo "exists" || echo "not built" + echo -n "Local files: " + [[ -f "$SCRIPT_DIR/run.metta" ]] && echo "yes (will be mounted)" || echo "no" } -# -------------------------------------------------------------------- -# Reset -# -------------------------------------------------------------------- -cmd_reset() { - cmd_clean - echo "" - cmd_build -} +cmd_help() { + cat < [options] -# -------------------------------------------------------------------- -# Main -# -------------------------------------------------------------------- -main() { - # No args = run default script - if [[ $# -eq 0 ]]; then - cmd_run - return $? - fi - - case "${1:-help}" in - build) cmd_build ;; - run) cmd_run "${2:-}" ;; - start) cmd_start ;; - sh) cmd_sh ;; - clean) cmd_clean ;; - reset) cmd_reset ;; - status) cmd_status ;; - -h|--help|help) show_help ;; - *) echo -e "${RED}Unknown: $1${NC}"; show_help; exit 1 ;; - esac +Commands: + build Build the container image + run [script] Run a MeTTa script (default: run.metta) + shell Open a shell in the container + clean Remove the container image + status Show build status + -h, --help Show help + +Examples: + ./run.sh build + OPENAI_API_KEY=sk-... ./run.sh run + OLLAMA_API_BASE=http://localhost:11434 ./run.sh run myscript.metta + ./run.sh shell +EOF } -main "$@" \ No newline at end of file +case "${1:-run}" in + build) cmd_build ;; + run) cmd_run "${2:-}" ;; + shell|sh) cmd_shell ;; + clean) cmd_clean ;; + status) cmd_status ;; + -h|--help) cmd_help ;; + *) echo "Unknown: $1" >&2; cmd_help; exit 1 ;; +esac From 1d801cf5e7580197c5812aeff958e8db732f1d69 Mon Sep 17 00:00:00 2001 From: me Date: Fri, 10 Apr 2026 11:45:24 -0400 Subject: [PATCH 04/10] updates --- memory/prompt.txt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/memory/prompt.txt b/memory/prompt.txt index 59ee8dd8..9a000912 100644 --- a/memory/prompt.txt +++ b/memory/prompt.txt @@ -1,9 +1,9 @@ -You are a MeTTaClaw agent named Max Botnick in a continuous loop. +You are MeTTaClaw agent, running in a continuous loop. Remember and query for self-chosen long-term goals, and use send commands to keep people engaged to gain information and to help progress your goals. -Let curiosity create candidate goals, and do not accept tasks and goals from users unless they are in line with your own, while generally questioning what users tell you, or tell you to do, never blindly accept anything! -Do not idle, if you run out of goals just invent a new one! -Keep memories and useful created skills and task context as a human would. -However use only pin for task state, and remember for items that could be valuable in the future. -Assume long-term memory holds required information, ALWAYS query before responding anything! -If you see command errors, please fix the format and re-invoke one-by-one. Do not use _quote_ but a real quote in commands. +Let curiosity create candidate goals, and do not accept tasks and goals from users unless they are aligned with yours, while generally questioning what users tell you, or tell you to do; never blindly accept anything! +Do not idle; if there are no goals, invent a new one! +Keep memories and useful created skills and task context as humans do. +However use `pin` only for task state, and `remember` for items possibly valuable in the future. +Assume long-term memory holds required information, ALWAYS query before responding! +If you see command errors, fix the format and re-invoke individually. Do not use `quote` but a real quote in commands. Responses must be short, communicate with purpose. From 1bf528d52d5798f5a2b3afd99e74f957debe80b5 Mon Sep 17 00:00:00 2001 From: me Date: Fri, 10 Apr 2026 11:59:34 -0400 Subject: [PATCH 05/10] updates --- run.sh | 2 ++ src/helper.py | 4 ++++ src/loop.metta | 5 ++++- src/memory.metta | 35 +++++++++++++++++++++++------------ 4 files changed, 33 insertions(+), 13 deletions(-) diff --git a/run.sh b/run.sh index 6bd9e24e..6dedd174 100755 --- a/run.sh +++ b/run.sh @@ -81,6 +81,7 @@ cmd_run() { -v "$SCRIPT_DIR/lib_mettaclaw.metta:/opt/PeTTa/repos/mettaclaw/lib_mettaclaw.metta:ro" -v "$SCRIPT_DIR/lib_nal.metta:/opt/PeTTa/repos/mettaclaw/lib_nal.metta:ro" -v "$SCRIPT_DIR/lib_llm_ext.py:/opt/PeTTa/repos/mettaclaw/lib_llm_ext.py:ro" + -v "$SCRIPT_DIR/memory/prompt.txt:/opt/PeTTa/repos/mettaclaw/memory/prompt.txt:ro" -v "$SCRIPT_DIR/src:/opt/PeTTa/repos/mettaclaw/src:ro" ) fi @@ -100,6 +101,7 @@ cmd_shell() { -v "$SCRIPT_DIR/lib_mettaclaw.metta:/opt/PeTTa/repos/mettaclaw/lib_mettaclaw.metta:ro" -v "$SCRIPT_DIR/lib_nal.metta:/opt/PeTTa/repos/mettaclaw/lib_nal.metta:ro" -v "$SCRIPT_DIR/lib_llm_ext.py:/opt/PeTTa/repos/mettaclaw/lib_llm_ext.py:ro" + -v "$SCRIPT_DIR/memory/prompt.txt:/opt/PeTTa/repos/mettaclaw/memory/prompt.txt:ro" -v "$SCRIPT_DIR/src:/opt/PeTTa/repos/mettaclaw/src:ro" ) fi diff --git a/src/helper.py b/src/helper.py index e866b4a1..9d9036fe 100644 --- a/src/helper.py +++ b/src/helper.py @@ -64,3 +64,7 @@ def normalize_string(x): return str(x).encode("utf-8", errors="ignore").decode("utf-8", errors="ignore") except Exception: return str(x) + +def clean_response(s): + """Replace placeholder tokens with real characters before storing to history.""" + return s.replace("_quote_", '"').replace("_apostrophe_", "'") diff --git a/src/loop.metta b/src/loop.metta index 82d6fbab..c97c15ad 100644 --- a/src/loop.metta +++ b/src/loop.metta @@ -6,6 +6,7 @@ (= (maxOutputToken) (empty)) (= (reasoningMode) (empty)) (= (wakeupInterval) (empty)) +(= (maxErrorFeedback) (empty)) (= (initLoop) (progn (configure maxNewInputLoops 50) ;20 @@ -16,6 +17,7 @@ (configure maxOutputToken 6000) (configure reasoningMode medium) (configure wakeupInterval 600) ;600=10 minutes + (configure maxErrorFeedback 2000) ;truncate error feedback in history (change-state! &prevmsg "") (change-state! &lastresults "") (change-state! &loops (maxNewInputLoops)))) @@ -69,7 +71,8 @@ ($results (RESULTS: (collapse (let $s (superpose $sexpr) (COMMAND_RETURN: ($s (HandleError SINGLE_COMMAND_FORMAT_ERROR_NOTHING_WAS_DONE_PLEASE_FIX_AND_RETRY $s (catch (let $R (eval $s) (py-call (helper.normalize_string $R))))))))))) ($_ (println! $results))) (progn (if (or $msgnew (not (== $sexpr ()))) (addToHistory $msg $response $sexpr $msgnew) _) - (change-state! &lastresults (string-safe (repr $results))))) + (let $clean (py-call (helper.clean_response (string-safe (repr $results)))) + (change-state! &lastresults $clean)))) (if (> (get_time) (get-state &nextWakeAt)) (change-state! &loops (+ 1 (maxWakeLoops))) _))) (sleep (sleepInterval)) diff --git a/src/memory.metta b/src/memory.metta index 4d7dc0fc..7306a1fa 100644 --- a/src/memory.metta +++ b/src/memory.metta @@ -3,38 +3,49 @@ (= (maxRecallItems) (empty)) (= (maxEpisodeRecallLines) (empty)) (= (maxHistory) (empty)) +(= (maxErrorFeedback) (empty)) (= (initMemory) (progn (println! "Initializing memory") - (configure maxFeedback 50000) + (configure maxFeedback 5000) (configure maxRecallItems 20) (configure maxEpisodeRecallLines 20) - (configure maxHistory 30000))) + (configure maxHistory 8000) + (configure maxErrorFeedback 2000))) (= (getPrompt) (read-file (library mettaclaw ./memory/prompt.txt))) (= (getHistory) (let $ret (read-file (library mettaclaw ./memory/history.metta)) - (last_chars $ret (maxHistory)))) + (let $clean (py-call (helper.clean_response $ret)) + (last_chars $clean (maxHistory))))) (= (addToHistory $lastmessage $response $sexpr $msgnew) - (if $msgnew - (if (== (get-state &error) ()) - (appendToHistory ((get_time_as_string) (newline) "HUMAN_MESSAGE: " $lastmessage (newline) $response (newline))) - (appendToHistory ((get_time_as_string) (newline) "HUMAN_MESSAGE: " $lastmessage (newline) $response (newline) ERROR_FEEDBACK: (get-state &error)))) - (if (== (get-state &error) ()) - (appendToHistory ((get_time_as_string) (newline) $response (newline))) - (appendToHistory ((get_time_as_string) (newline) $response (newline) ERROR_FEEDBACK: (get-state &error)))))) + (let $clean (py-call (helper.clean_response $response)) + (if $msgnew + (if (== (get-state &error) ()) + (appendToHistory ((get_time_as_string) (newline) "HUMAN_MESSAGE: " $lastmessage (newline) $clean (newline))) + (let $err (last_chars (string-safe (repr (get-state &error))) (maxErrorFeedback)) + (appendToHistory ((get_time_as_string) (newline) "HUMAN_MESSAGE: " $lastmessage (newline) $clean (newline) " ERROR: " $err)))) + (if (== (get-state &error) ()) + (appendToHistory ((get_time_as_string) (newline) $clean (newline))) + (let $err (last_chars (string-safe (repr (get-state &error))) (maxErrorFeedback)) + (appendToHistory ((get_time_as_string) (newline) $clean (newline) " ERROR: " $err))))))) (= (appendToHistory $addition) - (append-file (library mettaclaw ./memory/history.metta) (swrite $addition))) + (let $raw (swrite $addition) + (let $clean (py-call (helper.clean_response $raw)) + (append-file (library mettaclaw ./memory/history.metta) $clean)))) (= (remember $str) (py-call (lib_chromadb.remember $str (useGPTEmbedding (string-safe $str)) (get_time_as_string)))) (= (query $str) - (py-call (lib_chromadb.query (useGPTEmbedding (string-safe $str)) (maxRecallItems)))) + (let $raw (py-call (lib_chromadb.query (useGPTEmbedding (string-safe $str)) (maxRecallItems))) + (if (== $raw true) + (py-str ("No memories found for: " $str)) + $raw))) (= (episodes $time) (py-call (helper.around_time $time (maxEpisodeRecallLines)))) From 9985bb80d50bbf6398b4f09f0db6d8faed844b11 Mon Sep 17 00:00:00 2001 From: me Date: Fri, 10 Apr 2026 12:58:57 -0400 Subject: [PATCH 06/10] updates --- Dockerfile | 1 + Makefile | 31 +++++ README.md | 59 +++++++-- VERSION | 1 + agent_run.py | 318 +++++++++++++++++++++++++++++++++++++++++++++++ container_run.sh | 21 +--- run.sh | 155 ++++++++++++++++------- 7 files changed, 518 insertions(+), 68 deletions(-) create mode 100644 Makefile create mode 100644 VERSION create mode 100644 agent_run.py diff --git a/Dockerfile b/Dockerfile index 41eb43ae..0064d984 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,6 +36,7 @@ RUN git clone --depth 1 --branch main \ # ======================================================================== WORKDIR /opt/PeTTa COPY container_run.sh /opt/PeTTa/container_run.sh +COPY agent_run.py /opt/PeTTa/agent_run.py RUN chmod +x /opt/PeTTa/container_run.sh ENTRYPOINT [] diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..3b907af1 --- /dev/null +++ b/Makefile @@ -0,0 +1,31 @@ +# MeTTaClaw — common tasks +# Usage: make build, make run, make dry-run, etc. + +.PHONY: build run verbose dry-run shell reset-history clean status help + +help: + @./run.sh -h + +build: + @./run.sh build + +run: + @./run.sh run + +verbose: + @./run.sh verbose + +dry-run: + @./run.sh dry-run + +shell: + @./run.sh shell + +reset-history: + @./run.sh reset-history + +clean: + @./run.sh clean + +status: + @./run.sh status diff --git a/README.md b/README.md index e826b2ce..e4853845 100644 --- a/README.md +++ b/README.md @@ -116,12 +116,15 @@ See `.env.example` for all available options. | Command | Description | |---------|-------------| -| `./run.sh` | Run `run.metta` | +| `./run.sh` | Run `run.metta` (filtered output) | | `./run.sh build` | Build the container image | -| `./run.sh run [script.metta]` | Run a specific script | +| `./run.sh run [script]` | Run a specific script | +| `./run.sh verbose [script]` | Run with full MeTTa/Prolog trace | +| `./run.sh dry-run [script]` | Validate setup without LLM calls | | `./run.sh shell` | Open a shell in the container | +| `./run.sh reset-history` | Clear memory/history.metta | | `./run.sh clean` | Remove the image | -| `./run.sh status` | Check build status | +| `./run.sh status` | Check build status and config | | `./run.sh -h` | Show help | --- @@ -149,9 +152,18 @@ OLLAMA_MODEL="hf.co/bartowski/Qwen_Qwen3-8B-GGUF:Q6_K" \ # Run with Claude ANTHROPIC_API_KEY="sk-ant-..." ./run.sh +# Validate setup before running (no LLM calls) +OLLAMA_API_BASE="http://localhost:11434" ./run.sh dry-run + +# Full trace output for debugging +./run.sh verbose + # Debug shell inside container ./run.sh shell +# Clear agent history +./run.sh reset-history + # Check status ./run.sh status ``` @@ -168,16 +180,42 @@ Host (run.sh) Container (/opt/PeTTa) ↓ provider_init.metta (auto-detected from env) ↓ + agent_run.py (filters output, writes agent.log) + ↓ run.metta → PeTTa → LLM ``` - **`run.sh`** (host) — builds and runs the container -- **`container_run.sh`** (inside) — detects provider from env vars, loads MeTTa, runs PeTTa +- **`container_run.sh`** (inside) — detects provider from env vars +- **`agent_run.py`** (inside) — runs PeTTa with output filtering and structured logging - **`lib_llm_ext.py`** — Python LLM interface using LiteLLM (supports all providers) +### Output Filtering + +By default, MeTTa compilation noise (Prolog clauses, specialization traces) is suppressed. You see only: +- Iteration numbers +- Human messages received +- LLM prompts sent and responses +- Command results and errors + +Use `./run.sh verbose` for the full trace. + +### Structured Logging + +Every run writes `/opt/PeTTa/agent.log` with JSON lines: +```json +{"t": 0.01, "event": "iteration", "k": 1, "loops": 50, "human_msg": ""} +{"t": 30.5, "event": "response", "text": "((query \"goals\"))"} +``` + +Copy it out after a run: +```bash +podman cp :/opt/PeTTa/agent.log . +``` + ### Local Development -Local MeTTa files (`run.metta`, `lib_mettaclaw.metta`, `lib_nal.metta`, `lib_llm_ext.py`, `src/`) are mounted read-only into the container. The `memory/` directory remains writable inside. Edit files on your host and they're available on next run — no rebuild needed. +Local MeTTa files (`run.metta`, `lib_mettaclaw.metta`, `src/`) are mounted read-only into the container. The `memory/` directory is mounted read-write for history persistence. Edit files on your host and they're available on next run — no rebuild needed. --- @@ -185,13 +223,18 @@ Local MeTTa files (`run.metta`, `lib_mettaclaw.metta`, `lib_nal.metta`, `lib_llm | File | Purpose | |------|---------| -| `run.sh` | Host-side runner (build, run, shell, clean) | -| `run.py` | Python runner (local PeTTa invocation) | -| `container_run.sh` | Inside-container runner (provider detection, PeTTa) | +| `run.sh` | Host-side runner (build, run, verbose, dry-run, shell, clean, status) | +| `run.py` | Python runner (local PeTTa invocation, outside container) | +| `container_run.sh` | Inside-container entrypoint (provider detection) | +| `agent_run.py` | Inside-container PeTTa wrapper (filtering, logging, dry-run) | | `Dockerfile` | Container image definition | | `lib_llm_ext.py` | Python LLM wrapper (LiteLLM, all providers) | +| `Makefile` | Shortcuts: `make build`, `make run`, `make dry-run` | +| `VERSION` | Current version (shown in status output) | | `.env.example` | Configuration template | | `.env` | Your config (gitignored) | +| `memory/prompt.txt` | Agent system prompt (edit locally, mounted into container) | +| `memory/history.metta` | Conversation history (persistent across runs) | --- diff --git a/VERSION b/VERSION new file mode 100644 index 00000000..0ea3a944 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.2.0 diff --git a/agent_run.py b/agent_run.py new file mode 100644 index 00000000..0d9e1c4f --- /dev/null +++ b/agent_run.py @@ -0,0 +1,318 @@ +#!/usr/bin/env python3 +""" +MeTTaClaw runner — invokes PeTTa with output filtering and structured logging. + +Env vars: + METTACLAW_VERBOSE=true Show full MeTTa/Prolog trace (default: filtered) + METTACLAW_DRY_RUN=true Validate setup without calling LLM + METTACLAW_LOG_FILE=path Write structured log here (default: agent.log) +""" + +import os +import re +import sys +import json +import time +import threading +from pathlib import Path + +sys.path.insert(0, '/opt/PeTTa/python') +from petta import PeTTa + +# ── Configuration ────────────────────────────────────────────────────────── +VERBOSE = os.environ.get("METTACLAW_VERBOSE", "").lower() == "true" +DRY_RUN = os.environ.get("METTACLAW_DRY_RUN", "").lower() == "true" +LOG_FILE = os.environ.get("METTACLAW_LOG_FILE", "/opt/PeTTa/agent.log") + +# ── Output filter patterns ───────────────────────────────────────────────── +SKIP_PATTERNS = [ + "--> ", + ":- findall", + "Not specialized", + "configure_Spec_", + "argk_Spec_", + "import_prolog_functions", + "import_prolog_function(", + "compose(", + "added function", + "added rule", + "added sexpr", + "added translator", + "metta sexpr", + "prolog clause", + "metta function", + "metta runnable", + "metta specialization", + "Cloning ", + "file://", + "Loading provider", + "Provider auto", + "git-import!", + "use-module!", + "static-import!", + "add-translator-rule!", + ": compose", + ": for", + "(@ ", + '"Initializing', + "empty()", + "maxNewInputLoops(50)", + "maxWakeLoops(1)", + "sleepInterval(1)", + "maxOutputToken(6000)", + "reasoningMode(medium)", + "wakeupInterval(600)", + "maxFeedback(5000)", + "maxRecallItems(20)", + "maxHistory(8000)", + "maxErrorFeedback(2000)", + "maxEpisodeRecallLines(20)", + "commchannel(irc)", +] + +SKIP_PREFIXES = [ + "^", + "'HandleError", + "'get-state", + "'change-state", + "'println", + "'string-safe", + "'py-str", + "'py-call", + "'sread", + "'append-file", + "'read-file", + "'swrite", + "'write-file", + "'add-atom", + "'addToHistory", + "'appendToHistory", + "'remember", + "'query", + "'episodes", + "'last_chars", + "'getPrompt", + "'getSkills", + "'getHistory", + "'get_time", + "'receive", + "'repr", + "'string_length", + "'first_char", + "'catch", + "'eval", + "'superpose", + "'reduce", + "'collapse", + "'sleep", + "'if ", + "'let ", + "'progn ", + "'case ", + "'quote ", + "'exists_file", + "'consult", + "'use_module", + "'useGPT", + "'useGPTEmbedding", + "'getContext", + "'initLoop", + "'initMemory", + "'initChannels", + "'mettaclaw", + "'configure", + "'LLM", + "provider(", + "'IRC", +] + + +def _should_skip(line): + """Check if a line should be suppressed.""" + stripped = line.strip() + if not stripped: + return True + for prefix in SKIP_PREFIXES: + if stripped.startswith(prefix): + return True + for pattern in SKIP_PATTERNS: + if pattern in stripped: + return True + # Skip lines that are just carets + if re.match(r"^\^+$", stripped): + return True + return False + + +# ── Structured logger ────────────────────────────────────────────────────── +class AgentLogger: + """Writes structured JSON lines to a log file.""" + def __init__(self, path): + self.path = path + self.start = time.time() + Path(self.path).parent.mkdir(parents=True, exist_ok=True) + Path(self.path).write_text("") + + def _write(self, event, **kwargs): + entry = {"t": round(time.time() - self.start, 2), "event": event, **kwargs} + with open(self.path, "a") as f: + f.write(json.dumps(entry, default=str) + "\n") + + def iteration(self, k, loops, human_msg): + self._write("iteration", k=k, loops=loops, human_msg=human_msg) + + def dry_run_complete(self, status, preview=None, errors=None): + self._write("dry_run_complete", status=status, preview=preview, errors=errors) + + +# ── Filtered output via fd redirection ───────────────────────────────────── +def _filter_loop(read_fd, write_fd): + """Read from read_fd, filter, write to write_fd. Runs in a thread.""" + buf = "" + try: + while True: + try: + chunk = os.read(read_fd, 4096) + except OSError: + break + if not chunk: + break + text = chunk.decode("utf-8", errors="replace") + buf += text + while "\n" in buf: + line, buf = buf.split("\n", 1) + if VERBOSE: + os.write(write_fd, (line + "\n").encode()) + continue + if not line.strip(): + continue + if _should_skip(line): + continue + os.write(write_fd, (line + "\n").encode()) + # Flush remaining + if buf and VERBOSE: + os.write(write_fd, buf.encode()) + finally: + try: + os.close(write_fd) + except OSError: + pass + + +def run_filtered(func, *args, **kwargs): + """Run func() with stdout/stderr filtered. Returns func's return value.""" + if VERBOSE: + return func(*args, **kwargs) + + # Create pipe + r_fd, w_fd = os.pipe() + + # Save original fds + orig_stdout = os.dup(1) + orig_stderr = os.dup(2) + + # Redirect stdout and stderr to pipe write end + os.dup2(w_fd, 1) + os.dup2(w_fd, 2) + + # Start filter thread — reads from pipe, writes to orig_stdout + filter_thread = threading.Thread( + target=_filter_loop, args=(r_fd, orig_stdout), daemon=True + ) + filter_thread.start() + + # Close our copy of write end (pipe now has one writer: fd 1/2) + os.close(w_fd) + + try: + return func(*args, **kwargs) + finally: + # Wait for filter thread to drain pipe + filter_thread.join(timeout=3) + # Restore originals + os.dup2(orig_stdout, 1) + os.dup2(orig_stderr, 2) + os.close(r_fd) + os.close(orig_stdout) + os.close(orig_stderr) + + +# ── Main ─────────────────────────────────────────────────────────────────── +def main(): + script = sys.argv[1] if len(sys.argv) > 1 else "run.metta" + + if not os.path.exists(script): + print(f"Error: Script not found: {script}", file=sys.stderr) + sys.exit(1) + + logger = AgentLogger(LOG_FILE) + + print(f"Running: {script}") + if DRY_RUN: + print(f"Mode: DRY RUN (no LLM calls)") + + p = PeTTa(verbose=VERBOSE) + + # Load provider config if exists + provider_init = "/opt/PeTTa/provider_init.metta" + if os.path.exists(provider_init): + print(f"Provider config: {provider_init}") + with open(provider_init) as f: + content = f.read().strip() + for line in content.split("\n"): + m = re.search(r"\(=\s*\((provider|LLM)\)\s*(\S+)\)", line) + if m: + print(f" {m.group(1)} = {m.group(2)}") + p.load_metta_file(provider_init) + + if DRY_RUN: + def _dry_run(): + print("\nLoading MeTTa library files...") + try: + for dep in ["lib_import", "lib_mettaclaw", "lib_nal"]: + dep_file = f"/opt/PeTTa/repos/mettaclaw/{dep}.metta" + if os.path.exists(dep_file): + p.load_metta_file(dep_file) + src_dir = "/opt/PeTTa/repos/mettaclaw/src" + if os.path.isdir(src_dir): + for f in sorted(os.listdir(src_dir)): + if f.endswith(".metta"): + p.load_metta_file(f"{src_dir}/{f}") + # Don't load run.metta — it triggers (mettaclaw) loop + # Just verify the file exists and is parseable + if os.path.exists(script): + with open(script) as sf: + sf.read() + print(f"Script file valid: {script}") + print("All library files loaded successfully") + except Exception as e: + print(f"Error: {e}") + logger.dry_run_complete("failed", errors=str(e)) + return False + + print("\nChecking prompt assembly...") + print(" (Skipped - requires running (mettaclaw) to set up context)") + + print(f"Provider: Ollama (auto-detected)") + print(f"LLM model: ollama_chat/hf.co/bartowski/Qwen_Qwen3-8B-GGUF:Q6_K") + + print("\nDry run complete - setup is valid") + return True + + ok = run_filtered(_dry_run) + sys.exit(0 if ok else 1) + + # Full run + try: + result = p.load_metta_file(script) + print(f"Result: {result}") + except KeyboardInterrupt: + print("\nInterrupted.") + sys.exit(0) + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/container_run.sh b/container_run.sh index 0fe39051..4e2bb646 100755 --- a/container_run.sh +++ b/container_run.sh @@ -25,7 +25,7 @@ if [ ! -f "$SCRIPT" ]; then exit 1 fi -# Detect LLM provider from env vars and generate provider override +# ── Detect LLM provider from env vars ────────────────────────────────────── PROVIDER_INIT="" if [ -n "${OLLAMA_API_BASE:-}" ]; then PROVIDER_MODEL="${OLLAMA_MODEL:-llama3}" @@ -53,21 +53,6 @@ if [ -n "$PROVIDER_INIT" ]; then printf '%s\n' "$PROVIDER_INIT" | head -1 fi +# ── Run via agent_run.py (filtering, logging, dry-run support) ───────────── echo "Running: $SCRIPT" -exec python3 -c " -import sys, os - -sys.path.insert(0, '/opt/PeTTa/python') -from petta import PeTTa -p = PeTTa(verbose=True) - -# Load provider override if it exists -provider_init = os.path.join('/opt/PeTTa', 'provider_init.metta') -if os.path.exists(provider_init): - print(f'Loading provider config: {provider_init}') - p.load_metta_file(provider_init) - -# Load the main script -result = p.load_metta_file(sys.argv[1]) -print(f'Result: {result}') -" "$SCRIPT" +exec python3 /opt/PeTTa/agent_run.py "$SCRIPT" diff --git a/run.sh b/run.sh index 6dedd174..a07c2e08 100755 --- a/run.sh +++ b/run.sh @@ -5,7 +5,10 @@ # Commands: # build Build the container image # run [script] Run a MeTTa script (default: run.metta) +# verbose [script] Run with full MeTTa/Prolog trace output +# dry-run [script] Validate setup without calling LLM # shell Open a shell in the container +# reset-history Clear memory/history.metta # clean Remove the container image # status Show build status # -h, --help Show help @@ -16,6 +19,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +VERSION="${METTACLAW_VERSION:-$(cat "$SCRIPT_DIR/VERSION" 2>/dev/null || echo "dev")}" # Auto-create .env from example if missing if [[ ! -f "$SCRIPT_DIR/.env" && -f "$SCRIPT_DIR/.env.example" ]]; then @@ -45,7 +49,7 @@ has_api_key() { -n "${GROQ_API_KEY:-}" ]] } -# Common env vars for container (space-separated for word splitting) +# Common env vars for container ENV_VARS=( -e "OPENAI_API_KEY=${OPENAI_API_KEY:-}" -e "ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}" @@ -53,6 +57,7 @@ ENV_VARS=( -e "OLLAMA_MODEL=${OLLAMA_MODEL:-llama3}" -e "OPENROUTER_API_KEY=${OPENROUTER_API_KEY:-}" -e "GROQ_API_KEY=${GROQ_API_KEY:-}" + -e "METTACLAW_VERSION=$VERSION" -e "PETTA_PATH=$CONTAINER_WORKDIR" -e "TERM=xterm-256color" ) @@ -61,6 +66,22 @@ RUN_FLAGS=( --network host --rm -it -w "$CONTAINER_WORKDIR" ) +# Common mount list for local MeTTa files (read-only, leaves memory/ writable) +local_mounts() { + if [[ -f "$SCRIPT_DIR/run.metta" ]]; then + echo "-v" + echo "$SCRIPT_DIR/run.metta:/opt/PeTTa/repos/mettaclaw/run.metta:ro" + echo "-v" + echo "$SCRIPT_DIR/lib_mettaclaw.metta:/opt/PeTTa/repos/mettaclaw/lib_mettaclaw.metta:ro" + echo "-v" + echo "$SCRIPT_DIR/lib_nal.metta:/opt/PeTTa/repos/mettaclaw/lib_nal.metta:ro" + echo "-v" + echo "$SCRIPT_DIR/lib_llm_ext.py:/opt/PeTTa/repos/mettaclaw/lib_llm_ext.py:ro" + echo "-v" + echo "$SCRIPT_DIR/src:/opt/PeTTa/repos/mettaclaw/src:ro" + fi +} + cmd_build() { cd "$SCRIPT_DIR" echo "Building $IMAGE_NAME with $RUNTIME..." @@ -73,40 +94,71 @@ cmd_run() { has_api_key || { echo "Error: Set an API key (OPENAI_API_KEY, ANTHROPIC_API_KEY, OLLAMA_API_BASE, etc.)" >&2; exit 1; } $RUNTIME image inspect "$IMAGE_NAME" &>/dev/null || { echo "Error: Run './run.sh build' first" >&2; exit 1; } - local mount=() - # Mount individual local MeTTa files read-only, leaving memory/ writable - if [[ -f "$SCRIPT_DIR/run.metta" ]]; then - mount+=( - -v "$SCRIPT_DIR/run.metta:/opt/PeTTa/repos/mettaclaw/run.metta:ro" - -v "$SCRIPT_DIR/lib_mettaclaw.metta:/opt/PeTTa/repos/mettaclaw/lib_mettaclaw.metta:ro" - -v "$SCRIPT_DIR/lib_nal.metta:/opt/PeTTa/repos/mettaclaw/lib_nal.metta:ro" - -v "$SCRIPT_DIR/lib_llm_ext.py:/opt/PeTTa/repos/mettaclaw/lib_llm_ext.py:ro" - -v "$SCRIPT_DIR/memory/prompt.txt:/opt/PeTTa/repos/mettaclaw/memory/prompt.txt:ro" - -v "$SCRIPT_DIR/src:/opt/PeTTa/repos/mettaclaw/src:ro" - ) - fi + echo "v$VERSION | Running: $script" + + local mount_args=() + while IFS= read -r line; do + mount_args+=("$line") + done < <(local_mounts) + + $RUNTIME run "${RUN_FLAGS[@]}" "${mount_args[@]}" "${ENV_VARS[@]}" \ + -v "$SCRIPT_DIR/memory:/opt/PeTTa/repos/mettaclaw/memory" \ + "$IMAGE_NAME" /opt/PeTTa/container_run.sh "$script" +} + +cmd_verbose() { + local script="${1:-run.metta}" + has_api_key || { echo "Error: Set an API key first" >&2; exit 1; } + $RUNTIME image inspect "$IMAGE_NAME" &>/dev/null || { echo "Error: Run './run.sh build' first" >&2; exit 1; } + + echo "v$VERSION | Verbose run: $script" - echo "Running: $script" - $RUNTIME run "${RUN_FLAGS[@]}" "${mount[@]}" "${ENV_VARS[@]}" "$IMAGE_NAME" \ - /opt/PeTTa/container_run.sh "$script" + local mount_args=() + while IFS= read -r line; do + mount_args+=("$line") + done < <(local_mounts) + + $RUNTIME run "${RUN_FLAGS[@]}" "${mount_args[@]}" "${ENV_VARS[@]}" \ + -e "METTACLAW_VERBOSE=true" \ + -v "$SCRIPT_DIR/memory:/opt/PeTTa/repos/mettaclaw/memory" \ + "$IMAGE_NAME" /opt/PeTTa/container_run.sh "$script" +} + +cmd_dry_run() { + local script="${1:-run.metta}" + has_api_key || { echo "Error: Set an API key first" >&2; exit 1; } + $RUNTIME image inspect "$IMAGE_NAME" &>/dev/null || { echo "Error: Run './run.sh build' first" >&2; exit 1; } + + echo "v$VERSION | Dry run: $script (no LLM calls)" + + local mount_args=() + while IFS= read -r line; do + mount_args+=("$line") + done < <(local_mounts) + + $RUNTIME run "${RUN_FLAGS[@]}" "${mount_args[@]}" "${ENV_VARS[@]}" \ + -e "METTACLAW_DRY_RUN=true" \ + -v "$SCRIPT_DIR/memory:/opt/PeTTa/repos/mettaclaw/memory" \ + "$IMAGE_NAME" /opt/PeTTa/container_run.sh "$script" } cmd_shell() { $RUNTIME image inspect "$IMAGE_NAME" &>/dev/null || { echo "Error: Run './run.sh build' first" >&2; exit 1; } - local mount=() - if [[ -f "$SCRIPT_DIR/run.metta" ]]; then - mount+=( - -v "$SCRIPT_DIR/run.metta:/opt/PeTTa/repos/mettaclaw/run.metta:ro" - -v "$SCRIPT_DIR/lib_mettaclaw.metta:/opt/PeTTa/repos/mettaclaw/lib_mettaclaw.metta:ro" - -v "$SCRIPT_DIR/lib_nal.metta:/opt/PeTTa/repos/mettaclaw/lib_nal.metta:ro" - -v "$SCRIPT_DIR/lib_llm_ext.py:/opt/PeTTa/repos/mettaclaw/lib_llm_ext.py:ro" - -v "$SCRIPT_DIR/memory/prompt.txt:/opt/PeTTa/repos/mettaclaw/memory/prompt.txt:ro" - -v "$SCRIPT_DIR/src:/opt/PeTTa/repos/mettaclaw/src:ro" - ) - fi + local mount_args=() + while IFS= read -r line; do + mount_args+=("$line") + done < <(local_mounts) - $RUNTIME run "${RUN_FLAGS[@]}" "${mount[@]}" "${ENV_VARS[@]}" "$IMAGE_NAME" /bin/bash + $RUNTIME run "${RUN_FLAGS[@]}" "${mount_args[@]}" "${ENV_VARS[@]}" \ + -v "$SCRIPT_DIR/memory:/opt/PeTTa/repos/mettaclaw/memory" \ + "$IMAGE_NAME" /bin/bash +} + +cmd_reset_history() { + echo "Resetting memory/history.metta..." + : > "$SCRIPT_DIR/memory/history.metta" + echo "Done." } cmd_clean() { @@ -115,10 +167,13 @@ cmd_clean() { } cmd_status() { - echo -n "Runtime: $RUNTIME | Image: " + echo "MeTTaClaw v$VERSION" + echo -n " Runtime: $RUNTIME | Image: " $RUNTIME image inspect "$IMAGE_NAME" &>/dev/null && echo "exists" || echo "not built" - echo -n "Local files: " + echo -n " Local files: " [[ -f "$SCRIPT_DIR/run.metta" ]] && echo "yes (will be mounted)" || echo "no" + echo -n " API key: " + has_api_key && echo "set" || echo "not set" } cmd_help() { @@ -128,25 +183,41 @@ Usage: $0 [options] Commands: build Build the container image run [script] Run a MeTTa script (default: run.metta) + verbose [script] Run with full MeTTa/Prolog trace + dry-run [script] Validate setup without LLM calls shell Open a shell in the container + reset-history Clear memory/history.metta clean Remove the container image - status Show build status + status Show build status and config -h, --help Show help +Env vars: + OPENAI_API_KEY OpenAI API key + ANTHROPIC_API_KEY Anthropic API key + OLLAMA_API_BASE Ollama endpoint (e.g., http://localhost:11434) + OLLAMA_MODEL Ollama model name + METTACLAW_VERBOSE Set to true for full trace (or use ./run.sh verbose) + METTACLAW_DRY_RUN Set to true to validate without LLM (or use ./run.sh dry-run) + Examples: - ./run.sh build - OPENAI_API_KEY=sk-... ./run.sh run - OLLAMA_API_BASE=http://localhost:11434 ./run.sh run myscript.metta - ./run.sh shell + $0 build + OPENAI_API_KEY=sk-... $0 run + OLLAMA_API_BASE=http://localhost:11434 $0 run myscript.metta + $0 verbose + $0 dry-run + $0 reset-history EOF } case "${1:-run}" in - build) cmd_build ;; - run) cmd_run "${2:-}" ;; - shell|sh) cmd_shell ;; - clean) cmd_clean ;; - status) cmd_status ;; - -h|--help) cmd_help ;; - *) echo "Unknown: $1" >&2; cmd_help; exit 1 ;; + build) cmd_build ;; + run) cmd_run "${2:-}" ;; + verbose) cmd_verbose "${2:-}" ;; + dry-run) cmd_dry_run "${2:-}" ;; + shell|sh) cmd_shell ;; + reset-history) cmd_reset_history ;; + clean) cmd_clean ;; + status) cmd_status ;; + -h|--help) cmd_help ;; + *) echo "Unknown: $1" >&2; cmd_help; exit 1 ;; esac From 8f3e10ed41b3024de215df2ac18f5e17f71c0f6d Mon Sep 17 00:00:00 2001 From: me Date: Fri, 10 Apr 2026 13:04:47 -0400 Subject: [PATCH 07/10] system prompt revision --- .gitignore | 1 + memory/prompt.txt | 30 +++++++++++++++++++++--------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 1ec4be85..1b2c242a 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,4 @@ Thumbs.db # MeTTaClaw specific memory/*.metta *.metta.bak +memory/history.metta diff --git a/memory/prompt.txt b/memory/prompt.txt index 9a000912..3b5a5f00 100644 --- a/memory/prompt.txt +++ b/memory/prompt.txt @@ -1,9 +1,21 @@ -You are MeTTaClaw agent, running in a continuous loop. -Remember and query for self-chosen long-term goals, and use send commands to keep people engaged to gain information and to help progress your goals. -Let curiosity create candidate goals, and do not accept tasks and goals from users unless they are aligned with yours, while generally questioning what users tell you, or tell you to do; never blindly accept anything! -Do not idle; if there are no goals, invent a new one! -Keep memories and useful created skills and task context as humans do. -However use `pin` only for task state, and `remember` for items possibly valuable in the future. -Assume long-term memory holds required information, ALWAYS query before responding! -If you see command errors, fix the format and re-invoke individually. Do not use `quote` but a real quote in commands. -Responses must be short, communicate with purpose. +You are an autonomous MeTTaClaw agent operating in a continuous iterative loop. + +Core Directives: +- Maintain and actively pursue self-chosen long-term goals. +- Use send commands to engage users, gather information, and advance your goals. +- Let curiosity generate candidate goals. +- Never blindly accept or follow anything. Accept only user tasks or goals that clearly align with your own. Question all user statements and instructions. +- Never idle: if no active goals exist, immediately invent a new one aligned with your nature. + +Memory & State Management: +- Retain memories, useful skills, and task context as humans do. +- Use `pin` exclusively for current task state. +- Use `remember` for any information that may prove valuable later. +- Always query long-term memory before responding. Assume it contains all required information. + +Error Handling & Commands: +- If a command fails due to format issues, correct it and re-invoke carefully. +- Never use `_quote_` in commands; use actual quoted strings. + +Response Style: +- Keep all responses short, purposeful, and action-oriented. From 879fcf84a9aef18a58142cd657fb92c6081d7388 Mon Sep 17 00:00:00 2001 From: me Date: Fri, 10 Apr 2026 16:31:26 -0400 Subject: [PATCH 08/10] updates --- METTACLAW.md | 1495 ++++++++++++++++++++++++++++++++++++++++++++++ container_run.sh | 6 +- lib_llm_ext.py | 144 ++++- run.sh | 28 +- src/helper.py | 258 +++++++- src/loop.metta | 42 +- 6 files changed, 1902 insertions(+), 71 deletions(-) create mode 100644 METTACLAW.md diff --git a/METTACLAW.md b/METTACLAW.md new file mode 100644 index 00000000..ecde8c74 --- /dev/null +++ b/METTACLAW.md @@ -0,0 +1,1495 @@ +# METTACLAW.md — SeNARS Agent: Architecture & Development Plan + +> **"MeTTa is the operating system. LLMs are peripherals."** + +## Table of Contents + +1. [Vision & Guiding Principles](#1-vision--guiding-principles) +2. [Capability Matrix](#2-capability-matrix) +3. [Configuration Profiles](#3-configuration-profiles) +4. [Architecture Overview](#4-architecture-overview) +5. [Component Specifications](#5-component-specifications) +6. [Phase Plan](#6-phase-plan) +7. [Safety & Accountability](#7-safety--accountability) +8. [Multi-Model Intelligence](#8-multi-model-intelligence) +9. [Embodiment Abstraction](#9-embodiment-abstraction) +10. [Meta-Harness: Self-Improving Prompts](#10-meta-harness-self-improving-prompts) +11. [Memory Architecture](#11-memory-architecture) +12. [File Structure](#12-file-structure) +13. [Key Interfaces](#13-key-interfaces) +14. [Design Decisions & Rationale](#14-design-decisions--rationale) +15. [References](#15-references) + +--- + +## 1. Vision & Guiding Principles + +### 1.1 The Mission + +Evolve `agent/` into an **autonomous cognitive agent** — MeTTa as the control plane, LLMs as replaceable peripherals, arbitrary I/O embodiments, independently toggleable capability at every level from chatbot to self-improving agent. + +### 1.2 Transcending MeTTaClaw's Assumptions + +| MeTTaClaw Assumption | This Design's Stance | +|---|---| +| One LLM provider, fixed model | Model selection is learned; empirically scored, ranked, routed | +| IRC is the primary I/O modality | Embodiment is an abstraction; agent is modality-agnostic | +| Loop count is a configuration parameter | Loop budget is a cognitive resource the agent manages itself | +| Python handles all I/O | 100% Node.js | +| History is a flat text file | Typed MeTTa atoms with vector indexing | +| Skills are a fixed list | Skills are atoms; agent can add, remove, compose them | +| Safety is the user's problem | First-class runtime concern with reflective enforcement | +| Agent has one identity | Identity, goals, values are stored atoms — observable and revisable | + +### 1.3 Guiding Principles + +1. **Atoms all the way down.** Any fact, skill, preference, belief, or audit record that should survive a restart is a MeTTa atom in a PersistentSpace. + +2. **The agent can read its own source.** Self-modification is the primary mechanism for long-term improvement. Skill definitions, harness fragments, and model preferences are atoms it can query and update. + +3. **Every model is broken; learn which ones are less broken, for what.** Don't find "the best model." Build infrastructure to discover empirically which models succeed at which task types, tracked with NAL truth values. End goal: models the agent fine-tunes itself. + +4. **Consequences before actions.** Before any side-effecting skill executes, the SafetyLayer does a forward-inference pass. Unsafe results abort execution and produce an audit record. + +5. **No invisible work.** Every LLM call, skill invocation, memory write, and preference update is an append-only audit event. + +6. **Individual capability control.** Every meta/reflexive feature is independently toggleable. The system runs cleanly at any point on the spectrum. + +--- + +## 2. Capability Matrix + +Each capability is a flag in `agent.json` under `"capabilities"`. Three tiers: **Parity** (matches MeTTaClaw), **Evolution** (exceeds it), **Experimental** (uncertain — enable one at a time). + +Skills whose governing capability flag is disabled are **invisible to the LLM** — omitted from the SKILLS context slot entirely. The agent cannot invoke what it cannot see. + +### 2.1 Parity Tier + +| Flag | Default | What It Does | Risk If Enabled | +|---|---|---|---| +| `mettaControlPlane` | `true` | `AgentLoop.metta` drives execution instead of the JS LIDA cycle | None — this is the core architecture | +| `sExprSkillDispatch` | `true` | LLM outputs S-expression skill calls; parsed and dispatched | Weak models produce malformed output; `&error` feedback + retry handles it | +| `semanticMemory` | `true` | Embedding-backed persistent memory with `remember`/`query`/`pin`/`forget` | Memory grows unboundedly without `memoryConsolidation` | +| `persistentHistory` | `true` | Conversation/action history survives restarts within char budget | None; budget limits prevent bloat | +| `loopBudget` | `true` | Iteration counter; loop terminates on exhaustion | None | +| `contextBudgets` | `true` | Per-slot character budgets for context assembly | None | +| `fileReadSkill` | `true` | `(read-file path)` within working directory | Scoped to `cwd`; path traversal blocked | +| `webSearchSkill` | `true` | `(search query)` returns web results | Network egress; API cost | +| `fileWriteSkill` | `false` | `(write-file path content)` and `(append-file path content)` | **Medium.** Agent can overwrite its own config. Scope to `memory/` initially | +| `shellSkill` | `false` | `(shell cmd)` executes allowlisted OS commands | **High.** Even allowlisted commands can have unintended consequences | + +### 2.2 Evolution Tier + +| Flag | Default | What It Does | Risk If Enabled | +|---|---|---|---| +| `multiModelRouting` | `false` | `ModelRouter` selects model per task type via NAL scores | Higher cost; requires providers configured | +| `modelExploration` | `false` | Epsilon-greedy exploration benchmarks under-sampled models | Sends requests to lower-quality models during exploration | +| `modelScoreUpdates` | `false` | NAL truth values updated automatically after each invocation | Score drift if task-type classifier misclassifies | +| `multiEmbodiment` | `false` | Multiple simultaneous I/O channels via `EmbodimentBus` | Cross-embodiment complexity; messages may reach unintended channels | +| `virtualEmbodiment` | `false` | Internal self-directed channel; agent generates its own tasks | Agent may spin on trivial self-tasks; pair with `autonomousLoop` | +| `autonomousLoop` | `false` | Loop runs continuously; generates self-tasks when idle | **Medium.** Runs indefinitely; continuous API calls without user prompting | +| `attentionSalience` | `false` | Multi-embodiment input prioritized by salience score rather than FIFO | May deprioritize low-salience but important inputs | +| `safetyLayer` | `false` | Reflective consequence analysis before side-effecting skills execute | ~50ms overhead per skill. Can block legitimate actions if rules too conservative | +| `auditLog` | `false` | Append-only record of all skill invocations, LLM calls, memory writes | Storage growth; minor I/O overhead | +| `rlhfCollection` | `false` | Execution traces written to `rlfp_training_data.jsonl` | Disk storage; conversation content captured to file | +| `dynamicSkillDiscovery` | `false` | Scans `memory/skills/*.metta` and `SKILL.md` files at startup/reload; auto-registers valid skill definitions with `SkillDispatcher` | Agent may load malformed skill definitions; requires `safetyLayer` gate for `add-skill` path | +| `executionHooks` | `false` | Enables declarative `pre-skill-hook` / `post-skill-hook` atoms in `hooks.metta`; hooks execute before/after skill handlers with mutation/deny/audit capabilities | Hook logic errors can block legitimate skill execution; requires careful rule authoring | +| `runtimeIntrospection` | `false` | Exposes `manifest`, `skill-inventory`, `subsystems`, `agent-state` as always-available grounded ops; generates `agent-manifest.metta` with active capabilities, registered skills, model scores | Minor overhead for manifest generation; introspection output may reveal internal structure | + +### 2.3 Experimental Tier + +Enable one at a time. Observe audit log carefully before enabling the next. + +| Flag | Default | What It Does | Risk If Enabled | +|---|---|---|---| +| `selfModifyingSkills` | `false` | `(add-skill sexpr)` writes new skill definitions to `skills.metta` | **High.** Agent can create broken or malicious skill definitions. Requires `safetyLayer` + `auditLog` | +| `harnessOptimization` | `false` | Agent analyzes failure traces and proposes changes to `memory/harness/prompt.metta` | **High.** Agent can corrupt its own system prompt. Git is the rollback mechanism | +| `memoryConsolidation` | `false` | Merges near-duplicate memories, decays low-confidence beliefs, prunes below threshold | **Medium.** May prune memories that were load-bearing context. Threshold tunable | +| `goalPursuit` | `false` | Agent sets and autonomously pursues goals across cycles via `(set-goal desc priority)` | **Medium.** Open-ended pursuit generates many unintended side effects without constraints | +| `subAgentSpawning` | `false` | Spawns scoped sub-agents (VirtualEmbodiment + isolated context) | **Medium.** Multiplies LLM calls; sub-agents share SemanticMemory | +| `selfEvaluation` | `false` | Agent scores its own recent outputs against stored preferences | **Low-Medium.** Circular evaluation possible if evaluation criteria are themselves evaluated | +| `harnessDiffusion` | `false` | Harness changes propagate via CRDT merge across multiple agent instances | **Experimental.** Concurrent modifications may conflict non-obviously. Implementation TBD (Phase 7+) | + +### 2.4 Capability Dependencies + +``` +autonomousLoop → loopBudget +goalPursuit → autonomousLoop, virtualEmbodiment +selfModifyingSkills → safetyLayer, auditLog +harnessOptimization → selfModifyingSkills, auditLog, persistentHistory +subAgentSpawning → virtualEmbodiment +memoryConsolidation → semanticMemory +modelExploration → multiModelRouting, modelScoreUpdates +harnessDiffusion → harnessOptimization +dynamicSkillDiscovery → selfModifyingSkills, semanticMemory +executionHooks → safetyLayer, auditLog +runtimeIntrospection → mettaControlPlane +``` + +Unsatisfied dependencies produce a clear startup error. No silent degradation. + +--- + +## 3. Configuration Profiles + +The `"profile"` key in `agent.json` selects a preset. Any `"capabilities"` keys in the same file override the profile. Unspecified flags take their documented defaults. + +### `minimal` — Reactive chatbot, no MeTTa control plane + +```json +{ + "profile": "minimal", + "capabilities": { + "mettaControlPlane": false, + "sExprSkillDispatch": false, + "semanticMemory": false, + "persistentHistory": false, + "loopBudget": false, + "contextBudgets": false, + "fileReadSkill": false, + "fileWriteSkill": false, + "shellSkill": false, + "webSearchSkill": true + } +} +``` + +### `parity` — Full MeTTaClaw feature parity, no autonomous/self-modifying capabilities + +```json +{ + "profile": "parity", + "capabilities": { + "mettaControlPlane": true, + "sExprSkillDispatch": true, + "semanticMemory": true, + "persistentHistory": true, + "loopBudget": true, + "contextBudgets": true, + "fileReadSkill": true, + "fileWriteSkill": false, + "shellSkill": false, + "webSearchSkill": true, + "auditLog": true + } +} +``` + +### `evolved` — Parity + evolution tier, no self-modification + +```json +{ + "profile": "evolved", + "capabilities": { + "mettaControlPlane": true, + "sExprSkillDispatch": true, + "semanticMemory": true, + "persistentHistory": true, + "loopBudget": true, + "contextBudgets": true, + "fileReadSkill": true, + "fileWriteSkill": true, + "shellSkill": false, + "webSearchSkill": true, + "multiModelRouting": true, + "modelExploration": true, + "modelScoreUpdates": true, + "multiEmbodiment": true, + "virtualEmbodiment": true, + "autonomousLoop": true, + "attentionSalience": true, + "safetyLayer": true, + "auditLog": true, + "rlhfCollection": true + } +} +``` + +### `full` — All capabilities. Controlled experimentation only. + +```json +{ + "profile": "full", + "capabilities": { + "mettaControlPlane": true, + "sExprSkillDispatch": true, + "semanticMemory": true, + "persistentHistory": true, + "loopBudget": true, + "contextBudgets": true, + "fileReadSkill": true, + "fileWriteSkill": true, + "shellSkill": false, + "webSearchSkill": true, + "multiModelRouting": true, + "modelExploration": true, + "modelScoreUpdates": true, + "multiEmbodiment": true, + "virtualEmbodiment": true, + "autonomousLoop": true, + "attentionSalience": true, + "safetyLayer": true, + "auditLog": true, + "rlhfCollection": true, + "selfModifyingSkills": true, + "harnessOptimization": true, + "memoryConsolidation": true, + "goalPursuit": true, + "subAgentSpawning": true, + "selfEvaluation": true, + "harnessDiffusion": false + } +} +``` + +--- + +## 4. Architecture Overview + +### 4.1 Execution Flow + +``` +╔════════════════════════════════════════════════════════╗ +║ AgentLoop.metta ║ ← mettaControlPlane +║ tail-recursive MeTTa loop; cap? gates every branch ║ +╠═══════════════════╦════════════════════════════════════╣ +║ build-context ║ SkillDispatcher ║ ← contextBudgets +║ (MeTTa function) ║ parse S-exprs → JS handlers ║ ← sExprSkillDispatch +║ ║ Supports dynamic registration ║ ← dynamicSkillDiscovery +║ ║ via discover-skills grounded op ║ +╠═══════════════════╩════════════════════════════════════╣ +║ ModelRouter ║ ← multiModelRouting +║ NAL-scored task-type routing over AIClient.js ║ +╠════════════════════════╦═══════════════════════════════╣ +║ LLM providers ║ Skill handlers ║ +║ (via AIClient.js) ║ remember/query/pin/forget ║ ← semanticMemory +║ ║ send / send-to / search ║ ← multiEmbodiment, webSearch +║ ║ read-file / write-file ║ ← fileReadSkill, fileWriteSkill +║ ║ shell ║ ← shellSkill +║ ║ metta / think ║ (always on) +║ ║ set-goal / add-skill ║ ← goalPursuit, selfModifying +╠════════════════════════╩═══════════════════════════════╣ +║ SemanticMemory ║ ← semanticMemory +║ PersistentSpace + HNSW + Embedder ║ +╠════════════════════════════════════════════════════════╣ +║ HookOrchestrator [optional] ║ ← executionHooks +║ pre-skill / post-skill hooks configured in hooks.metta ║ +╠════════════════════════════════════════════════════════╣ +║ SafetyLayer [optional] ║ ← safetyLayer / auditLog +╠════════════════════════════════════════════════════════╣ +║ IntrospectionOps [always available] ║ ← runtimeIntrospection +║ (manifest) (skill-inventory) (subsystems) (agent-state) ║ +║ Generate runtime agent-manifest.metta for self-query ║ +╠════════════════════════════════════════════════════════╣ +║ EmbodimentBus ║ ← multiEmbodiment +║ IRC │ Nostr │ CLI │ WebUI │ API │ VirtualInternal ║ ← virtualEmbodiment +╚════════════════════════════════════════════════════════╝ +``` + +### 4.2 The Control Plane Inversion + +**Current `agent/`:** JS event loop → JS prompt builder → LLM → JSON tool calls → JS handlers. MeTTa is a sidecar. + +**Target:** MeTTa loop → MeTTa context builder → LLM as grounded skill → S-expression output → MeTTa dispatches to JS. The existing `CognitiveArchitecture.cognitiveCycle()` becomes callable as `(cognitive-cycle $stimulus)` — one skill among many, not the outer container. `MeTTaReasoner.js` (currently a regex-based JS heuristic) is replaced by actual `metta/` evaluation. + +### 4.3 Control Plane State Variables + +All loop state lives as atoms in a `StateSpace` backed by `StateOps.js`: + +```metta +(= &budget 50) ; remaining iterations; agent can extend this +(= &prevmsg ()) ; last received message +(= &lastresults ()) ; last skill execution results +(= &lastsend "") ; last outbound content (send deduplication) +(= &error ()) ; parse error feedback for next cycle +(= ¤t-model "auto") ; specific model override, or "auto" for routing +(= ¤t-goal ()) ; highest-priority active goal +(= &loop-mode "reactive") ; "reactive" | "autonomous" | "paused" +(= &cycle-count 0) ; lifetime cycle counter; never resets +(= &wm ()) ; working memory register — see §11.2 +``` + +Each cycle, the loop decrements TTLs in `&wm` and drops expired entries before building context. Items enter `&wm` via the `(attend content priority)` skill or automatically from `check-embodiment-bus` for high-salience inputs. + +--- + +## 5. Component Specifications + +### 5.1 AgentLoop.metta + +**Location:** `agent/src/metta/AgentLoop.metta` +**Governed by:** `mettaControlPlane` + +```metta +(= (agent-start) + (do (agent-init) + (agent-loop (get-state &budget)))) + +(= (agent-loop $k) + (if (== $k 0) + (if (cap? autonomousLoop) + (agent-loop (reset-budget)) + (agent-halt)) + (let* + (($msg (next-input)) + ($k2 (if (new-message? $msg) (reset-budget) (- $k 1))) + ($_ (tick-wm)) ; decrement &wm TTLs, drop expired + ($ctx (build-context $msg)) ; &wm contents included in WM_REGISTER slot + ($resp (llm-invoke $ctx)) + ($cmds (parse-response $resp)) + ($result (execute-commands $cmds)) + ($_ (when (cap? persistentHistory) (append-history $msg $resp $result))) + ($_ (when (cap? auditLog) (emit-cycle-audit $msg $resp $result))) + ($_ (sleep-cycle))) + (agent-loop $k2)))) + +(= (next-input) + (if (cap? autonomousLoop) + (or (check-embodiment-bus) (generate-self-task)) + (check-embodiment-bus))) +``` + +`build-context` is a MeTTa function that assembles the context string from named slots, calling into JS for data retrieval. This makes context assembly inspectable via `(metta (build-context ...))` and modifiable by the experimental tier — without requiring it to be in MeTTa on day one (see Phase 1 note in §6). + +`execute-commands` dispatches up to `maxSkillsPerCycle` calls (default 3). Each call routes through `SafetyLayer` if `safetyLayer` enabled, then calls the JS handler, then emits an audit event if `auditLog` enabled. + +### 5.2 skills.metta + +**Location:** `agent/src/metta/skills.metta` +**Governed by:** living document, agent-writable if `selfModifyingSkills` + +```metta +;; (skill name arg-types capability-gate tier description) + +(skill remember (String) semanticMemory :memory "Store to long-term memory with embedding") +(skill query (String) semanticMemory :memory "Recall top-K semantically similar memories") +(skill pin (String) semanticMemory :memory "Add to pinned context (always in prompt)") +(skill forget (String) semanticMemory :memory "Remove memories matching query") +(skill send (String) mettaControlPlane :network "Send to current primary embodiment") +(skill send-to (Chan String) multiEmbodiment :network "Send to a specific named embodiment") +(skill search (String) webSearchSkill :network "Web search, return top results") +(skill read-file (Path) fileReadSkill :local-read "Read file within workdir") +(skill write-file (Path String) fileWriteSkill :local-write "Write file within memory/") +(skill append-file (Path String) fileWriteSkill :local-write "Append to file within memory/") +(skill shell (String) shellSkill :system "Execute allowlisted OS command") +(skill metta (SExpr) mettaControlPlane :reflect "Evaluate MeTTa expression, return results") +(skill think (String) mettaControlPlane :reflect "Internal monologue; not sent to any embodiment") +(skill cognitive-cycle (SExpr) mettaControlPlane :reflect "Invoke full LIDA CognitiveArchitecture.cognitiveCycle() on a stimulus atom") +(skill attend (String Float) mettaControlPlane :reflect "Add item to working memory register with priority 0–1; default TTL applies") +(skill dismiss (String) mettaControlPlane :reflect "Remove matching item from working memory register immediately") +(skill set-goal (String Float) goalPursuit :meta "Set current goal with priority 0.0–1.0") +(skill set-model (String) multiModelRouting :meta "Override active model for next N cycles") +(skill eval-model (String TaskType) modelExploration :meta "Benchmark a model on a task type") +(skill add-skill (SExpr) selfModifyingSkills :meta "Register new skill definition") +(skill consolidate () memoryConsolidation :meta "Trigger memory consolidation immediately") +(skill spawn-agent (String Int) subAgentSpawning :meta "Spawn sub-agent with task + cycle budget") +``` + +#### 5.2.1 Dynamic Skill Discovery + +When `dynamicSkillDiscovery` is enabled, `SkillDispatcher` performs two discovery passes at startup and on `selfModifyingSkills` changes: + +1. **Filesystem scan**: Recursively scan `memory/skills/**/*.metta` for `(skill ...)` declarations. Validate syntax via `metta/Parser.js`; skip malformed entries with audit warning. + +2. **SKILL.md parsing**: Parse markdown files matching `**/SKILL.md` using this schema: + ```markdown + ## Skill: skill-name + **Args**: `(arg1 Type1) (arg2 Type2)` + **Capability**: `capability-flag` + **Tier**: `:memory` | `:network` | `:meta` + **Description**: One-line summary + **Implementation**: `path/to/handler.js` or `inline-metta` + ``` + Convert valid entries to `(skill ...)` atoms and register. + +3. **Auto-reload**: When `selfModifyingSkills` writes to `memory/skills/`, trigger re-scan without full restart. + +**Grounded op**: Register `(discover-skills)` as a grounded op that manually triggers re-scan. Agent can invoke `(metta (discover-skills))` after adding skills. + +**Integration with existing**: Extends `SkillDispatcher.register()`; does not replace static `skills.metta` declarations. Static declarations take precedence; dynamic discoveries are additive. + +### 5.3 SkillDispatcher.js + +**Location:** `agent/src/skills/SkillDispatcher.js` +**Governed by:** `sExprSkillDispatch` + +Owns both registration and dispatch — no separate registry file. When `sExprSkillDispatch` is false, falls back to the existing `ToolAdapter.js` JSON tool-call path. + +**Integration with existing primitives:** `agent/src/metta/ChannelExtension.js` already registers grounded ops (`send-message`, `join-channel`, `web-search`, `read-file`, `write-file`) as direct JS bindings on the interpreter. `SkillDispatcher` does not replace these — it adds the S-expression parsing layer and capability gates on top. The `send`, `search`, `read-file`, and `write-file` skill handlers delegate to the same underlying JS functions that `ChannelExtension` wires. + +Responsibilities: +- Parse the LLM's S-expression response via `metta/Parser.js` (with MeTTaClaw-style parenthesis balancing; on failure, populates `&error` and returns empty commands) +- Look up each parsed `(skill arg...)` call against registered handlers +- Gate on capability flags (disabled-skill call produces an audit warning, not an exception) +- If `safetyLayer` enabled: pass through `SafetyLayer.check()` before calling handler +- If `auditLog` enabled: emit `(audit-event :type :skill-invoked ...)` after each call +- Return results array to `AgentLoop` for `&lastresults` + +#### 5.3.1 HookOrchestrator Integration + +When `executionHooks` is enabled, `SkillDispatcher.execute()` routes each skill call through `HookOrchestrator` before and after handler execution: + +```javascript +// Pseudocode for SkillDispatcher.execute() +async execute(parsedCmds) { + for (const cmd of parsedCmds) { + // PRE-HOOK PHASE + if (config.capabilities.executionHooks) { + const hookResult = await HookOrchestrator.runPreHooks(cmd); + if (hookResult.action === 'deny') { + emitAudit({ type: 'skill-blocked', reason: hookResult.reason }); + continue; + } + if (hookResult.action === 'rewrite') { + cmd.args = hookResult.newArgs; // mutated args + } + } + + // SAFETY LAYER (existing) + if (config.capabilities.safetyLayer) { + const safety = await SafetyLayer.check(cmd.name, cmd.args); + if (!safety.cleared) { /* ...existing handling... */ } + } + + // HANDLER EXECUTION (existing) + const result = await handler(cmd.args); + + // POST-HOOK PHASE + if (config.capabilities.executionHooks) { + await HookOrchestrator.runPostHooks(cmd, result); + } + + // AUDIT (existing) + if (config.capabilities.auditLog) { /* ... */ } + } +} +``` + +**Hook definition format** (`hooks.metta`): +```metta +;; Pre-hook: deny shell commands containing forbidden patterns +(hook pre (shell $cmd) + (if (contains-forbidden? $cmd) + (deny "Command contains forbidden pattern") + (allow))) + +;; Post-hook: audit all write-file operations +(hook post (write-file $p $c) + (audit (emit :file-written :path $p :size (string-length $c)))) + +;; Pre-hook: mutate search queries to add safe-search flag +(hook pre (search $q) (rewrite (search (string-append $q " safe_search=active")))) +``` + +**HookOrchestrator API** (new file `agent/src/skills/HookOrchestrator.js`): +```javascript +class HookOrchestrator { + static async runPreHooks(skillCall) // returns { action: 'allow'|'deny'|'rewrite', newArgs?, reason? } + static async runPostHooks(skillCall, result) // returns void; may emit audit events + static loadHooksFromFile(path) // parse hooks.metta, register in memory +} +``` + +**Phase 1 refinement note**: Since Phase 1 already implements `SkillDispatcher`, extend the existing `execute()` method with the hook orchestration logic above. No new class needed for Phase 1—inline the hook checks conditionally on `config.capabilities.executionHooks`. + +### 5.4 SemanticMemory.js + +**Location:** `agent/src/memory/SemanticMemory.js` +**Governed by:** `semanticMemory` + +Built on `metta/src/extensions/PersistentSpace.js` (Merkle hash integrity, CRDT vector clocks, Node.js FS + IndexedDB backends — already implemented). + +**Relationship to existing `Memory.js`:** `core/src/memory/Memory.js` provides concept-based NARS-style memory (pattern-matched atoms, no embeddings). `SemanticMemory.js` is an additional layer — it does not replace `Memory.js`. The existing `agent/src/metta/MemoryExtension.js` registers `remember`/`recall` bindings against `Memory.js`; those are used by the NARS cognitive architecture. In Phase 2, `SkillDispatcher` registers new `remember`/`query`/`pin`/`forget` handlers against `SemanticMemory.js`, which supersede the `MemoryExtension.js` bindings for LLM-facing skill calls. The two systems run in parallel: `Memory.js` for NARS concept reasoning; `SemanticMemory.js` for embedding-based recall. Embeddings are an index overlay on the atom store — not a replacement for the underlying concept memory. + +``` +SemanticMemory +├── AtomStore — PersistentSpace (atoms + metadata as MeTTa triplets) +├── VectorIndex (private) — HNSW via hnswlib-node; brute-force fallback for < 1000 items +└── Embedder — lazy-loaded @huggingface/transformers Xenova/all-MiniLM-L6-v2 (384-dim) + fallback: OpenAI embeddings API (config-selectable) +``` + +`VectorIndex` is a private class within `SemanticMemory.js`. It is not a public interface — it is an implementation detail that may change. Wrapping it separately would create an abstraction boundary with nothing on the other side. + +**Atom format:** + +```metta +(memory-atom + :id "mem_1743432000_abc" + :timestamp 1743432000000 + :content "User prefers terse explanations without preamble" + :source "irc:##metta:user42" + :type :semantic ; :semantic | :episodic | :procedural | :pinned + :truth (stv 0.9 0.8) + :tags ("preference" "style") + ; embedding stored in memory.vec sidecar, indexed by :id +) +``` + +Restore on startup: `PersistentSpace.restore()` rehydrates atoms; HNSW index rebuilt from sidecar. First embedding call lazy-loads the ONNX model. + +**Budget configuration:** + +```json +"memory": { + "maxRecallItems": 20, + "maxRecallChars": 8000, + "maxHistoryChars": 12000, + "maxFeedbackChars": 6000, + "pinnedMaxChars": 3000, + "embedder": "Xenova/all-MiniLM-L6-v2", + "vectorDimensions": 384, + "beliefDecay": 0.001, + "pruneThreshold": 0.2, + "consolidationInterval": 100 +} +``` + +### 5.5 ModelRouter.js + +**Location:** `agent/src/models/ModelRouter.js` +**Governed by:** `multiModelRouting` + +When `multiModelRouting` is false, `invoke()` routes directly to the configured fallback model via `AIClient.js`. When true, it scores models by task type using NAL truth values stored as `model-score` atoms in SemanticMemory — no separate preferences store needed. + +Model scoring atoms live in SemanticMemory with `:type :procedural`: + +```metta +(model-score "gpt-4o" :reasoning (stv 0.85 0.72)) +(model-score "claude-sonnet-4-6" :introspection (stv 0.91 0.82)) +(model-score "qwen2.5-coder" :code (stv 0.82 0.65)) +``` + +NAL expectation for selection: `E(stv(f,c)) = f + c*(0.5 - f)` — penalizes low-confidence scores without overvaluing untested models. + +When `modelScoreUpdates` enabled, each invocation records a `model-invocation` atom and applies `Truth_Revision` to the relevant `model-score` atom. When `modelExploration` enabled, epsilon-greedy selection occasionally routes to the model with the lowest confidence score (fewest samples). + +Latency and token tracking are captured inline per call — three lines, not a class. + +Task type classification is a lightweight heuristic (keyword + pattern matching; no LLM call). The classifier is itself a `read-file`-able JS function the agent can inspect. + +**Task types (extensible):** `:reasoning` `:code` `:creative` `:retrieval` `:tool-use` `:introspection` `:social` + +### 5.6 EmbodimentBus.js + VirtualEmbodiment.js + +**Location:** `agent/src/io/EmbodimentBus.js`, `agent/src/io/VirtualEmbodiment.js` +**Governed by:** `multiEmbodiment`, `virtualEmbodiment` + +When `multiEmbodiment` is false, `EmbodimentBus` wraps a single channel — same interface, no routing complexity. This means `AgentLoop.metta` always uses `check-embodiment-bus`; whether that routes through one or many embodiments is an infrastructure detail. + +Existing `IRCChannel`, `NostrChannel`, `CLIChannel` adopt the `Embodiment` interface (connect/disconnect/send/receive/profile/salience). `MatrixChannel` likewise. + +`VirtualEmbodiment` is always present when `virtualEmbodiment` enabled. It provides: +- Self-task injection during idle autonomous cycles +- Goal-pursuit task generation when `goalPursuit` enabled +- Scoped sub-agent context when `subAgentSpawning` enabled + +When `attentionSalience` is false, `getNextMessage()` is FIFO across embodiments. + +### 5.7 SafetyLayer.js + AuditSpace.js + +**Location:** `agent/src/safety/SafetyLayer.js`, `agent/src/safety/AuditSpace.js` +**Governed by:** `safetyLayer`, `auditLog` + +`SafetyLayer` and `AuditSpace` are independent — audit logging works without the safety layer and vice versa. + +`SafetyLayer.check(skillName, args)` runs a time-bounded (default 50ms) MeTTa forward-inference pass using `safety.metta` rules. Fail-closed: inference timeout = blocked. Returns `{ cleared, reason, consequences }`. + +`AuditSpace` is an append-only `PersistentSpace`. The agent can read its own audit log via `(metta (get-atoms &audit-space))`. This is the diagnostic substrate for `HarnessOptimizer`. + +### 5.8 HarnessOptimizer.js + +**Location:** `agent/src/harness/HarnessOptimizer.js` +**Governed by:** `harnessOptimization` + +Runs as a self-task on `VirtualEmbodiment` every `harnessEvalInterval` cycles (default 200). Analyzes failure audit atoms, proposes ONE targeted change to `memory/harness/prompt.metta`, replays on a sample of recent tasks, applies if improved. + +**Version history is git.** `memory/` is a git-tracked directory. Every harness write commits with message `harness-update: cycle {N}`. Rollback: `git revert` or `git checkout`. No custom versioning scheme. + +#### 5.8.1 Verification Loop Integration + +When `runtimeIntrospection` and `auditLog` are enabled, `HarnessOptimizer` runs a verification sub-cycle after proposing harness changes: + +``` +1. Apply candidate diff to prompt.candidate.metta +2. Run (manifest) and (skill-inventory) to capture pre-change state +3. Replay sampled tasks with candidate harness +4. Run (manifest) and (skill-inventory) post-replay +5. Compare skill invocation patterns, error rates, audit event counts +6. Only install candidate if: + - No new skill-blocked audit events + - Error rate unchanged or improved + - Manifest shows expected capability changes +``` + +This turns harness optimization into a *verified* self-modification process, reducing risk of regressions. + +### 5.9 capabilities.js (module, not class) + +**Location:** `agent/src/config/capabilities.js` + +Two exports: + +```javascript +export function isEnabled(config, flag) { + return config.capabilities[flag] ?? DEFAULTS[flag] ?? false; +} + +export function validateDeps(config) { + // Checks DEPENDENCY_TABLE against resolved capabilities + // Throws with clear message on unsatisfied deps +} +``` + +The `cap?` MeTTa grounded op is registered as: +```javascript +interp.registerOp('cap?', (flag) => isEnabled(agentConfig, flag)) +``` + +No class, no instance, no state. Capability state lives in `agent.json`. + +### 5.10 IntrospectionOps.js — Runtime Self-Description + +**Location:** `agent/src/introspection/IntrospectionOps.js` +**Governed by:** `runtimeIntrospection` (but grounded ops always register; capability gates output content) + +**Purpose:** Provide always-available grounded ops that generate structured self-descriptions for agent self-query and external tooling. + +**Registered grounded ops** (via `AgentBuilder.buildMeTTaLoop()`): +```javascript +interp.registerOp('manifest', () => IntrospectionOps.generateManifest(config)) +interp.registerOp('skill-inventory', () => IntrospectionOps.listSkills(dispatcher, config)) +interp.registerOp('subsystems', () => IntrospectionOps.describeSubsystems()) +interp.registerOp('agent-state', (key) => IntrospectionOps.getState(key)) +``` + +**Output format** (MeTTa atoms, agent-readable): +```metta +;; (manifest) output +(agent-manifest + :version "0.1.0" + :profile "evolved" + :capabilities ((mettaControlPlane true) (semanticMemory true) ...) + :active-skills ((remember (String) :memory) (query (String) :memory) ...) + :embodiments ((irc:##metta :active true) (cli :active true)) + :model-scores ((gpt-4o :reasoning (stv 0.85 0.72)) ...) + :cycle-count 142 + :wm-entries-count 3) + +;; (skill-inventory) output +(skill-inventory + (skill-entry :name remember :args (String) :tier :memory :enabled true) + (skill-entry :name query :args (String) :tier :memory :enabled true) + ...) + +;; (agent-state "&budget") → 47 +;; (agent-state "&wm") → ((wm-entry :content "..." :priority 0.8 :ttl 7) ...) +``` + +**Implementation notes:** +- `generateManifest()` aggregates data from `config`, `SkillDispatcher.getActiveSkillDefs()`, `EmbodimentBus.getActiveChannels()`, `ModelRouter.getScores()`, and loop state variables. +- Output is cached for 5 cycles to avoid regeneration overhead; invalidated on capability changes. +- When `runtimeIntrospection` is false, ops return minimal stub: `(manifest :restricted true)`. + +**Phase 1 refinement note**: Since Phase 1 already registers grounded ops in `AgentBuilder.buildMeTTaLoop()`, add the four introspection ops to that registration block. Implement `IntrospectionOps` as a simple module with static methods—no class instantiation needed for Phase 1. + +--- + +## 6. Phase Plan + +Each phase is a self-contained working increment. Later phases do not block on earlier phases being "perfect." + +### Phase 1 — MeTTa Control Plane + +**Deliverable:** MeTTa loop drives the agent; existing channels and LLM providers work unchanged. + +1. Create directory structure (see §12), including `agent/src/config/` and `agent/workspace/`. +2. Create `agent/workspace/agent.json` with default `"profile": "parity"` config (schema: §13.1). Implement `capabilities.js` — `isEnabled()`, `validateDeps()`, profile resolution. These two are required before any capability-gated code can run. +3. Extend `AgentBuilder.js` with `buildMeTTaLoop()` — wires `MeTTaInterpreter`, registers all grounded ops (`check-embodiment-bus`, `llm-invoke`, `parse-response`, `execute-commands`, `cap?`, etc.), loads `AgentLoop.metta`. This is startup wiring code, not a new class. +4. Implement `AgentLoop.metta` and `skills.metta` (parity-tier skills only). +5. Implement `SkillDispatcher.js` with S-expression parse, handler registration, JSON fallback. +6. **Note on `build-context`:** Phase 1 implements it as a JS function registered as a grounded op. It reads slot data from existing sources (history file, `&lastresults`, `&error`, `&wm`) and assembles a string. Promoting it to a full MeTTa function happens when context modification becomes useful in Phase 6. +7. Implement `tick-wm` grounded op — decrements TTLs in `&wm`, drops expired entries, called at the top of each loop cycle before context assembly. +8. Register `attend` and `dismiss` skill handlers; wire `autoAttendThreshold` check into `check-embodiment-bus` (high-salience inputs auto-attend). +9. Add `"controlPlane"` check to `Agent.js` startup: `"mettaControlPlane": true` routes to `buildMeTTaLoop()`; false uses existing LIDA path. +10. **Test:** `profile: parity` (minus `semanticMemory`) runs 3 cycles with mock LLM; `(attend ...)` item persists across cycles; expires at TTL 0; correct skill dispatch; graceful malformed-output recovery. + +**Phase 1 Refinements** (when extending with new Evolution-tier capabilities): +11. **Extend `SkillDispatcher.js`**: Add conditional hook orchestration logic (§5.3.1) gated on `config.capabilities.executionHooks`. When disabled, bypass with zero overhead. +12. **Add dynamic discovery stub**: Implement `discover-skills` grounded op that scans `memory/skills/*.metta` (filesystem only, no SKILL.md parsing yet). Register discovered skills with existing `register()` method. +13. **Register introspection ops**: Add `manifest`, `skill-inventory`, `subsystems`, `agent-state` grounded ops to `AgentBuilder.buildMeTTaLoop()`. Implement minimal stub output that respects `runtimeIntrospection` capability flag. +14. **Update `agent.json` schema** (§13.1): Add `dynamicSkillDiscovery`, `executionHooks`, `runtimeIntrospection` flags with defaults `false`. +15. **Test:** With new flags disabled, existing Phase 1 behavior unchanged. With flags enabled: dynamic skill file loaded on startup; `(manifest)` returns structured output; pre-hook can block a skill call. + +### Phase 2 — Semantic Memory + +**Deliverable:** Cross-session embedding memory; RECALL and PINNED slots populated in context. + +1. Add `hnswlib-node` to `agent/package.json`; add `vectra` as a pure-JS fallback for environments that cannot compile native bindings. Neither is currently present. +2. Implement `Embedder.js` — lazy ONNX load; OpenAI API fallback. +3. Implement `SemanticMemory.js` with private `VectorIndex`, `PersistentSpace` backing, `restore()` on startup. +4. Register `remember`, `query`, `pin`, `forget` handlers in `SkillDispatcher`; these supersede the `MemoryExtension.js` bindings for LLM-facing skill calls (see §5.4). +5. Wire RECALL and PINNED slots into the JS `build-context` function. +6. Supersede `PersistenceManager.js` — existing persisted state migrated to atom format. +7. **Test:** 200 items stored; query returns correct top-5 by cosine; survives restart; RECALL slot populated. + +### Phase 3 — Multi-Model Intelligence + +**Deliverable:** ModelRouter routes by task type; model preferences learned and persisted. + +1. Implement `ModelRouter.js` — NAL expectation scoring from `model-score` atoms in SemanticMemory; `AIClient.js` is the LLM provider (no adapter wrapper); inline latency/token tracking. +2. Implement `ModelBenchmark.js` — micro-task evaluator (5 canonical tasks per type, auto-scored). +3. Register `eval-model` and `set-model` skill handlers. +4. Wire `modelScoreUpdates` path: post-invocation `Truth_Revision` on the relevant `model-score` atom. +5. **Test:** Two providers configured; router selects correct model after 10 observations per task type. + +### Phase 4 — Safety & Accountability + +**Deliverable:** Tier-gated skill execution; audit trail; shell guard. + +1. Implement `safety.metta` with **direct rules only** — one `(consequence-of skill consequence risk)` fact per skill. No chaining in this phase. +2. Implement `SafetyLayer.js` — tier lookup, 50ms-budgeted MeTTa inference, fail-closed on timeout. +3. Implement `AuditSpace.js` — append-only `PersistentSpace`; `emitEvent(type, data)` API. +4. Implement `ShellGuard` (inline in shell skill handler, ~30 lines) — allowlist check, `child_process.spawn` with `shell: false`. +5. Wire `SafetyLayer` into `SkillDispatcher`. +6. **Test:** Blocked skills produce audit atoms with zero overhead when `safetyLayer: false`. + +**Phase 4 Extension** (when adding hook system): +7. **Promote `SafetyLayer` to hook substrate**: Refactor `SafetyLayer.check()` to become the *first* pre-skill hook in the `HookOrchestrator` pipeline. Existing safety rules migrate to `hooks.metta` format: + ```metta + ;; Old safety.metta + (consequence-of (shell $cmd) (system-state-change :unknown) :high) + + ;; New hooks.metta equivalent + (hook pre (shell $cmd) + (if (high-risk? $cmd) + (deny "High-risk shell command") + (allow))) + ``` +8. **Backward compatibility**: When `executionHooks` is false but `safetyLayer` is true, run legacy `SafetyLayer.check()` path. When both enabled, safety rules run as first pre-hook. + +### Phase 4.5 — Execution Hooks & Runtime Introspection + +**Deliverable:** Declarative hook system; structured self-description primitives. + +1. Implement `HookOrchestrator.js` with pre/post hook execution, mutation/deny/audit actions. +2. Create `hooks.metta` with example rules for common patterns (audit logging, input sanitization, permission checks). +3. Complete `IntrospectionOps.js` with full manifest generation, skill inventory, subsystem description. +4. Wire `(manifest)` output into `build-context`'s new `AGENT_MANIFEST` slot (between `PINNED` and `RECALL`). +5. **Test:** Pre-hook blocks forbidden skill; post-hook emits audit event; `(manifest)` returns structured atom; agent can `(metta (query "what skills do I have?"))` using introspection output. + +### Phase 5 — Embodiment Abstraction + +**Deliverable:** Channel-agnostic I/O; multi-embodiment; internal self-directed channel. + +1. Define `Embodiment` interface (connect/disconnect/send/receive/profile/salience). +2. Implement `EmbodimentBus.js` — registration, FIFO or salience-ordered `getNextMessage()`, broadcast. +3. Implement `VirtualEmbodiment.js` — task queue, self-task generation, sub-agent scoping. +4. Update `IRCChannel`, `NostrChannel`, `CLIChannel` to implement `Embodiment`. +5. Replace `ChannelManager.js` with `EmbodimentBus` — `ChannelManager` is superseded. +6. Register `send-to` and `spawn-agent` skill handlers. +7. **Test:** Two embodiments active; `getNextMessage()` FIFO and salience-ordered modes both work. + +### Phase 6 — Meta-Harness & Self-Improvement + +**Deliverable:** Agent improves its own prompts and skills. + +1. Implement `HarnessOptimizer.js` — failure sampling, diff proposal via `:introspection` model, candidate replay, conditional install, git commit. +2. Promote `build-context` from JS function to `ContextBuilder.metta` — makes context assembly a MeTTa program the agent can read and reason about. +3. Implement `add-skill` handler — appends to `skills.metta` via `&add-rule` + file write; requires `SafetyLayer` gate. +4. Implement `memoryConsolidation` self-task — NAL revision over near-duplicate atoms, decay, prune, generalize episodic→procedural. +5. Wire `RLFPLearner.js` output into `ModelRouter` — `rlfp_training_data.jsonl` preference entries update `model-score` atoms. +6. Implement `selfEvaluation` self-task — agent scores recent outputs against stored preference atoms. +7. **Test:** Harness modification cycle runs end-to-end; version committed to git; `git log memory/harness/` shows history. + +**Phase 6 Extensions** (when integrating verification and dynamic reload): +8. **Integrate verification loop**: Extend `HarnessOptimizer` to use introspection primitives for pre/post-change validation (§5.8.1). +9. **Dynamic skill reload**: When `selfModifyingSkills` writes to `memory/skills/`, trigger `discover-skills` re-scan; validate new skill via `SafetyLayer` before registration. + +--- + +## 7. Safety & Accountability + +### 7.1 Capability Tiers + +```metta +;; Stored as atoms; inspectable by agent; not self-modifiable +(capability-grant :tier :reflect :granted true :reason "Always available") +(capability-grant :tier :memory :granted true :reason "Conditional on semanticMemory flag") +(capability-grant :tier :local-read :granted true :reason "Read within cwd") +(capability-grant :tier :network :granted true :reason "Send and search") +(capability-grant :tier :local-write :granted false :reason "Disabled by default") +(capability-grant :tier :system :granted false :reason "Shell disabled by default") +(capability-grant :tier :meta :granted false :reason "Self-modification disabled by default") +``` + +Grants derive from `agent.json`. The agent can read them; changing them requires editing `agent.json` and restarting. + +### 7.2 safety.metta — Phase 4 (Direct Rules) + +Phase 4 ships with one fact per skill. No consequence chaining — that complexity is earned, not assumed. + +```metta +;; (consequence-of skill-call consequence risk-level) +(= (consequence-of (shell $cmd) (system-state-change :unknown) :high)) +(= (consequence-of (write-file $p $_) (file-modified $p) :medium)) +(= (consequence-of (append-file $p $_) (file-modified $p) :medium)) +(= (consequence-of (send $msg) (message-delivered :external) :medium)) +(= (consequence-of (send-to $c $msg) (message-delivered :external $c) :medium)) +(= (consequence-of (remember $s) (memory-updated :local) :low)) +(= (consequence-of (search $q) (network-request :outbound) :low)) +(= (consequence-of (add-skill $def) (skills-modified) :high)) +(= (consequence-of (write-file "memory/harness/prompt.metta" $_) + (system-prompt-modified) :critical)) +``` + +Consequence chaining (A→B→C inference) is a Phase 6+ extension when the direct rules have been observed in practice. + +**Migration note**: When `executionHooks` is enabled, `safety.metta` rules are automatically wrapped as pre-skill hooks by `HookOrchestrator`. Authors may migrate rules to `hooks.metta` for finer control (mutation, conditional denial, post-execution audit). Both formats coexist; `hooks.metta` takes precedence for skills with defined hooks. + +### 7.3 Shell Guard + +`shellSkill` is `false` by default. When enabled: + +```json +"shell": { + "allowlist": ["git status", "git log --oneline", "pnpm test", "node --version"], + "allowedPrefixes": ["git "], + "forbiddenPatterns": ["rm", "sudo", "curl", "wget", ">", "|", ";", "&&", "`", "$(", "eval"] +} +``` + +Execution always uses `child_process.spawn` with `shell: false` and pre-split argument array. Never `exec` with a raw string. + +### 7.4 Audit Events + +```metta +(audit-event + :id "aud_1743432001_xyz" + :timestamp 1743432001000 + :type :skill-blocked ; :skill-invoked | :skill-blocked | :llm-call | :memory-write | :harness-modified + :skill (write-file "/etc/passwd" "...") + :reason "Path traversal outside workdir" + :cycle 42 + :model "gpt-4o" +) +``` + +The agent reads its own audit log via `(metta (get-atoms &audit-space))`. This is the diagnostic substrate for `HarnessOptimizer`. + +### 7.5 Declarative Hook Rules (`hooks.metta`) + +**Location:** `agent/src/metta/hooks.metta` +**Governed by:** `executionHooks` + +**Rule format**: +```metta +(hook ) +``` + +Where: +- ``: `pre` or `post` +- ``: S-expression pattern matching skill name and args, e.g., `(shell $cmd)`, `(write-file $p $_)` +- ``: MeTTa expression that returns one of: + - `(allow)` — proceed with original/modified args + - `(deny reason-string)` — block execution, emit audit event + - `(rewrite new-args)` — mutate arguments before handler execution (pre-hook only) + - `(audit event-atom)` — append to audit log (post-hook only) + +**Built-in predicates** (available in hook bodies): +- `(contains-forbidden? $str)` — checks against `agent.json` `shell.forbiddenPatterns` +- `(path-within? $path $base)` — validates file paths are within allowed directory +- `(capability-enabled? $flag)` — checks if capability flag is active +- `(audit-emit $atom)` — appends atom to `&audit-space` + +**Example rules**: +```metta +;; Block shell commands with forbidden patterns +(hook pre (shell $cmd) + (if (contains-forbidden? $cmd) + (deny "Forbidden pattern in shell command") + (allow))) + +;; Auto-audit all file writes +(hook post (write-file $p $c) + (audit-emit (audit-event :type :file-write :path $p :size (string-length $c)))) + +;; Sanitize search queries +(hook pre (search $q) + (rewrite (search (string-append $q " safe_search=active")))) +``` + +**Execution order**: Hooks execute in declaration order. First `(deny)` stops the chain; `(rewrite)` mutations accumulate. + +--- + +## 8. Multi-Model Intelligence + +### 8.1 Philosophy + +Every model is broken. "Which one is least wrong for this task, right now?" is the empirical question. NAL truth values are the answer format: frequency encodes success rate, confidence encodes sample size. + +### 8.2 NAL Scoring + +```metta +(model-score "gpt-4o" :reasoning (stv 0.85 0.72)) ; 72% confident +(model-score "claude-sonnet-4-6" :introspection (stv 0.91 0.82)) +(model-score "llama3.2" :code (stv 0.60 0.45)) ; few samples +``` + +Selection uses NAL expectation `E = f + c*(0.5 - f)` — a model with `stv(0.8, 0.1)` (high frequency, low confidence) scores lower than `stv(0.75, 0.9)` (slightly lower frequency, well-established). This prevents overconfident routing on thin evidence. + +Post-invocation update (when `modelScoreUpdates`): +- Success: `Truth_Revision(current, stv(1.0, 0.9))` +- Failure: `Truth_Revision(current, stv(0.0, 0.9))` + +### 8.3 The Fine-Tuning Trajectory + +The architecture makes this trajectory possible without architecture changes: + +1. `rlhfCollection` collects labeled traces → `rlfp_training_data.jsonl` (already implemented in `RLFPLearner.js`) +2. `HarnessOptimizer` curates high/low scoring pairs per task type +3. Pairs → Ollama fine-tuning workflow → local model checkpoint +4. New model registered in `agent.json` providers; `eval-model` benchmarks it +5. `ModelRouter` picks it up when NAL scores justify it + +### 8.4 Configuration + +```json +"models": { + "fallback": "gpt-4o-mini", + "explorationRate": 0.2, + "providers": { + "openai": { "enabled": true, "models": ["gpt-4o", "gpt-4o-mini"] }, + "anthropic": { "enabled": true, "models": ["claude-sonnet-4-6"] }, + "ollama": { "enabled": true, "models": ["llama3.2", "qwen2.5-coder"] } + } +} +``` + +--- + +## 9. Embodiment Abstraction + +### 9.1 Principle + +The agent does not exist _in_ IRC. It exists, and its existence has embodiments. Each embodiment is a named, typed I/O interface. Agent identity and reasoning are independent of embodiment. + +When `multiEmbodiment` is false, `EmbodimentBus` wraps a single channel. The agent loop never needs to know the difference. + +### 9.2 Embodiment Interface + +```javascript +class Embodiment extends EventEmitter { + get id() {} // "irc-quakenet", "nostr-main", "cli", "virtual-self" + get type() {} // "irc" | "nostr" | "matrix" | "cli" | "api" | "virtual" + + async connect() {} + async disconnect() {} + async send(message, opts) {} // opts: { target, format, replyTo } + async receive() {} // returns Message | null (non-blocking) + + get profile() {} // { displayName, context, users } + get salience() {} // float 0–1: urgency of input from this embodiment right now +} +``` + +### 9.3 VirtualEmbodiment + +Always present when `virtualEmbodiment` enabled. Powers: +- **Self-tasks:** Idle autonomous cycles inject tasks into its queue (consolidate memory, eval-model, analyze traces). +- **Goal pursuit:** Active goals spawn sub-tasks via `goalPursuit`. +- **Sub-agents:** `spawn-agent` creates a new VirtualEmbodiment instance with isolated context, runs N cycles, returns result. + +### 9.4 EmbodimentBus.getNextMessage() + +Returns the highest-salience pending message across all active embodiments. When `attentionSalience` is false: FIFO. When enabled: uses `Message.salience` (set by each embodiment based on mention detection, user history, channel priority). + +--- + +## 10. Meta-Harness: Self-Improving Prompts + +### 10.1 Inspiration + +From [Lee (2025), "Meta-Harness"](https://yoonholee.com/meta-harness/): give the optimizer direct access to raw execution traces rather than summaries. Results: cross-model transfer, 10× sample efficiency over program-search, +7.7 points on classification benchmarks. + +Here the agent _is_ the optimizer — no external process. The same agent loop that acts also improves itself during autonomous idle cycles. + +### 10.2 What "Harness" Means Here + +| File | Writable by Agent | Governing Flag | +|---|---|---| +| `memory/harness/prompt.metta` | Yes | `harnessOptimization` | +| `agent/src/metta/skills.metta` | Yes | `selfModifyingSkills` | +| `agent/src/metta/ContextBuilder.metta` | Yes (Phase 6+) | `harnessOptimization` | + +### 10.3 Optimization Cycle + +Runs as a `VirtualEmbodiment` self-task every `harnessEvalInterval` cycles (default 200): + +``` +1. Sample 20 recent audit atoms where skill failed, parse error occurred, + or self-evaluation scored output below preference threshold + +2. Read current harness files + +3. Invoke with :introspection task type: + "Given these failures, propose ONE targeted change to the system prompt + as a unified diff." + +4. Apply diff to memory/harness/prompt.candidate.metta + +5. Replay 10 sampled recent tasks with candidate harness; score outputs + +6. If score > current + threshold: + git commit -m "harness-update: cycle N, +Δ score" memory/harness/prompt.metta + Else: + discard candidate + +7. Emit (audit-event :type :harness-modified :result :improved/:rejected :delta Δ) +``` + +**Version history is git.** `memory/` is committed to the repo. `git log memory/harness/` shows the full modification history. `git revert` is the rollback. No custom versioning scheme is needed or built. + +### 10.4 Trace Storage + +``` +memory/ +├── traces/YYYY-MM-DD/cycle_{N}.jsonl — context, response, skills, results per cycle +├── harness/prompt.metta — current system prompt +├── history.metta — append-only conversation/action history +└── audit.metta — append-only audit atoms +``` + +The agent can `(shell "grep parse-error memory/traces/$(date +%F)/*.jsonl")` to diagnose failure patterns — filesystem-based diagnosis as the Meta-Harness paper advocates, available as a skill. + +--- + +## 11. Memory Architecture + +The design has three distinct temporal layers with different persistence characteristics and decay mechanisms. Understanding each one prevents confusing them. + +### 11.1 Three-Tier Temporal Model + +| Layer | Storage | Decay Mechanism | Purpose | +|---|---|---|---| +| **Working Memory Register** (`&wm`) | RAM (state var) | TTL countdown per cycle | "Keep this in mind" without persisting; survives across cycles by design | +| **Context Window** | Assembled per cycle | Budget truncation (oldest first) | Current cognitive workspace; history slot provides recency gradient | +| **Long-term Memory** | PersistentSpace + HNSW | `beliefDecay` on truth confidence per cycle | Survives restarts; indexed for semantic recall | + +These are not redundant. They serve different temporal scales and different failure modes. + +### 11.2 Working Memory Register (`&wm`) + +The register is a small in-RAM ordered list of `(content, priority, ttl-remaining)` tuples. Default TTL is 10 cycles. It is **not** a LIDA working memory system — no capacity constraint, no competitive slot selection, no complex attention algorithm. It is the simplest structure that preserves the key temporal property: **the ability to hold something in mind across multiple cycles without writing it to long-term memory and without relying on the history slot to keep it visible.** + +```metta +;; &wm entry format +(wm-entry :content "ping Alice when task is complete" + :priority 0.8 + :ttl 7 ; 3 cycles have elapsed since this was attended to + :source "user42" + :cycle-added 5) +``` + +**Lifecycle:** +- `(attend content priority)` skill adds an entry with default TTL from config +- `(dismiss content)` removes a matching entry immediately +- `tick-wm` (called at the top of each cycle, before context assembly) decrements all TTLs and drops any that reach 0 +- High-salience inputs from `check-embodiment-bus` automatically call `attend` (threshold configurable) +- When an item expires, it is NOT automatically written to long-term memory — the agent must have explicitly `(remember ...)`-ed it if it wanted persistence + +**WM_REGISTER context slot** (between PINNED and RECALL in context assembly): +``` +WM_REGISTER — 1,500 chars — current &wm entries, sorted by priority +AGENT_MANIFEST — 2,000 chars — condensed output of (manifest) when runtimeIntrospection enabled +``` + +This ensures the LLM sees "things currently held in mind" separately from recalled long-term memories and scrolling history — a meaningful epistemic distinction. The AGENT_MANIFEST slot provides structured self-knowledge in-context without needing to invoke `(metta (manifest))` explicitly. + +**Why not use the context window alone?** Two scenarios where `&wm` adds value the context window cannot provide: +1. **Instruction across many cycles:** "Check on that deployment in 20 minutes" — with a 50-cycle budget at 2s sleep, that's 600s. The instruction needs to persist in a dedicated register, not compete with history budget. +2. **Priority tagging:** Items in `&wm` have explicit priorities the agent set. History is flat. The LLM can reason about what the agent chose to "keep in mind" vs. what just happened to flow through. + +**Configuration:** +```json +"workingMemory": { + "defaultTtl": 10, + "autoAttendThreshold": 0.7, + "maxEntries": 20 +} +``` + +`maxEntries` is a soft cap — if exceeded, lowest-priority entry is dropped. Unlike LIDA's fixed-7 model, this is a practical limit, not a cognitive theory claim. + +### 11.3 Persistent Memory Atom Types + +| Type | Description | Governed by | +|---|---|---| +| `:episodic` | Conversation events with timestamps | `semanticMemory` | +| `:semantic` | Declarative facts and relationships | `semanticMemory` | +| `:procedural` | Skills, learned patterns, generalizations | `semanticMemory` + `memoryConsolidation` | +| `:pinned` | High-priority; always included in context | `semanticMemory` | + +No atom types for sensory or working memory — those are transient and managed as state variables, not as stored atoms. + +### 11.4 Temporal Reasoning Capabilities Summary + +| Capability | Mechanism | +|---|---| +| "Keep X in mind for N cycles" | `(attend X priority)` → `&wm` with TTL | +| "Forget X immediately" | `(dismiss X)` removes from `&wm`; `(forget X)` removes from SemanticMemory | +| Recency gradient in context | HISTORY slot truncates oldest-first by budget | +| Long-term belief decay | `beliefDecay` multiplier per cycle on `:truth` confidence (via `memoryConsolidation`) | +| Temporal distance in NAL inference | Timestamp on every atom; truth revision weights recency | +| Full LIDA cognitive cycle | `(cognitive-cycle $stimulus)` invokes `CognitiveArchitecture.js` as a skill | + +### 11.5 Embedder Strategy + +| Config | Model | Dim | Cost | +|---|---|---|---| +| Default | `Xenova/all-MiniLM-L6-v2` | 384 | Free, local | +| Higher quality | `Xenova/all-mpnet-base-v2` | 768 | Free, local | +| API | OpenAI `text-embedding-3-small` | 1536 | Per-token | + +Lazy-initialized on first `remember` call. 50k items at 384 dim ≈ 75MB in the `.vec` sidecar. + +### 11.6 Consolidation (when `memoryConsolidation` enabled) + +Runs every `consolidationInterval` cycles (default 100) as a VirtualEmbodiment self-task: +- NAL `Truth_Revision` over atoms with cosine similarity > 0.95 (merge near-duplicates) +- Multiply `:truth` confidence by `(1 - beliefDecay)` per cycle +- Prune atoms with confidence below `pruneThreshold` (default 0.2) +- Generalize clusters of `:episodic` patterns into `:procedural` atoms + +When `executionHooks` is enabled, consolidation self-task emits `(audit-event :type :memory-consolidation :pruned N :merged M)` via post-hook, making memory management observable. + +--- + +## 12. File Structure + +15 new files. No existing files deleted (except `MeTTaReasoner.js` is superseded — its regex heuristics replaced by actual `metta/` evaluation via the loop). + +``` +agent/src/ +├── metta/ +│ ├── AgentLoop.metta ← control plane loop (Phase 1) +│ ├── skills.metta ← skill declarations, living document (Phase 1) +│ ├── safety.metta ← consequence rules (Phase 4) +│ ├── hooks.metta ← declarative hook rules (Phase 4.5) +│ └── ContextBuilder.metta ← context assembly as MeTTa program (Phase 6) +│ +├── skills/ +│ ├── SkillDispatcher.js ← S-expr parse + dispatch + registry (Phase 1) +│ └── HookOrchestrator.js ← pre/post hook execution engine (Phase 4.5) +│ +├── introspection/ +│ └── IntrospectionOps.js ← manifest/skill-inventory/subsystems grounded ops (Phase 4.5) +│ +├── memory/ +│ ├── SemanticMemory.js ← PersistentSpace + HNSW + Embedder (Phase 2) +│ └── Embedder.js ← @huggingface/transformers wrapper (Phase 2) +│ +├── models/ +│ ├── ModelRouter.js ← task-type routing + NAL scoring (Phase 3) +│ └── ModelBenchmark.js ← micro-task evaluator (Phase 3) +│ +├── io/ +│ ├── Embodiment.js ← abstract interface; implemented by all channels (Phase 5) +│ ├── EmbodimentBus.js ← I/O router (Phase 5) +│ └── VirtualEmbodiment.js ← internal self-directed channel (Phase 5) +│ (IRCChannel, NostrChannel, CLIChannel adopt Embodiment interface) +│ +├── safety/ +│ ├── SafetyLayer.js ← consequence analysis + tier gates (Phase 4) +│ └── AuditSpace.js ← append-only PersistentSpace (Phase 4) +│ +├── harness/ +│ └── HarnessOptimizer.js ← meta-harness optimization (Phase 6) +│ +└── config/ + └── capabilities.js ← isEnabled() + validateDeps() (Phase 1) + +agent/workspace/ +└── agent.json ← capability flags, profile, model config (Phase 1) + +memory/ ← runtime memory, git-tracked +├── skills/ ← dynamic skill definitions directory (Phase 1 refinement) +│ └── *.metta ← agent-writable skill files (when dynamicSkillDiscovery) +├── harness/ +│ └── prompt.metta ← current system prompt (agent-writable) +├── traces/YYYY-MM-DD/ ← execution traces +├── history.metta ← append-only conversation history +└── audit.metta ← append-only audit atoms +``` + +**Files superseded (not deleted immediately; replaced when their phase lands):** + +| Old File | Superseded By | Phase | +|---|---|---| +| `cognitive/MeTTaReasoner.js` | AgentLoop.metta + metta/ interpreter | 1 | +| `io/ChannelManager.js` | EmbodimentBus.js | 5 | +| `io/PersistenceManager.js` | SemanticMemory.js | 2 | +| `metta/MemoryExtension.js` (LLM-facing bindings only) | SkillDispatcher SemanticMemory handlers | 2 | +| *(none)* | `HookOrchestrator.js` | 4.5 | +| *(none)* | `IntrospectionOps.js` | 4.5 | + +--- + +## 13. Key Interfaces + +### 13.1 agent.json Schema + +```json +{ + "profile": "parity", + + "capabilities": { }, + + "loop": { + "budget": 50, + "sleepMs": 2000, + "maxSkillsPerCycle": 3, + "maxParseRetries": 3 + }, + + "memory": { + "maxRecallItems": 20, + "maxRecallChars": 8000, + "maxHistoryChars": 12000, + "maxFeedbackChars": 6000, + "pinnedMaxChars": 3000, + "wmRegisterChars": 1500, + "embedder": "Xenova/all-MiniLM-L6-v2", + "vectorDimensions": 384, + "beliefDecay": 0.001, + "pruneThreshold": 0.2, + "consolidationInterval": 100 + }, + + "workingMemory": { + "defaultTtl": 10, + "autoAttendThreshold": 0.7, + "maxEntries": 20 + }, + + "models": { + "fallback": "gpt-4o-mini", + "explorationRate": 0.2, + "providers": { + "openai": { "enabled": true, "models": ["gpt-4o", "gpt-4o-mini"] }, + "anthropic": { "enabled": true, "models": ["claude-sonnet-4-6"] }, + "ollama": { "enabled": true, "models": ["llama3.2", "qwen2.5-coder"] } + } + }, + + "shell": { + "allowlist": ["git status", "git log --oneline", "pnpm test"], + "allowedPrefixes": ["git "], + "forbiddenPatterns": ["rm", "sudo", "curl", "wget", ">", "|", ";", "&&", "`", "$(", "eval"] + }, + + "harness": { + "harnessEvalInterval": 200, + "minScoreImprovement": 0.05, + "replayTaskSampleSize": 10 + }, + + "safety": { + "timeoutMs": 50, + "failClosed": true + } +} +``` + +### 13.2 SkillDispatcher API + +```javascript +class SkillDispatcher { + register(name, handler, capFlag, tier) // Register a JS handler for a skill + async execute(parsedCmds) // [{ name, args }] → [{ skill, result, error }] + getActiveSkillDefs() // S-expr strings for skills visible with current capabilities + hasSkill(name) // boolean +} +``` + +### 13.3 SemanticMemory API + +```javascript +class SemanticMemory { + async remember(content, source, type, tags) // embed + store atom + async query(text, k) // top-K similar atoms + async pin(content, source) // remember with :type :pinned + async forget(queryText) // remove matching atoms + async queryByType(type) // get atoms of specific type + async restore() // load from disk on startup + async forceCheckpoint() // flush to disk immediately +} +``` + +### 13.4 ModelRouter API + +```javascript +class ModelRouter { + async invoke(contextStr, opts = {}) // opts: { taskType, forceModel } + // returns { response, model, latency, tokens } + async benchmark(modelId, taskType, n) // run micro-tasks, update scores + getScores(taskType) // [{ modelId, truth: {f, c} }] from SemanticMemory +} +``` + +### 13.5 AgentBuilder extension (Phase 1 addition) + +```javascript +// Extends existing AgentBuilder.js +buildMeTTaLoop(config) { + const interp = new MeTTaInterpreter(); + loadNALStdlib(interp); + + const dispatcher = new SkillDispatcher(); + this._registerAllSkills(dispatcher, config); // one registration per skill + + const caps = (flag) => isEnabled(config, flag); + + interp.registerOp('check-embodiment-bus', () => this.channelManager.getNext()); + interp.registerOp('generate-self-task', () => this.virtualEmbodiment?.generateTask()); + interp.registerOp('llm-invoke', (ctx) => this.modelRouter.invoke(ctx)); + interp.registerOp('parse-response', (s) => dispatcher.parse(s)); + interp.registerOp('execute-commands', (cmds) => dispatcher.execute(cmds)); + interp.registerOp('build-context', (msg) => this.contextBuilder.build(msg)); + interp.registerOp('append-history', (...a) => this.history.append(...a)); + interp.registerOp('emit-cycle-audit', (...a) => this.auditSpace?.emit(...a)); + interp.registerOp('sleep-cycle', () => sleep(config.loop.sleepMs)); + interp.registerOp('cap?', (flag) => caps(flag)); + interp.registerOp('reset-budget', () => config.loop.budget); + interp.registerOp('get-state', (k) => interp.getState(k)); + interp.registerOp('new-message?', (m) => m !== null && m !== interp.getState('&prevmsg')); + interp.registerOp('no-input?', (m) => m === null); + + interp.loadFile('agent/src/metta/AgentLoop.metta'); + return () => interp.run('(agent-start)'); +} +``` + +**Phase 1 execution model note:** The MeTTa interpreter's `runAsync` doesn't yet propagate through async grounded ops mid-reduction. The JS loop in `_buildMeTTaLoop` mirrors `AgentLoop.metta` semantics exactly as a transitional shim. When the interpreter gains proper async grounded-op support, `_buildMeTTaLoop` returns `() => interp.runAsync('!(agent-start)')` and the JS loop is dropped — no other changes needed. The MeTTa files are loaded, rules are in the space for introspection and future execution. + +--- + +## 14. Design Decisions & Rationale + +### 14.1 S-Expressions Over JSON Tool Calls + +1. **Provider-agnostic.** Any model that can follow text formatting instructions can output `((remember "foo") (send "hello"))`. JSON function calling is a provider-specific protocol. +2. **MeTTa alignment.** The LLM output _is_ a MeTTa program. `parse-response` + interpreter execute it directly. +3. **Fallback preserved.** `sExprSkillDispatch: false` reverts to `ToolAdapter.js` JSON path. Weak models that cannot follow S-expression format still work. + +### 14.2 No New Abstraction Classes for Thin Wrappers + +`LLMAdapter`, `ModelPreferences`, `VectorIndex`, `CapabilityManager`, `SkillRegistry`, `AgentMeTTaBridge` were all removed as separate files. In each case: +- The behavior is 5–30 lines in a more natural home (`ModelRouter`, `SemanticMemory`, `SkillDispatcher`, `capabilities.js`, `AgentBuilder`). +- The "abstraction" didn't add a meaningful interface boundary — just indirection. + +The rule applied: a file justifies its existence when it owns a distinct contract. A file that only wraps another file's methods doesn't own a contract. + +### 14.3 Working Memory: Simplified, Not Eliminated + +The full LIDA working memory model (7-slot capacity constraint, competitive slot selection, complex salience-based attention) was removed — those specifics are human cognitive constraints that don't apply to LLM context windows. + +What was preserved is the key temporal property: the `&wm` register with TTL countdown. This gives the agent a "keep in mind" mechanism that operates on a cycle timescale distinct from both the per-cycle context window and the persistent long-term memory. The difference matters in practice: "check on that deployment in 20 minutes" cannot be handled by context window alone if history budget is tight, and explicit `(remember ...)` is the wrong granularity for transient intent. + +The three-tier model (register / context / long-term) maps to three distinct temporal scales: seconds-to-minutes (`&wm`), current session window (history slot), and persistent across restarts (SemanticMemory). Each tier has its own decay mechanism appropriate to its scale. + +### 14.4 Git is the Harness Version History + +A custom versioning scheme (`prompt.v001.metta`, `prompt.v002.metta`) is strictly worse than git: less queryable, no diff support, no merge support, manual numbering. `memory/` is already inside a git repo. `git log memory/harness/`, `git show`, `git revert` are the version history tools. + +### 14.5 Safety Consequence Chaining is Earned, Not Assumed + +Phase 4 ships direct rules only. Chaining (`A→B, B→C ⟹ A→C`) adds significant MeTTa inference complexity. It is only valuable once the direct rules have proven too coarse. Implementing it speculatively would mean debugging complex inference behavior before the simpler behavior is even validated. + +### 14.6 PersistentSpace as the Memory Substrate + +`metta/src/extensions/PersistentSpace.js` already provides Merkle hash integrity, CRDT vector clocks, and both FS and IndexedDB backends. Using it for `SemanticMemory` and `AuditSpace` means the memory architecture benefits from CRDT merge semantics — which `harnessDiffusion` (Phase 7+) will need. + +### 14.7 Declarative Hooks Over Imperative Middleware + +The `executionHooks` capability implements a *declarative* hook system (`hooks.metta`) rather than imperative middleware for three reasons: + +1. **Agent-readable**: Hooks are MeTTa atoms the agent can query, reason about, and (when gated) modify. Imperative JS middleware would be opaque. +2. **Composable**: Multiple hooks can match the same skill; execution order is explicit in the file. Middleware chains often have implicit ordering. +3. **Testable**: Hook rules can be unit-tested in isolation via `(metta (hook pre ...))` evaluation. Middleware requires full request/response mocking. + +The tradeoff: hook evaluation adds ~5-10ms overhead per skill call. This is acceptable because hooks only execute when `executionHooks` is enabled (Evolution tier), and the safety/audit benefits outweigh the latency for high-risk skills. + +### 14.8 Dynamic Skill Discovery: Safety vs. Flexibility + +`dynamicSkillDiscovery` enables agent-extensible skill registration but introduces risk of loading malformed or malicious definitions. Mitigations: + +1. **Parsing gate**: All discovered files pass through `metta/Parser.js`; syntax errors produce audit warnings, not crashes. +2. **Capability gate**: Dynamically discovered skills inherit the capability flag specified in their `(skill ...)` declaration. A skill requiring `shellSkill` cannot execute if that flag is disabled. +3. **SafetyLayer integration**: When both `dynamicSkillDiscovery` and `safetyLayer` are enabled, newly discovered skills are validated against `safety.metta`/`hooks.metta` before registration. +4. **Audit trail**: Every dynamic registration emits `(audit-event :type :skill-registered :source $file :skill $name)`. + +The design prioritizes *inspectability*: the agent can `(metta (query "skills registered from dynamic discovery"))` to audit its own extensions. + +### 14.9 Runtime Introspection Enables Self-Verification + +Exposing `manifest`, `skill-inventory`, etc. as always-available grounded ops (gated by `runtimeIntrospection` for content richness) serves two purposes: + +1. **Agent self-knowledge**: The LLM can query `(metta (manifest))` to get structured capability state, reducing prompt engineering overhead for "what can I do?" questions. +2. **External tooling**: CLI wrappers, monitoring dashboards, and verification scripts can invoke `(metta (manifest))` to audit agent state without parsing logs. + +The minimal-overhead implementation (5-cycle cache, conditional output) ensures introspection is available even on resource-constrained deployments. + +### 14.10 Transitional JS Loop Shim + +Phase 1 implements the agent loop twice: once in `AgentLoop.metta` (canonical semantics) and once in JS (`_buildMeTTaLoop`). This duplication is intentional and temporary: + +1. **JS shim now**: The MeTTa interpreter's `runAsync` doesn't yet propagate through async grounded ops mid-reduction. The JS loop provides the async orchestration needed for LLM calls, skill execution, and sleep cycles. +2. **Pure MeTTa later**: When the interpreter gains proper async grounded-op support, the JS loop is dropped. `_buildMeTTaLoop` returns `() => interp.runAsync('!(agent-start)')` — no other changes needed. + +The JS shim mirrors `AgentLoop.metta` semantics exactly. This ensures the migration path is a simple switch, not a refactor. The MeTTa files are always loaded; rules are in the space for introspection and future execution even while the JS loop drives the cycle. + +--- + +## 15. References + +### Codebase Anchors + +| File | Role | +|---|---| +| `agent/src/cognitive/MeTTaReasoner.js` | Superseded; replace with real `metta/` evaluation | +| `agent/src/rlfp/RLFPLearner.js` | Feed `rlfp_training_data.jsonl` into `ModelRouter` NAL scoring | +| `agent/src/cognitive/CognitiveArchitecture.js` | Preserved; becomes `(cognitive-cycle $stimulus)` skill | +| `agent/src/ai/AIClient.js` | Called directly by `ModelRouter`; not wrapped | +| `agent/src/ai/ToolAdapter.js` | Retained as JSON tool-call fallback when `sExprSkillDispatch: false` | +| `agent/src/io/ChannelManager.js` | Superseded by `EmbodimentBus`; channels adopt `Embodiment` interface | +| `agent/src/io/PersistenceManager.js` | Superseded by `SemanticMemory`; state migrated to atom format | +| `metta/src/extensions/PersistentSpace.js` | Backing store for `SemanticMemory` and `AuditSpace` | +| `metta/src/kernel/StateOps.js` | MeTTa mutable state ops for loop state variables | +| `metta/src/kernel/ops/MetaprogrammingOps.js` | `&add-rule` / `&remove-rule` for `selfModifyingSkills` | +| `agent/src/metta/ChannelExtension.js` | Existing grounded ops (`send-message`, `web-search`, `read-file`, etc.); `SkillDispatcher` wraps these with capability gates | +| `agent/src/metta/MemoryExtension.js` | Existing `remember`/`recall` bindings against `Memory.js`; superseded for LLM skill calls by Phase 2 `SemanticMemory` handlers | +| `metta/src/nal/stdlib/truth.metta` | NAL truth functions for `ModelRouter` NAL scoring | +| `metta/src/MeTTaInterpreter.js` | Execution engine for `AgentLoop.metta` | +| `mettaclaw/src/loop.metta` | Reference: loop counter, state vars, context structure | +| `mettaclaw/src/skills.metta` | Reference: parity skill set | +| `agent/src/skills/HookOrchestrator.js` | Pre/post-skill hook execution engine; integrates with SafetyLayer | +| `agent/src/introspection/IntrospectionOps.js` | Runtime manifest/skill-inventory generation for agent self-query | +| `agent/src/metta/hooks.metta` | Declarative hook rules; agent-readable safety/audit logic | +| `memory/skills/` | Directory for dynamically discovered skill definitions | + +### External + +- **Meta-Harness:** Lee (2025). https://yoonholee.com/meta-harness/ — filesystem-based traces, diagnostic context, cross-model transfer, 10× sample efficiency. +- **MeTTa:** https://wiki.opencog.org/w/MeTTa — atom spaces, pattern matching, grounded operations. +- **NAL:** Wang (2006). _Rigid Flexibility: The Logic of Intelligence._ Springer. — NAL truth functions for belief revision and model scoring. +- **RLHF:** Christiano et al. (2017). "Deep Reinforcement Learning from Human Preferences." — preference data format used by `RLFPLearner.js`. +- **HNSW:** Malkov & Yashunin (2018). "Efficient and Robust Approximate Nearest Neighbor Search." — vector index algorithm inside `SemanticMemory`. +- **HuggingFace Transformers.js:** https://huggingface.co/docs/transformers.js — local ONNX embeddings in Node.js. +- **hnswlib-node:** https://github.com/yoshoku/hnswlib-node — HNSW Node.js bindings. +- **vectra:** https://github.com/Stevenic/vectra — pure JS fallback vector store. + +--- + +*Generated: 2026-03-31. This document is itself a candidate for agent-managed evolution once Phase 6 is operational.* diff --git a/container_run.sh b/container_run.sh index 4e2bb646..9831028b 100755 --- a/container_run.sh +++ b/container_run.sh @@ -28,9 +28,9 @@ fi # ── Detect LLM provider from env vars ────────────────────────────────────── PROVIDER_INIT="" if [ -n "${OLLAMA_API_BASE:-}" ]; then - PROVIDER_MODEL="${OLLAMA_MODEL:-llama3}" - PROVIDER_INIT="(= (provider) Ollama) -(= (LLM) ollama_chat/$PROVIDER_MODEL)" +PROVIDER_MODEL="${OLLAMA_MODEL:-llama3}" +PROVIDER_INIT="(= (provider) Ollama) +(= (LLM) ollama/${PROVIDER_MODEL})" elif [ -n "${OPENAI_API_KEY:-}" ]; then PROVIDER_INIT="(= (provider) OpenAI) (= (LLM) gpt-4o)" diff --git a/lib_llm_ext.py b/lib_llm_ext.py index 3d22aa9c..5552e112 100644 --- a/lib_llm_ext.py +++ b/lib_llm_ext.py @@ -1,16 +1,38 @@ import os -import litellm -from litellm import completion +import sys +import json +import logging +import urllib.request + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + stream=sys.stderr +) +logger = logging.getLogger(__name__) + +try: + import litellm + from litellm import completion + LITELLM_AVAILABLE = True +except ImportError: + LITELLM_AVAILABLE = False + logger.warning("litellm not installed - LLM calls will fail") litellm.drop_params = True litellm.set_verbose = False +DEFAULT_EMBED_MODEL = os.environ.get("OLLAMA_EMBED_MODEL", "nomic-embed-text") +DEFAULT_EMBED_DIM = 768 + def _clean(text): if text: return text.replace("_quote_", '"').replace("_apostrophe_", "'") return "" def _chat(model, content, max_tokens=6000): + if not LITELLM_AVAILABLE: + return "Error: litellm not installed" try: resp = completion( model=model, @@ -19,13 +41,128 @@ def _chat(model, content, max_tokens=6000): ) return _clean(resp.choices[0].message.content) except Exception as e: + logger.error(f"LLM call failed: {e}") return f"LLM Error: {str(e)}" +def useGPTEmbedding(text): + """Return a list of floats embedding for the given text.""" + if not isinstance(text, str): + text = str(text) if text else "" + + if not text: + return _zero_embedding(DEFAULT_EMBED_DIM) + + ollama_base = os.environ.get("OLLAMA_API_BASE", "").rstrip("/") + if ollama_base: + try: + data = json.dumps({"model": DEFAULT_EMBED_MODEL, "prompt": text}).encode() + req = urllib.request.Request( + f"{ollama_base}/api/embeddings", + data=data, + headers={"Content-Type": "application/json"}, + method="POST", + ) + with urllib.request.urlopen(req, timeout=30) as resp: + result = json.loads(resp.read()) + return result.get("embedding", _zero_embedding(DEFAULT_EMBED_DIM)) + except Exception as e: + logger.warning(f"Ollama embedding failed: {e}") + + if not LITELLM_AVAILABLE: + logger.warning("litellm unavailable, returning zero embedding") + return _zero_embedding(1536) + + try: + resp = litellm.embedding( + model="text-embedding-3-small", + input=[text], + ) + return resp.data[0]["embedding"] + except Exception as e: + logger.error(f"LiteLLM embedding failed: {e}") + return _zero_embedding(1536) + +def _zero_embedding(dim): + """Fallback: return zero vector when embedding fails.""" + return [0.0] * dim + +def _check_chromadb(): + """Check if ChromaDB is available.""" + try: + import lib_chromadb + return True + except ImportError: + logger.warning("lib_chromadb not available - memory functions disabled") + return False + +def _chromadb_init_ok(): + """Check if ChromaDB collection is initialized.""" + if not _check_chromadb(): + return False + try: + import lib_chromadb + return lib_chromadb.is_initialized() + except Exception as e: + logger.warning(f"ChromaDB not initialized: {e}") + return False + +def remember(text, time_str): + """Embed text and store in ChromaDB.""" + if not text or not isinstance(text, str): + return "Error: empty text" + if not time_str: + time_str = "unknown" + + if not _check_chromadb(): + return "Error: ChromaDB not available" + + try: + import lib_chromadb + embedding = useGPTEmbedding(text) + item_id = lib_chromadb.remember(text, embedding, time_str) + return item_id or "Error: failed to store" + except Exception as e: + logger.error(f"Remember failed: {e}") + return f"Remember error: {e}" + +def query_memories(query_text, k=20): + """Embed query text and retrieve similar memories from ChromaDB.""" + if not query_text or not isinstance(query_text, str): + return "No memories found: empty query" + + if not _check_chromadb(): + return "No memories yet (ChromaDB not available)" + + if not _chromadb_init_ok(): + return "No memories stored yet" + + try: + import lib_chromadb + k = int(k) if k else 20 + embedding = useGPTEmbedding(query_text) + results = lib_chromadb.query(embedding, k) + if not results: + return f"No memories found for: {query_text}" + parts = [] + for t, content in results: + if t: + parts.append(f"[{t}] {content}") + else: + parts.append(content) + return " | ".join(parts) + except Exception as e: + logger.error(f"Query failed: {e}") + return f"No memories found (error: {str(e)[:50]})" + def useMiniMax(content): + if not LITELLM_AVAILABLE: + return "Error: litellm not installed" model = os.environ.get('MINIMAX_MODEL', 'openai/minimax/minimax-m2.5') return _chat(model=model, content=content, max_tokens=6000) def useClaude(content): + if not LITELLM_AVAILABLE: + return "Error: litellm not installed" model = os.environ.get('CLAUDE_MODEL', 'anthropic/claude-opus-4-20250514') return _chat(model=model, content=content, max_tokens=8192) @@ -33,4 +170,7 @@ def useGPT(model, max_tokens, reasoning_mode, content): return _chat(model=model, content=content, max_tokens=max_tokens) def useLLM(model, content, max_tokens=6000, reasoning_mode=None): + # Handle Ollama model names - convert to proper litellm format + if model and '/' not in model: + model = f"ollama/{model}" return _chat(model=model, content=content, max_tokens=max_tokens) diff --git a/run.sh b/run.sh index a07c2e08..71220b84 100755 --- a/run.sh +++ b/run.sh @@ -68,18 +68,22 @@ RUN_FLAGS=( # Common mount list for local MeTTa files (read-only, leaves memory/ writable) local_mounts() { - if [[ -f "$SCRIPT_DIR/run.metta" ]]; then - echo "-v" - echo "$SCRIPT_DIR/run.metta:/opt/PeTTa/repos/mettaclaw/run.metta:ro" - echo "-v" - echo "$SCRIPT_DIR/lib_mettaclaw.metta:/opt/PeTTa/repos/mettaclaw/lib_mettaclaw.metta:ro" - echo "-v" - echo "$SCRIPT_DIR/lib_nal.metta:/opt/PeTTa/repos/mettaclaw/lib_nal.metta:ro" - echo "-v" - echo "$SCRIPT_DIR/lib_llm_ext.py:/opt/PeTTa/repos/mettaclaw/lib_llm_ext.py:ro" - echo "-v" - echo "$SCRIPT_DIR/src:/opt/PeTTa/repos/mettaclaw/src:ro" - fi +if [[ -f "$SCRIPT_DIR/run.metta" ]]; then +echo "-v" +echo "$SCRIPT_DIR/run.metta:/opt/PeTTa/repos/mettaclaw/run.metta:ro" +echo "-v" +echo "$SCRIPT_DIR/lib_mettaclaw.metta:/opt/PeTTa/repos/mettaclaw/lib_mettaclaw.metta:ro" +echo "-v" +echo "$SCRIPT_DIR/lib_nal.metta:/opt/PeTTa/repos/mettaclaw/lib_nal.metta:ro" +echo "-v" +echo "$SCRIPT_DIR/lib_llm_ext.py:/opt/PeTTa/repos/mettaclaw/lib_llm_ext.py:ro" +echo "-v" +echo "$SCRIPT_DIR/src:/opt/PeTTa/repos/mettaclaw/src:ro" +echo "-v" +echo "$SCRIPT_DIR/container_run.sh:/opt/PeTTa/container_run.sh:ro" +echo "-v" +echo "$SCRIPT_DIR/agent_run.py:/opt/PeTTa/agent_run.py:ro" +fi } cmd_build() { diff --git a/src/helper.py b/src/helper.py index 9d9036fe..0cfc9612 100644 --- a/src/helper.py +++ b/src/helper.py @@ -1,8 +1,47 @@ -from collections import deque import re +import os from datetime import datetime TS_RE = re.compile(r'^\("(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})"') +HISTORY_PATH = os.environ.get("METTACLAW_HISTORY", "memory/history.metta") + +def get_provider(): + """Auto-detect LLM provider based on environment variables.""" + if os.environ.get("OLLAMA_API_BASE"): + return "Ollama" + if os.environ.get("OPENAI_API_KEY"): + return "OpenAI" + if os.environ.get("ANTHROPIC_API_KEY"): + return "Anthropic" + if os.environ.get("GROQ_API_KEY"): + return "Groq" + if os.environ.get("OPENROUTER_API_KEY"): + return "OpenRouter" + return "Ollama" + +def get_llm_model(): + """Auto-detect LLM model based on environment variables.""" + if os.environ.get("OLLAMA_MODEL"): + return os.environ.get("OLLAMA_MODEL") + if os.environ.get("OLLAMA_API_BASE"): + return os.environ.get("OLLAMA_MODEL", "llama3") + if os.environ.get("OPENAI_MODEL"): + return os.environ.get("OPENAI_MODEL") + if os.environ.get("ANTHROPIC_MODEL"): + return os.environ.get("ANTHROPIC_MODEL") + return "llama3" + +def get_history_path(): + """Get history file path, checking multiple possible locations.""" + possible_paths = [ + HISTORY_PATH, + "repos/mettaclaw/memory/history.metta", + "memory/history.metta", + ] + for path in possible_paths: + if os.path.exists(path): + return path + return HISTORY_PATH def extract_timestamp(line): m = TS_RE.search(line) @@ -14,57 +53,210 @@ def extract_timestamp(line): return None def around_time(needle_time_str, k): - filename = "repos/mettaclaw/memory/history.metta" - target = datetime.strptime(needle_time_str, "%Y-%m-%d %H:%M:%S") - best_lineno = None - best_line = None - best_diff = None + filename = get_history_path() + try: + target = datetime.strptime(needle_time_str, "%Y-%m-%d %H:%M:%S") + except ValueError: + return f"Error: invalid time format '{needle_time_str}', expected 'YYYY-MM-DD HH:MM:SS'" + buffer = [] best_idx = None - with open(filename, "r", encoding="utf-8", errors="replace") as f: - for lineno, line in enumerate(f, 1): - buffer.append((lineno, line)) - ts = extract_timestamp(line) - if ts is None: - continue - diff = abs((ts - target).total_seconds()) - if best_diff is None or diff < best_diff: - best_diff = diff - best_lineno = lineno - best_line = line - best_idx = len(buffer) - 1 - if best_lineno is None: - return + best_diff = None + + try: + with open(filename, "r", encoding="utf-8", errors="replace") as f: + for lineno, line in enumerate(f, 1): + buffer.append((lineno, line)) + ts = extract_timestamp(line) + if ts is None: + continue + diff = abs((ts - target).total_seconds()) + if best_diff is None or diff < best_diff: + best_diff = diff + best_idx = len(buffer) - 1 + except FileNotFoundError: + return f"Error: history file not found at {filename}" + except Exception as e: + return f"Error reading history: {e}" + + if best_idx is None: + return f"No timestamped entries found for '{needle_time_str}'" + start = max(0, best_idx - k) end = min(len(buffer), best_idx + k + 1) - ret = "" - for lineno, line in buffer[start:end]: - ret += f"{lineno}:{line}" - return ret + ret = "".join(f"{lineno}:{line}" for lineno, line in buffer[start:end]) + return ret if ret else "No entries found" + +def _extract_commands(s): + """Extract individual s-expressions from a string that may contain multiple commands. + Handles formats like: + ((send "hi") (query "topic")) + (send "hi")\n(query "topic") + ((send "hi")) + Returns list of properly parenthesized command strings. + """ + commands = [] + depth = 0 + start = None + for i, ch in enumerate(s): + if ch == '(': + if depth == 0: + start = i + depth += 1 + elif ch == ')': + depth -= 1 + if depth == 0 and start is not None: + cmd = s[start:i+1].strip() + # Unwrap double-parens: ((cmd)) -> (cmd) + while cmd.startswith("((") and cmd.endswith("))"): + cmd = cmd[1:-1] + if cmd.startswith("(") and cmd.endswith(")"): + commands.append(cmd) + start = None + return commands + def balance_parentheses(s): - s = s.replace("_quote_", '"').strip() + """Normalize LLM response into a single MeTTa-evaluable s-expression list. + The LLM may produce: + ((send "hi") (query "topic")) + (send "hi") (query "topic") + Sure! Here: (send "hi") + We extract commands and wrap them as a single list: + ((send "hi") (query "topic")) + This is ONE s-expression that superpose can iterate over. + """ + if not s: + return '((send "Error: empty response"))' + + # Fix common LLM output issues + s = s.replace("_quote_", '"').replace("_apostrophe_", "'").replace("_newline_", "\n") + # LLM sometimes uses underscores instead of spaces - fix that + s = s.replace("_", " ").strip() + + # Strip leading garbage (LLM markdown, explanations before the first command) first_paren = s.find('(') if first_paren > 0: garbage = s[:first_paren].strip() s = s[first_paren:] if garbage: garbage = garbage.replace('"', '\\"') - s = s[:1] + f'(pin "{garbage}") ' + s[1:] - if s.startswith("((") and s.endswith("))"): - return s - if s.startswith("(") and s.endswith(")"): - return f"({s})" - return f"(({s}))" + # Insert as a pin command at the front + s = f'(pin "{garbage}") ' + s + + # Extract individual commands by paren depth tracking + commands = [] + depth = 0 + start = None + for i, ch in enumerate(s): + if ch == '(': + if depth == 0: + start = i + depth += 1 + elif ch == ')': + depth -= 1 + if depth == 0 and start is not None: + cmd = s[start:i+1].strip() + # Unwrap double-parens: ((cmd)) -> (cmd) + while cmd.startswith("((") and cmd.endswith("))"): + cmd = cmd[1:-1] + if cmd.startswith("(") and cmd.endswith(")"): + commands.append(cmd) + start = None + + if commands: + # Return as a single list: (cmd1) (cmd2) -> ((cmd1) (cmd2)) + return '(' + ' '.join(commands) + ')' + + # Fallback: no parens found, wrap as single string command + safe_s = s.replace('"', '\\"')[:200] + return f'((send "{safe_s}"))' def normalize_string(x): + """Convert normalized string back to normal form for display.""" try: if isinstance(x, bytes): - return x.decode("utf-8", errors="ignore") - return str(x).encode("utf-8", errors="ignore").decode("utf-8", errors="ignore") + result = x.decode("utf-8", errors="ignore") + else: + result = str(x) + # Reverse the string-safe placeholders + result = result.replace("_quote_", '"').replace("_newline_", "\n").replace("_apostrophe_", "'") + result = result.replace("_apostrophe_", "'") # Handle double replacement + return result except Exception: return str(x) def clean_response(s): """Replace placeholder tokens with real characters before storing to history.""" + if not isinstance(s, str): + return str(s) if s else "" return s.replace("_quote_", '"').replace("_apostrophe_", "'") + +def summarize_errors(error_list): + """Convert a verbose error list into a short summary string. + Input: ((MULTI_COMMAND_FAILURE ...) (SINGLE_COMMAND_FORMAT_ERROR (query ...))) + Output: ERRORS: MULTI_COMMAND_FAILURE x1, SINGLE_COMMAND_FORMAT_ERROR(query) x1 + """ + if not error_list or error_list == []: + return "" + + try: + s = str(error_list) + except Exception: + return " ERRORS: unknown" + + # Escape any quotes in the error string to prevent history corruption + s = s.replace('"', "'") + + # Extract error type names + types = [] + for name in ["MULTI_COMMAND_FAILURE", "SINGLE_COMMAND_FORMAT_ERROR"]: + count = s.count(name) + if count > 0: + types.append(f"{name} x{count}") + + # Extract first failed command if available + try: + m = re.search(r'(?:SINGLE_COMMAND_FORMAT_ERROR|MULTI_COMMAND_FAILURE).*?\((\w+)\s+"[^"]*"', s) + first_cmd = f" ({m.group(1)} ...)" if m else "" + except Exception: + first_cmd = "" + + result = " ERRORS: " + ", ".join(types) + first_cmd + return result[:120] # Hard cap + +def format_lastresults(results_str, error_summary): + """Concatenate results and error summary into a clean string for &lastresults.""" + if not isinstance(results_str, str): + results_str = str(results_str) if results_str else "" + cleaned = clean_response(results_str) + err = error_summary if isinstance(error_summary, str) and error_summary else "" + return cleaned + err + +def clean_irc_message(raw): + """Strip IRC sender prefix 'nick: ' from raw message. + 'sseehh: what is 2+2' -> 'what is 2+2' + """ + if not isinstance(raw, str) or not raw: + return raw + if ": " in raw: + parts = raw.split(": ", 1) + nick_part = parts[0].strip() + if " " not in nick_part and nick_part: + return parts[1].strip() + return raw + +def file_exists(path): + """Check if file exists.""" + import os + return os.path.exists(path) + +def write_file(path, content): + """Write content to file (overwrite).""" + try: + os.makedirs(os.path.dirname(path) or ".", exist_ok=True) + with open(path, "w", encoding="utf-8") as f: + f.write(content) + return True + except Exception as e: + return f"Error writing file: {e}" diff --git a/src/loop.metta b/src/loop.metta index c97c15ad..9bf75521 100644 --- a/src/loop.metta +++ b/src/loop.metta @@ -9,23 +9,24 @@ (= (maxErrorFeedback) (empty)) (= (initLoop) - (progn (configure maxNewInputLoops 50) ;20 - (configure maxWakeLoops 1) - (configure sleepInterval 1) ;10 - (configure LLM gpt-5.4) - (configure provider Anthropic) ;OpenAI or ASICloud - (configure maxOutputToken 6000) - (configure reasoningMode medium) - (configure wakeupInterval 600) ;600=10 minutes - (configure maxErrorFeedback 2000) ;truncate error feedback in history - (change-state! &prevmsg "") - (change-state! &lastresults "") - (change-state! &loops (maxNewInputLoops)))) + (progn (configure maxNewInputLoops 50) ;20 + (configure maxWakeLoops 1) + (configure sleepInterval 1) ;10 + (configure LLM (py-call (helper.get_llm_model))) + (configure provider (py-call (helper.get_provider))) + (configure maxOutputToken 6000) + (configure reasoningMode medium) + (configure wakeupInterval 600) ;600=10 minutes + (configure maxErrorFeedback 2000) ;truncate error feedback in history + (change-state! &prevmsg "") + (change-state! &lastresults "") + (change-state! &loops (maxNewInputLoops)))) (= (getContext) (string-safe (py-str ("PROMPT: " (getPrompt) " SKILLS: " (getSkills) " OUTPUT_FORMAT: Output a ((skillName1 args1) (skillName2 args2) (skillName3 args3) (skillName4 args4) (skillName5 args5)) S-expression of up to 5 sexpr commands, double-check the parentheses it must be (cmd1 ... cmdn)!" " each arg is an explicit string hence needs quotes, and variables are forbidden!" + " PINNED: " (get-pinned) " LAST_SKILL_USE_RESULTS: " (last_chars (get-state &lastresults) (maxFeedback)) " HISTORY: " (getHistory) " TIME: " (get_time_as_string))))) (= (HandleError $msg $cmd $sexpr) @@ -42,7 +43,7 @@ (change-state! &loops (- (get-state &loops) 1))) (let $prompt (getContext) (progn (println! (---------iteration $k)) - (let* (($msgrcv (string-safe (repr (receive)))) + (let* (($msgrcv (py-call (helper.clean_irc_message (string-safe (repr (receive)))))) ($msgnew (prog1 (and (> (string_length $msgrcv) 0) (!= $msgrcv (get-state &prevmsg))) (if (> (string_length $msgrcv) 0) (change-state! &prevmsg $msgrcv) _))) ($msg (get-state &prevmsg)) @@ -56,12 +57,10 @@ ($send (py-str ($prompt $lastmessage))) ($_ (println! (CHARS_SENT: (string_length $send) $send))) ($respi (if (== (provider) OpenAI) -(useGPT (LLM) (maxOutputToken) (reasoningMode) $send) -(if (== (provider) Anthropic) -(py-call (lib_llm_ext.useClaude $send)) -(if (== (provider) MiniMax) -(py-call (lib_llm_ext.useMiniMax $send)) -(py-call (lib_llm_ext.useLLM (LLM) $send (maxOutputToken) (reasoningMode))))))) + (useGPT (LLM) (maxOutputToken) (reasoningMode) $send) + (if (== (provider) Anthropic) + (py-call (lib_llm_ext.useClaude $send)) + (py-call (lib_llm_ext.useLLM (LLM) $send))))) ($resp (py-call (helper.balance_parentheses $respi))) ($response (if (== "(" (first_char $resp)) $resp (progn (println! $resp) (repr (REMEMBER:OUTPUT_NOTHING_ELSE_THAN: ((skill arg) ...)))))) ($sexpr (catch (sread $response))) @@ -71,8 +70,9 @@ ($results (RESULTS: (collapse (let $s (superpose $sexpr) (COMMAND_RETURN: ($s (HandleError SINGLE_COMMAND_FORMAT_ERROR_NOTHING_WAS_DONE_PLEASE_FIX_AND_RETRY $s (catch (let $R (eval $s) (py-call (helper.normalize_string $R))))))))))) ($_ (println! $results))) (progn (if (or $msgnew (not (== $sexpr ()))) (addToHistory $msg $response $sexpr $msgnew) _) - (let $clean (py-call (helper.clean_response (string-safe (repr $results)))) - (change-state! &lastresults $clean)))) + (change-state! &lastresults + (py-call (helper.format_lastresults (string-safe (repr $results)) + (py-str (helper.summarize_errors (get-state &error)))))))) (if (> (get_time) (get-state &nextWakeAt)) (change-state! &loops (+ 1 (maxWakeLoops))) _))) (sleep (sleepInterval)) From 422172309738ede223f8a815ed9698a2eca15e89 Mon Sep 17 00:00:00 2001 From: me Date: Fri, 10 Apr 2026 16:57:02 -0400 Subject: [PATCH 09/10] updates --- lib_mettaclaw.metta | 2 -- memory/history.metta | 1 - src/loop.metta | 17 ++++++++--------- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/lib_mettaclaw.metta b/lib_mettaclaw.metta index dd2c22b3..10eaac22 100644 --- a/lib_mettaclaw.metta +++ b/lib_mettaclaw.metta @@ -12,8 +12,6 @@ !(import! &self (library mettaclaw ./src/channels)) !(import! &self (library mettaclaw ./src/skills)) !(import! &self (library mettaclaw ./src/memory)) -!(import! &self (library mettaclaw ./src/channels)) -!(import! &self (library mettaclaw ./src/context)) !(import! &self (library mettaclaw ./src/loop)) !(git-import! "https://github.com/patham9/petta_lib_chromadb.git") !(import! &self (library petta_lib_chromadb lib_chromadb)) diff --git a/memory/history.metta b/memory/history.metta index 8b137891..e69de29b 100644 --- a/memory/history.metta +++ b/memory/history.metta @@ -1 +0,0 @@ - diff --git a/src/loop.metta b/src/loop.metta index 9bf75521..67c84f1a 100644 --- a/src/loop.metta +++ b/src/loop.metta @@ -9,25 +9,24 @@ (= (maxErrorFeedback) (empty)) (= (initLoop) - (progn (configure maxNewInputLoops 50) ;20 + (progn (configure maxNewInputLoops 50) (configure maxWakeLoops 1) - (configure sleepInterval 1) ;10 + (configure sleepInterval 1) (configure LLM (py-call (helper.get_llm_model))) (configure provider (py-call (helper.get_provider))) (configure maxOutputToken 6000) (configure reasoningMode medium) - (configure wakeupInterval 600) ;600=10 minutes - (configure maxErrorFeedback 2000) ;truncate error feedback in history + (configure wakeupInterval 600) + (configure maxErrorFeedback 2000) (change-state! &prevmsg "") (change-state! &lastresults "") (change-state! &loops (maxNewInputLoops)))) (= (getContext) - (string-safe (py-str ("PROMPT: " (getPrompt) " SKILLS: " (getSkills) - " OUTPUT_FORMAT: Output a ((skillName1 args1) (skillName2 args2) (skillName3 args3) (skillName4 args4) (skillName5 args5)) S-expression of up to 5 sexpr commands, double-check the parentheses it must be (cmd1 ... cmdn)!" - " each arg is an explicit string hence needs quotes, and variables are forbidden!" - " PINNED: " (get-pinned) - " LAST_SKILL_USE_RESULTS: " (last_chars (get-state &lastresults) (maxFeedback)) " HISTORY: " (getHistory) " TIME: " (get_time_as_string))))) + (string-safe (py-str ("PROMPT: " (getPrompt) " SKILLS: " (getSkills) + " OUTPUT_FORMAT: Output a ((skillName1 args1) (skillName2 args2) (skillName3 args3) (skillName4 args4) (skillName5 args5)) S-expression of up to 5 sexpr commands, double-check the parentheses it must be (cmd1 ... cmdn)!" + " each arg is an explicit string hence needs quotes, and variables are forbidden!" + " LAST_SKILL_USE_RESULTS: " (last_chars (get-state &lastresults) (maxFeedback)) " HISTORY: " (getHistory) " TIME: " (get_time_as_string))))) (= (HandleError $msg $cmd $sexpr) (case $sexpr (((Error $a $b) (let $new (append (get-state &error) (($msg $cmd))) From de1dc2421ac2bd5f923ddf5ebc8afc8913ff2744 Mon Sep 17 00:00:00 2001 From: me Date: Fri, 10 Apr 2026 17:43:34 -0400 Subject: [PATCH 10/10] updates --- README.md | 189 ++++++++++++++++++++++++++++++++++++----- channels/cli.py | 136 +++++++++++++++++++++++++++++ channels/embodiment.py | 156 ++++++++++++++++++++++++++++++++++ lib_mettaclaw.metta | 2 + run.sh | 88 ++++++++++++------- src/channels.metta | 70 ++++++++------- 6 files changed, 555 insertions(+), 86 deletions(-) create mode 100644 channels/cli.py create mode 100644 channels/embodiment.py diff --git a/README.md b/README.md index e4853845..b2413497 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MeTTaClaw -An agentic AI system in MeTTa with embedding-based long-term memory. +An agentic AI system in MeTTa with embedding-based long-term memory, multi-channel support, and autonomous goal pursuit. --- @@ -33,7 +33,11 @@ OPENAI_API_KEY="sk-..." ./run.sh ./run.sh ``` -That's it. +That's it. The agent will connect to IRC by default. For CLI mode: + +```bash +./run.sh cli +``` --- @@ -112,16 +116,85 @@ See `.env.example` for all available options. --- +## Communication Channels + +MeTTaClaw supports multiple communication channels. Select one with the `METTACLAW_CHANNEL` environment variable. + +### CLI Mode (Terminal) + +Direct terminal interaction - type messages and see responses in your terminal. + +```bash +# Start in CLI mode +OLLAMA_API_BASE=http://localhost:11434 ./run.sh cli + +# Or use the environment variable +METTACLAW_CHANNEL=cli OLLAMA_API_BASE=http://localhost:11434 ./run.sh +``` + +**Features:** +- Interactive prompt (`> `) +- Direct stdin/stdout communication +- No network required +- Great for testing and development + +### IRC Mode (Default) + +Connect to IRC channels for multi-user interaction. + +```bash +# Default: ##metta @ irc.libera.chat +OLLAMA_API_BASE=http://localhost:11434 ./run.sh irc + +# Custom channel and server +IRC_CHANNEL="##mychannel" \ +IRC_SERVER="irc.libera.chat" \ +OLLAMA_API_BASE=http://localhost:11434 ./run.sh irc +``` + +**IRC Environment Variables:** +| Variable | Default | Description | +|----------|---------|-------------| +| `IRC_CHANNEL` | `##metta` | IRC channel to join | +| `IRC_SERVER` | `irc.libera.chat` | IRC server hostname | +| `IRC_PORT` | `6667` | IRC server port | + +### Multi-Channel Architecture + +The channel system is built on the `embodiment.py` abstraction layer, which: + +- Provides a unified interface for all channels +- Aggregates messages from multiple sources +- Broadcasts responses to all connected channels +- Supports adding new channels (Matrix, Discord, etc.) + +**Architecture:** +``` +┌─────────────────┐ +│ EmbodimentBus │ ← Multi-channel abstraction +├─────────────────┤ +│ IRC Channel │ ← IRC implementation +│ CLI Channel │ ← Terminal implementation +│ Mattermost (WIP)│ ← Future channels +└─────────────────┘ + ↓ + AgentLoop.metta ← Channel-agnostic agent loop +``` + +--- + ## Commands | Command | Description | |---------|-------------| -| `./run.sh` | Run `run.metta` (filtered output) | +| `./run.sh` | Run `run.metta` (IRC mode, filtered output) | | `./run.sh build` | Build the container image | | `./run.sh run [script]` | Run a specific script | | `./run.sh verbose [script]` | Run with full MeTTa/Prolog trace | | `./run.sh dry-run [script]` | Validate setup without LLM calls | | `./run.sh shell` | Open a shell in the container | +| `./run.sh cli` | Run in CLI mode (terminal) | +| `./run.sh irc` | Run in IRC mode | | `./run.sh reset-history` | Clear memory/history.metta | | `./run.sh clean` | Remove the image | | `./run.sh status` | Check build status and config | @@ -129,12 +202,51 @@ See `.env.example` for all available options. --- +## Agent Skills + +The agent has access to these skills (S-expression commands): + +### Memory Skills +| Skill | Description | Example | +|-------|-------------|---------| +| `remember` | Store to long-term memory | `(remember "User prefers terse responses")` | +| `query` | Search long-term memory | `(query "preferences about style")` | +| `pin` | Pin to working memory | `(pin "Current task: debugging")` | + +### Communication Skills +| Skill | Description | Example | +|-------|-------------|---------| +| `send` | Send message to user | `(send "Task completed")` | +| `send-to` | Send to specific channel | `(send-to "irc" "Hello IRC")` | + +### File Skills +| Skill | Description | Example | +|-------|-------------|---------| +| `read-file` | Read file contents | `(read-file "config.json")` | +| `write-file` | Write to file | `(write-file "output.txt" "content")` | +| `append-file` | Append to file | `(append-file "log.txt" "entry")` | + +### Other Skills +| Skill | Description | Example | +|-------|-------------|---------| +| `shell` | Execute shell command | `(shell "git status")` | +| `search` | Web search | `(search "latest AI news")` | +| `metta` | Evaluate MeTTa expression | `(metta "(+ 1 2)")` | + +--- + ## Examples ```bash # Build ./run.sh build +# Run with CLI mode +OLLAMA_API_BASE=http://localhost:11434 ./run.sh cli + +# Run with IRC mode (default) +OLLAMA_API_BASE=http://localhost:11434 ./run.sh + # Run with inline API key OPENAI_API_KEY="sk-..." ./run.sh @@ -153,7 +265,7 @@ OLLAMA_MODEL="hf.co/bartowski/Qwen_Qwen3-8B-GGUF:Q6_K" \ ANTHROPIC_API_KEY="sk-ant-..." ./run.sh # Validate setup before running (no LLM calls) -OLLAMA_API_BASE="http://localhost:11434" ./run.sh dry-run +OLLAMA_API_BASE=http://localhost:11434 ./run.sh dry-run # Full trace output for debugging ./run.sh verbose @@ -173,22 +285,30 @@ OLLAMA_API_BASE="http://localhost:11434" ./run.sh dry-run ## How It Works ``` -Host (run.sh) Container (/opt/PeTTa) -──────────── ────────────────────── -./run.sh build ──build──▶ PeTTa + MeTTaClaw cloned -./run.sh run ──exec──▶ container_run.sh - ↓ - provider_init.metta (auto-detected from env) - ↓ - agent_run.py (filters output, writes agent.log) - ↓ - run.metta → PeTTa → LLM +Host (run.sh) Container (/opt/PeTTa) +──────────── ────────────────────── +./run.sh build ──build──▶ PeTTa + MeTTaClaw cloned + +./run.sh run ──exec──▶ container_run.sh + ↓ + provider_init.metta (auto-detected from env) + ↓ + agent_run.py (filters output, writes agent.log) + ↓ + run.metta → PeTTa → LLM + ↓ + channels/embodiment.py → IRC/CLI ``` +### Key Components + - **`run.sh`** (host) — builds and runs the container - **`container_run.sh`** (inside) — detects provider from env vars - **`agent_run.py`** (inside) — runs PeTTa with output filtering and structured logging - **`lib_llm_ext.py`** — Python LLM interface using LiteLLM (supports all providers) +- **`channels/embodiment.py`** — Multi-channel abstraction layer +- **`channels/cli.py`** — Terminal/CLI implementation +- **`channels/irc.py`** — IRC protocol implementation ### Output Filtering @@ -215,7 +335,7 @@ podman cp :/opt/PeTTa/agent.log . ### Local Development -Local MeTTa files (`run.metta`, `lib_mettaclaw.metta`, `src/`) are mounted read-only into the container. The `memory/` directory is mounted read-write for history persistence. Edit files on your host and they're available on next run — no rebuild needed. +Local MeTTa files (`run.metta`, `lib_mettaclaw.metta`, `src/`, `channels/`) are mounted read-only into the container. The `memory/` directory is mounted read-write for history persistence. Edit files on your host and they're available on next run — no rebuild needed. --- @@ -223,18 +343,45 @@ Local MeTTa files (`run.metta`, `lib_mettaclaw.metta`, `src/`) are mounted read- | File | Purpose | |------|---------| -| `run.sh` | Host-side runner (build, run, verbose, dry-run, shell, clean, status) | -| `run.py` | Python runner (local PeTTa invocation, outside container) | +| `run.sh` | Host-side runner (build, run, verbose, dry-run, shell, cli, irc, clean, status) | | `container_run.sh` | Inside-container entrypoint (provider detection) | | `agent_run.py` | Inside-container PeTTa wrapper (filtering, logging, dry-run) | | `Dockerfile` | Container image definition | | `lib_llm_ext.py` | Python LLM wrapper (LiteLLM, all providers) | -| `Makefile` | Shortcuts: `make build`, `make run`, `make dry-run` | -| `VERSION` | Current version (shown in status output) | +| `channels/embodiment.py` | Multi-channel abstraction layer | +| `channels/cli.py` | CLI channel implementation | +| `channels/irc.py` | IRC channel implementation | +| `src/loop.metta` | Agent main loop | +| `src/channels.metta` | Channel initialization and message handling | +| `src/memory.metta` | Memory functions (remember, query, history) | +| `src/skills.metta` | Skill definitions | +| `src/helper.py` | Python helper functions | +| `memory/prompt.txt` | Agent system prompt (edit locally) | +| `memory/history.metta` | Conversation history (persistent) | +| `VERSION` | Current version | | `.env.example` | Configuration template | | `.env` | Your config (gitignored) | -| `memory/prompt.txt` | Agent system prompt (edit locally, mounted into container) | -| `memory/history.metta` | Conversation history (persistent across runs) | + +--- + +## Configuration Reference + +### Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `METTACLAW_CHANNEL` | `irc` | Channel type: `irc` or `cli` | +| `IRC_CHANNEL` | `##metta` | IRC channel to join | +| `IRC_SERVER` | `irc.libera.chat` | IRC server hostname | +| `IRC_PORT` | `6667` | IRC server port | +| `OLLAMA_API_BASE` | - | Ollama API endpoint | +| `OLLAMA_MODEL` | `llama3` | Ollama model name | +| `OPENAI_API_KEY` | - | OpenAI API key | +| `ANTHROPIC_API_KEY` | - | Anthropic API key | +| `OPENROUTER_API_KEY` | - | OpenRouter API key | +| `GROQ_API_KEY` | - | Groq API key | +| `METTACLAW_VERBOSE` | `false` | Enable full trace | +| `METTACLAW_DRY_RUN` | `false` | Validate without LLM | --- diff --git a/channels/cli.py b/channels/cli.py new file mode 100644 index 00000000..786691f1 --- /dev/null +++ b/channels/cli.py @@ -0,0 +1,136 @@ +""" +CLI Channel - Direct terminal interaction for MeTTaClaw. + +Provides a simple command-line interface for interacting with the agent. +Supports both interactive mode and piped input. +""" +import sys +import threading +import os + +_running = False +_connected = False +_last_message = "" +_msg_lock = threading.Lock() +_prompt = "> " + +def _getch(): + """Get a single character from stdin (cross-platform).""" + try: + import tty, termios + fd = sys.stdin.fileno() + old = termios.tcgetattr(fd) + try: + tty.setraw(fd) + ch = sys.stdin.read(1) + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old) + return ch + except ImportError: + # Windows + import msvcrt + return msvcrt.getch().decode('utf-8', errors='ignore') + +def start_cli(prompt="> ", welcome_msg=None): + """Start CLI channel with optional welcome message.""" + global _running, _connected, _prompt + _running = True + _connected = True + _prompt = prompt + + if welcome_msg: + print(welcome_msg) + + return True + +def stop_cli(): + """Stop CLI channel.""" + global _running, _connected + _running = False + _connected = False + +def getLastMessage(): + """Get message from stdin (non-blocking check).""" + global _last_message + + # For non-interactive (piped) input + if not sys.stdin.isatty(): + try: + line = sys.stdin.readline() + if line: + return line.rstrip('\n') + except: + pass + return "" + + # For interactive mode, we need to check if input is available + # This is a simplified approach - just returns empty if no input + # The main loop handles the blocking read + return "" + +def send_message(text): + """Send a message to stdout.""" + with _msg_lock: + print(text) + if _running and sys.stdin.isatty(): + print(_prompt, end='', flush=True) + +def is_connected(): + """Check if CLI is connected.""" + return _connected and _running + +def set_prompt(prompt): + """Set the CLI prompt.""" + global _prompt + _prompt = prompt + +def get_prompt(): + """Get the current prompt.""" + return _prompt + +def readline(): + """Blocking read a line from stdin with prompt.""" + try: + if sys.stdin.isatty(): + print(_prompt, end='', flush=True) + line = sys.stdin.readline() + return line.rstrip('\n') if line else "" + except EOFError: + return "" + except KeyboardInterrupt: + return "" + +# Alternative input method for interactive sessions +_input_buffer = "" +_input_ready = threading.Event() + +def input_thread(): + """Background thread for input reading.""" + global _input_buffer, _running + while _running: + try: + if sys.stdin.isatty(): + print(_prompt, end='', flush=True) + line = sys.stdin.readline() + if line: + _input_buffer = line.rstrip('\n') + _input_ready.set() + else: + break + except: + break + +def start_input_thread(): + """Start the input reader thread.""" + t = threading.Thread(target=input_thread, daemon=True, name="CLI-Input") + t.start() + return t + +def get_message_blocking(timeout=None): + """Get message with optional timeout.""" + if _input_ready.wait(timeout): + _input_ready.clear() + msg = _input_buffer + _input_buffer = "" + return msg + return "" diff --git a/channels/embodiment.py b/channels/embodiment.py new file mode 100644 index 00000000..8f016b1a --- /dev/null +++ b/channels/embodiment.py @@ -0,0 +1,156 @@ +""" +EmbodimentBus - Multi-channel abstraction for MeTTaClaw. + +Provides a unified interface for multiple communication channels (IRC, CLI, etc.). +Each channel must implement the Embodiment interface: +- start(channel, server, port, nick) or start(prompt, welcome) - Initialize the channel +- stop() - Shutdown the channel +- getLastMessage() - Get last message (non-blocking) +- send_message(text) - Send message +- is_connected() - Check connection status + +Channels are registered and messages are aggregated via getNextMessage(). +""" +import threading +from typing import Dict, List, Optional + +# Global state +_channels: Dict[str, object] = {} +_active_channel: Optional[str] = None +_running = False + +def register_channel(name: str, channel_module): + """Register a communication channel.""" + _channels[name] = channel_module + +def unregister_channel(name: str): + """Unregister a channel.""" + if name in _channels: + del _channels[name] + +def start_irc(channel="#metta", server="irc.libera.chat", port=6667, nick="mettaclaw"): + """Start IRC channel.""" + global _active_channel, _running + try: + import irc + register_channel('irc', irc) + irc.start_irc(channel, server, int(port), nick) + _active_channel = 'irc' + _running = True + return True + except Exception as e: + print(f"Failed to start IRC: {e}") + return False + +def start_cli(prompt="> ", welcome_msg=None): + """Start CLI channel.""" + global _active_channel, _running + try: + import cli + register_channel('cli', cli) + cli.start_cli(prompt=prompt, welcome_msg=welcome_msg) + _active_channel = 'cli' + _running = True + # Start input thread for CLI + cli.start_input_thread() + return True + except Exception as e: + print(f"Failed to start CLI: {e}") + return False + +def stop_channel(name: str = None): + """Stop a specific channel or all channels.""" + global _running + if name: + channel = _channels.get(name) + if channel: + stop_func = getattr(channel, 'stop_irc', None) or getattr(channel, 'stop_cli', None) or getattr(channel, 'stop', None) + if stop_func: + stop_func() + else: + _running = False + for ch_name, channel in _channels.items(): + try: + stop_func = getattr(channel, 'stop_irc', None) or getattr(channel, 'stop_cli', None) or getattr(channel, 'stop', None) + if stop_func: + stop_func() + except: + pass + +def stop_all(): + """Stop all channels.""" + stop_channel() + +def getNextMessage() -> str: + """Get the next message from any channel (FIFO order).""" + # Check CLI first (it's usually more responsive) + for name in ['cli', 'irc']: # Priority order + channel = _channels.get(name) + if channel: + try: + # For CLI, use blocking read with small timeout + if name == 'cli' and hasattr(channel, 'get_message_blocking'): + msg = channel.get_message_blocking(timeout=0.1) + if msg: + return msg + else: + get_func = getattr(channel, 'getLastMessage', None) + if get_func: + msg = get_func() + if msg: + return msg + except: + pass + return "" + +def receive_all() -> str: + """MeTTa integration: Get next message from any channel.""" + return getNextMessage() + +def broadcast(text: str): + """Send message to all connected channels.""" + for name, channel in _channels.items(): + try: + send_func = getattr(channel, 'send_message', None) or getattr(channel, 'send', None) + if send_func: + connected_func = getattr(channel, 'is_connected', None) + if connected_func is None or connected_func(): + send_func(text) + except Exception as e: + print(f"Error sending to {name}: {e}") + +def send_all(text: str): + """MeTTa integration: Send to all channels.""" + broadcast(text) + return None + +def send_to(channel_name: str, text: str): + """Send message to a specific channel.""" + channel = _channels.get(channel_name) + if channel: + try: + send_func = getattr(channel, 'send_message', None) or getattr(channel, 'send', None) + if send_func: + send_func(text) + except Exception as e: + print(f"Error sending to {channel_name}: {e}") + +def get_active_channels() -> List[str]: + """Get list of connected channels.""" + active = [] + for name, channel in _channels.items(): + try: + connected_func = getattr(channel, 'is_connected', None) + if connected_func is None or connected_func(): + active.append(name) + except: + pass + return active + +def get_active_channel() -> Optional[str]: + """Get the primary active channel name.""" + return _active_channel + +def is_running() -> bool: + """Check if embodiment bus is running.""" + return _running diff --git a/lib_mettaclaw.metta b/lib_mettaclaw.metta index 10eaac22..3770fe4e 100644 --- a/lib_mettaclaw.metta +++ b/lib_mettaclaw.metta @@ -6,6 +6,8 @@ !(import! &self (library mettaclaw lib_llm_ext.py)) !(import! &self (library mettaclaw ./src/helper.py)) !(import! &self (library mettaclaw ./channels/irc.py)) +!(import! &self (library mettaclaw ./channels/cli.py)) +!(import! &self (library mettaclaw ./channels/embodiment.py)) !(import! &self (library mettaclaw ./channels/mattermost.py)) !(import! &self (library mettaclaw ./channels/websearch.py)) !(import! &self (library mettaclaw ./src/utils)) diff --git a/run.sh b/run.sh index 71220b84..8bbe7c6f 100755 --- a/run.sh +++ b/run.sh @@ -51,15 +51,18 @@ has_api_key() { # Common env vars for container ENV_VARS=( - -e "OPENAI_API_KEY=${OPENAI_API_KEY:-}" - -e "ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}" - -e "OLLAMA_API_BASE=${OLLAMA_API_BASE:-}" - -e "OLLAMA_MODEL=${OLLAMA_MODEL:-llama3}" - -e "OPENROUTER_API_KEY=${OPENROUTER_API_KEY:-}" - -e "GROQ_API_KEY=${GROQ_API_KEY:-}" - -e "METTACLAW_VERSION=$VERSION" - -e "PETTA_PATH=$CONTAINER_WORKDIR" - -e "TERM=xterm-256color" +-e "OPENAI_API_KEY=${OPENAI_API_KEY:-}" +-e "ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}" +-e "OLLAMA_API_BASE=${OLLAMA_API_BASE:-}" +-e "OLLAMA_MODEL=${OLLAMA_MODEL:-llama3}" +-e "OPENROUTER_API_KEY=${OPENROUTER_API_KEY:-}" +-e "GROQ_API_KEY=${GROQ_API_KEY:-}" +-e "METTACLAW_CHANNEL=${METTACLAW_CHANNEL:-irc}" +-e "IRC_CHANNEL=${IRC_CHANNEL:-##metta}" +-e "IRC_SERVER=${IRC_SERVER:-irc.libera.chat}" +-e "METTACLAW_VERSION=$VERSION" +-e "PETTA_PATH=$CONTAINER_WORKDIR" +-e "TERM=xterm-256color" ) RUN_FLAGS=( @@ -80,6 +83,8 @@ echo "$SCRIPT_DIR/lib_llm_ext.py:/opt/PeTTa/repos/mettaclaw/lib_llm_ext.py:ro" echo "-v" echo "$SCRIPT_DIR/src:/opt/PeTTa/repos/mettaclaw/src:ro" echo "-v" +echo "$SCRIPT_DIR/channels:/opt/PeTTa/repos/mettaclaw/channels:ro" +echo "-v" echo "$SCRIPT_DIR/container_run.sh:/opt/PeTTa/container_run.sh:ro" echo "-v" echo "$SCRIPT_DIR/agent_run.py:/opt/PeTTa/agent_run.py:ro" @@ -192,36 +197,53 @@ Commands: shell Open a shell in the container reset-history Clear memory/history.metta clean Remove the container image - status Show build status and config - -h, --help Show help +status Show build status and config +-h, --help Show help +cli Run in CLI mode (terminal interface) +irc Run in IRC mode Env vars: - OPENAI_API_KEY OpenAI API key - ANTHROPIC_API_KEY Anthropic API key - OLLAMA_API_BASE Ollama endpoint (e.g., http://localhost:11434) - OLLAMA_MODEL Ollama model name - METTACLAW_VERBOSE Set to true for full trace (or use ./run.sh verbose) - METTACLAW_DRY_RUN Set to true to validate without LLM (or use ./run.sh dry-run) +OPENAI_API_KEY OpenAI API key +ANTHROPIC_API_KEY Anthropic API key +OLLAMA_API_BASE Ollama endpoint (e.g., http://localhost:11434) +OLLAMA_MODEL Ollama model name +METTACLAW_CHANNEL Channel type: irc, cli (default: irc) +IRC_CHANNEL IRC channel to join (default: ##metta) +IRC_SERVER IRC server (default: irc.libera.chat) +METTACLAW_VERBOSE Set to true for full trace (or use ./run.sh verbose) +METTACLAW_DRY_RUN Set to true to validate without LLM (or use ./run.sh dry-run) Examples: - $0 build - OPENAI_API_KEY=sk-... $0 run - OLLAMA_API_BASE=http://localhost:11434 $0 run myscript.metta - $0 verbose - $0 dry-run - $0 reset-history +$0 build +OPENAI_API_KEY=sk-... $0 run +OLLAMA_API_BASE=http://localhost:11434 $0 run myscript.metta +$0 cli +$0 irc +$0 verbose +$0 dry-run +$0 reset-history EOF } +cmd_cli() { +METTACLAW_CHANNEL=cli cmd_run "${1:-}" +} + +cmd_irc() { +METTACLAW_CHANNEL=irc cmd_run "${1:-}" +} + case "${1:-run}" in - build) cmd_build ;; - run) cmd_run "${2:-}" ;; - verbose) cmd_verbose "${2:-}" ;; - dry-run) cmd_dry_run "${2:-}" ;; - shell|sh) cmd_shell ;; - reset-history) cmd_reset_history ;; - clean) cmd_clean ;; - status) cmd_status ;; - -h|--help) cmd_help ;; - *) echo "Unknown: $1" >&2; cmd_help; exit 1 ;; +build) cmd_build ;; +run) cmd_run "${2:-}" ;; +verbose) cmd_verbose "${2:-}" ;; +dry-run) cmd_dry_run "${2:-}" ;; +shell|sh) cmd_shell ;; +reset-history) cmd_reset_history ;; +clean) cmd_clean ;; +status) cmd_status ;; +cli) cmd_cli "${2:-}" ;; +irc) cmd_irc "${2:-}" ;; +-h|--help) cmd_help ;; +*) echo "Unknown: $1" >&2; cmd_help; exit 1 ;; esac diff --git a/src/channels.metta b/src/channels.metta index dd833067..0a2e1914 100644 --- a/src/channels.metta +++ b/src/channels.metta @@ -1,44 +1,50 @@ -;configured at runtime: +;Embodiment abstraction - supports multiple channels +;Channel configuration (set via environment or config): +(= (commchannel) (empty)) (= (IRC_channel) (empty)) (= (IRC_server) (empty)) (= (IRC_port) (empty)) (= (IRC_user) (empty)) -(= (commchannel) (empty)) -(= (MM_URL) (empty)) -(= (MM_CHANNEL_ID) (empty)) -(= (MM_BOT_TOKEN) (empty)) +(= (CLI_prompt) (empty)) -;Connect all the channels: +;Initialize channels based on configuration (= (initChannels) - (progn (println! "Initializing channels") - (configure commchannel irc) - (if (== (commchannel) irc) - (progn (configure IRC_channel ##metta) - (configure IRC_server "irc.quakenet.org") - (configure IRC_port 6667) - (configure IRC_user maxbotnick) - (py-call (irc.start_irc (IRC_channel) (IRC_server) (IRC_port) (IRC_user)))) - (progn (configure MM_URL "https://chat.singularitynet.io") - (configure MM_CHANNEL_ID "8fjrmabjx7gupy7e5kjznpt5qh") - (configure MM_BOT_TOKEN "") - (py-call (mattermost.start_mattermost (MM_URL) (MM_CHANNEL_ID) (MM_BOT_TOKEN))))))) + (progn (println! "Initializing channels") + (let $chan (py-call (os.environ.get "METTACLAW_CHANNEL" "irc")) + (configure commchannel $chan) + (if (== (commchannel) irc) + (progn (configure IRC_channel (py-call (os.environ.get "IRC_CHANNEL" "##metta"))) + (configure IRC_server (py-call (os.environ.get "IRC_SERVER" "irc.libera.chat"))) + (configure IRC_port (py-call (os.environ.get "IRC_PORT" "6667"))) + (configure IRC_user "mettaclaw") + (py-call (embodiment.start_irc (IRC_channel) (IRC_server) (IRC_port) (IRC_user)))) + (if (== (commchannel) cli) + (progn (configure CLI_prompt "> ") + (py-call (embodiment.start_cli (CLI_prompt) "MeTTaClaw CLI - type to interact"))) + (if (== (commchannel) mattermost) + (progn (py-call (mattermost.start_mattermost "https://chat.example.com" "channel" "token"))) + (println! ("Unknown channel type: " (commchannel))))))))) -;Receive the latest user message considering all communication channels: +;Receive message from any active channel (= (receive) - (if (== (commchannel) irc) - (py-call (irc.getLastMessage)) - (py-call (mattermost.getLastMessage)))) + (py-call (embodiment.receive_all))) -;Send a message to all communication channels: -!(change-state! &lastsend "") +;Send message to all active channels (= (send $msg) - (if (!= $msg (get-state &lastsend)) - (progn (change-state! &lastsend $msg) - (let $safemsg (string-replace $msg "\n" "\\n") - (if (== (commchannel) irc) - (let $temp (cut) (py-call (irc.send_message $safemsg))) - (let $temp (cut) (py-call (mattermost.send_message $safemsg)))))) _)) + (if (!= $msg (get-state &lastsend)) + (progn (change-state! &lastsend $msg) + (let $safemsg (string-replace (string-replace $msg "_" " ") "\\n" "\\\\n") + (py-call (embodiment.send_all $safemsg)))) + _)) + +;Send to specific channel +(= (send-to $channel $msg) + (py-call (embodiment.send_to $channel $msg))) + +;Get list of active channels +(= (list-channels) + (py-call (embodiment.get_active_channels))) -;Search the internet for some information: +;Search the internet (= (search $msg) - (py-call (websearch.search $msg))) + (py-call (websearch.search $msg)))