@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
100 lines • 3.87 kB
JavaScript
/** First filter layer: drop tools the actor's role may not invoke. `can` may be async (authz). */
export async function filterToolsByRole(tools, actor, policy) {
const checked = await Promise.all(tools.map(async (tool) => ({
tool,
allowed: await (policy.canOffer ? policy.canOffer(actor, tool) : policy.can(actor, tool)),
})));
return checked.filter((entry) => entry.allowed).map((entry) => entry.tool);
}
/** Second filter layer: if the persona pins an allow-list, keep only those tool names. */
export function personaFilterTools(tools, allowedTools) {
if (allowedTools === undefined) {
return tools;
}
const allowed = new Set(allowedTools);
return tools.filter((tool) => allowed.has(tool.name));
}
/**
* The narrower of two tool allow-lists. `undefined` is "no restriction", so it yields the other; two
* lists yield the names on both, in the order of the first.
*/
export function intersectAllowLists(first, second) {
if (first === undefined) {
return second === undefined ? undefined : [...second];
}
if (second === undefined) {
return [...first];
}
const allowed = new Set(second);
return first.filter((name) => allowed.has(name));
}
/** The persona `id` of `definition`, or `undefined` when it declares none by that id. */
export function findPersona(definition, id) {
if (id === undefined) {
return undefined;
}
return definition?.personas?.find((persona) => persona.id === id);
}
/**
* The persona an agent runs under when nothing names one: its `defaultPersona`, else the persona
* whose id is `'default'` when it declares one. Undefined → none.
*/
export function defaultPersonaOf(definition) {
if (definition?.defaultPersona !== undefined) {
return definition.defaultPersona;
}
return findPersona(definition, 'default')?.id;
}
/** A persona as a picker reads it — never its prompt or its allow-list. */
export function personaCatalogEntry(persona) {
return {
id: persona.id,
label: persona.label,
...(persona.description !== undefined ? { description: persona.description } : {}),
};
}
/**
* Resolve `name` through the {@link Persona.aliases} of `definitions`: the agent and persona that
* answer for it, or `undefined` when no persona claims it. A name that IS a registered agent is never
* an alias — the caller checks that first, so a real agent always wins.
*/
export function resolvePersonaAlias(definitions, name) {
for (const definition of definitions) {
for (const persona of definition.personas ?? []) {
if (persona.aliases?.includes(name) === true) {
return { agent: definition.name, persona: persona.id };
}
}
}
return undefined;
}
/**
* The persona as the turn's prompt builders and tools see it: the frozen definition, never the
* config's — its prompt is the resolved text, so a reader of `ctx.persona.systemPrompt` sees what the
* turn actually ran on.
*/
export function personaFromTurn(turn) {
return {
id: turn.id,
label: turn.label,
...(turn.allowedTools !== undefined ? { allowedTools: [...turn.allowedTools] } : {}),
...(turn.prompt !== undefined ? { systemPrompt: turn.prompt } : {}),
};
}
/**
* A send named a persona its agent does not declare. The routes answer `400` with
* `code: 'persona_not_found'` — before a thread or a run is created.
*/
export class PersonaNotFoundError extends Error {
agentName;
persona;
status = 400;
code = 'persona_not_found';
constructor(agentName, persona) {
super(`agent "${agentName}" has no persona "${persona}"`);
this.agentName = agentName;
this.persona = persona;
this.name = 'PersonaNotFoundError';
}
}
//# sourceMappingURL=personas.js.map