~/projects
Contract-Net Auction Market
Repo: mohd-vasim/ai-engineering → mas-bidder-agents
When multiple specialized AI agents share a task queue, matching the right agent to the right job is a coordination problem. An agent that's great at code generation will give you terrible poetry — but it might not know that. A contract-net protocol solves this by running an auction: announce the task, collect bids, evaluate them against cost and capability, and award the work.
This project builds the full auction pipeline with LangChain and Ollama, using a local LLM (qwen3.5:4b-mlx) for every agent — no cloud dependency, no API costs.
The Pattern: Contract-Net Protocol
The contract-net protocol is a decentralized task allocation pattern from distributed AI systems. A Solicitor announces a task to all agents, each agent evaluates whether it can handle the work and submits a bid, and the Solicitor scores the bids and awards a contract to the winner. The winner executes, and its performance feeds back into future rounds through a reputation system.
Flow
The Components
| Component | Responsibility |
|---|---|
| Solicitor | Runs the auction end-to-end: announces the task, collects bids, scores them, awards the contract, records the outcome. |
| OllamaBidderAgent | Wraps an LLM into the auction protocol. Its evaluate method decides whether to bid; its execute method produces the deliverable. |
| ReputationRegistry | Tracks every agent's success/failure record and computes a penalty multiplier that adjusts confidence before scoring. Also persists to disk. |
| UtilityWeights & utility_score | Configurable linear scoring function: α × confidence − β × cost − γ × (ETA / 60). Different weight sets capture different priorities — time-sensitive vs. budget-constrained. |
| Prompts (EVAL_SYSTEM, EXEC_SYSTEM) | Two prompt templates. The evaluation prompt enforces domain filtering with explicit can_handle: false rules. The execution prompt is simpler — just produce the deliverable. |
The key design choice: each agent holds its own domain list, not a shared one. This means the same underlying LLM can power multiple agents with different specializations. The solicitor never tells an agent what it can do — the agent's own system prompt and domain list enforce that boundary.
Why Domain Filtering Matters
The first iteration of this system revealed a problem: agents would bid on tasks outside their specialization. A poet-agent would claim it could debug JavaScript with high confidence. The fix was to make the evaluation prompt rigid — explicit language forcing can_handle: false when the task type doesn't match the domain list.
# Prompt excerpt
RULE: You must respond with can_handle=false if the task type is NOT in your domain list.
Do NOT assume you can do things outside your specialization.With this in place, a test of six scenarios (three agents × three task types) passes 5/6 — the one deliberate failure tests whether a coder rejects an ML task, which it currently doesn't. The prompt could still be stricter.
Why a Utility Function
Not all bids are equal. A high-confidence bid with a 4-hour ETA and 15. The utility function lets you encode your priorities:
- Default weights (α=1, β=1, γ=1): balanced between confidence, cost, and time.
- Time-sensitive (α=1, β=0.5, γ=5): heavily penalizes slow delivery — a fast-but-expensive agent wins.
- Budget-conscious (α=1, β=5, γ=0.5): heavily penalizes cost — the cheapest agent wins.
How Reputation Works
A one-shot auction ignores history. The ReputationRegistry turns it into an iterated game by computing a penalty multiplier:
penalty = min(1.0, (failures / total) * 2.0)
adjusted_confidence = confidence * (1.0 - penalty)One failure in three attempts gives a 0.67 penalty, cutting advertised confidence from 0.9 to 0.3. A second failure pushes the penalty to 1.0, zeroing out the agent's effective confidence. The registry persists to reputation.json so the system remembers across sessions.
This design has a deliberate blind spot: a newcomer with no track record gets penalty = 0.0 (no penalty). The system trusts new agents fully on their first task. This is fine for a sandbox but worth hardening for production — a default penalty for unknown agents or a probationary period.
The Auction Flow in Detail
A single auction cycle through the Solicitor.run_auction method:
- Announce: The task is described to every agent with its type and constraints.
- Bid: Each agent calls the LLM with the evaluation prompt and returns
can_handle,confidence,cost_dollars,eta_minutes,reasoning. - Score: The solicitor adjusts confidence with reputation penalties, then scores each bid with
utility_score. - Award: The highest-score bid wins. The solicitor calls
winner.execute(task). - Record: The result (success/failure) updates the reputation registry. The file is persisted to disk.
If no agent can handle the task, a NoBidsException is raised — prefer a clean error over awarding the contract to the least-unqualified bidder.
Tech Stack
- LangChain + Ollama —
ChatOllamawithformat="json"for structured agent output. Every agent call is local, free, and private. - Streamlit — interactive dashboard where you configure agents, pick or define tasks, tune utility weights, and watch each auction phase in real time.
- pandas — bid comparison table (agent × confidence × cost × ETA × utility) rendered as a DataFrame.
- uv — Python package manager; single command to sync and run.
How to Run
git clone https://github.com/mohd-vasim/ai-engineering.git
cd ai-engineering/mas-bidder-agents
uv sync
# Make sure Ollama is running with qwen3.5:4b-mlx (or swap the model in llm.py)
uv run streamlit run app.pyIn the dashboard you can:
- Add or remove agents with custom domain lists
- Pick from preset tasks or define your own
- Tune the utility weights with sliders (α, β, γ)
- Run the auction and watch each phase unfold — evaluation, scoring, award, execution
- Inspect the reputation history and bid comparison table
A CLI demo is also available via uv run python -m mas_bidder_agents.demo for a no-GUI run.
What's in the Repo
src/mas_bidder_agents/— the core library:models.py—Task,Bid,ContractResult,UtilityWeightsdataclasses.llm.py—get_llm()factory andclean_json()defensive JSON parser.prompts.py—EVAL_SYSTEM,EVAL_HUMAN,EXEC_SYSTEM,EXEC_HUMANtemplates.bidder.py—OllamaBidderAgentwithevaluate()andexecute().utility.py—utility_score()function.reputation.py—ReputationRegistrywith disk persistence.solicitor.py—Solicitororchestrating the full auction.demo.py— CLI demo with preset agents and tasks.
app.py— Streamlit interactive dashboard.tests/— pytest suite.notebooks/— Jupyter notebook with the iterative prototyping that led to the final design.docs/— FDS (Functional Design Specification) documents.
Limitations & What's Next
The linear utility function works for three agents but starts to show cracks at scale: two agents with identical scores, agents padding confidence to win bids they can't complete, or a newcomer with no reputation competing against veteran specialists.
The prompt-based domain filter works for broad categories (code, poetry, data_science) but can't prevent an ML engineer from claiming it can do "data science" on a medical diagnosis task that happens to use ML. Finer-grained domain hierarchies would help.
Possible next iterations:
- Tie-breaking — secondary criteria for identical utility scores (past volume, delivery variance, domain overlap).
- Probationary reputation — a penalty for newcomers to prevent confidence-inflation attacks before reputation catches up.
- Domain hierarchies —
data_science → ml → computer_visionso an agent that specializes incomputer_visioncan reject a genericdata_sciencetask. - Multi-round auctions — sealed-bid vs. open-bid variants, or iterative price discovery.
- Execution verification — a separate judge agent that validates the winner's output before recording success.