From 276606fc70f149a3e5e42598d2246276ab56a81f Mon Sep 17 00:00:00 2001 From: Linghong Date: Thu, 20 Aug 2026 08:49:40 +0000 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E6=97=A0=E5=A4=B4=E5=BA=94?= =?UTF-8?q?=E7=AD=94=E5=99=A8=E6=94=B6=20stdin=20=E7=9A=84=E6=97=B6?= =?UTF-8?q?=E6=9C=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLI 的 REPL 是「先打印提示符再读行」,所以第一个「你>」出现时本轮还没开始跑:它 就是用来读我们这条任务的。在那里收 stdin,等确认卡弹出来已经是 EOF,自动应答器 根本没机会回 approve,一轮真实跑就这样收在 pending-confirmation。改成按提示符计 数,投递之后的下一个提示符才收口。 同时补上原本缺的那条:总控可能判定直接回复而不起持久 Run,那条路径没有 turn 回 执,只等回执会一直干等到超时——实测白等了十分钟。提示符收口对两种情况都成立。 Co-Authored-By: Claude Opus 5 --- .../scripts/agent-swarm-test-chat.mjs | 29 +++++++++++++++++-- .../tests/agentSwarmTestEntry.test.ts | 24 +++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/apps/ai-game-creator-shell/scripts/agent-swarm-test-chat.mjs b/apps/ai-game-creator-shell/scripts/agent-swarm-test-chat.mjs index fa10ae383..3229b753c 100644 --- a/apps/ai-game-creator-shell/scripts/agent-swarm-test-chat.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-swarm-test-chat.mjs @@ -946,6 +946,20 @@ export function nextSwarmAutoPilotReply(output) { return null; } +// CLI 的 REPL 是「先打印提示符再读行」,所以第一个「你>」出现时本轮还没开始跑: +// 它就是用来读我们这条任务的。收 stdin 必须等到投递之后的下一个提示符——那才是 +// 本轮结束、CLI 回到待输入状态。绝大多数情况下此前已经打印过 turn 回执,但总控也 +// 可能判定直接回复而不起持久 Run,那条路径没有回执,只等回执会一直干等到超时。 +const swarmChatPromptPattern = /(^|\n)你> $/u; + +export function swarmAutoPilotSitsAtPrompt(output) { + return swarmChatPromptPattern.test(output); +} + +export function swarmAutoPilotShouldCloseInput(output, promptsAfterSubmit) { + return swarmAutoPilotSitsAtPrompt(output) && promptsAfterSubmit >= 1; +} + async function runTaskCargo( cliArguments, task, @@ -960,6 +974,9 @@ async function runTaskCargo( const reportLines = []; let pendingLine = ''; let settled = false; + let taskSubmitted = false; + let promptsSeen = 0; + let sittingAtPrompt = false; child.stdout.setEncoding('utf8'); child.stdout.on('data', (chunk) => { process.stdout.write(chunk); @@ -974,8 +991,15 @@ async function runTaskCargo( } } if (!autoPilot || child.stdin.writableEnded) return; - // turn 已给出回执后不再应答,直接收 stdin 让 CLI 正常退出。 - if (settled) { + const atPrompt = swarmAutoPilotSitsAtPrompt(pendingLine); + if (atPrompt && !sittingAtPrompt) promptsSeen += 1; + sittingAtPrompt = atPrompt; + // turn 已给出回执、或 CLI 回到了投递之后的下一个提示符,都说明本轮结束。 + if ( + settled || + (taskSubmitted && + swarmAutoPilotShouldCloseInput(pendingLine, promptsSeen - 1)) + ) { child.stdin.end(); return; } @@ -987,6 +1011,7 @@ async function runTaskCargo( }); if (autoPilot) { child.stdin.write(`${task}\n`); + taskSubmitted = true; } else { child.stdin.end(`${task}\n`); } diff --git a/apps/ai-game-creator-shell/tests/agentSwarmTestEntry.test.ts b/apps/ai-game-creator-shell/tests/agentSwarmTestEntry.test.ts index be8b984fc..f91ba8689 100644 --- a/apps/ai-game-creator-shell/tests/agentSwarmTestEntry.test.ts +++ b/apps/ai-game-creator-shell/tests/agentSwarmTestEntry.test.ts @@ -46,6 +46,8 @@ import { resolveSwarmTestTimeoutMs, runnerEndpointFileName, shouldStartPersistentPreview, + swarmAutoPilotShouldCloseInput, + swarmAutoPilotSitsAtPrompt, terminateChildTree, testProjectPrefix, testProjectSentinelName, @@ -715,6 +717,28 @@ describe('Swarm test argument parsing', () => { ).toBeNull(); }); + it('closes the driven stdin only at the prompt that follows submission', () => { + // CLI 先打印提示符再读行:第一个「你>」是用来读我们这条任务的,此时本轮还没 + // 开始跑。在那里收 stdin,确认卡弹出来时已经是 EOF,自动应答器根本没机会回 + // approve——实测就这样把一轮跑成了 pending-confirmation。 + expect(swarmAutoPilotSitsAtPrompt('你> ')).toBe(true); + expect(swarmAutoPilotShouldCloseInput('你> ', 0)).toBe(false); + expect(swarmAutoPilotShouldCloseInput('你> ', 1)).toBe(true); + // 总控判定直接回复那条路径没有 turn 回执,同样靠这个提示符收口。 + expect(swarmAutoPilotShouldCloseInput('[意图] reply\n\n你> ', 1)).toBe( + true, + ); + // 提示符不在缓冲末尾、或本轮还在推进时不得收 stdin。 + expect( + swarmAutoPilotSitsAtPrompt( + '你> [决策] Agent 正在判断直接回复或调用持久能力。', + ), + ).toBe(false); + expect( + swarmAutoPilotSitsAtPrompt('[状态] project-supervisor running/planning'), + ).toBe(false); + }); + it('keeps persistent preview only for manual chat mode', () => { expect(shouldStartPersistentPreview(parseSwarmTestArguments([]))).toBe( true,