fix(headless): terminal model failures reach stderr
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 packages/bundle/headless/README.md
|
||||
README.md: 6faf1dc732345a86ec1d7b60c4f9a606e0491512
|
||||
README.zh.md: a189f77f57cf0f641abe7c11288dbc1cdf05355a
|
||||
README.md: fbdf80be3d587b0ef526c1a36952a761035f7312
|
||||
README.zh.md: 833737e08643e0ad6b585f93fdc6ae018242b937
|
||||
|
||||
@@ -4,7 +4,7 @@ English | [中文](README.zh.md)
|
||||
|
||||
The dsh one-shot bundle. [`cordis.patch.yml`](cordis.patch.yml) rides directly over [`dsh-base`](../base/README.md): it supplies the coding persona and tool mode, disables HMR, mounts Code Mode's worker as a core execution capability, and inserts this package's `headless-runner` plugin (config `{task}`). It mounts no Host, HTTP server, Web runtime, or browser plugin.
|
||||
|
||||
After the Loader settles, the runner reads the shared [`ctx.agentDefaultModel`](../../core/agent-default-model/README.md), creates one fresh persisted Agent through `ctx.agents`, submits the task as an ordinary user message, and waits for quiescence. It flushes the Session before folding the owned durable event interval, writes the last non-empty assistant text to stdout, and requests exit through the launcher-provided `ctx.headlessIo` seam (final `turn/end` completed → 0, otherwise 1). A successful run writes nothing to stderr and opens no listening port. The launcher patches the task text in (`dsh run "task"`) and fails loud when the selected profile lacks this row.
|
||||
After the Loader settles, the runner reads the shared [`ctx.agentDefaultModel`](../../core/agent-default-model/README.md), creates one fresh persisted Agent through `ctx.agents`, submits the task as an ordinary user message, and waits for quiescence. It flushes the Session before folding the owned durable event interval, writes the last non-empty assistant text to stdout, and requests exit through the launcher-provided `ctx.headlessIo` seam (final `turn/end` completed → 0, otherwise 1). A terminal `error` reason also writes its code and message to stderr; successful runs keep stderr empty. The process opens no listening port. The launcher patches the task text in (`dsh run "task"`) and fails loud when the selected profile lacks this row.
|
||||
|
||||
## Model Experience
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
dsh 一次性任务组合包。[`cordis.patch.yml`](cordis.patch.yml) 直接叠加在 [`dsh-base`](../base/README.md) 之上:提供编码 persona 和工具模式、禁用 HMR(热模块替换)、将 Code Mode 的 worker 作为核心执行能力挂载,并插入本包的 `headless-runner` 插件(配置为 `{task}`)。它不挂载任何 Host、HTTP server、Web runtime 或浏览器插件。
|
||||
|
||||
Loader 结算后,runner 读取共享的 [`ctx.agentDefaultModel`](../../core/agent-default-model/README.md),通过 `ctx.agents` 创建一个全新的持久化 Agent(智能体),将任务作为普通用户消息提交,并等待完全停稳。它对 Session 执行 flush 后再汇总自身持有的持久化事件区间,将最后一条非空 assistant 文本写入 stdout,再经启动器提供的 `ctx.headlessIo` seam 请求退出(最终 `turn/end` 完成 → 0,否则为 1)。成功运行时不会向 stderr 写入任何内容,也不会打开监听端口。启动器把任务文本 patch 进来(`dsh run "task"`);若所选 profile 缺少该行,则显式报错。
|
||||
Loader 结算后,runner 读取共享的 [`ctx.agentDefaultModel`](../../core/agent-default-model/README.md),通过 `ctx.agents` 创建一个全新的持久化 Agent(智能体),将任务作为普通用户消息提交,并等待完全停稳。它对 Session 执行 flush 后再汇总自身持有的持久化事件区间,将最后一条非空 assistant 文本写入 stdout,再经启动器提供的 `ctx.headlessIo` seam 请求退出(最终 `turn/end` 完成 → 0,否则为 1)。最终 reason 为 `error` 时,还会将持久化的 code 与 message 写入 stderr;成功运行时 stderr 保持为空。进程不会打开监听端口。启动器把任务文本 patch 进来(`dsh run "task"`);若所选 profile 缺少该行,则显式报错。
|
||||
|
||||
## 模型体验
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ export const Config: z<Config> = z.object({
|
||||
/** Outcome of one owned run interval. */
|
||||
interface RunOutcome {
|
||||
text: string
|
||||
reason: string
|
||||
reason: SessionEvent<'turn/end'>['data']['reason'] | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,7 +63,7 @@ declare module 'cordis' {
|
||||
function summarize(events: readonly SessionEvent[], firstSeq: number): RunOutcome {
|
||||
let started = false
|
||||
let text = ''
|
||||
let reason = 'error'
|
||||
let reason: SessionEvent<'turn/end'>['data']['reason'] | undefined
|
||||
for (const event of events) {
|
||||
if (event.seq < firstSeq) continue
|
||||
if (event.type === 'turn/start') {
|
||||
@@ -78,7 +78,7 @@ function summarize(events: readonly SessionEvent[], firstSeq: number): RunOutcom
|
||||
.join('')
|
||||
if (joined !== '') text = joined
|
||||
}
|
||||
if (event.type === 'turn/end') reason = event.data.reason.kind
|
||||
if (event.type === 'turn/end') reason = event.data.reason
|
||||
}
|
||||
return { text, reason }
|
||||
}
|
||||
@@ -125,7 +125,10 @@ async function run(ctx: Context, task: string, io: HeadlessIo): Promise<void> {
|
||||
await sessions.flush(agent.session)
|
||||
const outcome = summarize(agent.session.events, firstSeq)
|
||||
io.stdout.write(outcome.text + '\n')
|
||||
io.exit(outcome.reason === 'completed' ? 0 : 1)
|
||||
if (outcome.reason?.kind === 'error') {
|
||||
io.stderr.write(`dsh: ${outcome.reason.error.code}: ${outcome.reason.error.message}\n`)
|
||||
}
|
||||
io.exit(outcome.reason?.kind === 'completed' ? 0 : 1)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -151,6 +151,27 @@ describe('headless runner', () => {
|
||||
await test.ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('prints the durable model failure when the final turn ends in error', async () => {
|
||||
const test = await bench({
|
||||
afterPrompt(session, message) {
|
||||
session.append('turn/start', { turn: 1 })
|
||||
session.append('step/start', { turn: 1, step: 1 })
|
||||
session.append('user/message', message, { surfaceOp: 'append' })
|
||||
session.append('step/end', { turn: 1, step: 1 })
|
||||
session.append('turn/end', {
|
||||
turn: 1,
|
||||
reason: { kind: 'error', error: { code: 'SERVER', message: 'provider unavailable' } },
|
||||
})
|
||||
},
|
||||
})
|
||||
expect(await test.run()).toMatchObject({
|
||||
code: 1,
|
||||
out: '\n',
|
||||
err: 'dsh: SERVER: provider unavailable\n',
|
||||
})
|
||||
await test.ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('exits 1 when the owned interval contains no turn', async () => {
|
||||
const test = await bench({ afterPrompt: () => {} })
|
||||
expect(await test.run()).toMatchObject({ code: 1, out: '\n', err: '' })
|
||||
|
||||
Reference in New Issue
Block a user