DEV Community

Taylor Wang
Taylor Wang

Posted on

3-Agent Pipeline: Better Code from Free-Tier Models

A single free-tier completion is a coin flip: one call, one unreviewed guess. I split the same cheap model into three roles—generator, critic, and tester—and loop until a hidden check passes, which is how I get reviewed, test-gated code without paying for a stronger model.

MonkeyCode is an open-source AI coding project. It offers free model access and a free server option. Free tiers fit many small calls. This pattern exploits that fit.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

Why a single free-tier call still fails

A single prompt asks for code. The model guesses. No review. No test. No feedback. I used to treat that first listing as the answer. Free tiers make the flip cheaper. A cheap flip is still a flip, and I still spent the next hour reproducing a bug the model never saw.

Coordination is the fix I can afford. I do not switch models. I do not add tools. I send the same model three different jobs and I refuse to stop until a tester role emits PASS against a check I wrote myself.

Two paths, side by side:

  • One-shot. I paste the task, copy the code, run it later. Failures show up in my shell, not in the prompt loop.
  • Three-role pipeline. Generator writes. Critic lists issues. Tester sees code plus one hidden check. On FAIL, reviewer notes go back into the next generator prompt.

The pipeline spends more quota per task. It also matches how free tiers actually work: many small calls beat one overloaded prompt. Each message stays short. Each role has one job. That is divide and conquer, not autonomous agents.

The HTTP shape is ordinary chat completions—a system message and a user message—posted to an OpenAI-compatible /v1/chat/completions URL. I copy the schema from the OpenAI Chat Completions API.

Three roles I run on one model

Three roles cover most of the small functions I generate on a free tier.

  • Generator writes code. I tell it: senior engineer, correct, minimal Python, output only code.
  • Critic reviews code. I tell it: strict reviewer; name bugs, edge cases, and style issues; output a concise list.
  • Tester validates code. I tell it: test runner; given code and a hidden check, output PASS or FAIL with a short reason.

Each role uses the same model and a different system prompt. The split compensates for weaknesses I cannot prompt away. A generator that also “self-reviews” will often praise its own list(set(items)) and miss order. A critic that also “decides PASS” will treat style nits as a verdict. I keep the tester dumb on purpose: one check, a short PASS or FAIL.

I pin temperature at 0.2 so the generator does not wander and the tester does not flip-flop. I never send the tester the original product spec alone. The check is the contract. If I cannot write a one-line check, the task is too big for this pattern.

The coordinator script

I use a minimal coordinator. It calls three agents in sequence, collects outputs, and stops when the tester passes—or after max_rounds.

The script talks to an OpenAI-compatible endpoint. I point it at MonkeyCode's free server or any compatible API. I keep the loop sequential so each role sees the previous artifact. I still run it under asyncio with httpx so timeouts and later parallel tasks stay simple.

How I wire a new task:

  1. Fill in ENDPOINT, HEADERS, and MODEL.
  2. Write a one-sentence task and one executable check.
  3. Leave max_rounds at 3 so a stuck model cannot drain the allowance.
  4. Print round, code, and status, then compare against a one-shot baseline of the same model.
import asyncio
import json
import httpx

ENDPOINT = "https://your-server.example.com/v1/chat/completions"
HEADERS = {"Authorization": "Bearer YOUR_TOKEN"}
MODEL = "your-model-name"

SYSTEM_PROMPTS = {
    "generator": "You are a senior engineer. Write correct, minimal Python code for the user's request. Output only code.",
    "critic": "You are a strict code reviewer. Identify bugs, edge cases, and style issues in the code. Output a concise list of issues.",
    "tester": "You are a test runner. Given code and a hidden check, determine if the code passes. Output PASS or FAIL with a short reason.",
}

