@oh-my-pi/pi-coding-agent 17.2.12 → 17.2.13
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 +57 -0
- package/dist/{CHANGELOG-k9ghy5sn.md → CHANGELOG-d8xh7keh.md} +57 -0
- package/dist/cli.js +3069 -3047
- package/dist/types/advisor/delta-split.d.ts +24 -0
- package/dist/types/advisor/runtime.d.ts +2 -2
- package/dist/types/async/job-manager.d.ts +8 -1
- package/dist/types/cli/update-cli.d.ts +53 -1
- package/dist/types/config/keybindings.d.ts +10 -0
- package/dist/types/config/model-resolver.d.ts +15 -2
- package/dist/types/config/settings-schema.d.ts +4 -0
- package/dist/types/discovery/agents-md.d.ts +10 -1
- package/dist/types/eval/runner-cache.d.ts +12 -0
- package/dist/types/extensibility/extensions/runner.d.ts +12 -3
- package/dist/types/extensibility/extensions/types.d.ts +15 -4
- package/dist/types/extensibility/plugins/marketplace/manager.d.ts +4 -1
- package/dist/types/lib/xai-http.d.ts +0 -1
- package/dist/types/mcp/tool-bridge.d.ts +8 -5
- package/dist/types/modes/components/agent-hub-renderer.d.ts +6 -1
- package/dist/types/modes/components/status-line/types.d.ts +4 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -4
- package/dist/types/modes/interactive-mode.d.ts +17 -8
- package/dist/types/modes/types.d.ts +7 -10
- package/dist/types/modes/utils/hotkeys-markdown.d.ts +1 -1
- package/dist/types/session/agent-session-types.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +22 -3
- package/dist/types/session/messages.d.ts +20 -0
- package/dist/types/session/retry-fallback-chains.d.ts +13 -0
- package/dist/types/session/session-advisors.d.ts +1 -1
- package/dist/types/session/session-history-format.d.ts +10 -0
- package/dist/types/session/session-maintenance.d.ts +1 -1
- package/dist/types/session/session-tools.d.ts +24 -5
- package/dist/types/session/turn-recovery.d.ts +34 -5
- package/dist/types/slash-commands/types.d.ts +5 -1
- package/dist/types/task/executor.d.ts +1 -1
- package/dist/types/tools/approval.d.ts +7 -0
- package/dist/types/tools/todo.d.ts +14 -15
- package/dist/types/tools/write.d.ts +2 -2
- package/dist/types/utils/local-date.d.ts +2 -0
- package/dist/types/vibe/runtime.d.ts +1 -1
- package/dist/types/web/parallel.d.ts +1 -0
- package/dist/types/web/search/providers/brave.d.ts +8 -3
- package/dist/types/web/search/providers/codex.d.ts +6 -0
- package/dist/types/web/search/providers/firecrawl.d.ts +3 -2
- package/dist/types/web/search/providers/jina.d.ts +3 -3
- package/dist/types/web/search/providers/parallel.d.ts +1 -0
- package/dist/types/web/search/providers/perplexity.d.ts +4 -0
- package/dist/types/web/search/providers/tinyfish.d.ts +2 -0
- package/package.json +13 -13
- package/src/advisor/delta-split.ts +98 -0
- package/src/advisor/runtime.ts +321 -69
- package/src/async/job-manager.ts +14 -3
- package/src/cli/plugin-cli.ts +30 -2
- package/src/cli/update-cli.ts +259 -24
- package/src/config/keybindings.ts +52 -9
- package/src/config/model-resolver.ts +19 -3
- package/src/config/settings-schema.ts +5 -0
- package/src/cursor.ts +10 -5
- package/src/discovery/agents-md.ts +61 -23
- package/src/eval/jl/kernel.ts +2 -20
- package/src/eval/py/kernel.ts +2 -20
- package/src/eval/rb/kernel.ts +2 -20
- package/src/eval/runner-cache.ts +41 -0
- package/src/exec/non-interactive-env.ts +14 -3
- package/src/extensibility/extensions/loader.ts +5 -2
- package/src/extensibility/extensions/runner.ts +184 -66
- package/src/extensibility/extensions/types.ts +26 -2
- package/src/extensibility/extensions/wrapper.ts +13 -7
- package/src/extensibility/plugins/marketplace/manager.ts +6 -2
- package/src/hindsight/client.ts +1 -1
- package/src/lib/xai-http.ts +0 -4
- package/src/lsp/client.ts +2 -0
- package/src/lsp/servers.ts +1 -1
- package/src/mcp/tool-bridge.ts +15 -6
- package/src/modes/components/agent-hub-renderer.ts +9 -3
- package/src/modes/components/agent-hub.ts +2 -1
- package/src/modes/components/status-line/component.ts +58 -6
- package/src/modes/components/status-line/segments.ts +12 -1
- package/src/modes/components/status-line/types.ts +1 -0
- package/src/modes/components/user-message.ts +20 -5
- package/src/modes/controllers/event-controller.ts +48 -0
- package/src/modes/controllers/extension-ui-controller.ts +14 -7
- package/src/modes/controllers/input-controller.ts +25 -6
- package/src/modes/interactive-mode.ts +315 -129
- package/src/modes/rpc/rpc-frame.ts +13 -5
- package/src/modes/theme/tui-adapters.ts +4 -5
- package/src/modes/types.ts +13 -7
- package/src/modes/utils/hotkeys-markdown.ts +10 -6
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/registry/persisted-agents.ts +43 -8
- package/src/sdk.ts +139 -8
- package/src/session/agent-session-types.ts +2 -0
- package/src/session/agent-session.ts +66 -10
- package/src/session/messages.ts +98 -28
- package/src/session/retry-fallback-chains.ts +14 -0
- package/src/session/session-advisors.ts +32 -15
- package/src/session/session-history-format.ts +15 -1
- package/src/session/session-maintenance.ts +8 -8
- package/src/session/session-manager.ts +6 -2
- package/src/session/session-tools.ts +321 -184
- package/src/session/turn-recovery.ts +225 -47
- package/src/slash-commands/builtin-modes.ts +41 -12
- package/src/slash-commands/types.ts +5 -1
- package/src/task/executor.ts +92 -45
- package/src/task/structured-subagent.ts +5 -5
- package/src/tools/approval.ts +44 -10
- package/src/tools/fetch.ts +21 -2
- package/src/tools/image-gen.ts +6 -8
- package/src/tools/todo.ts +70 -26
- package/src/tools/tts.ts +3 -2
- package/src/tools/write.ts +7 -3
- package/src/utils/local-date.ts +13 -0
- package/src/utils/tools-manager.ts +2 -2
- package/src/vibe/runtime.ts +22 -14
- package/src/web/kagi.ts +91 -34
- package/src/web/parallel.ts +11 -2
- package/src/web/scrapers/crates-io.ts +2 -2
- package/src/web/scrapers/discogs.ts +2 -2
- package/src/web/scrapers/docs-rs.ts +2 -2
- package/src/web/scrapers/github.ts +2 -2
- package/src/web/scrapers/musicbrainz.ts +1 -2
- package/src/web/scrapers/pubmed.ts +2 -2
- package/src/web/scrapers/sec-edgar.ts +2 -2
- package/src/web/search/providers/brave.ts +121 -46
- package/src/web/search/providers/codex.ts +88 -12
- package/src/web/search/providers/exa.ts +45 -10
- package/src/web/search/providers/firecrawl.ts +53 -11
- package/src/web/search/providers/gemini.ts +139 -27
- package/src/web/search/providers/jina.ts +48 -25
- package/src/web/search/providers/parallel.ts +23 -9
- package/src/web/search/providers/perplexity.ts +24 -7
- package/src/web/search/providers/searxng.ts +77 -1
- package/src/web/search/providers/tavily.ts +23 -22
- package/src/web/search/providers/tinyfish.ts +44 -10
- package/src/web/search/providers/xai.ts +85 -14
package/src/eval/jl/kernel.ts
CHANGED
|
@@ -5,8 +5,6 @@
|
|
|
5
5
|
* Ruby runners via BaseKernel; this module supplies the Julia binary, runner
|
|
6
6
|
* script, and the runner's TSV/Base64 wire protocol.
|
|
7
7
|
*/
|
|
8
|
-
import * as fs from "node:fs";
|
|
9
|
-
import * as os from "node:os";
|
|
10
8
|
import * as path from "node:path";
|
|
11
9
|
import { $flag, Snowflake } from "@oh-my-pi/pi-utils";
|
|
12
10
|
import { $ } from "bun";
|
|
@@ -14,6 +12,7 @@ import { Settings } from "../../config/settings";
|
|
|
14
12
|
import { BaseKernel, getRemainingTimeMs, type KernelStartOptions } from "../kernel-base";
|
|
15
13
|
import type { KernelDisplayOutput } from "../py/display";
|
|
16
14
|
import { hostHasInheritableConsole, shouldDetachKernel, shouldHideKernelWindow } from "../py/spawn-options";
|
|
15
|
+
import { stageRunnerScript } from "../runner-cache";
|
|
17
16
|
import { JULIA_PRELUDE } from "./prelude";
|
|
18
17
|
import RUNNER_SCRIPT from "./runner.jl" with { type: "text" };
|
|
19
18
|
import {
|
|
@@ -30,23 +29,6 @@ export type { KernelDisplayOutput };
|
|
|
30
29
|
|
|
31
30
|
const TRACE_IPC = $flag("PI_JULIA_IPC_TRACE");
|
|
32
31
|
|
|
33
|
-
// Cache the runner script on disk so the subprocess loads it normally. Cached
|
|
34
|
-
// per script hash so installs don't race across versions.
|
|
35
|
-
const RUNNER_CACHE_DIR = path.join(os.tmpdir(), "omp-julia-runner");
|
|
36
|
-
let RUNNER_SCRIPT_PATH: string | null = null;
|
|
37
|
-
|
|
38
|
-
async function ensureRunnerScript(): Promise<string> {
|
|
39
|
-
if (RUNNER_SCRIPT_PATH) return RUNNER_SCRIPT_PATH;
|
|
40
|
-
await fs.promises.mkdir(RUNNER_CACHE_DIR, { recursive: true });
|
|
41
|
-
const hash = Bun.hash(RUNNER_SCRIPT).toString(36);
|
|
42
|
-
const target = path.join(RUNNER_CACHE_DIR, `runner-${hash}.jl`);
|
|
43
|
-
if (!fs.existsSync(target)) {
|
|
44
|
-
await Bun.write(target, RUNNER_SCRIPT);
|
|
45
|
-
}
|
|
46
|
-
RUNNER_SCRIPT_PATH = target;
|
|
47
|
-
return target;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
32
|
const SHUTDOWN_GRACE_MS = 1_000;
|
|
51
33
|
const STARTUP_TIMEOUT_MS = 15_000; // Julia compile/warmup can be slightly slower
|
|
52
34
|
const INTERRUPT_ESCALATION_MS = 5_000;
|
|
@@ -180,7 +162,7 @@ export class JuliaKernel extends BaseKernel<KernelExecuteOptions> {
|
|
|
180
162
|
if (typeof value === "string") spawnEnv[key] = value;
|
|
181
163
|
}
|
|
182
164
|
|
|
183
|
-
const scriptPath = await
|
|
165
|
+
const scriptPath = await stageRunnerScript("omp-julia-runner", "jl", RUNNER_SCRIPT);
|
|
184
166
|
const kernel = new JuliaKernel(Snowflake.next());
|
|
185
167
|
|
|
186
168
|
const proc = Bun.spawn(
|
package/src/eval/py/kernel.ts
CHANGED
|
@@ -7,13 +7,12 @@
|
|
|
7
7
|
* code. Shutdown writes `{"type":"exit"}` and escalates to SIGTERM/SIGKILL on
|
|
8
8
|
* timeout.
|
|
9
9
|
*/
|
|
10
|
-
import * as fs from "node:fs";
|
|
11
|
-
import * as os from "node:os";
|
|
12
10
|
import * as path from "node:path";
|
|
13
11
|
import { $flag, isBunTestRuntime, logger, Snowflake } from "@oh-my-pi/pi-utils";
|
|
14
12
|
import { $ } from "bun";
|
|
15
13
|
import { Settings } from "../../config/settings";
|
|
16
14
|
import { BaseKernel, getRemainingTimeMs, type KernelStartOptions } from "../kernel-base";
|
|
15
|
+
import { stageRunnerScript } from "../runner-cache";
|
|
17
16
|
import { PYTHON_PRELUDE } from "./prelude";
|
|
18
17
|
import RUNNER_SCRIPT from "./runner.py" with { type: "text" };
|
|
19
18
|
import {
|
|
@@ -38,23 +37,6 @@ export { renderKernelDisplay } from "./display";
|
|
|
38
37
|
|
|
39
38
|
const TRACE_IPC = $flag("PI_PYTHON_IPC_TRACE");
|
|
40
39
|
|
|
41
|
-
// Cache the runner script on disk so the subprocess loads it normally. Cached
|
|
42
|
-
// per script hash so installs don't race across versions.
|
|
43
|
-
const RUNNER_CACHE_DIR = path.join(os.tmpdir(), "omp-python-runner");
|
|
44
|
-
let RUNNER_SCRIPT_PATH: string | null = null;
|
|
45
|
-
|
|
46
|
-
async function ensureRunnerScript(): Promise<string> {
|
|
47
|
-
if (RUNNER_SCRIPT_PATH) return RUNNER_SCRIPT_PATH;
|
|
48
|
-
await fs.promises.mkdir(RUNNER_CACHE_DIR, { recursive: true });
|
|
49
|
-
const hash = Bun.hash(RUNNER_SCRIPT).toString(36);
|
|
50
|
-
const target = path.join(RUNNER_CACHE_DIR, `runner-${hash}.py`);
|
|
51
|
-
if (!fs.existsSync(target)) {
|
|
52
|
-
await Bun.write(target, RUNNER_SCRIPT);
|
|
53
|
-
}
|
|
54
|
-
RUNNER_SCRIPT_PATH = target;
|
|
55
|
-
return target;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
40
|
const SHUTDOWN_GRACE_MS = 1_000;
|
|
59
41
|
const STARTUP_TIMEOUT_MS = 10_000;
|
|
60
42
|
// How long to wait after SIGINT for the runner to emit `done`. If the cell is
|
|
@@ -189,7 +171,7 @@ export class PythonKernel extends BaseKernel {
|
|
|
189
171
|
spawnEnv.PYTHONUNBUFFERED = "1";
|
|
190
172
|
spawnEnv.PYTHONIOENCODING = "utf-8";
|
|
191
173
|
|
|
192
|
-
const scriptPath = await
|
|
174
|
+
const scriptPath = await stageRunnerScript("omp-python-runner", "py", RUNNER_SCRIPT);
|
|
193
175
|
const kernel = new PythonKernel(Snowflake.next());
|
|
194
176
|
|
|
195
177
|
const proc = Bun.spawn([runtime.pythonPath, "-u", scriptPath], {
|
package/src/eval/rb/kernel.ts
CHANGED
|
@@ -8,8 +8,6 @@
|
|
|
8
8
|
* (eval/py/kernel.ts); the IPC loop, lifecycle, and display rendering are shared
|
|
9
9
|
* with it via BaseKernel.
|
|
10
10
|
*/
|
|
11
|
-
import * as fs from "node:fs";
|
|
12
|
-
import * as os from "node:os";
|
|
13
11
|
import * as path from "node:path";
|
|
14
12
|
import { $flag, isBunTestRuntime, logger, Snowflake } from "@oh-my-pi/pi-utils";
|
|
15
13
|
import { $ } from "bun";
|
|
@@ -17,6 +15,7 @@ import { Settings } from "../../config/settings";
|
|
|
17
15
|
import { BaseKernel, getRemainingTimeMs, type KernelRuntimeEnv, type KernelStartOptions } from "../kernel-base";
|
|
18
16
|
import type { KernelDisplayOutput } from "../py/display";
|
|
19
17
|
import { hostHasInheritableConsole, shouldDetachKernel, shouldHideKernelWindow } from "../py/spawn-options";
|
|
18
|
+
import { stageRunnerScript } from "../runner-cache";
|
|
20
19
|
import { RUBY_PRELUDE } from "./prelude";
|
|
21
20
|
import RUNNER_SCRIPT from "./runner.rb" with { type: "text" };
|
|
22
21
|
import {
|
|
@@ -33,23 +32,6 @@ export { renderKernelDisplay } from "../py/display";
|
|
|
33
32
|
|
|
34
33
|
const TRACE_IPC = $flag("PI_RUBY_IPC_TRACE");
|
|
35
34
|
|
|
36
|
-
// Cache the runner script on disk so the subprocess loads it normally. Cached
|
|
37
|
-
// per script hash so installs don't race across versions.
|
|
38
|
-
const RUNNER_CACHE_DIR = path.join(os.tmpdir(), "omp-ruby-runner");
|
|
39
|
-
let RUNNER_SCRIPT_PATH: string | null = null;
|
|
40
|
-
|
|
41
|
-
async function ensureRunnerScript(): Promise<string> {
|
|
42
|
-
if (RUNNER_SCRIPT_PATH) return RUNNER_SCRIPT_PATH;
|
|
43
|
-
await fs.promises.mkdir(RUNNER_CACHE_DIR, { recursive: true });
|
|
44
|
-
const hash = Bun.hash(RUNNER_SCRIPT).toString(36);
|
|
45
|
-
const target = path.join(RUNNER_CACHE_DIR, `runner-${hash}.rb`);
|
|
46
|
-
if (!fs.existsSync(target)) {
|
|
47
|
-
await Bun.write(target, RUNNER_SCRIPT);
|
|
48
|
-
}
|
|
49
|
-
RUNNER_SCRIPT_PATH = target;
|
|
50
|
-
return target;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
35
|
const SHUTDOWN_GRACE_MS = 1_000;
|
|
54
36
|
const STARTUP_TIMEOUT_MS = 10_000;
|
|
55
37
|
// How long to wait after SIGINT for the runner to emit `done` before escalating
|
|
@@ -181,7 +163,7 @@ export class RubyKernel extends BaseKernel<KernelExecuteOptions> {
|
|
|
181
163
|
if (typeof value === "string") spawnEnv[key] = value;
|
|
182
164
|
}
|
|
183
165
|
|
|
184
|
-
const scriptPath = await
|
|
166
|
+
const scriptPath = await stageRunnerScript("omp-ruby-runner", "rb", RUNNER_SCRIPT);
|
|
185
167
|
const kernel = new RubyKernel(Snowflake.next());
|
|
186
168
|
|
|
187
169
|
const proc = Bun.spawn([runtime.rubyPath, scriptPath], {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared on-disk staging for subprocess kernel runner scripts.
|
|
3
|
+
*
|
|
4
|
+
* Each language kernel (Python/Julia/Ruby) ships its runner as a compiled-in
|
|
5
|
+
* text asset, then stages it under `os.tmpdir()` so the interpreter can load it
|
|
6
|
+
* as a normal file. Staging is cached per language directory so repeated kernel
|
|
7
|
+
* starts within a process avoid redundant writes.
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from "node:fs";
|
|
10
|
+
import * as os from "node:os";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
|
|
13
|
+
// Memoized staged path per cache directory. The value is re-validated on every
|
|
14
|
+
// call: a tmpdir sweep (e.g. macOS `periodic daily clean_tmps`) or any external
|
|
15
|
+
// clear must self-heal within a long-lived process, not only across restarts.
|
|
16
|
+
const stagedPaths = new Map<string, string>();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Stage `script` under `os.tmpdir()/<dirName>` and return the runner path.
|
|
20
|
+
*
|
|
21
|
+
* The staged path is memoized per `dirName` but re-checked with `fs.existsSync`
|
|
22
|
+
* before reuse, so a runner deleted mid-session is re-written on the next call
|
|
23
|
+
* instead of handing back a path to a missing file (issue #8140).
|
|
24
|
+
*
|
|
25
|
+
* @param dirName Cache subdirectory under the OS temp dir (unique per language).
|
|
26
|
+
* @param ext Runner file extension without the dot (e.g. `py`, `jl`, `rb`).
|
|
27
|
+
* @param script Runner source, hashed to key the cached file per version.
|
|
28
|
+
*/
|
|
29
|
+
export async function stageRunnerScript(dirName: string, ext: string, script: string): Promise<string> {
|
|
30
|
+
const memoized = stagedPaths.get(dirName);
|
|
31
|
+
if (memoized && fs.existsSync(memoized)) return memoized;
|
|
32
|
+
const dir = path.join(os.tmpdir(), dirName);
|
|
33
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
34
|
+
const hash = Bun.hash(script).toString(36);
|
|
35
|
+
const target = path.join(dir, `runner-${hash}.${ext}`);
|
|
36
|
+
if (!fs.existsSync(target)) {
|
|
37
|
+
await Bun.write(target, script);
|
|
38
|
+
}
|
|
39
|
+
stagedPaths.set(dirName, target);
|
|
40
|
+
return target;
|
|
41
|
+
}
|
|
@@ -23,7 +23,7 @@ export const NON_INTERACTIVE_ENV: Readonly<Record<string, string>> = {
|
|
|
23
23
|
EDITOR: "true",
|
|
24
24
|
GIT_TERMINAL_PROMPT: "0",
|
|
25
25
|
SSH_ASKPASS: "/usr/bin/false",
|
|
26
|
-
CI: "
|
|
26
|
+
CI: "true",
|
|
27
27
|
AGENT: "1",
|
|
28
28
|
// Package manager defaults for unattended execution.
|
|
29
29
|
npm_config_yes: "true",
|
|
@@ -96,17 +96,28 @@ function hasEnvGroupValue(
|
|
|
96
96
|
return false;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
/** Copy of the base env with `CI` removed, for the `PI_BASH_NO_CI` opt-out. */
|
|
100
|
+
function withoutCI(env: Readonly<Record<string, string>>): Record<string, string> {
|
|
101
|
+
const { CI: _ci, ...rest } = env;
|
|
102
|
+
return rest;
|
|
103
|
+
}
|
|
104
|
+
|
|
99
105
|
/** Builds the per-command environment for non-interactive child processes. */
|
|
100
106
|
export function buildNonInteractiveEnv(
|
|
101
107
|
overrides?: Record<string, string>,
|
|
102
108
|
baseEnv: Record<string, string | undefined> = Bun.env,
|
|
103
109
|
platform: NodeJS.Platform = process.platform,
|
|
104
110
|
): Record<string, string> {
|
|
111
|
+
// `PI_BASH_NO_CI` (and its legacy alias) opts out of the automatic `CI=true`
|
|
112
|
+
// injection. Mirrors the session-env gate in `procmgr.ts` so the opt-out
|
|
113
|
+
// reaches the per-command env, which otherwise overrides the session value.
|
|
114
|
+
const base =
|
|
115
|
+
baseEnv.PI_BASH_NO_CI || baseEnv.CLAUDE_BASH_NO_CI ? withoutCI(NON_INTERACTIVE_ENV) : NON_INTERACTIVE_ENV;
|
|
105
116
|
if (platform !== "win32") {
|
|
106
|
-
return overrides ? { ...
|
|
117
|
+
return overrides ? { ...base, ...overrides } : base;
|
|
107
118
|
}
|
|
108
119
|
|
|
109
|
-
const env: Record<string, string> = { ...
|
|
120
|
+
const env: Record<string, string> = { ...base };
|
|
110
121
|
for (const group of WINDOWS_UTF8_ENV_DEFAULT_GROUPS) {
|
|
111
122
|
if (hasEnvGroupValue(baseEnv, group, platform) || hasEnvGroupValue(overrides, group, platform)) {
|
|
112
123
|
continue;
|
|
@@ -175,10 +175,12 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
|
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
registerTool<TParams extends TSchema = TSchema, TDetails = unknown>(tool: ToolDefinition<TParams, TDetails>): void {
|
|
178
|
-
|
|
178
|
+
const registered = {
|
|
179
179
|
definition: tool,
|
|
180
180
|
extensionPath: this.extension.path,
|
|
181
|
-
}
|
|
181
|
+
};
|
|
182
|
+
this.extension.tools.set(tool.name, registered);
|
|
183
|
+
for (const listener of this.extension.toolRegistrationListeners ?? []) listener(tool.name);
|
|
182
184
|
}
|
|
183
185
|
|
|
184
186
|
registerCommand(
|
|
@@ -316,6 +318,7 @@ function createExtension(extensionPath: string, resolvedPath: string): Extension
|
|
|
316
318
|
resolvedPath,
|
|
317
319
|
handlers: new Map(),
|
|
318
320
|
tools: new Map(),
|
|
321
|
+
toolRegistrationListeners: new Set(),
|
|
319
322
|
assistantThinkingRenderers: [],
|
|
320
323
|
messageRenderers: new Map(),
|
|
321
324
|
commands: new Map(),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Extension runner - executes extensions and manages their lifecycle.
|
|
3
3
|
*/
|
|
4
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
4
5
|
import type {
|
|
5
6
|
AgentMessage,
|
|
6
7
|
AgentTool,
|
|
@@ -62,6 +63,7 @@ import type {
|
|
|
62
63
|
SessionStopEventResult,
|
|
63
64
|
ToolCallEvent,
|
|
64
65
|
ToolCallEventResult,
|
|
66
|
+
ToolRegistrationListener,
|
|
65
67
|
ToolResultEvent,
|
|
66
68
|
ToolResultEventResult,
|
|
67
69
|
UserBashEvent,
|
|
@@ -332,8 +334,15 @@ const noOpUIContext: ExtensionUIContext = {
|
|
|
332
334
|
setToolsExpanded: () => {},
|
|
333
335
|
};
|
|
334
336
|
|
|
337
|
+
interface ToolRegistrationScope {
|
|
338
|
+
pending: Set<Promise<void>>;
|
|
339
|
+
signal?: AbortSignal;
|
|
340
|
+
closed: boolean;
|
|
341
|
+
}
|
|
342
|
+
|
|
335
343
|
export class ExtensionRunner {
|
|
336
344
|
#uiContext: ExtensionUIContext;
|
|
345
|
+
#toolApprovalPreviewWaiter?: (toolCallId: string) => Promise<void>;
|
|
337
346
|
#errorListeners: Set<ExtensionErrorListener> = new Set();
|
|
338
347
|
#getModel: () => Model | undefined = () => undefined;
|
|
339
348
|
#isIdleFn: () => boolean = () => true;
|
|
@@ -352,6 +361,8 @@ export class ExtensionRunner {
|
|
|
352
361
|
#shutdownHandler: ShutdownHandler = () => {};
|
|
353
362
|
#getMemoryFn?: () => MemoryRuntimeContext | undefined;
|
|
354
363
|
#commandDiagnostics: Array<{ type: string; message: string; path: string }> = [];
|
|
364
|
+
#toolRegistrationScope = new AsyncLocalStorage<ToolRegistrationScope>();
|
|
365
|
+
#toolRegistrationBarrier: Promise<void> | undefined;
|
|
355
366
|
#initialized = false;
|
|
356
367
|
/**
|
|
357
368
|
* Buffer for `credential_disabled` events received via {@link emitCredentialDisabled}
|
|
@@ -521,7 +532,11 @@ export class ExtensionRunner {
|
|
|
521
532
|
this.runtime.appendEntry = actions.appendEntry;
|
|
522
533
|
this.runtime.getActiveTools = actions.getActiveTools;
|
|
523
534
|
this.runtime.getAllTools = actions.getAllTools;
|
|
524
|
-
this.runtime.setActiveTools =
|
|
535
|
+
this.runtime.setActiveTools = async toolNames => {
|
|
536
|
+
const registrationBarrier = this.#toolRegistrationBarrier;
|
|
537
|
+
if (registrationBarrier) await registrationBarrier;
|
|
538
|
+
await actions.setActiveTools(toolNames);
|
|
539
|
+
};
|
|
525
540
|
this.runtime.getCommands = actions.getCommands;
|
|
526
541
|
this.runtime.setModel = actions.setModel;
|
|
527
542
|
this.runtime.getThinkingLevel = actions.getThinkingLevel;
|
|
@@ -650,6 +665,18 @@ export class ExtensionRunner {
|
|
|
650
665
|
if (event.signal.aborted) return undefined;
|
|
651
666
|
return await this.emit({ type: "session_stop", ...event });
|
|
652
667
|
}
|
|
668
|
+
/** Registers the interactive transcript gate that must settle before a tool approval is presented. */
|
|
669
|
+
setToolApprovalPreviewWaiter(waiter: (toolCallId: string) => Promise<void>): () => void {
|
|
670
|
+
this.#toolApprovalPreviewWaiter = waiter;
|
|
671
|
+
return () => {
|
|
672
|
+
if (this.#toolApprovalPreviewWaiter === waiter) this.#toolApprovalPreviewWaiter = undefined;
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/** Waits until the interactive transcript can show the tool call being approved. */
|
|
677
|
+
async waitForToolApprovalPreview(toolCallId: string): Promise<void> {
|
|
678
|
+
await this.#toolApprovalPreviewWaiter?.(toolCallId);
|
|
679
|
+
}
|
|
653
680
|
|
|
654
681
|
getUIContext(): ExtensionUIContext {
|
|
655
682
|
return this.#uiContext;
|
|
@@ -674,6 +701,88 @@ export class ExtensionRunner {
|
|
|
674
701
|
return tools;
|
|
675
702
|
}
|
|
676
703
|
|
|
704
|
+
/** Get the effective registered tool for a name using normal last-extension-wins precedence. */
|
|
705
|
+
getRegisteredTool(name: string): RegisteredTool | undefined {
|
|
706
|
+
for (let index = this.extensions.length - 1; index >= 0; index -= 1) {
|
|
707
|
+
const tool = this.extensions[index]?.tools.get(name);
|
|
708
|
+
if (tool) return tool;
|
|
709
|
+
}
|
|
710
|
+
return undefined;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Observe tools registered after extension factories have loaded. Listener
|
|
715
|
+
* promises are drained before the lifecycle handler that registered them
|
|
716
|
+
* completes, keeping the model tool snapshot and system prompt coherent.
|
|
717
|
+
*/
|
|
718
|
+
onToolRegistered(listener: (tool: RegisteredTool, signal?: AbortSignal) => void | Promise<void>): () => void {
|
|
719
|
+
const subscriptions: Array<{ extension: Extension; listener: ToolRegistrationListener }> = [];
|
|
720
|
+
for (const extension of this.extensions) {
|
|
721
|
+
const trackRegistration = (pending: Promise<void>): void => {
|
|
722
|
+
const registrationBarrier = pending.then(
|
|
723
|
+
() => undefined,
|
|
724
|
+
() => undefined,
|
|
725
|
+
);
|
|
726
|
+
this.#toolRegistrationBarrier = registrationBarrier;
|
|
727
|
+
void registrationBarrier.then(() => {
|
|
728
|
+
if (this.#toolRegistrationBarrier === registrationBarrier) this.#toolRegistrationBarrier = undefined;
|
|
729
|
+
});
|
|
730
|
+
const scope = this.#toolRegistrationScope.getStore();
|
|
731
|
+
if (scope && !scope.closed) {
|
|
732
|
+
scope.pending.add(pending);
|
|
733
|
+
void pending.then(
|
|
734
|
+
() => scope.pending.delete(pending),
|
|
735
|
+
() => {},
|
|
736
|
+
);
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
void pending.catch(error => {
|
|
740
|
+
this.emitError({
|
|
741
|
+
extensionPath: extension.path,
|
|
742
|
+
event: "tool_registration",
|
|
743
|
+
error: error instanceof Error ? error.message : String(error),
|
|
744
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
745
|
+
});
|
|
746
|
+
});
|
|
747
|
+
};
|
|
748
|
+
const wrapped: ToolRegistrationListener = toolName => {
|
|
749
|
+
const tool = extension.tools.get(toolName);
|
|
750
|
+
if (!tool) return;
|
|
751
|
+
try {
|
|
752
|
+
const scope = this.#toolRegistrationScope.getStore();
|
|
753
|
+
const registrationSignal =
|
|
754
|
+
scope && !scope.closed ? scope.signal : AbortSignal.timeout(extensionHandlerTimeoutMs);
|
|
755
|
+
const pending = listener(tool, registrationSignal);
|
|
756
|
+
if (pending) trackRegistration(pending);
|
|
757
|
+
} catch (error) {
|
|
758
|
+
trackRegistration(Promise.reject(error));
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
extension.toolRegistrationListeners ??= new Set();
|
|
762
|
+
extension.toolRegistrationListeners.add(wrapped);
|
|
763
|
+
subscriptions.push({ extension, listener: wrapped });
|
|
764
|
+
}
|
|
765
|
+
return () => {
|
|
766
|
+
for (const subscription of subscriptions) {
|
|
767
|
+
subscription.extension.toolRegistrationListeners?.delete(subscription.listener);
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
async #flushToolRegistrations(pendingRegistrations: Set<Promise<void>>): Promise<void> {
|
|
773
|
+
let firstFailure: PromiseRejectedResult | undefined;
|
|
774
|
+
while (pendingRegistrations.size > 0) {
|
|
775
|
+
const pending = Array.from(pendingRegistrations);
|
|
776
|
+
const settled = await Promise.allSettled(pending);
|
|
777
|
+
for (let index = 0; index < settled.length; index += 1) {
|
|
778
|
+
pendingRegistrations.delete(pending[index]);
|
|
779
|
+
const result = settled[index];
|
|
780
|
+
if (!firstFailure && result?.status === "rejected") firstFailure = result;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
if (firstFailure) throw firstFailure.reason;
|
|
784
|
+
}
|
|
785
|
+
|
|
677
786
|
/**
|
|
678
787
|
* Aggregate the registered CLI flags across a set of extensions (last write
|
|
679
788
|
* wins on name collision). Static so callers that need the flag set before a
|
|
@@ -928,45 +1037,73 @@ export class ExtensionRunner {
|
|
|
928
1037
|
ctx: ExtensionContext,
|
|
929
1038
|
ext: Extension,
|
|
930
1039
|
timeoutMs: number,
|
|
1040
|
+
onFailure?: (kind: "timeout" | "error", message: string) => TResult,
|
|
931
1041
|
): Promise<TResult | undefined> {
|
|
932
1042
|
const signal =
|
|
933
1043
|
event.type === "session_stop" && "signal" in event && event.signal instanceof AbortSignal
|
|
934
1044
|
? event.signal
|
|
935
1045
|
: undefined;
|
|
936
1046
|
if (signal?.aborted) return undefined;
|
|
1047
|
+
const registrationScope: ToolRegistrationScope = { pending: new Set(), closed: false };
|
|
1048
|
+
let handlerResult: TResult | typeof EXTENSION_HANDLER_TIMEOUT | typeof EXTENSION_HANDLER_ABORTED | undefined;
|
|
1049
|
+
let handlerFailure: { error: unknown } | undefined;
|
|
937
1050
|
try {
|
|
938
|
-
|
|
939
|
-
handlerSignal =>
|
|
1051
|
+
handlerResult = await raceHandlerWithTimeout(
|
|
1052
|
+
async handlerSignal => {
|
|
1053
|
+
registrationScope.signal = handlerSignal;
|
|
1054
|
+
let result: TResult | undefined;
|
|
1055
|
+
try {
|
|
1056
|
+
result = await this.#toolRegistrationScope.run(registrationScope, () =>
|
|
1057
|
+
handler(event, createHandlerContext(ctx, handlerSignal)),
|
|
1058
|
+
);
|
|
1059
|
+
} catch (error) {
|
|
1060
|
+
handlerFailure = { error };
|
|
1061
|
+
} finally {
|
|
1062
|
+
registrationScope.closed = true;
|
|
1063
|
+
}
|
|
1064
|
+
try {
|
|
1065
|
+
await this.#flushToolRegistrations(registrationScope.pending);
|
|
1066
|
+
} catch (error) {
|
|
1067
|
+
handlerFailure ??= { error };
|
|
1068
|
+
}
|
|
1069
|
+
return result;
|
|
1070
|
+
},
|
|
940
1071
|
timeoutMs,
|
|
941
1072
|
signal,
|
|
942
1073
|
);
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1074
|
+
} catch (error) {
|
|
1075
|
+
handlerFailure = { error };
|
|
1076
|
+
} finally {
|
|
1077
|
+
registrationScope.closed = true;
|
|
1078
|
+
}
|
|
1079
|
+
if (handlerResult === EXTENSION_HANDLER_ABORTED) return undefined;
|
|
1080
|
+
if (handlerResult === EXTENSION_HANDLER_TIMEOUT) {
|
|
1081
|
+
const error = `handler timed out after ${timeoutMs}ms`;
|
|
1082
|
+
logger.warn("Extension handler timed out", {
|
|
1083
|
+
extensionPath: ext.path,
|
|
1084
|
+
event: event.type,
|
|
1085
|
+
timeoutMs,
|
|
1086
|
+
});
|
|
1087
|
+
this.emitError({
|
|
1088
|
+
extensionPath: ext.path,
|
|
1089
|
+
event: event.type,
|
|
1090
|
+
error,
|
|
1091
|
+
});
|
|
1092
|
+
return onFailure?.("timeout", error);
|
|
1093
|
+
}
|
|
1094
|
+
if (handlerFailure) {
|
|
1095
|
+
const message =
|
|
1096
|
+
handlerFailure.error instanceof Error ? handlerFailure.error.message : String(handlerFailure.error);
|
|
1097
|
+
const stack = handlerFailure.error instanceof Error ? handlerFailure.error.stack : undefined;
|
|
962
1098
|
this.emitError({
|
|
963
1099
|
extensionPath: ext.path,
|
|
964
1100
|
event: event.type,
|
|
965
1101
|
error: message,
|
|
966
1102
|
stack,
|
|
967
1103
|
});
|
|
968
|
-
return
|
|
1104
|
+
return onFailure?.("error", message);
|
|
969
1105
|
}
|
|
1106
|
+
return handlerResult as TResult | undefined;
|
|
970
1107
|
}
|
|
971
1108
|
|
|
972
1109
|
async emit<TEvent extends RunnerEmitEvent>(event: TEvent): Promise<RunnerEmitResult<TEvent>> {
|
|
@@ -1100,46 +1237,26 @@ export class ExtensionRunner {
|
|
|
1100
1237
|
if (!handlers || handlers.length === 0) continue;
|
|
1101
1238
|
|
|
1102
1239
|
for (const handler of handlers) {
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
extensionPath: ext.path,
|
|
1118
|
-
event: "tool_call",
|
|
1119
|
-
error,
|
|
1120
|
-
});
|
|
1121
|
-
return {
|
|
1122
|
-
block: true,
|
|
1123
|
-
reason: `Extension ${ext.path} timed out after ${timeoutMs}ms`,
|
|
1124
|
-
};
|
|
1125
|
-
}
|
|
1240
|
+
const handlerResult = await this.#runHandlerWithTimeout(
|
|
1241
|
+
handler,
|
|
1242
|
+
event,
|
|
1243
|
+
ctx,
|
|
1244
|
+
ext,
|
|
1245
|
+
timeoutMs,
|
|
1246
|
+
(kind, message) => ({
|
|
1247
|
+
block: true,
|
|
1248
|
+
reason:
|
|
1249
|
+
kind === "timeout"
|
|
1250
|
+
? `Extension ${ext.path} timed out after ${timeoutMs}ms`
|
|
1251
|
+
: `Extension ${ext.path} failed: ${message}`,
|
|
1252
|
+
}),
|
|
1253
|
+
);
|
|
1126
1254
|
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
}
|
|
1255
|
+
if (handlerResult) {
|
|
1256
|
+
result = handlerResult;
|
|
1257
|
+
if (result.block) {
|
|
1258
|
+
return result;
|
|
1132
1259
|
}
|
|
1133
|
-
} catch (err) {
|
|
1134
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1135
|
-
const stack = err instanceof Error ? err.stack : undefined;
|
|
1136
|
-
this.emitError({
|
|
1137
|
-
extensionPath: ext.path,
|
|
1138
|
-
event: "tool_call",
|
|
1139
|
-
error: message,
|
|
1140
|
-
stack,
|
|
1141
|
-
});
|
|
1142
|
-
return { block: true, reason: `Extension ${ext.path} failed: ${message}` };
|
|
1143
1260
|
}
|
|
1144
1261
|
}
|
|
1145
1262
|
}
|
|
@@ -1242,13 +1359,14 @@ export class ExtensionRunner {
|
|
|
1242
1359
|
| InputEventResult
|
|
1243
1360
|
| undefined;
|
|
1244
1361
|
if (result?.handled) return result;
|
|
1245
|
-
if (result?.text !== undefined)
|
|
1246
|
-
|
|
1247
|
-
currentImages = result.images ?? currentImages;
|
|
1248
|
-
}
|
|
1362
|
+
if (result?.text !== undefined) currentText = result.text;
|
|
1363
|
+
if (result?.images !== undefined) currentImages = result.images;
|
|
1249
1364
|
}
|
|
1250
1365
|
}
|
|
1251
|
-
|
|
1366
|
+
const transformed: InputEventResult = {};
|
|
1367
|
+
if (currentText !== text) transformed.text = currentText;
|
|
1368
|
+
if (currentImages !== images) transformed.images = currentImages;
|
|
1369
|
+
return transformed;
|
|
1252
1370
|
}
|
|
1253
1371
|
|
|
1254
1372
|
async emitContext(messages: AgentMessage[]): Promise<AgentMessage[]> {
|
|
@@ -38,7 +38,16 @@ import type {
|
|
|
38
38
|
TSchema,
|
|
39
39
|
} from "@oh-my-pi/pi-ai";
|
|
40
40
|
import type { OAuthCredentials, OAuthLoginCallbacks } from "@oh-my-pi/pi-ai/oauth/types";
|
|
41
|
-
import type {
|
|
41
|
+
import type {
|
|
42
|
+
AutocompleteItem,
|
|
43
|
+
AutocompleteProvider,
|
|
44
|
+
Component,
|
|
45
|
+
EditorTheme,
|
|
46
|
+
KeyId,
|
|
47
|
+
OverlayHandle,
|
|
48
|
+
OverlayOptions,
|
|
49
|
+
TUI,
|
|
50
|
+
} from "@oh-my-pi/pi-tui";
|
|
42
51
|
import type { logger as PiLogger } from "@oh-my-pi/pi-utils";
|
|
43
52
|
import type { KeybindingsManager } from "../../config/keybindings";
|
|
44
53
|
import type { ModelRegistry } from "../../config/model-registry";
|
|
@@ -105,6 +114,7 @@ import type {
|
|
|
105
114
|
} from "../shared-events";
|
|
106
115
|
import type { SlashCommandInfo } from "../slash-commands";
|
|
107
116
|
|
|
117
|
+
export type { OverlayHandle, OverlayOptions } from "@oh-my-pi/pi-tui";
|
|
108
118
|
export type { AppKeybinding, KeybindingsManager } from "../../config/keybindings";
|
|
109
119
|
export type { ExecOptions, ExecResult } from "../../exec/exec";
|
|
110
120
|
export type { AgentToolResult, AgentToolUpdateCallback };
|
|
@@ -214,6 +224,16 @@ export type ExtensionUiComponent = Component & { dispose?(): void };
|
|
|
214
224
|
export type ExtensionUiComponentFactory = (tui: TUI, theme: Theme) => ExtensionUiComponent;
|
|
215
225
|
export type ExtensionWidgetContent = string[] | ExtensionUiComponentFactory | undefined;
|
|
216
226
|
|
|
227
|
+
/** Options for `ExtensionUIContext.custom()` (overlay rendering of a custom component). */
|
|
228
|
+
export interface ExtensionCustomOptions {
|
|
229
|
+
/** Render the component as an overlay over the transcript instead of replacing the editor area. */
|
|
230
|
+
overlay?: boolean;
|
|
231
|
+
/** Static or lazily resolved overlay positioning/sizing options forwarded to `showOverlay`. */
|
|
232
|
+
overlayOptions?: OverlayOptions | (() => OverlayOptions);
|
|
233
|
+
/** Invoked with the overlay handle once the overlay is created (overlay mode only). */
|
|
234
|
+
onHandle?: (handle: OverlayHandle) => void;
|
|
235
|
+
}
|
|
236
|
+
|
|
217
237
|
/** Wrap the current autocomplete provider with additional behavior (pi-compatible). */
|
|
218
238
|
export type AutocompleteProviderFactory = (current: AutocompleteProvider) => AutocompleteProvider;
|
|
219
239
|
|
|
@@ -280,7 +300,7 @@ export interface ExtensionUIContext {
|
|
|
280
300
|
keybindings: KeybindingsManager,
|
|
281
301
|
done: (result: T) => void,
|
|
282
302
|
) => ExtensionUiComponent | Promise<ExtensionUiComponent>,
|
|
283
|
-
options?:
|
|
303
|
+
options?: ExtensionCustomOptions,
|
|
284
304
|
): Promise<T>;
|
|
285
305
|
|
|
286
306
|
/** Set the text in the core input editor. */
|
|
@@ -1467,6 +1487,9 @@ export interface RegisteredTool<TParams extends TSchema = TSchema, TDetails = un
|
|
|
1467
1487
|
extensionPath: string;
|
|
1468
1488
|
}
|
|
1469
1489
|
|
|
1490
|
+
/** Internal observer invoked when an already-loaded extension registers or replaces a tool. */
|
|
1491
|
+
export type ToolRegistrationListener = (toolName: string) => void;
|
|
1492
|
+
|
|
1470
1493
|
export interface ExtensionFlag {
|
|
1471
1494
|
name: string;
|
|
1472
1495
|
description?: string;
|
|
@@ -1589,6 +1612,7 @@ export interface Extension {
|
|
|
1589
1612
|
label?: string;
|
|
1590
1613
|
handlers: Map<string, HandlerFn[]>;
|
|
1591
1614
|
tools: Map<string, RegisteredTool<any, any>>;
|
|
1615
|
+
toolRegistrationListeners?: Set<ToolRegistrationListener>;
|
|
1592
1616
|
assistantThinkingRenderers: AssistantThinkingRenderer[];
|
|
1593
1617
|
messageRenderers: Map<string, MessageRenderer>;
|
|
1594
1618
|
commands: Map<string, RegisteredCommand>;
|