/** * System-prompt context injection primitives (issues #87, #92). * * The `before_agent_start` hook augments the chained system prompt with a * visible wiki-status footer. This module isolates that append so it can be * unit-tested for idempotency — a turn that aborts (network error / ESC) and is * retried can carry the prior injection forward, and a naive append stacks the * footer 2x, 3x, ... * * It also owns the cache-safety split (issue #92): VOLATILE per-turn context * (recall results, one-time topic-inference directive) must never enter the * system prompt — that is the provider's primary cache prefix, so per-turn * variation there forces a full cache miss every turn. Volatile content is * routed into a tail conversation message instead. `buildAgentStartInjection` * is the single, pure decision point for that split. */ /** The always-injected wiki-status footer (sans surrounding whitespace). */ export const WIKI_STATUS_BLOCK = "LLM Wiki active — use wiki_recall for deeper search, wiki_observe to record observations, wiki_retro to save insights."; /** * Append the wiki-status footer to a system prompt — idempotently (issue #87). * * Strips any already-present footer (with its leading blank line) before * appending exactly one. This makes the injection safe across aborted/retried * agent starts that carry the prior injection forward in the chained system * prompt, so the footer never stacks (2x, 3x, ...). * See test/inject-idempotent.test.ts. */ export function appendWikiStatus(systemPrompt: string): string { const base = systemPrompt.split(`\n\n${WIKI_STATUS_BLOCK}`).join(""); return `${base}\n\n${WIKI_STATUS_BLOCK}`; } /** Normalize upstream Pi and OMP system-prompt representations. */ export function normalizeSystemPrompt( systemPrompt: string | readonly string[] | null | undefined, ): string { if (Array.isArray(systemPrompt)) return systemPrompt.join("\n\n"); return typeof systemPrompt === "string" ? systemPrompt : ""; } /** customType of the hidden tail message carrying volatile per-turn context. */ export const WIKI_RECALL_MESSAGE_TYPE = "wiki-recall-context"; /** A hidden, LLM-visible tail message carrying volatile per-turn wiki context. */ export interface WikiRecallMessage { customType: typeof WIKI_RECALL_MESSAGE_TYPE; content: string; display: false; } /** Result of the cache-safe split for one `before_agent_start` turn. */ export interface AgentStartInjection { /** * The system prompt to return. ONLY ever the base plus the static footer — * never any volatile content — so it is byte-identical turn over turn and * the provider's prompt cache stays warm (issue #92). */ systemPrompt: string; /** Volatile blocks, delivered as a tail message. Omitted when empty. */ message?: WikiRecallMessage; } /** * Split a turn's injection into a cache-stable system prompt and a volatile * tail message (issue #92). * * - `systemPrompt` is always `appendWikiStatus(baseSystemPrompt)` — the static * footer only. It carries NONE of `dynamicBlocks`, so it does not vary with * recall results and never breaks the provider cache prefix. * - `dynamicBlocks` (recall context, topic-inference directive, ...) are * trimmed, emptied entries dropped, and joined with a blank line into the * message body. When nothing survives, no message is emitted. * * Pure and side-effect free — see test/agent-start-injection.test.ts. */ export function buildAgentStartInjection( baseSystemPrompt: string | readonly string[] | null | undefined, dynamicBlocks: Array, ): AgentStartInjection { const systemPrompt = appendWikiStatus(normalizeSystemPrompt(baseSystemPrompt)); const content = dynamicBlocks .map((b) => b?.trim()) .filter((b): b is string => Boolean(b)) .join("\n\n"); if (!content) return { systemPrompt }; return { systemPrompt, message: { customType: WIKI_RECALL_MESSAGE_TYPE, content, display: false }, }; }