2026-08-02 14:18:29 +08:00
/**
* Model-facing result rendering for the pwsh tool — the PowerShell twin of
2026-08-08 20:11:21 +08:00
* `dsh-tool-bash`'s renderer: stdout, a marked stderr section, sandbox
* denial/runner-failure markers (with the same-turn escalation hint), and
* truncation notices with spill paths, then exit-status markers. Non-zero
* exits are reported, not errored — the model decides how to react; only
* infrastructure failures (spawn errors, aborts) surface as isError
* results.
2026-08-02 14:18:29 +08:00
*
* @module @deepseek-ai/dsh-tool-pwsh/render
*/
2026-08-08 20:11:21 +08:00
import type { BashProcessRead , BashSandboxInfo , CollectedOutput } from '@deepseek-ai/dsh-bash'
import type { SandboxMode } from '@deepseek-ai/dsh-sandbox'
import { escalationHintMarker , sandboxDenialMarker } from '@deepseek-ai/dsh-sandbox'
2026-08-02 14:18:29 +08:00
2026-08-08 20:11:21 +08:00
/* jscpd:ignore-start -- deliberate twin of dsh-tool-bash/render.ts (Agent Note). */
2026-08-02 14:18:29 +08:00
/** Append the truncation notice (with the full-output spill path) to a stream's text. */
function streamText ( output : CollectedOutput ) : string {
if ( ! output . truncated ) return output . text
return ` ${ output . text } \ n[output truncated; full output: ${ output . spillPath ? ? '(unavailable)' } ] `
}
/** The renderable foreground result shape (the schema-derived value, no `kind`). */
export interface RenderablePwshResult {
exitCode : number | null
signal : string | null
timedOut : boolean
timeoutMs : number
stdout : CollectedOutput
stderr : CollectedOutput
2026-08-08 20:11:21 +08:00
sandbox? : BashSandboxInfo
2026-08-02 14:18:29 +08:00
}
/**
* Shape one finished run into the text the model sees: stdout, then a marked
* stderr section, then exit-status markers, matching the bash tool's story —
* a clean exit (0, no signal) produces no marker.
* @param result - the completed foreground run from the executor.
2026-08-08 20:11:21 +08:00
* @param escalationModes - the escalation targets this composition advertises;
* non-empty adds the same-turn escalation hint after a denial marker
* (default `[]`: no hint).
2026-08-02 14:18:29 +08:00
* @returns the model-facing text: output body (or `(no output)`), then any timeout/signal/exit markers, each on its own line.
*/
2026-08-08 20:11:21 +08:00
export function renderPwshResult (
result : RenderablePwshResult ,
escalationModes : readonly SandboxMode [ ] = [ ] ,
) : string {
2026-08-02 14:18:29 +08:00
const out = streamText ( result . stdout )
const err = streamText ( result . stderr )
let body = out
if ( err . length > 0 ) {
// Single newline between sections (stdout usually ends with one already).
if ( body . length > 0 && ! body . endsWith ( '\n' ) ) body += '\n'
body += ` [stderr] \ n ${ err } `
}
if ( body . length === 0 ) body = '(no output)'
const markers : string [ ] = [ ]
2026-08-08 20:11:21 +08:00
// Keep the exit marker last because parseExitStatus anchors there.
if ( result . sandbox ? . denied ) {
markers . push ( sandboxDenialMarker ( result . sandbox . mode ) )
// Hint only when the composition exposes escalation, before the final exit marker.
if ( escalationModes . length > 0 ) {
markers . push ( escalationHintMarker ( 'command' ) )
}
}
2026-08-02 14:18:29 +08:00
// A command may trap the termination and exit 0 after timeout; still report interruption.
if ( result . timedOut ) markers . push ( ` [timed out after ${ result . timeoutMs } ms] ` )
if ( result . signal !== null ) {
markers . push ( ` [killed by signal: ${ result . signal } ] ` )
} else if ( result . exitCode !== 0 ) {
markers . push ( ` [exit code: ${ result . exitCode } ] ` )
}
if ( markers . length === 0 ) return body
if ( ! body . endsWith ( '\n' ) ) body += '\n'
return body + markers . join ( '\n' )
}
/**
* Shape one background-process read into the `task_output` delta the model
* sees: the incremental delta, plus the lossy-read notice (with full-stream
* spill paths) when in-memory truncation dropped unread bytes.
* @param read - one incremental read from the process handle.
2026-08-08 20:11:21 +08:00
* @param sandbox - settled sandbox facts, when this was a confined process.
* @param escalationModes - escalation targets advertised by this composition.
* @returns the delta text with any loss or sandbox notice appended.
2026-08-02 14:18:29 +08:00
*/
2026-08-08 20:11:21 +08:00
export function renderPwshProcessRead (
read : BashProcessRead ,
sandbox? : BashSandboxInfo ,
escalationModes : readonly SandboxMode [ ] = [ ] ,
) : string {
2026-08-02 14:18:29 +08:00
const notices : string [ ] = [ ]
if ( read . lossy ) {
const paths = [ read . stdoutSpillPath , read . stderrSpillPath ] . filter ( ( path ) : path is string = > path !== undefined )
notices . push ( ` [some output was dropped from memory; full output: ${ paths . length > 0 ? paths . join ( ', ' ) : '(unavailable)' } ] ` )
}
2026-08-08 20:11:21 +08:00
if ( sandbox ? . runnerFailed ) {
notices . push ( ` [sandbox: the sandbox runner itself failed under ${ sandbox . mode } mode — the command did not run; this is a sandbox problem, not a command failure] ` )
} else if ( sandbox ? . denied ) {
notices . push ( sandboxDenialMarker ( sandbox . mode ) )
if ( escalationModes . length > 0 ) {
notices . push ( escalationHintMarker ( 'command' ) )
}
}
2026-08-02 14:18:29 +08:00
if ( notices . length === 0 ) return read . delta
return ` ${ read . delta } ${ read . delta . length > 0 && ! read . delta . endsWith ( '\n' ) ? '\n' : '' } ${ notices . join ( '\n' ) } `
}
/* jscpd:ignore-end */