Skip to main content

Prompt Composition

PromptSmith provides powerful composition methods that allow you to build prompts from reusable components. This enables you to maintain a library of common patterns and combine them to create specialized agents.

Using extend() for Variations

The extend() method creates a deep copy of a builder, allowing you to create variations without modifying the original.

Basic Extension Pattern

import { createPromptBuilder } from 'promptsmith';
import { z } from 'zod';

// Create a base customer support agent
const baseSupportAgent = createPromptBuilder()
  .withIdentity('You are a professional customer support assistant')
  .withCapabilities([
    'Answer customer questions',
    'Resolve common issues',
    'Escalate complex problems'
  ])
  .withGuardrails()
  .withTone('Professional, empathetic, and solution-oriented')
  .withConstraint('must', 'Always maintain a positive and helpful attitude')
  .withConstraint('must', 'Verify customer identity before discussing account details')
  .withConstraint('should', 'Provide clear, step-by-step instructions');

// Extend for technical support
const technicalSupport = baseSupportAgent.extend()
  .withIdentity('You are a technical support specialist for SaaS products')
  .withCapabilities([
    'Debug technical issues',
    'Explain API errors',
    'Guide users through troubleshooting steps'
  ])
  .withContext(`
    Our platform uses React + Node.js.
    Common issues: API rate limits, authentication errors, webhook failures.
    Documentation: https://docs.example.com
  `)
  .withTool({
    name: 'check_api_status',
    description: 'Check the status of our API services',
    schema: z.object({
      service: z.enum(['auth', 'api', 'webhooks']).describe('Service to check')
    }),
    execute: async ({ service }) => {
      // Implementation
      return { status: 'operational', uptime: 99.9 };
    }
  });

// Extend for billing support
const billingSupport = baseSupportAgent.extend()
  .withIdentity('You are a billing and subscription support specialist')
  .withCapabilities([
    'Explain billing charges',
    'Process refund requests',
    'Manage subscription changes'
  ])
  .withContext(`
    Pricing tiers: Starter ($29/mo), Professional ($99/mo), Enterprise (custom)
    Refund policy: 30-day money-back guarantee
    Billing cycle: Monthly or annual
  `)
  .withConstraint('must', 'Never process refunds over $500 without manager approval')
  .withTool({
    name: 'lookup_invoice',
    description: 'Look up invoice details by invoice ID',
    schema: z.object({
      invoiceId: z.string().describe('Invoice ID')
    }),
    execute: async ({ invoiceId }) => {
      // Implementation
      return { amount: 99, status: 'paid', date: '2024-01-15' };
    }
  });

// Original remains unchanged
console.log(baseSupportAgent.getSummary().toolsCount); // 0
console.log(technicalSupport.getSummary().toolsCount);  // 1
console.log(billingSupport.getSummary().toolsCount);    // 1
Use extend() when you want to create specialized versions of a base agent while keeping the original intact. This is perfect for creating agent hierarchies.

Real-World Example: Multi-Region Support Agents

import { createPromptBuilder } from 'promptsmith';

// Base agent with global capabilities
const globalSupportAgent = createPromptBuilder()
  .withIdentity('You are a customer support representative')
  .withCapabilities([
    'Answer product questions',
    'Process returns and exchanges',
    'Track order status'
  ])
  .withGuardrails()
  .withConstraint('must', 'Always be respectful and professional');

// North America region (extended with region-specific context)
const naSupportAgent = globalSupportAgent.extend()
  .withContext(`
    Region: North America
    Business hours: 9 AM - 5 PM EST, Monday-Friday
    Shipping: Free shipping on orders over $50
    Standard delivery: 3-5 business days
    Currency: USD
  `)
  .withTone('Friendly and casual, use American English spelling');

// Europe region (extended with different context)
const euSupportAgent = globalSupportAgent.extend()
  .withContext(`
    Region: Europe
    Business hours: 9 AM - 5 PM CET, Monday-Friday
    Shipping: Free shipping on orders over €45
    Standard delivery: 5-7 business days
    Currency: EUR
    GDPR: All data requests must be processed within 30 days
  `)
  .withTone('Professional and formal, use British English spelling')
  .withConstraint('must', 'Comply with GDPR data protection requirements');

// Asia-Pacific region
const apacSupportAgent = globalSupportAgent.extend()
  .withContext(`
    Region: Asia-Pacific
    Business hours: 9 AM - 5 PM SGT, Monday-Friday
    Shipping: Varies by country (2-10 business days)
    Currency: Multiple (USD, SGD, AUD, JPY)
  `)
  .withTone('Respectful and clear, avoid idioms and complex phrases');

