Skip to main content

Overview

The SystemPromptBuilder class is the core of PromptSmith. It implements a fluent API pattern that lets you incrementally build comprehensive system prompts by chaining method calls. Think of it as a recipe builder for AI agent behavior - you add ingredients (identity, capabilities, tools, constraints) and the builder generates a complete system prompt optimized for AI models.
Design Philosophy: The builder separates prompt generation (text for the AI model) from tool execution (runtime logic). Tools are registered with metadata only - actual execution happens in your application when the AI requests to use a tool.

Why Use a Builder?

Manually writing system prompts leads to several problems:
  • Inconsistent structure: Missing sections, poor organization
  • Token waste: Verbose formatting, redundant text
  • No validation: Duplicate tool names, conflicting constraints
  • Hard to maintain: Changes require editing long text blocks
  • No reusability: Can’t compose or extend prompts programmatically
The builder solves these by:
  1. Enforcing structure: Sections only appear when you add content
  2. Optimizing tokens: Choice of formats (markdown, TOON, compact)
  3. Validating config: Catches duplicate tools, missing sections, conflicts
  4. Enabling composition: Merge, extend, and conditional logic
  5. Type safety: Full TypeScript inference for tools and parameters

Core Workflow

1

Create Builder

Initialize a new builder instance using the factory function:
import { createPromptBuilder } from 'promptsmith';

const builder = createPromptBuilder();
2

Configure Agent

Chain method calls to define your agent’s behavior:
builder
  .withIdentity("You are a helpful coding assistant")
  .withCapabilities([
    "Explain code concepts",
    "Write code examples",
    "Debug issues"
  ])
  .withConstraint("must", "Always provide working code examples");
3

Build Prompt

Generate the final system prompt string:
const prompt = builder.build();
// Use with OpenAI, Anthropic, or any LLM API

Method Chaining Pattern

Every builder method returns this, enabling fluent chaining:
const prompt = createPromptBuilder()
  .withIdentity("Expert travel assistant")
  .withCapabilities([
    "Search for flights and hotels",
    "Provide destination recommendations",
    "Handle booking modifications"
  ])
  .withTool({
    name: "search_flights",
    description: "Search available flights",
    schema: z.object({
      origin: z.string().describe("Departure airport code"),
      destination: z.string().describe("Arrival airport code"),
      date: z.string().describe("Departure date (YYYY-MM-DD)")
    })
  })
  .withConstraint("must", "Always verify travel dates before searching")
  .withConstraint("must_not", "Never proceed with bookings without explicit confirmation")
  .withGuardrails()
  .build();
See builder.ts:75-2472 for the complete implementation.

Builder State & Caching

Internal State

The builder maintains private state for all configuration:
private _identity = "";
private readonly _capabilities: string[] = [];
private readonly _tools: ExecutableToolDefinition[] = [];
private readonly _constraints: Constraint[] = [];
private _format: PromptFormat = "markdown";
// ... and more
This state is mutable - calling methods modifies the builder instance. If you need immutability, use .extend() to create a copy first.

Automatic Caching

The builder caches generated prompts to avoid redundant work:
const prompt1 = builder.build(); // Generates prompt
const prompt2 = builder.build(); // Returns cached prompt (instant)
See cache.ts:1-90 for the caching implementation.

Composition & Reusability

Extending Builders

Create variations without modifying the original:
// Base support assistant
const baseSupport = createPromptBuilder()
  .withIdentity("You are a customer support assistant")
  .withCapabilities(["Answer questions", "Resolve issues"])
  .withGuardrails()
  .withTone("Professional and empathetic");

// Extend for technical support
const techSupport = baseSupport.extend()
  .withIdentity("You are a technical support specialist")
  .withCapabilities([
    "Debug technical issues",
    "Explain technical concepts"
  ])
  .withContext("Product: SaaS Platform, Tech: React + Node.js");

// Original remains unchanged
console.log(baseSupport.hasCapabilities()); // Still has original capabilities
See builder.ts:961-979 for the .extend() implementation.

Merging Builders

Combine reusable patterns:
// Reusable security pattern
const securityBuilder = createPromptBuilder()
  .withGuardrails()
  .withConstraint("must", "Always verify user identity before sharing data")
  .withConstraint("must_not", "Never log or store personal information")
  .withForbiddenTopics(["Internal system details", "Other users' data"]);

// Domain-specific builder
const customerService = createPromptBuilder()
  .withIdentity("Customer service assistant")
  .withCapabilities(["Process returns", "Track orders"])
  .withTone("Empathetic and solution-oriented");

