@kolisachint/hoocode-agent 0.4.94 → 0.4.96
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 +14 -0
- package/dist/cli/args.d.ts +2 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +7 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-session-services.d.ts +1 -0
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +1 -0
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/sdk.d.ts +7 -0
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +1 -0
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/settings-defaults.d.ts +1 -0
- package/dist/core/settings-defaults.d.ts.map +1 -1
- package/dist/core/settings-defaults.js +1 -0
- package/dist/core/settings-defaults.js.map +1 -1
- package/dist/core/settings-manager.d.ts +3 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +8 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/tools/browser-flow.d.ts +46 -0
- package/dist/core/tools/browser-flow.d.ts.map +1 -0
- package/dist/core/tools/browser-flow.js +163 -0
- package/dist/core/tools/browser-flow.js.map +1 -0
- package/dist/core/tools/browser-resume.d.ts +25 -0
- package/dist/core/tools/browser-resume.d.ts.map +1 -0
- package/dist/core/tools/browser-resume.js +71 -0
- package/dist/core/tools/browser-resume.js.map +1 -0
- package/dist/core/tools/browsertools-shared.d.ts +152 -0
- package/dist/core/tools/browsertools-shared.d.ts.map +1 -0
- package/dist/core/tools/browsertools-shared.js +274 -0
- package/dist/core/tools/browsertools-shared.js.map +1 -0
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +7 -1
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/index.d.ts +7 -1
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +18 -0
- package/dist/core/tools/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +7 -0
- package/dist/main.js.map +1 -1
- package/dist/utils/tools-manager.d.ts +1 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +24 -0
- package/dist/utils/tools-manager.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
|
@@ -0,0 +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 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});\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/** 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});\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\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\treturn await advanceFlow(client, outcome, 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 createBrowserFlowTool(\n\tcwd: string,\n\toptions?: BrowserFlowToolOptions,\n): AgentTool<typeof browserFlowSchema> {\n\treturn wrapToolDefinition(createBrowserFlowToolDefinition(cwd, options));\n}\n"]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `browser_resume` tool: answer a `NeedsParent` suspension from `browser_flow`.
|
|
3
|
+
*
|
|
4
|
+
* Looks up the paused serve session by its `ResumeToken`, issues `flow_resume`
|
|
5
|
+
* with the parent's decision (a `ParentResponse` object), and maps the next
|
|
6
|
+
* outcome the same way `browser_flow` does — completing, failing, or suspending
|
|
7
|
+
* again with a fresh token. The live serve process (and its browser state) is
|
|
8
|
+
* reused across rounds. See {@link browsertools-shared}.
|
|
9
|
+
*/
|
|
10
|
+
import type { AgentTool } from "@kolisachint/hoocode-agent-core";
|
|
11
|
+
import { type Static, Type } from "typebox";
|
|
12
|
+
import { type ToolDefinition } from "../extensions/types.js";
|
|
13
|
+
import { type BrowserFlowDetails } from "./browser-flow.js";
|
|
14
|
+
import { type BrowsertoolsToolOptions } from "./browsertools-shared.js";
|
|
15
|
+
declare const browserResumeSchema: Type.TObject<{
|
|
16
|
+
token: Type.TString;
|
|
17
|
+
response: Type.TRecord<"^.*$", Type.TUnknown>;
|
|
18
|
+
}>;
|
|
19
|
+
export type BrowserResumeInput = Static<typeof browserResumeSchema>;
|
|
20
|
+
export interface BrowserResumeToolOptions extends BrowsertoolsToolOptions {
|
|
21
|
+
}
|
|
22
|
+
export declare function createBrowserResumeToolDefinition(_cwd: string, options?: BrowserResumeToolOptions): ToolDefinition<typeof browserResumeSchema, BrowserFlowDetails>;
|
|
23
|
+
export declare function createBrowserResumeTool(cwd: string, options?: BrowserResumeToolOptions): AgentTool<typeof browserResumeSchema>;
|
|
24
|
+
export {};
|
|
25
|
+
//# sourceMappingURL=browser-resume.d.ts.map
|
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `browser_resume` tool: answer a `NeedsParent` suspension from `browser_flow`.
|
|
3
|
+
*
|
|
4
|
+
* Looks up the paused serve session by its `ResumeToken`, issues `flow_resume`
|
|
5
|
+
* with the parent's decision (a `ParentResponse` object), and maps the next
|
|
6
|
+
* outcome the same way `browser_flow` does — completing, failing, or suspending
|
|
7
|
+
* again with a fresh token. The live serve process (and its browser state) is
|
|
8
|
+
* reused across rounds. See {@link browsertools-shared}.
|
|
9
|
+
*/
|
|
10
|
+
import { Type } from "typebox";
|
|
11
|
+
import { defineTool } from "../extensions/types.js";
|
|
12
|
+
import { advanceFlow } from "./browser-flow.js";
|
|
13
|
+
import { resolveBrowsertoolsOptions, takeSession, } from "./browsertools-shared.js";
|
|
14
|
+
import { wrapToolDefinition } from "./tool-definition-wrapper.js";
|
|
15
|
+
const browserResumeSchema = Type.Object({
|
|
16
|
+
token: Type.String({ description: "The resume token returned by a browser_flow NeedsParent result." }),
|
|
17
|
+
response: Type.Record(Type.String(), Type.Unknown(), {
|
|
18
|
+
description: "The ParentResponse object answering the request, e.g. " +
|
|
19
|
+
'{ "response": "state", "state": "logged_in" } or { "response": "verified", "passed": true }.',
|
|
20
|
+
}),
|
|
21
|
+
});
|
|
22
|
+
export function createBrowserResumeToolDefinition(
|
|
23
|
+
// cwd is part of the factory signature for parity with other tools, but resume
|
|
24
|
+
// reuses the serve process parked by browser_flow, so it is not needed here.
|
|
25
|
+
_cwd, options) {
|
|
26
|
+
const opts = resolveBrowsertoolsOptions(options);
|
|
27
|
+
return defineTool({
|
|
28
|
+
name: "browser_resume",
|
|
29
|
+
label: "browser resume",
|
|
30
|
+
description: "Resume a browser flow that suspended with a NeedsParent request. Pass the token from the " +
|
|
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.",
|
|
34
|
+
parameters: browserResumeSchema,
|
|
35
|
+
async execute(_toolCallId, params, signal) {
|
|
36
|
+
if (signal?.aborted)
|
|
37
|
+
throw new Error("Operation aborted");
|
|
38
|
+
const session = takeSession(params.token);
|
|
39
|
+
if (!session) {
|
|
40
|
+
throw new Error(`No paused browser flow for token "${params.token}" — it may have completed, expired (idle ` +
|
|
41
|
+
`timeout), or never existed. Start a new flow with browser_flow.`);
|
|
42
|
+
}
|
|
43
|
+
const { client } = session;
|
|
44
|
+
const onAbort = () => client.dispose();
|
|
45
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
46
|
+
try {
|
|
47
|
+
const outcome = await client.request("flow_resume", {
|
|
48
|
+
token: params.token,
|
|
49
|
+
response: params.response,
|
|
50
|
+
});
|
|
51
|
+
if (signal?.aborted) {
|
|
52
|
+
client.dispose();
|
|
53
|
+
throw new Error("Operation aborted");
|
|
54
|
+
}
|
|
55
|
+
// Count this resume as one more NeedsParent round if it suspends again.
|
|
56
|
+
return await advanceFlow(client, outcome, session.rounds + 1, opts);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
client.dispose();
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
signal?.removeEventListener("abort", onAbort);
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
export function createBrowserResumeTool(cwd, options) {
|
|
69
|
+
return wrapToolDefinition(createBrowserResumeToolDefinition(cwd, options));
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=browser-resume.js.map
|
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared plumbing for the `browser_flow` and `browser_resume` tools.
|
|
3
|
+
*
|
|
4
|
+
* Unlike `webtools` (one-shot fetch/search subprocesses), `browsertools` drives
|
|
5
|
+
* a *stateful* parent-in-the-loop protocol: a single long-lived `browsertools
|
|
6
|
+
* serve` process holds the live Chromium and the paused-flow state in memory,
|
|
7
|
+
* and the parent (hoocode) talks to it over newline-delimited JSON-RPC on
|
|
8
|
+
* stdin/stdout. A flow can suspend mid-replay with `Outcome::NeedsParent`,
|
|
9
|
+
* yielding a typed `ParentRequest` + `ResumeToken`; the parent reasons about it
|
|
10
|
+
* (often by looking at a screenshot) and calls `flow_resume(token, response)` to
|
|
11
|
+
* continue. See https://github.com/kolisachint/browsertools (contract.rs).
|
|
12
|
+
*
|
|
13
|
+
* Because that single serve process must survive *between* the `browser_flow`
|
|
14
|
+
* call that starts the flow and the `browser_resume` call that answers a
|
|
15
|
+
* `NeedsParent`, this module owns a process-wide {@link sessionRegistry} keyed by
|
|
16
|
+
* `ResumeToken`. Each paused session keeps its serve client alive and is reaped
|
|
17
|
+
* after an idle timeout so an abandoned flow never leaks a Chromium forever.
|
|
18
|
+
*
|
|
19
|
+
* This module owns:
|
|
20
|
+
* - the persistent JSON-RPC serve client (id-correlated request/response),
|
|
21
|
+
* - the locked contract types (mirrors browsertools `src/contract.rs`),
|
|
22
|
+
* - the paused-session registry + lifecycle (idle reap, round cap, teardown),
|
|
23
|
+
* - binary + browser-path resolution.
|
|
24
|
+
*/
|
|
25
|
+
/** Opaque token identifying a paused flow, returned with every `NeedsParent`. */
|
|
26
|
+
export type ResumeToken = string;
|
|
27
|
+
/** Reference to bytes (e.g. a screenshot) the parent can fetch via `get_resource`. */
|
|
28
|
+
export type ResourceId = string;
|
|
29
|
+
/**
|
|
30
|
+
* A typed request the engine yields when replay hits ambiguity only an LLM can
|
|
31
|
+
* resolve. The `request` discriminant matches the serde tag in contract.rs.
|
|
32
|
+
* Fields beyond the discriminant are passed through verbatim to the model.
|
|
33
|
+
*/
|
|
34
|
+
export interface ParentRequest {
|
|
35
|
+
request: "classify_state" | "verify_visual" | "extract_semantic" | "decide_next_action" | "reidentify_element";
|
|
36
|
+
/** Resource id of the screenshot captured at the suspension point, if any. */
|
|
37
|
+
screenshot_ref?: ResourceId;
|
|
38
|
+
[key: string]: unknown;
|
|
39
|
+
}
|
|
40
|
+
/** Result of a `flow_start` / `flow_resume` RPC (the `result` field's `outcome`). */
|
|
41
|
+
export type FlowOutcome = {
|
|
42
|
+
outcome: "complete";
|
|
43
|
+
result?: unknown;
|
|
44
|
+
} | {
|
|
45
|
+
outcome: "needs_parent";
|
|
46
|
+
request: ParentRequest;
|
|
47
|
+
token: ResumeToken;
|
|
48
|
+
} | {
|
|
49
|
+
outcome: "failed";
|
|
50
|
+
step_id?: string;
|
|
51
|
+
kind?: string;
|
|
52
|
+
detail?: string;
|
|
53
|
+
};
|
|
54
|
+
/** Result of a `get_resource` RPC. */
|
|
55
|
+
export interface GetResourceResult {
|
|
56
|
+
ref: ResourceId;
|
|
57
|
+
mime: string;
|
|
58
|
+
len: number;
|
|
59
|
+
png_base64: string;
|
|
60
|
+
}
|
|
61
|
+
/** Default per-request RPC timeout. Flow steps drive a real browser, so this is
|
|
62
|
+
* generous relative to webtools' 15s. */
|
|
63
|
+
export declare const BROWSERTOOLS_REQUEST_TIMEOUT_MS = 120000;
|
|
64
|
+
/** Default idle window a paused flow may sit in the registry before it is reaped
|
|
65
|
+
* (killing its serve process + Chromium). Reset on every resume round. The
|
|
66
|
+
* parent may reason / look at a screenshot / make other tool calls between
|
|
67
|
+
* rounds, so this is minutes, not seconds. */
|
|
68
|
+
export declare const BROWSERTOOLS_DEFAULT_IDLE_MS: number;
|
|
69
|
+
/** Default hard cap on `NeedsParent` rounds for a single flow, bounding a runaway
|
|
70
|
+
* suspend/resume loop. */
|
|
71
|
+
export declare const BROWSERTOOLS_DEFAULT_MAX_ROUNDS = 12;
|
|
72
|
+
/** Options threaded into the browser tools by their factories (tests/overrides). */
|
|
73
|
+
export interface BrowsertoolsToolOptions {
|
|
74
|
+
/** Explicit path to the `browsertools` binary; otherwise resolved via {@link ensureTool}. */
|
|
75
|
+
binaryPath?: string;
|
|
76
|
+
/** Chromium/Chrome executable path forwarded to the serve process. */
|
|
77
|
+
browserPath?: string;
|
|
78
|
+
/** Idle reap window for paused sessions (ms). */
|
|
79
|
+
idleTimeoutMs?: number;
|
|
80
|
+
/** Hard cap on NeedsParent rounds per flow. */
|
|
81
|
+
maxParentRounds?: number;
|
|
82
|
+
/** Per-request RPC timeout (ms). */
|
|
83
|
+
requestTimeoutMs?: number;
|
|
84
|
+
/** Extra args appended after `serve` (mainly for tests). */
|
|
85
|
+
serveArgs?: string[];
|
|
86
|
+
}
|
|
87
|
+
/** Resolve effective options from explicit overrides, then env, then defaults. */
|
|
88
|
+
export declare function resolveBrowsertoolsOptions(options?: BrowsertoolsToolOptions): Required<Pick<BrowsertoolsToolOptions, "idleTimeoutMs" | "maxParentRounds" | "requestTimeoutMs">> & {
|
|
89
|
+
binaryPath?: string;
|
|
90
|
+
browserPath?: string;
|
|
91
|
+
serveArgs: string[];
|
|
92
|
+
};
|
|
93
|
+
/** Resolve the `browsertools` binary path (explicit override, else download/PATH). */
|
|
94
|
+
export declare function resolveBrowsertoolsBinary(options?: BrowsertoolsToolOptions): Promise<string>;
|
|
95
|
+
/**
|
|
96
|
+
* A live `browsertools serve` process spoken to over newline-delimited JSON-RPC.
|
|
97
|
+
*
|
|
98
|
+
* Wire format (browsertools `src/serve.rs`):
|
|
99
|
+
* request: `{ "id": <n>, "method": <string>, "params": <object> }\n`
|
|
100
|
+
* response: `{ "id": <n>, "result": <value> }` or `{ "id": <n>, "error": <RpcError> }`\n
|
|
101
|
+
*
|
|
102
|
+
* Responses are correlated by `id`, so concurrent requests are safe, though the
|
|
103
|
+
* tools drive it sequentially.
|
|
104
|
+
*/
|
|
105
|
+
export declare class BrowsertoolsServeClient {
|
|
106
|
+
private readonly proc;
|
|
107
|
+
private readonly pending;
|
|
108
|
+
private readonly requestTimeoutMs;
|
|
109
|
+
private buffer;
|
|
110
|
+
private nextId;
|
|
111
|
+
private closed;
|
|
112
|
+
private exitError;
|
|
113
|
+
private stderrTail;
|
|
114
|
+
constructor(binaryPath: string, opts: {
|
|
115
|
+
cwd?: string;
|
|
116
|
+
browserPath?: string;
|
|
117
|
+
serveArgs: string[];
|
|
118
|
+
requestTimeoutMs: number;
|
|
119
|
+
});
|
|
120
|
+
private onStdout;
|
|
121
|
+
private onLine;
|
|
122
|
+
/** Reject all in-flight requests and mark the client dead. Idempotent. */
|
|
123
|
+
private fail;
|
|
124
|
+
/** Send a JSON-RPC request and resolve with its `result` (or reject on `error`). */
|
|
125
|
+
request<T = unknown>(method: string, params: Record<string, unknown>): Promise<T>;
|
|
126
|
+
/** Kill the serve process and reject any outstanding requests. Idempotent. */
|
|
127
|
+
dispose(): void;
|
|
128
|
+
}
|
|
129
|
+
interface PausedSession {
|
|
130
|
+
client: BrowsertoolsServeClient;
|
|
131
|
+
/** Number of NeedsParent rounds yielded so far for this flow. */
|
|
132
|
+
rounds: number;
|
|
133
|
+
idleTimer: ReturnType<typeof setTimeout>;
|
|
134
|
+
idleTimeoutMs: number;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Park a paused flow's live serve client under its resume token and arm the idle
|
|
138
|
+
* reaper. Any previous entry for the token is disposed first.
|
|
139
|
+
*/
|
|
140
|
+
export declare function parkSession(token: ResumeToken, client: BrowsertoolsServeClient, rounds: number, idleTimeoutMs: number): void;
|
|
141
|
+
/**
|
|
142
|
+
* Remove a paused session from the registry and return it, stopping its idle
|
|
143
|
+
* timer. The caller now owns the client (to resume or dispose). Returns
|
|
144
|
+
* undefined if the token is unknown (expired/reaped/already completed).
|
|
145
|
+
*/
|
|
146
|
+
export declare function takeSession(token: ResumeToken): PausedSession | undefined;
|
|
147
|
+
/** Dispose every parked session. Used on shutdown and in tests. */
|
|
148
|
+
export declare function disposeAllSessions(): void;
|
|
149
|
+
/** Number of currently parked sessions (for tests/diagnostics). */
|
|
150
|
+
export declare function pausedSessionCount(): number;
|
|
151
|
+
export {};
|
|
152
|
+
//# sourceMappingURL=browsertools-shared.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browsertools-shared.d.ts","sourceRoot":"","sources":["../../../src/core/tools/browsertools-shared.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared plumbing for the `browser_flow` and `browser_resume` tools.\n *\n * Unlike `webtools` (one-shot fetch/search subprocesses), `browsertools` drives\n * a *stateful* parent-in-the-loop protocol: a single long-lived `browsertools\n * serve` process holds the live Chromium and the paused-flow state in memory,\n * and the parent (hoocode) talks to it over newline-delimited JSON-RPC on\n * stdin/stdout. A flow can suspend mid-replay with `Outcome::NeedsParent`,\n * yielding a typed `ParentRequest` + `ResumeToken`; the parent reasons about it\n * (often by looking at a screenshot) and calls `flow_resume(token, response)` to\n * continue. See https://github.com/kolisachint/browsertools (contract.rs).\n *\n * Because that single serve process must survive *between* the `browser_flow`\n * call that starts the flow and the `browser_resume` call that answers a\n * `NeedsParent`, this module owns a process-wide {@link sessionRegistry} keyed by\n * `ResumeToken`. Each paused session keeps its serve client alive and is reaped\n * after an idle timeout so an abandoned flow never leaks a Chromium forever.\n *\n * This module owns:\n * - the persistent JSON-RPC serve client (id-correlated request/response),\n * - the locked contract types (mirrors browsertools `src/contract.rs`),\n * - the paused-session registry + lifecycle (idle reap, round cap, teardown),\n * - binary + browser-path resolution.\n */\n\nimport { type ChildProcess, spawn } from \"node:child_process\";\nimport { ensureTool } from \"../../utils/tools-manager.js\";\n\n// ============================================================================\n// Contract types (locked against browsertools `src/contract.rs`)\n// ============================================================================\n\n/** Opaque token identifying a paused flow, returned with every `NeedsParent`. */\nexport type ResumeToken = string;\n\n/** Reference to bytes (e.g. a screenshot) the parent can fetch via `get_resource`. */\nexport type ResourceId = string;\n\n/**\n * A typed request the engine yields when replay hits ambiguity only an LLM can\n * resolve. The `request` discriminant matches the serde tag in contract.rs.\n * Fields beyond the discriminant are passed through verbatim to the model.\n */\nexport interface ParentRequest {\n\trequest: \"classify_state\" | \"verify_visual\" | \"extract_semantic\" | \"decide_next_action\" | \"reidentify_element\";\n\t/** Resource id of the screenshot captured at the suspension point, if any. */\n\tscreenshot_ref?: ResourceId;\n\t[key: string]: unknown;\n}\n\n/** Result of a `flow_start` / `flow_resume` RPC (the `result` field's `outcome`). */\nexport type FlowOutcome =\n\t| { outcome: \"complete\"; result?: unknown }\n\t| { outcome: \"needs_parent\"; request: ParentRequest; token: ResumeToken }\n\t| { outcome: \"failed\"; step_id?: string; kind?: string; detail?: string };\n\n/** Result of a `get_resource` RPC. */\nexport interface GetResourceResult {\n\tref: ResourceId;\n\tmime: string;\n\tlen: number;\n\tpng_base64: string;\n}\n\n// ============================================================================\n// Configuration\n// ============================================================================\n\n/** Default per-request RPC timeout. Flow steps drive a real browser, so this is\n * generous relative to webtools' 15s. */\nexport const BROWSERTOOLS_REQUEST_TIMEOUT_MS = 120_000;\n\n/** Default idle window a paused flow may sit in the registry before it is reaped\n * (killing its serve process + Chromium). Reset on every resume round. The\n * parent may reason / look at a screenshot / make other tool calls between\n * rounds, so this is minutes, not seconds. */\nexport const BROWSERTOOLS_DEFAULT_IDLE_MS = 5 * 60 * 1000;\n\n/** Default hard cap on `NeedsParent` rounds for a single flow, bounding a runaway\n * suspend/resume loop. */\nexport const BROWSERTOOLS_DEFAULT_MAX_ROUNDS = 12;\n\n/** Options threaded into the browser tools by their factories (tests/overrides). */\nexport interface BrowsertoolsToolOptions {\n\t/** Explicit path to the `browsertools` binary; otherwise resolved via {@link ensureTool}. */\n\tbinaryPath?: string;\n\t/** Chromium/Chrome executable path forwarded to the serve process. */\n\tbrowserPath?: string;\n\t/** Idle reap window for paused sessions (ms). */\n\tidleTimeoutMs?: number;\n\t/** Hard cap on NeedsParent rounds per flow. */\n\tmaxParentRounds?: number;\n\t/** Per-request RPC timeout (ms). */\n\trequestTimeoutMs?: number;\n\t/** Extra args appended after `serve` (mainly for tests). */\n\tserveArgs?: string[];\n}\n\nfunction envNumber(name: string): number | undefined {\n\tconst raw = process.env[name];\n\tif (!raw) return undefined;\n\tconst n = Number(raw);\n\treturn Number.isFinite(n) && n > 0 ? n : undefined;\n}\n\n/** Resolve effective options from explicit overrides, then env, then defaults. */\nexport function resolveBrowsertoolsOptions(options?: BrowsertoolsToolOptions): Required<\n\tPick<BrowsertoolsToolOptions, \"idleTimeoutMs\" | \"maxParentRounds\" | \"requestTimeoutMs\">\n> & {\n\tbinaryPath?: string;\n\tbrowserPath?: string;\n\tserveArgs: string[];\n} {\n\treturn {\n\t\tbinaryPath: options?.binaryPath,\n\t\tbrowserPath: options?.browserPath ?? (process.env.HOOCODE_BROWSERTOOLS_BROWSER_PATH?.trim() || undefined),\n\t\tidleTimeoutMs:\n\t\t\toptions?.idleTimeoutMs ?? envNumber(\"HOOCODE_BROWSERTOOLS_IDLE_MS\") ?? BROWSERTOOLS_DEFAULT_IDLE_MS,\n\t\tmaxParentRounds:\n\t\t\toptions?.maxParentRounds ?? envNumber(\"HOOCODE_BROWSERTOOLS_MAX_ROUNDS\") ?? BROWSERTOOLS_DEFAULT_MAX_ROUNDS,\n\t\trequestTimeoutMs:\n\t\t\toptions?.requestTimeoutMs ??\n\t\t\tenvNumber(\"HOOCODE_BROWSERTOOLS_REQUEST_TIMEOUT_MS\") ??\n\t\t\tBROWSERTOOLS_REQUEST_TIMEOUT_MS,\n\t\tserveArgs: options?.serveArgs ?? [],\n\t};\n}\n\nconst BINARY_MISSING_MESSAGE =\n\t\"browsertools binary unavailable and could not be downloaded — the browser tools require the `browsertools` CLI on PATH or a published release for this platform\";\n\n/** Resolve the `browsertools` binary path (explicit override, else download/PATH). */\nexport async function resolveBrowsertoolsBinary(options?: BrowsertoolsToolOptions): Promise<string> {\n\tif (options?.binaryPath) return options.binaryPath;\n\tconst binaryPath = await ensureTool(\"browsertools\", true);\n\tif (!binaryPath) throw new Error(BINARY_MISSING_MESSAGE);\n\treturn binaryPath;\n}\n\n// ============================================================================\n// Persistent JSON-RPC serve client\n// ============================================================================\n\ninterface PendingRequest {\n\tresolve: (value: unknown) => void;\n\treject: (error: Error) => void;\n\ttimer: ReturnType<typeof setTimeout>;\n}\n\n/**\n * A live `browsertools serve` process spoken to over newline-delimited JSON-RPC.\n *\n * Wire format (browsertools `src/serve.rs`):\n * request: `{ \"id\": <n>, \"method\": <string>, \"params\": <object> }\\n`\n * response: `{ \"id\": <n>, \"result\": <value> }` or `{ \"id\": <n>, \"error\": <RpcError> }`\\n\n *\n * Responses are correlated by `id`, so concurrent requests are safe, though the\n * tools drive it sequentially.\n */\nexport class BrowsertoolsServeClient {\n\tprivate readonly proc: ChildProcess;\n\tprivate readonly pending = new Map<number, PendingRequest>();\n\tprivate readonly requestTimeoutMs: number;\n\tprivate buffer = \"\";\n\tprivate nextId = 1;\n\tprivate closed = false;\n\tprivate exitError: Error | undefined;\n\tprivate stderrTail = \"\";\n\n\tconstructor(\n\t\tbinaryPath: string,\n\t\topts: { cwd?: string; browserPath?: string; serveArgs: string[]; requestTimeoutMs: number },\n\t) {\n\t\tthis.requestTimeoutMs = opts.requestTimeoutMs;\n\t\t// The exact CLI flag for the browser path is not part of the locked RPC\n\t\t// contract; v0.1.3 added a \"configurable browser path\". We forward it via\n\t\t// env vars the binary is likely to read. Passing extra env is harmless if\n\t\t// the binary ignores them, and centralizing it here makes it a one-line\n\t\t// change once the exact mechanism is confirmed.\n\t\tconst env = { ...process.env };\n\t\tif (opts.browserPath) {\n\t\t\tenv.BROWSERTOOLS_BROWSER_PATH = opts.browserPath;\n\t\t\tenv.CHROME_PATH = opts.browserPath;\n\t\t}\n\t\t// Run the serve process in the project cwd so relative flow paths and the\n\t\t// evidence store resolve the same way the user expects.\n\t\tthis.proc = spawn(binaryPath, [\"serve\", ...opts.serveArgs], {\n\t\t\tcwd: opts.cwd,\n\t\t\tshell: false,\n\t\t\tstdio: [\"pipe\", \"pipe\", \"pipe\"],\n\t\t\tenv,\n\t\t});\n\t\tthis.proc.stdout?.setEncoding(\"utf8\");\n\t\tthis.proc.stdout?.on(\"data\", (chunk: string) => this.onStdout(chunk));\n\t\tthis.proc.stderr?.setEncoding(\"utf8\");\n\t\tthis.proc.stderr?.on(\"data\", (chunk: string) => {\n\t\t\t// Keep only a bounded tail so a chatty serve can't grow unbounded; it is\n\t\t\t// surfaced in the error when the process dies unexpectedly.\n\t\t\tthis.stderrTail = (this.stderrTail + chunk).slice(-4000);\n\t\t});\n\t\tthis.proc.on(\"error\", (err) => this.fail(err instanceof Error ? err : new Error(String(err))));\n\t\tthis.proc.on(\"exit\", (code, sig) => {\n\t\t\tif (this.closed) return;\n\t\t\tconst detail = this.stderrTail.trim();\n\t\t\tthis.fail(\n\t\t\t\tnew Error(\n\t\t\t\t\t`browsertools serve exited unexpectedly (code=${code ?? \"null\"}, signal=${sig ?? \"null\"})` +\n\t\t\t\t\t\t(detail ? `: ${detail}` : \"\"),\n\t\t\t\t),\n\t\t\t);\n\t\t});\n\t}\n\n\tprivate onStdout(chunk: string): void {\n\t\tthis.buffer += chunk;\n\t\tlet newlineIndex = this.buffer.indexOf(\"\\n\");\n\t\twhile (newlineIndex !== -1) {\n\t\t\tconst line = this.buffer.slice(0, newlineIndex).trim();\n\t\t\tthis.buffer = this.buffer.slice(newlineIndex + 1);\n\t\t\tif (line) this.onLine(line);\n\t\t\tnewlineIndex = this.buffer.indexOf(\"\\n\");\n\t\t}\n\t}\n\n\tprivate onLine(line: string): void {\n\t\tlet message: { id?: unknown; result?: unknown; error?: unknown };\n\t\ttry {\n\t\t\tmessage = JSON.parse(line);\n\t\t} catch {\n\t\t\t// Non-JSON noise on stdout (shouldn't happen; logs go to stderr). Skip it\n\t\t\t// rather than corrupt id correlation.\n\t\t\treturn;\n\t\t}\n\t\tconst id = typeof message.id === \"number\" ? message.id : undefined;\n\t\tif (id === undefined) return;\n\t\tconst entry = this.pending.get(id);\n\t\tif (!entry) return;\n\t\tthis.pending.delete(id);\n\t\tclearTimeout(entry.timer);\n\t\tif (message.error !== undefined && message.error !== null) {\n\t\t\tentry.reject(new Error(`browsertools ${describeRpcError(message.error)}`));\n\t\t} else {\n\t\t\tentry.resolve(message.result);\n\t\t}\n\t}\n\n\t/** Reject all in-flight requests and mark the client dead. Idempotent. */\n\tprivate fail(error: Error): void {\n\t\tif (this.closed) return;\n\t\tthis.exitError = error;\n\t\tfor (const [, entry] of this.pending) {\n\t\t\tclearTimeout(entry.timer);\n\t\t\tentry.reject(error);\n\t\t}\n\t\tthis.pending.clear();\n\t}\n\n\t/** Send a JSON-RPC request and resolve with its `result` (or reject on `error`). */\n\trequest<T = unknown>(method: string, params: Record<string, unknown>): Promise<T> {\n\t\tif (this.closed) {\n\t\t\treturn Promise.reject(this.exitError ?? new Error(\"browsertools serve client is closed\"));\n\t\t}\n\t\tconst id = this.nextId++;\n\t\treturn new Promise<T>((resolve, reject) => {\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tif (this.pending.delete(id)) {\n\t\t\t\t\treject(new Error(`browsertools ${method} timed out after ${this.requestTimeoutMs}ms`));\n\t\t\t\t}\n\t\t\t}, this.requestTimeoutMs);\n\t\t\tthis.pending.set(id, { resolve: resolve as (v: unknown) => void, reject, timer });\n\t\t\tconst payload = `${JSON.stringify({ id, method, params })}\\n`;\n\t\t\tthis.proc.stdin?.write(payload, (err) => {\n\t\t\t\tif (err && this.pending.delete(id)) {\n\t\t\t\t\tclearTimeout(timer);\n\t\t\t\t\treject(err);\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t}\n\n\t/** Kill the serve process and reject any outstanding requests. Idempotent. */\n\tdispose(): void {\n\t\tif (this.closed) return;\n\t\tthis.fail(new Error(\"browsertools serve client disposed\"));\n\t\tthis.closed = true;\n\t\t// Best-effort graceful shutdown, then ensure the process is gone.\n\t\ttry {\n\t\t\tthis.proc.stdin?.write(`${JSON.stringify({ id: this.nextId++, method: \"shutdown\", params: {} })}\\n`);\n\t\t} catch {\n\t\t\t// stdin may already be closed; fall through to kill.\n\t\t}\n\t\tthis.proc.stdin?.end();\n\t\tthis.proc.kill(\"SIGTERM\");\n\t\tconst proc = this.proc;\n\t\tsetTimeout(() => {\n\t\t\tif (proc.exitCode === null && proc.signalCode === null) proc.kill(\"SIGKILL\");\n\t\t}, 3000).unref?.();\n\t}\n}\n\nfunction describeRpcError(error: unknown): string {\n\tif (error && typeof error === \"object\") {\n\t\tconst message = (error as { message?: unknown }).message;\n\t\tif (typeof message === \"string\" && message) return message;\n\t}\n\tif (typeof error === \"string\" && error) return error;\n\treturn `RPC error: ${JSON.stringify(error)}`;\n}\n\n// ============================================================================\n// Paused-session registry (process-wide, keyed by ResumeToken)\n// ============================================================================\n\ninterface PausedSession {\n\tclient: BrowsertoolsServeClient;\n\t/** Number of NeedsParent rounds yielded so far for this flow. */\n\trounds: number;\n\tidleTimer: ReturnType<typeof setTimeout>;\n\tidleTimeoutMs: number;\n}\n\nconst sessionRegistry = new Map<ResumeToken, PausedSession>();\n\n/**\n * Park a paused flow's live serve client under its resume token and arm the idle\n * reaper. Any previous entry for the token is disposed first.\n */\nexport function parkSession(\n\ttoken: ResumeToken,\n\tclient: BrowsertoolsServeClient,\n\trounds: number,\n\tidleTimeoutMs: number,\n): void {\n\tconst existing = sessionRegistry.get(token);\n\tif (existing && existing.client !== client) {\n\t\tclearTimeout(existing.idleTimer);\n\t\texisting.client.dispose();\n\t}\n\tconst idleTimer = setTimeout(() => {\n\t\tconst session = sessionRegistry.get(token);\n\t\tif (session) {\n\t\t\tsessionRegistry.delete(token);\n\t\t\tsession.client.dispose();\n\t\t}\n\t}, idleTimeoutMs);\n\tidleTimer.unref?.();\n\tsessionRegistry.set(token, { client, rounds, idleTimer, idleTimeoutMs });\n}\n\n/**\n * Remove a paused session from the registry and return it, stopping its idle\n * timer. The caller now owns the client (to resume or dispose). Returns\n * undefined if the token is unknown (expired/reaped/already completed).\n */\nexport function takeSession(token: ResumeToken): PausedSession | undefined {\n\tconst session = sessionRegistry.get(token);\n\tif (!session) return undefined;\n\tclearTimeout(session.idleTimer);\n\tsessionRegistry.delete(token);\n\treturn session;\n}\n\n/** Dispose every parked session. Used on shutdown and in tests. */\nexport function disposeAllSessions(): void {\n\tfor (const [, session] of sessionRegistry) {\n\t\tclearTimeout(session.idleTimer);\n\t\tsession.client.dispose();\n\t}\n\tsessionRegistry.clear();\n}\n\n/** Number of currently parked sessions (for tests/diagnostics). */\nexport function pausedSessionCount(): number {\n\treturn sessionRegistry.size;\n}\n"]}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared plumbing for the `browser_flow` and `browser_resume` tools.
|
|
3
|
+
*
|
|
4
|
+
* Unlike `webtools` (one-shot fetch/search subprocesses), `browsertools` drives
|
|
5
|
+
* a *stateful* parent-in-the-loop protocol: a single long-lived `browsertools
|
|
6
|
+
* serve` process holds the live Chromium and the paused-flow state in memory,
|
|
7
|
+
* and the parent (hoocode) talks to it over newline-delimited JSON-RPC on
|
|
8
|
+
* stdin/stdout. A flow can suspend mid-replay with `Outcome::NeedsParent`,
|
|
9
|
+
* yielding a typed `ParentRequest` + `ResumeToken`; the parent reasons about it
|
|
10
|
+
* (often by looking at a screenshot) and calls `flow_resume(token, response)` to
|
|
11
|
+
* continue. See https://github.com/kolisachint/browsertools (contract.rs).
|
|
12
|
+
*
|
|
13
|
+
* Because that single serve process must survive *between* the `browser_flow`
|
|
14
|
+
* call that starts the flow and the `browser_resume` call that answers a
|
|
15
|
+
* `NeedsParent`, this module owns a process-wide {@link sessionRegistry} keyed by
|
|
16
|
+
* `ResumeToken`. Each paused session keeps its serve client alive and is reaped
|
|
17
|
+
* after an idle timeout so an abandoned flow never leaks a Chromium forever.
|
|
18
|
+
*
|
|
19
|
+
* This module owns:
|
|
20
|
+
* - the persistent JSON-RPC serve client (id-correlated request/response),
|
|
21
|
+
* - the locked contract types (mirrors browsertools `src/contract.rs`),
|
|
22
|
+
* - the paused-session registry + lifecycle (idle reap, round cap, teardown),
|
|
23
|
+
* - binary + browser-path resolution.
|
|
24
|
+
*/
|
|
25
|
+
import { spawn } from "node:child_process";
|
|
26
|
+
import { ensureTool } from "../../utils/tools-manager.js";
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// Configuration
|
|
29
|
+
// ============================================================================
|
|
30
|
+
/** Default per-request RPC timeout. Flow steps drive a real browser, so this is
|
|
31
|
+
* generous relative to webtools' 15s. */
|
|
32
|
+
export const BROWSERTOOLS_REQUEST_TIMEOUT_MS = 120_000;
|
|
33
|
+
/** Default idle window a paused flow may sit in the registry before it is reaped
|
|
34
|
+
* (killing its serve process + Chromium). Reset on every resume round. The
|
|
35
|
+
* parent may reason / look at a screenshot / make other tool calls between
|
|
36
|
+
* rounds, so this is minutes, not seconds. */
|
|
37
|
+
export const BROWSERTOOLS_DEFAULT_IDLE_MS = 5 * 60 * 1000;
|
|
38
|
+
/** Default hard cap on `NeedsParent` rounds for a single flow, bounding a runaway
|
|
39
|
+
* suspend/resume loop. */
|
|
40
|
+
export const BROWSERTOOLS_DEFAULT_MAX_ROUNDS = 12;
|
|
41
|
+
function envNumber(name) {
|
|
42
|
+
const raw = process.env[name];
|
|
43
|
+
if (!raw)
|
|
44
|
+
return undefined;
|
|
45
|
+
const n = Number(raw);
|
|
46
|
+
return Number.isFinite(n) && n > 0 ? n : undefined;
|
|
47
|
+
}
|
|
48
|
+
/** Resolve effective options from explicit overrides, then env, then defaults. */
|
|
49
|
+
export function resolveBrowsertoolsOptions(options) {
|
|
50
|
+
return {
|
|
51
|
+
binaryPath: options?.binaryPath,
|
|
52
|
+
browserPath: options?.browserPath ?? (process.env.HOOCODE_BROWSERTOOLS_BROWSER_PATH?.trim() || undefined),
|
|
53
|
+
idleTimeoutMs: options?.idleTimeoutMs ?? envNumber("HOOCODE_BROWSERTOOLS_IDLE_MS") ?? BROWSERTOOLS_DEFAULT_IDLE_MS,
|
|
54
|
+
maxParentRounds: options?.maxParentRounds ?? envNumber("HOOCODE_BROWSERTOOLS_MAX_ROUNDS") ?? BROWSERTOOLS_DEFAULT_MAX_ROUNDS,
|
|
55
|
+
requestTimeoutMs: options?.requestTimeoutMs ??
|
|
56
|
+
envNumber("HOOCODE_BROWSERTOOLS_REQUEST_TIMEOUT_MS") ??
|
|
57
|
+
BROWSERTOOLS_REQUEST_TIMEOUT_MS,
|
|
58
|
+
serveArgs: options?.serveArgs ?? [],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const BINARY_MISSING_MESSAGE = "browsertools binary unavailable and could not be downloaded — the browser tools require the `browsertools` CLI on PATH or a published release for this platform";
|
|
62
|
+
/** Resolve the `browsertools` binary path (explicit override, else download/PATH). */
|
|
63
|
+
export async function resolveBrowsertoolsBinary(options) {
|
|
64
|
+
if (options?.binaryPath)
|
|
65
|
+
return options.binaryPath;
|
|
66
|
+
const binaryPath = await ensureTool("browsertools", true);
|
|
67
|
+
if (!binaryPath)
|
|
68
|
+
throw new Error(BINARY_MISSING_MESSAGE);
|
|
69
|
+
return binaryPath;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* A live `browsertools serve` process spoken to over newline-delimited JSON-RPC.
|
|
73
|
+
*
|
|
74
|
+
* Wire format (browsertools `src/serve.rs`):
|
|
75
|
+
* request: `{ "id": <n>, "method": <string>, "params": <object> }\n`
|
|
76
|
+
* response: `{ "id": <n>, "result": <value> }` or `{ "id": <n>, "error": <RpcError> }`\n
|
|
77
|
+
*
|
|
78
|
+
* Responses are correlated by `id`, so concurrent requests are safe, though the
|
|
79
|
+
* tools drive it sequentially.
|
|
80
|
+
*/
|
|
81
|
+
export class BrowsertoolsServeClient {
|
|
82
|
+
proc;
|
|
83
|
+
pending = new Map();
|
|
84
|
+
requestTimeoutMs;
|
|
85
|
+
buffer = "";
|
|
86
|
+
nextId = 1;
|
|
87
|
+
closed = false;
|
|
88
|
+
exitError;
|
|
89
|
+
stderrTail = "";
|
|
90
|
+
constructor(binaryPath, opts) {
|
|
91
|
+
this.requestTimeoutMs = opts.requestTimeoutMs;
|
|
92
|
+
// The exact CLI flag for the browser path is not part of the locked RPC
|
|
93
|
+
// contract; v0.1.3 added a "configurable browser path". We forward it via
|
|
94
|
+
// env vars the binary is likely to read. Passing extra env is harmless if
|
|
95
|
+
// the binary ignores them, and centralizing it here makes it a one-line
|
|
96
|
+
// change once the exact mechanism is confirmed.
|
|
97
|
+
const env = { ...process.env };
|
|
98
|
+
if (opts.browserPath) {
|
|
99
|
+
env.BROWSERTOOLS_BROWSER_PATH = opts.browserPath;
|
|
100
|
+
env.CHROME_PATH = opts.browserPath;
|
|
101
|
+
}
|
|
102
|
+
// Run the serve process in the project cwd so relative flow paths and the
|
|
103
|
+
// evidence store resolve the same way the user expects.
|
|
104
|
+
this.proc = spawn(binaryPath, ["serve", ...opts.serveArgs], {
|
|
105
|
+
cwd: opts.cwd,
|
|
106
|
+
shell: false,
|
|
107
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
108
|
+
env,
|
|
109
|
+
});
|
|
110
|
+
this.proc.stdout?.setEncoding("utf8");
|
|
111
|
+
this.proc.stdout?.on("data", (chunk) => this.onStdout(chunk));
|
|
112
|
+
this.proc.stderr?.setEncoding("utf8");
|
|
113
|
+
this.proc.stderr?.on("data", (chunk) => {
|
|
114
|
+
// Keep only a bounded tail so a chatty serve can't grow unbounded; it is
|
|
115
|
+
// surfaced in the error when the process dies unexpectedly.
|
|
116
|
+
this.stderrTail = (this.stderrTail + chunk).slice(-4000);
|
|
117
|
+
});
|
|
118
|
+
this.proc.on("error", (err) => this.fail(err instanceof Error ? err : new Error(String(err))));
|
|
119
|
+
this.proc.on("exit", (code, sig) => {
|
|
120
|
+
if (this.closed)
|
|
121
|
+
return;
|
|
122
|
+
const detail = this.stderrTail.trim();
|
|
123
|
+
this.fail(new Error(`browsertools serve exited unexpectedly (code=${code ?? "null"}, signal=${sig ?? "null"})` +
|
|
124
|
+
(detail ? `: ${detail}` : "")));
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
onStdout(chunk) {
|
|
128
|
+
this.buffer += chunk;
|
|
129
|
+
let newlineIndex = this.buffer.indexOf("\n");
|
|
130
|
+
while (newlineIndex !== -1) {
|
|
131
|
+
const line = this.buffer.slice(0, newlineIndex).trim();
|
|
132
|
+
this.buffer = this.buffer.slice(newlineIndex + 1);
|
|
133
|
+
if (line)
|
|
134
|
+
this.onLine(line);
|
|
135
|
+
newlineIndex = this.buffer.indexOf("\n");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
onLine(line) {
|
|
139
|
+
let message;
|
|
140
|
+
try {
|
|
141
|
+
message = JSON.parse(line);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// Non-JSON noise on stdout (shouldn't happen; logs go to stderr). Skip it
|
|
145
|
+
// rather than corrupt id correlation.
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const id = typeof message.id === "number" ? message.id : undefined;
|
|
149
|
+
if (id === undefined)
|
|
150
|
+
return;
|
|
151
|
+
const entry = this.pending.get(id);
|
|
152
|
+
if (!entry)
|
|
153
|
+
return;
|
|
154
|
+
this.pending.delete(id);
|
|
155
|
+
clearTimeout(entry.timer);
|
|
156
|
+
if (message.error !== undefined && message.error !== null) {
|
|
157
|
+
entry.reject(new Error(`browsertools ${describeRpcError(message.error)}`));
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
entry.resolve(message.result);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** Reject all in-flight requests and mark the client dead. Idempotent. */
|
|
164
|
+
fail(error) {
|
|
165
|
+
if (this.closed)
|
|
166
|
+
return;
|
|
167
|
+
this.exitError = error;
|
|
168
|
+
for (const [, entry] of this.pending) {
|
|
169
|
+
clearTimeout(entry.timer);
|
|
170
|
+
entry.reject(error);
|
|
171
|
+
}
|
|
172
|
+
this.pending.clear();
|
|
173
|
+
}
|
|
174
|
+
/** Send a JSON-RPC request and resolve with its `result` (or reject on `error`). */
|
|
175
|
+
request(method, params) {
|
|
176
|
+
if (this.closed) {
|
|
177
|
+
return Promise.reject(this.exitError ?? new Error("browsertools serve client is closed"));
|
|
178
|
+
}
|
|
179
|
+
const id = this.nextId++;
|
|
180
|
+
return new Promise((resolve, reject) => {
|
|
181
|
+
const timer = setTimeout(() => {
|
|
182
|
+
if (this.pending.delete(id)) {
|
|
183
|
+
reject(new Error(`browsertools ${method} timed out after ${this.requestTimeoutMs}ms`));
|
|
184
|
+
}
|
|
185
|
+
}, this.requestTimeoutMs);
|
|
186
|
+
this.pending.set(id, { resolve: resolve, reject, timer });
|
|
187
|
+
const payload = `${JSON.stringify({ id, method, params })}\n`;
|
|
188
|
+
this.proc.stdin?.write(payload, (err) => {
|
|
189
|
+
if (err && this.pending.delete(id)) {
|
|
190
|
+
clearTimeout(timer);
|
|
191
|
+
reject(err);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
/** Kill the serve process and reject any outstanding requests. Idempotent. */
|
|
197
|
+
dispose() {
|
|
198
|
+
if (this.closed)
|
|
199
|
+
return;
|
|
200
|
+
this.fail(new Error("browsertools serve client disposed"));
|
|
201
|
+
this.closed = true;
|
|
202
|
+
// Best-effort graceful shutdown, then ensure the process is gone.
|
|
203
|
+
try {
|
|
204
|
+
this.proc.stdin?.write(`${JSON.stringify({ id: this.nextId++, method: "shutdown", params: {} })}\n`);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
// stdin may already be closed; fall through to kill.
|
|
208
|
+
}
|
|
209
|
+
this.proc.stdin?.end();
|
|
210
|
+
this.proc.kill("SIGTERM");
|
|
211
|
+
const proc = this.proc;
|
|
212
|
+
setTimeout(() => {
|
|
213
|
+
if (proc.exitCode === null && proc.signalCode === null)
|
|
214
|
+
proc.kill("SIGKILL");
|
|
215
|
+
}, 3000).unref?.();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function describeRpcError(error) {
|
|
219
|
+
if (error && typeof error === "object") {
|
|
220
|
+
const message = error.message;
|
|
221
|
+
if (typeof message === "string" && message)
|
|
222
|
+
return message;
|
|
223
|
+
}
|
|
224
|
+
if (typeof error === "string" && error)
|
|
225
|
+
return error;
|
|
226
|
+
return `RPC error: ${JSON.stringify(error)}`;
|
|
227
|
+
}
|
|
228
|
+
const sessionRegistry = new Map();
|
|
229
|
+
/**
|
|
230
|
+
* Park a paused flow's live serve client under its resume token and arm the idle
|
|
231
|
+
* reaper. Any previous entry for the token is disposed first.
|
|
232
|
+
*/
|
|
233
|
+
export function parkSession(token, client, rounds, idleTimeoutMs) {
|
|
234
|
+
const existing = sessionRegistry.get(token);
|
|
235
|
+
if (existing && existing.client !== client) {
|
|
236
|
+
clearTimeout(existing.idleTimer);
|
|
237
|
+
existing.client.dispose();
|
|
238
|
+
}
|
|
239
|
+
const idleTimer = setTimeout(() => {
|
|
240
|
+
const session = sessionRegistry.get(token);
|
|
241
|
+
if (session) {
|
|
242
|
+
sessionRegistry.delete(token);
|
|
243
|
+
session.client.dispose();
|
|
244
|
+
}
|
|
245
|
+
}, idleTimeoutMs);
|
|
246
|
+
idleTimer.unref?.();
|
|
247
|
+
sessionRegistry.set(token, { client, rounds, idleTimer, idleTimeoutMs });
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Remove a paused session from the registry and return it, stopping its idle
|
|
251
|
+
* timer. The caller now owns the client (to resume or dispose). Returns
|
|
252
|
+
* undefined if the token is unknown (expired/reaped/already completed).
|
|
253
|
+
*/
|
|
254
|
+
export function takeSession(token) {
|
|
255
|
+
const session = sessionRegistry.get(token);
|
|
256
|
+
if (!session)
|
|
257
|
+
return undefined;
|
|
258
|
+
clearTimeout(session.idleTimer);
|
|
259
|
+
sessionRegistry.delete(token);
|
|
260
|
+
return session;
|
|
261
|
+
}
|
|
262
|
+
/** Dispose every parked session. Used on shutdown and in tests. */
|
|
263
|
+
export function disposeAllSessions() {
|
|
264
|
+
for (const [, session] of sessionRegistry) {
|
|
265
|
+
clearTimeout(session.idleTimer);
|
|
266
|
+
session.client.dispose();
|
|
267
|
+
}
|
|
268
|
+
sessionRegistry.clear();
|
|
269
|
+
}
|
|
270
|
+
/** Number of currently parked sessions (for tests/diagnostics). */
|
|
271
|
+
export function pausedSessionCount() {
|
|
272
|
+
return sessionRegistry.size;
|
|
273
|
+
}
|
|
274
|
+
//# sourceMappingURL=browsertools-shared.js.map
|