agentlang
Version:
The easiest way to build the most reliable AI agents - enterprise-grade teams of AI agents that collaborate with each other and humans
502 lines (454 loc) • 13.7 kB
text/typescript
import { default as ai } from './ai.js';
import { default as auth } from './auth.js';
import { default as files } from './files.js';
import {
DefaultModuleName,
DefaultModules,
escapeSpecialChars,
isString,
restoreSpecialChars,
} from '../util.js';
import { Instance, isInstanceOfType, makeInstance, newInstanceAttributes } from '../module.js';
import {
Environment,
evaluate,
evaluateStatements,
parseAndEvaluateStatement,
restartFlow,
} from '../interpreter.js';
import { logger } from '../logger.js';
import { Statement } from '../../language/generated/ast.js';
import { parseModule, parseStatements } from '../../language/parser.js';
import { Resolver } from '../resolvers/interface.js';
import { FlowSuspensionTag, ForceReadPermFlag, PathAttributeName } from '../defs.js';
import { getMonitor, getMonitorsForEvent, Monitor } from '../monitor.js';
const CoreModuleDefinition = `module ${DefaultModuleName}
import "./modules/core.js" @as Core
entity timer {
name String @id,
duration Int,
unit @enum("millisecond", "second", "minute", "hour") @default("second"),
trigger String,
status @enum("I", "C", "R") @default("I") // Inited, Cancelled, Running
}
entity auditlog {
id UUID @id @default(uuid()),
action @enum("c", "d", "u"), // Create, Delete, Update
resource String, // __path__
timestamp DateTime @default(now()),
previous_value Any @optional,
user String,
token String @optional
}
entity suspension {
id UUID @id,
continuation String[], // rest of the patterns to execute
env Any, // serialized environment-object
createdOn DateTime @default(now()),
createdBy String
}
entity activeSuspension {
id UUID @id
}
resolver suspensionResolver ["${DefaultModuleName}/activeSuspension"] {
query Core.lookupActiveSuspension
}
workflow createSuspension {
{suspension
{id createSuspension.id
continuation createSuspension.continuation,
env createSuspension.env,
createdBy createSuspension.createdBy}}
}
@public workflow restartSuspension {
await Core.restartSuspension(restartSuspension.id, restartSuspension.data)
}
entity Monitor {
id String @id,
eventInstance Any,
eventName String @indexed,
user String @optional,
totalLatencyMs Int,
data String
}
@public event fetchEventMonitor {
eventName String
}
workflow fetchEventMonitor {
{Monitor {eventName? fetchEventMonitor.eventName}} @as [m]
Core.eventMonitorData(m)
}
@public event fetchEventMonitors {
eventName String,
limit Int @default(0),
offset Int @default(0)
}
workflow fetchEventMonitors {
{Monitor {eventName? fetchEventMonitors.eventName}} @as result
Core.eventMonitorsData(result, fetchEventMonitors.limit, fetchEventMonitors.offset)
}
@public event EventMonitor {
id String
}
workflow EventMonitor {
{Monitor {id? EventMonitor.id}} @as [m];
Core.eventMonitorData(m)
}
record ValidationRequest {
data Any
}
record ValidationResult {
status @enum("ok", "error"),
reason String @optional
}
event validateModule extends ValidationRequest {
}
workflow validateModule {
await Core.validateModule(validateModule.data)
}
entity Migration {
appVersion String @id,
ups String @optional,
downs String @optional
}
`;
export const CoreModules: string[] = [];
export function registerCoreModules() {
DefaultModules.add(DefaultModuleName);
CoreModules.push(CoreModuleDefinition);
[auth, ai, files].forEach((mdef: string) => {
CoreModules.push(mdef);
DefaultModules.add(mdef);
});
}
export function setTimerRunning(timerInst: Instance) {
timerInst.attributes.set('status', 'R');
}
export async function maybeCancelTimer(name: string, timer: NodeJS.Timeout, env: Environment) {
await parseAndEvaluateStatement(`{agentlang/timer {name? "${name}"}}`, undefined, env).then(
(result: any) => {
if (result === null || (result instanceof Array && result.length == 0)) {
clearInterval(timer);
}
}
);
}
async function addAudit(
env: Environment,
action: 'c' | 'd' | 'u',
resource: string,
previuos_value?: Instance
) {
const user = env.getActiveUser();
const token = env.getActiveToken();
const newEnv = new Environment('auditlog', env).setInKernelMode(true);
newEnv.bind(ForceReadPermFlag, true);
const r: any = await parseAndEvaluateStatement(
`{agentlang/auditlog {
action "${action}",
resource "${resource}",
previous_value "${previuos_value ? escapeSpecialChars(JSON.stringify(previuos_value.asObject())) : ''}",
user "${user}",
token "${token ? token : ''}"
}}`,
user,
newEnv
);
if (!isInstanceOfType(r, 'agentlang/auditlog')) {
logger.warn(
`Failed to create auditlog for action ${action} and resource ${resource} for user ${user}`
);
}
}
export async function addCreateAudit(resource: string, env: Environment) {
await addAudit(env, 'c', resource);
}
export async function addDeleteAudit(
resource: string,
previous_value: Instance | undefined,
env: Environment
) {
await addAudit(env, 'd', resource, previous_value);
}
export async function addUpdateAudit(
resource: string,
previous_value: Instance | undefined,
env: Environment
) {
await addAudit(env, 'u', resource, previous_value);
}
export async function createSuspension(
suspId: string,
continuation: string[],
env: Environment
): Promise<string | undefined> {
const user = env.getActiveUser();
const newEnv = new Environment('susp', env).setInKernelMode(true);
const envObj = env.asSerializableObject();
const inst = makeInstance(
'agentlang',
'createSuspension',
newInstanceAttributes()
.set('id', suspId)
.set('continuation', continuation)
.set('env', envObj)
.set('createdBy', user)
);
const r: any = await evaluate(inst, undefined, newEnv);
if (!isInstanceOfType(r, 'agentlang/suspension')) {
logger.warn(`Failed to create suspension for user ${user}`);
return undefined;
}
return (r as Instance).lookup('id');
}
export type Suspension = {
continuation: Statement[];
flowContext?: string[];
env: Environment;
};
function isFlowSuspension(cont: string[]): boolean {
return cont.length > 0 && cont[0] == FlowSuspensionTag;
}
async function loadSuspension(suspId: string, env?: Environment): Promise<Suspension | undefined> {
const newEnv = new Environment('auditlog', env).setInKernelMode(true);
const r: any = await parseAndEvaluateStatement(
`{agentlang/suspension {id? "${suspId}"}}`,
undefined,
newEnv
);
if (r instanceof Array && r.length > 0) {
const inst: Instance = r[0];
const cont = inst.lookup('continuation');
const ifs = isFlowSuspension(cont);
const stmts: Statement[] = ifs ? new Array<Statement>() : await parseStatements(cont);
const envStr = inst.lookup('env');
const suspEnv: Environment = Environment.FromSerializableObject(JSON.parse(envStr));
return {
continuation: stmts,
env: suspEnv,
flowContext: ifs ? cont : undefined,
};
}
return undefined;
}
async function deleteSuspension(suspId: string, env?: Environment): Promise<any> {
try {
await parseAndEvaluateStatement(
`purge {agentlang/suspension {id? "${suspId}"}}`,
undefined,
env
);
return suspId;
} catch (err: any) {
logger.warn(`Failed to delete suspension ${suspId} - ${err}`);
return undefined;
}
}
export async function restartSuspension(
suspId: string,
userData: string,
env?: Environment
): Promise<any> {
const susp = await loadSuspension(suspId, env);
if (susp) {
if (susp.flowContext) {
await restartFlow(susp.flowContext, userData, susp.env);
} else {
susp.env.bindSuspensionUserData(userData);
await evaluateStatements(susp.continuation, susp.env);
}
await deleteSuspension(suspId, env);
return susp.env.getLastResult();
} else {
logger.warn(`Suspension ${suspId} not found`);
return undefined;
}
}
export async function lookupActiveSuspension(
resolver: Resolver,
inst: Instance,
queryAll: boolean
) {
if (!queryAll) {
const data = inst.lookupQueryVal(PathAttributeName).split('/')[1];
if (data) {
const parts = data.split(':');
const id = parts[0];
const userData = parts[1];
return await restartSuspension(id, userData, resolver.getEnvironment());
} else {
return [];
}
} else {
return [];
}
}
export async function flushMonitoringData(monitorId: string) {
const m = getMonitor(monitorId);
try {
if (m) {
const data = btoa(JSON.stringify(m.asObject()));
const inst = m.getEventInstance();
const eventInstance = inst ? btoa(JSON.stringify(inst.asSerializableObject())) : '';
const user = m.getUser() || 'admin';
const latency = m.getTotalLatencyMs();
const env = new Environment(`monitor-${monitorId}-env`);
const eventName = inst ? inst.getFqName() : monitorId;
await parseAndEvaluateStatement(
`{agentlang/Monitor {id "${monitorId}", eventName "${eventName}", eventInstance "${eventInstance}", user "${user}", totalLatencyMs ${latency}, data "${data}"}}`,
undefined,
env
);
await env.commitAllTransactions();
} else {
logger.warn(`Failed to locate monitor with id ${monitorId}`);
}
} catch (reason: any) {
logger.error(`Failed to flush monitor ${monitorId} - ${reason}`);
}
}
export async function fetchLatestMonitorForEvent(eventName: string): Promise<any> {
const monitors = getMonitorsForEvent(eventName);
const len = monitors.length;
if (len > 0) {
return [monitors[len - 1].asObject()];
}
return [];
}
export async function fetchMonitorsForEvent(
eventName: string,
limit: number,
offset: number
): Promise<any> {
const monitors = getMonitorsForEvent(eventName);
const r = limit === 0 ? monitors : monitors.slice(offset, offset + limit);
if (r.length > 0) {
return r.map((m: Monitor) => {
return m.asObject();
});
}
return [];
}
export function eventMonitorData(inst: Instance | null | undefined): any {
if (inst) return JSON.parse(atob(inst.lookup('data')));
else return null;
}
export function eventMonitorsData(
insts: Instance[] | null | undefined,
limit?: number,
offset?: number
): any {
if (insts) {
if (limit !== undefined && offset !== undefined) {
insts = limit === 0 ? insts : insts.slice(offset, offset + limit);
}
return insts.map((inst: Instance) => {
return eventMonitorData(inst);
});
} else return null;
}
export async function validateModule(moduleDef: any): Promise<Instance> {
try {
if (isString(moduleDef)) {
await parseModule(moduleDef);
return makeInstance(
'agentlang',
'ValidationResult',
newInstanceAttributes().set('status', 'ok')
);
} else {
const xs = Object.entries(moduleDef);
for (let i = 0; i < xs.length; ++i) {
const x = xs[i][1] as any;
if (isString(x) && x.trimStart().startsWith('module')) {
return await validateModule(x);
}
}
throw new Error(`no module definitions found in object`);
}
} catch (reason: any) {
return makeInstance(
'agentlang',
'ValidationResult',
newInstanceAttributes().set('status', 'error').set('reason', `${reason}`)
);
}
}
const SqlSep = ';\n\n';
export async function saveMigration(
version: string,
ups: string[] | undefined,
downs: string[] | undefined
): Promise<boolean> {
try {
const env = new Environment(`migrations-${version}-env`);
await parseAndEvaluateStatement(
`purge {agentlang/Migration {appVersion? "${version}"}}`,
undefined,
env
);
let ups_str = '';
if (ups) {
ups_str = escapeSpecialChars(
ups
.map((s: string) => {
return s.trim();
})
.join(SqlSep)
);
}
let downs_str = '';
if (downs) {
downs_str = escapeSpecialChars(
downs
.map((s: string) => {
return s.trim();
})
.join(SqlSep)
);
}
const inst: Instance = await parseAndEvaluateStatement(`{agentlang/Migration {
appVersion "${version}",
ups "${ups_str}",
downs "${downs_str}"}}`);
if (isInstanceOfType(inst, 'agentlang/Migration') && inst.lookup('appVersion') === version) {
await env.commitAllTransactions();
return true;
} else {
logger.warn(`Failed to save migration for version ${version}`);
}
} catch (reason: any) {
logger.error(`Failed to save migration for version ${version} - ${reason}`);
}
return false;
}
export async function loadMigration(version: string): Promise<Instance | undefined> {
try {
const env = new Environment(`migrations-${version}-env`);
const insts: Instance[] = await parseAndEvaluateStatement(
`{agentlang/Migration {appVersion? "${version}"}}`,
undefined,
env
);
if (insts && insts.length > 0) {
return insts[0];
}
} catch (reason: any) {
logger.error(`Failed to lookup migration for version ${version} - ${reason}`);
}
return undefined;
}
export function migrationUps(inst: Instance): string[] | undefined {
const ups: string | undefined = inst.lookup('ups');
if (ups) {
return restoreSpecialChars(ups).split(SqlSep);
}
return undefined;
}
export function migrationDowns(inst: Instance): string[] | undefined {
const downs: string | undefined = inst.lookup('downs');
if (downs) {
return restoreSpecialChars(downs).split(SqlSep);
}
return undefined;
}