@oh-my-pi/pi-coding-agent 13.16.0 → 13.16.1
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 +14 -0
- package/package.json +7 -7
- package/src/commit/agentic/tools/analyze-file.ts +1 -0
- package/src/extensibility/custom-tools/types.ts +3 -0
- package/src/extensibility/extensions/runner.ts +7 -0
- package/src/extensibility/extensions/types.ts +4 -0
- package/src/ipy/cancellation.ts +28 -0
- package/src/ipy/executor.ts +252 -77
- package/src/ipy/kernel.ts +181 -35
- package/src/ipy/modules.ts +39 -4
- package/src/modes/acp/acp-agent.ts +1 -0
- package/src/modes/controllers/extension-ui-controller.ts +3 -0
- package/src/modes/controllers/input-controller.ts +1 -0
- package/src/modes/print-mode.ts +1 -0
- package/src/modes/prompt-action-autocomplete.ts +5 -3
- package/src/modes/rpc/rpc-mode.ts +1 -0
- package/src/prompts/tools/grep.md +1 -1
- package/src/sdk.ts +17 -1
- package/src/session/agent-session.ts +6 -0
- package/src/task/executor.ts +4 -0
- package/src/task/index.ts +2 -0
- package/src/tools/find.ts +1 -0
- package/src/tools/grep.ts +21 -17
- package/src/tools/index.ts +3 -0
- package/src/tools/python.ts +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [13.16.1] - 2026-03-27
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `searchDb` parameter to `PromptActionAutocompleteProvider` constructor for native search database integration in autocomplete workflows
|
|
10
|
+
- Added `searchDb` parameter to enable native search database integration for grep and find operations
|
|
11
|
+
- Exported `SearchDb` type from tools module for type-safe search database usage
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Updated grep tool to accept and utilize `searchDb` parameter for improved search performance
|
|
16
|
+
- Updated find tool to pass `searchDb` parameter to underlying search operations
|
|
17
|
+
- Updated grep tool description to remove ripgrep-specific implementation detail
|
|
18
|
+
|
|
5
19
|
## [13.16.0] - 2026-03-27
|
|
6
20
|
### Added
|
|
7
21
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "13.16.
|
|
4
|
+
"version": "13.16.1",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -42,12 +42,12 @@
|
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@agentclientprotocol/sdk": "0.16.1",
|
|
44
44
|
"@mozilla/readability": "^0.6",
|
|
45
|
-
"@oh-my-pi/omp-stats": "13.16.
|
|
46
|
-
"@oh-my-pi/pi-agent-core": "13.16.
|
|
47
|
-
"@oh-my-pi/pi-ai": "13.16.
|
|
48
|
-
"@oh-my-pi/pi-natives": "13.16.
|
|
49
|
-
"@oh-my-pi/pi-tui": "13.16.
|
|
50
|
-
"@oh-my-pi/pi-utils": "13.16.
|
|
45
|
+
"@oh-my-pi/omp-stats": "13.16.1",
|
|
46
|
+
"@oh-my-pi/pi-agent-core": "13.16.1",
|
|
47
|
+
"@oh-my-pi/pi-ai": "13.16.1",
|
|
48
|
+
"@oh-my-pi/pi-natives": "13.16.1",
|
|
49
|
+
"@oh-my-pi/pi-tui": "13.16.1",
|
|
50
|
+
"@oh-my-pi/pi-utils": "13.16.1",
|
|
51
51
|
"@sinclair/typebox": "^0.34",
|
|
52
52
|
"@xterm/headless": "^6.0",
|
|
53
53
|
"ajv": "^8.18",
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
8
8
|
import type { Model } from "@oh-my-pi/pi-ai";
|
|
9
|
+
import type { SearchDb } from "@oh-my-pi/pi-natives";
|
|
9
10
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
10
11
|
import type { Static, TSchema } from "@sinclair/typebox";
|
|
11
12
|
import type { Rule } from "../../capability/rule";
|
|
@@ -71,6 +72,8 @@ export interface CustomToolContext {
|
|
|
71
72
|
modelRegistry: ModelRegistry;
|
|
72
73
|
/** Current model (may be undefined if no model is selected yet) */
|
|
73
74
|
model: Model | undefined;
|
|
75
|
+
/** Shared native search DB for grep/glob/fuzzyFind-backed workflows. */
|
|
76
|
+
searchDb?: SearchDb;
|
|
74
77
|
/** Whether the agent is idle (not streaming) */
|
|
75
78
|
isIdle(): boolean;
|
|
76
79
|
/** Whether there are queued messages waiting to be processed */
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
5
5
|
import type { ImageContent, Model } from "@oh-my-pi/pi-ai";
|
|
6
|
+
import type { SearchDb } from "@oh-my-pi/pi-natives";
|
|
6
7
|
import type { KeyId } from "@oh-my-pi/pi-tui";
|
|
7
8
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
8
9
|
import type { ModelRegistry } from "../../config/model-registry";
|
|
@@ -160,6 +161,7 @@ export class ExtensionRunner {
|
|
|
160
161
|
#uiContext: ExtensionUIContext;
|
|
161
162
|
#errorListeners: Set<ExtensionErrorListener> = new Set();
|
|
162
163
|
#getModel: () => Model | undefined = () => undefined;
|
|
164
|
+
#getSearchDbFn: () => SearchDb | undefined = () => undefined;
|
|
163
165
|
#isIdleFn: () => boolean = () => true;
|
|
164
166
|
#waitForIdleFn: () => Promise<void> = async () => {};
|
|
165
167
|
#abortFn: () => void = () => {};
|
|
@@ -205,6 +207,7 @@ export class ExtensionRunner {
|
|
|
205
207
|
|
|
206
208
|
// Context actions (required)
|
|
207
209
|
this.#getModel = contextActions.getModel;
|
|
210
|
+
this.#getSearchDbFn = contextActions.getSearchDb ?? (() => undefined);
|
|
208
211
|
this.#isIdleFn = contextActions.isIdle;
|
|
209
212
|
this.#abortFn = contextActions.abort;
|
|
210
213
|
this.#hasPendingMessagesFn = contextActions.hasPendingMessages;
|
|
@@ -376,6 +379,7 @@ export class ExtensionRunner {
|
|
|
376
379
|
|
|
377
380
|
createContext(): ExtensionContext {
|
|
378
381
|
const getModel = this.#getModel;
|
|
382
|
+
const getSearchDb = this.#getSearchDbFn;
|
|
379
383
|
return {
|
|
380
384
|
ui: this.#uiContext,
|
|
381
385
|
getContextUsage: () => this.#getContextUsageFn(),
|
|
@@ -387,6 +391,9 @@ export class ExtensionRunner {
|
|
|
387
391
|
get model() {
|
|
388
392
|
return getModel();
|
|
389
393
|
},
|
|
394
|
+
get searchDb() {
|
|
395
|
+
return getSearchDb();
|
|
396
|
+
},
|
|
390
397
|
isIdle: () => this.#isIdleFn(),
|
|
391
398
|
abort: () => this.#abortFn(),
|
|
392
399
|
hasPendingMessages: () => this.#hasPendingMessagesFn(),
|
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
ToolResultMessage,
|
|
23
23
|
} from "@oh-my-pi/pi-ai";
|
|
24
24
|
import type * as piCodingAgent from "@oh-my-pi/pi-coding-agent";
|
|
25
|
+
import type { SearchDb } from "@oh-my-pi/pi-natives";
|
|
25
26
|
import type { AutocompleteItem, Component, EditorComponent, EditorTheme, KeyId, TUI } from "@oh-my-pi/pi-tui";
|
|
26
27
|
import type { Static, TSchema } from "@sinclair/typebox";
|
|
27
28
|
import type { Rule } from "../../capability/rule";
|
|
@@ -229,6 +230,8 @@ export interface ExtensionContext {
|
|
|
229
230
|
modelRegistry: ModelRegistry;
|
|
230
231
|
/** Current model (may be undefined) */
|
|
231
232
|
model: Model | undefined;
|
|
233
|
+
/** Shared native search DB for grep/glob/fuzzyFind-backed workflows. */
|
|
234
|
+
searchDb?: SearchDb;
|
|
232
235
|
/** Whether the agent is idle (not streaming) */
|
|
233
236
|
isIdle(): boolean;
|
|
234
237
|
/** Abort the current agent operation */
|
|
@@ -1295,6 +1298,7 @@ export interface ExtensionActions {
|
|
|
1295
1298
|
/** Actions for ExtensionContext (ctx.* in event handlers). */
|
|
1296
1299
|
export interface ExtensionContextActions {
|
|
1297
1300
|
getModel: () => Model | undefined;
|
|
1301
|
+
getSearchDb?: () => SearchDb | undefined;
|
|
1298
1302
|
isIdle: () => boolean;
|
|
1299
1303
|
abort: () => void;
|
|
1300
1304
|
hasPendingMessages: () => boolean;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function getAbortReason(signal: AbortSignal | undefined, fallbackReason: string): Error {
|
|
2
|
+
if (signal?.reason instanceof Error) return signal.reason;
|
|
3
|
+
if (typeof signal?.reason === "string" && signal.reason.length > 0) {
|
|
4
|
+
return new Error(signal.reason);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
return new Error(fallbackReason);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function createCancellationError(name: "AbortError" | "TimeoutError", message: string): Error {
|
|
11
|
+
const error = new Error(message);
|
|
12
|
+
error.name = name;
|
|
13
|
+
return error;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getExecutionCancellationError(
|
|
17
|
+
result: { timedOut?: boolean },
|
|
18
|
+
signal: AbortSignal | undefined,
|
|
19
|
+
fallbackReason: string,
|
|
20
|
+
): Error {
|
|
21
|
+
if (signal?.aborted) {
|
|
22
|
+
return getAbortReason(signal, fallbackReason);
|
|
23
|
+
}
|
|
24
|
+
if (result.timedOut) {
|
|
25
|
+
return createCancellationError("TimeoutError", fallbackReason);
|
|
26
|
+
}
|
|
27
|
+
return createCancellationError("AbortError", fallbackReason);
|
|
28
|
+
}
|
package/src/ipy/executor.ts
CHANGED
|
@@ -24,6 +24,8 @@ export interface PythonExecutorOptions {
|
|
|
24
24
|
cwd?: string;
|
|
25
25
|
/** Timeout in milliseconds */
|
|
26
26
|
timeoutMs?: number;
|
|
27
|
+
/** Absolute wall-clock deadline in milliseconds since epoch */
|
|
28
|
+
deadlineMs?: number;
|
|
27
29
|
/** Callback for streaming output chunks (already sanitized) */
|
|
28
30
|
onChunk?: (chunk: string) => Promise<void> | void;
|
|
29
31
|
/** AbortSignal for cancellation */
|
|
@@ -86,6 +88,151 @@ const kernelSessions = new Map<string, KernelSession>();
|
|
|
86
88
|
let cachedPreludeDocs: PreludeHelper[] | null = null;
|
|
87
89
|
let cleanupTimer: NodeJS.Timeout | null = null;
|
|
88
90
|
|
|
91
|
+
interface KernelSessionExecutionOptions {
|
|
92
|
+
useSharedGateway?: boolean;
|
|
93
|
+
sessionFile?: string;
|
|
94
|
+
signal?: AbortSignal;
|
|
95
|
+
deadlineMs?: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
class PythonExecutionCancelledError extends Error {
|
|
99
|
+
readonly timedOut: boolean;
|
|
100
|
+
|
|
101
|
+
constructor(timedOut: boolean) {
|
|
102
|
+
super(timedOut ? "Command timed out" : "Command aborted");
|
|
103
|
+
this.name = timedOut ? "TimeoutError" : "AbortError";
|
|
104
|
+
this.timedOut = timedOut;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function getExecutionDeadlineMs(options?: Pick<PythonExecutorOptions, "deadlineMs" | "timeoutMs">): number | undefined {
|
|
109
|
+
if (options?.deadlineMs !== undefined) return options.deadlineMs;
|
|
110
|
+
if (options?.timeoutMs === undefined) return undefined;
|
|
111
|
+
return Date.now() + options.timeoutMs;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function getRemainingTimeoutMs(deadlineMs?: number): number | undefined {
|
|
115
|
+
if (deadlineMs === undefined) return undefined;
|
|
116
|
+
return deadlineMs - Date.now();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function requireRemainingTimeoutMs(deadlineMs?: number): number | undefined {
|
|
120
|
+
const remainingMs = getRemainingTimeoutMs(deadlineMs);
|
|
121
|
+
if (remainingMs === undefined) return undefined;
|
|
122
|
+
if (remainingMs <= 0) {
|
|
123
|
+
throw new PythonExecutionCancelledError(true);
|
|
124
|
+
}
|
|
125
|
+
return remainingMs;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isCancellationError(error: unknown): boolean {
|
|
129
|
+
return (
|
|
130
|
+
error instanceof PythonExecutionCancelledError ||
|
|
131
|
+
(error instanceof DOMException && (error.name === "AbortError" || error.name === "TimeoutError")) ||
|
|
132
|
+
(error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError"))
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isTimedOutCancellation(error: unknown, signal?: AbortSignal): boolean {
|
|
137
|
+
if (error instanceof PythonExecutionCancelledError) return error.timedOut;
|
|
138
|
+
if (error instanceof DOMException) return error.name === "TimeoutError";
|
|
139
|
+
if (error instanceof Error && error.name === "TimeoutError") return true;
|
|
140
|
+
const reason = signal?.reason;
|
|
141
|
+
if (reason instanceof DOMException) return reason.name === "TimeoutError";
|
|
142
|
+
return reason instanceof Error ? reason.name === "TimeoutError" : false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function waitForQueueTurn(
|
|
146
|
+
queue: Promise<void>,
|
|
147
|
+
options: Pick<KernelSessionExecutionOptions, "signal" | "deadlineMs">,
|
|
148
|
+
): Promise<void> {
|
|
149
|
+
if (options.signal?.aborted) {
|
|
150
|
+
throw new PythonExecutionCancelledError(isTimedOutCancellation(options.signal.reason, options.signal));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const remainingMs = getRemainingTimeoutMs(options.deadlineMs);
|
|
154
|
+
if (remainingMs !== undefined && remainingMs <= 0) {
|
|
155
|
+
throw new PythonExecutionCancelledError(true);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!options.signal && remainingMs === undefined) {
|
|
159
|
+
await queue;
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
await new Promise<void>((resolve, reject) => {
|
|
164
|
+
const cleanups: Array<() => void> = [];
|
|
165
|
+
const finish = (callback: () => void) => {
|
|
166
|
+
while (cleanups.length > 0) {
|
|
167
|
+
cleanups.pop()?.();
|
|
168
|
+
}
|
|
169
|
+
callback();
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const onAbort = () => {
|
|
173
|
+
finish(() =>
|
|
174
|
+
reject(new PythonExecutionCancelledError(isTimedOutCancellation(options.signal?.reason, options.signal))),
|
|
175
|
+
);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
if (options.signal) {
|
|
179
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
180
|
+
cleanups.push(() => options.signal?.removeEventListener("abort", onAbort));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (remainingMs !== undefined) {
|
|
184
|
+
const timeout = setTimeout(() => {
|
|
185
|
+
finish(() => reject(new PythonExecutionCancelledError(true)));
|
|
186
|
+
}, remainingMs);
|
|
187
|
+
timeout.unref();
|
|
188
|
+
cleanups.push(() => clearTimeout(timeout));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
queue.then(
|
|
192
|
+
() => finish(resolve),
|
|
193
|
+
error => finish(() => reject(error)),
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function formatTimeoutAnnotation(timeoutMs?: number): string | undefined {
|
|
199
|
+
if (timeoutMs === undefined) return "Command timed out";
|
|
200
|
+
const secs = Math.max(1, Math.round(timeoutMs / 1000));
|
|
201
|
+
return `Command timed out after ${secs} seconds`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function createCancelledPythonResult(timedOut: boolean, timeoutMs?: number): PythonResult {
|
|
205
|
+
const output = timedOut ? (formatTimeoutAnnotation(timeoutMs) ?? "Command timed out") : "";
|
|
206
|
+
const outputBytes = Buffer.byteLength(output, "utf-8");
|
|
207
|
+
const outputLines = output.length > 0 ? 1 : 0;
|
|
208
|
+
return {
|
|
209
|
+
output,
|
|
210
|
+
exitCode: undefined,
|
|
211
|
+
cancelled: true,
|
|
212
|
+
truncated: false,
|
|
213
|
+
totalLines: outputLines,
|
|
214
|
+
totalBytes: outputBytes,
|
|
215
|
+
outputLines,
|
|
216
|
+
outputBytes,
|
|
217
|
+
displayOutputs: [],
|
|
218
|
+
stdinRequested: false,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function buildKernelStartOptions(
|
|
223
|
+
cwd: string,
|
|
224
|
+
env: Record<string, string> | undefined,
|
|
225
|
+
options: KernelSessionExecutionOptions,
|
|
226
|
+
) {
|
|
227
|
+
return {
|
|
228
|
+
cwd,
|
|
229
|
+
env,
|
|
230
|
+
useSharedGateway: options.useSharedGateway,
|
|
231
|
+
signal: options.signal,
|
|
232
|
+
deadlineMs: options.deadlineMs,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
89
236
|
interface PreludeCacheSource {
|
|
90
237
|
path: string;
|
|
91
238
|
hash: string;
|
|
@@ -247,13 +394,10 @@ export async function warmPythonEnvironment(
|
|
|
247
394
|
const resolvedSessionId = sessionId ?? `session:${cwd}`;
|
|
248
395
|
try {
|
|
249
396
|
const docs = await logger.timeAsync("warmPython:withKernelSession", () =>
|
|
250
|
-
withKernelSession(
|
|
251
|
-
resolvedSessionId,
|
|
252
|
-
cwd,
|
|
253
|
-
async kernel => kernel.introspectPrelude(),
|
|
397
|
+
withKernelSession(resolvedSessionId, cwd, async kernel => kernel.introspectPrelude(), {
|
|
254
398
|
useSharedGateway,
|
|
255
399
|
sessionFile,
|
|
256
|
-
),
|
|
400
|
+
}),
|
|
257
401
|
);
|
|
258
402
|
cachedPreludeDocs = docs;
|
|
259
403
|
if (!isTestEnv && docs.length > 0) {
|
|
@@ -306,21 +450,22 @@ async function recoverFromResourceExhaustion(): Promise<void> {
|
|
|
306
450
|
async function createKernelSession(
|
|
307
451
|
sessionId: string,
|
|
308
452
|
cwd: string,
|
|
309
|
-
|
|
310
|
-
sessionFile?: string,
|
|
453
|
+
options: KernelSessionExecutionOptions = {},
|
|
311
454
|
isRetry?: boolean,
|
|
312
455
|
): Promise<KernelSession> {
|
|
313
|
-
|
|
456
|
+
requireRemainingTimeoutMs(options.deadlineMs);
|
|
457
|
+
const env: Record<string, string> | undefined = options.sessionFile
|
|
458
|
+
? { PI_SESSION_FILE: options.sessionFile }
|
|
459
|
+
: undefined;
|
|
460
|
+
const startOptions = buildKernelStartOptions(cwd, env, options);
|
|
314
461
|
|
|
315
462
|
let kernel: PythonKernel;
|
|
316
463
|
try {
|
|
317
|
-
kernel = await logger.timeAsync("createKernelSession:PythonKernel.start", () =>
|
|
318
|
-
PythonKernel.start({ cwd, useSharedGateway, env }),
|
|
319
|
-
);
|
|
464
|
+
kernel = await logger.timeAsync("createKernelSession:PythonKernel.start", () => PythonKernel.start(startOptions));
|
|
320
465
|
} catch (err) {
|
|
321
466
|
if (!isRetry && isResourceExhaustionError(err)) {
|
|
322
467
|
await recoverFromResourceExhaustion();
|
|
323
|
-
return createKernelSession(sessionId, cwd,
|
|
468
|
+
return createKernelSession(sessionId, cwd, options, true);
|
|
324
469
|
}
|
|
325
470
|
throw err;
|
|
326
471
|
}
|
|
@@ -347,20 +492,23 @@ async function createKernelSession(
|
|
|
347
492
|
async function restartKernelSession(
|
|
348
493
|
session: KernelSession,
|
|
349
494
|
cwd: string,
|
|
350
|
-
|
|
351
|
-
sessionFile?: string,
|
|
495
|
+
options: KernelSessionExecutionOptions = {},
|
|
352
496
|
): Promise<void> {
|
|
353
497
|
session.restartCount += 1;
|
|
354
498
|
if (session.restartCount > 1) {
|
|
355
499
|
throw new Error("Python kernel restarted too many times in this session");
|
|
356
500
|
}
|
|
501
|
+
requireRemainingTimeoutMs(options.deadlineMs);
|
|
357
502
|
try {
|
|
358
503
|
await session.kernel.shutdown();
|
|
359
504
|
} catch (err) {
|
|
360
505
|
logger.warn("Failed to shutdown crashed kernel", { error: err instanceof Error ? err.message : String(err) });
|
|
361
506
|
}
|
|
362
|
-
const env: Record<string, string> | undefined = sessionFile
|
|
363
|
-
|
|
507
|
+
const env: Record<string, string> | undefined = options.sessionFile
|
|
508
|
+
? { PI_SESSION_FILE: options.sessionFile }
|
|
509
|
+
: undefined;
|
|
510
|
+
const startOptions = buildKernelStartOptions(cwd, env, options);
|
|
511
|
+
const kernel = await PythonKernel.start(startOptions);
|
|
364
512
|
session.kernel = kernel;
|
|
365
513
|
session.dead = false;
|
|
366
514
|
session.lastUsedAt = Date.now();
|
|
@@ -382,23 +530,18 @@ async function withKernelSession<T>(
|
|
|
382
530
|
sessionId: string,
|
|
383
531
|
cwd: string,
|
|
384
532
|
handler: (kernel: PythonKernel) => Promise<T>,
|
|
385
|
-
|
|
386
|
-
sessionFile?: string,
|
|
533
|
+
options: KernelSessionExecutionOptions = {},
|
|
387
534
|
): Promise<T> {
|
|
388
535
|
let session = kernelSessions.get(sessionId);
|
|
389
536
|
if (!session) {
|
|
390
|
-
// Evict oldest session if at capacity
|
|
391
537
|
if (kernelSessions.size >= MAX_KERNEL_SESSIONS) {
|
|
392
538
|
await evictOldestSession();
|
|
393
539
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
useSharedGateway,
|
|
400
|
-
sessionFile,
|
|
401
|
-
);
|
|
540
|
+
requireRemainingTimeoutMs(options.deadlineMs);
|
|
541
|
+
if (options.signal?.aborted) {
|
|
542
|
+
throw new PythonExecutionCancelledError(isTimedOutCancellation(options.signal.reason, options.signal));
|
|
543
|
+
}
|
|
544
|
+
session = await logger.timeAsync("kernel:createKernelSession", createKernelSession, sessionId, cwd, options);
|
|
402
545
|
kernelSessions.set(sessionId, session);
|
|
403
546
|
startCleanupTimer();
|
|
404
547
|
}
|
|
@@ -406,14 +549,7 @@ async function withKernelSession<T>(
|
|
|
406
549
|
const run = async (): Promise<T> => {
|
|
407
550
|
session!.lastUsedAt = Date.now();
|
|
408
551
|
if (session!.dead || !session!.kernel.isAlive()) {
|
|
409
|
-
await logger.timeAsync(
|
|
410
|
-
"kernel:restartKernelSession",
|
|
411
|
-
restartKernelSession,
|
|
412
|
-
session!,
|
|
413
|
-
cwd,
|
|
414
|
-
useSharedGateway,
|
|
415
|
-
sessionFile,
|
|
416
|
-
);
|
|
552
|
+
await logger.timeAsync("kernel:restartKernelSession", restartKernelSession, session!, cwd, options);
|
|
417
553
|
}
|
|
418
554
|
try {
|
|
419
555
|
const result = await logger.timeAsync("kernel:withSession:handler", handler, session!.kernel);
|
|
@@ -423,26 +559,34 @@ async function withKernelSession<T>(
|
|
|
423
559
|
if (!session!.dead && session!.kernel.isAlive()) {
|
|
424
560
|
throw err;
|
|
425
561
|
}
|
|
426
|
-
await logger.timeAsync(
|
|
427
|
-
"kernel:restartKernelSession",
|
|
428
|
-
restartKernelSession,
|
|
429
|
-
session!,
|
|
430
|
-
cwd,
|
|
431
|
-
useSharedGateway,
|
|
432
|
-
sessionFile,
|
|
433
|
-
);
|
|
562
|
+
await logger.timeAsync("kernel:restartKernelSession", restartKernelSession, session!, cwd, options);
|
|
434
563
|
const result = await logger.timeAsync("kernel:postRestart:handler", handler, session!.kernel);
|
|
435
564
|
session!.restartCount = 0;
|
|
436
565
|
return result;
|
|
437
566
|
}
|
|
438
567
|
};
|
|
439
568
|
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
);
|
|
445
|
-
|
|
569
|
+
const queue = session.queue;
|
|
570
|
+
let releaseTurn: (() => void) | undefined;
|
|
571
|
+
const turn = new Promise<void>(resolve => {
|
|
572
|
+
releaseTurn = resolve;
|
|
573
|
+
});
|
|
574
|
+
session.queue = queue
|
|
575
|
+
.then(
|
|
576
|
+
() => turn,
|
|
577
|
+
() => turn,
|
|
578
|
+
)
|
|
579
|
+
.then(
|
|
580
|
+
() => undefined,
|
|
581
|
+
() => undefined,
|
|
582
|
+
);
|
|
583
|
+
|
|
584
|
+
try {
|
|
585
|
+
await waitForQueueTurn(queue, options);
|
|
586
|
+
return await run();
|
|
587
|
+
} finally {
|
|
588
|
+
releaseTurn?.();
|
|
589
|
+
}
|
|
446
590
|
}
|
|
447
591
|
|
|
448
592
|
async function executeWithKernel(
|
|
@@ -456,19 +600,20 @@ async function executeWithKernel(
|
|
|
456
600
|
artifactId: options?.artifactId,
|
|
457
601
|
});
|
|
458
602
|
const displayOutputs: KernelDisplayOutput[] = [];
|
|
603
|
+
const deadlineMs = getExecutionDeadlineMs(options);
|
|
604
|
+
let executionTimeoutMs: number | undefined;
|
|
459
605
|
|
|
460
606
|
try {
|
|
607
|
+
executionTimeoutMs = requireRemainingTimeoutMs(deadlineMs);
|
|
461
608
|
const result = await kernel.execute(code, {
|
|
462
609
|
signal: options?.signal,
|
|
463
|
-
timeoutMs:
|
|
610
|
+
timeoutMs: executionTimeoutMs,
|
|
464
611
|
onChunk: text => sink.push(text),
|
|
465
612
|
onDisplay: output => void displayOutputs.push(output),
|
|
466
613
|
});
|
|
467
614
|
|
|
468
615
|
if (result.cancelled) {
|
|
469
|
-
const
|
|
470
|
-
const annotation =
|
|
471
|
-
result.timedOut && secs !== undefined ? `Command timed out after ${secs} seconds` : undefined;
|
|
616
|
+
const annotation = result.timedOut ? formatTimeoutAnnotation(executionTimeoutMs) : undefined;
|
|
472
617
|
return {
|
|
473
618
|
exitCode: undefined,
|
|
474
619
|
cancelled: true,
|
|
@@ -497,6 +642,16 @@ async function executeWithKernel(
|
|
|
497
642
|
...(await sink.dump()),
|
|
498
643
|
};
|
|
499
644
|
} catch (err) {
|
|
645
|
+
if (isCancellationError(err) || options?.signal?.aborted) {
|
|
646
|
+
const timedOut = isTimedOutCancellation(err, options?.signal);
|
|
647
|
+
return {
|
|
648
|
+
exitCode: undefined,
|
|
649
|
+
cancelled: true,
|
|
650
|
+
displayOutputs,
|
|
651
|
+
stdinRequested: false,
|
|
652
|
+
...(await sink.dump(timedOut ? formatTimeoutAnnotation(executionTimeoutMs) : undefined)),
|
|
653
|
+
};
|
|
654
|
+
}
|
|
500
655
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
501
656
|
logger.error("Python execution failed", { error: error.message });
|
|
502
657
|
throw error;
|
|
@@ -513,34 +668,54 @@ export async function executePythonWithKernel(
|
|
|
513
668
|
|
|
514
669
|
export async function executePython(code: string, options?: PythonExecutorOptions): Promise<PythonResult> {
|
|
515
670
|
const cwd = options?.cwd ?? getProjectDir();
|
|
516
|
-
|
|
671
|
+
const deadlineMs = getExecutionDeadlineMs(options);
|
|
672
|
+
const executionOptions: PythonExecutorOptions = {
|
|
673
|
+
...(options ?? {}),
|
|
674
|
+
deadlineMs,
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
try {
|
|
678
|
+
requireRemainingTimeoutMs(deadlineMs);
|
|
679
|
+
if (executionOptions.signal?.aborted) {
|
|
680
|
+
throw new PythonExecutionCancelledError(
|
|
681
|
+
isTimedOutCancellation(executionOptions.signal.reason, executionOptions.signal),
|
|
682
|
+
);
|
|
683
|
+
}
|
|
517
684
|
|
|
518
|
-
|
|
519
|
-
const useSharedGateway = options?.useSharedGateway;
|
|
520
|
-
const sessionFile = options?.sessionFile;
|
|
685
|
+
await ensureKernelAvailable(cwd);
|
|
521
686
|
|
|
522
|
-
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
687
|
+
const kernelMode = executionOptions.kernelMode ?? "session";
|
|
688
|
+
const sessionFile = executionOptions.sessionFile;
|
|
689
|
+
|
|
690
|
+
if (kernelMode === "per-call") {
|
|
691
|
+
const env: Record<string, string> | undefined = sessionFile ? { PI_SESSION_FILE: sessionFile } : undefined;
|
|
692
|
+
requireRemainingTimeoutMs(deadlineMs);
|
|
693
|
+
const startOptions = buildKernelStartOptions(cwd, env, executionOptions);
|
|
694
|
+
const kernel = await PythonKernel.start(startOptions);
|
|
695
|
+
try {
|
|
696
|
+
return await executeWithKernel(kernel, code, executionOptions);
|
|
697
|
+
} finally {
|
|
698
|
+
await kernel.shutdown();
|
|
699
|
+
}
|
|
529
700
|
}
|
|
530
|
-
}
|
|
531
701
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
702
|
+
const sessionId = executionOptions.sessionId ?? `session:${cwd}`;
|
|
703
|
+
if (executionOptions.reset) {
|
|
704
|
+
const existing = kernelSessions.get(sessionId);
|
|
705
|
+
if (existing) {
|
|
706
|
+
await disposeKernelSession(existing);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return await withKernelSession(
|
|
710
|
+
sessionId,
|
|
711
|
+
cwd,
|
|
712
|
+
async kernel => executeWithKernel(kernel, code, executionOptions),
|
|
713
|
+
executionOptions,
|
|
714
|
+
);
|
|
715
|
+
} catch (err) {
|
|
716
|
+
if (isCancellationError(err) || executionOptions.signal?.aborted) {
|
|
717
|
+
return createCancelledPythonResult(isTimedOutCancellation(err, executionOptions.signal));
|
|
537
718
|
}
|
|
719
|
+
throw err;
|
|
538
720
|
}
|
|
539
|
-
return await withKernelSession(
|
|
540
|
-
sessionId,
|
|
541
|
-
cwd,
|
|
542
|
-
async kernel => executeWithKernel(kernel, code, options),
|
|
543
|
-
useSharedGateway,
|
|
544
|
-
sessionFile,
|
|
545
|
-
);
|
|
546
721
|
}
|