feat: remove LLM extract fallback, require YAML frontmatter

Agent output must contain valid YAML frontmatter matching the role schema.
If frontmatter parsing fails, the step fails immediately with a clear error
instead of falling back to an LLM extraction that can fabricate values.

The extract module remains as a public API export but is no longer used
in the agent run loop.

Breaking change: agents that relied on LLM extraction to produce valid
output will now fail. They must output proper frontmatter.
This commit is contained in:
2026-05-22 08:58:01 +00:00
parent 2eb5ee0666
commit 99a2890be2
+7 -18
View File
@@ -3,10 +3,9 @@ import type { CasRef, StepNodePayload, ThreadId } from "@uncaged/workflow-protoc
import { config as loadDotenv } from "dotenv"; import { config as loadDotenv } from "dotenv";
import { buildOutputFormatInstruction } from "./build-output-format-instruction.js"; import { buildOutputFormatInstruction } from "./build-output-format-instruction.js";
import { buildContextWithMeta } from "./context.js"; import { buildContextWithMeta } from "./context.js";
import { extract } from "./extract.js";
import { tryFrontmatterFastPath } from "./frontmatter.js"; import { tryFrontmatterFastPath } from "./frontmatter.js";
import type { AgentStore } from "./storage.js"; import type { AgentStore } from "./storage.js";
import { getEnvPath, loadWorkflowConfig, resolveStorageRoot } from "./storage.js"; import { getEnvPath, resolveStorageRoot } from "./storage.js";
import type { AgentContext, AgentOptions, AgentRunResult } from "./types.js"; import type { AgentContext, AgentOptions, AgentRunResult } from "./types.js";
function fail(message: string): never { function fail(message: string): never {
@@ -73,24 +72,19 @@ async function runAgent(options: AgentOptions, ctx: AgentContext): Promise<Agent
async function extractOutput( async function extractOutput(
rawOutput: string, rawOutput: string,
outputSchema: CasRef, outputSchema: CasRef,
storageRoot: string,
ctx: Awaited<ReturnType<typeof buildContextWithMeta>>, ctx: Awaited<ReturnType<typeof buildContextWithMeta>>,
): Promise<CasRef> { ): Promise<CasRef> {
const fastPath = await runWithMessage("frontmatter fast path", () => const fastPath = await tryFrontmatterFastPath(rawOutput, outputSchema, ctx.meta.store);
tryFrontmatterFastPath(rawOutput, outputSchema, ctx.meta.store),
).catch(() => null);
if (fastPath !== null) { if (fastPath !== null) {
return fastPath.outputHash; return fastPath.outputHash;
} }
const config = await runWithMessage("failed to load config", () => fail(
loadWorkflowConfig(storageRoot), "Agent output does not contain valid YAML frontmatter matching the role schema.\n" +
"The agent must output a YAML frontmatter block (--- delimited) as the first thing in its response.\n" +
`Raw output (first 500 chars): ${rawOutput.slice(0, 500)}`,
); );
const extracted = await runWithMessage("extract failed", () =>
extract(rawOutput, outputSchema, config),
);
return extracted.hash;
} }
async function persistStep(options: { async function persistStep(options: {
@@ -136,12 +130,7 @@ export function createAgent(options: AgentOptions): () => Promise<void> {
} }
const agentResult = await runAgent(options, ctx); const agentResult = await runAgent(options, ctx);
const outputHash = await extractOutput( const outputHash = await extractOutput(agentResult.output, roleDef.frontmatter, ctx);
agentResult.output,
roleDef.frontmatter,
storageRoot,
ctx,
);
const stepHash = await persistStep({ const stepHash = await persistStep({
ctx, ctx,
outputHash, outputHash,