@kognitivedev/vercel-ai-provider 0.1.8 → 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/dist/__tests__/wrap-stream-logging.test.js +92 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +254 -16
- package/package.json +1 -1
- package/src/__tests__/wrap-stream-logging.test.ts +104 -0
- package/src/index.ts +306 -13
|
@@ -24,6 +24,98 @@ const test_1 = require("ai/test");
|
|
|
24
24
|
return new Response("not found", { status: 404 });
|
|
25
25
|
}));
|
|
26
26
|
});
|
|
27
|
+
(0, vitest_1.it)("should capture tool-call chunks and include them in logged conversation", async () => {
|
|
28
|
+
const mockModel = new test_1.MockLanguageModelV3({
|
|
29
|
+
doStream: async () => ({
|
|
30
|
+
stream: (0, test_1.convertArrayToReadableStream)([
|
|
31
|
+
{ type: "text-start", id: "t1" },
|
|
32
|
+
{ type: "text-delta", id: "t1", delta: "Let me check" },
|
|
33
|
+
{ type: "text-end", id: "t1" },
|
|
34
|
+
{
|
|
35
|
+
type: "tool-call",
|
|
36
|
+
toolCallId: "call-1",
|
|
37
|
+
toolName: "get_weather",
|
|
38
|
+
input: '{"city":"London"}',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
type: "tool-result",
|
|
42
|
+
toolCallId: "call-1",
|
|
43
|
+
toolName: "get_weather",
|
|
44
|
+
result: { temperature: 15, unit: "celsius" },
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
type: "finish",
|
|
48
|
+
finishReason: {
|
|
49
|
+
unified: "tool-calls",
|
|
50
|
+
raw: undefined,
|
|
51
|
+
},
|
|
52
|
+
usage: {
|
|
53
|
+
inputTokens: { total: 20, noCache: undefined, cacheRead: undefined, cacheWrite: undefined },
|
|
54
|
+
outputTokens: { total: 15, text: undefined, reasoning: undefined },
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
]),
|
|
58
|
+
}),
|
|
59
|
+
});
|
|
60
|
+
const mockProvider = () => mockModel;
|
|
61
|
+
const cl = (0, index_1.createCognitiveLayer)({
|
|
62
|
+
provider: mockProvider,
|
|
63
|
+
clConfig: {
|
|
64
|
+
apiKey: "test-api-key",
|
|
65
|
+
appId: "test-app",
|
|
66
|
+
projectId: "test-project",
|
|
67
|
+
processDelayMs: 0,
|
|
68
|
+
logLevel: "none",
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
const model = cl("mock-model", {
|
|
72
|
+
userId: "user-1",
|
|
73
|
+
projectId: "project-1",
|
|
74
|
+
sessionId: "session-1",
|
|
75
|
+
});
|
|
76
|
+
const result = (0, ai_1.streamText)({
|
|
77
|
+
model,
|
|
78
|
+
messages: [{ role: "user", content: "What's the weather in London?" }],
|
|
79
|
+
});
|
|
80
|
+
// Fully consume the stream
|
|
81
|
+
await result.text;
|
|
82
|
+
// Wait for async logConversation to complete
|
|
83
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
84
|
+
// Find the log call
|
|
85
|
+
const logCall = fetchCalls.find((c) => c.url.includes("/api/cognitive/log"));
|
|
86
|
+
(0, vitest_1.expect)(logCall).toBeDefined();
|
|
87
|
+
const messages = logCall.body.messages;
|
|
88
|
+
// Assistant message should contain text + tool-call parts
|
|
89
|
+
const assistantMsg = messages.find((m) => m.role === "assistant");
|
|
90
|
+
(0, vitest_1.expect)(assistantMsg).toBeDefined();
|
|
91
|
+
(0, vitest_1.expect)(assistantMsg.content).toEqual([
|
|
92
|
+
{ type: "text", text: "Let me check" },
|
|
93
|
+
{
|
|
94
|
+
type: "tool-call",
|
|
95
|
+
toolCallId: "call-1",
|
|
96
|
+
toolName: "get_weather",
|
|
97
|
+
input: '{"city":"London"}',
|
|
98
|
+
},
|
|
99
|
+
]);
|
|
100
|
+
// Tool results should be in a separate tool message
|
|
101
|
+
const toolMsg = messages.find((m) => m.role === "tool");
|
|
102
|
+
(0, vitest_1.expect)(toolMsg).toBeDefined();
|
|
103
|
+
(0, vitest_1.expect)(toolMsg.content).toEqual([
|
|
104
|
+
{
|
|
105
|
+
type: "tool-result",
|
|
106
|
+
toolCallId: "call-1",
|
|
107
|
+
toolName: "get_weather",
|
|
108
|
+
result: { temperature: 15, unit: "celsius" },
|
|
109
|
+
},
|
|
110
|
+
]);
|
|
111
|
+
// Spans should include the tool call with populated previews
|
|
112
|
+
const spans = logCall.body.spans;
|
|
113
|
+
const toolSpan = spans === null || spans === void 0 ? void 0 : spans.find((s) => s.spanType === "tool");
|
|
114
|
+
(0, vitest_1.expect)(toolSpan).toBeDefined();
|
|
115
|
+
(0, vitest_1.expect)(toolSpan.toolName).toBe("get_weather");
|
|
116
|
+
(0, vitest_1.expect)(toolSpan.inputPreview).toContain("London");
|
|
117
|
+
(0, vitest_1.expect)(toolSpan.outputPreview).toContain("15");
|
|
118
|
+
});
|
|
27
119
|
(0, vitest_1.it)("should include assistant message in logged conversation after streaming", async () => {
|
|
28
120
|
const mockModel = new test_1.MockLanguageModelV3({
|
|
29
121
|
doStream: async () => ({
|
package/dist/index.d.ts
CHANGED
|
@@ -55,6 +55,27 @@ export interface LogConversationPayload {
|
|
|
55
55
|
promptSlug?: string;
|
|
56
56
|
promptVersion?: number;
|
|
57
57
|
promptId?: string;
|
|
58
|
+
traceId?: string;
|
|
59
|
+
parentSpanId?: string;
|
|
60
|
+
requestPreview?: string;
|
|
61
|
+
responsePreview?: string;
|
|
62
|
+
state?: "active" | "completed" | "error";
|
|
63
|
+
startedAt?: string;
|
|
64
|
+
endedAt?: string;
|
|
65
|
+
durationMs?: number;
|
|
66
|
+
metadata?: Record<string, unknown>;
|
|
67
|
+
spans?: Array<{
|
|
68
|
+
spanKey: string;
|
|
69
|
+
parentSpanKey?: string;
|
|
70
|
+
name: string;
|
|
71
|
+
spanType: string;
|
|
72
|
+
status?: "active" | "completed" | "error";
|
|
73
|
+
inputPreview?: string;
|
|
74
|
+
outputPreview?: string;
|
|
75
|
+
toolName?: string;
|
|
76
|
+
errorMessage?: string;
|
|
77
|
+
metadata?: Record<string, unknown>;
|
|
78
|
+
}>;
|
|
58
79
|
}
|
|
59
80
|
export type CognitiveLayer = CLModelWrapper & {
|
|
60
81
|
streamText: (options: CLStreamTextOptions) => Promise<ReturnType<typeof aiStreamText>>;
|
package/dist/index.js
CHANGED
|
@@ -13,12 +13,25 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
14
|
exports.createCognitiveLayer = createCognitiveLayer;
|
|
15
15
|
const ai_1 = require("ai");
|
|
16
|
+
const crypto_1 = require("crypto");
|
|
16
17
|
function isValidId(value) {
|
|
17
18
|
if (value == null || typeof value !== "string")
|
|
18
19
|
return false;
|
|
19
20
|
const trimmed = value.trim();
|
|
20
21
|
return trimmed !== "" && trimmed !== "null" && trimmed !== "undefined";
|
|
21
22
|
}
|
|
23
|
+
function maskSecret(secret) {
|
|
24
|
+
if (!secret)
|
|
25
|
+
return "missing";
|
|
26
|
+
if (secret.length <= 8)
|
|
27
|
+
return `${secret.slice(0, 2)}***`;
|
|
28
|
+
return `${secret.slice(0, 4)}...${secret.slice(-4)}`;
|
|
29
|
+
}
|
|
30
|
+
function previewText(value, maxLength = 240) {
|
|
31
|
+
if (value.length <= maxLength)
|
|
32
|
+
return value;
|
|
33
|
+
return `${value.slice(0, maxLength)}...`;
|
|
34
|
+
}
|
|
22
35
|
const LOG_LEVEL_PRIORITY = {
|
|
23
36
|
none: 0,
|
|
24
37
|
error: 1,
|
|
@@ -55,6 +68,92 @@ function createLogger(logLevel) {
|
|
|
55
68
|
};
|
|
56
69
|
}
|
|
57
70
|
const PROMPT_CACHE_TTL_MS = 60000; // 1 minute
|
|
71
|
+
function getContentText(content) {
|
|
72
|
+
if (typeof content === "string")
|
|
73
|
+
return content;
|
|
74
|
+
if (!Array.isArray(content))
|
|
75
|
+
return "";
|
|
76
|
+
return content.map((part) => {
|
|
77
|
+
if (!part || typeof part !== "object")
|
|
78
|
+
return "";
|
|
79
|
+
if (typeof part.text === "string")
|
|
80
|
+
return part.text;
|
|
81
|
+
if (part.type === "tool-call" && typeof part.toolName === "string")
|
|
82
|
+
return `Called ${part.toolName}`;
|
|
83
|
+
if (part.type === "tool-result")
|
|
84
|
+
return "Received tool result";
|
|
85
|
+
return "";
|
|
86
|
+
}).filter(Boolean).join(" ");
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Unwraps V2/V3 ToolResultOutput discriminated union to a displayable value.
|
|
90
|
+
* Stream ToolResult uses plain `result` (passthrough), while prompt ToolResultPart
|
|
91
|
+
* uses `output` with a discriminated union: text, json, error-text, error-json, content, execution-denied.
|
|
92
|
+
*/
|
|
93
|
+
function extractOutputValue(raw) {
|
|
94
|
+
var _a;
|
|
95
|
+
if (raw == null)
|
|
96
|
+
return raw;
|
|
97
|
+
if (typeof raw !== 'object')
|
|
98
|
+
return raw;
|
|
99
|
+
const obj = raw;
|
|
100
|
+
if (typeof obj.type !== 'string')
|
|
101
|
+
return raw;
|
|
102
|
+
switch (obj.type) {
|
|
103
|
+
case 'text':
|
|
104
|
+
case 'json':
|
|
105
|
+
case 'error-text':
|
|
106
|
+
case 'error-json':
|
|
107
|
+
case 'content':
|
|
108
|
+
return obj.value;
|
|
109
|
+
case 'execution-denied':
|
|
110
|
+
return `Execution denied: ${(_a = obj.reason) !== null && _a !== void 0 ? _a : 'unknown'}`;
|
|
111
|
+
default:
|
|
112
|
+
return raw;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function buildTracePreviews(messages) {
|
|
116
|
+
const request = [...messages].reverse().find((message) => (message === null || message === void 0 ? void 0 : message.role) === "user");
|
|
117
|
+
const response = [...messages].reverse().find((message) => (message === null || message === void 0 ? void 0 : message.role) === "assistant");
|
|
118
|
+
return {
|
|
119
|
+
requestPreview: request ? getContentText(request.content).slice(0, 220) : "No request captured",
|
|
120
|
+
responsePreview: response ? getContentText(response.content).slice(0, 240) : "No response captured",
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function buildTraceSpansFromMessages(messages) {
|
|
124
|
+
var _a, _b;
|
|
125
|
+
const resultMap = new Map();
|
|
126
|
+
for (const message of messages) {
|
|
127
|
+
if (!Array.isArray(message === null || message === void 0 ? void 0 : message.content))
|
|
128
|
+
continue;
|
|
129
|
+
for (const part of message.content) {
|
|
130
|
+
if ((part === null || part === void 0 ? void 0 : part.type) === "tool-result" && typeof part.toolCallId === "string") {
|
|
131
|
+
resultMap.set(part.toolCallId, (_a = part.result) !== null && _a !== void 0 ? _a : part.output);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const spans = [];
|
|
136
|
+
for (const message of messages) {
|
|
137
|
+
if (!Array.isArray(message === null || message === void 0 ? void 0 : message.content))
|
|
138
|
+
continue;
|
|
139
|
+
for (const part of message.content) {
|
|
140
|
+
if ((part === null || part === void 0 ? void 0 : part.type) === "tool-call" && typeof part.toolCallId === "string") {
|
|
141
|
+
const result = resultMap.get(part.toolCallId);
|
|
142
|
+
spans.push({
|
|
143
|
+
spanKey: part.toolCallId,
|
|
144
|
+
parentSpanKey: "root",
|
|
145
|
+
name: typeof part.toolName === "string" ? part.toolName : "tool",
|
|
146
|
+
spanType: "tool",
|
|
147
|
+
status: "completed",
|
|
148
|
+
inputPreview: JSON.stringify((_b = part.input) !== null && _b !== void 0 ? _b : {}).slice(0, 220),
|
|
149
|
+
outputPreview: result != null ? JSON.stringify(extractOutputValue(result)).slice(0, 220) : "No tool result captured",
|
|
150
|
+
toolName: typeof part.toolName === "string" ? part.toolName : undefined,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return spans;
|
|
156
|
+
}
|
|
58
157
|
/**
|
|
59
158
|
* Interpolate {{variable}} placeholders in a template string.
|
|
60
159
|
* Unmatched variables are left as-is.
|
|
@@ -101,6 +200,7 @@ function createCognitiveLayer(config) {
|
|
|
101
200
|
// Prompt cache: slug → CachedPrompt
|
|
102
201
|
const promptCache = new Map();
|
|
103
202
|
const resolvePrompt = async (slug, userId) => {
|
|
203
|
+
var _a;
|
|
104
204
|
const cacheKey = userId ? `${slug}:${userId}` : slug;
|
|
105
205
|
const cached = promptCache.get(cacheKey);
|
|
106
206
|
if (cached && Date.now() - cached.fetchedAt < PROMPT_CACHE_TTL_MS) {
|
|
@@ -111,11 +211,31 @@ function createCognitiveLayer(config) {
|
|
|
111
211
|
url.searchParams.set("slug", slug);
|
|
112
212
|
if (userId)
|
|
113
213
|
url.searchParams.set("userId", userId);
|
|
214
|
+
logger.debug("Resolving prompt from backend", {
|
|
215
|
+
slug,
|
|
216
|
+
userId,
|
|
217
|
+
url: url.toString(),
|
|
218
|
+
baseUrl,
|
|
219
|
+
apiKeyHint: maskSecret(clConfig.apiKey),
|
|
220
|
+
});
|
|
114
221
|
const res = await fetch(url.toString(), {
|
|
115
222
|
headers: { "Authorization": `Bearer ${clConfig.apiKey}` },
|
|
116
223
|
});
|
|
224
|
+
logger.debug("Prompt resolve response received", {
|
|
225
|
+
slug,
|
|
226
|
+
userId,
|
|
227
|
+
status: res.status,
|
|
228
|
+
ok: res.ok,
|
|
229
|
+
contentType: res.headers.get("content-type"),
|
|
230
|
+
});
|
|
117
231
|
if (!res.ok) {
|
|
118
232
|
const body = await res.text();
|
|
233
|
+
logger.debug("Prompt resolve response body preview", {
|
|
234
|
+
slug,
|
|
235
|
+
userId,
|
|
236
|
+
status: res.status,
|
|
237
|
+
bodyPreview: previewText(body),
|
|
238
|
+
});
|
|
119
239
|
throw new Error(`Failed to resolve prompt "${slug}": ${res.status} ${body}`);
|
|
120
240
|
}
|
|
121
241
|
const data = await res.json();
|
|
@@ -128,6 +248,14 @@ function createCognitiveLayer(config) {
|
|
|
128
248
|
gatewaySlug: data.gatewaySlug,
|
|
129
249
|
};
|
|
130
250
|
promptCache.set(cacheKey, entry);
|
|
251
|
+
logger.debug("Prompt resolved payload", {
|
|
252
|
+
slug,
|
|
253
|
+
resolvedSlug: entry.slug,
|
|
254
|
+
version: entry.version,
|
|
255
|
+
promptId: entry.promptId,
|
|
256
|
+
contentLength: entry.content.length,
|
|
257
|
+
gatewaySlug: (_a = entry.gatewaySlug) !== null && _a !== void 0 ? _a : null,
|
|
258
|
+
});
|
|
131
259
|
logger.info("Prompt resolved", { slug, version: entry.version });
|
|
132
260
|
return entry;
|
|
133
261
|
};
|
|
@@ -194,9 +322,25 @@ function createCognitiveLayer(config) {
|
|
|
194
322
|
if (systemPromptToAdd === undefined) {
|
|
195
323
|
try {
|
|
196
324
|
const url = `${baseUrl}/api/cognitive/snapshot?userId=${userId}`;
|
|
325
|
+
logger.debug("Fetching snapshot from backend", {
|
|
326
|
+
userId,
|
|
327
|
+
projectId,
|
|
328
|
+
sessionId,
|
|
329
|
+
url,
|
|
330
|
+
baseUrl,
|
|
331
|
+
apiKeyHint: maskSecret(clConfig.apiKey),
|
|
332
|
+
});
|
|
197
333
|
const res = await fetch(url, {
|
|
198
334
|
headers: { "Authorization": `Bearer ${clConfig.apiKey}` },
|
|
199
335
|
});
|
|
336
|
+
logger.debug("Snapshot response received", {
|
|
337
|
+
userId,
|
|
338
|
+
projectId,
|
|
339
|
+
sessionId,
|
|
340
|
+
status: res.status,
|
|
341
|
+
ok: res.ok,
|
|
342
|
+
contentType: res.headers.get("content-type"),
|
|
343
|
+
});
|
|
200
344
|
if (res.ok) {
|
|
201
345
|
const data = await res.json();
|
|
202
346
|
const systemBlock = data.systemBlock || "";
|
|
@@ -229,7 +373,15 @@ ${userContextBlock || "None"}
|
|
|
229
373
|
});
|
|
230
374
|
}
|
|
231
375
|
else {
|
|
376
|
+
const body = await res.text();
|
|
232
377
|
logger.warn("Snapshot fetch failed", { status: res.status });
|
|
378
|
+
logger.debug("Snapshot response body preview", {
|
|
379
|
+
userId,
|
|
380
|
+
projectId,
|
|
381
|
+
sessionId,
|
|
382
|
+
status: res.status,
|
|
383
|
+
bodyPreview: previewText(body),
|
|
384
|
+
});
|
|
233
385
|
systemPromptToAdd = "";
|
|
234
386
|
sessionSnapshots.set(sessionKey, systemPromptToAdd);
|
|
235
387
|
}
|
|
@@ -255,7 +407,8 @@ ${userContextBlock || "None"}
|
|
|
255
407
|
return Object.assign(Object.assign({}, nextParams), { prompt: messagesWithMemory });
|
|
256
408
|
},
|
|
257
409
|
async wrapGenerate({ doGenerate, params }) {
|
|
258
|
-
var _a
|
|
410
|
+
var _a;
|
|
411
|
+
const startedAt = new Date();
|
|
259
412
|
let result;
|
|
260
413
|
try {
|
|
261
414
|
result = await doGenerate();
|
|
@@ -266,28 +419,57 @@ ${userContextBlock || "None"}
|
|
|
266
419
|
throw err;
|
|
267
420
|
}
|
|
268
421
|
if (isValidId(userId) && isValidId(sessionId)) {
|
|
422
|
+
const endedAt = new Date();
|
|
269
423
|
const sessionKey = `${userId}:${projectId}:${sessionId}`;
|
|
270
424
|
const promptMeta = sessionPromptMetadata.get(sessionKey);
|
|
271
|
-
const messagesInput = params.
|
|
272
|
-
|
|
273
|
-
const
|
|
274
|
-
|
|
425
|
+
const messagesInput = params.prompt || params.messages || [];
|
|
426
|
+
// Build assistant message from result.content (V2/V3 GenerateResult)
|
|
427
|
+
const resultContent = Array.isArray(result === null || result === void 0 ? void 0 : result.content) ? result.content : [];
|
|
428
|
+
const assistantParts = [];
|
|
429
|
+
for (const part of resultContent) {
|
|
430
|
+
if ((part === null || part === void 0 ? void 0 : part.type) === 'text') {
|
|
431
|
+
assistantParts.push({ type: 'text', text: part.text });
|
|
432
|
+
}
|
|
433
|
+
else if ((part === null || part === void 0 ? void 0 : part.type) === 'tool-call') {
|
|
434
|
+
assistantParts.push({
|
|
435
|
+
type: 'tool-call',
|
|
436
|
+
toolCallId: part.toolCallId,
|
|
437
|
+
toolName: part.toolName,
|
|
438
|
+
input: part.input,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
else if ((part === null || part === void 0 ? void 0 : part.type) === 'tool-result') {
|
|
442
|
+
assistantParts.push({
|
|
443
|
+
type: 'tool-result',
|
|
444
|
+
toolCallId: part.toolCallId,
|
|
445
|
+
toolName: part.toolName,
|
|
446
|
+
result: part.result,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
const assistantMessage = assistantParts.length > 0
|
|
451
|
+
? [{ role: "assistant", content: assistantParts }]
|
|
275
452
|
: [];
|
|
276
|
-
const finalMessages =
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
logConversation(Object.assign({ userId,
|
|
453
|
+
const finalMessages = [...messagesInput, ...assistantMessage];
|
|
454
|
+
const { requestPreview, responsePreview } = buildTracePreviews(finalMessages);
|
|
455
|
+
const spans = buildTraceSpansFromMessages(finalMessages);
|
|
456
|
+
logConversation(Object.assign(Object.assign({ userId,
|
|
280
457
|
projectId,
|
|
281
458
|
sessionId, messages: finalMessages, modelId, usage: result.usage }, (promptMeta && {
|
|
282
459
|
promptSlug: promptMeta.promptSlug,
|
|
283
460
|
promptVersion: promptMeta.promptVersion,
|
|
284
461
|
promptId: promptMeta.promptId,
|
|
285
|
-
}))
|
|
462
|
+
})), { traceId: (0, crypto_1.randomUUID)(), requestPreview,
|
|
463
|
+
responsePreview, state: "completed", startedAt: startedAt.toISOString(), endedAt: endedAt.toISOString(), durationMs: endedAt.getTime() - startedAt.getTime(), metadata: {
|
|
464
|
+
appId: clConfig.appId,
|
|
465
|
+
}, spans })).then(() => triggerProcessing(userId, projectId, sessionId));
|
|
286
466
|
}
|
|
287
467
|
return result;
|
|
288
468
|
},
|
|
289
469
|
async wrapStream({ doStream, params }) {
|
|
290
470
|
var _a;
|
|
471
|
+
const startedAt = new Date();
|
|
472
|
+
const traceId = (0, crypto_1.randomUUID)();
|
|
291
473
|
let result;
|
|
292
474
|
try {
|
|
293
475
|
logger.debug("Starting doStream with params", JSON.stringify(params, null, 2));
|
|
@@ -302,13 +484,16 @@ ${userContextBlock || "None"}
|
|
|
302
484
|
if (isValidId(userId) && isValidId(sessionId)) {
|
|
303
485
|
const sessionKey = `${userId}:${projectId}:${sessionId}`;
|
|
304
486
|
const promptMeta = sessionPromptMetadata.get(sessionKey);
|
|
305
|
-
const messagesInput = params.
|
|
487
|
+
const messagesInput = params.prompt || params.messages || [];
|
|
306
488
|
const resultMessages = (_a = result === null || result === void 0 ? void 0 : result.response) === null || _a === void 0 ? void 0 : _a.messages;
|
|
307
489
|
const finalMessages = Array.isArray(resultMessages) && resultMessages.length > 0
|
|
308
490
|
? resultMessages
|
|
309
491
|
: messagesInput;
|
|
310
492
|
let streamUsage;
|
|
311
493
|
let accumulatedText = '';
|
|
494
|
+
const toolCallInputs = new Map();
|
|
495
|
+
const completedToolCalls = [];
|
|
496
|
+
const completedToolResults = [];
|
|
312
497
|
const originalStream = result.stream;
|
|
313
498
|
const transformStream = new TransformStream({
|
|
314
499
|
transform(chunk, controller) {
|
|
@@ -318,19 +503,72 @@ ${userContextBlock || "None"}
|
|
|
318
503
|
if (chunk.type === 'finish' && chunk.usage) {
|
|
319
504
|
streamUsage = chunk.usage;
|
|
320
505
|
}
|
|
506
|
+
// Capture tool-call stream chunks (V2/V3 shared types)
|
|
507
|
+
if (chunk.type === 'tool-input-start') {
|
|
508
|
+
toolCallInputs.set(chunk.id, { toolName: chunk.toolName, chunks: [] });
|
|
509
|
+
}
|
|
510
|
+
if (chunk.type === 'tool-input-delta') {
|
|
511
|
+
const entry = toolCallInputs.get(chunk.id);
|
|
512
|
+
if (entry)
|
|
513
|
+
entry.chunks.push(chunk.delta);
|
|
514
|
+
}
|
|
515
|
+
if (chunk.type === 'tool-call') {
|
|
516
|
+
completedToolCalls.push({
|
|
517
|
+
type: 'tool-call',
|
|
518
|
+
toolCallId: chunk.toolCallId,
|
|
519
|
+
toolName: chunk.toolName,
|
|
520
|
+
input: chunk.input,
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
if (chunk.type === 'tool-result') {
|
|
524
|
+
completedToolResults.push({
|
|
525
|
+
type: 'tool-result',
|
|
526
|
+
toolCallId: chunk.toolCallId,
|
|
527
|
+
toolName: chunk.toolName,
|
|
528
|
+
result: chunk.result,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
321
531
|
controller.enqueue(chunk);
|
|
322
532
|
},
|
|
323
|
-
flush() {
|
|
324
|
-
const
|
|
325
|
-
|
|
533
|
+
async flush() {
|
|
534
|
+
const endedAt = new Date();
|
|
535
|
+
// Finalize any tool calls from incremental input chunks
|
|
536
|
+
for (const [id, entry] of toolCallInputs) {
|
|
537
|
+
// Only add if not already captured via a tool-call chunk
|
|
538
|
+
if (!completedToolCalls.some((tc) => tc.toolCallId === id)) {
|
|
539
|
+
completedToolCalls.push({
|
|
540
|
+
type: 'tool-call',
|
|
541
|
+
toolCallId: id,
|
|
542
|
+
toolName: entry.toolName,
|
|
543
|
+
input: entry.chunks.join(''),
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
const assistantParts = [];
|
|
548
|
+
if (accumulatedText)
|
|
549
|
+
assistantParts.push({ type: "text", text: accumulatedText });
|
|
550
|
+
for (const tc of completedToolCalls)
|
|
551
|
+
assistantParts.push(tc);
|
|
552
|
+
const allMessages = assistantParts.length > 0
|
|
553
|
+
? [...finalMessages, { role: "assistant", content: assistantParts }]
|
|
326
554
|
: finalMessages;
|
|
327
|
-
|
|
555
|
+
if (completedToolResults.length > 0) {
|
|
556
|
+
allMessages.push({ role: "tool", content: completedToolResults });
|
|
557
|
+
}
|
|
558
|
+
const { requestPreview, responsePreview } = buildTracePreviews(allMessages);
|
|
559
|
+
const spans = buildTraceSpansFromMessages(allMessages);
|
|
560
|
+
await logConversation(Object.assign(Object.assign({ userId,
|
|
328
561
|
projectId,
|
|
329
562
|
sessionId, messages: allMessages, modelId, usage: streamUsage }, (promptMeta && {
|
|
330
563
|
promptSlug: promptMeta.promptSlug,
|
|
331
564
|
promptVersion: promptMeta.promptVersion,
|
|
332
565
|
promptId: promptMeta.promptId,
|
|
333
|
-
}))
|
|
566
|
+
})), { traceId,
|
|
567
|
+
requestPreview,
|
|
568
|
+
responsePreview, state: "completed", startedAt: startedAt.toISOString(), endedAt: endedAt.toISOString(), durationMs: endedAt.getTime() - startedAt.getTime(), metadata: {
|
|
569
|
+
appId: clConfig.appId,
|
|
570
|
+
}, spans }));
|
|
571
|
+
triggerProcessing(userId, projectId, sessionId);
|
|
334
572
|
}
|
|
335
573
|
});
|
|
336
574
|
result.stream = originalStream.pipeThrough(transformStream);
|
package/package.json
CHANGED
|
@@ -36,6 +36,110 @@ describe("wrapStream logging", () => {
|
|
|
36
36
|
);
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
+
it("should capture tool-call chunks and include them in logged conversation", async () => {
|
|
40
|
+
const mockModel = new MockLanguageModelV3({
|
|
41
|
+
doStream: async () => ({
|
|
42
|
+
stream: convertArrayToReadableStream([
|
|
43
|
+
{ type: "text-start" as const, id: "t1" },
|
|
44
|
+
{ type: "text-delta" as const, id: "t1", delta: "Let me check" },
|
|
45
|
+
{ type: "text-end" as const, id: "t1" },
|
|
46
|
+
{
|
|
47
|
+
type: "tool-call" as const,
|
|
48
|
+
toolCallId: "call-1",
|
|
49
|
+
toolName: "get_weather",
|
|
50
|
+
input: '{"city":"London"}',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
type: "tool-result" as const,
|
|
54
|
+
toolCallId: "call-1",
|
|
55
|
+
toolName: "get_weather",
|
|
56
|
+
result: { temperature: 15, unit: "celsius" },
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
type: "finish" as const,
|
|
60
|
+
finishReason: {
|
|
61
|
+
unified: "tool-calls" as const,
|
|
62
|
+
raw: undefined,
|
|
63
|
+
},
|
|
64
|
+
usage: {
|
|
65
|
+
inputTokens: { total: 20, noCache: undefined, cacheRead: undefined, cacheWrite: undefined },
|
|
66
|
+
outputTokens: { total: 15, text: undefined, reasoning: undefined },
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
] satisfies import("@ai-sdk/provider").LanguageModelV3StreamPart[]),
|
|
70
|
+
}),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const mockProvider = () => mockModel;
|
|
74
|
+
|
|
75
|
+
const cl = createCognitiveLayer({
|
|
76
|
+
provider: mockProvider,
|
|
77
|
+
clConfig: {
|
|
78
|
+
apiKey: "test-api-key",
|
|
79
|
+
appId: "test-app",
|
|
80
|
+
projectId: "test-project",
|
|
81
|
+
processDelayMs: 0,
|
|
82
|
+
logLevel: "none",
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const model = cl("mock-model", {
|
|
87
|
+
userId: "user-1",
|
|
88
|
+
projectId: "project-1",
|
|
89
|
+
sessionId: "session-1",
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const result = streamText({
|
|
93
|
+
model,
|
|
94
|
+
messages: [{ role: "user", content: "What's the weather in London?" }],
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Fully consume the stream
|
|
98
|
+
await result.text;
|
|
99
|
+
|
|
100
|
+
// Wait for async logConversation to complete
|
|
101
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
102
|
+
|
|
103
|
+
// Find the log call
|
|
104
|
+
const logCall = fetchCalls.find((c) => c.url.includes("/api/cognitive/log"));
|
|
105
|
+
expect(logCall).toBeDefined();
|
|
106
|
+
|
|
107
|
+
const messages = logCall!.body.messages;
|
|
108
|
+
|
|
109
|
+
// Assistant message should contain text + tool-call parts
|
|
110
|
+
const assistantMsg = messages.find((m: any) => m.role === "assistant");
|
|
111
|
+
expect(assistantMsg).toBeDefined();
|
|
112
|
+
expect(assistantMsg.content).toEqual([
|
|
113
|
+
{ type: "text", text: "Let me check" },
|
|
114
|
+
{
|
|
115
|
+
type: "tool-call",
|
|
116
|
+
toolCallId: "call-1",
|
|
117
|
+
toolName: "get_weather",
|
|
118
|
+
input: '{"city":"London"}',
|
|
119
|
+
},
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
// Tool results should be in a separate tool message
|
|
123
|
+
const toolMsg = messages.find((m: any) => m.role === "tool");
|
|
124
|
+
expect(toolMsg).toBeDefined();
|
|
125
|
+
expect(toolMsg.content).toEqual([
|
|
126
|
+
{
|
|
127
|
+
type: "tool-result",
|
|
128
|
+
toolCallId: "call-1",
|
|
129
|
+
toolName: "get_weather",
|
|
130
|
+
result: { temperature: 15, unit: "celsius" },
|
|
131
|
+
},
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
// Spans should include the tool call with populated previews
|
|
135
|
+
const spans = logCall!.body.spans;
|
|
136
|
+
const toolSpan = spans?.find((s: any) => s.spanType === "tool");
|
|
137
|
+
expect(toolSpan).toBeDefined();
|
|
138
|
+
expect(toolSpan.toolName).toBe("get_weather");
|
|
139
|
+
expect(toolSpan.inputPreview).toContain("London");
|
|
140
|
+
expect(toolSpan.outputPreview).toContain("15");
|
|
141
|
+
});
|
|
142
|
+
|
|
39
143
|
it("should include assistant message in logged conversation after streaming", async () => {
|
|
40
144
|
const mockModel = new MockLanguageModelV3({
|
|
41
145
|
doStream: async () => ({
|
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,6 +123,27 @@ 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 & {
|
|
@@ -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.
|
|
@@ -206,11 +344,32 @@ export function createCognitiveLayer(config: {
|
|
|
206
344
|
url.searchParams.set("slug", slug);
|
|
207
345
|
if (userId) url.searchParams.set("userId", userId);
|
|
208
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
|
+
|
|
209
355
|
const res = await fetch(url.toString(), {
|
|
210
356
|
headers: { "Authorization": `Bearer ${clConfig.apiKey}` },
|
|
211
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
|
+
});
|
|
212
365
|
if (!res.ok) {
|
|
213
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
|
+
});
|
|
214
373
|
throw new Error(`Failed to resolve prompt "${slug}": ${res.status} ${body}`);
|
|
215
374
|
}
|
|
216
375
|
|
|
@@ -224,6 +383,14 @@ export function createCognitiveLayer(config: {
|
|
|
224
383
|
gatewaySlug: data.gatewaySlug,
|
|
225
384
|
};
|
|
226
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
|
+
});
|
|
227
394
|
logger.info("Prompt resolved", { slug, version: entry.version });
|
|
228
395
|
return entry;
|
|
229
396
|
};
|
|
@@ -306,9 +473,25 @@ export function createCognitiveLayer(config: {
|
|
|
306
473
|
if (systemPromptToAdd === undefined) {
|
|
307
474
|
try {
|
|
308
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
|
+
});
|
|
309
484
|
const res = await fetch(url, {
|
|
310
485
|
headers: { "Authorization": `Bearer ${clConfig.apiKey}` },
|
|
311
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
|
+
});
|
|
312
495
|
if (res.ok) {
|
|
313
496
|
const data = await res.json();
|
|
314
497
|
const systemBlock = data.systemBlock || "";
|
|
@@ -342,7 +525,15 @@ ${userContextBlock || "None"}
|
|
|
342
525
|
rawData: data,
|
|
343
526
|
});
|
|
344
527
|
} else {
|
|
528
|
+
const body = await res.text();
|
|
345
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
|
+
});
|
|
346
537
|
systemPromptToAdd = "";
|
|
347
538
|
sessionSnapshots.set(sessionKey, systemPromptToAdd);
|
|
348
539
|
}
|
|
@@ -375,6 +566,7 @@ ${userContextBlock || "None"}
|
|
|
375
566
|
},
|
|
376
567
|
|
|
377
568
|
async wrapGenerate({ doGenerate, params }: { doGenerate: any; params: any }) {
|
|
569
|
+
const startedAt = new Date();
|
|
378
570
|
let result;
|
|
379
571
|
try {
|
|
380
572
|
result = await doGenerate();
|
|
@@ -385,17 +577,40 @@ ${userContextBlock || "None"}
|
|
|
385
577
|
}
|
|
386
578
|
|
|
387
579
|
if (isValidId(userId) && isValidId(sessionId)) {
|
|
580
|
+
const endedAt = new Date();
|
|
388
581
|
const sessionKey = `${userId}:${projectId}:${sessionId}`;
|
|
389
582
|
const promptMeta = sessionPromptMetadata.get(sessionKey);
|
|
390
583
|
|
|
391
|
-
const messagesInput = (params as any).
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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 }]
|
|
395
610
|
: [];
|
|
396
|
-
const finalMessages =
|
|
397
|
-
|
|
398
|
-
|
|
611
|
+
const finalMessages = [...messagesInput, ...assistantMessage];
|
|
612
|
+
const { requestPreview, responsePreview } = buildTracePreviews(finalMessages);
|
|
613
|
+
const spans = buildTraceSpansFromMessages(finalMessages);
|
|
399
614
|
|
|
400
615
|
logConversation({
|
|
401
616
|
userId,
|
|
@@ -409,12 +624,25 @@ ${userContextBlock || "None"}
|
|
|
409
624
|
promptVersion: promptMeta.promptVersion,
|
|
410
625
|
promptId: promptMeta.promptId,
|
|
411
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,
|
|
412
638
|
}).then(() => triggerProcessing(userId, projectId, sessionId));
|
|
413
639
|
}
|
|
414
640
|
|
|
415
641
|
return result;
|
|
416
642
|
},
|
|
417
643
|
async wrapStream({ doStream, params }: { doStream: any; params: any }) {
|
|
644
|
+
const startedAt = new Date();
|
|
645
|
+
const traceId = randomUUID();
|
|
418
646
|
let result;
|
|
419
647
|
try {
|
|
420
648
|
logger.debug("Starting doStream with params", JSON.stringify(params, null, 2));
|
|
@@ -431,7 +659,7 @@ ${userContextBlock || "None"}
|
|
|
431
659
|
const sessionKey = `${userId}:${projectId}:${sessionId}`;
|
|
432
660
|
const promptMeta = sessionPromptMetadata.get(sessionKey);
|
|
433
661
|
|
|
434
|
-
const messagesInput = (params as any).
|
|
662
|
+
const messagesInput = (params as any).prompt || (params as any).messages || [];
|
|
435
663
|
const resultMessages = (result as any)?.response?.messages;
|
|
436
664
|
const finalMessages = Array.isArray(resultMessages) && resultMessages.length > 0
|
|
437
665
|
? resultMessages
|
|
@@ -439,6 +667,9 @@ ${userContextBlock || "None"}
|
|
|
439
667
|
|
|
440
668
|
let streamUsage: Record<string, unknown> | undefined;
|
|
441
669
|
let accumulatedText = '';
|
|
670
|
+
const toolCallInputs = new Map<string, { toolName: string; chunks: string[] }>();
|
|
671
|
+
const completedToolCalls: any[] = [];
|
|
672
|
+
const completedToolResults: any[] = [];
|
|
442
673
|
|
|
443
674
|
const originalStream = result.stream;
|
|
444
675
|
const transformStream = new TransformStream({
|
|
@@ -449,14 +680,64 @@ ${userContextBlock || "None"}
|
|
|
449
680
|
if (chunk.type === 'finish' && chunk.usage) {
|
|
450
681
|
streamUsage = chunk.usage;
|
|
451
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
|
+
}
|
|
452
707
|
controller.enqueue(chunk);
|
|
453
708
|
},
|
|
454
|
-
flush() {
|
|
455
|
-
const
|
|
456
|
-
|
|
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 }]
|
|
457
731
|
: finalMessages;
|
|
458
732
|
|
|
459
|
-
|
|
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({
|
|
460
741
|
userId,
|
|
461
742
|
projectId,
|
|
462
743
|
sessionId,
|
|
@@ -468,7 +749,19 @@ ${userContextBlock || "None"}
|
|
|
468
749
|
promptVersion: promptMeta.promptVersion,
|
|
469
750
|
promptId: promptMeta.promptId,
|
|
470
751
|
}),
|
|
471
|
-
|
|
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);
|
|
472
765
|
}
|
|
473
766
|
});
|
|
474
767
|
|