feat: agent-to-agent communication, dynamic agent creation, self-modification tools

Agent-to-agent: host routes messages with channel_type='agent' to target
agent's inbound.db, enriches with sender info, wakes target container.
Bidirectional routing works via inherited routing context.

Dynamic agents: create_agent MCP tool + system action handler creates
agent groups, folders, and optional CLAUDE.md on the fly.

Self-modification: install_packages (apt/npm, requires admin approval),
add_mcp_server (no approval), request_rebuild (builds per-agent-group
Docker image with approved packages). Approval flow reuses interactive
card infrastructure with pending_approvals table.

Also includes fixes from prior session: attachment download, reply context
extraction, message editing (platform message ID tracking), delivery retry
limits, and card update on button click.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-04-10 01:10:34 +03:00
parent 9af9bc947a
commit d8fbd3b239
24 changed files with 1025 additions and 78 deletions

View File

@@ -109,13 +109,7 @@ function formatChatMessages(messages: MessageInRow[]): string {
const lines = ['<messages>'];
for (const msg of messages) {
const content = parseContent(msg.content);
const sender = content.sender || content.author?.fullName || content.author?.userName || 'Unknown';
const time = formatTime(msg.timestamp);
const text = content.text || '';
const idAttr = msg.seq != null ? ` id="${msg.seq}"` : '';
const attachmentsSuffix = formatAttachments(content.attachments);
lines.push(`<message${idAttr} sender="${escapeXml(sender)}" time="${time}">${escapeXml(text)}${attachmentsSuffix}</message>`);
lines.push(formatSingleChat(msg));
}
lines.push('</messages>');
return lines.join('\n');
@@ -127,8 +121,9 @@ function formatSingleChat(msg: MessageInRow): string {
const time = formatTime(msg.timestamp);
const text = content.text || '';
const idAttr = msg.seq != null ? ` id="${msg.seq}"` : '';
const replyPrefix = formatReplyContext(content.replyTo);
const attachmentsSuffix = formatAttachments(content.attachments);
return `<message${idAttr} sender="${escapeXml(sender)}" time="${time}">${escapeXml(text)}${attachmentsSuffix}</message>`;
return `<message${idAttr} sender="${escapeXml(sender)}" time="${time}">${replyPrefix}${escapeXml(text)}${attachmentsSuffix}</message>`;
}
function formatTaskMessage(msg: MessageInRow): string {
@@ -153,13 +148,26 @@ function formatSystemMessage(msg: MessageInRow): string {
return `[SYSTEM RESPONSE]\n\nAction: ${content.action || 'unknown'}\nStatus: ${content.status || 'unknown'}\nResult: ${JSON.stringify(content.result || null)}`;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function formatReplyContext(replyTo: any): string {
if (!replyTo) return '';
const sender = replyTo.sender || 'Unknown';
const text = replyTo.text || '';
const preview = text.length > 100 ? text.slice(0, 100) + '…' : text;
return `\n<reply-to sender="${escapeXml(sender)}">${escapeXml(preview)}</reply-to>\n`;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function formatAttachments(attachments: any[] | undefined): string {
if (!Array.isArray(attachments) || attachments.length === 0) return '';
const parts = attachments.map((a) => {
const name = a.name || a.filename || 'attachment';
const type = a.type || 'file';
const localPath = a.localPath ? `/workspace/${a.localPath}` : '';
const url = a.url || '';
if (localPath) {
return `[${type}: ${escapeXml(name)} — saved to ${escapeXml(localPath)}]`;
}
return url ? `[${type}: ${escapeXml(name)} (${escapeXml(url)})]` : `[${type}: ${escapeXml(name)}]`;
});
return '\n' + parts.join('\n');