/** * Prompt Section Registry — manages ordered prompt sections with sources, * budgets, and trim states. * * The registry collects prompt sections from multiple data sources (kernel * snapshots, facts, evidence, gate decisions, etc.), enforces per-section * and global budgets, and produces a final PromptContract. * * Sections are ordered by priority (lower number = higher priority = trimmed * last). Sections marked `always: true` are never trimmed regardless of * budget pressure. */ import type { CompileContext, PromptContract, PromptSection, PromptSectionSource, TrimLogEntry, } from "./prompt-contract.ts"; import { assembleContract, enforceBudgets } from "./prompt-contract.ts"; import type { KernelSnapshot, EvidenceSummary } from "../kernel/snapshots.ts"; import type { GateResult, KdFact, KdToolResultContract, KdWorkingSet } from "../types.ts"; // ── PromptSectionRegistry ──────────────────────────────────────────── /** * Registry that collects, orders, and trims prompt sections. * * Usage: * ```ts * const registry = new PromptSectionRegistry(); * registry.register({ id: "control", title: "Control Frame", ... }); * registry.register({ id: "facts", title: "Facts", ... }); * const contract = registry.compile(context); * ``` */ export class PromptSectionRegistry { private sections: PromptSection[] = []; /** * Register a prompt section. * * If a section with the same `id` already exists, it is replaced. * The replacement preserves insertion order (the new section takes the * position of the old one). */ register(section: PromptSection): void { const existingIndex = this.sections.findIndex((s) => s.id === section.id); if (existingIndex >= 0) { this.sections[existingIndex] = section; } else { this.sections.push(section); } } /** * Compile all registered sections into a PromptContract. * * Steps: * 1. Filter out sections with `include: false` or empty content (unless `always: true`) * 2. Sort by priority (ascending — lower = higher priority) * 3. Enforce per-section budgets, then global budget * 4. Assemble the final contract */ compile(context: CompileContext): PromptContract { // Filter: include !== false, and either always or has meaningful content const eligible = this.sections.filter( (section) => section.include !== false && (section.always || hasMeaningfulContent(section.content)), ); // Sort by priority ascending (lower number = higher priority = trimmed last) const sorted = [...eligible].sort((a, b) => a.priority - b.priority); // Enforce budgets const { sections: trimmed, trimLog } = enforceBudgets(sorted, context.totalBudget); // Assemble contract const contract = assembleContract(trimmed, context.totalBudget); return { ...contract, trimLog, }; } /** * Return all sections marked as uncuttable (`always: true`). */ getUntrimmables(): PromptSection[] { return this.sections.filter((s) => s.always); } /** * Return the current count of registered sections. */ get size(): number { return this.sections.length; } /** * Clear all registered sections. */ clear(): void { this.sections = []; } } // ── Section factory helpers ────────────────────────────────────────── /** * Create a prompt section from a kernel snapshot. * * Produces a compact status summary suitable for the "status" section * of the prompt contract. */ export function sectionFromKernelSnapshot(snapshot: KernelSnapshot): PromptSection { const content = [ `Run:${snapshot.runId}`, `状态:${snapshot.runStatus}`, `阶段/模式:${snapshot.phase}/${snapshot.mode}`, snapshot.product ? `产品:${snapshot.product}` : undefined, `事实:${snapshot.factsSummary.current} current / ${snapshot.factsSummary.total} total`, `未回答问题:${snapshot.openQuestionsCount}`, `门禁:${snapshot.gateResult.passed ? "通过" : `阻塞:${snapshot.gateResult.reason ?? "未说明原因"}`}`, `证据:${snapshot.evidenceSummary.total} entries (${snapshot.evidenceSummary.kinds.join(", ")})`, `生成时间:${snapshot.generatedAt}`, ] .filter(Boolean) .join("\n"); return { id: "kernel-snapshot", title: "Kernel Snapshot(运行状态快照)", content, source: "kernel-snapshot", priority: 10, always: true, include: true, traceId: `snapshot-${snapshot.runId}-${snapshot.generatedAt}`, }; } /** * Create a prompt section from gate decisions. */ export function sectionFromGateDecisions(gates: GateResult[], runId: string): PromptSection { if (gates.length === 0) { return { id: "gate-decisions", title: "门禁决策记录", content: "无门禁决策。", source: "gate-decision", priority: 20, always: false, include: false, traceId: `gates-${runId}-empty`, }; } const content = gates .map((gate, i) => { const status = gate.passed ? "通过" : "阻塞"; const reason = gate.reason ? `:${gate.reason}` : ""; return `- [${i + 1}] ${status}${reason} (${gate.checkedAt})`; }) .join("\n"); return { id: "gate-decisions", title: "门禁决策记录", content, source: "gate-decision", priority: 20, always: false, include: true, traceId: `gates-${runId}`, }; } /** * Create a prompt section from facts. */ export function sectionFromFacts(facts: KdFact[], runId: string): PromptSection { const currentFacts = facts.filter((f) => f.status === "current"); if (currentFacts.length === 0) { return { id: "facts", title: "结构化事实", content: "无当前事实。", source: "facts", priority: 30, always: false, include: false, traceId: `facts-${runId}-empty`, }; } const content = currentFacts .map((fact) => `- **${fact.label}**(${fact.key}):${fact.value}`) .join("\n"); return { id: "facts", title: "结构化事实", content, source: "facts", priority: 30, always: false, include: true, traceId: `facts-${runId}`, }; } /** * Create a prompt section from evidence summary. */ export function sectionFromEvidence(evidence: EvidenceSummary, runId: string): PromptSection { if (evidence.total === 0) { return { id: "evidence", title: "证据摘要", content: "无证据。", source: "evidence", priority: 40, always: false, include: false, traceId: `evidence-${runId}-empty`, }; } const content = [ `证据总数:${evidence.total}`, `类型:${evidence.kinds.join(", ")}`, `来源信任分布:${Object.entries(evidence.sourceTrust).map(([k, v]) => `${k}=${v}`).join(", ")}`, ].join("\n"); return { id: "evidence", title: "证据摘要", content, source: "evidence", priority: 40, always: false, include: true, traceId: `evidence-${runId}`, }; } /** * Create a prompt section from tool results. */ export function sectionFromToolResults(results: KdToolResultContract[], runId: string): PromptSection { if (results.length === 0) { return { id: "tool-results", title: "工具结果", content: "无工具结果。", source: "tool-result", priority: 50, always: false, include: false, traceId: `tools-${runId}-empty`, }; } const content = results .slice(-14) .map((r) => `- [${r.status}] ${r.toolName}(${r.kind}):${r.summary}`) .join("\n"); return { id: "tool-results", title: "工具结果", content, source: "tool-result", priority: 50, always: false, include: true, traceId: `tools-${runId}`, }; } /** * Create a prompt section from working set. */ export function sectionFromWorkingSet(workingSet: KdWorkingSet | undefined, runId: string): PromptSection { if (!workingSet) { return { id: "working-set", title: "工作集", content: "无工作集。", source: "working-set", priority: 60, always: false, include: false, traceId: `ws-${runId}-empty`, }; } const content = [ `焦点:${workingSet.focus}`, workingSet.activeGap ? `活跃缺口:${workingSet.activeGap}` : undefined, workingSet.currentAction ? `当前动作:${workingSet.currentAction}` : undefined, workingSet.blockedBy ? `阻塞于:${workingSet.blockedBy}` : undefined, `近期文件:${workingSet.recentFiles.map((f) => f.path).join(", ") || "无"}`, `禁止漂移:${workingSet.forbiddenDrift.join(", ") || "无"}`, ] .filter(Boolean) .join("\n"); return { id: "working-set", title: "工作集", content, source: "working-set", priority: 60, always: false, include: true, traceId: `ws-${runId}`, }; } /** * Create a prompt section from user input. */ export function sectionFromUserInput(userInput: string, runId: string): PromptSection { return { id: "user-input", title: "用户本轮输入", content: userInput, source: "user-input", priority: 5, always: true, include: true, traceId: `input-${runId}-${Date.now()}`, }; } // ── Internal helpers ───────────────────────────────────────────────── /** * Check whether content is meaningful (not empty, not a placeholder). */ function hasMeaningfulContent(content: string): boolean { const trimmed = content.trim(); return Boolean(trimmed && trimmed !== "无。" && trimmed !== "无。" && !/^无[^。]*。$/.test(trimmed)); }