@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/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
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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", () =>
|
|
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, {
|
|
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
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
576
|
-
|
|
577
|
-
|
|
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) });
|
package/src/ipy/modules.ts
CHANGED
|
@@ -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:
|
|
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, {
|
|
103
|
-
|
|
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
|
}
|
|
@@ -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(),
|
package/src/modes/print-mode.ts
CHANGED
|
@@ -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
|
|
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 {
|
|
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: () => {
|