Module 24 — Local-First, Cost-Aware Agents¶
Time: 4–6 days · Depends on: 10 Cost, 11 Single agents, 17 Small models, 20 Reliability · Next: Durable orchestration
Fictional teaching scenario.
A weekend hack ships a “personal repo agent” that calls a flagship model for list_dir. Four hours of thrash: $40, a warm laptop, and a summary you could have gotten from rg. The SLM on Ollama would have classified and extracted; the cloud model was only needed when JSON failed twice. There was no token budget, no tier, and no admission control. Local-first is not charity. It is the same routing idea as Module 10, applied to agents that otherwise loop.
Case question: Which steps belong on the local model, what explicit failure permits escalation, and where does the runtime refuse to exceed budget?
Learning objectives¶
- Run a useful agent on a laptop: local/SLM first, cloud only on escalate
- Enforce a token budget in the runtime (hard stop), not in the prompt
- Route by task class + remaining budget + schema failure, not by vibes
- Keep the course ethos: no GPU required for the teaching path; optional Ollama
Module 17 taught how to run small models. This module teaches how to assign work to them inside a plan–act–observe loop without lighting money on fire when they fail.
Intuition lock
Sticky picture: The local model is the on-call intern with a clipboard — classify, extract, route, short rewrite. The cloud model is the consultant you page when the schema fails or the task is plan. The token budget is a prepaid meter: the next prompt is refused if it would overflow. Hybrid routing is triage, not a personality contest.
Kill this idea: “Local models aren’t good enough for agents, so always use the biggest API.” → Replace with: Local/SLM owns narrow steps under a hard token cap; escalate on schema failure or hard tasks; abort when the meter is empty.
Mental model¶
flowchart TB
Goal --> B{TokenBudget remaining?}
B -->|no| Abort[abort: token budget]
B -->|yes| R[HybridAgentRouter]
R -->|classify / extract| L[local SLM]
R -->|plan / schema_failed| S[strong cloud]
R -->|long prompt / tight budget| M[cloud mini]
L --> V{Schema OK?}
V -->|no| S
V -->|yes| Act[tool / final]
S --> Act
Act --> Charge[budget.charge]
Charge --> B
Invariant: allow(estimate) runs before the provider call. Charging after a 8k completion is an autopsy (same rule as Module 20’s spend guard).
Core tutorial¶
1. Token budgets are circuit breakers for tokens¶
from src.local_agents import TokenBudget, estimate_tokens
budget = TokenBudget(max_tokens=4096)
est = estimate_tokens(prompt) + 64 # reserve for the reply
if not budget.allow(est):
abort("token_budget")
out = llm(prompt)
budget.charge(est + estimate_tokens(out))
⚠️
estimate_tokensin this repo is a ~4 chars/token heuristic — good enough to teach admission control, but it under- or over-counts real tokenizers by a wide margin (whitespace-heavy code, non-English text, and BPE merges all break the 4:1 ratio). Replace it with a real tokenizer (tiktoken, Hugging Face) before this number gates a production budget or a bill.
Pair with Module 10 UsageLedger if you also track USD.
2. Hybrid router: local → mini → strong¶
from src.local_agents import HybridAgentRouter, TokenBudget
r = HybridAgentRouter(
local_id="ollama:llama3.2",
mini_id="cloud-mini",
strong_id="cloud-strong",
)
tier, model = r.pick("classify", "short ticket", TokenBudget(4000))
assert tier == "local"
tier, model = r.pick("plan", "x", TokenBudget(4000), schema_failed=True)
assert tier == "strong"
| Signal | Tier |
|---|---|
classify / extract_fields / route and budget left |
local |
plan / deep_reason or JSON schema failed |
strong (mini if budget is tiny) |
| Prompt longer than local cap | mini/strong via Module 10 ModelRouter |
Re-eval after quantization (Module 17). A Q4 local model that cannot emit JSON will escalate every step and lose the cost plot.
Explainer
Escalate on validator failure, not on vibes. The intern (SLM) drafts JSON; Pydantic/json.loads is the exam. Fail → one strong call with the same schema. That is cheaper than starting every loop on a flagship model, and it keeps the laptop path honest when the network is down: classify still works; planning degrades or pauses.
sequenceDiagram
participant R as HybridAgentRouter
participant L as Local SLM
participant V as Schema validator
participant S as Strong / cloud model
participant B as TokenBudget
R->>B: allow(estimate)?
B-->>R: ok
R->>L: prompt (task=extract/classify)
L-->>R: draft JSON
R->>V: validate(draft)
alt schema ok
V-->>R: pass
R->>B: charge(actual)
else schema failed
V-->>R: fail
R->>S: same prompt + schema (one escalation)
S-->>R: draft JSON
R->>B: charge(actual, incl. escalation)
end
Escalation happens at most once per step with the same schema — the local attempt is not wasted work, it is the cheap first try that only costs a strong call when it actually fails the validator.
Routing is different from speculative decoding¶
This router selects which model owns an answer. Speculative decoding uses a cheaper proposer for draft tokens and keeps the target model in the verification path. Correct acceptance/correction rules can preserve the target distribution; delegating a whole answer to a local model makes no such promise. See the original paper.
Do not load a second model onto a constrained laptop just because draft generation sounds cheaper. Benchmark the extra memory and draft overhead against observed decode savings. For a short classification, the overhead may dominate. Module 28 is how you choose between reducing calls, routing calls, and optimizing generation within a call.
3. Wrap the Module 11 agent¶
from src.local_agents import TokenBudget, run_local_first
import json
def local_llm(prompt: str) -> str:
return json.dumps({"type": "final", "content": "ok"})
state, route_log = run_local_first(
{"local": local_llm, "mini": local_llm, "strong": local_llm},
tools={"echo": lambda text: text},
goal="say hi",
budget=TokenBudget(max_tokens=2048),
max_steps=6,
task="chat",
)
If the next prompt would exceed the budget, the helper aborts with abort_reason containing budget — a hard stop, same family as max_steps.
Laptop path without GPU:
Point local_llm at that base URL. Keep cloud keys out of the local process env when you do not intend to escalate (Module 21 env scrub).
Hardware is a routing input, not a footnote: Module 17 §7 sizes the local tier (recommend_local_setup) so the 3B you route to actually fits. An 8B that swaps is not “local-first”; it is a slower, hotter cloud.
4. What SLMs can own in an agent¶
| Own locally | Escalate |
|---|---|
| Intent classify, tool pick among 3–5 names | Multi-hop plans, novel APIs |
| Field extract into a schema | Ambiguous policy / legal tone |
| Short rewrite, commit message | Long synthesis from many tool dumps |
| Router (Module 19) | Critic on high-severity drafts |
Measure with Module 22: process score should drop loops when the local model cannot plan — because max_steps and the budget fire, not because you “hope.”
Think about it
Question: Local 3B model is free. Why still put a 4k token budget on it?
Reveal a strong answer
Local is not free in **wall-clock, battery, context quality, or fan noise**. Unbounded local loops still thrash tools, fill scratchpads, and block the UI. The budget is a **termination condition** and a product SLO (p95 latency), not only a cloud invoice. If you later add a mini escalate, the same meter prevents a surprise bill.Failure modes¶
| Symptom | Cause | Fix |
|---|---|---|
Every step hits strong |
Schema too hard for SLM / task labeled plan |
Narrow JSON; fix task tags |
| Budget never trips | Estimate always 0 | Floor estimate per step |
| Local path unused | prefer_local=False or huge prompts |
Pack context (Module 05); split subroutines (18) |
| Quality cliff after Q4 | No re-eval | Module 17 golden after quant |
Case checkpoint
Opening failure: A local agent used an expensive cloud model for trivial steps and had no admission-control budget.
What this lab demonstrates: The budget assertions, task-class router, schema-failure escalation, and stubbed local-first run make tier selection and refusal deterministic.
What it does not prove: Stub routes do not establish local-model quality or real token accounting until evaluated on representative tasks.
Lab¶
TokenBudget(10): assertallow(11)is false.- Router:
classify→ local;plan+schema_failed→ strong. run_local_firstwith a tiny budget; assert abort reason mentions budget.- Optional: point
localat Ollama if installed; leave CI on stubs.
Quizzes¶
Quiz · +25 XP
When should a token budget abort an agent?
Quiz · +25 XP
When is escalating from local to a strong model the right move?
Open source materials¶
| Resource | Use it for |
|---|---|
src/local_agents.py + tests |
Budget, router, run_local_first |
| Ollama / llama.cpp | Laptop inference |
| Modules 10, 17 | Unit economics; quant + re-eval |
Module 20 SpendGuard |
USD twin of the token meter |
Checkpoint¶
- Hard token (or $) budget on the agent loop
- Local/SLM owns at least one step class with a measured success rate
- Escalate path exists and is tested with a stub
- CI does not require a GPU or a paid key
Mark complete when a stubbed agent aborts on an empty token meter and routes classify locally.
Exercise¶
- Catalog: EX-24 — Local-first
- Prove:
TokenBudgetrefuses overflow; classify stays local; a tiny meter aborts with a budget reason. - Test:
pytest tests/test_local_agents.py -v
Return to the case: The runtime assigns narrow steps locally, escalates on explicit failure, and refuses work beyond the token budget. Local-first routing saves resources only while task-level quality remains measured.
Next: Durable orchestration