refactor(modules): extract agent-to-agent as registry-based module

Last extraction of Phase 3. Moves inter-agent messaging + create_agent +
destination projection into src/modules/agent-to-agent/. Core retains:

- `channel_type === 'agent'` dispatch in delivery.ts, guarded by
  hasTable('agent_destinations') + dynamic import into module.
- Channel-permission ACL in delivery.ts, guarded by hasTable, with
  inlined SQL (no module import from core).
- writeDestinations call in container-runner.ts, guarded by hasTable +
  dynamic import into module.
- createMessagingGroupAgent's destination side effect in db/messaging-groups.ts,
  guarded by hasTable. This is a documented transitional tier violation
  (core imports from optional module), analogous to src/access.ts.

Migration `004-agent-destinations.ts` renamed to `module-agent-to-agent-
destinations.ts` preserving `name: 'agent-destinations'` so existing DBs
don't re-run it.

delivery.ts: 600 → 449 lines. handleSystemAction's last switch case gone
(just registry + default log-and-drop). notifyAgent helper removed (only
create_agent used it).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-04-18 19:00:10 +03:00
parent c80a23e24f
commit 46b19dcf9c
13 changed files with 345 additions and 264 deletions

View File

@@ -15,9 +15,6 @@ import fs from 'fs';
import path from 'path';
import { DATA_DIR } from './config.js';
import { getAgentGroup } from './db/agent-groups.js';
import { getDestinations } from './db/agent-destinations.js';
import { getDb, hasTable } from './db/connection.js';
import { getMessagingGroup } from './db/messaging-groups.js';
import { createSession, findSession, findSessionByAgentGroup, getSession, updateSession } from './db/sessions.js';
import {
@@ -25,10 +22,8 @@ import {
openInboundDb as openInboundDbRaw,
openOutboundDb as openOutboundDbRaw,
upsertSessionRouting,
replaceDestinations,
insertMessage,
migrateMessagesInTable,
type DestinationRow,
} from './db/session-db.js';
import { log } from './log.js';
import type { Session } from './types.js';
@@ -130,16 +125,6 @@ export function initSessionFolder(agentGroupId: string, sessionId: string): void
ensureSchema(outboundDbPath(agentGroupId, sessionId), 'outbound');
}
/**
* Write the session's destination map into its inbound.db `destinations` table.
*
* Called before every container wake so admin changes take effect on next start —
* but the container also re-queries on demand, so mid-session admin changes
* (e.g. spawning a new child agent) can also call this to push the new map
* without restarting the container.
*
* Uses DELETE + INSERT in a transaction for a clean overwrite.
*/
/**
* Write the default reply routing for a session into its inbound.db.
*
@@ -147,8 +132,9 @@ export function initSessionFolder(agentGroupId: string, sessionId: string): void
* for outbound messages when the agent doesn't specify an explicit destination.
* Derived from session.messaging_group_id → messaging_groups row + session.thread_id.
*
* Called on every container wake alongside writeDestinations() so the latest
* routing is always in place, including after admin rewiring.
* Called on every container wake alongside the agent-to-agent module's
* writeDestinations() (when installed) so the latest routing is always in
* place, including after admin rewiring.
*/
export function writeSessionRouting(agentGroupId: string, sessionId: string): void {
const dbPath = inboundDbPath(agentGroupId, sessionId);
@@ -180,53 +166,6 @@ export function writeSessionRouting(agentGroupId: string, sessionId: string): vo
log.debug('Session routing written', { sessionId, channelType, platformId, threadId: session.thread_id });
}
export function writeDestinations(agentGroupId: string, sessionId: string): void {
const dbPath = inboundDbPath(agentGroupId, sessionId);
if (!fs.existsSync(dbPath)) return;
// Guarded: when the agent-to-agent module isn't installed, the
// `agent_destinations` table doesn't exist. Skip silently — core
// container spawn continues without projecting destinations.
if (!hasTable(getDb(), 'agent_destinations')) return;
const rows = getDestinations(agentGroupId);
const resolved: DestinationRow[] = [];
for (const row of rows) {
if (row.target_type === 'channel') {
const mg = getMessagingGroup(row.target_id);
if (!mg) continue;
resolved.push({
name: row.local_name,
display_name: mg.name ?? row.local_name,
type: 'channel',
channel_type: mg.channel_type,
platform_id: mg.platform_id,
agent_group_id: null,
});
} else if (row.target_type === 'agent') {
const ag = getAgentGroup(row.target_id);
if (!ag) continue;
resolved.push({
name: row.local_name,
display_name: ag.name,
type: 'agent',
channel_type: null,
platform_id: null,
agent_group_id: ag.id,
});
}
}
const db = openInboundDb(agentGroupId, sessionId);
try {
replaceDestinations(db, resolved);
} finally {
db.close();
}
log.debug('Destination map written', { sessionId, count: resolved.length });
}
/**
* Write a message to a session's inbound DB (messages_in). Host-only.
*