Merge pull request #6272 from deepseek-harness/fix/codex-partial-answer-phase

fix(subagent-codex): accept the partial_answer agent message phase
This commit is contained in:
Tianyi Cui authored and GitHub committed 2026-10-15 20:13:11 +08:00
commit 7f338ce3be
5 files changed
+87 -30

No files matched your search

@@ -9,8 +9,8 @@
en: c3f2c833cb91228d
zh: 3267d24936c5973f
/deepseek-ai-dsh-subagent-codex/summary:
en: 6eade95bb21eae52
zh: 72de7d6fcdc0c31a
en: 895eb3910428c51e
zh: c5eb0d32c789520d
/deepseek-ai-dsh-subagent-codex/table-of-contents:
en: d152484eb41ac6b4
zh: 09388d293f9be9cb
@@ -27,8 +27,8 @@
en: a36399c7ac98724d
zh: d43815e08689da6b
/deepseek-ai-dsh-subagent-codex/use-this-package/what-you-get:
en: 4b271f17f16fb162
zh: 154c909023f1366b
en: abd75016d4c7f61e
zh: 904158c896bd21ce
/deepseek-ai-dsh-subagent-codex/use-this-package/failure-and-recovery:
en: 4ffe0312c5a8f722
zh: 532a1997e9ae825b
@@ -42,8 +42,8 @@
en: 36647129f061610f
zh: 42947c700d1d9202
/deepseek-ai-dsh-subagent-codex/understand-the-implementation/run-flow:
en: 19b10ff145644564
zh: c015d530f1bd227e
en: b8244d26ba25f468
zh: 6cb77dfece19cce7
/deepseek-ai-dsh-subagent-codex/further-exploration:
en: e2e35a40060c1ba8
zh: c1676579ebac7c7f
@@ -66,17 +66,17 @@
en: 4c391bb16e3cd380
zh: ab3847efe35fa6dc
/deepseek-ai-dsh-subagent-codex/model-experience/parent-scheduling-and-results-indirectly/what-the-model-sees:
en: 18dcb74d6a52e96a
zh: 068c57f9fd959ecb
en: 9ca324fb0b034a6c
zh: 83ff78167bd31a14
/deepseek-ai-dsh-subagent-codex/model-experience/parent-scheduling-and-results-indirectly/token-effect:
en: 0c436fb0897a6eca
zh: 7ec0a953d0232af2
en: f618e05fde7c8b57
zh: 7306c4535b6fa4c0
/deepseek-ai-dsh-subagent-codex/model-experience/parent-scheduling-and-results-indirectly/kv-cache-effect:
en: 19f1316ea4bb1bb9
zh: 181231ddffdc8494
/deepseek-ai-dsh-subagent-codex/known-limitations-and-deferred-work:
en: 46cbb0a3f4f81a5a
zh: a27faaaefe61158f
en: e55a466c946cbce4
zh: 96dfb5c8f8210927
/deepseek-ai-dsh-subagent-codex/known-limitations-and-deferred-work/dev-note:
en: cacc98025ed8913e
zh: e7673a3087ec0c62
+6 -6
View File
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
## Summary
Install `@deepseek-ai/dsh-subagent-codex` into a Profile when delegated work should run in a genuine, unattended Codex session in the parent Session's workspace. Each delegation uses a fresh isolated Codex thread for one self-contained text task and returns only its final answer or a safe failure diagnostic. Native Codex configuration and authentication remain authoritative, while `permissionMode` selects the non-interactive approval and sandbox behavior. The Bundle supplies a compatible native Codex payload and adds its delegation tool as a global tool.
Install `@deepseek-ai/dsh-subagent-codex` into a Profile when delegated work should run in a genuine, unattended Codex session in the parent Session's workspace. Each delegation uses a fresh isolated Codex thread for one self-contained text task and returns only the selected answer or a safe failure diagnostic. Native Codex configuration and authentication remain authoritative, while `permissionMode` selects the non-interactive approval and sandbox behavior. The Bundle supplies a compatible native Codex payload and adds its delegation tool as a global tool.
## Table of Contents
@@ -74,7 +74,7 @@ The tool returns a child id after accepting the task and sends its result to the
### What you get
The completion notice contains the final Codex answer, or the stop reason and optional safe diagnostic. The parent Session also retains the external task identity and complete terminal result independently of the notice; no local child Session is created. Product reasoning, tool activity, raw stderr, and workspace diffs do not enter the parent Session.
The completion notice contains the selected Codex answer, or the stop reason and optional safe diagnostic. The parent Session also retains the external task identity and complete terminal result independently of the notice; no local child Session is created. Product reasoning, tool activity, raw stderr, and workspace diffs do not enter the parent Session.
### Failure and recovery
@@ -107,7 +107,7 @@ This section explains how the provider drives a real Codex app-server and where
### Run flow
A start accepts only a non-empty sequence of text blocks and uses the child cwd selected by the subagent service. It spawns the fixed command through the subprocess seam, performs the `initialize` → `initialized` handshake, maps the Profile-selected mode and optional model into official `thread/start` fields beside `{ cwd, ephemeral: true }`, and publishes the run only after Codex returns a valid ephemeral thread. The published result starts exactly one turn, accepts only notifications for that run's thread and turn, and waits for the authoritative `turn/completed` terminal. The latest `agentMessage` with `phase: "final_answer"` wins; when Codex emits no explicit final phase, the latest message with `phase: null` is the compatibility fallback. A successful turn with no nonblank answer settles as an error. Failed turns use the coarse categories `limit`, `access-policy`, `service`, `transport`, `product-error`, `invalid-result`, or `unknown`; an early app-server exit uses `process`, and applicable connection and stream failures retain a numeric `httpStatusCode`.
A start accepts only a non-empty sequence of text blocks and uses the child cwd selected by the subagent service. It spawns the fixed command through the subprocess seam, performs the `initialize` → `initialized` handshake, maps the Profile-selected mode and optional model into official `thread/start` fields beside `{ cwd, ephemeral: true }`, and publishes the run only after Codex returns a valid ephemeral thread. The published result starts exactly one turn, accepts only notifications for that run's thread and turn, and waits for the authoritative `turn/completed` terminal. The latest `agentMessage` with `phase: "final_answer"` wins; without one, the latest `phase: "partial_answer"` message (stable answer text that later assistant output or tool calls may follow) is selected; when Codex emits neither explicit answer phase, the latest message with `phase: null` is the compatibility fallback. `commentary` messages are never selected, and any other phase fails the run. A successful turn with no nonblank answer settles as an error. Failed turns use the coarse categories `limit`, `access-policy`, `service`, `transport`, `product-error`, `invalid-result`, or `unknown`; an early app-server exit uses `process`, and applicable connection and stream failures retain a numeric `httpStatusCode`.
</details>
@@ -147,11 +147,11 @@ Independent of the parent request cache. Reuse depends only on Codex's own provi
#### What the model sees
Through `dsh-tool-subagent`, the parent model first receives a child id, then the final Codex answer or a failure notice with its stop reason and safe diagnostic. Diagnostics contain only fixed stage, category, and observed protocol or process facts. Product reasoning, intermediate messages, tool activity, stderr, usage, product identifiers, commands, paths, and raw protocol payloads are not copied into the parent Session.
Through `dsh-tool-subagent`, the parent model first receives a child id, then the selected Codex answer (final, partial, or unphased, in that order of precedence) or a failure notice with its stop reason and safe diagnostic. Diagnostics contain only fixed stage, category, and observed protocol or process facts. Product reasoning, intermediate messages, tool activity, stderr, usage, product identifiers, commands, paths, and raw protocol payloads are not copied into the parent Session.
#### Token effect
Parent input grows by the start acknowledgement and completion notice, including the final answer or failure detail. Child tokens do not enter the parent context. This provider adds no parent tool schema by itself.
Parent input grows by the start acknowledgement and completion notice, including the selected answer or failure detail. Child tokens do not enter the parent context. This provider adds no parent tool schema by itself.
#### KV Cache effect
@@ -170,7 +170,7 @@ These limits define when this provider is a poor fit or needs special operationa
- **The native platform payload is required at delegation time** — installs that omit optional dependencies, unsupported platforms, and missing or damaged payloads fail at the first run; there is no host-CLI fallback.
- **Compatibility is pinned by development evidence** — upgrading from the verified 0.153.4 protocol baseline requires regenerating upstream schema evidence and rerunning handshake, answer-selection, approval, cancellation, keyless real-product, and credentialed DeepSeek nonce tests.
- **No human approval path** — known unattended approval requests are denied and unknown server requests fail closed; the three Profile modes never create a DSH interaction channel or per-call allow policy.
- **Assistant payload is final text only** — failed runs can also expose a separate safe diagnostic; reasoning, intermediate messages, tool traffic, usage, stderr, and workspace diffs remain outside the parent Session. Task identity and terminal results are retained in the parent log.
- **Assistant payload is the selected answer text only** — failed runs can also expose a separate safe diagnostic; reasoning, intermediate messages, tool traffic, usage, stderr, and workspace diffs remain outside the parent Session. Task identity and terminal results are retained in the parent log.
- **No optional shared capabilities** — `agentPreset`, `agentOptions`, output schemas, and harness depth enforcement are rejected by the shared service for this provider.
- **No wall-clock timeout or side-effect rollback** — the caller cancels long work, and files or external systems changed before cancellation are not restored.
@@ -9,7 +9,7 @@ kind: "package-bundle"
## 概述
当委派工作需要在父会话工作区中的真实无人值守 Codex 会话内运行时,把 `@deepseek-ai/dsh-subagent-codex` 安装进 Profile。每次委派都会为一个自包含文本任务使用全新且隔离的 Codex 线程,并且只返回其最终答案或安全失败诊断。原生 Codex 配置和身份验证继续作为权威来源,而 `permissionMode` 选择非交互式审批和沙箱行为。Bundle 提供兼容的原生 Codex 载荷,并以全局工具形式添加委派工具。
当委派工作需要在父会话工作区中的真实无人值守 Codex 会话内运行时,把 `@deepseek-ai/dsh-subagent-codex` 安装进 Profile。每次委派都会为一个自包含文本任务使用全新且隔离的 Codex 线程,并且只返回所选答案或安全失败诊断。原生 Codex 配置和身份验证继续作为权威来源,而 `permissionMode` 选择非交互式审批和沙箱行为。Bundle 提供兼容的原生 Codex 载荷,并以全局工具形式添加委派工具。
## 目录
@@ -74,7 +74,7 @@ Bundle 将 `tool-subagent-codex` 作为 Host 条目插入,因此 `subagent_cod
### 你会得到什么
完成通知包含 Codex 的最终答案,或停止原因与可选的安全诊断。父会话还独立保存外部任务的身份和完整终态结果;无需创建本地子 Session。产品推理、工具活动、原始 stderr 与工作区差异不会进入父会话。
完成通知包含所选的 Codex 答案,或停止原因与可选的安全诊断。父会话还独立保存外部任务的身份和完整终态结果;无需创建本地子 Session。产品推理、工具活动、原始 stderr 与工作区差异不会进入父会话。
### 失败与恢复
@@ -107,7 +107,7 @@ Bundle 将 `tool-subagent-codex` 作为 Host 条目插入,因此 `subagent_cod
### 运行流程
一次启动只接受非空的文本块序列,并使用子代理服务选择的子级 cwd。它经子进程 seam spawn 固定命令,完成 `initialize` → `initialized` 握手,把 Profile 选择的模式与可选模型映射为官方 `thread/start` 字段并与 `{ cwd, ephemeral: true }` 一起发送,且仅在 Codex 返回有效的临时线程后发布运行。已发布的结果恰好启动一个轮次,只接受与此次运行的线程和轮次匹配的通知,并等待权威的 `turn/completed` 终态。以最后一条 `phase: "final_answer"` 的 `agentMessage` 为准;若 Codex 没有发出明确的最终阶段,则以最后一条 `phase: null` 的消息作为兼容性回退。成功完成的轮次若没有非空白答案,结果也会判为错误。失败轮次使用粗粒度类别 `limit`、`access-policy`、`service`、`transport`、`product-error`、`invalid-result` 或 `unknown`;app-server 提前退出使用 `process`,适用的连接与 stream 失败保留数值 `httpStatusCode`。
一次启动只接受非空的文本块序列,并使用子代理服务选择的子级 cwd。它经子进程 seam spawn 固定命令,完成 `initialize` → `initialized` 握手,把 Profile 选择的模式与可选模型映射为官方 `thread/start` 字段并与 `{ cwd, ephemeral: true }` 一起发送,且仅在 Codex 返回有效的临时线程后发布运行。已发布的结果恰好启动一个轮次,只接受与此次运行的线程和轮次匹配的通知,并等待权威的 `turn/completed` 终态。以最后一条 `phase: "final_answer"` 的 `agentMessage` 为准;若没有,则选最后一条 `phase: "partial_answer"` 的消息(稳定的回答文本,其后仍可能有助手输出或工具调用);若 Codex 两种明确的回答阶段都没有发出,则以最后一条 `phase: null` 的消息作为兼容性回退。`commentary` 消息绝不会被选中,其它任何阶段都会使运行失败。成功完成的轮次若没有非空白答案,结果也会判为错误。失败轮次使用粗粒度类别 `limit`、`access-policy`、`service`、`transport`、`product-error`、`invalid-result` 或 `unknown`;app-server 提前退出使用 `process`,适用的连接与 stream 失败保留数值 `httpStatusCode`。
</details>
@@ -147,11 +147,11 @@ Codex 子级会在一个全新的临时线程中,以单个轮次接收这些
#### 模型看到什么
通过 `dsh-tool-subagent`,父模型先收到 child id,随后收到 Codex 最终答案或带停止原因与安全诊断的失败通知。诊断只包含固定的阶段、类别和已观测的协议或进程事实。产品推理、中间消息、工具活动、stderr、用量、产品标识符、命令、路径与原始协议载荷不会复制到父 Session。
通过 `dsh-tool-subagent`,父模型先收到 child id,随后收到所选的 Codex 答案(按 final、partial、未标阶段的优先次序)或带停止原因与安全诊断的失败通知。诊断只包含固定的阶段、类别和已观测的协议或进程事实。产品推理、中间消息、工具活动、stderr、用量、产品标识符、命令、路径与原始协议载荷不会复制到父 Session。
#### Token 影响
父级输入会增加启动确认与完成通知,包括最终答案或失败信息。子任务 token 不进入父级上下文。本提供方自身不添加父级工具 schema。
父级输入会增加启动确认与完成通知,包括所选答案或失败信息。子任务 token 不进入父级上下文。本提供方自身不添加父级工具 schema。
#### KV Cache 影响
@@ -170,7 +170,7 @@ Codex 子级会在一个全新的临时线程中,以单个轮次接收这些
- **委派时必须存在原生平台载荷**——省略 optional dependencies 的安装、不受支持的平台以及缺失或损坏的载荷都会在第一次运行时失败;不会回退到宿主 CLI。
- **兼容性由开发证据锁定**——若要从已验证的 0.153.4 协议基线升级,必须重新生成上游 schema 证据,并重新运行握手、答案选择、审批、取消、无密钥真实产品以及带密钥的 DeepSeek 随机数测试。
- **没有人工审批路径**——已知的无人值守审批请求会被拒绝,未知服务器请求会以默认拒绝方式使运行失败;三种 Profile 模式都不会创建 DSH 交互通道或逐次调用 allow 策略。
- **assistant 载荷仅包含最终文本**——失败运行还可公开独立的安全诊断;推理、中间消息、工具通信、用量、stderr 与工作区差异不会进入父 Session。任务身份与终态结果保存在父日志中。
- **assistant 载荷仅包含所选回答文本**——失败运行还可公开独立的安全诊断;推理、中间消息、工具通信、用量、stderr 与工作区差异不会进入父 Session。任务身份与终态结果保存在父日志中。
- **没有可选的共享能力**——对于本提供方,共享服务会拒绝 `agentPreset`、`agentOptions`、输出 schema 和 harness 深度强制约束。
- **没有按实际经过时间触发的超时或副作用回滚**——长时间运行的工作由调用方取消,且取消前已更改的文件或外部系统不会恢复原状。
+12 -4
View File
@@ -205,6 +205,7 @@ export class CodexAppServerWire {
readonly order: number
}> = []
private lastFinalAnswer: string | undefined
private lastPartialAnswer: string | undefined
private lastUnphasedAnswer: string | undefined
private diagnostic: string | undefined
private failure: CodexWireFailureFacts | undefined
@@ -368,7 +369,7 @@ export class CodexAppServerWire {
const output = this.collectOutput()
if (output.length === 0) {
this.recordFailure({ stage: 'turn', category: 'invalid-result' })
throw new Error('subagent-codex: Codex completed without a final answer')
throw new Error('subagent-codex: Codex completed without an answer')
}
return { output, stopReason: 'completed' }
}
@@ -386,11 +387,13 @@ export class CodexAppServerWire {
}
/**
* The best non-commentary answer observed so far, preserving exact bytes.
* @returns the selected final or nullable-phase text block, if any.
* The best non-commentary answer observed so far, preserving exact bytes:
* the latest `final_answer`, else the latest `partial_answer`, else the
* latest nullable-phase message.
* @returns the selected text block, if any.
*/
collectOutput(): ContentBlock[] {
const selected = this.lastFinalAnswer ?? this.lastUnphasedAnswer
const selected = this.lastFinalAnswer ?? this.lastPartialAnswer ?? this.lastUnphasedAnswer
return selected !== undefined && selected.trim().length > 0
? [{ type: 'text', text: selected }]
: []
@@ -667,6 +670,11 @@ export class CodexAppServerWire {
: (() => { throw new Error('subagent-codex: app-server returned an invalid agent message') })()
if (item.phase === 'final_answer') {
this.lastFinalAnswer = text
} else if (item.phase === 'partial_answer') {
// `MessagePhase::PartialAnswer` (openai/codex codex-rs/protocol/src/models.rs,
// app-server 0.162+): stable answer text that later assistant output
// or tool calls may follow; it outranks only unphased text.
this.lastPartialAnswer = text
} else if (item.phase === null) {
this.lastUnphasedAnswer = text
} else if (item.phase !== 'commentary') {
@@ -821,6 +821,55 @@ describe('CodexAppServerWire', () => {
wire.close()
})
it('ranks final_answer above partial_answer above nullable-phase answers and never selects commentary', async () => {
const scenarios: Array<{
readonly frames: JsonObject[]
readonly text: string
}> = [
{
frames: [
agentMessage('partial', 'partial_answer'),
agentMessage('final', 'final_answer'),
],
text: 'final',
},
{
frames: [
agentMessage('final', 'final_answer'),
agentMessage('partial after final', 'partial_answer'),
],
text: 'final',
},
{
frames: [agentMessage('only partial', 'partial_answer')],
text: 'only partial',
},
{
frames: [
agentMessage('unphased', null),
agentMessage('commentary before partial', 'commentary'),
agentMessage('first partial', 'partial_answer'),
agentMessage('last partial', 'partial_answer'),
agentMessage('commentary after partial', 'commentary'),
agentMessage('unphased after partial', null),
],
text: 'last partial',
},
]
for (const scenario of scenarios) {
const { child, wire } = await initializeWire()
const result = wire.runTurn(['task'], new AbortController().signal)
const turnStart = await child.peer.nextMethod('turn/start')
child.peer.respond(turnStart, { turn: { id: 'turn-1' } })
child.peer.send(...scenario.frames, turnCompleted('completed'))
await expect(result).resolves.toEqual({
output: [{ type: 'text', text: scenario.text }],
stopReason: 'completed',
})
wire.close()
}
})
it('groups representative string errors without changing stop reasons', async () => {
const scenarios = [
['contextWindowExceeded', 'limit', 'max-tokens'],
@@ -958,7 +1007,7 @@ describe('CodexAppServerWire', () => {
}> = [
{
frames: [turnCompleted('completed')],
message: 'without a final answer',
message: 'without an answer',
category: 'invalid-result',
},
{
@@ -967,7 +1016,7 @@ describe('CodexAppServerWire', () => {
agentMessage(' \n ', 'final_answer'),
turnCompleted('completed'),
],
message: 'without a final answer',
message: 'without an answer',
category: 'invalid-result',
},
{