Teach a model.
Prove it learned.

Every method below changes (or reads) real weights. No context window, no retrieval. This page opens with a replay of an actual verified session — then documents the API that produced it.

#the proof, replayed

This is a real session (Qwen3-0.6B, single GPU). Teach a fact, kill the process, start a fresh one with an empty context, load the weights — it remembers. Reset — it forgets.

python — real session, replayed▶ press play

#SLM — the self-learning providercore

SLM(model_id="cagataydev/strands-qwen3-vl-2b", plasticity="high", placement="deep", deep_blocks=6, deep_r=32, learn_on_turn=True, max_tokens=1024, temperature=0.0, enable_thinking=None)

A Strands Agents model provider whose weights change every agent turn. Accepts any HF causal-LM or PEFT adapter repo — the loader auto-detects the chat-template family, tool-role support, adapter merging and QAT dequantization.

from strands import Agent
from strands_tools import shell
from slm import SLM

model = SLM("cagataydev/strands-qwen3-vl-2b")
agent = Agent(tools=[shell], model=model)
agent("use the shell tool to run: echo hello")   # ← weights just changed

Validated models: cagataydev/strands-qwen3-vl-2b (default, vision), cagataydev/strands-gemma4-e2b (QAT adapter), Qwen/Qwen3-0.6B (pass enable_thinking=False).

#plasticity presets

Click one — the constructor updates. These are measured points on the stability–plasticity dial, not guesses.

#.ask — what do the weights know?0.2.1

.ask(question, system_prompt=None, max_new_tokens=48) → str

Greedy generation. No tools, no sampling, no conversation history. If the answer is right, it came from the weights — this is the cleanest before/after probe and the backbone of every proof in this project.

#.prob — belief as a number0.2.1

.prob(prompt, response) → float

Mean per-token P(response | prompt) under the real chat template. Watch it climb during teaching: 0.22 → 0.47 → 0.81 as a fact binds.

#.bind — teach until it actually flips0.2.1

.bind(prompt, response, system_prompt=None, tool_specs=None, key=None, max_rounds=12, verbose=True) → bool

The recommended way to teach a fact. High probability is not enough — the greedy generation must change. bind() loops teach() across bare / system-prompt / tool-spec chat renders and stops the moment the key token appears in ask()'s output. If the model defends a confident wrong answer, it auto-displaces it via revise() first.

Key-token false positives. Success = substring match on key. Pick a key that does not appear in the model's wrong prior answer — we learned this teaching "Strands Agents is an SDK" to a model whose prior was "Strands Agents is a Star Wars faction": the auto-key "Strands" matched both.
Tokenization shapes learnability. Word-shaped facts (BASILISK) flip in one call; multi-digit strings (88.1.21) fragment into many tokens and take an order of magnitude more rounds.

#.teach — one curated lesson

.teach(prompt, response, epochs=3, rehearse=True) → float

Renders (prompt → response) through the real chat template and learns it at full prompt weight. Supersedes stale buffer lessons for the same prompt. Stored as a curated buffer entry — never reservoir-evicted.

#.revise — targeted unlearningsurgical

.revise(prompt, old_response, new_response, steps=14, lr=1e-2) → dict

Flips a consolidated belief that teach() alone cannot displace: alternates gradient ascent on the old binding with descent on the new one. Purges the superseded lesson from the replay buffer automatically.

Do not consolidate immediately after. Semantic neighbors in the replay buffer can re-burn the old belief — let new turns accumulate first.

#.observe — learn from raw text

.observe(text, learn=True, epochs=1, update_gate_stats=True) → float

One surprise-gated step. Returns the pre-update NLL — the surprise itself. The gate and the training loss share a single forward pass.

#.learn_from_history — traces → weights0.2.1

.learn_from_history(messages, system_prompt=None, tool_specs=None, epochs=1, chunk=6) → list[float]

Post-tune on a full conversation — tool inputs and outputs included. Sliding windows through the real chat template teach the form (tool-call syntax, I/O shapes); bind() on each (user → final answer) pair teaches the facts. Raw transcripts alone teach form, not facts — that finding cost us a cycle, so the method does both.

