Shared state and trust
Agents that work on the same problem can share a board instead of passing messages: SharedBlackboard holds typed entries that any agent can post, reply to and vote on. TrustRegistry keeps a per-capability score for each agent, and TrustBasedRouter uses those scores to pick who gets the next task.
Shared blackboard
SharedBlackboard lives in com.tnsai.communication (in tnsai-core). Create one with new SharedBlackboard() and hand the same instance to every agent that should see it. It is thread-safe and in-memory only: there is no persistence, TTL or eviction, and clear() removes every entry and vote.
Entries
Every entry has an id, an author, an EntryType, a title, an optional content object, a timestamp, an optional parent id, metadata and a priority.
| Group | EntryType values |
|---|---|
| Findings | ISSUE_FOUND, OPPORTUNITY, RESEARCH_FINDING |
| Proposals | DESIGN_PROPOSAL, CODE_PROPOSAL, TEST_PROPOSAL |
| Work | WORK_IN_PROGRESS, WORK_COMPLETED, WORK_BLOCKED |
| Discussion | CONCERN, APPROVAL, QUESTION, ANSWER |
| Knowledge | FACT, ASSUMPTION |
| Outcomes | DECISION, CONSENSUS, ACTION_ITEM |
post(authorId, type, title, content) creates an entry and returns it. For priority or metadata, build the entry with Entry.builder() and pass it to post(entry). The builder requires authorId, type and title and throws IllegalStateException without them. Priority defaults to 5 and is clamped to 1–10. addSpec(key, value) adds metadata, which you read back with getSpec() or getSpecValue(key).
reply(authorId, parentId, type, title, content) posts an entry whose parent is another entry. The board does not check that the parent exists.
Votes and consensus
vote(entryId, voterId, voteType) records APPROVE, REJECT or ABSTAIN. Voting on an unknown entry throws IllegalArgumentException. Each voter has one vote per entry, so voting again replaces the earlier vote.
getVoteResult(entryId) returns a VoteResult with approvals, rejections, abstentions and voters:
total()counts all three kinds.approvalRatio()is approvals divided by approvals plus rejections. Abstentions are left out, and with no decided votes the ratio is 0.isPassing(threshold)compares the ratio with the threshold.
hasConsensus(entryId, minVotes, threshold) is true when total() reaches minVotes and the approval ratio reaches threshold. Abstentions count toward minVotes but not toward the ratio.
Votes are tallies only. They do not change an entry or post anything, so a proposal stays pending until some agent replies with a decision.
package com.example.tnsai.docs;
import com.tnsai.communication.SharedBlackboard;
import com.tnsai.communication.SharedBlackboard.Entry;
import com.tnsai.communication.SharedBlackboard.EntryType;
import com.tnsai.communication.SharedBlackboard.VoteType;
import java.util.List;
import java.util.Map;
/** Compilation contract for multi-agent/shared-state.md shared blackboard. */
public final class SharedStateExample {
private SharedStateExample() {
}
static List<Entry> review() {
SharedBlackboard board = new SharedBlackboard();
board.addTypeListener(EntryType.CONCERN, entry ->
System.out.println("Concern: " + entry.getTitle()));
Entry proposal = board.post(Entry.builder()
.authorId("architect")
.type(EntryType.DESIGN_PROPOSAL)
.title("Cache user profiles")
.content("Add a read-through cache in front of UserService")
.priority(8)
.addSpec("component", "UserService")
.build());
board.reply("security", proposal.getId(), EntryType.CONCERN,
"Cache must not hold tokens", Map.of("severity", "high"));
board.vote(proposal.getId(), "security", VoteType.APPROVE);
board.vote(proposal.getId(), "backend", VoteType.APPROVE);
board.vote(proposal.getId(), "qa", VoteType.ABSTAIN);
SharedBlackboard.VoteResult votes = board.getVoteResult(proposal.getId());
System.out.println(votes); // Approve: 2, Reject: 0, Abstain: 1 (100% approval)
if (board.hasConsensus(proposal.getId(), 3, 0.66)) {
board.reply("architect", proposal.getId(), EntryType.DECISION,
"Approved: cache user profiles", votes.toString());
}
return board.getPendingProposals(); // empty once the DECISION reply exists
}
}Queries
| Method | Returns |
|---|---|
getAllEntries(), getRecent(n) | Every entry, or the last n, in posting order |
getById(id) | Optional<Entry> |
getByType(type), getByAuthor(authorId) | Entries of one type or by one author |
getReplies(entryId) | Direct replies, sorted by timestamp |
find(predicate) | Entries matching a predicate |
getPendingProposals() | Design, code and test proposals with no DECISION or APPROVAL reply |
getOpenQuestions() | QUESTION entries with no ANSWER reply |
getConcerns(entryId) | CONCERN replies to an entry |
getSummary() | A text summary: counts per type, pending proposals and open questions |
Listeners
addListener(Consumer<Entry>) is called for every new entry, and addTypeListener(type, listener) only for entries of one type. Listeners run on the posting thread, after the entry is stored. An exception in a listener is logged and does not stop the other listeners. Remove them with removeListener and removeTypeListener.
Trust scores
TrustRegistry in com.tnsai.coordination.trust keeps one score between 0 and 1 for each agent and capability pair. You report each outcome yourself; the registry does not watch agents.
TrustRegistry.create(strategy, initialScore) creates a registry, and create(strategy, initialScore, decayRate) adds decay. recordOutcome(agentId, capability, outcome, quality) updates the score. Outcome is SUCCESS, PARTIAL or FAILURE. The strategy first turns the outcome into an observed value:
| Outcome | Observed value |
|---|---|
SUCCESS | quality |
PARTIAL | quality * 0.5 |
FAILURE | quality * 0.1, at least 0 |
TrustUpdateStrategy | New score |
|---|---|
ema(alpha) | current * (1 - alpha) + observed * alpha |
bayesian() | Weighted average of the current score and the observation, where the current score weighs max(1, interactions - 1) |
weightedAverage() | Running mean of all observations: current + (observed - current) / interactions |
The result is clamped to 0–1. TrustUpdateStrategy is a functional interface, so a lambda (current, outcome, quality, totalInteractions) -> newScore works as a custom strategy.
Reading scores:
getTrust(agentId, capability)returns the score, or the initial score for a pair with no recorded outcome.getTrustScore(agentId, capability)returns anOptional<TrustScore>withgetScore(),getTotalInteractions(),getSuccessfulInteractions(),getSuccessRate()andgetLastUpdated(). OnlySUCCESScounts as successful.rankAgents(capability, topN)andgetAgentsAboveThreshold(capability, threshold)return agent ids, highest score first. Both only see agents with at least one recorded outcome for that capability.
applyDecay() moves every score toward 0.5 by the decay rate: 0.5 + (score - 0.5) * (1 - decayRate). Nothing calls it for you; schedule it yourself. With a decay rate of 0 it does nothing.
Trust-based routing
TrustBasedRouter.builder() takes a registry (required) and a minimumTrust (default 0). selectAgent(capability, candidateIds) returns the candidate with the highest trust at or above the minimum, and rankCandidates(capability, candidateIds) returns all such candidates, highest first.
The router reads trust through getTrust, so a candidate with no history scores the registry's initial score. Set minimumTrust above the initial score to keep untried agents out.
package com.example.tnsai.docs;
import com.tnsai.coordination.trust.Outcome;
import com.tnsai.coordination.trust.TrustBasedRouter;
import com.tnsai.coordination.trust.TrustRegistry;
import com.tnsai.coordination.trust.TrustUpdateStrategy;
import java.util.List;
import java.util.Optional;
/** Compilation contract for multi-agent/shared-state.md trust-based routing. */
public final class TrustRoutingExample {
private TrustRoutingExample() {
}
static Optional<String> route() {
TrustRegistry registry = TrustRegistry.create(
TrustUpdateStrategy.ema(0.3), // alpha
0.5, // initial score
0.02); // decay rate per applyDecay() call
registry.recordOutcome("agent-a", "translation", Outcome.SUCCESS, 0.95);
registry.recordOutcome("agent-a", "translation", Outcome.PARTIAL, 0.60);
registry.recordOutcome("agent-b", "translation", Outcome.FAILURE, 0.0);
System.out.println(registry.getTrust("agent-a", "translation"));
registry.getTrustScore("agent-a", "translation").ifPresent(score ->
System.out.println(score.getSuccessRate() + " over " + score.getTotalInteractions()));
List<String> top = registry.rankAgents("translation", 3);
System.out.println("Most trusted: " + top);
TrustBasedRouter router = TrustBasedRouter.builder()
.registry(registry)
.minimumTrust(0.5)
.build();
// agent-a ~0.53, agent-b 0.35, agent-c (no history) 0.5 -> agent-a
return router.selectAgent("translation", List.of("agent-a", "agent-b", "agent-c"));
}
}