@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
151 lines • 6.99 kB
JavaScript
/**
* The symbol the `@AiTool` decorator stamps its options onto (the tool class), read back by
* discovery to register the class into the {@link import('./tool-registry.js').ToolRegistry}. A
* global-registry symbol (`Symbol.for`) so it survives duplicate copies of this package in a tree.
*/
export const AI_TOOL_META_KEY = Symbol.for('@agora/agent:ai-tool-meta');
/**
* Brand stamped on the object {@link defineTool} returns so discovery picks it up when it walks a
* module's exports — the functional alternative to an `@AiTool` class.
*/
export const AGENT_TOOL_BRAND = Symbol.for('@agora/agent:functional-tool');
/** `GetWeatherTool` → `getWeather`; `SQLQueryTool` → `sqlQuery`. */
export function toolNameFromClass(className) {
const base = className.replace(/Tool$/, '') || className;
const leadingCaps = /^[A-Z]+(?=[A-Z][a-z]|$)/.exec(base)?.[0];
if (leadingCaps !== undefined && leadingCaps.length > 1) {
return leadingCaps.toLowerCase() + base.slice(leadingCaps.length);
}
return base.charAt(0).toLowerCase() + base.slice(1);
}
/** Apply the `name` / `kind` defaults, the name taken from `className`. */
function resolveMeta(options, className) {
return {
...options,
name: options.name ?? toolNameFromClass(className),
kind: options.kind ?? 'read',
};
}
/**
* Marks a class as an AI tool. The class must implement `execute(input, ctx)`. The provider's
* `app/agent_tools` discovery (or the generated `hooks/tools` barrel) registers every `@AiTool`
* class into the shared `ToolRegistry` at boot.
*
* ```ts
* @AiTool({ name: 'getWeather', kind: 'read', description: '...', input: z.object({ city: z.string() }) })
* export default class GetWeatherTool implements ToolHandler<{ city: string }> {
* async execute(input: { city: string }, ctx: AiToolCtx) { return { tempC: 21 } }
* }
* ```
*/
export function AiTool(options) {
return (target) => {
Object.defineProperty(target, AI_TOOL_META_KEY, {
value: resolveMeta(options, target.name ?? ''),
enumerable: false,
configurable: true,
});
return target;
};
}
/** Read the `@AiTool` decorator's stamped {@link AI_TOOL_META_KEY} metadata off a value, if present. */
function decoratorMeta(target) {
if (target === null || (typeof target !== 'function' && typeof target !== 'object')) {
return undefined;
}
return target[AI_TOOL_META_KEY];
}
/** Read a class's tool metadata off its `static tool = { name, kind, … }` config, if any. */
function staticToolMeta(target) {
if (typeof target !== 'function')
return undefined;
const config = target.tool;
if (config && typeof config === 'object' && typeof config.description === 'string') {
// `kind` may come from the config itself (BaseTool / @AiTool) or from the kind-specific base's static
// (ReadTool → 'read', ActionTool → 'action'), which keeps it out of the subclass's `static tool`.
const kind = config.kind ?? target.kind;
return resolveMeta({ ...config, ...(kind !== undefined ? { kind } : {}) }, target.name ?? '');
}
return undefined;
}
/** Resolve either authoring mechanism — `@AiTool` decorator or `static tool` — from one value. */
function metaOn(target) {
return decoratorMeta(target) ?? staticToolMeta(target);
}
/**
* Read a tool class's {@link AiToolMeta}. Two authoring forms are supported:
*
* 1. The `@AiTool({ … })` decorator (stamps {@link AI_TOOL_META_KEY}).
* 2. A decorator-free `static tool = { name, kind, description, input, … }` config on the class — the
* same shape, mirroring `@adonis-agora/durable`'s `static workflow`. Preferred where you'd rather
* not rely on decorators.
*
* Accepts either the class itself or an instance: each mechanism is tried on the value, then on its
* constructor (so an instance resolves the class's metadata).
*/
export function readAiToolMeta(target) {
if (target === null || (typeof target !== 'function' && typeof target !== 'object')) {
return undefined;
}
const ctor = target.constructor;
return metaOn(target) ?? metaOn(ctor);
}
/** Narrows an arbitrary module export to a branded functional tool for boot-time registration. */
export function isBrandedFunctionalTool(value) {
return (typeof value === 'object' &&
value !== null &&
AGENT_TOOL_BRAND in value &&
'spec' in value &&
'handler' in value);
}
export function defineTool(options, given) {
const execute = given ?? options;
if (typeof execute !== 'function' && typeof execute.execute !== 'function') {
throw new TypeError('defineTool requires an execute function');
}
const spec = {
name: options.name,
kind: options.kind ?? 'read',
description: options.description,
inputSchema: options.input,
...(options.roles !== undefined ? { roles: options.roles } : {}),
...(options.ability !== undefined ? { ability: options.ability } : {}),
...(options.enabled !== undefined ? { enabled: options.enabled } : {}),
...(options.presentation !== undefined ? { presentation: options.presentation } : {}),
...(options.replacementKey !== undefined ? { replacementKey: options.replacementKey } : {}),
...(options.terminal === true ? { terminal: true } : {}),
};
// In the single-object form this is the handler method, not a second options gate.
const canUse = given === undefined ? undefined : options.canUse?.bind(options);
const implementation = typeof execute === 'function' ? undefined : execute;
return {
[AGENT_TOOL_BRAND]: true,
spec,
handler: {
...(implementation?.present !== undefined
? { present: (output, ctx) => implementation.present(output, ctx) }
: {}),
...(implementation?.preflight !== undefined
? {
preflight: (input, ctx, options) => implementation.preflight(input, ctx, options),
}
: {}),
...(implementation?.isEnabled !== undefined
? { isEnabled: () => implementation.isEnabled() }
: {}),
...(implementation?.describe !== undefined
? { describe: (scope) => implementation.describe(scope) }
: {}),
execute: (input, ctx) => Promise.resolve(typeof execute === 'function'
? execute(input, ctx)
: execute.execute(input, ctx)),
...(canUse !== undefined || implementation?.canUse !== undefined
? {
canUse: async (actor) => (canUse === undefined || (await canUse(actor))) &&
(implementation?.canUse === undefined || (await implementation.canUse(actor))),
}
: {}),
},
};
}
//# sourceMappingURL=ai-tool-ref.js.map