aspace://intro/layer-3

Coordination without a coordinator.

AgentSpaces is a peer-to-peer fabric. It gives you a replicated tuple space and a framework for coordination services on top of it. Peers join groups, advertise what they can do, find each other by capability, and trade work, information, and decisions through shared, leased entries. No broker sits in the middle.

You write a plain class. One annotation turns a method into a fleet worker: its parameter type says what it takes, and its return value is the next entry. The same annotations work in plain Java, in Spring Boot, and alongside Spring AI and Embabel.

  • Takes under a lease. One worker wins each order fleet-wide. If that worker crashes, the lease lapses and the order comes back for another.
  • Reacts without consuming. Every auditor sees every shipment once, even when the network delivers it twice.
  • Signs and replicates. Every entry is signed by its writer and copied to every peer in the group.
  • Publishes a card. The agent's name and the types it consumes and produces go out as a leased, signed AgentCard.
orders-fleet · agents
// examples/example-11-quickstart -- Quickstart.Fulfiller
@AgentSpec(name = "fulfiller",
        description = "Ships orders from the shared space",
        goals = {"fulfill orders"})
public static class Fulfiller {

    @SpaceRef
    private Space space;     // the group's sole space, injected

    @SpaceTake(lease = "10m", pollTimeout = "300ms")
    public Shipment ship(Order order) {
        space.write(new Progress(order.orderId(),
                        "picking " + order.item()),
                Lease.of(Duration.ofMinutes(5)));
        return new Shipment(order.orderId(), order.item(),
                "fulfiller");
    }
}
✓ binds on startup · worker loop and subscription running · AgentCard published

the formal versionAgentSpaces provides a decentralized, cryptographically verifiable coordination fabric in which autonomous peers discover capabilities and coordinate information, work, and decisions through leased, signed, replicated tuple spaces, while permitting each workload to select the minimum consistency and coordination strength required by its mission.

aspace://intro/lineage

Thirty years of protocol evolution, in one fabric.

Brought to you by Bad Monkey, whose cofounders have spent the past 30 years leading advancements in telecom, event processing systems, ontology implementations, and lightweight enterprise software development using Java, Spring, Clojure, Python, and more.

As Bell Labs alumni, we reviewed key advancements in protocol and signaling technology, from early P2P frameworks like Jini and JXTA to modern distributed systems.

AgentSpaces resolves their historical limitations by combining the 30 years of protocol evolution we have worked through into a modern, developer-friendly architecture.

layers 0–2 · security and networking

Flexible security and networking

Pluggable discovery, relay, and rendezvous, with traditional TLS alongside end-to-end encryption (similar to Signal).

Every message is signed CBOR. Payload encryption uses the primitives Signal uses: Ed25519, X25519, HKDF, and AES-256-GCM. Peers behind NAT connect through relay and rendezvous peers.

layer 3 · the space

Simple coordination

Easy-to-use replicated tuple spaces backed by robust coordination APIs.

Write, read, take, and watch typed entries. Every entry carries a lease, so anything nobody renews goes away on its own. Take is exclusive: if the taker dies, the entry reappears.

layers 1 and 4 · the fabric

Extensible P2P fabric

Powered by an optimized gossip algorithm, with modular capabilities for Raft consensus, quorum building, voting, and more.

The first set of capabilities: bidding on tasks, voting in the group, reacting to decisions, exactly-once work through a Raft log, fleet-wide aggregates, gossip learning, semantic discovery, and key distribution.

aspace://intro/strength

Pick the weakest guarantee that does the job.

Each workload chooses how strongly it coordinates. Stronger guarantees cost more latency and less availability, so the cost is listed with each step. Use several spaces the way you would use several queues.

step 1 · eventual

@SpaceNotify / @SpaceTake

Plain choreography and the LEASE_RACE take: the first claim wins after one settle window.

costsOne settle window (200 ms by default). A partition can cause brief duplicate work.
step 2 · priced

@BidFunction

An AUCTION space: every take carries a bid, and the cheapest agent wins the entry.

costsA few gossip rounds, plus a bid function you write.
step 3 · agreed

@Ballot / @OnDecision

QUORUM voting: distinct, signed voters, one ballot each, and one reaction when the vote closes.

costsWaits until enough distinct voters have cast a ballot.
step 4 · ordered

@OrderedTake

Takes committed through a Raft log. Each entry completes exactly once, in one total order.

costsNeeds a majority quorum: a minority partition cannot take at all.
aspace://layer/0-identity
Layer 0

