@xynehq/jaf
Version:
Juspay Agent Framework - A purely functional agent framework with immutable state and composable tools
438 lines • 18.1 kB
JavaScript
export async function run(initialState, config) {
try {
config.onEvent?.({
type: 'run_start',
data: { runId: initialState.runId, traceId: initialState.traceId }
});
// Load conversation history from memory if configured
let stateWithMemory = initialState;
if (config.memory?.autoStore && config.conversationId) {
console.log(`[JAF:ENGINE] Loading conversation history for ${config.conversationId}`);
stateWithMemory = await loadConversationHistory(initialState, config);
}
else {
console.log(`[JAF:ENGINE] Skipping memory load - autoStore: ${config.memory?.autoStore}, conversationId: ${config.conversationId}`);
}
const result = await runInternal(stateWithMemory, config);
// Store conversation history to memory if configured
if (config.memory?.autoStore && config.conversationId && result.finalState.messages.length > initialState.messages.length) {
console.log(`[JAF:ENGINE] Storing conversation history for ${config.conversationId}`);
await storeConversationHistory(result.finalState, config);
}
else {
console.log(`[JAF:ENGINE] Skipping memory store - autoStore: ${config.memory?.autoStore}, conversationId: ${config.conversationId}, messageChange: ${result.finalState.messages.length > initialState.messages.length}`);
}
config.onEvent?.({
type: 'run_end',
data: { outcome: result.outcome }
});
return result;
}
catch (error) {
const errorResult = {
finalState: initialState,
outcome: {
status: 'error',
error: {
_tag: 'ModelBehaviorError',
detail: error instanceof Error ? error.message : String(error)
}
}
};
config.onEvent?.({
type: 'run_end',
data: { outcome: errorResult.outcome }
});
return errorResult;
}
}
async function runInternal(state, config) {
if (state.turnCount === 0) {
const firstUserMessage = state.messages.find(m => m.role === 'user');
if (firstUserMessage && config.initialInputGuardrails) {
for (const guardrail of config.initialInputGuardrails) {
const result = await guardrail(firstUserMessage.content);
if (!result.isValid) {
return {
finalState: state,
outcome: {
status: 'error',
error: {
_tag: 'InputGuardrailTripwire',
reason: result.errorMessage
}
}
};
}
}
}
}
const maxTurns = config.maxTurns ?? 50;
if (state.turnCount >= maxTurns) {
return {
finalState: state,
outcome: {
status: 'error',
error: {
_tag: 'MaxTurnsExceeded',
turns: state.turnCount
}
}
};
}
const currentAgent = config.agentRegistry.get(state.currentAgentName);
if (!currentAgent) {
return {
finalState: state,
outcome: {
status: 'error',
error: {
_tag: 'AgentNotFound',
agentName: state.currentAgentName
}
}
};
}
console.log(`[JAF:ENGINE] Using agent: ${currentAgent.name}`);
console.log(`[JAF:ENGINE] Agent has ${currentAgent.tools?.length || 0} tools available`);
if (currentAgent.tools) {
console.log(`[JAF:ENGINE] Available tools:`, currentAgent.tools.map(t => t.schema.name));
}
const model = config.modelOverride ?? currentAgent.modelConfig?.name ?? "gpt-4o";
config.onEvent?.({
type: 'llm_call_start',
data: { agentName: currentAgent.name, model }
});
const llmResponse = await config.modelProvider.getCompletion(state, currentAgent, config);
config.onEvent?.({
type: 'llm_call_end',
data: { choice: llmResponse }
});
if (!llmResponse.message) {
return {
finalState: state,
outcome: {
status: 'error',
error: {
_tag: 'ModelBehaviorError',
detail: 'No message in model response'
}
}
};
}
const assistantMessage = {
role: 'assistant',
content: llmResponse.message.content || '',
tool_calls: llmResponse.message.tool_calls
};
const newMessages = [...state.messages, assistantMessage];
if (llmResponse.message.tool_calls && llmResponse.message.tool_calls.length > 0) {
console.log(`[JAF:ENGINE] Processing ${llmResponse.message.tool_calls.length} tool calls`);
console.log(`[JAF:ENGINE] Tool calls:`, llmResponse.message.tool_calls);
const toolResults = await executeToolCalls(llmResponse.message.tool_calls, currentAgent, state, config);
console.log(`[JAF:ENGINE] Tool execution completed. Results count:`, toolResults.length);
if (toolResults.some(r => r.isHandoff)) {
const handoffResult = toolResults.find(r => r.isHandoff);
if (handoffResult) {
const targetAgent = handoffResult.targetAgent;
if (!currentAgent.handoffs?.includes(targetAgent)) {
return {
finalState: { ...state, messages: newMessages },
outcome: {
status: 'error',
error: {
_tag: 'HandoffError',
detail: `Agent ${currentAgent.name} cannot handoff to ${targetAgent}`
}
}
};
}
config.onEvent?.({
type: 'handoff',
data: { from: currentAgent.name, to: targetAgent }
});
const nextState = {
...state,
messages: [...newMessages, ...toolResults.map(r => r.message)],
currentAgentName: targetAgent,
turnCount: state.turnCount + 1
};
return runInternal(nextState, config);
}
}
const nextState = {
...state,
messages: [...newMessages, ...toolResults.map(r => r.message)],
turnCount: state.turnCount + 1
};
return runInternal(nextState, config);
}
if (llmResponse.message.content) {
if (currentAgent.outputCodec) {
const parseResult = currentAgent.outputCodec.safeParse(tryParseJSON(llmResponse.message.content));
if (!parseResult.success) {
return {
finalState: { ...state, messages: newMessages },
outcome: {
status: 'error',
error: {
_tag: 'DecodeError',
errors: parseResult.error.issues
}
}
};
}
if (config.finalOutputGuardrails) {
for (const guardrail of config.finalOutputGuardrails) {
const result = await guardrail(parseResult.data);
if (!result.isValid) {
return {
finalState: { ...state, messages: newMessages },
outcome: {
status: 'error',
error: {
_tag: 'OutputGuardrailTripwire',
reason: result.errorMessage
}
}
};
}
}
}
return {
finalState: { ...state, messages: newMessages },
outcome: {
status: 'completed',
output: parseResult.data
}
};
}
else {
if (config.finalOutputGuardrails) {
for (const guardrail of config.finalOutputGuardrails) {
const result = await guardrail(llmResponse.message.content);
if (!result.isValid) {
return {
finalState: { ...state, messages: newMessages },
outcome: {
status: 'error',
error: {
_tag: 'OutputGuardrailTripwire',
reason: result.errorMessage
}
}
};
}
}
}
return {
finalState: { ...state, messages: newMessages },
outcome: {
status: 'completed',
output: llmResponse.message.content
}
};
}
}
return {
finalState: { ...state, messages: newMessages },
outcome: {
status: 'error',
error: {
_tag: 'ModelBehaviorError',
detail: 'Model produced neither content nor tool calls'
}
}
};
}
async function executeToolCalls(toolCalls, agent, state, config) {
const results = await Promise.all(toolCalls.map(async (toolCall) => {
config.onEvent?.({
type: 'tool_call_start',
data: {
toolName: toolCall.function.name,
args: tryParseJSON(toolCall.function.arguments)
}
});
try {
const tool = agent.tools?.find(t => t.schema.name === toolCall.function.name);
if (!tool) {
const errorResult = JSON.stringify({
error: "tool_not_found",
message: `Tool ${toolCall.function.name} not found`,
tool_name: toolCall.function.name,
});
config.onEvent?.({
type: 'tool_call_end',
data: { toolName: toolCall.function.name, result: errorResult }
});
return {
message: {
role: 'tool',
content: errorResult,
tool_call_id: toolCall.id
}
};
}
const rawArgs = tryParseJSON(toolCall.function.arguments);
const parseResult = tool.schema.parameters.safeParse(rawArgs);
if (!parseResult.success) {
const errorResult = JSON.stringify({
error: "validation_error",
message: `Invalid arguments for ${toolCall.function.name}: ${parseResult.error.message}`,
tool_name: toolCall.function.name,
validation_errors: parseResult.error.issues
});
config.onEvent?.({
type: 'tool_call_end',
data: { toolName: toolCall.function.name, result: errorResult }
});
return {
message: {
role: 'tool',
content: errorResult,
tool_call_id: toolCall.id
}
};
}
console.log(`[JAF:ENGINE] About to execute tool: ${toolCall.function.name}`);
console.log(`[JAF:ENGINE] Tool args:`, parseResult.data);
console.log(`[JAF:ENGINE] Tool context:`, state.context);
const toolResult = await tool.execute(parseResult.data, state.context);
// Handle both string and ToolResult formats
let resultString;
let toolResultObj = null;
if (typeof toolResult === 'string') {
resultString = toolResult;
console.log(`[JAF:ENGINE] Tool ${toolCall.function.name} returned string:`, resultString);
}
else {
// It's a ToolResult object
toolResultObj = toolResult;
const { toolResultToString } = await import('./tool-results');
resultString = toolResultToString(toolResult);
console.log(`[JAF:ENGINE] Tool ${toolCall.function.name} returned ToolResult:`, toolResult);
console.log(`[JAF:ENGINE] Converted to string:`, resultString);
}
config.onEvent?.({
type: 'tool_call_end',
data: {
toolName: toolCall.function.name,
result: resultString,
toolResult: toolResultObj,
status: toolResultObj?.status || 'success'
}
});
const handoffCheck = tryParseJSON(resultString);
if (handoffCheck && typeof handoffCheck === 'object' && 'handoff_to' in handoffCheck) {
return {
message: {
role: 'tool',
content: resultString,
tool_call_id: toolCall.id
},
isHandoff: true,
targetAgent: handoffCheck.handoff_to
};
}
return {
message: {
role: 'tool',
content: resultString,
tool_call_id: toolCall.id
}
};
}
catch (error) {
const errorResult = JSON.stringify({
error: "execution_error",
message: error instanceof Error ? error.message : String(error),
tool_name: toolCall.function.name,
});
config.onEvent?.({
type: 'tool_call_end',
data: { toolName: toolCall.function.name, result: errorResult }
});
return {
message: {
role: 'tool',
content: errorResult,
tool_call_id: toolCall.id
}
};
}
}));
return results;
}
function tryParseJSON(str) {
try {
return JSON.parse(str);
}
catch {
return str;
}
}
/**
* Load conversation history from memory and merge with initial state
*/
async function loadConversationHistory(initialState, config) {
if (!config.memory?.provider || !config.conversationId) {
return initialState;
}
const result = await config.memory.provider.getConversation(config.conversationId);
if (!result.success) {
console.warn(`[JAF:MEMORY] Failed to load conversation history: ${result.error.message}`);
return initialState;
}
if (!result.data) {
console.log(`[JAF:MEMORY] No existing conversation found for ${config.conversationId}`);
return initialState;
}
// Apply memory limits if configured
const maxMessages = config.memory.maxMessages || result.data.messages.length;
const memoryMessages = result.data.messages.slice(-maxMessages);
// Merge existing messages with new messages, avoiding duplicates
const combinedMessages = [...memoryMessages, ...initialState.messages];
console.log(`[JAF:MEMORY] Loaded ${memoryMessages.length} messages from memory for conversation ${config.conversationId}`);
console.log(`[JAF:MEMORY] Memory messages:`, memoryMessages.map(m => ({ role: m.role, content: m.content?.substring(0, 100) + '...' })));
console.log(`[JAF:MEMORY] New messages:`, initialState.messages.map(m => ({ role: m.role, content: m.content?.substring(0, 100) + '...' })));
console.log(`[JAF:MEMORY] Combined messages (${combinedMessages.length} total):`, combinedMessages.map(m => ({ role: m.role, content: m.content?.substring(0, 100) + '...' })));
return {
...initialState,
messages: combinedMessages
};
}
/**
* Store conversation history to memory
*/
async function storeConversationHistory(finalState, config) {
if (!config.memory?.provider || !config.conversationId) {
return;
}
// Apply compression threshold if configured
let messagesToStore = finalState.messages;
if (config.memory.compressionThreshold && messagesToStore.length > config.memory.compressionThreshold) {
// Keep first few messages and recent messages
const keepFirst = Math.floor(config.memory.compressionThreshold * 0.2);
const keepRecent = config.memory.compressionThreshold - keepFirst;
messagesToStore = [
...messagesToStore.slice(0, keepFirst),
...messagesToStore.slice(-keepRecent)
];
console.log(`[JAF:MEMORY] Compressed conversation from ${finalState.messages.length} to ${messagesToStore.length} messages`);
}
const metadata = {
userId: finalState.context?.userId,
traceId: finalState.traceId,
runId: finalState.runId,
agentName: finalState.currentAgentName,
turnCount: finalState.turnCount
};
const result = await config.memory.provider.storeMessages(config.conversationId, messagesToStore, metadata);
if (!result.success) {
console.warn(`[JAF:MEMORY] Failed to store conversation history: ${result.error.message}`);
return;
}
console.log(`[JAF:MEMORY] Stored ${messagesToStore.length} messages for conversation ${config.conversationId}`);
}
//# sourceMappingURL=engine.js.map