@kolisachint/hoocode-agent 0.4.106 → 0.4.107
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 +2 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +4 -4
- package/dist/cli/args.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +0 -1
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -2
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +1 -3
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +2 -2
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +1 -1
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/extension-mcp-servers.d.ts +1 -1
- package/dist/core/extension-mcp-servers.d.ts.map +1 -1
- package/dist/core/extension-mcp-servers.js +1 -1
- package/dist/core/extension-mcp-servers.js.map +1 -1
- package/dist/core/extensions/plugins/index.d.ts.map +1 -1
- package/dist/core/extensions/plugins/index.js +1 -1
- package/dist/core/extensions/plugins/index.js.map +1 -1
- package/dist/core/extensions/types.d.ts +1 -3
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/mode-prompts.d.ts +1 -1
- package/dist/core/mode-prompts.d.ts.map +1 -1
- package/dist/core/mode-prompts.js +1 -1
- package/dist/core/mode-prompts.js.map +1 -1
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +1 -1
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +1 -2
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts +2 -97
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +4 -65
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +2 -3
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/telemetry.d.ts.map +1 -1
- package/dist/core/telemetry.js +1 -1
- package/dist/core/telemetry.js.map +1 -1
- package/dist/core/timings.d.ts +1 -1
- package/dist/core/timings.d.ts.map +1 -1
- package/dist/core/timings.js +2 -2
- package/dist/core/timings.js.map +1 -1
- package/dist/core/tools/{browser-continue.d.ts → browser/browser-continue.d.ts} +1 -1
- package/dist/core/tools/browser/browser-continue.d.ts.map +1 -0
- package/dist/core/tools/{browser-continue.js → browser/browser-continue.js} +2 -2
- package/dist/core/tools/browser/browser-continue.js.map +1 -0
- package/dist/core/tools/{browser-run.d.ts → browser/browser-run.d.ts} +2 -2
- package/dist/core/tools/browser/browser-run.d.ts.map +1 -0
- package/dist/core/tools/{browser-run.js → browser/browser-run.js} +2 -2
- package/dist/core/tools/browser/browser-run.js.map +1 -0
- package/dist/core/tools/browser/browsertools-shared.d.ts.map +1 -0
- package/dist/core/tools/{browsertools-shared.js → browser/browsertools-shared.js} +1 -1
- package/dist/core/tools/browser/browsertools-shared.js.map +1 -0
- package/dist/core/tools/browser/index.d.ts +8 -0
- package/dist/core/tools/browser/index.d.ts.map +1 -0
- package/dist/core/tools/browser/index.js +8 -0
- package/dist/core/tools/browser/index.js.map +1 -0
- package/dist/core/tools/{docedit.d.ts → doc/docedit.d.ts} +2 -2
- package/dist/core/tools/doc/docedit.d.ts.map +1 -0
- package/dist/core/tools/{docedit.js → doc/docedit.js} +7 -7
- package/dist/core/tools/doc/docedit.js.map +1 -0
- package/dist/core/tools/{docgrep.d.ts → doc/docgrep.d.ts} +2 -2
- package/dist/core/tools/doc/docgrep.d.ts.map +1 -0
- package/dist/core/tools/{docgrep.js → doc/docgrep.js} +6 -6
- package/dist/core/tools/doc/docgrep.js.map +1 -0
- package/dist/core/tools/{docpeek.d.ts → doc/docpeek.d.ts} +2 -2
- package/dist/core/tools/{docpeek.js.map → doc/docpeek.d.ts.map} +1 -1
- package/dist/core/tools/{docpeek.js → doc/docpeek.js} +6 -6
- package/dist/core/tools/doc/docpeek.js.map +1 -0
- package/dist/core/tools/{docread.d.ts → doc/docread.d.ts} +2 -2
- package/dist/core/tools/doc/docread.d.ts.map +1 -0
- package/dist/core/tools/{docread.js → doc/docread.js} +6 -6
- package/dist/core/tools/doc/docread.js.map +1 -0
- package/dist/core/tools/{docscan.d.ts → doc/docscan.d.ts} +2 -2
- package/dist/core/tools/doc/docscan.d.ts.map +1 -0
- package/dist/core/tools/{docscan.js → doc/docscan.js} +6 -6
- package/dist/core/tools/doc/docscan.js.map +1 -0
- package/dist/core/tools/{docwrite.d.ts → doc/docwrite.d.ts} +1 -1
- package/dist/core/tools/doc/docwrite.d.ts.map +1 -0
- package/dist/core/tools/{docwrite.js → doc/docwrite.js} +6 -6
- package/dist/core/tools/doc/docwrite.js.map +1 -0
- package/dist/core/tools/doc/index.d.ts +11 -0
- package/dist/core/tools/doc/index.d.ts.map +1 -0
- package/dist/core/tools/doc/index.js +11 -0
- package/dist/core/tools/doc/index.js.map +1 -0
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +1 -1
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/index.d.ts +42 -28
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +61 -199
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/output-accumulator.d.ts.map +1 -1
- package/dist/core/tools/output-accumulator.js +1 -1
- package/dist/core/tools/output-accumulator.js.map +1 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +1 -1
- package/dist/core/tools/read.js.map +1 -1
- package/dist/extensions/core/ask-options.d.ts +10 -0
- package/dist/extensions/core/ask-options.d.ts.map +1 -0
- package/dist/extensions/core/ask-options.js +83 -0
- package/dist/extensions/core/ask-options.js.map +1 -0
- package/dist/extensions/core/config.d.ts +80 -0
- package/dist/extensions/core/config.d.ts.map +1 -0
- package/dist/extensions/core/config.js +110 -0
- package/dist/extensions/core/config.js.map +1 -0
- package/dist/extensions/core/cost.d.ts +11 -0
- package/dist/extensions/core/cost.d.ts.map +1 -0
- package/dist/extensions/core/cost.js +64 -0
- package/dist/extensions/core/cost.js.map +1 -0
- package/dist/extensions/core/hoo-core.d.ts +16 -179
- package/dist/extensions/core/hoo-core.d.ts.map +1 -1
- package/dist/extensions/core/hoo-core.js +27 -1663
- package/dist/extensions/core/hoo-core.js.map +1 -1
- package/dist/extensions/core/loop.d.ts +15 -0
- package/dist/extensions/core/loop.d.ts.map +1 -0
- package/dist/extensions/core/loop.js +235 -0
- package/dist/extensions/core/loop.js.map +1 -0
- package/dist/extensions/core/marketplace.d.ts +17 -0
- package/dist/extensions/core/marketplace.d.ts.map +1 -0
- package/dist/extensions/core/marketplace.js +167 -0
- package/dist/extensions/core/marketplace.js.map +1 -0
- package/dist/extensions/core/mcp-loader.d.ts +26 -0
- package/dist/extensions/core/mcp-loader.d.ts.map +1 -0
- package/dist/extensions/core/mcp-loader.js +391 -0
- package/dist/extensions/core/mcp-loader.js.map +1 -0
- package/dist/extensions/core/modes.d.ts +56 -0
- package/dist/extensions/core/modes.d.ts.map +1 -0
- package/dist/extensions/core/modes.js +268 -0
- package/dist/extensions/core/modes.js.map +1 -0
- package/dist/extensions/core/permission-gate.d.ts +10 -0
- package/dist/extensions/core/permission-gate.d.ts.map +1 -0
- package/dist/extensions/core/permission-gate.js +167 -0
- package/dist/extensions/core/permission-gate.js.map +1 -0
- package/dist/extensions/core/scaffold.d.ts +9 -0
- package/dist/extensions/core/scaffold.d.ts.map +1 -0
- package/dist/extensions/core/scaffold.js +146 -0
- package/dist/extensions/core/scaffold.js.map +1 -0
- package/dist/extensions/core/thinking-escalation.d.ts +18 -0
- package/dist/extensions/core/thinking-escalation.d.ts.map +1 -0
- package/dist/extensions/core/thinking-escalation.js +70 -0
- package/dist/extensions/core/thinking-escalation.js.map +1 -0
- package/dist/index.d.ts +2 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -4
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +2 -2
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/command-executor.d.ts +0 -5
- package/dist/modes/interactive/command-executor.d.ts.map +1 -1
- package/dist/modes/interactive/command-executor.js +0 -7
- package/dist/modes/interactive/command-executor.js.map +1 -1
- package/dist/modes/interactive/components/branch-summary-message.d.ts +1 -1
- package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.d.ts +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
- package/dist/modes/interactive/components/custom-message.d.ts +1 -1
- package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-message.js.map +1 -1
- package/dist/modes/interactive/components/index.d.ts +0 -2
- package/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/dist/modes/interactive/components/index.js +0 -2
- package/dist/modes/interactive/components/index.js.map +1 -1
- package/dist/modes/interactive/extension-chrome.d.ts +56 -0
- package/dist/modes/interactive/extension-chrome.d.ts.map +1 -0
- package/dist/modes/interactive/extension-chrome.js +172 -0
- package/dist/modes/interactive/extension-chrome.js.map +1 -0
- package/dist/modes/interactive/extension-dialogs.d.ts +82 -0
- package/dist/modes/interactive/extension-dialogs.d.ts.map +1 -0
- package/dist/modes/interactive/extension-dialogs.js +259 -0
- package/dist/modes/interactive/extension-dialogs.js.map +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts +11 -161
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +261 -1584
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/resource-display.d.ts +59 -0
- package/dist/modes/interactive/resource-display.d.ts.map +1 -0
- package/dist/modes/interactive/resource-display.js +499 -0
- package/dist/modes/interactive/resource-display.js.map +1 -0
- package/dist/modes/interactive/startup-checks.d.ts +17 -0
- package/dist/modes/interactive/startup-checks.d.ts.map +1 -0
- package/dist/modes/interactive/startup-checks.js +102 -0
- package/dist/modes/interactive/startup-checks.js.map +1 -0
- package/dist/modes/interactive/team-focus.d.ts +56 -0
- package/dist/modes/interactive/team-focus.d.ts.map +1 -0
- package/dist/modes/interactive/team-focus.js +167 -0
- package/dist/modes/interactive/team-focus.js.map +1 -0
- package/dist/modes/interactive/voice/voice-controller.d.ts +66 -0
- package/dist/modes/interactive/voice/voice-controller.d.ts.map +1 -0
- package/dist/modes/interactive/voice/voice-controller.js +312 -0
- package/dist/modes/interactive/voice/voice-controller.js.map +1 -0
- package/dist/modes/interactive/voice/voice-panel.d.ts.map +1 -0
- package/dist/modes/interactive/voice/voice-panel.js.map +1 -0
- package/dist/modes/interactive/voice/voice-transcribe.d.ts.map +1 -0
- package/dist/modes/interactive/voice/voice-transcribe.js.map +1 -0
- package/dist/modes/rpc/rpc-client.d.ts +1 -2
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +1 -2
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/resources-cli.d.ts.map +1 -1
- package/dist/resources-cli.js +1 -1
- package/dist/resources-cli.js.map +1 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +1 -1
- package/dist/utils/tools-manager.js.map +1 -1
- package/dist/utils/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +2 -2
- package/dist/utils/version-check.js.map +1 -1
- package/docs/project-local-resources.md +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
- package/dist/core/compaction/branch-summarization.d.ts +0 -88
- package/dist/core/compaction/branch-summarization.d.ts.map +0 -1
- package/dist/core/compaction/branch-summarization.js +0 -243
- package/dist/core/compaction/branch-summarization.js.map +0 -1
- package/dist/core/compaction/compaction.d.ts +0 -123
- package/dist/core/compaction/compaction.d.ts.map +0 -1
- package/dist/core/compaction/compaction.js +0 -635
- package/dist/core/compaction/compaction.js.map +0 -1
- package/dist/core/compaction/index.d.ts +0 -7
- package/dist/core/compaction/index.d.ts.map +0 -1
- package/dist/core/compaction/index.js +0 -7
- package/dist/core/compaction/index.js.map +0 -1
- package/dist/core/compaction/utils.d.ts +0 -38
- package/dist/core/compaction/utils.d.ts.map +0 -1
- package/dist/core/compaction/utils.js +0 -156
- package/dist/core/compaction/utils.js.map +0 -1
- package/dist/core/messages.d.ts +0 -124
- package/dist/core/messages.d.ts.map +0 -1
- package/dist/core/messages.js +0 -221
- package/dist/core/messages.js.map +0 -1
- package/dist/core/tools/browser-continue.d.ts.map +0 -1
- package/dist/core/tools/browser-continue.js.map +0 -1
- package/dist/core/tools/browser-run.d.ts.map +0 -1
- package/dist/core/tools/browser-run.js.map +0 -1
- package/dist/core/tools/browsertools-shared.d.ts.map +0 -1
- package/dist/core/tools/browsertools-shared.js.map +0 -1
- package/dist/core/tools/docedit.d.ts.map +0 -1
- package/dist/core/tools/docedit.js.map +0 -1
- package/dist/core/tools/docgrep.d.ts.map +0 -1
- package/dist/core/tools/docgrep.js.map +0 -1
- package/dist/core/tools/docpeek.d.ts.map +0 -1
- package/dist/core/tools/docread.d.ts.map +0 -1
- package/dist/core/tools/docread.js.map +0 -1
- package/dist/core/tools/docscan.d.ts.map +0 -1
- package/dist/core/tools/docscan.js.map +0 -1
- package/dist/core/tools/docwrite.d.ts.map +0 -1
- package/dist/core/tools/docwrite.js.map +0 -1
- package/dist/core/tools/output-compression.d.ts +0 -53
- package/dist/core/tools/output-compression.d.ts.map +0 -1
- package/dist/core/tools/output-compression.js +0 -392
- package/dist/core/tools/output-compression.js.map +0 -1
- package/dist/modes/interactive/components/armin.d.ts +0 -34
- package/dist/modes/interactive/components/armin.d.ts.map +0 -1
- package/dist/modes/interactive/components/armin.js +0 -333
- package/dist/modes/interactive/components/armin.js.map +0 -1
- package/dist/modes/interactive/components/daxnuts.d.ts +0 -23
- package/dist/modes/interactive/components/daxnuts.d.ts.map +0 -1
- package/dist/modes/interactive/components/daxnuts.js +0 -140
- package/dist/modes/interactive/components/daxnuts.js.map +0 -1
- package/dist/modes/interactive/components/voice-panel.d.ts.map +0 -1
- package/dist/modes/interactive/components/voice-panel.js.map +0 -1
- package/dist/modes/interactive/voice-transcribe.d.ts.map +0 -1
- package/dist/modes/interactive/voice-transcribe.js.map +0 -1
- /package/dist/core/tools/{browsertools-shared.d.ts → browser/browsertools-shared.d.ts} +0 -0
- /package/dist/modes/interactive/{components → voice}/voice-panel.d.ts +0 -0
- /package/dist/modes/interactive/{components → voice}/voice-panel.js +0 -0
- /package/dist/modes/interactive/{voice-transcribe.d.ts → voice/voice-transcribe.d.ts} +0 -0
- /package/dist/modes/interactive/{voice-transcribe.js → voice/voice-transcribe.js} +0 -0
package/dist/core/messages.js
DELETED
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Custom message types and transformers for the coding agent.
|
|
3
|
-
*
|
|
4
|
-
* Extends the base AgentMessage type with coding-agent specific message types,
|
|
5
|
-
* and provides a transformer to convert them to LLM-compatible messages.
|
|
6
|
-
*/
|
|
7
|
-
export const COMPACTION_SUMMARY_PREFIX = `The conversation history before this point was compacted into the following summary:
|
|
8
|
-
|
|
9
|
-
<summary>
|
|
10
|
-
`;
|
|
11
|
-
export const COMPACTION_SUMMARY_SUFFIX = `
|
|
12
|
-
</summary>`;
|
|
13
|
-
export const BRANCH_SUMMARY_PREFIX = `The following is a summary of a branch that this conversation came back from:
|
|
14
|
-
|
|
15
|
-
<summary>
|
|
16
|
-
`;
|
|
17
|
-
export const BRANCH_SUMMARY_SUFFIX = `</summary>`;
|
|
18
|
-
/**
|
|
19
|
-
* Convert a BashExecutionMessage to user message text for LLM context.
|
|
20
|
-
*/
|
|
21
|
-
export function bashExecutionToText(msg) {
|
|
22
|
-
let text = `Ran \`${msg.command}\`\n`;
|
|
23
|
-
if (msg.output) {
|
|
24
|
-
text += `\`\`\`\n${msg.output}\n\`\`\``;
|
|
25
|
-
}
|
|
26
|
-
else {
|
|
27
|
-
text += "(no output)";
|
|
28
|
-
}
|
|
29
|
-
if (msg.cancelled) {
|
|
30
|
-
text += "\n\n(command cancelled)";
|
|
31
|
-
}
|
|
32
|
-
else if (msg.exitCode !== null && msg.exitCode !== undefined && msg.exitCode !== 0) {
|
|
33
|
-
text += `\n\nCommand exited with code ${msg.exitCode}`;
|
|
34
|
-
}
|
|
35
|
-
if (msg.truncated && msg.fullOutputPath) {
|
|
36
|
-
text += `\n\n[Output truncated. Full output: ${msg.fullOutputPath}]`;
|
|
37
|
-
}
|
|
38
|
-
return text;
|
|
39
|
-
}
|
|
40
|
-
export function createBranchSummaryMessage(summary, fromId, timestamp) {
|
|
41
|
-
return {
|
|
42
|
-
role: "branchSummary",
|
|
43
|
-
summary,
|
|
44
|
-
fromId,
|
|
45
|
-
timestamp: new Date(timestamp).getTime(),
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
export function createCompactionSummaryMessage(summary, tokensBefore, timestamp, tokensAfter) {
|
|
49
|
-
return {
|
|
50
|
-
role: "compactionSummary",
|
|
51
|
-
summary: summary,
|
|
52
|
-
tokensBefore,
|
|
53
|
-
tokensAfter,
|
|
54
|
-
timestamp: new Date(timestamp).getTime(),
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
/** customType used for the follow-up message a finished background tool injects. */
|
|
58
|
-
export const BACKGROUND_TASK_CUSTOM_TYPE = "backgroundTask";
|
|
59
|
-
/** First non-empty line of a string, trimmed and capped for one-line display. */
|
|
60
|
-
function firstLine(text, max = 120) {
|
|
61
|
-
const line = (text.split("\n").find((l) => l.trim().length > 0) ?? "").trim();
|
|
62
|
-
return line.length > max ? `${line.slice(0, max - 1).trimEnd()}…` : line;
|
|
63
|
-
}
|
|
64
|
-
/** Summarize a tool's arguments as up to three `key: value` pairs for a one-liner. */
|
|
65
|
-
export function summarizeArgs(args) {
|
|
66
|
-
const parts = [];
|
|
67
|
-
for (const [key, value] of Object.entries(args)) {
|
|
68
|
-
if (value === undefined || value === null || value === "")
|
|
69
|
-
continue;
|
|
70
|
-
const rendered = typeof value === "string" ? value : JSON.stringify(value);
|
|
71
|
-
parts.push(`${key}: ${firstLine(rendered, 48)}`);
|
|
72
|
-
if (parts.length === 3)
|
|
73
|
-
break;
|
|
74
|
-
}
|
|
75
|
-
return parts.length > 0 ? parts.join(", ") : undefined;
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Describe a background tool call consistently, so the "started" placeholder, the
|
|
79
|
-
* "finished" follow-up message, and any UI all read the same way. Subagent `Task`
|
|
80
|
-
* calls are keyed off their `subagent_type`; MCP server tools off their `mcp_`
|
|
81
|
-
* prefix.
|
|
82
|
-
*/
|
|
83
|
-
export function describeBackgroundTool(toolCall) {
|
|
84
|
-
const args = toolCall.arguments ?? {};
|
|
85
|
-
const isMcpTool = toolCall.name.startsWith("mcp_");
|
|
86
|
-
if (isMcpTool) {
|
|
87
|
-
// Registered name is `mcp_<server>_<tool>`; drop the prefix for display.
|
|
88
|
-
const pretty = toolCall.name.replace(/^mcp_/, "");
|
|
89
|
-
return { isMcpTool, subagentType: toolCall.name, label: `MCP tool \`${pretty}\``, summary: summarizeArgs(args) };
|
|
90
|
-
}
|
|
91
|
-
const subagentType = typeof args.subagent_type === "string" ? args.subagent_type : toolCall.name;
|
|
92
|
-
const summary = typeof args.description === "string" && args.description.trim()
|
|
93
|
-
? args.description.trim()
|
|
94
|
-
: typeof args.prompt === "string"
|
|
95
|
-
? firstLine(args.prompt)
|
|
96
|
-
: undefined;
|
|
97
|
-
return { isMcpTool, subagentType, label: `subagent \`${subagentType}\``, summary };
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* Verbose, human-readable placeholder shown the moment a background tool is
|
|
101
|
-
* dispatched (before its result is known). Wired into the agent loop via
|
|
102
|
-
* `createBackgroundPlaceholder`, it replaces the generic "Started X in the
|
|
103
|
-
* background" line so the demo legibly explains what each background agent / MCP
|
|
104
|
-
* tool is doing. Kept in sync with the finish message via {@link describeBackgroundTool}.
|
|
105
|
-
*/
|
|
106
|
-
export function createBackgroundPlaceholderText(toolCall) {
|
|
107
|
-
const info = describeBackgroundTool(toolCall);
|
|
108
|
-
const what = info.summary ? ` — ${firstLine(info.summary, 80)}` : "";
|
|
109
|
-
if (info.isMcpTool) {
|
|
110
|
-
return `Started ${info.label} in the background${what}. Its result arrives as a follow-up; keep working.`;
|
|
111
|
-
}
|
|
112
|
-
// One compact line: a verbose multi-line placeholder per dispatch floods the
|
|
113
|
-
// transcript when several subagents spin up at once. The finish notification
|
|
114
|
-
// names the task and the body is pulled with TaskOutput; TaskOutput(list) shows
|
|
115
|
-
// progress meanwhile.
|
|
116
|
-
return `Delegated to ${info.label} in the background${what}. I'll be notified when it finishes; use TaskOutput to check progress or read the result.`;
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* Build the follow-up message injected when a background tool finishes.
|
|
120
|
-
*
|
|
121
|
-
* Handles both subagent `Task` calls and MCP tools, described consistently with
|
|
122
|
-
* the start placeholder via {@link describeBackgroundTool}. Rendered as a distinct
|
|
123
|
-
* custom message rather than a plain user message, and converted to a user message
|
|
124
|
-
* for the LLM.
|
|
125
|
-
*/
|
|
126
|
-
export function createBackgroundTaskMessage(result) {
|
|
127
|
-
const info = describeBackgroundTool(result.toolCall);
|
|
128
|
-
// Subagent `Task`: the tool already returns a compact, self-contained
|
|
129
|
-
// notification (the full body is retained in the inbox for TaskOutput to pull),
|
|
130
|
-
// so the follow-up is exactly that line — no second header, no inlined body.
|
|
131
|
-
if (!info.isMcpTool) {
|
|
132
|
-
return {
|
|
133
|
-
role: "custom",
|
|
134
|
-
customType: BACKGROUND_TASK_CUSTOM_TYPE,
|
|
135
|
-
content: result.result.content,
|
|
136
|
-
display: true,
|
|
137
|
-
details: { subagentType: info.subagentType, isMcpTool: false, isError: result.isError },
|
|
138
|
-
timestamp: Date.now(),
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
// MCP background tool: no inbox, so deliver the header + full result body.
|
|
142
|
-
const verb = result.isError ? "failed" : "finished";
|
|
143
|
-
const summary = info.summary ? ` (${info.summary})` : "";
|
|
144
|
-
const header = `Background ${info.label}${summary} ${verb}:`;
|
|
145
|
-
return {
|
|
146
|
-
role: "custom",
|
|
147
|
-
customType: BACKGROUND_TASK_CUSTOM_TYPE,
|
|
148
|
-
content: [{ type: "text", text: header }, ...result.result.content],
|
|
149
|
-
display: true,
|
|
150
|
-
details: { subagentType: info.subagentType, isMcpTool: true, isError: result.isError },
|
|
151
|
-
timestamp: Date.now(),
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
/** Convert CustomMessageEntry to AgentMessage format */
|
|
155
|
-
export function createCustomMessage(customType, content, display, details, timestamp) {
|
|
156
|
-
return {
|
|
157
|
-
role: "custom",
|
|
158
|
-
customType,
|
|
159
|
-
content,
|
|
160
|
-
display,
|
|
161
|
-
details,
|
|
162
|
-
timestamp: new Date(timestamp).getTime(),
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
/**
|
|
166
|
-
* Transform AgentMessages (including custom types) to LLM-compatible Messages.
|
|
167
|
-
*
|
|
168
|
-
* This is used by:
|
|
169
|
-
* - Agent's transormToLlm option (for prompt calls and queued messages)
|
|
170
|
-
* - Compaction's generateSummary (for summarization)
|
|
171
|
-
* - Custom extensions and tools
|
|
172
|
-
*/
|
|
173
|
-
export function convertToLlm(messages) {
|
|
174
|
-
return messages
|
|
175
|
-
.map((m) => {
|
|
176
|
-
switch (m.role) {
|
|
177
|
-
case "bashExecution":
|
|
178
|
-
// Skip messages excluded from context (!! prefix)
|
|
179
|
-
if (m.excludeFromContext) {
|
|
180
|
-
return undefined;
|
|
181
|
-
}
|
|
182
|
-
return {
|
|
183
|
-
role: "user",
|
|
184
|
-
content: [{ type: "text", text: bashExecutionToText(m) }],
|
|
185
|
-
timestamp: m.timestamp,
|
|
186
|
-
};
|
|
187
|
-
case "custom": {
|
|
188
|
-
const content = typeof m.content === "string" ? [{ type: "text", text: m.content }] : m.content;
|
|
189
|
-
return {
|
|
190
|
-
role: "user",
|
|
191
|
-
content,
|
|
192
|
-
timestamp: m.timestamp,
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
case "branchSummary":
|
|
196
|
-
return {
|
|
197
|
-
role: "user",
|
|
198
|
-
content: [{ type: "text", text: BRANCH_SUMMARY_PREFIX + m.summary + BRANCH_SUMMARY_SUFFIX }],
|
|
199
|
-
timestamp: m.timestamp,
|
|
200
|
-
};
|
|
201
|
-
case "compactionSummary":
|
|
202
|
-
return {
|
|
203
|
-
role: "user",
|
|
204
|
-
content: [
|
|
205
|
-
{ type: "text", text: COMPACTION_SUMMARY_PREFIX + m.summary + COMPACTION_SUMMARY_SUFFIX },
|
|
206
|
-
],
|
|
207
|
-
timestamp: m.timestamp,
|
|
208
|
-
};
|
|
209
|
-
case "user":
|
|
210
|
-
case "assistant":
|
|
211
|
-
case "toolResult":
|
|
212
|
-
return m;
|
|
213
|
-
default:
|
|
214
|
-
// biome-ignore lint/correctness/noSwitchDeclarations: fine
|
|
215
|
-
const _exhaustiveCheck = m;
|
|
216
|
-
return undefined;
|
|
217
|
-
}
|
|
218
|
-
})
|
|
219
|
-
.filter((m) => m !== undefined);
|
|
220
|
-
}
|
|
221
|
-
//# sourceMappingURL=messages.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"messages.js","sourceRoot":"","sources":["../../src/core/messages.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Custom message types and transformers for the coding agent.\n *\n * Extends the base AgentMessage type with coding-agent specific message types,\n * and provides a transformer to convert them to LLM-compatible messages.\n */\n\nimport type { AgentMessage, BackgroundToolResult } from \"@kolisachint/hoocode-agent-core\";\nimport type { ImageContent, Message, TextContent } from \"@kolisachint/hoocode-ai\";\n\nexport const COMPACTION_SUMMARY_PREFIX = `The conversation history before this point was compacted into the following summary:\n\n<summary>\n`;\n\nexport const COMPACTION_SUMMARY_SUFFIX = `\n</summary>`;\n\nexport const BRANCH_SUMMARY_PREFIX = `The following is a summary of a branch that this conversation came back from:\n\n<summary>\n`;\n\nexport const BRANCH_SUMMARY_SUFFIX = `</summary>`;\n\n/**\n * Message type for bash executions via the ! command.\n */\nexport interface BashExecutionMessage {\n\trole: \"bashExecution\";\n\tcommand: string;\n\toutput: string;\n\texitCode: number | undefined;\n\tcancelled: boolean;\n\ttruncated: boolean;\n\tfullOutputPath?: string;\n\ttimestamp: number;\n\t/** If true, this message is excluded from LLM context (!! prefix) */\n\texcludeFromContext?: boolean;\n}\n\n/**\n * Message type for extension-injected messages via sendMessage().\n * These are custom messages that extensions can inject into the conversation.\n */\nexport interface CustomMessage<T = unknown> {\n\trole: \"custom\";\n\tcustomType: string;\n\tcontent: string | (TextContent | ImageContent)[];\n\tdisplay: boolean;\n\tdetails?: T;\n\ttimestamp: number;\n}\n\nexport interface BranchSummaryMessage {\n\trole: \"branchSummary\";\n\tsummary: string;\n\tfromId: string;\n\ttimestamp: number;\n}\n\nexport interface CompactionSummaryMessage {\n\trole: \"compactionSummary\";\n\tsummary: string;\n\ttokensBefore: number;\n\t/** Estimated context tokens after compaction; absent on entries written before this field existed. */\n\ttokensAfter?: number;\n\ttimestamp: number;\n}\n\n// Extend CustomAgentMessages via declaration merging\ndeclare module \"@kolisachint/hoocode-agent-core\" {\n\tinterface CustomAgentMessages {\n\t\tbashExecution: BashExecutionMessage;\n\t\tcustom: CustomMessage;\n\t\tbranchSummary: BranchSummaryMessage;\n\t\tcompactionSummary: CompactionSummaryMessage;\n\t}\n}\n\n/**\n * Convert a BashExecutionMessage to user message text for LLM context.\n */\nexport function bashExecutionToText(msg: BashExecutionMessage): string {\n\tlet text = `Ran \\`${msg.command}\\`\\n`;\n\tif (msg.output) {\n\t\ttext += `\\`\\`\\`\\n${msg.output}\\n\\`\\`\\``;\n\t} else {\n\t\ttext += \"(no output)\";\n\t}\n\tif (msg.cancelled) {\n\t\ttext += \"\\n\\n(command cancelled)\";\n\t} else if (msg.exitCode !== null && msg.exitCode !== undefined && msg.exitCode !== 0) {\n\t\ttext += `\\n\\nCommand exited with code ${msg.exitCode}`;\n\t}\n\tif (msg.truncated && msg.fullOutputPath) {\n\t\ttext += `\\n\\n[Output truncated. Full output: ${msg.fullOutputPath}]`;\n\t}\n\treturn text;\n}\n\nexport function createBranchSummaryMessage(summary: string, fromId: string, timestamp: string): BranchSummaryMessage {\n\treturn {\n\t\trole: \"branchSummary\",\n\t\tsummary,\n\t\tfromId,\n\t\ttimestamp: new Date(timestamp).getTime(),\n\t};\n}\n\nexport function createCompactionSummaryMessage(\n\tsummary: string,\n\ttokensBefore: number,\n\ttimestamp: string,\n\ttokensAfter?: number,\n): CompactionSummaryMessage {\n\treturn {\n\t\trole: \"compactionSummary\",\n\t\tsummary: summary,\n\t\ttokensBefore,\n\t\ttokensAfter,\n\t\ttimestamp: new Date(timestamp).getTime(),\n\t};\n}\n\n/** customType used for the follow-up message a finished background tool injects. */\nexport const BACKGROUND_TASK_CUSTOM_TYPE = \"backgroundTask\";\n\n/** Minimal shape of a tool call, shared by the placeholder and follow-up builders. */\ninterface BackgroundToolCall {\n\tname: string;\n\targuments?: Record<string, unknown>;\n}\n\n/** A consistent, human-readable description of a background tool call. */\nexport interface BackgroundToolInfo {\n\t/** True for MCP server tools (registered as `mcp_<server>_<tool>`). */\n\tisMcpTool: boolean;\n\t/** The subagent type for `Task` calls; the tool name otherwise. */\n\tsubagentType: string;\n\t/** Short label used verbatim in both the start and finish messages (kept in sync). */\n\tlabel: string;\n\t/** One-line summary of what the call is doing, derived from its arguments. */\n\tsummary?: string;\n}\n\n/** First non-empty line of a string, trimmed and capped for one-line display. */\nfunction firstLine(text: string, max = 120): string {\n\tconst line = (text.split(\"\\n\").find((l) => l.trim().length > 0) ?? \"\").trim();\n\treturn line.length > max ? `${line.slice(0, max - 1).trimEnd()}…` : line;\n}\n\n/** Summarize a tool's arguments as up to three `key: value` pairs for a one-liner. */\nexport function summarizeArgs(args: Record<string, unknown>): string | undefined {\n\tconst parts: string[] = [];\n\tfor (const [key, value] of Object.entries(args)) {\n\t\tif (value === undefined || value === null || value === \"\") continue;\n\t\tconst rendered = typeof value === \"string\" ? value : JSON.stringify(value);\n\t\tparts.push(`${key}: ${firstLine(rendered, 48)}`);\n\t\tif (parts.length === 3) break;\n\t}\n\treturn parts.length > 0 ? parts.join(\", \") : undefined;\n}\n\n/**\n * Describe a background tool call consistently, so the \"started\" placeholder, the\n * \"finished\" follow-up message, and any UI all read the same way. Subagent `Task`\n * calls are keyed off their `subagent_type`; MCP server tools off their `mcp_`\n * prefix.\n */\nexport function describeBackgroundTool(toolCall: BackgroundToolCall): BackgroundToolInfo {\n\tconst args = toolCall.arguments ?? {};\n\tconst isMcpTool = toolCall.name.startsWith(\"mcp_\");\n\n\tif (isMcpTool) {\n\t\t// Registered name is `mcp_<server>_<tool>`; drop the prefix for display.\n\t\tconst pretty = toolCall.name.replace(/^mcp_/, \"\");\n\t\treturn { isMcpTool, subagentType: toolCall.name, label: `MCP tool \\`${pretty}\\``, summary: summarizeArgs(args) };\n\t}\n\n\tconst subagentType = typeof args.subagent_type === \"string\" ? args.subagent_type : toolCall.name;\n\tconst summary =\n\t\ttypeof args.description === \"string\" && args.description.trim()\n\t\t\t? args.description.trim()\n\t\t\t: typeof args.prompt === \"string\"\n\t\t\t\t? firstLine(args.prompt)\n\t\t\t\t: undefined;\n\treturn { isMcpTool, subagentType, label: `subagent \\`${subagentType}\\``, summary };\n}\n\n/**\n * Verbose, human-readable placeholder shown the moment a background tool is\n * dispatched (before its result is known). Wired into the agent loop via\n * `createBackgroundPlaceholder`, it replaces the generic \"Started X in the\n * background\" line so the demo legibly explains what each background agent / MCP\n * tool is doing. Kept in sync with the finish message via {@link describeBackgroundTool}.\n */\nexport function createBackgroundPlaceholderText(toolCall: BackgroundToolCall): string {\n\tconst info = describeBackgroundTool(toolCall);\n\tconst what = info.summary ? ` — ${firstLine(info.summary, 80)}` : \"\";\n\tif (info.isMcpTool) {\n\t\treturn `Started ${info.label} in the background${what}. Its result arrives as a follow-up; keep working.`;\n\t}\n\t// One compact line: a verbose multi-line placeholder per dispatch floods the\n\t// transcript when several subagents spin up at once. The finish notification\n\t// names the task and the body is pulled with TaskOutput; TaskOutput(list) shows\n\t// progress meanwhile.\n\treturn `Delegated to ${info.label} in the background${what}. I'll be notified when it finishes; use TaskOutput to check progress or read the result.`;\n}\n\n/**\n * Build the follow-up message injected when a background tool finishes.\n *\n * Handles both subagent `Task` calls and MCP tools, described consistently with\n * the start placeholder via {@link describeBackgroundTool}. Rendered as a distinct\n * custom message rather than a plain user message, and converted to a user message\n * for the LLM.\n */\nexport function createBackgroundTaskMessage(result: BackgroundToolResult): CustomMessage {\n\tconst info = describeBackgroundTool(result.toolCall);\n\t// Subagent `Task`: the tool already returns a compact, self-contained\n\t// notification (the full body is retained in the inbox for TaskOutput to pull),\n\t// so the follow-up is exactly that line — no second header, no inlined body.\n\tif (!info.isMcpTool) {\n\t\treturn {\n\t\t\trole: \"custom\",\n\t\t\tcustomType: BACKGROUND_TASK_CUSTOM_TYPE,\n\t\t\tcontent: result.result.content,\n\t\t\tdisplay: true,\n\t\t\tdetails: { subagentType: info.subagentType, isMcpTool: false, isError: result.isError },\n\t\t\ttimestamp: Date.now(),\n\t\t};\n\t}\n\t// MCP background tool: no inbox, so deliver the header + full result body.\n\tconst verb = result.isError ? \"failed\" : \"finished\";\n\tconst summary = info.summary ? ` (${info.summary})` : \"\";\n\tconst header = `Background ${info.label}${summary} ${verb}:`;\n\treturn {\n\t\trole: \"custom\",\n\t\tcustomType: BACKGROUND_TASK_CUSTOM_TYPE,\n\t\tcontent: [{ type: \"text\", text: header }, ...result.result.content],\n\t\tdisplay: true,\n\t\tdetails: { subagentType: info.subagentType, isMcpTool: true, isError: result.isError },\n\t\ttimestamp: Date.now(),\n\t};\n}\n\n/** Convert CustomMessageEntry to AgentMessage format */\nexport function createCustomMessage(\n\tcustomType: string,\n\tcontent: string | (TextContent | ImageContent)[],\n\tdisplay: boolean,\n\tdetails: unknown | undefined,\n\ttimestamp: string,\n): CustomMessage {\n\treturn {\n\t\trole: \"custom\",\n\t\tcustomType,\n\t\tcontent,\n\t\tdisplay,\n\t\tdetails,\n\t\ttimestamp: new Date(timestamp).getTime(),\n\t};\n}\n\n/**\n * Transform AgentMessages (including custom types) to LLM-compatible Messages.\n *\n * This is used by:\n * - Agent's transormToLlm option (for prompt calls and queued messages)\n * - Compaction's generateSummary (for summarization)\n * - Custom extensions and tools\n */\nexport function convertToLlm(messages: AgentMessage[]): Message[] {\n\treturn messages\n\t\t.map((m): Message | undefined => {\n\t\t\tswitch (m.role) {\n\t\t\t\tcase \"bashExecution\":\n\t\t\t\t\t// Skip messages excluded from context (!! prefix)\n\t\t\t\t\tif (m.excludeFromContext) {\n\t\t\t\t\t\treturn undefined;\n\t\t\t\t\t}\n\t\t\t\t\treturn {\n\t\t\t\t\t\trole: \"user\",\n\t\t\t\t\t\tcontent: [{ type: \"text\", text: bashExecutionToText(m) }],\n\t\t\t\t\t\ttimestamp: m.timestamp,\n\t\t\t\t\t};\n\t\t\t\tcase \"custom\": {\n\t\t\t\t\tconst content = typeof m.content === \"string\" ? [{ type: \"text\" as const, text: m.content }] : m.content;\n\t\t\t\t\treturn {\n\t\t\t\t\t\trole: \"user\",\n\t\t\t\t\t\tcontent,\n\t\t\t\t\t\ttimestamp: m.timestamp,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tcase \"branchSummary\":\n\t\t\t\t\treturn {\n\t\t\t\t\t\trole: \"user\",\n\t\t\t\t\t\tcontent: [{ type: \"text\" as const, text: BRANCH_SUMMARY_PREFIX + m.summary + BRANCH_SUMMARY_SUFFIX }],\n\t\t\t\t\t\ttimestamp: m.timestamp,\n\t\t\t\t\t};\n\t\t\t\tcase \"compactionSummary\":\n\t\t\t\t\treturn {\n\t\t\t\t\t\trole: \"user\",\n\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t{ type: \"text\" as const, text: COMPACTION_SUMMARY_PREFIX + m.summary + COMPACTION_SUMMARY_SUFFIX },\n\t\t\t\t\t\t],\n\t\t\t\t\t\ttimestamp: m.timestamp,\n\t\t\t\t\t};\n\t\t\t\tcase \"user\":\n\t\t\t\tcase \"assistant\":\n\t\t\t\tcase \"toolResult\":\n\t\t\t\t\treturn m;\n\t\t\t\tdefault:\n\t\t\t\t\t// biome-ignore lint/correctness/noSwitchDeclarations: fine\n\t\t\t\t\tconst _exhaustiveCheck: never = m;\n\t\t\t\t\treturn undefined;\n\t\t\t}\n\t\t})\n\t\t.filter((m) => m !== undefined);\n}\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"browser-continue.d.ts","sourceRoot":"","sources":["../../../src/core/tools/browser-continue.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,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACvE,OAAO,EAEN,KAAK,uBAAuB,EAI5B,MAAM,0BAA0B,CAAC;AAGlC,QAAA,MAAM,qBAAqB;;;EAOzB,CAAC;AAEH,MAAM,MAAM,oBAAoB,GAAG,MAAM,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAExE,MAAM,WAAW,0BAA2B,SAAQ,uBAAuB;CAAG;AAE9E,wBAAgB,mCAAmC,CAGlD,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,0BAA0B,GAClC,cAAc,CAAC,OAAO,qBAAqB,EAAE,iBAAiB,CAAC,CAyDjE;AAED,wBAAgB,yBAAyB,CACxC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,0BAA0B,GAClC,SAAS,CAAC,OAAO,qBAAqB,CAAC,CAEzC","sourcesContent":["/**\n * `browser_continue` tool: answer a `NeedsParent` suspension from `browser_run`.\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_run` 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 BrowserRunDetails } from \"./browser-run.js\";\nimport {\n\ttype BrowserClientConfig,\n\ttype BrowsertoolsToolOptions,\n\ttype FlowOutcome,\n\tresolveBrowsertoolsOptions,\n\ttakeSession,\n} from \"./browsertools-shared.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\n\nconst browserContinueSchema = Type.Object({\n\ttoken: Type.String({ description: \"The resume token returned by a browser_run 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 BrowserContinueInput = Static<typeof browserContinueSchema>;\n\nexport interface BrowserContinueToolOptions extends BrowsertoolsToolOptions {}\n\nexport function createBrowserContinueToolDefinition(\n\t// cwd is part of the factory signature for parity with other tools, but continue\n\t// reuses the serve process parked by browser_run, so it is not needed here.\n\t_cwd: string,\n\toptions?: BrowserContinueToolOptions,\n): ToolDefinition<typeof browserContinueSchema, BrowserRunDetails> {\n\tconst opts = resolveBrowsertoolsOptions(options);\n\treturn defineTool({\n\t\tname: \"browser_continue\",\n\t\tlabel: \"browser continue\",\n\t\tdescription:\n\t\t\t\"Continue a browser flow that suspended with a NeedsParent request. Pass the token from the \" +\n\t\t\t\"browser_run 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_continue 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: browserContinueSchema,\n\t\tasync execute(_toolCallId, params: BrowserContinueInput, 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_run.`,\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// Preserve the original browser config so the idle client can be reused\n\t\t\t\t// by a subsequent browser_run with the same headful/browserPath settings.\n\t\t\t\tconst browserConfig: BrowserClientConfig = {\n\t\t\t\t\theadful: session.headful,\n\t\t\t\t\tbrowserPath: session.browserPath,\n\t\t\t\t\tidleTimeoutMs: opts.idleTimeoutMs,\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, browserConfig);\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 createBrowserContinueTool(\n\tcwd: string,\n\toptions?: BrowserContinueToolOptions,\n): AgentTool<typeof browserContinueSchema> {\n\treturn wrapToolDefinition(createBrowserContinueToolDefinition(cwd, options));\n}\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"browser-continue.js","sourceRoot":"","sources":["../../../src/core/tools/browser-continue.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,EAA0B,MAAM,kBAAkB,CAAC;AACvE,OAAO,EAIN,0BAA0B,EAC1B,WAAW,GACX,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAElE,MAAM,qBAAqB,GAAG,IAAI,CAAC,MAAM,CAAC;IACzC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,gEAAgE,EAAE,CAAC;IACrG,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,mCAAmC;AAClD,iFAAiF;AACjF,4EAA4E;AAC5E,IAAY,EACZ,OAAoC,EAC8B;IAClE,MAAM,IAAI,GAAG,0BAA0B,CAAC,OAAO,CAAC,CAAC;IACjD,OAAO,UAAU,CAAC;QACjB,IAAI,EAAE,kBAAkB;QACxB,KAAK,EAAE,kBAAkB;QACzB,WAAW,EACV,6FAA6F;YAC7F,2FAA2F;YAC3F,iGAA+F;YAC/F,6FAA6F;YAC7F,gGAAgG;YAChG,+FAA+F;YAC/F,6FAA6F;YAC7F,2FAA2F;YAC3F,wFAAwF;YACxF,wBAAwB;QACzB,UAAU,EAAE,qBAAqB;QACjC,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,MAA4B,EAAE,MAAM,EAAE;YAChE,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,gEAAgE,CACjE,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,0EAA0E;gBAC1E,MAAM,aAAa,GAAwB;oBAC1C,OAAO,EAAE,OAAO,CAAC,OAAO;oBACxB,WAAW,EAAE,OAAO,CAAC,WAAW;oBAChC,aAAa,EAAE,IAAI,CAAC,aAAa;iBACjC,CAAC;gBACF,wEAAwE;gBACxE,OAAO,MAAM,WAAW,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,IAAI,EAAE,aAAa,CAAC,CAAC;YACpF,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,yBAAyB,CACxC,GAAW,EACX,OAAoC,EACM;IAC1C,OAAO,kBAAkB,CAAC,mCAAmC,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC;AAAA,CAC7E","sourcesContent":["/**\n * `browser_continue` tool: answer a `NeedsParent` suspension from `browser_run`.\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_run` 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 BrowserRunDetails } from \"./browser-run.js\";\nimport {\n\ttype BrowserClientConfig,\n\ttype BrowsertoolsToolOptions,\n\ttype FlowOutcome,\n\tresolveBrowsertoolsOptions,\n\ttakeSession,\n} from \"./browsertools-shared.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\n\nconst browserContinueSchema = Type.Object({\n\ttoken: Type.String({ description: \"The resume token returned by a browser_run 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 BrowserContinueInput = Static<typeof browserContinueSchema>;\n\nexport interface BrowserContinueToolOptions extends BrowsertoolsToolOptions {}\n\nexport function createBrowserContinueToolDefinition(\n\t// cwd is part of the factory signature for parity with other tools, but continue\n\t// reuses the serve process parked by browser_run, so it is not needed here.\n\t_cwd: string,\n\toptions?: BrowserContinueToolOptions,\n): ToolDefinition<typeof browserContinueSchema, BrowserRunDetails> {\n\tconst opts = resolveBrowsertoolsOptions(options);\n\treturn defineTool({\n\t\tname: \"browser_continue\",\n\t\tlabel: \"browser continue\",\n\t\tdescription:\n\t\t\t\"Continue a browser flow that suspended with a NeedsParent request. Pass the token from the \" +\n\t\t\t\"browser_run 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_continue 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: browserContinueSchema,\n\t\tasync execute(_toolCallId, params: BrowserContinueInput, 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_run.`,\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// Preserve the original browser config so the idle client can be reused\n\t\t\t\t// by a subsequent browser_run with the same headful/browserPath settings.\n\t\t\t\tconst browserConfig: BrowserClientConfig = {\n\t\t\t\t\theadful: session.headful,\n\t\t\t\t\tbrowserPath: session.browserPath,\n\t\t\t\t\tidleTimeoutMs: opts.idleTimeoutMs,\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, browserConfig);\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 createBrowserContinueTool(\n\tcwd: string,\n\toptions?: BrowserContinueToolOptions,\n): AgentTool<typeof browserContinueSchema> {\n\treturn wrapToolDefinition(createBrowserContinueToolDefinition(cwd, options));\n}\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"browser-run.d.ts","sourceRoot":"","sources":["../../../src/core/tools/browser-run.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,KAAK,mBAAmB,EACxB,uBAAuB,EACvB,KAAK,uBAAuB,EAC5B,KAAK,WAAW,EAEhB,KAAK,aAAa,EAGlB,KAAK,WAAW,EAEhB,0BAA0B,EAE1B,MAAM,0BAA0B,CAAC;AAGlC,QAAA,MAAM,gBAAgB;;;;;;;EAmDpB,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE9D,sEAAsE;AACtE,MAAM,WAAW,iBAAiB;IACjC,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,qBAAsB,SAAQ,uBAAuB;CAAG;AAkMzE;;;;;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,EACnD,aAAa,CAAC,EAAE,mBAAmB,GACjC,OAAO,CAAC,eAAe,CAAC,iBAAiB,CAAC,CAAC,CA2D7C;AAED,wBAAgB,8BAA8B,CAC7C,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,qBAAqB,GAC7B,cAAc,CAAC,OAAO,gBAAgB,EAAE,iBAAiB,CAAC,CAiG5D;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,SAAS,CAAC,OAAO,gBAAgB,CAAC,CAErH","sourcesContent":["/**\n * `browser_run` 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_continue` 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\ttype BrowserClientConfig,\n\tBrowsertoolsServeClient,\n\ttype BrowsertoolsToolOptions,\n\ttype FlowOutcome,\n\ttype GetResourceResult,\n\ttype ParentRequest,\n\tparkIdleClient,\n\tparkSession,\n\ttype ResumeToken,\n\tresolveBrowsertoolsBinary,\n\tresolveBrowsertoolsOptions,\n\ttakeIdleClient,\n} from \"./browsertools-shared.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\n\nconst browserRunSchema = 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`). Shape: \" +\n\t\t\t\t\"{ id: string, name: string, version: number, start_url: string, steps: Step[] }. \" +\n\t\t\t\t\"Each Step is { id: string, action: Action }. Action is a tagged object keyed by \" +\n\t\t\t\t\"`action`; the exact fields per variant (do NOT add extras like `goal` to the wrong \" +\n\t\t\t\t\"variant): \" +\n\t\t\t\t'{ action: \"navigate\", url: string } | ' +\n\t\t\t\t'{ action: \"click\", selector: string, fallbacks?: string[] } | ' +\n\t\t\t\t'{ action: \"fill\", selector: string, value_tpl: string } | ' +\n\t\t\t\t'{ action: \"select\", selector: string, value_tpl: string } | ' +\n\t\t\t\t'{ action: \"wait_settle\" } | ' +\n\t\t\t\t'{ action: \"checkpoint\", asserts: Invariant[] } | ' +\n\t\t\t\t'{ action: \"decide\", goal: string } | ' +\n\t\t\t\t'{ action: \"classify\" } | ' +\n\t\t\t\t'{ action: \"verify_visual\", expected_state: string } | ' +\n\t\t\t\t'{ action: \"extract_semantic\", fields: string[] }. ' +\n\t\t\t\t\"IMPORTANT: `extract_semantic.fields` MUST be an array of field-name strings \" +\n\t\t\t\t'(e.g. [\"person\", \"movie_count\"]), never an object/map, and `extract_semantic` takes ' +\n\t\t\t\t\"no `goal` (only `decide` does). Invariant (for checkpoint asserts) is one of: \" +\n\t\t\t\t'{ kind: \"element_present\", selector: string } | ' +\n\t\t\t\t'{ kind: \"text_present\", selector?: string, substr: string } | ' +\n\t\t\t\t'{ kind: \"url_matches\", pattern: string }.',\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 BrowserRunInput = Static<typeof browserRunSchema>;\n\n/** Structured details surfaced alongside the model-facing content. */\nexport interface BrowserRunDetails {\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 BrowserRunToolOptions 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_continue response: { \"response\": \"state\", \"state\": \"<your label>\" }';\n\t\tcase \"verify_visual\":\n\t\t\treturn 'Reply with browser_continue response: { \"response\": \"verified\", \"passed\": true | false }';\n\t\tcase \"extract_semantic\":\n\t\t\treturn 'Reply with browser_continue response: { \"response\": \"extracted\", \"fields\": { \"<field>\": \"<value>\", ... } }';\n\t\tcase \"decide_next_action\":\n\t\t\treturn 'Reply with browser_continue response: { \"response\": \"next_action\", \"action\": <action object> }';\n\t\tcase \"reidentify_element\":\n\t\t\treturn 'Reply with browser_continue response: { \"response\": \"element\", \"selector\": \"<css selector>\" }';\n\t\tdefault:\n\t\t\treturn \"Reply with browser_continue providing the appropriate ParentResponse object.\";\n\t}\n}\n\n/** A control fact from the browsertools observation (mirrors observe.rs InputFact). */\ninterface ObservationInput {\n\tkind?: string;\n\taccessible_name?: string;\n\tselector_hint?: string;\n}\n\n/** A landmark fact from the browsertools observation (mirrors observe.rs Landmark). */\ninterface ObservationLandmark {\n\trole?: string;\n\tname?: string;\n}\n\n/** The HTML-derived observation the engine attaches to decide/classify requests. */\ninterface Observation {\n\turl?: string;\n\ttitle?: string;\n\tinputs?: ObservationInput[];\n\tlandmarks?: ObservationLandmark[];\n\ttext_blocks?: string[];\n\thas_error_region?: boolean;\n}\n\n/** Truncate a string to `max` chars with an ellipsis, for compact rendering. */\nfunction clip(s: string, max = 80): string {\n\tconst t = s.trim();\n\treturn t.length > max ? `${t.slice(0, max - 1)}\\u2026` : t;\n}\n\n/** Render one observation control as a single compact line. */\nfunction renderControl(i: ObservationInput): string {\n\tconst sel = i.selector_hint && i.selector_hint !== \"button\" ? i.selector_hint : undefined;\n\tconst name = i.accessible_name ? `\"${clip(i.accessible_name, 60)}\"` : undefined;\n\tconst parts = [i.kind, name, sel].filter(Boolean);\n\treturn ` - ${parts.join(\" \")}`;\n}\n\n/**\n * Summarize a verbose `decide_next_action`/`classify_state` observation into a\n * compact, readable block instead of dumping the raw JSON (which on busy pages is\n * thousands of mostly-empty entries straight into the model context). The\n * screenshot already conveys the page; this is just the actionable text layer:\n * url/title, named controls, salient headings. Returns undefined when there is no\n * observation to summarize.\n */\nfunction summarizeObservation(observation: Observation | undefined): string | undefined {\n\tif (!observation || typeof observation !== \"object\") return undefined;\n\tconst lines: string[] = [];\n\tif (observation.title) lines.push(`page: ${clip(observation.title, 100)}`);\n\tif (observation.url) lines.push(`url: ${clip(observation.url, 120)}`);\n\tif (observation.has_error_region) lines.push(\"note: page has an error/alert region\");\n\n\tconst controls = (observation.inputs ?? []).filter((i) => i.kind !== \"_more\");\n\tif (controls.length) {\n\t\tlines.push(`controls (${controls.length}):`);\n\t\tfor (const c of controls.slice(0, 25)) lines.push(renderControl(c));\n\t\tif (controls.length > 25) lines.push(` - …+${controls.length - 25} more`);\n\t}\n\n\tconst text = (observation.text_blocks ?? []).filter((t) => t?.trim());\n\tif (text.length) {\n\t\tlines.push(\n\t\t\t`headings: ${text\n\t\t\t\t.slice(0, 12)\n\t\t\t\t.map((t) => clip(t, 50))\n\t\t\t\t.join(\" · \")}`,\n\t\t);\n\t}\n\treturn lines.length ? lines.join(\"\\n\") : undefined;\n}\n\n/**\n * Build the compact, model-facing text for a `NeedsParent` suspension. Renders\n * only the fields that matter per request kind (goal, expected_state, fields,\n * description) plus a summarized observation, rather than pretty-printing the\n * entire request — which is the dominant token sink for browser flows.\n */\nfunction formatParentRequest(request: ParentRequest): string {\n\tconst kind = request.request;\n\tconst lines: string[] = [`request: ${kind}`];\n\tconst r = request as ParentRequest & {\n\t\tgoal?: string;\n\t\texpected_state?: string;\n\t\tdescription?: string;\n\t\tfields?: string[];\n\t\tobservation?: Observation;\n\t};\n\tif (r.goal) lines.push(`goal: ${r.goal}`);\n\tif (r.expected_state) lines.push(`expected_state: ${r.expected_state}`);\n\tif (r.description) lines.push(`element: ${r.description}`);\n\tif (Array.isArray(r.fields) && r.fields.length) lines.push(`fields: ${r.fields.join(\", \")}`);\n\tconst summary = summarizeObservation(r.observation);\n\tif (summary) lines.push(summary);\n\treturn lines.join(\"\\n\");\n}\n\n/** Compact, model-facing reminder of the inline-flow action schema, appended to\n * an `invalid inline flow` error so the model can self-correct in one turn\n * instead of guessing field shapes across several rounds. */\nconst FLOW_SCHEMA_HINT =\n\t\"Inline flow shape: { id, name, version, start_url, steps: [{ id, action }] }. \" +\n\t\"Action variants (use EXACTLY these fields): \" +\n\t'{ action: \"navigate\", url } | { action: \"click\", selector, fallbacks? } | ' +\n\t'{ action: \"fill\", selector, value_tpl } | { action: \"select\", selector, value_tpl } | ' +\n\t'{ action: \"wait_settle\" } | { action: \"checkpoint\", asserts } | ' +\n\t'{ action: \"decide\", goal } | { action: \"classify\" } | ' +\n\t'{ action: \"verify_visual\", expected_state } | { action: \"extract_semantic\", fields }. ' +\n\t'`extract_semantic.fields` MUST be a string array like [\"person\",\"count\"] (not an object), ' +\n\t\"and it takes no `goal` — only `decide` does.\";\n\n/** If an error is a browsertools inline-flow validation failure, append the\n * schema hint so the model fixes the flow on the next call. Other errors pass\n * through unchanged. */\nfunction enrichFlowError(error: unknown): unknown {\n\tconst message = error instanceof Error ? error.message : String(error);\n\tif (message.includes(\"invalid inline flow\") && !message.includes(\"Action variants\")) {\n\t\treturn new Error(`${message}\\n\\n${FLOW_SCHEMA_HINT}`);\n\t}\n\treturn error;\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.\n *\n * When the serve client is reused across browser_run calls (the\n * reclaimed-idle path), the viewer is already running on the same port, so we\n * skip re-issuing `live_view_start` and skip auto-opening a second OS tab —\n * the user keeps the one tab they already have. */\nasync function startLiveView(client: BrowsertoolsServeClient): Promise<string | undefined> {\n\tif (client.liveViewUrl) {\n\t\treturn `Live view already open at: ${client.liveViewUrl}`;\n\t}\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\tclient.liveViewUrl = result.url;\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\tbrowserConfig?: BrowserClientConfig,\n): Promise<AgentToolResult<BrowserRunDetails>> {\n\tif (outcome.outcome === \"complete\") {\n\t\tif (browserConfig) {\n\t\t\tparkIdleClient(client, browserConfig.headful, browserConfig.browserPath, browserConfig.idleTimeoutMs);\n\t\t} else {\n\t\t\tclient.dispose();\n\t\t}\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\tif (browserConfig) {\n\t\t\tparkIdleClient(client, browserConfig.headful, browserConfig.browserPath, browserConfig.idleTimeoutMs);\n\t\t} else {\n\t\t\tclient.dispose();\n\t\t}\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(\n\t\t\ttoken,\n\t\t\tclient,\n\t\t\trounds,\n\t\t\tbrowserConfig?.idleTimeoutMs ?? opts.idleTimeoutMs,\n\t\t\tbrowserConfig?.headful ?? false,\n\t\t\tbrowserConfig?.browserPath,\n\t\t);\n\n\t\tconst text =\n\t\t\t`Flow suspended — parent decision required (NeedsParent).\\n` +\n\t\t\t`${formatParentRequest(request)}\\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 createBrowserRunToolDefinition(\n\tcwd: string,\n\toptions?: BrowserRunToolOptions,\n): ToolDefinition<typeof browserRunSchema, BrowserRunDetails> {\n\tconst opts = resolveBrowsertoolsOptions(options);\n\treturn defineTool({\n\t\tname: \"browser_run\",\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_continue 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_continue 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_continue `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: browserRunSchema,\n\t\tasync execute(_toolCallId, params: BrowserRunInput, 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_run requires either `flow_path` or `flow`\");\n\t\t\t}\n\n\t\t\tconst binaryPath = await resolveBrowsertoolsBinary(options);\n\t\t\tconst headful = params.headful ?? opts.headful;\n\t\t\t// Reuse the shared idle client if browser config matches, otherwise create a new\n\t\t\t// one. This keeps a single Chromium process and live-view port across calls.\n\t\t\tlet client = takeIdleClient(headful, opts.browserPath, opts.idleTimeoutMs);\n\t\t\tif (!client) {\n\t\t\t\tclient = new BrowsertoolsServeClient(binaryPath, {\n\t\t\t\t\tcwd,\n\t\t\t\t\tbrowserPath: opts.browserPath,\n\t\t\t\t\tserveArgs: opts.serveArgs,\n\t\t\t\t\trequestTimeoutMs: opts.requestTimeoutMs,\n\t\t\t\t\theadful,\n\t\t\t\t});\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 browserConfig: BrowserClientConfig = {\n\t\t\t\t\theadful,\n\t\t\t\t\tbrowserPath: opts.browserPath,\n\t\t\t\t\tidleTimeoutMs: opts.idleTimeoutMs,\n\t\t\t\t};\n\t\t\t\tconst result = await advanceFlow(client, outcome, 1, opts, browserConfig);\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 enrichFlowError(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 createBrowserRunTool(cwd: string, options?: BrowserRunToolOptions): AgentTool<typeof browserRunSchema> {\n\treturn wrapToolDefinition(createBrowserRunToolDefinition(cwd, options));\n}\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"browser-run.js","sourceRoot":"","sources":["../../../src/core/tools/browser-run.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `browser_run` 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_continue` 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\ttype BrowserClientConfig,\n\tBrowsertoolsServeClient,\n\ttype BrowsertoolsToolOptions,\n\ttype FlowOutcome,\n\ttype GetResourceResult,\n\ttype ParentRequest,\n\tparkIdleClient,\n\tparkSession,\n\ttype ResumeToken,\n\tresolveBrowsertoolsBinary,\n\tresolveBrowsertoolsOptions,\n\ttakeIdleClient,\n} from \"./browsertools-shared.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\n\nconst browserRunSchema = 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`). Shape: \" +\n\t\t\t\t\"{ id: string, name: string, version: number, start_url: string, steps: Step[] }. \" +\n\t\t\t\t\"Each Step is { id: string, action: Action }. Action is a tagged object keyed by \" +\n\t\t\t\t\"`action`; the exact fields per variant (do NOT add extras like `goal` to the wrong \" +\n\t\t\t\t\"variant): \" +\n\t\t\t\t'{ action: \"navigate\", url: string } | ' +\n\t\t\t\t'{ action: \"click\", selector: string, fallbacks?: string[] } | ' +\n\t\t\t\t'{ action: \"fill\", selector: string, value_tpl: string } | ' +\n\t\t\t\t'{ action: \"select\", selector: string, value_tpl: string } | ' +\n\t\t\t\t'{ action: \"wait_settle\" } | ' +\n\t\t\t\t'{ action: \"checkpoint\", asserts: Invariant[] } | ' +\n\t\t\t\t'{ action: \"decide\", goal: string } | ' +\n\t\t\t\t'{ action: \"classify\" } | ' +\n\t\t\t\t'{ action: \"verify_visual\", expected_state: string } | ' +\n\t\t\t\t'{ action: \"extract_semantic\", fields: string[] }. ' +\n\t\t\t\t\"IMPORTANT: `extract_semantic.fields` MUST be an array of field-name strings \" +\n\t\t\t\t'(e.g. [\"person\", \"movie_count\"]), never an object/map, and `extract_semantic` takes ' +\n\t\t\t\t\"no `goal` (only `decide` does). Invariant (for checkpoint asserts) is one of: \" +\n\t\t\t\t'{ kind: \"element_present\", selector: string } | ' +\n\t\t\t\t'{ kind: \"text_present\", selector?: string, substr: string } | ' +\n\t\t\t\t'{ kind: \"url_matches\", pattern: string }.',\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 BrowserRunInput = Static<typeof browserRunSchema>;\n\n/** Structured details surfaced alongside the model-facing content. */\nexport interface BrowserRunDetails {\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 BrowserRunToolOptions 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_continue response: { \"response\": \"state\", \"state\": \"<your label>\" }';\n\t\tcase \"verify_visual\":\n\t\t\treturn 'Reply with browser_continue response: { \"response\": \"verified\", \"passed\": true | false }';\n\t\tcase \"extract_semantic\":\n\t\t\treturn 'Reply with browser_continue response: { \"response\": \"extracted\", \"fields\": { \"<field>\": \"<value>\", ... } }';\n\t\tcase \"decide_next_action\":\n\t\t\treturn 'Reply with browser_continue response: { \"response\": \"next_action\", \"action\": <action object> }';\n\t\tcase \"reidentify_element\":\n\t\t\treturn 'Reply with browser_continue response: { \"response\": \"element\", \"selector\": \"<css selector>\" }';\n\t\tdefault:\n\t\t\treturn \"Reply with browser_continue providing the appropriate ParentResponse object.\";\n\t}\n}\n\n/** A control fact from the browsertools observation (mirrors observe.rs InputFact). */\ninterface ObservationInput {\n\tkind?: string;\n\taccessible_name?: string;\n\tselector_hint?: string;\n}\n\n/** A landmark fact from the browsertools observation (mirrors observe.rs Landmark). */\ninterface ObservationLandmark {\n\trole?: string;\n\tname?: string;\n}\n\n/** The HTML-derived observation the engine attaches to decide/classify requests. */\ninterface Observation {\n\turl?: string;\n\ttitle?: string;\n\tinputs?: ObservationInput[];\n\tlandmarks?: ObservationLandmark[];\n\ttext_blocks?: string[];\n\thas_error_region?: boolean;\n}\n\n/** Truncate a string to `max` chars with an ellipsis, for compact rendering. */\nfunction clip(s: string, max = 80): string {\n\tconst t = s.trim();\n\treturn t.length > max ? `${t.slice(0, max - 1)}\\u2026` : t;\n}\n\n/** Render one observation control as a single compact line. */\nfunction renderControl(i: ObservationInput): string {\n\tconst sel = i.selector_hint && i.selector_hint !== \"button\" ? i.selector_hint : undefined;\n\tconst name = i.accessible_name ? `\"${clip(i.accessible_name, 60)}\"` : undefined;\n\tconst parts = [i.kind, name, sel].filter(Boolean);\n\treturn ` - ${parts.join(\" \")}`;\n}\n\n/**\n * Summarize a verbose `decide_next_action`/`classify_state` observation into a\n * compact, readable block instead of dumping the raw JSON (which on busy pages is\n * thousands of mostly-empty entries straight into the model context). The\n * screenshot already conveys the page; this is just the actionable text layer:\n * url/title, named controls, salient headings. Returns undefined when there is no\n * observation to summarize.\n */\nfunction summarizeObservation(observation: Observation | undefined): string | undefined {\n\tif (!observation || typeof observation !== \"object\") return undefined;\n\tconst lines: string[] = [];\n\tif (observation.title) lines.push(`page: ${clip(observation.title, 100)}`);\n\tif (observation.url) lines.push(`url: ${clip(observation.url, 120)}`);\n\tif (observation.has_error_region) lines.push(\"note: page has an error/alert region\");\n\n\tconst controls = (observation.inputs ?? []).filter((i) => i.kind !== \"_more\");\n\tif (controls.length) {\n\t\tlines.push(`controls (${controls.length}):`);\n\t\tfor (const c of controls.slice(0, 25)) lines.push(renderControl(c));\n\t\tif (controls.length > 25) lines.push(` - …+${controls.length - 25} more`);\n\t}\n\n\tconst text = (observation.text_blocks ?? []).filter((t) => t?.trim());\n\tif (text.length) {\n\t\tlines.push(\n\t\t\t`headings: ${text\n\t\t\t\t.slice(0, 12)\n\t\t\t\t.map((t) => clip(t, 50))\n\t\t\t\t.join(\" · \")}`,\n\t\t);\n\t}\n\treturn lines.length ? lines.join(\"\\n\") : undefined;\n}\n\n/**\n * Build the compact, model-facing text for a `NeedsParent` suspension. Renders\n * only the fields that matter per request kind (goal, expected_state, fields,\n * description) plus a summarized observation, rather than pretty-printing the\n * entire request — which is the dominant token sink for browser flows.\n */\nfunction formatParentRequest(request: ParentRequest): string {\n\tconst kind = request.request;\n\tconst lines: string[] = [`request: ${kind}`];\n\tconst r = request as ParentRequest & {\n\t\tgoal?: string;\n\t\texpected_state?: string;\n\t\tdescription?: string;\n\t\tfields?: string[];\n\t\tobservation?: Observation;\n\t};\n\tif (r.goal) lines.push(`goal: ${r.goal}`);\n\tif (r.expected_state) lines.push(`expected_state: ${r.expected_state}`);\n\tif (r.description) lines.push(`element: ${r.description}`);\n\tif (Array.isArray(r.fields) && r.fields.length) lines.push(`fields: ${r.fields.join(\", \")}`);\n\tconst summary = summarizeObservation(r.observation);\n\tif (summary) lines.push(summary);\n\treturn lines.join(\"\\n\");\n}\n\n/** Compact, model-facing reminder of the inline-flow action schema, appended to\n * an `invalid inline flow` error so the model can self-correct in one turn\n * instead of guessing field shapes across several rounds. */\nconst FLOW_SCHEMA_HINT =\n\t\"Inline flow shape: { id, name, version, start_url, steps: [{ id, action }] }. \" +\n\t\"Action variants (use EXACTLY these fields): \" +\n\t'{ action: \"navigate\", url } | { action: \"click\", selector, fallbacks? } | ' +\n\t'{ action: \"fill\", selector, value_tpl } | { action: \"select\", selector, value_tpl } | ' +\n\t'{ action: \"wait_settle\" } | { action: \"checkpoint\", asserts } | ' +\n\t'{ action: \"decide\", goal } | { action: \"classify\" } | ' +\n\t'{ action: \"verify_visual\", expected_state } | { action: \"extract_semantic\", fields }. ' +\n\t'`extract_semantic.fields` MUST be a string array like [\"person\",\"count\"] (not an object), ' +\n\t\"and it takes no `goal` — only `decide` does.\";\n\n/** If an error is a browsertools inline-flow validation failure, append the\n * schema hint so the model fixes the flow on the next call. Other errors pass\n * through unchanged. */\nfunction enrichFlowError(error: unknown): unknown {\n\tconst message = error instanceof Error ? error.message : String(error);\n\tif (message.includes(\"invalid inline flow\") && !message.includes(\"Action variants\")) {\n\t\treturn new Error(`${message}\\n\\n${FLOW_SCHEMA_HINT}`);\n\t}\n\treturn error;\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.\n *\n * When the serve client is reused across browser_run calls (the\n * reclaimed-idle path), the viewer is already running on the same port, so we\n * skip re-issuing `live_view_start` and skip auto-opening a second OS tab —\n * the user keeps the one tab they already have. */\nasync function startLiveView(client: BrowsertoolsServeClient): Promise<string | undefined> {\n\tif (client.liveViewUrl) {\n\t\treturn `Live view already open at: ${client.liveViewUrl}`;\n\t}\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\tclient.liveViewUrl = result.url;\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\tbrowserConfig?: BrowserClientConfig,\n): Promise<AgentToolResult<BrowserRunDetails>> {\n\tif (outcome.outcome === \"complete\") {\n\t\tif (browserConfig) {\n\t\t\tparkIdleClient(client, browserConfig.headful, browserConfig.browserPath, browserConfig.idleTimeoutMs);\n\t\t} else {\n\t\t\tclient.dispose();\n\t\t}\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\tif (browserConfig) {\n\t\t\tparkIdleClient(client, browserConfig.headful, browserConfig.browserPath, browserConfig.idleTimeoutMs);\n\t\t} else {\n\t\t\tclient.dispose();\n\t\t}\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(\n\t\t\ttoken,\n\t\t\tclient,\n\t\t\trounds,\n\t\t\tbrowserConfig?.idleTimeoutMs ?? opts.idleTimeoutMs,\n\t\t\tbrowserConfig?.headful ?? false,\n\t\t\tbrowserConfig?.browserPath,\n\t\t);\n\n\t\tconst text =\n\t\t\t`Flow suspended — parent decision required (NeedsParent).\\n` +\n\t\t\t`${formatParentRequest(request)}\\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 createBrowserRunToolDefinition(\n\tcwd: string,\n\toptions?: BrowserRunToolOptions,\n): ToolDefinition<typeof browserRunSchema, BrowserRunDetails> {\n\tconst opts = resolveBrowsertoolsOptions(options);\n\treturn defineTool({\n\t\tname: \"browser_run\",\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_continue 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_continue 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_continue `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: browserRunSchema,\n\t\tasync execute(_toolCallId, params: BrowserRunInput, 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_run requires either `flow_path` or `flow`\");\n\t\t\t}\n\n\t\t\tconst binaryPath = await resolveBrowsertoolsBinary(options);\n\t\t\tconst headful = params.headful ?? opts.headful;\n\t\t\t// Reuse the shared idle client if browser config matches, otherwise create a new\n\t\t\t// one. This keeps a single Chromium process and live-view port across calls.\n\t\t\tlet client = takeIdleClient(headful, opts.browserPath, opts.idleTimeoutMs);\n\t\t\tif (!client) {\n\t\t\t\tclient = new BrowsertoolsServeClient(binaryPath, {\n\t\t\t\t\tcwd,\n\t\t\t\t\tbrowserPath: opts.browserPath,\n\t\t\t\t\tserveArgs: opts.serveArgs,\n\t\t\t\t\trequestTimeoutMs: opts.requestTimeoutMs,\n\t\t\t\t\theadful,\n\t\t\t\t});\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 browserConfig: BrowserClientConfig = {\n\t\t\t\t\theadful,\n\t\t\t\t\tbrowserPath: opts.browserPath,\n\t\t\t\t\tidleTimeoutMs: opts.idleTimeoutMs,\n\t\t\t\t};\n\t\t\t\tconst result = await advanceFlow(client, outcome, 1, opts, browserConfig);\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 enrichFlowError(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 createBrowserRunTool(cwd: string, options?: BrowserRunToolOptions): AgentTool<typeof browserRunSchema> {\n\treturn wrapToolDefinition(createBrowserRunToolDefinition(cwd, options));\n}\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
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_run` and `browser_continue` 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_run`\n * call that starts the flow and the `browser_continue` 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\t/** Launch a real on-screen Chromium window instead of headless (sets\n\t * `BROWSERTOOLS_HEADFUL=1` on the serve process). */\n\theadful?: boolean;\n\t/** Default the streamed live viewer on for every flow (the per-call `live_view`\n\t * param still overrides). Set by the --enable-browser-live-preview flag. */\n\tliveView?: boolean;\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\theadful: boolean;\n\tliveView: boolean;\n} {\n\treturn {\n\t\tbinaryPath: options?.binaryPath,\n\t\tbrowserPath: options?.browserPath ?? (process.env.HOOCODE_BROWSERTOOLS_BROWSER_PATH?.trim() || undefined),\n\t\theadful: options?.headful ?? false,\n\t\tliveView: options?.liveView ?? false,\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\t/** URL of the live viewer once `live_view_start` has been issued for this\n\t * serve process, cached so a reused (reclaimed-idle) client does not start a\n\t * second viewer or auto-open another OS tab on the next browser_run call. */\n\tliveViewUrl?: string;\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: {\n\t\t\tcwd?: string;\n\t\t\tbrowserPath?: string;\n\t\t\tserveArgs: string[];\n\t\t\trequestTimeoutMs: number;\n\t\t\theadful?: boolean;\n\t\t},\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// Opt into a real on-screen Chromium window. The serve process reads this\n\t\t// in `Driver::launch`; harmless if an older binary ignores it.\n\t\tif (opts.headful) {\n\t\t\tenv.BROWSERTOOLS_HEADFUL = \"1\";\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\t/** Whether the browser was launched in headful mode for this session. */\n\theadful: boolean;\n\t/** Explicit browser path used for this session, if any. */\n\tbrowserPath?: string;\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\theadful = false,\n\tbrowserPath?: string,\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, headful, browserPath });\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\tdisposeIdleClient();\n}\n\n/** Number of currently parked sessions (for tests/diagnostics). */\nexport function pausedSessionCount(): number {\n\treturn sessionRegistry.size;\n}\n\n// ============================================================================\n// Shared idle client (one per session, reused across browser_run calls)\n// ============================================================================\n\n/** Configuration preserved from the original flow call so the idle client can\n * be matched and reused by a subsequent browser_run with the same settings. */\nexport interface BrowserClientConfig {\n\theadful: boolean;\n\tbrowserPath?: string;\n\tidleTimeoutMs: number;\n}\n\ninterface IdleClientEntry {\n\tclient: BrowsertoolsServeClient;\n\theadful: boolean;\n\tbrowserPath?: string;\n\tidleTimer: ReturnType<typeof setTimeout>;\n}\n\nlet sharedIdleClient: IdleClientEntry | null = null;\n\n/**\n * Try to reclaim the shared idle client if its browser config matches.\n * Returns the live client (and clears the slot) on match, or null.\n */\nexport function takeIdleClient(\n\theadful: boolean,\n\tbrowserPath?: string,\n\t_idleTimeoutMs: number = BROWSERTOOLS_DEFAULT_IDLE_MS,\n): BrowsertoolsServeClient | null {\n\tif (!sharedIdleClient) return null;\n\tclearTimeout(sharedIdleClient.idleTimer);\n\tif (sharedIdleClient.headful === headful && sharedIdleClient.browserPath === browserPath) {\n\t\tconst client = sharedIdleClient.client;\n\t\tsharedIdleClient = null;\n\t\treturn client;\n\t}\n\t// Config changed — dispose the stale idle client.\n\tsharedIdleClient.client.dispose();\n\tsharedIdleClient = null;\n\treturn null;\n}\n\n/**\n * Park a completed/failed flow's client as idle so the next browser_run call\n * can reuse the same Chromium process and live-view port.\n */\nexport function parkIdleClient(\n\tclient: BrowsertoolsServeClient,\n\theadful: boolean,\n\tbrowserPath?: string,\n\tidleTimeoutMs: number = BROWSERTOOLS_DEFAULT_IDLE_MS,\n): void {\n\tif (sharedIdleClient) {\n\t\tclearTimeout(sharedIdleClient.idleTimer);\n\t\tsharedIdleClient.client.dispose();\n\t}\n\tconst idleTimer = setTimeout(() => {\n\t\tif (sharedIdleClient?.client === client) {\n\t\t\tclient.dispose();\n\t\t\tsharedIdleClient = null;\n\t\t}\n\t}, idleTimeoutMs);\n\tidleTimer.unref?.();\n\tsharedIdleClient = { client, headful, browserPath, idleTimer };\n}\n\n/** Dispose the shared idle client. Called on session shutdown. */\nexport function disposeIdleClient(): void {\n\tif (sharedIdleClient) {\n\t\tclearTimeout(sharedIdleClient.idleTimer);\n\t\tsharedIdleClient.client.dispose();\n\t\tsharedIdleClient = null;\n\t}\n}\n\n/** Number of currently idle shared clients (0 or 1, for tests/diagnostics). */\nexport function idleClientCount(): number {\n\treturn sharedIdleClient ? 1 : 0;\n}\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"browsertools-shared.js","sourceRoot":"","sources":["../../../src/core/tools/browsertools-shared.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared plumbing for the `browser_run` and `browser_continue` 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_run`\n * call that starts the flow and the `browser_continue` 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\t/** Launch a real on-screen Chromium window instead of headless (sets\n\t * `BROWSERTOOLS_HEADFUL=1` on the serve process). */\n\theadful?: boolean;\n\t/** Default the streamed live viewer on for every flow (the per-call `live_view`\n\t * param still overrides). Set by the --enable-browser-live-preview flag. */\n\tliveView?: boolean;\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\theadful: boolean;\n\tliveView: boolean;\n} {\n\treturn {\n\t\tbinaryPath: options?.binaryPath,\n\t\tbrowserPath: options?.browserPath ?? (process.env.HOOCODE_BROWSERTOOLS_BROWSER_PATH?.trim() || undefined),\n\t\theadful: options?.headful ?? false,\n\t\tliveView: options?.liveView ?? false,\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\t/** URL of the live viewer once `live_view_start` has been issued for this\n\t * serve process, cached so a reused (reclaimed-idle) client does not start a\n\t * second viewer or auto-open another OS tab on the next browser_run call. */\n\tliveViewUrl?: string;\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: {\n\t\t\tcwd?: string;\n\t\t\tbrowserPath?: string;\n\t\t\tserveArgs: string[];\n\t\t\trequestTimeoutMs: number;\n\t\t\theadful?: boolean;\n\t\t},\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// Opt into a real on-screen Chromium window. The serve process reads this\n\t\t// in `Driver::launch`; harmless if an older binary ignores it.\n\t\tif (opts.headful) {\n\t\t\tenv.BROWSERTOOLS_HEADFUL = \"1\";\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\t/** Whether the browser was launched in headful mode for this session. */\n\theadful: boolean;\n\t/** Explicit browser path used for this session, if any. */\n\tbrowserPath?: string;\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\theadful = false,\n\tbrowserPath?: string,\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, headful, browserPath });\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\tdisposeIdleClient();\n}\n\n/** Number of currently parked sessions (for tests/diagnostics). */\nexport function pausedSessionCount(): number {\n\treturn sessionRegistry.size;\n}\n\n// ============================================================================\n// Shared idle client (one per session, reused across browser_run calls)\n// ============================================================================\n\n/** Configuration preserved from the original flow call so the idle client can\n * be matched and reused by a subsequent browser_run with the same settings. */\nexport interface BrowserClientConfig {\n\theadful: boolean;\n\tbrowserPath?: string;\n\tidleTimeoutMs: number;\n}\n\ninterface IdleClientEntry {\n\tclient: BrowsertoolsServeClient;\n\theadful: boolean;\n\tbrowserPath?: string;\n\tidleTimer: ReturnType<typeof setTimeout>;\n}\n\nlet sharedIdleClient: IdleClientEntry | null = null;\n\n/**\n * Try to reclaim the shared idle client if its browser config matches.\n * Returns the live client (and clears the slot) on match, or null.\n */\nexport function takeIdleClient(\n\theadful: boolean,\n\tbrowserPath?: string,\n\t_idleTimeoutMs: number = BROWSERTOOLS_DEFAULT_IDLE_MS,\n): BrowsertoolsServeClient | null {\n\tif (!sharedIdleClient) return null;\n\tclearTimeout(sharedIdleClient.idleTimer);\n\tif (sharedIdleClient.headful === headful && sharedIdleClient.browserPath === browserPath) {\n\t\tconst client = sharedIdleClient.client;\n\t\tsharedIdleClient = null;\n\t\treturn client;\n\t}\n\t// Config changed — dispose the stale idle client.\n\tsharedIdleClient.client.dispose();\n\tsharedIdleClient = null;\n\treturn null;\n}\n\n/**\n * Park a completed/failed flow's client as idle so the next browser_run call\n * can reuse the same Chromium process and live-view port.\n */\nexport function parkIdleClient(\n\tclient: BrowsertoolsServeClient,\n\theadful: boolean,\n\tbrowserPath?: string,\n\tidleTimeoutMs: number = BROWSERTOOLS_DEFAULT_IDLE_MS,\n): void {\n\tif (sharedIdleClient) {\n\t\tclearTimeout(sharedIdleClient.idleTimer);\n\t\tsharedIdleClient.client.dispose();\n\t}\n\tconst idleTimer = setTimeout(() => {\n\t\tif (sharedIdleClient?.client === client) {\n\t\t\tclient.dispose();\n\t\t\tsharedIdleClient = null;\n\t\t}\n\t}, idleTimeoutMs);\n\tidleTimer.unref?.();\n\tsharedIdleClient = { client, headful, browserPath, idleTimer };\n}\n\n/** Dispose the shared idle client. Called on session shutdown. */\nexport function disposeIdleClient(): void {\n\tif (sharedIdleClient) {\n\t\tclearTimeout(sharedIdleClient.idleTimer);\n\t\tsharedIdleClient.client.dispose();\n\t\tsharedIdleClient = null;\n\t}\n}\n\n/** Number of currently idle shared clients (0 or 1, for tests/diagnostics). */\nexport function idleClientCount(): number {\n\treturn sharedIdleClient ? 1 : 0;\n}\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"docedit.d.ts","sourceRoot":"","sources":["../../../src/core/tools/docedit.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAEjE,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAG5C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAG7D,OAAO,EACN,KAAK,OAAO,EAUZ,MAAM,uBAAuB,CAAC;AAK/B,QAAA,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;EAKjB,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,OAAO,aAAa,CAAC,CAAC;AAE5D,MAAM,WAAW,kBAAkB;IAClC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,kBAAkB;IAClC,uDAAuD;IACvD,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAYD,wBAAgB,2BAA2B,CAC1C,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,kBAAkB,GAC1B,cAAc,CAAC,OAAO,aAAa,EAAE,kBAAkB,GAAG,SAAS,CAAC,CAmGtE;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,SAAS,CAAC,OAAO,aAAa,CAAC,CAE5G","sourcesContent":["import { mkdtempSync, renameSync, rmSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { basename, join } from \"node:path\";\nimport type { AgentTool } from \"@kolisachint/hoocode-agent-core\";\nimport { Container, Text } from \"@kolisachint/hoocode-tui\";\nimport { type Static, Type } from \"typebox\";\nimport { APP_NAME } from \"../../config.js\";\nimport { theme as appTheme } from \"../../modes/interactive/theme/theme.js\";\nimport type { ToolDefinition } from \"../extensions/types.js\";\nimport { renderEnvelopeText } from \"./docread.js\";\nimport { withFileMutationQueue } from \"./file-mutation-queue.js\";\nimport {\n\ttype DocNode,\n\tensureExtractRecord,\n\textractDocument,\n\tfindNodeById,\n\tinvalidateExtractRecord,\n\tpatchOpNodeId,\n\tpatchOpsSchema,\n\treconstructDocument,\n\tStalePatchError,\n\ttoPatch,\n} from \"./filetools-shared.js\";\nimport { resolveReadPath } from \"./path-utils.js\";\nimport { invalidArgText, shortenPath, str } from \"./render-utils.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\n\nconst docEditSchema = Type.Object({\n\tpath: Type.String({\n\t\tdescription: \"Path to the document to edit in place. Must have been opened with DocRead first.\",\n\t}),\n\tpatch: patchOpsSchema,\n});\n\nexport type DocEditToolInput = Static<typeof docEditSchema>;\n\nexport interface DocEditToolDetails {\n\tops?: number;\n\taffected?: DocNode[];\n}\n\nexport interface DocEditToolOptions {\n\t/** Timeout (seconds) for the filetools invocations. */\n\ttimeoutSecs?: number;\n}\n\nfunction formatDocEditCall(args: { path?: string; patch?: unknown[] } | undefined): string {\n\tconst path = str(args?.path);\n\tconst pathDisplay =\n\t\tpath === null ? invalidArgText(appTheme) : path ? shortenPath(path) : appTheme.fg(\"toolOutput\", \"...\");\n\tlet text = appTheme.fg(\"toolTitle\", appTheme.bold(\"DocEdit \")) + appTheme.fg(\"accent\", pathDisplay);\n\tconst ops = Array.isArray(args?.patch) ? args.patch.length : undefined;\n\tif (ops !== undefined) text += appTheme.fg(\"muted\", ` (${ops} op${ops === 1 ? \"\" : \"s\"})`);\n\treturn text;\n}\n\nexport function createDocEditToolDefinition(\n\tcwd: string,\n\toptions?: DocEditToolOptions,\n): ToolDefinition<typeof docEditSchema, DocEditToolDetails | undefined> {\n\treturn {\n\t\tname: \"DocEdit\",\n\t\tlabel: \"DocEdit\",\n\t\tdescription:\n\t\t\t\"Apply an id-based patch to a structured/binary document IN PLACE, losslessly. Targets node ids from a DocRead extract; if the cache is missing or the file changed on disk (e.g. an external script rewrote it) the document is re-extracted automatically, so a separate DocRead is not required. If the patch references ids that no longer exist, the call fails and returns the current structure with fresh ids so you can re-issue. Untouched bytes are preserved exactly; the file is re-extracted after the write so ids stay current for further edits. This is the canonical way to edit these formats: never fall back to ad-hoc scripts (python/openpyxl, docx, PyPDF2, unzip, sed) to rewrite them — that bypasses the lossless id-map and corrupts the file. Off by default; enabled with --enable-filetools.\",\n\t\tpromptSnippet: \"Patch a structured/binary document in place by node id\",\n\t\tpromptGuidelines: [\n\t\t\t\"Use DocEdit to modify a document opened with DocRead; pass a patch of id-based ops (replace/add/remove) targeting the #ids from the extract. It edits in place and re-extracts.\",\n\t\t\t\"Keep patches minimal — only the ops you actually need. DocEdit re-extracts after writing and refreshes ids, so do not DocRead again between edits; the full extract is token-heavy. Reach for a fresh writable DocRead only when ids have gone stale and the failure output isn't enough to continue.\",\n\t\t],\n\t\tparameters: docEditSchema,\n\t\tasync execute(_toolCallId, { path, patch }: DocEditToolInput, signal?: AbortSignal) {\n\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\t\t\tconst absolutePath = resolveReadPath(path, cwd);\n\n\t\t\treturn withFileMutationQueue(absolutePath, async () => {\n\t\t\t\t// Resolve affected nodes from the pre-reconstruct extract: the patch's\n\t\t\t\t// ids are valid against THIS extract, and a later re-extract\n\t\t\t\t// regenerates ids that would not match the patch. reconstructDocument\n\t\t\t\t// auto-extracts when the cache is missing/stale, so capture the record\n\t\t\t\t// the same way to stay in sync with what the patch is applied against.\n\t\t\t\tconst preRecord = await ensureExtractRecord(absolutePath, cwd, signal, {\n\t\t\t\t\ttimeoutSecs: options?.timeoutSecs,\n\t\t\t\t});\n\t\t\t\tconst affectedIds = new Set<string>();\n\t\t\t\tfor (const op of toPatch(patch).patch) {\n\t\t\t\t\tconst id = patchOpNodeId(op);\n\t\t\t\t\tif (id) affectedIds.add(id);\n\t\t\t\t}\n\t\t\t\tconst affectedNodes: DocNode[] = [];\n\t\t\t\tfor (const id of affectedIds) {\n\t\t\t\t\tconst node = findNodeById(preRecord.envelope.structure, id);\n\t\t\t\t\tif (node) affectedNodes.push(node);\n\t\t\t\t}\n\n\t\t\t\t// Reconstruct to a temp file, then atomically swap it into place so a\n\t\t\t\t// failed reconstruct never leaves a half-written document behind.\n\t\t\t\tconst stageDir = mkdtempSync(join(tmpdir(), `${APP_NAME}-docedit-`));\n\t\t\t\tconst stagePath = join(stageDir, basename(absolutePath));\n\t\t\t\ttry {\n\t\t\t\t\tawait reconstructDocument(absolutePath, toPatch(patch), stagePath, cwd, signal, {\n\t\t\t\t\t\ttimeoutSecs: options?.timeoutSecs,\n\t\t\t\t\t});\n\t\t\t\t\trenameSync(stagePath, absolutePath);\n\t\t\t\t} catch (err) {\n\t\t\t\t\tif (err instanceof StalePatchError) {\n\t\t\t\t\t\t// The document was rewritten out-of-band: the cache auto-refreshed\n\t\t\t\t\t\t// but the patch targets ids that no longer exist. Surface the\n\t\t\t\t\t\t// current structure so the agent can re-issue without a DocRead.\n\t\t\t\t\t\tthrow new Error(`${err.message}\\n\\n${renderEnvelopeText(err.envelope, false)}`);\n\t\t\t\t\t}\n\t\t\t\t\tthrow err;\n\t\t\t\t} finally {\n\t\t\t\t\trmSync(stageDir, { recursive: true, force: true });\n\t\t\t\t}\n\n\t\t\t\t// The source changed, so the cached id-map is stale. Re-extract to\n\t\t\t\t// refresh ids for subsequent edits (best-effort; a failure here does\n\t\t\t\t// not undo the successful write).\n\t\t\t\tinvalidateExtractRecord(absolutePath);\n\t\t\t\ttry {\n\t\t\t\t\tawait extractDocument(absolutePath, cwd, signal, { timeoutSecs: options?.timeoutSecs });\n\t\t\t\t} catch {\n\t\t\t\t\t// Re-extract failure leaves no cached record; the next DocEdit will\n\t\t\t\t\t// auto-extract (or fall back to DocRead), which is the correct path.\n\t\t\t\t}\n\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\ttext: `Applied ${patch.length} op${patch.length === 1 ? \"\" : \"s\"} to ${path} (re-extracted; ids refreshed).`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { ops: patch.length, affected: affectedNodes.length > 0 ? affectedNodes : undefined },\n\t\t\t\t};\n\t\t\t});\n\t\t},\n\t\trenderCall(args, _theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatDocEditCall(args));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, _options, _theme, context) {\n\t\t\tif (!context.isError) {\n\t\t\t\tconst component = (context.lastComponent as Container | undefined) ?? new Container();\n\t\t\t\tcomponent.clear();\n\t\t\t\treturn component;\n\t\t\t}\n\t\t\tconst output = (result.content as Array<{ type: string; text?: string }>)\n\t\t\t\t.filter((c) => c.type === \"text\")\n\t\t\t\t.map((c) => c.text || \"\")\n\t\t\t\t.join(\"\\n\");\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(output ? `\\n${appTheme.fg(\"error\", output)}` : \"\");\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createDocEditTool(cwd: string, options?: DocEditToolOptions): AgentTool<typeof docEditSchema> {\n\treturn wrapToolDefinition(createDocEditToolDefinition(cwd, options));\n}\n"]}
|