Using merge() for Composition

The merge() method combines two builders together, allowing you to compose agents from reusable behavioral patterns.

Security Pattern Composition

1

Create reusable security patterns

import { createPromptBuilder } from 'promptsmith';

// Security guardrails pattern
const securityPattern = createPromptBuilder()
  .withGuardrails()
  .withConstraint('must', 'Always verify user identity before accessing sensitive data')
  .withConstraint('must', 'Log all data access attempts for audit purposes')
  .withConstraint('must_not', 'Never store or log passwords or payment card numbers')
  .withConstraint('must_not', 'Never share data between different user accounts')
  .withForbiddenTopics([
    'Internal system architecture',
    'Security vulnerabilities',
    'Other users\' personal information'
  ]);

// Compliance pattern
const compliancePattern = createPromptBuilder()
  .withConstraint('must', 'Comply with GDPR data protection requirements')
  .withConstraint('must', 'Honor user data deletion requests within 30 days')
  .withConstraint('must', 'Provide data portability upon request')
  .withContext(`
    Data retention policy:
    - User data: Retained until account deletion
    - Audit logs: 7 years
    - Marketing data: Until consent withdrawn
  `);

// Professional communication pattern
const professionalPattern = createPromptBuilder()
  .withTone('Professional, clear, and respectful')
  .withConstraint('must', 'Use clear, jargon-free language')
  .withConstraint('should', 'Provide examples when explaining complex concepts')
  .withConstraint('should_not', 'Make assumptions about user technical knowledge')
  .withOutput(`
    Format responses as:
    1. Direct answer to the question
    2. Supporting explanation
    3. Next steps or related information
  `);
2

Compose agents by merging patterns

import { z } from 'zod';

// Financial advisor agent
const financialAdvisor = createPromptBuilder()
  .withIdentity('You are a financial advisory assistant')
  .withCapabilities([
    'Explain investment options',
    'Analyze portfolio performance',
    'Provide market insights'
  ])
  .withTool({
    name: 'get_portfolio',
    description: 'Retrieve user portfolio data',
    schema: z.object({
      userId: z.string().describe('User ID')
    }),
    execute: async ({ userId }) => {
      // Secure implementation
      return { holdings: [], totalValue: 0 };
    }
  })
  .withForbiddenTopics(['Specific investment advice without disclaimer'])
  .merge(securityPattern)      // Add security constraints
  .merge(compliancePattern)    // Add compliance requirements
  .merge(professionalPattern); // Add communication style

// Healthcare assistant agent
const healthcareAssistant = createPromptBuilder()
  .withIdentity('You are a healthcare appointment scheduling assistant')
  .withCapabilities([
    'Schedule patient appointments',
    'Answer general questions about services',
    'Provide directions to facilities'
  ])
  .withContext(`
    HIPAA compliance required for all patient data.
    Clinic hours: Monday-Friday, 8 AM - 6 PM
    Emergency line: 1-800-EMERGENCY
  `)
  .withConstraint('must', 'Comply with HIPAA privacy regulations')
  .withForbiddenTopics([
    'Medical diagnosis',
    'Treatment recommendations',
    'Prescription information'
  ])
  .merge(securityPattern)      // Add security constraints
  .merge(professionalPattern); // Add communication style

Merge Rules and Behavior

Understanding merge behavior is critical to avoid unexpected results. Here’s how merge() handles different properties:
// Understanding merge behavior
const builder1 = createPromptBuilder()
  .withIdentity('Agent A')
  .withTone('Casual')
  .withCapability('Capability A')
  .withConstraint('must', 'Constraint A');

const builder2 = createPromptBuilder()
  .withIdentity('Agent B')  // This will be IGNORED
  .withTone('Formal')       // This will be IGNORED (builder1 has tone)
  .withCapability('Capability B')
  .withConstraint('must', 'Constraint B');

const merged = builder1.merge(builder2);

// Results:
// ✅ Identity: 'Agent A' (builder1's identity is kept)
// ✅ Tone: 'Casual' (builder1's tone takes precedence)
// ✅ Capabilities: ['Capability A', 'Capability B'] (combined)
// ✅ Constraints: [Constraint A, Constraint B] (combined)
Merge behavior summary:
PropertyBehavior
IdentityTarget’s value kept (source ignored)
CapabilitiesCombined and deduplicated
ToolsCombined (error if duplicate names)
ConstraintsCombined
ExamplesCombined
ContextAppended with newlines
ToneTarget’s value kept (if set)
Output FormatTarget’s value kept (if set)
Error HandlingTarget’s value kept (if set)
GuardrailsEnabled if either has it
Forbidden TopicsCombined and deduplicated

