@browserbasehq/orca 3.2.0-preview.2 → 3.2.0-preview.3
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/cjs/lib/utils.d.ts +0 -1
- package/dist/cjs/lib/utils.js +0 -4
- package/dist/cjs/lib/utils.js.map +1 -1
- package/dist/cjs/lib/v3/agent/AnthropicCUAClient.js +6 -4
- package/dist/cjs/lib/v3/agent/AnthropicCUAClient.js.map +1 -1
- package/dist/cjs/lib/v3/agent/GoogleCUAClient.js +6 -4
- package/dist/cjs/lib/v3/agent/GoogleCUAClient.js.map +1 -1
- package/dist/cjs/lib/v3/agent/OpenAICUAClient.js +6 -4
- package/dist/cjs/lib/v3/agent/OpenAICUAClient.js.map +1 -1
- package/dist/cjs/lib/v3/flowLogger.d.ts +103 -62
- package/dist/cjs/lib/v3/flowLogger.js +773 -362
- package/dist/cjs/lib/v3/flowLogger.js.map +1 -1
- package/dist/cjs/lib/v3/handlers/handlerUtils/actHandlerUtils.js +33 -21
- package/dist/cjs/lib/v3/handlers/handlerUtils/actHandlerUtils.js.map +1 -1
- package/dist/cjs/lib/v3/handlers/v3AgentHandler.d.ts +4 -0
- package/dist/cjs/lib/v3/handlers/v3AgentHandler.js +31 -1
- package/dist/cjs/lib/v3/handlers/v3AgentHandler.js.map +1 -1
- package/dist/cjs/lib/v3/handlers/v3CuaAgentHandler.js +12 -10
- package/dist/cjs/lib/v3/handlers/v3CuaAgentHandler.js.map +1 -1
- package/dist/cjs/lib/v3/llm/aisdk.js +16 -10
- package/dist/cjs/lib/v3/llm/aisdk.js.map +1 -1
- package/dist/cjs/lib/v3/types/public/options.d.ts +0 -5
- package/dist/cjs/lib/v3/types/public/options.js.map +1 -1
- package/dist/cjs/lib/v3/understudy/cdp.d.ts +12 -3
- package/dist/cjs/lib/v3/understudy/cdp.js +10 -83
- package/dist/cjs/lib/v3/understudy/cdp.js.map +1 -1
- package/dist/cjs/lib/v3/understudy/page.js +17 -32
- package/dist/cjs/lib/v3/understudy/page.js.map +1 -1
- package/dist/cjs/lib/v3/v3.d.ts +0 -5
- package/dist/cjs/lib/v3/v3.js +157 -174
- package/dist/cjs/lib/v3/v3.js.map +1 -1
- package/dist/esm/lib/utils.d.ts +0 -1
- package/dist/esm/lib/utils.js +0 -3
- package/dist/esm/lib/utils.js.map +1 -1
- package/dist/esm/lib/v3/agent/AnthropicCUAClient.js +7 -5
- package/dist/esm/lib/v3/agent/AnthropicCUAClient.js.map +1 -1
- package/dist/esm/lib/v3/agent/GoogleCUAClient.js +7 -5
- package/dist/esm/lib/v3/agent/GoogleCUAClient.js.map +1 -1
- package/dist/esm/lib/v3/agent/OpenAICUAClient.js +7 -5
- package/dist/esm/lib/v3/agent/OpenAICUAClient.js.map +1 -1
- package/dist/esm/lib/v3/flowLogger.d.ts +103 -62
- package/dist/esm/lib/v3/flowLogger.js +762 -356
- package/dist/esm/lib/v3/flowLogger.js.map +1 -1
- package/dist/esm/lib/v3/handlers/handlerUtils/actHandlerUtils.js +34 -22
- package/dist/esm/lib/v3/handlers/handlerUtils/actHandlerUtils.js.map +1 -1
- package/dist/esm/lib/v3/handlers/v3AgentHandler.d.ts +4 -0
- package/dist/esm/lib/v3/handlers/v3AgentHandler.js +32 -2
- package/dist/esm/lib/v3/handlers/v3AgentHandler.js.map +1 -1
- package/dist/esm/lib/v3/handlers/v3CuaAgentHandler.js +13 -11
- package/dist/esm/lib/v3/handlers/v3CuaAgentHandler.js.map +1 -1
- package/dist/esm/lib/v3/llm/aisdk.js +17 -11
- package/dist/esm/lib/v3/llm/aisdk.js.map +1 -1
- package/dist/esm/lib/v3/types/public/options.d.ts +0 -5
- package/dist/esm/lib/v3/types/public/options.js.map +1 -1
- package/dist/esm/lib/v3/understudy/cdp.d.ts +12 -3
- package/dist/esm/lib/v3/understudy/cdp.js +10 -83
- package/dist/esm/lib/v3/understudy/cdp.js.map +1 -1
- package/dist/esm/lib/v3/understudy/page.js +18 -33
- package/dist/esm/lib/v3/understudy/page.js.map +1 -1
- package/dist/esm/lib/v3/v3.d.ts +0 -5
- package/dist/esm/lib/v3/v3.js +158 -175
- package/dist/esm/lib/v3/v3.js.map +1 -1
- package/package.json +1 -1
- package/dist/cjs/lib/v3/eventStore.d.ts +0 -41
- package/dist/cjs/lib/v3/eventStore.js +0 -375
- package/dist/cjs/lib/v3/eventStore.js.map +0 -1
- package/dist/esm/lib/v3/eventStore.d.ts +0 -41
- package/dist/esm/lib/v3/eventStore.js +0 -363
- package/dist/esm/lib/v3/eventStore.js.map +0 -1
|
@@ -1,242 +1,724 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { Writable } from "node:stream";
|
|
2
4
|
import { v7 as uuidv7 } from "uuid";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import pino from "pino";
|
|
7
|
+
// =============================================================================
|
|
8
|
+
// Constants
|
|
9
|
+
// =============================================================================
|
|
10
|
+
const MAX_LINE_LENGTH = 160;
|
|
11
|
+
// Flow logging config dir - empty string disables logging entirely
|
|
12
|
+
const CONFIG_DIR = process.env.BROWSERBASE_CONFIG_DIR || "";
|
|
13
|
+
const NOISY_CDP_EVENTS = new Set([
|
|
14
|
+
"Target.targetInfoChanged",
|
|
15
|
+
"Runtime.executionContextCreated",
|
|
16
|
+
"Runtime.executionContextDestroyed",
|
|
17
|
+
"Runtime.executionContextsCleared",
|
|
18
|
+
"Page.lifecycleEvent",
|
|
19
|
+
"Network.dataReceived",
|
|
20
|
+
"Network.loadingFinished",
|
|
21
|
+
"Network.requestWillBeSentExtraInfo",
|
|
22
|
+
"Network.responseReceivedExtraInfo",
|
|
23
|
+
"Network.requestWillBeSent",
|
|
24
|
+
"Network.responseReceived",
|
|
25
|
+
]);
|
|
26
|
+
const loggerContext = new AsyncLocalStorage();
|
|
27
|
+
// =============================================================================
|
|
28
|
+
// Formatting Utilities (used by pretty streams)
|
|
29
|
+
// =============================================================================
|
|
30
|
+
/** Calculate base64 data size in KB */
|
|
31
|
+
const dataToKb = (data) => ((data.length * 0.75) / 1024).toFixed(1);
|
|
32
|
+
/** Truncate CDP IDs: frameId:363F03EB...EF8 → frameId:363F…5EF8 */
|
|
33
|
+
function truncateCdpIds(value) {
|
|
34
|
+
return value.replace(/([iI]d:?"?)([0-9A-F]{32})(?="?[,})\s]|$)/g, (_, pre, id) => `${pre}${id.slice(0, 4)}…${id.slice(-4)}`);
|
|
35
|
+
}
|
|
36
|
+
/** Truncate line showing start...end */
|
|
37
|
+
function truncateLine(value, maxLen) {
|
|
38
|
+
const collapsed = value.replace(/\s+/g, " ");
|
|
39
|
+
if (collapsed.length <= maxLen)
|
|
40
|
+
return collapsed;
|
|
41
|
+
const endLen = Math.floor(maxLen * 0.3);
|
|
42
|
+
const startLen = maxLen - endLen - 1;
|
|
43
|
+
return `${collapsed.slice(0, startLen)}…${collapsed.slice(-endLen)}`;
|
|
44
|
+
}
|
|
45
|
+
function formatValue(value) {
|
|
46
|
+
if (typeof value === "string")
|
|
47
|
+
return `'${value}'`;
|
|
48
|
+
if (value == null || typeof value !== "object")
|
|
49
|
+
return String(value);
|
|
50
|
+
try {
|
|
51
|
+
return JSON.stringify(value);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return "[unserializable]";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function formatArgs(args) {
|
|
58
|
+
if (args === undefined)
|
|
59
|
+
return "";
|
|
60
|
+
return (Array.isArray(args) ? args : [args])
|
|
61
|
+
.filter((e) => e !== undefined)
|
|
62
|
+
.map(formatValue)
|
|
63
|
+
.filter((e) => e.length > 0)
|
|
64
|
+
.join(", ");
|
|
65
|
+
}
|
|
66
|
+
const shortId = (id) => id ? id.slice(-4) : "-";
|
|
67
|
+
function formatTag(label, id, icon) {
|
|
68
|
+
return id ? `[${icon} #${shortId(id)}${label ? " " + label : ""}]` : "⤑";
|
|
69
|
+
}
|
|
70
|
+
let nonce = 0;
|
|
71
|
+
function formatTimestamp() {
|
|
72
|
+
const d = new Date();
|
|
73
|
+
const pad = (n, w = 2) => String(n).padStart(w, "0");
|
|
74
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}${pad(nonce++ % 100)}`;
|
|
75
|
+
}
|
|
76
|
+
const SENSITIVE_KEYS = /apikey|api_key|key|secret|token|password|passwd|pwd|credential|auth/i;
|
|
77
|
+
function sanitizeOptions(options) {
|
|
78
|
+
const sanitize = (obj) => {
|
|
79
|
+
if (typeof obj !== "object" || obj === null)
|
|
80
|
+
return obj;
|
|
81
|
+
if (Array.isArray(obj))
|
|
82
|
+
return obj.map(sanitize);
|
|
83
|
+
const result = {};
|
|
84
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
85
|
+
result[key] = SENSITIVE_KEYS.test(key) ? "******" : sanitize(value);
|
|
86
|
+
}
|
|
87
|
+
return result;
|
|
88
|
+
};
|
|
89
|
+
return sanitize({ ...options });
|
|
90
|
+
}
|
|
91
|
+
/** Remove unescaped quotes for cleaner log output */
|
|
92
|
+
function removeQuotes(str) {
|
|
93
|
+
return str
|
|
94
|
+
.replace(/([^\\])["']/g, "$1")
|
|
95
|
+
.replace(/^["']|["']$/g, "")
|
|
96
|
+
.trim();
|
|
97
|
+
}
|
|
98
|
+
// =============================================================================
|
|
99
|
+
// Pretty Formatting (converts FlowEvent to human-readable log line)
|
|
100
|
+
// =============================================================================
|
|
101
|
+
function prettifyEvent(event) {
|
|
102
|
+
const parts = [];
|
|
103
|
+
// Build context tags - always add parent span tags (formatTag returns ⤑ for null IDs)
|
|
104
|
+
if (event.category === "AgentTask") {
|
|
105
|
+
parts.push(formatTag("", event.taskId, "🅰"));
|
|
106
|
+
}
|
|
107
|
+
else if (event.category === "StagehandStep") {
|
|
108
|
+
parts.push(formatTag("", event.taskId, "🅰"));
|
|
109
|
+
parts.push(formatTag(event.stepLabel, event.stepId, "🆂"));
|
|
110
|
+
}
|
|
111
|
+
else if (event.category === "UnderstudyAction") {
|
|
112
|
+
parts.push(formatTag("", event.taskId, "🅰"));
|
|
113
|
+
parts.push(formatTag(event.stepLabel, event.stepId, "🆂"));
|
|
114
|
+
parts.push(formatTag(event.actionLabel, event.actionId, "🆄"));
|
|
115
|
+
}
|
|
116
|
+
else if (event.category === "CDP") {
|
|
117
|
+
parts.push(formatTag("", event.taskId, "🅰"));
|
|
118
|
+
parts.push(formatTag(event.stepLabel, event.stepId, "🆂"));
|
|
119
|
+
parts.push(formatTag(event.actionLabel, event.actionId, "🆄"));
|
|
120
|
+
parts.push(formatTag("CDP", event.targetId, "🅲"));
|
|
121
|
+
}
|
|
122
|
+
else if (event.category === "LLM") {
|
|
123
|
+
parts.push(formatTag("", event.taskId, "🅰"));
|
|
124
|
+
parts.push(formatTag(event.stepLabel, event.stepId, "🆂"));
|
|
125
|
+
parts.push(formatTag("LLM", event.requestId, "🧠"));
|
|
126
|
+
}
|
|
127
|
+
// Build details based on event type
|
|
128
|
+
let details = "";
|
|
129
|
+
const argsStr = event.params ? formatArgs(event.params) : "";
|
|
130
|
+
if (event.category === "AgentTask") {
|
|
131
|
+
if (event.event === "started") {
|
|
132
|
+
details = `▷ ${event.method}(${argsStr})`;
|
|
133
|
+
}
|
|
134
|
+
else if (event.event === "completed") {
|
|
135
|
+
const m = event.metrics;
|
|
136
|
+
const durationSec = m?.durationMs
|
|
137
|
+
? (m.durationMs / 1000).toFixed(1)
|
|
138
|
+
: "?";
|
|
139
|
+
const llmStats = `${m?.llmRequests ?? 0} LLM calls ꜛ${m?.inputTokens ?? 0} ꜜ${m?.outputTokens ?? 0} tokens`;
|
|
140
|
+
const cdpStats = `${m?.cdpEvents ?? 0} CDP msgs`;
|
|
141
|
+
details = `✓ Agent.execute() DONE in ${durationSec}s | ${llmStats} | ${cdpStats}`;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else if (event.category === "StagehandStep") {
|
|
145
|
+
if (event.event === "started") {
|
|
146
|
+
details = `▷ ${event.method}(${argsStr})`;
|
|
147
|
+
}
|
|
148
|
+
else if (event.event === "completed") {
|
|
149
|
+
const durationSec = event.metrics?.durationMs
|
|
150
|
+
? (event.metrics.durationMs / 1000).toFixed(2)
|
|
151
|
+
: "?";
|
|
152
|
+
details = `✓ ${event.stepLabel || "STEP"} completed in ${durationSec}s`;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else if (event.category === "UnderstudyAction") {
|
|
156
|
+
if (event.event === "started") {
|
|
157
|
+
details = `▷ ${event.method}(${argsStr})`;
|
|
158
|
+
}
|
|
159
|
+
else if (event.event === "completed") {
|
|
160
|
+
const durationSec = event.metrics?.durationMs
|
|
161
|
+
? (event.metrics.durationMs / 1000).toFixed(2)
|
|
162
|
+
: "?";
|
|
163
|
+
details = `✓ ${event.actionLabel || "ACTION"} completed in ${durationSec}s`;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
else if (event.category === "CDP") {
|
|
167
|
+
const icon = event.event === "call" ? "⏵" : "⏴";
|
|
168
|
+
details = `${icon} ${event.method}(${argsStr})`;
|
|
169
|
+
}
|
|
170
|
+
else if (event.category === "LLM") {
|
|
171
|
+
if (event.event === "request") {
|
|
172
|
+
const promptStr = event.prompt ? " " + String(event.prompt) : "";
|
|
173
|
+
details = `${event.model} ⏴${promptStr}`;
|
|
18
174
|
}
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
175
|
+
else if (event.event === "response") {
|
|
176
|
+
const hasTokens = event.inputTokens !== undefined || event.outputTokens !== undefined;
|
|
177
|
+
const tokenStr = hasTokens
|
|
178
|
+
? ` ꜛ${event.inputTokens ?? 0} ꜜ${event.outputTokens ?? 0} |`
|
|
179
|
+
: "";
|
|
180
|
+
const outputStr = event.output ? " " + String(event.output) : "";
|
|
181
|
+
details = `${event.model} ↳${tokenStr}${outputStr}`;
|
|
23
182
|
}
|
|
24
|
-
this.eventType = input.eventType.endsWith("Event")
|
|
25
|
-
? input.eventType
|
|
26
|
-
: `${input.eventType}Event`;
|
|
27
|
-
this.eventId =
|
|
28
|
-
input.eventId ?? FlowEvent.createEventId(input.eventIdSuffix ?? "0");
|
|
29
|
-
this.eventParentIds = input.eventParentIds ?? [];
|
|
30
|
-
this.createdAt = input.createdAt ?? new Date().toISOString();
|
|
31
|
-
this.sessionId = input.sessionId;
|
|
32
|
-
this.data = input.data ?? {};
|
|
33
183
|
}
|
|
184
|
+
if (!details)
|
|
185
|
+
return null;
|
|
186
|
+
// Assemble line and apply final truncation
|
|
187
|
+
const fullLine = `${formatTimestamp()} ${parts.join(" ")} ${details}`;
|
|
188
|
+
const cleaned = removeQuotes(fullLine);
|
|
189
|
+
const processed = event.category === "CDP" ? truncateCdpIds(cleaned) : cleaned;
|
|
190
|
+
return truncateLine(processed, MAX_LINE_LENGTH);
|
|
34
191
|
}
|
|
35
|
-
|
|
36
|
-
function
|
|
37
|
-
|
|
192
|
+
/** Check if a CDP event should be filtered from pretty output */
|
|
193
|
+
function shouldFilterCdpEvent(event) {
|
|
194
|
+
if (event.category !== "CDP")
|
|
195
|
+
return false;
|
|
196
|
+
if (event.method?.endsWith(".enable") || event.method === "enable")
|
|
197
|
+
return true;
|
|
198
|
+
return event.event === "message" && NOISY_CDP_EVENTS.has(event.method);
|
|
38
199
|
}
|
|
39
200
|
// =============================================================================
|
|
40
|
-
//
|
|
201
|
+
// Stream Creation
|
|
41
202
|
// =============================================================================
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
203
|
+
const isWritable = (s) => !!(s && !s.destroyed && s.writable);
|
|
204
|
+
function createJsonlStream(ctx) {
|
|
205
|
+
return new Writable({
|
|
206
|
+
objectMode: true,
|
|
207
|
+
write(chunk, _, cb) {
|
|
208
|
+
if (ctx.initialized && isWritable(ctx.fileStreams.jsonl)) {
|
|
209
|
+
ctx.fileStreams.jsonl.write(chunk, cb);
|
|
210
|
+
}
|
|
211
|
+
else
|
|
212
|
+
cb();
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
function createPrettyStream(ctx, category, streamKey) {
|
|
217
|
+
return new Writable({
|
|
218
|
+
objectMode: true,
|
|
219
|
+
write(chunk, _, cb) {
|
|
220
|
+
const stream = ctx.fileStreams[streamKey];
|
|
221
|
+
if (!ctx.initialized || !isWritable(stream))
|
|
222
|
+
return cb();
|
|
223
|
+
try {
|
|
224
|
+
const event = JSON.parse(chunk);
|
|
225
|
+
if (event.category !== category || shouldFilterCdpEvent(event))
|
|
226
|
+
return cb();
|
|
227
|
+
const line = prettifyEvent(event);
|
|
228
|
+
if (line)
|
|
229
|
+
stream.write(line + "\n", cb);
|
|
230
|
+
else
|
|
231
|
+
cb();
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
cb();
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
// =============================================================================
|
|
240
|
+
// Public Helpers (used by external callers)
|
|
241
|
+
// =============================================================================
|
|
242
|
+
/**
|
|
243
|
+
* Get the config directory. Returns empty string if logging is disabled.
|
|
244
|
+
*/
|
|
245
|
+
export function getConfigDir() {
|
|
246
|
+
return CONFIG_DIR ? path.resolve(CONFIG_DIR) : "";
|
|
247
|
+
}
|
|
248
|
+
/** Extract text and image info from a content array (handles nested tool_result) */
|
|
249
|
+
function extractFromContent(content, result) {
|
|
250
|
+
for (const part of content) {
|
|
251
|
+
const p = part;
|
|
252
|
+
// Text
|
|
253
|
+
if (!result.text && p.text) {
|
|
254
|
+
result.text = p.type === "text" || !p.type ? p.text : undefined;
|
|
255
|
+
}
|
|
256
|
+
// Images - various formats
|
|
257
|
+
if (p.type === "image" || p.type === "image_url") {
|
|
258
|
+
const url = p.image_url?.url;
|
|
259
|
+
if (url?.startsWith("data:"))
|
|
260
|
+
result.extras.push(`${dataToKb(url)}kb image`);
|
|
261
|
+
else if (p.source?.data)
|
|
262
|
+
result.extras.push(`${dataToKb(p.source.data)}kb image`);
|
|
263
|
+
else
|
|
264
|
+
result.extras.push("image");
|
|
265
|
+
}
|
|
266
|
+
else if (p.source?.data) {
|
|
267
|
+
result.extras.push(`${dataToKb(p.source.data)}kb image`);
|
|
268
|
+
}
|
|
269
|
+
else if (p.inlineData?.data) {
|
|
270
|
+
result.extras.push(`${dataToKb(p.inlineData.data)}kb image`);
|
|
271
|
+
}
|
|
272
|
+
// Recurse into tool_result content
|
|
273
|
+
if (p.type === "tool_result" && Array.isArray(p.content)) {
|
|
274
|
+
extractFromContent(p.content, result);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/** Build final preview string with extras */
|
|
279
|
+
function buildPreview(text, extras, maxLen) {
|
|
280
|
+
if (!text && extras.length === 0)
|
|
281
|
+
return undefined;
|
|
282
|
+
let result = text || "";
|
|
283
|
+
if (maxLen && result.length > maxLen)
|
|
284
|
+
result = result.slice(0, maxLen) + "...";
|
|
285
|
+
if (extras.length > 0) {
|
|
286
|
+
const extrasStr = extras.map((e) => `+{${e}}`).join(" ");
|
|
287
|
+
result = result ? `${result} ${extrasStr}` : extrasStr;
|
|
288
|
+
}
|
|
289
|
+
return result || undefined;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Format a prompt preview from LLM messages for logging.
|
|
293
|
+
* Returns format like: "some text... +{5.8kb image} +{schema} +{12 tools}"
|
|
294
|
+
*/
|
|
295
|
+
export function formatLlmPromptPreview(messages, options) {
|
|
296
|
+
try {
|
|
297
|
+
const lastUserMsg = messages.filter((m) => m.role === "user").pop();
|
|
298
|
+
if (!lastUserMsg)
|
|
299
|
+
return undefined;
|
|
300
|
+
const result = {
|
|
301
|
+
text: undefined,
|
|
302
|
+
extras: [],
|
|
50
303
|
};
|
|
304
|
+
if (typeof lastUserMsg.content === "string") {
|
|
305
|
+
result.text = lastUserMsg.content;
|
|
306
|
+
}
|
|
307
|
+
else if (Array.isArray(lastUserMsg.content)) {
|
|
308
|
+
extractFromContent(lastUserMsg.content, result);
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
return undefined;
|
|
312
|
+
}
|
|
313
|
+
// Clean instruction prefix
|
|
314
|
+
if (result.text) {
|
|
315
|
+
result.text = result.text.replace(/^[Ii]nstruction: /, "");
|
|
316
|
+
}
|
|
317
|
+
if (options?.hasSchema)
|
|
318
|
+
result.extras.push("schema");
|
|
319
|
+
if (options?.toolCount)
|
|
320
|
+
result.extras.push(`${options.toolCount} tools`);
|
|
321
|
+
return buildPreview(result.text, result.extras);
|
|
51
322
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (
|
|
69
|
-
|
|
323
|
+
catch {
|
|
324
|
+
return undefined;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Extract a text preview from CUA-style messages.
|
|
329
|
+
* Accepts various message formats (Anthropic, OpenAI, Google).
|
|
330
|
+
*/
|
|
331
|
+
export function formatCuaPromptPreview(messages, maxLen = 100) {
|
|
332
|
+
try {
|
|
333
|
+
const lastMsg = messages
|
|
334
|
+
.filter((m) => {
|
|
335
|
+
const msg = m;
|
|
336
|
+
return msg.role === "user" || msg.type === "tool_result";
|
|
337
|
+
})
|
|
338
|
+
.pop();
|
|
339
|
+
if (!lastMsg)
|
|
340
|
+
return undefined;
|
|
341
|
+
const result = {
|
|
342
|
+
text: undefined,
|
|
343
|
+
extras: [],
|
|
344
|
+
};
|
|
345
|
+
if (typeof lastMsg.content === "string") {
|
|
346
|
+
result.text = lastMsg.content;
|
|
70
347
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
eventType,
|
|
74
|
-
data,
|
|
75
|
-
eventParentIds,
|
|
76
|
-
});
|
|
77
|
-
ctx.parentEvents.push(startedEvent);
|
|
78
|
-
try {
|
|
79
|
-
return await originalMethod();
|
|
348
|
+
else if (typeof lastMsg.text === "string") {
|
|
349
|
+
result.text = lastMsg.text;
|
|
80
350
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
FlowLogger.emit({
|
|
84
|
-
eventIdSuffix,
|
|
85
|
-
eventType: `${eventType}ErrorEvent`,
|
|
86
|
-
eventParentIds: [...startedEvent.eventParentIds, startedEvent.eventId],
|
|
87
|
-
data: {
|
|
88
|
-
error: error instanceof Error ? error.message : String(error),
|
|
89
|
-
durationMs: Date.now() - new Date(startedEvent.createdAt).getTime(),
|
|
90
|
-
},
|
|
91
|
-
});
|
|
92
|
-
throw error;
|
|
351
|
+
else if (Array.isArray(lastMsg.parts)) {
|
|
352
|
+
extractFromContent(lastMsg.parts, result);
|
|
93
353
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (parentEvent?.eventId === startedEvent.eventId && !caughtError) {
|
|
97
|
-
FlowLogger.emit({
|
|
98
|
-
eventIdSuffix,
|
|
99
|
-
eventType: `${eventType}CompletedEvent`,
|
|
100
|
-
eventParentIds: [
|
|
101
|
-
...startedEvent.eventParentIds,
|
|
102
|
-
startedEvent.eventId,
|
|
103
|
-
],
|
|
104
|
-
data: {
|
|
105
|
-
durationMs: Date.now() - new Date(startedEvent.createdAt).getTime(),
|
|
106
|
-
},
|
|
107
|
-
});
|
|
108
|
-
}
|
|
354
|
+
else if (Array.isArray(lastMsg.content)) {
|
|
355
|
+
extractFromContent(lastMsg.content, result);
|
|
109
356
|
}
|
|
357
|
+
return buildPreview(result.text, result.extras, maxLen);
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
return undefined;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
/** Format CUA response output for logging */
|
|
364
|
+
export function formatCuaResponsePreview(output, maxLen = 100) {
|
|
365
|
+
try {
|
|
366
|
+
// Handle Google format or array
|
|
367
|
+
const items = output
|
|
368
|
+
?.candidates?.[0]?.content?.parts ??
|
|
369
|
+
(Array.isArray(output) ? output : []);
|
|
370
|
+
const preview = items
|
|
371
|
+
.map((item) => {
|
|
372
|
+
const i = item;
|
|
373
|
+
if (i.text)
|
|
374
|
+
return i.text.slice(0, 50);
|
|
375
|
+
if (i.functionCall?.name)
|
|
376
|
+
return `fn:${i.functionCall.name}`;
|
|
377
|
+
if (i.type === "tool_use" && i.name)
|
|
378
|
+
return `tool_use:${i.name}`;
|
|
379
|
+
return i.type ? `[${i.type}]` : "[item]";
|
|
380
|
+
})
|
|
381
|
+
.join(" ");
|
|
382
|
+
return preview.slice(0, maxLen);
|
|
110
383
|
}
|
|
384
|
+
catch {
|
|
385
|
+
return "[error]";
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
// =============================================================================
|
|
389
|
+
// SessionFileLogger - Main API
|
|
390
|
+
// =============================================================================
|
|
391
|
+
export class SessionFileLogger {
|
|
111
392
|
/**
|
|
112
393
|
* Initialize a new logging context. Call this at the start of a session.
|
|
394
|
+
* If BROWSERBASE_CONFIG_DIR is not set, logging is disabled.
|
|
113
395
|
*/
|
|
114
|
-
static init(sessionId,
|
|
396
|
+
static init(sessionId, v3Options) {
|
|
397
|
+
const configDir = getConfigDir();
|
|
398
|
+
if (!configDir)
|
|
399
|
+
return; // Logging disabled
|
|
400
|
+
const sessionDir = path.join(configDir, "sessions", sessionId);
|
|
401
|
+
// Create context with placeholder logger (will be replaced after streams init)
|
|
115
402
|
const ctx = {
|
|
403
|
+
logger: pino({ level: "silent" }), // Placeholder, replaced below
|
|
404
|
+
metrics: {
|
|
405
|
+
llmRequests: 0,
|
|
406
|
+
llmInputTokens: 0,
|
|
407
|
+
llmOutputTokens: 0,
|
|
408
|
+
cdpEvents: 0,
|
|
409
|
+
},
|
|
116
410
|
sessionId,
|
|
117
|
-
|
|
118
|
-
|
|
411
|
+
sessionDir,
|
|
412
|
+
configDir,
|
|
413
|
+
initPromise: Promise.resolve(),
|
|
414
|
+
initialized: false,
|
|
415
|
+
// Span context - mutable, injected into every log via mixin
|
|
416
|
+
taskId: null,
|
|
417
|
+
stepId: null,
|
|
418
|
+
stepLabel: null,
|
|
419
|
+
actionId: null,
|
|
420
|
+
actionLabel: null,
|
|
421
|
+
fileStreams: {
|
|
422
|
+
agent: null,
|
|
423
|
+
stagehand: null,
|
|
424
|
+
understudy: null,
|
|
425
|
+
cdp: null,
|
|
426
|
+
llm: null,
|
|
427
|
+
jsonl: null,
|
|
428
|
+
},
|
|
119
429
|
};
|
|
430
|
+
// Store init promise for awaiting in log methods
|
|
431
|
+
ctx.initPromise = SessionFileLogger.initAsync(ctx, v3Options);
|
|
120
432
|
loggerContext.enterWith(ctx);
|
|
121
|
-
return ctx;
|
|
122
433
|
}
|
|
123
|
-
static async
|
|
124
|
-
|
|
434
|
+
static async initAsync(ctx, v3Options) {
|
|
435
|
+
try {
|
|
436
|
+
await fs.promises.mkdir(ctx.sessionDir, { recursive: true });
|
|
437
|
+
if (v3Options) {
|
|
438
|
+
const sanitizedOptions = sanitizeOptions(v3Options);
|
|
439
|
+
const sessionJsonPath = path.join(ctx.sessionDir, "session.json");
|
|
440
|
+
await fs.promises.writeFile(sessionJsonPath, JSON.stringify(sanitizedOptions, null, 2), "utf-8");
|
|
441
|
+
}
|
|
442
|
+
// Create symlink to latest session
|
|
443
|
+
const latestLink = path.join(ctx.configDir, "sessions", "latest");
|
|
444
|
+
try {
|
|
445
|
+
try {
|
|
446
|
+
await fs.promises.unlink(latestLink);
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
// Ignore if doesn't exist
|
|
450
|
+
}
|
|
451
|
+
await fs.promises.symlink(ctx.sessionId, latestLink, "dir");
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
// Symlink creation can fail on Windows or due to permissions
|
|
455
|
+
}
|
|
456
|
+
// Create file streams
|
|
457
|
+
const dir = ctx.sessionDir;
|
|
458
|
+
ctx.fileStreams.agent = fs.createWriteStream(path.join(dir, "agent_events.log"), { flags: "a" });
|
|
459
|
+
ctx.fileStreams.stagehand = fs.createWriteStream(path.join(dir, "stagehand_events.log"), { flags: "a" });
|
|
460
|
+
ctx.fileStreams.understudy = fs.createWriteStream(path.join(dir, "understudy_events.log"), { flags: "a" });
|
|
461
|
+
ctx.fileStreams.cdp = fs.createWriteStream(path.join(dir, "cdp_events.log"), { flags: "a" });
|
|
462
|
+
ctx.fileStreams.llm = fs.createWriteStream(path.join(dir, "llm_events.log"), { flags: "a" });
|
|
463
|
+
ctx.fileStreams.jsonl = fs.createWriteStream(path.join(dir, "session_events.jsonl"), { flags: "a" });
|
|
464
|
+
ctx.initialized = true;
|
|
465
|
+
// Create pino multistream: JSONL + pretty streams per category
|
|
466
|
+
const streams = [
|
|
467
|
+
{ stream: createJsonlStream(ctx) },
|
|
468
|
+
{ stream: createPrettyStream(ctx, "AgentTask", "agent") },
|
|
469
|
+
{ stream: createPrettyStream(ctx, "StagehandStep", "stagehand") },
|
|
470
|
+
{ stream: createPrettyStream(ctx, "UnderstudyAction", "understudy") },
|
|
471
|
+
{ stream: createPrettyStream(ctx, "CDP", "cdp") },
|
|
472
|
+
{ stream: createPrettyStream(ctx, "LLM", "llm") },
|
|
473
|
+
];
|
|
474
|
+
// Create logger with mixin that injects span context from AsyncLocalStorage
|
|
475
|
+
ctx.logger = pino({
|
|
476
|
+
level: "info",
|
|
477
|
+
// Mixin adds eventId and current span context to every log
|
|
478
|
+
mixin() {
|
|
479
|
+
const store = loggerContext.getStore();
|
|
480
|
+
return {
|
|
481
|
+
eventId: uuidv7(),
|
|
482
|
+
sessionId: store?.sessionId,
|
|
483
|
+
taskId: store?.taskId,
|
|
484
|
+
stepId: store?.stepId,
|
|
485
|
+
stepLabel: store?.stepLabel,
|
|
486
|
+
actionId: store?.actionId,
|
|
487
|
+
actionLabel: store?.actionLabel,
|
|
488
|
+
};
|
|
489
|
+
},
|
|
490
|
+
}, pino.multistream(streams));
|
|
491
|
+
}
|
|
492
|
+
catch {
|
|
493
|
+
// Fail silently
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
static async close() {
|
|
497
|
+
const ctx = loggerContext.getStore();
|
|
125
498
|
if (!ctx)
|
|
126
499
|
return;
|
|
127
|
-
ctx.
|
|
500
|
+
await ctx.initPromise;
|
|
501
|
+
SessionFileLogger.logAgentTaskCompleted();
|
|
502
|
+
await Promise.all(Object.values(ctx.fileStreams)
|
|
503
|
+
.filter(Boolean)
|
|
504
|
+
.map((s) => new Promise((r) => s.end(r)))).catch(() => { });
|
|
128
505
|
}
|
|
129
|
-
static get
|
|
130
|
-
|
|
131
|
-
if (!ctx) {
|
|
132
|
-
throw new Error("FlowLogger context is missing.");
|
|
133
|
-
}
|
|
134
|
-
return ctx;
|
|
506
|
+
static get sessionId() {
|
|
507
|
+
return loggerContext.getStore()?.sessionId ?? null;
|
|
135
508
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
509
|
+
static get sessionDir() {
|
|
510
|
+
return loggerContext.getStore()?.sessionDir ?? null;
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Get the current logger context object.
|
|
514
|
+
*/
|
|
515
|
+
static getContext() {
|
|
516
|
+
return loggerContext.getStore() ?? null;
|
|
517
|
+
}
|
|
518
|
+
// ===========================================================================
|
|
519
|
+
// Agent Task Events
|
|
520
|
+
// ===========================================================================
|
|
521
|
+
/**
|
|
522
|
+
* Start a new task and log it.
|
|
523
|
+
*/
|
|
524
|
+
static logAgentTaskStarted({ invocation, args, }) {
|
|
525
|
+
const ctx = loggerContext.getStore();
|
|
526
|
+
if (!ctx)
|
|
527
|
+
return;
|
|
528
|
+
// Set up task context
|
|
529
|
+
ctx.taskId = uuidv7();
|
|
530
|
+
ctx.stepId = null;
|
|
531
|
+
ctx.stepLabel = null;
|
|
532
|
+
ctx.actionId = null;
|
|
533
|
+
ctx.actionLabel = null;
|
|
534
|
+
// Reset metrics for new task
|
|
535
|
+
ctx.metrics = {
|
|
536
|
+
taskStartTime: Date.now(),
|
|
537
|
+
llmRequests: 0,
|
|
538
|
+
llmInputTokens: 0,
|
|
539
|
+
llmOutputTokens: 0,
|
|
540
|
+
cdpEvents: 0,
|
|
143
541
|
};
|
|
542
|
+
ctx.logger.info({
|
|
543
|
+
category: "AgentTask",
|
|
544
|
+
event: "started",
|
|
545
|
+
method: invocation,
|
|
546
|
+
params: args,
|
|
547
|
+
});
|
|
144
548
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
549
|
+
/**
|
|
550
|
+
* Log task completion with metrics summary.
|
|
551
|
+
*/
|
|
552
|
+
static logAgentTaskCompleted(options) {
|
|
553
|
+
const ctx = loggerContext.getStore();
|
|
554
|
+
if (!ctx || !ctx.metrics.taskStartTime)
|
|
555
|
+
return;
|
|
556
|
+
const durationMs = Date.now() - ctx.metrics.taskStartTime;
|
|
557
|
+
const event = {
|
|
558
|
+
category: "AgentTask",
|
|
559
|
+
event: "completed",
|
|
560
|
+
method: "Agent.execute",
|
|
561
|
+
metrics: {
|
|
562
|
+
durationMs,
|
|
563
|
+
llmRequests: ctx.metrics.llmRequests,
|
|
564
|
+
inputTokens: ctx.metrics.llmInputTokens,
|
|
565
|
+
outputTokens: ctx.metrics.llmOutputTokens,
|
|
566
|
+
cdpEvents: ctx.metrics.cdpEvents,
|
|
567
|
+
},
|
|
149
568
|
};
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
data: eventData,
|
|
153
|
-
}, () => originalMethod(...params));
|
|
154
|
-
if (!options.context && !(loggerContext.getStore() ?? null)) {
|
|
155
|
-
return originalMethod(...params);
|
|
569
|
+
if (options?.cacheHit) {
|
|
570
|
+
event.msg = "CACHE HIT, NO LLM NEEDED";
|
|
156
571
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
572
|
+
ctx.logger.info(event);
|
|
573
|
+
// Clear task context
|
|
574
|
+
ctx.taskId = null;
|
|
575
|
+
ctx.stepId = null;
|
|
576
|
+
ctx.stepLabel = null;
|
|
577
|
+
ctx.actionId = null;
|
|
578
|
+
ctx.actionLabel = null;
|
|
579
|
+
ctx.metrics.taskStartTime = undefined;
|
|
160
580
|
}
|
|
161
581
|
// ===========================================================================
|
|
162
|
-
//
|
|
582
|
+
// Stagehand Step Events
|
|
163
583
|
// ===========================================================================
|
|
164
|
-
static
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
584
|
+
static logStagehandStepEvent({ invocation, args, label, }) {
|
|
585
|
+
const ctx = loggerContext.getStore();
|
|
586
|
+
if (!ctx)
|
|
587
|
+
return uuidv7();
|
|
588
|
+
// Set up step context
|
|
589
|
+
ctx.stepId = uuidv7();
|
|
590
|
+
ctx.stepLabel = label.toUpperCase();
|
|
591
|
+
ctx.actionId = null;
|
|
592
|
+
ctx.actionLabel = null;
|
|
593
|
+
ctx.metrics.stepStartTime = Date.now();
|
|
594
|
+
ctx.logger.info({
|
|
595
|
+
category: "StagehandStep",
|
|
596
|
+
event: "started",
|
|
597
|
+
method: invocation,
|
|
598
|
+
params: args,
|
|
599
|
+
});
|
|
600
|
+
return ctx.stepId;
|
|
601
|
+
}
|
|
602
|
+
static logStagehandStepCompleted() {
|
|
603
|
+
const ctx = loggerContext.getStore();
|
|
604
|
+
if (!ctx || !ctx.stepId)
|
|
605
|
+
return;
|
|
606
|
+
const durationMs = ctx.metrics.stepStartTime
|
|
607
|
+
? Date.now() - ctx.metrics.stepStartTime
|
|
608
|
+
: 0;
|
|
609
|
+
ctx.logger.info({
|
|
610
|
+
category: "StagehandStep",
|
|
611
|
+
event: "completed",
|
|
612
|
+
metrics: { durationMs },
|
|
613
|
+
});
|
|
614
|
+
// Clear step context
|
|
615
|
+
ctx.stepId = null;
|
|
616
|
+
ctx.stepLabel = null;
|
|
617
|
+
ctx.actionId = null;
|
|
618
|
+
ctx.actionLabel = null;
|
|
619
|
+
ctx.metrics.stepStartTime = undefined;
|
|
620
|
+
}
|
|
621
|
+
// ===========================================================================
|
|
622
|
+
// Understudy Action Events
|
|
623
|
+
// ===========================================================================
|
|
624
|
+
static logUnderstudyActionEvent({ actionType, target, args, }) {
|
|
625
|
+
const ctx = loggerContext.getStore();
|
|
626
|
+
if (!ctx)
|
|
627
|
+
return uuidv7();
|
|
628
|
+
// Set up action context
|
|
629
|
+
ctx.actionId = uuidv7();
|
|
630
|
+
ctx.actionLabel = actionType
|
|
631
|
+
.toUpperCase()
|
|
632
|
+
.replace("UNDERSTUDY.", "")
|
|
633
|
+
.replace("PAGE.", "");
|
|
634
|
+
ctx.metrics.actionStartTime = Date.now();
|
|
635
|
+
const params = {};
|
|
636
|
+
if (target)
|
|
637
|
+
params.target = target;
|
|
638
|
+
if (args)
|
|
639
|
+
params.args = args;
|
|
640
|
+
ctx.logger.info({
|
|
641
|
+
category: "UnderstudyAction",
|
|
642
|
+
event: "started",
|
|
643
|
+
method: actionType,
|
|
644
|
+
params: Object.keys(params).length > 0 ? params : undefined,
|
|
645
|
+
});
|
|
646
|
+
return ctx.actionId;
|
|
202
647
|
}
|
|
203
|
-
static
|
|
204
|
-
|
|
648
|
+
static logUnderstudyActionCompleted() {
|
|
649
|
+
const ctx = loggerContext.getStore();
|
|
650
|
+
if (!ctx || !ctx.actionId)
|
|
651
|
+
return;
|
|
652
|
+
const durationMs = ctx.metrics.actionStartTime
|
|
653
|
+
? Date.now() - ctx.metrics.actionStartTime
|
|
654
|
+
: 0;
|
|
655
|
+
ctx.logger.info({
|
|
656
|
+
category: "UnderstudyAction",
|
|
657
|
+
event: "completed",
|
|
658
|
+
metrics: { durationMs },
|
|
659
|
+
});
|
|
660
|
+
// Clear action context
|
|
661
|
+
ctx.actionId = null;
|
|
662
|
+
ctx.actionLabel = null;
|
|
663
|
+
ctx.metrics.actionStartTime = undefined;
|
|
664
|
+
}
|
|
665
|
+
// ===========================================================================
|
|
666
|
+
// CDP Events
|
|
667
|
+
// ===========================================================================
|
|
668
|
+
static logCdpEvent(eventType, { method, params, targetId, }, explicitCtx) {
|
|
669
|
+
const ctx = explicitCtx ?? loggerContext.getStore();
|
|
670
|
+
if (!ctx)
|
|
671
|
+
return;
|
|
672
|
+
if (eventType === "call")
|
|
673
|
+
ctx.metrics.cdpEvents++;
|
|
674
|
+
ctx.logger.info({
|
|
675
|
+
category: "CDP",
|
|
676
|
+
event: eventType,
|
|
677
|
+
method,
|
|
678
|
+
params,
|
|
679
|
+
targetId,
|
|
680
|
+
});
|
|
205
681
|
}
|
|
206
|
-
static
|
|
207
|
-
|
|
682
|
+
static logCdpCallEvent(data, ctx) {
|
|
683
|
+
SessionFileLogger.logCdpEvent("call", data, ctx);
|
|
208
684
|
}
|
|
209
|
-
static logCdpMessageEvent(
|
|
210
|
-
|
|
211
|
-
...parentEvent.eventParentIds,
|
|
212
|
-
parentEvent.eventId,
|
|
213
|
-
]);
|
|
685
|
+
static logCdpMessageEvent(data, ctx) {
|
|
686
|
+
SessionFileLogger.logCdpEvent("message", data, ctx);
|
|
214
687
|
}
|
|
215
688
|
// ===========================================================================
|
|
216
689
|
// LLM Events
|
|
217
690
|
// ===========================================================================
|
|
218
|
-
static logLlmRequest({ requestId, model, prompt, }) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
691
|
+
static logLlmRequest({ requestId, model, prompt, }, explicitCtx) {
|
|
692
|
+
const ctx = explicitCtx ?? loggerContext.getStore();
|
|
693
|
+
if (!ctx)
|
|
694
|
+
return;
|
|
695
|
+
// Track LLM requests for task metrics
|
|
696
|
+
ctx.metrics.llmRequests++;
|
|
697
|
+
ctx.logger.info({
|
|
698
|
+
category: "LLM",
|
|
699
|
+
event: "request",
|
|
700
|
+
requestId,
|
|
701
|
+
method: "LLM.request",
|
|
702
|
+
model,
|
|
703
|
+
prompt,
|
|
227
704
|
});
|
|
228
705
|
}
|
|
229
|
-
static logLlmResponse({ requestId, model, output, inputTokens, outputTokens, }) {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
706
|
+
static logLlmResponse({ requestId, model, output, inputTokens, outputTokens, }, explicitCtx) {
|
|
707
|
+
const ctx = explicitCtx ?? loggerContext.getStore();
|
|
708
|
+
if (!ctx)
|
|
709
|
+
return;
|
|
710
|
+
// Track tokens for task metrics
|
|
711
|
+
ctx.metrics.llmInputTokens += inputTokens ?? 0;
|
|
712
|
+
ctx.metrics.llmOutputTokens += outputTokens ?? 0;
|
|
713
|
+
ctx.logger.info({
|
|
714
|
+
category: "LLM",
|
|
715
|
+
event: "response",
|
|
716
|
+
requestId,
|
|
717
|
+
method: "LLM.response",
|
|
718
|
+
model,
|
|
719
|
+
output,
|
|
720
|
+
inputTokens,
|
|
721
|
+
outputTokens,
|
|
240
722
|
});
|
|
241
723
|
}
|
|
242
724
|
// ===========================================================================
|
|
@@ -247,57 +729,69 @@ export class FlowLogger {
|
|
|
247
729
|
* Returns a no-op middleware when logging is disabled.
|
|
248
730
|
*/
|
|
249
731
|
static createLlmLoggingMiddleware(modelId) {
|
|
732
|
+
// No-op middleware when logging is disabled
|
|
733
|
+
if (!CONFIG_DIR) {
|
|
734
|
+
return {
|
|
735
|
+
wrapGenerate: async ({ doGenerate }) => doGenerate(),
|
|
736
|
+
};
|
|
737
|
+
}
|
|
250
738
|
return {
|
|
251
739
|
wrapGenerate: async ({ doGenerate, params }) => {
|
|
740
|
+
const ctx = SessionFileLogger.getContext();
|
|
741
|
+
// Skip logging overhead if no context (shouldn't happen but be safe)
|
|
742
|
+
if (!ctx) {
|
|
743
|
+
return doGenerate();
|
|
744
|
+
}
|
|
252
745
|
const llmRequestId = uuidv7();
|
|
253
746
|
const toolCount = Array.isArray(params.tools) ? params.tools.length : 0;
|
|
747
|
+
// Extract prompt preview from last non-system message
|
|
254
748
|
const messages = (params.prompt ?? []);
|
|
255
749
|
const lastMsg = messages.filter((m) => m.role !== "system").pop();
|
|
750
|
+
const extracted = {
|
|
751
|
+
text: undefined,
|
|
752
|
+
extras: [],
|
|
753
|
+
};
|
|
256
754
|
let rolePrefix = lastMsg?.role ?? "?";
|
|
257
|
-
let promptSummary = `(no text) +{${toolCount} tools}`;
|
|
258
755
|
if (lastMsg) {
|
|
259
756
|
if (typeof lastMsg.content === "string") {
|
|
260
|
-
|
|
757
|
+
extracted.text = lastMsg.content;
|
|
261
758
|
}
|
|
262
759
|
else if (Array.isArray(lastMsg.content)) {
|
|
263
|
-
|
|
760
|
+
// Check for tool-result first
|
|
761
|
+
const toolResult = lastMsg.content.find((p) => p.type === "tool-result");
|
|
264
762
|
if (toolResult) {
|
|
265
763
|
rolePrefix = `tool result: ${toolResult.toolName}()`;
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
764
|
+
const out = toolResult.output;
|
|
765
|
+
if (out?.type === "json" && out.value) {
|
|
766
|
+
extracted.text = JSON.stringify(out.value).slice(0, 150);
|
|
269
767
|
}
|
|
270
|
-
else if (Array.isArray(
|
|
271
|
-
|
|
272
|
-
content: toolResult.output.value,
|
|
273
|
-
}) ?? "(no text)"} +{${toolCount} tools}`;
|
|
768
|
+
else if (Array.isArray(out?.value)) {
|
|
769
|
+
extractFromContent(out.value, extracted);
|
|
274
770
|
}
|
|
275
771
|
}
|
|
276
772
|
else {
|
|
277
|
-
|
|
278
|
-
"(no text)"} +{${toolCount} tools}`;
|
|
773
|
+
extractFromContent(lastMsg.content, extracted);
|
|
279
774
|
}
|
|
280
775
|
}
|
|
281
|
-
promptSummary = `${rolePrefix}: ${promptSummary}`;
|
|
282
776
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
FlowLogger.logLlmRequest({
|
|
777
|
+
const promptText = extracted.text || "(no text)";
|
|
778
|
+
const promptPreview = `${rolePrefix}: ${promptText} +{${toolCount} tools}`;
|
|
779
|
+
SessionFileLogger.logLlmRequest({
|
|
287
780
|
requestId: llmRequestId,
|
|
288
781
|
model: modelId,
|
|
289
|
-
|
|
290
|
-
|
|
782
|
+
operation: "generateText",
|
|
783
|
+
prompt: promptPreview,
|
|
784
|
+
}, ctx);
|
|
291
785
|
const result = await doGenerate();
|
|
292
|
-
// Extract output
|
|
786
|
+
// Extract output preview
|
|
293
787
|
const res = result;
|
|
294
|
-
let
|
|
295
|
-
if (!
|
|
788
|
+
let outputPreview = res.text || "";
|
|
789
|
+
if (!outputPreview && res.content) {
|
|
296
790
|
if (typeof res.content === "string") {
|
|
297
|
-
|
|
791
|
+
outputPreview = res.content;
|
|
298
792
|
}
|
|
299
793
|
else if (Array.isArray(res.content)) {
|
|
300
|
-
|
|
794
|
+
outputPreview = res.content
|
|
301
795
|
.map((c) => c.text ||
|
|
302
796
|
(c.type === "tool-call"
|
|
303
797
|
? `tool call: ${c.toolName}()`
|
|
@@ -305,158 +799,70 @@ export class FlowLogger {
|
|
|
305
799
|
.join(" ");
|
|
306
800
|
}
|
|
307
801
|
}
|
|
308
|
-
if (!
|
|
309
|
-
|
|
802
|
+
if (!outputPreview && res.toolCalls?.length) {
|
|
803
|
+
outputPreview = `[${res.toolCalls.length} tool calls]`;
|
|
310
804
|
}
|
|
311
|
-
|
|
805
|
+
SessionFileLogger.logLlmResponse({
|
|
312
806
|
requestId: llmRequestId,
|
|
313
807
|
model: modelId,
|
|
314
|
-
|
|
808
|
+
operation: "generateText",
|
|
809
|
+
output: outputPreview || "[empty]",
|
|
315
810
|
inputTokens: result.usage?.inputTokens,
|
|
316
811
|
outputTokens: result.usage?.outputTokens,
|
|
317
|
-
});
|
|
812
|
+
}, ctx);
|
|
318
813
|
return result;
|
|
319
814
|
},
|
|
320
815
|
};
|
|
321
816
|
}
|
|
322
817
|
}
|
|
323
|
-
/**
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
if (!result.text && p.text) {
|
|
333
|
-
result.text = p.type === "text" || !p.type ? p.text : undefined;
|
|
334
|
-
}
|
|
335
|
-
// Images - various formats
|
|
336
|
-
if (p.type === "image" || p.type === "image_url") {
|
|
337
|
-
const url = p.image_url?.url;
|
|
338
|
-
if (url?.startsWith("data:"))
|
|
339
|
-
result.extras.push(`${dataToKb(url)}kb image`);
|
|
340
|
-
else if (p.source?.data)
|
|
341
|
-
result.extras.push(`${dataToKb(p.source.data)}kb image`);
|
|
342
|
-
else
|
|
343
|
-
result.extras.push("image");
|
|
344
|
-
}
|
|
345
|
-
else if (p.source?.data) {
|
|
346
|
-
result.extras.push(`${dataToKb(p.source.data)}kb image`);
|
|
347
|
-
}
|
|
348
|
-
else if (p.inlineData?.data) {
|
|
349
|
-
result.extras.push(`${dataToKb(p.inlineData.data)}kb image`);
|
|
818
|
+
/**
|
|
819
|
+
* Method decorator for logging understudy actions with automatic start/complete.
|
|
820
|
+
* Logs all arguments automatically. No-op when CONFIG_DIR is empty.
|
|
821
|
+
*/
|
|
822
|
+
export function logAction(actionType) {
|
|
823
|
+
return function (originalMethod) {
|
|
824
|
+
// No-op when logging is disabled
|
|
825
|
+
if (!CONFIG_DIR) {
|
|
826
|
+
return originalMethod;
|
|
350
827
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
828
|
+
return async function (...args) {
|
|
829
|
+
SessionFileLogger.logUnderstudyActionEvent({
|
|
830
|
+
actionType,
|
|
831
|
+
args: args.length > 0 ? args : undefined,
|
|
832
|
+
});
|
|
833
|
+
try {
|
|
834
|
+
return await originalMethod.apply(this, args);
|
|
356
835
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}
|
|
362
|
-
function extractLlmMessageSummary(input, options) {
|
|
363
|
-
const result = {
|
|
364
|
-
text: undefined,
|
|
365
|
-
extras: [...(options?.extras ?? [])],
|
|
836
|
+
finally {
|
|
837
|
+
SessionFileLogger.logUnderstudyActionCompleted();
|
|
838
|
+
}
|
|
839
|
+
};
|
|
366
840
|
};
|
|
367
|
-
if (typeof input.content === "string") {
|
|
368
|
-
result.text = input.content;
|
|
369
|
-
}
|
|
370
|
-
else if (typeof input.text === "string") {
|
|
371
|
-
result.text = input.text;
|
|
372
|
-
}
|
|
373
|
-
else if (Array.isArray(input.parts)) {
|
|
374
|
-
const summary = extractLlmMessageContent(input.parts);
|
|
375
|
-
result.text = summary.text;
|
|
376
|
-
result.extras.push(...summary.extras);
|
|
377
|
-
}
|
|
378
|
-
else if (Array.isArray(input.content)) {
|
|
379
|
-
const summary = extractLlmMessageContent(input.content);
|
|
380
|
-
result.text = summary.text;
|
|
381
|
-
result.extras.push(...summary.extras);
|
|
382
|
-
}
|
|
383
|
-
if (options?.trimInstructionPrefix && result.text) {
|
|
384
|
-
result.text = result.text.replace(/^[Ii]nstruction: /, "");
|
|
385
|
-
}
|
|
386
|
-
const text = result.text;
|
|
387
|
-
if (!text && result.extras.length === 0)
|
|
388
|
-
return undefined;
|
|
389
|
-
let summary = text || "";
|
|
390
|
-
if (result.extras.length > 0) {
|
|
391
|
-
const extrasStr = result.extras.map((e) => `+{${e}}`).join(" ");
|
|
392
|
-
summary = summary ? `${summary} ${extrasStr}` : extrasStr;
|
|
393
|
-
}
|
|
394
|
-
return summary || undefined;
|
|
395
|
-
}
|
|
396
|
-
/**
|
|
397
|
-
* Format a prompt summary from LLM messages for logging.
|
|
398
|
-
* Returns format like: "some text +{5.8kb image} +{schema} +{12 tools}"
|
|
399
|
-
*/
|
|
400
|
-
export function extractLlmPromptSummary(messages, options) {
|
|
401
|
-
try {
|
|
402
|
-
const lastUserMsg = messages.filter((m) => m.role === "user").pop();
|
|
403
|
-
if (!lastUserMsg)
|
|
404
|
-
return undefined;
|
|
405
|
-
return extractLlmMessageSummary(lastUserMsg, {
|
|
406
|
-
trimInstructionPrefix: true,
|
|
407
|
-
extras: [
|
|
408
|
-
...(options?.hasSchema ? ["schema"] : []),
|
|
409
|
-
...(options?.toolCount ? [`${options.toolCount} tools`] : []),
|
|
410
|
-
],
|
|
411
|
-
});
|
|
412
|
-
}
|
|
413
|
-
catch {
|
|
414
|
-
return undefined;
|
|
415
|
-
}
|
|
416
841
|
}
|
|
417
842
|
/**
|
|
418
|
-
*
|
|
419
|
-
*
|
|
843
|
+
* Method decorator for logging Stagehand step events (act, extract, observe).
|
|
844
|
+
* Only adds logging - does NOT wrap with withInstanceLogContext (caller handles that).
|
|
845
|
+
* No-op when CONFIG_DIR is empty.
|
|
420
846
|
*/
|
|
421
|
-
export function
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
.
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const items = output
|
|
442
|
-
?.candidates?.[0]?.content?.parts ??
|
|
443
|
-
(Array.isArray(output) ? output : []);
|
|
444
|
-
const summary = items
|
|
445
|
-
.map((item) => {
|
|
446
|
-
const i = item;
|
|
447
|
-
if (i.text)
|
|
448
|
-
return i.text;
|
|
449
|
-
if (i.functionCall?.name)
|
|
450
|
-
return i.functionCall.name;
|
|
451
|
-
if (i.type === "tool_use" && i.name)
|
|
452
|
-
return i.name;
|
|
453
|
-
return i.type ?? "[item]";
|
|
454
|
-
})
|
|
455
|
-
.join(" ");
|
|
456
|
-
return summary;
|
|
457
|
-
}
|
|
458
|
-
catch {
|
|
459
|
-
return "[error]";
|
|
460
|
-
}
|
|
847
|
+
export function logStagehandStep(invocation, label) {
|
|
848
|
+
return function (originalMethod) {
|
|
849
|
+
// No-op when logging is disabled
|
|
850
|
+
if (!CONFIG_DIR) {
|
|
851
|
+
return originalMethod;
|
|
852
|
+
}
|
|
853
|
+
return async function (...args) {
|
|
854
|
+
SessionFileLogger.logStagehandStepEvent({
|
|
855
|
+
invocation,
|
|
856
|
+
args: args.length > 0 ? args : undefined,
|
|
857
|
+
label,
|
|
858
|
+
});
|
|
859
|
+
try {
|
|
860
|
+
return await originalMethod.apply(this, args);
|
|
861
|
+
}
|
|
862
|
+
finally {
|
|
863
|
+
SessionFileLogger.logStagehandStepCompleted();
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
};
|
|
461
867
|
}
|
|
462
868
|
//# sourceMappingURL=flowLogger.js.map
|