async def call_agent(client, role, user_content):
    payload = {
        "model": MODEL,
        "messages": [
            {"role": "system", "content": SYSTEM_PROMPTS[role]},
            {"role": "user", "content": user_content},
        ],
        "temperature": 0.2,
    }
    r = await client.post(ENDPOINT, json=payload, headers=HEADERS, timeout=120)
    r.raise_for_status()
    return r.json()["choices"][0]["message"]["content"]

async def run_pipeline(task, check, max_rounds=3):
    async with httpx.AsyncClient() as client:
        for round_no in range(1, max_rounds + 1):
            code = await call_agent(client, "generator", task)
            review = await call_agent(client, "critic", f"Code:\n{code}\n\nReview it.")
            if "PASS" in review.upper() and "FAIL" not in review.upper():
                test_result = await call_agent(client, "tester", f"Code:\n{code}\n\nCheck: {check}")
                if "PASS" in test_result.upper():
                    return {"round": round_no, "code": code, "status": "PASS"}
            task = f"{task}\n\nPrevious attempt:\n{code}\n\nReviewer feedback:\n{review}\n\nFix the code."
        return {"round": max_rounds, "code": code, "status": "FAIL"}

async def main():
    task = "Write a Python function dedupe(items) that removes duplicates while preserving order."
    check = "dedupe([3, 1, 3, 2, 1]) == [3, 1, 2]"
    result = await run_pipeline(task, check)
    print(json.dumps(result, indent=2))

if __name__ == "__main__":
    asyncio.run(main())
Enter fullscreen mode Exit fullscreen mode

How context flows, with a dedupe walkthrough

The generator sees the task. The critic sees the code. The tester sees code and check. Context flows forward only. I do not give the critic the hidden check, so it cannot cheat by pattern-matching the assertion. I do not give the tester the full product story, so it cannot invent extra requirements.

The feedback loop is the part that matters. Reviewer comments become the next user prompt. The model improves by iteration, not by a larger context window. This is a structured pipeline, not autonomous agents. Structure is what makes a weak free-tier model usable.

Worked example: dedupe(items) must remove duplicates and preserve order. Hidden check: dedupe([3, 1, 3, 2, 1]) == [3, 1, 2].

  • Round 1. Generator returns return list(set(items)). Critic notes that set drops order. The coordinator folds that review into the next generator prompt instead of treating the first listing as done.
  • Round 2. Generator returns a dict-from-keys version, which keeps insertion order in modern Python. Tester confirms the check and I return that code.

Actual listings depend on the model. The control flow does not. If round 3 still fails, I keep the last code, mark FAIL, and either tighten the check or admit the model cannot do the task. I do not keep calling.

When this pattern helps—and when I skip it

Task type Multi-agent? Why
Single-function codegen Yes Catches edge cases the first listing misses
Test scaffolding Yes Critic and tester disagree in useful ways
Small refactor Yes Reviewer is more likely to spot a regression
Large architectural change No Context is too big for three short prompts
High-throughput batch No Call count explodes

Limitations I treat as hard:

  • Call count. A three-agent pipeline uses three to nine calls per task. I count that against the free allowance before I start a batch.
  • Shared blind spots. Coordination adds scrutiny, not knowledge. Critic and tester are the same model. When correctness matters, I still run real tests on my machine.
  • Latency. Sequential calls add up. asyncio helps me overlap independent work; it does not make one round faster than three round-trips.

Who should not use this:

  • Teams with a strict latency budget. I am trading latency for quality.
  • Anyone with a tiny token cap. One task can consume dozens of calls if I raise max_rounds.
  • Anyone who needs deterministic output. The feedback loop adds variability. A fixed one-shot pipeline is more repeatable.

Next step. Run the script against your endpoint. Start with one task. Count the calls. Compare the result to a single-call baseline of the same model.

The pattern turns a free tier into a poor man's agent. It is not a replacement for a strong model. It gets more from what I already have.

Try it on MonkeyCode's free server. The first pipeline takes minutes. The insight lasts longer.

Top comments (0)