@oh-my-pi/pi-coding-agent 6.9.69 → 7.0.0
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 +132 -51
- package/examples/sdk/04-skills.ts +1 -1
- package/package.json +6 -6
- package/src/core/agent-session.ts +112 -4
- package/src/core/auth-storage.ts +524 -202
- package/src/core/bash-executor.ts +1 -1
- package/src/core/model-registry.ts +7 -0
- package/src/core/python-executor.ts +29 -8
- package/src/core/python-gateway-coordinator.ts +55 -1
- package/src/core/python-prelude.py +201 -8
- package/src/core/tools/find.ts +18 -5
- package/src/core/tools/lsp/index.ts +13 -2
- package/src/core/tools/python.ts +1 -0
- package/src/core/tools/read.ts +4 -4
- package/src/modes/interactive/controllers/command-controller.ts +349 -0
- package/src/modes/interactive/controllers/input-controller.ts +55 -7
- package/src/modes/interactive/interactive-mode.ts +6 -1
- package/src/modes/interactive/types.ts +2 -1
- package/src/prompts/system/system-prompt.md +81 -79
- package/src/prompts/tools/python.md +0 -1
|
@@ -56,7 +56,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
56
56
|
cancelled: false,
|
|
57
57
|
...(await sink.dump()),
|
|
58
58
|
};
|
|
59
|
-
} catch (err) {
|
|
59
|
+
} catch (err: unknown) {
|
|
60
60
|
// Exception covers NonZeroExitError, AbortError, TimeoutError
|
|
61
61
|
if (err instanceof Exception) {
|
|
62
62
|
if (err.aborted) {
|
|
@@ -432,6 +432,13 @@ export class ModelRegistry {
|
|
|
432
432
|
return this.models.find((m) => m.provider === provider && m.id === modelId);
|
|
433
433
|
}
|
|
434
434
|
|
|
435
|
+
/**
|
|
436
|
+
* Get the base URL associated with a provider, if any model defines one.
|
|
437
|
+
*/
|
|
438
|
+
getProviderBaseUrl(provider: string): string | undefined {
|
|
439
|
+
return this.models.find((m) => m.provider === provider && m.baseUrl)?.baseUrl;
|
|
440
|
+
}
|
|
441
|
+
|
|
435
442
|
/**
|
|
436
443
|
* Get API key for a model.
|
|
437
444
|
*/
|
|
@@ -27,6 +27,8 @@ export interface PythonExecutorOptions {
|
|
|
27
27
|
reset?: boolean;
|
|
28
28
|
/** Use shared gateway across pi instances (default: true) */
|
|
29
29
|
useSharedGateway?: boolean;
|
|
30
|
+
/** Session file path for accessing task outputs */
|
|
31
|
+
sessionFile?: string;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
export interface PythonKernelExecutor {
|
|
@@ -79,6 +81,7 @@ export async function warmPythonEnvironment(
|
|
|
79
81
|
cwd: string,
|
|
80
82
|
sessionId?: string,
|
|
81
83
|
useSharedGateway?: boolean,
|
|
84
|
+
sessionFile?: string,
|
|
82
85
|
): Promise<{ ok: boolean; reason?: string; docs: PreludeHelper[] }> {
|
|
83
86
|
try {
|
|
84
87
|
await ensureKernelAvailable(cwd);
|
|
@@ -97,6 +100,7 @@ export async function warmPythonEnvironment(
|
|
|
97
100
|
cwd,
|
|
98
101
|
async (kernel) => kernel.introspectPrelude(),
|
|
99
102
|
useSharedGateway,
|
|
103
|
+
sessionFile,
|
|
100
104
|
);
|
|
101
105
|
cachedPreludeDocs = docs;
|
|
102
106
|
return { ok: true, docs };
|
|
@@ -115,8 +119,14 @@ export function resetPreludeDocsCache(): void {
|
|
|
115
119
|
cachedPreludeDocs = null;
|
|
116
120
|
}
|
|
117
121
|
|
|
118
|
-
async function createKernelSession(
|
|
119
|
-
|
|
122
|
+
async function createKernelSession(
|
|
123
|
+
sessionId: string,
|
|
124
|
+
cwd: string,
|
|
125
|
+
useSharedGateway?: boolean,
|
|
126
|
+
sessionFile?: string,
|
|
127
|
+
): Promise<KernelSession> {
|
|
128
|
+
const env = sessionFile ? { OMP_SESSION_FILE: sessionFile } : undefined;
|
|
129
|
+
const kernel = await PythonKernel.start({ cwd, useSharedGateway, env });
|
|
120
130
|
const session: KernelSession = {
|
|
121
131
|
id: sessionId,
|
|
122
132
|
kernel,
|
|
@@ -137,7 +147,12 @@ async function createKernelSession(sessionId: string, cwd: string, useSharedGate
|
|
|
137
147
|
return session;
|
|
138
148
|
}
|
|
139
149
|
|
|
140
|
-
async function restartKernelSession(
|
|
150
|
+
async function restartKernelSession(
|
|
151
|
+
session: KernelSession,
|
|
152
|
+
cwd: string,
|
|
153
|
+
useSharedGateway?: boolean,
|
|
154
|
+
sessionFile?: string,
|
|
155
|
+
): Promise<void> {
|
|
141
156
|
session.restartCount += 1;
|
|
142
157
|
if (session.restartCount > 1) {
|
|
143
158
|
throw new Error("Python kernel restarted too many times in this session");
|
|
@@ -147,7 +162,8 @@ async function restartKernelSession(session: KernelSession, cwd: string, useShar
|
|
|
147
162
|
} catch (err) {
|
|
148
163
|
logger.warn("Failed to shutdown crashed kernel", { error: err instanceof Error ? err.message : String(err) });
|
|
149
164
|
}
|
|
150
|
-
const
|
|
165
|
+
const env = sessionFile ? { OMP_SESSION_FILE: sessionFile } : undefined;
|
|
166
|
+
const kernel = await PythonKernel.start({ cwd, useSharedGateway, env });
|
|
151
167
|
session.kernel = kernel;
|
|
152
168
|
session.dead = false;
|
|
153
169
|
session.lastUsedAt = Date.now();
|
|
@@ -170,17 +186,18 @@ async function withKernelSession<T>(
|
|
|
170
186
|
cwd: string,
|
|
171
187
|
handler: (kernel: PythonKernel) => Promise<T>,
|
|
172
188
|
useSharedGateway?: boolean,
|
|
189
|
+
sessionFile?: string,
|
|
173
190
|
): Promise<T> {
|
|
174
191
|
let session = kernelSessions.get(sessionId);
|
|
175
192
|
if (!session) {
|
|
176
|
-
session = await createKernelSession(sessionId, cwd, useSharedGateway);
|
|
193
|
+
session = await createKernelSession(sessionId, cwd, useSharedGateway, sessionFile);
|
|
177
194
|
kernelSessions.set(sessionId, session);
|
|
178
195
|
}
|
|
179
196
|
|
|
180
197
|
const run = async (): Promise<T> => {
|
|
181
198
|
session!.lastUsedAt = Date.now();
|
|
182
199
|
if (session!.dead || !session!.kernel.isAlive()) {
|
|
183
|
-
await restartKernelSession(session!, cwd, useSharedGateway);
|
|
200
|
+
await restartKernelSession(session!, cwd, useSharedGateway, sessionFile);
|
|
184
201
|
}
|
|
185
202
|
try {
|
|
186
203
|
const result = await handler(session!.kernel);
|
|
@@ -190,7 +207,7 @@ async function withKernelSession<T>(
|
|
|
190
207
|
if (!session!.dead && session!.kernel.isAlive()) {
|
|
191
208
|
throw err;
|
|
192
209
|
}
|
|
193
|
-
await restartKernelSession(session!, cwd, useSharedGateway);
|
|
210
|
+
await restartKernelSession(session!, cwd, useSharedGateway, sessionFile);
|
|
194
211
|
const result = await handler(session!.kernel);
|
|
195
212
|
session!.restartCount = 0;
|
|
196
213
|
return result;
|
|
@@ -273,8 +290,11 @@ export async function executePython(code: string, options?: PythonExecutorOption
|
|
|
273
290
|
|
|
274
291
|
const kernelMode = options?.kernelMode ?? "session";
|
|
275
292
|
const useSharedGateway = options?.useSharedGateway;
|
|
293
|
+
const sessionFile = options?.sessionFile;
|
|
294
|
+
|
|
276
295
|
if (kernelMode === "per-call") {
|
|
277
|
-
const
|
|
296
|
+
const env = sessionFile ? { OMP_SESSION_FILE: sessionFile } : undefined;
|
|
297
|
+
const kernel = await PythonKernel.start({ cwd, useSharedGateway, env });
|
|
278
298
|
try {
|
|
279
299
|
return await executeWithKernel(kernel, code, options);
|
|
280
300
|
} finally {
|
|
@@ -294,5 +314,6 @@ export async function executePython(code: string, options?: PythonExecutorOption
|
|
|
294
314
|
cwd,
|
|
295
315
|
async (kernel) => executeWithKernel(kernel, code, options),
|
|
296
316
|
useSharedGateway,
|
|
317
|
+
sessionFile,
|
|
297
318
|
);
|
|
298
319
|
}
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "node:fs";
|
|
14
14
|
import { createServer } from "node:net";
|
|
15
15
|
import { delimiter, join } from "node:path";
|
|
16
|
-
import { logger } from "@oh-my-pi/pi-utils";
|
|
16
|
+
import { logger, postmortem } from "@oh-my-pi/pi-utils";
|
|
17
17
|
import type { Subprocess } from "bun";
|
|
18
18
|
import { getAgentDir } from "../config";
|
|
19
19
|
import { getShellConfig, killProcessTree } from "../utils/shell";
|
|
@@ -161,6 +161,58 @@ let localGatewayUrl: string | null = null;
|
|
|
161
161
|
let idleShutdownTimer: ReturnType<typeof setTimeout> | null = null;
|
|
162
162
|
let isCoordinatorInitialized = false;
|
|
163
163
|
let localClientFile: string | null = null;
|
|
164
|
+
let postmortemRegistered = false;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Register cleanup handler for process exit. Called lazily on first gateway acquisition.
|
|
168
|
+
* Ensures the gateway process we spawned is killed when omp exits, preventing orphaned processes.
|
|
169
|
+
*/
|
|
170
|
+
function ensurePostmortemCleanup(): void {
|
|
171
|
+
if (postmortemRegistered) return;
|
|
172
|
+
postmortemRegistered = true;
|
|
173
|
+
|
|
174
|
+
postmortem.register("shared-gateway", async () => {
|
|
175
|
+
cancelIdleShutdown();
|
|
176
|
+
|
|
177
|
+
// Clean up our client file first so refcount is accurate
|
|
178
|
+
if (localClientFile) {
|
|
179
|
+
try {
|
|
180
|
+
unlinkSync(localClientFile);
|
|
181
|
+
} catch {
|
|
182
|
+
// Ignore cleanup errors
|
|
183
|
+
}
|
|
184
|
+
localClientFile = null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// If we spawned the gateway, kill it only if no other clients remain
|
|
188
|
+
if (localGatewayProcess) {
|
|
189
|
+
const clients = pruneStaleClientInfos(listClientInfos());
|
|
190
|
+
const remainingRefs = clients.reduce((sum, c) => sum + c.info.refCount, 0);
|
|
191
|
+
|
|
192
|
+
if (remainingRefs === 0) {
|
|
193
|
+
logger.debug("Cleaning up shared gateway on process exit", { pid: localGatewayProcess.pid });
|
|
194
|
+
try {
|
|
195
|
+
await killProcessTree(localGatewayProcess.pid);
|
|
196
|
+
} catch (err) {
|
|
197
|
+
logger.warn("Failed to kill shared gateway on exit", {
|
|
198
|
+
error: err instanceof Error ? err.message : String(err),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
clearGatewayInfo();
|
|
202
|
+
} else {
|
|
203
|
+
logger.debug("Leaving shared gateway running for other clients", {
|
|
204
|
+
pid: localGatewayProcess.pid,
|
|
205
|
+
remainingRefs,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
localGatewayProcess = null;
|
|
210
|
+
localGatewayUrl = null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
isCoordinatorInitialized = false;
|
|
214
|
+
});
|
|
215
|
+
}
|
|
164
216
|
|
|
165
217
|
function filterEnv(env: Record<string, string | undefined>): Record<string, string | undefined> {
|
|
166
218
|
const filtered: Record<string, string | undefined> = {};
|
|
@@ -665,6 +717,8 @@ export async function acquireSharedGateway(cwd: string): Promise<AcquireResult |
|
|
|
665
717
|
return null;
|
|
666
718
|
}
|
|
667
719
|
|
|
720
|
+
ensurePostmortemCleanup();
|
|
721
|
+
|
|
668
722
|
try {
|
|
669
723
|
return await withGatewayLock(async () => {
|
|
670
724
|
const existingInfo = readGatewayInfo();
|
|
@@ -24,14 +24,6 @@ if "__omp_prelude_loaded__" not in globals():
|
|
|
24
24
|
_emit_status("pwd", path=str(p))
|
|
25
25
|
return p
|
|
26
26
|
|
|
27
|
-
@_category("Navigation")
|
|
28
|
-
def cd(path: str | Path) -> Path:
|
|
29
|
-
"""Change directory."""
|
|
30
|
-
p = Path(path).expanduser().resolve()
|
|
31
|
-
os.chdir(p)
|
|
32
|
-
_emit_status("cd", path=str(p))
|
|
33
|
-
return p
|
|
34
|
-
|
|
35
27
|
@_category("Shell")
|
|
36
28
|
def env(key: str | None = None, value: str | None = None):
|
|
37
29
|
"""Get/set environment variables."""
|
|
@@ -914,6 +906,207 @@ if "__omp_prelude_loaded__" not in globals():
|
|
|
914
906
|
_emit_status("git_has_changes", has_changes=has_changes)
|
|
915
907
|
return has_changes
|
|
916
908
|
|
|
909
|
+
@_category("Agent")
|
|
910
|
+
def output(
|
|
911
|
+
*ids: str,
|
|
912
|
+
format: str = "raw",
|
|
913
|
+
query: str | None = None,
|
|
914
|
+
offset: int | None = None,
|
|
915
|
+
limit: int | None = None,
|
|
916
|
+
) -> str | dict | list[dict]:
|
|
917
|
+
"""Read task/agent output by ID. Returns text or JSON depending on format.
|
|
918
|
+
|
|
919
|
+
Args:
|
|
920
|
+
*ids: Output IDs to read (e.g., 'explore_0', 'reviewer_1')
|
|
921
|
+
format: 'raw' (default), 'json' (dict with metadata), 'stripped' (no ANSI)
|
|
922
|
+
query: jq-like query for JSON outputs (e.g., '.endpoints[0].file')
|
|
923
|
+
offset: Line number to start reading from (1-indexed)
|
|
924
|
+
limit: Maximum number of lines to read
|
|
925
|
+
|
|
926
|
+
Returns:
|
|
927
|
+
Single ID: str (format='raw'/'stripped') or dict (format='json')
|
|
928
|
+
Multiple IDs: list of dict with 'id' and 'content'/'data' keys
|
|
929
|
+
|
|
930
|
+
Examples:
|
|
931
|
+
output('explore_0') # Read as raw text
|
|
932
|
+
output('reviewer_0', format='json') # Read with metadata
|
|
933
|
+
output('explore_0', query='.files[0]') # Extract JSON field
|
|
934
|
+
output('explore_0', offset=10, limit=20) # Lines 10-29
|
|
935
|
+
output('explore_0', 'reviewer_1') # Read multiple outputs
|
|
936
|
+
"""
|
|
937
|
+
session_file = os.environ.get("OMP_SESSION_FILE")
|
|
938
|
+
if not session_file:
|
|
939
|
+
_emit_status("output", error="No session file available")
|
|
940
|
+
raise RuntimeError("No session - output artifacts unavailable")
|
|
941
|
+
|
|
942
|
+
artifacts_dir = session_file.rsplit(".", 1)[0] # Strip .jsonl extension
|
|
943
|
+
if not Path(artifacts_dir).exists():
|
|
944
|
+
_emit_status("output", error="Artifacts directory not found", path=artifacts_dir)
|
|
945
|
+
raise RuntimeError(f"No artifacts directory found: {artifacts_dir}")
|
|
946
|
+
|
|
947
|
+
if not ids:
|
|
948
|
+
_emit_status("output", error="No IDs provided")
|
|
949
|
+
raise ValueError("At least one output ID is required")
|
|
950
|
+
|
|
951
|
+
if query and (offset is not None or limit is not None):
|
|
952
|
+
_emit_status("output", error="query cannot be combined with offset/limit")
|
|
953
|
+
raise ValueError("query cannot be combined with offset/limit")
|
|
954
|
+
|
|
955
|
+
results: list[dict] = []
|
|
956
|
+
not_found: list[str] = []
|
|
957
|
+
|
|
958
|
+
for output_id in ids:
|
|
959
|
+
output_path = Path(artifacts_dir) / f"{output_id}.md"
|
|
960
|
+
if not output_path.exists():
|
|
961
|
+
not_found.append(output_id)
|
|
962
|
+
continue
|
|
963
|
+
|
|
964
|
+
raw_content = output_path.read_text(encoding="utf-8")
|
|
965
|
+
raw_lines = raw_content.splitlines()
|
|
966
|
+
total_lines = len(raw_lines)
|
|
967
|
+
|
|
968
|
+
selected_content = raw_content
|
|
969
|
+
range_info: dict | None = None
|
|
970
|
+
|
|
971
|
+
# Handle query
|
|
972
|
+
if query:
|
|
973
|
+
try:
|
|
974
|
+
json_value = json.loads(raw_content)
|
|
975
|
+
except json.JSONDecodeError as e:
|
|
976
|
+
_emit_status("output", id=output_id, error=f"Not valid JSON: {e}")
|
|
977
|
+
raise ValueError(f"Output {output_id} is not valid JSON: {e}")
|
|
978
|
+
|
|
979
|
+
# Apply jq-like query
|
|
980
|
+
result_value = _apply_query(json_value, query)
|
|
981
|
+
try:
|
|
982
|
+
selected_content = json.dumps(result_value, indent=2) if result_value is not None else "null"
|
|
983
|
+
except (TypeError, ValueError):
|
|
984
|
+
selected_content = str(result_value)
|
|
985
|
+
|
|
986
|
+
# Handle offset/limit
|
|
987
|
+
elif offset is not None or limit is not None:
|
|
988
|
+
start_line = max(1, offset or 1)
|
|
989
|
+
if start_line > total_lines:
|
|
990
|
+
_emit_status("output", id=output_id, error=f"Offset {start_line} beyond end ({total_lines} lines)")
|
|
991
|
+
raise ValueError(f"Offset {start_line} is beyond end of output ({total_lines} lines) for {output_id}")
|
|
992
|
+
|
|
993
|
+
effective_limit = limit if limit is not None else total_lines - start_line + 1
|
|
994
|
+
end_line = min(total_lines, start_line + effective_limit - 1)
|
|
995
|
+
selected_lines = raw_lines[start_line - 1 : end_line]
|
|
996
|
+
selected_content = "\n".join(selected_lines)
|
|
997
|
+
range_info = {"start_line": start_line, "end_line": end_line, "total_lines": total_lines}
|
|
998
|
+
|
|
999
|
+
# Strip ANSI codes if requested
|
|
1000
|
+
if format == "stripped":
|
|
1001
|
+
import re
|
|
1002
|
+
selected_content = re.sub(r"\x1b\[[0-9;]*m", "", selected_content)
|
|
1003
|
+
|
|
1004
|
+
# Build result
|
|
1005
|
+
if format == "json":
|
|
1006
|
+
result_data = {
|
|
1007
|
+
"id": output_id,
|
|
1008
|
+
"path": str(output_path),
|
|
1009
|
+
"line_count": total_lines if not query else len(selected_content.splitlines()),
|
|
1010
|
+
"char_count": len(raw_content) if not query else len(selected_content),
|
|
1011
|
+
"content": selected_content,
|
|
1012
|
+
}
|
|
1013
|
+
if range_info:
|
|
1014
|
+
result_data["range"] = range_info
|
|
1015
|
+
if query:
|
|
1016
|
+
result_data["query"] = query
|
|
1017
|
+
results.append(result_data)
|
|
1018
|
+
else:
|
|
1019
|
+
results.append({"id": output_id, "content": selected_content})
|
|
1020
|
+
|
|
1021
|
+
# Handle not found
|
|
1022
|
+
if not_found:
|
|
1023
|
+
available = sorted(
|
|
1024
|
+
[f.stem for f in Path(artifacts_dir).glob("*.md")]
|
|
1025
|
+
)
|
|
1026
|
+
error_msg = f"Output not found: {', '.join(not_found)}"
|
|
1027
|
+
if available:
|
|
1028
|
+
error_msg += f"\n\nAvailable outputs: {', '.join(available[:20])}"
|
|
1029
|
+
if len(available) > 20:
|
|
1030
|
+
error_msg += f" (and {len(available) - 20} more)"
|
|
1031
|
+
_emit_status("output", not_found=not_found, available_count=len(available))
|
|
1032
|
+
raise FileNotFoundError(error_msg)
|
|
1033
|
+
|
|
1034
|
+
# Return format
|
|
1035
|
+
if len(ids) == 1:
|
|
1036
|
+
if format == "json":
|
|
1037
|
+
_emit_status("output", id=ids[0], chars=results[0]["char_count"])
|
|
1038
|
+
return results[0]
|
|
1039
|
+
_emit_status("output", id=ids[0], chars=len(results[0]["content"]))
|
|
1040
|
+
return results[0]["content"]
|
|
1041
|
+
|
|
1042
|
+
# Multiple IDs
|
|
1043
|
+
if format == "json":
|
|
1044
|
+
total_chars = sum(r["char_count"] for r in results)
|
|
1045
|
+
_emit_status("output", count=len(results), total_chars=total_chars)
|
|
1046
|
+
return results
|
|
1047
|
+
|
|
1048
|
+
combined_output: list[dict] = []
|
|
1049
|
+
for r in results:
|
|
1050
|
+
combined_output.append({"id": r["id"], "content": r["content"]})
|
|
1051
|
+
total_chars = sum(len(r["content"]) for r in combined_output)
|
|
1052
|
+
_emit_status("output", count=len(combined_output), total_chars=total_chars)
|
|
1053
|
+
return combined_output
|
|
1054
|
+
|
|
1055
|
+
def _apply_query(data: any, query: str) -> any:
|
|
1056
|
+
"""Apply jq-like query to data. Supports .key, [index], and chaining."""
|
|
1057
|
+
if not query:
|
|
1058
|
+
return data
|
|
1059
|
+
|
|
1060
|
+
query = query.strip()
|
|
1061
|
+
if query.startswith("."):
|
|
1062
|
+
query = query[1:]
|
|
1063
|
+
if not query:
|
|
1064
|
+
return data
|
|
1065
|
+
|
|
1066
|
+
# Parse query into tokens
|
|
1067
|
+
tokens = []
|
|
1068
|
+
current_token = ""
|
|
1069
|
+
i = 0
|
|
1070
|
+
while i < len(query):
|
|
1071
|
+
ch = query[i]
|
|
1072
|
+
if ch == ".":
|
|
1073
|
+
if current_token:
|
|
1074
|
+
tokens.append(("key", current_token))
|
|
1075
|
+
current_token = ""
|
|
1076
|
+
elif ch == "[":
|
|
1077
|
+
if current_token:
|
|
1078
|
+
tokens.append(("key", current_token))
|
|
1079
|
+
current_token = ""
|
|
1080
|
+
# Find matching ]
|
|
1081
|
+
j = i + 1
|
|
1082
|
+
while j < len(query) and query[j] != "]":
|
|
1083
|
+
j += 1
|
|
1084
|
+
bracket_content = query[i+1:j]
|
|
1085
|
+
if bracket_content.startswith('"') and bracket_content.endswith('"'):
|
|
1086
|
+
tokens.append(("key", bracket_content[1:-1]))
|
|
1087
|
+
else:
|
|
1088
|
+
tokens.append(("index", int(bracket_content)))
|
|
1089
|
+
i = j
|
|
1090
|
+
else:
|
|
1091
|
+
current_token += ch
|
|
1092
|
+
i += 1
|
|
1093
|
+
if current_token:
|
|
1094
|
+
tokens.append(("key", current_token))
|
|
1095
|
+
|
|
1096
|
+
# Apply tokens
|
|
1097
|
+
current = data
|
|
1098
|
+
for token_type, value in tokens:
|
|
1099
|
+
if token_type == "index":
|
|
1100
|
+
if not isinstance(current, list) or value >= len(current):
|
|
1101
|
+
return None
|
|
1102
|
+
current = current[value]
|
|
1103
|
+
elif token_type == "key":
|
|
1104
|
+
if not isinstance(current, dict) or value not in current:
|
|
1105
|
+
return None
|
|
1106
|
+
current = current[value]
|
|
1107
|
+
|
|
1108
|
+
return current
|
|
1109
|
+
|
|
917
1110
|
def __omp_prelude_docs__() -> list[dict[str, str]]:
|
|
918
1111
|
"""Return prelude helper docs for templating. Discovers functions by _omp_category attribute."""
|
|
919
1112
|
helpers: list[dict[str, str]] = []
|
package/src/core/tools/find.ts
CHANGED
|
@@ -87,10 +87,22 @@ export async function runFd(fdPath: string, args: string[], signal?: AbortSignal
|
|
|
87
87
|
throw err;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
let exitError: unknown;
|
|
91
|
+
try {
|
|
92
|
+
await child.exited;
|
|
93
|
+
} catch (err) {
|
|
94
|
+
exitError = err;
|
|
95
|
+
if (err instanceof ptree.Exception && err.aborted) {
|
|
96
|
+
throw new Error("Operation aborted");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const exitCode = child.exitCode ?? (exitError instanceof ptree.Exception ? exitError.exitCode : null);
|
|
101
|
+
|
|
90
102
|
return {
|
|
91
103
|
stdout,
|
|
92
104
|
stderr: child.peekStderr(),
|
|
93
|
-
exitCode
|
|
105
|
+
exitCode,
|
|
94
106
|
};
|
|
95
107
|
}
|
|
96
108
|
|
|
@@ -271,11 +283,12 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
271
283
|
const { stdout, stderr, exitCode } = await runFd(fdPath, args, signal);
|
|
272
284
|
const output = stdout.trim();
|
|
273
285
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
277
|
-
|
|
286
|
+
// fd exit codes: 0 = found files, 1 = no matches, other = error
|
|
287
|
+
// Treat exit code 1 with no output as "no files found"
|
|
278
288
|
if (!output) {
|
|
289
|
+
if (exitCode !== 0 && exitCode !== 1) {
|
|
290
|
+
throw new Error(stderr.trim() || `fd failed (exit ${exitCode})`);
|
|
291
|
+
}
|
|
279
292
|
return {
|
|
280
293
|
content: [{ type: "text", text: "No files found matching pattern" }],
|
|
281
294
|
details: { scopePath, fileCount: 0, files: [], truncated: false },
|
|
@@ -243,6 +243,14 @@ function getLspServerForFile(config: LspConfig, filePath: string): [string, Serv
|
|
|
243
243
|
|
|
244
244
|
const FILE_SEARCH_MAX_DEPTH = 5;
|
|
245
245
|
const IGNORED_DIRS = new Set(["node_modules", "target", "dist", "build", ".git"]);
|
|
246
|
+
const DIAGNOSTIC_MESSAGE_LIMIT = 50;
|
|
247
|
+
|
|
248
|
+
function limitDiagnosticMessages(messages: string[]): string[] {
|
|
249
|
+
if (messages.length <= DIAGNOSTIC_MESSAGE_LIMIT) {
|
|
250
|
+
return messages;
|
|
251
|
+
}
|
|
252
|
+
return messages.slice(0, DIAGNOSTIC_MESSAGE_LIMIT);
|
|
253
|
+
}
|
|
246
254
|
|
|
247
255
|
function findFileByExtensions(baseDir: string, extensions: string[], maxDepth: number): string | null {
|
|
248
256
|
const normalized = extensions.map((ext) => ext.toLowerCase());
|
|
@@ -563,12 +571,13 @@ async function getDiagnosticsForFile(
|
|
|
563
571
|
}
|
|
564
572
|
|
|
565
573
|
const formatted = uniqueDiagnostics.map((d) => formatDiagnostic(d, relPath));
|
|
574
|
+
const limited = limitDiagnosticMessages(formatted);
|
|
566
575
|
const summary = formatDiagnosticsSummary(uniqueDiagnostics);
|
|
567
576
|
const hasErrors = uniqueDiagnostics.some((d) => d.severity === 1);
|
|
568
577
|
|
|
569
578
|
return {
|
|
570
579
|
server: serverNames.join(", "),
|
|
571
|
-
messages:
|
|
580
|
+
messages: limited,
|
|
572
581
|
summary,
|
|
573
582
|
errored: hasErrors,
|
|
574
583
|
};
|
|
@@ -788,16 +797,18 @@ function mergeDiagnostics(
|
|
|
788
797
|
|
|
789
798
|
let summary = options.enableDiagnostics ? "no issues" : "OK";
|
|
790
799
|
let errored = false;
|
|
800
|
+
let limitedMessages = messages;
|
|
791
801
|
if (messages.length > 0) {
|
|
792
802
|
const summaryInfo = summarizeDiagnosticMessages(messages);
|
|
793
803
|
summary = summaryInfo.summary;
|
|
794
804
|
errored = summaryInfo.errored;
|
|
805
|
+
limitedMessages = limitDiagnosticMessages(messages);
|
|
795
806
|
}
|
|
796
807
|
const formatter = hasFormatter ? (formatted ? FileFormatResult.FORMATTED : FileFormatResult.UNCHANGED) : undefined;
|
|
797
808
|
|
|
798
809
|
return {
|
|
799
810
|
server: servers.size > 0 ? Array.from(servers).join(", ") : undefined,
|
|
800
|
-
messages,
|
|
811
|
+
messages: limitedMessages,
|
|
801
812
|
summary,
|
|
802
813
|
errored,
|
|
803
814
|
formatter,
|
package/src/core/tools/python.ts
CHANGED
|
@@ -269,6 +269,7 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
|
|
|
269
269
|
sessionId,
|
|
270
270
|
kernelMode: this.session.settings?.getPythonKernelMode?.() ?? "session",
|
|
271
271
|
useSharedGateway: this.session.settings?.getPythonSharedGateway?.() ?? true,
|
|
272
|
+
sessionFile: sessionFile ?? undefined,
|
|
272
273
|
};
|
|
273
274
|
|
|
274
275
|
for (let i = 0; i < cells.length; i++) {
|
package/src/core/tools/read.ts
CHANGED
|
@@ -223,11 +223,11 @@ async function listCandidateFiles(
|
|
|
223
223
|
const { stdout, stderr, exitCode } = await runFd(fdPath, args, signal);
|
|
224
224
|
const output = stdout.trim();
|
|
225
225
|
|
|
226
|
-
if (exitCode !== 0 && !output) {
|
|
227
|
-
return { files: [], truncated: false, error: stderr.trim() || `fd exited with code ${exitCode ?? -1}` };
|
|
228
|
-
}
|
|
229
|
-
|
|
230
226
|
if (!output) {
|
|
227
|
+
// fd exit codes: 0 = found, 1 = no matches, other = error
|
|
228
|
+
if (exitCode !== 0 && exitCode !== 1) {
|
|
229
|
+
return { files: [], truncated: false, error: stderr.trim() || `fd failed (exit ${exitCode})` };
|
|
230
|
+
}
|
|
231
231
|
return { files: [], truncated: false };
|
|
232
232
|
}
|
|
233
233
|
|