feat(agent): add structured outputs and media archive support

This commit is contained in:
2026-04-10 19:01:04 +02:00
parent a1df097f9c
commit 9732022461
34 changed files with 3276 additions and 482 deletions

View File

@@ -0,0 +1,171 @@
import { promises as fs } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { AGENT_DEFINITIONS, type AgentDefinitionId } from "../lib/agent-definitions";
export const AGENT_PROMPT_SEGMENT_KEYS = [
"role",
"style-rules",
"decision-framework",
"channel-notes",
] as const;
export type AgentPromptSegmentKey = (typeof AGENT_PROMPT_SEGMENT_KEYS)[number];
export type AgentPromptSegments = Record<AgentPromptSegmentKey, string>;
type CompileOptions = {
sourcePath: string;
};
function normalizeSegmentBody(raw: string): string {
return raw
.replace(/\r\n/g, "\n")
.split("\n")
.map((line) => line.trimEnd())
.join("\n")
.trim();
}
function countMarkerOccurrences(markdown: string, marker: string): number {
let count = 0;
let currentIndex = 0;
while (currentIndex >= 0) {
currentIndex = markdown.indexOf(marker, currentIndex);
if (currentIndex < 0) {
break;
}
count += 1;
currentIndex += marker.length;
}
return count;
}
function extractSegment(markdown: string, key: AgentPromptSegmentKey, sourcePath: string): string {
const startMarker = `<!-- AGENT_PROMPT_SEGMENT:${key}:start -->`;
const endMarker = `<!-- AGENT_PROMPT_SEGMENT:${key}:end -->`;
const startCount = countMarkerOccurrences(markdown, startMarker);
const endCount = countMarkerOccurrences(markdown, endMarker);
if (startCount !== 1 || endCount !== 1) {
throw new Error(
`[agent-docs] Missing or duplicate markers for '${key}' in ${sourcePath}. Expected exactly one start and one end marker.`,
);
}
const startIndex = markdown.indexOf(startMarker);
const contentStart = startIndex + startMarker.length;
const endIndex = markdown.indexOf(endMarker, contentStart);
if (startIndex < 0 || endIndex < 0 || endIndex <= contentStart) {
throw new Error(`[agent-docs] Invalid marker placement for '${key}' in ${sourcePath}.`);
}
const segment = normalizeSegmentBody(markdown.slice(contentStart, endIndex));
if (segment === "") {
throw new Error(`[agent-docs] Empty segment '${key}' in ${sourcePath}.`);
}
return segment;
}
export function compileAgentDocSegmentsFromMarkdown(
markdown: string,
options: CompileOptions,
): AgentPromptSegments {
const compiled = {} as AgentPromptSegments;
for (const key of AGENT_PROMPT_SEGMENT_KEYS) {
compiled[key] = extractSegment(markdown, key, options.sourcePath);
}
return compiled;
}
export function compileAgentDocSegmentsFromSources(
sources: ReadonlyArray<{ agentId: AgentDefinitionId; markdown: string; sourcePath: string }>,
): Record<AgentDefinitionId, AgentPromptSegments> {
const compiled = {} as Record<AgentDefinitionId, AgentPromptSegments>;
for (const source of sources) {
compiled[source.agentId] = compileAgentDocSegmentsFromMarkdown(source.markdown, {
sourcePath: source.sourcePath,
});
}
return compiled;
}
function escapeForTsString(raw: string): string {
return raw.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
}
export function renderCompiledAgentDocSegmentsModule(
compiled: Record<AgentDefinitionId, AgentPromptSegments>,
): string {
const lines: string[] = [
"// This file is generated by scripts/compile-agent-docs.ts",
"// Do not edit manually.",
"",
'import type { AgentDefinitionId } from "@/lib/agent-definitions";',
"",
"export type AgentDocPromptSegmentKey =",
' | "role"',
' | "style-rules"',
' | "decision-framework"',
' | "channel-notes";',
"",
"export type AgentDocPromptSegments = Record<AgentDocPromptSegmentKey, string>;",
"",
"export const AGENT_DOC_SEGMENTS: Record<AgentDefinitionId, AgentDocPromptSegments> = {",
];
const agentIds = Object.keys(compiled).sort() as AgentDefinitionId[];
for (const agentId of agentIds) {
lines.push(` \"${agentId}\": {`);
for (const key of AGENT_PROMPT_SEGMENT_KEYS) {
lines.push(` \"${key}\": \`${escapeForTsString(compiled[agentId][key])}\`,`);
}
lines.push(" },");
}
lines.push("};", "");
return lines.join("\n");
}
export async function compileAgentDocsToGeneratedModule(projectRoot: string): Promise<string> {
const sources: Array<{ agentId: AgentDefinitionId; markdown: string; sourcePath: string }> = [];
for (const definition of AGENT_DEFINITIONS) {
const sourcePath = definition.docs.markdownPath;
const absoluteSourcePath = path.resolve(projectRoot, sourcePath);
const markdown = await fs.readFile(absoluteSourcePath, "utf8");
sources.push({ agentId: definition.id, markdown, sourcePath });
}
const compiled = compileAgentDocSegmentsFromSources(sources);
const output = renderCompiledAgentDocSegmentsModule(compiled);
const outputPath = path.resolve(projectRoot, "lib/generated/agent-doc-segments.ts");
await fs.mkdir(path.dirname(outputPath), { recursive: true });
await fs.writeFile(outputPath, output, "utf8");
return outputPath;
}
const thisFilePath = fileURLToPath(import.meta.url);
const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : "";
if (invokedPath === thisFilePath) {
compileAgentDocsToGeneratedModule(process.cwd())
.then((outputPath) => {
process.stdout.write(`[agent-docs] Wrote ${outputPath}\n`);
})
.catch((error) => {
const message = error instanceof Error ? error.message : String(error);
process.stderr.write(`${message}\n`);
process.exitCode = 1;
});
}