@kognitivedev/vercel-ai-provider 0.1.9 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -151,7 +151,7 @@ Used with `cl.streamText()` and `cl.generateText()` to reference a managed promp
151
151
  ```typescript
152
152
  interface PromptConfig {
153
153
  slug: string;
154
- variables?: Record<string, string>;
154
+ variables?: Record<string, string | boolean>;
155
155
  }
156
156
  ```
157
157
 
@@ -265,7 +265,69 @@ const { text } = await generateText({
265
265
 
266
266
  Use `cl.streamText()` and `cl.generateText()` to resolve prompts by slug from the backend. Prompts are fetched from the `/api/cognitive/prompt` endpoint and cached for 60 seconds.
267
267
 
268
- Template variables use `{{variable}}` syntax and are interpolated before the prompt is sent to the model.
268
+ Prompt templates use [Handlebars](https://handlebarsjs.com/) syntax, giving you variable interpolation and conditional blocks. This means prompts can be fully managed from the dashboard without any string concatenation in application code.
269
+
270
+ #### Template Syntax
271
+
272
+ **Variable interpolation** — insert a value by name:
273
+
274
+ ```handlebars
275
+ Hello {{userName}}, welcome to {{companyName}}.
276
+ ```
277
+
278
+ **Conditional blocks** — include content only when a variable is truthy:
279
+
280
+ ```handlebars
281
+ {{#if hasImages}}
282
+ The user has attached images. Please analyze them.
283
+ {{/if}}
284
+ ```
285
+
286
+ **If / else** — provide a fallback when the condition is falsy:
287
+
288
+ ```handlebars
289
+ {{#if isPremium}}
290
+ You have access to all features.
291
+ {{else}}
292
+ Upgrade to premium for full access.
293
+ {{/if}}
294
+ ```
295
+
296
+ **Unless** — inverse of `if`, includes the block when the variable is falsy:
297
+
298
+ ```handlebars
299
+ {{#unless hasHistory}}
300
+ This is a new user. Introduce yourself.
301
+ {{/unless}}
302
+ ```
303
+
304
+ **Nesting** — conditionals can be nested arbitrarily:
305
+
306
+ ```handlebars
307
+ {{#if hasAttachments}}
308
+ Attachments provided.
309
+ {{#if hasImages}}
310
+ Includes images for visual analysis.
311
+ {{/if}}
312
+ {{/if}}
313
+ ```
314
+
315
+ #### Variable Types
316
+
317
+ Variables can be `string` or `boolean`:
318
+
319
+ | Type | Truthy | Falsy |
320
+ |------|--------|-------|
321
+ | `string` | Any non-empty string | `""` (empty string) |
322
+ | `boolean` | `true` | `false` |
323
+
324
+ Undefined variables render as empty string in interpolation and are treated as falsy in conditionals.
325
+
326
+ > **Note:** Content is not HTML-escaped — prompt text passes through as-is, which is the correct behavior for LLM prompts.
327
+
328
+ #### Usage with `cl.streamText()` / `cl.generateText()`
329
+
330
+ Pass variables in the `prompt.variables` field:
269
331
 
270
332
  ```typescript
271
333
  // Stream with a managed prompt
@@ -275,13 +337,15 @@ const result = await cl.streamText({
275
337
  sessionId: "session-abc"
276
338
  }),
277
339
  prompt: {
278
- slug: "customer-support",
340
+ slug: "fitness-coach",
279
341
  variables: {
280
- companyName: "Acme Corp",
281
- language: "English"
342
+ userName: "Sarah",
343
+ fitnessGoal: "Build muscle",
344
+ hasImages: true,
345
+ hasAttachments: false
282
346
  }
283
347
  },
284
- messages: [{ role: "user", content: "I need help with my order" }]
348
+ messages: [{ role: "user", content: "Check my squat form" }]
285
349
  });
286
350
 
287
351
  for await (const chunk of result.textStream) {
@@ -290,19 +354,59 @@ for await (const chunk of result.textStream) {
290
354
  ```
291
355
 
292
356
  ```typescript
293
- // Generate with a managed prompt
357
+ // Generate with a managed prompt (string-only variables work too)
294
358
  const { text } = await cl.generateText({
295
359
  model: cl("gpt-4o", {
296
360
  userId: "user-123",
297
361
  sessionId: "session-abc"
298
362
  }),
299
363
  prompt: {
300
- slug: "summarizer"
364
+ slug: "summarizer",
365
+ variables: {
366
+ language: "English"
367
+ }
301
368
  },
302
369
  messages: [{ role: "user", content: "Summarize this document..." }]
303
370
  });
304
371
  ```
305
372
 
373
+ #### Standalone `renderTemplate()`
374
+
375
+ The template engine is also exported directly for use outside of prompt management:
376
+
377
+ ```typescript
378
+ import { renderTemplate } from "@kognitivedev/vercel-ai-provider";
379
+
380
+ const result = renderTemplate(
381
+ "Hello {{name}}! {{#if isVip}}Welcome back, VIP.{{/if}}",
382
+ { name: "Alice", isVip: true }
383
+ );
384
+ // → "Hello Alice! Welcome back, VIP."
385
+ ```
386
+
387
+ #### Full Example Template
388
+
389
+ A prompt template stored in the dashboard might look like:
390
+
391
+ ```handlebars
392
+ You are a fitness coach AI assistant.
393
+
394
+ User: {{userName}}
395
+ Goal: {{fitnessGoal}}
396
+
397
+ {{#if hasImages}}
398
+ The user has attached images for form analysis. Please review them carefully.
399
+ {{/if}}
400
+ {{#if hasAttachments}}
401
+ Additional documents have been provided for context.
402
+ {{/if}}
403
+ {{#unless hasHistory}}
404
+ This is a new user with no prior conversation history. Introduce yourself.
405
+ {{/unless}}
406
+
407
+ Please provide personalized advice.
408
+ ```
409
+
306
410
  ### Gateway Routing
307
411
 
308
412
  When a prompt has a `gatewaySlug` configured on the backend, requests are automatically routed through the gateway endpoint at `{baseUrl}/api/cognitive/gateway/{gatewaySlug}`.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const template_1 = require("../template");
5
+ (0, vitest_1.describe)("renderTemplate", () => {
6
+ (0, vitest_1.describe)("variable interpolation", () => {
7
+ (0, vitest_1.it)("replaces a single variable", () => {
8
+ (0, vitest_1.expect)((0, template_1.renderTemplate)("Hello {{name}}", { name: "Alice" })).toBe("Hello Alice");
9
+ });
10
+ (0, vitest_1.it)("replaces multiple variables", () => {
11
+ (0, vitest_1.expect)((0, template_1.renderTemplate)("{{greeting}}, {{name}}!", { greeting: "Hi", name: "Bob" })).toBe("Hi, Bob!");
12
+ });
13
+ (0, vitest_1.it)("renders undefined variables as empty string", () => {
14
+ (0, vitest_1.expect)((0, template_1.renderTemplate)("Hello {{name}}", {})).toBe("Hello ");
15
+ });
16
+ (0, vitest_1.it)("does not HTML-escape content", () => {
17
+ (0, vitest_1.expect)((0, template_1.renderTemplate)("{{content}}", { content: "<b>bold</b> & \"quoted\"" })).toBe("<b>bold</b> & \"quoted\"");
18
+ });
19
+ });
20
+ (0, vitest_1.describe)("{{#if}} / {{/if}}", () => {
21
+ (0, vitest_1.it)("includes block when variable is truthy string", () => {
22
+ (0, vitest_1.expect)((0, template_1.renderTemplate)("start{{#if show}} visible{{/if}} end", { show: "yes" })).toBe("start visible end");
23
+ });
24
+ (0, vitest_1.it)("includes block when variable is true", () => {
25
+ (0, vitest_1.expect)((0, template_1.renderTemplate)("start{{#if show}} visible{{/if}} end", { show: true })).toBe("start visible end");
26
+ });
27
+ (0, vitest_1.it)("excludes block when variable is false", () => {
28
+ (0, vitest_1.expect)((0, template_1.renderTemplate)("start{{#if show}} visible{{/if}} end", { show: false })).toBe("start end");
29
+ });
30
+ (0, vitest_1.it)("excludes block when variable is undefined", () => {
31
+ (0, vitest_1.expect)((0, template_1.renderTemplate)("start{{#if show}} visible{{/if}} end", {})).toBe("start end");
32
+ });
33
+ (0, vitest_1.it)("excludes block when variable is empty string", () => {
34
+ (0, vitest_1.expect)((0, template_1.renderTemplate)("start{{#if show}} visible{{/if}} end", { show: "" })).toBe("start end");
35
+ });
36
+ });
37
+ (0, vitest_1.describe)("{{#unless}} / {{/unless}}", () => {
38
+ (0, vitest_1.it)("includes block when variable is falsy", () => {
39
+ (0, vitest_1.expect)((0, template_1.renderTemplate)("{{#unless premium}}Free tier{{/unless}}", { premium: false })).toBe("Free tier");
40
+ });
41
+ (0, vitest_1.it)("excludes block when variable is truthy", () => {
42
+ (0, vitest_1.expect)((0, template_1.renderTemplate)("{{#unless premium}}Free tier{{/unless}}", { premium: true })).toBe("");
43
+ });
44
+ });
45
+ (0, vitest_1.describe)("{{else}} branches", () => {
46
+ (0, vitest_1.it)("renders else branch when if condition is false", () => {
47
+ (0, vitest_1.expect)((0, template_1.renderTemplate)("{{#if vip}}VIP access{{else}}Standard access{{/if}}", { vip: false })).toBe("Standard access");
48
+ });
49
+ (0, vitest_1.it)("renders if branch when condition is true", () => {
50
+ (0, vitest_1.expect)((0, template_1.renderTemplate)("{{#if vip}}VIP access{{else}}Standard access{{/if}}", { vip: true })).toBe("VIP access");
51
+ });
52
+ });
53
+ (0, vitest_1.describe)("nested conditionals", () => {
54
+ (0, vitest_1.it)("handles nested if blocks", () => {
55
+ const template = "{{#if a}}A{{#if b}}-B{{/if}}{{/if}}";
56
+ (0, vitest_1.expect)((0, template_1.renderTemplate)(template, { a: true, b: true })).toBe("A-B");
57
+ (0, vitest_1.expect)((0, template_1.renderTemplate)(template, { a: true, b: false })).toBe("A");
58
+ (0, vitest_1.expect)((0, template_1.renderTemplate)(template, { a: false, b: true })).toBe("");
59
+ });
60
+ });
61
+ (0, vitest_1.describe)("variables inside conditionals", () => {
62
+ (0, vitest_1.it)("interpolates variables within conditional blocks", () => {
63
+ (0, vitest_1.expect)((0, template_1.renderTemplate)("{{#if hasName}}Name: {{name}}{{/if}}", { hasName: true, name: "Alice" })).toBe("Name: Alice");
64
+ });
65
+ });
66
+ (0, vitest_1.describe)("real-world fitness app template", () => {
67
+ const template = `You are a fitness coach AI assistant.
68
+
69
+ User: {{userName}}
70
+ Goal: {{fitnessGoal}}
71
+
72
+ {{#if hasImages}}
73
+ The user has attached images for form analysis. Please review them carefully.
74
+ {{/if}}
75
+ {{#if hasAttachments}}
76
+ Additional documents have been provided for context.
77
+ {{/if}}
78
+ {{#unless hasHistory}}
79
+ This is a new user with no prior conversation history. Introduce yourself.
80
+ {{/unless}}
81
+
82
+ Please provide personalized advice.`;
83
+ (0, vitest_1.it)("renders with all flags true", () => {
84
+ const result = (0, template_1.renderTemplate)(template, {
85
+ userName: "Sarah",
86
+ fitnessGoal: "Build muscle",
87
+ hasImages: true,
88
+ hasAttachments: true,
89
+ hasHistory: true,
90
+ });
91
+ (0, vitest_1.expect)(result).toContain("User: Sarah");
92
+ (0, vitest_1.expect)(result).toContain("Goal: Build muscle");
93
+ (0, vitest_1.expect)(result).toContain("attached images for form analysis");
94
+ (0, vitest_1.expect)(result).toContain("Additional documents");
95
+ (0, vitest_1.expect)(result).not.toContain("new user with no prior");
96
+ });
97
+ (0, vitest_1.it)("renders with all flags false", () => {
98
+ const result = (0, template_1.renderTemplate)(template, {
99
+ userName: "Tom",
100
+ fitnessGoal: "Lose weight",
101
+ hasImages: false,
102
+ hasAttachments: false,
103
+ hasHistory: false,
104
+ });
105
+ (0, vitest_1.expect)(result).toContain("User: Tom");
106
+ (0, vitest_1.expect)(result).toContain("Goal: Lose weight");
107
+ (0, vitest_1.expect)(result).not.toContain("attached images");
108
+ (0, vitest_1.expect)(result).not.toContain("Additional documents");
109
+ (0, vitest_1.expect)(result).toContain("new user with no prior");
110
+ });
111
+ });
112
+ });
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { streamText as aiStreamText, generateText as aiGenerateText, type LanguageModel } from "ai";
2
+ export { renderTemplate, type TemplateVariables } from "./template";
2
3
  /**
3
4
  * Log levels for controlling verbosity of CognitiveLayer logging.
4
5
  * - 'none': No logging
@@ -36,7 +37,7 @@ export type CLModelWrapper = (modelId: string, settings?: {
36
37
  }, providerOptions?: Record<string, unknown>) => LanguageModel;
37
38
  export interface PromptConfig {
38
39
  slug: string;
39
- variables?: Record<string, string>;
40
+ variables?: Record<string, string | boolean>;
40
41
  }
41
42
  export type CLStreamTextOptions = Omit<Parameters<typeof aiStreamText>[0], 'system' | 'prompt'> & {
42
43
  prompt: PromptConfig;
package/dist/index.js CHANGED
@@ -11,9 +11,13 @@ var __rest = (this && this.__rest) || function (s, e) {
11
11
  return t;
12
12
  };
13
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.renderTemplate = void 0;
14
15
  exports.createCognitiveLayer = createCognitiveLayer;
15
16
  const ai_1 = require("ai");
16
17
  const crypto_1 = require("crypto");
18
+ var template_1 = require("./template");
19
+ Object.defineProperty(exports, "renderTemplate", { enumerable: true, get: function () { return template_1.renderTemplate; } });
20
+ const template_2 = require("./template");
17
21
  function isValidId(value) {
18
22
  if (value == null || typeof value !== "string")
19
23
  return false;
@@ -154,13 +158,6 @@ function buildTraceSpansFromMessages(messages) {
154
158
  }
155
159
  return spans;
156
160
  }
157
- /**
158
- * Interpolate {{variable}} placeholders in a template string.
159
- * Unmatched variables are left as-is.
160
- */
161
- function interpolateTemplate(content, variables) {
162
- return content.replace(/\{\{(\w+)\}\}/g, (_, key) => { var _a; return (_a = variables[key]) !== null && _a !== void 0 ? _a : `{{${key}}}`; });
163
- }
164
161
  // Session-scoped snapshot cache: sessionKey → formatted memory block
165
162
  const sessionSnapshots = new Map();
166
163
  // Regex to detect if memory has already been injected
@@ -640,7 +637,7 @@ ${userContextBlock || "None"}
640
637
  let system;
641
638
  if (resolved) {
642
639
  system = promptConfig.variables
643
- ? interpolateTemplate(resolved.content, promptConfig.variables)
640
+ ? (0, template_2.renderTemplate)(resolved.content, promptConfig.variables)
644
641
  : resolved.content;
645
642
  // Store prompt metadata for the session (read by middleware during logging)
646
643
  if (session === null || session === void 0 ? void 0 : session.sessionId) {
@@ -679,7 +676,7 @@ ${userContextBlock || "None"}
679
676
  let system;
680
677
  if (resolved) {
681
678
  system = promptConfig.variables
682
- ? interpolateTemplate(resolved.content, promptConfig.variables)
679
+ ? (0, template_2.renderTemplate)(resolved.content, promptConfig.variables)
683
680
  : resolved.content;
684
681
  // Store prompt metadata for the session (read by middleware during logging)
685
682
  if (session === null || session === void 0 ? void 0 : session.sessionId) {
@@ -0,0 +1,2 @@
1
+ export type TemplateVariables = Record<string, string | boolean>;
2
+ export declare function renderTemplate(template: string, variables: TemplateVariables): string;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.renderTemplate = renderTemplate;
7
+ // Use the pre-built dist to avoid `require.extensions` warning in webpack/Next.js
8
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
9
+ const handlebars_1 = __importDefault(require("handlebars/dist/cjs/handlebars"));
10
+ function renderTemplate(template, variables) {
11
+ const compiled = handlebars_1.default.compile(template, { noEscape: true });
12
+ return compiled(variables);
13
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kognitivedev/vercel-ai-provider",
3
- "version": "0.1.9",
3
+ "version": "0.2.1",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "publishConfig": {
@@ -12,6 +12,9 @@
12
12
  "test": "vitest run",
13
13
  "prepublishOnly": "npm run build"
14
14
  },
15
+ "dependencies": {
16
+ "handlebars": "^4.7.8"
17
+ },
15
18
  "peerDependencies": {
16
19
  "ai": "^5.0.0 || ^6.0.0"
17
20
  },
@@ -0,0 +1,161 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { renderTemplate } from "../template";
3
+
4
+ describe("renderTemplate", () => {
5
+ describe("variable interpolation", () => {
6
+ it("replaces a single variable", () => {
7
+ expect(renderTemplate("Hello {{name}}", { name: "Alice" })).toBe("Hello Alice");
8
+ });
9
+
10
+ it("replaces multiple variables", () => {
11
+ expect(
12
+ renderTemplate("{{greeting}}, {{name}}!", { greeting: "Hi", name: "Bob" })
13
+ ).toBe("Hi, Bob!");
14
+ });
15
+
16
+ it("renders undefined variables as empty string", () => {
17
+ expect(renderTemplate("Hello {{name}}", {})).toBe("Hello ");
18
+ });
19
+
20
+ it("does not HTML-escape content", () => {
21
+ expect(renderTemplate("{{content}}", { content: "<b>bold</b> & \"quoted\"" })).toBe(
22
+ "<b>bold</b> & \"quoted\""
23
+ );
24
+ });
25
+ });
26
+
27
+ describe("{{#if}} / {{/if}}", () => {
28
+ it("includes block when variable is truthy string", () => {
29
+ expect(
30
+ renderTemplate("start{{#if show}} visible{{/if}} end", { show: "yes" })
31
+ ).toBe("start visible end");
32
+ });
33
+
34
+ it("includes block when variable is true", () => {
35
+ expect(
36
+ renderTemplate("start{{#if show}} visible{{/if}} end", { show: true })
37
+ ).toBe("start visible end");
38
+ });
39
+
40
+ it("excludes block when variable is false", () => {
41
+ expect(
42
+ renderTemplate("start{{#if show}} visible{{/if}} end", { show: false })
43
+ ).toBe("start end");
44
+ });
45
+
46
+ it("excludes block when variable is undefined", () => {
47
+ expect(
48
+ renderTemplate("start{{#if show}} visible{{/if}} end", {})
49
+ ).toBe("start end");
50
+ });
51
+
52
+ it("excludes block when variable is empty string", () => {
53
+ expect(
54
+ renderTemplate("start{{#if show}} visible{{/if}} end", { show: "" })
55
+ ).toBe("start end");
56
+ });
57
+ });
58
+
59
+ describe("{{#unless}} / {{/unless}}", () => {
60
+ it("includes block when variable is falsy", () => {
61
+ expect(
62
+ renderTemplate("{{#unless premium}}Free tier{{/unless}}", { premium: false })
63
+ ).toBe("Free tier");
64
+ });
65
+
66
+ it("excludes block when variable is truthy", () => {
67
+ expect(
68
+ renderTemplate("{{#unless premium}}Free tier{{/unless}}", { premium: true })
69
+ ).toBe("");
70
+ });
71
+ });
72
+
73
+ describe("{{else}} branches", () => {
74
+ it("renders else branch when if condition is false", () => {
75
+ expect(
76
+ renderTemplate(
77
+ "{{#if vip}}VIP access{{else}}Standard access{{/if}}",
78
+ { vip: false }
79
+ )
80
+ ).toBe("Standard access");
81
+ });
82
+
83
+ it("renders if branch when condition is true", () => {
84
+ expect(
85
+ renderTemplate(
86
+ "{{#if vip}}VIP access{{else}}Standard access{{/if}}",
87
+ { vip: true }
88
+ )
89
+ ).toBe("VIP access");
90
+ });
91
+ });
92
+
93
+ describe("nested conditionals", () => {
94
+ it("handles nested if blocks", () => {
95
+ const template = "{{#if a}}A{{#if b}}-B{{/if}}{{/if}}";
96
+ expect(renderTemplate(template, { a: true, b: true })).toBe("A-B");
97
+ expect(renderTemplate(template, { a: true, b: false })).toBe("A");
98
+ expect(renderTemplate(template, { a: false, b: true })).toBe("");
99
+ });
100
+ });
101
+
102
+ describe("variables inside conditionals", () => {
103
+ it("interpolates variables within conditional blocks", () => {
104
+ expect(
105
+ renderTemplate(
106
+ "{{#if hasName}}Name: {{name}}{{/if}}",
107
+ { hasName: true, name: "Alice" }
108
+ )
109
+ ).toBe("Name: Alice");
110
+ });
111
+ });
112
+
113
+ describe("real-world fitness app template", () => {
114
+ const template = `You are a fitness coach AI assistant.
115
+
116
+ User: {{userName}}
117
+ Goal: {{fitnessGoal}}
118
+
119
+ {{#if hasImages}}
120
+ The user has attached images for form analysis. Please review them carefully.
121
+ {{/if}}
122
+ {{#if hasAttachments}}
123
+ Additional documents have been provided for context.
124
+ {{/if}}
125
+ {{#unless hasHistory}}
126
+ This is a new user with no prior conversation history. Introduce yourself.
127
+ {{/unless}}
128
+
129
+ Please provide personalized advice.`;
130
+
131
+ it("renders with all flags true", () => {
132
+ const result = renderTemplate(template, {
133
+ userName: "Sarah",
134
+ fitnessGoal: "Build muscle",
135
+ hasImages: true,
136
+ hasAttachments: true,
137
+ hasHistory: true,
138
+ });
139
+ expect(result).toContain("User: Sarah");
140
+ expect(result).toContain("Goal: Build muscle");
141
+ expect(result).toContain("attached images for form analysis");
142
+ expect(result).toContain("Additional documents");
143
+ expect(result).not.toContain("new user with no prior");
144
+ });
145
+
146
+ it("renders with all flags false", () => {
147
+ const result = renderTemplate(template, {
148
+ userName: "Tom",
149
+ fitnessGoal: "Lose weight",
150
+ hasImages: false,
151
+ hasAttachments: false,
152
+ hasHistory: false,
153
+ });
154
+ expect(result).toContain("User: Tom");
155
+ expect(result).toContain("Goal: Lose weight");
156
+ expect(result).not.toContain("attached images");
157
+ expect(result).not.toContain("Additional documents");
158
+ expect(result).toContain("new user with no prior");
159
+ });
160
+ });
161
+ });
@@ -0,0 +1,4 @@
1
+ declare module "handlebars/dist/cjs/handlebars" {
2
+ import Handlebars from "handlebars";
3
+ export default Handlebars;
4
+ }
package/src/index.ts CHANGED
@@ -5,6 +5,8 @@ import {
5
5
  type LanguageModel,
6
6
  } from "ai";
7
7
  import { randomUUID } from "crypto";
8
+ export { renderTemplate, type TemplateVariables } from "./template";
9
+ import { renderTemplate } from "./template";
8
10
 
9
11
  /**
10
12
  * Log levels for controlling verbosity of CognitiveLayer logging.
@@ -101,7 +103,7 @@ export type CLModelWrapper = (
101
103
 
102
104
  export interface PromptConfig {
103
105
  slug: string;
104
- variables?: Record<string, string>;
106
+ variables?: Record<string, string | boolean>;
105
107
  }
106
108
 
107
109
  export type CLStreamTextOptions = Omit<Parameters<typeof aiStreamText>[0], 'system' | 'prompt'> & {
@@ -274,14 +276,6 @@ function buildTraceSpansFromMessages(messages: any[]): Array<{
274
276
  return spans;
275
277
  }
276
278
 
277
- /**
278
- * Interpolate {{variable}} placeholders in a template string.
279
- * Unmatched variables are left as-is.
280
- */
281
- function interpolateTemplate(content: string, variables: Record<string, string>): string {
282
- return content.replace(/\{\{(\w+)\}\}/g, (_, key) => variables[key] ?? `{{${key}}}`);
283
- }
284
-
285
279
  // Session-scoped snapshot cache: sessionKey → formatted memory block
286
280
  const sessionSnapshots = new Map<string, string>();
287
281
 
@@ -851,7 +845,7 @@ ${userContextBlock || "None"}
851
845
  let system: string | undefined;
852
846
  if (resolved) {
853
847
  system = promptConfig.variables
854
- ? interpolateTemplate(resolved.content, promptConfig.variables)
848
+ ? renderTemplate(resolved.content, promptConfig.variables)
855
849
  : resolved.content;
856
850
 
857
851
  // Store prompt metadata for the session (read by middleware during logging)
@@ -895,7 +889,7 @@ ${userContextBlock || "None"}
895
889
  let system: string | undefined;
896
890
  if (resolved) {
897
891
  system = promptConfig.variables
898
- ? interpolateTemplate(resolved.content, promptConfig.variables)
892
+ ? renderTemplate(resolved.content, promptConfig.variables)
899
893
  : resolved.content;
900
894
 
901
895
  // Store prompt metadata for the session (read by middleware during logging)
@@ -0,0 +1,10 @@
1
+ // Use the pre-built dist to avoid `require.extensions` warning in webpack/Next.js
2
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
3
+ import Handlebars from "handlebars/dist/cjs/handlebars";
4
+
5
+ export type TemplateVariables = Record<string, string | boolean>;
6
+
7
+ export function renderTemplate(template: string, variables: TemplateVariables): string {
8
+ const compiled = Handlebars.compile(template, { noEscape: true });
9
+ return compiled(variables);
10
+ }