Public/Invoke-LLMSwarm.ps1
|
function Invoke-LLMSwarm { <# .SYNOPSIS Decompose a goal into parallel sub-tasks, run them concurrently as worker agents, then synthesize the results — all driven by a single prompt. .DESCRIPTION Phase 1 DECOMPOSE — An orchestrator LLM call breaks the goal into a JSON task list with optional DependsOn relationships (a DAG). Phase 2 DISPATCH — Each task runs in a RunspacePool that clones the user's session. Workers call Invoke-LLMAgent with a dedicated runspace and $refs registry. Tasks with DependsOn wait until their dependencies complete. Results from prior tasks are substituted into dependent task prompts via {{result::<id>}}. Phase 3 SYNTHESIZE — The orchestrator receives all results and produces a single coherent final answer. .PARAMETER Goal The top-level objective. Accepts pipeline input. .PARAMETER Provider Anthropic or OpenAI. .PARAMETER Model Model override (applies to all phases). .PARAMETER MaxTasks Maximum tasks the orchestrator may create. Default 8. .PARAMETER TimeoutSec Wall-clock timeout across all workers. Default 300s. .PARAMETER Quiet Suppress the live task board and synthesis rendering. .OUTPUTS [LLMSwarmResult] with .Tasks, .Synthesis, .TotalTokens, .TotalSec .EXAMPLE Invoke-LLMSwarm "Audit this machine: running services, open ports, large processes, and disk usage" -Provider Anthropic .EXAMPLE Invoke-LLMSwarm "Research and compare: PS 5.1 vs PS 7 module compatibility" -Provider Anthropic -MaxTasks 4 .EXAMPLE # Pipeline "Summarise all .log files in C:\Logs" | Invoke-LLMSwarm -Provider Anthropic #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Mandatory, ValueFromPipeline, Position=0)] [string]$Goal, [ValidateSet('Anthropic','OpenAI')] [string]$Provider, [string]$Model, [ValidateRange(1,20)] [int]$MaxTasks = 8, [ValidateRange(30,3600)] [int]$TimeoutSec = 300, [switch]$Quiet ) begin { if (-not $Provider) { $Provider = $env:LLM_DEFAULT_PROVIDER ?? 'Anthropic' } if (-not $Model) { $Model = $script:Providers[$Provider].DefaultModel } } process { script:Push-Preferences $script:VerbosePreference = $VerbosePreference $script:DebugPreference = $DebugPreference try { Write-Verbose "Invoke-LLMSwarm: $Provider/$Model, maxTasks=$MaxTasks, timeout=${TimeoutSec}s" $swStart = [System.Diagnostics.Stopwatch]::StartNew() $totalTokens = 0 $totalInputTokens = 0 $totalOutputTokens = 0 # ── Phase 1: Decompose ───────────────────────────────────────────── if (-not $Quiet) { script:Write-Status "Decomposing goal into tasks…" 'info' } $envContext = "PSVersion: $($PSVersionTable.PSVersion) OS: $([System.Runtime.InteropServices.RuntimeInformation]::OSDescription)" $decomp = script:Invoke-OrchestratorDecompose -Goal $Goal -Provider $Provider ` -Model $Model -Context $envContext -MaxTasks $MaxTasks $tasks = $decomp.Tasks $totalTokens += $decomp.Tokens $totalInputTokens += $decomp.InputTokens $totalOutputTokens += $decomp.OutputTokens if (-not $Quiet) { script:Write-SwarmHeader -Goal $Goal -TaskCount $tasks.Count } # ── Phase 2: Dispatch DAG ───────────────────────────────────────── $script:SwarmShared.Clear() $finishedTasks = script:Invoke-SwarmDispatcher ` -Tasks $tasks -Provider $Provider -Model $Model ` -Shared $script:SwarmShared -MaxRunspaces ([Math]::Min($tasks.Count, 4)) ` -TimeoutSec $TimeoutSec $finishedTasks | ForEach-Object { if ($_.Result -is [PSCustomObject] -and $_.Result.TotalTokens) { $totalTokens += $_.Result.TotalTokens $totalInputTokens += $_.Result.InputTokens $totalOutputTokens += $_.Result.OutputTokens } } # ── Phase 3: Synthesize ─────────────────────────────────────────── if (-not $Quiet) { Write-Host "" script:Write-Status "Synthesizing results…" 'info' } $synth = script:Invoke-OrchestratorSynthesize -Goal $Goal ` -TaskResults $finishedTasks -Provider $Provider -Model $Model $totalTokens += $synth.Tokens $totalInputTokens += $synth.InputTokens $totalOutputTokens += $synth.OutputTokens $swStart.Stop() $result = [PSCustomObject]@{ PSTypeName = 'LLMSwarmResult' Goal = $Goal Provider = $Provider Model = $Model Tasks = $finishedTasks Synthesis = $synth.Content InputTokens = $totalInputTokens OutputTokens = $totalOutputTokens TotalTokens = $totalTokens TotalSec = $swStart.Elapsed.TotalSeconds StartedAt = [datetime]::UtcNow - $swStart.Elapsed } $dds = [System.Management.Automation.PSPropertySet]::new( 'DefaultDisplayPropertySet', [string[]]@('Goal','InputTokens','OutputTokens','TotalTokens','TotalSec','Synthesis')) $result.PSObject.Members.Add( [System.Management.Automation.PSMemberSet]::new('PSStandardMembers',[System.Management.Automation.PSMemberInfo[]]@($dds))) $globalName = script:Save-GlobalResult -Type 'swarm' -Prompt $Goal -Result $result $result | Add-Member -NotePropertyName GlobalName -NotePropertyValue $globalName if (-not $Quiet) { script:Write-SwarmSummary -Result $result } $done = @($finishedTasks | Where-Object Status -eq 'done').Count $failed = @($finishedTasks | Where-Object Status -eq 'failed').Count if ($failed -gt 0) { Write-Warning "Swarm completed with $failed failed task(s) out of $(@($finishedTasks).Count)" } Write-Verbose "Swarm complete: $done done, $failed failed, $totalTokens tokens, $([math]::Round($swStart.Elapsed.TotalSeconds,2))s" return $result } finally { script:Pop-Preferences } } } |