diff --git a/.agents/notes/implemented/feature/2026-08-21-headless-reasoning-progress.i18n.yaml b/.agents/notes/implemented/feature/2026-08-21-headless-reasoning-progress.i18n.yaml index 121aebf0f6..e78a592c98 100644 --- a/.agents/notes/implemented/feature/2026-08-21-headless-reasoning-progress.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-21-headless-reasoning-progress.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-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 diff --git a/.agents/notes/implemented/feature/2026-08-21-headless-reasoning-progress.md b/.agents/notes/implemented/feature/2026-08-21-headless-reasoning-progress.md index 714d9bc4d5..b3fc80859a 100644 --- a/.agents/notes/implemented/feature/2026-08-21-headless-reasoning-progress.md +++ b/.agents/notes/implemented/feature/2026-08-21-headless-reasoning-progress.md @@ -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. diff --git a/.agents/notes/implemented/feature/2026-08-21-headless-reasoning-progress.zh.md b/.agents/notes/implemented/feature/2026-08-21-headless-reasoning-progress.zh.md index 1698bb43ff..fde2ebac27 100644 --- a/.agents/notes/implemented/feature/2026-08-21-headless-reasoning-progress.zh.md +++ b/.agents/notes/implemented/feature/2026-08-21-headless-reasoning-progress.zh.md @@ -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 投影均不改变。 diff --git a/packages/bundle/headless/README.i18n.yaml b/packages/bundle/headless/README.i18n.yaml index 84d5c46259..aab3988b0f 100644 --- a/packages/bundle/headless/README.i18n.yaml +++ b/packages/bundle/headless/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/bundle/headless/README.md -README.md: 0bd2fac0d3ab59332d42788a2b1f15599b6bcab2 -README.zh.md: 610a908420ce5b214881242eb4b4a48c1262953a +README.md: 373e50c515ef45d09c32e7dd1b011f149d921250 +README.zh.md: 443945ced7e5e41899e91e8c169918f4e186de67 diff --git a/packages/bundle/headless/README.md b/packages/bundle/headless/README.md index 0bd2fac0d3..373e50c515 100644 --- a/packages/bundle/headless/README.md +++ b/packages/bundle/headless/README.md @@ -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. diff --git a/packages/bundle/headless/README.zh.md b/packages/bundle/headless/README.zh.md index 610a908420..443945ced7 100644 --- a/packages/bundle/headless/README.zh.md +++ b/packages/bundle/headless/README.zh.md @@ -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 会在激活时明确报错,直到宿主提供该退出请求。 diff --git a/packages/bundle/headless/src/index.ts b/packages/bundle/headless/src/index.ts index e520ecf950..75289736a5 100644 --- a/packages/bundle/headless/src/index.ts +++ b/packages/bundle/headless/src/index.ts @@ -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 () => { diff --git a/packages/bundle/headless/src/invariant.ts b/packages/bundle/headless/src/invariant.ts index cd435b5fcc..0d22891eb2 100644 --- a/packages/bundle/headless/src/invariant.ts +++ b/packages/bundle/headless/src/invariant.ts @@ -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 = () => {} diff --git a/packages/bundle/headless/tests/headless.spec.ts b/packages/bundle/headless/tests/headless.spec.ts index 652a0c9387..fb86ff8387 100644 --- a/packages/bundle/headless/tests/headless.spec.ts +++ b/packages/bundle/headless/tests/headless.spec.ts @@ -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()