mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
fix(headless): preserve reasoning block continuity
This commit is contained in:
@@ -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-headless-reasoning-progress.md
|
||||
2026-08-21-headless-reasoning-progress.md: 714d9bc4d5671c2ba777f8605472142401b5d532
|
||||
2026-08-21-headless-reasoning-progress.zh.md: 1698bb43ff3fff5ef748a4697028224c209d43dd
|
||||
2026-08-21-headless-reasoning-progress.md: b3fc80859a645431a3a172244d1a7b5a36deefc7
|
||||
2026-08-21-headless-reasoning-progress.zh.md: fde2ebac27512a75055ba75a35efc26918fb6eeb
|
||||
|
||||
@@ -12,7 +12,7 @@ The earlier [direct core entry-point decision](../architecture/2026-08-09-headle
|
||||
|
||||
## Decision
|
||||
|
||||
`headless-runner` observes the exact Session it creates after startup quiescence and before submitting the task. Once the owned interval opens with `turn/start`, each non-empty `assistant/chunk.reasoning-delta` is written immediately to stderr. A contiguous reasoning phase starts with `dsh: reasoning:` on its own line; deltas retain provider order without token-boundary decoration. The first later non-reasoning chunk, a new turn, or listener disposal terminates the phase with one newline when the provider supplied none.
|
||||
`headless-runner` observes the exact Session it creates after startup quiescence and before submitting the task. Once the owned interval opens with `turn/start`, each non-empty `assistant/chunk.reasoning-delta` is written immediately to stderr. A contiguous reasoning phase starts with `dsh: reasoning:` on its own line; deltas retain provider order without token-boundary decoration. Reasoning block boundaries and usage metadata keep that phase open; a later non-reasoning block or output delta, stream finish, new turn, or listener disposal terminates it with one newline when the provider supplied none.
|
||||
|
||||
This output is a transient projection of the existing durable Session event stream. The runner still derives final text and exit status from the flushed log rather than from progress-presentation state. The LLM adapter, agent loop, Session event types, persistence format, and SDK projections do not change.
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
`headless-runner` 在启动工作完全停稳后、提交任务前,观察其创建的精确 Session。自身持有的区间以 `turn/start` 打开后,每个非空的 `assistant/chunk.reasoning-delta` 都会立即写入 stderr。一段连续推理以独占一行的 `dsh: reasoning:` 开始;各分片保持提供方顺序,不添加 token 边界装饰。之后出现首个非推理分片、新轮次或 listener dispose(资源释放)时,如果提供方没有输出末尾换行,runner 会用一个换行终止该段。
|
||||
`headless-runner` 在启动工作完全停稳后、提交任务前,观察其创建的精确 Session。自身持有的区间以 `turn/start` 打开后,每个非空的 `assistant/chunk.reasoning-delta` 都会立即写入 stderr。一段连续推理以独占一行的 `dsh: reasoning:` 开始;各分片保持提供方顺序,不添加 token 边界装饰。推理块边界与用量元数据会保持该段打开;之后出现非推理块或输出分片、流结束、新轮次或 listener dispose(资源释放)时,如果提供方没有输出末尾换行,runner 会用一个换行终止该段。
|
||||
|
||||
该输出是既有持久化会话事件流的瞬时投影。runner 仍从 flush 后的日志而不是进度呈现状态推导最终文本与退出状态。LLM(大语言模型)适配器、agent loop(智能体循环)、Session 事件类型、持久化格式与 SDK 投影均不改变。
|
||||
|
||||
|
||||
@@ -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/bundle/headless/README.md
|
||||
README.md: 0bd2fac0d3ab59332d42788a2b1f15599b6bcab2
|
||||
README.zh.md: 610a908420ce5b214881242eb4b4a48c1262953a
|
||||
README.md: 373e50c515ef45d09c32e7dd1b011f149d921250
|
||||
README.zh.md: 443945ced7e5e41899e91e8c169918f4e186de67
|
||||
|
||||
@@ -20,4 +20,5 @@ None; the runner adds nothing to the request prefix.
|
||||
|
||||
- **One submitted task only** — the runner has no interactive follow-up surface; it waits through any work the Agent completes before returning to idle and prints the last non-empty assistant message in that interval.
|
||||
- **No pre-token heartbeat** — stderr remains silent until the provider emits a non-empty reasoning delta; a provider that delays its first streamed token exposes no earlier progress signal.
|
||||
- **Reasoning enters stderr logs** — redirection and supervisors may retain substantially more and potentially sensitive model output; route stderr to a controlled sink when that content must not be collected.
|
||||
- **`ctx.appExit` is launcher-owned** — booting the headless profile outside the `dsh` launcher fails loud at activation until the host provides the exit request.
|
||||
|
||||
@@ -20,4 +20,5 @@ Loader 结算后,runner 读取共享的 [`ctx.agentDefaultModel`](../../core/a
|
||||
|
||||
- **只提交一个任务**:runner 没有用于交互式后续输入的 surface;它会等待 Agent 在返回 idle 前完成的所有工作,并打印该区间内最后一条非空 assistant 消息。
|
||||
- **首个 token 前没有心跳**:在提供方发出非空推理分片前,stderr 保持静默;如果提供方延迟首个流式 token,系统不会提供更早的进度信号。
|
||||
- **推理会进入 stderr 日志**:重定向与监督进程可能保留明显更多且可能敏感的模型输出;不得收集该内容时,应将 stderr 送往受控目标。
|
||||
- **`ctx.appExit` 由启动器持有**:在 `dsh` 启动器之外启动 headless profile 会在激活时明确报错,直到宿主提供该退出请求。
|
||||
|
||||
@@ -123,7 +123,15 @@ function streamReasoning(
|
||||
endsWithNewline = chunk.text.endsWith('\n')
|
||||
return
|
||||
}
|
||||
if (chunk.type === 'block-start' && chunk.blockType === 'reasoning') return
|
||||
if (chunk.type === 'block-start') {
|
||||
if (chunk.blockType !== 'reasoning') close()
|
||||
return
|
||||
}
|
||||
if (chunk.type === 'block-end') {
|
||||
if (chunk.block.type !== 'reasoning') close()
|
||||
return
|
||||
}
|
||||
if (chunk.type === 'usage') return
|
||||
close()
|
||||
})
|
||||
return () => {
|
||||
|
||||
@@ -14,10 +14,10 @@ export const name = 'headless-invariant'
|
||||
export const inject = ['invariants']
|
||||
|
||||
/**
|
||||
* No runtime invariant: the runner is a one-shot driver over the API carrier
|
||||
* whose observable contract (final text on stdout, exit code by turn-end
|
||||
* reason) is process-level and owned by the launcher e2e; it registers
|
||||
* nothing and holds no mutable relation to audit inside the tree.
|
||||
* No runtime invariant: the runner's observable contract (provider reasoning
|
||||
* on stderr, final text on stdout, exit code by turn-end reason) is
|
||||
* process-level and owned by the launcher e2e; it registers nothing and holds
|
||||
* no mutable relation to audit inside the tree.
|
||||
*/
|
||||
const install: InvariantInstaller = () => {}
|
||||
|
||||
|
||||
@@ -173,12 +173,42 @@ describe('headless runner', () => {
|
||||
step: 1,
|
||||
chunk: { type: 'reasoning-delta', index: 0, text: ' safely\n' },
|
||||
})
|
||||
session.append('assistant/chunk', {
|
||||
turn: 1,
|
||||
step: 1,
|
||||
chunk: { type: 'block-end', index: 0, block: { type: 'reasoning', text: 'checking the workspace safely\n' } },
|
||||
})
|
||||
session.append('assistant/chunk', {
|
||||
turn: 1,
|
||||
step: 1,
|
||||
chunk: { type: 'usage', usage: { inputTokens: 1, outputTokens: 2, reasoningTokens: 2 } },
|
||||
})
|
||||
session.append('assistant/chunk', {
|
||||
turn: 1,
|
||||
step: 1,
|
||||
chunk: { type: 'block-start', index: 1, blockType: 'reasoning' },
|
||||
})
|
||||
session.append('assistant/chunk', {
|
||||
turn: 1,
|
||||
step: 1,
|
||||
chunk: { type: 'reasoning-delta', index: 1, text: 'second pass\n' },
|
||||
})
|
||||
reasoningAppended.resolve(undefined)
|
||||
await release.promise
|
||||
session.append('assistant/chunk', {
|
||||
turn: 1,
|
||||
step: 1,
|
||||
chunk: { type: 'block-start', index: 1, blockType: 'text' },
|
||||
chunk: { type: 'block-start', index: 2, blockType: 'text' },
|
||||
})
|
||||
session.append('assistant/chunk', {
|
||||
turn: 1,
|
||||
step: 1,
|
||||
chunk: { type: 'text-delta', index: 2, text: 'done' },
|
||||
})
|
||||
session.append('assistant/chunk', {
|
||||
turn: 1,
|
||||
step: 1,
|
||||
chunk: { type: 'block-end', index: 2, block: { type: 'text', text: 'done' } },
|
||||
})
|
||||
session.append('assistant/message', {
|
||||
turn: 1,
|
||||
@@ -207,13 +237,13 @@ describe('headless runner', () => {
|
||||
const result = await running
|
||||
expect(streamed).toEqual({
|
||||
out: '',
|
||||
err: 'dsh: reasoning:\nchecking the workspace safely\n',
|
||||
err: 'dsh: reasoning:\nchecking the workspace safely\nsecond pass\n',
|
||||
order: [],
|
||||
})
|
||||
expect(result).toEqual({
|
||||
code: 0,
|
||||
out: 'done\n',
|
||||
err: 'dsh: reasoning:\nchecking the workspace safely\n',
|
||||
err: 'dsh: reasoning:\nchecking the workspace safely\nsecond pass\n',
|
||||
order: ['flush', 'exit'],
|
||||
})
|
||||
await test.ctx.fiber.dispose()
|
||||
|
||||
Reference in New Issue
Block a user