DEV Community

Riley Li
Riley Li

Posted on

Keep the Agent Loop Portable, Then Choose Free Shared Compute

Free shared compute is the right first lane when your agent loop can move tomorrow without rewriting tools, traces, or secrets. Paid APIs and self-hosted boxes win when that loop is welded to private data, mutating tools, or evals you cannot replay. I treat the reverse order as the expensive mistake, because you pay in migrations rather than invoices. Why start with the price tag when the part you cannot unwind is the coupling?

Coupling is not latency, and it is not your monthly bill

I use coupling to mean how much of the agent definition lives outside your repository. Prompts, tool schemas, trace format, secret handling, and retry policy should stay yours even if the GPU does not. If swapping the endpoint forces a rewrite of those five pieces, you did not rent compute. You rented an architecture, and that is a much stickier invoice.

This is a proposed checklist, not a production study, and I am not attaching fabricated timings to it. Treat the scores as a conversation you can have in a pull request, then replace the weights with your constraints. The point is to decide the lane before the loop grows a dozen irreversible side effects. Can your standup even name those side effects today?

If the agent is still a trench coat, stay on free

A lot of so-called agents are still a classifier plus a switch statement wearing a confident system prompt. Those loops barely couple to anything, so a shared free lane is usually enough and I would not apologize for using one. The coupling checklist will green-light them quickly, and that result is correct rather than naive. Are you sure your loop is more than a trench coat, or have the tools never left the slide deck?

The moment you add tools that send mail, open pull requests, or touch billing, the score should flop toward a private lane. I want that flop to happen on paper, before a weekend prototype writes to production because someone pasted a key. List every tool out loud and mark each one read-only or mutating. If you cannot finish that list, you are not choosing compute yet. You are still designing the agent.

Six questions that actually change the lane

Ask these in order, and stop at the first hard no whenever the data is truly sensitive. I keep dollars out of this pass on purpose so the architecture argument stays visible.

  1. Can I export every trace as JSON that another runner can ingest without a vendor SDK?
  2. Do any tools mutate production systems, or do they only read fixtures and stubs?
  3. Do prompts or retrieval chunks contain customer data I cannot put on a shared tenant?
  4. Must evals pin behavior across weeks, or is directional quality enough for this loop?
  5. Can the loop tolerate noisy-neighbor latency, or does a human wait on every tool call?
  6. If the free lane vanished tonight, could I rerun the same fixtures on another endpoint this week?

Notice that none of those questions mention a unit price for tokens. Cost still matters later, after you know whether the loop is portable at all. If question two or three is a hard no, skip the free shared lane and go to a paid API or a box you control. Would you like a prettier matrix than that? Not until those two answers are honest.

A five-step workflow before anyone pastes a key

Here is the sequence I want written down before CI learns a new endpoint. The code is a proposed harness. It checks the scorer and the loop contract, not a live vendor and not a benchmark I did not run.

Step 1: Freeze the loop in your repo

Write the agent as a plain function that takes a task record and returns a trace. Keep system prompts, tool JSON, and retry limits in files you can grep without clicking a hosted UI. If the product console is the only place those live, you are already coupled and the rest of this article will not save you.

# loop_contract.py — proposed interface, not a vendor SDK
from dataclasses import dataclass, field
from typing import Any, Callable

@dataclass
class Task:
    id: str
    input: str
    fixtures: dict[str, Any]

@dataclass
class TraceEvent:
    role: str
    name: str
    payload: dict[str, Any]

@dataclass
class Trace:
    task_id: str
    events: list[TraceEvent] = field(default_factory=list)
    ok: bool = False

def run_loop(task: Task, complete: Callable[[str, list[dict]], str]) -> Trace:
    """One turn-taking loop. `complete` is the only endpoint-shaped hole."""
    trace = Trace(task_id=task.id)
    system = open("prompts/system.md", encoding="utf-8").read()
    messages = [
        {"role": "system", "content": system},
        {"role": "user", "content": task.input},
    ]
    # Dispatch tools against task.fixtures only. Never against live prod.
    _ = messages
    return trace
Enter fullscreen mode Exit fullscreen mode

Step 2: Score coupling with explicit weights

I keep the weights boring so a reviewer can argue with them in the diff. Hide them in a private spreadsheet and the decision turns back into vibes. Change the numbers in review if your world is harsher than mine.

# coupling_score.py
from dataclasses import dataclass

@dataclass
class CouplingAnswers:
    traces_exportable: bool
    tools_are_read_only: bool
    no_customer_data_in_prompts: bool
    directional_evals_ok: bool
    latency_slack_ok: bool
    can_rerun_elsewhere_this_week: bool

WEIGHTS = {
    "traces_exportable": 2,
    "tools_are_read_only": 3,
    "no_customer_data_in_prompts": 3,
    "directional_evals_ok": 1,
    "latency_slack_ok": 1,
    "can_rerun_elsewhere_this_week": 2,
}

def coupling_score(a: CouplingAnswers) -> tuple[int, str]:
    flags = a.__dict__
    score = sum(WEIGHTS[k] for k, ok in flags.items() if ok)
    hard_block = (not a.tools_are_read_only) or (not a.no_customer_data_in_prompts)
    if hard_block:
        return score, "self_hosted_or_paid_private"
    if score >= 10 and a.can_rerun_elsewhere_this_week:
        return score, "free_shared_ok"
    if score >= 7:
        return score, "paid_api_ok"
    return score, "self_hosted_or_paid_private"
Enter fullscreen mode Exit fullscreen mode

