mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
refactor(subagent): keep ACP permission diagnostics minimal
This commit is contained in:
+2
-2
@@ -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-08-21-out-of-process-subagent-minimal-diagnostics.md
|
||||
2026-08-21-out-of-process-subagent-minimal-diagnostics.md: 38cf32dc3de3fe157f73e1546a827df9b3622fa6
|
||||
2026-08-21-out-of-process-subagent-minimal-diagnostics.zh.md: 386f85e6b5665c8006e10a0ed0aa49845b6ffed0
|
||||
2026-08-21-out-of-process-subagent-minimal-diagnostics.md: fa795fc12aafe2d7d7707e97cf8c4ca49103e089
|
||||
2026-08-21-out-of-process-subagent-minimal-diagnostics.zh.md: 9516e924b7d8d13403cf3944b55efabfd2c80dff
|
||||
|
||||
+3
-3
@@ -16,7 +16,7 @@ Each out-of-process provider owns a small mapping from facts it already receives
|
||||
|
||||
### Safe failure text
|
||||
|
||||
The first line has this fixed field order:
|
||||
Generic error diagnostics have this fixed field order:
|
||||
|
||||
```text
|
||||
Subagent failure (provider: <provider>; stage: <stage>; category: <category>; stop reason: <reason>; exit code: <code>; signal: <signal>)
|
||||
@@ -24,7 +24,7 @@ Subagent failure (provider: <provider>; stage: <stage>; category: <category>; st
|
||||
|
||||
Unavailable optional fields are omitted. The complete result is limited to 4096 UTF-8 bytes by the shared settlement boundary. Successful results and local cancellation carry no failure diagnostic. Partial assistant output remains in `SubagentResult.output` and is presented separately.
|
||||
|
||||
When an ACP permission request contributes to a non-completed result, a second fixed line records `policy`, the closed ACP tool `request` kind, and `decision`. Tool titles, raw input, locations, option names, and metadata are excluded. A diagnostic-bearing remote `aborted` result keeps its public stop reason; the one-shot Job adapter treats it as failed, while diagnostic-free local cancellation remains killed.
|
||||
When an ACP permission request contributes to a non-completed result, a fixed line records `policy`, the closed ACP tool `request` kind, and `decision`. Tool titles, raw input, locations, option names, and metadata are excluded. For `max-tokens`, `refusal`, or remote `aborted`, the public stop reason already carries the terminal fact, so the permission line is the complete diagnostic; generic error paths append it after the failure line. A diagnostic-bearing remote `aborted` result keeps its public stop reason; the one-shot Job adapter treats it as failed, while diagnostic-free local cancellation remains killed.
|
||||
|
||||
### ACP facts
|
||||
|
||||
@@ -32,7 +32,7 @@ When an ACP permission request contributes to a non-completed result, a second f
|
||||
| --- | --- | --- |
|
||||
| `initialize` | Parent workspace resolution, spawn, and ACP initialize | `configuration`, `transport`, `process-start`, or `process-exit` |
|
||||
| `new-session` | ACP `session/new` and returned session-id validation | `protocol`, `transport`, or `process-exit` |
|
||||
| `prompt` | ACP prompt request, remote stop reason, and permission callback | `remote-limit`, `remote-refusal`, `permission`, `transport`, or `unknown` |
|
||||
| `prompt` | ACP prompt request, remote stop reason, and permission callback | `remote-limit`, `transport`, `unknown`, or a permission-only diagnostic |
|
||||
| `process` | Managed child exits before a prompt terminal response | `process-exit` plus independently observed exit code and signal |
|
||||
| `teardown` | EOF quiescence and managed process-tree termination | Fixed teardown facts; the original cleanup failure remains internal |
|
||||
|
||||
|
||||
+3
-3
@@ -16,7 +16,7 @@ ACP 子进程可能因为达到远端限制、拒绝必需权限、失去协议
|
||||
|
||||
### 安全失败文本
|
||||
|
||||
首行采用以下固定字段顺序:
|
||||
通用 error 诊断采用以下固定字段顺序:
|
||||
|
||||
```text
|
||||
Subagent failure (provider: <provider>; stage: <stage>; category: <category>; stop reason: <reason>; exit code: <code>; signal: <signal>)
|
||||
@@ -24,7 +24,7 @@ Subagent failure (provider: <provider>; stage: <stage>; category: <category>; st
|
||||
|
||||
不可用的可选字段会被省略。共享结算边界会把完整结果限制在 4096 个 UTF-8 字节以内。成功结果和本地取消不携带失败诊断。部分 assistant 输出继续保留在 `SubagentResult.output` 中,并与诊断分开呈现。
|
||||
|
||||
当 ACP 权限请求参与非完成结果时,第二个固定行会记录 `policy`、ACP 闭集工具 `request` 种类和 `decision`。工具标题、raw input、位置、选项名称与 metadata 均被排除。带诊断的远端 `aborted` 结果仍保持公共结束原因;一次性 Job adapter 会把它判为 failed,而不带诊断的本地取消仍是 killed。
|
||||
当 ACP 权限请求参与非完成结果时,一个固定行会记录 `policy`、ACP 闭集工具 `request` 种类和 `decision`。工具标题、raw input、位置、选项名称与 metadata 均被排除。对于 `max-tokens`、`refusal` 或远端 `aborted`,公共结束原因已经携带终态事实,因此权限行就是完整诊断;通用 error 路径则把它附在失败行之后。带诊断的远端 `aborted` 结果仍保持公共结束原因;一次性 Job adapter 会把它判为 failed,而不带诊断的本地取消仍是 killed。
|
||||
|
||||
### ACP 事实
|
||||
|
||||
@@ -32,7 +32,7 @@ Subagent failure (provider: <provider>; stage: <stage>; category: <category>; st
|
||||
| --- | --- | --- |
|
||||
| `initialize` | 父工作区解析、spawn 与 ACP initialize | `configuration`、`transport`、`process-start` 或 `process-exit` |
|
||||
| `new-session` | ACP `session/new` 与返回 session id 校验 | `protocol`、`transport` 或 `process-exit` |
|
||||
| `prompt` | ACP prompt 请求、远端结束原因与权限回调 | `remote-limit`、`remote-refusal`、`permission`、`transport` 或 `unknown` |
|
||||
| `prompt` | ACP prompt 请求、远端结束原因与权限回调 | `remote-limit`、`transport`、`unknown` 或仅权限诊断 |
|
||||
| `process` | 受管子进程先于 prompt 终态响应退出 | `process-exit`,以及分别观测到的退出码与信号 |
|
||||
| `teardown` | EOF 停稳与受管进程树终止 | 固定 teardown 事实;原始清理失败仍留在内部 |
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
{"type":"assistant/chunk","seq":13,"time":1787254574889,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":14,"time":1787254574889,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_acp_foreground","name":"subagent_acp","arguments":"{\"description\":\"Observe ACP foreground failure\",\"prompt\":\"Return the scripted ACP failure.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-pro"},"id":"ef8e9ff0-886c-4d55-bbdb-8e878258fb53"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":15,"time":1787254574890,"data":{"turn":1,"step":1,"callId":"call_acp_foreground","name":"subagent_acp","arguments":"{\"description\":\"Observe ACP foreground failure\",\"prompt\":\"Return the scripted ACP failure.\",\"run_in_background\":false}"}}
|
||||
{"type":"tool/result","seq":16,"time":1787254574996,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_acp_foreground"},"content":[{"type":"tool-result","toolCallId":"call_acp_foreground","content":[{"type":"text","text":"Error: subagent run was cancelled\nDiagnostic: Subagent failure (provider: ACP; stage: prompt; category: permission; stop reason: cancelled)\nACP unattended decision (policy: reject; request: execute; decision: denied)"}],"isError":true}],"role":"user","id":"720dc6b6-6788-4f6f-8426-b888ccafc84a"}},"sourceEventSeqs":[15],"surfaceOp":"append"}
|
||||
{"type":"tool/result","seq":16,"time":1787254574996,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_acp_foreground"},"content":[{"type":"tool-result","toolCallId":"call_acp_foreground","content":[{"type":"text","text":"Error: subagent run was cancelled\nDiagnostic: ACP unattended decision (policy: reject; request: execute; decision: denied)"}],"isError":true}],"role":"user","id":"b1bef631-eac4-4139-84ab-809d61493b2c"}},"sourceEventSeqs":[15],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":17,"time":1787254574996,"data":{"turn":1,"step":1}}
|
||||
{"type":"step/start","seq":18,"time":1787254575002,"data":{"turn":1,"step":2}}
|
||||
{"type":"assistant/chunk","seq":19,"time":1787254575006,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
|
||||
@@ -35,7 +35,7 @@
|
||||
{"type":"assistant/chunk","seq":33,"time":1787254575021,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":34,"time":1787254575021,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_acp_output","name":"job_output","arguments":"{\"job_id\":\"subagent-1\",\"wait\":true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-pro"},"id":"b20d64f1-7fcf-498d-84ec-afe518983863"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[29,30,31,32,33],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":35,"time":1787254575021,"data":{"turn":1,"step":3,"callId":"call_acp_output","name":"job_output","arguments":"{\"job_id\":\"subagent-1\",\"wait\":true}"}}
|
||||
{"type":"tool/result","seq":36,"time":1787254575110,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"call_acp_output"},"content":[{"type":"tool-result","toolCallId":"call_acp_output","content":[{"type":"text","text":"(no new output)\n[status: failed, aborted; diagnostic: Subagent failure (provider: ACP; stage: prompt; category: permission; stop reason: cancelled)\nACP unattended decision (policy: reject; request: execute; decision: denied)]"}],"isError":false}],"role":"user","id":"372a7fa1-e148-46ba-bc6a-33a3e0963276"}},"sourceEventSeqs":[35],"surfaceOp":"append"}
|
||||
{"type":"tool/result","seq":36,"time":1787254575110,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"call_acp_output"},"content":[{"type":"tool-result","toolCallId":"call_acp_output","content":[{"type":"text","text":"(no new output)\n[status: failed, aborted; diagnostic: ACP unattended decision (policy: reject; request: execute; decision: denied)]"}],"isError":false}],"role":"user","id":"53294746-f70e-4494-a005-a97df6199d83"}},"sourceEventSeqs":[35],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":37,"time":1787254575110,"data":{"turn":1,"step":3}}
|
||||
{"type":"step/start","seq":38,"time":1787254575116,"data":{"turn":1,"step":4}}
|
||||
{"type":"assistant/chunk","seq":39,"time":1787254575121,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
|
||||
|
||||
@@ -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/subagent/subagent-acp/README.md
|
||||
README.md: e01785a7a8cd5406fa545cc5e97b0a09d4f57fa1
|
||||
README.zh.md: 9082ab4d57b4393a07dc2e02cf6ce95ca259ae8d
|
||||
README.md: 6366a224b8f56f0b86466d6946f75ab45fee3997
|
||||
README.zh.md: 490c17dd883224bcfe129bd3b29eea973a4e69c7
|
||||
|
||||
@@ -58,15 +58,15 @@ ACP advertises no start-time capabilities because this process cannot enforce th
|
||||
|
||||
## Failure diagnostics
|
||||
|
||||
The first line has a fixed field order:
|
||||
Failure diagnostics for generic error paths have a fixed field order:
|
||||
|
||||
```text
|
||||
Subagent failure (provider: ACP; stage: <stage>; category: <category>; stop reason: <reason>; exit code: <code>; signal: <signal>)
|
||||
```
|
||||
|
||||
Unavailable optional fields are omitted. The provider derives `initialize`, `new-session`, `prompt`, `process`, or `teardown` at the operation that owns the failure. Categories distinguish configuration, protocol or transport failure, process start/exit, remote limits or refusal, permission-related cancellation, and the fixed unknown fallback. Exit code and signal come only from the managed subprocess outcome; stderr, exception messages, task text, tool input, paths, environment values, credentials, and protocol payloads never enter the diagnostic. The shared result boundary limits the complete text to 4096 UTF-8 bytes.
|
||||
Unavailable optional fields are omitted. The provider derives `initialize`, `new-session`, `prompt`, `process`, or `teardown` at the operation that owns the failure. Categories distinguish configuration, protocol or transport failure, process start/exit, remote limits, and the fixed unknown fallback. Exit code and signal come only from the managed subprocess outcome; stderr, exception messages, task text, tool input, paths, environment values, credentials, and protocol payloads never enter the diagnostic. The shared result boundary limits the complete text to 4096 UTF-8 bytes.
|
||||
|
||||
When a run requested permission and did not complete, a second fixed line records the configured policy, the ACP closed tool kind, and whether the provider allowed or denied it. Tool titles, raw input, locations, and option text are excluded. Successful results and local cancellation omit both lines. A permission-diagnosed remote `aborted` result remains `aborted`; foreground presentation includes its diagnostic, while the one-shot Job adapter classifies that diagnostic-bearing remote abort as failed instead of conflating it with local cancellation.
|
||||
When a run requested permission and did not complete, a fixed permission line records the configured policy, the ACP closed tool kind, and whether the provider allowed or denied it. Tool titles, raw input, locations, and option text are excluded. For `max-tokens`, `refusal`, or remote `aborted`, this is the complete diagnostic because the public stop reason already carries the terminal fact; generic error paths put it after the failure line. Successful results and local cancellation omit it. A permission-diagnosed remote `aborted` result remains `aborted`; foreground presentation includes its diagnostic, while the one-shot Job adapter classifies that diagnostic-bearing remote abort as failed instead of conflating it with local cancellation.
|
||||
|
||||
## Process boundary
|
||||
|
||||
|
||||
@@ -58,15 +58,15 @@ ACP 不声明任何启动时能力,因为当前进程无法强制执行远程
|
||||
|
||||
## 失败诊断
|
||||
|
||||
首行采用固定字段顺序:
|
||||
通用 error 路径的失败诊断采用固定字段顺序:
|
||||
|
||||
```text
|
||||
Subagent failure (provider: ACP; stage: <stage>; category: <category>; stop reason: <reason>; exit code: <code>; signal: <signal>)
|
||||
```
|
||||
|
||||
不可用的可选字段会被省略。提供方从实际拥有失败的操作派生 `initialize`、`new-session`、`prompt`、`process` 或 `teardown`。category 区分配置、协议或传输失败、进程启动/退出、远端限制或拒绝、权限相关取消以及固定 unknown 回退。退出码与信号只来自受管子进程结果;stderr、异常消息、任务文本、工具输入、路径、环境值、凭证和协议 payload 绝不会进入诊断。共享结果边界会把完整文本限制在 4096 个 UTF-8 字节以内。
|
||||
不可用的可选字段会被省略。提供方从实际拥有失败的操作派生 `initialize`、`new-session`、`prompt`、`process` 或 `teardown`。category 区分配置、协议或传输失败、进程启动/退出、远端限制以及固定 unknown 回退。退出码与信号只来自受管子进程结果;stderr、异常消息、任务文本、工具输入、路径、环境值、凭证和协议 payload 绝不会进入诊断。共享结果边界会把完整文本限制在 4096 个 UTF-8 字节以内。
|
||||
|
||||
当运行请求过权限且最终未完成时,第二个固定行会记录已配置策略、ACP 闭集工具种类以及提供方允许还是拒绝。工具标题、raw input、位置与选项文本均被排除。成功结果和本地取消会省略两行。带权限诊断的远端 `aborted` 结果仍保持 `aborted`;前台会呈现该诊断,而一次性 Job adapter 会把这种带诊断的远端取消判为 failed,避免与本地取消混淆。
|
||||
当运行请求过权限且最终未完成时,一个固定权限行会记录已配置策略、ACP 闭集工具种类以及提供方允许还是拒绝。工具标题、raw input、位置与选项文本均被排除。对于 `max-tokens`、`refusal` 或远端 `aborted`,公共结束原因已经携带终态事实,因此该权限行就是完整诊断;通用 error 路径则把它放在失败行之后。成功结果和本地取消会省略权限行。带权限诊断的远端 `aborted` 结果仍保持 `aborted`;前台会呈现该诊断,而一次性 Job adapter 会把这种带诊断的远端取消判为 failed,避免与本地取消混淆。
|
||||
|
||||
## 进程边界
|
||||
|
||||
|
||||
@@ -95,8 +95,6 @@ type AcpFailureCategory =
|
||||
| 'process-start'
|
||||
| 'process-exit'
|
||||
| 'remote-limit'
|
||||
| 'remote-refusal'
|
||||
| 'permission'
|
||||
| 'unknown'
|
||||
|
||||
interface AcpFailureFacts {
|
||||
@@ -311,17 +309,11 @@ function terminalFailure(
|
||||
stopReason: 'max_turn_requests',
|
||||
}, permission)
|
||||
case 'max_tokens':
|
||||
return permission === undefined
|
||||
? undefined
|
||||
: diagnosticText({ stage: 'prompt', category: 'remote-limit', stopReason: reason }, permission)
|
||||
case 'refusal':
|
||||
return permission === undefined
|
||||
? undefined
|
||||
: diagnosticText({ stage: 'prompt', category: 'remote-refusal', stopReason: reason }, permission)
|
||||
case 'cancelled':
|
||||
return permission === undefined
|
||||
? undefined
|
||||
: diagnosticText({ stage: 'prompt', category: 'permission', stopReason: reason }, permission)
|
||||
: permissionDiagnostic(permission)
|
||||
default:
|
||||
return diagnosticText({ stage: 'prompt', category: 'unknown', stopReason: 'unknown' }, permission)
|
||||
}
|
||||
|
||||
@@ -455,9 +455,9 @@ describe('dsh-subagent-acp', () => {
|
||||
})
|
||||
|
||||
it.each([
|
||||
['max_tokens', 'max-tokens', 'remote-limit'],
|
||||
['refusal', 'refusal', 'remote-refusal'],
|
||||
] as const)('adds a permission fact to %s without changing its stop reason', async (remote, stopReason, category) => {
|
||||
['max_tokens', 'max-tokens'],
|
||||
['refusal', 'refusal'],
|
||||
] as const)('adds a permission fact to %s without changing its stop reason', async (remote, stopReason) => {
|
||||
const ctx = await setup({
|
||||
MOCK_PERMISSION: '1',
|
||||
MOCK_PERMISSION_IGNORE_DECISION: '1',
|
||||
@@ -467,10 +467,7 @@ describe('dsh-subagent-acp', () => {
|
||||
const run = await ctx.subagents.start('acp', request())
|
||||
const result = await run.result
|
||||
expect(result.stopReason).toBe(stopReason)
|
||||
expect(result.diagnostic).toBe(
|
||||
`${expectedFailure(`stage: prompt; category: ${category}; stop reason: ${remote}`)}\n`
|
||||
+ expectedPermission('reject', 'read', 'denied'),
|
||||
)
|
||||
expect(result.diagnostic).toBe(expectedPermission('reject', 'read', 'denied'))
|
||||
await run.dispose()
|
||||
})
|
||||
|
||||
@@ -830,10 +827,7 @@ describe('dsh-subagent-acp', () => {
|
||||
const result = await run.result
|
||||
// The child asked permission, the backend rejected, the child returned cancelled.
|
||||
expect(result.stopReason).toBe('aborted')
|
||||
expect(result.diagnostic).toBe(
|
||||
`${expectedFailure('stage: prompt; category: permission; stop reason: cancelled')}\n`
|
||||
+ expectedPermission('reject', 'execute', 'denied'),
|
||||
)
|
||||
expect(result.diagnostic).toBe(expectedPermission('reject', 'execute', 'denied'))
|
||||
await run.dispose()
|
||||
})
|
||||
|
||||
@@ -854,10 +848,7 @@ describe('dsh-subagent-acp', () => {
|
||||
const run = await ctx.subagents.start('acp', request())
|
||||
const result = await run.result
|
||||
expect(result.stopReason).toBe('aborted')
|
||||
expect(result.diagnostic).toBe(
|
||||
`${expectedFailure('stage: prompt; category: permission; stop reason: cancelled')}\n`
|
||||
+ expectedPermission('allow', 'unknown', 'denied'),
|
||||
)
|
||||
expect(result.diagnostic).toBe(expectedPermission('allow', 'unknown', 'denied'))
|
||||
await run.dispose()
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user