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

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