Naalya Handbook
AI System

The tool registry

RPC_AGENT_TOOLS is the only registration — the types derived from it, the mode and role filters, and the agent map the product reads.

One as const array is the entire registration surface. No enum, no worker list, no second file to update.

libs/shared/src/naalya-ai/tools/index.ts
const RPC_AGENT_TOOLS = [
  ...staffTools,
  ...campusTools,
  ...departmentTools,
  // …every domain array
] as const

Derived types

type AnyAgentTool = (typeof RPC_AGENT_TOOLS)[number]
type AgentToolEvent = AnyAgentTool['event']
// 'list.staff' | 'create.department' | …

type AgentToolPayload = {
  [T in AnyAgentTool as T['event']]: AgentToolRequest<z.input<T['schema']>>
}

AgentToolPayload uses each tool's Zod input type, not z.infer's output — the payload crosses RMQ as JSON, so args is what a caller sends, before defaults and transforms. That matches AgentToolCaller.call and execute().

Type a handler payload as AgentToolPayload[typeof createDepartmentTool.event], never a hand-written interface. Change the schema and the handler breaks at compile time.

These types only work because defineAgentTool preserved each literal event and concrete schema.

Mode and role filters

How a definition becomes "on Rover's graph" or "on Angie's graph" without anyone editing a worker array.

const toolsForMode = (mode: AgentRunModeName): ReadonlyArray<AnyAgentTool> => {
  const allowed = TOOL_SCOPES_BY_MODE[mode]
  return RPC_AGENT_TOOLS.filter((definition) =>
    allowed.includes(definition.scope),
  )
}

const toolsForRole = (
  role: AgentToolRole,
  mode: AgentRunModeName,
): ReadonlyArray<AnyAgentTool> => {
  const allowed = TOOL_SCOPES_BY_MODE[mode]
  return RPC_AGENT_TOOLS.filter(
    (definition) =>
      (definition.shared || definition.roles?.includes(role)) &&
      allowed.includes(definition.scope),
  )
}
HelperReturnsCalled by
toolsForMode(mode)Every RPC tool whose scope is allowed in that run mode. Filtered again afterwards to drop tools with a non-empty roles array.The supervisor, via buildAgentTools
toolsForRole(role, mode)Tools listing that role or marked shared, still narrowed by run mode. Not a partition — a tool may list several roles.Angie / Bean / Sarah, via buildRoleTools

Both wrappers then pass each definition through toRpcProxy. You never call these yourself when adding a tool — tagging the definition is enough.

The agent map

The same registry is how the product describes who has which tool. libs/shared/src/naalya-ai/agent-map.ts backs both the HTTP registry endpoint and the worker-local get_agent_map tool, so the chat roster and the profile list cannot drift.

HelperWhat it does
ROLE_CAPABILITIESMaps class_teacherisClassTeacher, and so on — the single source of truth for eligibility. Key order is the delegate roster's order, and it reaches the supervisor prompt, so reordering is a behaviour change.
eligibleAgentRoles(capabilities)Which of the three roles this caller actually holds. Used by the registry and by the worker's selectSubagents.
scopeAgentMap(map, capabilities)Supervisor (no availability.capability) plus each eligible subagent. Filters; never rebuilds.
buildAgentMap(mode?)Fresh map from toolsForMode / toolsForRole. Default mode is school (the tool superset); 'campus' narrows each agent to what that surface reaches. Excludes worker-local tools — they never cross the RPC seam this map derives from, so get_agent_map splices them into its own copy.
filterAgentMapByAgent(map, agentRef)Resolves "Angie" or "class_teacher", case-insensitively. On a miss, returns every valid id/persona pair so the caller can compose a model-readable error.
resolveSearchScope(map, agentRef?)Whole map, one agent, or zero agents on a bad ref — never throws.
searchAgentTools(map, query, agentRef?)Keyword against name/event/description, grouped by tool (keyed by event), every holder listed once. Empty query → { tools: [] }, not an error.

You do not edit this file when you add a tool — buildAgentMap already reads RPC_AGENT_TOOLS. You do add a ROLE_CAPABILITIES row (and a worker SUBAGENT_FACTORIES entry) if you invent a fourth role, which needs a product reason.

AgentToolCaller

libs/shared/src/naalya-ai/naalya-ai.types.ts
type AgentToolCaller = {
  call<E extends AgentToolEvent>(
    event: E,
    payload: AgentToolPayload[E],
  ): Promise<AgentToolResult>
}

The only surface the proxy needs from the RPC transport. Structural, not a Nest interface, so the graph loads without decorator metadata (LangGraph Studio runs graphs through tsx). The worker's AgentToolService is the production implementation — see the hop.

Where to go next

On this page