@xynehq/jaf
Version:
Juspay Agent Framework - A purely functional agent framework with immutable state and composable tools
337 lines • 12.5 kB
JavaScript
/**
* Pure functional A2A client
* All client operations are pure functions
*/
// Pure function to create A2A client
export const createA2AClient = (baseUrl, config) => ({
config: {
baseUrl: baseUrl.replace(/\/$/, ''), // Remove trailing slash
timeout: config?.timeout || 30000
},
sessionId: `client_${Date.now()}_${Math.random().toString(36).substring(2)}`
});
// Pure function to create message request
export const createMessageRequest = (message, sessionId, configuration) => ({
jsonrpc: '2.0',
id: `req_${Date.now()}_${Math.random().toString(36).substring(2)}`,
method: 'message/send',
params: {
message: {
role: 'user',
parts: [{ kind: 'text', text: message }],
messageId: `msg_${Date.now()}_${Math.random().toString(36).substring(2)}`,
contextId: sessionId,
kind: 'message'
},
configuration
}
});
// Pure function to create streaming message request
export const createStreamingMessageRequest = (message, sessionId, configuration) => ({
jsonrpc: '2.0',
id: `req_${Date.now()}_${Math.random().toString(36).substring(2)}`,
method: 'message/stream',
params: {
message: {
role: 'user',
parts: [{ kind: 'text', text: message }],
messageId: `msg_${Date.now()}_${Math.random().toString(36).substring(2)}`,
contextId: sessionId,
kind: 'message'
},
configuration
}
});
// Pure function to send HTTP request
const sendHttpRequest = async (url, body, timeout = 30000) => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(body),
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
}
catch (error) {
clearTimeout(timeoutId);
if (error instanceof Error && error.name === 'AbortError') {
throw new Error(`Request timeout after ${timeout}ms`);
}
throw error;
}
};
// Pure function to send A2A request
export const sendA2ARequest = async (client, request) => {
const url = `${client.config.baseUrl}/a2a`;
return await sendHttpRequest(url, request, client.config.timeout);
};
// Pure function to send message
export const sendMessage = async (client, message, configuration) => {
const request = createMessageRequest(message, client.sessionId, configuration);
const response = await sendA2ARequest(client, request);
if (response.error) {
throw new Error(`A2A Error ${response.error.code}: ${response.error.message}`);
}
return extractTextResponse(response.result);
};
// Pure function to stream message
export const streamMessage = async function* (client, message, configuration) {
const request = createStreamingMessageRequest(message, client.sessionId, configuration);
const url = `${client.config.baseUrl}/a2a`;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), client.config.timeout);
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'text/event-stream'
},
body: JSON.stringify(request),
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
if (!response.body) {
throw new Error('No response body for streaming');
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done)
break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data.trim()) {
try {
const event = JSON.parse(data);
if (event.result) {
yield event.result;
}
}
catch (error) {
console.warn('Failed to parse SSE data:', data);
}
}
}
}
}
}
finally {
reader.releaseLock();
}
}
catch (error) {
clearTimeout(timeoutId);
if (error instanceof Error && error.name === 'AbortError') {
throw new Error(`Stream timeout after ${client.config.timeout}ms`);
}
throw error;
}
};
// Pure function to get agent card
export const getAgentCard = async (client) => {
const url = `${client.config.baseUrl}/.well-known/agent-card`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Accept': 'application/json'
}
});
if (!response.ok) {
throw new Error(`Failed to get agent card: HTTP ${response.status}`);
}
return await response.json();
};
// Pure function to discover agents
export const discoverAgents = async (baseUrl) => {
const client = createA2AClient(baseUrl);
return await getAgentCard(client);
};
// Pure function to send message to specific agent
export const sendMessageToAgent = async (client, agentName, message, configuration) => {
const request = createMessageRequest(message, client.sessionId, configuration);
const url = `${client.config.baseUrl}/a2a/agents/${agentName}`;
const response = await sendHttpRequest(url, request, client.config.timeout);
if (response.error) {
throw new Error(`A2A Error ${response.error.code}: ${response.error.message}`);
}
return extractTextResponse(response.result);
};
// Pure function to stream message to specific agent
export const streamMessageToAgent = async function* (client, agentName, message, configuration) {
const request = createStreamingMessageRequest(message, client.sessionId, configuration);
const url = `${client.config.baseUrl}/a2a/agents/${agentName}`;
// Use same streaming logic as general streamMessage
yield* streamToUrl(url, request, client.config.timeout);
};
// Pure helper function for streaming to URL
const streamToUrl = async function* (url, request, timeout = 30000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'text/event-stream'
},
body: JSON.stringify(request),
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
if (!response.body) {
throw new Error('No response body for streaming');
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done)
break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data.trim()) {
try {
const event = JSON.parse(data);
if (event.result) {
yield event.result;
}
}
catch (error) {
console.warn('Failed to parse SSE data:', data);
}
}
}
}
}
}
finally {
reader.releaseLock();
}
}
catch (error) {
clearTimeout(timeoutId);
if (error instanceof Error && error.name === 'AbortError') {
throw new Error(`Stream timeout after ${timeout}ms`);
}
throw error;
}
};
// Pure function to extract text response
export const extractTextResponse = (result) => {
// Handle direct string response
if (typeof result === 'string') {
return result;
}
// Handle task response
if (result?.kind === 'task') {
// Extract from artifacts
if (result.artifacts?.length > 0) {
const textArtifact = result.artifacts.find((artifact) => artifact.parts?.some((part) => part.kind === 'text'));
if (textArtifact) {
const textPart = textArtifact.parts.find((part) => part.kind === 'text');
return textPart?.text || 'No text content';
}
}
// Extract from history
if (result.history?.length > 0) {
const lastMessage = result.history[result.history.length - 1];
if (lastMessage?.parts) {
const textParts = lastMessage.parts
.filter((part) => part.kind === 'text')
.map((part) => part.text);
if (textParts.length > 0) {
return textParts.join('\n');
}
}
}
return 'Task completed but no text response available';
}
// Handle message response
if (result?.kind === 'message') {
if (result.parts) {
const textParts = result.parts
.filter((part) => part.kind === 'text')
.map((part) => part.text);
if (textParts.length > 0) {
return textParts.join('\n');
}
}
}
// Handle object responses
if (typeof result === 'object' && result !== null) {
return JSON.stringify(result, null, 2);
}
return 'No response content available';
};
// Pure function to check A2A server health
export const checkA2AHealth = async (client) => {
const url = `${client.config.baseUrl}/a2a/health`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Accept': 'application/json'
}
});
if (!response.ok) {
throw new Error(`Health check failed: HTTP ${response.status}`);
}
return await response.json();
};
// Pure function to get A2A capabilities
export const getA2ACapabilities = async (client) => {
const url = `${client.config.baseUrl}/a2a/capabilities`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Accept': 'application/json'
}
});
if (!response.ok) {
throw new Error(`Capabilities request failed: HTTP ${response.status}`);
}
return await response.json();
};
// Pure function to connect to A2A agent (convenience function)
export const connectToA2AAgent = async (baseUrl) => {
const client = createA2AClient(baseUrl);
const agentCard = await getAgentCard(client);
return {
client,
agentCard,
ask: (message, config) => sendMessage(client, message, config),
stream: (message, config) => streamMessage(client, message, config),
health: () => checkA2AHealth(client),
capabilities: () => getA2ACapabilities(client)
};
};
//# sourceMappingURL=client.js.map