ai-functions
Version:
Core AI primitives for building intelligent applications
751 lines (680 loc) • 22.8 kB
text/typescript
/**
* 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 type { DigitalObjectsProvider, Thing, Action } from 'digital-objects'
import type {
FunctionRegistry,
DefinedFunction,
FunctionDefinition,
CodeFunctionDefinition,
GenerativeFunctionDefinition,
AgenticFunctionDefinition,
HumanFunctionDefinition,
} from './types.js'
import { getLogger } from './logger.js'
/**
* Noun names for function types
*/
export const FUNCTION_NOUNS = {
CODE: 'CodeFunction',
GENERATIVE: 'GenerativeFunction',
AGENTIC: 'AgenticFunction',
HUMAN: 'HumanFunction',
} as const
/**
* Verb names for function actions
*/
export const FUNCTION_VERBS = {
DEFINE: 'define',
CALL: 'call',
COMPLETE: 'complete',
FAIL: 'fail',
} as const
/**
* Stored function definition shape
*/
export interface StoredFunctionDefinition {
name: string
type: 'code' | 'generative' | 'agentic' | 'human'
description?: string
args: unknown
returnType?: unknown
// Type-specific fields
/** Inline deterministic code body for a `code` function (handlers are not persistable). */
code?: string
language?: string
instructions?: string
/** @deprecated Legacy code-authoring fields; retained only for back-compat reads. */
includeTests?: boolean
/** @deprecated Legacy code-authoring fields; retained only for back-compat reads. */
includeExamples?: boolean
output?: string
system?: string
promptTemplate?: string
model?: string
temperature?: number
tools?: unknown[]
maxIterations?: number
stream?: boolean
channel?: string
timeout?: number
assignee?: string
}
/**
* Function call data stored in actions
*/
export interface FunctionCallData {
args: unknown
result?: unknown
error?: string
duration?: number
}
/**
* Options for creating a DigitalObjectsFunctionRegistry
*/
export interface DigitalObjectsRegistryOptions {
/** The digital-objects provider to use for storage */
provider: DigitalObjectsProvider
/** Whether to auto-initialize nouns and verbs (default: true) */
autoInitialize?: boolean
}
/**
* Map function type to noun name
*/
function typeToNoun(type: FunctionDefinition['type']): string {
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: FunctionDefinition): StoredFunctionDefinition {
const base: StoredFunctionDefinition = {
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 as CodeFunctionDefinition
// 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 as GenerativeFunctionDefinition
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 as AgenticFunctionDefinition
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 as HumanFunctionDefinition
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: StoredFunctionDefinition): FunctionDefinition {
switch (data.type) {
case 'code': {
const def = {
type: 'code' as const,
name: data.name,
args: data.args,
} as CodeFunctionDefinition
if (data.description !== undefined)
(def as { description: string }).description = data.description
if (data.returnType !== undefined)
(def as { returnType: unknown }).returnType = data.returnType
if (data.code !== undefined) (def as { code: string }).code = data.code
if (data.language !== undefined)
(def as { language: CodeFunctionDefinition['language'] }).language =
data.language as CodeFunctionDefinition['language']
if (data.instructions !== undefined)
(def as { instructions: string }).instructions = data.instructions
return def
}
case 'generative': {
const def: GenerativeFunctionDefinition = {
type: 'generative',
name: data.name,
args: data.args,
output: (data.output ?? 'string') as GenerativeFunctionDefinition['output'],
}
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' as const,
name: data.name,
args: data.args,
instructions: data.instructions ?? '',
} as AgenticFunctionDefinition
if (data.description !== undefined)
(def as { description: string }).description = data.description
if (data.returnType !== undefined)
(def as { returnType: unknown }).returnType = data.returnType
if (data.promptTemplate !== undefined)
(def as { promptTemplate: string }).promptTemplate = data.promptTemplate
if (data.tools !== undefined)
(def as { tools: AgenticFunctionDefinition['tools'] }).tools =
data.tools as AgenticFunctionDefinition['tools']
if (data.maxIterations !== undefined)
(def as { maxIterations: number }).maxIterations = data.maxIterations
if (data.model !== undefined) (def as { model: string }).model = data.model
if (data.stream !== undefined) (def as { stream: boolean }).stream = data.stream
return def
}
case 'human': {
const def: HumanFunctionDefinition = {
type: 'human',
name: data.name,
args: data.args,
channel: (data.channel ?? 'web') as HumanFunctionDefinition['channel'],
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 implements FunctionRegistry {
private provider: DigitalObjectsProvider
private initialized = false
private autoInitialize: boolean
private initPromise: Promise<void> | null = null
// In-memory cache for DefinedFunction instances (they contain the call implementation)
private functionCache = new Map<string, DefinedFunction>()
constructor(options: DigitalObjectsRegistryOptions) {
this.provider = options.provider
this.autoInitialize = options.autoInitialize ?? true
}
/**
* Initialize the registry by defining all necessary nouns and verbs
*/
async initialize(): Promise<void> {
if (this.initialized) return
if (this.initPromise) return this.initPromise
this.initPromise = this._initialize()
await this.initPromise
this.initialized = true
}
private async _initialize(): Promise<void> {
// 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
*/
private async ensureInitialized(): Promise<void> {
if (this.autoInitialize && !this.initialized) {
await this.initialize()
}
}
/**
* Get a function by name
*/
get(name: string): DefinedFunction | undefined {
// Return from cache if available
return this.functionCache.get(name)
}
/**
* Get a function by name (async version for loading from storage)
*/
async getAsync(name: string): Promise<DefinedFunction | undefined> {
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<StoredFunctionDefinition>(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: string, fn: DefinedFunction): void {
// 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: string, fn: DefinedFunction): Promise<Thing<StoredFunctionDefinition>> {
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<StoredFunctionDefinition>(noun, { name })
const existingThing = existing[0]
let thing: Thing<StoredFunctionDefinition>
if (existingThing) {
// Update existing
thing = await this.provider.update<StoredFunctionDefinition>(existingThing.id, data)
} else {
// Create new
thing = await this.provider.create<StoredFunctionDefinition>(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: string): boolean {
return this.functionCache.has(name)
}
/**
* Check if a function exists (async version that also checks storage)
*/
async hasAsync(name: string): Promise<boolean> {
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<StoredFunctionDefinition>(noun, { name })
if (things.length > 0) return true
}
return false
}
/**
* List all function names
*/
list(): string[] {
return Array.from(this.functionCache.keys())
}
/**
* List all function names (async version that includes storage)
*/
async listAsync(): Promise<string[]> {
await this.ensureInitialized()
const names = new Set<string>(this.functionCache.keys())
// Get all functions from storage
for (const noun of Object.values(FUNCTION_NOUNS)) {
const things = await this.provider.list<StoredFunctionDefinition>(noun)
for (const thing of things) {
names.add(thing.data.name)
}
}
return Array.from(names)
}
/**
* Delete a function
*/
delete(name: string): boolean {
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: string): Promise<boolean> {
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<StoredFunctionDefinition>(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(): void {
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(): Promise<void> {
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<StoredFunctionDefinition>(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: string, args: unknown): Promise<Action<FunctionCallData>> {
await this.ensureInitialized()
// Find the function thing
let functionId: string | undefined
for (const noun of Object.values(FUNCTION_NOUNS)) {
const things = await this.provider.find<StoredFunctionDefinition>(noun, {
name: functionName,
})
const firstThing = things[0]
if (firstThing) {
functionId = firstThing.id
break
}
}
return this.provider.perform<FunctionCallData>(
FUNCTION_VERBS.CALL,
undefined, // subject (caller)
functionId, // object (the function)
{ args }
)
}
/**
* Record a successful function completion
*/
async trackCompletion(
callActionId: string,
result: unknown,
duration?: number
): Promise<Action<FunctionCallData>> {
await this.ensureInitialized()
const data: FunctionCallData = { args: undefined, result }
if (duration !== undefined) data.duration = duration
return this.provider.perform<FunctionCallData>(
FUNCTION_VERBS.COMPLETE,
undefined,
callActionId,
data
)
}
/**
* Record a function failure
*/
async trackFailure(
callActionId: string,
error: string,
duration?: number
): Promise<Action<FunctionCallData>> {
await this.ensureInitialized()
const data: FunctionCallData = { args: undefined, error }
if (duration !== undefined) data.duration = duration
return this.provider.perform<FunctionCallData>(
FUNCTION_VERBS.FAIL,
undefined,
callActionId,
data
)
}
/**
* Get call history for a function
*/
async getCallHistory(functionName: string): Promise<Action<FunctionCallData>[]> {
await this.ensureInitialized()
// Find the function thing
for (const noun of Object.values(FUNCTION_NOUNS)) {
const things = await this.provider.find<StoredFunctionDefinition>(noun, {
name: functionName,
})
const firstThing = things[0]
if (firstThing) {
return this.provider.listActions<FunctionCallData>({
verb: FUNCTION_VERBS.CALL,
object: firstThing.id,
})
}
}
return []
}
/**
* Get all recent function calls
*/
async getRecentCalls(limit = 10): Promise<Action<FunctionCallData>[]> {
await this.ensureInitialized()
return this.provider.listActions<FunctionCallData>({
verb: FUNCTION_VERBS.CALL,
limit,
})
}
/**
* Get the underlying provider for advanced operations
*/
getProvider(): DigitalObjectsProvider {
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: DigitalObjectsRegistryOptions
): Promise<DigitalObjectsFunctionRegistry> {
const registry = new DigitalObjectsFunctionRegistry(options)
if (options.autoInitialize !== false) {
await registry.initialize()
}
return registry
}