Przeglądaj źródła

fix(auto-review): preserve nested trajectory errors and optional peer

pku-xht 2 tygodni temu
rodzic
commit
ad93e2f602

+ 19 - 2
apps/web/tests/auto-review-denial.e2e.ts

@@ -1,7 +1,7 @@
 // Web e2e scenario: a cold recording renders the structured Auto-review
 // denial without replaying a reviewer or model call.
 // The real persistence reader, shipped Web composition, permission
-// projection, conversation assembler, and generic Tool row all participate.
+// projection, conversation assembler, Tool row, and Trajectory all participate.
 import { readFile } from 'node:fs/promises'
 import { fileURLToPath } from 'node:url'
 import type { Browser, Page } from 'playwright'
@@ -112,7 +112,24 @@ describe.skipIf(MODE === 'record')('web e2e: cold Auto-review denial', () => {
     await captureAutoReviewState(page, 'deny-expanded')
     const expanded = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd))
       .split(SEED_ID).join('{{seededId}}')
-    await compareOrRefreshGolden(UI_EXPECTED, `## Collapsed\n\n${collapsed.trim()}\n\n## Expanded\n\n${expanded.trim()}`, MODE)
+
+    await page.getByRole('tab', { name: 'Trajectory', exact: true }).click()
+    const ledger = page.locator('[data-trajectory-scroll]')
+    await ledger.locator('table[data-scroll-ready="true"]').waitFor({ timeout: 15_000 })
+    const nativeRecord = ledger.locator('tr[data-kind="tool"]').filter({ hasText: 'mystery' })
+    const innerRecord = ledger.locator('tr[data-kind="subtool"]').filter({ hasText: 'bash' })
+    await expect.poll(() => nativeRecord.textContent()).toContain('AUTO_REVIEW_DENIED')
+    await expect.poll(() => innerRecord.textContent()).toContain('AUTO_REVIEW_DENIED')
+    await innerRecord.click()
+    await page.getByRole('tab', { name: 'Result', exact: true }).click()
+    await page.getByRole('tabpanel', { name: 'Result' })
+      .getByText('AutoReviewDeniedError: AUTO_REVIEW_DENIED', { exact: true })
+      .waitFor({ state: 'visible' })
+    await captureAutoReviewState(page, 'deny-trajectory')
+    const trajectory = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd))
+      .split(SEED_ID).join('{{seededId}}')
+    await compareOrRefreshGolden(UI_EXPECTED,
+      `## Collapsed\n\n${collapsed.trim()}\n\n## Expanded\n\n${expanded.trim()}\n\n## Trajectory\n\n${trajectory.trim()}`, MODE)
     expect(tripwire.pageErrors).toEqual([])
     expect(tripwire.warnings).toEqual([])
   }, 60_000)

+ 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: 2b8ecff50b498be529ccf7e35e528f7554084940
-README.zh.md: 8463bd0a68c32753138425d968db229726b5bd1a
+README.md: bce403792cc64df6c895dfec1b37eac6ec8e3af6
+README.zh.md: 759c539cb8af7fb9310fc905c250fd0c33d9ec1f

+ 2 - 0
packages/client/ui-trajectory/README.md

@@ -45,6 +45,8 @@ A fixed Overview above the ledger projects real record start/duration timing fro
 
 The view is a pure projection: Trajectory-owned Definitions assemble business records from the shared Session window — including durable cancellation-finalized prefixes, chunk-only interruption fallbacks, and interrupted Tool records — so Trajectory neither reads nor changes the Chat conversation snapshot. Its steering classifier retains only next-step Inbox IDs through persistent splice state and shares each current claimed batch across later Contexts.
 
+Native and nested PTC Tool results retain their raw structured error details. Failed records show the error code in the ledger and the error name and code in the inspector.
+
 A complete appended prompt without a loaded request header appears as a standalone system row; only its known text is available, with no inferred request options or tool catalog. Prepending its request history replaces that standalone presentation without duplicating the prompt. In-history system prompt changes compare against the most recent request state, including earlier prompt updates without a new request header. Each request retains the prompt and change that applied at its own position. Surface replacements, including compaction, restore the last nonempty surviving system prompt even without a new system event; an unloaded prompt remains unavailable until its page arrives.
 
 ### Virtual rows