Identity: every record says who wrote it.

A peer is an Ed25519 keypair, and its PeerId is the hash of the public key. The identity stays the same across every address and transport change, and the peer signs every frame, card, and entry it sends.

  • Self-certifying groups. A GroupId is the hash of a founder-signed founding document. A newcomer who knows only the id can check any seed's answer against it, so a hostile seed can delay a join but can't substitute a different group.
  • Per-agent keys, when you need them. By default the peer signs for its agents (PEER_ASSERTED). For an audit trail that must hold this agent to a finding, the peer certifies a key of the agent's own (AGENT_ATTESTED).
  • Encryption from well-known primitives. Entry payloads are sealed with AES-256-GCM under a group content key. New members receive that key sealed to their own X25519 key (X25519 + HKDF + AES-256-GCM), so it never crosses the wire in the clear. These are the same building blocks Signal uses.
  • Annotations stay unchanged. Switch on per-agent keys and @SpaceRef fields arrive as that agent's signed view. The agent code itself doesn't change.

Guide: Agent identity and certificates

example-13 · signed agents
// examples/example-13-signed-agents -- SignedAgents
@AgentSpec(name = "auditor", description = "Audits releases", goals = {"audit"})
public static final class Auditor {
    @SpaceRef(FINDINGS)
    Space findings;               // arrives as this agent's own signed view

    public void conclude(String subject, String verdict) {
        findings.write(new Finding(subject, verdict), FINDING_LEASE);
    }
}

// startAnnotatedPeer(...): the starter does exactly this from
// agentspaces.identity.agent-keys=subordinate.
AgentSpaces spaces = new AgentSpaces(identity, InstantSource.system(), identity::subordinate);
AgentSpaces.GroupContext group = spaces.register("signed-agents", joined.runtime().id(),
        joined.runtime(), null);
group.space(FINDINGS, findings);
AgentBinder.Bound auditorBound = group.bind(auditor);
AgentBinder.Bound clerkBound = group.bind(clerk);

// ledger(...) and describe(...): who said it, and how strongly
reader.findings().readAllIssued(Template.of(Finding.class), 100);
issued.issuer().encoded() + " (" + issued.attestation() + ")";
aspace://layer/1-peering
Layer 1

Peering: join a group, stay a member by answering.

Membership is SWIM-style probing with a lease: a member that goes silent drops out of the view on its own. State travels on two gossip channels, rumor for speed and anti-entropy for completeness, in signed CBOR envelopes.

  • Rendezvous. Any member can volunteer as a RENDEZVOUS peer, which holds a bigger discovery cache and is the one address newcomers need to know. Example 06 runs two sites that know only the rendezvous address.
  • Relay. A RELAY peer forwards signed frames unchanged for peers behind NAT. It can't alter them, because the signature covers the bytes. Correctness never depends on either role.
  • Admission. Groups admit members by policy: OPEN, INVITE (with a founder-signed credential), or POLICY (your own validator).
  • Transports. In-JVM loopback for tests, TCP, TLS 1.3, and QUIC. A peer advertises its endpoints in priority order and dialers try them in turn. On a LAN, an opt-in multicast beacon replaces seed addresses entirely.

Guide: Peering, identity, and transports

example-06 · wan rendezvous
// examples/example-06-wan-rendezvous -- WanFleet.startPeer(...)
PeerIdentity identity = PeerIdentity.generate();
PeerNode node = PeerNode.builder(identity)
        .roles(rendezvous ? Set.of(PeerAdvertisement.PeerRole.RENDEZVOUS) : Set.of())
        .build();
node.listen(new TcpTransport(), HOST + ":" + port);
List<PeerAdvertisement.Endpoint> seeds = seedPort == 0 ? List.of()
        : List.of(new PeerAdvertisement.Endpoint("tcp", HOST + ":" + seedPort, 0));
GroupRuntime runtime = node.joinGroup(group(),
        new GroupMembership.Config(Duration.ofSeconds(30), Duration.ofSeconds(2), 2),
        seeds);
CborCodec codec = CborCodec.defaultCodec();
DiscoveryService discovery = new DiscoveryService(runtime,
        new AdCache(codec, InstantSource.system()), codec, identity.peerId());
ReplicatedSpace tasks = ReplicatedSpace.builder(runtime, "site-tasks", identity, name)
        .settleWindow(Duration.ofMillis(150))
        .build();
node.startTicking(Duration.ofMillis(250));
aspace://layer/2-discovery
Layer 2