// Merge security into customer service
const secureCustomerService = customerService.merge(securityBuilder);
// Now has both customer service features AND security constraints
Merge Conflicts: Merging throws an error if both builders have tools with the same name. This prevents accidental overwrites.
See builder.ts:1020-1076 for merge rules and implementation.

Conditional Configuration

Use conditional methods for environment-specific behavior:
const isProd = process.env.NODE_ENV === 'production';
const hasAuth = config.authEnabled;

const builder = createPromptBuilder()
  .withIdentity("Customer assistant")
  .withToolIf(hasAuth, {
    name: "access_user_data",
    description: "Access authenticated user data",
    schema: z.object({ userId: z.string() })
  })
  .withConstraintIf(isProd, "must", "Log all security events")
  .withConstraintIf(!isProd, "should", "Provide verbose debug information");
See builder.ts:399-405 and builder.ts:506-516 for conditional methods.

Introspection & Debugging

Query Builder State

Check what’s configured without building:
builder.hasIdentity();        // true/false
builder.hasTools();           // true/false
builder.hasConstraints();     // true/false
builder.hasGuardrails();      // true/false
builder.hasForbiddenTopics(); // true/false
builder.hasExamples();        // true/false
builder.hasCapabilities();    // true/false
See builder.ts:1098-1344 for introspection methods.

Debug Output

Get comprehensive diagnostics:
builder.debug();
Outputs detailed information including:
  • Configuration summary with counts
  • Section previews
  • Validation warnings (missing sections, potential issues)
  • Suggestions for improvement
  • Prompt size estimate (~tokens)
  • Token savings comparison (markdown vs TOON)
See builder.ts:1379-1491 for debug implementation.

Validation

Catch configuration issues before building:
const result = builder.validate();

if (!result.valid) {
  console.error('Validation errors:', result.errors);
  // Example error:
  // { severity: 'error', code: 'DUPLICATE_TOOL', message: 'Duplicate tool name: "search"' }
}

if (result.warnings.length > 0) {
  console.warn('Warnings:', result.warnings);
  // Example warning:
  // { severity: 'warning', code: 'MISSING_IDENTITY', message: 'No identity set' }
}

if (result.info.length > 0) {
  console.info('Suggestions:', result.info);
  // Example info:
  // { severity: 'info', code: 'TOOLS_WITHOUT_EXAMPLES', 
  //   message: 'Tools defined without usage examples' }
}
  • Duplicate tool names (error)
  • Missing identity (warning)
  • Empty capabilities (warning)
  • Empty constraints (warning)
  • Tools without examples (info)
  • Tools without guardrails (info)
  • No ‘must’ constraints (info)
  • Conflicting constraints (warning)
See validation.ts:61-284 for validation logic.

Export Formats

AI SDK Integration

Export for Vercel AI SDK:
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';

const response = await generateText({
  model: openai('gpt-4'),
  ...builder.toAiSdk(), // Spreads { system, tools }
  prompt: "User's question"
});
See builder.ts:1618-1709 for AI SDK exports.

Mastra Integration

Export for Mastra agents:
import { Agent } from '@mastra/core/agent';

const { instructions, tools } = builder.toMastra();

const agent = new Agent({
  name: 'my-agent',
  instructions,
  model: 'openai/gpt-4o',
  tools // Already in Mastra format
});
See builder.ts:1757-1801 for Mastra export implementation.

JSON Export

Export configuration as plain object:
const config = builder.toJSON();
fs.writeFileSync('agent-config.json', JSON.stringify(config, null, 2));
Zod schemas in tool definitions may not serialize perfectly to JSON due to their internal structure.
See builder.ts:1827-1842 for JSON export.

Best Practices

Start Simple

Begin with identity and capabilities. Add complexity only as needed.

Use Templates

Start from templates (codingAssistant, customerService) and customize.

Validate Early

Call .validate() or .debug() before deploying to catch issues.

Optimize Format

Use markdown during development, toon in production for 30-60% token savings.
  1. Forgetting to call .build(): The builder returns itself from methods, not the prompt. Always call .build() at the end.
  2. Mutating shared builders: Builders are mutable. Use .extend() if you need to create variations.
  3. Ignoring validation warnings: Warnings like missing examples or guardrails can impact quality.
  4. Overly complex prompts: More isn’t always better. Keep prompts focused on essential behaviors.

Tools

Learn about tool integration and Zod schemas

Constraints

Understand behavioral constraints and severity levels

Formats

Explore output formats and token optimization

Build docs developers (and LLMs) love