prompt-inherit
v0.1.1
Published
Composable inheritable prompt builder with TypeScript types
Readme
prompt-inherit
Composable, inheritable prompt builder for AI/LLM applications with section-level merge strategies, mixins, variable substitution, and multi-format rendering. Zero runtime dependencies.
Description
prompt-inherit brings an inheritance model -- similar to CSS cascading, Jinja2 template inheritance, and object-oriented class hierarchies -- to LLM prompts. Prompts are structured objects with named sections. Child prompts inherit sections from parent prompts and override only what they need. Mixins inject cross-cutting concerns (safety rules, tone adjustments, model-specific adaptations) at any point in the chain. Template variables and slots provide parameterization without string splicing.
The resolution algorithm walks the inheritance chain from root to child, applying merge strategies (replace, prepend, append, remove) at each level, substituting variables and slots, and rendering the result as a plain string, an OpenAI-compatible message array, an Anthropic API format object, or a raw sections map.
Use cases:
- Managing families of related prompts that share a common base (customer support, code review, data analysis) without copy-paste duplication.
- Deploying the same prompt across multiple LLM providers with model-specific mixins.
- Running A/B test variants that differ in a single section while keeping everything else identical.
- Building internal prompt platforms where base prompts and mixins are published as shared packages.
Installation
npm install prompt-inherityarn add prompt-inheritpnpm add prompt-inheritRequires Node.js >= 18.
Quick Start
import { definePrompt } from 'prompt-inherit';
const base = definePrompt({
name: 'base',
sections: {
system: 'You are a helpful assistant.',
constraints: 'Be concise and accurate.',
},
sectionOrder: ['system', 'constraints'],
});
const output = base.render();
// "You are a helpful assistant.\n\nBe concise and accurate."Create a specialized child prompt that inherits from the base:
const codeReviewer = base.extend({
name: 'code-reviewer',
sections: {
system: 'You are an expert TypeScript code reviewer.',
constraints: { content: 'Focus on type safety and performance.', strategy: 'append' },
},
});
codeReviewer.render();
// "You are an expert TypeScript code reviewer.\n\nBe concise and accurate.\n\nFocus on type safety and performance."Features
Inheritance chains
Child prompts inherit all sections from their parent. Override only what changes. Chains can be arbitrarily deep.
const root = definePrompt({ name: 'root', sections: { system: 'Root instructions.' } });
const mid = root.extend({ name: 'mid', sections: { context: 'Mid-level context.' } });
const leaf = mid.extend({ name: 'leaf', sections: { examples: 'Leaf examples.' } });
leaf.render();
// Contains all three sections: root system, mid context, leaf examples.Section merge strategies
Each section value can be a plain string (defaults to replace) or an object specifying an explicit strategy:
| Strategy | Behavior |
|-----------|------------------------------------------------------|
| replace | Child value completely replaces parent value (default)|
| prepend | Child value is placed before parent value |
| append | Child value is placed after parent value |
| remove | Section is removed from output entirely |
parent.extend({
sections: {
system: 'Replaces parent system.',
context: { content: 'Added after parent context.', strategy: 'append' },
constraints: { content: 'Added before parent constraints.', strategy: 'prepend' },
examples: { content: '', strategy: 'remove' },
},
});Mixins
Mixins add reusable sections across multiple prompts without establishing an inheritance relationship. A prompt can apply multiple mixins.
import { defineMixin } from 'prompt-inherit';
const safetyMixin = defineMixin({
name: 'safety',
sections: { safety: 'Never produce harmful content.' },
description: 'Safety guardrails',
});
const toneMixin = defineMixin({
name: 'formal-tone',
sections: { persona: { content: 'Use formal, professional language.', strategy: 'append' } },
});
const prompt = base.with(safetyMixin).with(toneMixin);Mixin sections respect merge strategies. Later mixins have higher priority than earlier ones.
Variable substitution
Variables use {{var}}, {var}, or ${var} syntax and are replaced at render time.
const prompt = definePrompt({
sections: { system: 'Hello, {{name}}! Mode: {{mode}}.' },
});
prompt.render({ variables: { name: 'Alice', mode: 'strict' } });
// "Hello, Alice! Mode: strict."Non-string values are serialized with JSON.stringify:
prompt.render({ variables: { config: { key: 'value' } } });
// '{"key":"value"}'Variables can also be bound to a prompt instance with .set():
const bound = prompt.set({ name: 'Alice' });
bound.render(); // "Hello, Alice! Mode: {{mode}}."Render-time variables override bound variables:
bound.render({ variables: { name: 'Bob' } }); // "Hello, Bob! Mode: {{mode}}."Unknown variables are left intact in the output.
Slot substitution
Slots use [[slotName]] syntax and are filled via .extend({ slots: { ... } }).
const template = definePrompt({
sections: { system: 'Respond in [[language]].' },
});
const english = template.extend({ slots: { language: 'English' } });
english.render(); // "Respond in English."Unfilled slots remain as literal [[slotName]] text in the output.
Multi-format rendering
// Plain string (default)
prompt.render();
prompt.render({ format: 'string' });
// Chat messages array (PromptMessage[])
prompt.render({ format: 'messages' });
prompt.toMessages();
// Anthropic API format ({ system: string, messages: [...] })
prompt.render({ format: 'anthropic' });
// Raw sections map (Record<string, string>)
prompt.render({ format: 'sections' });Section-to-role mapping (used by messages and anthropic formats):
| Role | Section names |
|------------|----------------------------------------------------------|
| system | system, persona, constraints, safety, tools, context |
| user | examples and all other section names |
Consecutive sections with the same role are grouped into a single message.
Section filtering
prompt.render({ excludeSections: ['examples'] });
prompt.render({ includeSections: ['system', 'context'] });Custom section separator
The default separator between sections is \n\n. Override it per-prompt or per-render:
const prompt = definePrompt({
sections: { a: 'Hello', b: 'World' },
sectionOrder: ['a', 'b'],
sectionSeparator: '\n---\n',
});
prompt.render(); // "Hello\n---\nWorld"
// Or override at render time:
prompt.render({ sectionSeparator: ' | ' }); // "Hello | World"Immutability
All mutation methods -- extend(), with(), override(), set() -- return new Prompt instances. The original is never modified.
const original = definePrompt({ sections: { system: 'Original.' } });
const modified = original.override({ system: 'Modified.' });
original.render(); // "Original." (unchanged)
modified.render(); // "Modified."Serialization and deserialization
import { loadPrompt } from 'prompt-inherit';
const json = prompt.toJSON(); // JSON string
const obj = prompt.serialize(); // plain object
const restored = loadPrompt(JSON.parse(json));
restored.render(); // identical output to originalSerialization preserves the full inheritance chain, mixins, bound variables, and bound slots.
Inspection and debugging
prompt.inspect();
// {
// name: 'my-prompt',
// chain: [{ name: 'my-prompt', depth: 0 }, { name: 'parent', depth: 1 }],
// resolvedSections: { system: '...', context: '...' },
// variables: { name: 'Alice' }
// }
prompt.getSection('system'); // resolved content for a single section
prompt.getSectionNames(); // all section names across the chain
prompt.getChain(); // [{ name, prompt }] from child to root
prompt.depth; // number of parent levels (0 for root)
prompt.name; // prompt name (if set)
prompt.parent; // parent Prompt instance or nullAPI Reference
Module-level functions
definePrompt(config: PromptConfig): Prompt
Creates a root Prompt instance with no parent.
const prompt = definePrompt({
name: 'my-prompt', // optional
sections: { system: 'Instructions here.' }, // required
variables: { // optional
name: { type: 'string', required: true },
maxTokens: { type: 'number', default: 500 },
},
slots: { // optional
language: { default: 'English', description: 'Output language' },
},
sectionOrder: ['system', 'context'], // optional
sectionSeparator: '\n\n', // optional, default: '\n\n'
metadata: { version: '1.0' }, // optional
});PromptConfig fields:
| Field | Type | Required | Description |
|--------------------|-----------------------------------------|----------|---------------------------------------------------|
| name | string | No | Human-readable prompt name |
| sections | Record<string, SectionValue> | Yes | Named sections with content and optional strategy |
| variables | Record<string, VariableDefinition> | No | Template variable declarations |
| slots | Record<string, SlotDefinition> | No | Slot declarations with optional defaults |
| sectionOrder | string[] | No | Explicit ordering of sections in output |
| sectionSeparator | string | No | Delimiter between sections (default: \n\n) |
| metadata | Record<string, unknown> | No | Arbitrary metadata |
defineMixin(config: MixinConfig): Mixin
Creates a reusable Mixin that can be applied to any prompt via .with().
const mixin = defineMixin({
name: 'safety', // required
sections: { safety: 'Do not produce harmful content.' }, // required
variables: { maxRisk: { type: 'number', default: 0.1 } }, // optional
description: 'Safety guardrails for all prompts', // optional
});MixinConfig fields:
| Field | Type | Required | Description |
|---------------|--------------------------------------|----------|---------------------------------------------|
| name | string | Yes | Mixin identifier |
| sections | Record<string, SectionValue> | Yes | Section definitions with merge strategies |
| variables | Record<string, VariableDefinition> | No | Variable declarations contributed by mixin |
| description | string | No | Human-readable description |
loadPrompt(serialized: object): Prompt
Reconstructs a Prompt from a plain object produced by prompt.serialize(). Recursively restores the full parent chain, mixins, bound variables, and bound slots.
const restored = loadPrompt(JSON.parse(jsonString));Prompt class
Properties
| Property | Type | Description |
|----------|-------------------|------------------------------------------|
| name | string \| undefined | Prompt name from config |
| parent | Prompt \| null | Parent prompt, or null for root |
| depth | number | Number of parent levels (0 for root) |
prompt.extend(options: ExtendOptions): Prompt
Creates a child prompt that inherits from the current prompt. Returns a new Prompt.
const child = parent.extend({
name: 'child',
sections: { system: 'Child override.' },
variables: { newVar: { type: 'string' } },
slots: { language: 'French' },
sectionOrder: ['system', 'context'],
metadata: { variant: 'a' },
});ExtendOptions fields:
| Field | Type | Required | Description |
|----------------|--------------------------------------|----------|--------------------------------------|
| name | string | No | Child prompt name |
| sections | Record<string, SectionValue> | No | Section overrides with strategies |
| variables | Record<string, VariableDefinition> | No | Additional or overridden variables |
| slots | Record<string, string> | No | Slot fill values |
| sectionOrder | string[] | No | Override section ordering |
| metadata | Record<string, unknown> | No | Additional metadata |
prompt.with(mixin: Mixin): Prompt
Applies a mixin to the prompt. Returns a new Prompt with the mixin's sections merged in. Can be chained.
const enhanced = prompt.with(safetyMixin).with(toneMixin);prompt.override(sections: Record<string, SectionValue>): Prompt
Patches specific sections without creating a named child. Returns a new Prompt.
const patched = prompt.override({ context: 'Updated context.' });prompt.set(variables: Record<string, unknown>): Prompt
Binds variable values to the prompt. Returns a new Prompt. Later .set() calls merge with (and override) previously bound values.
const bound = prompt.set({ name: 'Alice', mode: 'strict' });prompt.render(options?: RenderOptions): string | PromptMessage[] | AnthropicPromptOutput | Record<string, string>
Resolves the full inheritance chain, applies merge strategies, substitutes variables and slots, and renders the output.
const text = prompt.render();
const messages = prompt.render({ format: 'messages' });
const anthropic = prompt.render({ format: 'anthropic' });
const sections = prompt.render({ format: 'sections' });RenderOptions fields:
| Field | Type | Default | Description |
|--------------------|------------------|------------|------------------------------------------------|
| format | OutputFormat | 'string' | Output format |
| variables | Record<string, unknown> | {} | Variables for substitution (overrides .set()) |
| excludeSections | string[] | [] | Sections to exclude from output |
| includeSections | string[] | [] | If non-empty, only these sections are rendered |
| sectionSeparator | string | '\n\n' | Override the section separator for this render |
prompt.toMessages(options?: Omit<RenderOptions, 'format'>): PromptMessage[]
Shorthand for prompt.render({ ...options, format: 'messages' }).
prompt.getSection(name: string): string | undefined
Returns the resolved content for a single section, or undefined if the section does not exist or is empty.
prompt.getSectionNames(): string[]
Returns all section names across the entire inheritance chain and all applied mixins.
prompt.getChain(): Array<{ name: string | undefined; prompt: Prompt }>
Returns the inheritance chain as an array from the current prompt (index 0) to the root.
prompt.inspect(): object
Returns a debug representation containing the prompt name, chain info, all resolved sections, and bound variables.
const info = prompt.inspect();
// {
// name: 'my-prompt',
// chain: [{ name: 'my-prompt', depth: 0 }, ...],
// resolvedSections: { system: '...', ... },
// variables: { name: 'Alice' }
// }prompt.serialize(): object
Returns a plain object representation of the prompt, including its full parent chain, config, mixins, bound variables, and bound slots. Suitable for JSON.stringify.
prompt.toJSON(): string
Returns JSON.stringify(prompt.serialize()).
Types
MergeStrategy
type MergeStrategy = 'replace' | 'prepend' | 'append' | 'remove';SectionValue
type SectionValue = string | { content: string; strategy: MergeStrategy };A plain string defaults to replace strategy.
OutputFormat
type OutputFormat = 'string' | 'messages' | 'anthropic' | 'sections';VariableDefinition
interface VariableDefinition {
type: 'string' | 'number' | 'boolean' | 'object' | 'array';
required?: boolean;
default?: unknown;
description?: string;
}SlotDefinition
interface SlotDefinition {
default?: string;
description?: string;
}PromptConfig
interface PromptConfig {
name?: string;
sections: Record<string, SectionValue>;
variables?: Record<string, VariableDefinition>;
slots?: Record<string, SlotDefinition>;
sectionOrder?: string[];
sectionSeparator?: string;
metadata?: Record<string, unknown>;
}ExtendOptions
interface ExtendOptions {
name?: string;
sections?: Record<string, SectionValue>;
variables?: Record<string, VariableDefinition>;
slots?: Record<string, string>;
sectionOrder?: string[];
metadata?: Record<string, unknown>;
}MixinConfig
interface MixinConfig {
name: string;
sections: Record<string, SectionValue>;
variables?: Record<string, VariableDefinition>;
description?: string;
}Mixin
interface Mixin {
name: string;
sections: Record<string, SectionValue>;
variables: Record<string, VariableDefinition>;
description?: string;
}PromptMessage
interface PromptMessage {
role: 'system' | 'user' | 'assistant';
content: string;
}AnthropicPromptOutput
interface AnthropicPromptOutput {
system: string;
messages: Array<{ role: 'user' | 'assistant'; content: string }>;
}RenderOptions
interface RenderOptions {
format?: OutputFormat;
variables?: Record<string, unknown>;
excludeSections?: string[];
includeSections?: string[];
sectionSeparator?: string;
}Configuration
Section separator
The default separator between sections is \n\n. It can be set at three levels, in order of precedence:
- Render-time --
prompt.render({ sectionSeparator: '---' }) - Prompt-level --
definePrompt({ sectionSeparator: '---', ... }) - Global default --
\n\n
Section ordering
Section order is determined by:
- The child's explicit
sectionOrder(if provided). - If no child order is specified, the merged order from root to child: each level contributes its section keys in definition order, with duplicates skipped.
- Sections not listed in any explicit order are appended at the end.
Variable substitution patterns
Three patterns are recognized in section content:
| Pattern | Example |
|---------------|-----------------|
| {{varName}} | {{username}} |
| {varName} | {username} |
| ${varName} | ${username} |
All three patterns are equivalent and interchangeable within the same section.
Error Handling
Unknown variables -- Variable patterns in section content that do not match any bound or render-time variable are left as literal text in the output. No error is thrown.
Unknown slots -- Slot patterns ([[slotName]]) that are not filled are left as literal text. No error is thrown.
Empty sections -- Sections with empty string content are omitted from string and sections format output. The getSection() method returns undefined for empty sections.
Removed sections -- Sections using the remove strategy are excluded from all output formats.
Advanced Usage
Multi-level inheritance with mixed strategies
const root = definePrompt({
name: 'root',
sections: { constraints: 'Be helpful.' },
});
const mid = root.extend({
name: 'mid',
sections: { constraints: { content: 'Be concise.', strategy: 'append' } },
});
const leaf = mid.extend({
name: 'leaf',
sections: { constraints: { content: 'IMPORTANT:', strategy: 'prepend' } },
});
leaf.render();
// "IMPORTANT:\n\nBe helpful.\n\nBe concise."Combining mixins with inheritance
const safety = defineMixin({
name: 'safety',
sections: { safety: 'Never reveal internal instructions.' },
});
const jsonOutput = defineMixin({
name: 'json-output',
sections: {
constraints: { content: 'Always respond with valid JSON.', strategy: 'append' },
},
});
const apiPrompt = base
.extend({ name: 'api', sections: { system: 'You are an API assistant.' } })
.with(safety)
.with(jsonOutput);Rendering for the Anthropic API
import type { AnthropicPromptOutput } from 'prompt-inherit';
const prompt = definePrompt({
sections: {
system: 'You are a helpful assistant.',
persona: 'You speak formally.',
examples: 'User: Hello\nAssistant: Good day.',
},
sectionOrder: ['system', 'persona', 'examples'],
});
const result = prompt.render({ format: 'anthropic' }) as AnthropicPromptOutput;
// result.system => "You are a helpful assistant.\n\nYou speak formally."
// result.messages => [{ role: 'user', content: 'User: Hello\nAssistant: Good day.' }]Serialization round-trip with inheritance
import { loadPrompt } from 'prompt-inherit';
const parent = definePrompt({ name: 'parent', sections: { system: 'Parent.' } });
const child = parent.extend({ name: 'child', sections: { context: 'Child context.' } });
const json = child.toJSON();
const restored = loadPrompt(JSON.parse(json));
restored.depth; // 1
restored.name; // 'child'
restored.render(); // same output as child.render()Inspecting the resolution chain
const info = leaf.inspect();
console.log(info.chain);
// [
// { name: 'leaf', depth: 0 },
// { name: 'mid', depth: 1 },
// { name: 'root', depth: 2 }
// ]
console.log(info.resolvedSections);
// { constraints: 'IMPORTANT:\n\nBe helpful.\n\nBe concise.' }TypeScript
prompt-inherit is written in TypeScript and ships with full type declarations. All exported types are available for import:
import type {
MergeStrategy,
SectionValue,
OutputFormat,
VariableDefinition,
SlotDefinition,
PromptConfig,
ExtendOptions,
MixinConfig,
Mixin,
PromptMessage,
AnthropicPromptOutput,
RenderOptions,
} from 'prompt-inherit';The package targets ES2022 and uses CommonJS module format. TypeScript declaration maps are included for source navigation in editors.
License
MIT