I also keep the answers in YAML so a human can read the gate without opening Python. That file belongs next to the prompts, not in a wiki that nobody updates.

# coupling.yml — proposed answers for a read-only sketch loop
traces_exportable: true
tools_are_read_only: true
no_customer_data_in_prompts: true
directional_evals_ok: true
latency_slack_ok: true
can_rerun_elsewhere_this_week: true
Enter fullscreen mode Exit fullscreen mode
# load_coupling.py — proposed
import yaml
from coupling_score import CouplingAnswers, coupling_score

def recommend(path: str = "coupling.yml") -> tuple[int, str]:
    with open(path, encoding="utf-8") as fh:
        raw = yaml.safe_load(fh)
    return coupling_score(CouplingAnswers(**raw))
Enter fullscreen mode Exit fullscreen mode

Step 3: Prove the score with a tiny test, not a slide

Slides do not fail in CI. These tests do, and they only prove the scorer, which is the entire point of this artifact.

# test_coupling_score.py
from coupling_score import CouplingAnswers, coupling_score

def test_mutating_tools_never_land_on_shared_free():
    a = CouplingAnswers(
        traces_exportable=True,
        tools_are_read_only=False,
        no_customer_data_in_prompts=True,
        directional_evals_ok=True,
        latency_slack_ok=True,
        can_rerun_elsewhere_this_week=True,
    )
    _, lane = coupling_score(a)
    assert lane == "self_hosted_or_paid_private"

def test_portable_read_only_loop_may_use_free_shared():
    a = CouplingAnswers(
        traces_exportable=True,
        tools_are_read_only=True,
        no_customer_data_in_prompts=True,
        directional_evals_ok=True,
        latency_slack_ok=True,
        can_rerun_elsewhere_this_week=True,
    )
    score, lane = coupling_score(a)
    assert score == 12
    assert lane == "free_shared_ok"

def test_private_prompts_are_a_hard_block():
    a = CouplingAnswers(
        traces_exportable=True,
        tools_are_read_only=True,
        no_customer_data_in_prompts=False,
        directional_evals_ok=True,
        latency_slack_ok=True,
        can_rerun_elsewhere_this_week=True,
    )
    _, lane = coupling_score(a)
    assert lane == "self_hosted_or_paid_private"
Enter fullscreen mode Exit fullscreen mode
python -m pytest test_coupling_score.py -q
python -c "from load_coupling import recommend; print(recommend())"
Enter fullscreen mode Exit fullscreen mode

Step 4: Map the lane to an actual substrate

Use the table, then write the choice into the pull request, not into Slack memory. I care less about the brand on the GPU than about which refusals you are signing up for.

Lane Fit You accept You refuse
Free shared models plus a free server Read-only tools, no customer prompts, exportable traces Noisy neighbors, weaker pinning, possible queueing Secrets, production mutators, compliance boundaries
Paid API You need a clearer endpoint contract and less queueing An invoice, still someone else's tenancy Full control of weights and disk
Self-hosted Mutating tools, private corpora, pinned evals Ops load, patching, capacity planning Treating GPUs as a weekend experiment

Step 5: Keep a kill switch in the runner

If the abort feels harsh, good. The cheap lane should be hard to enter once tools start writing. I would rather fail a job than discover coupling during an incident review.

# runner.py — proposed
import os
from coupling_score import CouplingAnswers, coupling_score

LANE = os.environ.get("AGENT_LANE", "free_shared_ok")

def assert_lane_matches(answers: CouplingAnswers) -> None:
    _, recommended = coupling_score(answers)
    if LANE == "free_shared_ok" and recommended != "free_shared_ok":
        raise SystemExit(
            f"refusing to start: coupling recommends {recommended}, not {LANE}"
        )
Enter fullscreen mode Exit fullscreen mode
export AGENT_LANE=free_shared_ok
python runner.py  # should refuse when coupling.yml is a hard block
Enter fullscreen mode Exit fullscreen mode

Where a free model lane still wins

Plenty of loops are sketches: classifying fixtures, drafting refactors against a public repo, or teaching the team how tool calling actually looks. Those loops should not wait for a purchase order, and they should not wait for a cluster either. I want them cheap, portable, and easy to throw away when the prompt is wrong.

Disclosure: This article was prepared as part of MonkeyCode's product outreach. I mention MonkeyCode here only because its free model access and free server option are a concrete shared lane you can point this checklist at, not because a coupling test requires that product. If you try that lane, keep the complete() hole tiny and keep traces in your repository. The free server is useful when you want a process that is not your laptop, not when you want a new source of truth.

Would I put a customer corpus there? No. Would I put a read-only loop that I can replay on another endpoint? Yes, and I would keep the pytest from Step 3 in CI so the answer cannot drift quietly. That is the whole rehearsal: prove portability on a lane that is allowed to be imperfect.

Limitations, and who should skip this

This scoring does not measure model quality, and it does not promise that any free lane stays free. I am not publishing quotas, hardware lists, or model names I cannot verify, and you should not treat my weights as research. Teams under HIPAA, PCI, or similar rules should not use a shared free tenant for prompts or traces, period.

Skip this approach if your agent is already a production mutator with no stubbed tools. Skip it if you cannot export traces into JSON you own. Skip it if leadership wants a vendor demo more than a portable loop. A checklist will not save a design that stored secrets inside a web UI and called that an architecture.

The honest close is simple. Score coupling first, pick the cheapest lane that survives the hard blocks, and keep the loop in git. If a free shared option helps you rehearse that discipline on a non-sensitive task, use it as a rehearsal stage rather than a forever home.

Top comments (0)