ai-functions
Version:
Core AI primitives for building intelligent applications
617 lines • 21.7 kB
JavaScript
/**
* DigitalObjectsFunctionRegistry - Persistent function registry using digital-objects
*
* This implementation stores function definitions as Things and function calls as Actions,
* providing full persistence and audit trail capabilities.
*
* Nouns:
* - CodeFunction: Functions that generate executable code
* - GenerativeFunction: Functions that use AI to generate content
* - AgenticFunction: Functions that run in a loop with tools
* - HumanFunction: Functions that require human input/approval
*
* Verbs:
* - define: Function definition action
* - call: Function invocation action
* - complete: Successful completion action
* - fail: Failed execution action
*
* @packageDocumentation
*/
import { getLogger } from './logger.js';
/**
* Noun names for function types
*/
export const FUNCTION_NOUNS = {
CODE: 'CodeFunction',
GENERATIVE: 'GenerativeFunction',
AGENTIC: 'AgenticFunction',
HUMAN: 'HumanFunction',
};
/**
* Verb names for function actions
*/
export const FUNCTION_VERBS = {
DEFINE: 'define',
CALL: 'call',
COMPLETE: 'complete',
FAIL: 'fail',
};
/**
* Map function type to noun name
*/
function typeToNoun(type) {
switch (type) {
case 'code':
return FUNCTION_NOUNS.CODE;
case 'generative':
return FUNCTION_NOUNS.GENERATIVE;
case 'agentic':
return FUNCTION_NOUNS.AGENTIC;
case 'human':
return FUNCTION_NOUNS.HUMAN;
}
}
/**
* Convert a FunctionDefinition to storable data
*/
function definitionToData(definition) {
const base = {
name: definition.name,
type: definition.type,
...(definition.description !== undefined && { description: definition.description }),
args: definition.args,
...(definition.returnType !== undefined && { returnType: definition.returnType }),
};
switch (definition.type) {
case 'code': {
const codeDef = definition;
// A `handler` is a live function reference and cannot be persisted; only
// an inline `code` body round-trips. Definitions stored with a handler
// (no `code`) must be re-supplied with their handler when reloaded.
return {
...base,
...(codeDef.code !== undefined && { code: codeDef.code }),
...(codeDef.language !== undefined && { language: codeDef.language }),
...(codeDef.instructions !== undefined && { instructions: codeDef.instructions }),
};
}
case 'generative': {
const genDef = definition;
return {
...base,
...(genDef.output !== undefined && { output: genDef.output }),
...(genDef.system !== undefined && { system: genDef.system }),
...(genDef.promptTemplate !== undefined && { promptTemplate: genDef.promptTemplate }),
...(genDef.model !== undefined && { model: genDef.model }),
...(genDef.temperature !== undefined && { temperature: genDef.temperature }),
};
}
case 'agentic': {
const agentDef = definition;
return {
...base,
instructions: agentDef.instructions,
...(agentDef.promptTemplate !== undefined && { promptTemplate: agentDef.promptTemplate }),
...(agentDef.tools !== undefined && { tools: agentDef.tools }),
...(agentDef.maxIterations !== undefined && { maxIterations: agentDef.maxIterations }),
...(agentDef.model !== undefined && { model: agentDef.model }),
...(agentDef.stream !== undefined && { stream: agentDef.stream }),
};
}
case 'human': {
const humanDef = definition;
return {
...base,
...(humanDef.channel !== undefined && { channel: humanDef.channel }),
instructions: humanDef.instructions,
...(humanDef.promptTemplate !== undefined && { promptTemplate: humanDef.promptTemplate }),
...(humanDef.timeout !== undefined && { timeout: humanDef.timeout }),
...(humanDef.assignee !== undefined && { assignee: humanDef.assignee }),
};
}
}
}
/**
* Convert stored data back to a FunctionDefinition
*/
function dataToDefinition(data) {
switch (data.type) {
case 'code': {
const def = {
type: 'code',
name: data.name,
args: data.args,
};
if (data.description !== undefined)
def.description = data.description;
if (data.returnType !== undefined)
def.returnType = data.returnType;
if (data.code !== undefined)
def.code = data.code;
if (data.language !== undefined)
def.language =
data.language;
if (data.instructions !== undefined)
def.instructions = data.instructions;
return def;
}
case 'generative': {
const def = {
type: 'generative',
name: data.name,
args: data.args,
output: (data.output ?? 'string'),
};
if (data.description !== undefined)
def.description = data.description;
if (data.returnType !== undefined)
def.returnType = data.returnType;
if (data.system !== undefined)
def.system = data.system;
if (data.promptTemplate !== undefined)
def.promptTemplate = data.promptTemplate;
if (data.model !== undefined)
def.model = data.model;
if (data.temperature !== undefined)
def.temperature = data.temperature;
return def;
}
case 'agentic': {
const def = {
type: 'agentic',
name: data.name,
args: data.args,
instructions: data.instructions ?? '',
};
if (data.description !== undefined)
def.description = data.description;
if (data.returnType !== undefined)
def.returnType = data.returnType;
if (data.promptTemplate !== undefined)
def.promptTemplate = data.promptTemplate;
if (data.tools !== undefined)
def.tools =
data.tools;
if (data.maxIterations !== undefined)
def.maxIterations = data.maxIterations;
if (data.model !== undefined)
def.model = data.model;
if (data.stream !== undefined)
def.stream = data.stream;
return def;
}
case 'human': {
const def = {
type: 'human',
name: data.name,
args: data.args,
channel: (data.channel ?? 'web'),
instructions: data.instructions ?? '',
};
if (data.description !== undefined)
def.description = data.description;
if (data.returnType !== undefined)
def.returnType = data.returnType;
if (data.promptTemplate !== undefined)
def.promptTemplate = data.promptTemplate;
if (data.timeout !== undefined)
def.timeout = data.timeout;
if (data.assignee !== undefined)
def.assignee = data.assignee;
return def;
}
}
}
/**
* DigitalObjectsFunctionRegistry - Persistent function registry using digital-objects
*
* This class implements the FunctionRegistry interface using digital-objects for storage.
* Function definitions are stored as Things, and function calls are tracked as Actions.
*
* @example
* ```ts
* import { createMemoryProvider } from 'digital-objects'
* import { createDigitalObjectsRegistry, defineFunction } from 'ai-functions'
*
* const provider = createMemoryProvider()
* const registry = await createDigitalObjectsRegistry({ provider })
*
* // Define a function
* const summarize = defineFunction({
* type: 'generative',
* name: 'summarize',
* args: { text: 'Text to summarize' },
* output: 'string',
* })
*
* // Store it in the registry
* registry.set('summarize', summarize)
*
* // Later, retrieve it
* const fn = registry.get('summarize')
* if (fn) {
* const result = await fn.call({ text: 'Long article...' })
* }
* ```
*/
export class DigitalObjectsFunctionRegistry {
provider;
initialized = false;
autoInitialize;
initPromise = null;
// In-memory cache for DefinedFunction instances (they contain the call implementation)
functionCache = new Map();
constructor(options) {
this.provider = options.provider;
this.autoInitialize = options.autoInitialize ?? true;
}
/**
* Initialize the registry by defining all necessary nouns and verbs
*/
async initialize() {
if (this.initialized)
return;
if (this.initPromise)
return this.initPromise;
this.initPromise = this._initialize();
await this.initPromise;
this.initialized = true;
}
async _initialize() {
// Define function type nouns
await Promise.all([
this.provider.defineNoun({
name: FUNCTION_NOUNS.CODE,
description: 'Function that generates executable code',
schema: {
name: 'string',
description: 'string?',
language: 'string?',
instructions: 'string?',
},
}),
this.provider.defineNoun({
name: FUNCTION_NOUNS.GENERATIVE,
description: 'Function that uses AI to generate content',
schema: {
name: 'string',
description: 'string?',
output: 'string',
system: 'string?',
promptTemplate: 'string?',
},
}),
this.provider.defineNoun({
name: FUNCTION_NOUNS.AGENTIC,
description: 'Function that runs in a loop with tools',
schema: {
name: 'string',
description: 'string?',
instructions: 'string',
maxIterations: 'number?',
},
}),
this.provider.defineNoun({
name: FUNCTION_NOUNS.HUMAN,
description: 'Function that requires human input or approval',
schema: {
name: 'string',
description: 'string?',
channel: 'string',
instructions: 'string',
},
}),
]);
// Define function action verbs
await Promise.all([
this.provider.defineVerb({
name: FUNCTION_VERBS.DEFINE,
description: 'Define a new function',
}),
this.provider.defineVerb({
name: FUNCTION_VERBS.CALL,
description: 'Call/invoke a function',
}),
this.provider.defineVerb({
name: FUNCTION_VERBS.COMPLETE,
description: 'Mark a function call as successfully completed',
}),
this.provider.defineVerb({
name: FUNCTION_VERBS.FAIL,
description: 'Mark a function call as failed',
}),
]);
}
/**
* Ensure the registry is initialized before operations
*/
async ensureInitialized() {
if (this.autoInitialize && !this.initialized) {
await this.initialize();
}
}
/**
* Get a function by name
*/
get(name) {
// Return from cache if available
return this.functionCache.get(name);
}
/**
* Get a function by name (async version for loading from storage)
*/
async getAsync(name) {
await this.ensureInitialized();
// Check cache first
const cached = this.functionCache.get(name);
if (cached)
return cached;
// Search across all function nouns
for (const noun of Object.values(FUNCTION_NOUNS)) {
const things = await this.provider.find(noun, { name });
const firstThing = things[0];
if (firstThing) {
const definition = dataToDefinition(firstThing.data);
// Note: The caller needs to provide the call implementation
// This returns the definition info but not a callable function
return {
definition,
call: async () => {
throw new Error(`Function '${name}' was loaded from storage but has no call implementation. ` +
'Use defineFunction() to create a callable function.');
},
asTool: () => ({
name: definition.name,
description: definition.description ?? `Execute ${definition.name}`,
parameters: { type: 'object', properties: {}, required: [] },
handler: async () => {
throw new Error('Function loaded from storage is not callable');
},
}),
};
}
}
return undefined;
}
/**
* Store a function definition
*/
set(name, fn) {
// Store in cache for immediate access
this.functionCache.set(name, fn);
// Store in digital-objects asynchronously (fire and forget for sync interface)
this.setAsync(name, fn).catch((err) => {
getLogger().error(`Failed to persist function '${name}' to digital-objects:`, err);
});
}
/**
* Store a function definition (async version)
*/
async setAsync(name, fn) {
await this.ensureInitialized();
const definition = fn.definition;
const noun = typeToNoun(definition.type);
const data = definitionToData(definition);
// Check if function already exists
const existing = await this.provider.find(noun, { name });
const existingThing = existing[0];
let thing;
if (existingThing) {
// Update existing
thing = await this.provider.update(existingThing.id, data);
}
else {
// Create new
thing = await this.provider.create(noun, data);
// Record the define action
await this.provider.perform(FUNCTION_VERBS.DEFINE, undefined, // subject (could be user ID in future)
thing.id, { name, type: definition.type });
}
// Update cache
this.functionCache.set(name, fn);
return thing;
}
/**
* Check if a function exists
*/
has(name) {
return this.functionCache.has(name);
}
/**
* Check if a function exists (async version that also checks storage)
*/
async hasAsync(name) {
if (this.functionCache.has(name))
return true;
await this.ensureInitialized();
// Search across all function nouns
for (const noun of Object.values(FUNCTION_NOUNS)) {
const things = await this.provider.find(noun, { name });
if (things.length > 0)
return true;
}
return false;
}
/**
* List all function names
*/
list() {
return Array.from(this.functionCache.keys());
}
/**
* List all function names (async version that includes storage)
*/
async listAsync() {
await this.ensureInitialized();
const names = new Set(this.functionCache.keys());
// Get all functions from storage
for (const noun of Object.values(FUNCTION_NOUNS)) {
const things = await this.provider.list(noun);
for (const thing of things) {
names.add(thing.data.name);
}
}
return Array.from(names);
}
/**
* Delete a function
*/
delete(name) {
const existed = this.functionCache.has(name);
this.functionCache.delete(name);
// Delete from storage asynchronously
this.deleteAsync(name).catch((err) => {
getLogger().error(`Failed to delete function '${name}' from digital-objects:`, err);
});
return existed;
}
/**
* Delete a function (async version)
*/
async deleteAsync(name) {
await this.ensureInitialized();
this.functionCache.delete(name);
// Search across all function nouns and delete
for (const noun of Object.values(FUNCTION_NOUNS)) {
const things = await this.provider.find(noun, { name });
for (const thing of things) {
await this.provider.delete(thing.id);
}
if (things.length > 0)
return true;
}
return false;
}
/**
* Clear all functions
*/
clear() {
this.functionCache.clear();
// Clear storage asynchronously
this.clearAsync().catch((err) => {
getLogger().error('Failed to clear functions from digital-objects:', err);
});
}
/**
* Clear all functions (async version)
*/
async clearAsync() {
await this.ensureInitialized();
this.functionCache.clear();
// Delete all functions from storage
for (const noun of Object.values(FUNCTION_NOUNS)) {
const things = await this.provider.list(noun);
for (const thing of things) {
await this.provider.delete(thing.id);
}
}
}
// ============================================================================
// Function Call Tracking (Actions)
// ============================================================================
/**
* Record a function call as an Action
*/
async trackCall(functionName, args) {
await this.ensureInitialized();
// Find the function thing
let functionId;
for (const noun of Object.values(FUNCTION_NOUNS)) {
const things = await this.provider.find(noun, {
name: functionName,
});
const firstThing = things[0];
if (firstThing) {
functionId = firstThing.id;
break;
}
}
return this.provider.perform(FUNCTION_VERBS.CALL, undefined, // subject (caller)
functionId, // object (the function)
{ args });
}
/**
* Record a successful function completion
*/
async trackCompletion(callActionId, result, duration) {
await this.ensureInitialized();
const data = { args: undefined, result };
if (duration !== undefined)
data.duration = duration;
return this.provider.perform(FUNCTION_VERBS.COMPLETE, undefined, callActionId, data);
}
/**
* Record a function failure
*/
async trackFailure(callActionId, error, duration) {
await this.ensureInitialized();
const data = { args: undefined, error };
if (duration !== undefined)
data.duration = duration;
return this.provider.perform(FUNCTION_VERBS.FAIL, undefined, callActionId, data);
}
/**
* Get call history for a function
*/
async getCallHistory(functionName) {
await this.ensureInitialized();
// Find the function thing
for (const noun of Object.values(FUNCTION_NOUNS)) {
const things = await this.provider.find(noun, {
name: functionName,
});
const firstThing = things[0];
if (firstThing) {
return this.provider.listActions({
verb: FUNCTION_VERBS.CALL,
object: firstThing.id,
});
}
}
return [];
}
/**
* Get all recent function calls
*/
async getRecentCalls(limit = 10) {
await this.ensureInitialized();
return this.provider.listActions({
verb: FUNCTION_VERBS.CALL,
limit,
});
}
/**
* Get the underlying provider for advanced operations
*/
getProvider() {
return this.provider;
}
}
/**
* Create a DigitalObjectsFunctionRegistry
*
* @param options - Configuration options including the provider
* @returns An initialized DigitalObjectsFunctionRegistry
*
* @example
* ```ts
* import { createMemoryProvider } from 'digital-objects'
* import { createDigitalObjectsRegistry } from 'ai-functions'
*
* const provider = createMemoryProvider()
* const registry = await createDigitalObjectsRegistry({ provider })
*
* // Use the registry
* registry.set('myFunc', definedFunction)
* const fn = registry.get('myFunc')
* ```
*/
export async function createDigitalObjectsRegistry(options) {
const registry = new DigitalObjectsFunctionRegistry(options);
if (options.autoInitialize !== false) {
await registry.initialize();
}
return registry;
}
//# sourceMappingURL=digital-objects-registry.js.map