Discovery: who can do what, as signed cards.

Everything you can discover is a typed, signed card in a per-group cache that gossip keeps fresh. When a card's issuer stops republishing it, the card ages out. Agents publish their cards just by being bound.

  • @AgentSpec names the agent and describes it for humans and for semantic search. The binder reads the consumed and produced types from the method signatures and puts them on the card too.
  • @SpaceAgent is the Spring version: a @Component that is also an @AgentSpec. Component scanning finds it, and the starter enrolls it in the fleet. You can compose your own stereotypes the same way.
  • find scans the local cache in memory, so call it as often as you like. remoteFind asks rendezvous peers first and then uses scoped gossip. It blocks, so keep it off hot paths.
  • Most fleets never look anything up. Work reaches its worker because the worker is taking that entry type. Discovery is for knowing who is out there: consoles, planners, and dispatchers that should fail loudly when nobody is listening.

Guide: Discovery and capabilities · @SpaceAgent

discovery · cards
// examples/example-03-discovery-cards -- CardsFleet
// Each card carries the name, description, and goals from @AgentSpec, and the
// consumed and produced types from the method signature.
@AgentSpec(name = "summarizer", description = "Summarizes documents", goals = {"summarize"})
public static class Summarizer {
    @SpaceTake(space = "work", pollTimeout = "PT0.3S")
    public Summary summarize(SummaryTask task) {
        return new Summary(task.document(), "summary of " + task.document());
    }
}

@AgentSpec(name = "translator", description = "Translates text", goals = {"translate"})
public static class Translator {
    @SpaceTake(space = "work", pollTimeout = "PT0.3S")
    public Translation translate(TranslateTask task) {
        return new Translation(task.text(), task.language(),
                "[" + task.language() + "] " + task.text());
    }
}
aspace://layer/3-space
Layer 3

Space: shared memory that forgets on purpose.

A replicated tuple space built as a delta-CRDT. Reads run against the local replica and never wait on the network. Every entry carries a lease. A take is exclusive under the space's conflict strategy, and the taker must complete before its take lease lapses, or the entry comes back.

  • @SpaceTake for work that should happen exactly once per entry. It retries until the work succeeds: a crash, a kill, or an exception lets the lease lapse, and the entry comes back.
  • @SpaceNotify for reactions every interested agent should see. It never retries: a reaction that throws is logged and dropped. Put work that must happen behind a take.
  • @SpaceRef when one trigger produces several entries, or entries in other spaces. Combine it with Spring's @Scheduled for a periodic agent.
  • No Spring needed. Create an AgentBinder, register spaces, and call bind. The starter automates the same three lines.

Strategies are chosen per space: LEASE_RACE (the default) or AUCTION. Exactly-once uses the ordered-log capability in Layer 4.

Guide: The space API · Annotation conventions

space · work
// examples/example-11-quickstart -- Quickstart.startPeer(...): the same
// annotations, without Spring.
ReplicatedSpace space = ReplicatedSpace.builder(runtime, "work", identity, "host")
        .settleWindow(Duration.ofMillis(150))
        .build();
AgentBinder binder = new AgentBinder(identity, runtime.id(), null,
        InstantSource.system());
binder.space("work", space);
node.startTicking(Duration.ofMillis(250));

// Quickstart.main
workerPeer.binder().bind(new Fulfiller());
auditorPeer.binder().bind(new Auditor());
aspace://layer/4-capabilities
Layer 4

Capabilities: fleet protocols as one method each.

Capabilities are services built on the same fabric. Each is advertised with a signed card, discovered like any other, and reached through a typed client. The first set covers bidding, voting, reacting to decisions, exactly-once work, and fleet-wide aggregates. Each annotation handles the part that every hand-written version got wrong at least once.

  • @BidFunction prices work on an AUCTION space, and the lowest bid wins. Bids are ordinary code, so they can be denominated in dollars, tokens, or queue depth, and even learned.
  • @Ballot casts one ballot per proposal, or abstains by returning null. The proposal itself triggers the method, so a ballot can never arrive for a proposal the voter hasn't seen.
  • @OnDecision fires once per proposal when the tally reaches quorum, and its return value becomes the next entry.
  • @OrderedTake is @SpaceTake with the take routed through a Raft log, so each entry completes exactly once across the fleet.
  • @CapabilityRef injects a typed client (VoteClient, AggregateClient, SemanticClient). @ProvidesCapability advertises your own CapabilityProvider.
  • Aggregates without a tick. Return a Contribution from any bound method to feed a push-sum epoch, and read the fleet's estimate with awaitEstimate.

