Compare commits

...

5 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
xiaomo 52879c0028 Merge pull request 'feat(cli-workflow): implement multi-strategy workflow resolution' (#438) from fix/428-multi-strategy-workflow-resolution into main 2026-05-23 11:12:56 +00:00
xiaomo 9e4527bb89 Merge pull request 'fix(cli): disable YAML anchor/alias in output' (#437) from fix/yaml-no-alias into main 2026-05-23 11:09:11 +00:00
xingyue 5209cfa7ac fix(cli): disable YAML anchor/alias + fix biome errors in setup.ts
- Disable aliasDuplicateObjects in YAML stringify to prevent &a1/*a1
  anchors when multiple steps have identical output
- Fix unused discoverAgents function (prefixed with _) and format issue
  in setup.ts
2026-05-23 19:07:36 +08:00
5 changed files with 40 additions and 11 deletions
+6 -3
View File
@@ -141,7 +141,7 @@ function apiKeyEnvName(providerName: string): string {
* Discover uwf-* agent binaries in PATH.
* Returns sorted list of binary names (e.g., ["uwf-hermes", "uwf-claude-code"]).
*/
async function discoverAgents(): Promise<string[]> {
async function _discoverAgents(): Promise<string[]> {
try {
// Use which -a to find all uwf-* binaries in PATH
const proc = Bun.spawn(["which", "-a", "uwf-hermes", "uwf-claude-code", "uwf-cursor"], {
@@ -186,12 +186,15 @@ async function discoverAgents(): Promise<string[]> {
}
// Parse which output - each line is a path to a binary
const paths = text.trim().split("\n").filter((line) => line.length > 0);
const paths = text
.trim()
.split("\n")
.filter((line) => line.length > 0);
const agents = new Set<string>();
for (const path of paths) {
const basename = path.split("/").pop();
if (basename && basename.startsWith("uwf-") && basename !== "uwf") {
if (basename?.startsWith("uwf-") && basename !== "uwf") {
agents.add(basename);
}
}
+1 -1
View File
@@ -539,7 +539,7 @@ function collectOrderedSteps(
}
function formatYaml(value: unknown): string {
return stringify(value).trimEnd();
return stringify(value, { aliasDuplicateObjects: false }).trimEnd();
}
function formatCompactStep(index: number, item: OrderedStepItem, outputYaml: string): string {
+1 -1
View File
@@ -7,6 +7,6 @@ export function formatOutput(data: unknown, format: OutputFormat): string {
case "json":
return JSON.stringify(data);
case "yaml":
return stringify(data).trimEnd();
return stringify(data, { aliasDuplicateObjects: false }).trimEnd();
}
}
+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[] = [];