@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 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.0",
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.0",
46
- "@oh-my-pi/pi-agent-core": "13.16.0",
47
- "@oh-my-pi/pi-ai": "13.16.0",
48
- "@oh-my-pi/pi-natives": "13.16.0",
49
- "@oh-my-pi/pi-tui": "13.16.0",
50
- "@oh-my-pi/pi-utils": "13.16.0",
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",
@@ -43,6 +43,7 @@ function buildToolSession(
43
43
  settings: options.settings,
44
44
  authStorage: options.authStorage,
45
45
  modelRegistry: options.modelRegistry,
46
+ searchDb: ctx.searchDb,
46
47
  };
47
48
  }
48
49
 
@@ -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
+ }
@@ -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
- useSharedGateway?: boolean,
310
- sessionFile?: string,
453
+ options: KernelSessionExecutionOptions = {},
311
454
  isRetry?: boolean,
312
455
  ): Promise<KernelSession> {
313
- const env: Record<string, string> | undefined = sessionFile ? { PI_SESSION_FILE: sessionFile } : undefined;
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, useSharedGateway, sessionFile, true);
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
- useSharedGateway?: boolean,
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 ? { PI_SESSION_FILE: sessionFile } : undefined;
363
- const kernel = await PythonKernel.start({ cwd, useSharedGateway, env });
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
- useSharedGateway?: boolean,
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
- session = await logger.timeAsync(
395
- "kernel:createKernelSession",
396
- createKernelSession,
397
- sessionId,
398
- cwd,
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 task = session.queue.then(run, run);
441
- session.queue = task.then(
442
- () => undefined,
443
- () => undefined,
444
- );
445
- return task;
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: options?.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 secs = options?.timeoutMs ? Math.round(options.timeoutMs / 1000) : undefined;
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
- await ensureKernelAvailable(cwd);
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
- const kernelMode = options?.kernelMode ?? "session";
519
- const useSharedGateway = options?.useSharedGateway;
520
- const sessionFile = options?.sessionFile;
685
+ await ensureKernelAvailable(cwd);
521
686
 
522
- if (kernelMode === "per-call") {
523
- const env: Record<string, string> | undefined = sessionFile ? { PI_SESSION_FILE: sessionFile } : undefined;
524
- const kernel = await PythonKernel.start({ cwd, useSharedGateway, env });
525
- try {
526
- return await executeWithKernel(kernel, code, options);
527
- } finally {
528
- await kernel.shutdown();
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
- const sessionId = options?.sessionId ?? `session:${cwd}`;
533
- if (options?.reset) {
534
- const existing = kernelSessions.get(sessionId);
535
- if (existing) {
536
- await disposeKernelSession(existing);
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
  }