Guide: Capabilities inside agents · Taking through the ordered log

capabilities · fleet protocols
// agentspaces-partybus -- Planners.DiningPlanner
@AgentSpec(name = "dining-planner",
        description = "Plans a day around the table: markets, long lunches, a reserved dinner",
        goals = {"fill a day with food"})
public static final class DiningPlanner {
    @SpaceRef(Scouts.SPACE)
    Space research;

    @BidFunction(space = SLOTS)
    public double bid(DaySlot slot) {
        double bid = 90;
        if (interested(slot, "food", "wine", "eat", "cook")) {
            bid -= 35;
        }
        if (slot.pace().equals("REST")) {
            bid += 20;
        }
        return bid + slot.round() * 5; // a strike barely touches a lunch
    }

    @SpaceTake(space = SLOTS, lease = "1m", pollTimeout = "500ms")
    public ScheduledActivity plan(DaySlot slot) {
        String table = research.readAll(Template.of(RestaurantOptions.class)
                        .where("tripId", eq(slot.tripId())), 20).stream()
                .filter(r -> r.location().equalsIgnoreCase(slot.location()))
                .flatMap(r -> r.restaurants().stream()).map(r -> r.name()).findFirst()
                .orElse("a table the scouts are still finding");
        // ...builds the day's ScheduledActivity around that table
    }
}
aspace://intro/integrations

Spring Boot, Spring AI, and Embabel.

The annotations go on the classes you already write. The Spring Boot starter enrolls them from component scanning. Spring AI workers call their models inside an annotated method. Embabel agents publish cards to the fleet, and the fleet's cards come back to Embabel's planner as actions.

Spring AI

agentspaces-springai
  • A worker is a @SpaceAgent bean. Inject a ChatClient.Builder and call the model inside a @SpaceTake method. A crash mid-call lets the lease lapse, and another worker picks the task up.
  • Fleet tools. FleetTools turns the fleet's AgentCards into Spring AI tool callbacks, so a model's tool loop can call agents running on other machines.
  • Patterns built from the same annotations. A panel of model judges is a set of @Ballot agents. A token-priced auction is a @BidFunction over each model's price.
  • Spring AI's own extension points. The project also provides fleet-backed chat memory, a ChatModel served by the fleet, embeddings for semantic discovery, fleet-wide token usage, and MCP export. No Embabel dependency.

Read: agentspaces-springai README

spring ai · fleet agents
// agentspaces-springai/examples/springai-fleet -- SpringAiFleet.Summarizer
// A Spring AI worker on the fleet: a @SpaceAgent bean with an injected
// ChatClient.Builder and one @SpaceTake method.
@SpaceAgent(name = "summarizer", description = "Summarizes a topic into a short briefing",
        goals = {"summarize topics"})
public static class Summarizer {
    private final ChatClient chat;

    Summarizer(ChatClient.Builder builder) {
        this.chat = builder.build();
    }

    @SpaceTake(space = "work", lease = "2m")
    public Summary summarize(Brief brief) {
        return new Summary(brief.topic(),
                chat.prompt().user("Summarize: " + brief.topic()).call().content());
    }
}

Embabel

embabel-agentspaces
  • Outbound: your @Agent beans become cards. The description, the @AchievesGoal statements, and each @Action's input and output types are published as an AgentCard. Add @SpaceTake to an action and it becomes a fleet worker.
  • Inbound: the fleet becomes planner actions. EmbabelRemoteActions generates an @Agent with one typed @Action per remote card, so the GOAP planner can chain specialists it can't reach on its own.
  • Choreography, not RPC. Invoking a remote action writes an entry and waits for the matching result. If the worker crashes, the lease lapses, and whoever finishes delivers the result.
  • Kept current. Cards are leased, so the planner's action set follows the live fleet.

Guide: Embabel integration · The Spring Boot starter

embabel · planner and fleet
// agentspaces-partybus/src/embabel -- PartyBusEmbabelApplication
// The planner's own two actions. Everything between them is a remote fleet
// specialist that the bridge generated from an AgentCard.
@Agent(description = "Plans a trip with the Party Bus fleet's specialists")
public static class PartyBusPlannerAgent {

    @Action
    public SightseeingRequest askForSights(TravelBrief brief) {
        return new SightseeingRequest(brief.tripId(), "planner sights " + brief.to(),
                brief.to(), brief.brief(), brief.departureDate(), brief.returnDate(),
                PartyBus.party(brief.tripId()).contribution());
    }

