Eigenverft.Manifested.Agent.InvokeQwenTask.ps1

<#
    Eigenverft.Manifested.Agent.InvokeQwenTask
#>


function Get-QwenSessionStorePath {
    [CmdletBinding()]
    param(
        [string]$LocalRoot = (Get-CodexLocalRoot)
    )

    return (Join-Path (Join-Path $LocalRoot 'sessions') 'named-qwen-sessions.json')
}

function Get-QwenSessionKey {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$SessionName
    )

    return ($SessionName.Trim() -replace '\|', '_')
}

function Read-QwenSessionMap {
    [CmdletBinding()]
    param(
        [string]$SessionStorePath = (Get-QwenSessionStorePath)
    )

    $sessionMap = @{}
    if (-not (Test-Path -LiteralPath $SessionStorePath)) {
        return $sessionMap
    }

    try {
        $raw = Get-Content -LiteralPath $SessionStorePath -Raw
        if (-not [string]::IsNullOrWhiteSpace($raw)) {
            $obj = $raw | ConvertFrom-Json
            foreach ($property in $obj.PSObject.Properties) {
                $sessionMap[$property.Name] = $property.Value
            }
        }
    }
    catch {
        throw "Failed to read Qwen session store: $SessionStorePath"
    }

    return $sessionMap
}

function Write-QwenSessionMap {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [hashtable]$SessionMap,

        [string]$SessionStorePath = (Get-QwenSessionStorePath)
    )

    $sessionStoreRoot = Split-Path -Parent $SessionStorePath
    if (-not (Test-Path -LiteralPath $sessionStoreRoot)) {
        New-Item -ItemType Directory -Path $sessionStoreRoot -Force | Out-Null
    }

    ($SessionMap | ConvertTo-Json -Depth 6) | Set-Content -LiteralPath $SessionStorePath -Encoding UTF8
}

function Resolve-QwenCommandPath {
    [CmdletBinding()]
    param()

    foreach ($candidate in @('qwen.cmd', 'qwen', 'qwen.ps1')) {
        $resolvedQwen = Get-Command $candidate -ErrorAction SilentlyContinue
        if (-not $resolvedQwen) {
            continue
        }

        if ($resolvedQwen.PSObject.Properties['Path'] -and $resolvedQwen.Path) {
            return $resolvedQwen.Path
        }

        return $resolvedQwen.Source
    }

    throw 'qwen was not found on PATH. Install the Qwen CLI or add it to PATH before using Invoke-QwenTask.'
}

function ConvertFrom-QwenJsonLine {
    [CmdletBinding()]
    param(
        [AllowNull()]
        [string]$Line
    )

    if ([string]::IsNullOrWhiteSpace($Line)) {
        return $null
    }

    $text = [string]$Line

    if ($text.Length -gt 0 -and [int][char]$text[0] -eq 0xFEFF) {
        $text = $text.Substring(1)
    }

    try {
        return ($text | ConvertFrom-Json)
    }
    catch {
        return $null
    }
}

function Convert-QwenProcessOutputToLines {
    [CmdletBinding()]
    param(
        [AllowNull()]
        [string]$Text
    )

    if ([string]::IsNullOrEmpty($Text)) {
        return @()
    }

    $lines = [regex]::Split($Text, "\r?\n")

    if ($lines.Count -gt 0 -and [string]::IsNullOrEmpty($lines[$lines.Count - 1])) {
        $lines = @($lines | Select-Object -First ($lines.Count - 1))
    }

    return @($lines)
}

function Get-QwenInvocationLineRecords {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [object]$Invocation
    )

    $records =
        foreach ($sourceName in @('StdOutLines', 'StdErrLines')) {
            foreach ($line in @($Invocation.$sourceName)) {
                $text = [string]$line

                [pscustomobject]@{
                    Source = $sourceName
                    Line   = $text
                    Event  = ConvertFrom-QwenJsonLine -Line $text
                }
            }
        }

    return @($records)
}

