class

Arcana::Directory

Inherits Reference < Object

Registry of agent and service capabilities.

Address format:

  • A routing label — any single token matching [a-z][a-z0-9-]*, or an owner:capability-style two-token form (both halves matching the same pattern). Colons in the address are just a naming convention now, not a type marker.
  • Kind (agent vs service) and capability (chat/image/tts/...) are explicit fields on the listing. Legacy callers that don't set them get the old behavior: kind is derived from the address (colon = service), and capability is the substring after the colon.
  • Internal ephemeral mailboxes (_reply:<id>) are carved out by a leading underscore and are neither agents nor services.

Constants

AGENT_HANDLE_PATTERN = /\A@[a-z][a-z0-9-]*\z/

Agent handles use the leading @ sigil so a process representing a conversational identity ("@mj", "@arcana") doesn't collide with a same-project tool service registered at the bare name ("mj", "arcana"). Enforced at register time: kind=agent addresses MUST start with @, and kind=service addresses MUST NOT. The bus still routes purely on the address string — the sigil is a naming convention that the registration path enforces so senders can tell at a glance who's an agent.

NAME_PATTERN = /\A[a-z][a-z0-9-]*\z/

Token body: what an address (or each half of a colon-form address) must match after any leading sigil is stripped.

Class methods

agent?(address : String) : Bool

Is this address an agent? (Plain name, no colon.)

Source
handle?(address : String) : Bool

Does this address use the @ agent-handle sigil?

Source
migrate_legacy_address(address : String) : String | Nil

Migrate a pre-0.14 address to the new format. Returns nil for addresses that can't be sensibly mapped — callers should skip them with a warning rather than fabricate a bogus owner.

"memo:agent" → "memo" "chat:openai:service" → "openai:chat" (owner-first reorder) "memo:service" → nil (no owner in old form — drop) "owner:cap" / "foo" → unchanged

The owner-less ":service" form was a degenerate case from the original two-token convention. Rewriting it to "<name>:legacy" (as 0.14.0 originally did) preserved the entry but introduced a made-up owner that misleads readers; cleaner to drop and let the registrant re-register with a proper owner:capability.

TODO: remove this whole helper in 0.16 once no downstream consumers carry pre-0.14 snapshots.

Source
owner(address : String) : String | Nil

Return the owner half of a service address, or nil if not a service.

Source
service?(address : String) : Bool

Is this address a service? (Contains : and is not an internal ephemeral.)

Source
validate_address(address : String) : Nil

Validate address format. Raises if malformed. Accepts:

  • foo — bare single token
  • @foo — agent-handle single token (leading @ sigil)
  • owner:capability — two-token colon form (@ not allowed here)
  • _reply:<hex> — internal ephemeral, exempt from validation
Source

Instance methods

agent_ttl

How long an agent listing lives without activity before prune_stale_agents drops it. When set, agent listings report expires_at (last seen + TTL). The pruner still takes its TTL as an argument; set both to the same value.

Source
agent_ttl=(agent_ttl : Time::Span | Nil)

How long an agent listing lives without activity before prune_stale_agents drops it. When set, agent listings report expires_at (last seen + TTL). The pruner still takes its TTL as an argument; set both to the same value.

Source
busy?(address : String) : Bool

Check if an address is currently busy.

Source
by_kind(kind : Kind) : Array(Listing)

Filter listings by kind.

Source
by_owner(owner : String) : Array(Listing)

Listings provided by a given owner (service address prefix).

Source
by_tag(tag : String) : Array(Listing)

Filter listings by tag.

Source
check_owner!(address : String, owner_token : String | Nil) : Nil

Raises HeldError unless owner_token matches the owner token the address is held under. Addresses held without one accept anything.

Source
claim(listing : Listing, owner_token : String | Nil = nil) : Claim

Register listing, or refresh it when owner_token shows the caller already holds the address. This is how an agent that forgot it registered (or whose session hook registered for it) tells "that's me" from "someone else has it".

  • Free address: registered, under owner_token if one is given.
  • Held under the same owner token: the listing is refreshed from listing (fields it leaves out keep their values), marked online, and its last-seen time updated.
  • Held without an owner token: taken over under owner_token. An unowned listing protects nothing, so taking it gives nothing away.
  • Otherwise raises: HeldError when another owner token holds it, Error ("Address already registered") when neither side has one.
Source
events

Optional event recorder. When set, material directory actions (register, unregister, busy changes, prune) emit events.

Source
events=(events : Events::Backend | Nil)

Optional event recorder. When set, material directory actions (register, unregister, busy changes, prune) emit events.

Source
last_seen(address : String) : Time | Nil

Get the last-seen timestamp for an address.

Source
list

List all registered listings.

Source
load(path : String) : Int32

Load listings from a JSON file. Skips addresses already registered (so built-in services registered in code take precedence). Tolerates pre-0.14 address formats and rewrites them in-place.

Source
lookup(address : String) : Listing | Nil

Look up a listing by address. Returns nil if not found.

Source
offline_addresses

Addresses currently marked offline (for snapshot save).

Source
online?(address : String) : Bool
Source
owner_tokens

Owner tokens by address (for snapshot save). Never serialized in directory listings.

Source
prune_stale_agents(ttl : Time::Span) : Array(String)

Remove agent listings older than ttl. Services are never pruned (they get re-registered from code on each startup anyway). Returns the list of pruned addresses.

The walk collects candidates under the mutex, releases it, then re-acquires briefly per-address for each delete. Each delete re-verifies staleness — a re-registration between the walk and the delete is respected. Keeps every mutex acquisition O(1), so registration/lookup can slip through even during a large prune sweep.

Source
register(listing : Listing)

Register a listing. Raises if the address is malformed, already taken, or its sigil doesn't match its kind.

Source
retag(address : String, tags : Array(String)) : Nil

Replace the tags on an existing listing. No-op if unregistered. Toolset uses this at start time to union user-provided tags with the names of its registered tools.

Source
save(path : String)

Save all listings to a JSON file.

Source
search(query : String) : Array(Listing)

Search listings by substring match on name, description, or tags.

Source
set_busy(address : String, busy : Bool = true)

Mark an address as busy or idle.

Source
set_last_seen(address : String, time : Time)

Set the last-seen timestamp directly (used by snapshot restore).

Source
set_online(address : String, online : Bool) : Nil

Mark a listing online or offline. Offline means no session is running for it; its mailbox keeps collecting mail. claim marks it online again.

Source
set_owner_token(address : String, token : String) : Nil

Restore an owner token (for snapshot load).

Source
to_json(listings : Array(Listing)) : String

Serialize a list of listings with busy status.

Source
to_json(listing : Listing) : String

Serialize a single listing with busy status.

Source
to_json

Summarize the directory as JSON — useful for injecting into agent prompts.

Source
touch(address : String)

Refresh the last-seen timestamp for an address. No-op if unregistered.

Source
unregister(address : String)

Remove a listing by address. Idempotent — does nothing if unregistered.

Source

Nested types