@xynehq/jaf
Version:
Juspay Agent Framework - A purely functional agent framework with immutable state and composable tools
553 lines (552 loc) • 24.9 kB
JavaScript
import { jsonParseLLMOutput, createRunId, createTraceId, getTextContent, validateGuardrailsConfig } from './types.js';
import { safeConsole } from '../utils/logger.js';
// Constants for content length limits
const SHORT_TIMEOUT_MAX_CONTENT = 10000;
const LONG_TIMEOUT_MAX_CONTENT = 50000;
const CIRCUIT_BREAKER_CLEANUP_MAX_AGE = 10 * 60 * 1000; // 10 minutes
class GuardrailCircuitBreaker {
failures = 0;
lastFailureTime = 0;
maxFailures = 5;
resetTimeMs = 60000; // 1 minute
isOpen() {
if (this.failures < this.maxFailures)
return false;
const timeSinceLastFailure = Date.now() - this.lastFailureTime;
if (timeSinceLastFailure > this.resetTimeMs) {
this.failures = 0;
return false;
}
return true;
}
recordFailure() {
this.failures++;
this.lastFailureTime = Date.now();
}
recordSuccess() {
this.failures = 0;
}
// Public method for cleanup eligibility check
shouldBeCleanedUp(maxAge) {
const now = Date.now();
return this.lastFailureTime > 0 &&
(now - this.lastFailureTime) > maxAge &&
!this.isOpen();
}
}
const circuitBreakers = new Map();
class GuardrailCache {
cache = new Map();
maxSize;
ttlMs;
constructor(maxSize = 1000, ttlMs = 300000) {
this.maxSize = maxSize;
this.ttlMs = ttlMs;
}
createKey(stage, rulePrompt, content, modelName) {
const contentHash = this.hashString(content.substring(0, 1000));
const ruleHash = this.hashString(rulePrompt);
return `guardrail_${stage}_${modelName}_${ruleHash}_${contentHash}_${content.length}`;
}
hashString(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return Math.abs(hash).toString(36);
}
isExpired(entry) {
return Date.now() - entry.timestamp > this.ttlMs;
}
evictLRU() {
if (this.cache.size < this.maxSize)
return;
let lruKey = null;
let lruScore = Infinity;
const now = Date.now();
for (const [key, entry] of this.cache.entries()) {
const ageHours = (now - entry.timestamp) / (1000 * 60 * 60);
const score = entry.hitCount / (1 + ageHours);
if (score < lruScore) {
lruScore = score;
lruKey = key;
}
}
if (lruKey) {
this.cache.delete(lruKey);
}
}
get(stage, rulePrompt, content, modelName) {
const key = this.createKey(stage, rulePrompt, content, modelName);
const entry = this.cache.get(key);
if (!entry || this.isExpired(entry)) {
if (entry)
this.cache.delete(key);
return null;
}
entry.hitCount++;
entry.timestamp = Date.now();
return entry.result;
}
set(stage, rulePrompt, content, modelName, result) {
const key = this.createKey(stage, rulePrompt, content, modelName);
this.evictLRU();
this.cache.set(key, {
result,
timestamp: Date.now(),
hitCount: 1
});
}
clear() {
this.cache.clear();
}
getStats() {
return {
size: this.cache.size,
maxSize: this.maxSize
};
}
}
const guardrailCache = new GuardrailCache();
function getCircuitBreaker(stage, modelName) {
const key = `${stage}-${modelName}`;
if (!circuitBreakers.has(key)) {
circuitBreakers.set(key, new GuardrailCircuitBreaker());
}
return circuitBreakers.get(key);
}
async function withTimeout(promise, timeoutMs, errorMessage) {
const timeoutPromise = new Promise((_, reject) => {
const timeoutId = setTimeout(() => {
reject(new Error(`Timeout: ${errorMessage}`));
}, timeoutMs);
promise.finally(() => clearTimeout(timeoutId));
});
return Promise.race([promise, timeoutPromise]);
}
async function createLLMGuardrail(config, stage, rulePrompt, fastModel, failSafe = 'allow', timeoutMs = 30000) {
return async (content) => {
const modelToUse = fastModel || config.defaultFastModel;
if (!modelToUse) {
const message = `[JAF:GUARDRAILS] No fast model available for LLM guardrail evaluation, using failSafe: ${failSafe}`;
safeConsole.warn(message);
return failSafe === 'allow'
? { isValid: true }
: { isValid: false, errorMessage: 'No model available for guardrail evaluation' };
}
const cachedResult = guardrailCache.get(stage, rulePrompt, content, modelToUse);
if (cachedResult) {
safeConsole.log(`[JAF:GUARDRAILS] Cache hit for ${stage} guardrail`);
safeConsole.log(`[JAF:GUARDRAILS] Cache performance - hit for ${stage} on model ${modelToUse}`);
return cachedResult;
}
const circuitBreaker = getCircuitBreaker(stage, modelToUse);
if (circuitBreaker.isOpen()) {
const message = `[JAF:GUARDRAILS] Circuit breaker open for ${stage} guardrail on model ${modelToUse}, using failSafe: ${failSafe}`;
safeConsole.warn(message);
return failSafe === 'allow'
? { isValid: true }
: { isValid: false, errorMessage: 'Circuit breaker open - too many recent failures' };
}
if (!content || typeof content !== 'string') {
safeConsole.warn(`[JAF:GUARDRAILS] Invalid content provided to ${stage} guardrail`);
return failSafe === 'allow'
? { isValid: true }
: { isValid: false, errorMessage: 'Invalid content provided to guardrail' };
}
const maxContentLength = timeoutMs < 10000 ? SHORT_TIMEOUT_MAX_CONTENT : LONG_TIMEOUT_MAX_CONTENT;
if (content.length > maxContentLength) {
safeConsole.warn(`[JAF:GUARDRAILS] Content too large for ${stage} guardrail (${content.length} chars, max: ${maxContentLength})`);
return failSafe === 'allow'
? { isValid: true }
: { isValid: false, errorMessage: `Content too large for guardrail evaluation (${content.length} > ${maxContentLength} chars)` };
}
const sanitizedContent = content
.replace(/"""/g, '[TRIPLE_QUOTE]')
.replace(/\n/g, ' ')
.substring(0, Math.min(content.length, 2000));
const evalPrompt = `You are a guardrail validator for ${stage}.
Rules:
${rulePrompt}
Decide if the ${stage === "input" ? "user message" : "assistant output"} complies with the rules.
Return a JSON object with keys: {"allowed": boolean, "reason": string}. Do not include extra text.
${stage === "input" ? "User message" : "Assistant output"}:
"""
${sanitizedContent}
"""`;
try {
const tempState = {
runId: createRunId('guardrail-eval'),
traceId: createTraceId('guardrail-eval'),
messages: [{ role: 'user', content: evalPrompt }],
currentAgentName: 'guardrail-evaluator',
context: {},
turnCount: 0
};
const evalAgent = {
name: 'guardrail-evaluator',
instructions: () => 'You are a guardrail validator. Return only valid JSON.',
modelConfig: { name: modelToUse }
};
const guardrailConfig = {
modelProvider: config.modelProvider,
agentRegistry: config.agentRegistry,
maxTurns: 1,
defaultFastModel: config.defaultFastModel,
modelOverride: modelToUse,
initialInputGuardrails: undefined,
finalOutputGuardrails: undefined,
onEvent: undefined
};
const completionPromise = config.modelProvider.getCompletion(tempState, evalAgent, guardrailConfig);
const response = await withTimeout(completionPromise, timeoutMs, `${stage} guardrail evaluation timed out after ${timeoutMs}ms`);
if (!response.message?.content) {
return { isValid: true };
}
const parsed = jsonParseLLMOutput(response.message.content);
const allowed = Boolean(parsed?.allowed);
const reason = typeof parsed?.reason === "string" ? parsed.reason : "Guardrail violation";
circuitBreaker.recordSuccess();
const result = allowed
? { isValid: true }
: { isValid: false, errorMessage: reason };
guardrailCache.set(stage, rulePrompt, content, modelToUse, result);
return result;
}
catch (e) {
circuitBreaker.recordFailure();
let errorMessage = 'Unknown error';
let isTimeout = false;
if (e instanceof Error) {
errorMessage = e.message;
isTimeout = e.message.includes('Timeout');
}
const logMessage = `[JAF:GUARDRAILS] ${stage} guardrail evaluation failed`;
if (isTimeout) {
safeConsole.warn(`${logMessage} due to timeout (${timeoutMs}ms), using failSafe: ${failSafe}`, {
stage,
modelToUse,
contentLength: content.length,
timeoutMs
});
}
else {
safeConsole.warn(`${logMessage}, using failSafe: ${failSafe}`, {
stage,
modelToUse,
error: errorMessage,
contentLength: content.length
});
}
return failSafe === 'allow'
? { isValid: true }
: { isValid: false, errorMessage: `Guardrail evaluation failed: ${errorMessage}` };
}
};
}
export async function buildEffectiveGuardrails(currentAgent, config) {
let effectiveInputGuardrails = [];
let effectiveOutputGuardrails = [];
try {
const rawGuardrailsCfg = currentAgent.advancedConfig?.guardrails || {};
const guardrailsCfg = validateGuardrailsConfig(rawGuardrailsCfg);
const fastModel = guardrailsCfg.fastModel || config.defaultFastModel;
if (!fastModel && (guardrailsCfg.inputPrompt || guardrailsCfg.outputPrompt)) {
safeConsole.warn('[JAF:GUARDRAILS] No fast model available for LLM guardrails - skipping LLM-based validation');
}
safeConsole.log('[JAF:GUARDRAILS] Configuration:', {
hasInputPrompt: !!guardrailsCfg.inputPrompt,
hasOutputPrompt: !!guardrailsCfg.outputPrompt,
requireCitations: guardrailsCfg.requireCitations,
executionMode: guardrailsCfg.executionMode,
failSafe: guardrailsCfg.failSafe,
timeoutMs: guardrailsCfg.timeoutMs,
fastModel: fastModel || 'none'
});
const llmGuardrail = async (stage, rulePrompt, content) => {
const failSafe = guardrailsCfg.failSafe || 'allow';
const timeoutMs = guardrailsCfg.timeoutMs || 30000;
if (!fastModel) {
safeConsole.warn(`[JAF:GUARDRAILS] No model available for ${stage} guardrail - using failSafe: ${failSafe}`);
return failSafe === 'allow'
? { isValid: true }
: { isValid: false, errorMessage: 'No model available for guardrail evaluation' };
}
safeConsole.log(`[JAF:GUARDRAILS] Evaluating ${stage} guardrail`);
config.onEvent?.({
type: 'guardrail_check',
data: { guardrailName: `${stage}-guardrail`, content, isValid: undefined }
});
try {
const evaluator = await createLLMGuardrail(config, stage, rulePrompt, fastModel, failSafe, timeoutMs);
const result = await evaluator(content);
safeConsole.log(`[JAF:GUARDRAILS] ${stage} guardrail result:`, result);
config.onEvent?.({
type: 'guardrail_check',
data: { guardrailName: `${stage}-guardrail`, content, isValid: result.isValid, errorMessage: result.isValid ? undefined : result.errorMessage }
});
return result;
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error in guardrail evaluation';
safeConsole.error(`[JAF:GUARDRAILS] Failed to create or execute ${stage} guardrail:`, error);
config.onEvent?.({
type: 'guardrail_check',
data: { guardrailName: `${stage}-guardrail`, content, isValid: false, errorMessage }
});
return failSafe === 'allow'
? { isValid: true }
: { isValid: false, errorMessage };
}
};
effectiveInputGuardrails = [...(config.initialInputGuardrails || [])];
if (guardrailsCfg?.inputPrompt && typeof guardrailsCfg.inputPrompt === "string" && guardrailsCfg.inputPrompt.trim().length > 0) {
const inputPrompt = guardrailsCfg.inputPrompt;
effectiveInputGuardrails.push(async (userText) => {
const content = typeof userText === "string" ? userText : String(userText);
return llmGuardrail("input", inputPrompt, content);
});
}
effectiveOutputGuardrails = [...(config.finalOutputGuardrails || [])];
if (guardrailsCfg?.requireCitations) {
effectiveOutputGuardrails.push((output) => {
const findText = (val) => {
if (typeof val === "string")
return val;
if (Array.isArray(val))
return val.map(findText).join(" ");
if (val && typeof val === "object")
return Object.values(val).map(findText).join(" ");
return "";
};
const str = typeof output === "string" ? output : findText(output);
const ok = /\[(\d+)\]/.test(str);
return ok
? { isValid: true }
: { isValid: false, errorMessage: "Missing required [n] citation in output" };
});
}
if (guardrailsCfg?.outputPrompt && typeof guardrailsCfg.outputPrompt === "string" && guardrailsCfg.outputPrompt.trim().length > 0) {
const outputPrompt = guardrailsCfg.outputPrompt;
effectiveOutputGuardrails.push(async (output) => {
const toString = (val) => {
try {
if (typeof val === "string")
return val;
return JSON.stringify(val);
}
catch {
return String(val);
}
};
const content = toString(output);
return llmGuardrail("output", outputPrompt, content);
});
}
}
catch (e) {
safeConsole.error('[JAF:GUARDRAILS] Failed to configure advanced guardrails:', e);
effectiveInputGuardrails = [...(config.initialInputGuardrails || [])];
effectiveOutputGuardrails = [...(config.finalOutputGuardrails || [])];
}
return {
inputGuardrails: effectiveInputGuardrails,
outputGuardrails: effectiveOutputGuardrails
};
}
export async function executeInputGuardrailsSequential(inputGuardrails, firstUserMessage, config) {
if (inputGuardrails.length === 0) {
return { isValid: true };
}
safeConsole.log(`[JAF:GUARDRAILS] Starting ${inputGuardrails.length} input guardrails (sequential)`);
const messageContent = firstUserMessage?.content;
const content = getTextContent(messageContent);
for (let i = 0; i < inputGuardrails.length; i++) {
const guardrail = inputGuardrails[i];
const guardrailName = `input-guardrail-${i + 1}`;
try {
safeConsole.log(`[JAF:GUARDRAILS] Starting ${guardrailName}`);
const timeoutMs = 10000;
const guardrailResult = guardrail(content);
const result = await withTimeout(Promise.resolve(guardrailResult), timeoutMs, `${guardrailName} execution timed out after ${timeoutMs}ms`);
safeConsole.log(`[JAF:GUARDRAILS] ${guardrailName} completed:`, result);
if (!result.isValid) {
const errorMessage = 'errorMessage' in result ? result.errorMessage : 'Guardrail violation';
safeConsole.log(`🚨 ${guardrailName} violation: ${errorMessage}`);
config.onEvent?.({
type: 'guardrail_violation',
data: { stage: 'input', reason: errorMessage }
});
return { isValid: false, errorMessage };
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
safeConsole.error(`[JAF:GUARDRAILS] ${guardrailName} failed:`, errorMessage);
const isSystemError = errorMessage.includes('Timeout') || errorMessage.includes('Circuit breaker');
if (isSystemError) {
safeConsole.warn(`[JAF:GUARDRAILS] ${guardrailName} system error, continuing: ${errorMessage}`);
continue;
}
else {
config.onEvent?.({
type: 'guardrail_violation',
data: { stage: 'input', reason: errorMessage }
});
return { isValid: false, errorMessage };
}
}
}
safeConsole.log(`✅ All input guardrails passed (sequential).`);
return { isValid: true };
}
export async function executeInputGuardrailsParallel(inputGuardrails, firstUserMessage, config) {
if (inputGuardrails.length === 0) {
return { isValid: true };
}
safeConsole.log(`[JAF:GUARDRAILS] Starting ${inputGuardrails.length} input guardrails`);
const inputGuardrailPromises = inputGuardrails.map(async (guardrail, index) => {
const guardrailName = `input-guardrail-${index + 1}`;
try {
safeConsole.log(`[JAF:GUARDRAILS] Starting ${guardrailName}`);
const timeoutMs = config.defaultFastModel ? 10000 : 5000;
const messageContent = firstUserMessage?.content;
const content = getTextContent(messageContent);
const guardrailResult = guardrail(content);
const result = await withTimeout(Promise.resolve(guardrailResult), timeoutMs, `${guardrailName} execution timed out after ${timeoutMs}ms`);
safeConsole.log(`[JAF:GUARDRAILS] ${guardrailName} completed:`, result);
return { ...result, guardrailIndex: index };
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
safeConsole.error(`[JAF:GUARDRAILS] ${guardrailName} failed:`, {
error: errorMessage,
index,
stack: error instanceof Error ? error.stack : undefined
});
return {
isValid: true,
guardrailIndex: index,
warning: `Guardrail ${index + 1} failed but was skipped: ${errorMessage}`
};
}
});
try {
const settledResults = await Promise.allSettled(inputGuardrailPromises);
safeConsole.log(`[JAF:GUARDRAILS] Input guardrails completed. Checking results...`);
const results = [];
const warnings = [];
for (let i = 0; i < settledResults.length; i++) {
const settled = settledResults[i];
if (settled.status === 'fulfilled') {
const result = settled.value;
results.push(result);
if ('warning' in result && result.warning) {
warnings.push(result.warning);
}
}
else {
const errorMessage = settled.reason instanceof Error ? settled.reason.message : 'Unknown error';
safeConsole.warn(`[JAF:GUARDRAILS] Input guardrail ${i + 1} promise rejected:`, errorMessage);
warnings.push(`Guardrail ${i + 1} failed: ${errorMessage}`);
results.push({ isValid: true, guardrailIndex: i, warning: `Promise rejected: ${errorMessage}` });
}
}
if (warnings.length > 0) {
safeConsole.warn(`[JAF:GUARDRAILS] ${warnings.length} guardrail warnings:`, warnings);
}
for (const result of results) {
if (!result.isValid) {
const errorMessage = 'errorMessage' in result ? result.errorMessage : 'Guardrail violation';
safeConsole.log(`🚨 Input guardrail ${result.guardrailIndex + 1} violation: ${errorMessage}`);
config.onEvent?.({
type: 'guardrail_violation',
data: { stage: 'input', reason: errorMessage }
});
return { isValid: false, errorMessage };
}
}
safeConsole.log(`✅ All input guardrails passed.`);
return { isValid: true };
}
catch (error) {
safeConsole.error(`[JAF:GUARDRAILS] Catastrophic failure in input guardrail execution:`, error);
return { isValid: true };
}
}
export async function executeOutputGuardrails(outputGuardrails, output, config) {
if (outputGuardrails.length === 0) {
return { isValid: true };
}
safeConsole.log(`[JAF:GUARDRAILS] Checking ${outputGuardrails.length} output guardrails`);
for (let i = 0; i < outputGuardrails.length; i++) {
const guardrail = outputGuardrails[i];
const guardrailName = `output-guardrail-${i + 1}`;
try {
const timeoutMs = 15000;
const guardrailResult = guardrail(output);
const result = await withTimeout(Promise.resolve(guardrailResult), timeoutMs, `${guardrailName} execution timed out after ${timeoutMs}ms`);
if (!result.isValid) {
const errorMessage = 'errorMessage' in result ? result.errorMessage : 'Guardrail violation';
safeConsole.log(`🚨 ${guardrailName} violation: ${errorMessage}`);
config.onEvent?.({
type: 'guardrail_violation',
data: { stage: 'output', reason: errorMessage }
});
return { isValid: false, errorMessage };
}
safeConsole.log(`✅ ${guardrailName} passed`);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
safeConsole.error(`[JAF:GUARDRAILS] ${guardrailName} failed:`, {
error: errorMessage,
index: i,
stack: error instanceof Error ? error.stack : undefined
});
const isSystemError = errorMessage.includes('Timeout') || errorMessage.includes('Circuit breaker');
if (isSystemError) {
safeConsole.warn(`[JAF:GUARDRAILS] ${guardrailName} system error, allowing output: ${errorMessage}`);
continue;
}
else {
config.onEvent?.({
type: 'guardrail_violation',
data: { stage: 'output', reason: errorMessage }
});
return { isValid: false, errorMessage };
}
}
}
safeConsole.log(`✅ All output guardrails passed`);
return { isValid: true };
}
export function cleanupCircuitBreakers() {
for (const [key, breaker] of circuitBreakers.entries()) {
if (breaker.shouldBeCleanedUp(CIRCUIT_BREAKER_CLEANUP_MAX_AGE)) {
circuitBreakers.delete(key);
}
}
}
export const guardrailCacheManager = {
getStats: () => guardrailCache.getStats(),
clear: () => guardrailCache.clear(),
getMetrics: () => {
const stats = guardrailCache.getStats();
return {
...stats,
utilizationPercent: (stats.size / stats.maxSize) * 100,
circuitBreakersCount: circuitBreakers.size
};
},
logStats: () => {
const metrics = guardrailCacheManager.getMetrics();
safeConsole.log('[JAF:GUARDRAILS] Cache stats:', metrics);
},
cleanup: () => {
cleanupCircuitBreakers();
}
};
//# sourceMappingURL=guardrails.js.map