@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/src/ipy/kernel.ts CHANGED
@@ -2,6 +2,7 @@ import { $env, logger, Snowflake } from "@oh-my-pi/pi-utils";
2
2
  import { $ } from "bun";
3
3
  import { Settings } from "../config/settings";
4
4
  import { htmlToBasicMarkdown } from "../web/scrapers/types";
5
+ import { createCancellationError, getAbortReason, getExecutionCancellationError } from "./cancellation";
5
6
  import { acquireSharedGateway, releaseSharedGateway, shutdownSharedGateway } from "./gateway-coordinator";
6
7
  import { loadPythonModules } from "./modules";
7
8
  import { PYTHON_PRELUDE } from "./prelude";
@@ -35,6 +36,94 @@ function getExternalGatewayConfig(): ExternalGatewayConfig | null {
35
36
  };
36
37
  }
37
38
 
39
+ const STARTUP_CLEANUP_TIMEOUT_MS = 2_000;
40
+ const WEBSOCKET_CONNECT_TIMEOUT_MS = 10_000;
41
+
42
+ interface KernelLifecycleOptions {
43
+ signal?: AbortSignal;
44
+ deadlineMs?: number;
45
+ }
46
+
47
+ interface KernelShutdownOptions {
48
+ signal?: AbortSignal;
49
+ timeoutMs?: number;
50
+ }
51
+
52
+ function getRemainingTimeMs(deadlineMs?: number): number | undefined {
53
+ if (deadlineMs === undefined) return undefined;
54
+ return Math.max(0, deadlineMs - Date.now());
55
+ }
56
+
57
+ function throwIfStartupExecutionFailed(
58
+ result: Pick<KernelExecuteResult, "cancelled" | "status" | "timedOut">,
59
+ signal: AbortSignal | undefined,
60
+ failureMessage: string,
61
+ ): void {
62
+ if (result.cancelled) {
63
+ throw getExecutionCancellationError(result, signal, failureMessage);
64
+ }
65
+ if (result.status === "error") {
66
+ throw new Error(failureMessage);
67
+ }
68
+ }
69
+
70
+ function createAbortedSignal(reason: Error): AbortSignal {
71
+ const controller = new AbortController();
72
+ controller.abort(reason);
73
+ return controller.signal;
74
+ }
75
+
76
+ function combineAbortSignal(
77
+ options: KernelLifecycleOptions,
78
+ timeoutCapMs?: number,
79
+ fallbackReason = "Operation aborted",
80
+ ): AbortSignal | undefined {
81
+ if (options.signal?.aborted) {
82
+ return options.signal;
83
+ }
84
+
85
+ const signals: AbortSignal[] = [];
86
+ if (options.signal) {
87
+ signals.push(options.signal);
88
+ }
89
+
90
+ const remainingMs = getRemainingTimeMs(options.deadlineMs);
91
+ const timeoutMs =
92
+ remainingMs === undefined
93
+ ? timeoutCapMs
94
+ : timeoutCapMs === undefined
95
+ ? remainingMs
96
+ : Math.min(remainingMs, timeoutCapMs);
97
+
98
+ if (timeoutMs !== undefined) {
99
+ if (timeoutMs <= 0) {
100
+ return createAbortedSignal(createCancellationError("TimeoutError", fallbackReason));
101
+ }
102
+ signals.push(AbortSignal.timeout(timeoutMs));
103
+ }
104
+
105
+ if (signals.length === 0) return undefined;
106
+ return signals.length === 1 ? signals[0] : AbortSignal.any(signals);
107
+ }
108
+
109
+ function throwIfAborted(signal: AbortSignal | undefined, fallbackReason: string): void {
110
+ if (!signal?.aborted) return;
111
+ throw getAbortReason(signal, fallbackReason);
112
+ }
113
+
114
+ function getStartupExecuteOptions(options: KernelLifecycleOptions): Pick<KernelExecuteOptions, "signal" | "timeoutMs"> {
115
+ return {
116
+ signal: combineAbortSignal(options, undefined, "Python kernel startup aborted"),
117
+ timeoutMs: getRemainingTimeMs(options.deadlineMs),
118
+ };
119
+ }
120
+
121
+ function getStartupCleanupTimeoutMs(deadlineMs?: number): number {
122
+ const remainingMs = getRemainingTimeMs(deadlineMs);
123
+ if (remainingMs === undefined || remainingMs <= 0) return STARTUP_CLEANUP_TIMEOUT_MS;
124
+ return Math.min(STARTUP_CLEANUP_TIMEOUT_MS, remainingMs);
125
+ }
126
+
38
127
  export interface JupyterHeader {
39
128
  msg_id: string;
40
129
  session: string;
@@ -93,7 +182,7 @@ export interface PreludeHelper {
93
182
  category: string;
94
183
  }
95
184
 
96
- interface KernelStartOptions {
185
+ interface KernelStartOptions extends KernelLifecycleOptions {
97
186
  cwd: string;
98
187
  env?: Record<string, string | undefined>;
99
188
  useSharedGateway?: boolean;
@@ -339,9 +428,12 @@ export class PythonKernel {
339
428
  throw new Error(availability.reason ?? "Python kernel unavailable");
340
429
  }
341
430
 
431
+ const startup = { signal: options.signal, deadlineMs: options.deadlineMs };
432
+ const startupSignal = combineAbortSignal(startup, undefined, "Python kernel startup aborted");
433
+
342
434
  const externalConfig = getExternalGatewayConfig();
343
435
  if (externalConfig) {
344
- return PythonKernel.#startWithExternalGateway(externalConfig, options.cwd, options.env);
436
+ return PythonKernel.#startWithExternalGateway(externalConfig, options.cwd, options.env, startup);
345
437
  }
346
438
 
347
439
  if (options.useSharedGateway === false) {
@@ -349,6 +441,7 @@ export class PythonKernel {
349
441
  }
350
442
 
351
443
  for (let attempt = 0; attempt < 2; attempt += 1) {
444
+ throwIfAborted(startupSignal, "Python kernel startup aborted");
352
445
  try {
353
446
  const sharedResult = await logger.timeAsync("PythonKernel.start:acquireSharedGateway", () =>
354
447
  acquireSharedGateway(options.cwd),
@@ -357,7 +450,7 @@ export class PythonKernel {
357
450
  throw new Error("Shared Python gateway unavailable");
358
451
  }
359
452
  const kernel = await logger.timeAsync("PythonKernel.start:startWithSharedGateway", () =>
360
- PythonKernel.#startWithSharedGateway(sharedResult.url, options.cwd, options.env),
453
+ PythonKernel.#startWithSharedGateway(sharedResult.url, options.cwd, options.env, startup),
361
454
  );
362
455
  return kernel;
363
456
  } catch (err) {
@@ -382,16 +475,20 @@ export class PythonKernel {
382
475
  config: ExternalGatewayConfig,
383
476
  cwd: string,
384
477
  env?: Record<string, string | undefined>,
478
+ startup: KernelLifecycleOptions = {},
385
479
  ): Promise<PythonKernel> {
386
480
  const headers: Record<string, string> = { "Content-Type": "application/json" };
387
481
  if (config.token) {
388
482
  headers.Authorization = `token ${config.token}`;
389
483
  }
390
484
 
485
+ const startupSignal = combineAbortSignal(startup, undefined, "Python kernel startup aborted");
486
+ throwIfAborted(startupSignal, "Python kernel startup aborted");
391
487
  const createResponse = await fetch(`${config.url}/api/kernels`, {
392
488
  method: "POST",
393
489
  headers,
394
490
  body: JSON.stringify({ name: "python3" }),
491
+ signal: startupSignal,
395
492
  });
396
493
 
397
494
  if (!createResponse.ok) {
@@ -412,16 +509,23 @@ export class PythonKernel {
412
509
  );
413
510
 
414
511
  try {
415
- await kernel.#connectWebSocket();
416
- await kernel.#initializeKernelEnvironment(cwd, env);
417
- const preludeResult = await kernel.execute(PYTHON_PRELUDE, { silent: true, storeHistory: false });
418
- if (preludeResult.cancelled || preludeResult.status === "error") {
419
- throw new Error("Failed to initialize Python kernel prelude");
420
- }
421
- await loadPythonModules(kernel, { cwd });
512
+ await kernel.#connectWebSocket(startup);
513
+ await kernel.#initializeKernelEnvironment(cwd, env, startup);
514
+ const preludeOptions = getStartupExecuteOptions(startup);
515
+ const preludeResult = await kernel.execute(PYTHON_PRELUDE, {
516
+ ...preludeOptions,
517
+ silent: true,
518
+ storeHistory: false,
519
+ });
520
+ throwIfStartupExecutionFailed(
521
+ preludeResult,
522
+ preludeOptions.signal,
523
+ "Failed to initialize Python kernel prelude",
524
+ );
525
+ await loadPythonModules(kernel, { cwd, signal: startup.signal, deadlineMs: startup.deadlineMs });
422
526
  return kernel;
423
527
  } catch (err: unknown) {
424
- await kernel.shutdown();
528
+ await kernel.shutdown({ timeoutMs: getStartupCleanupTimeoutMs(startup.deadlineMs) });
425
529
  throw err;
426
530
  }
427
531
  }
@@ -430,12 +534,16 @@ export class PythonKernel {
430
534
  gatewayUrl: string,
431
535
  cwd: string,
432
536
  env?: Record<string, string | undefined>,
537
+ startup: KernelLifecycleOptions = {},
433
538
  ): Promise<PythonKernel> {
539
+ const startupSignal = combineAbortSignal(startup, undefined, "Python kernel startup aborted");
540
+ throwIfAborted(startupSignal, "Python kernel startup aborted");
434
541
  const createResponse = await logger.timeAsync("startWithSharedGateway:createKernel", () =>
435
542
  fetch(`${gatewayUrl}/api/kernels`, {
436
543
  method: "POST",
437
544
  headers: { "Content-Type": "application/json" },
438
545
  body: JSON.stringify({ name: "python3" }),
546
+ signal: startupSignal,
439
547
  }),
440
548
  );
441
549
 
@@ -458,46 +566,70 @@ export class PythonKernel {
458
566
  const kernel = new PythonKernel(Snowflake.next(), kernelId, gatewayUrl, Snowflake.next(), "omp", true);
459
567
 
460
568
  try {
461
- await logger.timeAsync("startWithSharedGateway:connectWS", () => kernel.#connectWebSocket());
462
- await logger.timeAsync("startWithSharedGateway:initEnv", () => kernel.#initializeKernelEnvironment(cwd, env));
569
+ await logger.timeAsync("startWithSharedGateway:connectWS", () => kernel.#connectWebSocket(startup));
570
+ await logger.timeAsync("startWithSharedGateway:initEnv", () =>
571
+ kernel.#initializeKernelEnvironment(cwd, env, startup),
572
+ );
573
+ const preludeOptions = getStartupExecuteOptions(startup);
463
574
  const preludeResult = await logger.timeAsync("startWithSharedGateway:prelude", () =>
464
- kernel.execute(PYTHON_PRELUDE, { silent: true, storeHistory: false }),
575
+ kernel.execute(PYTHON_PRELUDE, {
576
+ ...preludeOptions,
577
+ silent: true,
578
+ storeHistory: false,
579
+ }),
580
+ );
581
+ throwIfStartupExecutionFailed(
582
+ preludeResult,
583
+ preludeOptions.signal,
584
+ "Failed to initialize Python kernel prelude",
585
+ );
586
+ await logger.timeAsync("startWithSharedGateway:loadModules", () =>
587
+ loadPythonModules(kernel, { cwd, signal: startup.signal, deadlineMs: startup.deadlineMs }),
465
588
  );
466
- if (preludeResult.cancelled || preludeResult.status === "error") {
467
- throw new Error("Failed to initialize Python kernel prelude");
468
- }
469
- await logger.timeAsync("startWithSharedGateway:loadModules", () => loadPythonModules(kernel, { cwd }));
470
589
  return kernel;
471
590
  } catch (err: unknown) {
472
- await kernel.shutdown();
591
+ await kernel.shutdown({ timeoutMs: getStartupCleanupTimeoutMs(startup.deadlineMs) });
473
592
  throw err;
474
593
  }
475
594
  }
476
595
 
477
- async #connectWebSocket(): Promise<void> {
596
+ async #connectWebSocket(options: KernelLifecycleOptions = {}): Promise<void> {
478
597
  const wsBase = this.gatewayUrl.replace(/^http/, "ws");
479
598
  let wsUrl = `${wsBase}/api/kernels/${this.kernelId}/channels`;
480
599
  if (this.#authToken) {
481
600
  wsUrl += `?token=${encodeURIComponent(this.#authToken)}`;
482
601
  }
483
602
 
603
+ const connectSignal = combineAbortSignal(options, WEBSOCKET_CONNECT_TIMEOUT_MS, "WebSocket connection timeout");
604
+ throwIfAborted(connectSignal, "WebSocket connection timeout");
605
+
484
606
  const { promise, resolve, reject } = Promise.withResolvers<void>();
485
607
  const ws = new WebSocket(wsUrl);
486
608
  ws.binaryType = "arraybuffer";
487
609
  let settled = false;
488
610
 
489
- const timeout = setTimeout(() => {
490
- ws.close();
491
- if (!settled) {
492
- settled = true;
493
- reject(new Error("WebSocket connection timeout"));
611
+ const finalize = (): void => {
612
+ if (connectSignal) {
613
+ connectSignal.removeEventListener("abort", onAbort);
494
614
  }
495
- }, 10000);
615
+ };
616
+
617
+ const onAbort = () => {
618
+ ws.close();
619
+ if (settled) return;
620
+ settled = true;
621
+ finalize();
622
+ reject(getAbortReason(connectSignal, "WebSocket connection timeout"));
623
+ };
624
+
625
+ if (connectSignal) {
626
+ connectSignal.addEventListener("abort", onAbort, { once: true });
627
+ }
496
628
 
497
629
  ws.onopen = () => {
498
630
  if (settled) return;
499
631
  settled = true;
500
- clearTimeout(timeout);
632
+ finalize();
501
633
  this.#ws = ws;
502
634
  resolve();
503
635
  };
@@ -506,7 +638,7 @@ export class PythonKernel {
506
638
  const error = new Error(`WebSocket error: ${event}`);
507
639
  if (!settled) {
508
640
  settled = true;
509
- clearTimeout(timeout);
641
+ finalize();
510
642
  reject(error);
511
643
  return;
512
644
  }
@@ -520,7 +652,7 @@ export class PythonKernel {
520
652
  this.#ws = null;
521
653
  if (!settled) {
522
654
  settled = true;
523
- clearTimeout(timeout);
655
+ finalize();
524
656
  reject(new Error("WebSocket closed before connection"));
525
657
  return;
526
658
  }
@@ -561,7 +693,11 @@ export class PythonKernel {
561
693
  return promise;
562
694
  }
563
695
 
564
- async #initializeKernelEnvironment(cwd: string, env?: Record<string, string | undefined>): Promise<void> {
696
+ async #initializeKernelEnvironment(
697
+ cwd: string,
698
+ env?: Record<string, string | undefined>,
699
+ options: KernelLifecycleOptions = {},
700
+ ): Promise<void> {
565
701
  const envEntries = Object.entries(env ?? {}).filter(([, value]) => value !== undefined);
566
702
  const envPayload = Object.fromEntries(envEntries);
567
703
  const initScript = [
@@ -572,10 +708,13 @@ export class PythonKernel {
572
708
  "for __omp_key, __omp_val in __omp_env.items():\n os.environ[__omp_key] = __omp_val",
573
709
  "if __omp_cwd not in sys.path:\n sys.path.insert(0, __omp_cwd)",
574
710
  ].join("\n");
575
- const result = await this.execute(initScript, { silent: true, storeHistory: false });
576
- if (result.cancelled || result.status === "error") {
577
- throw new Error("Failed to initialize Python kernel environment");
578
- }
711
+ const executeOptions = getStartupExecuteOptions(options);
712
+ const result = await this.execute(initScript, {
713
+ ...executeOptions,
714
+ silent: true,
715
+ storeHistory: false,
716
+ });
717
+ throwIfStartupExecutionFailed(result, executeOptions.signal, "Failed to initialize Python kernel environment");
579
718
  }
580
719
 
581
720
  #abortPendingExecutions(reason: string): void {
@@ -842,16 +981,23 @@ export class PythonKernel {
842
981
  }
843
982
  }
844
983
 
845
- async shutdown(): Promise<void> {
984
+ async shutdown(options?: KernelShutdownOptions): Promise<void> {
846
985
  if (this.#disposed) return;
847
986
  this.#disposed = true;
848
987
  this.#alive = false;
849
988
  this.#abortPendingExecutions("Kernel shutdown");
850
989
 
990
+ const shutdownSignal = combineAbortSignal(
991
+ { signal: options?.signal },
992
+ options?.timeoutMs,
993
+ "Python kernel shutdown timed out",
994
+ );
995
+
851
996
  try {
852
997
  await fetch(`${this.gatewayUrl}/api/kernels/${this.kernelId}`, {
853
998
  method: "DELETE",
854
999
  headers: this.#authHeaders(),
1000
+ signal: shutdownSignal,
855
1001
  });
856
1002
  } catch (err: unknown) {
857
1003
  logger.warn("Failed to delete kernel via API", { error: err instanceof Error ? err.message : String(err) });
@@ -1,6 +1,7 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import { getAgentModulesDir, getProjectDir, getProjectModulesDir } from "@oh-my-pi/pi-utils";
4
+ import { getExecutionCancellationError } from "./cancellation";
4
5
 
5
6
  export type PythonModuleSource = "user" | "project";
6
7
 
@@ -13,13 +14,14 @@ export interface PythonModuleEntry {
13
14
  export interface PythonModuleExecuteResult {
14
15
  status: "ok" | "error";
15
16
  cancelled: boolean;
17
+ timedOut?: boolean;
16
18
  error?: { name: string; value: string; traceback: string[] };
17
19
  }
18
20
 
19
21
  export interface PythonModuleExecutor {
20
22
  execute: (
21
23
  code: string,
22
- options?: { silent?: boolean; storeHistory?: boolean },
24
+ options?: { signal?: AbortSignal; timeoutMs?: number; silent?: boolean; storeHistory?: boolean },
23
25
  ) => Promise<PythonModuleExecuteResult>;
24
26
  }
25
27
 
@@ -30,6 +32,12 @@ export interface DiscoverPythonModulesOptions {
30
32
  agentDir?: string;
31
33
  }
32
34
 
35
+ export interface LoadPythonModulesOptions extends DiscoverPythonModulesOptions {
36
+ signal?: AbortSignal;
37
+ timeoutMs?: number;
38
+ deadlineMs?: number;
39
+ }
40
+
33
41
  interface ModuleCandidate {
34
42
  name: string;
35
43
  path: string;
@@ -61,6 +69,25 @@ async function readModuleContent(candidate: ModuleCandidate): Promise<PythonModu
61
69
  }
62
70
  }
63
71
 
72
+ function createTimeoutError(message: string): Error {
73
+ const error = new Error(message);
74
+ error.name = "TimeoutError";
75
+ return error;
76
+ }
77
+
78
+ function requireModuleExecutionTimeoutMs(options: LoadPythonModulesOptions): number | undefined {
79
+ if (options.deadlineMs === undefined) {
80
+ return options.timeoutMs;
81
+ }
82
+
83
+ const remainingMs = options.deadlineMs - Date.now();
84
+ if (remainingMs <= 0) {
85
+ throw createTimeoutError("Python module loading timed out");
86
+ }
87
+
88
+ return remainingMs;
89
+ }
90
+
64
91
  /**
65
92
  * Discover Python prelude extension modules from user and project directories.
66
93
  */
@@ -95,12 +122,20 @@ export async function discoverPythonModules(options: DiscoverPythonModulesOption
95
122
  */
96
123
  export async function loadPythonModules(
97
124
  executor: PythonModuleExecutor,
98
- options: DiscoverPythonModulesOptions = {},
125
+ options: LoadPythonModulesOptions = {},
99
126
  ): Promise<PythonModuleEntry[]> {
100
127
  const modules = await discoverPythonModules(options);
101
128
  for (const module of modules) {
102
- const result = await executor.execute(module.content, { silent: true, storeHistory: false });
103
- if (result.cancelled || result.status === "error") {
129
+ const result = await executor.execute(module.content, {
130
+ signal: options.signal,
131
+ timeoutMs: requireModuleExecutionTimeoutMs(options),
132
+ silent: true,
133
+ storeHistory: false,
134
+ });
135
+ if (result.cancelled) {
136
+ throw getExecutionCancellationError(result, options.signal, `Failed to load Python module ${module.path}`);
137
+ }
138
+ if (result.status === "error") {
104
139
  const details = result.error ? `${result.error.name}: ${result.error.value}` : "unknown error";
105
140
  throw new Error(`Failed to load Python module ${module.path}: ${details}`);
106
141
  }
@@ -832,6 +832,7 @@ export class AcpAgent implements Agent {
832
832
  },
833
833
  {
834
834
  getModel: () => this.#session.model,
835
+ getSearchDb: () => this.#session.searchDb,
835
836
  isIdle: () => !this.#session.isStreaming,
836
837
  abort: () => {
837
838
  void this.#session.abort();
@@ -123,6 +123,7 @@ export class ExtensionUiController {
123
123
  };
124
124
  const contextActions: ExtensionContextActions = {
125
125
  getModel: () => this.ctx.session.model,
126
+ getSearchDb: () => this.ctx.session.searchDb,
126
127
  isIdle: () => !this.ctx.session.isStreaming,
127
128
  abort: () => this.ctx.session.abort(),
128
129
  hasPendingMessages: () => this.ctx.session.queuedMessageCount > 0,
@@ -384,6 +385,7 @@ export class ExtensionUiController {
384
385
  };
385
386
  const contextActions: ExtensionContextActions = {
386
387
  getModel: () => this.ctx.session.model,
388
+ getSearchDb: () => this.ctx.session.searchDb,
387
389
  isIdle: () => !this.ctx.session.isStreaming,
388
390
  abort: () => this.ctx.session.abort(),
389
391
  hasPendingMessages: () => this.ctx.session.queuedMessageCount > 0,
@@ -581,6 +583,7 @@ export class ExtensionUiController {
581
583
  sessionManager: this.ctx.session.sessionManager,
582
584
  modelRegistry: this.ctx.session.modelRegistry,
583
585
  model: this.ctx.session.model,
586
+ searchDb: this.ctx.session.searchDb,
584
587
  isIdle: () => !this.ctx.session.isStreaming,
585
588
  hasPendingMessages: () => this.ctx.session.queuedMessageCount > 0,
586
589
  hasQueuedMessages: () => this.ctx.session.queuedMessageCount > 0,
@@ -550,6 +550,7 @@ export class InputController {
550
550
  return createPromptActionAutocompleteProvider({
551
551
  commands,
552
552
  basePath,
553
+ searchDb: this.ctx.session.searchDb,
553
554
  keybindings: this.ctx.keybindings,
554
555
  copyCurrentLine: () => this.handleCopyCurrentLine(),
555
556
  copyPrompt: () => this.handleCopyPrompt(),
@@ -76,6 +76,7 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
76
76
  // ExtensionContextActions
77
77
  {
78
78
  getModel: () => session.model,
79
+ getSearchDb: () => session.searchDb,
79
80
  isIdle: () => !session.isStreaming,
80
81
  abort: () => session.abort(),
81
82
  hasPendingMessages: () => session.queuedMessageCount > 0,
@@ -1,3 +1,4 @@
1
+ import type { SearchDb } from "@oh-my-pi/pi-natives";
1
2
  import {
2
3
  type AutocompleteItem,
3
4
  type AutocompleteProvider,
@@ -23,6 +24,7 @@ interface PromptActionAutocompleteItem extends AutocompleteItem {
23
24
  interface PromptActionAutocompleteOptions {
24
25
  commands: SlashCommand[];
25
26
  basePath: string;
27
+ searchDb?: SearchDb;
26
28
  keybindings: KeybindingsManager;
27
29
  copyCurrentLine: () => void;
28
30
  copyPrompt: () => void;
@@ -90,8 +92,8 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
90
92
  #baseProvider: CombinedAutocompleteProvider;
91
93
  #actions: PromptActionDefinition[];
92
94
 
93
- constructor(commands: SlashCommand[], basePath: string, actions: PromptActionDefinition[]) {
94
- this.#baseProvider = new CombinedAutocompleteProvider(commands, basePath);
95
+ constructor(commands: SlashCommand[], basePath: string, actions: PromptActionDefinition[], searchDb?: SearchDb) {
96
+ this.#baseProvider = new CombinedAutocompleteProvider(commands, basePath, searchDb);
95
97
  this.#actions = actions;
96
98
  }
97
99
 
@@ -227,5 +229,5 @@ export function createPromptActionAutocompleteProvider(
227
229
  },
228
230
  ];
229
231
 
230
- return new PromptActionAutocompleteProvider(options.commands, options.basePath, actions);
232
+ return new PromptActionAutocompleteProvider(options.commands, options.basePath, actions, options.searchDb);
231
233
  }
@@ -404,6 +404,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
404
404
  // ExtensionContextActions
405
405
  {
406
406
  getModel: () => session.agent.state.model,
407
+ getSearchDb: () => session.searchDb,
407
408
  isIdle: () => !session.isStreaming,
408
409
  abort: () => session.abort(),
409
410
  hasPendingMessages: () => session.queuedMessageCount > 0,
@@ -1,4 +1,4 @@
1
- Searches files using powerful regex matching built on ripgrep.
1
+ Searches files using powerful regex matching.
2
2
 
3
3
  <instruction>
4
4
  - Supports full regex syntax (e.g., `log.*Error`, `function\\s+\\w+`); literal braces need escaping (`interface\\{\\}` for `interface{}` in Go)
package/src/sdk.ts CHANGED
@@ -9,8 +9,17 @@ import {
9
9
  import type { Message, Model } from "@oh-my-pi/pi-ai";
10
10
 
11
11
  import { prewarmOpenAICodexResponses } from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
12
+ import { SearchDb } from "@oh-my-pi/pi-natives";
12
13
  import type { Component } from "@oh-my-pi/pi-tui";
13
- import { $env, getAgentDbPath, getAgentDir, getProjectDir, logger, postmortem } from "@oh-my-pi/pi-utils";
14
+ import {
15
+ $env,
16
+ getAgentDbPath,
17
+ getAgentDir,
18
+ getProjectDir,
19
+ getSearchDbDir,
20
+ logger,
21
+ postmortem,
22
+ } from "@oh-my-pi/pi-utils";
14
23
  import chalk from "chalk";
15
24
  import { AsyncJobManager } from "./async";
16
25
  import { createAutoresearchExtension } from "./autoresearch";
@@ -131,6 +140,8 @@ export interface CreateAgentSessionOptions {
131
140
  authStorage?: AuthStorage;
132
141
  /** Model registry. Default: discoverModels(authStorage, agentDir) */
133
142
  modelRegistry?: ModelRegistry;
143
+ /** Shared native search DB for grep/glob/fuzzyFind-backed workflows. */
144
+ searchDb?: SearchDb;
134
145
 
135
146
  /** Model to use. Default: from settings, else first available */
136
147
  model?: Model;
@@ -381,6 +392,7 @@ function createCustomToolContext(ctx: ExtensionContext): CustomToolContext {
381
392
  sessionManager: ctx.sessionManager,
382
393
  modelRegistry: ctx.modelRegistry,
383
394
  model: ctx.model,
395
+ searchDb: ctx.searchDb,
384
396
  isIdle: ctx.isIdle,
385
397
  hasQueuedMessages: ctx.hasPendingMessages,
386
398
  abort: ctx.abort,
@@ -862,6 +874,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
862
874
  })
863
875
  : undefined;
864
876
 
877
+ const searchDb = options.searchDb ?? new SearchDb(getSearchDbDir(agentDir));
865
878
  const pendingActionStore = new PendingActionStore();
866
879
  const toolSession: ToolSession = {
867
880
  cwd,
@@ -911,6 +924,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
911
924
  modelRegistry,
912
925
  asyncJobManager,
913
926
  pendingActionStore,
927
+ searchDb,
914
928
  };
915
929
 
916
930
  // Initialize internal URL router for internal protocols (agent://, artifact://, memory://, skill://, rule://, mcp://, local://)
@@ -1173,6 +1187,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1173
1187
  sessionManager,
1174
1188
  modelRegistry,
1175
1189
  model: agent?.state.model,
1190
+ searchDb,
1176
1191
  isIdle: () => !session?.isStreaming,
1177
1192
  hasQueuedMessages: () => (session?.queuedMessageCount ?? 0) > 0,
1178
1193
  abort: () => session?.abort(),
@@ -1539,6 +1554,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1539
1554
  obfuscator,
1540
1555
  asyncJobManager,
1541
1556
  pendingActionStore,
1557
+ searchDb,
1542
1558
  });
1543
1559
 
1544
1560
  if (model?.api === "openai-codex-responses") {
@@ -50,6 +50,7 @@ import {
50
50
  modelsAreEqual,
51
51
  parseRateLimitReason,
52
52
  } from "@oh-my-pi/pi-ai";
53
+ import type { SearchDb } from "@oh-my-pi/pi-natives";
53
54
  import { abortableSleep, getAgentDbPath, isEnoent, logger } from "@oh-my-pi/pi-utils";
54
55
  import type { AsyncJob, AsyncJobManager } from "../async";
55
56
  import type { Rule } from "../capability/rule";
@@ -237,6 +238,8 @@ export interface AgentSessionConfig {
237
238
  obfuscator?: SecretObfuscator;
238
239
  /** Pending action store for preview/apply workflows */
239
240
  pendingActionStore?: PendingActionStore;
241
+ /** Shared native search DB for grep/glob/fuzzyFind-backed workflows. */
242
+ searchDb?: SearchDb;
240
243
  }
241
244
 
242
245
  /** Options for AgentSession.prompt() */
@@ -348,6 +351,7 @@ export class AgentSession {
348
351
  readonly agent: Agent;
349
352
  readonly sessionManager: SessionManager;
350
353
  readonly settings: Settings;
354
+ readonly searchDb: SearchDb | undefined;
351
355
 
352
356
  #asyncJobManager: AsyncJobManager | undefined = undefined;
353
357
  #scopedModels: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>;
@@ -462,6 +466,7 @@ export class AgentSession {
462
466
  this.agent = config.agent;
463
467
  this.sessionManager = config.sessionManager;
464
468
  this.settings = config.settings;
469
+ this.searchDb = config.searchDb;
465
470
  this.#asyncJobManager = config.asyncJobManager;
466
471
  this.#scopedModels = config.scopedModels ?? [];
467
472
  this.#thinkingLevel = config.thinkingLevel;
@@ -1888,6 +1893,7 @@ export class AgentSession {
1888
1893
  sessionManager: this.sessionManager,
1889
1894
  modelRegistry: this.#modelRegistry,
1890
1895
  model: this.model,
1896
+ searchDb: this.searchDb,
1891
1897
  isIdle: () => !this.isStreaming,
1892
1898
  hasQueuedMessages: () => this.queuedMessageCount > 0,
1893
1899
  abort: () => {