@kognitivedev/vercel-ai-provider 0.1.7 → 0.1.9

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/src/index.ts CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  generateText as aiGenerateText,
5
5
  type LanguageModel,
6
6
  } from "ai";
7
+ import { randomUUID } from "crypto";
7
8
 
8
9
  /**
9
10
  * Log levels for controlling verbosity of CognitiveLayer logging.
@@ -21,6 +22,17 @@ function isValidId(value: string | undefined | null): value is string {
21
22
  return trimmed !== "" && trimmed !== "null" && trimmed !== "undefined";
22
23
  }
23
24
 
25
+ function maskSecret(secret: string | undefined | null): string {
26
+ if (!secret) return "missing";
27
+ if (secret.length <= 8) return `${secret.slice(0, 2)}***`;
28
+ return `${secret.slice(0, 4)}...${secret.slice(-4)}`;
29
+ }
30
+
31
+ function previewText(value: string, maxLength = 240): string {
32
+ if (value.length <= maxLength) return value;
33
+ return `${value.slice(0, maxLength)}...`;
34
+ }
35
+
24
36
  const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
25
37
  none: 0,
26
38
  error: 1,
@@ -111,12 +123,33 @@ export interface LogConversationPayload {
111
123
  promptSlug?: string;
112
124
  promptVersion?: number;
113
125
  promptId?: string;
126
+ traceId?: string;
127
+ parentSpanId?: string;
128
+ requestPreview?: string;
129
+ responsePreview?: string;
130
+ state?: "active" | "completed" | "error";
131
+ startedAt?: string;
132
+ endedAt?: string;
133
+ durationMs?: number;
134
+ metadata?: Record<string, unknown>;
135
+ spans?: Array<{
136
+ spanKey: string;
137
+ parentSpanKey?: string;
138
+ name: string;
139
+ spanType: string;
140
+ status?: "active" | "completed" | "error";
141
+ inputPreview?: string;
142
+ outputPreview?: string;
143
+ toolName?: string;
144
+ errorMessage?: string;
145
+ metadata?: Record<string, unknown>;
146
+ }>;
114
147
  }
115
148
 
116
149
  export type CognitiveLayer = CLModelWrapper & {
117
150
  streamText: (options: CLStreamTextOptions) => Promise<ReturnType<typeof aiStreamText>>;
118
151
  generateText: (options: CLGenerateTextOptions) => ReturnType<typeof aiGenerateText>;
119
- resolvePrompt: (slug: string) => Promise<CachedPrompt>;
152
+ resolvePrompt: (slug: string, userId?: string) => Promise<CachedPrompt>;
120
153
  logConversation: (payload: LogConversationPayload) => Promise<void>;
121
154
  triggerProcessing: (userId: string, projectId: string, sessionId: string) => void;
122
155
  clearPromptCache: () => void;
@@ -136,6 +169,111 @@ export interface CachedPrompt {
136
169
 
137
170
  const PROMPT_CACHE_TTL_MS = 60_000; // 1 minute
138
171
 
172
+ function getContentText(content: any): string {
173
+ if (typeof content === "string") return content;
174
+ if (!Array.isArray(content)) return "";
175
+
176
+ return content.map((part) => {
177
+ if (!part || typeof part !== "object") return "";
178
+ if (typeof part.text === "string") return part.text;
179
+ if (part.type === "tool-call" && typeof part.toolName === "string") return `Called ${part.toolName}`;
180
+ if (part.type === "tool-result") return "Received tool result";
181
+ return "";
182
+ }).filter(Boolean).join(" ");
183
+ }
184
+
185
+ /**
186
+ * Unwraps V2/V3 ToolResultOutput discriminated union to a displayable value.
187
+ * Stream ToolResult uses plain `result` (passthrough), while prompt ToolResultPart
188
+ * uses `output` with a discriminated union: text, json, error-text, error-json, content, execution-denied.
189
+ */
190
+ function extractOutputValue(raw: unknown): unknown {
191
+ if (raw == null) return raw;
192
+ if (typeof raw !== 'object') return raw;
193
+ const obj = raw as Record<string, unknown>;
194
+ if (typeof obj.type !== 'string') return raw;
195
+ switch (obj.type) {
196
+ case 'text':
197
+ case 'json':
198
+ case 'error-text':
199
+ case 'error-json':
200
+ case 'content':
201
+ return obj.value;
202
+ case 'execution-denied':
203
+ return `Execution denied: ${obj.reason ?? 'unknown'}`;
204
+ default:
205
+ return raw;
206
+ }
207
+ }
208
+
209
+ function buildTracePreviews(messages: any[]): { requestPreview: string; responsePreview: string } {
210
+ const request = [...messages].reverse().find((message) => message?.role === "user");
211
+ const response = [...messages].reverse().find((message) => message?.role === "assistant");
212
+
213
+ return {
214
+ requestPreview: request ? getContentText(request.content).slice(0, 220) : "No request captured",
215
+ responsePreview: response ? getContentText(response.content).slice(0, 240) : "No response captured",
216
+ };
217
+ }
218
+
219
+ function buildTraceSpansFromMessages(messages: any[]): Array<{
220
+ spanKey: string;
221
+ parentSpanKey?: string;
222
+ name: string;
223
+ spanType: string;
224
+ status?: "active" | "completed" | "error";
225
+ inputPreview?: string;
226
+ outputPreview?: string;
227
+ toolName?: string;
228
+ errorMessage?: string;
229
+ metadata?: Record<string, unknown>;
230
+ }> {
231
+ const resultMap = new Map<string, unknown>();
232
+
233
+ for (const message of messages) {
234
+ if (!Array.isArray(message?.content)) continue;
235
+ for (const part of message.content) {
236
+ if (part?.type === "tool-result" && typeof part.toolCallId === "string") {
237
+ resultMap.set(part.toolCallId, part.result ?? part.output);
238
+ }
239
+ }
240
+ }
241
+
242
+ const spans: Array<{
243
+ spanKey: string;
244
+ parentSpanKey?: string;
245
+ name: string;
246
+ spanType: string;
247
+ status?: "active" | "completed" | "error";
248
+ inputPreview?: string;
249
+ outputPreview?: string;
250
+ toolName?: string;
251
+ errorMessage?: string;
252
+ metadata?: Record<string, unknown>;
253
+ }> = [];
254
+
255
+ for (const message of messages) {
256
+ if (!Array.isArray(message?.content)) continue;
257
+ for (const part of message.content) {
258
+ if (part?.type === "tool-call" && typeof part.toolCallId === "string") {
259
+ const result = resultMap.get(part.toolCallId);
260
+ spans.push({
261
+ spanKey: part.toolCallId,
262
+ parentSpanKey: "root",
263
+ name: typeof part.toolName === "string" ? part.toolName : "tool",
264
+ spanType: "tool",
265
+ status: "completed",
266
+ inputPreview: JSON.stringify(part.input ?? {}).slice(0, 220),
267
+ outputPreview: result != null ? JSON.stringify(extractOutputValue(result)).slice(0, 220) : "No tool result captured",
268
+ toolName: typeof part.toolName === "string" ? part.toolName : undefined,
269
+ });
270
+ }
271
+ }
272
+ }
273
+
274
+ return spans;
275
+ }
276
+
139
277
  /**
140
278
  * Interpolate {{variable}} placeholders in a template string.
141
279
  * Unmatched variables are left as-is.
@@ -194,18 +332,44 @@ export function createCognitiveLayer(config: {
194
332
  // Prompt cache: slug → CachedPrompt
195
333
  const promptCache = new Map<string, CachedPrompt>();
196
334
 
197
- const resolvePrompt = async (slug: string): Promise<CachedPrompt> => {
198
- const cached = promptCache.get(slug);
335
+ const resolvePrompt = async (slug: string, userId?: string): Promise<CachedPrompt> => {
336
+ const cacheKey = userId ? `${slug}:${userId}` : slug;
337
+ const cached = promptCache.get(cacheKey);
199
338
  if (cached && Date.now() - cached.fetchedAt < PROMPT_CACHE_TTL_MS) {
200
339
  logger.debug("Using cached prompt", { slug, version: cached.version });
201
340
  return cached;
202
341
  }
203
342
 
204
- const res = await fetch(`${baseUrl}/api/cognitive/prompt?slug=${encodeURIComponent(slug)}`, {
343
+ const url = new URL(`${baseUrl}/api/cognitive/prompt`);
344
+ url.searchParams.set("slug", slug);
345
+ if (userId) url.searchParams.set("userId", userId);
346
+
347
+ logger.debug("Resolving prompt from backend", {
348
+ slug,
349
+ userId,
350
+ url: url.toString(),
351
+ baseUrl,
352
+ apiKeyHint: maskSecret(clConfig.apiKey),
353
+ });
354
+
355
+ const res = await fetch(url.toString(), {
205
356
  headers: { "Authorization": `Bearer ${clConfig.apiKey}` },
206
357
  });
358
+ logger.debug("Prompt resolve response received", {
359
+ slug,
360
+ userId,
361
+ status: res.status,
362
+ ok: res.ok,
363
+ contentType: res.headers.get("content-type"),
364
+ });
207
365
  if (!res.ok) {
208
366
  const body = await res.text();
367
+ logger.debug("Prompt resolve response body preview", {
368
+ slug,
369
+ userId,
370
+ status: res.status,
371
+ bodyPreview: previewText(body),
372
+ });
209
373
  throw new Error(`Failed to resolve prompt "${slug}": ${res.status} ${body}`);
210
374
  }
211
375
 
@@ -218,7 +382,15 @@ export function createCognitiveLayer(config: {
218
382
  fetchedAt: Date.now(),
219
383
  gatewaySlug: data.gatewaySlug,
220
384
  };
221
- promptCache.set(slug, entry);
385
+ promptCache.set(cacheKey, entry);
386
+ logger.debug("Prompt resolved payload", {
387
+ slug,
388
+ resolvedSlug: entry.slug,
389
+ version: entry.version,
390
+ promptId: entry.promptId,
391
+ contentLength: entry.content.length,
392
+ gatewaySlug: entry.gatewaySlug ?? null,
393
+ });
222
394
  logger.info("Prompt resolved", { slug, version: entry.version });
223
395
  return entry;
224
396
  };
@@ -301,9 +473,25 @@ export function createCognitiveLayer(config: {
301
473
  if (systemPromptToAdd === undefined) {
302
474
  try {
303
475
  const url = `${baseUrl}/api/cognitive/snapshot?userId=${userId}`;
476
+ logger.debug("Fetching snapshot from backend", {
477
+ userId,
478
+ projectId,
479
+ sessionId,
480
+ url,
481
+ baseUrl,
482
+ apiKeyHint: maskSecret(clConfig.apiKey),
483
+ });
304
484
  const res = await fetch(url, {
305
485
  headers: { "Authorization": `Bearer ${clConfig.apiKey}` },
306
486
  });
487
+ logger.debug("Snapshot response received", {
488
+ userId,
489
+ projectId,
490
+ sessionId,
491
+ status: res.status,
492
+ ok: res.ok,
493
+ contentType: res.headers.get("content-type"),
494
+ });
307
495
  if (res.ok) {
308
496
  const data = await res.json();
309
497
  const systemBlock = data.systemBlock || "";
@@ -337,7 +525,15 @@ ${userContextBlock || "None"}
337
525
  rawData: data,
338
526
  });
339
527
  } else {
528
+ const body = await res.text();
340
529
  logger.warn("Snapshot fetch failed", { status: res.status });
530
+ logger.debug("Snapshot response body preview", {
531
+ userId,
532
+ projectId,
533
+ sessionId,
534
+ status: res.status,
535
+ bodyPreview: previewText(body),
536
+ });
341
537
  systemPromptToAdd = "";
342
538
  sessionSnapshots.set(sessionKey, systemPromptToAdd);
343
539
  }
@@ -370,6 +566,7 @@ ${userContextBlock || "None"}
370
566
  },
371
567
 
372
568
  async wrapGenerate({ doGenerate, params }: { doGenerate: any; params: any }) {
569
+ const startedAt = new Date();
373
570
  let result;
374
571
  try {
375
572
  result = await doGenerate();
@@ -380,17 +577,40 @@ ${userContextBlock || "None"}
380
577
  }
381
578
 
382
579
  if (isValidId(userId) && isValidId(sessionId)) {
580
+ const endedAt = new Date();
383
581
  const sessionKey = `${userId}:${projectId}:${sessionId}`;
384
582
  const promptMeta = sessionPromptMetadata.get(sessionKey);
385
583
 
386
- const messagesInput = (params as any).messages || (params as any).prompt || [];
387
- const resultMessages = (result as any)?.response?.messages;
388
- const assistantMessage = (result as any)?.text
389
- ? [{ role: "assistant", content: [{ type: "text", text: (result as any).text }] }]
584
+ const messagesInput = (params as any).prompt || (params as any).messages || [];
585
+
586
+ // Build assistant message from result.content (V2/V3 GenerateResult)
587
+ const resultContent = Array.isArray(result?.content) ? result.content : [];
588
+ const assistantParts: any[] = [];
589
+ for (const part of resultContent) {
590
+ if (part?.type === 'text') {
591
+ assistantParts.push({ type: 'text', text: part.text });
592
+ } else if (part?.type === 'tool-call') {
593
+ assistantParts.push({
594
+ type: 'tool-call',
595
+ toolCallId: part.toolCallId,
596
+ toolName: part.toolName,
597
+ input: part.input,
598
+ });
599
+ } else if (part?.type === 'tool-result') {
600
+ assistantParts.push({
601
+ type: 'tool-result',
602
+ toolCallId: part.toolCallId,
603
+ toolName: part.toolName,
604
+ result: part.result,
605
+ });
606
+ }
607
+ }
608
+ const assistantMessage = assistantParts.length > 0
609
+ ? [{ role: "assistant", content: assistantParts }]
390
610
  : [];
391
- const finalMessages = Array.isArray(resultMessages) && resultMessages.length > 0
392
- ? resultMessages
393
- : [...messagesInput, ...assistantMessage];
611
+ const finalMessages = [...messagesInput, ...assistantMessage];
612
+ const { requestPreview, responsePreview } = buildTracePreviews(finalMessages);
613
+ const spans = buildTraceSpansFromMessages(finalMessages);
394
614
 
395
615
  logConversation({
396
616
  userId,
@@ -404,12 +624,25 @@ ${userContextBlock || "None"}
404
624
  promptVersion: promptMeta.promptVersion,
405
625
  promptId: promptMeta.promptId,
406
626
  }),
627
+ traceId: randomUUID(),
628
+ requestPreview,
629
+ responsePreview,
630
+ state: "completed",
631
+ startedAt: startedAt.toISOString(),
632
+ endedAt: endedAt.toISOString(),
633
+ durationMs: endedAt.getTime() - startedAt.getTime(),
634
+ metadata: {
635
+ appId: clConfig.appId,
636
+ },
637
+ spans,
407
638
  }).then(() => triggerProcessing(userId, projectId, sessionId));
408
639
  }
409
640
 
410
641
  return result;
411
642
  },
412
643
  async wrapStream({ doStream, params }: { doStream: any; params: any }) {
644
+ const startedAt = new Date();
645
+ const traceId = randomUUID();
413
646
  let result;
414
647
  try {
415
648
  logger.debug("Starting doStream with params", JSON.stringify(params, null, 2));
@@ -426,7 +659,7 @@ ${userContextBlock || "None"}
426
659
  const sessionKey = `${userId}:${projectId}:${sessionId}`;
427
660
  const promptMeta = sessionPromptMetadata.get(sessionKey);
428
661
 
429
- const messagesInput = (params as any).messages || (params as any).prompt || [];
662
+ const messagesInput = (params as any).prompt || (params as any).messages || [];
430
663
  const resultMessages = (result as any)?.response?.messages;
431
664
  const finalMessages = Array.isArray(resultMessages) && resultMessages.length > 0
432
665
  ? resultMessages
@@ -434,6 +667,9 @@ ${userContextBlock || "None"}
434
667
 
435
668
  let streamUsage: Record<string, unknown> | undefined;
436
669
  let accumulatedText = '';
670
+ const toolCallInputs = new Map<string, { toolName: string; chunks: string[] }>();
671
+ const completedToolCalls: any[] = [];
672
+ const completedToolResults: any[] = [];
437
673
 
438
674
  const originalStream = result.stream;
439
675
  const transformStream = new TransformStream({
@@ -444,14 +680,64 @@ ${userContextBlock || "None"}
444
680
  if (chunk.type === 'finish' && chunk.usage) {
445
681
  streamUsage = chunk.usage;
446
682
  }
683
+ // Capture tool-call stream chunks (V2/V3 shared types)
684
+ if (chunk.type === 'tool-input-start') {
685
+ toolCallInputs.set(chunk.id, { toolName: chunk.toolName, chunks: [] });
686
+ }
687
+ if (chunk.type === 'tool-input-delta') {
688
+ const entry = toolCallInputs.get(chunk.id);
689
+ if (entry) entry.chunks.push(chunk.delta);
690
+ }
691
+ if (chunk.type === 'tool-call') {
692
+ completedToolCalls.push({
693
+ type: 'tool-call',
694
+ toolCallId: chunk.toolCallId,
695
+ toolName: chunk.toolName,
696
+ input: chunk.input,
697
+ });
698
+ }
699
+ if (chunk.type === 'tool-result') {
700
+ completedToolResults.push({
701
+ type: 'tool-result',
702
+ toolCallId: chunk.toolCallId,
703
+ toolName: chunk.toolName,
704
+ result: chunk.result,
705
+ });
706
+ }
447
707
  controller.enqueue(chunk);
448
708
  },
449
- flush() {
450
- const allMessages = accumulatedText
451
- ? [...finalMessages, { role: "assistant", content: [{ type: "text", text: accumulatedText }] }]
709
+ async flush() {
710
+ const endedAt = new Date();
711
+
712
+ // Finalize any tool calls from incremental input chunks
713
+ for (const [id, entry] of toolCallInputs) {
714
+ // Only add if not already captured via a tool-call chunk
715
+ if (!completedToolCalls.some((tc: any) => tc.toolCallId === id)) {
716
+ completedToolCalls.push({
717
+ type: 'tool-call',
718
+ toolCallId: id,
719
+ toolName: entry.toolName,
720
+ input: entry.chunks.join(''),
721
+ });
722
+ }
723
+ }
724
+
725
+ const assistantParts: any[] = [];
726
+ if (accumulatedText) assistantParts.push({ type: "text", text: accumulatedText });
727
+ for (const tc of completedToolCalls) assistantParts.push(tc);
728
+
729
+ const allMessages = assistantParts.length > 0
730
+ ? [...finalMessages, { role: "assistant", content: assistantParts }]
452
731
  : finalMessages;
453
732
 
454
- logConversation({
733
+ if (completedToolResults.length > 0) {
734
+ allMessages.push({ role: "tool", content: completedToolResults });
735
+ }
736
+
737
+ const { requestPreview, responsePreview } = buildTracePreviews(allMessages);
738
+ const spans = buildTraceSpansFromMessages(allMessages);
739
+
740
+ await logConversation({
455
741
  userId,
456
742
  projectId,
457
743
  sessionId,
@@ -463,7 +749,19 @@ ${userContextBlock || "None"}
463
749
  promptVersion: promptMeta.promptVersion,
464
750
  promptId: promptMeta.promptId,
465
751
  }),
466
- }).then(() => triggerProcessing(userId, projectId, sessionId));
752
+ traceId,
753
+ requestPreview,
754
+ responsePreview,
755
+ state: "completed",
756
+ startedAt: startedAt.toISOString(),
757
+ endedAt: endedAt.toISOString(),
758
+ durationMs: endedAt.getTime() - startedAt.getTime(),
759
+ metadata: {
760
+ appId: clConfig.appId,
761
+ },
762
+ spans,
763
+ });
764
+ triggerProcessing(userId, projectId, sessionId);
467
765
  }
468
766
  });
469
767
 
@@ -528,8 +826,10 @@ ${userContextBlock || "None"}
528
826
  }) as LanguageModel;
529
827
 
530
828
  // Track session settings on the model for use in cl.streamText/cl.generateText
531
- if (isValidId(userId) && isValidId(sessionId)) {
532
- (wrappedModel as any)[SESSION_KEY] = { userId, projectId, sessionId };
829
+ // Always store if userId is valid — sessionId may be missing but userId is still
830
+ // needed for prompt resolution (e.g. A/B test assignment)
831
+ if (isValidId(userId)) {
832
+ (wrappedModel as any)[SESSION_KEY] = { userId, projectId, sessionId: isValidId(sessionId) ? sessionId : undefined };
533
833
  }
534
834
 
535
835
  return wrappedModel;
@@ -538,61 +838,89 @@ ${userContextBlock || "None"}
538
838
  const clStreamText = async (options: CLStreamTextOptions) => {
539
839
  const { prompt: promptConfig, ...rest } = options;
540
840
 
541
- // Resolve and interpolate prompt
542
- const resolved = await resolvePrompt(promptConfig.slug);
543
- const system = promptConfig.variables
544
- ? interpolateTemplate(resolved.content, promptConfig.variables)
545
- : resolved.content;
546
-
547
- // Store prompt metadata for the session (read by middleware during logging)
548
- const session = (options.model as any)[SESSION_KEY] as { userId: string; projectId: string; sessionId: string } | undefined;
549
- if (session) {
550
- const sessionKey = `${session.userId}:${session.projectId}:${session.sessionId}`;
551
- sessionPromptMetadata.set(sessionKey, {
552
- promptSlug: resolved.slug,
553
- promptVersion: resolved.version,
554
- promptId: resolved.promptId,
555
- });
841
+ const session = (options.model as any)[SESSION_KEY] as { userId: string; projectId: string; sessionId?: string } | undefined;
842
+
843
+ // Resolve and interpolate prompt (graceful fallback on failure)
844
+ let resolved: CachedPrompt | null = null;
845
+ try {
846
+ resolved = await resolvePrompt(promptConfig.slug, session?.userId);
847
+ } catch (err) {
848
+ logger.warn(`Failed to resolve prompt "${promptConfig.slug}", streaming without system prompt.`, err);
556
849
  }
557
850
 
558
- logger.info("cl.streamText called", {
559
- slug: promptConfig.slug,
560
- version: resolved.version,
561
- systemLength: system.length,
562
- });
851
+ let system: string | undefined;
852
+ if (resolved) {
853
+ system = promptConfig.variables
854
+ ? interpolateTemplate(resolved.content, promptConfig.variables)
855
+ : resolved.content;
856
+
857
+ // Store prompt metadata for the session (read by middleware during logging)
858
+ if (session?.sessionId) {
859
+ const sessionKey = `${session.userId}:${session.projectId}:${session.sessionId}`;
860
+ sessionPromptMetadata.set(sessionKey, {
861
+ promptSlug: resolved.slug,
862
+ promptVersion: resolved.version,
863
+ promptId: resolved.promptId,
864
+ });
865
+ }
866
+
867
+ logger.info("cl.streamText called", {
868
+ slug: promptConfig.slug,
869
+ version: resolved.version,
870
+ systemLength: system.length,
871
+ });
872
+ } else {
873
+ logger.info("cl.streamText called without resolved prompt", {
874
+ slug: promptConfig.slug,
875
+ });
876
+ }
563
877
 
564
- const model = resolveModel(options.model, resolved.gatewaySlug);
565
- return aiStreamText({ ...rest, model, system } as any);
878
+ const model = resolveModel(options.model, resolved?.gatewaySlug);
879
+ return aiStreamText({ ...rest, model, ...(system && { system }) } as any);
566
880
  };
567
881
 
568
882
  const clGenerateText = async (options: CLGenerateTextOptions) => {
569
883
  const { prompt: promptConfig, ...rest } = options;
570
884
 
571
- // Resolve and interpolate prompt
572
- const resolved = await resolvePrompt(promptConfig.slug);
573
- const system = promptConfig.variables
574
- ? interpolateTemplate(resolved.content, promptConfig.variables)
575
- : resolved.content;
576
-
577
- // Store prompt metadata for the session (read by middleware during logging)
578
- const session = (options.model as any)[SESSION_KEY] as { userId: string; projectId: string; sessionId: string } | undefined;
579
- if (session) {
580
- const sessionKey = `${session.userId}:${session.projectId}:${session.sessionId}`;
581
- sessionPromptMetadata.set(sessionKey, {
582
- promptSlug: resolved.slug,
583
- promptVersion: resolved.version,
584
- promptId: resolved.promptId,
585
- });
885
+ const session = (options.model as any)[SESSION_KEY] as { userId: string; projectId: string; sessionId?: string } | undefined;
886
+
887
+ // Resolve and interpolate prompt (graceful fallback on failure)
888
+ let resolved: CachedPrompt | null = null;
889
+ try {
890
+ resolved = await resolvePrompt(promptConfig.slug, session?.userId);
891
+ } catch (err) {
892
+ logger.warn(`Failed to resolve prompt "${promptConfig.slug}", generating without system prompt.`, err);
586
893
  }
587
894
 
588
- logger.info("cl.generateText called", {
589
- slug: promptConfig.slug,
590
- version: resolved.version,
591
- systemLength: system.length,
592
- });
895
+ let system: string | undefined;
896
+ if (resolved) {
897
+ system = promptConfig.variables
898
+ ? interpolateTemplate(resolved.content, promptConfig.variables)
899
+ : resolved.content;
900
+
901
+ // Store prompt metadata for the session (read by middleware during logging)
902
+ if (session?.sessionId) {
903
+ const sessionKey = `${session.userId}:${session.projectId}:${session.sessionId}`;
904
+ sessionPromptMetadata.set(sessionKey, {
905
+ promptSlug: resolved.slug,
906
+ promptVersion: resolved.version,
907
+ promptId: resolved.promptId,
908
+ });
909
+ }
910
+
911
+ logger.info("cl.generateText called", {
912
+ slug: promptConfig.slug,
913
+ version: resolved.version,
914
+ systemLength: system.length,
915
+ });
916
+ } else {
917
+ logger.info("cl.generateText called without resolved prompt", {
918
+ slug: promptConfig.slug,
919
+ });
920
+ }
593
921
 
594
- const model = resolveModel(options.model, resolved.gatewaySlug);
595
- return aiGenerateText({ ...rest, model, system } as any);
922
+ const model = resolveModel(options.model, resolved?.gatewaySlug);
923
+ return aiGenerateText({ ...rest, model, ...(system && { system }) } as any);
596
924
  };
597
925
 
598
926
  // Return the model wrapper function with streamText/generateText attached