@oh-my-pi/pi-coding-agent 17.1.2 → 17.1.4
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 +63 -0
- package/dist/cli.js +3806 -3798
- package/dist/types/cli/__tests__/auth-gateway-catalog.test.d.ts +1 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +8 -0
- package/dist/types/cli/update-cli.d.ts +41 -0
- package/dist/types/cli/usage-cli.d.ts +4 -2
- package/dist/types/commands/update.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +19 -0
- package/dist/types/config/settings-schema.d.ts +30 -7
- package/dist/types/cursor.d.ts +47 -1
- package/dist/types/eval/js/process-entry.d.ts +2 -1
- package/dist/types/eval/js/worker-core.d.ts +4 -1
- package/dist/types/extensibility/extensions/runner.d.ts +1 -0
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +4 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +4 -0
- package/dist/types/extensibility/legacy-pi-tui-shim.d.ts +15 -7
- package/dist/types/extensibility/shared-events.d.ts +2 -0
- package/dist/types/modes/components/assistant-message.d.ts +9 -0
- package/dist/types/modes/components/custom-editor.d.ts +12 -8
- package/dist/types/plan-mode/approved-plan.d.ts +3 -2
- package/dist/types/secrets/obfuscator.d.ts +7 -31
- package/dist/types/session/agent-session.d.ts +19 -4
- package/dist/types/session/prewalk.d.ts +2 -1
- package/dist/types/session/session-advisors.d.ts +8 -0
- package/dist/types/session/session-metadata.d.ts +29 -0
- package/dist/types/session/turn-recovery.d.ts +5 -5
- package/dist/types/stt/sherpa-runtime.d.ts +38 -0
- package/dist/types/thinking.d.ts +24 -0
- package/dist/types/tools/auto-generated-guard.d.ts +5 -2
- package/dist/types/tools/bash.d.ts +5 -4
- package/dist/types/tools/computer/exposure.d.ts +8 -0
- package/dist/types/tools/computer/supervisor.d.ts +1 -1
- package/dist/types/tools/computer/worker-entry.d.ts +1 -1
- package/dist/types/tools/computer/worker.d.ts +1 -1
- package/dist/types/tools/computer.d.ts +6 -0
- package/dist/types/tools/memory-render.d.ts +1 -4
- package/dist/types/tools/shell-tokenize.d.ts +14 -0
- package/dist/types/tools/todo.d.ts +7 -1
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +80 -10
- package/src/advisor/runtime.ts +27 -1
- package/src/cli/__tests__/auth-gateway-catalog.test.ts +111 -0
- package/src/cli/auth-gateway-cli.ts +63 -16
- package/src/cli/config-cli.ts +25 -7
- package/src/cli/update-cli.ts +229 -29
- package/src/cli/usage-cli.ts +144 -15
- package/src/cli.ts +21 -15
- package/src/commands/update.ts +6 -0
- package/src/config/__tests__/model-registry.test.ts +42 -7
- package/src/config/model-registry.ts +67 -4
- package/src/config/settings-schema.ts +39 -8
- package/src/config/settings.ts +24 -2
- package/src/cursor.ts +153 -0
- package/src/dap/session.ts +70 -11
- package/src/edit/hashline/filesystem.ts +1 -1
- package/src/edit/modes/patch.ts +1 -1
- package/src/eval/__tests__/js-context-manager.test.ts +9 -1
- package/src/eval/__tests__/process-entry-import.test.ts +111 -1
- package/src/eval/js/process-entry.ts +9 -5
- package/src/eval/js/worker-core.ts +6 -2
- package/src/exec/bash-executor.ts +4 -2
- package/src/extensibility/extensions/runner.ts +23 -8
- package/src/extensibility/legacy-pi-ai-shim.ts +9 -0
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +14 -2
- package/src/extensibility/legacy-pi-tui-shim.ts +33 -0
- package/src/extensibility/shared-events.ts +2 -0
- package/src/main.ts +22 -1
- package/src/modes/acp/acp-agent.ts +6 -0
- package/src/modes/components/assistant-message.ts +88 -11
- package/src/modes/components/chat-transcript-builder.ts +2 -0
- package/src/modes/components/custom-editor.test.ts +170 -0
- package/src/modes/components/custom-editor.ts +79 -29
- package/src/modes/components/settings-defs.ts +5 -1
- package/src/modes/controllers/event-controller.ts +96 -4
- package/src/modes/controllers/selector-controller.ts +14 -0
- package/src/modes/interactive-mode.ts +34 -8
- package/src/modes/utils/context-usage.ts +19 -2
- package/src/modes/utils/interactive-context-helpers.ts +1 -0
- package/src/plan-mode/approved-plan.ts +18 -10
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/system-prompt.md +10 -1
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/sdk.ts +4 -0
- package/src/secrets/obfuscator.ts +36 -126
- package/src/session/agent-session.ts +69 -60
- package/src/session/prewalk.ts +8 -3
- package/src/session/session-advisors.ts +67 -3
- package/src/session/session-metadata.ts +53 -0
- package/src/session/session-provider-boundary.ts +0 -1
- package/src/session/session-tools.ts +35 -1
- package/src/session/stream-guards.ts +1 -1
- package/src/session/turn-recovery.ts +36 -19
- package/src/slash-commands/builtin-registry.ts +49 -7
- package/src/stt/asr-worker.ts +2 -37
- package/src/stt/sherpa-runtime.ts +71 -0
- package/src/task/executor.ts +6 -5
- package/src/thinking.ts +39 -0
- package/src/tools/auto-generated-guard.ts +18 -5
- package/src/tools/bash.ts +43 -15
- package/src/tools/computer/exposure.ts +38 -0
- package/src/tools/computer/supervisor.ts +61 -13
- package/src/tools/computer/worker-entry.ts +28 -19
- package/src/tools/computer/worker.ts +3 -7
- package/src/tools/computer.ts +65 -10
- package/src/tools/gh-cache-invalidation.ts +2 -82
- package/src/tools/memory-render.ts +11 -2
- package/src/tools/render-utils.ts +8 -3
- package/src/tools/shell-tokenize.ts +83 -0
- package/src/tools/todo.ts +44 -17
- package/src/tools/write.ts +1 -1
|
@@ -7,7 +7,12 @@ import { APP_NAME, getProjectDir, setProjectDir } from "@oh-my-pi/pi-utils";
|
|
|
7
7
|
import { reset as resetCapabilities } from "../capability";
|
|
8
8
|
import { COLLAB_GUEST_ALLOWED_COMMANDS, CollabGuestLink } from "../collab/guest";
|
|
9
9
|
import { CollabHost } from "../collab/host";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
expandRoleAlias,
|
|
12
|
+
formatModelString,
|
|
13
|
+
getModelMatchPreferences,
|
|
14
|
+
resolveCliModel,
|
|
15
|
+
} from "../config/model-resolver";
|
|
11
16
|
import { applyProviderGlobalsFromSettings } from "../config/provider-globals";
|
|
12
17
|
import type { SettingPath, SettingValue } from "../config/settings";
|
|
13
18
|
import { settings } from "../config/settings";
|
|
@@ -37,6 +42,8 @@ import type { SessionOAuthAccountList } from "../session/agent-session-types";
|
|
|
37
42
|
import { COMPACT_MODES, parseCompactArgs } from "../session/compact-modes";
|
|
38
43
|
import { resolveResumableSession } from "../session/session-listing";
|
|
39
44
|
import { formatShakeSummary, type ShakeMode } from "../session/shake-types";
|
|
45
|
+
import type { ComputerTool } from "../tools/computer";
|
|
46
|
+
import { computerExposureMode } from "../tools/computer/exposure";
|
|
40
47
|
import { expandTilde, resolveToCwd } from "../tools/path-utils";
|
|
41
48
|
import { urlHyperlinkAlways } from "../tui";
|
|
42
49
|
import {
|
|
@@ -90,9 +97,41 @@ function formatFastModeStatus(session: AgentSession): string {
|
|
|
90
97
|
return session.isFastModeEnabled() ? "on" : "off";
|
|
91
98
|
}
|
|
92
99
|
|
|
93
|
-
/**
|
|
100
|
+
/** Detailed, session-effective `/computer status` diagnostics. */
|
|
94
101
|
function formatComputerUseStatus(session: AgentSession): string {
|
|
95
|
-
|
|
102
|
+
const enabled = session.settings.get("computer.enabled");
|
|
103
|
+
const active = session.getEnabledToolNames().includes("computer");
|
|
104
|
+
const model = session.model;
|
|
105
|
+
const modelName = model ? formatModelString(model) : "none";
|
|
106
|
+
const exposure = !enabled || !active ? "not exposed" : computerExposureMode(model);
|
|
107
|
+
const toolState = active ? "active" : enabled ? "unavailable" : "inactive";
|
|
108
|
+
const configured = {
|
|
109
|
+
backend: session.settings.get("computer.backend"),
|
|
110
|
+
display: session.settings.get("computer.display"),
|
|
111
|
+
maxWidth: session.settings.get("computer.maxWidth"),
|
|
112
|
+
maxHeight: session.settings.get("computer.maxHeight"),
|
|
113
|
+
};
|
|
114
|
+
const computerTool = session.getToolByName("computer") as Pick<ComputerTool, "effectiveConfiguration"> | undefined;
|
|
115
|
+
const effective = computerTool?.effectiveConfiguration ?? configured;
|
|
116
|
+
const configurationChanged =
|
|
117
|
+
effective.backend !== configured.backend ||
|
|
118
|
+
effective.display !== configured.display ||
|
|
119
|
+
effective.maxWidth !== configured.maxWidth ||
|
|
120
|
+
effective.maxHeight !== configured.maxHeight;
|
|
121
|
+
return [
|
|
122
|
+
`Computer use: ${enabled ? "enabled" : "disabled"}`,
|
|
123
|
+
`tool: ${toolState}`,
|
|
124
|
+
`backend: ${effective.backend}`,
|
|
125
|
+
`display: ${effective.display}`,
|
|
126
|
+
`capture: ${effective.maxWidth}×${effective.maxHeight}`,
|
|
127
|
+
...(configurationChanged
|
|
128
|
+
? [
|
|
129
|
+
`next-session settings: backend=${configured.backend}, display=${configured.display}, capture=${configured.maxWidth}×${configured.maxHeight}`,
|
|
130
|
+
]
|
|
131
|
+
: []),
|
|
132
|
+
`model: ${modelName}`,
|
|
133
|
+
`exposure: ${exposure}`,
|
|
134
|
+
].join(" · ");
|
|
96
135
|
}
|
|
97
136
|
|
|
98
137
|
/**
|
|
@@ -107,7 +146,9 @@ async function applyComputerUseToggle(session: AgentSession, enable: boolean): P
|
|
|
107
146
|
return "Computer use is unavailable in this session.";
|
|
108
147
|
}
|
|
109
148
|
session.settings.override("computer.enabled", enable);
|
|
110
|
-
return
|
|
149
|
+
return enable
|
|
150
|
+
? `Computer use enabled for this session. ${formatComputerUseStatus(session)}`
|
|
151
|
+
: "Computer use disabled for this session.";
|
|
111
152
|
}
|
|
112
153
|
|
|
113
154
|
const AUTOCOMPLETE_DETAIL_LIMIT = 48;
|
|
@@ -573,11 +614,12 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
573
614
|
{ name: "status", description: "Show computer use status" },
|
|
574
615
|
],
|
|
575
616
|
allowArgs: true,
|
|
576
|
-
getTuiAutocompleteDescription: runtime =>
|
|
617
|
+
getTuiAutocompleteDescription: runtime =>
|
|
618
|
+
`Computer: ${runtime.ctx.session.settings.get("computer.enabled") ? "on" : "off"}`,
|
|
577
619
|
handle: async (command, runtime) => {
|
|
578
620
|
const arg = command.args.trim().toLowerCase();
|
|
579
621
|
if (arg === "status") {
|
|
580
|
-
await runtime.output(
|
|
622
|
+
await runtime.output(formatComputerUseStatus(runtime.session));
|
|
581
623
|
return commandConsumed();
|
|
582
624
|
}
|
|
583
625
|
if (!arg || arg === "toggle" || arg === "on" || arg === "off") {
|
|
@@ -590,7 +632,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
590
632
|
handleTui: async (command, runtime) => {
|
|
591
633
|
const arg = command.args.trim().toLowerCase();
|
|
592
634
|
if (arg === "status") {
|
|
593
|
-
runtime.ctx.showStatus(
|
|
635
|
+
runtime.ctx.showStatus(formatComputerUseStatus(runtime.ctx.session));
|
|
594
636
|
runtime.ctx.editor.setText("");
|
|
595
637
|
return;
|
|
596
638
|
}
|
package/src/stt/asr-worker.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
type SttModelKey,
|
|
36
36
|
type TransformersSttModelSpec,
|
|
37
37
|
} from "./models";
|
|
38
|
+
import { loadSourceSherpaRuntime, type SherpaOfflineRecognizer, type SherpaRuntime } from "./sherpa-runtime";
|
|
38
39
|
|
|
39
40
|
const ASR_TASK = "automatic-speech-recognition";
|
|
40
41
|
const SHERPA_PACKAGE = "sherpa-onnx-node";
|
|
@@ -50,7 +51,6 @@ const HF_RESOLVE_BASE = "https://huggingface.co";
|
|
|
50
51
|
// Coalesce download progress so streaming a multi-hundred-MB model file doesn't
|
|
51
52
|
// flood the IPC channel with one event per chunk.
|
|
52
53
|
const PROGRESS_EMIT_BYTES = 4_000_000;
|
|
53
|
-
const sourceRequire = createRequire(import.meta.url);
|
|
54
54
|
|
|
55
55
|
const sttModelDevicePreference = resolveTinyModelDevicePreference();
|
|
56
56
|
const sttModelDtypeOverride = resolveTinyModelDtypeOverride();
|
|
@@ -90,41 +90,6 @@ interface TransformersRuntime {
|
|
|
90
90
|
) => Promise<AutomaticSpeechRecognitionPipeline>;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
/** Recognition result returned by `sherpa-onnx-node`'s offline recognizer. */
|
|
94
|
-
interface SherpaOfflineResult {
|
|
95
|
-
text?: string;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** A sherpa-onnx offline stream that accepts a single waveform before decoding. */
|
|
99
|
-
interface SherpaOfflineStream {
|
|
100
|
-
acceptWaveform(audio: { samples: Float32Array; sampleRate: number }): void;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
interface SherpaOfflineRecognizer {
|
|
104
|
-
createStream(): SherpaOfflineStream;
|
|
105
|
-
decodeAsync(stream: SherpaOfflineStream): Promise<SherpaOfflineResult>;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** Offline recognizer config passed to `sherpa-onnx-node` (transducer family). */
|
|
109
|
-
interface SherpaOfflineConfig {
|
|
110
|
-
modelConfig: {
|
|
111
|
-
transducer: { encoder: string; decoder: string; joiner: string };
|
|
112
|
-
tokens: string;
|
|
113
|
-
modelType: string;
|
|
114
|
-
numThreads: number;
|
|
115
|
-
provider: string;
|
|
116
|
-
debug: number;
|
|
117
|
-
};
|
|
118
|
-
decodingMethod: string;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** Subset of the native `sherpa-onnx-node` module surface we use. */
|
|
122
|
-
interface SherpaRuntime {
|
|
123
|
-
OfflineRecognizer: {
|
|
124
|
-
createAsync(config: SherpaOfflineConfig): Promise<SherpaOfflineRecognizer>;
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
|
|
128
93
|
/** A warm model plus the engine that loaded it; cached per tier key. */
|
|
129
94
|
type LoadedModel =
|
|
130
95
|
| { engine: "transformers"; pipeline: AutomaticSpeechRecognitionPipeline }
|
|
@@ -182,7 +147,7 @@ function getSherpaRuntimeDir(): string {
|
|
|
182
147
|
*/
|
|
183
148
|
function loadSherpaRuntime(transport: SttTransport, requestId: string, modelKey: SttModelKey): Promise<SherpaRuntime> {
|
|
184
149
|
return sherpaRuntime.load(async () => {
|
|
185
|
-
if (!isCompiledBinary()) return
|
|
150
|
+
if (!isCompiledBinary()) return loadSourceSherpaRuntime(import.meta.url);
|
|
186
151
|
const runtimeDir = await ensureRuntimeInstalled({
|
|
187
152
|
runtimeDir: getSherpaRuntimeDir(),
|
|
188
153
|
install: { dependencies: { [SHERPA_PACKAGE]: getSherpaVersionSpec() } },
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import { resolveRuntimeModule } from "@oh-my-pi/pi-utils";
|
|
4
|
+
|
|
5
|
+
const SHERPA_PACKAGE = "sherpa-onnx-node";
|
|
6
|
+
|
|
7
|
+
interface SherpaOfflineResult {
|
|
8
|
+
text?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface SherpaOfflineStream {
|
|
12
|
+
acceptWaveform(audio: { samples: Float32Array; sampleRate: number }): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface SherpaOfflineConfig {
|
|
16
|
+
modelConfig: {
|
|
17
|
+
transducer: { encoder: string; decoder: string; joiner: string };
|
|
18
|
+
tokens: string;
|
|
19
|
+
modelType: string;
|
|
20
|
+
numThreads: number;
|
|
21
|
+
provider: string;
|
|
22
|
+
debug: number;
|
|
23
|
+
};
|
|
24
|
+
decodingMethod: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A sherpa-onnx recognizer instance used by the STT worker. */
|
|
28
|
+
export interface SherpaOfflineRecognizer {
|
|
29
|
+
createStream(): SherpaOfflineStream;
|
|
30
|
+
decodeAsync(stream: SherpaOfflineStream): Promise<SherpaOfflineResult>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The native sherpa-onnx module surface used by the STT worker. */
|
|
34
|
+
export interface SherpaRuntime {
|
|
35
|
+
OfflineRecognizer: {
|
|
36
|
+
createAsync(config: SherpaOfflineConfig): Promise<SherpaOfflineRecognizer>;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Loads the nearest working source-workspace sherpa wrapper, including hoisted fallbacks. */
|
|
41
|
+
export function loadSourceSherpaRuntime(sourceUrl: string): SherpaRuntime {
|
|
42
|
+
const sourceRequire = createRequire(sourceUrl);
|
|
43
|
+
const nearestEntry = sourceRequire.resolve(SHERPA_PACKAGE);
|
|
44
|
+
try {
|
|
45
|
+
return createRequire(nearestEntry)(nearestEntry);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (!(error instanceof Error && error.message.startsWith("Could not find sherpa-onnx-node. Tried"))) {
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
const platform = os.platform();
|
|
51
|
+
const platformPackage = `sherpa-onnx-${platform === "win32" ? "win" : platform}-${os.arch()}`;
|
|
52
|
+
for (const nodeModules of sourceRequire.resolve.paths(SHERPA_PACKAGE) ?? []) {
|
|
53
|
+
if (!resolveRuntimeModule(nodeModules, platformPackage)) continue;
|
|
54
|
+
const entry = resolveRuntimeModule(nodeModules, SHERPA_PACKAGE);
|
|
55
|
+
if (!entry || entry === nearestEntry) continue;
|
|
56
|
+
try {
|
|
57
|
+
return createRequire(entry)(entry);
|
|
58
|
+
} catch (candidateError) {
|
|
59
|
+
if (
|
|
60
|
+
!(
|
|
61
|
+
candidateError instanceof Error &&
|
|
62
|
+
candidateError.message.startsWith("Could not find sherpa-onnx-node. Tried")
|
|
63
|
+
)
|
|
64
|
+
) {
|
|
65
|
+
throw candidateError;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
package/src/task/executor.ts
CHANGED
|
@@ -46,7 +46,7 @@ import type { AuthStorage } from "../session/auth-storage";
|
|
|
46
46
|
import { SKILL_PROMPT_MESSAGE_TYPE, USER_INTERRUPT_LABEL } from "../session/messages";
|
|
47
47
|
import { SessionManager } from "../session/session-manager";
|
|
48
48
|
import { truncateTail } from "../session/streaming-output";
|
|
49
|
-
import { type ConfiguredThinkingLevel, resolveTaskEffortLevel, type TaskEffort } from "../thinking";
|
|
49
|
+
import { type ConfiguredThinkingLevel, prewalkWouldBeNoop, resolveTaskEffortLevel, type TaskEffort } from "../thinking";
|
|
50
50
|
import type { ContextFileEntry, ToolSession } from "../tools";
|
|
51
51
|
import { resolveEvalBackends } from "../tools/eval-backends";
|
|
52
52
|
import { isIrcEnabled } from "../tools/hub";
|
|
@@ -2683,10 +2683,11 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2683
2683
|
pattern: prewalkPattern,
|
|
2684
2684
|
warning: resolvedPrewalk.warning,
|
|
2685
2685
|
});
|
|
2686
|
-
} else if (model
|
|
2687
|
-
//
|
|
2688
|
-
// the plan/checklist nudges — skip.
|
|
2689
|
-
|
|
2686
|
+
} else if (prewalkWouldBeNoop(model, effectiveThinkingLevel, target, resolvedPrewalk.thinkingLevel)) {
|
|
2687
|
+
// Same model AND same effective thinking level: switching would only
|
|
2688
|
+
// inject the plan/checklist nudges for no gain — skip. An effort-only
|
|
2689
|
+
// delta on the same model still arms (it is a real cheapening hand-off).
|
|
2690
|
+
logger.debug("Subagent prewalk target matches starting model and thinking level; skipping prewalk", {
|
|
2690
2691
|
agent: agent.name,
|
|
2691
2692
|
pattern: prewalkPattern,
|
|
2692
2693
|
});
|
package/src/thinking.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ResolvedThinkingLevel, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import { Effort, type Model, THINKING_EFFORTS } from "@oh-my-pi/pi-ai";
|
|
3
3
|
import { clampThinkingLevelForModel, getSupportedEfforts } from "@oh-my-pi/pi-catalog/model-thinking";
|
|
4
|
+
import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Metadata used to render thinking selector values in the coding-agent UI.
|
|
@@ -142,6 +143,44 @@ export function concreteThinkingLevel(level: ConfiguredThinkingLevel | undefined
|
|
|
142
143
|
return level === AUTO_THINKING ? undefined : level;
|
|
143
144
|
}
|
|
144
145
|
|
|
146
|
+
/**
|
|
147
|
+
* True when a prewalk hand-off from `current`/`currentLevel` to
|
|
148
|
+
* `target`/`targetLevel` would change nothing observable: same model id, same
|
|
149
|
+
* auto/fixed mode, and the same model-clamped effective effort. Prewalk arms and
|
|
150
|
+
* switches only when this is false.
|
|
151
|
+
*
|
|
152
|
+
* An effort-only delta on the same model id is a legitimate cheapening hand-off
|
|
153
|
+
* — on a reasoning model the effort is the bulk of the cost — so it is NOT a
|
|
154
|
+
* no-op and must still switch. A `targetLevel` of `undefined` means the prewalk
|
|
155
|
+
* pattern carried no explicit `:level` suffix (no effort change requested),
|
|
156
|
+
* which on the same model is a no-op.
|
|
157
|
+
*
|
|
158
|
+
* `auto` mode is compared before efforts: `auto` and a fixed selector that both
|
|
159
|
+
* resolve to `undefined` effort (e.g. `:inherit`) are NOT interchangeable —
|
|
160
|
+
* applying the fixed selector clears per-turn classification, so switching
|
|
161
|
+
* auto↔fixed is always a real change even when the clamped efforts match.
|
|
162
|
+
*
|
|
163
|
+
* Efforts are otherwise compared AFTER model clamping, so a target the model
|
|
164
|
+
* cannot honor (e.g. `:xhigh` on a model capped at `high`) — which
|
|
165
|
+
* `setThinkingLevel` would clamp straight back to the active effort — is
|
|
166
|
+
* recognized as a no-op instead of triggering an ephemeral reset and the
|
|
167
|
+
* plan/checklist nudges for nothing.
|
|
168
|
+
*/
|
|
169
|
+
export function prewalkWouldBeNoop(
|
|
170
|
+
current: Model | undefined,
|
|
171
|
+
currentLevel: ConfiguredThinkingLevel | undefined,
|
|
172
|
+
target: Model,
|
|
173
|
+
targetLevel: ConfiguredThinkingLevel | undefined,
|
|
174
|
+
): boolean {
|
|
175
|
+
if (!modelsAreEqual(current, target)) return false;
|
|
176
|
+
if (targetLevel === undefined) return true;
|
|
177
|
+
if ((targetLevel === AUTO_THINKING) !== (currentLevel === AUTO_THINKING)) return false;
|
|
178
|
+
return (
|
|
179
|
+
resolveThinkingLevelForModel(target, concreteThinkingLevel(targetLevel)) ===
|
|
180
|
+
resolveThinkingLevelForModel(target, concreteThinkingLevel(currentLevel))
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
145
184
|
/** Metadata used to render the `auto` selector value alongside concrete levels. */
|
|
146
185
|
export interface ConfiguredThinkingLevelMetadata {
|
|
147
186
|
value: ConfiguredThinkingLevel;
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import { isEnoent, peekFile } from "@oh-my-pi/pi-utils";
|
|
9
9
|
import { LRUCache } from "lru-cache/raw";
|
|
10
|
-
import { settings } from "../config/settings";
|
|
10
|
+
import { settings as globalSettings, isSettingsInitialized, type Settings } from "../config/settings";
|
|
11
|
+
import { getDefault } from "../config/settings-schema";
|
|
11
12
|
import { ToolError } from "./tool-errors";
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -283,15 +284,26 @@ async function getAutoGeneratedMarker(filePath: string): Promise<string | undefi
|
|
|
283
284
|
return marker;
|
|
284
285
|
}
|
|
285
286
|
|
|
287
|
+
function shouldBlockAutoGeneratedFiles(activeSettings?: Settings): boolean {
|
|
288
|
+
if (activeSettings) return activeSettings.get("edit.blockAutoGenerated");
|
|
289
|
+
if (isSettingsInitialized()) return globalSettings.get("edit.blockAutoGenerated");
|
|
290
|
+
return getDefault("edit.blockAutoGenerated");
|
|
291
|
+
}
|
|
292
|
+
|
|
286
293
|
/**
|
|
287
294
|
* Check if a file is auto-generated by examining its content.
|
|
288
295
|
* Throws ToolError if the file appears to be auto-generated.
|
|
289
296
|
*
|
|
290
297
|
* @param absolutePath - Absolute path to the file
|
|
291
298
|
* @param displayPath - Path to show in error messages (relative or as provided)
|
|
299
|
+
* @param activeSettings - Session settings; falls back to global settings or the schema default
|
|
292
300
|
*/
|
|
293
|
-
export async function assertEditableFile(
|
|
294
|
-
|
|
301
|
+
export async function assertEditableFile(
|
|
302
|
+
absolutePath: string,
|
|
303
|
+
displayPath?: string,
|
|
304
|
+
activeSettings?: Settings,
|
|
305
|
+
): Promise<void> {
|
|
306
|
+
if (!shouldBlockAutoGeneratedFiles(activeSettings)) {
|
|
295
307
|
return;
|
|
296
308
|
}
|
|
297
309
|
const pathForDisplay = displayPath ?? absolutePath;
|
|
@@ -308,9 +320,10 @@ export async function assertEditableFile(absolutePath: string, displayPath?: str
|
|
|
308
320
|
*
|
|
309
321
|
* @param content - File content to check (can be full content or prefix)
|
|
310
322
|
* @param displayPath - Path to show in error messages
|
|
323
|
+
* @param activeSettings - Session settings; falls back to global settings or the schema default
|
|
311
324
|
*/
|
|
312
|
-
export function assertEditableFileContent(content: string, displayPath: string): void {
|
|
313
|
-
if (!
|
|
325
|
+
export function assertEditableFileContent(content: string, displayPath: string, activeSettings?: Settings): void {
|
|
326
|
+
if (!shouldBlockAutoGeneratedFiles(activeSettings)) {
|
|
314
327
|
return;
|
|
315
328
|
}
|
|
316
329
|
|
package/src/tools/bash.ts
CHANGED
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
previewWindowRows,
|
|
49
49
|
replaceTabs,
|
|
50
50
|
} from "./render-utils";
|
|
51
|
+
import { tokenizeShellSegments } from "./shell-tokenize";
|
|
51
52
|
import { ToolAbortError, ToolError } from "./tool-errors";
|
|
52
53
|
import { toolResult } from "./tool-result";
|
|
53
54
|
import { clampTimeout, TOOL_TIMEOUTS } from "./tool-timeouts";
|
|
@@ -71,12 +72,13 @@ const BASH_PATTERN_APPROVAL_VALUES = new Set(["allow", "deny", "prompt"]);
|
|
|
71
72
|
* containing a space, pipe, `&&`, redirect, or `$(...)`.
|
|
72
73
|
*
|
|
73
74
|
* The wrap reuses the same shell binary + args the local `bash-executor` would
|
|
74
|
-
* pick via `settings.getShellConfig()` — Git Bash / `bash.exe` on Windows
|
|
75
|
+
* pick via `settings.getShellConfig()` — Git Bash / `bash.exe` on Windows
|
|
76
|
+
* (`cmd.exe /c` as the last-resort fallback when no bash exists on the host),
|
|
75
77
|
* `$SHELL` (bash/zsh) with the `sh` fallback on POSIX — so the ACP path
|
|
76
78
|
* preserves `bash` tool semantics (`$VAR`, `$(...)`, `source`, POSIX quoting,
|
|
77
|
-
* `-l`)
|
|
78
|
-
*
|
|
79
|
-
*
|
|
79
|
+
* `-l`) wherever a POSIX shell is available. The agent host's shell path is
|
|
80
|
+
* used as a proxy for the client's, matching the near-universal ACP
|
|
81
|
+
* deployment shape of an editor spawning omp as a co-hosted subprocess.
|
|
80
82
|
*/
|
|
81
83
|
export function wrapShellLineForClientTerminal(
|
|
82
84
|
line: string,
|
|
@@ -189,16 +191,43 @@ function commandMatchesBashApprovalPattern(command: string, pattern: string): bo
|
|
|
189
191
|
return bashApprovalPatternToRegExp(pattern).test(normalizedCommand);
|
|
190
192
|
}
|
|
191
193
|
|
|
194
|
+
// `deny`/`prompt` rules are matched per segment so a dangerous command buried in
|
|
195
|
+
// a compound line (`cd x && rm -rf /`, `sleep 1 & rm -rf /`) is still caught.
|
|
196
|
+
// Reuse the shared shell tokenizer so segmentation stays in one place and honors
|
|
197
|
+
// every command boundary (`;`, `&&`, `||`, `|`, `&`, subshells, newlines).
|
|
198
|
+
function bashCommandSegments(command: string): string[] {
|
|
199
|
+
return tokenizeShellSegments(command)
|
|
200
|
+
.map(segment => segment.join(" "))
|
|
201
|
+
.filter(segment => segment.length > 0);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// `deny`/`prompt` matching: the rule fires when its glob matches the whole
|
|
205
|
+
// command or any single segment of a compound command.
|
|
206
|
+
function commandSegmentMatchesBashApprovalPattern(command: string, pattern: string): boolean {
|
|
207
|
+
const regex = bashApprovalPatternToRegExp(pattern);
|
|
208
|
+
const normalizedCommand = normalizeBashApprovalPattern(command);
|
|
209
|
+
if (normalizedCommand.length === 0) return false;
|
|
210
|
+
if (regex.test(normalizedCommand)) return true;
|
|
211
|
+
return bashCommandSegments(command).some(segment => regex.test(segment));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// A rule "applies" to a command under approval-specific semantics: `allow` must
|
|
215
|
+
// vouch for the ENTIRE command and never rides a compound line (shell control
|
|
216
|
+
// syntax could smuggle an unsafe segment past a narrow allow), while `deny` and
|
|
217
|
+
// `prompt` fire on any matching segment so they mean what they appear to.
|
|
218
|
+
function bashApprovalRuleMatches(command: string, rule: BashApprovalPatternRule): boolean {
|
|
219
|
+
if (rule.approval === "allow") {
|
|
220
|
+
if (BASH_APPROVAL_SHELL_CONTROL_RE.test(command)) return false;
|
|
221
|
+
return commandMatchesBashApprovalPattern(command, rule.match);
|
|
222
|
+
}
|
|
223
|
+
return commandSegmentMatchesBashApprovalPattern(command, rule.match);
|
|
224
|
+
}
|
|
225
|
+
|
|
192
226
|
function findBashApprovalPatternRule(
|
|
193
227
|
command: string,
|
|
194
228
|
rules: readonly BashApprovalPatternRule[],
|
|
195
229
|
): BashApprovalPatternRule | undefined {
|
|
196
|
-
return rules.find(rule =>
|
|
197
|
-
if (rule.approval === "allow" && BASH_APPROVAL_SHELL_CONTROL_RE.test(command)) {
|
|
198
|
-
return false;
|
|
199
|
-
}
|
|
200
|
-
return commandMatchesBashApprovalPattern(command, rule.match);
|
|
201
|
-
});
|
|
230
|
+
return rules.find(rule => bashApprovalRuleMatches(command, rule));
|
|
202
231
|
}
|
|
203
232
|
|
|
204
233
|
async function saveBashOriginalArtifact(session: ToolSession, originalText: string): Promise<string | undefined> {
|
|
@@ -459,7 +488,7 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
459
488
|
const rawCommand = (args as Partial<BashToolInput>).command;
|
|
460
489
|
const command = typeof rawCommand === "string" ? rawCommand : "";
|
|
461
490
|
const patternRules = getBashApprovalPatternRules(this.session.settings.get("bash.patterns"));
|
|
462
|
-
const patternRule =
|
|
491
|
+
const patternRule = findBashApprovalPatternRule(command, patternRules);
|
|
463
492
|
if (patternRule?.approval === "deny") {
|
|
464
493
|
return {
|
|
465
494
|
tier: "exec",
|
|
@@ -471,14 +500,13 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
471
500
|
if (command !== "" && CRITICAL_BASH_PATTERNS.some(pattern => pattern.test(command))) {
|
|
472
501
|
return { tier: "exec", override: true, reason: "Critical pattern detected" };
|
|
473
502
|
}
|
|
474
|
-
|
|
475
|
-
if (
|
|
476
|
-
if (safePatternRule?.approval === "prompt") {
|
|
503
|
+
if (patternRule?.approval === "allow") return { tier: "write", policy: "allow" };
|
|
504
|
+
if (patternRule?.approval === "prompt") {
|
|
477
505
|
return {
|
|
478
506
|
tier: "exec",
|
|
479
507
|
override: true,
|
|
480
508
|
policy: "prompt",
|
|
481
|
-
reason: `Prompt required by bash pattern: ${
|
|
509
|
+
reason: `Prompt required by bash pattern: ${patternRule.match}`,
|
|
482
510
|
};
|
|
483
511
|
}
|
|
484
512
|
return "exec";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Model } from "@oh-my-pi/pi-ai";
|
|
2
|
+
|
|
3
|
+
export type ComputerExposureMode = "native" | "function" | "unavailable";
|
|
4
|
+
|
|
5
|
+
export interface ComputerExposureOptions {
|
|
6
|
+
azureBaseUrl?: string;
|
|
7
|
+
azureResourceName?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function isFirstPartyAzureEndpoint(baseUrl: string): boolean {
|
|
11
|
+
try {
|
|
12
|
+
const url = new URL(baseUrl);
|
|
13
|
+
return (
|
|
14
|
+
url.protocol === "https:" &&
|
|
15
|
+
(url.hostname.endsWith(".openai.azure.com") || url.hostname === "models.inference.ai.azure.com")
|
|
16
|
+
);
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Match the provider transport's effective Computer Use tool representation. */
|
|
23
|
+
export function computerExposureMode(
|
|
24
|
+
model: Model | undefined,
|
|
25
|
+
options?: ComputerExposureOptions,
|
|
26
|
+
): ComputerExposureMode {
|
|
27
|
+
if (!model) return "unavailable";
|
|
28
|
+
if (model.supportsComputerUse !== true) return "function";
|
|
29
|
+
if (model.api !== "azure-openai-responses" || model.supportsComputerUseConfig !== undefined) return "native";
|
|
30
|
+
const baseUrl =
|
|
31
|
+
options?.azureBaseUrl?.trim() ||
|
|
32
|
+
process.env.AZURE_OPENAI_BASE_URL?.trim() ||
|
|
33
|
+
(options?.azureResourceName || process.env.AZURE_OPENAI_RESOURCE_NAME
|
|
34
|
+
? `https://${options?.azureResourceName || process.env.AZURE_OPENAI_RESOURCE_NAME}.openai.azure.com/openai/v1`
|
|
35
|
+
: undefined) ||
|
|
36
|
+
model.baseUrl;
|
|
37
|
+
return baseUrl && isFirstPartyAzureEndpoint(baseUrl) ? "native" : "function";
|
|
38
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { DesktopAction, DesktopCapabilities, DesktopCapture, DesktopSessionOptions } from "@oh-my-pi/pi-natives";
|
|
2
|
-
import { withTimeout
|
|
2
|
+
import { withTimeout } from "@oh-my-pi/pi-utils/async";
|
|
3
|
+
import * as logger from "@oh-my-pi/pi-utils/logger";
|
|
4
|
+
import { workerHostEntry } from "@oh-my-pi/pi-utils/worker-host";
|
|
3
5
|
import { ToolAbortError, ToolError } from "../tool-errors";
|
|
4
6
|
import {
|
|
5
7
|
COMPUTER_WORKER_ARG,
|
|
@@ -138,11 +140,23 @@ export class ComputerSupervisor implements ComputerController {
|
|
|
138
140
|
if (this.#startPromise) return this.#startPromise;
|
|
139
141
|
const ready = Promise.withResolvers<void>();
|
|
140
142
|
try {
|
|
143
|
+
logger.debug("Starting native computer worker", {
|
|
144
|
+
backend: this.options.backend,
|
|
145
|
+
display: this.options.display,
|
|
146
|
+
maxWidth: this.options.maxWidth,
|
|
147
|
+
maxHeight: this.options.maxHeight,
|
|
148
|
+
});
|
|
141
149
|
const worker = this.createWorker();
|
|
142
150
|
this.#worker = worker;
|
|
143
151
|
this.#unsubscribeMessage = worker.onMessage(message => {
|
|
144
152
|
if (message.type === "ready") {
|
|
145
153
|
this.#capabilities = message.capabilities;
|
|
154
|
+
logger.debug("Native computer worker ready", {
|
|
155
|
+
backend: message.capabilities.backend,
|
|
156
|
+
capturePermission: message.capabilities.capturePermission,
|
|
157
|
+
inputPermission: message.capabilities.inputPermission,
|
|
158
|
+
displayCount: message.capabilities.displayCount,
|
|
159
|
+
});
|
|
146
160
|
ready.resolve();
|
|
147
161
|
return;
|
|
148
162
|
}
|
|
@@ -151,10 +165,22 @@ export class ComputerSupervisor implements ComputerController {
|
|
|
151
165
|
const pending = this.#pending.get(message.id);
|
|
152
166
|
this.#pending.delete(message.id);
|
|
153
167
|
pending?.resolve(message.capture);
|
|
168
|
+
logger.debug("Native computer capture completed", {
|
|
169
|
+
requestId: message.id,
|
|
170
|
+
backend: message.capture.backend,
|
|
171
|
+
capturePermission: message.capture.capturePermission,
|
|
172
|
+
inputPermission: message.capture.inputPermission,
|
|
173
|
+
width: message.capture.width,
|
|
174
|
+
height: message.capture.height,
|
|
175
|
+
});
|
|
154
176
|
return;
|
|
155
177
|
}
|
|
156
178
|
if (message.type === "error") {
|
|
157
179
|
const error = workerError(message.error);
|
|
180
|
+
logger.warn("Native computer worker request failed", {
|
|
181
|
+
requestId: message.id,
|
|
182
|
+
errorName: message.error.name,
|
|
183
|
+
});
|
|
158
184
|
if (message.id) {
|
|
159
185
|
const pending = this.#pending.get(message.id);
|
|
160
186
|
this.#pending.delete(message.id);
|
|
@@ -184,6 +210,9 @@ export class ComputerSupervisor implements ComputerController {
|
|
|
184
210
|
}
|
|
185
211
|
|
|
186
212
|
async #terminate(reason: unknown): Promise<void> {
|
|
213
|
+
logger.debug("Terminating native computer worker", {
|
|
214
|
+
reason: reason instanceof Error ? reason.name : typeof reason,
|
|
215
|
+
});
|
|
187
216
|
const worker = this.#worker;
|
|
188
217
|
this.#worker = undefined;
|
|
189
218
|
this.#startPromise = undefined;
|
|
@@ -239,20 +268,39 @@ export async function releaseComputerSessionsForOwner(ownerId: string | undefine
|
|
|
239
268
|
await Promise.allSettled(Array.from(controllers, controller => controller.close()));
|
|
240
269
|
}
|
|
241
270
|
|
|
242
|
-
export async function smokeTestComputerWorker(
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
271
|
+
export async function smokeTestComputerWorker(
|
|
272
|
+
timeoutMs = SMOKE_TIMEOUT_MS,
|
|
273
|
+
createWorker: ComputerWorkerFactory = spawnComputerWorker,
|
|
274
|
+
): Promise<void> {
|
|
275
|
+
const worker = createWorker();
|
|
276
|
+
const exchange = async (
|
|
277
|
+
message: ComputerWorkerInbound,
|
|
278
|
+
expected: ComputerWorkerOutbound["type"],
|
|
279
|
+
failureMessage: string,
|
|
280
|
+
): Promise<void> => {
|
|
281
|
+
const response = Promise.withResolvers<void>();
|
|
282
|
+
const unsubscribeMessage = worker.onMessage(received => {
|
|
283
|
+
if (received.type === expected) response.resolve();
|
|
284
|
+
else if (received.type === "error") response.reject(workerError(received.error));
|
|
285
|
+
});
|
|
286
|
+
const unsubscribeError = worker.onError(error => response.reject(error));
|
|
287
|
+
try {
|
|
288
|
+
worker.send(message);
|
|
289
|
+
await withTimeout(response.promise, timeoutMs, failureMessage);
|
|
290
|
+
} finally {
|
|
291
|
+
unsubscribeMessage();
|
|
292
|
+
unsubscribeError();
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
250
296
|
try {
|
|
251
|
-
|
|
252
|
-
|
|
297
|
+
await exchange(
|
|
298
|
+
{ type: "init", options: { backend: "auto", display: "all", maxWidth: 1920, maxHeight: 1200 } },
|
|
299
|
+
"ready",
|
|
300
|
+
"Computer worker smoke initialization timed out",
|
|
301
|
+
);
|
|
302
|
+
await exchange({ type: "close" }, "closed", "Computer worker smoke close timed out");
|
|
253
303
|
} finally {
|
|
254
|
-
unsubscribeMessage();
|
|
255
|
-
unsubscribeError();
|
|
256
304
|
await worker.terminate();
|
|
257
305
|
}
|
|
258
306
|
}
|