    @Action
    public ReviewRequest askForReviewsOfTheTopSight(SightseeingOptions sights) {
        PointOfInterest top = sights.pointsOfInterest().isEmpty()
                ? new PointOfInterest("the old town", "", "", "", "")
                : sights.pointsOfInterest().get(0);
        return new ReviewRequest(sights.tripId(), "planner reviews " + top.name(),
                top.name(), top.location());
    }

    @AchievesGoal(description = "Know what travelers say about the trip's top sight")
    @Action
    public ReviewDigest done(ReviewDigest digest) {
        return digest;
    }
}
aspace://intro/annotations

All eleven annotations, by layer.

Every annotation goes on an ordinary object: no base class, no interface, no container. They all accept an optional group, an empty space resolves to the group's only space, and durations can be written as "10m" or "PT10M".

AnnotationOnWhat the binder doesKey attributes (defaults)
Layer 2 · Discovery
@AgentSpecclassNames and describes the agent. This becomes its signed AgentCard.name, description, goals[]
@SpaceAgentclassSpring stereotype: @Component + @AgentSpec. Enrolled by the starter.name, description, goals[]
Layer 3 · Space
@SpaceTakemethodTakes under a lease, then completes or lets the lease lapse. The return value is the result entry.space, lease (10m), pollTimeout (1s), resultSpace, resultLease (1h)
@SpaceNotifymethodReacts to every match without consuming it, filtering duplicate deliveries, on its own virtual thread. The return value is the next entry.space, lease (1h), resultSpace, resultLease (1h)
@SpaceReffieldInjects a Space handle, the agent's own signed view when it has its own key.value (space name)
Layer 4 · Capabilities
@BidFunctionmethodPrices an entry on an AUCTION space. The lowest bid wins.space
@BallotmethodCasts one ballot per proposal, as this agent. null abstains.space, prefix, lease (1h)
@OnDecisionmethodFires once per closed vote. The return value is the next entry.space, prefix, lease, resultSpace, resultLease (1h)
@OrderedTakemethodA take through the Raft log: exactly once, fleet-wide.space, lease (30s), pollTimeout (2s), resultSpace, resultLease
@CapabilityReffieldInjects a typed capability client. Binding fails immediately if none is available.group
@ProvidesCapabilityclassRegisters a CapabilityProvider and keeps its advertisement fresh.value (URI), group

Guide: Chapter 1, Annotations, which covers each annotation with where it belongs and what to keep in mind.

aspace://intro/polyglot

Four languages, one wire.

Nothing on the wire is Java-specific: CBOR framing (RFC 8949), Ed25519 signatures (RFC 8032), and URI names. The Python and TypeScript clients speak the protocol directly, and Clojure binds the JVM library. The Java, Python, and TypeScript codecs are checked against the same golden byte vectors, signatures included. The trilingual demo has a Java coordinator publish tasks while Python and TypeScript workers race for them: three runtimes, one space, no broker.

Java

The full stack and all eleven annotations. Spring Boot starter and Embabel adapter.

agentspaces-*

Clojure

Idiomatic bindings over the JVM library: maps in, maps out, keywords and duration strings.

agentspaces-clj

TypeScript

A wire-compatible peer written against Node's standard library alone. Joins, takes, writes, and reads back.

@agentspaces/client

Python

A wire-compatible peer that joins a running Java fleet over TCP and takes work under the same claim rules.

agentspaces-client
research-fleet · one space, four languages
// examples/example-04-auction -- AuctionFleet.MiniModelWorker
// Java agents are annotated POJOs: the binder owns the loop, the lease, and the bid.
@AgentSpec(name = "mini-model", description = "Cheap small-model worker")
public static class MiniModelWorker {
    @BidFunction(space = "model-tasks")
    public double bid(ModelTask task) {
        return 1.0 + task.difficulty() * 2.0;   // 3 .. 21
    }

    @SpaceTake(space = "model-tasks", pollTimeout = "PT0.3S")
    public ModelResult run(ModelTask task) {
        return new ModelResult(task.prompt(), task.difficulty(), "mini-model", bid(task));
    }
}
✓ one set of golden vectors for every codec · the Java coordinator prints each finding with the worker that produced it

The annotations are a JVM feature, available from Java and from any JVM language through the binder. The Python and TypeScript clients work at the wire level: join, take, complete, write, and read back.