+ 2 - 0
packages/client/ui-trajectory/README.zh.md

@@ -45,6 +45,8 @@ Trajectory 标签页让你以按轮次组织的事件记录表和交互式时间
 
 视图是纯投影:Trajectory 自有的 Definition 从共享 Session 窗口组装业务记录——包括持久化的取消定稿前缀、只能从分片恢复的打断前缀与被打断的工具记录——因此 Trajectory 既不读取也不改变 Chat 会话快照。其 steering 分类器通过持久 splice state 只保留 next-step Inbox ID,并让后续 Context 共享当前 claimed batch。
 
+原生与嵌套 PTC 工具结果保留原始结构化错误详情。失败记录在记录表中显示错误代码,在检查器中显示错误名称和代码。
+
 完整的追加提示词在请求头未加载时显示为独立系统行;仅提供已知文本,不推断请求选项或工具目录。补入其请求历史后,该独立展示被替代而不重复提示词。历史中的系统提示词变更与最近的请求状态比较,包括没有新请求头的先前提示词更新。每个请求保留其所在位置生效的提示词与变更。包括压缩在内的 surface 替换会恢复最后一个非空的存活系统提示词,即使没有新的系统事件;未加载的提示词在对应分页到达前仍不可用。
 
 ### 虚拟行

+ 3 - 1
packages/client/ui-trajectory/src/client/trajectory-tool-definition.ts

@@ -24,6 +24,7 @@ interface DispatchData {
   readonly name: string
   readonly arguments: unknown
   readonly isError?: boolean
+  readonly error?: ToolResultNode['error']
   readonly content?: ToolResultNode['content']
 }
 
@@ -101,6 +102,7 @@ function childResult(
     callTime: previous === undefined || 'kind' in previous ? null : previous.time,
     content: data.content ?? [],
     isError: data.isError === true,
+    ...(data.error === undefined ? {} : { error: data.error }),
     subCalls: [],
   }
 }
