@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.
Files changed (69) hide show
  1. package/dist/cjs/lib/utils.d.ts +0 -1
  2. package/dist/cjs/lib/utils.js +0 -4
  3. package/dist/cjs/lib/utils.js.map +1 -1
  4. package/dist/cjs/lib/v3/agent/AnthropicCUAClient.js +6 -4
  5. package/dist/cjs/lib/v3/agent/AnthropicCUAClient.js.map +1 -1
  6. package/dist/cjs/lib/v3/agent/GoogleCUAClient.js +6 -4
  7. package/dist/cjs/lib/v3/agent/GoogleCUAClient.js.map +1 -1
  8. package/dist/cjs/lib/v3/agent/OpenAICUAClient.js +6 -4
  9. package/dist/cjs/lib/v3/agent/OpenAICUAClient.js.map +1 -1
  10. package/dist/cjs/lib/v3/flowLogger.d.ts +103 -62
  11. package/dist/cjs/lib/v3/flowLogger.js +773 -362
  12. package/dist/cjs/lib/v3/flowLogger.js.map +1 -1
  13. package/dist/cjs/lib/v3/handlers/handlerUtils/actHandlerUtils.js +33 -21
  14. package/dist/cjs/lib/v3/handlers/handlerUtils/actHandlerUtils.js.map +1 -1
  15. package/dist/cjs/lib/v3/handlers/v3AgentHandler.d.ts +4 -0
  16. package/dist/cjs/lib/v3/handlers/v3AgentHandler.js +31 -1
  17. package/dist/cjs/lib/v3/handlers/v3AgentHandler.js.map +1 -1
  18. package/dist/cjs/lib/v3/handlers/v3CuaAgentHandler.js +12 -10
  19. package/dist/cjs/lib/v3/handlers/v3CuaAgentHandler.js.map +1 -1
  20. package/dist/cjs/lib/v3/llm/aisdk.js +16 -10
  21. package/dist/cjs/lib/v3/llm/aisdk.js.map +1 -1
  22. package/dist/cjs/lib/v3/types/public/options.d.ts +0 -5
  23. package/dist/cjs/lib/v3/types/public/options.js.map +1 -1
  24. package/dist/cjs/lib/v3/understudy/cdp.d.ts +12 -3
  25. package/dist/cjs/lib/v3/understudy/cdp.js +10 -83
  26. package/dist/cjs/lib/v3/understudy/cdp.js.map +1 -1
  27. package/dist/cjs/lib/v3/understudy/page.js +17 -32
  28. package/dist/cjs/lib/v3/understudy/page.js.map +1 -1
  29. package/dist/cjs/lib/v3/v3.d.ts +0 -5
  30. package/dist/cjs/lib/v3/v3.js +157 -174
  31. package/dist/cjs/lib/v3/v3.js.map +1 -1
  32. package/dist/esm/lib/utils.d.ts +0 -1
  33. package/dist/esm/lib/utils.js +0 -3
  34. package/dist/esm/lib/utils.js.map +1 -1
  35. package/dist/esm/lib/v3/agent/AnthropicCUAClient.js +7 -5
  36. package/dist/esm/lib/v3/agent/AnthropicCUAClient.js.map +1 -1
  37. package/dist/esm/lib/v3/agent/GoogleCUAClient.js +7 -5
  38. package/dist/esm/lib/v3/agent/GoogleCUAClient.js.map +1 -1
  39. package/dist/esm/lib/v3/agent/OpenAICUAClient.js +7 -5
  40. package/dist/esm/lib/v3/agent/OpenAICUAClient.js.map +1 -1
  41. package/dist/esm/lib/v3/flowLogger.d.ts +103 -62
  42. package/dist/esm/lib/v3/flowLogger.js +762 -356
  43. package/dist/esm/lib/v3/flowLogger.js.map +1 -1
  44. package/dist/esm/lib/v3/handlers/handlerUtils/actHandlerUtils.js +34 -22
  45. package/dist/esm/lib/v3/handlers/handlerUtils/actHandlerUtils.js.map +1 -1
  46. package/dist/esm/lib/v3/handlers/v3AgentHandler.d.ts +4 -0
  47. package/dist/esm/lib/v3/handlers/v3AgentHandler.js +32 -2
  48. package/dist/esm/lib/v3/handlers/v3AgentHandler.js.map +1 -1
  49. package/dist/esm/lib/v3/handlers/v3CuaAgentHandler.js +13 -11
  50. package/dist/esm/lib/v3/handlers/v3CuaAgentHandler.js.map +1 -1
  51. package/dist/esm/lib/v3/llm/aisdk.js +17 -11
  52. package/dist/esm/lib/v3/llm/aisdk.js.map +1 -1
  53. package/dist/esm/lib/v3/types/public/options.d.ts +0 -5
  54. package/dist/esm/lib/v3/types/public/options.js.map +1 -1
  55. package/dist/esm/lib/v3/understudy/cdp.d.ts +12 -3
  56. package/dist/esm/lib/v3/understudy/cdp.js +10 -83
  57. package/dist/esm/lib/v3/understudy/cdp.js.map +1 -1
  58. package/dist/esm/lib/v3/understudy/page.js +18 -33
  59. package/dist/esm/lib/v3/understudy/page.js.map +1 -1
  60. package/dist/esm/lib/v3/v3.d.ts +0 -5
  61. package/dist/esm/lib/v3/v3.js +158 -175
  62. package/dist/esm/lib/v3/v3.js.map +1 -1
  63. package/package.json +1 -1
  64. package/dist/cjs/lib/v3/eventStore.d.ts +0 -41
  65. package/dist/cjs/lib/v3/eventStore.js +0 -375
  66. package/dist/cjs/lib/v3/eventStore.js.map +0 -1
  67. package/dist/esm/lib/v3/eventStore.d.ts +0 -41
  68. package/dist/esm/lib/v3/eventStore.js +0 -363
  69. 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
