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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions configs/features/mem0_memory.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# ─────────────────────────────────────────────────────────────────────────────
# Feature: optional mem0-backed persistent memory (community-contributed)
# ─────────────────────────────────────────────────────────────────────────────
#
# This is NOT a default SkillOpt setting and was NOT used to produce the
# numbers reported in the paper. It is off unless you turn it on here.
#
# IMPORTANT — this feature sends data to a third-party hosted service.
# Enabling it means skill text and reflection summaries leave your machine.
# Setting MEM0_API_KEY alone does *nothing*; `mem0_enabled: true` below is the
# only thing that turns uploading on, and a key set for some other application
# will never enable it by accident.
#
# What it does:
# - After each evaluation gate, stores the current skill and its score.
# - After each Reflect stage, stores a summary of the patches produced.
# - Before each Reflect stage, makes ONE bounded retrieval call and appends
# the relevant history to that stage's reflection input, so previous runs
# can inform the patches produced now.
#
# When to consider this:
# - You run the same project repeatedly and want later runs to benefit from
# what earlier ones discovered.
# - You are comfortable with the data-handling note above.
#
# When NOT to use this:
# - When reproducing the paper.
# - When the skill text or trajectories are sensitive. Payloads are redacted
# (see docs/reference/config.md → "What leaves the machine"), but redaction
# is a mitigation, not a guarantee about content you know to be private.
#
# Install the optional dependency first:
# pip install 'skillopt[mem0]'
#
# Then export your key and use this file:
# export MEM0_API_KEY=m0-...
# _base_: ../features/mem0_memory.yaml
# or copy the `train:` block below into your config.
# ─────────────────────────────────────────────────────────────────────────────

_base_: ../_base_/default.yaml

train:
# The master switch. Nothing is uploaded while this is false.
mem0_enabled: true

# Optional: set the key in config instead of the MEM0_API_KEY env var.
# Leave empty to use the environment (recommended — keeps keys out of YAML).
mem0_api_key: ""

# Optional: override the namespace. Default is derived per project as
# `skillopt:<env>:<sha256-prefix-of-project-root>`, which is stable across
# runs and distinct across projects. Set this only if you deliberately want
# several checkouts to share one memory pool.
mem0_namespace: ""

# Read memory back into reflection. Setting this false keeps the writes but
# removes the retrieval call — useful for measuring whether retrieval is what
# actually helps, rather than assuming it does.
mem0_retrieval_enabled: true

# Records fetched per retrieval.
mem0_retrieval_limit: 5

# Hard per-call wall-clock bound. On timeout the step continues unchanged, so
# an unreachable service costs one bounded pause rather than a stalled run.
mem0_timeout_seconds: 5.0

# Cap on any single stored payload, applied AFTER redaction so the limit is
# measured on exactly the text that would be transmitted.
mem0_max_chars: 4000
48 changes: 48 additions & 0 deletions docs/reference/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,54 @@ defaults to `claude` and can be overridden with `CLAUDE_CLI_BIN`.

Benchmark-specific `env` keys are passed through to the adapter.

## Persistent Memory (mem0) — optional, off by default

