@browserbasehq/orca 3.2.0-preview.1 → 3.2.0-preview.2

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