{"slug": "show-hn-tadpole-an-llm-agent-built-from-scratch-then-post-trained-on-4b-model", "title": "Show HN: Tadpole – an LLM agent built from scratch then post-trained on 4B model", "summary": "A developer operating under the GitHub handle shakewingo released Tadpole, a tool-using LLM agent written in about 3,500 lines of plain Python with no agent framework, which runs on a local 4B model or any OpenAI-compatible API from a terminal or as a Telegram bot. The project's stage-by-stage post-training pipeline lifted Qwen3-4B from 0.419 to 0.774 pass@1 on task types it never trained on, through supervised fine-tuning, expert iteration and a GRPO pilot. Tadpole requires Python 3.11 or later and ships with downloadable LoRA adapters (sft-1, sft-2, ei-3) for llama.cpp.", "body_md": "*An LLM agent built from scratch, and grown one stage at a time.*\n\nA 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.\n\n- **An agent you can use.** Files, shell, web search and fetch, sessions and compaction, on a\nlocal 4B model or any OpenAI-compatible API, in a terminal or as a Telegram bot.\n- **A from-scratch tutorial.** Each stage adds one mechanism with tests and measured evidence,\nfrom the first agent loop to SFT, expert iteration and a GRPO pilot. Post-training lifts\nQwen3-4B from**0.419 to 0.774** pass@1 on task types it never trained on\n([results](#post-training-results) ).\n\nPython 3.11 or later. Run every command from the repository root with the venv active.\n\n```\ngit clone https://github.com/shakewingo/Tadpole.git\ncd Tadpole\npython3 -m venv .venv && source .venv/bin/activate\npip install -e .\n```\n\nUse a strong model through any OpenAI-compatible API: DeepSeek, OpenAI, OpenRouter, or your own\nvLLM or LM Studio server. `export` these for the terminal, or put them in the\n[bot's env file](https://github.com/shakewingo/Tadpole/blob/main/agent_from_scratch/deploy/telegram-bot.env.example):\n\n```\nexport AGENT_BASE_URL=https://api.deepseek.com   # the API root; often ends in /v1\nexport AGENT_MODEL=deepseek-flash                # the provider's model ID\nexport AGENT_API_KEY=sk-...\nexport AGENT_N_CTX=32768                         # optional: the model's context window\n```\n\n## **Run Qwen3-4B locally, base or post-trained**: the model this project trains, for\nstudying the training rather than everyday use\n\n| Option | Variables | \n|---|---|\n| 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` | \n| llama.cpp in-process, macOS; Qwen2 or Qwen3 GGUFs | leave `AGENT_BASE_URL` unset;`AGENT_MODEL_PATH` defaults to`models/Qwen3-4B-Instruct-2507-Q4_K_M.gguf` | \n| A Tadpole [adapter](#download-the-adapters-and-data) 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 | \n\n```\n# Ollama: pull, then serve with a window large enough for the agent\nollama pull hf.co/unsloth/Qwen3-4B-Instruct-2507-GGUF:Q4_K_M\nOLLAMA_CONTEXT_LENGTH=32768 ollama serve\n\n# llama.cpp: the default AGENT_MODEL_PATH\nhf download unsloth/Qwen3-4B-Instruct-2507-GGUF Qwen3-4B-Instruct-2507-Q4_K_M.gguf --local-dir models\n\n# An adapter, on the Q8_0 base it was measured on\nhf download unsloth/Qwen3-4B-Instruct-2507-GGUF Qwen3-4B-Instruct-2507-Q8_0.gguf --local-dir models\nhf download Shakewin/tadpole-qwen3-4b-ei-3 ei-3-lora-f16.gguf --local-dir models\n```\n\nllama.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.\n\n```\npython -m agent_from_scratch.agent\n```\n\n1. \nMessage [@BotFather](https://t.me/BotFather) with`/newbot` for a**bot token** , and[@userinfobot](https://t.me/userinfobot) for your numeric**user ID** .\n2. \nPut both, and your `AGENT_*` variables if you use an API, in the env file:\n\n```\ncp agent_from_scratch/deploy/telegram-bot.env.example agent_from_scratch/deploy/telegram-bot.env\n```\n\n3. \nStart it, and message your bot: `sh agent_from_scratch/scripts/run_telegram_bot.sh`\n\nWarning\n\nThe agent runs shell commands and edits files with your user's permissions. It answers only\n`TELEGRAM_ALLOWED_USER_ID`, but anyone with the token controls the bot: keep the env file private.\n\nIn Telegram and the terminal: `/new` starts a session, `/reset` clears it, `/session <id>`\nswitches to a named one, and `/compact` summarizes the history.\n\nUse 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).\n\n```\n# Linux (systemd)\nUNIT=~/.config/systemd/user/tadpole-bot.service\nmkdir -p \"$(dirname \"$UNIT\")\" && cp agent_from_scratch/deploy/tadpole-bot.service \"$UNIT\"\nsed -i \"s|/REPLACE/WITH/REPO/PATH|$PWD|g\" \"$UNIT\"\nsystemctl --user enable --now tadpole-bot && sudo loginctl enable-linger \"$USER\"\n\n# macOS (launchd); also turn on \"Prevent automatic sleeping when the display is off\"\nPLIST=~/Library/LaunchAgents/com.tadpole.telegram-bot.plist\ncp agent_from_scratch/deploy/com.tadpole.telegram-bot.plist \"$PLIST\"\nsed -i '' \"s|/REPLACE/WITH/REPO/PATH|$PWD|g\" \"$PLIST\"\nmkdir -p outputs && launchctl load \"$PLIST\"\n```\n\nLogs: `journalctl --user -u tadpole-bot -f` on Linux, `outputs/telegram_bot.log` on macOS.\n\nEach stage adds one mechanism, with a deliberate failure, focused tests (601, none needing a model) and evidence from real model runs.\n\n| Stage | What you build | Code | Read and run | \n|---|---|---|---|\n| 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](https://github.com/shakewingo/Tadpole/blob/main/agent_from_scratch/examples/walkthrough.ipynb) | \n| 1 | Multi-turn sessions, structured traces, explicit stop reasons | `session.py` ,`trace.py` | notebook §2 | \n| 2 | Files, shell, web search and fetch tools behind one registry | `tools/` | notebook §2 | \n| 3 | Context budgeting, compaction, a summary checkpoint that survives restart | `context.py` ,`compact.py` | notebook §3 | \n| 8 | A generated benchmark, an independent verifier, a frozen protocol | `evals/bench/` | notebook §4, [datasets](https://github.com/shakewingo/Tadpole/blob/main/agent_from_scratch/evals/README.md#datasets) ,[add a task type](https://github.com/shakewingo/Tadpole/blob/main/agent_from_scratch/evals/README.md#adding-a-task-type) | \n| 9 | SFT: verified trajectories → examples → LoRA adapter | `evals/sft/` | notebook §5–6, [SFT data](https://github.com/shakewingo/Tadpole/blob/main/agent_from_scratch/evals/README.md#building-sft-data) ,[training](https://github.com/shakewingo/Tadpole/blob/main/agent_from_scratch/evals/README.md#training-an-adapter) | \n| 10 | Expert iteration and a GRPO pilot | `evals/sft/grpo.py` | notebook §7, [EI and GRPO](https://github.com/shakewingo/Tadpole/blob/main/agent_from_scratch/evals/README.md#expert-iteration-and-grpo) | \n| 11 | One final comparison on held-out tests | `evals/report.py` | [results](https://github.com/shakewingo/Tadpole/blob/main/agent_from_scratch/docs/results.md) | \n\nStart with **[the walkthrough notebook](https://github.com/shakewingo/Tadpole/blob/main/agent_from_scratch/examples/walkthrough.ipynb)** (outputs\nsaved, so it reads on GitHub), then **[the build log](https://github.com/shakewingo/Tadpole/blob/main/agent_from_scratch/docs/STAGE.md)** for\neach stage's evidence and **[evaluating and post-training](https://github.com/shakewingo/Tadpole/blob/main/agent_from_scratch/evals/README.md)**\nto run it yourself. Training needs a 24 GB CUDA GPU; the whole post-training line cost about\n$140 on RunPod.\n\nThe agent's code, prompts and tools were frozen, with planning off (no `update_plan` tool); only\nthe weights changed. Every choice was made\non val, and each final model ran once on two held-out sets whose task types no training set\ncontains: 140 single-turn tasks (test) and 80 leveled tasks (hard test), three samples each.\n\n|  | Base | SFT-1 | SFT-2 | EI-3 | \n|---|---|---|---|---|\n| Test pass@1 | 0.419 | **0.774** | 0.738 | *0.745* | \n| Test pass^3 (all three samples pass) | 0.364 | **0.707** | 0.607 | *0.657* | \n| Hard test pass@1 | 0.275 | 0.429 | **0.492** | *0.471* | \n| Hard test pass^3 | 0.125 | 0.250 | *0.325* | **0.338** | \n| BFCL multi-turn† | *0.360* | 0.285 | 0.325 | **0.395** | \n\n**Bold** is best, *italic* second. SFT-1: verified teacher trajectories plus When2Call and Toucan\nrows. SFT-2: plus multi-turn demonstrations. EI-3: SFT-2 after three rounds of expert iteration.\n† A local adaptation of BFCL's multi-turn base category, not a leaderboard score.\n\n- **SFT on verified demonstrations gives the large gain** , and false \"done\" claims fall from 31\nto 1 in 420 runs.\n- **The three post-trained models are level on held-out tasks** (0.648, 0.648, 0.645 over all\n220); EI-3 is the most balanced, and beats SFT-2 on BFCL (p = 0.034).\n- **Val overstated expert iteration** (0.752 → 0.852), and**GRPO lost to it** at equal rollouts.\nNeither learned a behaviour the model never produced.\n\nOne training run per model, on a small local benchmark: not a claim about general agent ability.\nFull tables, paired tests and limitations: [docs/results.md](https://github.com/shakewingo/Tadpole/blob/main/agent_from_scratch/docs/results.md).\n\nDownload all adapters and training data from [Hugging Face](https://huggingface.co/Shakewin).\nEach card includes serving or data-reproduction instructions.\n\n- **Memory** across sessions, with remember, search and forget tools.\n- **Skills and MCP** behind the same tool registry.\n- **Background jobs** for the always-on bot, and a read-only sub-agent.\n- **A self-improving harness** : with the model fixed, let execution feedback improve the prompts\nand tools.\n\nDetails: [the build log](https://github.com/shakewingo/Tadpole/blob/main/agent_from_scratch/docs/STAGE.md#what-comes-next).\n\nThe tool implementations adapt parts of [nanobot](https://github.com/HKUDS/nanobot)\n([attribution](https://github.com/shakewingo/Tadpole/blob/main/agent_from_scratch/tools/THIRD_PARTY.md)). Thanks to the authors of\n[Qwen3](https://huggingface.co/Qwen/Qwen3-4B-Instruct-2507),\n[When2Call](https://huggingface.co/datasets/nvidia/When2Call),\n[Toucan](https://huggingface.co/datasets/Agent-Ark/Toucan-1.5M) and\n[BFCL](https://github.com/ShishirPatil/gorilla).\n\n[MIT](https://github.com/shakewingo/Tadpole/blob/main/LICENSE), Copyright (c) 2026 shakewingo. Third-party material keeps its own license (the\nnanobot tools MIT, the Qwen-derived chat template Apache-2.0), and the Hugging Face adapters are\nApache-2.0, the SFT data MIT; see [Third-party notices](https://github.com/shakewingo/Tadpole/blob/main/THIRD_PARTY_NOTICES.md).", "url": "https://wpnews.pro/news/show-hn-tadpole-an-llm-agent-built-from-scratch-then-post-trained-on-4b-model", "canonical_source": "https://github.com/shakewingo/Tadpole", "published_at": "2026-10-31 12:43:42+00:00", "updated_at": "2026-10-31 13:18:41.364405+00:00", "lang": "en", "topics": ["ai-agents", "large-language-models", "ai-tools", "ai-research", "developer-tools"], "entities": ["Tadpole", "shakewingo", "Qwen3-4B", "Qwen3-4B-Instruct-2507", "llama.cpp", "Ollama", "Telegram", "GitHub"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/show-hn-tadpole-an-llm-agent-built-from-scratch-then-post-trained-on-4b-model", "markdown": "https://wpnews.pro/news/show-hn-tadpole-an-llm-agent-built-from-scratch-then-post-trained-on-4b-model.md", "text": "https://wpnews.pro/news/show-hn-tadpole-an-llm-agent-built-from-scratch-then-post-trained-on-4b-model.txt", "jsonld": "https://wpnews.pro/news/show-hn-tadpole-an-llm-agent-built-from-scratch-then-post-trained-on-4b-model.jsonld"}}