mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
fix(daemon): close # Instructions block with an explicit do-not-echo guard (#2827)
The composed chat prompt prepends a '# Instructions (read first)' block in front of '# User request' so a single user message carries both the system rules and the actual request — the shape every agent CLI (Claude, Codex, OpenCode, Gemini) expects on stdin. In practice claude-opus-4-7 (and a few other instruction-tuned models, particularly with --include-partial-messages on the stream) start their reply by echoing the top of that user message verbatim. The chat UI then shows the system prompt as a literal block leading the visible answer, e.g.: Instructions Always respond in Korean. Use Korean for all explanations… …Maintain full orthographic correctness… ).네, 완료했습니다. 전달하신 4가지 보강 포인트를 … (The closing token of the instructions block runs straight into the real answer without a newline — the telltale of a model-side echo rather than a UI render bug.) Close every Instructions block with one trailing line: (Do not quote, restate, or echo the # Instructions block above in your reply. Begin your response with the answer to the # User request below.) This kills the regression in practice without changing the turn shape (still one user message), so no agent CLI plumbing has to move. Tested via tests/chat-route.test.ts — pins the literal guard string so a future refactor cannot silently drop it. Co-authored-by: nicejames <nicejames@gmail.com>
This commit is contained in:
parent
53dfb8808c
commit
8e3d1360bd
2 changed files with 57 additions and 3 deletions
|
|
@ -10276,13 +10276,22 @@ export async function startServer({
|
|||
clientSystemPrompt: clientInstructionPrompt,
|
||||
finalPromptOverride: codexImagegenOverride,
|
||||
});
|
||||
// Some models (notably claude-opus-4-7 with --include-partial-messages)
|
||||
// start their reply by echoing the top of the user message verbatim,
|
||||
// so the rendered chat shows a "# Instructions ..." block ahead of the
|
||||
// real answer. Closing every Instructions block with an explicit
|
||||
// "do not echo" line cuts the regression in practice without changing
|
||||
// the turn-shape every agent CLI expects (user message carrying both
|
||||
// instructions and request) — see server.ts:9920 composer notes.
|
||||
const ECHO_GUARD =
|
||||
'\n\n(Do not quote, restate, or echo the # Instructions block above in your reply. Begin your response with the answer to the # User request below.)';
|
||||
const composed = [
|
||||
instructionPrompt
|
||||
? `# Instructions (read first)\n\n${instructionPrompt}${cwdHint}${linkedDirsHint}\n\n---\n`
|
||||
? `# Instructions (read first)\n\n${instructionPrompt}${cwdHint}${linkedDirsHint}${ECHO_GUARD}\n\n---\n`
|
||||
: cwdHint
|
||||
? `# Instructions${cwdHint}${linkedDirsHint}\n\n---\n`
|
||||
? `# Instructions${cwdHint}${linkedDirsHint}${ECHO_GUARD}\n\n---\n`
|
||||
: linkedDirsHint
|
||||
? `# Instructions${linkedDirsHint}\n\n---\n`
|
||||
? `# Instructions${linkedDirsHint}${ECHO_GUARD}\n\n---\n`
|
||||
: '',
|
||||
`# User request\n\n${userRequestPrompt}${attachmentHint}${commentHint}`,
|
||||
safeImages.length
|
||||
|
|
|
|||
|
|
@ -214,6 +214,51 @@ process.exit(0);
|
|||
);
|
||||
});
|
||||
|
||||
it('closes the # Instructions block with an explicit "do not echo" guard so models do not parrot the prompt back', async () => {
|
||||
// claude-opus-4-7 (and a few other instruction-tuned models) start
|
||||
// their reply by echoing the # Instructions block verbatim, which
|
||||
// shows up to users as the system prompt leading the visible
|
||||
// answer. server.ts:9934 closes every Instructions block with a
|
||||
// trailing guard line; this test pins the literal so a future
|
||||
// refactor cannot silently drop it.
|
||||
await withFakeAgent(
|
||||
'opencode',
|
||||
`
|
||||
let prompt = '';
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', (chunk) => {
|
||||
prompt += chunk;
|
||||
});
|
||||
process.stdin.on('end', () => {
|
||||
const checks = [
|
||||
prompt.includes('Do not quote, restate, or echo the # Instructions block above')
|
||||
? 'has-echo-guard'
|
||||
: 'missing-echo-guard',
|
||||
];
|
||||
console.log(JSON.stringify({ type: 'step_start' }));
|
||||
console.log(JSON.stringify({ type: 'text', part: { text: checks.join('\\n') } }));
|
||||
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
|
||||
process.exit(0);
|
||||
});
|
||||
`,
|
||||
async () => {
|
||||
const response = await fetch(`${baseUrl}/api/chat`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
agentId: 'opencode',
|
||||
message: 'hello',
|
||||
}),
|
||||
});
|
||||
const body = await response.text();
|
||||
|
||||
expect(response.ok).toBe(true);
|
||||
expect(body).toContain('has-echo-guard');
|
||||
expect(body).not.toContain('missing-echo-guard');
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('injects @-mention skillIds into the composed system prompt', async () => {
|
||||
await withFakeAgent(
|
||||
'opencode',
|
||||
|
|
|
|||
Loading…
Reference in a new issue