Skip to content

Bug: Compile() method shares Template field across multiple prompts causing wrong template execution #362

@Zereker

Description

@Zereker

Bug Description

When using the Compile() method multiple times on the same Dotprompt instance, all compiled PromptFunction closures share the same dp.Template field. This causes subsequent prompt executions to use the wrong template.

Root Cause

In dotprompt/dotprompt.go, the Compile method:

  1. Creates a new renderTpl from parsing the source (line 231)
  2. Stores it to the shared dp.Template field via initializeTemplate() (line 235)
  3. Returns a closure renderFunc that references dp.Template (line 262)
func (dp *Dotprompt) Compile(source string, additionalMetadata *PromptMetadata) (PromptFunction, error) {
    // ...
    renderTpl, err := raymond.Parse(parsedPrompt.Template)  // Line 231: Creates local template
    // ...
    dp.initializeTemplate(renderTpl)  // Line 235: Stores to shared dp.Template
    
    renderFunc := func(data *DataArgument, options *PromptMetadata) (RenderedPrompt, error) {
        // ...
        renderedString, err := dp.Template.ExecWith(inputContext, privDF, ...)  // Line 262: Uses shared dp.Template
        // ...
    }
    return renderFunc, nil
}

When another prompt is compiled, dp.Template gets overwritten, causing all previous renderFunc closures to use the wrong template.

Steps to Reproduce

dp := dotprompt.NewDotprompt(nil)

// Compile first prompt about weather
prompt1, _ := dp.Compile("Talk about weather: {{topic}}", nil)

// Compile second prompt about programming  
prompt2, _ := dp.Compile("Talk about programming: {{language}}", nil)

// Execute first prompt - BUG: uses second template instead!
result1, _ := prompt1(&dotprompt.DataArgument{Input: map[string]any{"topic": "sunny day"}}, nil)
// Expected: "Talk about weather: sunny day"
// Actual: "Talk about programming: sunny day" (wrong template!)

Expected Behavior

Each compiled PromptFunction should use its own template, not a shared one.

Proposed Fix

Change the closure to capture the local renderTpl variable instead of referencing dp.Template:

func (dp *Dotprompt) Compile(source string, additionalMetadata *PromptMetadata) (PromptFunction, error) {
    // ...
    renderTpl, err := raymond.Parse(parsedPrompt.Template)
    // ...
    dp.initializeTemplate(renderTpl)
    
    // Register helpers and partials
    if err = dp.RegisterHelpers(dp.Template); err != nil {
        return nil, err
    }
    if err = dp.RegisterPartials(dp.Template, parsedPrompt.Template); err != nil {
        return nil, err
    }
    
    // Capture local template for this closure
    localTemplate := dp.Template  // <-- FIX: capture the current template
    
    renderFunc := func(data *DataArgument, options *PromptMetadata) (RenderedPrompt, error) {
        // ...
        renderedString, err := localTemplate.ExecWith(inputContext, privDF, ...)  // <-- FIX: use localTemplate
        // ...
    }
    return renderFunc, nil
}

Environment

  • Go version: 1.23
  • dotprompt version: v0.0.0-20251105222245-c1c33196b7a4
  • Used via: firebase/genkit Go SDK v1.2.0

Impact

This bug affects anyone using genkit.LookupPrompt() with multiple .prompt files. The first prompt executed after loading will use the template of the last loaded prompt.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions