فهرست منبع

fix(trajectory): identify PTC calls from recorded tool metadata

_Kerman 1 ماه پیش
والد
کامیت
b6858789f0

+ 2 - 2
.agents/notes/implemented/feature/2026-09-09-ptc-trajectory-code-inspection.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-09-ptc-trajectory-code-inspection.md
-2026-09-09-ptc-trajectory-code-inspection.md: 55cbbae97371b69b7574ebb0674c2178b586b83a
-2026-09-09-ptc-trajectory-code-inspection.zh.md: 94b6993a4e4e2d1f3e93256f79a68121f910169d
+2026-09-09-ptc-trajectory-code-inspection.md: 477d199f519b5d515e5d58430bd902d9d209e46b
+2026-09-09-ptc-trajectory-code-inspection.zh.md: f305569176214c63ac549b0ec5103289e3e57a88

+ 2 - 2
.agents/notes/implemented/feature/2026-09-09-ptc-trajectory-code-inspection.md

@@ -10,7 +10,7 @@ PTC programs arrive as JSON string arguments. Escaping makes long programs diffi
 
 ## Decision
 
-[Trajectory](../../../../packages/client/ui-trajectory/README.md) derives a code inspector from recorded `run_code` arguments. It preserves source bytes for copying and exposes the original JSON through a separate toggle. Syntax highlighting requires an unambiguous TypeScript or Python hint in that call's recorded parameter description; unknown or conflicting hints leave plain text. Unsupported arguments retain the generic inspector.
+[Trajectory](../../../../packages/client/ui-trajectory/README.md) identifies calls by their recorded tool name and derives a code inspector from validated `run_code` arguments. The validated program carries the original JSON text; copying preserves source and argument bytes and exposes the original JSON through a separate toggle. Syntax highlighting requires an unambiguous TypeScript or Python hint in that call's recorded parameter description; unknown or conflicting hints leave plain text. Unsupported arguments retain the generic inspector.
 
 The result view preserves recorded text and uses a tree only for complete JSON objects or arrays. Errors retain captured output. Code views sample the shared wrapping preference when opened and keep their own choice while mounted.
 
@@ -26,4 +26,4 @@ The [PTC runtime decision](2026-06-15-ptc.md) still owns execution and settlemen
 
 ## Consequences
 
-Readers can inspect and copy recorded programs without changing replay data. Schemas with no recognizable language hint receive no syntax highlighting. Existing component tests cover fallback, exact copying, output states, and independent wrapping; the [PTC browser scenario](../../../../apps/web/tests/ptc-round.e2e.ts) pins the assembled inspector and verifies overflow and the original-JSON round trip.
+Readers can inspect and copy recorded programs without changing replay data. Schemas with no recognizable language hint receive no syntax highlighting. Component tests cover recorded-name recognition, schema fallback, exact source and argument copying, output states, and independent wrapping. JSON-tree tests cover clipping geometry, missing `ResizeObserver`, clipboard settlement after hover changes or unmount, and value-read counts during hover. Thinking tests cover body arrival, manual disclosure, and switching records; the [PTC browser scenario](../../../../apps/web/tests/ptc-round.e2e.ts) pins the assembled inspector and verifies overflow and the original-JSON round trip.

+ 2 - 2
.agents/notes/implemented/feature/2026-09-09-ptc-trajectory-code-inspection.zh.md

@@ -10,7 +10,7 @@ PTC 程序以 JSON 字符串参数传入。转义使长程序在通用参数树
 
 ## 决策
 
-[Trajectory](../../../../packages/client/ui-trajectory/README.zh.md) 从已记录的 `run_code` 参数派生代码检查器。复制时保留源码字节,并通过独立切换按钮展示原始 JSON。语法高亮要求该调用记录的参数说明包含明确且无冲突的 TypeScript 或 Python 提示;未知或冲突的提示使用纯文本。不支持的参数保留通用检查器。
+[Trajectory](../../../../packages/client/ui-trajectory/README.zh.md) 按已记录的工具名识别调用,并从已验证的 `run_code` 参数派生代码检查器。验证后的程序携带原始 JSON 文本;复制时保留源码与参数字节,并通过独立切换按钮展示原始 JSON。语法高亮要求该调用记录的参数说明包含明确且无冲突的 TypeScript 或 Python 提示;未知或冲突的提示使用纯文本。不支持的参数保留通用检查器。
 
 结果视图保留记录的文本,只对完整 JSON 对象或数组使用树形展示。错误保留已捕获的输出。代码视图在打开时读取共享换行偏好,并在挂载期间保留自身的选择。
 
@@ -26,4 +26,4 @@ PTC 程序以 JSON 字符串参数传入。转义使长程序在通用参数树
 
 ## 后果
 
-读者可以检查和复制已记录的程序,无需修改回放数据。Schema 没有可识别的语言提示时不提供语法高亮。现有组件测试覆盖回退、原样复制、输出状态与独立换行;[PTC 浏览器场景](../../../../apps/web/tests/ptc-round.e2e.ts) 固定组装后的检查器展示,并验证溢出和原始 JSON 的往返切换。
+读者可以检查和复制已记录的程序,无需修改回放数据。Schema 没有可识别的语言提示时不提供语法高亮。组件测试覆盖记录工具名识别、Schema 回退、源码与参数原样复制、输出状态及独立换行。JSON 树测试覆盖裁剪几何、缺少 `ResizeObserver`、悬停切换或卸载后剪贴板写入落定,以及悬停期间读取值的次数。思考测试覆盖正文到达、手动展开折叠及记录切换;[PTC 浏览器场景](../../../../apps/web/tests/ptc-round.e2e.ts) 固定组装后的检查器展示,并验证溢出和原始 JSON 的往返切换。

+ 2 - 2
packages/client/ui-trajectory/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/client/ui-trajectory/README.md
-README.md: 80d8898e0fabd4671581a84198250fe408d51009
-README.zh.md: 5c85faaadb56be6dc3cc85753494c7fd88333c92
+README.md: 6530eb83563620fc26761c421204427c9698732b
+README.zh.md: 09ffd59282e407f61126b6e5064cf75740bb52c1

+ 1 - 1
packages/client/ui-trajectory/README.md

@@ -29,7 +29,7 @@ Open the Trajectory tab in the conversation's view ring to inspect agent activit
 
 ### Inspecting records
 
-For `run_code`, the row shows the program description and the inspector opens numbered, highlighted source. The Code tab provides wrapping, a `{}` toggle for the original JSON arguments, and exact source copying, including trailing newlines. Each newly opened code view takes the last wrapping choice; changing it leaves other open views as they are. Output preserves the recorded text, using a tree for complete JSON objects or arrays. Highlighting uses only an unambiguous TypeScript or Python hint in the recorded tool schema; missing or conflicting hints leave plain source. See the [PTC inspection decision](../../../.agents/notes/implemented/feature/2026-09-09-ptc-trajectory-code-inspection.md) for replay constraints.
+Calls are identified by their recorded tool name. For `run_code`, the row shows the program description and the inspector opens numbered, highlighted source. The Code tab provides wrapping, a `{}` toggle for the original JSON arguments, and exact source copying, including trailing newlines. Copying the original arguments retains their recorded JSON whitespace. Each newly opened code view takes the last wrapping choice; changing it leaves other open views as they are. Output preserves the recorded text, using a tree for complete JSON objects or arrays. Highlighting uses only an unambiguous TypeScript or Python hint in the recorded tool schema; missing or conflicting hints leave plain source. See the [PTC inspection decision](../../../.agents/notes/implemented/feature/2026-09-09-ptc-trajectory-code-inspection.md) for replay constraints.
 
 Selection, timeline navigation, folding, and search cover the React-visible window. Request numbers and cumulative usage cover the complete resident snapshot. Selecting a record opens a local inspector for token usage, duration, Input, Output, Timing, and durable images. Image URLs use the Conversation-owned per-session cache, so Chat and Trajectory share one authorized read per attachment. A user record shows the generic-file count beside its text, while a record without text shows its image and file counts. A standalone compaction request appears chronologically in its own `Between turns` section, while a numbered compaction remains inside its owning turn.
 

+ 1 - 1
packages/client/ui-trajectory/README.zh.md

@@ -29,7 +29,7 @@ Trajectory 标签页让你以按轮次组织的事件记录表和交互式时间
 
 ### 检查记录
 
-对于 `run_code`,记录行展示程序说明,检查器显示带行号和语法高亮的源码。“代码”页提供换行、查看原始 JSON 参数的 `{}` 切换按钮,以及保留末尾换行的源码原样复制。每个新打开的代码视图采用上次选择的换行状态;切换时,其他已打开视图保持原状。输出保留记录中的文本,完整 JSON 对象或数组使用树形展示。高亮只采用已记录工具 Schema 中明确且无冲突的 TypeScript 或 Python 提示;提示缺失或冲突时显示纯文本源码。回放约束见 [PTC 检查决策](../../../.agents/notes/implemented/feature/2026-09-09-ptc-trajectory-code-inspection.zh.md)。
+调用按已记录的工具名识别。对于 `run_code`,记录行展示程序说明,检查器显示带行号和语法高亮的源码。“代码”页提供换行、查看原始 JSON 参数的 `{}` 切换按钮,以及保留末尾换行的源码原样复制。复制原始参数时保留记录中 JSON 的空白。每个新打开的代码视图采用上次选择的换行状态;切换时,其他已打开视图保持原状。输出保留记录中的文本,完整 JSON 对象或数组使用树形展示。高亮只采用已记录工具 Schema 中明确且无冲突的 TypeScript 或 Python 提示;提示缺失或冲突时显示纯文本源码。回放约束见 [PTC 检查决策](../../../.agents/notes/implemented/feature/2026-09-09-ptc-trajectory-code-inspection.zh.md)。
 
 选择、时间线导航、折叠与搜索只覆盖 React 可见窗口。请求编号与累计用量覆盖完整的驻留 snapshot。选择记录会打开局部检查器,查看 token 用量、耗时、输入、输出、计时与持久图片。图片 URL 使用 Conversation 拥有的逐会话缓存,因此 Chat 与 Trajectory 对每个附件共享一次已授权读取。用户记录会在文本旁显示通用文件数量;记录没有文本时,则显示图片与文件数量。独立运行的压缩请求会按时间顺序显示在自己的 `Between turns` 区段中,而带编号的压缩仍位于其所属轮次内。
 

+ 1 - 0
packages/client/ui-trajectory/src/client/TrajectoryCell.tsx

