param(
|
[Parameter(Mandatory = $true)]
|
[string]$Agent,
|
|
[Parameter(Mandatory = $true)]
|
[string]$Message,
|
|
[string]$RegistryPath = "D:/manage_system/data/codex-bridge/agents.json",
|
|
[string]$ServerUrl,
|
|
[string]$ThreadId,
|
|
[string]$Cwd,
|
|
[switch]$CreateThreadIfMissing,
|
|
[switch]$NoWait,
|
|
[int]$TimeoutSeconds = 180
|
)
|
|
Set-StrictMode -Version Latest
|
$ErrorActionPreference = "Stop"
|
|
function Read-AgentConfig {
|
param([string]$Path, [string]$Name)
|
|
if (!(Test-Path -LiteralPath $Path)) {
|
throw "Agent registry not found: $Path. Copy agents.example.json to this path and fill observer config."
|
}
|
|
$registry = Get-Content -LiteralPath $Path -Encoding UTF8 -Raw | ConvertFrom-Json
|
if (-not ($registry.PSObject.Properties.Name -contains $Name)) {
|
throw "Agent '$Name' not found in registry: $Path"
|
}
|
return $registry.$Name
|
}
|
|
function New-ClientWebSocket {
|
param([string]$Url)
|
|
$ws = [System.Net.WebSockets.ClientWebSocket]::new()
|
$ct = [Threading.CancellationToken]::None
|
try {
|
$ws.ConnectAsync([Uri]$Url, $ct).GetAwaiter().GetResult()
|
} catch {
|
throw "Failed to connect Codex app-server at $Url. Ensure 'codex.cmd app-server --listen $Url' is running and actually listening. Detail: $($_.Exception.Message)"
|
}
|
return $ws
|
}
|
|
function Send-JsonRpc {
|
param(
|
[System.Net.WebSockets.ClientWebSocket]$WebSocket,
|
[object]$Payload
|
)
|
|
$json = $Payload | ConvertTo-Json -Depth 100 -Compress
|
$bytes = [Text.Encoding]::UTF8.GetBytes($json)
|
$segment = [ArraySegment[byte]]::new($bytes)
|
$ct = [Threading.CancellationToken]::None
|
$WebSocket.SendAsync($segment, [System.Net.WebSockets.WebSocketMessageType]::Text, $true, $ct).GetAwaiter().GetResult()
|
}
|
|
function Receive-JsonRpc {
|
param(
|
[System.Net.WebSockets.ClientWebSocket]$WebSocket,
|
[int]$TimeoutSeconds
|
)
|
|
$deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds)
|
$buffer = New-Object byte[] 65536
|
$ms = New-Object System.IO.MemoryStream
|
$ct = [Threading.CancellationToken]::None
|
|
do {
|
if ([DateTime]::UtcNow -gt $deadline) {
|
throw "Timed out waiting for Codex app-server message after $TimeoutSeconds seconds."
|
}
|
|
$segment = [ArraySegment[byte]]::new($buffer)
|
$task = $WebSocket.ReceiveAsync($segment, $ct)
|
if (-not $task.Wait(1000)) {
|
continue
|
}
|
$result = $task.GetAwaiter().GetResult()
|
if ($result.MessageType -eq [System.Net.WebSockets.WebSocketMessageType]::Close) {
|
throw "Codex app-server closed the WebSocket."
|
}
|
if ($result.Count -gt 0) {
|
$ms.Write($buffer, 0, $result.Count)
|
}
|
} until ($result.EndOfMessage)
|
|
$text = [Text.Encoding]::UTF8.GetString($ms.ToArray())
|
return $text | ConvertFrom-Json
|
}
|
|
function Wait-Response {
|
param(
|
[System.Net.WebSockets.ClientWebSocket]$WebSocket,
|
[int]$Id,
|
[int]$TimeoutSeconds
|
)
|
|
while ($true) {
|
$msg = Receive-JsonRpc -WebSocket $WebSocket -TimeoutSeconds $TimeoutSeconds
|
if (($msg.PSObject.Properties.Name -contains "id") -and [int]$msg.id -eq $Id) {
|
if ($msg.PSObject.Properties.Name -contains "error") {
|
throw "Codex app-server returned error for request $Id`: $($msg.error | ConvertTo-Json -Depth 20 -Compress)"
|
}
|
return $msg.result
|
}
|
Write-Host ($msg | ConvertTo-Json -Depth 20 -Compress)
|
}
|
}
|
|
function Wait-TurnCompleted {
|
param(
|
[System.Net.WebSockets.ClientWebSocket]$WebSocket,
|
[string]$TurnId,
|
[int]$TimeoutSeconds
|
)
|
|
while ($true) {
|
$msg = Receive-JsonRpc -WebSocket $WebSocket -TimeoutSeconds $TimeoutSeconds
|
Write-Host ($msg | ConvertTo-Json -Depth 30 -Compress)
|
if (($msg.PSObject.Properties.Name -contains "method") -and $msg.method -eq "turn/completed") {
|
if (-not $TurnId) { return $msg.params }
|
if (($msg.params.PSObject.Properties.Name -contains "turn") -and $msg.params.turn.id -eq $TurnId) {
|
return $msg.params
|
}
|
}
|
}
|
}
|
|
$agentConfig = Read-AgentConfig -Path $RegistryPath -Name $Agent
|
|
if (-not $ServerUrl) { $ServerUrl = [string]$agentConfig.server_url }
|
if (-not $ThreadId) { $ThreadId = [string]$agentConfig.thread_id }
|
if (-not $Cwd) { $Cwd = [string]$agentConfig.cwd }
|
|
if (-not $ServerUrl) {
|
throw "server_url is required for agent '$Agent'."
|
}
|
|
$ws = New-ClientWebSocket -Url $ServerUrl
|
try {
|
$nextId = 1
|
|
Send-JsonRpc -WebSocket $ws -Payload @{
|
id = $nextId
|
method = "initialize"
|
params = @{
|
clientInfo = @{
|
name = "codex-bridge"
|
version = "0.1.0"
|
}
|
capabilities = @{
|
experimentalApi = $true
|
}
|
}
|
}
|
[void](Wait-Response -WebSocket $ws -Id $nextId -TimeoutSeconds $TimeoutSeconds)
|
$nextId++
|
|
if (-not $ThreadId) {
|
if (-not $CreateThreadIfMissing) {
|
throw "thread_id is empty for agent '$Agent'. Use -CreateThreadIfMissing to create a new thread, then write the returned thread_id back to $RegistryPath."
|
}
|
|
$threadParams = @{
|
cwd = $Cwd
|
approvalPolicy = $agentConfig.approval_policy
|
sandbox = $agentConfig.sandbox
|
model = $agentConfig.model
|
persistExtendedHistory = $true
|
}
|
Send-JsonRpc -WebSocket $ws -Payload @{ id = $nextId; method = "thread/start"; params = $threadParams }
|
$threadResult = Wait-Response -WebSocket $ws -Id $nextId -TimeoutSeconds $TimeoutSeconds
|
$ThreadId = [string]$threadResult.thread.id
|
Write-Host "CREATED_THREAD_ID=$ThreadId"
|
$nextId++
|
} else {
|
$resumeParams = @{
|
threadId = $ThreadId
|
cwd = $Cwd
|
approvalPolicy = $agentConfig.approval_policy
|
sandbox = $agentConfig.sandbox
|
model = $agentConfig.model
|
excludeTurns = $true
|
}
|
Send-JsonRpc -WebSocket $ws -Payload @{ id = $nextId; method = "thread/resume"; params = $resumeParams }
|
[void](Wait-Response -WebSocket $ws -Id $nextId -TimeoutSeconds $TimeoutSeconds)
|
$nextId++
|
}
|
|
$turnParams = @{
|
threadId = $ThreadId
|
cwd = $Cwd
|
approvalPolicy = $agentConfig.approval_policy
|
sandboxPolicy = $null
|
model = $agentConfig.model
|
input = @(
|
@{
|
type = "text"
|
text = $Message
|
}
|
)
|
}
|
Send-JsonRpc -WebSocket $ws -Payload @{ id = $nextId; method = "turn/start"; params = $turnParams }
|
$turnResult = Wait-Response -WebSocket $ws -Id $nextId -TimeoutSeconds $TimeoutSeconds
|
$turnId = [string]$turnResult.turn.id
|
Write-Host "TURN_ID=$turnId"
|
|
if (-not $NoWait) {
|
[void](Wait-TurnCompleted -WebSocket $ws -TurnId $turnId -TimeoutSeconds $TimeoutSeconds)
|
}
|
} finally {
|
if ($ws) { $ws.Dispose() }
|
}
|