feat(councils): add deterministic councils engine and council.run tool
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { createCouncilRunTool } from './council-run.js';
|
||||
import type { AgentConfigRegistry } from '../../agents/registry.js';
|
||||
|
||||
function createRegistry(): AgentConfigRegistry {
|
||||
const configs = new Map<string, { name: string; modelTier?: 'fast' | 'default' | 'complex'; systemPrompt?: string }>([
|
||||
['council_d_arbiter', { name: 'council_d_arbiter', modelTier: 'default', systemPrompt: 'D Arbiter' }],
|
||||
['council_d_freethinker', { name: 'council_d_freethinker', modelTier: 'default', systemPrompt: 'D FT' }],
|
||||
['council_p_arbiter', { name: 'council_p_arbiter', modelTier: 'default', systemPrompt: 'P Arbiter' }],
|
||||
['council_p_freethinker', { name: 'council_p_freethinker', modelTier: 'default', systemPrompt: 'P FT' }],
|
||||
['council_meta_arbiter', { name: 'council_meta_arbiter', modelTier: 'default', systemPrompt: 'Meta' }],
|
||||
]);
|
||||
return {
|
||||
get: (name: string) => configs.get(name),
|
||||
list: () => [...configs.values()],
|
||||
} as unknown as AgentConfigRegistry;
|
||||
}
|
||||
|
||||
const config = {
|
||||
enabled: true,
|
||||
defaults: {
|
||||
max_rounds: 1,
|
||||
ideas_per_round: 2,
|
||||
top_ideas_for_bridge: 1,
|
||||
bridge_packet_max_chars: 5000,
|
||||
bridge_field_max_bullets: 5,
|
||||
bridge_entry_max_chars: 300,
|
||||
novelty_delta_threshold: 10,
|
||||
repetition_threshold: 70,
|
||||
},
|
||||
strict_grounding: false,
|
||||
strict_meta_validation: true,
|
||||
groups: {
|
||||
D: {
|
||||
arbiter_agent: 'council_d_arbiter',
|
||||
freethinker_agent: 'council_d_freethinker',
|
||||
group_prompt_prefix: 'D',
|
||||
novelty_bias: 'low',
|
||||
risk_tolerance: 'low',
|
||||
forbidden_approaches: [],
|
||||
},
|
||||
P: {
|
||||
arbiter_agent: 'council_p_arbiter',
|
||||
freethinker_agent: 'council_p_freethinker',
|
||||
group_prompt_prefix: 'P',
|
||||
novelty_bias: 'high',
|
||||
risk_tolerance: 'high',
|
||||
forbidden_approaches: [],
|
||||
},
|
||||
},
|
||||
meta_arbiter_agent: 'council_meta_arbiter',
|
||||
} as const;
|
||||
|
||||
describe('council.run tool', () => {
|
||||
it('runs council pipeline and returns output summary', async () => {
|
||||
const delegate = vi.fn(async ({ message }: { message: string }) => {
|
||||
const payload = JSON.parse(message);
|
||||
if (payload.brief_D && payload.brief_P) {
|
||||
return {
|
||||
content: JSON.stringify({
|
||||
schema_version: '1.0.0',
|
||||
selected_primary: [payload.brief_D.shortlist[0]],
|
||||
selected_secondary: [payload.brief_P.shortlist[0]],
|
||||
merges: [],
|
||||
rejections: [],
|
||||
open_questions: ['q1'],
|
||||
next_experiments: ['e1'],
|
||||
}),
|
||||
usage: { inputTokens: 10, outputTokens: 5 },
|
||||
tier: 'default' as const,
|
||||
};
|
||||
}
|
||||
if (payload.shortlisted_ideas) {
|
||||
return {
|
||||
content: JSON.stringify({ grounded: payload.shortlisted_ideas.map((idea: any) => ({ idea_id: idea.idea_id, mve: 'm', constraints: ['c'], falsifiability_checks: ['f'] })) }),
|
||||
usage: { inputTokens: 10, outputTokens: 5 },
|
||||
tier: 'default' as const,
|
||||
};
|
||||
}
|
||||
if (payload.ideas) {
|
||||
return {
|
||||
content: JSON.stringify({
|
||||
assessments: payload.ideas.map((idea: any, idx: number) => ({
|
||||
idea_id: idea.idea_id,
|
||||
scores: { novelty: 50, feasibility: 50, impact: 50, testability: 50 },
|
||||
decision: idx === 0 ? 'shortlist' : 'hold',
|
||||
notes: 'note',
|
||||
})),
|
||||
assumptions: ['a'],
|
||||
risks: ['r'],
|
||||
asks: ['k'],
|
||||
what_to_steal: ['w'],
|
||||
convergence_signal: false,
|
||||
novelty_score: 60,
|
||||
repetition_rate: 10,
|
||||
}),
|
||||
usage: { inputTokens: 10, outputTokens: 5 },
|
||||
tier: 'default' as const,
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: JSON.stringify({
|
||||
ideas: [
|
||||
{ title: 't1', hypothesis: 'h1', mechanism: 'm1', expected_outcome: 'o1' },
|
||||
{ title: 't2', hypothesis: 'h2', mechanism: 'm2', expected_outcome: 'o2' },
|
||||
],
|
||||
}),
|
||||
usage: { inputTokens: 10, outputTokens: 5 },
|
||||
tier: 'default' as const,
|
||||
};
|
||||
});
|
||||
|
||||
const tool = createCouncilRunTool({
|
||||
registry: createRegistry(),
|
||||
orchestrator: { delegate },
|
||||
config: config as any,
|
||||
});
|
||||
|
||||
const result = await tool.execute({ task: 'plan migration' });
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.output).toContain('Council pipeline v1.0.0');
|
||||
expect(result.output).toContain('Meta selection');
|
||||
});
|
||||
|
||||
it('returns error on invalid input', async () => {
|
||||
const tool = createCouncilRunTool({
|
||||
registry: createRegistry(),
|
||||
orchestrator: { delegate: vi.fn() as any },
|
||||
config: config as any,
|
||||
});
|
||||
const result = await tool.execute({});
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
import type { AgentConfigRegistry } from '../../agents/registry.js';
|
||||
import type { Tool, ToolResult } from '../types.js';
|
||||
import { CouncilsOrchestrator, type CouncilsConfig } from '../../councils/orchestrator.js';
|
||||
import { councilRunInputSchema } from '../../councils/types.js';
|
||||
|
||||
interface DelegateRunner {
|
||||
delegate(request: {
|
||||
tier: 'fast' | 'default' | 'complex' | 'local';
|
||||
systemPrompt: string;
|
||||
message: string;
|
||||
maxTokens?: number;
|
||||
}): Promise<{
|
||||
content: string;
|
||||
usage: { inputTokens: number; outputTokens: number };
|
||||
tier: 'fast' | 'default' | 'complex' | 'local';
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface CouncilRunDeps {
|
||||
registry: AgentConfigRegistry;
|
||||
orchestrator: DelegateRunner;
|
||||
config: CouncilsConfig;
|
||||
}
|
||||
|
||||
export function createCouncilRunTool(deps: CouncilRunDeps): Tool {
|
||||
return {
|
||||
name: 'council.run',
|
||||
description:
|
||||
'Run the deterministic dual-council pipeline (D/P groups with bridge-only exchange and meta merge).',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
task: { type: 'string', description: 'Primary task or question to explore' },
|
||||
constraints: { type: 'object', description: 'Optional constraints object (or pass string)' },
|
||||
success_definition: { type: 'string', description: 'What success looks like for this run' },
|
||||
budget: { type: 'object', description: 'Optional budget limits/time/cost constraints' },
|
||||
timebox: { type: 'string', description: 'Optional timebox (e.g. 30m)' },
|
||||
output_format: { type: 'string', description: 'Desired output format' },
|
||||
max_rounds: { type: 'number', description: 'Override configured max rounds (1-6)' },
|
||||
session_id: { type: 'string', description: 'Optional external session/run id' },
|
||||
},
|
||||
required: ['task'],
|
||||
},
|
||||
execute: async (rawArgs: unknown): Promise<ToolResult> => {
|
||||
try {
|
||||
const args = councilRunInputSchema.parse(rawArgs);
|
||||
const runner = new CouncilsOrchestrator({
|
||||
registry: deps.registry,
|
||||
orchestrator: deps.orchestrator,
|
||||
config: deps.config,
|
||||
});
|
||||
const result = await runner.run(args);
|
||||
|
||||
const lines = [
|
||||
`[Council pipeline v${result.pipeline_version}]`,
|
||||
`Stop reason: ${result.stop_snapshot.stop_reason} (round ${result.stop_snapshot.round_reached})`,
|
||||
`D shortlist: ${result.stop_snapshot.final_shortlist_D.join(', ') || 'none'}`,
|
||||
`P shortlist: ${result.stop_snapshot.final_shortlist_P.join(', ') || 'none'}`,
|
||||
`Bridge validated: ${result.stop_snapshot.bridge_validated ? 'yes' : 'no'}`,
|
||||
`Grounding failures: ${result.stop_snapshot.grounding_failures_count}`,
|
||||
'',
|
||||
'Meta selection:',
|
||||
`- Primary: ${result.meta.selected_primary.join(', ') || 'none'}`,
|
||||
`- Secondary: ${result.meta.selected_secondary.join(', ') || 'none'}`,
|
||||
`- Open questions: ${result.meta.open_questions.length}`,
|
||||
`- Next experiments: ${result.meta.next_experiments.length}`,
|
||||
];
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: `${lines.join('\n')}\n\n${JSON.stringify(result)}`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -34,6 +34,8 @@ export { createK8sTools } from './k8s.js';
|
||||
export { screenCaptureTool, cameraCaptureTool } from './capture.js';
|
||||
export { createAgentDelegateTool } from './agent-delegate.js';
|
||||
export type { AgentDelegateDeps } from './agent-delegate.js';
|
||||
export { createCouncilRunTool } from './council-run.js';
|
||||
export type { CouncilRunDeps } from './council-run.js';
|
||||
|
||||
import type { Tool } from '../types.js';
|
||||
import type { MemoryStore } from '../../memory/store.js';
|
||||
|
||||
+2
-1
@@ -5,8 +5,9 @@ export { ToolExecutor } from './executor.js';
|
||||
export type { ToolExecutorConfig } from './executor.js';
|
||||
export { ToolPolicy } from './policy.js';
|
||||
export type { ToolPolicyContext } from './policy.js';
|
||||
export { allBuiltinTools, createWebSearchTools, createProcessTools, ProcessManager, BrowserManager, createBrowserTools, createMediaSendTool, createAudioTranscribeTool, createSessionTools, createAgentsListTool, createMessageSendTool, createCronTools, createGmailTools, createGcalTools, createGdocsTools, createGdriveTools, createGtasksTools, createMinioShareTool, createMinioIngestTool, createMinioSyncTool, createK8sTools, createAgentDelegateTool } from './builtin/index.js';
|
||||
export { allBuiltinTools, createWebSearchTools, createProcessTools, ProcessManager, BrowserManager, createBrowserTools, createMediaSendTool, createAudioTranscribeTool, createSessionTools, createAgentsListTool, createMessageSendTool, createCronTools, createGmailTools, createGcalTools, createGdocsTools, createGdriveTools, createGtasksTools, createMinioShareTool, createMinioIngestTool, createMinioSyncTool, createK8sTools, createAgentDelegateTool, createCouncilRunTool } from './builtin/index.js';
|
||||
export type { AgentDelegateDeps } from './builtin/index.js';
|
||||
export type { CouncilRunDeps } from './builtin/index.js';
|
||||
export type { WebSearchConfig } from './builtin/web-search.js';
|
||||
export type { ProcessManagerConfig } from './builtin/process/index.js';
|
||||
export type { BrowserManagerConfig } from './builtin/browser/index.js';
|
||||
|
||||
+3
-1
@@ -47,6 +47,7 @@ const PROFILE_TOOLS: Record<ToolProfile, Set<string>> = {
|
||||
'k8s.logs',
|
||||
'agent.delegate',
|
||||
'agents.list',
|
||||
'council.run',
|
||||
]),
|
||||
coding: new Set([
|
||||
'file.read',
|
||||
@@ -101,6 +102,7 @@ const PROFILE_TOOLS: Record<ToolProfile, Set<string>> = {
|
||||
'browser.evaluate',
|
||||
'agent.delegate',
|
||||
'agents.list',
|
||||
'council.run',
|
||||
]),
|
||||
full: new Set(), // Special: matches everything
|
||||
};
|
||||
@@ -121,7 +123,7 @@ export const TOOL_GROUPS: Record<string, string[]> = {
|
||||
'group:cron': ['cron.list', 'cron.trigger', 'cron.create', 'cron.delete'],
|
||||
'group:minio': ['minio.share', 'minio.ingest', 'minio.sync'],
|
||||
'group:k8s': ['k8s.pods', 'k8s.deployments', 'k8s.logs'],
|
||||
'group:agents': ['agent.delegate', 'agents.list'],
|
||||
'group:agents': ['agent.delegate', 'agents.list', 'council.run'],
|
||||
};
|
||||
|
||||
/** Expand group references in a list of tool names/patterns. */
|
||||
|
||||
Reference in New Issue
Block a user