How the AI system works
Rover and the role subagents — where they run, how they reach school data, and the difference between a tool and a skill.
Staff talk to Rover in the Education Hub. They type "create a Science department at Lugazi" or "who's in my S2 stream?", and something on the backend does the work. That something is not a chatbot bolted onto the side of the app. It is a LangGraph agent sitting on top of the same services the REST API uses — with one extra rule that keeps it safe: the model never touches the database.
Read this section in order — each page is the input to the next. It is the architecture only. The how-tos live in Adding AI Tools under Platform Guides, and they assume you have already met every helper they name.
Who is in the room
Rover is the supervisor. Three role subagents sit behind it. Rover decides whether to answer itself or to hand the turn to a specialist via the task tool.
| You say | Wire id | Persona folder | When they appear |
|---|---|---|---|
| Rover | rover | rover/ | Always — the supervisor |
| Angie | class_teacher | angie/ | Caller is a class (form) teacher |
| Bean | subject_teacher | bean/ | Caller is a subject teacher |
| Sarah | dos | sarah/ | Caller holds director-of-studies authority |
The teaching pair is resolved from real assignment rows (ClassXTeacher / SubjectXTeacher). Sarah is permission-derived — the same authority the HTTP approveReport route requires — not a teaching assignment. A caller with none of those capabilities talks to Rover alone; the extra graphs are not even registered that turn.
Wire ids and persona folders are two vocabularies on purpose
The stream and the task tool speak class_teacher. Skill files live under angie/. WIRE_ID_TO_SKILL_PERSONA is the only map between them. When you add a skill, you pick the persona folder. When you tag a tool, you pick the wire id.
Two processes, one seam
The agent does not run inside the HTTP API. The worker (apps/worker) hosts the graph. The server (apps/server) authorizes and executes anything that reads or writes school data. They talk over RabbitMQ — the same request/response transport as Queues & Messaging, on the agent-tool queue.
Education Hub --SSE stream--> worker (Rover / Angie / Bean / Sarah)
|
| RPC: event + { identity, scope, args }
v
server (AgentToolsController)
|
v
domain service (the same one REST uses)That split is the security model. The language model runs in the worker. It can only name a tool and fill in arguments. Identity, tenant, and permission are minted by the server at dispatch and checked again on every tool call. The model cannot widen its own access.
Tools vs skills
Two different things, easy to mix up:
- A tool is a function the model may call.
list_staff,create_department,get_current_date. It has a name, a description, an input schema, and an implementation. - A skill is a playbook — a
SKILL.mdthe model reads when the work matches its description. "How to write an NCDC scheme of work." "How to orient on a class before answering." Skills do not run code. They tell the agent which tools to use, in what order, with which constraints.
Stuffing procedures into the always-on system prompt wastes context. Skills are progressive disclosure: the model sees the skill's description, and only opens the file when the task needs it.
There are also two kinds of tool. That choice is the first decision on any "give Rover X" ticket:
| RPC tool | Local tool | |
|---|---|---|
| Touches school data? | Yes | No |
| Runs | Server, over RabbitMQ | Inside the worker |
| Auth, tenant, audit | Yes — that is why it exists | None of those apply |
| Lives | libs/shared/src/naalya-ai/tools/ | apps/worker/src/naalya-agent/core/tools/local/ |
Choosing RPC or local is the decision, and it opens the how-tos. If it is RPC, the rest of this section is the contract (what crosses the wire), the definition, the registry, then the hop (every worker and server helper, in call order). The add-tool page is only the file list.
Where the code lives
libs/shared/src/naalya-ai/
agent-tools.contract.ts identity, run mode, request/result envelopes
tools/ defineAgentTool, RPC_AGENT_TOOLS, toolsForMode
agent-map.ts buildAgentMap, eligibleAgentRoles
apps/server/src/app/naalya-ai/
agent-tools.controller.ts execute(), postures, @MessagePattern handlers
tools/tool-scope.ts inTenantScope, runInToolScope, auditToolWrite
apps/worker/src/naalya-agent/
core/rover.graph.ts Rover's createDeepAgent call
core/tools/ toRpcProxy, AgentToolService, local/ registry
core/skills/ playbooks: general/ rover/ angie/ bean/ sarah/
core/backends/ skillSourcesFor, SKILLS_PERMISSIONS
core/subagents/ selectSubagents, Angie / Bean / Sarah graphsYou almost never edit the worker when you add an RPC tool. The proxy in build-agent-tools.ts is generic — one body for every definition. Registration is data: spread the definition into RPC_AGENT_TOOLS, and the worker picks it up.
Read in this order
What happens on a chat turn
One staff message: context minted, graph built, tool called, reply streamed.
The shared contract
Runtime context, identity, call scope, and the request/result envelope.
Defining a tool
defineAgentTool's eight fields — naming, visibility, placement, schema rules.
The tool registry
RPC_AGENT_TOOLS, the derived types, and the agent map.
The RPC hop
toRpcProxy through execute(), both postures, tenant helpers, audit.