r the current runtime state. It does not store history; history resides in version control. The registry provides the lookup mechanism and integrity verification.
Code Implementation
import { createHash } from 'crypto';
// --- Types ---
interface PromptArtifact {
readonly id: string;
readonly version: string;
readonly content: string;
readonly contentHash: string;
readonly registeredAt: number;
}
interface TemplateArtifact {
readonly id: string;
readonly version: string;
readonly template: string;
readonly templateHash: string;
}
// --- Core Registry ---
class PromptRegistry {
private artifacts = new Map<string, PromptArtifact>();
register(id: string, version: string, content: string): PromptArtifact {
const hash = computeSha256Prefix(content);
const artifact: PromptArtifact = {
id,
version,
content,
contentHash: hash,
registeredAt: Date.now(),
};
this.artifacts.set(id, artifact);
return artifact;
}
resolve(id: string): PromptArtifact {
const artifact = this.artifacts.get(id);
if (!artifact) {
throw new Error(`Prompt artifact '${id}' is not registered.`);
}
return artifact;
}
verifyIntegrity(id: string, expectedHash: string): boolean {
const artifact = this.resolve(id);
return artifact.contentHash === expectedHash;
}
}
// --- Template Support ---
class TemplateManager {
private templates = new Map<string, TemplateArtifact>();
registerTemplate(id: string, version: string, template: string): TemplateArtifact {
const artifact: TemplateArtifact = {
id,
version,
template,
templateHash: computeSha256Prefix(template),
};
this.templates.set(id, artifact);
return artifact;
}
render(id: string, variables: Record<string, string>): { text: string; renderHash: string } {
const artifact = this.templates.get(id);
if (!artifact) throw new Error(`Template '${id}' not found.`);
const renderedText = artifact.template.replace(/\{(\w+)\}/g, (_, key) => variables[key] ?? '');
const renderHash = computeSha256Prefix(renderedText);
return { text: renderedText, renderHash };
}
}
// --- Utilities ---
function computeSha256Prefix(input: string): string {
return createHash('sha256').update(input, 'utf8').digest('hex').slice(0, 8);
}
// --- Usage Example ---
const registry = new PromptRegistry();
const templates = new TemplateManager();
// Register a static prompt
const supportPrompt = registry.register(
'customer_support_v1',
'2.1.0',
'You are a support agent. Focus on resolution. Escalate if stuck.'
);
// Register a template
const emailTemplate = templates.registerTemplate(
'follow_up_email',
'1.0.0',
'Hi {name}, regarding your order {order_id}...'
);
// Simulate LLM call with provenance
async function handleUserQuery(userId: string, query: string) {
const prompt = registry.resolve('customer_support_v1');
// Render template if needed
const { text: emailBody, renderHash } = templates.render('follow_up_email', {
name: 'Alice',
order_id: 'ORD-992',
});
// Construct payload
const response = await callModel({
system: prompt.content,
user: query,
metadata: {
promptId: prompt.id,
promptVersion: prompt.version,
promptHash: prompt.contentHash,
templateRenderHash: renderHash,
},
});
// Log with provenance
logger.info('LLM Response', {
responseId: response.id,
tokens: response.usage,
...response.metadata, // Provenance travels with the log
});
return response.text;
}
Architecture Decisions
- Explicit Versioning: The registry requires explicit version strings. Automatic bumping is avoided because semantic meaning (e.g.,
v2-hotfix, v3-experiment) is more valuable than sequential integers.
- Hash Truncation: Using 8 hex characters balances log size with collision resistance. For extremely high-volume systems, increasing to 12 characters is recommended.
- Separation of Template and Render Hashes: Templates must be versioned independently of their rendered instances.
templateHash identifies the template structure, while renderHash identifies the specific input combination. This allows you to track how different variable values affect outputs without losing template lineage.
- No History Storage: The registry holds only the active artifact. Storing history in the registry creates memory bloat and duplicates version control. Git remains the source of truth for past versions.
Pitfall Guide
1. Hashing Rendered Output Instead of Template
- Mistake: Developers hash the filled template string and use that as the version identifier.
- Impact: The hash changes with every variable substitution, making it impossible to group logs by template version.
- Fix: Maintain a
templateHash for the raw template string and a separate renderHash for the instance. Log both.
2. Implicit Version Drift
- Mistake: Updating the prompt content without changing the version string.
- Impact: Logs show the same version, but the content hash differs, causing confusion.
- Fix: Enforce a policy where content changes require a version update. Use the
verifyIntegrity check at startup to catch mismatches between expected and actual hashes.
3. Storing Prompt History in the Registry
- Mistake: Implementing a registry that keeps all past versions in memory.
- Impact: Memory leaks and unnecessary complexity. The registry should be ephemeral per process.
- Fix: Treat the registry as a runtime cache. Rely on Git tags or deployment artifacts for historical retrieval.
4. Ignoring Hash Collisions in Analytics
- Mistake: Assuming 8-character hashes are unique across all time and tenants.
- Impact: In multi-tenant or massive-scale systems, collisions may occur, leading to misattribution.
- Fix: For global uniqueness, include the
promptId alongside the hash in queries. For critical audits, use the full SHA-256 hash.
5. Hardcoding Prompts in Business Logic
- Mistake: Embedding prompt strings directly inside service functions.
- Impact: Prompts cannot be registered, hashed, or versioned.
- Fix: Externalize prompts. Load them from files or environment variables and register them during application initialization.
6. Missing Hash in Log Aggregation
- Mistake: Logging the version string but omitting the content hash.
- Impact: You cannot detect content drift if the version string is reused or mislabeled.
- Fix: Always include
contentHash in log payloads. The hash is the ground truth; the version is metadata.
7. Over-Engineering One-Off Scripts
- Mistake: Implementing a full registry for a script that runs once.
- Impact: Unnecessary complexity and boilerplate.
- Fix: Reserve the registry pattern for long-running services, agents, and systems where prompts evolve over time.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Production Agent Service | Hashed Registry with Integrity Checks | Ensures auditability and rollback safety. | Low CPU overhead; high operational value. |
| A/B Testing Prompts | Multi-Registry or Feature Flag Routing | Isolates cohorts and enables clean metric grouping. | Moderate complexity; high insight value. |
| One-Off Data Script | No Registry | Simplicity outweighs audit needs. | Zero overhead. |
| High-Volume Inference | Truncated Hash (8 chars) | Balances log size with collision resistance. | Minimal storage cost. |
| Regulatory Compliance | Full Hash + Immutable Logs | Meets strict audit requirements. | Higher storage cost; mandatory. |
Configuration Template
Use this TypeScript configuration to load prompts from the file system and register them with environment-driven versions.
// config/prompts.ts
import fs from 'fs';
import path from 'path';
import { PromptRegistry, TemplateManager } from './registry';
export function initializePrompts(registry: PromptRegistry, templates: TemplateManager) {
const promptDir = path.resolve(__dirname, '../prompts');
const envVersion = process.env.PROMPT_VERSION || 'dev';
// Load static prompts
const supportContent = fs.readFileSync(path.join(promptDir, 'support.txt'), 'utf8');
registry.register('support_agent', `${envVersion}-stable`, supportContent);
// Load templates
const emailTemplate = fs.readFileSync(path.join(promptDir, 'email-template.txt'), 'utf8');
templates.registerTemplate('order_email', `${envVersion}-v1`, emailTemplate);
// Verify integrity against deployment manifest
const expectedHash = process.env.EXPECTED_SUPPORT_HASH;
if (expectedHash && !registry.verifyIntegrity('support_agent', expectedHash)) {
throw new Error('Prompt integrity check failed. Deployment mismatch.');
}
}
Quick Start Guide
- Create Registry: Instantiate
PromptRegistry and TemplateManager in your app entry point.
- Register Prompts: Call
register for each prompt, providing a unique ID, version string, and content.
- Attach Metadata: In your LLM call wrapper, resolve the prompt artifact and attach
contentHash to the log context.
- Query Logs: Use your log viewer to filter by
promptHash. Verify that responses match the expected prompt version.
- Iterate: Update the version string and content when making changes. The new hash will appear in logs, allowing immediate comparison with previous versions.