fix(builtin): split prompt into system/user messages

System message = agent identity (role prompt + output format instruction)
User message = moderator speech (task + edge prompt + history)

This reflects the workflow's core model: moderator speaks to agent
via the graph's edge prompt. Previously all content was in a single
system message with no user message, causing Claude API 400 errors.

- buildBuiltinPrompt now returns { system, user } instead of string
- agent.ts sends system + user as separate messages
- Tests updated accordingly
This commit is contained in:
2026-05-23 17:15:23 +08:00
parent 0e5b494e12
commit 44147da419
4 changed files with 45 additions and 20 deletions
@@ -26,22 +26,30 @@ function minimalContext(overrides: Partial<AgentContext> = {}): AgentContext {
start: { workflow: "wf-hash", prompt: "Fix the bug" },
steps: [],
outputFormatInstruction: "---\nstatus: done\n---",
edgePrompt: "Implement the fix described in the plan.",
isFirstVisit: true,
...overrides,
};
}
describe("buildBuiltinPrompt", () => {
test("includes output format, task, and role goal", () => {
const prompt = buildBuiltinPrompt(minimalContext());
expect(prompt).toContain("status: done");
expect(prompt).toContain("## Goal");
expect(prompt).toContain("Ship the fix");
expect(prompt).toContain("## Task");
expect(prompt).toContain("Fix the bug");
test("system includes output format and role goal", () => {
const { system } = buildBuiltinPrompt(minimalContext());
expect(system).toContain("status: done");
expect(system).toContain("## Goal");
expect(system).toContain("Ship the fix");
});
test("includes history when steps exist", () => {
const prompt = buildBuiltinPrompt(
test("user includes task and edge prompt", () => {
const { user } = buildBuiltinPrompt(minimalContext());
expect(user).toContain("## Task");
expect(user).toContain("Fix the bug");
expect(user).toContain("## Current Step Instruction");
expect(user).toContain("Implement the fix");
});
test("user includes history when steps exist", () => {
const { user } = buildBuiltinPrompt(
minimalContext({
steps: [
{
@@ -53,7 +61,7 @@ describe("buildBuiltinPrompt", () => {
],
}),
);
expect(prompt).toContain("## Previous Steps");
expect(prompt).toContain("planner");
expect(user).toContain("## Previous Steps");
expect(user).toContain("planner");
});
});
+5 -2
View File
@@ -69,8 +69,11 @@ async function runBuiltin(ctx: AgentContext): Promise<AgentRunResult> {
const provider = resolveModel(config, config.defaultModel);
const sessionId = generateUlid(Date.now());
const systemPrompt = buildBuiltinPrompt(ctx);
const messages: ChatMessage[] = [{ role: "system", content: systemPrompt }];
const promptParts = buildBuiltinPrompt(ctx);
const messages: ChatMessage[] = [
{ role: "system", content: promptParts.system },
{ role: "user", content: promptParts.user },
];
const session: BuiltinSessionState = {
sessionId,
View File
+21 -7
View File
@@ -19,18 +19,32 @@ function buildHistorySummary(steps: AgentContext["steps"]): string {
return lines.join("\n");
}
/** Assemble output format, role prompt, task, and history (aligned with buildHermesPrompt). */
export function buildBuiltinPrompt(ctx: AgentContext): string {
export type BuiltinPromptParts = {
system: string;
user: string;
};
/** Assemble system prompt (role + format) and user prompt (task + edge + history). */
export function buildBuiltinPrompt(ctx: AgentContext): BuiltinPromptParts {
const roleDef = ctx.workflow.roles[ctx.role];
const rolePrompt = roleDef !== undefined ? buildRolePrompt(roleDef) : "";
const parts: string[] = [];
const systemParts: string[] = [];
if (ctx.outputFormatInstruction !== "") {
parts.push(ctx.outputFormatInstruction, "");
systemParts.push(ctx.outputFormatInstruction, "");
}
systemParts.push(rolePrompt);
const userParts: string[] = ["## Task", ctx.start.prompt];
if (ctx.edgePrompt !== "") {
userParts.push("", "## Current Step Instruction", ctx.edgePrompt);
}
parts.push(rolePrompt, "", "## Task", ctx.start.prompt);
const historyBlock = buildHistorySummary(ctx.steps);
if (historyBlock !== "") {
parts.push("", historyBlock);
userParts.push("", historyBlock);
}
return parts.join("\n");
return {
system: systemParts.join("\n"),
user: userParts.join("\n"),
};
}