Stores skill iterations and reflection summaries in [mem0](https://mem0.ai) and
reads a small amount of relevant history back into the Reflect stage.

**Disabled unless `mem0_enabled` is explicitly true.** A `MEM0_API_KEY` present
in the environment for another application does *not* enable it. Install with
`pip install 'skillopt[mem0]'`.

Set these under `train:` in a structured config (every shipped config is
structured), at the top level of a flat config, or via
`--cfg-options mem0_enabled=true`. A ready-made example ships at
`configs/features/mem0_memory.yaml`.

| Parameter | Type | Default | Description |
|---|---|---|---|
| `train.mem0_enabled` | bool | `false` | Master switch. Nothing is sent unless this is true |
| `train.mem0_api_key` | str | empty | Falls back to `MEM0_API_KEY`, read only when enabled |
| `train.mem0_namespace` | str | derived | Override the namespace; default is derived per project |
| `train.mem0_retrieval_enabled` | bool | `true` | Read memory back into reflection; writes continue if false |
| `train.mem0_retrieval_limit` | int | `5` | Max records fetched per retrieval |
| `train.mem0_timeout_seconds` | float | `5.0` | Hard per-call bound; on timeout training continues |
| `train.mem0_max_chars` | int | `4000` | Cap on any single stored payload, applied after redaction |

### What leaves the machine

When enabled, exactly two record types are sent, both redacted first:

1. **Skill iteration** — epoch, step, score, skill hash and length, the `env`
and model names, and the skill text (capped at `mem0_max_chars`).
2. **Reflection summary** — epoch, step, patch count, rollout scores, and a
JSON summary of up to 10 patches with any `skill_text` field removed.

Retrieval sends the first 600 characters of the current skill as a similarity
query.

Before transmission every payload passes through
`skillopt.memory.redaction.redact_for_upload`, which strips vendor API keys,
bearer/basic tokens, JWTs, private keys, `key = value` secret assignments, the
project root, and `/home/<user>`-style prefixes. Relative paths and filenames
are deliberately preserved so stored memories stay useful.

### Namespacing

Memories are scoped to `skillopt:<env>:<digest>`, where the digest is a SHA-256
prefix of the absolute project root. Stable across runs of one project, distinct
across projects, and the raw path is never transmitted.

## Credential Environment Variables

### Azure-family backend
Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ searchqa = ["datasets>=2.18.0"]
docs = ["mkdocs-material>=9.5.0", "mkdocstrings[python]>=0.24.0"]
# WebUI dashboard
webui = ["gradio>=4.0.0"]
# Optional mem0-backed persistent memory. Deliberately excluded from `all`:
# installing it is harmless (uploads still require mem0_enabled: true), but
# a data-export integration should be an explicit choice at install time too.
mem0 = ["mem0ai>=0.1.0"]
# Development tools
dev = ["ruff>=0.4.0", "pytest>=8.0.0"]
# All optional dependencies (except docs/dev/webui)
Expand Down
8 changes: 8 additions & 0 deletions skillopt/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,14 @@
"train.batch_size": "batch_size",
"train.accumulation": "accumulation",
"train.seed": "seed",
# Optional mem0 memory (off unless train.mem0_enabled is true).
"train.mem0_enabled": "mem0_enabled",
"train.mem0_api_key": "mem0_api_key",
"train.mem0_namespace": "mem0_namespace",
"train.mem0_retrieval_enabled": "mem0_retrieval_enabled",
"train.mem0_retrieval_limit": "mem0_retrieval_limit",
"train.mem0_timeout_seconds": "mem0_timeout_seconds",
"train.mem0_max_chars": "mem0_max_chars",
"gradient.minibatch_size": "minibatch_size",
"gradient.merge_batch_size": "merge_batch_size",
"gradient.analyst_workers": "analyst_workers",
Expand Down
26 changes: 25 additions & 1 deletion skillopt/engine/trainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@
set_optimizer_deployment,
)
from skillopt.utils import compute_score, skill_hash
from skillopt.memory.trainer_hooks import (
maybe_init_mem0,
hook_pre_reflect,
hook_post_evaluate,
hook_post_reflect,
)


# ── Skill-aware reflection: appendix flush ───────────────────────────────────
Expand Down Expand Up @@ -1042,6 +1048,8 @@ def _persist_runtime_state(last_completed_step: int) -> None:
# ── Training loop ────────────────────────────────────────────────
t_loop_start = time.time()

memory = maybe_init_mem0(cfg)

Comment on lines 1048 to +1052
if resume_from > total_steps:
print(f"\n [skip] all {total_steps} steps complete — jumping to evaluation")

Expand Down Expand Up @@ -1158,11 +1166,18 @@ def _persist_runtime_state(last_completed_step: int) -> None:
# Build step context from buffer
step_buffer_context = _format_step_buffer(step_buffer)

# Memory read: relevant history for THIS reflection only.
# Kept in a separate name so the LR decision and rewrite
# prompts below continue to see the unaugmented context.
reflect_context = hook_pre_reflect(
memory, current_skill, step_buffer_context,
)

raw_patches = adapter.reflect(
rollout_results, current_skill, batch_dir,
prediction_dir=pred_dir, patches_dir=patches_dir,
random_seed=batch_seed,
step_buffer_context=step_buffer_context,
step_buffer_context=reflect_context,
meta_skill_context=active_meta_skill,
)
failure_patches, success_patches = _normalise_patches(
Expand Down Expand Up @@ -1204,6 +1219,11 @@ def _persist_runtime_state(last_completed_step: int) -> None:
step_rec["timing"]["rollout_s"] = round(total_rollout_time, 1)
step_rec["timing"]["reflect_s"] = round(total_reflect_time, 1)

hook_post_reflect(
memory, epoch, global_step, all_raw_patches,
scores={"hard": agg_hard, "soft": agg_soft},
)

n_total_patches = len(all_failure_patches) + len(all_success_patches)
step_rec["n_patches"] = n_total_patches
step_rec["n_failure_patches"] = len(all_failure_patches)
Expand Down Expand Up @@ -1522,6 +1542,10 @@ def _persist_runtime_state(last_completed_step: int) -> None:
):
best_origin = current_origin

hook_post_evaluate(
memory, epoch, global_step, current_skill, current_score, cfg,
)

if use_skill_aware:
current_skill = _flush_skill_aware_appendix(
current_skill, all_raw_patches, step_rec, step_dir, cfg,
Expand Down
17 changes: 17 additions & 0 deletions skillopt/memory/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""skillopt.memory — optional, opt-in mem0-backed persistent memory.

Disabled unless a config sets ``mem0_enabled: true``; see
:mod:`skillopt.memory.settings` for the resolution rules and
:mod:`skillopt.memory.redaction` for what is stripped before anything is sent.
"""
from skillopt.memory.mem0_backend import SkillMemory, mem0_available
from skillopt.memory.redaction import redact_for_upload
from skillopt.memory.settings import Mem0Settings, resolve_settings

__all__ = [
"Mem0Settings",
"SkillMemory",
"mem0_available",
"redact_for_upload",
"resolve_settings",
]
Loading