@oh-my-pi/pi-coding-agent 15.10.11 → 15.11.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 +103 -2
- package/dist/cli.js +5790 -5731
- package/dist/types/async/index.d.ts +0 -1
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/gallery-fixtures/types.d.ts +5 -0
- package/dist/types/cli-commands.d.ts +12 -0
- package/dist/types/commands/launch.d.ts +4 -0
- package/dist/types/config/api-key-resolver.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +6 -1
- package/dist/types/config/model-registry.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +18 -0
- package/dist/types/config/settings-schema.d.ts +85 -34
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/edit/hashline/noop-loop-guard.d.ts +72 -0
- package/dist/types/eval/py/executor.d.ts +5 -0
- package/dist/types/eval/py/kernel.d.ts +6 -1
- package/dist/types/eval/py/runtime.d.ts +9 -0
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
- package/dist/types/extensibility/extensions/runner.d.ts +3 -2
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/extensibility/shared-events.d.ts +2 -2
- package/dist/types/internal-urls/history-protocol.d.ts +14 -0
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/types.d.ts +1 -1
- package/dist/types/irc/bus.d.ts +66 -0
- package/dist/types/memory-backend/index.d.ts +1 -0
- package/dist/types/memory-backend/runtime.d.ts +4 -0
- package/dist/types/memory-backend/types.d.ts +66 -1
- package/dist/types/modes/components/agent-hub.d.ts +30 -0
- package/dist/types/modes/components/compaction-summary-message.d.ts +10 -4
- package/dist/types/modes/components/custom-editor.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/components/ttsr-notification.d.ts +5 -1
- package/dist/types/modes/components/welcome.d.ts +3 -9
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
- package/dist/types/modes/index.d.ts +3 -3
- package/dist/types/modes/interactive-mode.d.ts +10 -4
- package/dist/types/modes/oauth-manual-input.d.ts +7 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +39 -2
- package/dist/types/modes/rpc/rpc-mode.d.ts +31 -2
- package/dist/types/modes/rpc/rpc-subagents.d.ts +24 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +75 -1
- package/dist/types/modes/setup-wizard/index.d.ts +5 -1
- package/dist/types/modes/setup-wizard/lazy.d.ts +2 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +5 -2
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
- package/dist/types/registry/agent-lifecycle.d.ts +51 -0
- package/dist/types/registry/agent-registry.d.ts +16 -5
- package/dist/types/secrets/index.d.ts +1 -1
- package/dist/types/secrets/obfuscator.d.ts +8 -2
- package/dist/types/session/agent-session.d.ts +49 -32
- package/dist/types/session/messages.d.ts +2 -4
- package/dist/types/session/session-history-format.d.ts +12 -0
- package/dist/types/session/session-manager.d.ts +21 -3
- package/dist/types/session/streaming-output.d.ts +46 -0
- package/dist/types/slash-commands/acp-builtins.d.ts +16 -0
- package/dist/types/slash-commands/builtin-registry.d.ts +1 -0
- package/dist/types/slash-commands/types.d.ts +1 -1
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +12 -2
- package/dist/types/task/index.d.ts +13 -6
- package/dist/types/task/output-manager.d.ts +0 -7
- package/dist/types/task/repair-args.d.ts +8 -7
- package/dist/types/task/types.d.ts +63 -51
- package/dist/types/thinking.d.ts +4 -0
- package/dist/types/tiny/title-client.d.ts +11 -0
- package/dist/types/tiny/title-protocol.d.ts +1 -0
- package/dist/types/tools/browser/tab-worker.d.ts +3 -1
- package/dist/types/tools/find.d.ts +0 -11
- package/dist/types/tools/grouped-file-output.d.ts +0 -49
- package/dist/types/tools/index.d.ts +7 -3
- package/dist/types/tools/irc.d.ts +76 -38
- package/dist/types/tools/job.d.ts +7 -1
- package/dist/types/utils/git.d.ts +15 -2
- package/dist/types/utils/title-generator.d.ts +3 -2
- package/examples/extensions/with-deps/package.json +1 -0
- package/package.json +11 -10
- package/scripts/bundle-dist.ts +28 -19
- package/src/async/index.ts +0 -1
- package/src/auto-thinking/classifier.ts +1 -0
- package/src/cli/args.ts +3 -0
- package/src/cli/gallery-cli.ts +1 -1
- package/src/cli/gallery-fixtures/agentic.ts +230 -115
- package/src/cli/gallery-fixtures/types.ts +5 -0
- package/src/cli-commands.ts +29 -0
- package/src/cli.ts +28 -15
- package/src/commands/launch.ts +4 -0
- package/src/commit/agentic/tools/analyze-file.ts +38 -19
- package/src/commit/model-selection.ts +3 -2
- package/src/config/api-key-resolver.ts +8 -6
- package/src/config/keybindings.ts +6 -1
- package/src/config/model-registry.ts +97 -30
- package/src/config/model-resolver.ts +60 -0
- package/src/config/settings-schema.ts +99 -55
- package/src/config/settings.ts +68 -3
- package/src/edit/hashline/execute.ts +39 -2
- package/src/edit/hashline/noop-loop-guard.ts +99 -0
- package/src/eval/__tests__/agent-bridge.test.ts +5 -3
- package/src/eval/agent-bridge.ts +3 -16
- package/src/eval/completion-bridge.ts +1 -0
- package/src/eval/js/shared/prelude.txt +1 -1
- package/src/eval/py/executor.ts +29 -7
- package/src/eval/py/index.ts +6 -1
- package/src/eval/py/kernel.ts +31 -11
- package/src/eval/py/prelude.py +5 -6
- package/src/eval/py/runtime.ts +37 -0
- package/src/exec/bash-executor.ts +82 -3
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +38 -13
- package/src/extensibility/custom-tools/types.ts +2 -2
- package/src/extensibility/extensions/get-commands-handler.ts +2 -1
- package/src/extensibility/extensions/runner.ts +6 -1
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/shared-events.ts +2 -2
- package/src/hindsight/bank.ts +17 -2
- package/src/internal-urls/docs-index.generated.ts +11 -11
- package/src/internal-urls/history-protocol.ts +113 -0
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/router.ts +3 -1
- package/src/internal-urls/types.ts +1 -1
- package/src/irc/bus.ts +292 -0
- package/src/main.ts +26 -66
- package/src/memories/index.ts +2 -0
- package/src/memory-backend/index.ts +1 -0
- package/src/memory-backend/local-backend.ts +9 -0
- package/src/memory-backend/off-backend.ts +9 -0
- package/src/memory-backend/runtime.ts +66 -0
- package/src/memory-backend/types.ts +81 -1
- package/src/mnemopi/backend.ts +151 -4
- package/src/modes/acp/acp-agent.ts +119 -11
- package/src/modes/components/{session-observer-overlay.ts → agent-hub.ts} +586 -367
- package/src/modes/components/assistant-message.ts +19 -21
- package/src/modes/components/compaction-summary-message.ts +68 -32
- package/src/modes/components/custom-editor.ts +10 -0
- package/src/modes/components/footer.ts +3 -1
- package/src/modes/components/status-line/component.ts +118 -34
- package/src/modes/components/tool-execution.ts +31 -1
- package/src/modes/components/ttsr-notification.ts +72 -30
- package/src/modes/components/welcome.ts +9 -33
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/event-controller.ts +65 -0
- package/src/modes/controllers/extension-ui-controller.ts +8 -8
- package/src/modes/controllers/input-controller.ts +19 -2
- package/src/modes/controllers/mcp-command-controller.ts +38 -3
- package/src/modes/controllers/selector-controller.ts +21 -17
- package/src/modes/index.ts +3 -21
- package/src/modes/interactive-mode.ts +47 -22
- package/src/modes/oauth-manual-input.ts +30 -3
- package/src/modes/rpc/rpc-client.ts +154 -3
- package/src/modes/rpc/rpc-mode.ts +97 -12
- package/src/modes/rpc/rpc-subagents.ts +265 -0
- package/src/modes/rpc/rpc-types.ts +81 -1
- package/src/modes/setup-wizard/index.ts +12 -2
- package/src/modes/setup-wizard/lazy.ts +16 -0
- package/src/modes/theme/theme.ts +18 -5
- package/src/modes/types.ts +5 -5
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +51 -49
- package/src/prompts/system/irc-incoming.md +3 -4
- package/src/prompts/system/orchestrate-notice.md +2 -2
- package/src/prompts/system/subagent-system-prompt.md +0 -5
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/system/workflow-notice.md +2 -2
- package/src/prompts/tools/eval.md +3 -3
- package/src/prompts/tools/irc.md +29 -19
- package/src/prompts/tools/read.md +2 -2
- package/src/prompts/tools/task-summary.md +5 -16
- package/src/prompts/tools/task.md +38 -29
- package/src/registry/agent-lifecycle.ts +218 -0
- package/src/registry/agent-registry.ts +16 -5
- package/src/sdk.ts +37 -10
- package/src/secrets/index.ts +8 -1
- package/src/secrets/obfuscator.ts +39 -18
- package/src/session/agent-session.ts +422 -291
- package/src/session/messages.ts +11 -78
- package/src/session/session-history-format.ts +246 -0
- package/src/session/session-manager.ts +59 -5
- package/src/session/streaming-output.ts +226 -10
- package/src/slash-commands/acp-builtins.ts +24 -0
- package/src/slash-commands/builtin-registry.ts +20 -0
- package/src/slash-commands/types.ts +1 -1
- package/src/system-prompt.ts +14 -0
- package/src/task/executor.ts +851 -461
- package/src/task/index.ts +721 -796
- package/src/task/output-manager.ts +0 -11
- package/src/task/render.ts +148 -63
- package/src/task/repair-args.ts +21 -9
- package/src/task/types.ts +82 -66
- package/src/thinking.ts +7 -0
- package/src/tiny/title-client.ts +34 -5
- package/src/tiny/title-protocol.ts +1 -1
- package/src/tiny/worker.ts +6 -4
- package/src/tools/ask.ts +4 -2
- package/src/tools/bash.ts +61 -10
- package/src/tools/browser/tab-worker.ts +26 -7
- package/src/tools/browser.ts +28 -1
- package/src/tools/find.ts +2 -27
- package/src/tools/grouped-file-output.ts +1 -118
- package/src/tools/image-gen.ts +11 -4
- package/src/tools/index.ts +17 -13
- package/src/tools/inspect-image.ts +1 -0
- package/src/tools/irc.ts +596 -171
- package/src/tools/job.ts +41 -7
- package/src/tools/read.ts +57 -1
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resolve.ts +4 -1
- package/src/utils/commit-message-generator.ts +1 -0
- package/src/utils/git.ts +267 -13
- package/src/utils/title-generator.ts +24 -5
- package/dist/types/async/support.d.ts +0 -2
- package/dist/types/modes/components/session-observer-overlay.d.ts +0 -11
- package/dist/types/task/simple-mode.d.ts +0 -8
- package/src/async/support.ts +0 -5
- package/src/task/simple-mode.ts +0 -27
package/src/config/settings.ts
CHANGED
|
@@ -64,6 +64,8 @@ export interface SettingsOptions {
|
|
|
64
64
|
inMemory?: boolean;
|
|
65
65
|
/** Initial overrides */
|
|
66
66
|
overrides?: Partial<Record<SettingPath, unknown>>;
|
|
67
|
+
/** Extra config.yml-style overlays loaded after global/project settings */
|
|
68
|
+
configFiles?: string[];
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -116,10 +118,12 @@ type PathScopedStringArrayEntry = {
|
|
|
116
118
|
providers?: unknown;
|
|
117
119
|
};
|
|
118
120
|
|
|
121
|
+
function expandTilde(p: string): string {
|
|
122
|
+
return p === "~" ? os.homedir() : p.startsWith("~/") ? path.join(os.homedir(), p.slice(2)) : p;
|
|
123
|
+
}
|
|
124
|
+
|
|
119
125
|
function normalizePathPrefix(prefix: string): string {
|
|
120
|
-
|
|
121
|
-
prefix === "~" ? os.homedir() : prefix.startsWith("~/") ? path.join(os.homedir(), prefix.slice(2)) : prefix;
|
|
122
|
-
return path.resolve(expanded);
|
|
126
|
+
return path.resolve(expandTilde(prefix));
|
|
123
127
|
}
|
|
124
128
|
|
|
125
129
|
function pathMatchesPrefix(cwd: string, prefix: string): boolean {
|
|
@@ -193,10 +197,13 @@ export class Settings {
|
|
|
193
197
|
#agentDir: string;
|
|
194
198
|
#storage: AgentStorage | null = null;
|
|
195
199
|
|
|
200
|
+
#configFiles: string[] = [];
|
|
196
201
|
/** Global settings from config.yml */
|
|
197
202
|
#global: RawSettings = {};
|
|
198
203
|
/** Project settings from .claude/settings.yml etc */
|
|
199
204
|
#project: RawSettings = {};
|
|
205
|
+
/** Extra config.yml-style overlays passed by CLI */
|
|
206
|
+
#configOverlay: RawSettings = {};
|
|
200
207
|
/** Runtime overrides (not persisted) */
|
|
201
208
|
#overrides: RawSettings = {};
|
|
202
209
|
/** Merged view (global + project + overrides) */
|
|
@@ -221,6 +228,7 @@ export class Settings {
|
|
|
221
228
|
this.#cwd = path.normalize(options.cwd ?? getProjectDir());
|
|
222
229
|
this.#agentDir = path.normalize(options.agentDir ?? getAgentDir());
|
|
223
230
|
this.#configPath = options.inMemory ? null : path.join(this.#agentDir, "config.yml");
|
|
231
|
+
this.#configFiles = options.configFiles?.map(file => path.resolve(this.#cwd, expandTilde(file))) ?? [];
|
|
224
232
|
this.#persist = !options.inMemory;
|
|
225
233
|
|
|
226
234
|
if (options.overrides) {
|
|
@@ -256,6 +264,7 @@ export class Settings {
|
|
|
256
264
|
},
|
|
257
265
|
error => {
|
|
258
266
|
globalInstance = null;
|
|
267
|
+
globalInstancePromise = null;
|
|
259
268
|
clearBoundSettingsMethods();
|
|
260
269
|
throw error;
|
|
261
270
|
},
|
|
@@ -303,6 +312,14 @@ export class Settings {
|
|
|
303
312
|
return resolved as SettingValue<P>;
|
|
304
313
|
}
|
|
305
314
|
|
|
315
|
+
/**
|
|
316
|
+
* Whether `path` has an explicitly configured value (global config, project
|
|
317
|
+
* config, or runtime override) rather than falling back to the schema default.
|
|
318
|
+
*/
|
|
319
|
+
isConfigured(path: SettingPath): boolean {
|
|
320
|
+
return getByPath(this.#merged, SETTING_PATH_SEGMENTS[path]) !== undefined;
|
|
321
|
+
}
|
|
322
|
+
|
|
306
323
|
/**
|
|
307
324
|
* Set a setting value (sync).
|
|
308
325
|
* Updates global settings and queues a background save.
|
|
@@ -386,6 +403,8 @@ export class Settings {
|
|
|
386
403
|
cloned.#storage = this.#storage;
|
|
387
404
|
cloned.#global = structuredClone(this.#global);
|
|
388
405
|
cloned.#project = this.#persist ? await cloned.#loadProjectSettings() : structuredClone(this.#project);
|
|
406
|
+
cloned.#configFiles = [...this.#configFiles];
|
|
407
|
+
cloned.#configOverlay = structuredClone(this.#configOverlay);
|
|
389
408
|
cloned.#overrides = structuredClone(this.#overrides);
|
|
390
409
|
cloned.#rebuildMerged();
|
|
391
410
|
cloned.#fireAllHooks();
|
|
@@ -557,6 +576,7 @@ export class Settings {
|
|
|
557
576
|
}
|
|
558
577
|
|
|
559
578
|
this.#project = await projectPromise;
|
|
579
|
+
this.#configOverlay = await this.#loadConfigOverlays();
|
|
560
580
|
|
|
561
581
|
// Build merged view (global → project → overrides; project wins over global)
|
|
562
582
|
this.#rebuildMerged();
|
|
@@ -594,6 +614,43 @@ export class Settings {
|
|
|
594
614
|
}
|
|
595
615
|
}
|
|
596
616
|
|
|
617
|
+
async #loadConfigOverlays(): Promise<RawSettings> {
|
|
618
|
+
let merged: RawSettings = {};
|
|
619
|
+
for (const filePath of this.#configFiles) {
|
|
620
|
+
merged = this.#deepMerge(merged, await this.#loadOverlayYaml(filePath));
|
|
621
|
+
}
|
|
622
|
+
return merged;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Strict loader for explicit `--config` overlays: unlike `#loadYaml`,
|
|
627
|
+
* missing or malformed files are hard errors so a typo'd path cannot
|
|
628
|
+
* silently fall back to the persistent settings.
|
|
629
|
+
*/
|
|
630
|
+
async #loadOverlayYaml(filePath: string): Promise<RawSettings> {
|
|
631
|
+
let content: string;
|
|
632
|
+
try {
|
|
633
|
+
content = await Bun.file(filePath).text();
|
|
634
|
+
} catch (error) {
|
|
635
|
+
throw new Error(
|
|
636
|
+
isEnoent(error)
|
|
637
|
+
? `Config overlay not found: ${filePath}`
|
|
638
|
+
: `Failed to read config overlay ${filePath}: ${String(error)}`,
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
let parsed: unknown;
|
|
642
|
+
try {
|
|
643
|
+
parsed = YAML.parse(content);
|
|
644
|
+
} catch (error) {
|
|
645
|
+
throw new Error(`Failed to parse config overlay ${filePath}: ${String(error)}`);
|
|
646
|
+
}
|
|
647
|
+
if (parsed === null || parsed === undefined) return {};
|
|
648
|
+
if (typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
649
|
+
throw new Error(`Config overlay must be a YAML mapping: ${filePath}`);
|
|
650
|
+
}
|
|
651
|
+
return this.#migrateRawSettings(parsed as RawSettings);
|
|
652
|
+
}
|
|
653
|
+
|
|
597
654
|
async #migrateFromLegacy(): Promise<void> {
|
|
598
655
|
if (!this.#configPath) return;
|
|
599
656
|
|
|
@@ -688,6 +745,13 @@ export class Settings {
|
|
|
688
745
|
delete isolationObj.enabled;
|
|
689
746
|
}
|
|
690
747
|
|
|
748
|
+
// task.simple: removed — the task tool no longer accepts a per-call
|
|
749
|
+
// schema (workflows drive structured output via eval agent()) and the
|
|
750
|
+
// batch/context shape is gated by task.batch instead.
|
|
751
|
+
if (taskObj && "simple" in taskObj) {
|
|
752
|
+
delete taskObj.simple;
|
|
753
|
+
}
|
|
754
|
+
|
|
691
755
|
// task.isolation.mode: legacy values from before the pi-iso PAL refactor.
|
|
692
756
|
// `worktree` was git worktree → now lives under `rcopy`. `fuse-overlay`
|
|
693
757
|
// and `fuse-projfs` are now the platform-named `overlayfs` / `projfs`
|
|
@@ -898,6 +962,7 @@ export class Settings {
|
|
|
898
962
|
|
|
899
963
|
#rebuildMerged(): void {
|
|
900
964
|
this.#merged = this.#deepMerge(this.#deepMerge({}, this.#global), this.#project);
|
|
965
|
+
this.#merged = this.#deepMerge(this.#merged, this.#configOverlay);
|
|
901
966
|
this.#merged = this.#deepMerge(this.#merged, this.#overrides);
|
|
902
967
|
this.#resolvedCache.clear();
|
|
903
968
|
}
|
|
@@ -23,11 +23,13 @@ import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
|
23
23
|
import type { FileDiagnosticsResult, WritethroughCallback, WritethroughDeferredHandle } from "../../lsp";
|
|
24
24
|
import type { ToolSession } from "../../tools";
|
|
25
25
|
import { outputMeta } from "../../tools/output-meta";
|
|
26
|
+
import { ToolError } from "../../tools/tool-errors";
|
|
26
27
|
import { generateDiffString } from "../diff";
|
|
27
28
|
import { getFileSnapshotStore } from "../file-snapshot-store";
|
|
28
29
|
import type { EditToolDetails, EditToolPerFileResult, LspBatchRequest } from "../renderer";
|
|
29
30
|
import { nativeBlockResolver } from "./block-resolver";
|
|
30
31
|
import { HashlineFilesystem } from "./filesystem";
|
|
32
|
+
import { hashPatchInput, NOOP_HARD_LIMIT, recordNoopEdit, resetNoopEdit } from "./noop-loop-guard";
|
|
31
33
|
import { type HashlineParams, hashlineEditParamsSchema } from "./params";
|
|
32
34
|
|
|
33
35
|
export interface ExecuteHashlineSingleOptions {
|
|
@@ -54,6 +56,24 @@ function noChangeDiagnostic(path: string): string {
|
|
|
54
56
|
);
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Escalated diagnostic surfaced once the same payload has no-op'd
|
|
61
|
+
* {@link NOOP_HARD_LIMIT} times in a row on the same canonical path. Thrown as
|
|
62
|
+
* a {@link ToolError} so the agent loop sees a tool *failure* — empirically
|
|
63
|
+
* far more effective at breaking a no-op edit loop than the soft hint alone
|
|
64
|
+
* (issue #2081 saw 182 byte-identical no-op results in 205 calls before the
|
|
65
|
+
* user aborted).
|
|
66
|
+
*/
|
|
67
|
+
function noChangeLoopDiagnostic(path: string, count: number): string {
|
|
68
|
+
return (
|
|
69
|
+
`STOP. Edits to ${path} have been a byte-identical no-op ${count} times in a row — ` +
|
|
70
|
+
`the patch body matches the file at the targeted lines and the soft hint did not break the cycle. ` +
|
|
71
|
+
`Cease re-issuing this payload. Either the intended change is already on disk (move on), ` +
|
|
72
|
+
`or your anchor is wrong (re-read the file with \`read\` to observe the current line numbers and ` +
|
|
73
|
+
`tag, then author a different edit). This exact payload will keep being rejected until it changes.`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
57
77
|
function assertUniqueCanonicalPaths(prepared: readonly PreparedSection[]): void {
|
|
58
78
|
const seen = new Map<string, string>();
|
|
59
79
|
for (const entry of prepared) {
|
|
@@ -156,13 +176,19 @@ export async function executeHashlineSingle(
|
|
|
156
176
|
const patcher = new Patcher({ fs, snapshots, blockResolver: nativeBlockResolver });
|
|
157
177
|
|
|
158
178
|
// Single-section fast path: prepare, commit, render.
|
|
179
|
+
const inputHash = hashPatchInput(options.input);
|
|
159
180
|
if (patch.sections.length === 1) {
|
|
160
181
|
fs.setBatchRequest(narrowBatchRequest(options.batchRequest, true));
|
|
161
182
|
const prepared = await patcher.prepare(patch.sections[0]);
|
|
162
183
|
const sectionResult = await patcher.commit(prepared);
|
|
163
184
|
if (sectionResult.op === "noop") {
|
|
185
|
+
const { count, escalate } = recordNoopEdit(options.session, sectionResult.canonicalPath, inputHash);
|
|
186
|
+
if (escalate) {
|
|
187
|
+
throw new ToolError(noChangeLoopDiagnostic(sectionResult.path, count));
|
|
188
|
+
}
|
|
164
189
|
return renderSection(sectionResult, undefined).toolResult;
|
|
165
190
|
}
|
|
191
|
+
resetNoopEdit(options.session, sectionResult.canonicalPath);
|
|
166
192
|
return renderSection(sectionResult, fs.consumeDiagnostics(sectionResult.path)).toolResult;
|
|
167
193
|
}
|
|
168
194
|
|
|
@@ -172,7 +198,12 @@ export async function executeHashlineSingle(
|
|
|
172
198
|
for (const section of patch.sections) prepared.push(await patcher.prepare(section));
|
|
173
199
|
assertUniqueCanonicalPaths(prepared);
|
|
174
200
|
for (const entry of prepared) {
|
|
175
|
-
if (entry.isNoop)
|
|
201
|
+
if (entry.isNoop) {
|
|
202
|
+
const { count, escalate } = recordNoopEdit(options.session, entry.canonicalPath, inputHash);
|
|
203
|
+
throw escalate
|
|
204
|
+
? new ToolError(noChangeLoopDiagnostic(entry.section.path, count))
|
|
205
|
+
: new ToolError(noChangeDiagnostic(entry.section.path));
|
|
206
|
+
}
|
|
176
207
|
}
|
|
177
208
|
// Then commit each one, narrowing the LSP batch flush flag to the final
|
|
178
209
|
// section only. A no-op apply mid-batch is treated as a hard failure —
|
|
@@ -182,7 +213,13 @@ export async function executeHashlineSingle(
|
|
|
182
213
|
const isLast = i === prepared.length - 1;
|
|
183
214
|
fs.setBatchRequest(narrowBatchRequest(options.batchRequest, isLast));
|
|
184
215
|
const sectionResult = await patcher.commit(prepared[i]);
|
|
185
|
-
if (sectionResult.op === "noop")
|
|
216
|
+
if (sectionResult.op === "noop") {
|
|
217
|
+
const { count, escalate } = recordNoopEdit(options.session, sectionResult.canonicalPath, inputHash);
|
|
218
|
+
throw escalate
|
|
219
|
+
? new ToolError(noChangeLoopDiagnostic(sectionResult.path, count))
|
|
220
|
+
: new ToolError(noChangeDiagnostic(sectionResult.path));
|
|
221
|
+
}
|
|
222
|
+
resetNoopEdit(options.session, sectionResult.canonicalPath);
|
|
186
223
|
rendered.push(renderSection(sectionResult, fs.consumeDiagnostics(sectionResult.path)));
|
|
187
224
|
}
|
|
188
225
|
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-session guard against subagents looping on byte-identical no-op edits.
|
|
3
|
+
*
|
|
4
|
+
* A hashline patch can apply cleanly yet produce no change when the body rows
|
|
5
|
+
* are already byte-identical to the targeted lines. {@link executeHashlineSingle}
|
|
6
|
+
* surfaces a soft hint ("re-read the file before issuing another edit"), but in
|
|
7
|
+
* the wild some models ignore the hint and keep re-issuing the same bytes
|
|
8
|
+
* (issue #2081 captured 182 such repeats in 205 calls before the user aborted).
|
|
9
|
+
*
|
|
10
|
+
* This module tracks consecutive byte-identical no-op edits per canonical file
|
|
11
|
+
* path within a single session. Once the same payload no-ops {@link NOOP_HARD_LIMIT}
|
|
12
|
+
* times in a row the caller is expected to escalate from a soft text result to
|
|
13
|
+
* a thrown {@link ToolError} so the agent loop sees a tool *failure* — empirically
|
|
14
|
+
* far more effective at breaking the cycle than the soft hint alone.
|
|
15
|
+
*
|
|
16
|
+
* A successful (non-noop) commit for a path resets that path's counter; a
|
|
17
|
+
* different payload on the same path also resets it because the body hash
|
|
18
|
+
* changed, which is a sign of model progress and deserves another soft hint.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
interface NoopLoopEntry {
|
|
22
|
+
/** Hash of the most recent input that no-op'd on this canonical path. */
|
|
23
|
+
hash: string;
|
|
24
|
+
/** Consecutive no-op count for the same `hash` on this path. */
|
|
25
|
+
count: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Cross-session-safe state slot held on the `ToolSession`. */
|
|
29
|
+
export interface NoopLoopGuard {
|
|
30
|
+
entries: Map<string, NoopLoopEntry>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* After this many consecutive byte-identical no-op edits on the same path,
|
|
35
|
+
* {@link recordNoopEdit} returns `escalate: true`. Picked deliberately small
|
|
36
|
+
* so the soft hint still fires once or twice before we escalate — the model
|
|
37
|
+
* deserves a chance to recover, but a tight bound is what actually breaks
|
|
38
|
+
* loops in practice.
|
|
39
|
+
*/
|
|
40
|
+
export const NOOP_HARD_LIMIT = 3;
|
|
41
|
+
|
|
42
|
+
interface NoopLoopGuardOwner {
|
|
43
|
+
noopLoopGuard?: NoopLoopGuard;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Lazily create the per-session guard, mirroring `getFileSnapshotStore`. */
|
|
47
|
+
export function getNoopLoopGuard(session: NoopLoopGuardOwner): NoopLoopGuard {
|
|
48
|
+
if (!session.noopLoopGuard) session.noopLoopGuard = { entries: new Map() };
|
|
49
|
+
return session.noopLoopGuard;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Result of recording one no-op against the guard. */
|
|
53
|
+
export interface NoopRecordResult {
|
|
54
|
+
/** Consecutive identical no-op count, including the current one. */
|
|
55
|
+
count: number;
|
|
56
|
+
/** True once `count >= NOOP_HARD_LIMIT` and the caller MUST escalate. */
|
|
57
|
+
escalate: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Record a no-op edit for `canonicalPath` keyed by `inputHash` (a stable hash
|
|
62
|
+
* of the raw patch input bytes). Returns the running consecutive-no-op count
|
|
63
|
+
* and whether the caller should escalate from a soft text result to a thrown
|
|
64
|
+
* error.
|
|
65
|
+
*
|
|
66
|
+
* `inputHash` is intentionally derived from the raw model-authored bytes
|
|
67
|
+
* rather than from file content: when the model emits a different payload
|
|
68
|
+
* (even whitespace-only) that's progress and earns a fresh soft hint, but
|
|
69
|
+
* re-issuing the same bytes after being warned is what we want to break.
|
|
70
|
+
*/
|
|
71
|
+
export function recordNoopEdit(
|
|
72
|
+
session: NoopLoopGuardOwner,
|
|
73
|
+
canonicalPath: string,
|
|
74
|
+
inputHash: string,
|
|
75
|
+
): NoopRecordResult {
|
|
76
|
+
const guard = getNoopLoopGuard(session);
|
|
77
|
+
const prev = guard.entries.get(canonicalPath);
|
|
78
|
+
const count = prev && prev.hash === inputHash ? prev.count + 1 : 1;
|
|
79
|
+
guard.entries.set(canonicalPath, { hash: inputHash, count });
|
|
80
|
+
return { count, escalate: count >= NOOP_HARD_LIMIT };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Clear the no-op counter for `canonicalPath`. Call after a non-noop commit
|
|
85
|
+
* for the same path so a future no-op starts fresh from the soft hint.
|
|
86
|
+
*/
|
|
87
|
+
export function resetNoopEdit(session: NoopLoopGuardOwner, canonicalPath: string): void {
|
|
88
|
+
const guard = session.noopLoopGuard;
|
|
89
|
+
if (!guard) return;
|
|
90
|
+
guard.entries.delete(canonicalPath);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Stable hash of the raw patch input. Bun's `Bun.hash` is xxHash64 — fast,
|
|
95
|
+
* non-cryptographic, more than adequate for "is this the same payload?".
|
|
96
|
+
*/
|
|
97
|
+
export function hashPatchInput(input: string): string {
|
|
98
|
+
return Bun.hash(input).toString(16);
|
|
99
|
+
}
|
|
@@ -99,6 +99,7 @@ function singleResult(options: ExecutorOptions, overrides: Partial<SingleResult>
|
|
|
99
99
|
truncated: false,
|
|
100
100
|
durationMs: 1,
|
|
101
101
|
tokens: 0,
|
|
102
|
+
requests: 0,
|
|
102
103
|
...overrides,
|
|
103
104
|
};
|
|
104
105
|
}
|
|
@@ -178,7 +179,7 @@ describe("runEvalAgent", () => {
|
|
|
178
179
|
expect(runSpy).not.toHaveBeenCalled();
|
|
179
180
|
});
|
|
180
181
|
|
|
181
|
-
it("passes
|
|
182
|
+
it("passes parent execution options and only sets outputSchema when schema is supplied", async () => {
|
|
182
183
|
mockAgents();
|
|
183
184
|
const runSpy = vi.spyOn(taskExecutor, "runSubprocess").mockImplementation(async options => singleResult(options));
|
|
184
185
|
const abortController = new AbortController();
|
|
@@ -186,7 +187,7 @@ describe("runEvalAgent", () => {
|
|
|
186
187
|
const session = makeSession({ depth: 2, activeModel: "p/current", modelString: "p/fallback" });
|
|
187
188
|
|
|
188
189
|
await runEvalAgent(
|
|
189
|
-
{ prompt: " hello ",
|
|
190
|
+
{ prompt: " hello ", label: "My Agent", model: "p/override", schema },
|
|
190
191
|
{ session, signal: abortController.signal },
|
|
191
192
|
);
|
|
192
193
|
await runEvalAgent({ prompt: "plain" }, { session });
|
|
@@ -199,7 +200,6 @@ describe("runEvalAgent", () => {
|
|
|
199
200
|
expect(firstOptions.parentActiveModelPattern).toBe("p/current");
|
|
200
201
|
expect(firstOptions.outputSchema).toBe(schema);
|
|
201
202
|
expect(firstOptions.assignment).toBe("hello");
|
|
202
|
-
expect(firstOptions.context).toBe("context");
|
|
203
203
|
expect(firstOptions.description).toBe("My Agent");
|
|
204
204
|
expect(firstOptions.modelOverride).toEqual(["p/override"]);
|
|
205
205
|
expect(secondOptions.outputSchema).toBeUndefined();
|
|
@@ -542,6 +542,7 @@ describe("agent() through eval runtimes", () => {
|
|
|
542
542
|
recentOutput: [],
|
|
543
543
|
toolCount: 0,
|
|
544
544
|
tokens: 0,
|
|
545
|
+
requests: 0,
|
|
545
546
|
cost: 0,
|
|
546
547
|
durationMs: 0,
|
|
547
548
|
...overrides,
|
|
@@ -674,6 +675,7 @@ describe("agent() through eval runtimes", () => {
|
|
|
674
675
|
recentOutput: [],
|
|
675
676
|
toolCount: i,
|
|
676
677
|
tokens: 0,
|
|
678
|
+
requests: 0,
|
|
677
679
|
cost: 0,
|
|
678
680
|
durationMs: i * 10,
|
|
679
681
|
});
|
package/src/eval/agent-bridge.ts
CHANGED
|
@@ -34,7 +34,6 @@ const agentArgsSchema = z.object({
|
|
|
34
34
|
prompt: z.string().min(1, "prompt must be a non-empty string"),
|
|
35
35
|
agentType: z.string().min(1).optional(),
|
|
36
36
|
model: z.union([z.string().min(1), z.array(z.string().min(1)).min(1)]).optional(),
|
|
37
|
-
context: z.string().optional(),
|
|
38
37
|
label: z.string().optional(),
|
|
39
38
|
schema: z.unknown().optional(),
|
|
40
39
|
});
|
|
@@ -43,7 +42,6 @@ interface EvalAgentArgs {
|
|
|
43
42
|
prompt: string;
|
|
44
43
|
agentType?: string;
|
|
45
44
|
model?: string | string[];
|
|
46
|
-
context?: string;
|
|
47
45
|
label?: string;
|
|
48
46
|
schema?: unknown;
|
|
49
47
|
}
|
|
@@ -111,7 +109,7 @@ function assertNotPlanMode(session: ToolSession): void {
|
|
|
111
109
|
}
|
|
112
110
|
|
|
113
111
|
function renderSubagentPrompt(assignment: string): string {
|
|
114
|
-
return prompt.render(subagentUserPromptTemplate, { assignment: assignment.trim()
|
|
112
|
+
return prompt.render(subagentUserPromptTemplate, { assignment: assignment.trim() });
|
|
115
113
|
}
|
|
116
114
|
|
|
117
115
|
function trimToUndefined(value: string | undefined): string | undefined {
|
|
@@ -135,20 +133,12 @@ function getOutputManager(session: ToolSession): AgentOutputManager {
|
|
|
135
133
|
async function getArtifacts(session: ToolSession): Promise<{
|
|
136
134
|
sessionFile: string | null;
|
|
137
135
|
artifactsDir: string;
|
|
138
|
-
contextFile?: string;
|
|
139
136
|
}> {
|
|
140
137
|
const sessionFile = session.getSessionFile();
|
|
141
138
|
const sessionArtifactsDir = sessionFile ? sessionFile.slice(0, -6) : null;
|
|
142
139
|
const artifactsDir = sessionArtifactsDir ?? path.join(os.tmpdir(), `omp-eval-agent-${Snowflake.next()}`);
|
|
143
140
|
await fs.mkdir(artifactsDir, { recursive: true });
|
|
144
|
-
|
|
145
|
-
const shouldWriteConversationContext = session.settings.get("irc.enabled") !== true;
|
|
146
|
-
const compactContext = shouldWriteConversationContext ? session.getCompactContext?.() : undefined;
|
|
147
|
-
if (!compactContext) return { sessionFile, artifactsDir };
|
|
148
|
-
|
|
149
|
-
const contextFile = path.join(artifactsDir, "context.md");
|
|
150
|
-
await Bun.write(contextFile, compactContext);
|
|
151
|
-
return { sessionFile, artifactsDir, contextFile };
|
|
141
|
+
return { sessionFile, artifactsDir };
|
|
152
142
|
}
|
|
153
143
|
|
|
154
144
|
function emitProgressStatus(emitStatus: ((event: JsStatusEvent) => void) | undefined, progress: AgentProgress): void {
|
|
@@ -246,11 +236,10 @@ export async function runEvalAgent(args: unknown, options: EvalAgentBridgeOption
|
|
|
246
236
|
};
|
|
247
237
|
const parentArtifactManager = options.session.getArtifactManager?.() ?? undefined;
|
|
248
238
|
const mcpManager = options.session.mcpManager ?? MCPManager.instance();
|
|
249
|
-
const { sessionFile, artifactsDir
|
|
239
|
+
const { sessionFile, artifactsDir } = await getArtifacts(options.session);
|
|
250
240
|
const outputManager = getOutputManager(options.session);
|
|
251
241
|
const id = await outputManager.allocate(outputIdBase(parsed.label, agentName));
|
|
252
242
|
const assignment = parsed.prompt.trim();
|
|
253
|
-
const context = trimToUndefined(parsed.context);
|
|
254
243
|
// Suspend eval timeout accounting while the subagent owns control. The
|
|
255
244
|
// timeout clock restarts once the bridge returns to the cell runtime.
|
|
256
245
|
const result = await withBridgeTimeoutPause(options.emitStatus, () =>
|
|
@@ -259,7 +248,6 @@ export async function runEvalAgent(args: unknown, options: EvalAgentBridgeOption
|
|
|
259
248
|
agent: effectiveAgent,
|
|
260
249
|
task: renderSubagentPrompt(assignment),
|
|
261
250
|
assignment,
|
|
262
|
-
context,
|
|
263
251
|
description: trimToUndefined(parsed.label),
|
|
264
252
|
index: 0,
|
|
265
253
|
id,
|
|
@@ -271,7 +259,6 @@ export async function runEvalAgent(args: unknown, options: EvalAgentBridgeOption
|
|
|
271
259
|
sessionFile,
|
|
272
260
|
persistArtifacts: Boolean(sessionFile),
|
|
273
261
|
artifactsDir,
|
|
274
|
-
contextFile,
|
|
275
262
|
// Eval `agent()` subagents are short-lived programmatic helpers (data
|
|
276
263
|
// collection, structured output, parallel() fan-out). LSP server
|
|
277
264
|
// cold-start costs tens of seconds and is pure overhead here, so it is
|
|
@@ -163,6 +163,7 @@ export async function runEvalCompletion(
|
|
|
163
163
|
apiKey: registry.resolver(model.provider, {
|
|
164
164
|
sessionId: options.session.getSessionId?.() ?? undefined,
|
|
165
165
|
baseUrl: model.baseUrl,
|
|
166
|
+
modelId: model.id,
|
|
166
167
|
}),
|
|
167
168
|
signal: options.signal,
|
|
168
169
|
reasoning: reasoningForTier(tier, model),
|
|
@@ -65,7 +65,7 @@ if (!globalThis.__omp_js_prelude_loaded__) {
|
|
|
65
65
|
};
|
|
66
66
|
|
|
67
67
|
const agent = async (prompt, opts, ...rest) => {
|
|
68
|
-
const o = optionsArg("agent", opts, rest, "{ agentType, model,
|
|
68
|
+
const o = optionsArg("agent", opts, rest, "{ agentType, model, label, schema }");
|
|
69
69
|
const res = await globalThis.__omp_call_tool__("__agent__", { prompt, ...o });
|
|
70
70
|
const text = res && typeof res === "object" ? res.text : res;
|
|
71
71
|
return hasOwn(o, "schema") ? JSON.parse(text) : text;
|
package/src/eval/py/executor.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
1
2
|
import * as path from "node:path";
|
|
2
3
|
|
|
3
4
|
import { getProjectDir, logger } from "@oh-my-pi/pi-utils";
|
|
@@ -15,6 +16,7 @@ import {
|
|
|
15
16
|
type KernelRuntimeEnv,
|
|
16
17
|
PythonKernel,
|
|
17
18
|
} from "./kernel";
|
|
19
|
+
import { resolveExplicitPythonRuntime } from "./runtime";
|
|
18
20
|
import { ensurePyToolBridge, registerPyToolBridge } from "./tool-bridge";
|
|
19
21
|
|
|
20
22
|
export type PythonKernelMode = "session" | "per-call";
|
|
@@ -42,6 +44,11 @@ export interface PythonExecutorOptions {
|
|
|
42
44
|
kernelOwnerId?: string;
|
|
43
45
|
/** Kernel mode (session reuse vs per-call) */
|
|
44
46
|
kernelMode?: PythonKernelMode;
|
|
47
|
+
/**
|
|
48
|
+
* Explicit interpreter path (`python.interpreter` resolved from the
|
|
49
|
+
* session's settings). Skips automatic runtime discovery when set.
|
|
50
|
+
*/
|
|
51
|
+
interpreter?: string;
|
|
45
52
|
/** Restart the kernel before executing */
|
|
46
53
|
reset?: boolean;
|
|
47
54
|
/** Session file path for accessing task outputs */
|
|
@@ -116,9 +123,9 @@ export interface PythonResult {
|
|
|
116
123
|
// ---------------------------------------------------------------------------
|
|
117
124
|
// Session bookkeeping
|
|
118
125
|
//
|
|
119
|
-
// One PythonKernel subprocess per (session id, cwd) tuple. The
|
|
120
|
-
// process-global cwd/sys.path during execution, so cross-directory
|
|
121
|
-
// never share a live kernel. Multiple agent owners can still register against
|
|
126
|
+
// One PythonKernel subprocess per (session id, cwd, interpreter) tuple. The
|
|
127
|
+
// runner mutates process-global cwd/sys.path during execution, so cross-directory
|
|
128
|
+
// work must never share a live kernel. Multiple agent owners can still register against
|
|
122
129
|
// the same tuple; the kernel stays alive until the last owner detaches.
|
|
123
130
|
// ---------------------------------------------------------------------------
|
|
124
131
|
|
|
@@ -139,8 +146,19 @@ function normalizeSessionCwd(cwd: string): string {
|
|
|
139
146
|
return path.resolve(cwd);
|
|
140
147
|
}
|
|
141
148
|
|
|
142
|
-
function
|
|
143
|
-
|
|
149
|
+
function normalizeExplicitInterpreter(cwd: string, interpreter: string | undefined): string {
|
|
150
|
+
if (interpreter === undefined) return "";
|
|
151
|
+
const resolved = resolveExplicitPythonRuntime(interpreter, cwd, {}).pythonPath;
|
|
152
|
+
try {
|
|
153
|
+
return fs.realpathSync.native(resolved);
|
|
154
|
+
} catch {
|
|
155
|
+
return resolved;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function buildSessionKey(sessionId: string, cwd: string, interpreter: string | undefined): string {
|
|
160
|
+
const normalizedCwd = normalizeSessionCwd(cwd);
|
|
161
|
+
return `${sessionId}\0${normalizedCwd}\0${normalizeExplicitInterpreter(normalizedCwd, interpreter)}`;
|
|
144
162
|
}
|
|
145
163
|
|
|
146
164
|
// ---------------------------------------------------------------------------
|
|
@@ -326,6 +344,7 @@ async function startKernel(cwd: string, options: PythonExecutorOptions): Promise
|
|
|
326
344
|
env: buildKernelEnv(options),
|
|
327
345
|
signal: options.signal,
|
|
328
346
|
deadlineMs: options.deadlineMs,
|
|
347
|
+
interpreter: options.interpreter,
|
|
329
348
|
});
|
|
330
349
|
}
|
|
331
350
|
|
|
@@ -587,7 +606,10 @@ async function executeWithKernel(
|
|
|
587
606
|
}
|
|
588
607
|
|
|
589
608
|
async function ensureKernelAvailable(cwd: string, options: PythonExecutorOptions): Promise<void> {
|
|
590
|
-
const availability = await waitForPromiseWithCancellation(
|
|
609
|
+
const availability = await waitForPromiseWithCancellation(
|
|
610
|
+
checkPythonKernelAvailability(cwd, options.interpreter),
|
|
611
|
+
options,
|
|
612
|
+
);
|
|
591
613
|
if (!availability.ok) {
|
|
592
614
|
throw new Error(availability.reason ?? "Python kernel unavailable");
|
|
593
615
|
}
|
|
@@ -618,7 +640,7 @@ async function executePerCall(code: string, cwd: string, options: PythonExecutor
|
|
|
618
640
|
|
|
619
641
|
async function executeOnSession(code: string, cwd: string, options: PythonExecutorOptions): Promise<PythonResult> {
|
|
620
642
|
const sessionId = options.sessionId ?? `session:${cwd}`;
|
|
621
|
-
const sessionKey = buildSessionKey(sessionId, cwd);
|
|
643
|
+
const sessionKey = buildSessionKey(sessionId, cwd, options.interpreter);
|
|
622
644
|
if (options.bridge && !options.bridgeSessionId) {
|
|
623
645
|
options.bridgeSessionId = sessionId;
|
|
624
646
|
}
|
package/src/eval/py/index.ts
CHANGED
|
@@ -19,13 +19,17 @@ function readSetting<T>(session: ToolSession, key: string): T | undefined {
|
|
|
19
19
|
return settings?.get?.(key);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
function readInterpreterSetting(session: ToolSession): string | undefined {
|
|
23
|
+
return readSetting<string>(session, "python.interpreter")?.trim() || undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
22
26
|
export default {
|
|
23
27
|
id: "python",
|
|
24
28
|
label: "Python",
|
|
25
29
|
highlightLang: "python",
|
|
26
30
|
|
|
27
31
|
async isAvailable(session: ToolSession): Promise<boolean> {
|
|
28
|
-
const availability = await checkPythonKernelAvailability(session.cwd);
|
|
32
|
+
const availability = await checkPythonKernelAvailability(session.cwd, readInterpreterSetting(session));
|
|
29
33
|
return availability.ok;
|
|
30
34
|
},
|
|
31
35
|
|
|
@@ -37,6 +41,7 @@ export default {
|
|
|
37
41
|
signal: opts.signal,
|
|
38
42
|
sessionId: namespaceSessionId(opts.sessionId),
|
|
39
43
|
kernelMode,
|
|
44
|
+
interpreter: readInterpreterSetting(opts.session),
|
|
40
45
|
sessionFile: opts.sessionFile,
|
|
41
46
|
artifactsDir: opts.session.getArtifactsDir?.() ?? undefined,
|
|
42
47
|
localRoots: resolveEvalUrlRoots(opts.session),
|