|
|
@@ -23,8 +23,9 @@ import type {} from '@deepseek-ai/dsh-system-prompt'
|
|
|
import type {} from '@deepseek-ai/dsh-user-approval'
|
|
|
import type { SandboxMode } from '@deepseek-ai/dsh-sandbox'
|
|
|
import { BashTaskId, DSH_ENV_PREFIX, OwnerToken, effectiveSandboxMode } from '@deepseek-ai/dsh-bash'
|
|
|
-import type { BashRunResult, BashTask, CollectedOutput, DshEnvironment, DshEnvironmentKey } from '@deepseek-ai/dsh-bash'
|
|
|
+import type { BashTask, DshEnvironment, DshEnvironmentKey } from '@deepseek-ai/dsh-bash'
|
|
|
import { DSH_HOME_ENV, resolveDshHome } from '@deepseek-ai/dsh-home'
|
|
|
+import { parseExitStatus, renderResult } from './render.ts'
|
|
|
|
|
|
declare module 'cordis' {
|
|
|
interface Context {
|
|
|
@@ -295,65 +296,6 @@ function bashDescription(escalationModes: readonly SandboxMode[]): string {
|
|
|
+ 'it — but it does not forbid attempting or escalating other commands later.'
|
|
|
}
|
|
|
|
|
|
-/** 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)'}]`
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * Shape one finished run into model-visible stdout, marked stderr, and status
|
|
|
- * facts. Non-zero exits and sandbox denials remain ordinary results; only
|
|
|
- * infrastructure failure or abort makes the tool call itself fail.
|
|
|
- *
|
|
|
- * @param result - the completed foreground run from the executor.
|
|
|
- * @param escalationModes - the escalation targets this composition advertises; non-empty
|
|
|
- * adds the same-turn escalation hint after a denial marker (default `[]`: no hint).
|
|
|
- * @returns the model-facing text: output body (or `(no output)`), then any
|
|
|
- * timeout/signal/exit markers, each on its own line.
|
|
|
- */
|
|
|
-export function renderResult(
|
|
|
- result: BashRunResult,
|
|
|
- escalationModes: readonly SandboxMode[] = [],
|
|
|
-): string {
|
|
|
- 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[] = []
|
|
|
- // Keep `[exit code: N]` last so parseExitStatus() can recover it. A denial,
|
|
|
- // like a timeout, remains a reported fact for the model to handle.
|
|
|
- if (result.sandbox?.denied) {
|
|
|
- markers.push(`[sandbox: file access denied under ${result.sandbox.mode} mode]`)
|
|
|
- // Add the retry hint only when the schema advertises escalation, before
|
|
|
- // the final exit marker.
|
|
|
- if (escalationModes.length > 0) {
|
|
|
- markers.push('[sandbox: escalation available — retry this exact command once with sandbox_permissions (the narrowest wider mode that suffices) + justification; the approval prompt asks the user]')
|
|
|
- }
|
|
|
- }
|
|
|
- // Timeout is reported independently of how the process actually ended: a
|
|
|
- // command can trap SIGTERM and exit 0 after our timer fired (e.g.
|
|
|
- // `trap "exit 0" TERM; sleep 60`), giving timedOut:true / exitCode:0 /
|
|
|
- // signal:null — the model must still see that the command was cut short.
|
|
|
- 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')
|
|
|
-}
|
|
|
-
|
|
|
// Pure tool-owned presentation used for both live events and replay.
|
|
|
|
|
|
/**
|
|
|
@@ -401,18 +343,6 @@ function presentBashResult(args: unknown, result: ToolResult): ToolResultView |
|
|
|
return { card: 'terminal', output: raw, ...parseExitStatus(raw) }
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * Recover exit status from the final marked line emitted by {@link renderResult}.
|
|
|
- * A program whose own final line exactly mimics a marker remains ambiguous for UI display.
|
|
|
- */
|
|
|
-function parseExitStatus(text: string): { exitCode: number } | { signal: string } {
|
|
|
- const signal = /\n\[killed by signal: ([^\]\n]+)\]$/.exec(text)
|
|
|
- if (signal?.[1] !== undefined) return { signal: signal[1] }
|
|
|
- const exit = /\n\[exit code: (\d+)\]$/.exec(text)
|
|
|
- if (exit?.[1] !== undefined) return { exitCode: Number(exit[1]) }
|
|
|
- return { exitCode: 0 }
|
|
|
-}
|
|
|
-
|
|
|
/** Pending-state presentation for `bash_output`/`bash_kill` (background-task tools). */
|
|
|
function presentTaskCall(verb: string, args: { task_id: string }): GenericCallView {
|
|
|
return { card: 'generic', title: `${verb} background task ${args.task_id}`, kind: 'execute', rawInput: args.task_id }
|