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:
@@ -64,7 +64,7 @@ function generateId(): string {
|
||||
*/
|
||||
export function resolveSession(
|
||||
agentGroupId: string,
|
||||
messagingGroupId: string,
|
||||
messagingGroupId: string | null,
|
||||
threadId: string | null,
|
||||
sessionMode: 'shared' | 'per-thread' | 'agent-shared',
|
||||
): { session: Session; created: boolean } {
|
||||
@@ -74,7 +74,7 @@ export function resolveSession(
|
||||
if (existing) {
|
||||
return { session: existing, created: false };
|
||||
}
|
||||
} else {
|
||||
} else if (messagingGroupId) {
|
||||
const lookupThreadId = sessionMode === 'shared' ? null : threadId;
|
||||
const existing = findSession(messagingGroupId, lookupThreadId);
|
||||
if (existing) {
|
||||
@@ -144,6 +144,9 @@ export function writeSessionMessage(
|
||||
recurrence?: string | null;
|
||||
},
|
||||
): void {
|
||||
// Extract base64 attachment data, save to inbox, replace with file paths
|
||||
const content = extractAttachmentFiles(agentGroupId, sessionId, message.id, message.content);
|
||||
|
||||
const dbPath = inboundDbPath(agentGroupId, sessionId);
|
||||
const db = new Database(dbPath);
|
||||
db.pragma('journal_mode = DELETE');
|
||||
@@ -166,7 +169,7 @@ export function writeSessionMessage(
|
||||
platformId: message.platformId ?? null,
|
||||
channelType: message.channelType ?? null,
|
||||
threadId: message.threadId ?? null,
|
||||
content: message.content,
|
||||
content,
|
||||
processAfter: message.processAfter ?? null,
|
||||
recurrence: message.recurrence ?? null,
|
||||
});
|
||||
@@ -177,6 +180,44 @@ export function writeSessionMessage(
|
||||
updateSession(sessionId, { last_active: new Date().toISOString() });
|
||||
}
|
||||
|
||||
/**
|
||||
* If message content has attachments with base64 `data`, save them to
|
||||
* the session's inbox directory and replace with `localPath`.
|
||||
*/
|
||||
function extractAttachmentFiles(
|
||||
agentGroupId: string,
|
||||
sessionId: string,
|
||||
messageId: string,
|
||||
contentStr: string,
|
||||
): string {
|
||||
let parsed: Record<string, unknown>;
|
||||
try {
|
||||
parsed = JSON.parse(contentStr);
|
||||
} catch {
|
||||
return contentStr;
|
||||
}
|
||||
|
||||
const attachments = parsed.attachments as Array<Record<string, unknown>> | undefined;
|
||||
if (!Array.isArray(attachments)) return contentStr;
|
||||
|
||||
let changed = false;
|
||||
for (const att of attachments) {
|
||||
if (typeof att.data === 'string') {
|
||||
const inboxDir = path.join(sessionDir(agentGroupId, sessionId), 'inbox', messageId);
|
||||
fs.mkdirSync(inboxDir, { recursive: true });
|
||||
const filename = (att.name as string) || `attachment-${Date.now()}`;
|
||||
const filePath = path.join(inboxDir, filename);
|
||||
fs.writeFileSync(filePath, Buffer.from(att.data as string, 'base64'));
|
||||
att.localPath = `inbox/${messageId}/${filename}`;
|
||||
delete att.data;
|
||||
changed = true;
|
||||
log.debug('Saved attachment to inbox', { messageId, filename, size: att.size });
|
||||
}
|
||||
}
|
||||
|
||||
return changed ? JSON.stringify(parsed) : contentStr;
|
||||
}
|
||||
|
||||
/** Open the inbound DB for a session (host reads/writes). */
|
||||
export function openInboundDb(agentGroupId: string, sessionId: string): Database.Database {
|
||||
const dbPath = inboundDbPath(agentGroupId, sessionId);
|
||||
@@ -201,6 +242,27 @@ export function openSessionDb(agentGroupId: string, sessionId: string): Database
|
||||
return openInboundDb(agentGroupId, sessionId);
|
||||
}
|
||||
|
||||
/** Write a system response to a session's inbound.db so the container's findQuestionResponse() picks it up. */
|
||||
export function writeSystemResponse(
|
||||
agentGroupId: string,
|
||||
sessionId: string,
|
||||
requestId: string,
|
||||
status: string,
|
||||
result: Record<string, unknown>,
|
||||
): void {
|
||||
writeSessionMessage(agentGroupId, sessionId, {
|
||||
id: `sys-resp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
kind: 'system',
|
||||
timestamp: new Date().toISOString(),
|
||||
content: JSON.stringify({
|
||||
type: 'question_response',
|
||||
questionId: requestId,
|
||||
status,
|
||||
result,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
/** Mark a container as running for a session. */
|
||||
export function markContainerRunning(sessionId: string): void {
|
||||
updateSession(sessionId, { container_status: 'running', last_active: new Date().toISOString() });
|
||||
|
||||
Reference in New Issue
Block a user