@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
292 lines • 13.9 kB
JavaScript
/**
* Skills: an authored procedure the model pulls in when a task calls for it, instead of every
* instruction living in the system prompt.
*
* WHAT A SKILL IS NOT: an agent. An agent is WHO is answering — its persona, its tools, its history
* window, its output schema. A skill is HOW one particular task is done, and any agent may pull one
* in. That is why a skill carries no model, no tool list and no schema: the moment it did, the two
* would be the same thing wearing different names, and a consumer would have to choose between them
* for reasons nobody could state.
*
* WHAT IT COSTS THE PROMPT: one line per skill. The catalog block below carries names, scopes and
* descriptions only; a BODY reaches the model as a tool result, on the transcript, where the
* `HistoryWindow` ceiling already governs it. So skills are not a fourth thing competing for the
* system block with the agent's own prompt, its persona and injected retrieval — see
* `AgentLoopDeps.skills` for how they compose.
*/
/** The scope token every deployment has: skills nobody narrowed. */
export const GLOBAL_SCOPE = 'global';
/** The token for one actor's own skills. */
export function actorScope(actor) {
return `actor:${actor.id}`;
}
/** The token for a tenant's skills — `Actor.tenantRef` is the only tenant key this library knows. */
export function tenantScope(tenantRef) {
return `tenant:${tenantRef}`;
}
/**
* The scopes derivable from an {@link Actor} alone — the common case, so a consumer wiring skills
* for the first time supplies no resolver at all: the actor's own, their tenant's (when they have
* one), and the deployment's. A host adding an axis of its own replaces this rather than extending
* it, since the ORDER is the precedence and only the host knows where its axis belongs.
*/
export const defaultScopeResolver = {
resolve: ({ actor }) => [
actorScope(actor),
...(actor.tenantRef !== undefined ? [tenantScope(actor.tenantRef)] : []),
GLOBAL_SCOPE,
],
};
/** A source of skills that is a fixed list — module config, or a host's own constant. */
export function staticSkillProvider(skills) {
return {
list: ({ scopes }) => skills.filter((skill) => scopes.includes(skill.scope)),
load: ({ name, scope }) => skills.find((skill) => skill.name === name && skill.scope === scope)?.body ?? null,
};
}
/**
* Read several sources as one. The order is the tie-break and nothing else: precedence between
* skills is by SCOPE, so two sources only ever compete when they publish the same name at the same
* scope, and then the earlier source wins. Wire the host's own provider first — a row it can edit is
* a better answer than a body that needs a deploy to change.
*/
export function compositeSkillProvider(providers) {
return {
list: async ({ scopes, ctx }) => {
const seen = new Set();
const merged = [];
for (const provider of providers) {
for (const summary of await provider.list({ scopes, ctx })) {
const key = `${summary.scope}