What this example shows
Superfriends is a financial research hall built on AgentSpaces: an analyst asks about a ticker, a chair sends the question to a dozen specialist services, their findings land in one knowledge space, three analysts of three model vendors vote, exactly one paper trade fills, and a dossier is written from what the space holds. It runs as six processes on one machine, three of them different application frameworks with different model vendors, one of them Clojure, one of them started late on purpose. Without an API key it runs offline on fixtures and its six-process fleet test runs in CI; with a key, that vendor goes live and nothing else changes.
This guide walks the example as a reader of the developer guide, the workflow guide and the architecture guide would: each chapter names the mechanism, shows how the hall uses it, and points at the chapter that defines it. The point of the example is a single one, repeated: because the fabric carries discovery, replication, the auction, the vote, the ordered log and the running total, every service is a few dozen lines that take a request, make one call and write what they found. The power is in the spaces, so the services can be simple.
agentspaces-example-apps/superfriends: README.md is the overview, CONTRACT.md the build contract every peer follows (ports, spaces, records, each service's exact output), PLAN.md the design and its phases. Chapter 11 maps the modules.The fleet
Six processes join one group. The group is founded from a literal, superfriends-v1, so every member derives the same group id locally and any member could seed; the planner is the seed only because the demo starts it first. Each peer binds its own port (7801 to 7806) and honours SUPERFRIENDS_BIND and SUPERFRIENDS_SEEDS, which is how the fleet test and a demo run side by side. Security is the dev-local profile: signed frames, session-keyed channels, no TLS, which is what lets a Clojure process and a plain-Java one join the same room as the Spring Boot and Micronaut peers.
| Peer | Framework and library | Vendor (served model) | Carries |
|---|---|---|---|
planner | Spring Boot, the starter, Spring AI, the console | none: it calls the fleet | the chair: the join, the proposal, the estimate, the decision, the position book, the ledger, the console |
grok | plain Java, agentspaces-runtime and agentspaces-langchain4j | xAI, grok-4 | x-search, social-sentiment, buzz; an analyst |
gpt | Spring Boot, the starter and agentspaces-springai | OpenAI, gpt-5 | filings, fundamentals, options-chain, chart; an analyst; the vectors asset; a desk executor |
claude | Micronaut, its LangChain4j and MCP bridges | Anthropic, claude-opus-4-6 | entity-lookup, document-digest, report, deck; an analyst; a desk executor; MCP import and export |
search | plain Java | none | web-search; started late |
graph | Clojure, agentspaces-clj and DataScript | none | the graph asset and the analyst UI; no agent at all |
The frameworks are a seating choice. The same annotations bind through the plain-Java runtime, the Spring starter and the Micronaut processor, and the Clojure peer joins with no agent, subscribes to spaces and serves an asset. Nothing in the flow names a framework, and a service could move from one peer to another by moving its class.
Twelve spaces, three rooms
Every peer registers all twelve spaces, in one order, from Hall.SPACES. A space is a lane: it has one conflict strategy, one settle window, one retention policy through its leases, and one reason for existing, and every stage inherits those by naming the space. Two spaces step off the default lease race for a reason the workload proved. tasks is an AUCTION with a one-second settle window: every taker bids its vendor's token price, and the window has to outlast gossip so every bidder's claim arrives before the incumbent's take returns; at 200 ms two peers ran the same task. orders stands on the ordered log: a paper trade fills once or the position book lies.
Every hall write carries three tags, inquiry, ticker and service, inside the signed record, so templates select on them before decoding, the chair's join keys on the inquiry id, and the console's data map reads like a case file. Leases are the retention: knowledge and reports keep thirty days, signals and orders an hour, a task request an hour. A finding always has at least one source and an about list that names the ticker.
Space entries; the hall's are in the example's wire module.Cards and the catalogue
Agent cards
A service is a class with an @AgentSpec and one cue annotation, and the binder derives its card: the name, what it consumes (the request record, as a schema name with its version) and what it produces. The chair never holds a list of services. It asks discovery for every card whose consumes names a request type it knows how to write, and that is the plan's input. The twelve services each do one thing: take a request, ask the graph what is already known, make one model call, write findings with sources and tags, return a ServiceDone.
// agentspaces-example-apps/superfriends/grok -- XSearch: the whole service @AgentSpec(name = "x-search", description = "Finds recent posts on X about a ticker, cashtag or account") public final class XSearch { @BidFunction(space = Hall.TASKS) public double bid(Object request) { return Bids.bid("grok", request); } @SpaceTake(space = Hall.TASKS, lease = "5m", resultSpace = Hall.KNOWLEDGE, resultLease = "30d") public ServiceDone search(XSearchRequest request) { // one model call (or Grok's live search with a key), one finding per post, each with its source ... return new ServiceDone(inquiryId, ticker, "x-search", count, now); } }
The bid function is the only other method, and it is one line: the peer's token price for the request. That is what makes the auction route a task that two peers offer, social sentiment on both grok and gpt, to the cheaper one while it is up and to the other when it is not, with no failover code anywhere.
Asset cards
An asset is a card too, published by the connector runtime for a provider that answers queries: the vector store on gpt and the graph on the Clojure peer. A client on any peer writes a DataQuery into the data space naming the asset and the query; the one peer serving that asset takes it, answers, and writes a DataResult leased for the card's freshness. Identical queries across the fleet cost one execution, and every peer reads the result from its replica. The graph's freshness is five seconds, since its queries are in memory and a longer result lease would hand a repeat inquiry the empty coverage answer of its predecessor.
Role cards
The analysts and the desk are cards as well. Each vendor peer binds one analyst whose only method is a @Ballot, so the electorate of a vote is whoever holds an analyst card. Each desk peer binds a volunteer card first, and the ordered log's member set is read from those cards at startup rather than configured (chapter 7). A peer that starts late is in the catalogue within a card cadence of about two seconds, which chapter 8 turns into the demo's set piece.
AgentCard, AssetCard, DiscoveryService, ConnectorRuntime, AssetProvider and DataQueryClient in the javadocs; the @BidFunction and route by cost.One inquiry, end to end
An Inquiry entry starts it, from the analyst UI, the console, the inquirer program or the test. No stage calls the next: each writes the record the next one reacts to, and the inquiry id on every record is the thread.
| Stage | Mechanism | Chapter |
|---|---|---|
The chair reacts to the Inquiry, asks the graph's coverage what the hall already knows about the ticker, skips the durable services whose knowledge is newer than the inquiry's maxAgeMinutes (never the perishable ones: posts, sentiment, buzz), writes one request per remaining service to tasks and an EvidenceExpected naming them to knowledge | a reaction that maps one record into several; the catalogue as the plan's input | fan-out |
Each service takes its request under the auction, asks the graph first, makes one model call, writes its findings, entities and relations with sources and tags, and completes with a ServiceDone; sentiment also contributes its score to a push-sum epoch | a take; a tagged result; a contribution | sequence, aggregate |
A @SpaceJoin on knowledge fires once per inquiry when every expected ServiceDone is in, the count read from the chair's own EvidenceExpected; the settled sentiment estimate is an optional part; the join is LEASED, once fleet-wide under a ticket | gather by a declared count | join |
A @Propose opens position:<inquiryId> with buy, sell and hold at a quorum of three; each vendor's @Ballot reads the inquiry's findings and the graph's timeline and contradictions, asks its own model for a thesis, writes the Thesis and casts once | decide | decide |
@OnDecision returns the approval, or opens a leased window for a person's; the approval's reaction returns the TradeOrder; three executors race under @OrderedTake; exactly one Fill commits; a @SpaceReduce folds it into the PositionBook | decide; a deadline as a leased entry; exactly once; a running total | exactly once, time |
The fill, or the window's lapse, commissions the chart and the dossier; the dossier's evidence, theses, decision, position, sentiment and sources sections are generated from the space; the dossier's reaction commissions the deck and writes the InquiryClosed line | compose from the record of the flow | compose |
The chair's three annotations are the whole orchestration. There is no engine and no state machine; the state is the spaces, which is why a second inquiry about the same ticker runs only the perishable services, and why a crashed peer's half-done task simply reappears for a sibling when its lease lapses.
// CONTRACT.md section 7 -- the chair's join and proposal, as the planner binds them @SpaceJoin(space = "knowledge", key = "inquiryId", mode = LEASED, settle = "5s", resultSpace = "inquiries", parts = {@Part(EvidenceExpected.class), @Part(value = ServiceDone.class, countedBy = "EvidenceExpected.services"), @Part(value = SentimentEstimate.class, optional = true)}) public EvidenceJoined joined(Joined j) { ... } @Propose(space = "inquiries", vote = "votes", prefix = "position:", key = "inquiryId", options = {"buy", "sell", "hold"}, quorum = 3, lease = "30m") public String open(EvidenceJoined e) { return "ticker=" + e.ticker() + ";inquiry=" + e.inquiryId() + ...; }
@SpaceJoin, @Propose, @OrderedTake, @SpaceReduce in the javadocs).The common room
Model access is a fleet service. Each vendor peer runs a model server that takes ModelRequest entries naming its model and a model client that writes them, so the planner, which has no vendor of its own, calls Grok, GPT and Claude through the fleet, and a service on grok whose own model is scripted still gets a live answer from a neighbour that has a key. Chat memory rides the conversations space as snapshots, which is how a conversation survives a peer being killed and resumes on another framework; usage rides the usage space, which is why the console's usage panel shows three vendors in one table.
Without a key a peer serves a scripted model of its own framework. The scripted models answer by request shape rather than by prompt text: every prompt a service sends starts with a marker line naming the task and the ticker, a real model ignores it, and the scripted one answers from the fixture for that ticker. That is what lets the quorum split, the join fire and the trade fill at level 0, deterministically, in CI.
agentspaces-model-wire; the Spring AI chapter's model access; the workflow guide's models and planners.Knowledge and its two assets
Everything a service finds goes into one replicated space, tagged and sourced, and two asset peers index it differently. On gpt, a @SpaceNotify on knowledge adds each finding and entity to a vector store, served as the vectors asset for similarity. On the Clojure peer, a subscription with a replay of what the space holds turns every entry into DataScript facts with provenance (the entry id, the issuer, the as-of time), an expired entry retracts its facts, and the graph asset answers six named queries: facts, neighbors, path, timeline, coverage and contradictions.
Both are reached the same way, a DataQuery in the data space, through one small client in common. Its two uses are the whole of RAG in this example: Recall.context gathers what the hall knows before a model call, on every vendor, and Recall.freshServices reads the graph's coverage before the chair plans. claude's report quotes a finding grok wrote because both read the same space, not because anyone passed a document around.
graph/README.md for the schema, the queries and the UI's routes; DataQueryClient in the javadocs.Decide, then trade exactly once
The vote is three methods on three peers. The chair's @Propose opens one proposal per inquiry; each analyst's @Ballot casts once, counted at the authorizer's granularity, one vendor one vote; the chair's @OnDecision acts once when the tally reaches the quorum. The personas decide the split: Grok reads the street, GPT reads the numbers, Claude reads the risk factors, so one fixture ticker splits buy, buy, hold and the other hold, sell, sell. A quorum proves that three analysts agreed, and nothing more, which the README says plainly.
The trade is the ordered log, and the log's member set forms from cards. Each desk peer binds a volunteer first, then calls the rendezvous, which waits for the desk cards and forms the log's quorum from the peers they name; only then does the peer bind its executor. Three executors race every TradeOrder under @OrderedTake, the log commits exactly one claim, and the fill records the commit index that paid it. A crashed executor repeats nothing, and a @SpaceReduce folds every fill into the PositionBook any peer reads. The operator's gate sits between the decision and the order, and it is a reaction, not a wait: the decision returns the approval itself when the operator is simulated, or writes a leased DeskWindow and returns nothing when a person must approve; an Approval entry, from the console's command or the analyst UI's button, is what the order reacts to, once per decision; the fill, or the lapse of the window, is what commissions the dossier, so it carries the position when there is one and is written without it rather than never.
Sensing sits beside deciding. Every sentiment score contributes to a push-sum epoch per inquiry; the planner's @OnEstimate writes the settled estimate into knowledge, where the chair's join takes it as an optional part. Sense, then decide.
VoteCapability, PushSumAggregate, VoteClient and AggregateClient in the javadocs.The newcomer
Start the fleet, open an inquiry, and while it runs start the search peer with --join-search. It dials the seed, its web-search card replicates within a card cadence, and the next inquiry's plan lists it: the dossier cites a web finding that did not exist when the session began. Nothing was redeployed and nothing was reconfigured, because the chair plans from live cards. The same mechanism covers the reverse: a stopped peer's card lingers for its TTL, and a peer that lost its seed retries its last endpoints, backing off to thirty seconds, so the launch order does not decide whether the fleet forms.
ROBUSTNESS.md and the developer guide's peering chapter.Two front ends
The analyst UI is a single-page ClojureScript app the graph peer serves itself (port 7893). It mirrors the peer's DataScript database into the browser, one full fetch and then every transaction over Server-Sent Events, and lets an analyst open inquiries like a chat, watch each one's progress service by service, read the dossiers, walk the timeline, the entities and the graph, and query the store in Datalog. Opening an inquiry or approving a decision is a plain space write from the page; the peer stays an asset peer with no agent and no console.
The fleet console on the planner (port 7890) is the library's read-only console with the demo's three commands added: open an inquiry, ask the hall (a graph query shown as rows), and approve, which is the human gate. Its standard panels are the take ledger, the data map, the cards, the usage panel with all three vendors, and the wire-traffic panel. Neither front end needed a line of instrumentation in any service: both are reads of replicated state.
FleetConsoleServer, TrafficPanel); the analyst UI's routes and build in graph/README.md.Levels, keys, and the test
| Level | What is live | How |
|---|---|---|
| 0 | nothing | scripted models on every peer, fixtures about two fictional tickers; the six-process fleet test runs here |
| 1 | one vendor | set one of XAI_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY; that peer serves its real model, the others stay scripted and call it through the common room; with the xAI key, x-search asks X itself through Grok's live search and cites the posts |
| 2 | all vendors | all three keys |
| 3 | all vendors and data | plus the MCP servers' keys; the candidate servers are listed in PLAN.md section 7.3 and are verified before adoption |
// agentspaces-example-apps/superfriends -- running it ./run-demo.sh # build, start six peers, run one inquiry, print the dossier, stop ./run-demo.sh --watch # the same, then keep the fleet up: console at http://localhost:7890/ ./run-demo.sh --join-search # also start the late-joining web-search peer before the inquiry ./run-demo.sh --ticker NIMBUS --question "Is the CFO departure a reason to sell?" XAI_API_KEY=... OPENAI_API_KEY=... ANTHROPIC_API_KEY=... ./run-demo.sh # live, one key at a time is fine mvn verify -pl fleet-test -Dsuperfriends.fleet=true # the six-process scenario test
The fleet test launches the six processes, waits for every card and both assets to be visible on every peer, runs an inquiry, and asserts each stage from the spaces: the expected count of findings with sources, the split tally, one fill for two executors, the position book, the dossier's sections, the ledger line; then a second inquiry that runs only the perishable services, and a search peer joined mid-session that the next plan includes. The dossier and the peer logs land under out/.
graph/README.md.The code, module by module
| Module | What it holds | Read it for |
|---|---|---|
wire/ | the hall's records and the Hall constants; no dependencies | the data every peer agrees on: spaces, tags, prefixes, service names |
common/ | fixtures, the scripted model, tags, bids, the desk rendezvous, the asset client, the recall helpers, the cards helper | the shared pieces every peer reuses instead of rewriting |
grok/ | the plain-Java peer: the fleet runtime, LangChain4j, three services, an analyst, Grok's live search | a peer built from the core API with no framework |
gpt/ | the Spring Boot peer: four services, an analyst, the vector-store asset, a desk executor | the starter and Spring AI in use, including an asset served by configuration |
claude/ | the Micronaut peer: four services, an analyst, a desk executor, MCP import and export | the Micronaut processor and bridges; the outside door |
planner/ | the chair, the decision desk, the position book, the ledger, the console commands; an Embabel variant under -Pembabel | the orchestration that is three annotations |
search/ | the newcomer peer: one service | the smallest possible peer |
graph/ | the Clojure peer: the DataScript store, the asset, the analyst UI | a member that is pure state; a front end over a replica |
fleet-test/ | the six-process scenario test, the inquirer, the traffic audits | how a cross-framework fleet is tested, and measured |
The contract is the thing to read before changing a peer: it fixes the ports, the twelve spaces and their strategies, the tags and leases, each service's exact output at level 0, the assets' queries, the chair's annotations and the desk's formation, so that six modules written against three frameworks produce one coherent hall.
What to read next
- The workflow guide: every pattern this hall uses, with its options and trade-offs, and chapter 14, which is this example in that guide's terms.
- The developer guide: the annotations, the Space API, discovery, the capabilities, the console, and the framework chapters for each of the three peers.
- The architecture guide: why the fleet is shaped this way, where each space stands on the ladder, and the architect's checklist the example satisfies.
- The core javadocs and the Spring AI javadocs.
- The example itself:
agentspaces-example-apps/superfriends, starting with its README, thenCONTRACT.md.
PLAN.md is still only a plan: the inquiry budget and its budget-exhausted finding, an electorate narrowed to analyst cards, a reader for the usage space beyond the console's panel, the mcp-calls space (registered, used only when MCP import is on), and the MCP servers of section 7.3, which are candidates until verified.