Compare commits

..

2 Commits

Author SHA1 Message Date
xingyue 0eeb4a8ed8 fix(builtin): strip preamble before frontmatter + stronger prompt
- Add stripPreamble() to handle LLM output with text before ---
- Strengthen system prompt: CRITICAL instruction for --- at position 0
- Fixes frontmatter parsing failures on first output turn
2026-05-23 20:37:14 +08:00
xingyue a3fac708b6 fix(builtin-agent): don't delete session jsonl until process exits
Previously runBuiltinWithMessages deleted the session jsonl after each
run/continue call. This meant the createAgent retry mechanism (which
calls continue on frontmatter validation failure) would lose all
previous turn data — each continue started with an empty jsonl.

Now the session jsonl accumulates across run + continue calls, so the
final storeBuiltinDetail captures all turns. The jsonl file is left
behind for debugging; it's small and can be cleaned up on next startup.

Also add a workflow hint to the system prompt reminding the LLM to use
tools before outputting frontmatter, preventing premature text-only
responses on the first turn.
2026-05-23 20:32:38 +08:00
3 changed files with 32 additions and 17 deletions
+20 -6
View File
@@ -13,10 +13,28 @@ import { storeBuiltinDetail } from "./detail.js";
import type { ChatMessage } from "./llm/index.js";
import { BUILTIN_CONTINUE_MAX_TURNS, BUILTIN_MAX_TURNS, runBuiltinLoop } from "./loop.js";
import { buildBuiltinMessages } from "./prompt.js";
import { initSessionDir, removeSession } from "./session.js";
import { initSessionDir } from "./session.js";
const log = createLogger({ sink: { kind: "stderr" } });
const FRONTMATTER_FENCE = "---";
/**
* Strip any text before the first `---` fence.
* LLMs sometimes emit preamble text before the frontmatter block.
*/
function stripPreamble(text: string): string {
if (text.startsWith(FRONTMATTER_FENCE)) {
return text;
}
const idx = text.indexOf(`\n${FRONTMATTER_FENCE}\n`);
if (idx !== -1) {
log("6GWRP3QX", `stripped ${idx + 1} chars of preamble before frontmatter`);
return text.slice(idx + 1);
}
return text;
}
type SessionRecord = {
sessionId: string;
model: string;
@@ -62,7 +80,6 @@ async function runBuiltinWithMessages(
if (loopResult.turnCount === 0) {
log("5RWTK9NB", "no turns produced, returning empty output");
await removeSession(storageRoot, session.sessionId);
return { output: "", detailHash: "", sessionId: session.sessionId };
}
@@ -75,10 +92,7 @@ async function runBuiltinWithMessages(
session.startedAtMs,
);
// Clean up session jsonl
await removeSession(storageRoot, session.sessionId);
return { output: loopResult.finalText, detailHash, sessionId: session.sessionId };
return { output: stripPreamble(loopResult.finalText), detailHash, sessionId: session.sessionId };
}
async function runBuiltin(ctx: AgentContext): Promise<AgentRunResult> {
@@ -59,6 +59,18 @@ export function buildBuiltinMessages(ctx: AgentContext): ChatMessage[] {
}
systemParts.push(rolePrompt);
systemParts.push(
"",
"## Workflow",
"",
"You have tools available (read_file, write_file, run_command). " +
"Use them to complete your task — read files, run commands, make changes as needed. " +
"When you are done, output your final response with the YAML frontmatter block as specified above. " +
"Do NOT output the frontmatter until you have completed all necessary work. " +
"CRITICAL: Your final output MUST start with the `---` fence on the very first line — " +
"no preamble text, no explanation before it. The parser requires `---` at position 0.",
);
const messages: ChatMessage[] = [{ role: "system", content: systemParts.join("\n") }];
const roleVisitIndices: number[] = [];
@@ -1,5 +1,4 @@
import { spawn } from "node:child_process";
import { mkdirSync, writeFileSync } from "node:fs";
import type { Store } from "@uncaged/json-cas";
import {
type AgentContext,
@@ -118,17 +117,7 @@ function spawnClaudeResume(
]);
}
const NDJSON_DUMP_DIR = "/tmp/uwf-ndjson-dump";
async function processClaudeOutput(stdout: string, store: Store): Promise<AgentRunResult> {
// Debug dump: save raw NDJSON for issue #439 investigation
try {
mkdirSync(NDJSON_DUMP_DIR, { recursive: true });
writeFileSync(`${NDJSON_DUMP_DIR}/${Date.now()}.ndjson`, stdout);
} catch {
// ignore dump failures
}
const parsed = parseClaudeCodeStreamOutput(stdout);
if (parsed !== null) {