@kolisachint/hoocode-agent 0.4.97 → 0.4.98

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.98] - 2026-06-28
4
+
5
+ ### Changed
6
+
7
+ - Expanded the `browser_flow` and `browser_resume` tool descriptions to document the
8
+ agentic screenshot loop: build flows from `decide`/`extract_semantic` steps that
9
+ suspend with a screenshot, answer with `browser_resume`, and keep looping until the
10
+ outcome is `complete` rather than falling back to `webfetch` to read page content.
11
+ Added the per-kind `ParentResponse` shapes, the `next_action` Action schema, and
12
+ guidance to always pass a `fallbacks` selector array for click/fill.
13
+
3
14
  ## [0.4.97] - 2026-06-28
4
15
 
5
16
  ### Added
@@ -1 +1 @@
1
- {"version":3,"file":"browser-flow.d.ts","sourceRoot":"","sources":["../../../src/core/tools/browser-flow.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAEjE,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACzE,OAAO,EACN,uBAAuB,EACvB,KAAK,uBAAuB,EAC5B,KAAK,WAAW,EAEhB,KAAK,aAAa,EAElB,KAAK,WAAW,EAEhB,0BAA0B,EAC1B,MAAM,0BAA0B,CAAC;AAGlC,QAAA,MAAM,iBAAiB;;;;;;;EA8BrB,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAEhE,sEAAsE;AACtE,MAAM,WAAW,kBAAkB;IAClC,MAAM,EAAE,UAAU,GAAG,cAAc,CAAC;IACpC,qEAAqE;IACrE,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,yEAAyE;IACzE,WAAW,CAAC,EAAE,aAAa,CAAC,SAAS,CAAC,CAAC;IACvC,mEAAmE;IACnE,MAAM,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,sBAAuB,SAAQ,uBAAuB;CAAG;AAgE1E;;;;;GAKG;AACH,wBAAsB,WAAW,CAChC,MAAM,EAAE,uBAAuB,EAC/B,OAAO,EAAE,WAAW,EACpB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,UAAU,CAAC,OAAO,0BAA0B,CAAC,GACjD,OAAO,CAAC,eAAe,CAAC,kBAAkB,CAAC,CAAC,CA4C9C;AAED,wBAAgB,+BAA+B,CAC9C,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,sBAAsB,GAC9B,cAAc,CAAC,OAAO,iBAAiB,EAAE,kBAAkB,CAAC,CAgE9D;AAED,wBAAgB,qBAAqB,CACpC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,sBAAsB,GAC9B,SAAS,CAAC,OAAO,iBAAiB,CAAC,CAErC","sourcesContent":["/**\n * `browser_flow` tool: start a deterministic browsertools flow.\n *\n * Spawns a `browsertools serve` process and issues `flow_start`. If the flow\n * runs to completion deterministically it returns the evidence inline. If replay\n * hits a point only an LLM can resolve, the serve process suspends with\n * `Outcome::NeedsParent`: this tool fetches the suspension screenshot, parks the\n * live session under its `ResumeToken`, and returns the typed `ParentRequest`\n * (plus the screenshot as an image) to the agent. The agent reasons and answers\n * with the companion `browser_resume` tool. See {@link browsertools-shared}.\n */\n\nimport { exec } from \"node:child_process\";\nimport type { AgentTool } from \"@kolisachint/hoocode-agent-core\";\nimport type { ImageContent, TextContent } from \"@kolisachint/hoocode-ai\";\nimport { type Static, Type } from \"typebox\";\nimport type { AgentToolResult } from \"../extensions/types.js\";\nimport { defineTool, type ToolDefinition } from \"../extensions/types.js\";\nimport {\n\tBrowsertoolsServeClient,\n\ttype BrowsertoolsToolOptions,\n\ttype FlowOutcome,\n\ttype GetResourceResult,\n\ttype ParentRequest,\n\tparkSession,\n\ttype ResumeToken,\n\tresolveBrowsertoolsBinary,\n\tresolveBrowsertoolsOptions,\n} from \"./browsertools-shared.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\n\nconst browserFlowSchema = Type.Object({\n\tflow_path: Type.Optional(\n\t\tType.String({ description: \"Path to the .flow.json file to execute. Provide this or `flow`.\" }),\n\t),\n\tflow: Type.Optional(\n\t\tType.Record(Type.String(), Type.Unknown(), {\n\t\t\tdescription: \"Inline flow definition object (alternative to `flow_path`).\",\n\t\t}),\n\t),\n\tvars: Type.Optional(\n\t\tType.Record(Type.String(), Type.Unknown(), {\n\t\t\tdescription: \"Variables interpolated into the flow ({{var}} placeholders).\",\n\t\t}),\n\t),\n\tstore: Type.Optional(Type.String({ description: \"Path to the evidence store directory for this run.\" })),\n\tlive_view: Type.Optional(\n\t\tType.Boolean({\n\t\t\tdescription:\n\t\t\t\t\"Start a live viewer that streams the page and the agent's tool-call log over a local \" +\n\t\t\t\t\"WebSocket, and auto-open it in your default browser. Set HOOCODE_BROWSERTOOLS_NO_OPEN=1 to \" +\n\t\t\t\t\"print the URL without opening. Best for flows that suspend or run long.\",\n\t\t}),\n\t),\n\theadful: Type.Optional(\n\t\tType.Boolean({\n\t\t\tdescription:\n\t\t\t\t\"Launch a real on-screen Chromium window instead of a headless browser. Requires a desktop \" +\n\t\t\t\t\"display; unlike live_view it does not show the tool-call log.\",\n\t\t}),\n\t),\n});\n\nexport type BrowserFlowInput = Static<typeof browserFlowSchema>;\n\n/** Structured details surfaced alongside the model-facing content. */\nexport interface BrowserFlowDetails {\n\tstatus: \"complete\" | \"needs_parent\";\n\t/** Present when status is \"needs_parent\": resume with this token. */\n\ttoken?: ResumeToken;\n\t/** Present when status is \"needs_parent\": the kind of parent request. */\n\trequestKind?: ParentRequest[\"request\"];\n\t/** Present when status is \"complete\": the flow result/evidence. */\n\tresult?: unknown;\n}\n\nexport interface BrowserFlowToolOptions extends BrowsertoolsToolOptions {}\n\n/** ParentResponse shape hint per ParentRequest kind (mirrors contract.rs). */\nfunction parentResponseHint(kind: ParentRequest[\"request\"]): string {\n\tswitch (kind) {\n\t\tcase \"classify_state\":\n\t\t\treturn 'Reply with browser_resume response: { \"response\": \"state\", \"state\": \"<your label>\" }';\n\t\tcase \"verify_visual\":\n\t\t\treturn 'Reply with browser_resume response: { \"response\": \"verified\", \"passed\": true | false }';\n\t\tcase \"extract_semantic\":\n\t\t\treturn 'Reply with browser_resume response: { \"response\": \"extracted\", \"fields\": { \"<field>\": \"<value>\", ... } }';\n\t\tcase \"decide_next_action\":\n\t\t\treturn 'Reply with browser_resume response: { \"response\": \"next_action\", \"action\": <action object> }';\n\t\tcase \"reidentify_element\":\n\t\t\treturn 'Reply with browser_resume response: { \"response\": \"element\", \"selector\": \"<css selector>\" }';\n\t\tdefault:\n\t\t\treturn \"Reply with browser_resume providing the appropriate ParentResponse object.\";\n\t}\n}\n\n/** Best-effort: open a URL in the OS default browser. Never throws. Suppressed by\n * HOOCODE_BROWSERTOOLS_NO_OPEN (the URL is still surfaced to the agent). */\nfunction openInBrowser(url: string): boolean {\n\tconst suppress = process.env.HOOCODE_BROWSERTOOLS_NO_OPEN?.trim();\n\tif (suppress === \"1\" || suppress?.toLowerCase() === \"true\") return false;\n\tconst openCmd = process.platform === \"darwin\" ? \"open\" : process.platform === \"win32\" ? \"start\" : \"xdg-open\";\n\ttry {\n\t\texec(`${openCmd} \"${url}\"`);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/** Start the streamed live viewer and auto-open it. Best-effort: a failure here\n * must not abort the flow, so it degrades to returning undefined. Returns a\n * human-readable status line to prepend to the tool result, or undefined. */\nasync function startLiveView(client: BrowsertoolsServeClient): Promise<string | undefined> {\n\ttry {\n\t\tconst result = await client.request<{ url?: string; error?: string }>(\"live_view_start\", {});\n\t\tif (!result?.url) return undefined;\n\t\tconst opened = openInBrowser(result.url);\n\t\treturn opened ? `Live view opened in your browser: ${result.url}` : `Live view available at: ${result.url}`;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/** Fetch the suspension screenshot for a ParentRequest as an ImageContent block.\n * Best-effort: a fetch failure degrades to no image rather than failing the flow. */\nasync function fetchScreenshot(\n\tclient: BrowsertoolsServeClient,\n\trequest: ParentRequest,\n): Promise<ImageContent | undefined> {\n\tif (!request.screenshot_ref) return undefined;\n\ttry {\n\t\tconst resource = await client.request<GetResourceResult>(\"get_resource\", { ref: request.screenshot_ref });\n\t\tif (!resource?.png_base64) return undefined;\n\t\treturn { type: \"image\", data: resource.png_base64, mimeType: resource.mime || \"image/png\" };\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Map a `flow_start`/`flow_resume` outcome to a tool result, owning the live\n * client: dispose it on terminal outcomes, or park it under the new resume token\n * when the flow suspends again. `rounds` is the number of NeedsParent yields seen\n * so far for this flow (including the one being processed), used for the cap.\n */\nexport async function advanceFlow(\n\tclient: BrowsertoolsServeClient,\n\toutcome: FlowOutcome,\n\trounds: number,\n\topts: ReturnType<typeof resolveBrowsertoolsOptions>,\n): Promise<AgentToolResult<BrowserFlowDetails>> {\n\tif (outcome.outcome === \"complete\") {\n\t\tclient.dispose();\n\t\tconst result = outcome.result;\n\t\treturn {\n\t\t\tcontent: [{ type: \"text\", text: `Flow complete.\\n${JSON.stringify(result ?? {}, null, 2)}` }],\n\t\t\tdetails: { status: \"complete\", result },\n\t\t};\n\t}\n\n\tif (outcome.outcome === \"failed\") {\n\t\tclient.dispose();\n\t\tconst where = outcome.step_id ? ` at step \"${outcome.step_id}\"` : \"\";\n\t\tconst kind = outcome.kind ? ` (${outcome.kind})` : \"\";\n\t\tthrow new Error(`browsertools flow failed${where}: ${outcome.detail ?? \"unknown error\"}${kind}`);\n\t}\n\n\tif (outcome.outcome === \"needs_parent\") {\n\t\tif (rounds > opts.maxParentRounds) {\n\t\t\tclient.dispose();\n\t\t\tthrow new Error(\n\t\t\t\t`browsertools flow exceeded the maximum of ${opts.maxParentRounds} NeedsParent rounds; aborting to avoid a runaway loop`,\n\t\t\t);\n\t\t}\n\t\tconst { request, token } = outcome;\n\t\tconst image = await fetchScreenshot(client, request);\n\t\tparkSession(token, client, rounds, opts.idleTimeoutMs);\n\n\t\tconst text =\n\t\t\t`Flow suspended — parent decision required (NeedsParent).\\n` +\n\t\t\t`request: ${JSON.stringify(request, null, 2)}\\n` +\n\t\t\t`resume token: ${token}\\n` +\n\t\t\t`${parentResponseHint(request.request)}\\n` +\n\t\t\t(image ? \"A screenshot of the current page is attached.\" : \"(no screenshot available)\");\n\t\tconst content: (TextContent | ImageContent)[] = [{ type: \"text\", text }];\n\t\tif (image) content.push(image);\n\t\treturn {\n\t\t\tcontent,\n\t\t\tdetails: { status: \"needs_parent\", token, requestKind: request.request },\n\t\t};\n\t}\n\n\tclient.dispose();\n\tthrow new Error(`browsertools returned an unrecognized flow outcome: ${JSON.stringify(outcome)}`);\n}\n\nexport function createBrowserFlowToolDefinition(\n\tcwd: string,\n\toptions?: BrowserFlowToolOptions,\n): ToolDefinition<typeof browserFlowSchema, BrowserFlowDetails> {\n\tconst opts = resolveBrowsertoolsOptions(options);\n\treturn defineTool({\n\t\tname: \"browser_flow\",\n\t\tlabel: \"browser flow\",\n\t\tdescription:\n\t\t\t\"Start a deterministic browser flow (browsertools). Runs a saved .flow.json (or inline flow) \" +\n\t\t\t\"against a headless browser and returns the evidence on completion. If the flow needs an LLM \" +\n\t\t\t\"decision mid-replay (classify a page state, verify a visual, extract a value, decide the next \" +\n\t\t\t\"action, or re-identify a drifted element) it suspends and returns a typed request plus a \" +\n\t\t\t\"screenshot; answer it with the browser_resume tool using the returned token. Off by default; \" +\n\t\t\t\"enabled with --enable-browsertools.\",\n\t\tpromptSnippet: \"Run a deterministic browser flow, pausing for LLM decisions when needed\",\n\t\tparameters: browserFlowSchema,\n\t\tasync execute(_toolCallId, params: BrowserFlowInput, signal) {\n\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\t\t\tif (!params.flow_path && !params.flow) {\n\t\t\t\tthrow new Error(\"browser_flow requires either `flow_path` or `flow`\");\n\t\t\t}\n\n\t\t\tconst binaryPath = await resolveBrowsertoolsBinary(options);\n\t\t\tconst client = new BrowsertoolsServeClient(binaryPath, {\n\t\t\t\tcwd,\n\t\t\t\tbrowserPath: opts.browserPath,\n\t\t\t\tserveArgs: opts.serveArgs,\n\t\t\t\trequestTimeoutMs: opts.requestTimeoutMs,\n\t\t\t\theadful: params.headful ?? opts.headful,\n\t\t\t});\n\n\t\t\t// If the call is aborted before we hand the client to the registry, make\n\t\t\t// sure the serve process is torn down.\n\t\t\tconst onAbort = () => client.dispose();\n\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\ttry {\n\t\t\t\tconst startParams: Record<string, unknown> = {};\n\t\t\t\tif (params.flow_path) startParams.flow_path = params.flow_path;\n\t\t\t\tif (params.flow) startParams.flow = params.flow;\n\t\t\t\tif (params.vars) startParams.vars = params.vars;\n\t\t\t\tif (params.store) startParams.store = params.store;\n\n\t\t\t\t// Bring up the live viewer before the flow runs so the page render and\n\t\t\t\t// tool-call log are visible from the first step. The per-call param wins\n\t\t\t\t// over the instance default (--enable-browser-live-preview).\n\t\t\t\tconst liveViewEnabled = params.live_view ?? opts.liveView;\n\t\t\t\tconst liveViewStatus = liveViewEnabled ? await startLiveView(client) : undefined;\n\n\t\t\t\tconst outcome = await client.request<FlowOutcome>(\"flow_start\", startParams);\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\tclient.dispose();\n\t\t\t\t\tthrow new Error(\"Operation aborted\");\n\t\t\t\t}\n\t\t\t\tconst result = await advanceFlow(client, outcome, 1, opts);\n\t\t\t\tif (liveViewStatus) {\n\t\t\t\t\tresult.content.unshift({ type: \"text\", text: liveViewStatus });\n\t\t\t\t}\n\t\t\t\treturn result;\n\t\t\t} catch (error) {\n\t\t\t\tclient.dispose();\n\t\t\t\tthrow error;\n\t\t\t} finally {\n\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t}\n\t\t},\n\t});\n}\n\nexport function createBrowserFlowTool(\n\tcwd: string,\n\toptions?: BrowserFlowToolOptions,\n): AgentTool<typeof browserFlowSchema> {\n\treturn wrapToolDefinition(createBrowserFlowToolDefinition(cwd, options));\n}\n"]}
1
+ {"version":3,"file":"browser-flow.d.ts","sourceRoot":"","sources":["../../../src/core/tools/browser-flow.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAEjE,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACzE,OAAO,EACN,uBAAuB,EACvB,KAAK,uBAAuB,EAC5B,KAAK,WAAW,EAEhB,KAAK,aAAa,EAElB,KAAK,WAAW,EAEhB,0BAA0B,EAC1B,MAAM,0BAA0B,CAAC;AAGlC,QAAA,MAAM,iBAAiB;;;;;;;EAuCrB,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAEhE,sEAAsE;AACtE,MAAM,WAAW,kBAAkB;IAClC,MAAM,EAAE,UAAU,GAAG,cAAc,CAAC;IACpC,qEAAqE;IACrE,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,yEAAyE;IACzE,WAAW,CAAC,EAAE,aAAa,CAAC,SAAS,CAAC,CAAC;IACvC,mEAAmE;IACnE,MAAM,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,sBAAuB,SAAQ,uBAAuB;CAAG;AAgE1E;;;;;GAKG;AACH,wBAAsB,WAAW,CAChC,MAAM,EAAE,uBAAuB,EAC/B,OAAO,EAAE,WAAW,EACpB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,UAAU,CAAC,OAAO,0BAA0B,CAAC,GACjD,OAAO,CAAC,eAAe,CAAC,kBAAkB,CAAC,CAAC,CA4C9C;AAED,wBAAgB,+BAA+B,CAC9C,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,sBAAsB,GAC9B,cAAc,CAAC,OAAO,iBAAiB,EAAE,kBAAkB,CAAC,CAsF9D;AAED,wBAAgB,qBAAqB,CACpC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,sBAAsB,GAC9B,SAAS,CAAC,OAAO,iBAAiB,CAAC,CAErC","sourcesContent":["/**\n * `browser_flow` tool: start a deterministic browsertools flow.\n *\n * Spawns a `browsertools serve` process and issues `flow_start`. If the flow\n * runs to completion deterministically it returns the evidence inline. If replay\n * hits a point only an LLM can resolve, the serve process suspends with\n * `Outcome::NeedsParent`: this tool fetches the suspension screenshot, parks the\n * live session under its `ResumeToken`, and returns the typed `ParentRequest`\n * (plus the screenshot as an image) to the agent. The agent reasons and answers\n * with the companion `browser_resume` tool. See {@link browsertools-shared}.\n */\n\nimport { exec } from \"node:child_process\";\nimport type { AgentTool } from \"@kolisachint/hoocode-agent-core\";\nimport type { ImageContent, TextContent } from \"@kolisachint/hoocode-ai\";\nimport { type Static, Type } from \"typebox\";\nimport type { AgentToolResult } from \"../extensions/types.js\";\nimport { defineTool, type ToolDefinition } from \"../extensions/types.js\";\nimport {\n\tBrowsertoolsServeClient,\n\ttype BrowsertoolsToolOptions,\n\ttype FlowOutcome,\n\ttype GetResourceResult,\n\ttype ParentRequest,\n\tparkSession,\n\ttype ResumeToken,\n\tresolveBrowsertoolsBinary,\n\tresolveBrowsertoolsOptions,\n} from \"./browsertools-shared.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\n\nconst browserFlowSchema = Type.Object({\n\tflow_path: Type.Optional(\n\t\tType.String({ description: \"Path to the .flow.json file to execute. Provide this or `flow`.\" }),\n\t),\n\tflow: Type.Optional(\n\t\tType.Record(Type.String(), Type.Unknown(), {\n\t\t\tdescription:\n\t\t\t\t\"Inline flow definition object (alternative to `flow_path`). \" +\n\t\t\t\t\"Required fields: id (string), name (string), version (number), start_url (string), \" +\n\t\t\t\t\"steps (array of Step objects). Each step has: id (string), action (Action object). \" +\n\t\t\t\t\"Available actions: { action: 'navigate', url: string }, { action: 'click', selector: string }, \" +\n\t\t\t\t\"{ action: 'fill', selector: string, value_tpl: string }, { action: 'wait_settle' }, \" +\n\t\t\t\t\"{ action: 'checkpoint', asserts: [...] }, { action: 'decide', goal: string }. \" +\n\t\t\t\t\"Example: { id: 'nav', name: 'Navigate', version: 1, start_url: 'https://example.com', \" +\n\t\t\t\t\"steps: [{ id: 's1', action: { action: 'navigate', url: 'https://example.com' } }, \" +\n\t\t\t\t\"{ id: 's2', action: { action: 'wait_settle' } }] }\",\n\t\t}),\n\t),\n\tvars: Type.Optional(\n\t\tType.Record(Type.String(), Type.Unknown(), {\n\t\t\tdescription: \"Variables interpolated into the flow ({{var}} placeholders).\",\n\t\t}),\n\t),\n\tstore: Type.Optional(Type.String({ description: \"Path to the evidence store directory for this run.\" })),\n\tlive_view: Type.Optional(\n\t\tType.Boolean({\n\t\t\tdescription:\n\t\t\t\t\"Start a live viewer that streams the page and the agent's tool-call log over a local \" +\n\t\t\t\t\"WebSocket, and auto-open it in your default browser. Set HOOCODE_BROWSERTOOLS_NO_OPEN=1 to \" +\n\t\t\t\t\"print the URL without opening. Best for flows that suspend or run long.\",\n\t\t}),\n\t),\n\theadful: Type.Optional(\n\t\tType.Boolean({\n\t\t\tdescription:\n\t\t\t\t\"Launch a real on-screen Chromium window instead of a headless browser. Requires a desktop \" +\n\t\t\t\t\"display; unlike live_view it does not show the tool-call log.\",\n\t\t}),\n\t),\n});\n\nexport type BrowserFlowInput = Static<typeof browserFlowSchema>;\n\n/** Structured details surfaced alongside the model-facing content. */\nexport interface BrowserFlowDetails {\n\tstatus: \"complete\" | \"needs_parent\";\n\t/** Present when status is \"needs_parent\": resume with this token. */\n\ttoken?: ResumeToken;\n\t/** Present when status is \"needs_parent\": the kind of parent request. */\n\trequestKind?: ParentRequest[\"request\"];\n\t/** Present when status is \"complete\": the flow result/evidence. */\n\tresult?: unknown;\n}\n\nexport interface BrowserFlowToolOptions extends BrowsertoolsToolOptions {}\n\n/** ParentResponse shape hint per ParentRequest kind (mirrors contract.rs). */\nfunction parentResponseHint(kind: ParentRequest[\"request\"]): string {\n\tswitch (kind) {\n\t\tcase \"classify_state\":\n\t\t\treturn 'Reply with browser_resume response: { \"response\": \"state\", \"state\": \"<your label>\" }';\n\t\tcase \"verify_visual\":\n\t\t\treturn 'Reply with browser_resume response: { \"response\": \"verified\", \"passed\": true | false }';\n\t\tcase \"extract_semantic\":\n\t\t\treturn 'Reply with browser_resume response: { \"response\": \"extracted\", \"fields\": { \"<field>\": \"<value>\", ... } }';\n\t\tcase \"decide_next_action\":\n\t\t\treturn 'Reply with browser_resume response: { \"response\": \"next_action\", \"action\": <action object> }';\n\t\tcase \"reidentify_element\":\n\t\t\treturn 'Reply with browser_resume response: { \"response\": \"element\", \"selector\": \"<css selector>\" }';\n\t\tdefault:\n\t\t\treturn \"Reply with browser_resume providing the appropriate ParentResponse object.\";\n\t}\n}\n\n/** Best-effort: open a URL in the OS default browser. Never throws. Suppressed by\n * HOOCODE_BROWSERTOOLS_NO_OPEN (the URL is still surfaced to the agent). */\nfunction openInBrowser(url: string): boolean {\n\tconst suppress = process.env.HOOCODE_BROWSERTOOLS_NO_OPEN?.trim();\n\tif (suppress === \"1\" || suppress?.toLowerCase() === \"true\") return false;\n\tconst openCmd = process.platform === \"darwin\" ? \"open\" : process.platform === \"win32\" ? \"start\" : \"xdg-open\";\n\ttry {\n\t\texec(`${openCmd} \"${url}\"`);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/** Start the streamed live viewer and auto-open it. Best-effort: a failure here\n * must not abort the flow, so it degrades to returning undefined. Returns a\n * human-readable status line to prepend to the tool result, or undefined. */\nasync function startLiveView(client: BrowsertoolsServeClient): Promise<string | undefined> {\n\ttry {\n\t\tconst result = await client.request<{ url?: string; error?: string }>(\"live_view_start\", {});\n\t\tif (!result?.url) return undefined;\n\t\tconst opened = openInBrowser(result.url);\n\t\treturn opened ? `Live view opened in your browser: ${result.url}` : `Live view available at: ${result.url}`;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/** Fetch the suspension screenshot for a ParentRequest as an ImageContent block.\n * Best-effort: a fetch failure degrades to no image rather than failing the flow. */\nasync function fetchScreenshot(\n\tclient: BrowsertoolsServeClient,\n\trequest: ParentRequest,\n): Promise<ImageContent | undefined> {\n\tif (!request.screenshot_ref) return undefined;\n\ttry {\n\t\tconst resource = await client.request<GetResourceResult>(\"get_resource\", { ref: request.screenshot_ref });\n\t\tif (!resource?.png_base64) return undefined;\n\t\treturn { type: \"image\", data: resource.png_base64, mimeType: resource.mime || \"image/png\" };\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Map a `flow_start`/`flow_resume` outcome to a tool result, owning the live\n * client: dispose it on terminal outcomes, or park it under the new resume token\n * when the flow suspends again. `rounds` is the number of NeedsParent yields seen\n * so far for this flow (including the one being processed), used for the cap.\n */\nexport async function advanceFlow(\n\tclient: BrowsertoolsServeClient,\n\toutcome: FlowOutcome,\n\trounds: number,\n\topts: ReturnType<typeof resolveBrowsertoolsOptions>,\n): Promise<AgentToolResult<BrowserFlowDetails>> {\n\tif (outcome.outcome === \"complete\") {\n\t\tclient.dispose();\n\t\tconst result = outcome.result;\n\t\treturn {\n\t\t\tcontent: [{ type: \"text\", text: `Flow complete.\\n${JSON.stringify(result ?? {}, null, 2)}` }],\n\t\t\tdetails: { status: \"complete\", result },\n\t\t};\n\t}\n\n\tif (outcome.outcome === \"failed\") {\n\t\tclient.dispose();\n\t\tconst where = outcome.step_id ? ` at step \"${outcome.step_id}\"` : \"\";\n\t\tconst kind = outcome.kind ? ` (${outcome.kind})` : \"\";\n\t\tthrow new Error(`browsertools flow failed${where}: ${outcome.detail ?? \"unknown error\"}${kind}`);\n\t}\n\n\tif (outcome.outcome === \"needs_parent\") {\n\t\tif (rounds > opts.maxParentRounds) {\n\t\t\tclient.dispose();\n\t\t\tthrow new Error(\n\t\t\t\t`browsertools flow exceeded the maximum of ${opts.maxParentRounds} NeedsParent rounds; aborting to avoid a runaway loop`,\n\t\t\t);\n\t\t}\n\t\tconst { request, token } = outcome;\n\t\tconst image = await fetchScreenshot(client, request);\n\t\tparkSession(token, client, rounds, opts.idleTimeoutMs);\n\n\t\tconst text =\n\t\t\t`Flow suspended — parent decision required (NeedsParent).\\n` +\n\t\t\t`request: ${JSON.stringify(request, null, 2)}\\n` +\n\t\t\t`resume token: ${token}\\n` +\n\t\t\t`${parentResponseHint(request.request)}\\n` +\n\t\t\t(image ? \"A screenshot of the current page is attached.\" : \"(no screenshot available)\");\n\t\tconst content: (TextContent | ImageContent)[] = [{ type: \"text\", text }];\n\t\tif (image) content.push(image);\n\t\treturn {\n\t\t\tcontent,\n\t\t\tdetails: { status: \"needs_parent\", token, requestKind: request.request },\n\t\t};\n\t}\n\n\tclient.dispose();\n\tthrow new Error(`browsertools returned an unrecognized flow outcome: ${JSON.stringify(outcome)}`);\n}\n\nexport function createBrowserFlowToolDefinition(\n\tcwd: string,\n\toptions?: BrowserFlowToolOptions,\n): ToolDefinition<typeof browserFlowSchema, BrowserFlowDetails> {\n\tconst opts = resolveBrowsertoolsOptions(options);\n\treturn defineTool({\n\t\tname: \"browser_flow\",\n\t\tlabel: \"browser flow\",\n\t\tdescription:\n\t\t\t\"Start a deterministic browser flow (browsertools). Runs a saved .flow.json (or inline flow) \" +\n\t\t\t\"against a headless browser and returns the evidence on completion. If the flow needs an LLM \" +\n\t\t\t\"decision mid-replay (classify a page state, verify a visual, extract a value, decide the next \" +\n\t\t\t\"action, or re-identify a drifted element) it suspends and returns a typed request plus a \" +\n\t\t\t\"screenshot; answer it with the browser_resume tool using the returned token. Off by default; \" +\n\t\t\t\"enabled with --enable-browsertools.\\n\\n\" +\n\t\t\t\"AGENTIC LOOP (preferred for exploration): for any task where you must read or navigate based on \" +\n\t\t\t\"page content, build the flow from `decide`/`extract_semantic`/`classify`/`verify_visual` steps. \" +\n\t\t\t\"Each such step SUSPENDS and hands you a screenshot of the current page. Read the screenshot, then \" +\n\t\t\t\"call browser_resume with the next action, and keep looping until the outcome is `complete`. Do \" +\n\t\t\t\"NOT fall back to webfetch/curl to read page content you could read from the screenshot — that \" +\n\t\t\t\"bypasses the live session and breaks on auth-gated or JS-rendered pages. A flow ENDS as soon as \" +\n\t\t\t\"its last step runs, so chain several `decide` steps (interleaved with `wait_settle`) when you \" +\n\t\t\t\"need a multi-step journey (search -> open result -> scroll -> extract).\\n\\n\" +\n\t\t\t\"RESUME RESPONSE SHAPES (browser_resume `response` field): decide_next_action -> \" +\n\t\t\t'{ response: \"next_action\", action: <Action> }; classify_state -> { response: \"state\", state: \"<label>\" }; ' +\n\t\t\t'verify_visual -> { response: \"verified\", passed: true|false }; extract_semantic -> ' +\n\t\t\t'{ response: \"extracted\", fields: { <field>: <value> } }; reidentify_element -> ' +\n\t\t\t'{ response: \"element\", selector: \"<css>\" }.\\n\\n' +\n\t\t\t\"ACTION (for next_action) is the same shape as a flow step's action: { action: 'navigate', url }, \" +\n\t\t\t\"{ action: 'click', selector, fallbacks?: string[] }, { action: 'fill', selector, value_tpl }, \" +\n\t\t\t\"{ action: 'select', selector, value_tpl }, { action: 'wait_settle' }. Prefer stable CSS/id \" +\n\t\t\t\"selectors, and ALWAYS pass a `fallbacks` array of alternate selectors for click/fill, because \" +\n\t\t\t\"the primary selector often drifts (e.g. click '.suggestion-link' with fallbacks \" +\n\t\t\t\"['a.mw-searchSuggest-link', '#typeahead-suggestions a']).\\n\\n\" +\n\t\t\t\"VISIBILITY: pass headful:true to launch a real on-screen browser window the user can watch; \" +\n\t\t\t\"live_view:true additionally streams a mirror + tool-call log to a local URL (set live_view:false \" +\n\t\t\t\"to suppress the mirror when the instance defaults it on).\",\n\t\tpromptSnippet: \"Run a deterministic browser flow, pausing for LLM decisions when needed\",\n\t\tparameters: browserFlowSchema,\n\t\tasync execute(_toolCallId, params: BrowserFlowInput, signal) {\n\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\t\t\tif (!params.flow_path && !params.flow) {\n\t\t\t\tthrow new Error(\"browser_flow requires either `flow_path` or `flow`\");\n\t\t\t}\n\n\t\t\tconst binaryPath = await resolveBrowsertoolsBinary(options);\n\t\t\tconst client = new BrowsertoolsServeClient(binaryPath, {\n\t\t\t\tcwd,\n\t\t\t\tbrowserPath: opts.browserPath,\n\t\t\t\tserveArgs: opts.serveArgs,\n\t\t\t\trequestTimeoutMs: opts.requestTimeoutMs,\n\t\t\t\theadful: params.headful ?? opts.headful,\n\t\t\t});\n\n\t\t\t// If the call is aborted before we hand the client to the registry, make\n\t\t\t// sure the serve process is torn down.\n\t\t\tconst onAbort = () => client.dispose();\n\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\ttry {\n\t\t\t\tconst startParams: Record<string, unknown> = {};\n\t\t\t\tif (params.flow_path) startParams.flow_path = params.flow_path;\n\t\t\t\tif (params.flow) startParams.flow = params.flow;\n\t\t\t\tif (params.vars) startParams.vars = params.vars;\n\t\t\t\tif (params.store) startParams.store = params.store;\n\n\t\t\t\t// Bring up the live viewer before the flow runs so the page render and\n\t\t\t\t// tool-call log are visible from the first step. The per-call param wins\n\t\t\t\t// over the instance default (--enable-browser-live-preview).\n\t\t\t\tconst liveViewEnabled = params.live_view ?? opts.liveView;\n\t\t\t\tconst liveViewStatus = liveViewEnabled ? await startLiveView(client) : undefined;\n\n\t\t\t\tconst outcome = await client.request<FlowOutcome>(\"flow_start\", startParams);\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\tclient.dispose();\n\t\t\t\t\tthrow new Error(\"Operation aborted\");\n\t\t\t\t}\n\t\t\t\tconst result = await advanceFlow(client, outcome, 1, opts);\n\t\t\t\tif (liveViewStatus) {\n\t\t\t\t\tresult.content.unshift({ type: \"text\", text: liveViewStatus });\n\t\t\t\t}\n\t\t\t\treturn result;\n\t\t\t} catch (error) {\n\t\t\t\tclient.dispose();\n\t\t\t\tthrow error;\n\t\t\t} finally {\n\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t}\n\t\t},\n\t});\n}\n\nexport function createBrowserFlowTool(\n\tcwd: string,\n\toptions?: BrowserFlowToolOptions,\n): AgentTool<typeof browserFlowSchema> {\n\treturn wrapToolDefinition(createBrowserFlowToolDefinition(cwd, options));\n}\n"]}
@@ -17,7 +17,15 @@ import { wrapToolDefinition } from "./tool-definition-wrapper.js";
17
17
  const browserFlowSchema = Type.Object({
18
18
  flow_path: Type.Optional(Type.String({ description: "Path to the .flow.json file to execute. Provide this or `flow`." })),
19
19
  flow: Type.Optional(Type.Record(Type.String(), Type.Unknown(), {
20
- description: "Inline flow definition object (alternative to `flow_path`).",
20
+ description: "Inline flow definition object (alternative to `flow_path`). " +
21
+ "Required fields: id (string), name (string), version (number), start_url (string), " +
22
+ "steps (array of Step objects). Each step has: id (string), action (Action object). " +
23
+ "Available actions: { action: 'navigate', url: string }, { action: 'click', selector: string }, " +
24
+ "{ action: 'fill', selector: string, value_tpl: string }, { action: 'wait_settle' }, " +
25
+ "{ action: 'checkpoint', asserts: [...] }, { action: 'decide', goal: string }. " +
26
+ "Example: { id: 'nav', name: 'Navigate', version: 1, start_url: 'https://example.com', " +
27
+ "steps: [{ id: 's1', action: { action: 'navigate', url: 'https://example.com' } }, " +
28
+ "{ id: 's2', action: { action: 'wait_settle' } }] }",
21
29
  })),
22
30
  vars: Type.Optional(Type.Record(Type.String(), Type.Unknown(), {
23
31
  description: "Variables interpolated into the flow ({{var}} placeholders).",
@@ -150,7 +158,29 @@ export function createBrowserFlowToolDefinition(cwd, options) {
150
158
  "decision mid-replay (classify a page state, verify a visual, extract a value, decide the next " +
151
159
  "action, or re-identify a drifted element) it suspends and returns a typed request plus a " +
152
160
  "screenshot; answer it with the browser_resume tool using the returned token. Off by default; " +
153
- "enabled with --enable-browsertools.",
161
+ "enabled with --enable-browsertools.\n\n" +
162
+ "AGENTIC LOOP (preferred for exploration): for any task where you must read or navigate based on " +
163
+ "page content, build the flow from `decide`/`extract_semantic`/`classify`/`verify_visual` steps. " +
164
+ "Each such step SUSPENDS and hands you a screenshot of the current page. Read the screenshot, then " +
165
+ "call browser_resume with the next action, and keep looping until the outcome is `complete`. Do " +
166
+ "NOT fall back to webfetch/curl to read page content you could read from the screenshot — that " +
167
+ "bypasses the live session and breaks on auth-gated or JS-rendered pages. A flow ENDS as soon as " +
168
+ "its last step runs, so chain several `decide` steps (interleaved with `wait_settle`) when you " +
169
+ "need a multi-step journey (search -> open result -> scroll -> extract).\n\n" +
170
+ "RESUME RESPONSE SHAPES (browser_resume `response` field): decide_next_action -> " +
171
+ '{ response: "next_action", action: <Action> }; classify_state -> { response: "state", state: "<label>" }; ' +
172
+ 'verify_visual -> { response: "verified", passed: true|false }; extract_semantic -> ' +
173
+ '{ response: "extracted", fields: { <field>: <value> } }; reidentify_element -> ' +
174
+ '{ response: "element", selector: "<css>" }.\n\n' +
175
+ "ACTION (for next_action) is the same shape as a flow step's action: { action: 'navigate', url }, " +
176
+ "{ action: 'click', selector, fallbacks?: string[] }, { action: 'fill', selector, value_tpl }, " +
177
+ "{ action: 'select', selector, value_tpl }, { action: 'wait_settle' }. Prefer stable CSS/id " +
178
+ "selectors, and ALWAYS pass a `fallbacks` array of alternate selectors for click/fill, because " +
179
+ "the primary selector often drifts (e.g. click '.suggestion-link' with fallbacks " +
180
+ "['a.mw-searchSuggest-link', '#typeahead-suggestions a']).\n\n" +
181
+ "VISIBILITY: pass headful:true to launch a real on-screen browser window the user can watch; " +
182
+ "live_view:true additionally streams a mirror + tool-call log to a local URL (set live_view:false " +
183
+ "to suppress the mirror when the instance defaults it on).",
154
184
  promptSnippet: "Run a deterministic browser flow, pausing for LLM decisions when needed",
155
185
  parameters: browserFlowSchema,
156
186
  async execute(_toolCallId, params, signal) {
@@ -1 +1 @@
1
- {"version":3,"file":"browser-flow.js","sourceRoot":"","sources":["../../../src/core/tools/browser-flow.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `browser_flow` tool: start a deterministic browsertools flow.\n *\n * Spawns a `browsertools serve` process and issues `flow_start`. If the flow\n * runs to completion deterministically it returns the evidence inline. If replay\n * hits a point only an LLM can resolve, the serve process suspends with\n * `Outcome::NeedsParent`: this tool fetches the suspension screenshot, parks the\n * live session under its `ResumeToken`, and returns the typed `ParentRequest`\n * (plus the screenshot as an image) to the agent. The agent reasons and answers\n * with the companion `browser_resume` tool. See {@link browsertools-shared}.\n */\n\nimport { exec } from \"node:child_process\";\nimport type { AgentTool } from \"@kolisachint/hoocode-agent-core\";\nimport type { ImageContent, TextContent } from \"@kolisachint/hoocode-ai\";\nimport { type Static, Type } from \"typebox\";\nimport type { AgentToolResult } from \"../extensions/types.js\";\nimport { defineTool, type ToolDefinition } from \"../extensions/types.js\";\nimport {\n\tBrowsertoolsServeClient,\n\ttype BrowsertoolsToolOptions,\n\ttype FlowOutcome,\n\ttype GetResourceResult,\n\ttype ParentRequest,\n\tparkSession,\n\ttype ResumeToken,\n\tresolveBrowsertoolsBinary,\n\tresolveBrowsertoolsOptions,\n} from \"./browsertools-shared.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\n\nconst browserFlowSchema = Type.Object({\n\tflow_path: Type.Optional(\n\t\tType.String({ description: \"Path to the .flow.json file to execute. Provide this or `flow`.\" }),\n\t),\n\tflow: Type.Optional(\n\t\tType.Record(Type.String(), Type.Unknown(), {\n\t\t\tdescription: \"Inline flow definition object (alternative to `flow_path`).\",\n\t\t}),\n\t),\n\tvars: Type.Optional(\n\t\tType.Record(Type.String(), Type.Unknown(), {\n\t\t\tdescription: \"Variables interpolated into the flow ({{var}} placeholders).\",\n\t\t}),\n\t),\n\tstore: Type.Optional(Type.String({ description: \"Path to the evidence store directory for this run.\" })),\n\tlive_view: Type.Optional(\n\t\tType.Boolean({\n\t\t\tdescription:\n\t\t\t\t\"Start a live viewer that streams the page and the agent's tool-call log over a local \" +\n\t\t\t\t\"WebSocket, and auto-open it in your default browser. Set HOOCODE_BROWSERTOOLS_NO_OPEN=1 to \" +\n\t\t\t\t\"print the URL without opening. Best for flows that suspend or run long.\",\n\t\t}),\n\t),\n\theadful: Type.Optional(\n\t\tType.Boolean({\n\t\t\tdescription:\n\t\t\t\t\"Launch a real on-screen Chromium window instead of a headless browser. Requires a desktop \" +\n\t\t\t\t\"display; unlike live_view it does not show the tool-call log.\",\n\t\t}),\n\t),\n});\n\nexport type BrowserFlowInput = Static<typeof browserFlowSchema>;\n\n/** Structured details surfaced alongside the model-facing content. */\nexport interface BrowserFlowDetails {\n\tstatus: \"complete\" | \"needs_parent\";\n\t/** Present when status is \"needs_parent\": resume with this token. */\n\ttoken?: ResumeToken;\n\t/** Present when status is \"needs_parent\": the kind of parent request. */\n\trequestKind?: ParentRequest[\"request\"];\n\t/** Present when status is \"complete\": the flow result/evidence. */\n\tresult?: unknown;\n}\n\nexport interface BrowserFlowToolOptions extends BrowsertoolsToolOptions {}\n\n/** ParentResponse shape hint per ParentRequest kind (mirrors contract.rs). */\nfunction parentResponseHint(kind: ParentRequest[\"request\"]): string {\n\tswitch (kind) {\n\t\tcase \"classify_state\":\n\t\t\treturn 'Reply with browser_resume response: { \"response\": \"state\", \"state\": \"<your label>\" }';\n\t\tcase \"verify_visual\":\n\t\t\treturn 'Reply with browser_resume response: { \"response\": \"verified\", \"passed\": true | false }';\n\t\tcase \"extract_semantic\":\n\t\t\treturn 'Reply with browser_resume response: { \"response\": \"extracted\", \"fields\": { \"<field>\": \"<value>\", ... } }';\n\t\tcase \"decide_next_action\":\n\t\t\treturn 'Reply with browser_resume response: { \"response\": \"next_action\", \"action\": <action object> }';\n\t\tcase \"reidentify_element\":\n\t\t\treturn 'Reply with browser_resume response: { \"response\": \"element\", \"selector\": \"<css selector>\" }';\n\t\tdefault:\n\t\t\treturn \"Reply with browser_resume providing the appropriate ParentResponse object.\";\n\t}\n}\n\n/** Best-effort: open a URL in the OS default browser. Never throws. Suppressed by\n * HOOCODE_BROWSERTOOLS_NO_OPEN (the URL is still surfaced to the agent). */\nfunction openInBrowser(url: string): boolean {\n\tconst suppress = process.env.HOOCODE_BROWSERTOOLS_NO_OPEN?.trim();\n\tif (suppress === \"1\" || suppress?.toLowerCase() === \"true\") return false;\n\tconst openCmd = process.platform === \"darwin\" ? \"open\" : process.platform === \"win32\" ? \"start\" : \"xdg-open\";\n\ttry {\n\t\texec(`${openCmd} \"${url}\"`);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/** Start the streamed live viewer and auto-open it. Best-effort: a failure here\n * must not abort the flow, so it degrades to returning undefined. Returns a\n * human-readable status line to prepend to the tool result, or undefined. */\nasync function startLiveView(client: BrowsertoolsServeClient): Promise<string | undefined> {\n\ttry {\n\t\tconst result = await client.request<{ url?: string; error?: string }>(\"live_view_start\", {});\n\t\tif (!result?.url) return undefined;\n\t\tconst opened = openInBrowser(result.url);\n\t\treturn opened ? `Live view opened in your browser: ${result.url}` : `Live view available at: ${result.url}`;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/** Fetch the suspension screenshot for a ParentRequest as an ImageContent block.\n * Best-effort: a fetch failure degrades to no image rather than failing the flow. */\nasync function fetchScreenshot(\n\tclient: BrowsertoolsServeClient,\n\trequest: ParentRequest,\n): Promise<ImageContent | undefined> {\n\tif (!request.screenshot_ref) return undefined;\n\ttry {\n\t\tconst resource = await client.request<GetResourceResult>(\"get_resource\", { ref: request.screenshot_ref });\n\t\tif (!resource?.png_base64) return undefined;\n\t\treturn { type: \"image\", data: resource.png_base64, mimeType: resource.mime || \"image/png\" };\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Map a `flow_start`/`flow_resume` outcome to a tool result, owning the live\n * client: dispose it on terminal outcomes, or park it under the new resume token\n * when the flow suspends again. `rounds` is the number of NeedsParent yields seen\n * so far for this flow (including the one being processed), used for the cap.\n */\nexport async function advanceFlow(\n\tclient: BrowsertoolsServeClient,\n\toutcome: FlowOutcome,\n\trounds: number,\n\topts: ReturnType<typeof resolveBrowsertoolsOptions>,\n): Promise<AgentToolResult<BrowserFlowDetails>> {\n\tif (outcome.outcome === \"complete\") {\n\t\tclient.dispose();\n\t\tconst result = outcome.result;\n\t\treturn {\n\t\t\tcontent: [{ type: \"text\", text: `Flow complete.\\n${JSON.stringify(result ?? {}, null, 2)}` }],\n\t\t\tdetails: { status: \"complete\", result },\n\t\t};\n\t}\n\n\tif (outcome.outcome === \"failed\") {\n\t\tclient.dispose();\n\t\tconst where = outcome.step_id ? ` at step \"${outcome.step_id}\"` : \"\";\n\t\tconst kind = outcome.kind ? ` (${outcome.kind})` : \"\";\n\t\tthrow new Error(`browsertools flow failed${where}: ${outcome.detail ?? \"unknown error\"}${kind}`);\n\t}\n\n\tif (outcome.outcome === \"needs_parent\") {\n\t\tif (rounds > opts.maxParentRounds) {\n\t\t\tclient.dispose();\n\t\t\tthrow new Error(\n\t\t\t\t`browsertools flow exceeded the maximum of ${opts.maxParentRounds} NeedsParent rounds; aborting to avoid a runaway loop`,\n\t\t\t);\n\t\t}\n\t\tconst { request, token } = outcome;\n\t\tconst image = await fetchScreenshot(client, request);\n\t\tparkSession(token, client, rounds, opts.idleTimeoutMs);\n\n\t\tconst text =\n\t\t\t`Flow suspended — parent decision required (NeedsParent).\\n` +\n\t\t\t`request: ${JSON.stringify(request, null, 2)}\\n` +\n\t\t\t`resume token: ${token}\\n` +\n\t\t\t`${parentResponseHint(request.request)}\\n` +\n\t\t\t(image ? \"A screenshot of the current page is attached.\" : \"(no screenshot available)\");\n\t\tconst content: (TextContent | ImageContent)[] = [{ type: \"text\", text }];\n\t\tif (image) content.push(image);\n\t\treturn {\n\t\t\tcontent,\n\t\t\tdetails: { status: \"needs_parent\", token, requestKind: request.request },\n\t\t};\n\t}\n\n\tclient.dispose();\n\tthrow new Error(`browsertools returned an unrecognized flow outcome: ${JSON.stringify(outcome)}`);\n}\n\nexport function createBrowserFlowToolDefinition(\n\tcwd: string,\n\toptions?: BrowserFlowToolOptions,\n): ToolDefinition<typeof browserFlowSchema, BrowserFlowDetails> {\n\tconst opts = resolveBrowsertoolsOptions(options);\n\treturn defineTool({\n\t\tname: \"browser_flow\",\n\t\tlabel: \"browser flow\",\n\t\tdescription:\n\t\t\t\"Start a deterministic browser flow (browsertools). Runs a saved .flow.json (or inline flow) \" +\n\t\t\t\"against a headless browser and returns the evidence on completion. If the flow needs an LLM \" +\n\t\t\t\"decision mid-replay (classify a page state, verify a visual, extract a value, decide the next \" +\n\t\t\t\"action, or re-identify a drifted element) it suspends and returns a typed request plus a \" +\n\t\t\t\"screenshot; answer it with the browser_resume tool using the returned token. Off by default; \" +\n\t\t\t\"enabled with --enable-browsertools.\",\n\t\tpromptSnippet: \"Run a deterministic browser flow, pausing for LLM decisions when needed\",\n\t\tparameters: browserFlowSchema,\n\t\tasync execute(_toolCallId, params: BrowserFlowInput, signal) {\n\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\t\t\tif (!params.flow_path && !params.flow) {\n\t\t\t\tthrow new Error(\"browser_flow requires either `flow_path` or `flow`\");\n\t\t\t}\n\n\t\t\tconst binaryPath = await resolveBrowsertoolsBinary(options);\n\t\t\tconst client = new BrowsertoolsServeClient(binaryPath, {\n\t\t\t\tcwd,\n\t\t\t\tbrowserPath: opts.browserPath,\n\t\t\t\tserveArgs: opts.serveArgs,\n\t\t\t\trequestTimeoutMs: opts.requestTimeoutMs,\n\t\t\t\theadful: params.headful ?? opts.headful,\n\t\t\t});\n\n\t\t\t// If the call is aborted before we hand the client to the registry, make\n\t\t\t// sure the serve process is torn down.\n\t\t\tconst onAbort = () => client.dispose();\n\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\ttry {\n\t\t\t\tconst startParams: Record<string, unknown> = {};\n\t\t\t\tif (params.flow_path) startParams.flow_path = params.flow_path;\n\t\t\t\tif (params.flow) startParams.flow = params.flow;\n\t\t\t\tif (params.vars) startParams.vars = params.vars;\n\t\t\t\tif (params.store) startParams.store = params.store;\n\n\t\t\t\t// Bring up the live viewer before the flow runs so the page render and\n\t\t\t\t// tool-call log are visible from the first step. The per-call param wins\n\t\t\t\t// over the instance default (--enable-browser-live-preview).\n\t\t\t\tconst liveViewEnabled = params.live_view ?? opts.liveView;\n\t\t\t\tconst liveViewStatus = liveViewEnabled ? await startLiveView(client) : undefined;\n\n\t\t\t\tconst outcome = await client.request<FlowOutcome>(\"flow_start\", startParams);\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\tclient.dispose();\n\t\t\t\t\tthrow new Error(\"Operation aborted\");\n\t\t\t\t}\n\t\t\t\tconst result = await advanceFlow(client, outcome, 1, opts);\n\t\t\t\tif (liveViewStatus) {\n\t\t\t\t\tresult.content.unshift({ type: \"text\", text: liveViewStatus });\n\t\t\t\t}\n\t\t\t\treturn result;\n\t\t\t} catch (error) {\n\t\t\t\tclient.dispose();\n\t\t\t\tthrow error;\n\t\t\t} finally {\n\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t}\n\t\t},\n\t});\n}\n\nexport function createBrowserFlowTool(\n\tcwd: string,\n\toptions?: BrowserFlowToolOptions,\n): AgentTool<typeof browserFlowSchema> {\n\treturn wrapToolDefinition(createBrowserFlowToolDefinition(cwd, options));\n}\n"]}
1
+ {"version":3,"file":"browser-flow.js","sourceRoot":"","sources":["../../../src/core/tools/browser-flow.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `browser_flow` tool: start a deterministic browsertools flow.\n *\n * Spawns a `browsertools serve` process and issues `flow_start`. If the flow\n * runs to completion deterministically it returns the evidence inline. If replay\n * hits a point only an LLM can resolve, the serve process suspends with\n * `Outcome::NeedsParent`: this tool fetches the suspension screenshot, parks the\n * live session under its `ResumeToken`, and returns the typed `ParentRequest`\n * (plus the screenshot as an image) to the agent. The agent reasons and answers\n * with the companion `browser_resume` tool. See {@link browsertools-shared}.\n */\n\nimport { exec } from \"node:child_process\";\nimport type { AgentTool } from \"@kolisachint/hoocode-agent-core\";\nimport type { ImageContent, TextContent } from \"@kolisachint/hoocode-ai\";\nimport { type Static, Type } from \"typebox\";\nimport type { AgentToolResult } from \"../extensions/types.js\";\nimport { defineTool, type ToolDefinition } from \"../extensions/types.js\";\nimport {\n\tBrowsertoolsServeClient,\n\ttype BrowsertoolsToolOptions,\n\ttype FlowOutcome,\n\ttype GetResourceResult,\n\ttype ParentRequest,\n\tparkSession,\n\ttype ResumeToken,\n\tresolveBrowsertoolsBinary,\n\tresolveBrowsertoolsOptions,\n} from \"./browsertools-shared.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\n\nconst browserFlowSchema = Type.Object({\n\tflow_path: Type.Optional(\n\t\tType.String({ description: \"Path to the .flow.json file to execute. Provide this or `flow`.\" }),\n\t),\n\tflow: Type.Optional(\n\t\tType.Record(Type.String(), Type.Unknown(), {\n\t\t\tdescription:\n\t\t\t\t\"Inline flow definition object (alternative to `flow_path`). \" +\n\t\t\t\t\"Required fields: id (string), name (string), version (number), start_url (string), \" +\n\t\t\t\t\"steps (array of Step objects). Each step has: id (string), action (Action object). \" +\n\t\t\t\t\"Available actions: { action: 'navigate', url: string }, { action: 'click', selector: string }, \" +\n\t\t\t\t\"{ action: 'fill', selector: string, value_tpl: string }, { action: 'wait_settle' }, \" +\n\t\t\t\t\"{ action: 'checkpoint', asserts: [...] }, { action: 'decide', goal: string }. \" +\n\t\t\t\t\"Example: { id: 'nav', name: 'Navigate', version: 1, start_url: 'https://example.com', \" +\n\t\t\t\t\"steps: [{ id: 's1', action: { action: 'navigate', url: 'https://example.com' } }, \" +\n\t\t\t\t\"{ id: 's2', action: { action: 'wait_settle' } }] }\",\n\t\t}),\n\t),\n\tvars: Type.Optional(\n\t\tType.Record(Type.String(), Type.Unknown(), {\n\t\t\tdescription: \"Variables interpolated into the flow ({{var}} placeholders).\",\n\t\t}),\n\t),\n\tstore: Type.Optional(Type.String({ description: \"Path to the evidence store directory for this run.\" })),\n\tlive_view: Type.Optional(\n\t\tType.Boolean({\n\t\t\tdescription:\n\t\t\t\t\"Start a live viewer that streams the page and the agent's tool-call log over a local \" +\n\t\t\t\t\"WebSocket, and auto-open it in your default browser. Set HOOCODE_BROWSERTOOLS_NO_OPEN=1 to \" +\n\t\t\t\t\"print the URL without opening. Best for flows that suspend or run long.\",\n\t\t}),\n\t),\n\theadful: Type.Optional(\n\t\tType.Boolean({\n\t\t\tdescription:\n\t\t\t\t\"Launch a real on-screen Chromium window instead of a headless browser. Requires a desktop \" +\n\t\t\t\t\"display; unlike live_view it does not show the tool-call log.\",\n\t\t}),\n\t),\n});\n\nexport type BrowserFlowInput = Static<typeof browserFlowSchema>;\n\n/** Structured details surfaced alongside the model-facing content. */\nexport interface BrowserFlowDetails {\n\tstatus: \"complete\" | \"needs_parent\";\n\t/** Present when status is \"needs_parent\": resume with this token. */\n\ttoken?: ResumeToken;\n\t/** Present when status is \"needs_parent\": the kind of parent request. */\n\trequestKind?: ParentRequest[\"request\"];\n\t/** Present when status is \"complete\": the flow result/evidence. */\n\tresult?: unknown;\n}\n\nexport interface BrowserFlowToolOptions extends BrowsertoolsToolOptions {}\n\n/** ParentResponse shape hint per ParentRequest kind (mirrors contract.rs). */\nfunction parentResponseHint(kind: ParentRequest[\"request\"]): string {\n\tswitch (kind) {\n\t\tcase \"classify_state\":\n\t\t\treturn 'Reply with browser_resume response: { \"response\": \"state\", \"state\": \"<your label>\" }';\n\t\tcase \"verify_visual\":\n\t\t\treturn 'Reply with browser_resume response: { \"response\": \"verified\", \"passed\": true | false }';\n\t\tcase \"extract_semantic\":\n\t\t\treturn 'Reply with browser_resume response: { \"response\": \"extracted\", \"fields\": { \"<field>\": \"<value>\", ... } }';\n\t\tcase \"decide_next_action\":\n\t\t\treturn 'Reply with browser_resume response: { \"response\": \"next_action\", \"action\": <action object> }';\n\t\tcase \"reidentify_element\":\n\t\t\treturn 'Reply with browser_resume response: { \"response\": \"element\", \"selector\": \"<css selector>\" }';\n\t\tdefault:\n\t\t\treturn \"Reply with browser_resume providing the appropriate ParentResponse object.\";\n\t}\n}\n\n/** Best-effort: open a URL in the OS default browser. Never throws. Suppressed by\n * HOOCODE_BROWSERTOOLS_NO_OPEN (the URL is still surfaced to the agent). */\nfunction openInBrowser(url: string): boolean {\n\tconst suppress = process.env.HOOCODE_BROWSERTOOLS_NO_OPEN?.trim();\n\tif (suppress === \"1\" || suppress?.toLowerCase() === \"true\") return false;\n\tconst openCmd = process.platform === \"darwin\" ? \"open\" : process.platform === \"win32\" ? \"start\" : \"xdg-open\";\n\ttry {\n\t\texec(`${openCmd} \"${url}\"`);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/** Start the streamed live viewer and auto-open it. Best-effort: a failure here\n * must not abort the flow, so it degrades to returning undefined. Returns a\n * human-readable status line to prepend to the tool result, or undefined. */\nasync function startLiveView(client: BrowsertoolsServeClient): Promise<string | undefined> {\n\ttry {\n\t\tconst result = await client.request<{ url?: string; error?: string }>(\"live_view_start\", {});\n\t\tif (!result?.url) return undefined;\n\t\tconst opened = openInBrowser(result.url);\n\t\treturn opened ? `Live view opened in your browser: ${result.url}` : `Live view available at: ${result.url}`;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/** Fetch the suspension screenshot for a ParentRequest as an ImageContent block.\n * Best-effort: a fetch failure degrades to no image rather than failing the flow. */\nasync function fetchScreenshot(\n\tclient: BrowsertoolsServeClient,\n\trequest: ParentRequest,\n): Promise<ImageContent | undefined> {\n\tif (!request.screenshot_ref) return undefined;\n\ttry {\n\t\tconst resource = await client.request<GetResourceResult>(\"get_resource\", { ref: request.screenshot_ref });\n\t\tif (!resource?.png_base64) return undefined;\n\t\treturn { type: \"image\", data: resource.png_base64, mimeType: resource.mime || \"image/png\" };\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Map a `flow_start`/`flow_resume` outcome to a tool result, owning the live\n * client: dispose it on terminal outcomes, or park it under the new resume token\n * when the flow suspends again. `rounds` is the number of NeedsParent yields seen\n * so far for this flow (including the one being processed), used for the cap.\n */\nexport async function advanceFlow(\n\tclient: BrowsertoolsServeClient,\n\toutcome: FlowOutcome,\n\trounds: number,\n\topts: ReturnType<typeof resolveBrowsertoolsOptions>,\n): Promise<AgentToolResult<BrowserFlowDetails>> {\n\tif (outcome.outcome === \"complete\") {\n\t\tclient.dispose();\n\t\tconst result = outcome.result;\n\t\treturn {\n\t\t\tcontent: [{ type: \"text\", text: `Flow complete.\\n${JSON.stringify(result ?? {}, null, 2)}` }],\n\t\t\tdetails: { status: \"complete\", result },\n\t\t};\n\t}\n\n\tif (outcome.outcome === \"failed\") {\n\t\tclient.dispose();\n\t\tconst where = outcome.step_id ? ` at step \"${outcome.step_id}\"` : \"\";\n\t\tconst kind = outcome.kind ? ` (${outcome.kind})` : \"\";\n\t\tthrow new Error(`browsertools flow failed${where}: ${outcome.detail ?? \"unknown error\"}${kind}`);\n\t}\n\n\tif (outcome.outcome === \"needs_parent\") {\n\t\tif (rounds > opts.maxParentRounds) {\n\t\t\tclient.dispose();\n\t\t\tthrow new Error(\n\t\t\t\t`browsertools flow exceeded the maximum of ${opts.maxParentRounds} NeedsParent rounds; aborting to avoid a runaway loop`,\n\t\t\t);\n\t\t}\n\t\tconst { request, token } = outcome;\n\t\tconst image = await fetchScreenshot(client, request);\n\t\tparkSession(token, client, rounds, opts.idleTimeoutMs);\n\n\t\tconst text =\n\t\t\t`Flow suspended — parent decision required (NeedsParent).\\n` +\n\t\t\t`request: ${JSON.stringify(request, null, 2)}\\n` +\n\t\t\t`resume token: ${token}\\n` +\n\t\t\t`${parentResponseHint(request.request)}\\n` +\n\t\t\t(image ? \"A screenshot of the current page is attached.\" : \"(no screenshot available)\");\n\t\tconst content: (TextContent | ImageContent)[] = [{ type: \"text\", text }];\n\t\tif (image) content.push(image);\n\t\treturn {\n\t\t\tcontent,\n\t\t\tdetails: { status: \"needs_parent\", token, requestKind: request.request },\n\t\t};\n\t}\n\n\tclient.dispose();\n\tthrow new Error(`browsertools returned an unrecognized flow outcome: ${JSON.stringify(outcome)}`);\n}\n\nexport function createBrowserFlowToolDefinition(\n\tcwd: string,\n\toptions?: BrowserFlowToolOptions,\n): ToolDefinition<typeof browserFlowSchema, BrowserFlowDetails> {\n\tconst opts = resolveBrowsertoolsOptions(options);\n\treturn defineTool({\n\t\tname: \"browser_flow\",\n\t\tlabel: \"browser flow\",\n\t\tdescription:\n\t\t\t\"Start a deterministic browser flow (browsertools). Runs a saved .flow.json (or inline flow) \" +\n\t\t\t\"against a headless browser and returns the evidence on completion. If the flow needs an LLM \" +\n\t\t\t\"decision mid-replay (classify a page state, verify a visual, extract a value, decide the next \" +\n\t\t\t\"action, or re-identify a drifted element) it suspends and returns a typed request plus a \" +\n\t\t\t\"screenshot; answer it with the browser_resume tool using the returned token. Off by default; \" +\n\t\t\t\"enabled with --enable-browsertools.\\n\\n\" +\n\t\t\t\"AGENTIC LOOP (preferred for exploration): for any task where you must read or navigate based on \" +\n\t\t\t\"page content, build the flow from `decide`/`extract_semantic`/`classify`/`verify_visual` steps. \" +\n\t\t\t\"Each such step SUSPENDS and hands you a screenshot of the current page. Read the screenshot, then \" +\n\t\t\t\"call browser_resume with the next action, and keep looping until the outcome is `complete`. Do \" +\n\t\t\t\"NOT fall back to webfetch/curl to read page content you could read from the screenshot — that \" +\n\t\t\t\"bypasses the live session and breaks on auth-gated or JS-rendered pages. A flow ENDS as soon as \" +\n\t\t\t\"its last step runs, so chain several `decide` steps (interleaved with `wait_settle`) when you \" +\n\t\t\t\"need a multi-step journey (search -> open result -> scroll -> extract).\\n\\n\" +\n\t\t\t\"RESUME RESPONSE SHAPES (browser_resume `response` field): decide_next_action -> \" +\n\t\t\t'{ response: \"next_action\", action: <Action> }; classify_state -> { response: \"state\", state: \"<label>\" }; ' +\n\t\t\t'verify_visual -> { response: \"verified\", passed: true|false }; extract_semantic -> ' +\n\t\t\t'{ response: \"extracted\", fields: { <field>: <value> } }; reidentify_element -> ' +\n\t\t\t'{ response: \"element\", selector: \"<css>\" }.\\n\\n' +\n\t\t\t\"ACTION (for next_action) is the same shape as a flow step's action: { action: 'navigate', url }, \" +\n\t\t\t\"{ action: 'click', selector, fallbacks?: string[] }, { action: 'fill', selector, value_tpl }, \" +\n\t\t\t\"{ action: 'select', selector, value_tpl }, { action: 'wait_settle' }. Prefer stable CSS/id \" +\n\t\t\t\"selectors, and ALWAYS pass a `fallbacks` array of alternate selectors for click/fill, because \" +\n\t\t\t\"the primary selector often drifts (e.g. click '.suggestion-link' with fallbacks \" +\n\t\t\t\"['a.mw-searchSuggest-link', '#typeahead-suggestions a']).\\n\\n\" +\n\t\t\t\"VISIBILITY: pass headful:true to launch a real on-screen browser window the user can watch; \" +\n\t\t\t\"live_view:true additionally streams a mirror + tool-call log to a local URL (set live_view:false \" +\n\t\t\t\"to suppress the mirror when the instance defaults it on).\",\n\t\tpromptSnippet: \"Run a deterministic browser flow, pausing for LLM decisions when needed\",\n\t\tparameters: browserFlowSchema,\n\t\tasync execute(_toolCallId, params: BrowserFlowInput, signal) {\n\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\t\t\tif (!params.flow_path && !params.flow) {\n\t\t\t\tthrow new Error(\"browser_flow requires either `flow_path` or `flow`\");\n\t\t\t}\n\n\t\t\tconst binaryPath = await resolveBrowsertoolsBinary(options);\n\t\t\tconst client = new BrowsertoolsServeClient(binaryPath, {\n\t\t\t\tcwd,\n\t\t\t\tbrowserPath: opts.browserPath,\n\t\t\t\tserveArgs: opts.serveArgs,\n\t\t\t\trequestTimeoutMs: opts.requestTimeoutMs,\n\t\t\t\theadful: params.headful ?? opts.headful,\n\t\t\t});\n\n\t\t\t// If the call is aborted before we hand the client to the registry, make\n\t\t\t// sure the serve process is torn down.\n\t\t\tconst onAbort = () => client.dispose();\n\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\ttry {\n\t\t\t\tconst startParams: Record<string, unknown> = {};\n\t\t\t\tif (params.flow_path) startParams.flow_path = params.flow_path;\n\t\t\t\tif (params.flow) startParams.flow = params.flow;\n\t\t\t\tif (params.vars) startParams.vars = params.vars;\n\t\t\t\tif (params.store) startParams.store = params.store;\n\n\t\t\t\t// Bring up the live viewer before the flow runs so the page render and\n\t\t\t\t// tool-call log are visible from the first step. The per-call param wins\n\t\t\t\t// over the instance default (--enable-browser-live-preview).\n\t\t\t\tconst liveViewEnabled = params.live_view ?? opts.liveView;\n\t\t\t\tconst liveViewStatus = liveViewEnabled ? await startLiveView(client) : undefined;\n\n\t\t\t\tconst outcome = await client.request<FlowOutcome>(\"flow_start\", startParams);\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\tclient.dispose();\n\t\t\t\t\tthrow new Error(\"Operation aborted\");\n\t\t\t\t}\n\t\t\t\tconst result = await advanceFlow(client, outcome, 1, opts);\n\t\t\t\tif (liveViewStatus) {\n\t\t\t\t\tresult.content.unshift({ type: \"text\", text: liveViewStatus });\n\t\t\t\t}\n\t\t\t\treturn result;\n\t\t\t} catch (error) {\n\t\t\t\tclient.dispose();\n\t\t\t\tthrow error;\n\t\t\t} finally {\n\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t}\n\t\t},\n\t});\n}\n\nexport function createBrowserFlowTool(\n\tcwd: string,\n\toptions?: BrowserFlowToolOptions,\n): AgentTool<typeof browserFlowSchema> {\n\treturn wrapToolDefinition(createBrowserFlowToolDefinition(cwd, options));\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"browser-resume.d.ts","sourceRoot":"","sources":["../../../src/core/tools/browser-resume.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AACjE,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACzE,OAAO,EAAe,KAAK,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACzE,OAAO,EACN,KAAK,uBAAuB,EAI5B,MAAM,0BAA0B,CAAC;AAGlC,QAAA,MAAM,mBAAmB;;;EAOvB,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEpE,MAAM,WAAW,wBAAyB,SAAQ,uBAAuB;CAAG;AAE5E,wBAAgB,iCAAiC,CAGhD,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,wBAAwB,GAChC,cAAc,CAAC,OAAO,mBAAmB,EAAE,kBAAkB,CAAC,CA4ChE;AAED,wBAAgB,uBAAuB,CACtC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,wBAAwB,GAChC,SAAS,CAAC,OAAO,mBAAmB,CAAC,CAEvC","sourcesContent":["/**\n * `browser_resume` tool: answer a `NeedsParent` suspension from `browser_flow`.\n *\n * Looks up the paused serve session by its `ResumeToken`, issues `flow_resume`\n * with the parent's decision (a `ParentResponse` object), and maps the next\n * outcome the same way `browser_flow` does — completing, failing, or suspending\n * again with a fresh token. The live serve process (and its browser state) is\n * reused across rounds. See {@link browsertools-shared}.\n */\n\nimport type { AgentTool } from \"@kolisachint/hoocode-agent-core\";\nimport { type Static, Type } from \"typebox\";\nimport { defineTool, type ToolDefinition } from \"../extensions/types.js\";\nimport { advanceFlow, type BrowserFlowDetails } from \"./browser-flow.js\";\nimport {\n\ttype BrowsertoolsToolOptions,\n\ttype FlowOutcome,\n\tresolveBrowsertoolsOptions,\n\ttakeSession,\n} from \"./browsertools-shared.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\n\nconst browserResumeSchema = Type.Object({\n\ttoken: Type.String({ description: \"The resume token returned by a browser_flow NeedsParent result.\" }),\n\tresponse: Type.Record(Type.String(), Type.Unknown(), {\n\t\tdescription:\n\t\t\t\"The ParentResponse object answering the request, e.g. \" +\n\t\t\t'{ \"response\": \"state\", \"state\": \"logged_in\" } or { \"response\": \"verified\", \"passed\": true }.',\n\t}),\n});\n\nexport type BrowserResumeInput = Static<typeof browserResumeSchema>;\n\nexport interface BrowserResumeToolOptions extends BrowsertoolsToolOptions {}\n\nexport function createBrowserResumeToolDefinition(\n\t// cwd is part of the factory signature for parity with other tools, but resume\n\t// reuses the serve process parked by browser_flow, so it is not needed here.\n\t_cwd: string,\n\toptions?: BrowserResumeToolOptions,\n): ToolDefinition<typeof browserResumeSchema, BrowserFlowDetails> {\n\tconst opts = resolveBrowsertoolsOptions(options);\n\treturn defineTool({\n\t\tname: \"browser_resume\",\n\t\tlabel: \"browser resume\",\n\t\tdescription:\n\t\t\t\"Resume a browser flow that suspended with a NeedsParent request. Pass the token from the \" +\n\t\t\t\"browser_flow result and a ParentResponse object answering the request. The flow continues \" +\n\t\t\t\"deterministically and either completes, fails, or suspends again with a new token. Off by \" +\n\t\t\t\"default; enabled with --enable-browsertools.\",\n\t\tparameters: browserResumeSchema,\n\t\tasync execute(_toolCallId, params: BrowserResumeInput, signal) {\n\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\n\t\t\tconst session = takeSession(params.token);\n\t\t\tif (!session) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`No paused browser flow for token \"${params.token}\" — it may have completed, expired (idle ` +\n\t\t\t\t\t\t`timeout), or never existed. Start a new flow with browser_flow.`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst { client } = session;\n\t\t\tconst onAbort = () => client.dispose();\n\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\ttry {\n\t\t\t\tconst outcome = await client.request<FlowOutcome>(\"flow_resume\", {\n\t\t\t\t\ttoken: params.token,\n\t\t\t\t\tresponse: params.response,\n\t\t\t\t});\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\tclient.dispose();\n\t\t\t\t\tthrow new Error(\"Operation aborted\");\n\t\t\t\t}\n\t\t\t\t// Count this resume as one more NeedsParent round if it suspends again.\n\t\t\t\treturn await advanceFlow(client, outcome, session.rounds + 1, opts);\n\t\t\t} catch (error) {\n\t\t\t\tclient.dispose();\n\t\t\t\tthrow error;\n\t\t\t} finally {\n\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t}\n\t\t},\n\t});\n}\n\nexport function createBrowserResumeTool(\n\tcwd: string,\n\toptions?: BrowserResumeToolOptions,\n): AgentTool<typeof browserResumeSchema> {\n\treturn wrapToolDefinition(createBrowserResumeToolDefinition(cwd, options));\n}\n"]}
1
+ {"version":3,"file":"browser-resume.d.ts","sourceRoot":"","sources":["../../../src/core/tools/browser-resume.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AACjE,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACzE,OAAO,EAAe,KAAK,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACzE,OAAO,EACN,KAAK,uBAAuB,EAI5B,MAAM,0BAA0B,CAAC;AAGlC,QAAA,MAAM,mBAAmB;;;EAOvB,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEpE,MAAM,WAAW,wBAAyB,SAAQ,uBAAuB;CAAG;AAE5E,wBAAgB,iCAAiC,CAGhD,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,wBAAwB,GAChC,cAAc,CAAC,OAAO,mBAAmB,EAAE,kBAAkB,CAAC,CAkDhE;AAED,wBAAgB,uBAAuB,CACtC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,wBAAwB,GAChC,SAAS,CAAC,OAAO,mBAAmB,CAAC,CAEvC","sourcesContent":["/**\n * `browser_resume` tool: answer a `NeedsParent` suspension from `browser_flow`.\n *\n * Looks up the paused serve session by its `ResumeToken`, issues `flow_resume`\n * with the parent's decision (a `ParentResponse` object), and maps the next\n * outcome the same way `browser_flow` does — completing, failing, or suspending\n * again with a fresh token. The live serve process (and its browser state) is\n * reused across rounds. See {@link browsertools-shared}.\n */\n\nimport type { AgentTool } from \"@kolisachint/hoocode-agent-core\";\nimport { type Static, Type } from \"typebox\";\nimport { defineTool, type ToolDefinition } from \"../extensions/types.js\";\nimport { advanceFlow, type BrowserFlowDetails } from \"./browser-flow.js\";\nimport {\n\ttype BrowsertoolsToolOptions,\n\ttype FlowOutcome,\n\tresolveBrowsertoolsOptions,\n\ttakeSession,\n} from \"./browsertools-shared.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\n\nconst browserResumeSchema = Type.Object({\n\ttoken: Type.String({ description: \"The resume token returned by a browser_flow NeedsParent result.\" }),\n\tresponse: Type.Record(Type.String(), Type.Unknown(), {\n\t\tdescription:\n\t\t\t\"The ParentResponse object answering the request, e.g. \" +\n\t\t\t'{ \"response\": \"state\", \"state\": \"logged_in\" } or { \"response\": \"verified\", \"passed\": true }.',\n\t}),\n});\n\nexport type BrowserResumeInput = Static<typeof browserResumeSchema>;\n\nexport interface BrowserResumeToolOptions extends BrowsertoolsToolOptions {}\n\nexport function createBrowserResumeToolDefinition(\n\t// cwd is part of the factory signature for parity with other tools, but resume\n\t// reuses the serve process parked by browser_flow, so it is not needed here.\n\t_cwd: string,\n\toptions?: BrowserResumeToolOptions,\n): ToolDefinition<typeof browserResumeSchema, BrowserFlowDetails> {\n\tconst opts = resolveBrowsertoolsOptions(options);\n\treturn defineTool({\n\t\tname: \"browser_resume\",\n\t\tlabel: \"browser resume\",\n\t\tdescription:\n\t\t\t\"Resume a browser flow that suspended with a NeedsParent request. Pass the token from the \" +\n\t\t\t\"browser_flow result and a ParentResponse object answering the request. The flow continues \" +\n\t\t\t\"deterministically and either completes, fails, or suspends again with a new token — in which \" +\n\t\t\t\"case read the new screenshot and call browser_resume again, looping until the outcome is \" +\n\t\t\t\"`complete`. Do not abandon the loop to read the page with webfetch. ParentResponse by request \" +\n\t\t\t'kind: decide_next_action -> { response: \"next_action\", action: <Action e.g. {action:\"click\", ' +\n\t\t\t'selector, fallbacks?}> }; classify_state -> { response: \"state\", state }; verify_visual -> ' +\n\t\t\t'{ response: \"verified\", passed }; extract_semantic -> { response: \"extracted\", fields }; ' +\n\t\t\t'reidentify_element -> { response: \"element\", selector }. Off by default; enabled with ' +\n\t\t\t\"--enable-browsertools.\",\n\t\tparameters: browserResumeSchema,\n\t\tasync execute(_toolCallId, params: BrowserResumeInput, signal) {\n\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\n\t\t\tconst session = takeSession(params.token);\n\t\t\tif (!session) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`No paused browser flow for token \"${params.token}\" — it may have completed, expired (idle ` +\n\t\t\t\t\t\t`timeout), or never existed. Start a new flow with browser_flow.`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst { client } = session;\n\t\t\tconst onAbort = () => client.dispose();\n\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\ttry {\n\t\t\t\tconst outcome = await client.request<FlowOutcome>(\"flow_resume\", {\n\t\t\t\t\ttoken: params.token,\n\t\t\t\t\tresponse: params.response,\n\t\t\t\t});\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\tclient.dispose();\n\t\t\t\t\tthrow new Error(\"Operation aborted\");\n\t\t\t\t}\n\t\t\t\t// Count this resume as one more NeedsParent round if it suspends again.\n\t\t\t\treturn await advanceFlow(client, outcome, session.rounds + 1, opts);\n\t\t\t} catch (error) {\n\t\t\t\tclient.dispose();\n\t\t\t\tthrow error;\n\t\t\t} finally {\n\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t}\n\t\t},\n\t});\n}\n\nexport function createBrowserResumeTool(\n\tcwd: string,\n\toptions?: BrowserResumeToolOptions,\n): AgentTool<typeof browserResumeSchema> {\n\treturn wrapToolDefinition(createBrowserResumeToolDefinition(cwd, options));\n}\n"]}
@@ -29,8 +29,14 @@ _cwd, options) {
29
29
  label: "browser resume",
30
30
  description: "Resume a browser flow that suspended with a NeedsParent request. Pass the token from the " +
31
31
  "browser_flow result and a ParentResponse object answering the request. The flow continues " +
32
- "deterministically and either completes, fails, or suspends again with a new token. Off by " +
33
- "default; enabled with --enable-browsertools.",
32
+ "deterministically and either completes, fails, or suspends again with a new token in which " +
33
+ "case read the new screenshot and call browser_resume again, looping until the outcome is " +
34
+ "`complete`. Do not abandon the loop to read the page with webfetch. ParentResponse by request " +
35
+ 'kind: decide_next_action -> { response: "next_action", action: <Action e.g. {action:"click", ' +
36
+ 'selector, fallbacks?}> }; classify_state -> { response: "state", state }; verify_visual -> ' +
37
+ '{ response: "verified", passed }; extract_semantic -> { response: "extracted", fields }; ' +
38
+ 'reidentify_element -> { response: "element", selector }. Off by default; enabled with ' +
39
+ "--enable-browsertools.",
34
40
  parameters: browserResumeSchema,
35
41
  async execute(_toolCallId, params, signal) {
36
42
  if (signal?.aborted)
@@ -1 +1 @@
1
- {"version":3,"file":"browser-resume.js","sourceRoot":"","sources":["../../../src/core/tools/browser-resume.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAAe,IAAI,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAuB,MAAM,wBAAwB,CAAC;AACzE,OAAO,EAAE,WAAW,EAA2B,MAAM,mBAAmB,CAAC;AACzE,OAAO,EAGN,0BAA0B,EAC1B,WAAW,GACX,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAElE,MAAM,mBAAmB,GAAG,IAAI,CAAC,MAAM,CAAC;IACvC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,iEAAiE,EAAE,CAAC;IACtG,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,CAAC,OAAO,EAAE,EAAE;QACpD,WAAW,EACV,wDAAwD;YACxD,8FAA8F;KAC/F,CAAC;CACF,CAAC,CAAC;AAMH,MAAM,UAAU,iCAAiC;AAChD,+EAA+E;AAC/E,6EAA6E;AAC7E,IAAY,EACZ,OAAkC,EAC+B;IACjE,MAAM,IAAI,GAAG,0BAA0B,CAAC,OAAO,CAAC,CAAC;IACjD,OAAO,UAAU,CAAC;QACjB,IAAI,EAAE,gBAAgB;QACtB,KAAK,EAAE,gBAAgB;QACvB,WAAW,EACV,2FAA2F;YAC3F,4FAA4F;YAC5F,4FAA4F;YAC5F,8CAA8C;QAC/C,UAAU,EAAE,mBAAmB;QAC/B,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,MAA0B,EAAE,MAAM,EAAE;YAC9D,IAAI,MAAM,EAAE,OAAO;gBAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;YAE1D,MAAM,OAAO,GAAG,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAC1C,IAAI,CAAC,OAAO,EAAE,CAAC;gBACd,MAAM,IAAI,KAAK,CACd,qCAAqC,MAAM,CAAC,KAAK,6CAA2C;oBAC3F,iEAAiE,CAClE,CAAC;YACH,CAAC;YAED,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;YAC3B,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACvC,MAAM,EAAE,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;YAC3D,IAAI,CAAC;gBACJ,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,OAAO,CAAc,aAAa,EAAE;oBAChE,KAAK,EAAE,MAAM,CAAC,KAAK;oBACnB,QAAQ,EAAE,MAAM,CAAC,QAAQ;iBACzB,CAAC,CAAC;gBACH,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;oBACrB,MAAM,CAAC,OAAO,EAAE,CAAC;oBACjB,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;gBACtC,CAAC;gBACD,wEAAwE;gBACxE,OAAO,MAAM,WAAW,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC;YACrE,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBAChB,MAAM,CAAC,OAAO,EAAE,CAAC;gBACjB,MAAM,KAAK,CAAC;YACb,CAAC;oBAAS,CAAC;gBACV,MAAM,EAAE,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC/C,CAAC;QAAA,CACD;KACD,CAAC,CAAC;AAAA,CACH;AAED,MAAM,UAAU,uBAAuB,CACtC,GAAW,EACX,OAAkC,EACM;IACxC,OAAO,kBAAkB,CAAC,iCAAiC,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC;AAAA,CAC3E","sourcesContent":["/**\n * `browser_resume` tool: answer a `NeedsParent` suspension from `browser_flow`.\n *\n * Looks up the paused serve session by its `ResumeToken`, issues `flow_resume`\n * with the parent's decision (a `ParentResponse` object), and maps the next\n * outcome the same way `browser_flow` does — completing, failing, or suspending\n * again with a fresh token. The live serve process (and its browser state) is\n * reused across rounds. See {@link browsertools-shared}.\n */\n\nimport type { AgentTool } from \"@kolisachint/hoocode-agent-core\";\nimport { type Static, Type } from \"typebox\";\nimport { defineTool, type ToolDefinition } from \"../extensions/types.js\";\nimport { advanceFlow, type BrowserFlowDetails } from \"./browser-flow.js\";\nimport {\n\ttype BrowsertoolsToolOptions,\n\ttype FlowOutcome,\n\tresolveBrowsertoolsOptions,\n\ttakeSession,\n} from \"./browsertools-shared.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\n\nconst browserResumeSchema = Type.Object({\n\ttoken: Type.String({ description: \"The resume token returned by a browser_flow NeedsParent result.\" }),\n\tresponse: Type.Record(Type.String(), Type.Unknown(), {\n\t\tdescription:\n\t\t\t\"The ParentResponse object answering the request, e.g. \" +\n\t\t\t'{ \"response\": \"state\", \"state\": \"logged_in\" } or { \"response\": \"verified\", \"passed\": true }.',\n\t}),\n});\n\nexport type BrowserResumeInput = Static<typeof browserResumeSchema>;\n\nexport interface BrowserResumeToolOptions extends BrowsertoolsToolOptions {}\n\nexport function createBrowserResumeToolDefinition(\n\t// cwd is part of the factory signature for parity with other tools, but resume\n\t// reuses the serve process parked by browser_flow, so it is not needed here.\n\t_cwd: string,\n\toptions?: BrowserResumeToolOptions,\n): ToolDefinition<typeof browserResumeSchema, BrowserFlowDetails> {\n\tconst opts = resolveBrowsertoolsOptions(options);\n\treturn defineTool({\n\t\tname: \"browser_resume\",\n\t\tlabel: \"browser resume\",\n\t\tdescription:\n\t\t\t\"Resume a browser flow that suspended with a NeedsParent request. Pass the token from the \" +\n\t\t\t\"browser_flow result and a ParentResponse object answering the request. The flow continues \" +\n\t\t\t\"deterministically and either completes, fails, or suspends again with a new token. Off by \" +\n\t\t\t\"default; enabled with --enable-browsertools.\",\n\t\tparameters: browserResumeSchema,\n\t\tasync execute(_toolCallId, params: BrowserResumeInput, signal) {\n\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\n\t\t\tconst session = takeSession(params.token);\n\t\t\tif (!session) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`No paused browser flow for token \"${params.token}\" — it may have completed, expired (idle ` +\n\t\t\t\t\t\t`timeout), or never existed. Start a new flow with browser_flow.`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst { client } = session;\n\t\t\tconst onAbort = () => client.dispose();\n\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\ttry {\n\t\t\t\tconst outcome = await client.request<FlowOutcome>(\"flow_resume\", {\n\t\t\t\t\ttoken: params.token,\n\t\t\t\t\tresponse: params.response,\n\t\t\t\t});\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\tclient.dispose();\n\t\t\t\t\tthrow new Error(\"Operation aborted\");\n\t\t\t\t}\n\t\t\t\t// Count this resume as one more NeedsParent round if it suspends again.\n\t\t\t\treturn await advanceFlow(client, outcome, session.rounds + 1, opts);\n\t\t\t} catch (error) {\n\t\t\t\tclient.dispose();\n\t\t\t\tthrow error;\n\t\t\t} finally {\n\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t}\n\t\t},\n\t});\n}\n\nexport function createBrowserResumeTool(\n\tcwd: string,\n\toptions?: BrowserResumeToolOptions,\n): AgentTool<typeof browserResumeSchema> {\n\treturn wrapToolDefinition(createBrowserResumeToolDefinition(cwd, options));\n}\n"]}
1
+ {"version":3,"file":"browser-resume.js","sourceRoot":"","sources":["../../../src/core/tools/browser-resume.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `browser_resume` tool: answer a `NeedsParent` suspension from `browser_flow`.\n *\n * Looks up the paused serve session by its `ResumeToken`, issues `flow_resume`\n * with the parent's decision (a `ParentResponse` object), and maps the next\n * outcome the same way `browser_flow` does — completing, failing, or suspending\n * again with a fresh token. The live serve process (and its browser state) is\n * reused across rounds. See {@link browsertools-shared}.\n */\n\nimport type { AgentTool } from \"@kolisachint/hoocode-agent-core\";\nimport { type Static, Type } from \"typebox\";\nimport { defineTool, type ToolDefinition } from \"../extensions/types.js\";\nimport { advanceFlow, type BrowserFlowDetails } from \"./browser-flow.js\";\nimport {\n\ttype BrowsertoolsToolOptions,\n\ttype FlowOutcome,\n\tresolveBrowsertoolsOptions,\n\ttakeSession,\n} from \"./browsertools-shared.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\n\nconst browserResumeSchema = Type.Object({\n\ttoken: Type.String({ description: \"The resume token returned by a browser_flow NeedsParent result.\" }),\n\tresponse: Type.Record(Type.String(), Type.Unknown(), {\n\t\tdescription:\n\t\t\t\"The ParentResponse object answering the request, e.g. \" +\n\t\t\t'{ \"response\": \"state\", \"state\": \"logged_in\" } or { \"response\": \"verified\", \"passed\": true }.',\n\t}),\n});\n\nexport type BrowserResumeInput = Static<typeof browserResumeSchema>;\n\nexport interface BrowserResumeToolOptions extends BrowsertoolsToolOptions {}\n\nexport function createBrowserResumeToolDefinition(\n\t// cwd is part of the factory signature for parity with other tools, but resume\n\t// reuses the serve process parked by browser_flow, so it is not needed here.\n\t_cwd: string,\n\toptions?: BrowserResumeToolOptions,\n): ToolDefinition<typeof browserResumeSchema, BrowserFlowDetails> {\n\tconst opts = resolveBrowsertoolsOptions(options);\n\treturn defineTool({\n\t\tname: \"browser_resume\",\n\t\tlabel: \"browser resume\",\n\t\tdescription:\n\t\t\t\"Resume a browser flow that suspended with a NeedsParent request. Pass the token from the \" +\n\t\t\t\"browser_flow result and a ParentResponse object answering the request. The flow continues \" +\n\t\t\t\"deterministically and either completes, fails, or suspends again with a new token — in which \" +\n\t\t\t\"case read the new screenshot and call browser_resume again, looping until the outcome is \" +\n\t\t\t\"`complete`. Do not abandon the loop to read the page with webfetch. ParentResponse by request \" +\n\t\t\t'kind: decide_next_action -> { response: \"next_action\", action: <Action e.g. {action:\"click\", ' +\n\t\t\t'selector, fallbacks?}> }; classify_state -> { response: \"state\", state }; verify_visual -> ' +\n\t\t\t'{ response: \"verified\", passed }; extract_semantic -> { response: \"extracted\", fields }; ' +\n\t\t\t'reidentify_element -> { response: \"element\", selector }. Off by default; enabled with ' +\n\t\t\t\"--enable-browsertools.\",\n\t\tparameters: browserResumeSchema,\n\t\tasync execute(_toolCallId, params: BrowserResumeInput, signal) {\n\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\n\t\t\tconst session = takeSession(params.token);\n\t\t\tif (!session) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`No paused browser flow for token \"${params.token}\" — it may have completed, expired (idle ` +\n\t\t\t\t\t\t`timeout), or never existed. Start a new flow with browser_flow.`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst { client } = session;\n\t\t\tconst onAbort = () => client.dispose();\n\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\ttry {\n\t\t\t\tconst outcome = await client.request<FlowOutcome>(\"flow_resume\", {\n\t\t\t\t\ttoken: params.token,\n\t\t\t\t\tresponse: params.response,\n\t\t\t\t});\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\tclient.dispose();\n\t\t\t\t\tthrow new Error(\"Operation aborted\");\n\t\t\t\t}\n\t\t\t\t// Count this resume as one more NeedsParent round if it suspends again.\n\t\t\t\treturn await advanceFlow(client, outcome, session.rounds + 1, opts);\n\t\t\t} catch (error) {\n\t\t\t\tclient.dispose();\n\t\t\t\tthrow error;\n\t\t\t} finally {\n\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t}\n\t\t},\n\t});\n}\n\nexport function createBrowserResumeTool(\n\tcwd: string,\n\toptions?: BrowserResumeToolOptions,\n): AgentTool<typeof browserResumeSchema> {\n\treturn wrapToolDefinition(createBrowserResumeToolDefinition(cwd, options));\n}\n"]}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-extension-custom-provider-anthropic",
3
3
  "private": true,
4
- "version": "0.2.94",
4
+ "version": "0.2.95",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "bun": ">=1.0.0"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-extension-custom-provider-gitlab-duo",
3
3
  "private": true,
4
- "version": "0.2.94",
4
+ "version": "0.2.95",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "bun": ">=1.0.0"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-extension-sandbox",
3
3
  "private": true,
4
- "version": "0.2.94",
4
+ "version": "0.2.95",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "bun": ">=1.0.0"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-extension-with-deps",
3
3
  "private": true,
4
- "version": "0.2.94",
4
+ "version": "0.2.95",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "bun": ">=1.0.0"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-agent",
3
- "version": "0.4.97",
3
+ "version": "0.4.98",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "hoocodeConfig": {
@@ -45,9 +45,9 @@
45
45
  "prepublishOnly": "npm run clean && npm run build"
46
46
  },
47
47
  "dependencies": {
48
- "@kolisachint/hoocode-agent-core": "^0.4.97",
49
- "@kolisachint/hoocode-ai": "^0.4.97",
50
- "@kolisachint/hoocode-tui": "^0.4.97",
48
+ "@kolisachint/hoocode-agent-core": "^0.4.98",
49
+ "@kolisachint/hoocode-ai": "^0.4.98",
50
+ "@kolisachint/hoocode-tui": "^0.4.98",
51
51
  "@silvia-odwyer/photon-node": "^0.3.4",
52
52
  "chalk": "^5.5.0",
53
53
  "cli-highlight": "^2.1.11",