- export class FlowEvent {
4
- static createEventId(eventIdSuffix) {
5
- const rawEventId = uuidv7();
6
- return `${rawEventId.slice(0, -1)}${eventIdSuffix || "0"}`;
7
- }
8
- // base required fields for all events:
9
- eventType;
10
- eventId;
11
- eventParentIds;
12
- createdAt;
13
- sessionId;
14
- data; // event payload (e.g. params, action, result, error, etc.)
15
- constructor(input) {
16
- if (!input.sessionId) {
17
- throw new Error("FlowEvent.sessionId is required.");
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 (input.eventId &&
20
- input.eventIdSuffix &&
21
- !input.eventId.endsWith(input.eventIdSuffix)) {
22
- throw new Error("FlowEvent cannot take both eventId and eventIdSuffix.");
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
- const loggerContext = new AsyncLocalStorage();
36
- function dataToKb(data) {
37
- return ((data.length * 0.75) / 1024).toFixed(1);
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
- // Flow Logger - Main API
201
+ // Stream Creation
41
202
  // =============================================================================
42
- export class FlowLogger {
43
- static cloneContext(ctx) {
44
- return {
45
- ...ctx,
46
- parentEvents: ctx.parentEvents.map((event) => ({
47
- ...event,
48
- eventParentIds: [...event.eventParentIds],
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
- static emit(event) {
53
- const ctx = FlowLogger.currentContext;
54
- const emittedEvent = new FlowEvent({
55
- ...event,
56
- eventParentIds: event.eventParentIds ??
57
- ctx.parentEvents.map((parent) => parent.eventId),
58
- sessionId: ctx.sessionId,
59
- });
60
- ctx.eventBus.emit(emittedEvent.eventType, emittedEvent);
61
- return emittedEvent;
62
- }
63
- static async runWithAutoStatusEventLogging(options, originalMethod) {
64
- const ctx = FlowLogger.currentContext;
65
- const { data, eventParentIds, eventType, eventIdSuffix } = options;
66
- let caughtError = null;
67
- // if eventParentIds is explicitly [], this is a root event, clear the parent events in context
68
- if (eventParentIds && eventParentIds.length === 0) {
69
- ctx.parentEvents = [];
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
- const startedEvent = FlowLogger.emit({
72
- eventIdSuffix,
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
- catch (error) {
82
- caughtError = error;
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
- finally {
95
- const parentEvent = ctx.parentEvents.pop();
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, eventBus) {
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
- eventBus,
118
- parentEvents: [],
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 close(context) {
124
- const ctx = context ?? loggerContext.getStore() ?? null;
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.parentEvents = [];
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 currentContext() {
130
- const ctx = loggerContext.getStore() ?? null;
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
- // decorator method to wrap a class method with automatic started/completed/error events
137
- static wrapWithLogging(options) {
138
- return function (originalMethod) {
139
- const wrappedMethod = async function (...args) {
140
- return await FlowLogger.runWithLogging(options, (...boundArgs) => originalMethod.apply(this, boundArgs), args);
141
- };
142
- return wrappedMethod;
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
- static runWithLogging(options, originalMethod, params) {
146
- const eventData = {
147
- ...(options.data ?? {}),
148
- params: [...params],
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
- const execute = () => FlowLogger.runWithAutoStatusEventLogging({
151
- ...options,
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
- return options.context
158
- ? loggerContext.run(FlowLogger.cloneContext(options.context), execute)
159
- : execute();
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
- // CDP Events
582
+ // Stagehand Step Events
163
583
  // ===========================================================================
164
- static NOISY_CDP_EVENTS = new Set([
165
- "Target.targetInfoChanged",
166
- "Runtime.executionContextCreated",
167
- "Runtime.executionContextDestroyed",
168
- "Runtime.executionContextsCleared",
169
- "Page.lifecycleEvent",
170
- "Network.dataReceived",
171
- "Network.loadingFinished",
172
- "Network.requestWillBeSentExtraInfo",
173
- "Network.responseReceivedExtraInfo",
174
- "Network.requestWillBeSent",
175
- "Network.responseReceived",
176
- ]);
177
- static logCdpEvent(context, eventType, { method, params, result, error, targetId, }, eventParentIds) {
178
- if (method.endsWith(".enable") || method === "enable") {
179
- return null;
180
- }
181
- if (eventType === "message" && FlowLogger.NOISY_CDP_EVENTS.has(method)) {
182
- return null;
183
- }
184
- return loggerContext.run(FlowLogger.cloneContext(context), () => FlowLogger.emit({
185
- eventIdSuffix: "6",
186
- eventType: eventType === "call"
187
- ? "CdpCallEvent"
188
- : eventType === "response"
189
- ? "CdpResponseEvent"
190
- : eventType === "responseError"
191
- ? "CdpResponseErrorEvent"
192
- : "CdpMessageEvent",
193
- eventParentIds,
194
- data: {
195
- method,
196
- params,
197
- result,
198
- error,
199
- targetId,
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 logCdpCallEvent(context, data) {
204
- return FlowLogger.logCdpEvent(context, "call", data);
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 logCdpResponseEvent(context, parentEvent, data) {
207
- FlowLogger.logCdpEvent(context, data.error ? "responseError" : "response", data, [...parentEvent.eventParentIds, parentEvent.eventId]);
682
+ static logCdpCallEvent(data, ctx) {
683
+ SessionFileLogger.logCdpEvent("call", data, ctx);
208
684
  }
209
- static logCdpMessageEvent(context, parentEvent, data) {
210
- FlowLogger.logCdpEvent(context, "message", data, [
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
- FlowLogger.emit({
220
- eventIdSuffix: "7",
221
- eventType: "LlmRequestEvent",
222
- data: {
223
- requestId,
224
- model,
225
- prompt,
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
- FlowLogger.emit({
231
- eventIdSuffix: "7",
232
- eventType: "LlmResponseEvent",
233
- data: {
234
- requestId,
235
- model,
236
- output,
237
- inputTokens,
238
- outputTokens,
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
- promptSummary = `${lastMsg.content} +{${toolCount} tools}`;
757
+ extracted.text = lastMsg.content;
261
758
  }
262
759
  else if (Array.isArray(lastMsg.content)) {
263
- const toolResult = lastMsg.content.find((part) => part.type === "tool-result");
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
- if (toolResult.output?.type === "json" &&
267
- toolResult.output.value) {
268
- promptSummary = `${JSON.stringify(toolResult.output.value)} +{${toolCount} tools}`;
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(toolResult.output?.value)) {
271
- promptSummary = `${extractLlmMessageSummary({
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
- promptSummary = `${extractLlmMessageSummary({ content: lastMsg.content }) ??
278
- "(no text)"} +{${toolCount} tools}`;
773
+ extractFromContent(lastMsg.content, extracted);
279
774
  }
280
775
  }
281
- promptSummary = `${rolePrefix}: ${promptSummary}`;
282
776
  }
283
- else {
284
- promptSummary = `?: ${promptSummary}`;
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
- prompt: promptSummary,
290
- });
782
+ operation: "generateText",
783
+ prompt: promptPreview,
784
+ }, ctx);
291
785
  const result = await doGenerate();
292
- // Extract output summary
786
+ // Extract output preview
293
787
  const res = result;
294
- let outputSummary = res.text || "";
295
- if (!outputSummary && res.content) {
788
+ let outputPreview = res.text || "";
789
+ if (!outputPreview && res.content) {
296
790
  if (typeof res.content === "string") {
297
- outputSummary = res.content;
791
+ outputPreview = res.content;
298
792
  }
299
793
  else if (Array.isArray(res.content)) {
300
- outputSummary = res.content
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 (!outputSummary && res.toolCalls?.length) {
309
- outputSummary = `[${res.toolCalls.length} tool calls]`;
802
+ if (!outputPreview && res.toolCalls?.length) {
803
+ outputPreview = `[${res.toolCalls.length} tool calls]`;
310
804
  }
311
- FlowLogger.logLlmResponse({
805
+ SessionFileLogger.logLlmResponse({
312
806
  requestId: llmRequestId,
313
807
  model: modelId,
314
- output: outputSummary || "[empty]",
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
- /** Extract text and image info from a content array (handles nested tool_result) */
324
- function extractLlmMessageContent(content) {
325
- const result = {
326
- text: undefined,
327
- extras: [],
328
- };
329
- for (const part of content) {
330
- const p = part;
331
- // Text
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
- // Recurse into tool_result content
352
- if (p.type === "tool_result" && Array.isArray(p.content)) {
353
- const nested = extractLlmMessageContent(p.content);
354
- if (!result.text && nested.text) {
355
- result.text = nested.text;
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
- result.extras.push(...nested.extras);
358
- }
359
- }
360
- return result;
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
- * Extract a text summary from CUA-style messages.
419
- * Accepts various message formats (Anthropic, OpenAI, Google).
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 extractLlmCuaPromptSummary(messages) {
422
- try {
423
- const lastMsg = messages
424
- .filter((m) => {
425
- const msg = m;
426
- return msg.role === "user" || msg.type === "tool_result";
427
- })
428
- .pop();
429
- if (!lastMsg)
430
- return undefined;
431
- return extractLlmMessageSummary(lastMsg);
432
- }
433
- catch {
434
- return undefined;
435
- }
436
- }
437
- /** Format a CUA response summary for logging */
438
- export function extractLlmCuaResponseSummary(output) {
439
- try {
440
- // Handle Google format or array
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