Repository navigation
Add author Mauro Mura and cross-post "Your agents don't need an LLM to negotiate" #103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
FDelporte
merged 3 commits into
foojayio:main
from
mauro-mura:add-mauro-mura-and-contract-net
Oct 5, 2026
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| --- | ||
| title: "Mauro Mura" | ||
| avatar: "avatar.jpg" | ||
| bio: "Software engineer working with Java, Spring and full-stack development. Builds Agenor, multi-agent coordination for the JVM." | ||
| github: "https://github.com/mauro-mura" | ||
| linkedin: "https://www.linkedin.com/in/mauromura" | ||
| website: "https://inginfo.eu/" | ||
| --- |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
143 changes: 143 additions & 0 deletions
143
draft/your-agents-dont-need-an-llm-to-negotiate/index.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| --- | ||
| title: "Your agents don't need an LLM to negotiate" | ||
| date: "2026-10-06" | ||
| description: "A manager hands a job to one of three workers, and nothing in the program calls a model. It explains the protocol, and what doesn't survive a restart." | ||
| authors: | ||
| - "mauro-mura" | ||
| image: "cover.jpg" | ||
| categories: | ||
| - "Java" | ||
| - "AI" | ||
| - "Design Patterns" | ||
| canonical: "https://bitautonomi.substack.com/p/your-agents-dont-need-an-llm-to-negotiate" | ||
| --- | ||
|
|
||
| In agents that negotiate, the negotiating usually ends up inside a language model. You describe | ||
| the situation in a prompt, the model answers, and you parse what comes back. It works, and it | ||
| costs a network round trip and a line on a bill for every decision. | ||
|
|
||
| For the narrow case of handing work to whoever should do it, there's a protocol that predates | ||
| all of this. It's called Contract Net, and it's small enough to read in an afternoon. | ||
| [Agenor](https://github.com/mauro-mura/agenor) ships it. The example below has a manager hand a job to one of three workers in a single JVM, and its imports | ||
| contain nothing from any model provider. | ||
|
|
||
| ## The shape of it | ||
|
|
||
| One agent has work to give out. It broadcasts a call for proposals to the agents that might take | ||
| it on. Each of them answers with an offer or turns it down. The caller picks one offer, tells | ||
| that agent it's accepted, and waits for the result. | ||
|
|
||
| Those message kinds have names, borrowed from the agent communication languages this all comes | ||
| out of: `CFP`, then `PROPOSE` or `REFUSE`, then `AGREE`, and `INFORM` or `FAILURE` once the work | ||
| is over. Agenor has ten of them in `Performative`. Contract Net uses those six and | ||
| `CANCEL`, which the caller can send while it's still collecting. | ||
|
|
||
| ## The side that bids | ||
|
|
||
| ```java | ||
| @DialogueHandler(performatives = Performative.CFP) | ||
| public void handleCFP(DialogueMessage msg) { | ||
| Task task; | ||
| try { | ||
| task = msg.contentAs(Task.class); | ||
| } catch (IllegalArgumentException e) { | ||
| task = null; | ||
| } | ||
| if (task == null || task.type() == null) { | ||
| dialogue.refuse(msg, "Invalid task"); | ||
| return; | ||
| } | ||
|
|
||
| double cost = task.complexity() / efficiency * (0.9 + random.nextDouble() * 0.2); | ||
| int time = (int) (task.complexity() / 10 / efficiency); | ||
|
mauro-mura marked this conversation as resolved.
|
||
| dialogue.propose(msg, new Bid(cost, time)); | ||
| } | ||
| ``` | ||
|
|
||
| That annotation registers the handler, and there's no other registration code. What the method | ||
| does with the call is your own code, and here it's two lines of arithmetic: the worker prices a | ||
| job by its own efficiency, with a bit of noise on top. The rule that decides what a job is worth | ||
| to an agent goes in the same place, written as ordinary Java. | ||
|
|
||
| ## The side that chooses | ||
|
|
||
| ```java | ||
| List<DialogueMessage> proposals = responses.stream() | ||
| .filter(r -> r.performative() == Performative.PROPOSE) | ||
| .toList(); | ||
|
|
||
| if (proposals.isEmpty()) { | ||
| return CompletableFuture.completedFuture("NO_PROPOSALS"); | ||
| } | ||
|
|
||
| DialogueMessage best = proposals.stream() | ||
| .min(Comparator.comparingDouble(p -> p.contentAs(Bid.class).cost())) | ||
| .orElseThrow(); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| dialogue.reply(best, Performative.AGREE, task); | ||
|
mauro-mura marked this conversation as resolved.
|
||
| ``` | ||
|
|
||
| `responses` comes from `callForProposals(workerIds, task, Duration.ofSeconds(10))`, a future that | ||
| completes with whatever arrived before the timeout. A worker that's down contributes nothing and | ||
| the caller goes on without it. | ||
|
|
||
| Running the example gives three offers and one winner: | ||
|
|
||
| ``` | ||
| [Manager] Broadcasting CFP to 3 workers | ||
| [worker-1] PROPOSE: cost=179.16 | ||
| [worker-3] PROPOSE: cost=235.86 | ||
| [worker-2] PROPOSE: cost=102.97 | ||
| [Manager] Proposals: | ||
| - worker-2: cost=102.97, time=11s | ||
| - worker-1: cost=179.16, time=16s | ||
| - worker-3: cost=235.86, time=25s | ||
| [Manager] Selected: worker-2 | ||
| [worker-2] Task done! | ||
| ``` | ||
|
|
||
| The numbers move between runs, because the bid has a random factor in it. The ranking stays the | ||
| same: the three workers have fixed efficiencies, so `worker-2` wins every time. | ||
|
|
||
| ## What the framework contributes | ||
|
|
||
| The protocol is a state machine, written out in `ContractNetProtocol`. A `PROPOSE` or a `REFUSE` | ||
| leaves the caller in `AWAITING_RESPONSE`, still collecting. An `AGREE` moves it to `AGREED`. An | ||
| `INFORM` closes it at `COMPLETED` and a `FAILURE` at `FAILED`. | ||
|
|
||
| It also contributes the rule for who does the work after an `AGREE`: under Contract Net it's the | ||
| agent that made the offer, and Agenor asks `Protocol.senderPerforms` because the answer changes | ||
| from one protocol to the next. | ||
|
|
||
| ## Where it stops | ||
|
|
||
| A message that's out of sequence for the current state, like a `PROPOSE` after the conversation | ||
| is already `AGREED`, doesn't block anything: it still enters the history and the state machine | ||
| still runs its transition, which for these protocols leaves the state unchanged. The only effect | ||
| is a WARN log line naming the conversation, the sender and the state. Throwing an exception there | ||
| would have broken every peer already out there that doesn't follow the protocol to the letter. | ||
| The practical result, for a peer that ignores the rules, is the same it would get with no check | ||
| at all: only that log line changes. | ||
|
|
||
| Conversation state lives in memory, in each agent's own process. There's no central registry of | ||
| conversations. Every agent keeps only the ones it takes part in. | ||
|
|
||
| A restart wipes that state. The agent loses the conversations it was tracking and the commitments | ||
| it had recorded, and the peer gets no notification. What's left is the timeout the peer armed | ||
| when it called, and that timeout is the failover mechanism. So don't build anything durable on a | ||
| `Conversation`. If a business process has to survive a restart, keep it in the agent's own | ||
| persistent state (`@Persist`), and use dialogue to carry the steps. | ||
|
|
||
| There's a serialisation trap left. `contentAs()` converts the message's content, which is why | ||
| the example calls it instead of casting `msg.content()` to `Task`: that cast works only as long | ||
| as every agent shares a JVM, but put a transport underneath that serialises, and the payload | ||
| arrives as a `Map`, so the cast throws. | ||
|
|
||
| ## Where a model belongs | ||
|
|
||
| A model belongs where the decision doesn't fit into a cost function: reading a job that arrived | ||
| as prose, or weighing offers that don't reduce to a single number. An agent can call a model for | ||
| that decision, inside the same handler. The example above gets from a call for proposals to a | ||
| finished job without calling a model. | ||
|
|
||
| The code above is `ContractNetExample`, in the examples module at tag [`v0.35.0`](https://github.com/mauro-mura/agenor/tree/v0.35.0). | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.