Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions apps/mcp-server/src/mcp/handlers/council-scene.builder.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { describe, it, expect } from 'vitest';
import { buildCouncilScene } from './council-scene.builder';
import type { CouncilPreset } from '../../agent/council-preset.types';
import type { VisualData } from '../../keyword/keyword.types';

describe('buildCouncilScene', () => {
const planPreset: CouncilPreset = {
mode: 'PLAN',
primary: 'technical-planner',
specialists: ['architecture-specialist', 'security-specialist'],
};

const evalPreset: CouncilPreset = {
mode: 'EVAL',
primary: 'code-reviewer',
specialists: ['security-specialist', 'performance-specialist'],
};

const visual: VisualData = {
banner: '╭━━━━━╮\n┃ ◊‿◊ ┃ PLAN mode!\n╰━━┳━━╯',
agents: [
{ name: 'Technical Planner', face: '◇‿◇', color: 'magenta', status: 'analyzing' },
{ name: 'Architecture Specialist', face: '⬡‿⬡', color: 'blue', status: 'waiting' },
{ name: 'Security Specialist', face: '◮‿◮', color: 'red', status: 'waiting' },
],
collaboration: { format: 'minimal', renderHint: 'Display agent collaboration' },
};

it('returns undefined for ACT mode', () => {
expect(buildCouncilScene('ACT', planPreset, visual)).toBeUndefined();
});

it('returns councilScene for PLAN mode with councilPreset', () => {
const scene = buildCouncilScene('PLAN', planPreset, undefined);
expect(scene).toBeDefined();
expect(scene!.enabled).toBe(true);
expect(scene!.format).toBe('tiny-actor-grid');
expect(scene!.moderatorCopy).toContain('design this together');
expect(scene!.cast).toHaveLength(3);
expect(scene!.cast[0]).toEqual({
name: 'technical-planner',
role: 'primary',
face: '●‿●',
});
expect(scene!.cast[1].role).toBe('specialist');
});

it('returns councilScene for EVAL mode with councilPreset', () => {
const scene = buildCouncilScene('EVAL', evalPreset, undefined);
expect(scene).toBeDefined();
expect(scene!.enabled).toBe(true);
expect(scene!.moderatorCopy).toContain('Review council');
expect(scene!.cast[0].name).toBe('code-reviewer');
expect(scene!.cast[0].role).toBe('primary');
});

it('returns councilScene for AUTO mode from visual agents', () => {
const scene = buildCouncilScene('AUTO', undefined, visual);
expect(scene).toBeDefined();
expect(scene!.enabled).toBe(true);
expect(scene!.moderatorCopy).toContain('Autonomous council');
expect(scene!.cast[0].role).toBe('primary');
expect(scene!.cast[0].name).toBe('Technical Planner');
expect(scene!.cast[0].face).toBe('◇‿◇');
expect(scene!.cast.slice(1).every(m => m.role === 'specialist')).toBe(true);
});

it('returns councilScene for AUTO mode from fallback when visual is undefined', () => {
const scene = buildCouncilScene('AUTO', undefined, undefined, {
delegatesTo: 'agent-architect',
specialists: ['security-specialist'],
});
expect(scene).toBeDefined();
expect(scene!.cast[0]).toEqual({
name: 'agent-architect',
role: 'primary',
face: '●‿●',
});
expect(scene!.cast[1]).toEqual({
name: 'security-specialist',
role: 'specialist',
face: '●‿●',
});
});

it('returns undefined when no data is available', () => {
const scene = buildCouncilScene('AUTO', undefined, undefined);
expect(scene).toBeUndefined();
});

it('cross-references faces from visual data for councilPreset agents', () => {
const scene = buildCouncilScene('PLAN', planPreset, visual);
expect(scene).toBeDefined();
// "technical-planner" slug matches "Technical Planner" → face "◇‿◇"
expect(scene!.cast[0].face).toBe('◇‿◇');
// "architecture-specialist" slug matches "Architecture Specialist" → face "⬡‿⬡"
expect(scene!.cast[1].face).toBe('⬡‿⬡');
});

it('cast has exactly one primary member', () => {
const scene = buildCouncilScene('PLAN', planPreset, visual);
expect(scene).toBeDefined();
const primaries = scene!.cast.filter(m => m.role === 'primary');
expect(primaries).toHaveLength(1);
});

it('is serializable JSON', () => {
const scene = buildCouncilScene('PLAN', planPreset, visual);
expect(scene).toBeDefined();
const roundTripped = JSON.parse(JSON.stringify(scene));
expect(roundTripped).toEqual(scene);
});
});
183 changes: 183 additions & 0 deletions apps/mcp-server/src/mcp/handlers/council-scene.builder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/**
* Council Scene Builder — constructs the opening council-scene contract (#1366).
*
* Pure function: no I/O, no side-effects. Depends only on data already
* resolved by the handler (councilPreset, visual data, agent IDs).
*/

import type { CouncilScene, CouncilSceneCastMember } from './council-scene.types';
import type { CouncilPreset } from '../../agent/council-preset.types';
import type { Mode, VisualData, AgentVisualInfo } from '../../keyword/keyword.types';

/** Default face when no visual data is available */
const DEFAULT_FACE = '●‿●';

/** Mode-specific moderator opening lines (deterministic, testable) */
const MODERATOR_COPY: Partial<Record<Mode, string>> = {
PLAN: 'Council assembled — let us design this together.',
EVAL: 'Review council convened — specialists are ready.',
AUTO: 'Autonomous council activated — full cycle begins.',
};

