import type { AgentConfigRegistry } from '../../agents/registry.js'; import { mkdirSync, writeFileSync } from 'node:fs'; import { homedir } from 'node:os'; import { resolve } from 'node:path'; import type { ChatResponseFormat } from '../../models/types.js'; import type { Tool, ToolResult } from '../types.js'; import { CouncilsOrchestrator, type CouncilsConfig } from '../../councils/orchestrator.js'; import type { CouncilScaffold } from '../../councils/scaffold.js'; import { councilRunInputSchema } from '../../councils/types.js'; interface DelegateRunner { delegate(request: { tier: 'fast' | 'default' | 'complex' | 'local'; systemPrompt: string; message: string; maxTokens?: number; responseFormat?: ChatResponseFormat; }): Promise<{ content: string; usage: { inputTokens: number; outputTokens: number }; tier: 'fast' | 'default' | 'complex' | 'local'; }>; } export interface CouncilRunDeps { registry: AgentConfigRegistry; orchestrator: DelegateRunner; config: CouncilsConfig; scaffold?: CouncilScaffold; } function slugifyTask(task: string): string { const trimmed = task.trim().toLowerCase(); if (!trimmed) { return 'council-run'; } const slug = trimmed .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '') .slice(0, 80); return slug || 'council-run'; } function formatTimestamp(date: Date): string { return date.toISOString().replace(/[:.]/g, '-'); } function getCouncilsDir(): string { const dataDir = process.env.FLYNN_DATA_DIR ?? resolve(homedir(), '.local/share/flynn'); return resolve(dataDir, 'councils'); } function writeCouncilArtifacts( task: string, summaryLines: string[], conversationLog: string, resultJson: string, ): { jsonPath: string; markdownPath: string } { const dir = getCouncilsDir(); mkdirSync(dir, { recursive: true }); const stamp = formatTimestamp(new Date()); const base = `${stamp}-${slugifyTask(task)}`; const jsonPath = resolve(dir, `${base}.json`); const markdownPath = resolve(dir, `${base}.md`); const markdown = [ ...summaryLines, '', 'Conversations:', '', conversationLog || '(none)', '', 'Raw Result JSON:', '```json', resultJson, '```', '', ].join('\n'); writeFileSync(jsonPath, `${resultJson}\n`, 'utf-8'); writeFileSync(markdownPath, markdown, 'utf-8'); return { jsonPath, markdownPath }; } function classifyCouncilFailure(message: string): 'network_or_latency' | 'json_format' | 'config' | 'cap_overflow' | 'user_cancelled' | 'unknown' { const lower = message.toLowerCase(); if ( lower.includes('operation cancelled by user') || lower.includes('aborterror') || lower.includes('aborted') || lower.includes('cancelled') ) { return 'user_cancelled'; } if ( lower.includes('connection error') || lower.includes('timed out') || lower.includes('econn') || lower.includes('enotfound') || lower.includes('all model providers failed') ) { return 'network_or_latency'; } if ( lower.includes('repair_failed') || lower.includes('parse_failed') || lower.includes('json') ) { return 'json_format'; } if ( lower.includes('not configured') || lower.includes('disabled') || lower.includes('meta_validation_failed') || lower.includes('grounding_failed') || lower.includes('bridge_validation_failed') ) { return 'config'; } if ( lower.includes('cap_exceeded') || lower.includes('cap_top_ideas') || lower.includes('cap_field_bullets') || lower.includes('cap_entry_chars') || lower.includes('cap_total_chars') ) { return 'cap_overflow'; } return 'unknown'; } function buildFailureHint(kind: ReturnType): string { switch (kind) { case 'network_or_latency': return 'Likely network/provider latency issue. Check endpoint reachability and consider faster council tiers.'; case 'json_format': return 'Likely model output-format issue. Council JSON repair/retry was unable to normalize output.'; case 'config': return 'Likely councils/agent configuration issue. Verify council agent names, tiers, and strict validation settings.'; case 'cap_overflow': return 'Bridge payload cap exceeded. Reduce task breadth, lower max rounds, or raise councils.defaults bridge cap settings.'; case 'user_cancelled': return 'Run was cancelled by user input (Esc/Ctrl+C). Re-run the council task when ready.'; default: return 'No deterministic diagnosis from error text.'; } } 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 => { try { const args = councilRunInputSchema.parse(rawArgs); const runner = new CouncilsOrchestrator({ registry: deps.registry, orchestrator: deps.orchestrator, config: deps.config, scaffold: deps.scaffold, }); const result = await runner.run(args); const timedTrace = result.trace.filter((event) => typeof event.latency_ms === 'number'); const totalLatencyMs = timedTrace.reduce((sum, event) => sum + (event.latency_ms ?? 0), 0); const phaseLatency = new Map(); for (const event of timedTrace) { const phase = event.phase_index; phaseLatency.set(phase, (phaseLatency.get(phase) ?? 0) + (event.latency_ms ?? 0)); } const phaseLatencyLines = [...phaseLatency.entries()] .sort((a, b) => a[0] - b[0]) .map(([phase, latency]) => `- Phase ${phase}: ${latency}ms`); const slowestCalls = [...timedTrace] .sort((a, b) => (b.latency_ms ?? 0) - (a.latency_ms ?? 0)) .slice(0, 5) .map((event) => `- ${event.call_id}: ${event.latency_ms ?? 0}ms`); 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}`, '', 'Timing:', `- Timed calls: ${timedTrace.length}`, `- Total model latency (summed): ${totalLatencyMs}ms`, ...phaseLatencyLines, ...( slowestCalls.length > 0 ? ['', 'Slowest calls:', ...slowestCalls] : [] ), '', `Agent conversations: ${result.conversations.length}`, ]; const conversationLog = result.conversations .map((turn, index) => { const prompt = JSON.stringify(turn.prompt_payload); return [ `#${index + 1} ${turn.call_id} [${turn.agent} @ ${turn.tier}]`, `Prompt payload: ${prompt}`, `Response: ${turn.response}`, ].join('\n'); }) .join('\n\n'); const resultJson = JSON.stringify(result, null, 2); const artifacts = writeCouncilArtifacts(args.task, lines, conversationLog, resultJson); return { success: true, output: [ ...lines, '', 'Artifacts:', `- Summary report: ${artifacts.markdownPath}`, `- Full JSON: ${artifacts.jsonPath}`, ].join('\n'), }; } catch (error) { const message = error instanceof Error ? error.message : String(error); const kind = classifyCouncilFailure(message); const hint = buildFailureHint(kind); return { success: false, output: '', error: [ message, `Likely root cause: ${kind}`, `Hint: ${hint}`, `Council config: D=${deps.config.groups.D.arbiter_agent}/${deps.config.groups.D.freethinker_agent}@${deps.config.groups.D.model_tier ?? 'agent-tier'}, ` + `P=${deps.config.groups.P.arbiter_agent}/${deps.config.groups.P.freethinker_agent}@${deps.config.groups.P.model_tier ?? 'agent-tier'}, ` + `meta=${deps.config.meta_arbiter_agent}@${deps.config.meta_model_tier ?? 'agent-tier'}`, ].join('\n'), }; } }, }; }