@oh-my-pi/pi-coding-agent 17.1.3 → 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 +54 -0
- package/dist/cli.js +3759 -3751
- 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/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/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/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/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/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/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/shell-tokenize.ts +83 -0
- package/src/tools/todo.ts +44 -17
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;
|
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
|
}
|
|
@@ -1,25 +1,34 @@
|
|
|
1
1
|
import { parentPort } from "node:worker_threads";
|
|
2
|
-
import { consumeWorkerInbox } from "@oh-my-pi/pi-utils/worker-host";
|
|
2
|
+
import { consumeWorkerInbox, isWorkerHostSelector } from "@oh-my-pi/pi-utils/worker-host";
|
|
3
3
|
import type { ComputerWorkerInbound, ComputerWorkerTransport } from "./protocol";
|
|
4
4
|
import { ComputerWorkerCore } from "./worker";
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
export function startComputerWorker(): void {
|
|
7
|
+
if (!parentPort) throw new Error("computer-worker-entry: missing parentPort");
|
|
7
8
|
|
|
8
|
-
const port = parentPort;
|
|
9
|
-
const inbox = consumeWorkerInbox();
|
|
10
|
-
const transport: ComputerWorkerTransport = {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
};
|
|
9
|
+
const port = parentPort;
|
|
10
|
+
const inbox = consumeWorkerInbox();
|
|
11
|
+
const transport: ComputerWorkerTransport = {
|
|
12
|
+
send(message, transfer) {
|
|
13
|
+
port.postMessage(message, transfer ?? []);
|
|
14
|
+
},
|
|
15
|
+
onMessage(handler) {
|
|
16
|
+
if (inbox) return inbox.bind(message => handler(message as ComputerWorkerInbound));
|
|
17
|
+
const listener = (message: unknown): void => handler(message as ComputerWorkerInbound);
|
|
18
|
+
port.on("message", listener);
|
|
19
|
+
return () => port.off("message", listener);
|
|
20
|
+
},
|
|
21
|
+
close() {
|
|
22
|
+
port.close();
|
|
23
|
+
},
|
|
24
|
+
};
|
|
24
25
|
|
|
25
|
-
new ComputerWorkerCore(transport);
|
|
26
|
+
new ComputerWorkerCore(transport);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Bun workers report `import.meta.main === false`. The source fallback still
|
|
30
|
+
// enters this file directly, while packaged CLI workers carry the selector and
|
|
31
|
+
// start the named entry only after installing its inbox.
|
|
32
|
+
if (!Bun.isMainThread && !process.argv.some(isWorkerHostSelector) && import.meta.path === Bun.main) {
|
|
33
|
+
startComputerWorker();
|
|
34
|
+
}
|
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
type DesktopCapture,
|
|
4
|
-
DesktopSession,
|
|
5
|
-
type DesktopSessionOptions,
|
|
6
|
-
} from "@oh-my-pi/pi-natives";
|
|
1
|
+
import type { DesktopAction, DesktopCapture, DesktopSession, DesktopSessionOptions } from "@oh-my-pi/pi-natives";
|
|
2
|
+
import { createDesktopSession } from "@oh-my-pi/pi-natives/desktop";
|
|
7
3
|
import type { ComputerWorkerError, ComputerWorkerInbound, ComputerWorkerTransport } from "./protocol";
|
|
8
4
|
|
|
9
5
|
export interface NativeDesktopSession {
|
|
@@ -44,7 +40,7 @@ export class ComputerWorkerCore {
|
|
|
44
40
|
|
|
45
41
|
constructor(
|
|
46
42
|
private readonly transport: ComputerWorkerTransport,
|
|
47
|
-
private readonly createSession: NativeDesktopSessionFactory =
|
|
43
|
+
private readonly createSession: NativeDesktopSessionFactory = createDesktopSession,
|
|
48
44
|
) {
|
|
49
45
|
this.#unsubscribe = transport.onMessage(message => this.#onMessage(message));
|
|
50
46
|
}
|
package/src/tools/computer.ts
CHANGED
|
@@ -5,7 +5,8 @@ import type {
|
|
|
5
5
|
AgentToolUpdateCallback,
|
|
6
6
|
ToolApprovalDecision,
|
|
7
7
|
} from "@oh-my-pi/pi-agent-core";
|
|
8
|
-
import type { ComputerAction, ComputerSafetyCheck, ComputerToolCallMetadata } from "@oh-my-pi/pi-ai";
|
|
8
|
+
import type { ComputerAction, ComputerSafetyCheck, ComputerToolCallMetadata, Model } from "@oh-my-pi/pi-ai";
|
|
9
|
+
import { isClaudeModelId } from "@oh-my-pi/pi-catalog/identity";
|
|
9
10
|
import type {
|
|
10
11
|
DesktopAction,
|
|
11
12
|
DesktopCapabilities,
|
|
@@ -21,6 +22,36 @@ import { type ComputerController, ComputerSupervisor, registerComputerController
|
|
|
21
22
|
import type { ToolSession } from "./index";
|
|
22
23
|
import { ToolError, throwIfAborted } from "./tool-errors";
|
|
23
24
|
|
|
25
|
+
// Image transports that cannot preserve native screenshot detail resize frames
|
|
26
|
+
// without returning transformed dimensions. Keep their native coordinate frames
|
|
27
|
+
// below the empirically verified threshold so pointer actions match what the
|
|
28
|
+
// model sees. Claude paths predate the resolved transport capability and retain
|
|
29
|
+
// their established model-family fallback.
|
|
30
|
+
const COORDINATE_SAFE_MAX_CAPTURE_WIDTH = 1280;
|
|
31
|
+
const COORDINATE_SAFE_MAX_CAPTURE_HEIGHT = 896;
|
|
32
|
+
|
|
33
|
+
function usesCoordinateSafeImageSizing(model: Model | undefined): boolean {
|
|
34
|
+
if (!model) return false;
|
|
35
|
+
const compat = model.compat;
|
|
36
|
+
return (
|
|
37
|
+
(!!compat && "supportsImageDetailOriginal" in compat && compat.supportsImageDetailOriginal === false) ||
|
|
38
|
+
isClaudeModelId(model.id) ||
|
|
39
|
+
(model.requestModelId !== undefined && isClaudeModelId(model.requestModelId)) ||
|
|
40
|
+
(typeof model.name === "string" && /^claude(?:\s|$)/i.test(model.name))
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function captureOptions(session: ToolSession, coordinateSafeImageSizing: boolean): DesktopSessionOptions {
|
|
45
|
+
const maxWidth = session.settings.get("computer.maxWidth");
|
|
46
|
+
const maxHeight = session.settings.get("computer.maxHeight");
|
|
47
|
+
return {
|
|
48
|
+
backend: session.settings.get("computer.backend"),
|
|
49
|
+
display: session.settings.get("computer.display"),
|
|
50
|
+
maxWidth: coordinateSafeImageSizing ? Math.min(maxWidth, COORDINATE_SAFE_MAX_CAPTURE_WIDTH) : maxWidth,
|
|
51
|
+
maxHeight: coordinateSafeImageSizing ? Math.min(maxHeight, COORDINATE_SAFE_MAX_CAPTURE_HEIGHT) : maxHeight,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
24
55
|
// Desktop actions cross the N-API boundary as i32; out-of-range JS numbers
|
|
25
56
|
// must fail closed here instead of truncating in the napi conversion.
|
|
26
57
|
const INT32_MIN = -2_147_483_648;
|
|
@@ -230,7 +261,7 @@ function isComputerAction(value: unknown): value is ComputerAction {
|
|
|
230
261
|
function parseActions(value: unknown): ComputerAction[] {
|
|
231
262
|
// Missing or empty action batches degrade to a plain screenshot so a
|
|
232
263
|
// function-calling model can observe the screen before acting.
|
|
233
|
-
if (value
|
|
264
|
+
if (value == null) return [{ type: "screenshot" }];
|
|
234
265
|
if (!Array.isArray(value)) throw new ToolError("Computer call requires an array of actions");
|
|
235
266
|
if (value.length === 0) return [{ type: "screenshot" }];
|
|
236
267
|
if (!value.every(isComputerAction)) throw new ToolError("Computer call contains an invalid action");
|
|
@@ -286,6 +317,7 @@ function callMetadata(context: AgentToolContext | undefined): ComputerToolCallMe
|
|
|
286
317
|
export function computerApproval(args: unknown): ToolApprovalDecision {
|
|
287
318
|
const actions =
|
|
288
319
|
args && typeof args === "object" && "actions" in args ? (args as { actions?: unknown }).actions : undefined;
|
|
320
|
+
if (actions == null) return "read";
|
|
289
321
|
if (!Array.isArray(actions)) return "exec";
|
|
290
322
|
return actions.every(action => {
|
|
291
323
|
if (!action || typeof action !== "object") return false;
|
|
@@ -356,8 +388,16 @@ export class ComputerTool implements AgentTool<typeof computerSchema, ComputerTo
|
|
|
356
388
|
const actions = args && typeof args === "object" ? (args as { actions?: unknown }).actions : undefined;
|
|
357
389
|
return approvalActionSummary(actions);
|
|
358
390
|
};
|
|
359
|
-
|
|
360
|
-
|
|
391
|
+
/**
|
|
392
|
+
* Settings snapshot used to create the tool's current controller; refreshed
|
|
393
|
+
* when a model switch crosses the coordinate-safe sizing boundary. Surfaced
|
|
394
|
+
* by `/computer status`.
|
|
395
|
+
*/
|
|
396
|
+
effectiveConfiguration: Readonly<DesktopSessionOptions>;
|
|
397
|
+
readonly #createController: ComputerControllerFactory;
|
|
398
|
+
#controller: ComputerController;
|
|
399
|
+
#unregisterOwner: () => void;
|
|
400
|
+
#usesCoordinateSafeImageSizing: boolean;
|
|
361
401
|
#closed = false;
|
|
362
402
|
#description?: string;
|
|
363
403
|
|
|
@@ -365,12 +405,10 @@ export class ComputerTool implements AgentTool<typeof computerSchema, ComputerTo
|
|
|
365
405
|
readonly session: ToolSession,
|
|
366
406
|
createController: ComputerControllerFactory = options => new ComputerSupervisor(options),
|
|
367
407
|
) {
|
|
368
|
-
this.#
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
maxHeight: session.settings.get("computer.maxHeight"),
|
|
373
|
-
});
|
|
408
|
+
this.#createController = createController;
|
|
409
|
+
this.#usesCoordinateSafeImageSizing = usesCoordinateSafeImageSizing(session.getActiveModel?.());
|
|
410
|
+
this.effectiveConfiguration = Object.freeze(captureOptions(session, this.#usesCoordinateSafeImageSizing));
|
|
411
|
+
this.#controller = createController(this.effectiveConfiguration);
|
|
374
412
|
this.#unregisterOwner = registerComputerController(
|
|
375
413
|
session.getEvalKernelOwnerId?.() ?? undefined,
|
|
376
414
|
this.#controller,
|
|
@@ -381,6 +419,21 @@ export class ComputerTool implements AgentTool<typeof computerSchema, ComputerTo
|
|
|
381
419
|
return this.#description;
|
|
382
420
|
}
|
|
383
421
|
|
|
422
|
+
async #refreshControllerForModel(): Promise<void> {
|
|
423
|
+
const nextUsesCoordinateSafeImageSizing = usesCoordinateSafeImageSizing(this.session.getActiveModel?.());
|
|
424
|
+
if (nextUsesCoordinateSafeImageSizing === this.#usesCoordinateSafeImageSizing) return;
|
|
425
|
+
|
|
426
|
+
const previous = this.#controller;
|
|
427
|
+
const nextOptions = Object.freeze(captureOptions(this.session, nextUsesCoordinateSafeImageSizing));
|
|
428
|
+
const next = this.#createController(nextOptions);
|
|
429
|
+
this.#unregisterOwner();
|
|
430
|
+
this.#controller = next;
|
|
431
|
+
this.#unregisterOwner = registerComputerController(this.session.getEvalKernelOwnerId?.() ?? undefined, next);
|
|
432
|
+
this.#usesCoordinateSafeImageSizing = nextUsesCoordinateSafeImageSizing;
|
|
433
|
+
this.effectiveConfiguration = nextOptions;
|
|
434
|
+
await previous.close();
|
|
435
|
+
}
|
|
436
|
+
|
|
384
437
|
async execute(
|
|
385
438
|
_toolCallId: string,
|
|
386
439
|
params: ComputerParams,
|
|
@@ -396,6 +449,8 @@ export class ComputerTool implements AgentTool<typeof computerSchema, ComputerTo
|
|
|
396
449
|
if (pendingSafetyChecks.length > 0 && context?.providerSafetyApproved !== true) {
|
|
397
450
|
throw new ToolError("Provider safety checks require interactive approval before computer input");
|
|
398
451
|
}
|
|
452
|
+
await this.#refreshControllerForModel();
|
|
453
|
+
throwIfAborted(signal);
|
|
399
454
|
const capture = await this.#controller.execute(actions.map(toDesktopAction), signal);
|
|
400
455
|
throwIfAborted(signal);
|
|
401
456
|
const data = Buffer.from(capture.data).toBase64();
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* dwarfs the cost of one cache miss.
|
|
19
19
|
*/
|
|
20
20
|
import { invalidateAllForNumber, invalidateAllForRepo } from "./github-cache";
|
|
21
|
+
import { tokenizeShellSegments } from "./shell-tokenize";
|
|
21
22
|
|
|
22
23
|
const PR_URL_PATTERN = /^https:\/\/github\.com\/([^/\s]+\/[^/\s]+)\/pull\/(\d+)(?:[/?#].*)?$/i;
|
|
23
24
|
const ISSUE_URL_PATTERN = /^https:\/\/github\.com\/([^/\s]+\/[^/\s]+)\/issues\/(\d+)(?:[/?#].*)?$/i;
|
|
@@ -154,87 +155,6 @@ function detectGhMutation(tokens: readonly string[]): { number?: number; repo?:
|
|
|
154
155
|
return repo !== undefined ? { repo } : {};
|
|
155
156
|
}
|
|
156
157
|
|
|
157
|
-
/**
|
|
158
|
-
* Conservative tokenizer that splits a bash command into individual word
|
|
159
|
-
* tokens. Handles single/double-quoted strings, backslash escapes, and
|
|
160
|
-
* standard operators (`;`, `&&`, `||`, `|`, `&`, newlines) as token
|
|
161
|
-
* boundaries that emit a sentinel `";"` so the caller treats the segments
|
|
162
|
-
* as independent command sequences. We do not attempt full POSIX shell
|
|
163
|
-
* parsing — heredocs, command substitution, and arithmetic expansion are
|
|
164
|
-
* out of scope; the detector simply falls through when it cannot find a
|
|
165
|
-
* clean `gh issue|pr <subcmd>` triple.
|
|
166
|
-
*/
|
|
167
|
-
function tokenize(command: string): string[][] {
|
|
168
|
-
const segments: string[][] = [];
|
|
169
|
-
let current: string[] = [];
|
|
170
|
-
let buffer = "";
|
|
171
|
-
let inSingle = false;
|
|
172
|
-
let inDouble = false;
|
|
173
|
-
const pushBuffer = () => {
|
|
174
|
-
if (buffer.length > 0) {
|
|
175
|
-
current.push(buffer);
|
|
176
|
-
buffer = "";
|
|
177
|
-
}
|
|
178
|
-
};
|
|
179
|
-
const pushSegment = () => {
|
|
180
|
-
pushBuffer();
|
|
181
|
-
if (current.length > 0) segments.push(current);
|
|
182
|
-
current = [];
|
|
183
|
-
};
|
|
184
|
-
for (let i = 0; i < command.length; i++) {
|
|
185
|
-
const ch = command[i];
|
|
186
|
-
if (inSingle) {
|
|
187
|
-
if (ch === "'") {
|
|
188
|
-
inSingle = false;
|
|
189
|
-
continue;
|
|
190
|
-
}
|
|
191
|
-
buffer += ch;
|
|
192
|
-
continue;
|
|
193
|
-
}
|
|
194
|
-
if (inDouble) {
|
|
195
|
-
if (ch === "\\" && i + 1 < command.length) {
|
|
196
|
-
const next = command[i + 1];
|
|
197
|
-
if (next === '"' || next === "\\" || next === "$" || next === "`") {
|
|
198
|
-
buffer += next;
|
|
199
|
-
i++;
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
if (ch === '"') {
|
|
204
|
-
inDouble = false;
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
buffer += ch;
|
|
208
|
-
continue;
|
|
209
|
-
}
|
|
210
|
-
if (ch === "'") {
|
|
211
|
-
inSingle = true;
|
|
212
|
-
continue;
|
|
213
|
-
}
|
|
214
|
-
if (ch === '"') {
|
|
215
|
-
inDouble = true;
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
if (ch === "\\" && i + 1 < command.length) {
|
|
219
|
-
buffer += command[i + 1];
|
|
220
|
-
i++;
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
223
|
-
if (ch === " " || ch === "\t") {
|
|
224
|
-
pushBuffer();
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
if (ch === "\n" || ch === ";" || ch === "&" || ch === "|" || ch === "(" || ch === ")") {
|
|
228
|
-
pushSegment();
|
|
229
|
-
// `&&`, `||` already collapsed by the segment break above.
|
|
230
|
-
continue;
|
|
231
|
-
}
|
|
232
|
-
buffer += ch;
|
|
233
|
-
}
|
|
234
|
-
pushSegment();
|
|
235
|
-
return segments;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
158
|
/**
|
|
239
159
|
* Drop `github-cache` rows for any `gh issue|pr <mutating-subcmd>` call
|
|
240
160
|
* embedded in `command`. Safe to invoke unconditionally; no-op when the
|
|
@@ -242,7 +162,7 @@ function tokenize(command: string): string[][] {
|
|
|
242
162
|
*/
|
|
243
163
|
export function invalidateGithubCacheForBashCommand(command: string): void {
|
|
244
164
|
if (!command?.includes("gh")) return;
|
|
245
|
-
const segments =
|
|
165
|
+
const segments = tokenizeShellSegments(command);
|
|
246
166
|
for (const segment of segments) {
|
|
247
167
|
const hit = detectGhMutation(segment);
|
|
248
168
|
if (!hit) continue;
|