# 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.

<!-- java-contract: src/main/java/com/example/tnsai/docs/SharedStateExample.java -->
```java
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 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.

<!-- java-contract: src/main/java/com/example/tnsai/docs/TrustRoutingExample.java -->
```java
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"));
    }
}
```

## Related

- [Fan-out and agent pools](fan-out.md)
- [Council and voting](council.md)
- [Messaging](messaging.md)
- [Topology selection](choose-topology.md)
