feat: Add intelligent auto-router and enhanced integrations

- Add intelligent-router.sh hook for automatic agent routing
- Add AUTO-TRIGGER-SUMMARY.md documentation
- Add FINAL-INTEGRATION-SUMMARY.md documentation
- Complete Prometheus integration (6 commands + 4 tools)
- Complete Dexto integration (12 commands + 5 tools)
- Enhanced Ralph with access to all agents
- Fix /clawd command (removed disable-model-invocation)
- Update hooks.json to v5 with intelligent routing
- 291 total skills now available
- All 21 commands with automatic routing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
admin
2026-01-28 00:27:56 +04:00
Unverified
parent 3b128ba3bd
commit b52318eeae
1724 changed files with 351216 additions and 0 deletions

View File

@@ -0,0 +1,272 @@
/**
* A2A Message Format Converters
*
* Bidirectional conversion between A2A protocol message format
* and Dexto's internal message format.
*
* These converters live at the server boundary, translating between
* wire format (A2A) and internal format (DextoAgent).
*/
import type { InternalMessage } from '@dexto/core';
import type { Message, Part, MessageRole, ConvertedMessage } from '../types.js';
import { randomUUID } from 'crypto';
/**
* Convert A2A message to internal format for agent.run().
*
* Extracts text, image, and file from A2A parts array.
* agent.run() expects these as separate parameters.
*
* @param a2aMsg A2A protocol message
* @returns Converted message parts for agent.run()
*/
export function a2aToInternalMessage(a2aMsg: Message): ConvertedMessage {
let text = '';
let image: ConvertedMessage['image'] | undefined;
let file: ConvertedMessage['file'] | undefined;
for (const part of a2aMsg.parts) {
switch (part.kind) {
case 'text':
text += (text ? ' ' : '') + part.text;
break;
case 'file': {
// Determine if this is an image or general file
const fileData = part.file;
const mimeType = fileData.mimeType || '';
const isImage = mimeType.startsWith('image/');
if (isImage && !image) {
// Treat as image (agent.run() supports one image)
const data = 'bytes' in fileData ? fileData.bytes : fileData.uri;
image = {
image: data,
mimeType: mimeType,
};
} else if (!file) {
// Take first file only (agent.run() supports one file)
const data = 'bytes' in fileData ? fileData.bytes : fileData.uri;
const fileObj: { data: string; mimeType: string; filename?: string } = {
data: data,
mimeType: mimeType,
};
if (fileData.name) {
fileObj.filename = fileData.name;
}
file = fileObj;
}
break;
}
case 'data':
// Convert structured data to JSON text
text += (text ? '\n' : '') + JSON.stringify(part.data, null, 2);
break;
}
}
return { text, image, file };
}
/**
* Convert internal message to A2A format.
*
* Maps Dexto's internal message structure to A2A protocol format.
*
* Role mapping:
* - 'user' → 'user'
* - 'assistant' → 'agent'
* - 'system' → filtered out (not part of A2A conversation)
* - 'tool' → 'agent' (tool results presented as agent responses)
*
* @param msg Internal message from session history
* @param taskId Optional task ID to associate message with
* @param contextId Optional context ID to associate message with
* @returns A2A protocol message or null if message should be filtered
*/
export function internalToA2AMessage(
msg: InternalMessage,
taskId?: string,
contextId?: string
): Message | null {
// Filter out system messages (internal context, not part of A2A conversation)
if (msg.role === 'system') {
return null;
}
// Map role
const role: MessageRole = msg.role === 'user' ? 'user' : 'agent';
// Convert content to parts
const parts: Part[] = [];
if (typeof msg.content === 'string') {
// Simple text content
if (msg.content) {
parts.push({ kind: 'text', text: msg.content });
}
} else if (msg.content === null) {
// Null content (tool-only messages) - skip for A2A
// These are internal details, not part of user-facing conversation
} else if (Array.isArray(msg.content)) {
// Multi-part content
for (const part of msg.content) {
switch (part.type) {
case 'text':
parts.push({ kind: 'text', text: part.text });
break;
case 'image': {
const imageData = part.image;
const mimeType = part.mimeType || 'image/png';
// Convert different input types to base64 or URL
let fileObj: any;
if (
imageData instanceof URL ||
(typeof imageData === 'string' && imageData.startsWith('http'))
) {
// URL reference
fileObj = {
uri: imageData.toString(),
mimeType,
};
} else if (Buffer.isBuffer(imageData)) {
// Buffer -> base64
fileObj = {
bytes: imageData.toString('base64'),
mimeType,
};
} else if (imageData instanceof Uint8Array) {
// Uint8Array -> base64
fileObj = {
bytes: Buffer.from(imageData).toString('base64'),
mimeType,
};
} else if (imageData instanceof ArrayBuffer) {
// ArrayBuffer -> base64
fileObj = {
bytes: Buffer.from(imageData).toString('base64'),
mimeType,
};
} else if (typeof imageData === 'string') {
// Assume already base64 if string but not a URL
fileObj = {
bytes: imageData,
mimeType,
};
}
if (fileObj) {
parts.push({
kind: 'file',
file: fileObj,
});
}
break;
}
case 'file': {
const fileData = part.data;
const mimeType = part.mimeType;
// Convert different input types to base64 or URL
let fileObj: any;
if (
fileData instanceof URL ||
(typeof fileData === 'string' && fileData.startsWith('http'))
) {
// URL reference
fileObj = {
uri: fileData.toString(),
mimeType,
};
} else if (Buffer.isBuffer(fileData)) {
// Buffer -> base64
fileObj = {
bytes: fileData.toString('base64'),
mimeType,
};
} else if (fileData instanceof Uint8Array) {
// Uint8Array -> base64
fileObj = {
bytes: Buffer.from(fileData).toString('base64'),
mimeType,
};
} else if (fileData instanceof ArrayBuffer) {
// ArrayBuffer -> base64
fileObj = {
bytes: Buffer.from(fileData).toString('base64'),
mimeType,
};
} else if (typeof fileData === 'string') {
// Assume already base64 if string but not a URL
fileObj = {
bytes: fileData,
mimeType,
};
}
if (fileObj) {
// Add filename if present
if (part.filename) {
fileObj.name = part.filename;
}
parts.push({
kind: 'file',
file: fileObj,
});
}
break;
}
}
}
}
// If no parts, return null (don't include empty messages in A2A)
if (parts.length === 0) {
return null;
}
const message: Message = {
role,
parts,
messageId: randomUUID(),
kind: 'message',
};
if (taskId) message.taskId = taskId;
if (contextId) message.contextId = contextId;
return message;
}
/**
* Convert array of internal messages to A2A messages.
*
* Filters out system messages and empty messages.
*
* @param messages Internal messages from session history
* @param taskId Optional task ID to associate messages with
* @param contextId Optional context ID to associate messages with
* @returns Array of A2A protocol messages
*/
export function internalMessagesToA2A(
messages: InternalMessage[],
taskId?: string,
contextId?: string
): Message[] {
const a2aMessages: Message[] = [];
for (const msg of messages) {
const a2aMsg = internalToA2AMessage(msg, taskId, contextId);
if (a2aMsg !== null) {
a2aMessages.push(a2aMsg);
}
}
return a2aMessages;
}