pi-agent-browser-native 0.2.32 → 0.2.33

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 (62) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +27 -16
  3. package/docs/ARCHITECTURE.md +3 -2
  4. package/docs/COMMAND_REFERENCE.md +18 -10
  5. package/docs/ELECTRON.md +23 -4
  6. package/docs/RELEASE.md +4 -2
  7. package/docs/REQUIREMENTS.md +1 -1
  8. package/docs/SUPPORT_MATRIX.md +28 -16
  9. package/docs/TOOL_CONTRACT.md +29 -24
  10. package/extensions/agent-browser/index.ts +404 -4371
  11. package/extensions/agent-browser/lib/input-modes/electron.ts +170 -0
  12. package/extensions/agent-browser/lib/input-modes/job.ts +203 -0
  13. package/extensions/agent-browser/lib/input-modes/lookups.ts +447 -0
  14. package/extensions/agent-browser/lib/input-modes/params.ts +188 -0
  15. package/extensions/agent-browser/lib/input-modes/semantic-action.ts +107 -0
  16. package/extensions/agent-browser/lib/input-modes/shared.ts +46 -0
  17. package/extensions/agent-browser/lib/input-modes/types.ts +221 -0
  18. package/extensions/agent-browser/lib/input-modes.ts +41 -0
  19. package/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts +696 -0
  20. package/extensions/agent-browser/lib/orchestration/browser-run/final-result.ts +450 -0
  21. package/extensions/agent-browser/lib/orchestration/browser-run/index.ts +46 -0
  22. package/extensions/agent-browser/lib/orchestration/browser-run/prepare.ts +711 -0
  23. package/extensions/agent-browser/lib/orchestration/browser-run/process-output.ts +386 -0
  24. package/extensions/agent-browser/lib/orchestration/browser-run/session-state.ts +868 -0
  25. package/extensions/agent-browser/lib/orchestration/browser-run/types.ts +476 -0
  26. package/extensions/agent-browser/lib/orchestration/browser-run.ts +1 -0
  27. package/extensions/agent-browser/lib/orchestration/input-plan.ts +338 -0
  28. package/extensions/agent-browser/lib/playbook.ts +12 -11
  29. package/extensions/agent-browser/lib/process.ts +106 -4
  30. package/extensions/agent-browser/lib/results/action-recommendations.ts +269 -0
  31. package/extensions/agent-browser/lib/results/artifact-manifest.ts +114 -0
  32. package/extensions/agent-browser/lib/results/artifact-state.ts +13 -0
  33. package/extensions/agent-browser/lib/results/categories.ts +106 -0
  34. package/extensions/agent-browser/lib/results/contracts.ts +220 -0
  35. package/extensions/agent-browser/lib/results/editable-ref-evidence.ts +72 -0
  36. package/extensions/agent-browser/lib/results/envelope.ts +2 -1
  37. package/extensions/agent-browser/lib/results/network.ts +64 -0
  38. package/extensions/agent-browser/lib/results/next-actions.ts +117 -0
  39. package/extensions/agent-browser/lib/results/presentation/artifacts.ts +506 -0
  40. package/extensions/agent-browser/lib/results/presentation/batch.ts +355 -0
  41. package/extensions/agent-browser/lib/results/presentation/common.ts +53 -0
  42. package/extensions/agent-browser/lib/results/presentation/content.ts +36 -0
  43. package/extensions/agent-browser/lib/results/presentation/diagnostics.ts +730 -0
  44. package/extensions/agent-browser/lib/results/presentation/errors.ts +125 -0
  45. package/extensions/agent-browser/lib/results/presentation/large-output.ts +182 -0
  46. package/extensions/agent-browser/lib/results/presentation/navigation.ts +216 -0
  47. package/extensions/agent-browser/lib/results/presentation/registry.ts +154 -0
  48. package/extensions/agent-browser/lib/results/presentation/skills.ts +143 -0
  49. package/extensions/agent-browser/lib/results/presentation.ts +87 -2399
  50. package/extensions/agent-browser/lib/results/recovery-actions.ts +139 -0
  51. package/extensions/agent-browser/lib/results/recovery-next-actions.ts +71 -0
  52. package/extensions/agent-browser/lib/results/selector-recovery.ts +312 -0
  53. package/extensions/agent-browser/lib/results/shared.ts +17 -789
  54. package/extensions/agent-browser/lib/results/snapshot-high-value-controls.ts +262 -0
  55. package/extensions/agent-browser/lib/results/snapshot-refs.ts +100 -0
  56. package/extensions/agent-browser/lib/results/snapshot-segments.ts +366 -0
  57. package/extensions/agent-browser/lib/results/snapshot-spill.ts +63 -0
  58. package/extensions/agent-browser/lib/results/snapshot.ts +37 -489
  59. package/extensions/agent-browser/lib/results/text.ts +40 -0
  60. package/extensions/agent-browser/lib/results.ts +16 -5
  61. package/extensions/agent-browser/lib/session-page-state.ts +486 -0
  62. package/package.json +2 -1
