mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
* fix(contracts): pin API-mode override above discovery layer (#313) The old streamFormat='plain' rule was appended at the BOTTOM of the composed prompt, but DISCOVERY_AND_PHILOSOPHY is pinned at the TOP with its own 'these override anything later' header — so its hard rules ('TodoWrite on turn 3', 'brand-spec extraction via Bash + Read + WebFetch') still won precedence in API mode. With no real tools wired through to the Anthropic Messages path, the agent narrated pseudo-tool markup (<todo-list>...</todo-list>, [读取 X]) instead of emitting structured tool_use events the UI could render. Move the API-mode override to the absolute top of the prompt so it beats the discovery layer, name every unavailable tool, and explicitly forbid the pseudo-tool / fake-protocol markup observed in #313. <artifact> output and <question-form> discovery are still allowed — both are markup the UI parses, not tool calls. * fix(daemon): mirror API-mode override above discovery layer (#313) Address Codex + mrcfps review on #1207: the daemon has its own copy of composeSystemPrompt that is hit by any adapter declaring streamFormat: 'plain' (e.g. DeepSeek) via server.ts:6190. That copy still appended the obsolete bottom '## API mode rule', which loses the precedence war against DISCOVERY_AND_PHILOSOPHY's 'these override anything later' header — so plain-stream daemon agents could still narrate <todo-list> / [读取 X] pseudo-tool markup. Mirror the same top-anchored API_MODE_OVERRIDE here (byte-identical to the contracts copy) so both code paths produce the same behaviour. Adds 8 daemon-side tests including the indexOf-based positional assertion that pins the override above the discovery layer header.
96 lines
4.4 KiB
TypeScript
96 lines
4.4 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import { composeSystemPrompt } from '../src/prompts/system.js';
|
|
|
|
/**
|
|
* Regression coverage for #313 — Anthropic API mode renders TodoWrite /
|
|
* Read progress as raw text instead of tool UI cards.
|
|
*
|
|
* Root cause: `DISCOVERY_AND_PHILOSOPHY` (pinned at the TOP of the composed
|
|
* prompt with an explicit "these override anything later" header) tells the
|
|
* agent to call `TodoWrite`, `Bash`, `Read`, etc. on turn 3+. In API/BYOK
|
|
* mode none of those tools are wired through to the model, so the agent
|
|
* either narrates `<todo-list>` pseudo-markup or emits `[读取 X]`
|
|
* fake-protocol prose. The old `streamFormat: 'plain'` rule was appended at
|
|
* the BOTTOM of the prompt — lower precedence than the discovery layer —
|
|
* which is why it was load-bearing-by-position-only and didn't actually
|
|
* suppress the pseudo-tool output.
|
|
*
|
|
* Fix: the API-mode override must sit ABOVE the discovery layer and
|
|
* explicitly invalidate any later "call TodoWrite / Read / Bash" rule.
|
|
*/
|
|
|
|
describe('composeSystemPrompt — API mode (#313)', () => {
|
|
describe('daemon mode (no streamFormat)', () => {
|
|
it('keeps the TodoWrite hard rule from the discovery layer (control)', () => {
|
|
const prompt = composeSystemPrompt({});
|
|
expect(prompt).toMatch(/TodoWrite/);
|
|
});
|
|
|
|
it('does not inject the API-mode preamble', () => {
|
|
const prompt = composeSystemPrompt({});
|
|
expect(prompt).not.toMatch(/API mode — no tools available/i);
|
|
});
|
|
});
|
|
|
|
describe('API mode (streamFormat: plain)', () => {
|
|
it('injects the API-mode override section', () => {
|
|
const prompt = composeSystemPrompt({ streamFormat: 'plain' });
|
|
expect(prompt).toMatch(/API mode — no tools available/i);
|
|
});
|
|
|
|
it('pins the override at the top so it overrides the discovery layer', () => {
|
|
// The discovery layer (DISCOVERY_AND_PHILOSOPHY) starts with the
|
|
// string `# OD core directives`. The API-mode override must appear
|
|
// BEFORE that header — otherwise the discovery layer's own
|
|
// "these override anything later" preamble wins precedence and
|
|
// re-enables TodoWrite/Read/Write/Edit/Bash mentions later in the
|
|
// prompt.
|
|
const prompt = composeSystemPrompt({ streamFormat: 'plain' });
|
|
const overrideIdx = prompt.search(/API mode — no tools available/i);
|
|
const discoveryIdx = prompt.indexOf('# OD core directives');
|
|
expect(overrideIdx).toBeGreaterThanOrEqual(0);
|
|
expect(discoveryIdx).toBeGreaterThanOrEqual(0);
|
|
expect(overrideIdx).toBeLessThan(discoveryIdx);
|
|
});
|
|
|
|
it('names every tool the agent must not pretend to call', () => {
|
|
const prompt = composeSystemPrompt({ streamFormat: 'plain' });
|
|
// Each tool the discovery layer / base prompt assumes is available
|
|
// must be explicitly listed as unavailable so the model knows the
|
|
// later instructions are describing daemon-mode behavior.
|
|
expect(prompt).toMatch(/\bTodoWrite\b/);
|
|
expect(prompt).toMatch(/\bRead\b/);
|
|
expect(prompt).toMatch(/\bWrite\b/);
|
|
expect(prompt).toMatch(/\bEdit\b/);
|
|
expect(prompt).toMatch(/\bBash\b/);
|
|
expect(prompt).toMatch(/\bWebFetch\b/);
|
|
});
|
|
|
|
it('forbids the pseudo-tool markup observed in #313 (`<todo-list>` and `[读取 ...]`)', () => {
|
|
const prompt = composeSystemPrompt({ streamFormat: 'plain' });
|
|
expect(prompt).toMatch(/<todo-list>/);
|
|
expect(prompt).toMatch(/\[读取/);
|
|
});
|
|
|
|
it('tells the agent to state its plan in prose instead of pretending to call TodoWrite', () => {
|
|
const prompt = composeSystemPrompt({ streamFormat: 'plain' });
|
|
expect(prompt).toMatch(/state.*plan.*prose|describe.*plan.*prose|plan.*as prose/i);
|
|
});
|
|
|
|
it('explicitly invalidates later "call TodoWrite" / tool-use instructions', () => {
|
|
const prompt = composeSystemPrompt({ streamFormat: 'plain' });
|
|
// The override must say "ignore later instructions that tell you to
|
|
// call <tool>" — otherwise the discovery layer's RULE 3 "your first
|
|
// tool call is TodoWrite" still applies.
|
|
expect(prompt).toMatch(/override|ignore|do not follow/i);
|
|
expect(prompt).toMatch(/later instructions|rules below|rest of this prompt|elsewhere/i);
|
|
});
|
|
|
|
it('still allows <artifact> HTML output', () => {
|
|
const prompt = composeSystemPrompt({ streamFormat: 'plain' });
|
|
expect(prompt).toMatch(/<artifact>/);
|
|
});
|
|
});
|
|
});
|