feat(subagents): add multi-turn subagent session runtime

This commit is contained in:
William Valentin
2026-02-26 13:07:34 -08:00
parent e887c3c964
commit 2171346116
21 changed files with 1111 additions and 12 deletions
+58 -7
View File
@@ -3,13 +3,13 @@ import type { Attachment } from '../channels/types.js';
import { isSupportedAudio, transcribeAudio } from '../models/media.js';
import { synthesizeSpeechAttachment } from '../models/tts.js';
import { supportsAudioInput } from '../models/capabilities.js';
import { AgentOrchestrator, type DelegationConfig } from '../backends/index.js';
import { AgentOrchestrator, SubagentManager, type DelegationConfig } from '../backends/index.js';
import { OutboundAttachmentCollector } from '../backends/native/attachments.js';
import type { ExternalBackend, ExternalBackendName } from '../backends/index.js';
import type { InboundMessage, OutboundMessage } from '../channels/index.js';
import { MemoryStore } from '../memory/index.js';
import type { Tool } from '../tools/types.js';
import { createMediaSendTool, createAgentDelegateTool, createCouncilRunTool } from '../tools/index.js';
import { createMediaSendTool, createAgentDelegateTool, createCouncilRunTool, createSubagentTools } from '../tools/index.js';
import type { AgentDelegateDeps } from '../tools/index.js';
import { createSandboxedShellTool, createSandboxedProcessStartTool, SandboxManager } from '../sandbox/index.js';
import { MODEL_PROVIDERS, type Config, type CouncilsConfig, type ModelConfig, type ModelProvider } from '../config/index.js';
@@ -382,10 +382,18 @@ export function createMessageRouter(deps: {
setBackendMode?: (mode: BackendRuntimeMode) => void;
}): {
handler: (msg: InboundMessage, reply: (response: OutboundMessage) => Promise<void>) => Promise<void>;
agents: Map<string, { orchestrator: AgentOrchestrator; collector: OutboundAttachmentCollector }>;
agents: Map<string, {
orchestrator: AgentOrchestrator;
collector: OutboundAttachmentCollector;
subagentManager?: SubagentManager;
}>;
} {
// Cache agents by session ID + agent config name to avoid recreating on every message
const agents = new Map<string, { orchestrator: AgentOrchestrator; collector: OutboundAttachmentCollector }>();
const agents = new Map<string, {
orchestrator: AgentOrchestrator;
collector: OutboundAttachmentCollector;
subagentManager?: SubagentManager;
}>();
const talkModeUntil = new Map<string, number>();
const activeRuns = new Map<string, AgentOrchestrator>();
const reactionCooldowns = new Map<string, number>();
@@ -530,7 +538,16 @@ export function createMessageRouter(deps: {
}
}
function getOrCreateAgent(channel: string, senderId: string, metadata?: Record<string, unknown>, agentOverride?: string): { orchestrator: AgentOrchestrator; collector: OutboundAttachmentCollector } {
function getOrCreateAgent(
channel: string,
senderId: string,
metadata?: Record<string, unknown>,
agentOverride?: string,
): {
orchestrator: AgentOrchestrator;
collector: OutboundAttachmentCollector;
subagentManager?: SubagentManager;
} {
// Resolve agent config name via routing (sender → channel → default fallback)
const agentConfigName = agentOverride ?? deps.agentRouter?.resolve(channel, senderId);
const agentConfig = agentConfigName ? deps.agentConfigRegistry?.get(agentConfigName) : undefined;
@@ -664,6 +681,28 @@ export function createMessageRouter(deps: {
effectiveToolRegistry = effectiveToolRegistry.clone();
effectiveToolRegistry.register(createMediaSendTool(collector));
let subagentManager: SubagentManager | undefined;
const subagentsEnabled = deps.config.agents.subagents?.enabled ?? true;
const maxSubagentSessions = deps.config.agents.subagents?.max_active_sessions ?? 6;
if (subagentsEnabled && deps.agentConfigRegistry && deps.agentConfigRegistry.list().length > 0) {
subagentManager = new SubagentManager({
parentSessionId: session.id,
modelRouter: deps.modelRouter,
sessionManager: deps.sessionManager,
toolRegistry: effectiveToolRegistry,
toolExecutor: deps.toolExecutor,
agentConfigRegistry: deps.agentConfigRegistry,
delegation: delegationConfig,
maxDelegationDepth: deps.config.agents.max_delegation_depth ?? 3,
defaultPrimaryTier: effectiveTier,
maxIterations: deps.config.agents.max_iterations,
maxActiveSessions: maxSubagentSessions,
});
for (const tool of createSubagentTools(subagentManager)) {
effectiveToolRegistry.register(tool);
}
}
// Register delegation tools with lazy orchestrator reference (resolved after construction)
let resolveOrchestrator: ((o: AgentOrchestrator) => void) | undefined;
if (deps.agentConfigRegistry && deps.agentConfigRegistry.list().length > 0) {
@@ -766,7 +805,7 @@ export function createMessageRouter(deps: {
// Resolve the lazy orchestrator reference for agent.delegate
resolveOrchestrator?.(orchestrator);
entry = { orchestrator, collector };
entry = { orchestrator, collector, subagentManager };
agents.set(sessionId, entry);
}
return entry;
@@ -960,7 +999,12 @@ export function createMessageRouter(deps: {
const agentConfigName = intentAgentOverride ?? deps.agentRouter?.resolve(msg.channel, msg.senderId);
const agentConfig = agentConfigName ? deps.agentConfigRegistry?.get(agentConfigName) : undefined;
const { orchestrator: agent, collector } = getOrCreateAgent(msg.channel, msg.senderId, effectiveMetadata, agentConfigName);
const { orchestrator: agent, collector, subagentManager } = getOrCreateAgent(
msg.channel,
msg.senderId,
effectiveMetadata,
agentConfigName,
);
const commandInput = msg.metadata?.isCommand && typeof msg.metadata.command === 'string'
? `/${msg.metadata.command}${msg.metadata.commandArgs ? ` ${msg.metadata.commandArgs}` : ''}`
@@ -999,6 +1043,13 @@ export function createMessageRouter(deps: {
names.add('council.run');
}
}
if (subagentManager) {
names.add('subagent.spawn');
names.add('subagent.send');
names.add('subagent.list');
names.add('subagent.cancel');
names.add('subagent.delete');
}
const sorted = [...names].sort();
return [
`Available tools (${sorted.length}):`,