@oh-my-pi/pi-coding-agent 10.3.2 → 10.6.0
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 +61 -0
- package/package.json +16 -11
- package/src/capability/index.ts +9 -0
- package/src/cli/update-cli.ts +2 -5
- package/src/config/settings-schema.ts +39 -1
- package/src/cursor.ts +1 -1
- package/src/extensibility/custom-tools/wrapper.ts +9 -33
- package/src/extensibility/extensions/wrapper.ts +18 -31
- package/src/extensibility/hooks/tool-wrapper.ts +6 -16
- package/src/extensibility/tool-proxy.ts +25 -0
- package/src/index.ts +1 -0
- package/src/ipy/executor.ts +107 -3
- package/src/ipy/gateway-coordinator.ts +0 -4
- package/src/ipy/kernel.ts +65 -175
- package/src/main.ts +17 -0
- package/src/mcp/render.ts +10 -226
- package/src/modes/components/tool-execution.ts +83 -96
- package/src/modes/controllers/input-controller.ts +38 -0
- package/src/modes/interactive-mode.ts +13 -0
- package/src/patch/index.ts +1 -0
- package/src/prompts/system/system-prompt.md +5 -2
- package/src/prompts/tools/ask.md +6 -9
- package/src/prompts/tools/browser.md +26 -0
- package/src/prompts/tools/grep.md +4 -8
- package/src/prompts/tools/task.md +29 -4
- package/src/sdk.ts +21 -0
- package/src/session/session-manager.ts +1 -0
- package/src/task/executor.ts +5 -47
- package/src/tools/ask.ts +60 -71
- package/src/tools/bash.ts +1 -0
- package/src/tools/browser.ts +1138 -0
- package/src/tools/find.ts +11 -2
- package/src/tools/grep.ts +111 -107
- package/src/tools/index.ts +4 -0
- package/src/tools/json-tree.ts +231 -0
- package/src/tools/notebook.ts +1 -0
- package/src/tools/puppeteer/00_stealth_tampering.txt +63 -0
- package/src/tools/puppeteer/01_stealth_activity.txt +20 -0
- package/src/tools/puppeteer/02_stealth_hairline.txt +11 -0
- package/src/tools/puppeteer/03_stealth_botd.txt +384 -0
- package/src/tools/puppeteer/04_stealth_iframe.txt +81 -0
- package/src/tools/puppeteer/05_stealth_webgl.txt +75 -0
- package/src/tools/puppeteer/06_stealth_screen.txt +72 -0
- package/src/tools/puppeteer/07_stealth_fonts.txt +97 -0
- package/src/tools/puppeteer/08_stealth_audio.txt +51 -0
- package/src/tools/puppeteer/09_stealth_locale.txt +46 -0
- package/src/tools/puppeteer/10_stealth_plugins.txt +206 -0
- package/src/tools/puppeteer/11_stealth_hardware.txt +8 -0
- package/src/tools/puppeteer/12_stealth_codecs.txt +40 -0
- package/src/tools/puppeteer/13_stealth_worker.txt +74 -0
- package/src/tools/python.ts +1 -0
- package/src/tools/ssh.ts +1 -0
- package/src/tools/todo-write.ts +1 -0
- package/src/tools/write.ts +1 -0
- package/src/web/search/index.ts +15 -4
- package/src/web/search/providers/jina.ts +76 -0
- package/src/web/search/render.ts +3 -1
- package/src/web/search/types.ts +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,67 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [10.6.0] - 2026-02-04
|
|
6
|
+
### Breaking Changes
|
|
7
|
+
|
|
8
|
+
- Removed `output_mode` parameter from grep tool—results now always use content mode with formatted match output
|
|
9
|
+
- Renamed grep context parameters from `context_pre`/`context_post` to `pre`/`post`
|
|
10
|
+
- Removed `n` (show line numbers) parameter—line numbers are now always displayed in grep results
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Added Jina as a web search provider option alongside Exa, Perplexity, and Anthropic
|
|
15
|
+
- Added support for Jina Reader API integration with automatic provider detection when JINA_API_KEY is configured
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Reformatted grep output to display matches grouped by file with numbered match headers and aligned context lines
|
|
20
|
+
- Updated grep output to use `>>` prefix for match lines and aligned spacing for context lines for improved readability
|
|
21
|
+
- Changed multiline matching to automatically enable when pattern contains literal newlines (`
|
|
22
|
+
`)
|
|
23
|
+
- Split grep context parameter into separate `context_pre` and `context_post` options for independent control of lines before and after matches
|
|
24
|
+
- Updated grep tool to use configurable default context settings from `grep.contextBefore` and `grep.contextAfter` configuration
|
|
25
|
+
- Added configurable grep context defaults and reduced the default to 1 line before, 3 lines after
|
|
26
|
+
- Enabled the browser tool by default
|
|
27
|
+
|
|
28
|
+
### Removed
|
|
29
|
+
|
|
30
|
+
- Removed `filesWithMatches` and `count` output modes from grep tool
|
|
31
|
+
|
|
32
|
+
## [10.5.0] - 2026-02-04
|
|
33
|
+
|
|
34
|
+
### Breaking Changes
|
|
35
|
+
|
|
36
|
+
- Changed `ask` tool to require `questions` array parameter; single-question mode with `question`, `options`, `multi`, and `recommended` parameters is no longer supported
|
|
37
|
+
- Removed support for local Python kernel gateway startup; shared gateway is now required
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- Added browser tool powered by Ulixee Hero with support for navigation, DOM interaction, screenshots, and readable content extraction
|
|
42
|
+
- Added `/browser` command to toggle browser headless vs visible mode in interactive sessions
|
|
43
|
+
- Added `browser.enabled` and `browser.headless` settings to control browser automation behavior
|
|
44
|
+
- Added Python prelude caching to improve startup performance by storing compiled prelude helpers and module metadata
|
|
45
|
+
- Added `OMP_DEBUG_STARTUP` environment variable for conditional startup performance debugging output
|
|
46
|
+
- Added autonomous memory system with storage, memory tools, and context injection
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- Updated task tool guidance to enforce small, well-defined task scope with maximum 3-5 files per task to prevent timeouts and improve parallel execution
|
|
51
|
+
- Updated browser viewport to use 1.25x device scale factor for improved rendering on high-DPI displays
|
|
52
|
+
- Modified device pixel ratio detection to respect actual screen capabilities instead of forcing 1x ratio
|
|
53
|
+
- Updated system prompt guidance to state assumptions and proceed without asking for confirmation, reducing unnecessary round-trips
|
|
54
|
+
- Tightened `ask` tool conditions to require multiple approaches with significantly different tradeoffs before prompting user
|
|
55
|
+
- Strengthened `ask` tool guidance to default to action and only ask when genuinely blocked by decisions with materially different outcomes
|
|
56
|
+
- Changed refactor workflow to automatically remove now-unused elements and note removals instead of asking for confirmation
|
|
57
|
+
- Enforced exclusive concurrency mode for all file-modifying tools (edit, write, bash, python, ssh, todo-write) to prevent concurrent execution conflicts
|
|
58
|
+
- Updated `ask` tool guidance to prioritize proactive problem-solving and default to action, asking only when truly blocked by decisions that materially change scope or behavior
|
|
59
|
+
- Changed Python kernel initialization to require shared gateway mode; local gateway startup has been removed
|
|
60
|
+
- Changed shared gateway error handling to retry on server errors (5xx status codes) before failing
|
|
61
|
+
|
|
62
|
+
### Fixed
|
|
63
|
+
|
|
64
|
+
- Fixed glob search returning no results when all files are ignored by gitignore by automatically retrying without gitignore filtering
|
|
65
|
+
|
|
5
66
|
## [10.3.2] - 2026-02-03
|
|
6
67
|
### Added
|
|
7
68
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.6.0",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -79,31 +79,36 @@
|
|
|
79
79
|
"test": "bun test"
|
|
80
80
|
},
|
|
81
81
|
"dependencies": {
|
|
82
|
-
"@
|
|
83
|
-
"@oh-my-pi/
|
|
84
|
-
"@oh-my-pi/pi-
|
|
85
|
-
"@oh-my-pi/pi-
|
|
86
|
-
"@oh-my-pi/pi-
|
|
87
|
-
"@oh-my-pi/pi-
|
|
88
|
-
"@
|
|
82
|
+
"@mozilla/readability": "0.6.0",
|
|
83
|
+
"@oh-my-pi/omp-stats": "10.6.0",
|
|
84
|
+
"@oh-my-pi/pi-agent-core": "10.6.0",
|
|
85
|
+
"@oh-my-pi/pi-ai": "10.6.0",
|
|
86
|
+
"@oh-my-pi/pi-natives": "10.6.0",
|
|
87
|
+
"@oh-my-pi/pi-tui": "10.6.0",
|
|
88
|
+
"@oh-my-pi/pi-utils": "10.6.0",
|
|
89
|
+
"@openai/agents": "^0.4.5",
|
|
89
90
|
"@sinclair/typebox": "^0.34.48",
|
|
90
91
|
"ajv": "^8.17.1",
|
|
91
92
|
"chalk": "^5.6.2",
|
|
92
93
|
"diff": "^8.0.3",
|
|
93
94
|
"file-type": "^21.3.0",
|
|
94
|
-
"glob": "^13.0.
|
|
95
|
+
"glob": "^13.0.1",
|
|
95
96
|
"handlebars": "^4.7.8",
|
|
96
97
|
"ignore": "^7.0.5",
|
|
98
|
+
"jsdom": "28.0.0",
|
|
97
99
|
"marked": "^17.0.1",
|
|
98
100
|
"nanoid": "^5.1.6",
|
|
99
101
|
"node-html-parser": "^7.0.2",
|
|
102
|
+
"puppeteer": "^24.36.1",
|
|
100
103
|
"smol-toml": "^1.6.0",
|
|
101
104
|
"zod": "^4.3.6"
|
|
102
105
|
},
|
|
103
106
|
"devDependencies": {
|
|
104
|
-
"@types/diff": "^
|
|
107
|
+
"@types/diff": "^7.0.2",
|
|
108
|
+
"@types/jsdom": "27.0.0",
|
|
109
|
+
"bun-types": "^1.3.8",
|
|
105
110
|
"@types/ms": "^2.1.0",
|
|
106
|
-
"@types/
|
|
111
|
+
"@types/bun": "^1.3.8",
|
|
107
112
|
"ms": "^2.1.3"
|
|
108
113
|
},
|
|
109
114
|
"keywords": [
|
package/src/capability/index.ts
CHANGED
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import * as os from "node:os";
|
|
10
10
|
import * as path from "node:path";
|
|
11
|
+
|
|
12
|
+
/** Conditional startup debug prints (stderr) when OMP_DEBUG_STARTUP is set */
|
|
13
|
+
const debugStartup = process.env.OMP_DEBUG_STARTUP
|
|
14
|
+
? (stage: string) => process.stderr.write(`[startup] ${stage}\n`)
|
|
15
|
+
: () => {};
|
|
16
|
+
|
|
11
17
|
import type { Settings } from "../config/settings";
|
|
12
18
|
import { clearCache as clearFsCache, cacheStats as fsCacheStats, invalidate as invalidateFs } from "./fs";
|
|
13
19
|
import type {
|
|
@@ -109,9 +115,12 @@ async function loadImpl<T>(
|
|
|
109
115
|
const results = await Promise.all(
|
|
110
116
|
providers.map(async provider => {
|
|
111
117
|
try {
|
|
118
|
+
debugStartup(`capability:${capability.id}:${provider.id}:start`);
|
|
112
119
|
const result = await provider.load(ctx);
|
|
120
|
+
debugStartup(`capability:${capability.id}:${provider.id}:done`);
|
|
113
121
|
return { provider, result };
|
|
114
122
|
} catch (error) {
|
|
123
|
+
debugStartup(`capability:${capability.id}:${provider.id}:error`);
|
|
115
124
|
return { provider, error };
|
|
116
125
|
}
|
|
117
126
|
}),
|
package/src/cli/update-cli.ts
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
import { execSync, spawnSync } from "node:child_process";
|
|
8
8
|
import * as fs from "node:fs";
|
|
9
9
|
import * as path from "node:path";
|
|
10
|
-
import { Readable } from "node:stream";
|
|
11
10
|
import { pipeline } from "node:stream/promises";
|
|
12
11
|
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
13
12
|
import chalk from "chalk";
|
|
@@ -200,8 +199,7 @@ async function updateViaBinary(release: ReleaseInfo): Promise<void> {
|
|
|
200
199
|
}
|
|
201
200
|
|
|
202
201
|
const fileStream = fs.createWriteStream(tempPath, { mode: 0o755 });
|
|
203
|
-
|
|
204
|
-
await pipeline(nodeStream, fileStream);
|
|
202
|
+
await pipeline(response.body, fileStream);
|
|
205
203
|
|
|
206
204
|
// Download native addon
|
|
207
205
|
console.log(chalk.dim(`Downloading ${nativeAddonName}...`));
|
|
@@ -212,8 +210,7 @@ async function updateViaBinary(release: ReleaseInfo): Promise<void> {
|
|
|
212
210
|
}
|
|
213
211
|
|
|
214
212
|
const nativeFileStream = fs.createWriteStream(nativeTempPath, { mode: 0o755 });
|
|
215
|
-
|
|
216
|
-
await pipeline(nativeNodeStream, nativeFileStream);
|
|
213
|
+
await pipeline(nativeResponse.body, nativeFileStream);
|
|
217
214
|
|
|
218
215
|
// Replace current binary
|
|
219
216
|
console.log(chalk.dim("Installing update..."));
|
|
@@ -329,6 +329,26 @@ export const SETTINGS_SCHEMA = {
|
|
|
329
329
|
default: true,
|
|
330
330
|
ui: { tab: "tools", label: "Enable Grep", description: "Enable the grep tool for content searching" },
|
|
331
331
|
},
|
|
332
|
+
"grep.contextBefore": {
|
|
333
|
+
type: "number",
|
|
334
|
+
default: 1,
|
|
335
|
+
ui: {
|
|
336
|
+
tab: "tools",
|
|
337
|
+
label: "Grep context before",
|
|
338
|
+
description: "Lines of context before each grep match",
|
|
339
|
+
submenu: true,
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
"grep.contextAfter": {
|
|
343
|
+
type: "number",
|
|
344
|
+
default: 3,
|
|
345
|
+
ui: {
|
|
346
|
+
tab: "tools",
|
|
347
|
+
label: "Grep context after",
|
|
348
|
+
description: "Lines of context after each grep match",
|
|
349
|
+
submenu: true,
|
|
350
|
+
},
|
|
351
|
+
},
|
|
332
352
|
"notebook.enabled": {
|
|
333
353
|
type: "boolean",
|
|
334
354
|
default: true,
|
|
@@ -358,6 +378,24 @@ export const SETTINGS_SCHEMA = {
|
|
|
358
378
|
description: "Enable the calculator tool for basic calculations",
|
|
359
379
|
},
|
|
360
380
|
},
|
|
381
|
+
"browser.enabled": {
|
|
382
|
+
type: "boolean",
|
|
383
|
+
default: true,
|
|
384
|
+
ui: {
|
|
385
|
+
tab: "tools",
|
|
386
|
+
label: "Enable Browser",
|
|
387
|
+
description: "Enable the browser tool (Ulixee Hero)",
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
"browser.headless": {
|
|
391
|
+
type: "boolean",
|
|
392
|
+
default: true,
|
|
393
|
+
ui: {
|
|
394
|
+
tab: "tools",
|
|
395
|
+
label: "Browser headless",
|
|
396
|
+
description: "Launch browser in headless mode (disable to show browser UI)",
|
|
397
|
+
},
|
|
398
|
+
},
|
|
361
399
|
|
|
362
400
|
// ─────────────────────────────────────────────────────────────────────────
|
|
363
401
|
// Startup settings
|
|
@@ -467,7 +505,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
467
505
|
// ─────────────────────────────────────────────────────────────────────────
|
|
468
506
|
"providers.webSearch": {
|
|
469
507
|
type: "enum",
|
|
470
|
-
values: ["auto", "exa", "perplexity", "anthropic"] as const,
|
|
508
|
+
values: ["auto", "exa", "jina", "perplexity", "anthropic"] as const,
|
|
471
509
|
default: "auto",
|
|
472
510
|
ui: { tab: "services", label: "Web search provider", description: "Provider for web search tool", submenu: true },
|
|
473
511
|
},
|
package/src/cursor.ts
CHANGED
|
@@ -167,7 +167,7 @@ export class CursorExecHandlers implements ICursorExecHandlers {
|
|
|
167
167
|
pattern: args.pattern,
|
|
168
168
|
path: args.path || undefined,
|
|
169
169
|
glob: args.glob || undefined,
|
|
170
|
-
|
|
170
|
+
mode: args.outputMode || undefined,
|
|
171
171
|
context: args.context ?? args.contextBefore ?? args.contextAfter ?? undefined,
|
|
172
172
|
ignore_case: args.caseInsensitive || undefined,
|
|
173
173
|
type: args.type || undefined,
|
|
@@ -1,34 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CustomToolAdapter wraps CustomTool instances into AgentTool for use with the agent.
|
|
3
3
|
*/
|
|
4
|
-
import type { AgentTool,
|
|
5
|
-
import type { Component } from "@oh-my-pi/pi-tui";
|
|
4
|
+
import type { AgentTool, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
6
5
|
import type { Static, TSchema } from "@sinclair/typebox";
|
|
7
6
|
import type { Theme } from "../../modes/theme/theme";
|
|
7
|
+
import { applyToolProxy } from "../tool-proxy";
|
|
8
8
|
import type { CustomTool, CustomToolContext, LoadedCustomTool } from "./types";
|
|
9
9
|
|
|
10
10
|
export class CustomToolAdapter<TParams extends TSchema = TSchema, TDetails = any, TTheme extends Theme = Theme>
|
|
11
11
|
implements AgentTool<TParams, TDetails, TTheme>
|
|
12
12
|
{
|
|
13
|
+
declare name: string;
|
|
14
|
+
declare label: string;
|
|
15
|
+
declare description: string;
|
|
16
|
+
declare parameters: TParams;
|
|
17
|
+
|
|
13
18
|
constructor(
|
|
14
19
|
private tool: CustomTool<TParams, TDetails>,
|
|
15
20
|
private getContext: () => CustomToolContext,
|
|
16
|
-
) {
|
|
17
|
-
|
|
18
|
-
get name() {
|
|
19
|
-
return this.tool.name;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
get label() {
|
|
23
|
-
return this.tool.label;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
get description() {
|
|
27
|
-
return this.tool.description;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
get parameters() {
|
|
31
|
-
return this.tool.parameters;
|
|
21
|
+
) {
|
|
22
|
+
applyToolProxy(tool, this);
|
|
32
23
|
}
|
|
33
24
|
|
|
34
25
|
execute(
|
|
@@ -41,21 +32,6 @@ export class CustomToolAdapter<TParams extends TSchema = TSchema, TDetails = any
|
|
|
41
32
|
return this.tool.execute(toolCallId, params, onUpdate, context ?? this.getContext(), signal);
|
|
42
33
|
}
|
|
43
34
|
|
|
44
|
-
/** Optional custom rendering for tool call display (returns UI component) */
|
|
45
|
-
renderCall(args: Static<TParams>, theme: TTheme): Component | undefined {
|
|
46
|
-
return this.tool.renderCall?.(args, theme);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Optional custom rendering for tool result display (returns UI component) */
|
|
50
|
-
renderResult(
|
|
51
|
-
result: AgentToolResult<TDetails>,
|
|
52
|
-
options: RenderResultOptions,
|
|
53
|
-
theme: TTheme,
|
|
54
|
-
args?: Static<TParams>,
|
|
55
|
-
): Component | undefined {
|
|
56
|
-
return this.tool.renderResult?.(result, options, theme, args);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
35
|
/**
|
|
60
36
|
* Backward-compatible export of factory function for existing callers.
|
|
61
37
|
* Prefer CustomToolAdapter constructor directly.
|
|
@@ -5,6 +5,7 @@ import type { AgentTool, AgentToolContext, AgentToolUpdateCallback } from "@oh-m
|
|
|
5
5
|
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
6
6
|
import type { Static, TSchema } from "@sinclair/typebox";
|
|
7
7
|
import type { Theme } from "../../modes/theme/theme";
|
|
8
|
+
import { applyToolProxy } from "../tool-proxy";
|
|
8
9
|
import type { ExtensionRunner } from "./runner";
|
|
9
10
|
import type { RegisteredTool, ToolCallEventResult, ToolResultEventResult } from "./types";
|
|
10
11
|
|
|
@@ -12,20 +13,16 @@ import type { RegisteredTool, ToolCallEventResult, ToolResultEventResult } from
|
|
|
12
13
|
* Adapts a RegisteredTool into an AgentTool.
|
|
13
14
|
*/
|
|
14
15
|
export class RegisteredToolAdapter implements AgentTool<any, any, any> {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
declare name: string;
|
|
17
|
+
declare description: string;
|
|
18
|
+
declare parameters: any;
|
|
19
|
+
declare label: string;
|
|
19
20
|
|
|
20
21
|
constructor(
|
|
21
22
|
private registeredTool: RegisteredTool,
|
|
22
23
|
private runner: ExtensionRunner,
|
|
23
24
|
) {
|
|
24
|
-
|
|
25
|
-
this.name = definition.name;
|
|
26
|
-
this.label = definition.label || "";
|
|
27
|
-
this.description = definition.description;
|
|
28
|
-
this.parameters = definition.parameters;
|
|
25
|
+
applyToolProxy(registeredTool.definition, this);
|
|
29
26
|
}
|
|
30
27
|
|
|
31
28
|
async execute(
|
|
@@ -74,35 +71,25 @@ export function wrapRegisteredTools(registeredTools: RegisteredTool[], runner: E
|
|
|
74
71
|
export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetails = unknown>
|
|
75
72
|
implements AgentTool<TParameters, TDetails>
|
|
76
73
|
{
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
74
|
+
declare name: string;
|
|
75
|
+
declare description: string;
|
|
76
|
+
declare parameters: TParameters;
|
|
77
|
+
declare label: string;
|
|
81
78
|
|
|
82
79
|
constructor(
|
|
83
80
|
private tool: AgentTool<TParameters, TDetails>,
|
|
84
81
|
private runner: ExtensionRunner,
|
|
85
82
|
) {
|
|
86
|
-
|
|
87
|
-
this.renderResult = tool.renderResult?.bind(tool);
|
|
88
|
-
this.mergeCallAndResult = (tool as { mergeCallAndResult?: boolean }).mergeCallAndResult;
|
|
89
|
-
this.inline = (tool as { inline?: boolean }).inline;
|
|
83
|
+
applyToolProxy(tool, this);
|
|
90
84
|
}
|
|
91
85
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
get description(): string {
|
|
101
|
-
return this.tool.description;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
get parameters(): TParameters {
|
|
105
|
-
return this.tool.parameters;
|
|
86
|
+
/**
|
|
87
|
+
* Forward browser mode changes when available.
|
|
88
|
+
*/
|
|
89
|
+
restartForModeChange(): Promise<void> {
|
|
90
|
+
const target = this.tool as { restartForModeChange?: () => Promise<void> };
|
|
91
|
+
if (!target.restartForModeChange) return Promise.resolve();
|
|
92
|
+
return target.restartForModeChange();
|
|
106
93
|
}
|
|
107
94
|
|
|
108
95
|
async execute(
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { AgentTool, AgentToolContext, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
5
5
|
import type { Static, TSchema } from "@sinclair/typebox";
|
|
6
|
+
import { applyToolProxy } from "../tool-proxy";
|
|
6
7
|
import type { HookRunner } from "./runner";
|
|
7
8
|
import type { ToolCallEventResult, ToolResultEventResult } from "./types";
|
|
8
9
|
|
|
@@ -17,27 +18,16 @@ import type { ToolCallEventResult, ToolResultEventResult } from "./types";
|
|
|
17
18
|
export class HookToolWrapper<TParameters extends TSchema = TSchema, TDetails = unknown>
|
|
18
19
|
implements AgentTool<TParameters, TDetails>
|
|
19
20
|
{
|
|
20
|
-
name: string;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
renderCall?: AgentTool<TParameters, TDetails>["renderCall"];
|
|
25
|
-
renderResult?: AgentTool<TParameters, TDetails>["renderResult"];
|
|
26
|
-
mergeCallAndResult?: boolean;
|
|
27
|
-
inline?: boolean;
|
|
21
|
+
declare name: string;
|
|
22
|
+
declare description: string;
|
|
23
|
+
declare parameters: TParameters;
|
|
24
|
+
declare label: string;
|
|
28
25
|
|
|
29
26
|
constructor(
|
|
30
27
|
private tool: AgentTool<TParameters, TDetails>,
|
|
31
28
|
private hookRunner: HookRunner,
|
|
32
29
|
) {
|
|
33
|
-
this
|
|
34
|
-
this.label = tool.label ?? "";
|
|
35
|
-
this.description = tool.description;
|
|
36
|
-
this.parameters = tool.parameters;
|
|
37
|
-
this.renderCall = tool.renderCall?.bind(tool);
|
|
38
|
-
this.renderResult = tool.renderResult?.bind(tool);
|
|
39
|
-
this.mergeCallAndResult = (tool as { mergeCallAndResult?: boolean }).mergeCallAndResult;
|
|
40
|
-
this.inline = (tool as { inline?: boolean }).inline;
|
|
30
|
+
applyToolProxy(tool, this);
|
|
41
31
|
}
|
|
42
32
|
|
|
43
33
|
async execute(
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defines lazy proxy properties on a wrapper so it forwards to the underlying tool.
|
|
3
|
+
*/
|
|
4
|
+
export function applyToolProxy<TTool extends object>(tool: TTool, wrapper: object): void {
|
|
5
|
+
const visited = new Set<PropertyKey>();
|
|
6
|
+
let current: object | null = tool;
|
|
7
|
+
|
|
8
|
+
while (current && current !== Object.prototype) {
|
|
9
|
+
for (const key of Reflect.ownKeys(current)) {
|
|
10
|
+
if (key === "constructor" || visited.has(key) || key in wrapper) {
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
visited.add(key);
|
|
14
|
+
Object.defineProperty(wrapper, key, {
|
|
15
|
+
get() {
|
|
16
|
+
const value = (tool as Record<PropertyKey, unknown>)[key];
|
|
17
|
+
return typeof value === "function" ? value.bind(tool) : value;
|
|
18
|
+
},
|
|
19
|
+
enumerable: true,
|
|
20
|
+
configurable: true,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
current = Object.getPrototypeOf(current);
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/index.ts
CHANGED
package/src/ipy/executor.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import { getAgentDir } from "../config";
|
|
2
4
|
import { OutputSink } from "../session/streaming-output";
|
|
3
5
|
import { time } from "../utils/timings";
|
|
4
6
|
import { shutdownSharedGateway } from "./gateway-coordinator";
|
|
@@ -10,6 +12,12 @@ import {
|
|
|
10
12
|
type PreludeHelper,
|
|
11
13
|
PythonKernel,
|
|
12
14
|
} from "./kernel";
|
|
15
|
+
import { discoverPythonModules } from "./modules";
|
|
16
|
+
import { PYTHON_PRELUDE } from "./prelude";
|
|
17
|
+
|
|
18
|
+
const debugStartup = process.env.OMP_DEBUG_STARTUP
|
|
19
|
+
? (stage: string) => process.stderr.write(`[startup] ${stage}\n`)
|
|
20
|
+
: () => {};
|
|
13
21
|
|
|
14
22
|
const IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
15
23
|
const MAX_KERNEL_SESSIONS = 4;
|
|
@@ -86,6 +94,72 @@ const kernelSessions = new Map<string, KernelSession>();
|
|
|
86
94
|
let cachedPreludeDocs: PreludeHelper[] | null = null;
|
|
87
95
|
let cleanupTimer: NodeJS.Timeout | null = null;
|
|
88
96
|
|
|
97
|
+
interface PreludeCacheSource {
|
|
98
|
+
path: string;
|
|
99
|
+
hash: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface PreludeCachePayload {
|
|
103
|
+
helpers: PreludeHelper[];
|
|
104
|
+
sources: PreludeCacheSource[];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface PreludeCacheState {
|
|
108
|
+
cacheKey: string;
|
|
109
|
+
cachePath: string;
|
|
110
|
+
sources: PreludeCacheSource[];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const PRELUDE_CACHE_DIR = "pycache";
|
|
114
|
+
|
|
115
|
+
function hashPreludeContent(content: string): string {
|
|
116
|
+
return Bun.hash(content).toString(16);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function buildPreludeCacheState(cwd: string): Promise<PreludeCacheState> {
|
|
120
|
+
const modules = await discoverPythonModules({ cwd });
|
|
121
|
+
const moduleSources = modules
|
|
122
|
+
.map(module => ({ path: module.path, hash: hashPreludeContent(module.content) }))
|
|
123
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
124
|
+
const sources: PreludeCacheSource[] = [
|
|
125
|
+
{ path: "omp:prelude", hash: hashPreludeContent(PYTHON_PRELUDE) },
|
|
126
|
+
...moduleSources,
|
|
127
|
+
];
|
|
128
|
+
const composite = sources.map(source => `${source.path}:${source.hash}`).join("|");
|
|
129
|
+
const cacheKey = Bun.hash(composite).toString(16);
|
|
130
|
+
const cachePath = path.join(getAgentDir(), PRELUDE_CACHE_DIR, `${cacheKey}.json`);
|
|
131
|
+
return { cacheKey, cachePath, sources };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function readPreludeCache(state: PreludeCacheState): Promise<PreludeHelper[] | null> {
|
|
135
|
+
let raw: string;
|
|
136
|
+
try {
|
|
137
|
+
raw = await Bun.file(state.cachePath).text();
|
|
138
|
+
} catch (err) {
|
|
139
|
+
if (isEnoent(err)) return null;
|
|
140
|
+
logger.warn("Failed to read Python prelude cache", { path: state.cachePath, error: String(err) });
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
const parsed = JSON.parse(raw) as PreludeCachePayload | PreludeHelper[];
|
|
145
|
+
const helpers = Array.isArray(parsed) ? parsed : parsed.helpers;
|
|
146
|
+
if (!Array.isArray(helpers) || helpers.length === 0) return null;
|
|
147
|
+
return helpers;
|
|
148
|
+
} catch (err) {
|
|
149
|
+
logger.warn("Failed to parse Python prelude cache", { path: state.cachePath, error: String(err) });
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function writePreludeCache(state: PreludeCacheState, helpers: PreludeHelper[]): Promise<void> {
|
|
155
|
+
const payload: PreludeCachePayload = { helpers, sources: state.sources };
|
|
156
|
+
try {
|
|
157
|
+
await Bun.write(state.cachePath, JSON.stringify(payload));
|
|
158
|
+
} catch (err) {
|
|
159
|
+
logger.warn("Failed to write Python prelude cache", { path: state.cachePath, error: String(err) });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
89
163
|
function startCleanupTimer(): void {
|
|
90
164
|
if (cleanupTimer) return;
|
|
91
165
|
cleanupTimer = setInterval(() => {
|
|
@@ -153,19 +227,37 @@ export async function warmPythonEnvironment(
|
|
|
153
227
|
useSharedGateway?: boolean,
|
|
154
228
|
sessionFile?: string,
|
|
155
229
|
): Promise<{ ok: boolean; reason?: string; docs: PreludeHelper[] }> {
|
|
230
|
+
const isTestEnv = process.env.BUN_ENV === "test" || process.env.NODE_ENV === "test";
|
|
231
|
+
let cacheState: PreludeCacheState | null = null;
|
|
156
232
|
try {
|
|
233
|
+
debugStartup("warmPython:ensureKernel:start");
|
|
157
234
|
await ensureKernelAvailable(cwd);
|
|
235
|
+
debugStartup("warmPython:ensureKernel:done");
|
|
158
236
|
time("warmPython:ensureKernelAvailable");
|
|
159
237
|
} catch (err: unknown) {
|
|
160
238
|
const reason = err instanceof Error ? err.message : String(err);
|
|
161
239
|
cachedPreludeDocs = [];
|
|
162
240
|
return { ok: false, reason, docs: [] };
|
|
163
241
|
}
|
|
242
|
+
if (!isTestEnv) {
|
|
243
|
+
try {
|
|
244
|
+
cacheState = await buildPreludeCacheState(cwd);
|
|
245
|
+
const cached = await readPreludeCache(cacheState);
|
|
246
|
+
if (cached) {
|
|
247
|
+
cachedPreludeDocs = cached;
|
|
248
|
+
return { ok: true, docs: cached };
|
|
249
|
+
}
|
|
250
|
+
} catch (err) {
|
|
251
|
+
logger.warn("Failed to resolve Python prelude cache", { error: String(err) });
|
|
252
|
+
cacheState = null;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
164
255
|
if (cachedPreludeDocs && cachedPreludeDocs.length > 0) {
|
|
165
256
|
return { ok: true, docs: cachedPreludeDocs };
|
|
166
257
|
}
|
|
167
258
|
const resolvedSessionId = sessionId ?? `session:${cwd}`;
|
|
168
259
|
try {
|
|
260
|
+
debugStartup("warmPython:withKernelSession:start");
|
|
169
261
|
const docs = await withKernelSession(
|
|
170
262
|
resolvedSessionId,
|
|
171
263
|
cwd,
|
|
@@ -173,8 +265,13 @@ export async function warmPythonEnvironment(
|
|
|
173
265
|
useSharedGateway,
|
|
174
266
|
sessionFile,
|
|
175
267
|
);
|
|
268
|
+
debugStartup("warmPython:withKernelSession:done");
|
|
176
269
|
time("warmPython:withKernelSession");
|
|
177
270
|
cachedPreludeDocs = docs;
|
|
271
|
+
if (!isTestEnv && docs.length > 0) {
|
|
272
|
+
const state = cacheState ?? (await buildPreludeCacheState(cwd));
|
|
273
|
+
await writePreludeCache(state, docs);
|
|
274
|
+
}
|
|
178
275
|
return { ok: true, docs };
|
|
179
276
|
} catch (err: unknown) {
|
|
180
277
|
const reason = err instanceof Error ? err.message : String(err);
|
|
@@ -226,6 +323,7 @@ async function createKernelSession(
|
|
|
226
323
|
artifactsDir?: string,
|
|
227
324
|
isRetry?: boolean,
|
|
228
325
|
): Promise<KernelSession> {
|
|
326
|
+
debugStartup("kernel:createSession:entry");
|
|
229
327
|
const env: Record<string, string> | undefined =
|
|
230
328
|
sessionFile || artifactsDir
|
|
231
329
|
? {
|
|
@@ -236,7 +334,9 @@ async function createKernelSession(
|
|
|
236
334
|
|
|
237
335
|
let kernel: PythonKernel;
|
|
238
336
|
try {
|
|
337
|
+
debugStartup("kernel:PythonKernel.start:start");
|
|
239
338
|
kernel = await PythonKernel.start({ cwd, useSharedGateway, env });
|
|
339
|
+
debugStartup("kernel:PythonKernel.start:done");
|
|
240
340
|
time("createKernelSession:PythonKernel.start");
|
|
241
341
|
} catch (err) {
|
|
242
342
|
if (!isRetry && isResourceExhaustionError(err)) {
|
|
@@ -314,6 +414,7 @@ async function withKernelSession<T>(
|
|
|
314
414
|
sessionFile?: string,
|
|
315
415
|
artifactsDir?: string,
|
|
316
416
|
): Promise<T> {
|
|
417
|
+
debugStartup("kernel:withSession:entry");
|
|
317
418
|
let session = kernelSessions.get(sessionId);
|
|
318
419
|
if (!session) {
|
|
319
420
|
// Evict oldest session if at capacity
|
|
@@ -321,17 +422,21 @@ async function withKernelSession<T>(
|
|
|
321
422
|
await evictOldestSession();
|
|
322
423
|
}
|
|
323
424
|
session = await createKernelSession(sessionId, cwd, useSharedGateway, sessionFile, artifactsDir);
|
|
425
|
+
debugStartup("kernel:withSession:created");
|
|
324
426
|
kernelSessions.set(sessionId, session);
|
|
325
427
|
startCleanupTimer();
|
|
326
428
|
}
|
|
327
429
|
|
|
328
430
|
const run = async (): Promise<T> => {
|
|
431
|
+
debugStartup("kernel:withSession:run");
|
|
329
432
|
session!.lastUsedAt = Date.now();
|
|
330
433
|
if (session!.dead || !session!.kernel.isAlive()) {
|
|
331
434
|
await restartKernelSession(session!, cwd, useSharedGateway, sessionFile, artifactsDir);
|
|
332
435
|
}
|
|
333
436
|
try {
|
|
437
|
+
debugStartup("kernel:withSession:handler:start");
|
|
334
438
|
const result = await handler(session!.kernel);
|
|
439
|
+
debugStartup("kernel:withSession:handler:done");
|
|
335
440
|
session!.restartCount = 0;
|
|
336
441
|
return result;
|
|
337
442
|
} catch (err) {
|
|
@@ -424,8 +529,7 @@ export async function executePython(code: string, options?: PythonExecutorOption
|
|
|
424
529
|
await ensureKernelAvailable(cwd);
|
|
425
530
|
|
|
426
531
|
const kernelMode = options?.kernelMode ?? "session";
|
|
427
|
-
const
|
|
428
|
-
const useSharedGateway = isTestEnv ? false : options?.useSharedGateway;
|
|
532
|
+
const useSharedGateway = options?.useSharedGateway;
|
|
429
533
|
const sessionFile = options?.sessionFile;
|
|
430
534
|
const artifactsDir = options?.artifactsDir;
|
|
431
535
|
|