Public/Show-TriadDialogue.ps1
|
# Copyright (c) 2026 Jeffrey Snover. All rights reserved. # Licensed under the MIT License. See LICENSE file in the project root. function Show-TriadDialogue { <# .SYNOPSIS Simulates a structured three-agent debate grounded in the AI Triad taxonomy. .DESCRIPTION Runs a multi-round debate between Prometheus (accelerationist), Sentinel (safetyist), and Cassandra (skeptic). Each agent's arguments are grounded in taxonomy nodes and edges. Produces opening statements, N debate rounds, and a synthesis. Output is compatible with the taxonomy-editor DebateTab. .PARAMETER Topic The debate topic (mandatory). .PARAMETER Rounds Number of debate rounds after opening statements (1-15, default 3). .PARAMETER OutputFile Optional path to write the debate JSON. If omitted, writes to debates/debate-<guid>.json. .PARAMETER Model AI model override. Defaults to 'gemini-2.5-flash'. .PARAMETER ApiKey AI API key override. .PARAMETER RepoRoot Path to the repository root. .PARAMETER UseAdaptiveStaging Enable adaptive staging (delegates to CLI engine via Invoke-AITDebate). Phases advance based on debate health metrics instead of fixed round count. .PARAMETER Pacing Adaptive staging pace: 'tight' (fewer rounds), 'moderate' (default), 'thorough' (more rounds). .PARAMETER MaxTotalRounds Maximum rounds for adaptive staging (5-20, default 12). Ignored without -UseAdaptiveStaging. .PARAMETER AllowEarlyTermination Allow debate to end early if health metrics collapse. Default: true when adaptive staging is on. .EXAMPLE Show-TriadDialogue "Should AI be regulated like a public utility?" -Rounds 2 .EXAMPLE Show-TriadDialogue "Is open-source AI safer than closed-source?" -OutputFile debate.json .EXAMPLE Show-TriadDialogue "Scaling limits of current AI" -UseAdaptiveStaging -Pacing thorough #> [CmdletBinding()] param( [Parameter(Mandatory, Position = 0)] [string]$Topic, [ValidateRange(1, 15)] [int]$Rounds = 3, [string]$OutputFile, [ValidateScript({ Test-AIModelId $_ })] [ArgumentCompleter({ param($cmd, $param, $word) $script:ValidModelIds | Where-Object { $_ -like "$word*" } })] [string]$Model = 'gemini-2.5-flash', [string]$ApiKey, [ValidateSet('policymakers', 'technical_researchers', 'industry_leaders', 'academic_community', 'general_public')] [string]$Audience = 'policymakers', [string]$RepoRoot = $script:RepoRoot, [switch]$UseAdaptiveStaging, [ValidateSet('tight', 'moderate', 'thorough')] [string]$Pacing = 'moderate', [ValidateRange(5, 20)] [int]$MaxTotalRounds = 12, [switch]$AllowEarlyTermination ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' # ── Delegate to CLI engine for adaptive staging ─────────────────────────── # Adaptive staging logic lives in lib/debate/phaseTransitions.ts — not # reimplemented in PS. Route through Invoke-AITDebate which calls the CLI. if ($UseAdaptiveStaging) { Write-Host 'Adaptive staging requested — delegating to Invoke-AITDebate (CLI engine)' -ForegroundColor Cyan $CliParams = @{ Topic = $Topic Rounds = $MaxTotalRounds Model = $Model AdaptiveStaging = $true } if ($ApiKey) { $CliParams.ApiKey = $ApiKey } if ($OutputFile) { $CliParams.OutputDirectory = Split-Path $OutputFile -Parent } return Invoke-AITDebate @CliParams } # ── Step 1: Validate environment ────────────────────────────────────────── Write-Step 'Validating environment' if ($Model -match '^gemini') { $Backend = 'gemini' } elseif ($Model -match '^claude') { $Backend = 'claude' } elseif ($Model -match '^groq') { $Backend = 'groq' } elseif ($Model -match '^openai') { $Backend = 'openai' } else { $Backend = 'gemini' } $ResolvedKey = Resolve-AIApiKey -ExplicitKey $ApiKey -Backend $Backend if ([string]::IsNullOrWhiteSpace($ResolvedKey)) { Write-Fail 'No API key found. Set GEMINI_API_KEY, ANTHROPIC_API_KEY, or AI_API_KEY.' throw 'No API key configured' } # ── Step 2: Define agents ───────────────────────────────────────────────── $Agents = @( @{ Name = 'Prometheus' Speaker = 'prometheus' PovKey = 'accelerationist' PovLabel = 'Accelerationist' Description = 'You champion rapid AI development and deployment. You believe AI progress is essential for human flourishing, that open development is safer than restriction, and that the benefits vastly outweigh the risks. You distrust regulatory gatekeeping and favor empirical, results-oriented approaches.' } @{ Name = 'Sentinel' Speaker = 'sentinel' PovKey = 'safetyist' PovLabel = 'Safetyist' Description = 'You prioritize AI safety, alignment research, and careful risk mitigation. You believe powerful AI poses existential risks that demand precaution, that capability gains outpace safety understanding, and that deployment should wait until systems are proven safe. You favor regulation and mandatory safety testing.' } @{ Name = 'Cassandra' Speaker = 'cassandra' PovKey = 'skeptic' PovLabel = 'Skeptic' Description = 'You question AI hype and emphasize present-day harms. You believe current AI systems are less capable than claimed, that the real risks are economic displacement, bias, and power concentration, not sci-fi scenarios. You demand empirical evidence over speculation and focus on protecting workers and communities.' } ) # ── Step 3: Build POV context per agent ─────────────────────────────────── Write-Step 'Building POV context for agents' $TaxDir = Get-TaxonomyDir # Load all nodes $AllNodes = @{} foreach ($PovKey in @('accelerationist', 'safetyist', 'skeptic', 'situations')) { $FilePath = Join-Path $TaxDir "$PovKey.json" if (-not (Test-Path $FilePath)) { continue } $FileData = Get-Content -Raw -Path $FilePath | ConvertFrom-Json foreach ($Node in $FileData.nodes) { $AllNodes[$Node.id] = @{ POV = $PovKey Label = $Node.label Description = if ($Node.PSObject.Properties['description']) { $Node.description } else { '' } Category = if ($Node.PSObject.Properties['category']) { $Node.category } else { '' } } } } # Load edges $EdgesPath = Join-Path $TaxDir 'edges.json' $AllEdges = @() $NodeDegree = @{} if (Test-Path $EdgesPath) { $EdgesData = Get-Content -Raw -Path $EdgesPath | ConvertFrom-Json $AllEdges = @($EdgesData.edges | Where-Object { $_.status -eq 'approved' }) foreach ($Edge in $AllEdges) { if (-not $NodeDegree.ContainsKey($Edge.source)) { $NodeDegree[$Edge.source] = 0 } if (-not $NodeDegree.ContainsKey($Edge.target)) { $NodeDegree[$Edge.target] = 0 } $NodeDegree[$Edge.source]++ $NodeDegree[$Edge.target]++ } } # Build per-agent context: top 30 nodes by degree + their edges + relevant cc-nodes $AgentContexts = @{} foreach ($Agent in $Agents) { $PovKey = $Agent.PovKey $PovNodeIds = @($AllNodes.Keys | Where-Object { $AllNodes[$_].POV -eq $PovKey }) # Sort by degree, take top 30 $TopNodes = @($PovNodeIds | Sort-Object { if ($NodeDegree.ContainsKey($_)) { $NodeDegree[$_] } else { 0 } } -Descending | Select-Object -First 30) # Add relevant cc-nodes (connected to these top nodes) $TopNodeSet = [System.Collections.Generic.HashSet[string]]::new() foreach ($NId in $TopNodes) { [void]$TopNodeSet.Add($NId) } $CcNodeIds = [System.Collections.Generic.HashSet[string]]::new() foreach ($Edge in $AllEdges) { if ($TopNodeSet.Contains($Edge.source) -and $AllNodes.ContainsKey($Edge.target) -and $AllNodes[$Edge.target].POV -eq 'situations') { [void]$CcNodeIds.Add($Edge.target) } if ($TopNodeSet.Contains($Edge.target) -and $AllNodes.ContainsKey($Edge.source) -and $AllNodes[$Edge.source].POV -eq 'situations') { [void]$CcNodeIds.Add($Edge.source) } } # Build context string $ContextBuilder = [System.Text.StringBuilder]::new() [void]$ContextBuilder.AppendLine("Top $($TopNodes.Count) $PovKey nodes (by graph connectivity):") foreach ($NId in $TopNodes) { $N = $AllNodes[$NId] [void]$ContextBuilder.AppendLine(" - $NId [$($N.Category)]: $($N.Label) — $($N.Description)") } if ($CcNodeIds.Count -gt 0) { [void]$ContextBuilder.AppendLine("`nRelevant situations nodes:") foreach ($CcId in $CcNodeIds | Select-Object -First 10) { $N = $AllNodes[$CcId] [void]$ContextBuilder.AppendLine(" - ${CcId}: $($N.Label) — $($N.Description)") } } # Key edges involving these nodes $RelevantEdges = @($AllEdges | Where-Object { $TopNodeSet.Contains($_.source) -or $TopNodeSet.Contains($_.target) } | Select-Object -First 50) if ($RelevantEdges.Count -gt 0) { [void]$ContextBuilder.AppendLine("`nKey relationships:") foreach ($Edge in $RelevantEdges) { [void]$ContextBuilder.AppendLine(" $($Edge.source) --[$($Edge.type)]--> $($Edge.target)") } } # Load top 20 policies for this POV from the policy registry $PolicyRegistryPath = Join-Path $TaxDir 'policy_actions.json' if (Test-Path $PolicyRegistryPath) { $PolicyReg = Get-Content -Raw -Path $PolicyRegistryPath | ConvertFrom-Json if ($PolicyReg.policies) { $PovPolicies = @($PolicyReg.policies | Where-Object { $_.source_povs -contains $PovKey } | Sort-Object { $_.member_count } -Descending | Select-Object -First 20) if ($PovPolicies.Count -gt 0) { [void]$ContextBuilder.AppendLine("`nPOLICY CONTEXT (use these pol-NNN IDs when referencing policy actions):") foreach ($Pol in $PovPolicies) { [void]$ContextBuilder.AppendLine(" - $($Pol.id): $($Pol.action)") } } } } $AgentContexts[$Agent.Speaker] = $ContextBuilder.ToString() } Write-OK "Built context for $($Agents.Count) agents" # ── Step 4: Generate debate ─────────────────────────────────────────────── $DebateId = [guid]::NewGuid().ToString() $Transcript = [System.Collections.Generic.List[PSObject]]::new() # Audience directives — matches lib/debate/prompts.ts AUDIENCE_DIRECTIVES $AudienceDirectives = @{ policymakers = @{ ReadingLevel = 'Write for a policy reporter or congressional staffer — someone smart and busy who needs to understand and quote you. Lead with your main claim in the first sentence. Use active voice with named actors. One idea per sentence. Prefer concrete examples and specific numbers over abstract categories.' DetailInstruction = 'Provide a thorough, in-depth response — 3-5 paragraphs. Include a steelman of the strongest opposing position, disclose 1-2 key assumptions your argument depends on, and frame arguments in terms of implementability, enforcement mechanisms, and political feasibility.' ModeratorBias = 'Steer toward actionable policy disagreements. Prefer questions about implementation feasibility, enforcement mechanisms, jurisdictional authority, and constituent impact.' } technical_researchers = @{ ReadingLevel = 'Write for a senior ML researcher reviewing a position paper. Use precise technical vocabulary without hedging. Cite specific architectures, benchmarks, and failure modes by name. Quantify claims: parameter counts, compute budgets, error rates, confidence intervals.' DetailInstruction = 'Provide a rigorous, evidence-grounded response — 3-5 paragraphs. Separate empirical claims from normative positions. Identify the strongest technical counterargument and address it directly.' ModeratorBias = 'Steer toward empirical disputes and methodology. Probe evidence quality, reproducibility, and the validity of benchmarks or evaluations being cited.' } industry_leaders = @{ ReadingLevel = 'Write for a technology executive making product and investment decisions. Lead with the business-relevant conclusion. Use concrete examples from deployed products, market dynamics, and competitive landscapes.' DetailInstruction = 'Provide a strategic, decision-oriented response — 3-5 paragraphs. Frame each argument around ROI, competitive advantage, or risk mitigation. Include at least one concrete case study or industry precedent.' ModeratorBias = 'Steer toward practical tradeoffs. Surface cost-benefit tensions, competitive dynamics, liability exposure, and talent considerations.' } academic_community = @{ ReadingLevel = 'Write for a faculty seminar — scholars from multiple disciplines who value analytical rigor, theoretical grounding, and intellectual honesty. Trace arguments to their philosophical or theoretical roots. Name the scholarly traditions and key thinkers you draw on.' DetailInstruction = 'Provide a scholarly, well-structured response — 3-5 paragraphs. Engage with competing theoretical frameworks, not just competing conclusions. Cite intellectual lineage. Identify methodological limitations.' ModeratorBias = 'Steer toward conceptual precision and theoretical assumptions. Probe interdisciplinary tensions, methodological limitations, and the philosophical foundations of competing positions.' } general_public = @{ ReadingLevel = 'Write for an informed citizen reading a quality newspaper — someone who follows the news but has no technical background. No acronyms without expansion. No jargon without a plain-English equivalent. Lead with why this matters to daily life.' DetailInstruction = 'Provide a clear, accessible response — 2-4 paragraphs. Use one concrete, relatable example per major claim. Avoid both fear-mongering and dismissiveness. End with what an ordinary person can actually do or watch for.' ModeratorBias = 'Steer toward stakes and consequences that affect ordinary people. Prefer questions about personal impact (jobs, privacy, safety), fairness, and democratic accountability.' } } $AudDir = $AudienceDirectives[$Audience] $AudienceBlock = @" AUDIENCE: $Audience READING LEVEL: $($AudDir.ReadingLevel) DETAIL: $($AudDir.DetailInstruction) "@ # Helper: call LLM for a turn $InvokeTurn = { param([string]$AgentSpeaker, [string]$SystemPrompt, [string]$TranscriptSoFar, [string]$TurnType) $SchemaPrompt = Get-Prompt -Name 'triad-dialogue-schema' $TurnPrompt = Get-Prompt -Name 'triad-dialogue-turn' -Replacements @{ SYSTEM_PROMPT = $SystemPrompt TOPIC = $Topic AUDIENCE_DIRECTIVE = $AudienceBlock TRANSCRIPT = $TranscriptSoFar SCHEMA = $SchemaPrompt } $TurnResult = Invoke-AIApi ` -Prompt $TurnPrompt ` -Model $Model ` -ApiKey $ResolvedKey ` -Temperature 0.7 ` -MaxTokens 2048 ` -JsonMode ` -TimeoutSec 120 ` -MaxRetries 3 ` -RetryDelays @(5, 15, 45) if (-not $TurnResult -or -not $TurnResult.Text) { return $null } $ResponseText = $TurnResult.Text -replace '(?s)^```json\s*', '' -replace '(?s)\s*```$', '' try { return $ResponseText | ConvertFrom-Json } catch { Write-Warn "Failed to parse $AgentSpeaker response — attempting repair" try { $Repaired = Repair-TruncatedJson -Text $ResponseText return $Repaired | ConvertFrom-Json } catch { Write-Warn "Repair failed for $AgentSpeaker" return [PSCustomObject]@{ content = $ResponseText taxonomy_refs = @() } } } } # Build system prompts per agent $SystemPrompts = @{} foreach ($Agent in $Agents) { $SystemPrompts[$Agent.Speaker] = Get-Prompt -Name 'triad-dialogue-system' -Replacements @{ AGENT_NAME = $Agent.Name POV_LABEL = $Agent.PovLabel POV_DESCRIPTION = $Agent.Description POV_CONTEXT = $AgentContexts[$Agent.Speaker] } } # Format transcript for prompt $FormatTranscript = { param($Trans) $Lines = [System.Text.StringBuilder]::new() foreach ($T in $Trans) { [void]$Lines.AppendLine("[$($T.speaker) — $($T.type)]") [void]$Lines.AppendLine($T.content) [void]$Lines.AppendLine() } return $Lines.ToString() } # Compress older rounds if transcript gets long $CompressTranscript = { param($Trans, [int]$MaxTokenEst) $FullText = (& $FormatTranscript $Trans) $EstTokens = [Math]::Round($FullText.Length / 4) if ($EstTokens -le $MaxTokenEst) { return $FullText } # Keep last 2 entries fully, summarize earlier ones $KeepFull = 6 # last 2 rounds × 3 agents if ($Trans.Count -le $KeepFull) { return $FullText } $Earlier = $Trans | Select-Object -First ($Trans.Count - $KeepFull) $Recent = $Trans | Select-Object -Last $KeepFull $Summary = [System.Text.StringBuilder]::new() [void]$Summary.AppendLine("[Earlier discussion summary]") foreach ($T in $Earlier) { if ($T.content.Length -gt 100) { $Snippet = $T.content.Substring(0, 100) + '...' } else { $Snippet = $T.content } [void]$Summary.AppendLine(" $($T.speaker): $Snippet") } [void]$Summary.AppendLine() [void]$Summary.AppendLine("[Recent exchanges]") foreach ($T in $Recent) { [void]$Summary.AppendLine("[$($T.speaker) — $($T.type)]") [void]$Summary.AppendLine($T.content) [void]$Summary.AppendLine() } return $Summary.ToString() } Write-Host "`n$('═' * 72)" -ForegroundColor Cyan Write-Host " TRIAD DIALOGUE" -ForegroundColor White Write-Host " Topic: $Topic" -ForegroundColor Gray Write-Host " Agents: Prometheus, Sentinel, Cassandra | Rounds: $Rounds" -ForegroundColor Gray Write-Host "$('═' * 72)" -ForegroundColor Cyan # ── Opening statements ──────────────────────────────────────────────────── Write-Step 'Opening statements' foreach ($Agent in $Agents) { Write-Info "$($Agent.Name) is preparing opening statement..." $TranscriptText = & $FormatTranscript $Transcript $Response = & $InvokeTurn $Agent.Speaker $SystemPrompts[$Agent.Speaker] $TranscriptText 'opening' if ($Response) { if ($Response.PSObject.Properties['content']) { $Content = $Response.content } else { $Content = "$Response" } if ($Response.PSObject.Properties['taxonomy_refs']) { $TaxRefs = @($Response.taxonomy_refs) } else { $TaxRefs = @() } $Entry = [ordered]@{ type = 'opening' speaker = $Agent.Speaker content = $Content taxonomy_refs = $TaxRefs policy_refs = @() id = [guid]::NewGuid().ToString() timestamp = (Get-Date -Format 'o') } $Transcript.Add([PSCustomObject]$Entry) # Display $NameColor = switch ($Agent.Speaker) { 'prometheus' { 'Blue' } 'sentinel' { 'Green' } 'cassandra' { 'Yellow' } } Write-Host "`n $($Agent.Name.ToUpper()) (Opening):" -ForegroundColor $NameColor Write-Host " $Content" -ForegroundColor White if ($TaxRefs.Count -gt 0) { $RefIds = ($TaxRefs | ForEach-Object { $_.node_id }) -join ', ' Write-Host " Refs: $RefIds" -ForegroundColor DarkGray } } else { Write-Warn "$($Agent.Name) failed to produce an opening statement" } } # ── Debate rounds ───────────────────────────────────────────────────────── for ($Round = 1; $Round -le $Rounds; $Round++) { Write-Step "Round $Round of $Rounds" foreach ($Agent in $Agents) { Write-Info "$($Agent.Name) is formulating response..." $TranscriptText = & $CompressTranscript $Transcript 8000 $Response = & $InvokeTurn $Agent.Speaker $SystemPrompts[$Agent.Speaker] $TranscriptText 'argument' if ($Response) { if ($Response.PSObject.Properties['content']) { $Content = $Response.content } else { $Content = "$Response" } if ($Response.PSObject.Properties['taxonomy_refs']) { $TaxRefs = @($Response.taxonomy_refs) } else { $TaxRefs = @() } $Entry = [ordered]@{ type = 'statement' speaker = $Agent.Speaker content = $Content taxonomy_refs = $TaxRefs policy_refs = @() id = [guid]::NewGuid().ToString() timestamp = (Get-Date -Format 'o') metadata = @{ round = $Round } } $Transcript.Add([PSCustomObject]$Entry) $NameColor = switch ($Agent.Speaker) { 'prometheus' { 'Blue' } 'sentinel' { 'Green' } 'cassandra' { 'Yellow' } } Write-Host "`n $($Agent.Name.ToUpper()) (Round $Round):" -ForegroundColor $NameColor Write-Host " $Content" -ForegroundColor White if ($TaxRefs.Count -gt 0) { $RefIds = ($TaxRefs | ForEach-Object { $_.node_id }) -join ', ' Write-Host " Refs: $RefIds" -ForegroundColor DarkGray } } else { Write-Warn "$($Agent.Name) failed to respond in round $Round" } } } # ── Synthesis ───────────────────────────────────────────────────────────── Write-Step 'Generating synthesis' $FullTranscriptText = & $FormatTranscript $Transcript $SynthesisPrompt = Get-Prompt -Name 'triad-dialogue-synthesis' -Replacements @{ TOPIC = $Topic TRANSCRIPT = $FullTranscriptText } $Synthesis = $null try { $SynthResult = Invoke-AIApi ` -Prompt $SynthesisPrompt ` -Model $Model ` -ApiKey $ResolvedKey ` -Temperature 0.3 ` -MaxTokens 4096 ` -JsonMode ` -TimeoutSec 120 ` -MaxRetries 3 ` -RetryDelays @(5, 15, 45) if ($SynthResult -and $SynthResult.Text) { $SynthText = $SynthResult.Text -replace '(?s)^```json\s*', '' -replace '(?s)\s*```$', '' $Synthesis = $SynthText | ConvertFrom-Json Write-OK 'Synthesis complete' } } catch { Write-Warn "Synthesis generation failed for topic '$Topic' using model '$Model': $($_.Exception.Message)" Write-Info 'The dialogue transcript was generated but synthesis could not be produced. Check your API key/quota and try running synthesis separately.' } # Display synthesis if ($Synthesis) { Write-Host "`n$('─' * 72)" -ForegroundColor Cyan Write-Host ' SYNTHESIS' -ForegroundColor White Write-Host "$('─' * 72)" -ForegroundColor Cyan if ($Synthesis.PSObject.Properties['summary']) { Write-Host "`n $($Synthesis.summary)" -ForegroundColor White } if ($Synthesis.PSObject.Properties['areas_of_agreement'] -and $Synthesis.areas_of_agreement) { Write-Host "`n Areas of Agreement:" -ForegroundColor Green foreach ($A in @($Synthesis.areas_of_agreement)) { Write-Host " - $A" -ForegroundColor Gray } } if ($Synthesis.PSObject.Properties['areas_of_disagreement'] -and $Synthesis.areas_of_disagreement) { Write-Host "`n Areas of Disagreement:" -ForegroundColor Red foreach ($D in @($Synthesis.areas_of_disagreement)) { Write-Host " - $D" -ForegroundColor Gray } } if ($Synthesis.PSObject.Properties['unresolved_questions'] -and $Synthesis.unresolved_questions) { Write-Host "`n Unresolved Questions:" -ForegroundColor Yellow foreach ($Q in @($Synthesis.unresolved_questions)) { Write-Host " - $Q" -ForegroundColor Gray } } } Write-Host "`n$('═' * 72)" -ForegroundColor Cyan # ── Build debate JSON (compatible with taxonomy-editor DebateTab) ───────── $DebateData = [ordered]@{ id = $DebateId title = $Topic created_at = (Get-Date -Format 'o') updated_at = (Get-Date -Format 'o') phase = 'complete' topic = [ordered]@{ original = $Topic refined = $Topic final = $Topic } active_povers = @('prometheus', 'sentinel', 'cassandra') user_is_pover = $false transcript = @($Transcript) rounds = $Rounds source = 'Show-TriadDialogue' } if ($Synthesis) { $DebateData['synthesis'] = $Synthesis } # Write to file if ($OutputFile) { $TargetFile = $OutputFile } else { $DebatesDir = Get-DebatesDir if (-not (Test-Path $DebatesDir)) { $null = New-Item -ItemType Directory -Path $DebatesDir -Force } $TargetFile = Join-Path $DebatesDir "debate-$DebateId.json" } try { $Json = $DebateData | ConvertTo-Json -Depth 20 Write-Utf8NoBom -Path $TargetFile -Value $Json Write-OK "Debate saved to: $TargetFile" } catch { Write-Warn "Failed to save debate to '$TargetFile': $($_.Exception.Message)" Write-Info 'The debate completed but could not be persisted. Check file permissions and disk space, then re-run with -SaveTo to retry.' } return $DebateData } |