pi-agent-browser-native 0.2.45 → 0.2.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@
4
4
 
5
5
  No changes yet.
6
6
 
7
+ ## 0.2.46 - 2026-06-07
8
+
9
+ ### Changed
10
+
11
+ - Reduced native extension startup cost by replacing heavy top-level Pi and TypeBox runtime imports with lightweight local schema and event helpers while preserving the public tool schemas.
12
+ - Kept custom browser tool rendering compact without importing Pi's full coding-agent runtime during extension load.
13
+
14
+ ### Fixed
15
+
16
+ - Fixed issue #84 by cutting local cold extension import plus factory registration from roughly 1.1 seconds to roughly 76 milliseconds in checkout measurements.
17
+ - Stabilized timeout partial-progress diagnostics so post-navigation timeouts are recognized from later completed-step evidence even when live URL recovery is unavailable under load.
18
+
19
+ ### Validation
20
+
21
+ - Added a cold-start budget test for the real extension entrypoint, schema parity coverage against the canonical TypeBox/StringEnum builders, rendering coverage for JSON highlighting plus collapsed-output expand hints, and stabilized deterministic dogfood smoke by avoiding a rapid Windows browser close/relaunch race.
22
+
7
23
  ## 0.2.45 - 2026-06-06
8
24
 
9
25
  ### Added
@@ -10,11 +10,10 @@ import type { ChildProcess } from "node:child_process";
10
10
  import { dirname, join, resolve } from "node:path";
11
11
  import { fileURLToPath } from "node:url";
12
12
 
