@oh-my-pi/pi-coding-agent 15.3.2 → 15.4.2
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 +110 -0
- package/dist/types/cli/file-processor.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +45 -3
- package/dist/types/config/settings.d.ts +1 -1
- package/dist/types/debug/raw-sse.d.ts +2 -0
- package/dist/types/edit/file-read-cache.d.ts +15 -4
- package/dist/types/edit/index.d.ts +3 -8
- package/dist/types/edit/renderer.d.ts +1 -2
- package/dist/types/eval/__tests__/shared-executors.test.d.ts +1 -0
- package/dist/types/eval/js/shared/local-module-loader.d.ts +16 -0
- package/dist/types/eval/js/shared/rewrite-imports.d.ts +4 -0
- package/dist/types/eval/js/shared/runtime.d.ts +14 -8
- package/dist/types/eval/py/executor.d.ts +1 -2
- package/dist/types/eval/py/kernel.d.ts +6 -0
- package/dist/types/eval/py/tool-bridge.d.ts +1 -5
- package/dist/types/eval/session-id.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +1 -3
- package/dist/types/hashline/anchors.d.ts +15 -9
- package/dist/types/hashline/constants.d.ts +0 -2
- package/dist/types/hashline/diff.d.ts +1 -2
- package/dist/types/hashline/executor.d.ts +52 -0
- package/dist/types/hashline/hash.d.ts +44 -93
- package/dist/types/hashline/index.d.ts +2 -1
- package/dist/types/hashline/input.d.ts +2 -9
- package/dist/types/hashline/recovery.d.ts +3 -9
- package/dist/types/hashline/tokenizer.d.ts +91 -0
- package/dist/types/hashline/types.d.ts +5 -7
- package/dist/types/modes/components/extensions/types.d.ts +0 -4
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +11 -15
- package/dist/types/session/agent-storage.d.ts +11 -10
- package/dist/types/slash-commands/acp-builtins.d.ts +3 -3
- package/dist/types/slash-commands/types.d.ts +0 -5
- package/dist/types/task/executor.d.ts +2 -0
- package/dist/types/tool-discovery/tool-index.d.ts +0 -50
- package/dist/types/tools/index.d.ts +2 -8
- package/dist/types/tools/match-line-format.d.ts +4 -4
- package/dist/types/tools/output-schema-validator.d.ts +64 -0
- package/dist/types/tools/review.d.ts +13 -0
- package/dist/types/tools/search-tool-bm25.d.ts +1 -1
- package/dist/types/tools/search.d.ts +4 -3
- package/dist/types/utils/edit-mode.d.ts +1 -1
- package/dist/types/web/kagi.d.ts +4 -2
- package/dist/types/web/parallel.d.ts +4 -3
- package/dist/types/web/scrapers/types.d.ts +2 -1
- package/dist/types/web/search/index.d.ts +12 -4
- package/dist/types/web/search/provider.d.ts +2 -1
- package/dist/types/web/search/providers/anthropic.d.ts +9 -4
- package/dist/types/web/search/providers/base.d.ts +34 -2
- package/dist/types/web/search/providers/brave.d.ts +8 -1
- package/dist/types/web/search/providers/codex.d.ts +13 -9
- package/dist/types/web/search/providers/exa.d.ts +10 -1
- package/dist/types/web/search/providers/gemini.d.ts +20 -23
- package/dist/types/web/search/providers/jina.d.ts +2 -1
- package/dist/types/web/search/providers/kagi.d.ts +4 -1
- package/dist/types/web/search/providers/kimi.d.ts +10 -1
- package/dist/types/web/search/providers/parallel.d.ts +3 -2
- package/dist/types/web/search/providers/perplexity.d.ts +5 -2
- package/dist/types/web/search/providers/searxng.d.ts +2 -1
- package/dist/types/web/search/providers/synthetic.d.ts +5 -8
- package/dist/types/web/search/providers/tavily.d.ts +11 -4
- package/dist/types/web/search/providers/utils.d.ts +8 -6
- package/dist/types/web/search/providers/zai.d.ts +12 -3
- package/package.json +7 -7
- package/src/cli/file-processor.ts +12 -2
- package/src/cli.ts +0 -8
- package/src/commands/commit.ts +8 -8
- package/src/config/prompt-templates.ts +6 -6
- package/src/config/settings-schema.ts +47 -3
- package/src/config/settings.ts +5 -5
- package/src/debug/raw-sse.ts +68 -3
- package/src/edit/file-read-cache.ts +68 -25
- package/src/edit/index.ts +6 -37
- package/src/edit/renderer.ts +9 -47
- package/src/edit/streaming.ts +43 -56
- package/src/eval/__tests__/shared-executors.test.ts +520 -0
- package/src/eval/js/context-manager.ts +64 -53
- package/src/eval/js/shared/local-module-loader.ts +265 -0
- package/src/eval/js/shared/prelude.txt +4 -0
- package/src/eval/js/shared/rewrite-imports.ts +85 -0
- package/src/eval/js/shared/runtime.ts +129 -86
- package/src/eval/js/worker-core.ts +23 -38
- package/src/eval/py/executor.ts +155 -84
- package/src/eval/py/kernel.ts +10 -1
- package/src/eval/py/prelude.py +22 -24
- package/src/eval/py/runner.py +203 -85
- package/src/eval/py/tool-bridge.ts +17 -10
- package/src/eval/session-id.ts +8 -0
- package/src/exec/bash-executor.ts +27 -16
- package/src/extensibility/extensions/runner.ts +0 -1
- package/src/extensibility/extensions/types.ts +1 -3
- package/src/hashline/anchors.ts +56 -65
- package/src/hashline/apply.ts +29 -31
- package/src/hashline/constants.ts +0 -3
- package/src/hashline/diff-preview.ts +4 -5
- package/src/hashline/diff.ts +30 -4
- package/src/hashline/execute.ts +91 -26
- package/src/hashline/executor.ts +239 -0
- package/src/hashline/grammar.lark +12 -10
- package/src/hashline/hash.ts +69 -114
- package/src/hashline/index.ts +2 -1
- package/src/hashline/input.ts +48 -41
- package/src/hashline/prefixes.ts +21 -11
- package/src/hashline/recovery.ts +63 -71
- package/src/hashline/stream.ts +2 -2
- package/src/hashline/tokenizer.ts +467 -0
- package/src/hashline/types.ts +6 -8
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/modes/components/extensions/types.ts +0 -5
- package/src/modes/components/session-observer-overlay.ts +11 -2
- package/src/modes/components/settings-selector.ts +10 -1
- package/src/modes/components/tree-selector.ts +10 -2
- package/src/modes/controllers/command-controller.ts +1 -3
- package/src/modes/controllers/extension-ui-controller.ts +10 -11
- package/src/modes/controllers/selector-controller.ts +5 -5
- package/src/modes/theme/theme.ts +4 -2
- package/src/modes/types.ts +4 -1
- package/src/modes/utils/ui-helpers.ts +4 -0
- package/src/prompts/agents/explore.md +1 -1
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/eval.md +1 -1
- package/src/prompts/tools/hashline.md +73 -94
- package/src/prompts/tools/read.md +4 -4
- package/src/prompts/tools/search.md +3 -3
- package/src/sdk.ts +33 -26
- package/src/session/agent-session.ts +59 -66
- package/src/session/agent-storage.ts +13 -14
- package/src/slash-commands/acp-builtins.ts +3 -3
- package/src/slash-commands/types.ts +0 -6
- package/src/task/executor.ts +26 -57
- package/src/task/index.ts +8 -4
- package/src/tool-discovery/tool-index.ts +0 -134
- package/src/tools/ast-edit.ts +36 -13
- package/src/tools/ast-grep.ts +45 -4
- package/src/tools/browser/tab-worker.ts +3 -2
- package/src/tools/eval.ts +2 -1
- package/src/tools/fetch.ts +23 -14
- package/src/tools/index.ts +2 -8
- package/src/tools/irc.ts +59 -5
- package/src/tools/match-line-format.ts +5 -7
- package/src/tools/output-schema-validator.ts +132 -0
- package/src/tools/read.ts +142 -31
- package/src/tools/review.ts +23 -0
- package/src/tools/search-tool-bm25.ts +3 -30
- package/src/tools/search.ts +48 -16
- package/src/tools/write.ts +3 -3
- package/src/tools/yield.ts +32 -41
- package/src/utils/edit-mode.ts +1 -2
- package/src/utils/file-mentions.ts +2 -2
- package/src/web/kagi.ts +15 -6
- package/src/web/parallel.ts +9 -6
- package/src/web/scrapers/types.ts +7 -1
- package/src/web/scrapers/youtube.ts +13 -7
- package/src/web/search/index.ts +37 -11
- package/src/web/search/provider.ts +5 -3
- package/src/web/search/providers/anthropic.ts +30 -21
- package/src/web/search/providers/base.ts +35 -2
- package/src/web/search/providers/brave.ts +4 -4
- package/src/web/search/providers/codex.ts +118 -89
- package/src/web/search/providers/exa.ts +3 -2
- package/src/web/search/providers/gemini.ts +58 -155
- package/src/web/search/providers/jina.ts +4 -4
- package/src/web/search/providers/kagi.ts +17 -11
- package/src/web/search/providers/kimi.ts +29 -13
- package/src/web/search/providers/parallel.ts +171 -23
- package/src/web/search/providers/perplexity.ts +38 -37
- package/src/web/search/providers/searxng.ts +3 -1
- package/src/web/search/providers/synthetic.ts +16 -19
- package/src/web/search/providers/tavily.ts +23 -18
- package/src/web/search/providers/utils.ts +11 -17
- package/src/web/search/providers/zai.ts +16 -8
- package/dist/types/hashline/parser.d.ts +0 -7
- package/dist/types/mcp/discoverable-tool-metadata.d.ts +0 -7
- package/dist/types/tools/vim.d.ts +0 -58
- package/dist/types/vim/buffer.d.ts +0 -41
- package/dist/types/vim/commands.d.ts +0 -6
- package/dist/types/vim/engine.d.ts +0 -47
- package/dist/types/vim/parser.d.ts +0 -3
- package/dist/types/vim/render.d.ts +0 -25
- package/dist/types/vim/types.d.ts +0 -182
- package/src/hashline/parser.ts +0 -246
- package/src/mcp/discoverable-tool-metadata.ts +0 -24
- package/src/prompts/tools/vim.md +0 -98
- package/src/tools/vim.ts +0 -949
- package/src/vim/buffer.ts +0 -309
- package/src/vim/commands.ts +0 -382
- package/src/vim/engine.ts +0 -2409
- package/src/vim/parser.ts +0 -134
- package/src/vim/render.ts +0 -252
- package/src/vim/types.ts +0 -197
|
@@ -3,6 +3,7 @@ import { JsRuntime, type RuntimeHooks } from "./shared/runtime";
|
|
|
3
3
|
import type { RunErrorPayload, SessionSnapshot, ToolReply, Transport, WorkerInbound } from "./worker-protocol";
|
|
4
4
|
|
|
5
5
|
interface PendingTool {
|
|
6
|
+
runId: string;
|
|
6
7
|
resolve(value: unknown): void;
|
|
7
8
|
reject(error: Error): void;
|
|
8
9
|
}
|
|
@@ -36,8 +37,7 @@ function errorFromPayload(payload: RunErrorPayload): Error {
|
|
|
36
37
|
export class WorkerCore {
|
|
37
38
|
#transport: Transport;
|
|
38
39
|
#runtime: JsRuntime | null = null;
|
|
39
|
-
#
|
|
40
|
-
#active: ActiveRun | null = null;
|
|
40
|
+
#runs = new Map<string, ActiveRun>();
|
|
41
41
|
#unsubscribe: () => void;
|
|
42
42
|
|
|
43
43
|
constructor(transport: Transport) {
|
|
@@ -52,7 +52,7 @@ export class WorkerCore {
|
|
|
52
52
|
this.#ensureRuntime(msg.snapshot);
|
|
53
53
|
return;
|
|
54
54
|
case "run":
|
|
55
|
-
this.#
|
|
55
|
+
void this.#runOne(msg.runId, msg.code, msg.filename, msg.snapshot);
|
|
56
56
|
return;
|
|
57
57
|
case "tool-reply":
|
|
58
58
|
this.#deliverToolReply(msg.id, msg.reply);
|
|
@@ -71,73 +71,58 @@ export class WorkerCore {
|
|
|
71
71
|
this.#runtime = new JsRuntime({
|
|
72
72
|
initialCwd: snapshot.cwd,
|
|
73
73
|
sessionId: snapshot.sessionId,
|
|
74
|
-
getHooks: () => this.#hooksForCurrentRun(),
|
|
75
74
|
});
|
|
76
75
|
return this.#runtime;
|
|
77
76
|
}
|
|
78
77
|
|
|
79
|
-
#
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
78
|
+
async #runOne(runId: string, code: string, filename: string, snapshot: SessionSnapshot): Promise<void> {
|
|
79
|
+
const runtime = this.#ensureRuntime(snapshot);
|
|
80
|
+
runtime.setCwd(snapshot.cwd);
|
|
81
|
+
const active: ActiveRun = { runId, pendingTools: new Map() };
|
|
82
|
+
this.#runs.set(runId, active);
|
|
83
|
+
const hooks: RuntimeHooks = {
|
|
84
84
|
onText: chunk => this.#transport.send({ type: "text", runId, chunk }),
|
|
85
85
|
onDisplay: output => this.#transport.send({ type: "display", runId, output }),
|
|
86
86
|
callTool: (name, args) => this.#callTool(active, name, args),
|
|
87
87
|
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
#enqueueRun(runId: string, code: string, filename: string, snapshot: SessionSnapshot): void {
|
|
91
|
-
const previous = this.#queue;
|
|
92
|
-
const next = (async () => {
|
|
93
|
-
await previous.catch(() => undefined);
|
|
94
|
-
await this.#runOne(runId, code, filename, snapshot);
|
|
95
|
-
})();
|
|
96
|
-
this.#queue = next.catch(() => undefined);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
async #runOne(runId: string, code: string, filename: string, snapshot: SessionSnapshot): Promise<void> {
|
|
100
|
-
const runtime = this.#ensureRuntime(snapshot);
|
|
101
|
-
runtime.setCwd(snapshot.cwd);
|
|
102
|
-
this.#active = { runId, pendingTools: new Map() };
|
|
103
88
|
try {
|
|
104
|
-
const value = await runtime.run(code, filename);
|
|
105
|
-
runtime.displayValue(value);
|
|
89
|
+
const value = await runtime.run(code, filename, hooks, { runId, cwd: snapshot.cwd });
|
|
90
|
+
runtime.displayValue(value, hooks);
|
|
106
91
|
this.#transport.send({ type: "result", runId, ok: true });
|
|
107
92
|
} catch (error) {
|
|
108
93
|
this.#transport.send({ type: "result", runId, ok: false, error: errorPayload(error) });
|
|
109
94
|
} finally {
|
|
110
|
-
this.#
|
|
95
|
+
this.#runs.delete(runId);
|
|
111
96
|
}
|
|
112
97
|
}
|
|
113
98
|
|
|
114
99
|
async #callTool(active: ActiveRun, name: string, args: unknown): Promise<unknown> {
|
|
115
100
|
const id = `tc-${active.runId}-${crypto.randomUUID()}`;
|
|
116
101
|
const { promise, resolve, reject } = Promise.withResolvers<unknown>();
|
|
117
|
-
active.pendingTools.set(id, { resolve, reject });
|
|
102
|
+
active.pendingTools.set(id, { runId: active.runId, resolve, reject });
|
|
118
103
|
this.#transport.send({ type: "tool-call", id, runId: active.runId, name, args });
|
|
119
104
|
return await promise;
|
|
120
105
|
}
|
|
121
106
|
|
|
122
107
|
#deliverToolReply(id: string, reply: ToolReply): void {
|
|
123
|
-
const active
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
108
|
+
for (const active of this.#runs.values()) {
|
|
109
|
+
const pending = active.pendingTools.get(id);
|
|
110
|
+
if (!pending) continue;
|
|
111
|
+
active.pendingTools.delete(id);
|
|
112
|
+
if (reply.ok) pending.resolve(reply.value);
|
|
113
|
+
else pending.reject(errorFromPayload(reply.error));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
130
116
|
}
|
|
131
117
|
|
|
132
118
|
#close(): void {
|
|
133
|
-
const active
|
|
134
|
-
if (active) {
|
|
119
|
+
for (const active of this.#runs.values()) {
|
|
135
120
|
for (const pending of active.pendingTools.values()) {
|
|
136
121
|
pending.reject(new ToolError("JS worker closed"));
|
|
137
122
|
}
|
|
138
123
|
active.pendingTools.clear();
|
|
139
124
|
}
|
|
140
|
-
this.#
|
|
125
|
+
this.#runs.clear();
|
|
141
126
|
this.#runtime = null;
|
|
142
127
|
this.#transport.send({ type: "closed" });
|
|
143
128
|
this.#unsubscribe();
|
package/src/eval/py/executor.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
|
|
1
3
|
import { getProjectDir, logger } from "@oh-my-pi/pi-utils";
|
|
2
4
|
import { Settings } from "../../config/settings";
|
|
3
5
|
import { OutputSink } from "../../session/streaming-output";
|
|
4
6
|
import type { ToolSession } from "../../tools";
|
|
5
7
|
import { resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "../../tools/output-meta";
|
|
6
8
|
import type { JsStatusEvent } from "../js/shared/types";
|
|
7
|
-
import type { KernelDisplayOutput } from "./display";
|
|
8
9
|
import {
|
|
9
10
|
checkPythonKernelAvailability,
|
|
11
|
+
type KernelDisplayOutput,
|
|
10
12
|
type KernelExecuteOptions,
|
|
11
13
|
type KernelExecuteResult,
|
|
14
|
+
type KernelRuntimeEnv,
|
|
12
15
|
PythonKernel,
|
|
13
16
|
} from "./kernel";
|
|
14
17
|
import { ensurePyToolBridge, registerPyToolBridge } from "./tool-bridge";
|
|
@@ -92,20 +95,32 @@ export interface PythonResult {
|
|
|
92
95
|
// ---------------------------------------------------------------------------
|
|
93
96
|
// Session bookkeeping
|
|
94
97
|
//
|
|
95
|
-
// One PythonKernel subprocess per session id
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
+
// One PythonKernel subprocess per (session id, cwd) tuple. The runner mutates
|
|
99
|
+
// process-global cwd/sys.path during execution, so cross-directory work MUST
|
|
100
|
+
// never share a live kernel. Multiple agent owners can still register against
|
|
101
|
+
// the same tuple; the kernel stays alive until the last owner detaches.
|
|
98
102
|
// ---------------------------------------------------------------------------
|
|
99
103
|
|
|
100
104
|
interface PythonSession {
|
|
105
|
+
sessionKey: string;
|
|
101
106
|
sessionId: string;
|
|
107
|
+
cwd: string;
|
|
102
108
|
kernel: PythonKernel;
|
|
103
109
|
ownerIds: Set<string>;
|
|
104
110
|
hasFallbackOwner: boolean;
|
|
105
|
-
queue: Promise<void>;
|
|
106
111
|
}
|
|
107
112
|
|
|
108
113
|
const sessions = new Map<string, PythonSession>();
|
|
114
|
+
const startingSessions = new Map<string, Promise<PythonSession>>();
|
|
115
|
+
const resettingSessions = new Set<string>();
|
|
116
|
+
|
|
117
|
+
function normalizeSessionCwd(cwd: string): string {
|
|
118
|
+
return path.resolve(cwd);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function buildSessionKey(sessionId: string, cwd: string): string {
|
|
122
|
+
return `${sessionId}\0${normalizeSessionCwd(cwd)}`;
|
|
123
|
+
}
|
|
109
124
|
|
|
110
125
|
// ---------------------------------------------------------------------------
|
|
111
126
|
// Cancellation plumbing
|
|
@@ -240,19 +255,40 @@ function createCancelledPythonResult(timedOut: boolean, timeoutMs?: number): Pyt
|
|
|
240
255
|
// Kernel start helpers
|
|
241
256
|
// ---------------------------------------------------------------------------
|
|
242
257
|
|
|
258
|
+
const MANAGED_KERNEL_ENV_KEYS = [
|
|
259
|
+
"PI_SESSION_FILE",
|
|
260
|
+
"PI_ARTIFACTS_DIR",
|
|
261
|
+
"PI_TOOL_BRIDGE_URL",
|
|
262
|
+
"PI_TOOL_BRIDGE_TOKEN",
|
|
263
|
+
"PI_TOOL_BRIDGE_SESSION",
|
|
264
|
+
] as const;
|
|
265
|
+
|
|
266
|
+
function buildKernelEnvPatch(options: {
|
|
267
|
+
sessionFile?: string;
|
|
268
|
+
artifactsDir?: string;
|
|
269
|
+
bridgeSessionId?: string;
|
|
270
|
+
bridge?: { url: string; token: string };
|
|
271
|
+
}): KernelRuntimeEnv {
|
|
272
|
+
return {
|
|
273
|
+
PI_SESSION_FILE: options.sessionFile ?? null,
|
|
274
|
+
PI_ARTIFACTS_DIR: options.artifactsDir ?? null,
|
|
275
|
+
PI_TOOL_BRIDGE_URL: options.bridge?.url ?? null,
|
|
276
|
+
PI_TOOL_BRIDGE_TOKEN: options.bridge?.token ?? null,
|
|
277
|
+
PI_TOOL_BRIDGE_SESSION: options.bridge && options.bridgeSessionId ? options.bridgeSessionId : null,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
243
281
|
function buildKernelEnv(options: {
|
|
244
282
|
sessionFile?: string;
|
|
245
283
|
artifactsDir?: string;
|
|
246
284
|
bridgeSessionId?: string;
|
|
247
285
|
bridge?: { url: string; token: string };
|
|
248
286
|
}): Record<string, string> | undefined {
|
|
287
|
+
const patch = buildKernelEnvPatch(options);
|
|
249
288
|
const env: Record<string, string> = {};
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
env.PI_TOOL_BRIDGE_URL = options.bridge.url;
|
|
254
|
-
env.PI_TOOL_BRIDGE_TOKEN = options.bridge.token;
|
|
255
|
-
env.PI_TOOL_BRIDGE_SESSION = options.bridgeSessionId;
|
|
289
|
+
for (const key of MANAGED_KERNEL_ENV_KEYS) {
|
|
290
|
+
const value = patch[key];
|
|
291
|
+
if (value !== null) env[key] = value;
|
|
256
292
|
}
|
|
257
293
|
return Object.keys(env).length > 0 ? env : undefined;
|
|
258
294
|
}
|
|
@@ -282,23 +318,44 @@ function attachOwner(session: PythonSession, sessionId: string, ownerId: string
|
|
|
282
318
|
}
|
|
283
319
|
}
|
|
284
320
|
|
|
285
|
-
async function acquireSession(
|
|
286
|
-
|
|
321
|
+
async function acquireSession(
|
|
322
|
+
sessionKey: string,
|
|
323
|
+
sessionId: string,
|
|
324
|
+
cwd: string,
|
|
325
|
+
options: PythonExecutorOptions,
|
|
326
|
+
): Promise<PythonSession> {
|
|
327
|
+
const existing = sessions.get(sessionKey);
|
|
287
328
|
if (existing) {
|
|
288
329
|
attachOwner(existing, sessionId, options.kernelOwnerId);
|
|
289
330
|
return existing;
|
|
290
331
|
}
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
332
|
+
const starting = startingSessions.get(sessionKey);
|
|
333
|
+
if (starting) {
|
|
334
|
+
const session = await starting;
|
|
335
|
+
attachOwner(session, sessionId, options.kernelOwnerId);
|
|
336
|
+
return session;
|
|
337
|
+
}
|
|
338
|
+
const startup = (async () => {
|
|
339
|
+
const kernel = await startKernel(cwd, options);
|
|
340
|
+
const session: PythonSession = {
|
|
341
|
+
sessionKey,
|
|
342
|
+
sessionId,
|
|
343
|
+
cwd,
|
|
344
|
+
kernel,
|
|
345
|
+
ownerIds: new Set(),
|
|
346
|
+
hasFallbackOwner: false,
|
|
347
|
+
};
|
|
348
|
+
sessions.set(sessionKey, session);
|
|
349
|
+
return session;
|
|
350
|
+
})();
|
|
351
|
+
startingSessions.set(sessionKey, startup);
|
|
352
|
+
try {
|
|
353
|
+
const session = await startup;
|
|
354
|
+
attachOwner(session, sessionId, options.kernelOwnerId);
|
|
355
|
+
return session;
|
|
356
|
+
} finally {
|
|
357
|
+
if (startingSessions.get(sessionKey) === startup) startingSessions.delete(sessionKey);
|
|
358
|
+
}
|
|
302
359
|
}
|
|
303
360
|
|
|
304
361
|
async function replaceSessionKernel(
|
|
@@ -311,52 +368,40 @@ async function replaceSessionKernel(
|
|
|
311
368
|
await old
|
|
312
369
|
.shutdown(remaining !== undefined ? { timeoutMs: Math.max(0, remaining) } : undefined)
|
|
313
370
|
.catch(() => undefined);
|
|
314
|
-
if (sessions.get(session.
|
|
371
|
+
if (sessions.get(session.sessionKey) !== session) {
|
|
315
372
|
throw new PythonExecutionCancelledError(false);
|
|
316
373
|
}
|
|
317
374
|
requireRemainingTimeoutMs(options.deadlineMs);
|
|
318
375
|
const next = await startKernel(cwd, options);
|
|
319
|
-
if (sessions.get(session.
|
|
376
|
+
if (sessions.get(session.sessionKey) !== session) {
|
|
320
377
|
await next.shutdown().catch(() => undefined);
|
|
321
378
|
throw new PythonExecutionCancelledError(false);
|
|
322
379
|
}
|
|
323
380
|
session.kernel = next;
|
|
324
381
|
}
|
|
325
382
|
|
|
326
|
-
async function resetSession(
|
|
327
|
-
const existing = sessions.get(
|
|
383
|
+
async function resetSession(sessionKey: string): Promise<void> {
|
|
384
|
+
const existing = sessions.get(sessionKey) ?? (await startingSessions.get(sessionKey)?.catch(() => undefined));
|
|
328
385
|
if (!existing) return;
|
|
329
|
-
sessions.delete(
|
|
386
|
+
sessions.delete(sessionKey);
|
|
330
387
|
await existing.kernel.shutdown().catch(() => undefined);
|
|
331
388
|
}
|
|
332
389
|
|
|
333
|
-
async function runQueued<T>(
|
|
334
|
-
session: PythonSession,
|
|
335
|
-
options: Pick<PythonExecutorOptions, "signal" | "deadlineMs">,
|
|
336
|
-
work: () => Promise<T>,
|
|
337
|
-
): Promise<T> {
|
|
338
|
-
const previous = session.queue;
|
|
339
|
-
const { promise: ourSlot, resolve: releaseSlot } = Promise.withResolvers<void>();
|
|
340
|
-
// Keep the queue chained even if WE bail out: future runs must still wait
|
|
341
|
-
// for `previous` to finish before they touch the kernel.
|
|
342
|
-
session.queue = previous.catch(() => undefined).then(() => ourSlot);
|
|
343
|
-
try {
|
|
344
|
-
await waitForPromiseWithCancellation(
|
|
345
|
-
previous.catch(() => undefined),
|
|
346
|
-
options,
|
|
347
|
-
);
|
|
348
|
-
return await work();
|
|
349
|
-
} finally {
|
|
350
|
-
releaseSlot();
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
390
|
// ---------------------------------------------------------------------------
|
|
355
391
|
// Public dispose entry points
|
|
356
392
|
// ---------------------------------------------------------------------------
|
|
357
393
|
|
|
358
394
|
export async function disposeAllKernelSessions(): Promise<void> {
|
|
395
|
+
const pending = [...startingSessions.values()];
|
|
396
|
+
startingSessions.clear();
|
|
397
|
+
const started = await Promise.allSettled(pending);
|
|
359
398
|
const all = [...sessions.entries()];
|
|
399
|
+
for (const result of started) {
|
|
400
|
+
if (result.status !== "fulfilled") continue;
|
|
401
|
+
if (!all.some(([, session]) => session === result.value)) {
|
|
402
|
+
all.push([result.value.sessionKey, result.value]);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
360
405
|
for (const [id, session] of all) {
|
|
361
406
|
if (sessions.get(id) === session) sessions.delete(id);
|
|
362
407
|
}
|
|
@@ -366,7 +411,12 @@ export async function disposeAllKernelSessions(): Promise<void> {
|
|
|
366
411
|
const result = results[i];
|
|
367
412
|
if (result.status === "fulfilled" && result.value?.confirmed !== false) continue;
|
|
368
413
|
const reason = result.status === "rejected" ? result.reason : "not confirmed";
|
|
369
|
-
logger.warn("Python kernel shutdown not confirmed", {
|
|
414
|
+
logger.warn("Python kernel shutdown not confirmed", {
|
|
415
|
+
sessionId: session.sessionId,
|
|
416
|
+
sessionKey: id,
|
|
417
|
+
cwd: session.cwd,
|
|
418
|
+
reason,
|
|
419
|
+
});
|
|
370
420
|
if (!sessions.has(id)) sessions.set(id, session);
|
|
371
421
|
}
|
|
372
422
|
}
|
|
@@ -382,7 +432,7 @@ export async function disposeKernelSessionsByOwner(ownerId: string): Promise<voi
|
|
|
382
432
|
session.ownerIds.delete(ownerId);
|
|
383
433
|
}
|
|
384
434
|
for (const session of toShutdown) {
|
|
385
|
-
if (sessions.get(session.
|
|
435
|
+
if (sessions.get(session.sessionKey) === session) sessions.delete(session.sessionKey);
|
|
386
436
|
}
|
|
387
437
|
const results = await Promise.allSettled(toShutdown.map(session => session.kernel.shutdown()));
|
|
388
438
|
for (let i = 0; i < toShutdown.length; i += 1) {
|
|
@@ -393,8 +443,13 @@ export async function disposeKernelSessionsByOwner(ownerId: string): Promise<voi
|
|
|
393
443
|
continue;
|
|
394
444
|
}
|
|
395
445
|
const reason = result.status === "rejected" ? result.reason : "not confirmed";
|
|
396
|
-
logger.warn("Python kernel shutdown not confirmed", {
|
|
397
|
-
|
|
446
|
+
logger.warn("Python kernel shutdown not confirmed", {
|
|
447
|
+
sessionId: session.sessionId,
|
|
448
|
+
sessionKey: session.sessionKey,
|
|
449
|
+
cwd: session.cwd,
|
|
450
|
+
reason,
|
|
451
|
+
});
|
|
452
|
+
if (!sessions.has(session.sessionKey)) sessions.set(session.sessionKey, session);
|
|
398
453
|
}
|
|
399
454
|
}
|
|
400
455
|
|
|
@@ -424,9 +479,10 @@ async function executeWithKernel(
|
|
|
424
479
|
((event: JsStatusEvent) => {
|
|
425
480
|
displayOutputs.push({ type: "status", event });
|
|
426
481
|
});
|
|
482
|
+
const runId = `py-${crypto.randomUUID()}`;
|
|
427
483
|
const unregisterBridge =
|
|
428
484
|
options?.toolSession && options?.bridgeSessionId
|
|
429
|
-
? registerPyToolBridge(options.bridgeSessionId, {
|
|
485
|
+
? registerPyToolBridge(options.bridgeSessionId, runId, {
|
|
430
486
|
toolSession: options.toolSession,
|
|
431
487
|
signal: options.signal,
|
|
432
488
|
emitStatus,
|
|
@@ -436,6 +492,9 @@ async function executeWithKernel(
|
|
|
436
492
|
try {
|
|
437
493
|
executionTimeoutMs = requireRemainingTimeoutMs(deadlineMs);
|
|
438
494
|
const result = await kernel.execute(code, {
|
|
495
|
+
cwd: options?.cwd,
|
|
496
|
+
env: buildKernelEnvPatch(options ?? {}),
|
|
497
|
+
id: runId,
|
|
439
498
|
signal: options?.signal,
|
|
440
499
|
timeoutMs: executionTimeoutMs,
|
|
441
500
|
onChunk: text => sink.push(text),
|
|
@@ -516,7 +575,7 @@ async function executePerCall(code: string, cwd: string, options: PythonExecutor
|
|
|
516
575
|
}
|
|
517
576
|
const kernel = await startKernel(cwd, options);
|
|
518
577
|
try {
|
|
519
|
-
return await executeWithKernel(kernel, code, options);
|
|
578
|
+
return await executeWithKernel(kernel, code, { ...options, cwd: undefined });
|
|
520
579
|
} finally {
|
|
521
580
|
await kernel.shutdown().catch(() => undefined);
|
|
522
581
|
}
|
|
@@ -524,42 +583,53 @@ async function executePerCall(code: string, cwd: string, options: PythonExecutor
|
|
|
524
583
|
|
|
525
584
|
async function executeOnSession(code: string, cwd: string, options: PythonExecutorOptions): Promise<PythonResult> {
|
|
526
585
|
const sessionId = options.sessionId ?? `session:${cwd}`;
|
|
586
|
+
const sessionKey = buildSessionKey(sessionId, cwd);
|
|
527
587
|
if (options.bridge && !options.bridgeSessionId) {
|
|
528
588
|
options.bridgeSessionId = sessionId;
|
|
529
589
|
}
|
|
530
590
|
if (options.reset) {
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
591
|
+
if (resettingSessions.has(sessionKey)) {
|
|
592
|
+
throw new Error("Python kernel reset already in progress");
|
|
593
|
+
}
|
|
594
|
+
resettingSessions.add(sessionKey);
|
|
595
|
+
try {
|
|
596
|
+
await resetSession(sessionKey);
|
|
597
|
+
} finally {
|
|
598
|
+
resettingSessions.delete(sessionKey);
|
|
537
599
|
}
|
|
538
|
-
|
|
600
|
+
} else if (resettingSessions.has(sessionKey)) {
|
|
601
|
+
throw new Error("Python kernel reset in progress");
|
|
602
|
+
}
|
|
603
|
+
const session = await acquireSession(sessionKey, sessionId, cwd, options);
|
|
604
|
+
if (options.signal?.aborted) {
|
|
605
|
+
throw new PythonExecutionCancelledError(isTimedOutCancellation(options.signal.reason, options.signal));
|
|
606
|
+
}
|
|
607
|
+
if (sessions.get(session.sessionKey) !== session) {
|
|
608
|
+
throw new PythonExecutionCancelledError(false);
|
|
609
|
+
}
|
|
610
|
+
if (!session.kernel.isAlive()) {
|
|
611
|
+
await replaceSessionKernel(session, cwd, options);
|
|
612
|
+
if (sessions.get(session.sessionKey) !== session) {
|
|
539
613
|
throw new PythonExecutionCancelledError(false);
|
|
540
614
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
615
|
+
}
|
|
616
|
+
const runOptions = { ...options, cwd: undefined };
|
|
617
|
+
try {
|
|
618
|
+
return await executeWithKernel(session.kernel, code, runOptions);
|
|
619
|
+
} catch (err) {
|
|
620
|
+
if (isCancellationError(err) || options.signal?.aborted) throw err;
|
|
621
|
+
if (session.kernel.isAlive()) throw err;
|
|
622
|
+
if (sessions.get(session.sessionKey) !== session) {
|
|
623
|
+
throw new PythonExecutionCancelledError(false);
|
|
546
624
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
if (sessions.get(session.sessionId) !== session) {
|
|
553
|
-
throw new PythonExecutionCancelledError(false);
|
|
554
|
-
}
|
|
555
|
-
// Kernel died during execute. Replace it and retry once on a fresh one.
|
|
556
|
-
await replaceSessionKernel(session, cwd, options);
|
|
557
|
-
if (sessions.get(session.sessionId) !== session) {
|
|
558
|
-
throw new PythonExecutionCancelledError(false);
|
|
559
|
-
}
|
|
560
|
-
return await executeWithKernel(session.kernel, code, options);
|
|
625
|
+
// Shared kernels are keyed by cwd, so a dead kernel can be recreated in place
|
|
626
|
+
// without risking cross-directory state bleed.
|
|
627
|
+
await replaceSessionKernel(session, cwd, options);
|
|
628
|
+
if (sessions.get(session.sessionKey) !== session) {
|
|
629
|
+
throw new PythonExecutionCancelledError(false);
|
|
561
630
|
}
|
|
562
|
-
|
|
631
|
+
return await executeWithKernel(session.kernel, code, runOptions);
|
|
632
|
+
}
|
|
563
633
|
}
|
|
564
634
|
|
|
565
635
|
export async function executePythonWithKernel(
|
|
@@ -571,10 +641,11 @@ export async function executePythonWithKernel(
|
|
|
571
641
|
}
|
|
572
642
|
|
|
573
643
|
export async function executePython(code: string, options?: PythonExecutorOptions): Promise<PythonResult> {
|
|
574
|
-
const cwd = options?.cwd ?? getProjectDir();
|
|
644
|
+
const cwd = normalizeSessionCwd(options?.cwd ?? getProjectDir());
|
|
575
645
|
const deadlineMs = getExecutionDeadlineMs(options);
|
|
576
646
|
const executionOptions: PythonExecutorOptions = {
|
|
577
647
|
...(options ?? {}),
|
|
648
|
+
cwd,
|
|
578
649
|
deadlineMs,
|
|
579
650
|
};
|
|
580
651
|
|
package/src/eval/py/kernel.ts
CHANGED
|
@@ -51,7 +51,14 @@ const STARTUP_TIMEOUT_MS = 10_000;
|
|
|
51
51
|
// kernel's state, so we only kill as a last-resort recovery path.
|
|
52
52
|
const INTERRUPT_ESCALATION_MS = 5_000;
|
|
53
53
|
|
|
54
|
+
export type KernelRuntimeEnv = Record<string, string | null>;
|
|
55
|
+
|
|
54
56
|
export interface KernelExecuteOptions {
|
|
57
|
+
id?: string;
|
|
58
|
+
/** Runtime working directory applied immediately before this request executes. */
|
|
59
|
+
cwd?: string;
|
|
60
|
+
/** Managed runtime environment variables applied immediately before this request executes. */
|
|
61
|
+
env?: KernelRuntimeEnv;
|
|
55
62
|
signal?: AbortSignal;
|
|
56
63
|
onChunk?: (text: string) => Promise<void> | void;
|
|
57
64
|
onDisplay?: (output: KernelDisplayOutput) => Promise<void> | void;
|
|
@@ -260,7 +267,7 @@ export class PythonKernel {
|
|
|
260
267
|
throw new Error("Python kernel is not running");
|
|
261
268
|
}
|
|
262
269
|
|
|
263
|
-
const msgId = Snowflake.next();
|
|
270
|
+
const msgId = options?.id ?? Snowflake.next();
|
|
264
271
|
const { promise, resolve } = Promise.withResolvers<KernelExecuteResult>();
|
|
265
272
|
const pending: PendingExecution = {
|
|
266
273
|
resolve,
|
|
@@ -345,6 +352,8 @@ export class PythonKernel {
|
|
|
345
352
|
const payload = JSON.stringify({
|
|
346
353
|
id: msgId,
|
|
347
354
|
code,
|
|
355
|
+
cwd: options?.cwd,
|
|
356
|
+
env: options?.env,
|
|
348
357
|
silent: options?.silent ?? false,
|
|
349
358
|
storeHistory: options?.storeHistory ?? !(options?.silent ?? false),
|
|
350
359
|
});
|
package/src/eval/py/prelude.py
CHANGED
|
@@ -377,13 +377,20 @@ if "__omp_prelude_loaded__" not in globals():
|
|
|
377
377
|
return current
|
|
378
378
|
|
|
379
379
|
|
|
380
|
+
def _tool_proxy_from_env() -> tuple[str, str, str]:
|
|
381
|
+
base = os.environ.get("PI_TOOL_BRIDGE_URL")
|
|
382
|
+
token = os.environ.get("PI_TOOL_BRIDGE_TOKEN")
|
|
383
|
+
session = os.environ.get("PI_TOOL_BRIDGE_SESSION")
|
|
384
|
+
if not base or not token or not session:
|
|
385
|
+
raise RuntimeError("tool bridge is unavailable in this kernel")
|
|
386
|
+
return (base.rstrip("/"), token, session)
|
|
387
|
+
|
|
380
388
|
class _ToolCallable:
|
|
381
389
|
"""Invokes one host-side tool via the loopback HTTP bridge."""
|
|
382
390
|
|
|
383
|
-
__slots__ = ("
|
|
391
|
+
__slots__ = ("_name",)
|
|
384
392
|
|
|
385
|
-
def __init__(self,
|
|
386
|
-
self._proxy = proxy
|
|
393
|
+
def __init__(self, name: str):
|
|
387
394
|
self._name = name
|
|
388
395
|
|
|
389
396
|
def __repr__(self) -> str:
|
|
@@ -402,16 +409,19 @@ if "__omp_prelude_loaded__" not in globals():
|
|
|
402
409
|
merged.update(kwargs)
|
|
403
410
|
if "_i" not in merged:
|
|
404
411
|
merged["_i"] = "py prelude"
|
|
412
|
+
base, token, session = _tool_proxy_from_env()
|
|
413
|
+
_run_id_getter = globals().get("__omp_current_run_id__")
|
|
414
|
+
_run_id = _run_id_getter() if callable(_run_id_getter) else globals().get("__omp_run_id__")
|
|
405
415
|
payload = json.dumps(
|
|
406
|
-
{"session":
|
|
416
|
+
{"session": session, "run": _run_id, "name": self._name, "args": merged}
|
|
407
417
|
).encode("utf-8")
|
|
408
418
|
req = urllib.request.Request(
|
|
409
|
-
f"{
|
|
419
|
+
f"{base}/v1/tool",
|
|
410
420
|
data=payload,
|
|
411
421
|
method="POST",
|
|
412
422
|
headers={
|
|
413
423
|
"Content-Type": "application/json",
|
|
414
|
-
"Authorization": f"Bearer {
|
|
424
|
+
"Authorization": f"Bearer {token}",
|
|
415
425
|
},
|
|
416
426
|
)
|
|
417
427
|
try:
|
|
@@ -433,30 +443,18 @@ if "__omp_prelude_loaded__" not in globals():
|
|
|
433
443
|
class _ToolProxy:
|
|
434
444
|
"""`tool.<name>(args)` proxy mirroring the JS runtime bridge."""
|
|
435
445
|
|
|
436
|
-
__slots__ = (
|
|
437
|
-
|
|
438
|
-
def __init__(self, base: str, token: str, session: str):
|
|
439
|
-
self._base = base.rstrip("/")
|
|
440
|
-
self._token = token
|
|
441
|
-
self._session = session
|
|
446
|
+
__slots__ = ()
|
|
442
447
|
|
|
443
448
|
def __getattr__(self, name: str) -> _ToolCallable:
|
|
444
449
|
if name.startswith("_"):
|
|
445
450
|
raise AttributeError(name)
|
|
446
|
-
return _ToolCallable(
|
|
451
|
+
return _ToolCallable(name)
|
|
447
452
|
|
|
448
453
|
def __getitem__(self, name: str) -> _ToolCallable:
|
|
449
|
-
return _ToolCallable(
|
|
454
|
+
return _ToolCallable(name)
|
|
450
455
|
|
|
451
456
|
def __repr__(self) -> str:
|
|
452
|
-
|
|
457
|
+
session = os.environ.get("PI_TOOL_BRIDGE_SESSION")
|
|
458
|
+
return f"<tool proxy session={session}>" if session else "<tool proxy unavailable>"
|
|
453
459
|
|
|
454
|
-
|
|
455
|
-
_k in os.environ
|
|
456
|
-
for _k in ("PI_TOOL_BRIDGE_URL", "PI_TOOL_BRIDGE_TOKEN", "PI_TOOL_BRIDGE_SESSION")
|
|
457
|
-
):
|
|
458
|
-
tool = _ToolProxy(
|
|
459
|
-
os.environ["PI_TOOL_BRIDGE_URL"],
|
|
460
|
-
os.environ["PI_TOOL_BRIDGE_TOKEN"],
|
|
461
|
-
os.environ["PI_TOOL_BRIDGE_SESSION"],
|
|
462
|
-
)
|
|
460
|
+
tool = _ToolProxy()
|