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,