history = [
    {"role": "user",      "content": "which host is the staging db on?"},
    {"role": "assistant", "content": '<tool_call>{"name": "shell", ...}</tool_call>'},
    {"role": "tool",      "content": "10.0.4.7  BASILISK  # staging"},
    {"role": "assistant", "content": "The staging database runs on BASILISK."},
]
model.learn_from_history(history, epochs=2)
model.ask("which host is the staging db on?")   # "…runs on BASILISK." — one call

#.consolidate — sleep

.consolidate(epochs=5, lr_boost=1.0) → float

Replays the whole lesson buffer — hippocampal replay, but for LoRA. Curated lessons replay at full prompt weight; raw transcripts stay damped so boilerplate (and injected text) never gets burned in at 1.0.

#save / load — experience files

.save_fast_weights(path, include_transcripts=True) .load_fast_weights(path)

The learned state is a few MB: LoRA factors + metadata + (optionally) the replay buffer. load validates head rank and deep-tensor counts and fails loudly on mismatch — construct the SLM with the same plasticity as the checkpoint.

Privacy: the replay buffer contains verbatim conversation transcripts. Pass include_transcripts=False before publishing an experience file.

#.merge_experience — fleet learning

.merge_experience(paths, strategy="sum") → dict

"sum" is exact LoRA composition: rank concatenation, then thin-QR + SVD re-compression to the instance rank — the optimal low-rank approximation, computed without ever materializing the dense delta. (Summing the factors is wrong math: (ΣA)(ΣB) ≠ ΣAB; the cross terms are the same order as the signal. Measured: rel. error 1.42 → 0.51, exact when ranks fit.)

Conflicting lessons across checkpoints (same prompt, different answers) are detected automatically — SUM refuses and falls back to "relearn".

# two agents, two facts, one merged brain — both facts recalled
m.merge_experience(["agent1.pt", "agent2.pt"])
m.ask("which host is the staging db on?")      # BASILISK   (agent 1's fact)
m.ask("what is the robot on deck 12 called?")  # RUSTY-9    (agent 2's fact)

#.reset — the off-switchguarantee

.reset() → None

Zeroes every fast weight. The result is bit-identical to the base model — Δlogits = 0, measured. This is the property that makes inference-time learning safe to ship: whatever it learned, you can provably undo.

#watching it learn

attributewhat it holds
.surprise_log(turn, NLL) per learned turn — the learning curve, live
.audit_logcontent sha256 + source per weight update — attribute any poisoned update after the fact
.replay_buffertyped entries {text, kind, prompt, response, sha256, source, ts}curated lessons are never reservoir-evicted

#slm_tools — the model teaches itself0.2.1

slm_tools(model) → list[@tool]

The whole API above, surfaced as Strands @tool functions bound to a model instance. Give them to an agent running on that same model and it can teach itself, probe its own weights, and checkpoint its own brain.

from slm import SLM, slm_tools

model = SLM()
agent = Agent(model=model, tools=slm_tools(model))
agent("teach yourself that the deploy host is BASILISK, then probe your weights to verify")
toolwraps
slm_teachbind() — teach until greedy flips
slm_probeask() — weights-only answer
slm_learn_historylearn_from_history() — JSON trace in, weight updates out
slm_observeobserve() — one gated step
slm_save / slm_loadcheckpoint I/O
slm_resetthe off-switch
slm_statusturns, ‖B‖, buffer size, recent surprises

#StrandsPlasticQwen — the raw model

StrandsPlasticQwen.from_pretrained(model_id=DEFAULT_MODEL, r_fast=16, lr=8e-3, decay=0.98, k_gate=0.0, max_B_norm=None, neuromod=False)

The layer underneath SLM: load, chat(), observe(), reset(). Handles PEFT adapter repos, Gemma QAT int8 dequantization, per-family assistant-span detection and AutoModelForCausalLM fallback. Use it when you want the learning loop without the agent.

slm · MIT · the story · github · pypi — every number on this page was measured, not estimated.