From 54cbf1713391925742ca5e0de453c98288c24bec Mon Sep 17 00:00:00 2001 From: William Valentin Date: Sat, 21 Feb 2026 10:49:20 -0800 Subject: [PATCH] feat(runtime): wire council command and routing integration --- src/commands/builtin/index.test.ts | 30 +++++++++++++++++++++- src/commands/builtin/index.ts | 24 +++++++++++++++++ src/commands/types.ts | 1 + src/daemon/routing.ts | 41 +++++++++++++++++++++++++++--- 4 files changed, 92 insertions(+), 4 deletions(-) diff --git a/src/commands/builtin/index.test.ts b/src/commands/builtin/index.test.ts index 5e0407d..898f8b0 100644 --- a/src/commands/builtin/index.test.ts +++ b/src/commands/builtin/index.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, vi } from 'vitest'; -import { createApproveCommand, createApprovalsCommand, createContextCommand, createDenyCommand, createElevateCommand, createModelCommand, createQueueCommand, createResearchCommand, createSkillCommand, createStopCommand, createTransferCommand } from './index.js'; +import { createApproveCommand, createApprovalsCommand, createContextCommand, createCouncilCommand, createDenyCommand, createElevateCommand, createModelCommand, createQueueCommand, createResearchCommand, createSkillCommand, createStopCommand, createTransferCommand } from './index.js'; describe('builtin /model command', () => { it('passes through the full argument string', async () => { @@ -66,6 +66,34 @@ describe('builtin /research command', () => { }); }); +describe('builtin /council command', () => { + it('passes the full task to runCouncil', async () => { + const cmd = createCouncilCommand(); + const runCouncil = vi.fn(() => 'council output'); + const result = await cmd.execute(['design', 'a', 'backup', 'strategy'], { + channel: 'test', + senderId: 'user', + sessionId: 's1', + rawInput: '/council design a backup strategy', + services: { runCouncil }, + }); + expect(runCouncil).toHaveBeenCalledWith('design a backup strategy'); + expect(result).toEqual({ handled: true, text: 'council output' }); + }); + + it('returns usage when no task is provided', async () => { + const cmd = createCouncilCommand(); + const result = await cmd.execute([], { + channel: 'test', + senderId: 'user', + sessionId: 's1', + rawInput: '/council', + services: {}, + }); + expect(result).toEqual({ handled: true, text: 'Usage: /council ' }); + }); +}); + describe('builtin /elevate command', () => { it('passes through the full argument string', async () => { const cmd = createElevateCommand(); diff --git a/src/commands/builtin/index.ts b/src/commands/builtin/index.ts index 1870b69..0e51018 100644 --- a/src/commands/builtin/index.ts +++ b/src/commands/builtin/index.ts @@ -235,6 +235,29 @@ export function createResearchCommand(): CommandDefinition { }; } +export function createCouncilCommand(): CommandDefinition { + return { + name: 'council', + description: 'Run the D/P councils pipeline for a task', + execute: async (args, ctx) => { + const task = args.join(' ').trim(); + if (!task) { + return { + handled: true, + text: 'Usage: /council ', + }; + } + if (!ctx.services?.runCouncil) { + return notAvailable('Council command'); + } + return { + handled: true, + text: await ctx.services.runCouncil(task), + }; + }, + }; +} + export function createTransferCommand(): CommandDefinition { return { name: 'transfer', @@ -323,6 +346,7 @@ export function registerBuiltinCommands(registry: CommandRegistry): void { registry.register(createUsageCommand()); registry.register(createContextCommand()); registry.register(createResearchCommand()); + registry.register(createCouncilCommand()); registry.register(createModelCommand()); registry.register(createCompactCommand()); registry.register(createResetCommand()); diff --git a/src/commands/types.ts b/src/commands/types.ts index dc81509..3c5d0a6 100644 --- a/src/commands/types.ts +++ b/src/commands/types.ts @@ -27,6 +27,7 @@ export interface CommandServices { compact?: () => Promise | string; reset?: () => Promise | string; delegateAgent?: (agentName: string, task: string) => Promise | string; + runCouncil?: (task: string) => Promise | string; getElevation?: () => Promise | string; setElevation?: (input: string) => Promise | string; diff --git a/src/daemon/routing.ts b/src/daemon/routing.ts index c7e80fc..832e051 100644 --- a/src/daemon/routing.ts +++ b/src/daemon/routing.ts @@ -9,10 +9,10 @@ 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 } from '../tools/index.js'; +import { createMediaSendTool, createAgentDelegateTool, createCouncilRunTool } 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 ModelConfig, type ModelProvider } from '../config/index.js'; +import { MODEL_PROVIDERS, type Config, type CouncilsConfig, type ModelConfig, type ModelProvider } from '../config/index.js'; import { ModelRouter, type ModelClient, type ModelTier } from '../models/index.js'; import { ToolRegistry, ToolExecutor } from '../tools/index.js'; import { SessionManager } from '../session/index.js'; @@ -384,7 +384,7 @@ export function createMessageRouter(deps: { effectiveToolRegistry = effectiveToolRegistry.clone(); effectiveToolRegistry.register(createMediaSendTool(collector)); - // Register agent.delegate tool with lazy orchestrator reference (resolved after construction) + // Register delegation tools with lazy orchestrator reference (resolved after construction) let resolveOrchestrator: ((o: AgentOrchestrator) => void) | undefined; if (deps.agentConfigRegistry && deps.agentConfigRegistry.list().length > 0) { let lazyOrchestrator: AgentOrchestrator | null = null; @@ -398,6 +398,19 @@ export function createMessageRouter(deps: { return lazyOrchestrator; }, } as AgentDelegateDeps)); + + if (deps.config.councils?.enabled) { + effectiveToolRegistry.register(createCouncilRunTool({ + registry: deps.agentConfigRegistry, + config: deps.config.councils as CouncilsConfig, + get orchestrator(): AgentOrchestrator { + if (!lazyOrchestrator) { + throw new Error('Agent orchestrator not yet initialized'); + } + return lazyOrchestrator; + }, + })); + } } const toolPolicyContext = { @@ -814,6 +827,28 @@ export function createMessageRouter(deps: { return `[Agent: ${target} | Tier: ${result.tier} | Tokens: ${result.usage.inputTokens}+${result.usage.outputTokens}]\n\n${result.content}`; }, + runCouncil: async (task: string) => { + const message = task.trim(); + if (!message) { + return 'Usage: /council '; + } + if (!deps.config.councils?.enabled) { + return 'Councils are disabled. Set councils.enabled: true in config.'; + } + if (!deps.agentConfigRegistry || deps.agentConfigRegistry.list().length === 0) { + return 'No agent configurations are registered. Add council_* agent_configs first.'; + } + const tool = createCouncilRunTool({ + registry: deps.agentConfigRegistry, + orchestrator: agent, + config: deps.config.councils as CouncilsConfig, + }); + const result = await tool.execute({ task: message }); + if (!result.success) { + return `Council run failed: ${result.error ?? 'unknown error'}`; + } + return result.output; + }, getElevation: () => { return getElevationStatusMessage({