Skip to content
tnsaiDocs
GitHub

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.

GroupEntryType values
FindingsISSUE_FOUND, OPPORTUNITY, RESEARCH_FINDING
ProposalsDESIGN_PROPOSAL, CODE_PROPOSAL, TEST_PROPOSAL
WorkWORK_IN_PROGRESS, WORK_COMPLETED, WORK_BLOCKED
DiscussionCONCERN, APPROVAL, QUESTION, ANSWER
KnowledgeFACT, ASSUMPTION
OutcomesDECISION, 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

MethodReturns
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:

OutcomeObserved value
SUCCESSquality
PARTIALquality * 0.5
FAILUREquality * 0.1, at least 0
TrustUpdateStrategyNew 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 an Optional<TrustScore> with getScore(), getTotalInteractions(), getSuccessfulInteractions(), getSuccessRate() and getLastUpdated(). Only SUCCESS counts as successful.
  • rankAgents(capability, topN) and getAgentsAboveThreshold(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"));
    }
}