Are you the author? Sign in to claim
A master orchestrator delegates routine coding to cheap apprentice models — local Qwen + Gemini — behind one MCP server,
A local, multi-provider code-delegation pipeline. A master orchestrator (e.g. Claude Code) delegates routine coding to apprentice models — a local model on your own GPU (via Ollama), Gemini (Vertex AI), GPT/Codex (OpenAI), or any OpenAI-compatible endpoint (Groq, OpenRouter, LM Studio, vLLM, …) — then mechanically verifies, corrects, and learns from the results over time via in-context retrieval. The expensive brain is spent on judgment; the cheap brains do the typing.
The economics in one line: local apprentices are free but weaker; cloud apprentices are stronger but metered — so the pipeline verifies everything mechanically (compile → lint → your project's own tests), starts cheap, escalates only on failure, prices every cloud call, and enforces daily budgets.
Two ways to use it:
apprentice chat gives you a coding agent in your terminal, driven by
one model of your choice. No orchestrator subscription needed. Every change it makes is
verified and auto-reverted if it breaks your tests. → docs/AGENT.mddelegate, assign, log_correction, so
an orchestrator (Claude Code, …) can offload routine coding to cheaper models and stay the
judge.Both share the same engine: providers, mechanical gate, tests, budgets, and the corrections store that makes the workers better over time.
New here? Start with docs/MULTI_AGENT.md — it explains, in beginner terms, what an "agent" is and how the boss + two-worker model fits together.
Note: the project was formerly
qwen-pipeline. Its default working directory and the MCP server id are stillqwen-pipeline/qwen; only the project brand is Apprentice.
Many orchestrators (Claude Code among them) can only run their own model family — there's no way to point a sub-agent at a local model or at Gemini. So the worker models live behind a local MCP server instead. The orchestrator calls a tool, the server runs the chosen model, the orchestrator reviews the output.
┌──────────────────────────────────────────────┐
│ ORCHESTRATOR — the "boss" / decision-maker │
│ splits tasks, picks provider, REVIEWS output, │
│ fixes mistakes, logs corrections, commits │
└───────────────┬────────────────────────────────┘
│ MCP tool call (stdio)
▼
┌──────────────────────────────────────────────┐
│ MCP server (src/server.py, FastMCP) │
│ delegate(task, role, provider?, model?, …) │
│ assign(task, done_when, repo, …) │
│ log_correction(…) │
└───┬───────────────┬───────────────┬────────────┘
▼ ▼ ▼
qwen gemini openai + any
(Ollama, (Vertex AI: openai-compatible
local GPU, flash / pro, endpoint (GPT/Codex,
FREE) metered $) Groq, LM Studio, …)
Providers are config, not code. Any entry in config providers.<name> with a known kind
(ollama-local, openai-compatible, vertex-ai) becomes a valid provider= value — adding
Groq or a second local model is a 6-line JSON block. See
docs/CONFIGURATION.md. Every provider
runs through the same gate, retries, retrieval, metering, and budgets.
The "specialized agents" (test writer, C++ implementer, …) are not separate models — they are
role values that select a different system prompt for the same worker.
assign file-aware agent) Aider in its own venv.google-genai + Google Cloud Vertex AI credentials.The reference machine is an RTX 5090 (32 GB VRAM) + 64 GB RAM, but the pipeline runs anywhere Ollama can serve a model — scale the worker model to your hardware.
pipx install git+https://github.com/m-555/Apprentice.git # or: pip install apprentice-pipeline
apprentice init # creates the data home (~/.apprentice or $APPRENTICE_HOME),
# seeds the config, checks Ollama, prints the MCP registration cmd
apprentice doctor # environment check any time
# pull the worker + embedder models (scale the worker to your hardware)
ollama pull qwen3-coder-next
ollama pull nomic-embed-text
# register with your orchestrator (Claude Code example; `init` prints this too)
claude mcp add --scope local qwen -- apprentice serve
Config and data live in ~/.apprentice (override with APPRENTICE_HOME). Edit
~/.apprentice/config/qwen.local.json for machine-local values and secrets. The Gemini
provider is an extra: pipx install 'apprentice-pipeline[gemini] @ git+https://github.com/m-555/Apprentice.git'.
git clone https://github.com/m-555/Apprentice.git qwen-pipeline
cd qwen-pipeline
# 1. core deps (pinned)
python -m venv .venv
.venv/Scripts/pip install -r requirements.txt # Windows
# .venv/bin/pip install -r requirements.txt # Linux/macOS
# 2. pull the worker + embedder models (sizes approximate)
ollama pull qwen3-coder-next # ~51 GB, Q4_K_M — scale down for smaller GPUs
ollama pull nomic-embed-text # ~274 MB — for retrieval
# 3. (optional) the file-aware `assign` agent, in an ISOLATED venv
python -m venv .aider-venv
.aider-venv/Scripts/pip install -r requirements-aider.txt
# 4. register the MCP server with your orchestrator (Claude Code example)
claude mcp add --scope local qwen -- ".venv/Scripts/python.exe" "src/server.py"
claude mcp list # -> qwen ... ✓ Connected
In a checkout, config and data live in the repo (config/, corrections/, outputs/,
metrics/) — see docs/CONFIGURATION.md. To add the Gemini worker,
see Enabling Gemini below.
| Reference setup | |
|---|---|
| Worker model | qwen3-coder-next:latest — 79.7B MoE (~3B active), Q4_K_M, 262k ctx, tools-capable |
| Embedder | nomic-embed-text (768-dim) — for retrieval |
| Runner | Ollama (HTTP API on 127.0.0.1:11434) |
Expert-offload: the ~48 GB model does not fit in 32 GB VRAM. Ollama keeps the attention/shared layers on the GPU and streams the MoE experts from system RAM. GPU utilisation looks low because it is memory-bandwidth-bound — normal for an MoE with few active params. Warm throughput ≈ 50–58 tok/s; cold load ≈ 55 s.
Warm model: Ollama keeps the model resident for keep_alive after the last request (default
30m here). Requests within that window skip the load (~0.1 s). It's deliberately not infinite
— a warm model holds the whole GPU, so a moderate timeout frees it for other work when idle.
Model storage gotcha (Ollama desktop app): the desktop app stores its model location in
db.sqliteand, when it spawns the server, setsOLLAMA_MODELSto that value — overriding your env var. If a largeollama pullfills the wrong disk, point both the env var and the app's DB at your intended path, then confirm withollama list.
cd /path/to/your-repo
apprentice chat # local free model, verification on
apprentice chat --provider gemini --model pro # a stronger cloud model
apprentice run "add mul(a,b)" --done-when "pytest -q" # unattended
It reads, searches, edits, and runs commands in your repo — and after every turn its changes are checked. If they fail your project's tests, they are reverted byte-for-byte and the verbatim failure goes back to the model to fix:
-> read_file(calc.py)
-> edit_file(calc.py)
-> run_tests()
[OK] verified (tests)
--verify off | gate | tests picks how strict that is; /undo, /provider, /cost and
friends work mid-session; a model that keeps failing escalates to a stronger tier
automatically. Shell commands are allowlist/denylist-checked and prompt before running.
Add --json to stream JSON-lines events instead of text — the integration surface for a
VS Code extension, a web UI, or CI. Full guide: docs/AGENT.md.
delegate(task, role, provider="", context="", model="", repo="", context_files="", apply_to="", apply_mode="append", test_cmd="", return_mode="") -> strSends {system: ROLES[role] (+ repo conventions), user: task (+context)} to the chosen provider
and returns the generated text, plus a status footer with the gate verdict and an output_id.
The pipeline mechanically verifies the output and auto-retries the worker on failure before
returning.
ts_implementer, cpp_implementer, py_implementer, test_writer, refactorerqwen (local, default), gemini (Vertex AI), openai (GPT/Codex), or any
config-defined providergemini, "flash" (routine) or "pro" (hard).Token-cheap mode — the orchestrator's expensive output tokens should never carry code:
repo= + context_files="src/a.ts src/b.ts:20-80" — send paths, not code; the server
reads the content locally (size-capped, path-traversal-guarded) and builds the context block.apply_to="src/a.ts" (+ apply_mode: append|create|overwrite) — the server writes the
gate-passed code into the real file itself.test_cmd="npx vitest run …" — after applying, the server runs your project's own
acceptance command in repo; on red it reverts the file, bounces the verbatim test output
back to the worker, and retries — a full TDD loop with zero orchestrator tokens. The tree is
never left broken. If the tier keeps failing, the task escalates through the cascade
(e.g. local → gemini) carrying the failing code + test output, budget-guarded, before it ever
reaches the orchestrator (footer shows test_tier= when that happened).return_mode="summary" — receive only the status footer + a one-line preview instead of the
full code (it's already in the file and the output store).A routine function then costs the orchestrator roughly: task spec in, two-line footer out.
delegate(task="Add mul(a,b)…", role="py_implementer",
repo="/path/to/proj", context_files="mathx.py",
apply_to="mathx.py", test_cmd="python test_mathx.py",
return_mode="summary")
→ [summary] code_lines=3 first_line='def mul(a: float, b: float) -> float:'
[qwen-pipeline] machine_verified=true check=py_compile attempts=1 tier=qwen
output_id=… applied=true apply_to=mathx.py test=pass test_attempts=1
assign(task, done_when, repo, provider="", files="", max_iters=0, apply=True, model="") -> dictA file-aware worker agent (Aider) that reads repo itself and grinds a whole task to an
objective "done" with no orchestrator in the loop. The boss's role = define task + define
done + commit.
When to use which: for a known target file, prefer
delegatein token-cheap mode (context_files+apply_to+test_cmd) — it's simpler, faster, and needs no Aider install. Reach forassignwhen the task is genuinely exploratory or multi-file ("find where X is handled and fix it") — that's what the repo-map agent is for.
repo's HEAD — the real
tree is untouched. Loops: worker edits → run done_when (a shell cmd that must exit 0) → on
failure feed the verbatim output back to the worker (up to max_iters).apply, mechanically
applies it to the real tree. You then just commit.{done_passed, applied, iterations, files_changed, patch_path, done_log_tail, worker_log_tail, output_id} — the full diff is in patch_path.log_correction(role, task, error_category, explanation, output_id="", correction_patch="", …) -> {"ok": true}Appends one record to corrections/corrections.jsonl (and indexes it for retrieval). Call it
after every delegation, even when the worker was correct (error_category="none", empty patch).
error_category: logic | compile | style | edge_case | security | api_misuse | none.output_id (from the delegate footer) + a unified-diff
correction_patch instead of re-sending the code — the pipeline reconstructs both sides.delegate(...) (snippet) or assign(...) (whole file-aware task) with the right
role/provider — see config/routing.md.log_correction(...) — always.The mechanical gate + worker→worker auto-retry handle most fixes with zero orchestrator tokens;
the boss only steps in for judgment. Full routing rules: config/routing.md.
The routing philosophy the pipeline is built around:
| Tier | Strength | Cost | When |
|---|---|---|---|
local (qwen, or your Ollama model) | weakest | free | Start every routine task here. |
cloud routine (e.g. gemini flash, GPT-mini) | medium | cheap | GPU busy, or local keeps failing a routine task. |
cloud hard (e.g. gemini pro, GPT/Codex) | strong | pricier | Genuinely hard, well-specified tasks. |
| the orchestrator itself | judgment | most expensive | Security, architecture, ambiguity — never delegated. |
What keeps this honest:
cascade) retries a persistently failing task one tier up, carrying the
failed attempt + checker error so the stronger model doesn't start cold.providers.<name>.cost (USD per Mtok, flat or per tier) and every cloud
call is priced into metrics/metrics.jsonl; python src/metering.py shows per-tier and total
estimated spend.metering.budgets.<name>_tokens_per_day / <name>_usd_per_day:
over-budget providers are refused with a clear error and the cascade won't escalate to them.
A runaway retry loop cannot drain your credits.The pipeline gets better over time via retrieval, not weight training (an 80B can't be
fine-tuned on one 32 GB GPU). The mechanism (src/retrieval.py):
log_correction: the task is embedded with nomic-embed-text and a compact entry
(vector + role/provider/category + few-shot fields) is appended to corrections/index.jsonl.delegate: the incoming task is embedded, the top-k most similar past corrections
for the same provider+role are selected (favoring real mistakes per mistake_vs_correct_mix)
and injected into the system prompt as few-shot examples.python src/retrieval.py reindex.Tunables in config/qwen.json → retrieval: enabled, top_k, role_filter,
prefer_error_categories, mistake_vs_correct_mix.
Secrets and machine-local values go in config/qwen.local.json (gitignored), which is
deep-merged over config/qwen.json at load time — so the committed config never holds a secret.
.venv/Scripts/pip install -r requirements-gemini.txtcp config/qwen.local.example.json config/qwen.local.json, then fill in your GCP project,
the service-account JSON path (credentials_file), the model ids for flash/pro, and
set enabled: true.delegate(..., provider="gemini", model="pro") or
assign(..., provider="gemini", model="flash").⚠️ The
assign(Aider) model ids must use litellm'svertex_ai/prefix for a service account (e.g.vertex_ai/gemini-2.5-pro), NOTgemini/(the AI-Studio API-key path). Full walkthrough + the two-model-id-forms gotcha: docs/CONFIGURATION.md.
Nothing in the gate/agent layer is tied to a particular codebase. To use it on another repo:
delegate(repo="/path/to/your-repo", …) / assign(repo=…, done_when=…) at it.<your-repo>/.qwen-pipeline.json for per-project settings:
{
"conventions": "TypeScript strict; no `any`. Zod for validation. snake_case file names.",
"agent": { "max_iters": 4, "diff_excludes": [".aider*", "node_modules", "dist", "*.pyc"] }
}
conventions is injected into the worker prompt on every delegate(repo=…) — your style
rules are enforced up front instead of corrected after the fact. The agent block merges
over config/qwen.json → agent (repo wins) for assign.gate.languages.*) and any batched build step are config-driven — enable/point
them per project. The MCP surface (delegate, assign, log_correction) is unchanged.qwen-pipeline/
├── README.md
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE # MIT
├── requirements.txt # core, PINNED (mcp, numpy)
├── requirements-gemini.txt # optional: Gemini/Vertex provider
├── requirements-aider.txt # optional: the `assign` agent (install in .aider-venv)
├── config/
│ ├── qwen.json # canonical config (committed, NO secrets)
│ ├── qwen.local.example.json # template for the gitignored local overlay
│ ├── qwen.local.json # GITIGNORED: project id, creds path, model ids, enabled flags
│ └── routing.md # what to delegate, to which provider/role/tier
├── docs/
│ ├── CONFIGURATION.md # config reference + enabling Gemini
│ └── MULTI_AGENT.md # how the boss + two-worker model works (beginner-friendly)
├── src/
│ ├── cli.py # the `apprentice` command (init/chat/run/serve/doctor/…)
│ ├── server.py # FastMCP stdio server: delegate / assign / log_correction
│ ├── providers.py # provider registry: ollama-local / openai-compatible / vertex-ai
│ ├── chat_providers.py # multi-turn chat + tool calls, normalized across providers
│ ├── loop.py / session.py # the agent loop; history, repo map, compaction, transcripts
│ ├── tools.py / verify.py # agent tools (repo-scoped) ; snapshot → check → revert
│ ├── chat_ui.py # the REPL for `apprentice chat` / `apprentice run`
│ ├── deliver.py # server-side context fetch + apply/test/revert (token-cheap mode)
│ ├── budgets.py / corrections.py # shared daily caps; the corrections writer
│ ├── agent.py # the `assign` file-aware agent (Aider + disposable worktree)
│ ├── gate.py / gate_cli.py # mechanical gate (compile/lint) + worker-retry
│ ├── store.py # output-id store + unified-diff apply
│ ├── retrieval.py # embed + cosine retrieval of past corrections
│ ├── metering.py # per-delegation cost/outcome log
│ ├── host_verify.py # optional batched build/test runner (project-specific)
│ └── roles.py # role -> system-prompt map
├── tests/test_pipeline.py # deterministic, offline (stubs providers/embeddings)
└── corrections/ # GITIGNORED contents: corrections + retrieval index (local only)
Gitignored (never pushed): config/qwen.local.json, secrets/, corrections/*.jsonl, outputs/,
metrics/, models/, .venv/, .aider-venv/, node_modules/.
| Symptom | Fix |
|---|---|
delegate errors "Could not reach Ollama" | Server down. Run ollama serve (or start the desktop app). Check ollama list. |
| MCP server not connected | Run the launch command directly to see the error: .venv/Scripts/python.exe src/server.py |
| New tools not visible in a running session | They load in new sessions automatically; in a running one, reconnect (e.g. /mcp). |
| A pull fills the wrong disk | Ollama isn't using your intended path — see the model-storage gotcha above. |
| VRAM near OOM with big context | Cap num_ctx (KV cache grows with context). Prefer this over downgrading the quant. |
| Gemini "not enabled yet" | Expected until Vertex creds are configured — see Enabling Gemini. |
| "Daily token/USD budget … exhausted" | Working as intended — the provider hit its metering.budgets cap. Use the local provider or raise the cap in qwen.local.json. |
delegate test=fail — REVERTED | The worker never satisfied test_cmd; the target file was restored. Review the test output tail in the footer, tighten the task/spec, or take the task yourself. |
| Retrieval not injecting examples | Index empty/stale or embedder down. Rebuild: python src/retrieval.py reindex. |
corrections/ may contain private code — it stays local (gitignored) and is not committed.config/routing.md).MIT © 2026 Mohsen Mirzaei.
Run Claude Code as an MCP server so any agent can delegate coding tasks to it
Browser automation using accessibility snapshots instead of screenshots
Google's universal MCP server supporting PostgreSQL, MySQL, MongoDB, Redis, and 10+ databases
Official GitHub integration for repos, issues, PRs, and CI/CD workflows