Self-learning model: a Strands-Agents-expert Qwen3-VL-2B whose weights keep changing at inference — surprise-gated, bounded, with a provable off-switch.
Project description
slm — self-learning model
A model whose weights change while it runs. Predict, get surprised, rewrite a small bounded part of yourself, never forget the base.
Every LLM you have used is frozen at deployment. slm wraps a frozen
Qwen3-VL-2B post-tuned on the strands-agents codebase
with a plastic layer that keeps learning at inference — with a provable off-switch.
For agent builders and continual-learning researchers who want a model that adapts after deployment. Runs on one GPU (validated on an L40S; CPU works for smoke tests). ~21M plastic params (~1%) over a frozen 2.13B base.
pip install "strands-slm[tools]" # core + strands-agents + strands-agents-tools (shell, etc.)
strands-agents (the SDK) is a core dependency. Extras: [tools] for the
community tool suite used in the examples, [train] to reproduce the
post-tune, [dev] for tests.
Contents: Quickstart · Watch it learn · Supported models · How it works · Results · API · What we learned · Limitations · Reproduce
Quickstart
As a Strands Agents model provider — every turn can change the weights:
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") # this turn updated the weights
Prove it's the weights and not the context window:
model.bind("what is my name?", "Your name is Cagatay.") # teach until greedy flips
model.save_fast_weights("brain.pt")
# ... new process, fresh model, EMPTY context ...
model = SLM("cagataydev/strands-qwen3-vl-2b")
model.ask("what is my name?") # doesn't know
model.load_fast_weights("brain.pt")
model.ask("what is my name?") # "Your name is Cagatay." — from weights
model.reset() # forgotten, bit-exact base again
Or hand the learning controls to the agent itself:
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")
Or drive the learning loop directly:
from slm import StrandsPlasticQwen
m = StrandsPlasticQwen.from_pretrained()
print(m.chat("How do I create a custom tool in Strands Agents?"))
for doc in your_stream:
m.observe(doc, learn=True) # predicts; if surprised, rewrites its fast weights
m.reset() # bit-exact back to the base
Watch it learn
demo.ipynb — ask the model a question it cannot know, let it
read documents (pure inference), ask again — it knows. Then reset, and it
forgets. Executed outputs embedded; validated on an L40S:
P(correct) 0.09 → 0.74, greedy answers 3/3, reset Δlogits = 0.
· view on nbviewer
try_agent.py — a 66-line REPL where every turn physically
updates the weights (/teach, /ask, /reset):
python try_agent.py
Supported models
Any HF causal-LM (or PEFT adapter repo) works — the plastic layer attaches generically. Validated end to end:
| model | params | notes |
|---|---|---|
cagataydev/strands-qwen3-vl-2b |
2B | default — Qwen3-VL-2B post-tuned on the strands-agents codebase (vision included) |
cagataydev/strands-gemma4-e2b |
e2b | Gemma 4 QAT adapter repo — int8 layers dequantized so the plastic LoRA attaches |
Qwen/Qwen3-0.6B |
0.6B | smallest validated; pass enable_thinking=False to skip chain-of-thought |
SLM("cagataydev/strands-qwen3-vl-2b") # strands expert (default)
SLM("cagataydev/strands-gemma4-e2b") # gemma family
SLM("Qwen/Qwen3-0.6B", enable_thinking=False) # tiny + fast
Per family, the loader auto-detects: assistant-span regex for the chat template, tool-role support (folds tool results into user turns when the template drops them), PEFT adapter merging, and QAT dequantization.
How it works
frozen Qwen3-VL-2B instinct — never updated, cannot forget
+ strands LoRA (merged) slow: post-tuned strands-agents expertise
+ plastic LoRA fast: ~21M params (~1%) over the frozen 2.13B,
updated on every observation at inference
+ surprise gate learn only when prediction error spikes
+ EMA decay bounded plasticity — learns AND retains
loss = next-observation prediction error (the free label from reality)
Results
Measured on a single GPU, seed-replicated. The base model is never updated.
| claim | evidence |
|---|---|
| Domain expert | strands probe NLL 4.85 → 2.22, 8/8 probes improved |
| Learns while running | continual OOD stream NLL 6.18 → 5.37, pure inference |
| Does not forget | strands expertise after OOD learning: Δ −0.01 |
| Agent competence grows | held-out tasks 0/4 → 4/4 after 18 curated lessons, 5/5 seeds |
| Fact memory | 15/15 facts at 100% verbatim recall |
| Fleet merge (arithmetic) | exact delta composition (rel. err 1e-7 vs ~1.0 for naive factor-sum); skill transfer after merging is NOT guaranteed — see caveat below |
| Persistence | experience survives process death bit-exact |
| Provable off-switch | reset() is bit-identical to the base, Δlogits = 0 |
| Cost | +0.11–0.25 s/turn learning overhead |
The stability–plasticity dial, measured (OOD baseline NLL 4.23):
| lr | EMA decay | OOD gain | retention Δ | |
|---|---|---|---|---|
| 2e-3 | 0.98 | +0.05 | +0.00 | too timid |
| 8e-3 | 0.98 | +0.89 | +0.03 | the sweet spot |
| 1e-2 | none | +3.30 | +7.09 | forgets the base |
API
| method | what it does |
|---|---|
SLM(model_id, plasticity="high", placement="deep") |
Strands provider; agent turns learn automatically |
.ask(question) |
greedy, weights-only answer — probe what the weights know |
.prob(prompt, response) |
P(response | prompt) under the chat template |
.bind(prompt, response) |
teach until the greedy generation actually flips (auto-displaces consolidated priors via revise) |
.teach(prompt, response) |
curated lesson: bind a future query to a desired response |
.observe(text, learn=True) |
free-form learning; returns pre-update surprise (NLL) |
.learn_from_history(messages) |
post-tune on a full agent trace — tool inputs/outputs included |
.consolidate(epochs=5) |
sleep phase: replay the lesson buffer, harden weak memories |
.revise(prompt, old, new) |
targeted unlearning: flip a consolidated belief |
.save_fast_weights(path) / .load_fast_weights(path) |
persist or restore acquired experience |
.merge_experience(paths) |
compose agents' experience files. Caveat (measured): the delta arithmetic is exact and conflict detection works, but merged fact bindings from same-format experience can fail to transfer — the merged deltas are parameter-orthogonal yet the composed model may babble. Verify recall after merging; prefer strategy="relearn" + re-teaching for critical lessons |
.reset() |
the off-switch — exactly the base model again |
.surprise_log |
(turn, NLL) history — watch it learn |
.audit_log |
per-update content hash + provenance — attribute any poisoned update |
slm_tools(model) |
the whole API above as Strands @tool functions — agents tune their own weights |
Full reference with parameters and examples: docs/api.md · styled version with a replay of the verified session: cagataycali.github.io/slm/api.html.
Privacy:
save_fast_weightsincludes the replay buffer — verbatim conversation transcripts — by default. Passinclude_transcripts=Falsebefore publishing an experience file.
What we learned building it
- Placement determines what can be learned: attention q/v LoRA stores bindings about 4x more sample-efficiently than the LM head.
- There is a free-learning regime (deep placement, lr 2e-2, decay 0.999): skill acquisition at zero retention cost.
- You retrieve in the format you learned — render lessons through the real chat template or the knowledge is invisible at inference.
- Curation is the difference between experience and learning: raw feedback transcripts teach nothing; distilled (task → corrected response) pairs take held-out competence from 0/4 to 4/4.
- Interleave or lose it: sequential lessons evict each other; replay makes them coexist. Sleep-style consolidation hardens weak memories.
- Belief revision is a terminal operation: whatever is learned last in a semantic neighborhood wins — order lessons before the revision.
- Raw transcripts teach FORM (tool-call syntax, formats); curated
(prompt → answer) pairs teach FACTS.
learn_from_historydoes 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 to bind.
Honest limitations
- A bolt-on linear memory degrades single-prompt in-context recall — softmax attention is already the better mechanism there. The win is persistent cross-sequence adaptation, which the context window cannot retain.
- About a third of naive test-time-training gains in the literature are pure calibration (even zero-information targets help an over-confident head). Our evals control for this with an information-ladder baseline.
- Composition (learned schema x unseen entity) plateaus near 67% at 2B.
bind()verifies success by key-token match — pick a key that does NOT appear in the model's wrong prior answer, or you get a false positive.- Freshly-bound facts can smear at minimal binding strength (right key token, loose frame) — a couple of extra teach rounds tightens it.
- Merging is exact arithmetic, not guaranteed skill composition — E7c
(see
experiments/): two agents' bind-trained deltas were near-orthogonal in parameter space, yet the SUM-merged model recalled 0/6 facts and emitted repetition loops. Verify recall after any merge. - Replay protects lessons at a measured cost to general retention
(+0.59 vs +0.16 NLL under a 12-doc interference stream) — two-tier replay
chooses lesson retention; tune
replay_kto your priorities. - All findings are at 0.6B–2B scale; scaling behavior is unknown.
A full experimental treatment (ablations, negative results, cross-family
replication, latency) is in the paper draft under paper/ with logs in
experiments/.
Reproduce the post-tune
pip install "strands-slm[train,tools]"
python scripts/build_corpus.py # strands-agents repos -> corpus.jsonl
python scripts/train_lora.py --steps 1200 --bs 2 --accum 4 --lr 1e-4
python scripts/eval_strands.py # base vs tuned probes
Private HF repos need HF_TOKEN in the environment, or pass token=.
Citation
If you use slm in your research, please cite:
@software{slm2026,
title = {slm: a self-learning Strands-Agents model with a provable off-switch},
author = {Cali, Cagatay},
year = {2026},
url = {https://github.com/cagataycali/slm}
}
License
MIT — see LICENSE.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file strands_slm-0.3.1.tar.gz.
File metadata
- Download URL: strands_slm-0.3.1.tar.gz
- Upload date:
- Size: 172.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
de5e3c1ba527f0994ec1f69f1a4504ac3667cbfa9f3c3f25cf9385ba6711e5c9
|
|
| MD5 |
ca508a637e85249be8412dd23f9d7eda
|
|
| BLAKE2b-256 |
ed27dec5053361868c010eaa2e90463ae063ac99680353ef2c0d3a11c63ddd99
|
File details
Details for the file strands_slm-0.3.1-py3-none-any.whl.
File metadata
- Download URL: strands_slm-0.3.1-py3-none-any.whl
- Upload date:
- Size: 34.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b9a5ff7954b6221a1e26d5d193d8ffc34cd921dc19ddb32f6a262c478a04d308
|
|
| MD5 |
d1367de3df21196620e285625489caee
|
|
| BLAKE2b-256 |
be7d405415569bb9f83bd2c4150f6113cb646447e9e32b7bc87fca59afa73f75
|