An LLM agent built from scratch, and grown one stage at a time.
A tool-using agent in about 3,500 lines of plain Python, with no agent framework, that you can run around the clock from Telegram, and a stage-by-stage record of how it was built, measured and post-trained.
- An agent you can use. Files, shell, web search and fetch, sessions and compaction, on a local 4B model or any OpenAI-compatible API, in a terminal or as a Telegram bot.
- A from-scratch tutorial. Each stage adds one mechanism with tests and measured evidence, from the first agent loop to SFT, expert iteration and a GRPO pilot. Post-training lifts Qwen3-4B from0.419 to 0.774 pass@1 on task types it never trained on (results ).
Python 3.11 or later. Run every command from the repository root with the venv active.
git clone https://github.com/shakewingo/Tadpole.git
cd Tadpole
python3 -m venv .venv && source .venv/bin/activate
pip install -e .
Use a strong model through any OpenAI-compatible API: DeepSeek, OpenAI, OpenRouter, or your own
vLLM or LM Studio server. export these for the terminal, or put them in the
bot's env file:
export AGENT_BASE_URL=https://api.deepseek.com # the API root; often ends in /v1
export AGENT_MODEL=deepseek-flash # the provider's model ID
export AGENT_API_KEY=sk-...
export AGENT_N_CTX=32768 # optional: the model's context window
Run Qwen3-4B locally, base or post-trained: the model this project trains, for #
studying the training rather than everyday use
| Option | Variables |
|---|---|
| Ollama, any OS | AGENT_BASE_URL=http://localhost:11434/v1 ,AGENT_MODEL=hf.co/unsloth/Qwen3-4B-Instruct-2507-GGUF:Q4_K_M ,AGENT_N_CTX=32768 |
| llama.cpp in-process, macOS; Qwen2 or Qwen3 GGUFs | leave AGENT_BASE_URL unset;AGENT_MODEL_PATH defaults tomodels/Qwen3-4B-Instruct-2507-Q4_K_M.gguf |
| A Tadpole adapter on llama.cpp, macOS | AGENT_MODEL_PATH=models/Qwen3-4B-Instruct-2507-Q8_0.gguf ,AGENT_LORA_PATH=models/ei-3-lora-f16.gguf (or sft-1, sft-2); planning turns off, as in training |
ollama pull hf.co/unsloth/Qwen3-4B-Instruct-2507-GGUF:Q4_K_M
OLLAMA_CONTEXT_LENGTH=32768 ollama serve
hf download unsloth/Qwen3-4B-Instruct-2507-GGUF Qwen3-4B-Instruct-2507-Q4_K_M.gguf --local-dir models
hf download unsloth/Qwen3-4B-Instruct-2507-GGUF Qwen3-4B-Instruct-2507-Q8_0.gguf --local-dir models
hf download Shakewin/tadpole-qwen3-4b-ei-3 ei-3-lora-f16.gguf --local-dir models
llama.cpp keeps the model in memory while the agent runs: about 8.5 GB for the Q8_0 base with its 32k window, which slows a 16 GB Mac.
python -m agent_from_scratch.agent
Message @BotFather with/newbot for abot token , and@userinfobot for your numericuser ID .
2.
Put both, and your AGENT_* variables if you use an API, in the env file:
cp agent_from_scratch/deploy/telegram-bot.env.example agent_from_scratch/deploy/telegram-bot.env
Start it, and message your bot: sh agent_from_scratch/scripts/run_telegram_bot.sh
Warning
The agent runs shell commands and edits files with your user's permissions. It answers only
TELEGRAM_ALLOWED_USER_ID, but anyone with the token controls the bot: keep the env file private.
In Telegram and the terminal: /new starts a session, /reset clears it, /session <id>
switches to a named one, and /compact summarizes the history.
Use an always-on machine: a Mac that does not sleep, a home server, or a small cloud VM with an API model (1 GB is enough).
UNIT=~/.config/systemd/user/tadpole-bot.service
mkdir -p "$(dirname "$UNIT")" && cp agent_from_scratch/deploy/tadpole-bot.service "$UNIT"
sed -i "s|/REPLACE/WITH/REPO/PATH|$PWD|g" "$UNIT"
systemctl --user enable --now tadpole-bot && sudo loginctl enable-linger "$USER"
PLIST=~/Library/LaunchAgents/com.tadpole.telegram-bot.plist
cp agent_from_scratch/deploy/com.tadpole.telegram-bot.plist "$PLIST"
sed -i '' "s|/REPLACE/WITH/REPO/PATH|$PWD|g" "$PLIST"
mkdir -p outputs && launchctl load "$PLIST"
Logs: journalctl --user -u tadpole-bot -f on Linux, outputs/telegram_bot.log on macOS.
Each stage adds one mechanism, with a deliberate failure, focused tests (601, none needing a model) and evidence from real model runs.
| Stage | What you build | Code | Read and run |
|---|---|---|---|
| 0.5 | One request: model → tool call → result → answer, with validated calls and errors fed back | llm.py ,agent.py ,tools/base.py |
notebook §1 |
| 1 | Multi-turn sessions, structured traces, explicit stop reasons | session.py ,trace.py |
notebook §2 |
| 2 | Files, shell, web search and fetch tools behind one registry | tools/ |
notebook §2 |
| 3 | Context budgeting, compaction, a summary checkpoint that survives restart | context.py ,compact.py |
notebook §3 |
| 8 | A generated benchmark, an independent verifier, a frozen protocol | evals/bench/ |
notebook §4, datasets ,add a task type |
| 9 | SFT: verified trajectories → examples → LoRA adapter | evals/sft/ |
notebook §5–6, SFT data ,training |
| 10 | Expert iteration and a GRPO pilot | evals/sft/grpo.py |
notebook §7, EI and GRPO |
| 11 | One final comparison on held-out tests | evals/report.py |
results |
Start with the walkthrough notebook (outputs saved, so it reads on GitHub), then the build log for each stage's evidence and evaluating and post-training to run it yourself. Training needs a 24 GB CUDA GPU; the whole post-training line cost about $140 on RunPod.
The agent's code, prompts and tools were frozen, with planning off (no update_plan tool); only
the weights changed. Every choice was made
on val, and each final model ran once on two held-out sets whose task types no training set
contains: 140 single-turn tasks (test) and 80 leveled tasks (hard test), three samples each.
| Base | SFT-1 | SFT-2 | EI-3 | |
|---|---|---|---|---|
| Test pass@1 | 0.419 | 0.774 | 0.738 | 0.745 |
| Test pass^3 (all three samples pass) | 0.364 | 0.707 | 0.607 | 0.657 |
| Hard test pass@1 | 0.275 | 0.429 | 0.492 | 0.471 |
| Hard test pass^3 | 0.125 | 0.250 | 0.325 | 0.338 |
| BFCL multi-turn† | 0.360 | 0.285 | 0.325 | 0.395 |
Bold is best, italic second. SFT-1: verified teacher trajectories plus When2Call and Toucan rows. SFT-2: plus multi-turn demonstrations. EI-3: SFT-2 after three rounds of expert iteration. † A local adaptation of BFCL's multi-turn base category, not a leaderboard score.
- SFT on verified demonstrations gives the large gain , and false "done" claims fall from 31 to 1 in 420 runs.
- The three post-trained models are level on held-out tasks (0.648, 0.648, 0.645 over all 220); EI-3 is the most balanced, and beats SFT-2 on BFCL (p = 0.034).
- Val overstated expert iteration (0.752 → 0.852), andGRPO lost to it at equal rollouts. Neither learned a behaviour the model never produced.
One training run per model, on a small local benchmark: not a claim about general agent ability. Full tables, paired tests and limitations: docs/results.md.
Download all adapters and training data from Hugging Face. Each card includes serving or data-reproduction instructions.
- Memory across sessions, with remember, search and forget tools.
- Skills and MCP behind the same tool registry.
- Background jobs for the always-on bot, and a read-only sub-agent.
- A self-improving harness : with the model fixed, let execution feedback improve the prompts and tools.
Details: the build log.
The tool implementations adapt parts of nanobot (attribution). Thanks to the authors of Qwen3, When2Call, Toucan and BFCL.
MIT, Copyright (c) 2026 shakewingo. Third-party material keeps its own license (the nanobot tools MIT, the Qwen-derived chat template Apache-2.0), and the Hugging Face adapters are Apache-2.0, the SFT data MIT; see Third-party notices.