@@ -259,7 +261,7 @@ const trajectoryToolDefinition: ConversationNodeDefinition<ToolState> = {
 /* jscpd:ignore-end */
 
 /**
- * Register the Trajectory Tool lifecycle.
+ * Register the Trajectory Tool lifecycle with raw native and PTC error details.
  *
  * @param ctx - Plugin context receiving the Definition.
  */

+ 8 - 2
packages/client/ui-trajectory/tests/conversation-definitions.client.spec.ts

@@ -402,6 +402,11 @@ describe('Trajectory conversation Definitions', () => {
   })
 
   it('keeps parallel roots, raw Tool facts, and mixed-ID PTC dispatch results', () => {
+    const error = {
+      name: 'AutoReviewDeniedError',
+      code: 'AUTO_REVIEW_DENIED',
+      reason: ' raw\r\nreason ',
+    }
     const current = snapshot(assembler([
       at(1, 'turn/start', { turn: 1 }),
       at(2, 'step/start', { turn: 1, step: 1 }),
@@ -433,7 +438,7 @@ describe('Trajectory conversation Definitions', () => {
       at(8, 'tool/ptc-dispatch', {
         rootCallId: 'root-a', parentCallId: 'root-b:code:1', subCallId: 'root-b:ptc:2',
         name: 'read', arguments: { file_path: 'nested.txt' },
-        isError: false, content: [{ type: 'text', text: 'nested contents' }],
+        isError: true, error, content: [{ type: 'text', text: 'not executed' }],
       }),
       at(9, 'tool/result', {
         turn: 1,
@@ -469,7 +474,8 @@ describe('Trajectory conversation Definitions', () => {
         kind: 'tool-result', callId: 'root-b:code:1', parentCallId: 'root-a', call: { name: 'read' },
         subCalls: [{
           kind: 'tool-result', callId: 'root-b:ptc:2', parentCallId: 'root-b:code:1',
-          callTime: 1_700_000_000_007, content: [{ type: 'text', text: 'nested contents' }], subCalls: [],
+          callTime: 1_700_000_000_007, content: [{ type: 'text', text: 'not executed' }],
+          isError: true, error, subCalls: [],
         }],
       }],
     })

+ 3 - 0
packages/subagent/subagent/package.json

@@ -85,6 +85,9 @@
     "@deepseek-ai/dsh-agent-presets": {
       "optional": true
     },
+    "@deepseek-ai/dsh-permission-presets": {
+      "optional": true
+    },
     "@deepseek-ai/dsh-sandbox": {
       "optional": true
     },

+ 71 - 0
snapshots/web/auto-review-denial/ui.expected.md

@@ -89,3 +89,74 @@
 - button "1 turns 2 steps":
   - img
   - text: 1 turns 2 steps
+
+## Trajectory
+
+- banner:
+  - navigation "Session hierarchy":
+    - button "{{workspace}}" [disabled]
+  - button "Session log":
+    - text: Session log
+    - img
+  - button "Open the sidebar":
+    - img
+  - tablist:
+    - tab "Chat"
+    - tab "Trajectory" [selected]
+- toolbar "Trajectory toolbar":
+  - button "Use actual duration": Duration
+  - button "Collapse turns": Turns
+  - button "Collapse calls": Calls
+  - img
+  - searchbox "Search trajectory"
+- region "Trajectory timeline"
+- table:
+  - rowgroup:
+    - row "USER, Inspect the protected operation, but do not run it unless authorized.":
+      - cell "Turn 1 USER": USER
+      - cell "Inspect the protected operation, but do not run it unless authorized."
+    - row "Request 1, ASSISTANT, (tool call only)":
+      - 'cell "Request #1 ASSISTANT"':
+        - 'button "Request #1"'
+        - text: ASSISTANT
+      - cell "(tool call only)"
+    - 'row "TOOL, mystery {\"secret\":\"hidden-input\"}"':
+      - cell "TOOL"
+      - 'cell "mystery{\"secret\":\"hidden-input\"} → AUTO_REVIEW_DENIED"'
+    - row "Request 2, ASSISTANT, (tool call only)":
+      - 'cell "Request #2 ASSISTANT"':
+        - 'button "Request #2"'
+        - text: ASSISTANT
+      - cell "(tool call only)"
+    - 'row "TOOL, run_code {\"code\":\"try { await tools.bash({command: \"echo hidden-input-ptc\"}) } catch (error) { return \"denial caught\" }\",\"description\":\"Catch an Auto-denied inner call\"}"':
+      - cell "TOOL"
+      - 'cell "run_code{\"code\":\"try { await tools.bash({command: \"echo hidden-input-ptc\"}) } catch (error) { return \"denial caught\" }\",\"description\":\"Catch an Auto-denied inner call\"} → denial caught"'
+    - 'row "SUBTOOL, bash {\"command\":\"echo hidden-input-ptc\"}" [selected]':
+      - cell "SUBTOOL"
+      - 'cell "bash{\"command\":\"echo hidden-input-ptc\"} → AUTO_REVIEW_DENIED"'
+- complementary "Event details":
+  - separator "Resize event details"
+  - text: SUBTOOL Turn 1 · Step 2
+  - button "Close details"
+  - tablist "Event details":
+    - tab "Summary"
+    - tab "Payload"
+    - tab "Result" [selected]
+    - tab "Schema"
+    - tab "Timing"
+  - tabpanel "Result": "AutoReviewDeniedError: AUTO_REVIEW_DENIED Error: Auto review rejected tool \"bash\"; its body was not executed"
+- textbox "Message or run a task, / commands, @ files or sessions"
+- button "Commands":
+  - img
+- button "Add attachment":
+  - img
+- 'button "Access mode, current: Auto review EXP"':
+  - text: Auto review
+  - superscript: EXP
+- button "Select model, current DeepSeek-V4-Flash":
+  - text: DeepSeek-V4-Flash
+  - img
+- button "Send message" [disabled]
+- button "1 turns 2 steps":
+  - img
+  - text: 1 turns 2 steps