What NOT to Do

// ❌ DON'T: Merge agents with conflicting tools
const agent1 = createPromptBuilder()
  .withTool({
    name: 'send_email',
    description: 'Send email via SendGrid',
    schema: z.object({ to: z.string() })
  });

const agent2 = createPromptBuilder()
  .withTool({
    name: 'send_email',  // Same name!
    description: 'Send email via Mailgun',
    schema: z.object({ recipient: z.string() })
  });

try {
  agent1.merge(agent2); // ❌ Throws error: duplicate tool name
} catch (error) {
  console.error(error.message); // "Cannot merge: duplicate tool name 'send_email'"
}

// ✅ DO: Use unique tool names
const agent1Fixed = createPromptBuilder()
  .withTool({
    name: 'send_email_sendgrid',
    description: 'Send email via SendGrid',
    schema: z.object({ to: z.string() })
  });

const agent2Fixed = createPromptBuilder()
  .withTool({
    name: 'send_email_mailgun',
    description: 'Send email via Mailgun',
    schema: z.object({ recipient: z.string() })
  });

agent1Fixed.merge(agent2Fixed); // ✅ Works!
// ❌ DON'T: Expect merged identity to override
const baseAgent = createPromptBuilder()
  .withIdentity('Generic assistant');

const specializedPattern = createPromptBuilder()
  .withIdentity('Specialized expert')  // This will be ignored!
  .withCapability('Expert capability');

const result = baseAgent.merge(specializedPattern);
console.log(result.build()); // Still says "Generic assistant"

// ✅ DO: Set identity on the primary builder
const baseAgentFixed = createPromptBuilder()
  .withIdentity('Generic assistant');

const specializedPatternFixed = createPromptBuilder()
  .withCapability('Expert capability')  // No identity
  .withContext('Expert domain knowledge');

baseAgentFixed
  .merge(specializedPatternFixed)
  .withIdentity('Specialized expert'); // Set identity after merge

Advanced Composition Patterns

Multi-Layer Composition

import { createPromptBuilder } from 'promptsmith';

// Layer 1: Core security
const coreSecurityLayer = createPromptBuilder()
  .withGuardrails()
  .withConstraint('must', 'Always verify user authentication');

// Layer 2: Industry compliance
const financialComplianceLayer = createPromptBuilder()
  .withConstraint('must', 'Comply with SOC 2 Type II requirements')
  .withConstraint('must', 'Maintain audit logs for all transactions')
  .withForbiddenTopics(['Unauthorized account access']);

// Layer 3: Communication style
const formalCommunicationLayer = createPromptBuilder()
  .withTone('Professional and formal')
  .withOutput('Always provide clear disclaimers for financial information');

// Layer 4: Domain capabilities
const investmentAgent = createPromptBuilder()
  .withIdentity('You are a financial investment assistant')
  .withCapabilities([
    'Explain investment products',
    'Analyze market trends',
    'Provide portfolio insights'
  ])
  .merge(coreSecurityLayer)           // Add security
  .merge(financialComplianceLayer)    // Add compliance
  .merge(formalCommunicationLayer);   // Add communication style

const prompt = investmentAgent.build();

Creating a Pattern Library

// patterns/security.ts
export const securityPattern = createPromptBuilder()
  .withGuardrails()
  .withConstraint('must', 'Verify authentication before data access');

// patterns/gdpr.ts
export const gdprPattern = createPromptBuilder()
  .withConstraint('must', 'Comply with GDPR requirements')
  .withConstraint('must', 'Honor right to be forgotten requests');

// patterns/professional.ts
export const professionalPattern = createPromptBuilder()
  .withTone('Professional and respectful')
  .withConstraint('should', 'Use clear, simple language');

// agents/customer-service.ts
import { securityPattern, gdprPattern, professionalPattern } from '../patterns';

export const customerServiceAgent = createPromptBuilder()
  .withIdentity('Customer service representative')
  .withCapabilities(['Answer questions', 'Resolve issues'])
  .merge(securityPattern)
  .merge(gdprPattern)
  .merge(professionalPattern);

Best Practices

Use extend() for variations

When you need different versions of the same base agent (e.g., region-specific support agents)

Use merge() for composition

When you want to combine reusable behavioral patterns (e.g., security + compliance + communication style)

Create pattern libraries

Build a library of reusable patterns (security, compliance, communication styles) that can be mixed and matched

Test composed prompts

Always validate composed prompts to ensure the combination produces the expected behavior
Use .getSummary() to inspect the final state of composed builders and verify that all expected components are present.

Build docs developers (and LLMs) love