/** Optional agent ID fallback for modes without a council preset */
export interface CouncilSceneFallback {
/** Primary agent ID (e.g. "agent-architect") */
delegatesTo?: string;
/** Specialist agent IDs (e.g. ["security-specialist", ...]) */
specialists?: string[];
}

/**
* Build a CouncilScene for the given mode, or return undefined for ACT mode.
*
* Resolution strategy:
* - PLAN/EVAL → uses councilPreset (primary + specialists), faces from visual
* - AUTO → visual.agents first, then agent ID fallback
* - ACT → undefined (no council scene)
*/
export function buildCouncilScene(
mode: Mode,
councilPreset: CouncilPreset | undefined,
visual: VisualData | undefined,
fallback?: CouncilSceneFallback,
): CouncilScene | undefined {
if (mode === 'ACT') {
return undefined;
}

const moderatorCopy = MODERATOR_COPY[mode];
if (!moderatorCopy) {
return undefined;
}

const cast = buildCast(councilPreset, visual, fallback);
if (cast.length === 0) {
return undefined;
}

return {
enabled: true,
cast,
moderatorCopy,
format: 'tiny-actor-grid',
};
}

/**
* Build the cast list for the council scene.
*/
function buildCast(
councilPreset: CouncilPreset | undefined,
visual: VisualData | undefined,
fallback?: CouncilSceneFallback,
): CouncilSceneCastMember[] {
// Build a face lookup from visual agents (keyed by display name and slug)
const faceLookup = new Map<string, string>();
if (visual?.agents) {
for (const agent of visual.agents) {
faceLookup.set(agent.name, agent.face);
const slug = agent.name.toLowerCase().replace(/\s+/g, '-');
faceLookup.set(slug, agent.face);
}
}

// Priority 1: councilPreset (PLAN/EVAL)
if (councilPreset) {
return buildCastFromPreset(councilPreset, faceLookup);
}

// Priority 2: visual agents (when loaded)
if (visual?.agents?.length) {
return buildCastFromVisual(visual.agents);
}

// Priority 3: agent ID fallback (AUTO mode when visual loading fails)
if (fallback?.delegatesTo) {
return buildCastFromFallback(fallback, faceLookup);
}

return [];
}

/**
* Build cast from a council preset, cross-referencing faces from visual data.
*/
function buildCastFromPreset(
preset: CouncilPreset,
faceLookup: Map<string, string>,
): CouncilSceneCastMember[] {
const cast: CouncilSceneCastMember[] = [];

cast.push({
name: preset.primary,
role: 'primary',
face: faceLookup.get(preset.primary) ?? DEFAULT_FACE,
});

for (const specialist of preset.specialists) {
cast.push({
name: specialist,
role: 'specialist',
face: faceLookup.get(specialist) ?? DEFAULT_FACE,
});
}

return cast;
}

/**
* Build cast from visual agent info.
* First agent is tagged as primary, rest as specialists.
*/
function buildCastFromVisual(agents: AgentVisualInfo[]): CouncilSceneCastMember[] {
const [first, ...rest] = agents;
const cast: CouncilSceneCastMember[] = [];

if (first) {
cast.push({
name: first.name,
role: 'primary',
face: first.face || DEFAULT_FACE,
});
}

for (const agent of rest) {
cast.push({
name: agent.name,
role: 'specialist',
face: agent.face || DEFAULT_FACE,
});
}

return cast;
}

/**
* Build cast from agent ID fallback (when visual data is unavailable).
*/
function buildCastFromFallback(
fallback: CouncilSceneFallback,
faceLookup: Map<string, string>,
): CouncilSceneCastMember[] {
const cast: CouncilSceneCastMember[] = [];

if (fallback.delegatesTo) {
cast.push({
name: fallback.delegatesTo,
role: 'primary',
face: faceLookup.get(fallback.delegatesTo) ?? DEFAULT_FACE,
});
}

if (fallback.specialists) {
for (const specialist of fallback.specialists) {
cast.push({
name: specialist,
role: 'specialist',
face: faceLookup.get(specialist) ?? DEFAULT_FACE,
});
}
}

return cast;
}
34 changes: 34 additions & 0 deletions apps/mcp-server/src/mcp/handlers/council-scene.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Council Scene Types — first-response council-scene contract (#1366).
*
* When present in the parse_mode response, the consuming assistant should
* open its first visible response with a Tiny Actor council scene followed
* by structured consensus output (risks / disagreements / next step).
*/

/** A single cast member in the council scene */
export interface CouncilSceneCastMember {
/** Agent identifier (kebab-case slug, e.g. "technical-planner") */
name: string;
/** Role in the council */
role: 'primary' | 'specialist';
/** ASCII face expression (e.g. "◇‿◇") */
face: string;
}

/**
* Opening council-scene contract for eligible workflow modes.
*
* Scoped to PLAN, EVAL, and AUTO modes. ACT mode and unrelated
* prompts must NOT include this field.
*/
export interface CouncilScene {
/** Whether the council scene should be rendered */
enabled: boolean;
/** Ordered list of council members (primary first) */
cast: CouncilSceneCastMember[];
/** Opening moderator line for the scene */
moderatorCopy: string;
/** Rendering format hint */
format: 'tiny-actor-grid';
}
Loading
Loading