13
- import {
14
- isToolCallEventType,
15
- type AgentToolResult,
16
- type ExtensionAPI,
17
- type ExtensionContext,
13
+ import type {
14
+ AgentToolResult,
15
+ ExtensionAPI,
16
+ ExtensionContext,
18
17
  } from "@earendil-works/pi-coding-agent";
19
18
  import { Text } from "@earendil-works/pi-tui";
20
19
  import {
@@ -91,7 +90,7 @@ import {
91
90
  restoreElectronLaunchRecordsFromBranch,
92
91
  type ElectronLaunchRecord,
93
92
  } from "./lib/orchestration/electron-host/index.js";
94
- import { buildValidationFailureResult, resolveAgentBrowserInput } from "./lib/orchestration/input-plan.js";
93
+ import { buildValidationFailureResult, resolveAgentBrowserInput, type AgentBrowserExecuteParams } from "./lib/orchestration/input-plan.js";
95
94
  import { applyAgentBrowserOutputPath } from "./lib/orchestration/output-file.js";
96
95
  import type { NetworkRouteRecord } from "./lib/results/contracts.js";
97
96
  import type { SessionArtifactManifest } from "./lib/results/contracts.js";
@@ -131,6 +130,16 @@ import {
131
130
 
132
131
  const DEFAULT_SESSION_MODE = "auto" as const;
133
132
 
133
+ type BashToolCallLike = {
134
+ input: { command: string };
135
+ toolName: "bash";
136
+ };
137
+
138
+ function isBashToolCallEvent(event: unknown): event is BashToolCallLike {
139
+ if (!isRecord(event) || event.toolName !== "bash" || !isRecord(event.input)) return false;
140
+ return typeof event.input.command === "string";
141
+ }
142
+
134
143
  type OwnedManagedSession = {
135
144
  branchOwned: boolean;
136
145
  cwd: string;
@@ -753,7 +762,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
753
762
  pi.on("tool_call", async (event, ctx) => {
754
763
  const promptPolicy = buildPromptPolicy(getLatestUserPrompt(ctx.sessionManager.getBranch()));
755
764
  if (
756
- isToolCallEventType("bash", event) &&
765
+ isBashToolCallEvent(event) &&
757
766
  !promptPolicy.allowLegacyAgentBrowserBash &&
758
767
  looksLikeDirectAgentBrowserBash(event.input.command) &&
759
768
  !isHarmlessAgentBrowserInspectionCommand(event.input.command) &&
@@ -789,7 +798,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
789
798
  component.setState(formatAgentBrowserRenderResult(result, options, theme, context.isError), options.expanded, theme);
790
799
  return component;
791
800
  },
792
- async execute(_toolCallId, params, signal, onUpdate, ctx) {
801
+ async execute(_toolCallId, params: AgentBrowserExecuteParams, signal, onUpdate, ctx) {
793
802
  const promptPolicy = buildPromptPolicy(getLatestUserPrompt(ctx.sessionManager.getBranch()));
794
803
  const outputPath = isRecord(params) && typeof params.outputPath === "string" ? params.outputPath : undefined;
795
804
  const resolvedInput = resolveAgentBrowserInput({
@@ -4,8 +4,8 @@
4
4
  * Scope: Schema-only; behavioral validation lives in the mode compilers.
5
5
  */
6
6
 
7
- import { StringEnum } from "@earendil-works/pi-ai";
8
- import { Type } from "typebox";
7
+ import { JsonSchema, type JsonSchemaBuilder } from "../json-schema.js";
8
+ import { StringEnum as localStringEnum, type StringEnumBuilder } from "../string-enum-schema.js";
9
9
 
10
10
  import {
11
11
  ELECTRON_DISCOVERY_DEFAULT_MAX_RESULTS,
@@ -23,7 +23,11 @@ import {
23
23
  SOURCE_LOOKUP_MAX_WORKSPACE_FILES,
24
24
  } from "./types.js";
25
25
 
26
- export const AGENT_BROWSER_PARAMS = Type.Object({
26
+ export function createAgentBrowserParamsSchema(
27
+ Type: JsonSchemaBuilder = JsonSchema,
28
+ StringEnum: StringEnumBuilder = localStringEnum,
29
+ ) {
30
+ return Type.Object({
27
31
 
28
32
  args: Type.Optional(
29
33
  Type.Array(Type.String({ description: "Exact agent-browser CLI arguments, excluding the binary name. Do not pass --json; the wrapper injects it. First-call recipe: open → snapshot -i → click/fill @eN → snapshot -i." }), {
@@ -195,4 +199,7 @@ export const AGENT_BROWSER_PARAMS = Type.Object({
195
199
  default: DEFAULT_SESSION_MODE,
196
200
  }),
197
201
  ),
198
- }, { additionalProperties: false });
202
+ }, { additionalProperties: false });
203
+ }
204
+
205
+ export const AGENT_BROWSER_PARAMS = createAgentBrowserParamsSchema();
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Purpose: Build the small JSON Schema subset used by Pi tool schemas without importing TypeBox at runtime.
3
+ * Responsibilities: Preserve plain JSON Schema objects Pi consumes while keeping extension startup cheap.
4
+ * Scope: Schema construction only; runtime validation still belongs to Pi and the tool input compilers.
5
+ */
6
+
7
+ import type { TSchema, TSchemaOptions, TUnsafe } from "typebox";
8
+
9
+ const OPTIONAL_SCHEMA = Symbol("pi-agent-browser-optional-schema");
10
+
11
+ type SchemaObject = TSchema & { [OPTIONAL_SCHEMA]?: true };
12
+ type SchemaProperties = Record<string, TSchema>;
13
+
14
+ function withOptions(schema: Record<string, unknown>, options?: TSchemaOptions): TSchema {
15
+ return { ...schema, ...(options ?? {}) } as TSchema;
16
+ }
17
+
18
+ function literalType(value: unknown): "boolean" | "number" | "string" | undefined {
19
+ const valueType = typeof value;
20
+ return valueType === "string" || valueType === "number" || valueType === "boolean" ? valueType : undefined;
21
+ }
22
+
23
+ function propertySchema(schema: TSchema): TSchema {
24
+ const clone = { ...(schema as SchemaObject & Record<PropertyKey, unknown>) };
25
+ delete clone[OPTIONAL_SCHEMA];
26
+ return clone as TSchema;
27
+ }
28
+
29
+ export const JsonSchema = {
30
+ Array(items: TSchema, options?: TSchemaOptions): TSchema {
31
+ return withOptions({ type: "array", items }, options);
32
+ },
33
+ Boolean(options?: TSchemaOptions): TSchema {
34
+ return withOptions({ type: "boolean" }, options);
35
+ },
36
+ Integer(options?: TSchemaOptions): TSchema {
37
+ return withOptions({ type: "integer" }, options);
38
+ },
39
+ Literal(value: unknown, options?: TSchemaOptions): TSchema {
40
+ const type = literalType(value);
41
+ return withOptions(type ? { type, const: value } : { const: value }, options);
42
+ },
43
+ Number(options?: TSchemaOptions): TSchema {
44
+ return withOptions({ type: "number" }, options);
45
+ },
46
+ Object(properties: SchemaProperties, options?: TSchemaOptions): TSchema {
47
+ const required = globalThis.Object.entries(properties)
48
+ .filter(([, schema]) => (schema as SchemaObject)[OPTIONAL_SCHEMA] !== true)
49
+ .map(([key]) => key);
50
+ return withOptions({
51
+ type: "object",
52
+ properties: globalThis.Object.fromEntries(
53
+ globalThis.Object.entries(properties).map(([key, schema]) => [key, propertySchema(schema)]),
54
+ ),
55
+ ...(required.length > 0 ? { required } : {}),
56
+ }, options);
57
+ },
58
+ Optional(schema: TSchema): TSchema {
59
+ return { ...(schema as SchemaObject), [OPTIONAL_SCHEMA]: true } as TSchema;
60
+ },
61
+ String(options?: TSchemaOptions): TSchema {
62
+ return withOptions({ type: "string" }, options);
63
+ },
64
+ Union(types: TSchema[], options?: TSchemaOptions): TSchema {
65
+ return withOptions({ anyOf: types }, options);
66
+ },
67
+ Unsafe<Value>(schema: TSchema): TUnsafe<Value> {
68
+ return schema as TUnsafe<Value>;
69
+ },
70
+ };
71
+
72
+ export type JsonSchemaBuilder = typeof JsonSchema;
73
+ export type { TSchema, TSchemaOptions, TUnsafe };
@@ -824,6 +824,12 @@ function buildTimeoutProgressSteps(options: {
824
824
  step.reason = "Later step completion evidence indicates the batch advanced past this step before timeout.";
825
825
  }
826
826
  }
827
+ for (const step of progressSteps) {
828
+ const command = step.args[0];
829
+ if (step.status === "completed" && (isOpenNavigationCommand(command) || command === "pushstate")) {
830
+ lastCompletedNavigationIndex = Math.max(lastCompletedNavigationIndex ?? 0, step.index);
831
+ }
832
+ }
827
833
  for (const step of progressSteps) {
828
834
  if (step.status === "completed") continue;
829
835
  if (!retryStep) {
@@ -1,6 +1,5 @@
1
1
  import type { AgentToolResult, Theme, ToolResultEvent } from "@earendil-works/pi-coding-agent";
2
- import { highlightCode, keyHint } from "@earendil-works/pi-coding-agent";
3
- import { Text, truncateToWidth } from "@earendil-works/pi-tui";
2
+ import { getKeybindings, Text, truncateToWidth } from "@earendil-works/pi-tui";
4
3
 
5
4
  import {
6
5
  compileAgentBrowserElectron,
@@ -16,6 +15,7 @@ import { redactInvocationArgs } from "./runtime.js";
16
15
  const TUI_INVOCATION_PREVIEW_MAX_CHARS = 160;
17
16
  const TUI_COLLAPSED_OUTPUT_MAX_LINES = 12;
18
17
  const ANSI_CONTROL_SEQUENCE_PATTERN = /\x1B(?:\][^\x07\x1B]*(?:\x07|\x1B\\)|\[[0-?]*[ -/]*[@-~]|P[^\x1B]*(?:\x1B\\)|_[^\x1B]*(?:\x1B\\)|\^[^\x1B]*(?:\x1B\\)|[@-Z\\-_])/g;
18
+ const JSON_TOKEN_PATTERN = /"(?:\\.|[^"\\])*"(?=\s*:)|"(?:\\.|[^"\\])*"|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null|[{}\[\],:]/g;
19
19
  const UNSAFE_DISPLAY_CONTROL_PATTERN = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\x80-\x9F]/g;
20
20
 
21
21
  function sanitizeDisplayText(value: string): string {
@@ -48,6 +48,26 @@ function isJsonDocumentText(value: string): boolean {
48
48
  }
49
49
  }
50
50
 
51
+ function colorizeJsonLine(line: string, theme: Theme): string {
52
+ let output = "";
53
+ let cursor = 0;
54
+ for (const match of line.matchAll(JSON_TOKEN_PATTERN)) {
55
+ const token = match[0];
56
+ const index = match.index ?? 0;
57
+ output += line.slice(cursor, index);
58
+ const color = token.startsWith('"')
59
+ ? /"\s*$/.test(token) && line.slice(index + token.length).trimStart().startsWith(":")
60
+ ? "syntaxVariable"
61
+ : "syntaxString"
62
+ : /^[{}\[\],:]$/.test(token)
63
+ ? "syntaxPunctuation"
64
+ : "syntaxType";
65
+ output += theme.fg(color, token);
66
+ cursor = index + token.length;
67
+ }
68
+ return output + line.slice(cursor);
69
+ }
70
+
51
71
  function getPrimaryTextContent(result: AgentToolResult<unknown>): string {
52
72
  const textContent = result.content.find((item) => item.type === "text");
53
73
  return textContent?.type === "text" ? textContent.text : "";
@@ -57,23 +77,24 @@ function colorizeToolOutputLines(outputText: string, theme: Theme, isError: bool
57
77
  const normalizedLines = trimTrailingBlankLines(replaceTabsForDisplay(sanitizeDisplayText(outputText)).split("\n"));
58
78
  const normalizedText = normalizedLines.join("\n");
59
79
  if (normalizedText.length === 0) return [];
60
- if (isJsonDocumentText(normalizedText)) {
61
- return highlightCode(normalizedText, "json");
62
- }
80
+ const isJsonDocument = !isError && isJsonDocumentText(normalizedText);
63
81
  return normalizedLines.map((line) => {
64
82
  if (line.length === 0) {
65
83
  return "";
66
84
  }
85
+ if (isJsonDocument) return colorizeJsonLine(line, theme);
67
86
  return isError ? theme.fg("error", line) : theme.fg("toolOutput", line);
68
87
  });
69
88
  }
70
89
 
71
90
  function formatExpandHint(theme: Theme): string {
72
91
  try {
73
- return keyHint("app.tools.expand", "to expand");
92
+ const [key] = getKeybindings().getKeys("app.tools.expand" as never);
93
+ if (key) return `${theme.fg("dim", key)} ${theme.fg("muted", "to expand")}`;
74
94
  } catch {
75
- return `${theme.fg("dim", "ctrl+o")} ${theme.fg("muted", "to expand")}`;
95
+ // Fall through to the built-in default key when coding-agent keybindings are unavailable.
76
96
  }
97
+ return `${theme.fg("dim", "ctrl+o")} ${theme.fg("muted", "to expand")}`;
77
98
  }
78
99
 
79
100
  function formatVisualTruncationNotice(remainingLines: number, totalLines: number, theme: Theme, width: number): string {
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Purpose: Build compact JSON-schema string enums without importing pi runtime helpers.
3
+ * Responsibilities: Mirror pi-ai StringEnum's `{ type: "string", enum: [...] }` shape while keeping extension startup imports light.
4
+ * Scope: Schema construction only.
5
+ */
6
+
7
+ import { JsonSchema, type TSchemaOptions, type TUnsafe } from "./json-schema.js";
8
+
9
+ export type StringEnumBuilder = typeof StringEnum;
10
+
11
+ export function StringEnum<const Values extends readonly string[]>(
12
+ values: Values,
13
+ options?: TSchemaOptions,
14
+ ): TUnsafe<Values[number]> {
15
+ return JsonSchema.Unsafe<Values[number]>({
16
+ type: "string",
17
+ enum: [...values],
18
+ ...options,
19
+ });
20
+ }
@@ -4,9 +4,8 @@
4
4
  * Scope: Live web search only; browser automation remains in the `agent_browser` tool.
5
5
  */
6
6
 
7
- import { StringEnum } from "@earendil-works/pi-ai";
8
- import { defineTool } from "@earendil-works/pi-coding-agent";
9
- import { Type } from "typebox";
7
+ import { JsonSchema, type JsonSchemaBuilder } from "./json-schema.js";
8
+ import { StringEnum as localStringEnum, type StringEnumBuilder } from "./string-enum-schema.js";
10
9
  import {
11
10
  DEFAULT_WEB_SEARCH_PROVIDER,
12
11
  WEB_SEARCH_PROVIDERS,
@@ -119,8 +118,12 @@ export interface WebSearchProviderAdapter<Request = unknown, Response = unknown>
119
118
  provider: WebSearchProvider;
120
119
  }
121
120
 
122
- export const AgentBrowserWebSearchParams = Type.Object(
123
- {
121
+ export function createAgentBrowserWebSearchParamsSchema(
122
+ Type: JsonSchemaBuilder = JsonSchema,
123
+ StringEnum: StringEnumBuilder = localStringEnum,
124
+ ) {
125
+ return Type.Object(
126
+ {
124
127
  query: Type.String({
125
128
  minLength: 1,
126
129
  description: "Search query to run with the configured Exa or Brave web search provider.",
@@ -172,9 +175,12 @@ export const AgentBrowserWebSearchParams = Type.Object(
172
175
  description: "Optional freshness window: pd=past day, pw=past week, pm=past month, py=past year.",
173
176
  }),
174
177
  ),
175
- },
176
- { additionalProperties: false },
177
- );
178
+ },
179
+ { additionalProperties: false },
180
+ );
181
+ }
182
+
183
+ export const AgentBrowserWebSearchParams = createAgentBrowserWebSearchParamsSchema();
178
184
 
179
185
  const HTML_ENTITY_REPLACEMENTS: Readonly<Record<string, string>> = {
180
186
  amp: "&",
@@ -644,9 +650,21 @@ function buildMissingCredentialError(provider: WebSearchProviderParam): string {
644
650
  return "No Exa or Brave web search credential resolved. Configure webSearch.exaApiKey or webSearch.braveApiKey, or load EXA_API_KEY/BRAVE_API_KEY in the runtime environment.";
645
651
  }
646
652
 
653
+ type AgentBrowserWebSearchParamsInput = {
654
+ country?: string;
655
+ count?: number;
656
+ freshness?: SearchFreshness;
657
+ offset?: number;
658
+ provider?: WebSearchProviderParam;
659
+ query: string;
660
+ safesearch?: "off" | "moderate" | "strict";
661
+ searchLang?: string;
662
+ searchType?: ExaSearchType;
663
+ };
664
+
647
665
  export function createAgentBrowserWebSearchTool(configState: AgentBrowserConfigState) {
648
666
  const requestGate = new WebSearchRequestGate();
649
- return defineTool({
667
+ return {
650
668
  name: AGENT_BROWSER_WEB_SEARCH_TOOL_NAME,
651
669
  label: "Agent Browser Web Search",
652
670
  description: `Search the web with Exa or Brave when configured. Returns up to ${MAX_SEARCH_RESULT_COUNT} concise web results.`,
@@ -659,7 +677,7 @@ export function createAgentBrowserWebSearchTool(configState: AgentBrowserConfigS
659
677
  "After using agent_browser_web_search, cite result URLs in the final answer when web evidence informed the answer.",
660
678
  ],
661
679
  parameters: AgentBrowserWebSearchParams,
662
- async execute(_toolCallId, params, signal) {
680
+ async execute(_toolCallId: string, params: AgentBrowserWebSearchParamsInput, signal?: AbortSignal) {
663
681
  if (!configState.webSearchEnabled) {
664
682
  throw new Error("agent_browser_web_search is disabled by pi-agent-browser-native config.");
665
683
  }
@@ -695,9 +713,9 @@ export function createAgentBrowserWebSearchTool(configState: AgentBrowserConfigS
695
713
  results: normalized.results,
696
714
  };
697
715
  return {
698
- content: [{ type: "text", text: formatSearchResults(adapter.provider, normalized.returnedQuery, normalized.results) }],
716
+ content: [{ type: "text" as const, text: formatSearchResults(adapter.provider, normalized.returnedQuery, normalized.results) }],
699
717
  details,
700
718
  };
701
719
  },
702
- });
720
+ };
703
721
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-agent-browser-native",
3
- "version": "0.2.45",
3
+ "version": "0.2.46",
4
4
  "description": "pi extension that exposes agent-browser as a native tool for browser automation",
5
5
  "type": "module",
6
6
  "author": "Mitch Fultz (https://github.com/fitchmultz)",