@@ -0,0 +1,338 @@
1
+ import { validateToolArgs, redactInvocationArgs, redactSensitiveText } from "../runtime.js";
2
+ import { buildAgentBrowserResultCategoryDetails } from "../results/categories.js";
3
+ import {
4
+ compileAgentBrowserElectron,
5
+ compileAgentBrowserJob,
6
+ compileAgentBrowserNetworkSourceLookup,
7
+ compileAgentBrowserQaPreset,
8
+ compileAgentBrowserSemanticAction,
9
+ compileAgentBrowserSourceLookup,
10
+ redactNetworkSourceLookupArgs,
11
+ redactNetworkSourceLookupUrl,
12
+ type CompiledAgentBrowserElectron,
13
+ type CompiledAgentBrowserJob,
14
+ type CompiledAgentBrowserNetworkSourceLookup,
15
+ type CompiledAgentBrowserQaPreset,
16
+ type CompiledAgentBrowserSemanticAction,
17
+ type CompiledAgentBrowserSourceLookup,
18
+ } from "../input-modes.js";
19
+
20
+ export interface AgentBrowserExecuteParams {
21
+ args?: string[];
22
+ electron?: unknown;
23
+ job?: unknown;
24
+ networkSourceLookup?: unknown;
25
+ qa?: unknown;
26
+ semanticAction?: unknown;
27
+ sessionMode?: "auto" | "fresh";
28
+ sourceLookup?: unknown;
29
+ stdin?: string;
30
+ }
31
+
32
+ export type ResolvedAgentBrowserInputKind = "args" | "electron" | "job" | "networkSourceLookup" | "qa" | "semanticAction" | "sourceLookup";
33
+
34
+ type ResolvedAgentBrowserInputModeFields = {
35
+ compiledElectron?: CompiledAgentBrowserElectron;
36
+ compiledGeneratedBatch?: CompiledAgentBrowserJob | CompiledAgentBrowserNetworkSourceLookup | CompiledAgentBrowserSourceLookup;
37
+ compiledJob?: CompiledAgentBrowserJob;
38
+ compiledNetworkSourceLookup?: CompiledAgentBrowserNetworkSourceLookup;
39
+ compiledQaPreset?: CompiledAgentBrowserQaPreset;
40
+ compiledSemanticAction?: CompiledAgentBrowserSemanticAction;
41
+ compiledSourceLookup?: CompiledAgentBrowserSourceLookup;
42
+ redactedCompiledElectron?: CompiledAgentBrowserElectron;
43
+ redactedCompiledJob?: CompiledAgentBrowserJob;
44
+ redactedCompiledNetworkSourceLookup?: CompiledAgentBrowserNetworkSourceLookup;
45
+ redactedCompiledQaPreset?: CompiledAgentBrowserQaPreset;
46
+ redactedCompiledSemanticAction?: CompiledAgentBrowserSemanticAction;
47
+ redactedCompiledSourceLookup?: CompiledAgentBrowserSourceLookup;
48
+ };
49
+
50
+ interface ResolvedAgentBrowserInputBase {
51
+ redactedArgs: string[];
52
+ toolArgs: string[];
53
+ toolStdin?: string;
54
+ }
55
+
56
+ interface ResolvedAgentBrowserValidInputBase extends ResolvedAgentBrowserInputBase {
57
+ status: "valid";
58
+ validationError?: undefined;
59
+ }
60
+
61
+ export interface ResolvedAgentBrowserInvalidInput extends ResolvedAgentBrowserInputBase, ResolvedAgentBrowserInputModeFields {
62
+ attemptedKind?: ResolvedAgentBrowserInputKind;
63
+ kind: "invalid";
64
+ status: "invalid";
65
+ validationError: string;
66
+ }
67
+
68
+ export type ResolvedAgentBrowserValidInput =
69
+ | (ResolvedAgentBrowserValidInputBase & {
70
+ kind: "args";
71
+ })
72
+ | (ResolvedAgentBrowserValidInputBase & {
73
+ compiledElectron: CompiledAgentBrowserElectron;
74
+ kind: "electron";
75
+ redactedCompiledElectron: CompiledAgentBrowserElectron;
76
+ })
77
+ | (ResolvedAgentBrowserValidInputBase & {
78
+ compiledGeneratedBatch: CompiledAgentBrowserJob;
79
+ compiledJob: CompiledAgentBrowserJob;
80
+ kind: "job";
81
+ redactedCompiledJob: CompiledAgentBrowserJob;
82
+ })
83
+ | (ResolvedAgentBrowserValidInputBase & {
84
+ compiledGeneratedBatch: CompiledAgentBrowserNetworkSourceLookup;
85
+ compiledNetworkSourceLookup: CompiledAgentBrowserNetworkSourceLookup;
86
+ kind: "networkSourceLookup";
87
+ redactedCompiledNetworkSourceLookup: CompiledAgentBrowserNetworkSourceLookup;
88
+ })
89
+ | (ResolvedAgentBrowserValidInputBase & {
90
+ compiledGeneratedBatch: CompiledAgentBrowserQaPreset;
91
+ compiledJob: CompiledAgentBrowserQaPreset;
92
+ compiledQaPreset: CompiledAgentBrowserQaPreset;
93
+ kind: "qa";
94
+ redactedCompiledJob: CompiledAgentBrowserJob;
95
+ redactedCompiledQaPreset: CompiledAgentBrowserQaPreset;
96
+ })
97
+ | (ResolvedAgentBrowserValidInputBase & {
98
+ compiledSemanticAction: CompiledAgentBrowserSemanticAction;
99
+ kind: "semanticAction";
100
+ redactedCompiledSemanticAction: CompiledAgentBrowserSemanticAction;
101
+ })
102
+ | (ResolvedAgentBrowserValidInputBase & {
103
+ compiledGeneratedBatch: CompiledAgentBrowserSourceLookup;
104
+ compiledSourceLookup: CompiledAgentBrowserSourceLookup;
105
+ kind: "sourceLookup";
106
+ redactedCompiledSourceLookup: CompiledAgentBrowserSourceLookup;
107
+ });
108
+
109
+ export type ResolvedAgentBrowserInput = ResolvedAgentBrowserInvalidInput | ResolvedAgentBrowserValidInput;
110
+
111
+ function redactCompiledElectron(compiled: CompiledAgentBrowserElectron | undefined): CompiledAgentBrowserElectron | undefined {
112
+ if (!compiled) return undefined;
113
+ if (compiled.action === "list") {
114
+ return { ...compiled, query: compiled.query ? redactSensitiveText(compiled.query) : undefined };
115
+ }
116
+ if (compiled.action === "launch") {
117
+ return { ...compiled, appArgs: compiled.appArgs ? redactInvocationArgs(compiled.appArgs) : undefined };
118
+ }
119
+ return { ...compiled };
120
+ }
121
+
122
+ function redactCompiledJob(compiled: CompiledAgentBrowserJob | undefined): CompiledAgentBrowserJob | undefined {
123
+ const redactedSteps = compiled?.steps.map((step) => ({ ...step, args: redactInvocationArgs(step.args) }));
124
+ return compiled && redactedSteps
125
+ ? { ...compiled, stdin: JSON.stringify(redactedSteps.map((step) => step.args)), steps: redactedSteps }
126
+ : undefined;
127
+ }
128
+
129
+ function redactCompiledSourceLookup(compiled: CompiledAgentBrowserSourceLookup | undefined): CompiledAgentBrowserSourceLookup | undefined {
130
+ const redactedSteps = compiled?.steps.map((step) => ({ ...step, args: redactInvocationArgs(step.args) }));
131
+ return compiled && redactedSteps
132
+ ? { ...compiled, stdin: JSON.stringify(redactedSteps.map((step) => step.args)), steps: redactedSteps }
133
+ : undefined;
134
+ }
135
+
136
+ function redactCompiledNetworkSourceLookup(compiled: CompiledAgentBrowserNetworkSourceLookup | undefined): CompiledAgentBrowserNetworkSourceLookup | undefined {
137
+ const redactedSteps = compiled?.steps.map((step) => ({ ...step, args: redactNetworkSourceLookupArgs(step.args) }));
138
+ return compiled && redactedSteps
139
+ ? {
140
+ ...compiled,
141
+ args: redactNetworkSourceLookupArgs(compiled.args),
142
+ query: {
143
+ ...compiled.query,
144
+ filter: redactNetworkSourceLookupUrl(compiled.query.filter),
145
+ url: redactNetworkSourceLookupUrl(compiled.query.url),
146
+ },
147
+ stdin: JSON.stringify(redactedSteps.map((step) => step.args)),
148
+ steps: redactedSteps,
149
+ }
150
+ : undefined;
151
+ }
152
+
153
+ export function resolveAgentBrowserInput(options: {
154
+ getBatchAnnotateValidationError: (args: string[], stdin: string | undefined) => string | undefined;
155
+ managedSessionActive: boolean;
156
+ params: AgentBrowserExecuteParams;
157
+ }): ResolvedAgentBrowserInput {
158
+ const { getBatchAnnotateValidationError, managedSessionActive, params } = options;
159
+ const semanticActionResult = params.semanticAction === undefined ? {} : compileAgentBrowserSemanticAction(params.semanticAction);
160
+ const jobResult = params.job === undefined ? {} : compileAgentBrowserJob(params.job);
161
+ const qaResult = params.qa === undefined ? {} : compileAgentBrowserQaPreset(params.qa);
162
+ const sourceLookupResult = params.sourceLookup === undefined ? {} : compileAgentBrowserSourceLookup(params.sourceLookup);
163
+ const networkSourceLookupResult = params.networkSourceLookup === undefined ? {} : compileAgentBrowserNetworkSourceLookup(params.networkSourceLookup);
164
+ const electronResult = params.electron === undefined ? {} : compileAgentBrowserElectron(params.electron);
165
+
166
+ const hasExplicitArgs = Array.isArray(params.args);
167
+ const explicitInputModes = [
168
+ hasExplicitArgs,
169
+ Boolean(semanticActionResult.compiled),
170
+ Boolean(jobResult.compiled),
171
+ Boolean(qaResult.compiled),
172
+ Boolean(sourceLookupResult.compiled),
173
+ Boolean(networkSourceLookupResult.compiled),
174
+ Boolean(electronResult.compiled),
175
+ ].filter(Boolean).length;
176
+ const inputModeError = explicitInputModes !== 1
177
+ ? "Provide exactly one of args, semanticAction, job, qa, sourceLookup, networkSourceLookup, or electron."
178
+ : undefined;
179
+
180
+ const compiledSemanticAction = semanticActionResult.compiled;
181
+ const compiledQaPreset = qaResult.compiled;
182
+ const compiledSourceLookup = sourceLookupResult.compiled;
183
+ const compiledNetworkSourceLookup = networkSourceLookupResult.compiled;
184
+ const compiledElectron = electronResult.compiled;
185
+ const compiledJob = jobResult.compiled ?? compiledQaPreset;
186
+ const compiledGeneratedBatch = compiledNetworkSourceLookup ?? compiledSourceLookup ?? compiledJob;
187
+ const toolArgs = compiledElectron ? [] : compiledSemanticAction?.args ?? compiledGeneratedBatch?.args ?? params.args ?? [];
188
+ const toolStdin = compiledGeneratedBatch?.stdin ?? params.stdin;
189
+ const redactedArgs = redactInvocationArgs(toolArgs);
190
+ const generatedStdinError = params.stdin !== undefined
191
+ ? compiledGeneratedBatch
192
+ ? "Do not provide stdin with job, qa, sourceLookup, or networkSourceLookup; those modes generate their own batch stdin."
193
+ : compiledElectron
194
+ ? "Do not provide stdin with electron; electron mode is host-only or manages its own input."
195
+ : undefined
196
+ : undefined;
197
+ const attachedQaSessionError = compiledQaPreset?.checks.attached
198
+ ? params.sessionMode === "fresh"
199
+ ? "qa.attached cannot be used with sessionMode=fresh; attach or launch a session first, then run qa.attached with the current session."
200
+ : !managedSessionActive
201
+ ? "qa.attached requires an active attached session. Run electron.launch or connect to an Electron debug port first."
202
+ : undefined
203
+ : undefined;
204
+ const validationError = semanticActionResult.error
205
+ ?? jobResult.error
206
+ ?? qaResult.error
207
+ ?? sourceLookupResult.error
208
+ ?? networkSourceLookupResult.error
209
+ ?? electronResult.error
210
+ ?? inputModeError
211
+ ?? generatedStdinError
212
+ ?? attachedQaSessionError
213
+ ?? (compiledElectron ? undefined : validateToolArgs(toolArgs) ?? getBatchAnnotateValidationError(toolArgs, toolStdin));
214
+ const redactedCompiledJob = redactCompiledJob(compiledJob);
215
+ const redactedCompiledSemanticAction = compiledSemanticAction
216
+ ? { ...compiledSemanticAction, args: redactInvocationArgs(compiledSemanticAction.args) }
217
+ : undefined;
218
+ const attemptedKind: ResolvedAgentBrowserInputKind | undefined = compiledElectron
219
+ ? "electron"
220
+ : compiledNetworkSourceLookup
221
+ ? "networkSourceLookup"
222
+ : compiledSourceLookup
223
+ ? "sourceLookup"
224
+ : compiledQaPreset
225
+ ? "qa"
226
+ : jobResult.compiled
227
+ ? "job"
228
+ : compiledSemanticAction
229
+ ? "semanticAction"
230
+ : hasExplicitArgs
231
+ ? "args"
232
+ : undefined;
233
+
234
+ const redactedCompiledElectron = redactCompiledElectron(compiledElectron);
235
+ const redactedCompiledNetworkSourceLookup = redactCompiledNetworkSourceLookup(compiledNetworkSourceLookup);
236
+ const redactedCompiledQaPreset = compiledQaPreset && redactedCompiledJob ? { ...redactedCompiledJob, checks: compiledQaPreset.checks } : undefined;
237
+ const redactedCompiledSourceLookup = redactCompiledSourceLookup(compiledSourceLookup);
238
+ const resolvedBase: ResolvedAgentBrowserInputBase = { redactedArgs, toolArgs, toolStdin };
239
+ if (validationError) {
240
+ return {
241
+ ...resolvedBase,
242
+ attemptedKind,
243
+ compiledElectron,
244
+ compiledGeneratedBatch,
245
+ compiledJob,
246
+ compiledNetworkSourceLookup,
247
+ compiledQaPreset,
248
+ compiledSemanticAction,
249
+ compiledSourceLookup,
250
+ kind: "invalid",
251
+ redactedCompiledElectron,
252
+ redactedCompiledJob,
253
+ redactedCompiledNetworkSourceLookup,
254
+ redactedCompiledQaPreset,
255
+ redactedCompiledSemanticAction,
256
+ redactedCompiledSourceLookup,
257
+ status: "invalid",
258
+ validationError,
259
+ };
260
+ }
261
+ if (compiledElectron && redactedCompiledElectron) {
262
+ return { ...resolvedBase, compiledElectron, kind: "electron", redactedCompiledElectron, status: "valid" };
263
+ }
264
+ if (compiledNetworkSourceLookup && redactedCompiledNetworkSourceLookup) {
265
+ return {
266
+ ...resolvedBase,
267
+ compiledGeneratedBatch: compiledNetworkSourceLookup,
268
+ compiledNetworkSourceLookup,
269
+ kind: "networkSourceLookup",
270
+ redactedCompiledNetworkSourceLookup,
271
+ status: "valid",
272
+ };
273
+ }
274
+ if (compiledSourceLookup && redactedCompiledSourceLookup) {
275
+ return {
276
+ ...resolvedBase,
277
+ compiledGeneratedBatch: compiledSourceLookup,
278
+ compiledSourceLookup,
279
+ kind: "sourceLookup",
280
+ redactedCompiledSourceLookup,
281
+ status: "valid",
282
+ };
283
+ }
284
+ if (compiledQaPreset && redactedCompiledJob && redactedCompiledQaPreset) {
285
+ return {
286
+ ...resolvedBase,
287
+ compiledGeneratedBatch: compiledQaPreset,
288
+ compiledJob: compiledQaPreset,
289
+ compiledQaPreset,
290
+ kind: "qa",
291
+ redactedCompiledJob,
292
+ redactedCompiledQaPreset,
293
+ status: "valid",
294
+ };
295
+ }
296
+ if (jobResult.compiled && redactedCompiledJob) {
297
+ return {
298
+ ...resolvedBase,
299
+ compiledGeneratedBatch: jobResult.compiled,
300
+ compiledJob: jobResult.compiled,
301
+ kind: "job",
302
+ redactedCompiledJob,
303
+ status: "valid",
304
+ };
305
+ }
306
+ if (compiledSemanticAction && redactedCompiledSemanticAction) {
307
+ return { ...resolvedBase, compiledSemanticAction, kind: "semanticAction", redactedCompiledSemanticAction, status: "valid" };
308
+ }
309
+ return { ...resolvedBase, kind: "args", status: "valid" };
310
+ }
311
+
312
+ export function buildValidationFailureResult(input: ResolvedAgentBrowserInvalidInput): {
313
+ content: Array<{ text: string; type: "text" }>;
314
+ details: Record<string, unknown>;
315
+ isError: true;
316
+ } {
317
+ const validationError = input.validationError ?? "Invalid agent_browser input.";
318
+ return {
319
+ content: [{ type: "text", text: validationError }],
320
+ details: {
321
+ args: input.redactedArgs,
322
+ compiledElectron: input.redactedCompiledElectron,
323
+ compiledJob: input.redactedCompiledJob,
324
+ compiledQaPreset: input.redactedCompiledQaPreset,
325
+ compiledSourceLookup: input.redactedCompiledSourceLookup,
326
+ compiledNetworkSourceLookup: input.redactedCompiledNetworkSourceLookup,
327
+ compiledSemanticAction: input.redactedCompiledSemanticAction,
328
+ ...buildAgentBrowserResultCategoryDetails({
329
+ args: input.redactedArgs,
330
+ errorText: validationError,
331
+ succeeded: false,
332
+ validationError,
333
+ }),
334
+ validationError,
335
+ },
336
+ isError: true,
337
+ };
338
+ }
@@ -21,7 +21,7 @@ export const QUICK_START_GUIDELINES = [
21
21
  "Quick start mental model: use exactly one of args (exact agent-browser CLI args after the binary), semanticAction (a thin shorthand compiled to find argv for locator actions or select argv for native dropdowns), job (a constrained short-workflow schema compiled to batch), qa (a lightweight QA preset built on job/batch, including qa.attached for current sessions), electron (desktop Electron list/launch/status/cleanup/probe), or the experimental sourceLookup / networkSourceLookup helpers (each compiled to batch); stdin is only for batch, eval --stdin, auth save --password-stdin, and wrapper-generated batch stdin from job, qa, sourceLookup, or networkSourceLookup, and is rejected with electron; sessionMode=fresh switches the extension-managed pi-scoped session to a fresh upstream launch when you need new --profile, --session-name, --cdp, --state, --auto-connect, --init-script, --enable, -p/--provider, or iOS --device state.",
22
22
  "There is no first-class reusable named browser recipe runtime above top-level job, the qa preset, and raw batch stdin; keep recurring flows in documentation examples or those inputs (closed RQ-0068; see docs/ARCHITECTURE.md#no-reusable-recipe-layer-yet).",
23
23
  "Common first calls: { args: [\"open\", \"https://example.com\"] } then { args: [\"snapshot\", \"-i\"] }; after navigation, use { args: [\"click\", \"@e2\"] } then { args: [\"snapshot\", \"-i\"] }.",
24
- "Locator-first clicks/fills and native select changes without hand-building argv: { semanticAction: { action: \"click\", locator: \"text\", value: \"Close\" } }, { semanticAction: { action: \"fill\", locator: \"label\", value: \"Email\", text: \"user@example.com\" } }, or { semanticAction: { action: \"select\", selector: \"#flavor\", value: \"chocolate\" } }; add semanticAction.session when targeting a named upstream browser session; details.compiledSemanticAction shows the semantic target, while details.effectiveArgs may show a resolved current @ref for active-session role/name click/check/uncheck actions to avoid hidden duplicate matches; selector-not-found failures may append bounded try-*-candidate next actions (and an Agent-browser candidate fallbacks prose block) for specific placeholder/text/label shapes, and stale-ref failures can return retry-semantic-action-after-stale-ref for compiled find actions when retry safety is provable.",
24
+ "Locator-first clicks/fills and native select changes without hand-building argv: { semanticAction: { action: \"click\", locator: \"text\", value: \"Close\" } }, { semanticAction: { action: \"fill\", locator: \"label\", value: \"Email\", text: \"user@example.com\" } }, or { semanticAction: { action: \"select\", selector: \"#flavor\", value: \"chocolate\" } }; add semanticAction.session when targeting a named upstream browser session; details.compiledSemanticAction shows the semantic target, while details.effectiveArgs may show a resolved current @ref for active-session role/name click/check/uncheck actions to avoid hidden duplicate matches; selector-not-found failures may append bounded click try-*-candidate next actions or, for fill misses with current editable refs, details.richInputRecovery with focus/click actions that do not copy fill text; stale-ref failures can return retry-semantic-action-after-stale-ref for compiled find actions when retry safety is provable.",
25
25
  "Common advanced calls: { args: [\"batch\"], stdin: \"[[\\\"open\\\",\\\"https://example.com\\\"],[\\\"snapshot\\\",\\\"-i\\\"]]\" }, { job: { steps: [{ action: \"open\", url: \"https://example.com\" }, { action: \"assertText\", text: \"Example Domain\" }, { action: \"screenshot\", path: \".dogfood/example.png\" }] } }, { qa: { url: \"https://example.com\", expectedText: \"Example Domain\", screenshotPath: \".dogfood/qa-example.png\" } }, { electron: { action: \"list\", query: \"code\" } }, { electron: { action: \"launch\", appName: \"Visual Studio Code\", handoff: \"snapshot\" } }, { electron: { action: \"probe\" } }, { qa: { attached: true, expectedText: \"Explorer\" } }, { args: [\"eval\", \"--stdin\"], stdin: \"document.title\" }, { args: [\"auth\", \"save\", \"name\", \"--password-stdin\"], stdin: \"<password from user-approved secret source>\" }, { args: [\"--profile\", \"Default\", \"open\", \"https://example.com/account\"], sessionMode: \"fresh\" }, and { args: [\"open\", \"--enable\", \"react-devtools\", \"https://example.com\"], sessionMode: \"fresh\" }. For app pages with a native dropdown, job steps can include { action: \"select\", selector: \"#flavor\", value: \"chocolate\" } before the dependent assertion.",
26
26
  "High-value command reference: select <selector> <value...> changes native dropdown values; download <selector> <path> saves a file triggered by a click; get title/url/text/html/value/attr/count reads page state; screenshot [path] captures an image; pdf <path> saves a PDF; tab list and tab <tab-id-or-label> inspect or recover the active tab; react tree/inspect/renders/suspense introspect React after --enable react-devtools; vitals [url] measures Core Web Vitals; pushstate <url> performs SPA navigation.",
27
27
  "For artifact-producing commands, read the visible artifact block and details.artifactVerification before using files: check requested path, absolute path, existence, size bytes, artifact kind, optional mediaType, status, optional limitation, and verified/missing/pending/unverified counts. details.artifacts contains per-file metadata. Browser close does not delete explicit saved files; if close reports details.artifactCleanup, use host file tools to remove paths listed in explicitArtifactPaths (when non-empty) after inspection. For annotated screenshots inside batch, put --annotate in top-level args (for example { args: [\"--annotate\", \"batch\"], stdin: \"[[\\\"screenshot\\\",\\\"/tmp/page.png\\\"]]\" }) rather than inside the screenshot step.",
@@ -36,6 +36,7 @@ export const SHARED_BROWSER_PLAYBOOK_GUIDELINES = [
36
36
  "For ordinary forms from one snapshot, batch multiple fill @refs before the submit/click step to avoid serial tool calls; if a fill may autosubmit, navigate, or rerender later fields, split the flow and refresh refs first.",
37
37
  "When snapshot -i compacts because the tree is oversized, scan visible output for Omitted high-value controls and optional details.data.highValueControlRefIds before opening the spill file: those list bounded searchboxes, textboxes, comboboxes, buttons, tabs, checkboxes, radios, options, and menuitems that did not fit the key/other ref previews.",
38
38
  "When a visible text or accessible-name target should survive ref churn, prefer find locators such as role, text, label, placeholder, alt, title, or testid with the intended action instead of guessing a CSS selector.",
39
+ "For desktop or host-controlled rich inputs, if semanticAction fill misses, refresh refs and prefer a current editable @ref from details.richInputRecovery or the latest snapshot; focus or click that ref, then use keyboard inserttext or keyboard type with the intended text. Do not auto-submit with Enter or a submit button unless the user flow explicitly calls for it.",
39
40
  "Do not assume Playwright selector dialects such as text=Close or button:has-text('Close') are supported wrapper syntax unless current upstream agent-browser behavior has been verified.",
40
41
  "For authenticated or user-specific content explicitly requested by the user, such as feeds, inboxes, account pages, or private dashboards, prefer --profile Default on the first browser call and let the implicit session carry continuity. Do not use a real profile for public pages just because they are dashboards. Treat visible page content from real profiles as model-visible transcript data; use --auto-connect only if profile-based reuse is unavailable or the task is specifically about attaching to a running debug-enabled browser.",
41
42
  "Do not invent fixed explicit session names for routine tasks. Use the implicit session unless you truly need multiple isolated browser sessions in the same conversation.",
@@ -46,10 +47,10 @@ export const SHARED_BROWSER_PLAYBOOK_GUIDELINES = [
46
47
  "For stateful browser context work, prefer purpose-specific page actions before dumping browser data: use auth save --password-stdin with the tool stdin field for credentials, state save/load for portable test state, cookies get/set/clear and storage local|session only when the task needs those values, and expect cookie/storage/auth/state summaries to redact credential-like fields.",
47
48
  "For batch chains that touch cookies, storage, auth, or other secret-bearing commands, use details.batchSteps for per-step artifacts, categories, spill paths, and full structured errors; top-level details.data on batch is only a compact redacted step matrix (success, argv-redacted command, redacted result or scrubbed error text) built from the same presentation rules as standalone calls.",
48
49
  "For non-core families, pass current upstream commands through the native tool directly: network route/requests/har, diff snapshot/screenshot/url, trace/profiler/record, console/errors/highlight/inspect/clipboard, stream enable/disable/status, dashboard start/stop, and chat. For compact network requests output, prefer details.nextActions for request detail, actionable failed-request networkSourceLookup, filtering, or HAR capture follow-ups instead of guessing request-id syntax. Artifact-producing commands report details.artifacts and verification state; long-running starts such as stream, dashboard, trace/profiler, and record should be paired with the matching stop/disable command when the task is done.",
49
- "For Electron desktop apps, prefer top-level electron for wrapper-owned discovery, isolated launch, status, compact probe, and cleanup: list first, treat likely-sensitive annotations as hints rather than enforcement, launch with the default snapshot handoff unless handoff: \"tabs\" is the safer diagnostic starting point, use electron.probe or snapshot -i/qa.attached for current-session state, and always cleanup the returned launchId when done. electron.launch uses an isolated temporary profile; it does not reuse the app's normal signed-in profile or attach to an already-running authenticated app. For signed-in local app state, host-launch the normal app with --remote-debugging-port when appropriate, then use raw args connect <port|url>; leave shutdown/profile cleanup to the host owner.",
50
+ "For Electron desktop apps, prefer top-level electron for wrapper-owned discovery, isolated launch, status, compact probe, and cleanup: list first, treat likely-sensitive annotations as hints rather than enforcement, launch with the default snapshot handoff unless handoff: \"tabs\" is the safer diagnostic starting point, use electron.probe or snapshot -i/qa.attached for current-session state, and always cleanup the returned launchId when done. electron.launch uses an isolated temporary profile; it does not reuse the app's normal signed-in profile or attach to an already-running authenticated app. For signed-in local app state, host-launch the normal app with --remote-debugging-port when appropriate, then use raw args connect <port|url>; after connect, inspect tab list, select the stable tab id such as tab t2, then run a condition wait or snapshot -i before using refs. close only closes the browser/CDP session; leave manually launched app shutdown, profile cleanup, and explicit artifacts to the host owner.",
50
51
  "For provider or specialized app workflows, load version-matched upstream guidance with skills get agentcore|electron|slack|dogfood|vercel-sandbox through the native tool. Provider launches such as -p ios, --provider browserbase/kernel/browseruse/browserless/agentcore, and iOS --device are upstream-owned setup paths; use sessionMode fresh when switching providers and expect external credentials or local Appium/Xcode setup to be required.",
51
52
  "For dialogs and frames, use dialog status/accept/dismiss and frame <selector|main> through native args; when --confirm-actions produces a pending confirmation, use details.nextActions or exact confirm <id> / deny <id> calls instead of inventing ids.",
52
- "If a session lands on the wrong page or tab, an interaction changes origin unexpectedly, or an open call returns blocked, blank, or otherwise unexpected results, use tab list / tab <tab-id-or-label> / snapshot -i to recover state before retrying different URLs or fallback strategies. Only use wait with an explicit argument like milliseconds, --load <state>, --url <matcher>, --fn <js>, or --text <matcher>.",
53
+ "If a session lands on the wrong page or tab, an interaction changes origin unexpectedly, or an open call returns blocked, blank, or otherwise unexpected results, use tab list / tab <tab-id-or-label> / snapshot -i to recover state before retrying different URLs or fallback strategies. For desktop readiness, prefer real conditions first: wait --text, wait --url, wait --fn, wait --load <state>, wait --download, or qa.attached; use electron.probe/status for wrapper-owned launch health or target mismatch. Fixed waits are a last resort, must stay below the wrapper IPC budget (wait 30000 is intentionally blocked), and a successful payload like \"waited\":\"timeout\" means elapsed time only—verify completion with an observed condition, fresh snapshot, or screenshot.",
53
54
  "For feed, timeline, or inbox reading tasks, focus on the main timeline/list region and read the first item there rather than unrelated composer or sidebar content.",
54
55
  "For read-only browsing tasks, prefer extracting the answer from the current snapshot, structured ref labels, or eval --stdin on the current page before navigating away. Only click into media viewers, detail routes, or new pages when the current view does not contain the needed information.",
55
56
  "For downloads, prefer download <selector> <path> when an element click should save a file. Do not rely on click alone when you need the downloaded file on disk.",
@@ -79,7 +80,7 @@ export const WRAPPER_TAB_RECOVERY_BEHAVIOR = [
79
80
  "After launch-scoped open/goto/navigate calls that can restore existing tabs (for example --profile, --session-name, or --state), agent_browser best-effort re-selects the tab whose URL matches the returned page when restored tabs steal focus during launch.",
80
81
  "After the wrapper observes tab-drift risk for a session (for example profile restore correction, overlapping stale opens, or resumed session state), later active-tab commands best-effort pin that tab inside the same upstream invocation. Routine same-session commands are not preflighted with tab list just because a target tab is known.",
81
82
  "For sessions with observed tab-drift risk, after a successful command on a known target tab, agent_browser also best-effort restores that intended tab if a restored/background tab steals focus after the command completes. Routine same-session commands skip this post-command tab-list probe.",
82
- "If a known session target unexpectedly reports about:blank, agent_browser preserves the prior intended target, best-effort re-selects it when it still exists, and reports exact recovery guidance when it cannot be re-selected.",
83
+ "If a known session target unexpectedly reports about:blank, agent_browser best-effort re-selects the prior intended target when it still exists; if recovery fails, it records the observed about:blank target and reports exact recovery guidance instead of treating the prior page as active.",
83
84
  ] as const;
84
85
 
85
86
  export function buildSharedBrowserPlaybookGuidelines(options: { includeBraveSearch: boolean }): string[] {
@@ -92,14 +93,14 @@ export function buildSharedBrowserPlaybookGuidelines(options: { includeBraveSear
92
93
 
93
94
  const RUNTIME_PROMPT_GUIDELINES = [
94
95
  "Use exactly one input mode: args, semanticAction, job, qa, sourceLookup, networkSourceLookup, or electron. Use stdin only for batch, eval --stdin, auth save --password-stdin, or wrapper-generated batch modes; electron rejects caller stdin.",
95
- "Common flow: open, snapshot -i, interact with current @refs or semanticAction, then re-snapshot after navigation, scrolling, rerenders, or DOM changes. For ordinary forms, batch same-snapshot fill @refs before the submit/click step; split if a fill may autosubmit, navigate, or rerender later fields. Respect explicit stop boundaries: if the user says to stop before order/post/purchase/submit, do not click that final action.",
96
- "Prefer stable locators for visible text/names: semanticAction or upstream find with role/text/label/placeholder/alt/title/testid. For native selects, prefer select <selector> <value...> or semanticAction/job select over clicking option refs. Use current @refs only from the latest same-page snapshot.",
97
- "For signed-in/account-specific content on the web, start with --profile Default plus sessionMode=fresh unless asked otherwise; visible content is model-visible. For desktop/local Electron apps, use electron.list first, not web open; electron.launch is isolated. For signed-in desktop content, if not running, host-launch with --remote-debugging-port then agent_browser connect; if running without a port, ask before relaunch. Use sessionMode=fresh for other launch-scoped state; otherwise keep implicit continuity.",
98
- "For requested screenshots, recordings, downloads, PDFs, or HARs, save the exact user path and read details.artifactVerification before claiming success; report unavailable/missing artifacts instead of silently substituting paths. record stop needs ffmpeg on PATH. close does not delete saved files; cleanup is host-owned. Electron cleanup only covers wrapper-launched Electron processes/profiles for the returned launchId, not explicit artifacts or externally launched apps.",
99
- "When details.nextActions is present, prefer those exact follow-up payloads over prose or guessed selectors; network request diagnostics may include request-detail, actionable failed-request networkSourceLookup, filter, or HAR-capture follow-ups, and Electron lifecycle results may include status/cleanup/tab/snapshot actions keyed to the wrapper launchId/session.",
96
+ "Common flow: open, snapshot -i, interact with current @refs or semanticAction, then re-snapshot after navigation, scrolling, rerenders, or DOM changes. Batch same-snapshot form fills unless a fill may submit, navigate, or rerender. If semanticAction fill misses on a rich host input, use richInputRecovery/current editable refs, focus/click, then keyboard inserttext/type; do not auto-submit. Respect explicit stop boundaries: if the user says to stop before order/post/purchase/submit, do not click that final action.",
97
+ "Prefer stable locators for visible text/names: semanticAction or find role/text/label/placeholder/alt/title/testid. For native selects, use select <selector> <value...> or semanticAction/job select. Use current @refs only from the latest same-page snapshot.",
98
+ "For signed-in/account-specific content on the web, start with --profile Default plus sessionMode=fresh unless asked otherwise; visible content is model-visible. For desktop/Electron apps, use electron.list first. electron.launch is isolated; for signed-in desktop content, host-launch with --remote-debugging-port, connect, tab list, select the stable tab id, then condition-wait or snapshot -i. Use sessionMode=fresh for other launch-scoped state; otherwise keep implicit continuity.",
99
+ "For screenshots, recordings, downloads, PDFs, or HARs, save the exact user path and check details.artifactVerification before claiming success. record stop needs ffmpeg on PATH. close does not delete saved files; cleanup is host-owned. Electron cleanup only covers wrapper-launched launchIds. For desktop readiness, prefer condition waits, qa.attached, electron.probe/status, tab list/tab tN, snapshot, or screenshot; wait 30000 is blocked, and \"waited\":\"timeout\" is not completion evidence.",
100
+ "When details.nextActions is present, prefer those exact follow-up payloads over prose or guessed selectors; diagnostics may include request detail, networkSourceLookup, HAR capture, Electron status/cleanup, tab selection, rich-input focus/click recovery, or snapshot actions.",
100
101
  "For dense snapshots, check Omitted high-value controls and details.data.highValueControlRefIds before opening large spill files.",
101
- "For dashboards, verify scroll with screenshot/snapshot; if nothing moved, use scrollintoview <@ref> or target the real scroll region. For native selects use select/semanticAction/job select instead of option refs; custom combobox clicks may only focus, so re-snapshot and fall back to type, Enter/arrows, or visible option refs.",
102
- "For extraction, prefer get title/url/text/html/value/attr/count or eval --stdin with a plain expression in the tool stdin field; do not rely on console.log. When reading several known refs/selectors, use batch with JSON-array stdin (for example [[\"get\",\"text\",\"@e1\"]]) or eval --stdin instead of many serial get calls. If selector visibility warnings appear, prefer visible @refs or nextActions.",
102
+ "For dashboards, verify scroll with screenshot/snapshot; if nothing moved, use scrollintoview <@ref> or target the real scroll region. Native selects need select/semanticAction/job select; custom comboboxes may need re-snapshot, type, Enter/arrows, or visible option refs.",
103
+ "For extraction, prefer get title/url/text/html/value/attr/count or eval --stdin with a plain expression; do not rely on console.log. When reading several known refs/selectors, use batch JSON-array stdin (for example [[\"get\",\"text\",\"@e1\"]]) or eval --stdin instead of many serial gets. If selector visibility warnings appear, prefer visible @refs or nextActions.",
103
104
  "For non-core debugging, pass upstream commands through args: network, diff, trace/profiler/record, console/errors, stream, dashboard, chat, react, vitals, pushstate, dialog, frame, tab.",
104
105
  ] as const;
105
106
 
@@ -6,7 +6,7 @@
6
6
  * Invariants/Assumptions: The binary name is always `agent-browser`, the wrapper never shells out, and callers handle semantic success/error interpretation.
7
7
  */
8
8
 
9
- import { spawn } from "node:child_process";
9
+ import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
10
10
  import { chmod, mkdir } from "node:fs/promises";
11
11
  import { env as processEnv, platform as processPlatform } from "node:process";
12
12
 
@@ -22,6 +22,8 @@ const PI_AGENT_BROWSER_PROCESS_TIMEOUT_ENV = "PI_AGENT_BROWSER_PROCESS_TIMEOUT_M
22
22
  const DEFAULT_AGENT_BROWSER_SOCKET_DIR_PREFIX = "/tmp/piab";
23
23
  export const SAFE_AGENT_BROWSER_OPERATION_TIMEOUT_MS = 25_000;
24
24
  const DEFAULT_AGENT_BROWSER_PROCESS_TIMEOUT_MS = 28_000;
25
+ /** Grace period after `exit` before resolving when `close` is delayed by inherited stdio handles. */
26
+ const EXIT_STDIO_GRACE_MS = 100;
25
27
  const httpProxyEnvName = "http_proxy";
26
28
  const httpsProxyEnvName = "https_proxy";
27
29
  const allProxyEnvName = "all_proxy";
@@ -105,6 +107,94 @@ function appendTail(text: string, addition: string, maxChars: number): string {
105
107
  return combined.length <= maxChars ? combined : combined.slice(combined.length - maxChars);
106
108
  }
107
109
 
110
+ /** Exported for unit tests that lock subprocess exit-code precedence. */
111
+ export function resolveSpawnedChildExitCode(input: {
112
+ closeCode?: number | null;
113
+ exitCode?: number | null;
114
+ useExitFallback: boolean;
115
+ timedOut: boolean;
116
+ spawnError?: Error;
117
+ }): number {
118
+ // Precedence: observed `close` code when present, then wrapper timeout (124), then
119
+ // post-`exit` fallback when inherited stdio delays `close`, then spawn failure (127).
120
+ if (input.closeCode !== null && input.closeCode !== undefined) {
121
+ return input.closeCode;
122
+ }
123
+ if (input.timedOut) {
124
+ return 124;
125
+ }
126
+ if (input.useExitFallback && input.exitCode !== null && input.exitCode !== undefined) {
127
+ return input.exitCode;
128
+ }
129
+ return input.spawnError ? 127 : 0;
130
+ }
131
+
132
+ interface SpawnedChildCompletionWatcher {
133
+ clear: () => void;
134
+ }
135
+
136
+ function watchSpawnedChildCompletion(
137
+ child: ChildProcessWithoutNullStreams,
138
+ options: {
139
+ graceMs: number;
140
+ onComplete: (exitCode: number) => void;
141
+ getContext: () => { timedOut: boolean; spawnError?: Error };
142
+ },
143
+ ): SpawnedChildCompletionWatcher {
144
+ let exited = false;
145
+ let exitCode: number | null = null;
146
+ let postExitTimer: NodeJS.Timeout | undefined;
147
+ // `completed` suppresses duplicate exit/close callbacks; `settled` in `finish` guards async spill cleanup.
148
+ let completed = false;
149
+
150
+ const complete = (closeCode?: number | null) => {
151
+ if (completed) return;
152
+ completed = true;
153
+ if (postExitTimer) {
154
+ clearTimeout(postExitTimer);
155
+ postExitTimer = undefined;
156
+ }
157
+ const context = options.getContext();
158
+ options.onComplete(
159
+ resolveSpawnedChildExitCode({
160
+ closeCode,
161
+ exitCode,
162
+ useExitFallback: exited,
163
+ timedOut: context.timedOut,
164
+ spawnError: context.spawnError,
165
+ }),
166
+ );
167
+ };
168
+
169
+ child.once("exit", (code) => {
170
+ exited = true;
171
+ exitCode = code;
172
+ postExitTimer = setTimeout(() => {
173
+ destroySpawnedChildStreams(child);
174
+ complete(undefined);
175
+ }, options.graceMs);
176
+ postExitTimer.unref?.();
177
+ });
178
+ child.once("close", (code) => {
179
+ complete(code);
180
+ });
181
+
182
+ return {
183
+ clear: () => {
184
+ if (postExitTimer) {
185
+ clearTimeout(postExitTimer);
186
+ postExitTimer = undefined;
187
+ }
188
+ },
189
+ };
190
+ }
191
+
192
+ function destroySpawnedChildStreams(child: ChildProcessWithoutNullStreams): void {
193
+ child.stdin?.destroy();
194
+ child.stdout?.destroy();
195
+ child.stderr?.destroy();
196
+ }
197
+
108
198
  function parsePositiveIntegerEnv(value: string | undefined): number | undefined {
109
199
  if (value === undefined || !/^\d+$/.test(value.trim())) {
110
200
  return undefined;
@@ -207,6 +297,7 @@ export async function runAgentBrowserProcess(options: {
207
297
  let timeoutTimer: NodeJS.Timeout | undefined;
208
298
  let abortListener: (() => void) | undefined;
209
299
  let timedOut = false;
300
+ let completionWatcher: SpawnedChildCompletionWatcher | undefined;
210
301
 
211
302
  const queueStdoutChunk = (buffer: Buffer) => {
212
303
  stdoutTail = appendTail(stdoutTail, buffer.toString("utf8"), MAX_BUFFERED_STDOUT_TAIL_CHARS);
@@ -258,12 +349,15 @@ export async function runAgentBrowserProcess(options: {
258
349
  if (timeoutTimer) {
259
350
  clearTimeout(timeoutTimer);
260
351
  }
352
+ completionWatcher?.clear();
261
353
  if (stdoutSpillHandle) {
262
354
  await stdoutSpillHandle.close().catch(() => undefined);
263
355
  }
264
356
  if (!spawnError && stdoutSpillError) {
265
357
  spawnError = stdoutSpillError;
266
358
  }
359
+ // Idempotent teardown: streams may already be destroyed by the post-`exit` fallback.
360
+ destroySpawnedChildStreams(child);
267
361
  resolve({
268
362
  aborted,
269
363
  exitCode,
@@ -324,10 +418,18 @@ export async function runAgentBrowserProcess(options: {
324
418
  child.stdin.on("error", recordStdinError);
325
419
  child.once("error", (error) => {
326
420
  spawnError = error instanceof Error ? error : new Error(String(error));
327
- finish(127);
421
+ finish(
422
+ resolveSpawnedChildExitCode({
423
+ useExitFallback: false,
424
+ timedOut,
425
+ spawnError,
426
+ }),
427
+ );
328
428
  });
329
- child.once("close", (code) => {
330
- finish(code ?? (timedOut ? 124 : spawnError ? 127 : 0));
429
+ completionWatcher = watchSpawnedChildCompletion(child, {
430
+ graceMs: EXIT_STDIO_GRACE_MS,
431
+ onComplete: finish,
432
+ getContext: () => ({ timedOut, spawnError }),
331
433
  });
332
434
  child.stdout.on("data", (chunk: Buffer | string) => {
333
435
  queueStdoutChunk(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));