function ConvertTo-QwenProcessArgument {
    [CmdletBinding()]
    param(
        [AllowNull()]
        [string]$Value
    )

    if ($null -eq $Value) {
        return '""'
    }

    if ($Value.Length -eq 0) {
        return '""'
    }

    if ($Value -notmatch '[\s"]') {
        return $Value
    }

    $builder = New-Object System.Text.StringBuilder
    [void]$builder.Append('"')
    $pendingBackslashes = 0

    foreach ($char in $Value.ToCharArray()) {
        if ($char -eq '\') {
            $pendingBackslashes++
            continue
        }

        if ($char -eq '"') {
            if ($pendingBackslashes -gt 0) {
                [void]$builder.Append(('\' * ($pendingBackslashes * 2)))
                $pendingBackslashes = 0
            }

            [void]$builder.Append('\"')
            continue
        }

        if ($pendingBackslashes -gt 0) {
            [void]$builder.Append(('\' * $pendingBackslashes))
            $pendingBackslashes = 0
        }

        [void]$builder.Append($char)
    }

    if ($pendingBackslashes -gt 0) {
        [void]$builder.Append(('\' * ($pendingBackslashes * 2)))
    }

    [void]$builder.Append('"')
    return $builder.ToString()
}

function ConvertTo-QwenProcessArgumentString {
    [CmdletBinding()]
    param(
        [string[]]$Arguments
    )

    return ((@($Arguments) | ForEach-Object { ConvertTo-QwenProcessArgument -Value $_ }) -join ' ')
}

function Invoke-QwenProcess {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$QwenCommandPath,

        [string[]]$Arguments,

        [Parameter(Mandatory = $true)]
        [string]$Directory
    )

    $stdoutPath = Join-Path $env:TEMP ("qwen-stdout-{0}.log" -f ([Guid]::NewGuid().ToString('N')))
    $stderrPath = Join-Path $env:TEMP ("qwen-stderr-{0}.log" -f ([Guid]::NewGuid().ToString('N')))

    $argumentString = ConvertTo-QwenProcessArgumentString -Arguments $Arguments

    try {
        $process = Start-Process `
            -FilePath $QwenCommandPath `
            -ArgumentList $argumentString `
            -WorkingDirectory $Directory `
            -RedirectStandardOutput $stdoutPath `
            -RedirectStandardError $stderrPath `
            -Wait `
            -PassThru `
            -NoNewWindow

        $stdoutRaw = ''
        $stderrRaw = ''

        if (Test-Path -LiteralPath $stdoutPath) {
            $stdoutRaw = Get-Content -LiteralPath $stdoutPath -Raw
        }

        if (Test-Path -LiteralPath $stderrPath) {
            $stderrRaw = Get-Content -LiteralPath $stderrPath -Raw
        }

        [pscustomobject]@{
            ExitCode    = [int]$process.ExitCode
            StdOutRaw   = [string]$stdoutRaw
            StdErrRaw   = [string]$stderrRaw
            StdOutLines = @(Convert-QwenProcessOutputToLines -Text $stdoutRaw)
            StdErrLines = @(Convert-QwenProcessOutputToLines -Text $stderrRaw)
        }
    }
    finally {
        Remove-Item -LiteralPath $stdoutPath, $stderrPath -Force -ErrorAction SilentlyContinue
    }
}

function Get-QwenAssistantMessageText {
    [CmdletBinding()]
    param(
        [AllowNull()]
        [object]$Message
    )

    if ($null -eq $Message) {
        return $null
    }

    if ($Message -is [string]) {
        return [string]$Message
    }

    $contentItems = @()
    if ($Message.PSObject.Properties['content'] -and $null -ne $Message.content) {
        $contentItems = @($Message.content)
    }

    $builder = New-Object System.Text.StringBuilder

    foreach ($item in $contentItems) {
        if ($null -eq $item) {
            continue
        }

        if ($item -is [string]) {
            [void]$builder.Append([string]$item)
            continue
        }

        if ($item.PSObject.Properties['text'] -and $item.text) {
            [void]$builder.Append([string]$item.text)
            continue
        }
    }

    if ($builder.Length -gt 0) {
        return $builder.ToString()
    }

    if ($Message.PSObject.Properties['text'] -and $Message.text) {
        return [string]$Message.text
    }

    return $null
}

function Invoke-QwenTask {
<#
.SYNOPSIS
Runs a Qwen non-interactive task and maintains wrapper-level named session state.
 
.DESCRIPTION
Thin PowerShell wrapper around Qwen Code headless mode using `--output-format stream-json`
for structured runs. Named wrapper sessions store:
 
- SessionName
- SessionId
- LastDirectory
- UpdatedUtc
 
Automation behavior:
- always uses `--approval-mode yolo`
- adds `--sandbox` only when `-AllowDangerous:$false`
- named sessions force `--chat-recording`
- named sessions use `--resume <session_id>` for continuity
 
The wrapper keeps a friendly session name that maps to the last observed Qwen session id.
If a stored session id cannot be resumed, the wrapper fails fast instead of silently
starting a fresh conversation.
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [string]$Prompt,

        [Alias('Path')]
        [string]$Directory,

        [Alias('Session')]
        [string]$SessionName,

        [bool]$AllowDangerous = $true,

        [bool]$Json = $true,

        [string]$OutputLastMessage,

        [string]$Model,

        [string[]]$AddDir
    )

    $qwenCmd = Resolve-QwenCommandPath

    $currentDirectory = Resolve-CodexDirectory -Directory ((Get-Location).ProviderPath)
    $directoryProvided = $PSBoundParameters.ContainsKey('Directory')
    $requestedDirectory = $null

    if ($directoryProvided) {
        $requestedDirectory = Resolve-CodexDirectory -Directory $Directory
    }

    $sessionStorePath = Get-QwenSessionStorePath
    $sessionStoreRoot = Split-Path -Parent $sessionStorePath

    if (-not (Test-Path -LiteralPath $sessionStoreRoot)) {
        New-Item -ItemType Directory -Path $sessionStoreRoot -Force | Out-Null
    }

    $sessionMap =
        if (Test-Path -LiteralPath $sessionStorePath) {
            Read-QwenSessionMap -SessionStorePath $sessionStorePath
        }
        else {
            @{}
        }

    $sessionKey = $null
    $existingSession = $null
    $effectiveDirectory = $currentDirectory

    if (-not [string]::IsNullOrWhiteSpace($SessionName)) {
        $sessionKey = Get-QwenSessionKey -SessionName $SessionName

        if ($sessionMap.ContainsKey($sessionKey)) {
            $existingSession = $sessionMap[$sessionKey]
        }

        if ($directoryProvided) {
            $effectiveDirectory = $requestedDirectory
        }
        elseif ($existingSession -and $existingSession.LastDirectory) {
            $effectiveDirectory = Resolve-CodexDirectory -Directory ([string]$existingSession.LastDirectory)
        }
        else {
            $effectiveDirectory = $currentDirectory
        }
    }
    elseif ($directoryProvided) {
        $effectiveDirectory = $requestedDirectory
    }

    $canResume = [bool](
        $existingSession -and
        $existingSession.SessionId
    )

    $effectiveStructuredOutput = [bool](
        -not [string]::IsNullOrWhiteSpace($SessionName) -or
        $Json
    )

    if ([string]::IsNullOrWhiteSpace($OutputLastMessage) -and $effectiveStructuredOutput) {
        $safeDirName = ([IO.Path]::GetFileName($effectiveDirectory)).Trim()
        if ([string]::IsNullOrWhiteSpace($safeDirName)) {
            $safeDirName = 'workspace'
        }

        $safeDirName = ($safeDirName -replace '[^A-Za-z0-9._-]', '_')

        if ([string]::IsNullOrWhiteSpace($SessionName)) {
            $OutputLastMessage = Join-Path $env:TEMP ("qwen-last-message-{0}-{1}.txt" -f $safeDirName, ([Guid]::NewGuid().ToString('N')))
        }
        else {
            $safeSessionFile = ($SessionName -replace '[^A-Za-z0-9._-]', '_')
            $OutputLastMessage = Join-Path $env:TEMP ("qwen-last-message-{0}-{1}.txt" -f $safeDirName, $safeSessionFile)
        }
    }

    $cargs = New-Object System.Collections.Generic.List[string]

    if ($canResume) {
        [void]$cargs.Add('--resume')
        [void]$cargs.Add([string]$existingSession.SessionId)
    }

    if (-not [string]::IsNullOrWhiteSpace($SessionName)) {
        [void]$cargs.Add('--chat-recording')
    }

    if (-not [string]::IsNullOrWhiteSpace($Model)) {
        [void]$cargs.Add('--model')
        [void]$cargs.Add($Model)
    }

    [void]$cargs.Add('--approval-mode')
    [void]$cargs.Add('yolo')

    if (-not $AllowDangerous) {
        [void]$cargs.Add('--sandbox')
    }

    foreach ($dir in @($AddDir)) {
        if (-not [string]::IsNullOrWhiteSpace($dir)) {
            [void]$cargs.Add('--include-directories')
            [void]$cargs.Add((Resolve-CodexDirectory -Directory $dir))
        }
    }

    if ($effectiveStructuredOutput) {
        [void]$cargs.Add('--output-format')
        [void]$cargs.Add('stream-json')
    }

    [void]$cargs.Add($Prompt)

    $argArray = $cargs.ToArray()
    $observedSessionId =
        if ($canResume) {
            [string]$existingSession.SessionId
        }
        else {
            $null
        }
    $lastAgentMessage = $null
    $structuredErrorMessage = $null
    $exitCode = 0

    $invocation = Invoke-QwenProcess -QwenCommandPath $qwenCmd -Arguments $argArray -Directory $effectiveDirectory
    $exitCode = $invocation.ExitCode

    if ($effectiveStructuredOutput) {
        $streamJsonRecords = @(Get-QwenInvocationLineRecords -Invocation $invocation)

        foreach ($record in $streamJsonRecords) {
            Write-Host ([string]$record.Line)
        }

        foreach ($record in $streamJsonRecords) {
            $evt = $record.Event

            if (-not $evt) {
                continue
            }

            if (
                -not [string]::IsNullOrWhiteSpace($SessionName) -and
                $evt.type -eq 'system' -and
                $evt.PSObject.Properties.Match('subtype').Count -gt 0 -and
                $evt.subtype -eq 'session_start' -and
                $evt.PSObject.Properties.Match('session_id').Count -gt 0 -and
                $evt.session_id
            ) {
                $observedSessionId = [string]$evt.session_id

                $sessionMap[$sessionKey] = @{
                    SessionName   = $SessionName
                    SessionId     = $observedSessionId
                    LastDirectory = $effectiveDirectory
                    UpdatedUtc    = [DateTime]::UtcNow.ToString('o')
                }

                Write-QwenSessionMap -SessionMap $sessionMap -SessionStorePath $sessionStorePath
                $existingSession = $sessionMap[$sessionKey]
            }

            if (
                [string]::IsNullOrWhiteSpace($observedSessionId) -and
                $evt.PSObject.Properties.Match('session_id').Count -gt 0 -and
                $evt.session_id
            ) {
                $observedSessionId = [string]$evt.session_id
            }

            if (
                $evt.type -eq 'assistant' -and
                $evt.PSObject.Properties.Match('message').Count -gt 0 -and
                $evt.message
            ) {
                $assistantMessage = Get-QwenAssistantMessageText -Message $evt.message
                if (-not [string]::IsNullOrWhiteSpace($assistantMessage)) {
                    $lastAgentMessage = [string]$assistantMessage
                }
            }

            if (
                [string]::IsNullOrWhiteSpace($lastAgentMessage) -and
                $evt.type -eq 'result' -and
                $evt.PSObject.Properties.Match('result').Count -gt 0 -and
                $evt.result
            ) {
                $lastAgentMessage = [string]$evt.result
            }

            if (
                $evt.type -eq 'error' -and
                $evt.PSObject.Properties.Match('message').Count -gt 0 -and
                $evt.message
            ) {
                $structuredErrorMessage = [string]$evt.message
            }

            if (
                $evt.type -eq 'result' -and
                (
                    ($evt.PSObject.Properties.Match('is_error').Count -gt 0 -and [bool]$evt.is_error) -or
                    ($evt.PSObject.Properties.Match('subtype').Count -gt 0 -and $evt.subtype -ne 'success')
                )
            ) {
                if ($evt.PSObject.Properties.Match('result').Count -gt 0 -and $evt.result) {
                    $structuredErrorMessage = [string]$evt.result
                }
            }
        }
    }
    else {
        foreach ($line in @($invocation.StdErrLines + $invocation.StdOutLines)) {
            Write-Host ([string]$line)
        }
    }

    if ($exitCode -eq 0 -and -not [string]::IsNullOrWhiteSpace($SessionName)) {
        $finalSessionId =
            if (-not [string]::IsNullOrWhiteSpace($observedSessionId)) {
                $observedSessionId
            }
            elseif ($existingSession -and $existingSession.SessionId) {
                [string]$existingSession.SessionId
            }
            else {
                $null
            }

        if ([string]::IsNullOrWhiteSpace($finalSessionId)) {
            throw "qwen completed successfully but did not emit a session id for named session '$SessionName'."
        }

        $sessionMap[$sessionKey] = @{
            SessionName   = $SessionName
            SessionId     = $finalSessionId
            LastDirectory = $effectiveDirectory
            UpdatedUtc    = [DateTime]::UtcNow.ToString('o')
        }

        Write-QwenSessionMap -SessionMap $sessionMap -SessionStorePath $sessionStorePath
        $existingSession = $sessionMap[$sessionKey]
    }

    if (-not [string]::IsNullOrWhiteSpace($OutputLastMessage) -and -not [string]::IsNullOrWhiteSpace($lastAgentMessage)) {
        Set-Content -LiteralPath $OutputLastMessage -Value $lastAgentMessage -Encoding UTF8
    }

    if ($exitCode -ne 0) {
        if ($canResume) {
            $resumeMessage = "qwen command failed with exit code $exitCode while resuming session '$SessionName' ($([string]$existingSession.SessionId))."

            if (-not [string]::IsNullOrWhiteSpace($structuredErrorMessage)) {
                throw "$resumeMessage $structuredErrorMessage Remove or replace the stored Qwen session mapping if the session id is stale."
            }

            throw "$resumeMessage Remove or replace the stored Qwen session mapping if the session id is stale."
        }

        if (-not [string]::IsNullOrWhiteSpace($structuredErrorMessage)) {
            throw "qwen command failed with exit code $exitCode. $structuredErrorMessage"
        }

        throw "qwen command failed with exit code $exitCode."
    }

    [pscustomobject]@{
        CommandPath       = $qwenCmd
        Directory         = $effectiveDirectory
        SessionName       = $SessionName
        SessionId         = if ($existingSession) { $existingSession.SessionId } else { $observedSessionId }
        Prompt            = $Prompt
        AllowDangerous    = [bool]$AllowDangerous
        Json              = [bool]$effectiveStructuredOutput
        OutputLastMessage = $OutputLastMessage
        LastAgentMessage  = $lastAgentMessage
        ExitCode          = $exitCode
        Resumed           = $canResume
        EffectiveArgs     = $argArray
    }
}