@oh-my-pi/pi-coding-agent 5.5.0 → 5.6.70
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 +105 -0
- package/docs/python-repl.md +77 -0
- package/examples/hooks/snake.ts +7 -7
- package/package.json +5 -5
- package/src/bun-imports.d.ts +6 -0
- package/src/cli/args.ts +7 -0
- package/src/cli/setup-cli.ts +231 -0
- package/src/cli.ts +2 -0
- package/src/core/agent-session.ts +118 -15
- package/src/core/bash-executor.ts +3 -84
- package/src/core/compaction/compaction.ts +10 -5
- package/src/core/extensions/index.ts +2 -0
- package/src/core/extensions/loader.ts +13 -1
- package/src/core/extensions/runner.ts +50 -2
- package/src/core/extensions/types.ts +67 -2
- package/src/core/keybindings.ts +51 -1
- package/src/core/prompt-templates.ts +15 -0
- package/src/core/python-executor-display.test.ts +42 -0
- package/src/core/python-executor-lifecycle.test.ts +99 -0
- package/src/core/python-executor-mapping.test.ts +41 -0
- package/src/core/python-executor-per-call.test.ts +49 -0
- package/src/core/python-executor-session.test.ts +103 -0
- package/src/core/python-executor-streaming.test.ts +77 -0
- package/src/core/python-executor-timeout.test.ts +35 -0
- package/src/core/python-executor.lifecycle.test.ts +139 -0
- package/src/core/python-executor.result.test.ts +49 -0
- package/src/core/python-executor.test.ts +180 -0
- package/src/core/python-executor.ts +313 -0
- package/src/core/python-gateway-coordinator.ts +832 -0
- package/src/core/python-kernel-display.test.ts +54 -0
- package/src/core/python-kernel-env.test.ts +138 -0
- package/src/core/python-kernel-session.test.ts +87 -0
- package/src/core/python-kernel-ws.test.ts +104 -0
- package/src/core/python-kernel.lifecycle.test.ts +249 -0
- package/src/core/python-kernel.test.ts +461 -0
- package/src/core/python-kernel.ts +1182 -0
- package/src/core/python-modules.test.ts +102 -0
- package/src/core/python-modules.ts +110 -0
- package/src/core/python-prelude.py +889 -0
- package/src/core/python-prelude.test.ts +140 -0
- package/src/core/python-prelude.ts +3 -0
- package/src/core/sdk.ts +24 -6
- package/src/core/session-manager.ts +174 -82
- package/src/core/settings-manager-python.test.ts +23 -0
- package/src/core/settings-manager.ts +202 -0
- package/src/core/streaming-output.test.ts +26 -0
- package/src/core/streaming-output.ts +100 -0
- package/src/core/system-prompt.python.test.ts +17 -0
- package/src/core/system-prompt.ts +3 -1
- package/src/core/timings.ts +1 -1
- package/src/core/tools/bash.ts +13 -2
- package/src/core/tools/edit-diff.ts +9 -1
- package/src/core/tools/index.test.ts +50 -23
- package/src/core/tools/index.ts +83 -1
- package/src/core/tools/python-execution.test.ts +68 -0
- package/src/core/tools/python-fallback.test.ts +72 -0
- package/src/core/tools/python-renderer.test.ts +36 -0
- package/src/core/tools/python-tool-mode.test.ts +43 -0
- package/src/core/tools/python.test.ts +121 -0
- package/src/core/tools/python.ts +760 -0
- package/src/core/tools/renderers.ts +2 -0
- package/src/core/tools/schema-validation.test.ts +1 -0
- package/src/core/tools/task/executor.ts +146 -3
- package/src/core/tools/task/worker-protocol.ts +32 -2
- package/src/core/tools/task/worker.ts +182 -15
- package/src/index.ts +6 -0
- package/src/main.ts +136 -40
- package/src/modes/interactive/components/custom-editor.ts +16 -31
- package/src/modes/interactive/components/extensions/extension-dashboard.ts +5 -16
- package/src/modes/interactive/components/extensions/extension-list.ts +5 -13
- package/src/modes/interactive/components/history-search.ts +5 -8
- package/src/modes/interactive/components/hook-editor.ts +3 -4
- package/src/modes/interactive/components/hook-input.ts +3 -3
- package/src/modes/interactive/components/hook-selector.ts +5 -15
- package/src/modes/interactive/components/index.ts +1 -0
- package/src/modes/interactive/components/keybinding-hints.ts +66 -0
- package/src/modes/interactive/components/model-selector.ts +53 -66
- package/src/modes/interactive/components/oauth-selector.ts +5 -5
- package/src/modes/interactive/components/session-selector.ts +29 -23
- package/src/modes/interactive/components/settings-defs.ts +404 -196
- package/src/modes/interactive/components/settings-selector.ts +14 -10
- package/src/modes/interactive/components/status-line-segment-editor.ts +7 -7
- package/src/modes/interactive/components/tool-execution.ts +8 -0
- package/src/modes/interactive/components/tree-selector.ts +29 -23
- package/src/modes/interactive/components/user-message-selector.ts +6 -17
- package/src/modes/interactive/controllers/command-controller.ts +86 -37
- package/src/modes/interactive/controllers/event-controller.ts +8 -0
- package/src/modes/interactive/controllers/extension-ui-controller.ts +51 -0
- package/src/modes/interactive/controllers/input-controller.ts +42 -6
- package/src/modes/interactive/interactive-mode.ts +56 -30
- package/src/modes/interactive/theme/theme-schema.json +2 -2
- package/src/modes/interactive/types.ts +6 -1
- package/src/modes/interactive/utils/ui-helpers.ts +2 -1
- package/src/modes/print-mode.ts +23 -0
- package/src/modes/rpc/rpc-mode.ts +21 -0
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/system/system-prompt.md +32 -1
- package/src/prompts/tools/python.md +91 -0
|
@@ -17,6 +17,7 @@ import { lsToolRenderer } from "./ls";
|
|
|
17
17
|
import { lspToolRenderer } from "./lsp/render";
|
|
18
18
|
import { notebookToolRenderer } from "./notebook";
|
|
19
19
|
import { outputToolRenderer } from "./output";
|
|
20
|
+
import { pythonToolRenderer } from "./python";
|
|
20
21
|
import { readToolRenderer } from "./read";
|
|
21
22
|
import { sshToolRenderer } from "./ssh";
|
|
22
23
|
import { taskToolRenderer } from "./task/render";
|
|
@@ -41,6 +42,7 @@ type ToolRenderer = {
|
|
|
41
42
|
export const toolRenderers: Record<string, ToolRenderer> = {
|
|
42
43
|
ask: askToolRenderer as ToolRenderer,
|
|
43
44
|
bash: bashToolRenderer as ToolRenderer,
|
|
45
|
+
python: pythonToolRenderer as ToolRenderer,
|
|
44
46
|
calc: calculatorToolRenderer as ToolRenderer,
|
|
45
47
|
edit: editToolRenderer as ToolRenderer,
|
|
46
48
|
find: findToolRenderer as ToolRenderer,
|
|
@@ -10,6 +10,9 @@ import type { EventBus } from "../../event-bus";
|
|
|
10
10
|
import { callTool } from "../../mcp/client";
|
|
11
11
|
import type { MCPManager } from "../../mcp/manager";
|
|
12
12
|
import type { ModelRegistry } from "../../model-registry";
|
|
13
|
+
import { checkPythonKernelAvailability } from "../../python-kernel";
|
|
14
|
+
import type { ToolSession } from "..";
|
|
15
|
+
import { createPythonTool } from "../python";
|
|
13
16
|
import { ensureArtifactsDir, getArtifactPaths } from "./artifacts";
|
|
14
17
|
import { resolveModelPattern } from "./model-resolver";
|
|
15
18
|
import { subprocessToolRegistry } from "./subprocess-tool-registry";
|
|
@@ -26,6 +29,8 @@ import {
|
|
|
26
29
|
import type {
|
|
27
30
|
MCPToolCallRequest,
|
|
28
31
|
MCPToolMetadata,
|
|
32
|
+
PythonToolCallCancel,
|
|
33
|
+
PythonToolCallRequest,
|
|
29
34
|
SubagentWorkerRequest,
|
|
30
35
|
SubagentWorkerResponse,
|
|
31
36
|
} from "./worker-protocol";
|
|
@@ -52,7 +57,12 @@ export interface ExecutorOptions {
|
|
|
52
57
|
mcpManager?: MCPManager;
|
|
53
58
|
authStorage?: AuthStorage;
|
|
54
59
|
modelRegistry?: ModelRegistry;
|
|
55
|
-
settingsManager?: {
|
|
60
|
+
settingsManager?: {
|
|
61
|
+
serialize: () => import("../../settings-manager").Settings;
|
|
62
|
+
getPythonToolMode?: () => "ipy-only" | "bash-only" | "both";
|
|
63
|
+
getPythonKernelMode?: () => "session" | "per-call";
|
|
64
|
+
getPythonSharedGateway?: () => boolean;
|
|
65
|
+
};
|
|
56
66
|
}
|
|
57
67
|
|
|
58
68
|
/**
|
|
@@ -269,14 +279,34 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
269
279
|
}
|
|
270
280
|
}
|
|
271
281
|
|
|
282
|
+
const pythonToolMode = options.settingsManager?.getPythonToolMode?.() ?? "ipy-only";
|
|
283
|
+
if (toolNames?.includes("exec")) {
|
|
284
|
+
const expanded = toolNames.filter((name) => name !== "exec");
|
|
285
|
+
if (pythonToolMode === "bash-only") {
|
|
286
|
+
expanded.push("bash");
|
|
287
|
+
} else if (pythonToolMode === "ipy-only") {
|
|
288
|
+
expanded.push("python");
|
|
289
|
+
} else {
|
|
290
|
+
expanded.push("python", "bash");
|
|
291
|
+
}
|
|
292
|
+
toolNames = Array.from(new Set(expanded));
|
|
293
|
+
}
|
|
294
|
+
|
|
272
295
|
const serializedSettings = options.settingsManager?.serialize();
|
|
273
296
|
const availableModels = options.modelRegistry?.getAvailable().map((model) => `${model.provider}/${model.id}`);
|
|
274
297
|
|
|
275
298
|
// Resolve and add model
|
|
276
299
|
const resolvedModel = await resolveModelPattern(modelOverride || agent.model, availableModels, serializedSettings);
|
|
277
|
-
const sessionFile = subtaskSessionFile ??
|
|
300
|
+
const sessionFile = subtaskSessionFile ?? null;
|
|
278
301
|
const spawnsEnv = agent.spawns === undefined ? "" : agent.spawns === "*" ? "*" : agent.spawns.join(",");
|
|
279
302
|
|
|
303
|
+
const pythonToolRequested = toolNames === undefined || toolNames.includes("python");
|
|
304
|
+
let pythonProxyEnabled = pythonToolRequested && pythonToolMode !== "bash-only";
|
|
305
|
+
if (pythonProxyEnabled) {
|
|
306
|
+
const availability = await checkPythonKernelAvailability(cwd);
|
|
307
|
+
pythonProxyEnabled = availability.ok;
|
|
308
|
+
}
|
|
309
|
+
|
|
280
310
|
let worker: Worker;
|
|
281
311
|
try {
|
|
282
312
|
worker = new Worker(new URL("./worker.ts", import.meta.url), { type: "module" });
|
|
@@ -311,6 +341,49 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
311
341
|
let finalize: ((message: Extract<SubagentWorkerResponse, { type: "done" }>) => void) | null = null;
|
|
312
342
|
const listenerController = new AbortController();
|
|
313
343
|
const listenerSignal = listenerController.signal;
|
|
344
|
+
const withTimeout = async <T>(promise: Promise<T>, timeoutMs?: number): Promise<T> => {
|
|
345
|
+
if (timeoutMs === undefined) return promise;
|
|
346
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
347
|
+
try {
|
|
348
|
+
return await Promise.race([
|
|
349
|
+
promise,
|
|
350
|
+
new Promise<T>((_resolve, reject) => {
|
|
351
|
+
timeoutId = setTimeout(() => {
|
|
352
|
+
reject(new Error(`Tool call timed out after ${timeoutMs}ms`));
|
|
353
|
+
}, timeoutMs);
|
|
354
|
+
}),
|
|
355
|
+
]);
|
|
356
|
+
} finally {
|
|
357
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const combineSignals = (signals: Array<AbortSignal | undefined>): AbortSignal | undefined => {
|
|
362
|
+
const filtered = signals.filter((value): value is AbortSignal => Boolean(value));
|
|
363
|
+
if (filtered.length === 0) return undefined;
|
|
364
|
+
if (filtered.length === 1) return filtered[0];
|
|
365
|
+
return AbortSignal.any(filtered);
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const createTimeoutSignal = (timeoutMs?: number): AbortSignal | undefined => {
|
|
369
|
+
if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
370
|
+
return undefined;
|
|
371
|
+
}
|
|
372
|
+
return AbortSignal.timeout(timeoutMs);
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const pythonSessionFile = sessionFile ?? `subtask:${taskId}`;
|
|
376
|
+
const pythonToolSession: ToolSession = {
|
|
377
|
+
cwd,
|
|
378
|
+
hasUI: false,
|
|
379
|
+
enableLsp: false,
|
|
380
|
+
getSessionFile: () => pythonSessionFile,
|
|
381
|
+
getSessionSpawns: () => spawnsEnv,
|
|
382
|
+
settings: options.settingsManager as ToolSession["settings"],
|
|
383
|
+
settingsManager: options.settingsManager,
|
|
384
|
+
};
|
|
385
|
+
const pythonTool = pythonProxyEnabled ? createPythonTool(pythonToolSession) : null;
|
|
386
|
+
const pythonCallControllers = new Map<string, AbortController>();
|
|
314
387
|
|
|
315
388
|
// Accumulate usage incrementally from message_end events (no memory for streaming events)
|
|
316
389
|
const accumulatedUsage = {
|
|
@@ -360,6 +433,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
360
433
|
if (resolved) return;
|
|
361
434
|
abortSent = true;
|
|
362
435
|
abortReason = reason;
|
|
436
|
+
for (const controller of pythonCallControllers.values()) {
|
|
437
|
+
controller.abort();
|
|
438
|
+
}
|
|
439
|
+
pythonCallControllers.clear();
|
|
363
440
|
const abortMessage: SubagentWorkerRequest = { type: "abort" };
|
|
364
441
|
try {
|
|
365
442
|
worker.postMessage(abortMessage);
|
|
@@ -606,6 +683,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
606
683
|
serializedModels: options.modelRegistry?.serialize(),
|
|
607
684
|
serializedSettings,
|
|
608
685
|
mcpTools: options.mcpManager ? extractMCPToolMetadata(options.mcpManager) : undefined,
|
|
686
|
+
pythonToolProxy: pythonProxyEnabled,
|
|
609
687
|
},
|
|
610
688
|
};
|
|
611
689
|
|
|
@@ -642,7 +720,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
642
720
|
if (!parsed) throw new Error(`Invalid MCP tool name: ${request.toolName}`);
|
|
643
721
|
const connection = mcpManager.getConnection(parsed.serverName);
|
|
644
722
|
if (!connection) throw new Error(`MCP server not connected: ${parsed.serverName}`);
|
|
645
|
-
const result = await callTool(connection, parsed.toolName, request.params);
|
|
723
|
+
const result = await withTimeout(callTool(connection, parsed.toolName, request.params), request.timeoutMs);
|
|
646
724
|
worker.postMessage({
|
|
647
725
|
type: "mcp_tool_result",
|
|
648
726
|
callId: request.callId,
|
|
@@ -657,6 +735,63 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
657
735
|
}
|
|
658
736
|
};
|
|
659
737
|
|
|
738
|
+
const getPythonCallTimeoutMs = (params: { timeout?: number }): number | undefined => {
|
|
739
|
+
const timeout = params.timeout;
|
|
740
|
+
if (typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0) {
|
|
741
|
+
return Math.max(1000, Math.round(timeout * 1000) + 1000);
|
|
742
|
+
}
|
|
743
|
+
return undefined;
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
const handlePythonCall = async (request: PythonToolCallRequest) => {
|
|
747
|
+
if (!pythonTool) {
|
|
748
|
+
worker.postMessage({
|
|
749
|
+
type: "python_tool_result",
|
|
750
|
+
callId: request.callId,
|
|
751
|
+
error: "Python proxy not available",
|
|
752
|
+
});
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
const callController = new AbortController();
|
|
756
|
+
pythonCallControllers.set(request.callId, callController);
|
|
757
|
+
const timeoutMs = getPythonCallTimeoutMs(request.params as { timeout?: number });
|
|
758
|
+
const timeoutSignal = createTimeoutSignal(timeoutMs);
|
|
759
|
+
const combinedSignal = combineSignals([signal, callController.signal, timeoutSignal]);
|
|
760
|
+
try {
|
|
761
|
+
const result = await pythonTool.execute(
|
|
762
|
+
request.callId,
|
|
763
|
+
request.params as { code: string; timeout?: number; workdir?: string; reset?: boolean },
|
|
764
|
+
combinedSignal,
|
|
765
|
+
);
|
|
766
|
+
worker.postMessage({
|
|
767
|
+
type: "python_tool_result",
|
|
768
|
+
callId: request.callId,
|
|
769
|
+
result: { content: result.content ?? [], details: result.details },
|
|
770
|
+
});
|
|
771
|
+
} catch (error) {
|
|
772
|
+
const message =
|
|
773
|
+
timeoutSignal?.aborted && timeoutMs !== undefined
|
|
774
|
+
? `Python tool call timed out after ${timeoutMs}ms`
|
|
775
|
+
: error instanceof Error
|
|
776
|
+
? error.message
|
|
777
|
+
: String(error);
|
|
778
|
+
worker.postMessage({
|
|
779
|
+
type: "python_tool_result",
|
|
780
|
+
callId: request.callId,
|
|
781
|
+
error: message,
|
|
782
|
+
});
|
|
783
|
+
} finally {
|
|
784
|
+
pythonCallControllers.delete(request.callId);
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
const handlePythonCancel = (request: PythonToolCallCancel) => {
|
|
789
|
+
const controller = pythonCallControllers.get(request.callId);
|
|
790
|
+
if (controller) {
|
|
791
|
+
controller.abort();
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
|
|
660
795
|
const onMessage = (event: WorkerMessageEvent<SubagentWorkerResponse>) => {
|
|
661
796
|
const message = event.data;
|
|
662
797
|
if (!message || resolved) return;
|
|
@@ -664,6 +799,14 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
664
799
|
handleMCPCall(message as MCPToolCallRequest);
|
|
665
800
|
return;
|
|
666
801
|
}
|
|
802
|
+
if (message.type === "python_tool_call") {
|
|
803
|
+
handlePythonCall(message as PythonToolCallRequest);
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
if (message.type === "python_tool_cancel") {
|
|
807
|
+
handlePythonCancel(message as PythonToolCallCancel);
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
667
810
|
if (message.type === "event") {
|
|
668
811
|
try {
|
|
669
812
|
processEvent(message.event);
|
|
@@ -24,6 +24,7 @@ export interface MCPToolCallRequest {
|
|
|
24
24
|
callId: string;
|
|
25
25
|
toolName: string;
|
|
26
26
|
params: Record<string, unknown>;
|
|
27
|
+
timeoutMs?: number;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
/**
|
|
@@ -39,6 +40,30 @@ export interface MCPToolCallResponse {
|
|
|
39
40
|
error?: string;
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
export interface PythonToolCallRequest {
|
|
44
|
+
type: "python_tool_call";
|
|
45
|
+
callId: string;
|
|
46
|
+
params: Record<string, unknown>;
|
|
47
|
+
timeoutMs?: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface PythonToolCallResponse {
|
|
51
|
+
type: "python_tool_result";
|
|
52
|
+
callId: string;
|
|
53
|
+
result?: {
|
|
54
|
+
content: Array<{ type: string; text?: string; [key: string]: unknown }>;
|
|
55
|
+
details?: unknown;
|
|
56
|
+
isError?: boolean;
|
|
57
|
+
};
|
|
58
|
+
error?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface PythonToolCallCancel {
|
|
62
|
+
type: "python_tool_cancel";
|
|
63
|
+
callId: string;
|
|
64
|
+
reason?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
42
67
|
export interface SubagentWorkerStartPayload {
|
|
43
68
|
cwd: string;
|
|
44
69
|
task: string;
|
|
@@ -54,14 +79,19 @@ export interface SubagentWorkerStartPayload {
|
|
|
54
79
|
serializedModels?: SerializedModelRegistry;
|
|
55
80
|
serializedSettings?: Settings;
|
|
56
81
|
mcpTools?: MCPToolMetadata[];
|
|
82
|
+
pythonToolProxy?: boolean;
|
|
57
83
|
}
|
|
58
84
|
|
|
59
85
|
export type SubagentWorkerRequest =
|
|
60
86
|
| { type: "start"; payload: SubagentWorkerStartPayload }
|
|
61
87
|
| { type: "abort" }
|
|
62
|
-
| MCPToolCallResponse
|
|
88
|
+
| MCPToolCallResponse
|
|
89
|
+
| PythonToolCallResponse
|
|
90
|
+
| PythonToolCallCancel;
|
|
63
91
|
|
|
64
92
|
export type SubagentWorkerResponse =
|
|
65
93
|
| { type: "event"; event: AgentEvent }
|
|
66
94
|
| { type: "done"; exitCode: number; durationMs: number; error?: string; aborted?: boolean }
|
|
67
|
-
| MCPToolCallRequest
|
|
95
|
+
| MCPToolCallRequest
|
|
96
|
+
| PythonToolCallRequest
|
|
97
|
+
| PythonToolCallCancel;
|
|
@@ -19,15 +19,18 @@ import type { TSchema } from "@sinclair/typebox";
|
|
|
19
19
|
import type { AgentSessionEvent } from "../../agent-session";
|
|
20
20
|
import { AuthStorage } from "../../auth-storage";
|
|
21
21
|
import type { CustomTool } from "../../custom-tools/types";
|
|
22
|
+
import { logger } from "../../logger";
|
|
22
23
|
import { ModelRegistry } from "../../model-registry";
|
|
23
24
|
import { parseModelPattern, parseModelString } from "../../model-resolver";
|
|
24
25
|
import { createAgentSession, discoverAuthStorage, discoverModels } from "../../sdk";
|
|
25
26
|
import { SessionManager } from "../../session-manager";
|
|
26
27
|
import { SettingsManager } from "../../settings-manager";
|
|
27
28
|
import { untilAborted } from "../../utils";
|
|
29
|
+
import { getPythonToolDescription, type PythonToolDetails, type PythonToolParams, pythonSchema } from "../python";
|
|
28
30
|
import type {
|
|
29
31
|
MCPToolCallResponse,
|
|
30
32
|
MCPToolMetadata,
|
|
33
|
+
PythonToolCallResponse,
|
|
31
34
|
SubagentWorkerRequest,
|
|
32
35
|
SubagentWorkerResponse,
|
|
33
36
|
SubagentWorkerStartPayload,
|
|
@@ -49,14 +52,26 @@ interface PendingMCPCall {
|
|
|
49
52
|
timeoutId: ReturnType<typeof setTimeout>;
|
|
50
53
|
}
|
|
51
54
|
|
|
55
|
+
interface PendingPythonCall {
|
|
56
|
+
resolve: (result: PythonToolCallResponse["result"]) => void;
|
|
57
|
+
reject: (error: Error) => void;
|
|
58
|
+
timeoutId?: ReturnType<typeof setTimeout>;
|
|
59
|
+
}
|
|
60
|
+
|
|
52
61
|
const pendingMCPCalls = new Map<string, PendingMCPCall>();
|
|
62
|
+
const pendingPythonCalls = new Map<string, PendingPythonCall>();
|
|
53
63
|
const MCP_CALL_TIMEOUT_MS = 60_000;
|
|
54
64
|
let mcpCallIdCounter = 0;
|
|
65
|
+
let pythonCallIdCounter = 0;
|
|
55
66
|
|
|
56
67
|
function generateMCPCallId(): string {
|
|
57
68
|
return `mcp_${Date.now()}_${++mcpCallIdCounter}`;
|
|
58
69
|
}
|
|
59
70
|
|
|
71
|
+
function generatePythonCallId(): string {
|
|
72
|
+
return `python_${Date.now()}_${++pythonCallIdCounter}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
60
75
|
function callMCPToolViaParent(
|
|
61
76
|
toolName: string,
|
|
62
77
|
params: Record<string, unknown>,
|
|
@@ -80,14 +95,16 @@ function callMCPToolViaParent(
|
|
|
80
95
|
pendingMCPCalls.delete(callId);
|
|
81
96
|
};
|
|
82
97
|
|
|
83
|
-
signal?.addEventListener
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
98
|
+
if (typeof signal?.addEventListener === "function") {
|
|
99
|
+
signal.addEventListener(
|
|
100
|
+
"abort",
|
|
101
|
+
() => {
|
|
102
|
+
cleanup();
|
|
103
|
+
reject(new Error("Aborted"));
|
|
104
|
+
},
|
|
105
|
+
{ once: true },
|
|
106
|
+
);
|
|
107
|
+
}
|
|
91
108
|
|
|
92
109
|
pendingMCPCalls.set(callId, {
|
|
93
110
|
resolve: (result) => {
|
|
@@ -106,6 +123,72 @@ function callMCPToolViaParent(
|
|
|
106
123
|
callId,
|
|
107
124
|
toolName,
|
|
108
125
|
params,
|
|
126
|
+
timeoutMs,
|
|
127
|
+
} as SubagentWorkerResponse);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function callPythonToolViaParent(
|
|
132
|
+
params: PythonToolParams,
|
|
133
|
+
signal?: AbortSignal,
|
|
134
|
+
timeoutMs?: number,
|
|
135
|
+
): Promise<PythonToolCallResponse["result"]> {
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
const callId = generatePythonCallId();
|
|
138
|
+
if (signal?.aborted) {
|
|
139
|
+
reject(new Error("Aborted"));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const sendCancel = (reason: string) => {
|
|
144
|
+
postMessageSafe({ type: "python_tool_cancel", callId, reason } as SubagentWorkerResponse);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const timeoutId =
|
|
148
|
+
typeof timeoutMs === "number" && Number.isFinite(timeoutMs)
|
|
149
|
+
? setTimeout(() => {
|
|
150
|
+
pendingPythonCalls.delete(callId);
|
|
151
|
+
sendCancel(`Python call timed out after ${timeoutMs}ms`);
|
|
152
|
+
reject(new Error(`Python call timed out after ${timeoutMs}ms`));
|
|
153
|
+
}, timeoutMs)
|
|
154
|
+
: undefined;
|
|
155
|
+
|
|
156
|
+
const cleanup = () => {
|
|
157
|
+
if (timeoutId) {
|
|
158
|
+
clearTimeout(timeoutId);
|
|
159
|
+
}
|
|
160
|
+
pendingPythonCalls.delete(callId);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
if (typeof signal?.addEventListener === "function") {
|
|
164
|
+
signal.addEventListener(
|
|
165
|
+
"abort",
|
|
166
|
+
() => {
|
|
167
|
+
cleanup();
|
|
168
|
+
sendCancel("Aborted");
|
|
169
|
+
reject(new Error("Aborted"));
|
|
170
|
+
},
|
|
171
|
+
{ once: true },
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
pendingPythonCalls.set(callId, {
|
|
176
|
+
resolve: (result) => {
|
|
177
|
+
cleanup();
|
|
178
|
+
resolve(result ?? { content: [] });
|
|
179
|
+
},
|
|
180
|
+
reject: (error) => {
|
|
181
|
+
cleanup();
|
|
182
|
+
reject(error);
|
|
183
|
+
},
|
|
184
|
+
timeoutId,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
postMessageSafe({
|
|
188
|
+
type: "python_tool_call",
|
|
189
|
+
callId,
|
|
190
|
+
params,
|
|
191
|
+
timeoutMs,
|
|
109
192
|
} as SubagentWorkerResponse);
|
|
110
193
|
});
|
|
111
194
|
}
|
|
@@ -120,6 +203,32 @@ function handleMCPToolResult(response: MCPToolCallResponse): void {
|
|
|
120
203
|
}
|
|
121
204
|
}
|
|
122
205
|
|
|
206
|
+
function handlePythonToolResult(response: PythonToolCallResponse): void {
|
|
207
|
+
const pending = pendingPythonCalls.get(response.callId);
|
|
208
|
+
if (!pending) return;
|
|
209
|
+
if (response.error) {
|
|
210
|
+
pending.reject(new Error(response.error));
|
|
211
|
+
} else {
|
|
212
|
+
pending.resolve(response.result);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function rejectPendingCalls(reason: string): void {
|
|
217
|
+
const error = new Error(reason);
|
|
218
|
+
const mcpCalls = Array.from(pendingMCPCalls.values());
|
|
219
|
+
const pythonCalls = Array.from(pendingPythonCalls.values());
|
|
220
|
+
pendingMCPCalls.clear();
|
|
221
|
+
pendingPythonCalls.clear();
|
|
222
|
+
for (const pending of mcpCalls) {
|
|
223
|
+
clearTimeout(pending.timeoutId);
|
|
224
|
+
pending.reject(error);
|
|
225
|
+
}
|
|
226
|
+
for (const pending of pythonCalls) {
|
|
227
|
+
clearTimeout(pending.timeoutId);
|
|
228
|
+
pending.reject(error);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
123
232
|
function createMCPProxyTool(metadata: MCPToolMetadata): CustomTool<TSchema> {
|
|
124
233
|
return {
|
|
125
234
|
name: metadata.name,
|
|
@@ -157,6 +266,36 @@ function createMCPProxyTool(metadata: MCPToolMetadata): CustomTool<TSchema> {
|
|
|
157
266
|
};
|
|
158
267
|
}
|
|
159
268
|
|
|
269
|
+
function getPythonCallTimeoutMs(params: PythonToolParams): number | undefined {
|
|
270
|
+
const timeout = params.timeout;
|
|
271
|
+
if (typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0) {
|
|
272
|
+
return Math.max(1000, Math.round(timeout * 1000) + 1000);
|
|
273
|
+
}
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function createPythonProxyTool(): CustomTool<typeof pythonSchema> {
|
|
278
|
+
return {
|
|
279
|
+
name: "python",
|
|
280
|
+
label: "Python",
|
|
281
|
+
description: getPythonToolDescription(),
|
|
282
|
+
parameters: pythonSchema,
|
|
283
|
+
execute: async (_toolCallId, params, _onUpdate, _ctx, signal) => {
|
|
284
|
+
const timeoutMs = getPythonCallTimeoutMs(params as PythonToolParams);
|
|
285
|
+
const result = await callPythonToolViaParent(params as PythonToolParams, signal, timeoutMs);
|
|
286
|
+
return {
|
|
287
|
+
content:
|
|
288
|
+
result?.content?.map((c) =>
|
|
289
|
+
c.type === "text"
|
|
290
|
+
? { type: "text" as const, text: c.text ?? "" }
|
|
291
|
+
: { type: "text" as const, text: JSON.stringify(c) },
|
|
292
|
+
) ?? [],
|
|
293
|
+
details: result?.details as PythonToolDetails | undefined,
|
|
294
|
+
};
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
160
299
|
interface WorkerMessageEvent<T> {
|
|
161
300
|
data: T;
|
|
162
301
|
}
|
|
@@ -284,8 +423,12 @@ async function runTask(runState: RunState, payload: SubagentWorkerStartPayload):
|
|
|
284
423
|
checkAbort();
|
|
285
424
|
}
|
|
286
425
|
|
|
287
|
-
// Create MCP proxy tools if provided
|
|
288
|
-
const mcpProxyTools = payload.mcpTools?.map(createMCPProxyTool) ?? [];
|
|
426
|
+
// Create MCP/python proxy tools if provided
|
|
427
|
+
const mcpProxyTools: CustomTool<TSchema>[] = payload.mcpTools?.map(createMCPProxyTool) ?? [];
|
|
428
|
+
const pythonProxyTools: CustomTool<TSchema>[] = payload.pythonToolProxy
|
|
429
|
+
? [createPythonProxyTool() as unknown as CustomTool<TSchema>]
|
|
430
|
+
: [];
|
|
431
|
+
const proxyTools = [...mcpProxyTools, ...pythonProxyTools];
|
|
289
432
|
|
|
290
433
|
// Resolve model override (equivalent to CLI's parseModelPattern with --model)
|
|
291
434
|
const { model, thinkingLevel: modelThinkingLevel } = resolveModelOverride(payload.model, modelRegistry);
|
|
@@ -325,8 +468,8 @@ async function runTask(runState: RunState, payload: SubagentWorkerStartPayload):
|
|
|
325
468
|
enableLsp: payload.enableLsp ?? true,
|
|
326
469
|
// Disable local MCP discovery if using proxy tools
|
|
327
470
|
enableMCP: !payload.mcpTools,
|
|
328
|
-
// Add
|
|
329
|
-
customTools:
|
|
471
|
+
// Add proxy tools
|
|
472
|
+
customTools: proxyTools.length > 0 ? proxyTools : undefined,
|
|
330
473
|
});
|
|
331
474
|
|
|
332
475
|
runState.session = session;
|
|
@@ -349,17 +492,24 @@ async function runTask(runState: RunState, payload: SubagentWorkerStartPayload):
|
|
|
349
492
|
{
|
|
350
493
|
sendMessage: (message, options) => {
|
|
351
494
|
session.sendCustomMessage(message, options).catch((e) => {
|
|
352
|
-
|
|
495
|
+
logger.error("Extension sendMessage failed", {
|
|
496
|
+
error: e instanceof Error ? e.message : String(e),
|
|
497
|
+
});
|
|
353
498
|
});
|
|
354
499
|
},
|
|
355
500
|
sendUserMessage: (content, options) => {
|
|
356
501
|
session.sendUserMessage(content, options).catch((e) => {
|
|
357
|
-
|
|
502
|
+
logger.error("Extension sendUserMessage failed", {
|
|
503
|
+
error: e instanceof Error ? e.message : String(e),
|
|
504
|
+
});
|
|
358
505
|
});
|
|
359
506
|
},
|
|
360
507
|
appendEntry: (customType, data) => {
|
|
361
508
|
session.sessionManager.appendCustomEntry(customType, data);
|
|
362
509
|
},
|
|
510
|
+
setLabel: (targetId, label) => {
|
|
511
|
+
session.sessionManager.appendLabelChange(targetId, label);
|
|
512
|
+
},
|
|
363
513
|
getActiveTools: () => session.getActiveToolNames(),
|
|
364
514
|
getAllTools: () => session.getAllToolNames(),
|
|
365
515
|
setActiveTools: (toolNames: string[]) => session.setActiveToolsByName(toolNames),
|
|
@@ -379,10 +529,19 @@ async function runTask(runState: RunState, payload: SubagentWorkerStartPayload):
|
|
|
379
529
|
abort: () => session.abort(),
|
|
380
530
|
hasPendingMessages: () => session.queuedMessageCount > 0,
|
|
381
531
|
shutdown: () => {},
|
|
532
|
+
getContextUsage: () => session.getContextUsage(),
|
|
533
|
+
compact: async (instructionsOrOptions) => {
|
|
534
|
+
const instructions = typeof instructionsOrOptions === "string" ? instructionsOrOptions : undefined;
|
|
535
|
+
const options =
|
|
536
|
+
instructionsOrOptions && typeof instructionsOrOptions === "object"
|
|
537
|
+
? instructionsOrOptions
|
|
538
|
+
: undefined;
|
|
539
|
+
await session.compact(instructions, options);
|
|
540
|
+
},
|
|
382
541
|
},
|
|
383
542
|
);
|
|
384
543
|
extensionRunner.onError((err) => {
|
|
385
|
-
|
|
544
|
+
logger.error("Extension error", { path: err.extensionPath, error: err.error });
|
|
386
545
|
});
|
|
387
546
|
await extensionRunner.emit({ type: "session_start" });
|
|
388
547
|
}
|
|
@@ -444,6 +603,7 @@ Call complete now.`;
|
|
|
444
603
|
}
|
|
445
604
|
|
|
446
605
|
sessionAbortController.abort();
|
|
606
|
+
rejectPendingCalls("Worker finished");
|
|
447
607
|
|
|
448
608
|
if (runState.unsubscribe) {
|
|
449
609
|
try {
|
|
@@ -485,8 +645,10 @@ function handleAbort(): void {
|
|
|
485
645
|
const runState = activeRun;
|
|
486
646
|
if (!runState) {
|
|
487
647
|
pendingAbort = true;
|
|
648
|
+
rejectPendingCalls("Aborted");
|
|
488
649
|
return;
|
|
489
650
|
}
|
|
651
|
+
rejectPendingCalls("Aborted");
|
|
490
652
|
runState.abortController.abort();
|
|
491
653
|
if (runState.session) {
|
|
492
654
|
void runState.session.abort();
|
|
@@ -556,6 +718,11 @@ globalThis.addEventListener("message", (event: WorkerMessageEvent<SubagentWorker
|
|
|
556
718
|
return;
|
|
557
719
|
}
|
|
558
720
|
|
|
721
|
+
if (message.type === "python_tool_result") {
|
|
722
|
+
handlePythonToolResult(message);
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
|
|
559
726
|
if (message.type === "start") {
|
|
560
727
|
// Only allow one task per worker
|
|
561
728
|
if (activeRun) return;
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
export { StringEnum } from "@oh-my-pi/pi-ai";
|
|
6
6
|
// Re-export TUI components for custom tool rendering
|
|
7
7
|
export { Container, Markdown, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
8
|
+
export { getAgentDir, VERSION } from "./config";
|
|
8
9
|
export {
|
|
9
10
|
AgentSession,
|
|
10
11
|
type AgentSessionConfig,
|
|
@@ -82,6 +83,8 @@ export type {
|
|
|
82
83
|
ExtensionShortcut,
|
|
83
84
|
ExtensionUIContext,
|
|
84
85
|
ExtensionUIDialogOptions,
|
|
86
|
+
InputEvent,
|
|
87
|
+
InputEventResult,
|
|
85
88
|
KeybindingsManager,
|
|
86
89
|
LoadExtensionsResult,
|
|
87
90
|
MessageRenderer,
|
|
@@ -107,6 +110,7 @@ export {
|
|
|
107
110
|
} from "./core/extensions/index";
|
|
108
111
|
// Hook system types (legacy re-export)
|
|
109
112
|
export type * from "./core/hooks/index";
|
|
113
|
+
export { formatKeyHint, formatKeyHints } from "./core/keybindings";
|
|
110
114
|
// Logging
|
|
111
115
|
export { type Logger, logger } from "./core/logger";
|
|
112
116
|
export { convertToLlm } from "./core/messages";
|
|
@@ -205,6 +209,7 @@ export {
|
|
|
205
209
|
type LsOperations,
|
|
206
210
|
type LsToolDetails,
|
|
207
211
|
type LsToolOptions,
|
|
212
|
+
type PythonToolDetails,
|
|
208
213
|
type ReadToolDetails,
|
|
209
214
|
type TruncationOptions,
|
|
210
215
|
type TruncationResult,
|
|
@@ -269,3 +274,4 @@ export {
|
|
|
269
274
|
Theme,
|
|
270
275
|
type ThemeColor,
|
|
271
276
|
} from "./modes/interactive/theme/theme";
|
|
277
|
+
export { getShellConfig } from "./utils/shell";
|