prism-inference 0.1.21

Bounded Rust CPU inference and tokenization for Prism-compatible GGUF models
#requires -Version 7.6
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $true

function Invoke-BonsaiNativeProcess {
    <#
    .SYNOPSIS
    Captures an ordinary tool's separate output streams with owned process cleanup.
    .DESCRIPTION
    Inherits the host environment and passes arguments without a shell. A zero
    timeout leaves execution unbounded; output drainage is bounded after exit.
    Nonzero exit codes are returned for the caller to interpret. Optional logs
    contain stdout followed by stderr, including when execution times out.
    Resource-limited model processes must use the separate Windows Job launcher.
    #>
    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory)][string]$Executable,
        [AllowEmptyCollection()][AllowEmptyString()][string[]]$ArgumentList = @(),
        [Parameter(Mandatory)][string]$WorkingDirectory,
        [ValidateRange(0,86400)][int]$TimeoutSeconds = 0,
        [Collections.IDictionary]$Environment,
        [string]$LogPath
    )
    if (-not [IO.Path]::IsPathFullyQualified($WorkingDirectory) -or $WorkingDirectory -match '%[^%]+%' -or
        -not [IO.Directory]::Exists($WorkingDirectory)) {
        throw 'Native working directory must be an existing expanded absolute directory.'
    }
    if ($LogPath -and (-not [IO.Path]::IsPathFullyQualified($LogPath) -or $LogPath -match '%[^%]+%')) {
        throw 'Native log path must be an expanded absolute path.'
    }
    $start = [Diagnostics.ProcessStartInfo]::new($Executable)
    $start.WorkingDirectory = $WorkingDirectory
    $start.UseShellExecute = $false
    $start.CreateNoWindow = $true
    $start.RedirectStandardOutput = $true
    $start.RedirectStandardError = $true
    foreach ($argument in $ArgumentList) { $start.ArgumentList.Add($argument) }
    if ($Environment) {
        foreach ($key in $Environment.Keys) { $start.Environment[$key] = [string]$Environment[$key] }
    }
    $process = [Diagnostics.Process]::new()
    $process.StartInfo = $start
    $started = $false
    try {
        if (-not $process.Start()) { throw "Native process failed to start: $Executable" }
        $started = $true
        # Drain both pipes concurrently so compiler output cannot block the child.
        $stdout = $process.StandardOutput.ReadToEndAsync()
        $stderr = $process.StandardError.ReadToEndAsync()
        $timedOut = $false
        if ($TimeoutSeconds -eq 0) { $process.WaitForExit() }
        elseif (-not $process.WaitForExit($TimeoutSeconds * 1000)) {
            $timedOut = $true
            $process.Kill($true)
            if (-not $process.WaitForExit(10000)) { throw "Native process did not terminate: $Executable" }
        }
        # A descendant may keep a redirected pipe open after the root exits.
        if (-not $stdout.Wait(10000) -or -not $stderr.Wait(10000)) { throw "Native process output drain timed out: $Executable" }
        $result = [pscustomobject]@{
            ExitCode = $process.ExitCode
            StandardOutput = $stdout.GetAwaiter().GetResult()
            StandardError = $stderr.GetAwaiter().GetResult()
        }
        if ($LogPath) { [IO.File]::WriteAllText($LogPath, $result.StandardOutput + $result.StandardError) }
        if ($timedOut) {
            $evidence = if ($LogPath) { "; see $LogPath" } else { '' }
            throw "Native process exceeded $TimeoutSeconds seconds: $Executable$evidence"
        }
        return $result
    } finally {
        try {
            if ($started -and -not $process.HasExited) {
                $process.Kill($true)
                if (-not $process.WaitForExit(10000)) { throw "Native process did not terminate during cleanup: $Executable" }
            }
        } finally { $process.Dispose() }
    }
}

Export-ModuleMember -Function Invoke-BonsaiNativeProcess