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:
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
Regular → Executable
@@ -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"),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user