Kaynağa Gözat

fix: keep bash status rendering and parsing together

Tianyi Cui 3 ay önce
ebeveyn
işleme
419370ea4b

+ 1 - 34
packages/bash/tool-bash/src/index.ts

@@ -69,7 +69,7 @@ import type {} from '@deepseek-ai/dsh-user-approval'
 import type { SandboxMode } from '@deepseek-ai/dsh-sandbox'
 import { BashTaskId, OwnerToken, effectiveSandboxMode } from '@deepseek-ai/dsh-bash'
 import type { BashTask } from '@deepseek-ai/dsh-bash'
-import { renderResult } from './render.ts'
+import { parseExitStatus, renderResult } from './render.ts'
 
 export const name = 'tool-bash'
 export const inject = ['tools', 'bash', 'systemPrompt']
@@ -275,39 +275,6 @@ function presentBashResult(args: unknown, result: ToolResult): ToolResultView |
   return { card: 'terminal', output: raw, ...parseExitStatus(raw) }
 }
 
-/**
- * Recover the structured exit status from a rendered `renderResult` string — the
- * inverse of the status markers it appends. A `[killed by signal: SIG]` marker
- * yields `{signal}`; otherwise an `[exit code: N]` marker yields `{exitCode:N}`;
- * absent both we report `{exitCode:0}` (a clean run appends no marker — and a
- * trapped-timeout run that exits 0 also has none and is accurately exit 0).
- *
- * Why parse rendered text at all: `presentResult` is replay-safe and on a
- * `session/load` the ONLY thing persisted is this content text — the structured
- * `BashRunResult` is long gone — so unless the exit were added to the persisted
- * event schema (deliberately NOT done; see the terminal-rendering RFC), parsing
- * is the only channel. The match is anchored to a LEADING newline + end-of-string
- * because `renderResult` always inserts a `\n` before the marker (line ~124) onto
- * a non-empty body: a real marker is therefore always its own final line. That
- * defeats the common spoof (program output that simply ENDS in `[exit code: 5]`
- * with no trailing newline — a clean exit 0 — no longer reads as a failure).
- *
- * KNOWN RESIDUAL (inherent to the replay-only-sees-text design): a clean exit 0
- * whose body's FINAL line is itself exactly the marker text — `[exit code: N]`
- * or `[killed by signal: SIG]`, printed by the program with nothing after — is
- * still indistinguishable from a real marker and would show a wrong pill. This is
- * display-only (execution and the model-facing text are unaffected) and narrow;
- * the complete fix is to persist a structured exit on the result event, which the
- * RFC names as the escape hatch.
- */
-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 }

+ 22 - 0
packages/bash/tool-bash/src/render.ts

@@ -68,3 +68,25 @@ export function renderResult(
   if (!body.endsWith('\n')) body += '\n'
   return body + markers.join('\n')
 }
+
+/**
+ * Recover the structured exit status from a rendered {@link renderResult}
+ * string — the inverse of the status markers it appends. A killed marker
+ * yields `signal`; otherwise a non-zero marker yields `exitCode`; absent both
+ * means a clean exit 0.
+ *
+ * Replay only retains the rendered content text, not the original
+ * `BashRunResult`, so terminal presentation must recover the exit pill here.
+ * Requiring a leading newline and the end of the string keeps ordinary output
+ * that merely ends with marker-like text from matching unless the final line
+ * is indistinguishable from a real marker.
+ * @param text - rendered model-facing bash result.
+ * @returns the recovered terminal exit code or signal.
+ */
+export 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 }
+}