@@ -57,6 +57,7 @@ export function TrajectoryCell({
   assistantMetrics: _assistantMetrics,
   result: _result,
   callId: _callId,
+  toolName: _toolName,
   isError: _isError,
   timeSeconds,
   startedAt: _startedAt,

+ 16 - 14
packages/client/ui-trajectory/src/client/TrajectoryTable.tsx

@@ -36,7 +36,7 @@ import type { TrajectoryTurnModel } from './layout.ts'
 import { trajectoryPreviewText } from './trajectory-preview.ts'
 import type { TrajectoryKey, TrajectoryTranslate } from './locales.ts'
 import { COMPACTION_INTERRUPTED_ERROR } from './copy-codes.ts'
-import { codeProgram, type CodeProgram } from './code-program.ts'
+import { codeProgram, PTC_TOOL_NAME, type CodeProgram } from './code-program.ts'
 import css from './TrajectoryTable.module.css'
 
 const BOTTOM_FOLLOW_THRESHOLD_PX = 2
@@ -190,6 +190,7 @@ interface ParentRecords {
 }
 
 interface ToolCallTextParts {
+  program: boolean
   name: string
   args?: string
 }
@@ -1011,7 +1012,7 @@ function detailTabs(record: TableRecord): readonly DetailTabItem[] {
 function recordDisplayText(cell: TrajectoryCellProps, t: TrajectoryTranslate): string {
   if (isToolCallOnly(cell, t)) return ''
   const program = codeProgram(cell)
-  if (program !== undefined) return `${cell.text.split(' · ', 1)[0]} · ${program.description.replace(/\s+/g, ' ')}`
+  if (program !== undefined) return `${PTC_TOOL_NAME} · ${program.description.replace(/\s+/g, ' ')}`
   if (cell.previewMarkdown !== undefined) {
     const preview = trajectoryPreviewText(cell.previewMarkdown)
     if (cell.text === '') return preview
@@ -1033,13 +1034,15 @@ function recordResultText(cell: TrajectoryCellProps): string | undefined {
 }
 
 function toolCallTextParts(
-  kind: TrajectoryCellKind,
+  cell: TrajectoryCellProps,
   text: string,
 ): ToolCallTextParts | undefined {
-  if (kind !== 'tool' && kind !== 'subtool') return undefined
+  if (cell.kind !== 'tool' && cell.kind !== 'subtool') return undefined
+  const program = cell.toolName === PTC_TOOL_NAME
   const separator = text.indexOf(' · ')
-  if (separator === -1) return { name: text }
+  if (separator === -1) return { name: text, program }
   return {
+    program,
     name: text.slice(0, separator),
     args: text.slice(separator + 3),
   }
@@ -1072,7 +1075,7 @@ function RecordPresentation({
   const displayText = useMemo(
     () => recordDisplayText(cell, t),
     [
-      cell.kind, cell.text, cell.previewMarkdown,
+      cell.kind, cell.text, cell.toolName, cell.previewMarkdown,
       cell.inputDetail, cell.outputDetail, cell.thinkingDetail, t,
     ],
   )
@@ -1081,7 +1084,7 @@ function RecordPresentation({
     [cell.result, cell.resultPreviewMarkdown],
   )
   const toolCallOnly = isToolCallOnly(cell, t)
-  const toolCallText = toolCallTextParts(cell.kind, displayText)
+  const toolCallText = toolCallTextParts(cell, displayText)
   const listDisplayText = toolCallOnly
     ? t('record.toolCallOnly')
     : toolCallText === undefined
@@ -1111,11 +1114,11 @@ function RecordListText({
   return (
     <>
       <span className={css.toolCallNameTypeface}>
-        {toolCallText.name === 'run_code' && <IconCodeOutline16 className={css.programIcon} size={12} />}
+        {toolCallText.program && <IconCodeOutline16 className={css.programIcon} size={12} />}
         {toolCallText.name || '—'}
       </span>
       {toolCallText.args !== undefined && (
-        <span className={toolCallText.name === 'run_code' ? css.programSummary : css.toolCallPayload}>
+        <span className={toolCallText.program ? css.programSummary : css.toolCallPayload}>
           {toolCallText.args}
         </span>
       )}
@@ -1840,9 +1843,8 @@ function InspectorCopyButton({ text, label, t }: {
   )
 }
 
-function ProgramInput({ program, rawInput, initialWrapped, stringWrapping, onOpen, t }: {
+function ProgramInput({ program, initialWrapped, stringWrapping, onOpen, t }: {
   program: CodeProgram
-  rawInput: string
   initialWrapped: boolean
   stringWrapping: JsonTreeProps['stringWrapping']
   onOpen?: () => void
@@ -1891,7 +1893,7 @@ function ProgramInput({ program, rawInput, initialWrapped, stringWrapping, onOpe
         </button>
       )}
       <InspectorCopyButton
-        text={showJson ? rawInput : program.source}
+        text={showJson ? program.rawInput : program.source}
         label={t(showJson ? 'copy.json' : 'code.copySource')}
         t={t}
       />
@@ -3350,7 +3352,7 @@ export function TrajectoryTable({
                     ? (
                       <>
                         <ProgramInput key={`preview:${selectedRecordId}`} program={selectedProgram}
-                          rawInput={selected.cell.inputDetail as string} initialWrapped={codeWrappingOnOpen}
+                          initialWrapped={codeWrappingOnOpen}
                           stringWrapping={stringWrapping} onOpen={() => { activateTab('input') }} t={t} />
                         <ProgramOutput record={selected} stringWrapping={stringWrapping}
                           onOpen={() => { activateTab('output') }} t={t} />
@@ -3437,7 +3439,7 @@ export function TrajectoryTable({
               selectedProgram === undefined
                 ? <RecordPayload record={selected} direction="input" renderImages={renderImages} stringWrapping={stringWrapping} t={t} />
                 : <ProgramInput key={`input:${selectedRecordId}`} program={selectedProgram}
-                  rawInput={selected.cell.inputDetail as string} initialWrapped={codeWrappingOnOpen}
+                  initialWrapped={codeWrappingOnOpen}
                   stringWrapping={stringWrapping} t={t} />
             )}
             {!promptSelected && selected !== undefined && activeTab === 'output' && (

+ 6 - 1
packages/client/ui-trajectory/src/client/code-program.ts

@@ -1,8 +1,12 @@
 /** Read replayable PTC source and language hints from recorded tool arguments and schemas. */
 import type { TrajectoryCellProps } from './trajectory-record.ts'
 
+/** Recorded name of the programmatic tool-calling entry point. */
+export const PTC_TOOL_NAME = 'run_code'
+
 /** Validated source and original arguments for one recorded run_code call. */
 export interface CodeProgram {
+  rawInput: string
   source: string
   description: string
   arguments: Record<string, unknown>
@@ -42,11 +46,12 @@ function recordedLanguage(schemaRaw: string | undefined): CodeProgram['language'
  */
 export function codeProgram(cell: TrajectoryCellProps): CodeProgram | undefined {
   if (cell.kind !== 'tool' && cell.kind !== 'subtool') return undefined
-  if (cell.text.split(' · ', 1)[0] !== 'run_code') return undefined
+  if (cell.toolName !== PTC_TOOL_NAME || cell.inputDetail === undefined) return undefined
   const args = parseRecord(cell.inputDetail)
   if (typeof args?.code !== 'string') return undefined
   if (args.description !== undefined && typeof args.description !== 'string') return undefined
   return {
+    rawInput: cell.inputDetail,
     source: args.code,
     description: typeof args.description === 'string' && args.description.trim() !== ''
       ? args.description

+ 2 - 1
packages/client/ui-trajectory/src/client/layout.ts

@@ -1062,8 +1062,9 @@ function expandSubCalls(
 function summarizeCall(
   name: string,
   argsRaw: string,
-): Pick<TrajectoryCellProps, 'text' | 'previewMarkdown'> {
+): Pick<TrajectoryCellProps, 'text' | 'previewMarkdown' | 'toolName'> {
   return {
+    toolName: name,
     text: name,
     ...(argsRaw === '' ? {} : { previewMarkdown: argsRaw }),
   }

+ 2 - 0
packages/client/ui-trajectory/src/client/trajectory-record.ts

@@ -80,6 +80,8 @@ export interface TrajectoryCellProps extends HTMLAttributes<HTMLDivElement> {
   resultPreviewMarkdown?: string
   /** Tool call id used to link message source blocks to tool records. */
   callId?: string
+  /** Recorded tool name; independent of the display summary. */
+  toolName?: string
   /** Tool-only result failure state. */
   isError?: boolean
   /** Own duration in seconds, or `null` when no duration is known. */

+ 1 - 1
packages/client/ui-trajectory/tests/code-inspector.client.spec.tsx

@@ -15,7 +15,7 @@ afterEach(() => {
 const SOURCE = 'const value = await tools.bash({ command: "pwd" })\nreturn value\n'
 const ARGS = JSON.stringify({ code: SOURCE, description: 'Read the working directory' })
 const CELL: TrajectoryCellProps = {
-  index: 1, kind: 'tool', text: `run_code · ${ARGS}`, callId: 'code-1',
+  index: 1, kind: 'tool', toolName: 'run_code', text: `run_code · ${ARGS}`, callId: 'code-1',
   inputDetail: ARGS, outputDetail: 'printed line\n42', result: 'printed line', timeSeconds: 0.5,
 }
 

+ 12 - 2
packages/client/ui-trajectory/tests/code-program.client.spec.ts

@@ -4,13 +4,14 @@ import { codeProgram } from '../src/client/code-program.ts'
 import type { TrajectoryCellProps } from '../src/client/trajectory-record.ts'
 
 const CELL: TrajectoryCellProps = {
-  index: 1, kind: 'tool', text: 'run_code · {}', timeSeconds: 1,
+  index: 1, kind: 'tool', toolName: 'run_code', text: 'run_code · {}', timeSeconds: 1,
   inputDetail: JSON.stringify({ code: 'return 42\n', description: 'Compute the answer' }),
 }
 
 describe('recorded code programs', () => {
   it('keeps the program and arguments verbatim', () => {
     expect(codeProgram(CELL)).toEqual({
+      rawInput: CELL.inputDetail,
       source: 'return 42\n', description: 'Compute the answer',
       arguments: { code: 'return 42\n', description: 'Compute the answer' }, language: undefined,
     })
@@ -52,9 +53,18 @@ describe('recorded code programs', () => {
     },
   )
 
+  it('uses recorded tool metadata independently of display text', () => {
+    expect(codeProgram({ ...CELL, text: 'Execute program' })?.source).toBe('return 42\n')
+    const withoutName = { ...CELL }
+    delete withoutName.toolName
+    expect(codeProgram(withoutName)).toBeUndefined()
+    const rawInput = ' { "code": "return 42" }\n'
+    expect(codeProgram({ ...CELL, inputDetail: rawInput })?.rawInput).toBe(rawInput)
+  })
+
   it('does not specialize other tools or non-tool records', () => {
     expect(codeProgram({ ...CELL, kind: 'message' })).toBeUndefined()
-    expect(codeProgram({ ...CELL, text: 'run_code_extra · {}' })).toBeUndefined()
+    expect(codeProgram({ ...CELL, toolName: 'run_code_extra' })).toBeUndefined()
     expect(codeProgram({ ...CELL, kind: 'subtool' })?.source).toBe('return 42\n')
   })
 })