@oh-my-pi/pi-coding-agent 17.1.0 → 17.1.2
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 +55 -0
- package/dist/cli.js +6325 -4420
- package/dist/types/cli/bench-cli.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +6 -3
- package/dist/types/config/settings-schema.d.ts +62 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +3 -0
- package/dist/types/launch/broker-list-order.test.d.ts +1 -0
- package/dist/types/live/attestation.d.ts +2 -0
- package/dist/types/live/controller.d.ts +10 -2
- package/dist/types/live/protocol.d.ts +1 -1
- package/dist/types/live/transport.d.ts +6 -19
- package/dist/types/live/visualizer.d.ts +8 -11
- package/dist/types/modes/components/assistant-message.d.ts +1 -0
- package/dist/types/modes/components/custom-message.d.ts +1 -1
- package/dist/types/modes/components/message-frame.d.ts +8 -4
- package/dist/types/modes/components/session-account-selector.d.ts +11 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +3 -6
- package/dist/types/modes/types.d.ts +9 -5
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -6
- package/dist/types/session/agent-session-types.d.ts +8 -1
- package/dist/types/session/agent-session.d.ts +20 -1
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/eval-runner.d.ts +2 -0
- package/dist/types/session/messages.d.ts +2 -0
- package/dist/types/session/session-tools.d.ts +15 -0
- package/dist/types/session/streaming-output.d.ts +8 -0
- package/dist/types/slash-commands/helpers/session-pin.d.ts +9 -0
- package/dist/types/stt/index.d.ts +0 -2
- package/dist/types/stt/stt-controller.d.ts +7 -0
- package/dist/types/task/executor.d.ts +3 -1
- package/dist/types/task/structured-subagent.d.ts +3 -0
- package/dist/types/task/types.d.ts +11 -11
- package/dist/types/thinking.d.ts +13 -0
- package/dist/types/tiny/title-client.d.ts +10 -0
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/computer/protocol.d.ts +43 -0
- package/dist/types/tools/computer/supervisor.d.ts +32 -0
- package/dist/types/tools/computer/worker-entry.d.ts +1 -0
- package/dist/types/tools/computer/worker.d.ts +15 -0
- package/dist/types/tools/computer-renderer.d.ts +22 -0
- package/dist/types/tools/computer.d.ts +71 -0
- package/dist/types/tools/context.d.ts +2 -0
- package/dist/types/tools/default-renderer.d.ts +21 -0
- package/dist/types/tools/eval-format/index.d.ts +7 -0
- package/dist/types/tools/eval-format/javascript.d.ts +2 -0
- package/dist/types/tools/eval-format/julia.d.ts +2 -0
- package/dist/types/tools/eval-format/python.d.ts +2 -0
- package/dist/types/tools/eval-format/ruby.d.ts +2 -0
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/resolve.d.ts +7 -0
- package/dist/types/tools/todo.d.ts +4 -1
- package/dist/types/tts/streaming-player.d.ts +10 -43
- package/dist/types/utils/tools-manager.d.ts +1 -2
- package/dist/types/web/search/providers/base.d.ts +16 -0
- package/dist/types/web/search/providers/brave.d.ts +2 -0
- package/dist/types/web/search/providers/firecrawl.d.ts +2 -0
- package/dist/types/web/search/providers/gemini.d.ts +3 -0
- package/dist/types/web/search/providers/jina.d.ts +2 -0
- package/dist/types/web/search/providers/kagi.d.ts +2 -0
- package/dist/types/web/search/providers/kimi.d.ts +2 -0
- package/dist/types/web/search/providers/parallel.d.ts +2 -0
- package/dist/types/web/search/providers/perplexity.d.ts +3 -0
- package/dist/types/web/search/providers/searxng.d.ts +10 -0
- package/dist/types/web/search/providers/tavily.d.ts +8 -0
- package/dist/types/web/search/query.d.ts +190 -0
- package/package.json +12 -12
- package/src/cli/args.ts +1 -0
- package/src/cli/bench-cli.ts +12 -1
- package/src/cli/setup-cli.ts +2 -14
- package/src/cli/web-search-cli.ts +7 -0
- package/src/cli.ts +8 -0
- package/src/config/model-registry.ts +6 -0
- package/src/config/model-resolver.ts +17 -6
- package/src/config/settings-schema.ts +66 -0
- package/src/debug/raw-sse-buffer.ts +157 -30
- package/src/eval/executor-base.ts +1 -0
- package/src/eval/js/executor.ts +2 -0
- package/src/exec/bash-executor.ts +1 -0
- package/src/export/share.ts +4 -3
- package/src/extensibility/extensions/wrapper.ts +68 -12
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -1
- package/src/launch/broker-list-order.test.ts +89 -0
- package/src/launch/broker.ts +49 -8
- package/src/live/attestation.ts +91 -0
- package/src/live/controller.ts +76 -23
- package/src/live/protocol.test.ts +3 -3
- package/src/live/protocol.ts +1 -1
- package/src/live/transport.ts +72 -140
- package/src/live/visualizer.ts +114 -134
- package/src/modes/components/assistant-message.ts +7 -2
- package/src/modes/components/custom-message.ts +4 -1
- package/src/modes/components/message-frame.ts +14 -8
- package/src/modes/components/session-account-selector.ts +62 -0
- package/src/modes/components/tool-execution.ts +17 -110
- package/src/modes/controllers/input-controller.ts +55 -47
- package/src/modes/controllers/live-command-controller.ts +82 -5
- package/src/modes/controllers/selector-controller.ts +62 -0
- package/src/modes/interactive-mode.ts +79 -5
- package/src/modes/types.ts +10 -4
- package/src/modes/utils/ui-helpers.ts +7 -8
- package/src/prompts/system/computer-safety.md +14 -0
- package/src/prompts/system/resolve-device-reminder.md +1 -1
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/ast-edit.md +1 -0
- package/src/prompts/tools/bash.md +1 -0
- package/src/prompts/tools/computer.md +26 -0
- package/src/prompts/tools/eval.md +2 -2
- package/src/prompts/tools/task.md +5 -2
- package/src/prompts/tools/web-search.md +2 -0
- package/src/sdk.ts +13 -4
- package/src/session/agent-session-types.ts +9 -0
- package/src/session/agent-session.ts +70 -0
- package/src/session/auth-storage.ts +1 -0
- package/src/session/eval-runner.ts +5 -0
- package/src/session/messages.ts +3 -0
- package/src/session/queued-messages.ts +7 -1
- package/src/session/session-tools.ts +37 -0
- package/src/session/streaming-output.ts +52 -5
- package/src/slash-commands/builtin-registry.ts +165 -9
- package/src/slash-commands/helpers/session-pin.ts +44 -0
- package/src/stt/downloader.ts +0 -2
- package/src/stt/index.ts +0 -2
- package/src/stt/stt-controller.ts +57 -146
- package/src/system-prompt.ts +4 -0
- package/src/task/executor.ts +17 -9
- package/src/task/index.ts +14 -34
- package/src/task/structured-subagent.ts +4 -0
- package/src/task/types.ts +15 -13
- package/src/thinking.ts +27 -0
- package/src/tiny/title-client.ts +22 -0
- package/src/tools/ast-edit.ts +4 -1
- package/src/tools/bash-interactive.ts +90 -86
- package/src/tools/bash.ts +13 -0
- package/src/tools/browser/render.ts +2 -1
- package/src/tools/builtin-names.ts +1 -0
- package/src/tools/computer/protocol.ts +28 -0
- package/src/tools/computer/supervisor.ts +258 -0
- package/src/tools/computer/worker-entry.ts +25 -0
- package/src/tools/computer/worker.ts +135 -0
- package/src/tools/computer-renderer.ts +108 -0
- package/src/tools/computer.ts +433 -0
- package/src/tools/context.ts +2 -0
- package/src/tools/default-renderer.ts +139 -0
- package/src/tools/essential-tools.ts +1 -0
- package/src/tools/eval-format/index.ts +24 -0
- package/src/tools/eval-format/javascript.ts +952 -0
- package/src/tools/eval-format/julia.ts +446 -0
- package/src/tools/eval-format/python.ts +544 -0
- package/src/tools/eval-format/ruby.ts +380 -0
- package/src/tools/eval-render.ts +12 -6
- package/src/tools/index.ts +5 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resolve.ts +10 -1
- package/src/tools/todo.ts +58 -6
- package/src/tools/xdev.ts +54 -26
- package/src/tts/streaming-player.ts +81 -340
- package/src/utils/clipboard.ts +1 -30
- package/src/utils/mac-file-urls.applescript +37 -0
- package/src/utils/tool-choice.ts +14 -0
- package/src/utils/tools-manager.ts +1 -19
- package/src/web/search/index.ts +28 -5
- package/src/web/search/providers/anthropic.ts +62 -4
- package/src/web/search/providers/base.ts +16 -0
- package/src/web/search/providers/brave.ts +30 -3
- package/src/web/search/providers/codex.ts +14 -1
- package/src/web/search/providers/duckduckgo.ts +23 -1
- package/src/web/search/providers/ecosia.ts +5 -1
- package/src/web/search/providers/exa.ts +28 -1
- package/src/web/search/providers/firecrawl.ts +37 -3
- package/src/web/search/providers/gemini.ts +12 -2
- package/src/web/search/providers/google.ts +2 -1
- package/src/web/search/providers/jina.ts +31 -8
- package/src/web/search/providers/kagi.ts +10 -1
- package/src/web/search/providers/kimi.ts +16 -1
- package/src/web/search/providers/mojeek.ts +14 -1
- package/src/web/search/providers/parallel.ts +48 -2
- package/src/web/search/providers/perplexity.ts +94 -6
- package/src/web/search/providers/searxng.ts +145 -9
- package/src/web/search/providers/startpage.ts +8 -2
- package/src/web/search/providers/synthetic.ts +7 -1
- package/src/web/search/providers/tavily.ts +52 -3
- package/src/web/search/providers/tinyfish.ts +6 -1
- package/src/web/search/providers/xai.ts +49 -2
- package/src/web/search/providers/zai.ts +19 -1
- package/src/web/search/query.ts +850 -0
- package/dist/types/stt/recorder.d.ts +0 -30
- package/dist/types/stt/transcriber.d.ts +0 -14
- package/dist/types/stt/wav.d.ts +0 -29
- package/dist/types/tts/player.d.ts +0 -32
- package/src/live/audio-worklet.txt +0 -59
- package/src/live/browser-runtime.txt +0 -221
- package/src/stt/recorder.ts +0 -551
- package/src/stt/transcriber.ts +0 -60
- package/src/stt/wav.ts +0 -173
- package/src/tts/player.ts +0 -137
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentTool,
|
|
3
|
+
AgentToolContext,
|
|
4
|
+
AgentToolResult,
|
|
5
|
+
AgentToolUpdateCallback,
|
|
6
|
+
ToolApprovalDecision,
|
|
7
|
+
} from "@oh-my-pi/pi-agent-core";
|
|
8
|
+
import type { ComputerAction, ComputerSafetyCheck, ComputerToolCallMetadata } from "@oh-my-pi/pi-ai";
|
|
9
|
+
import type {
|
|
10
|
+
DesktopAction,
|
|
11
|
+
DesktopCapabilities,
|
|
12
|
+
DesktopCapture,
|
|
13
|
+
DesktopDisplay,
|
|
14
|
+
DesktopSessionOptions,
|
|
15
|
+
} from "@oh-my-pi/pi-natives";
|
|
16
|
+
import { prompt, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
17
|
+
import { type } from "arktype";
|
|
18
|
+
import computerDescription from "../prompts/tools/computer.md" with { type: "text" };
|
|
19
|
+
import { truncateForPrompt } from "./approval";
|
|
20
|
+
import { type ComputerController, ComputerSupervisor, registerComputerController } from "./computer/supervisor";
|
|
21
|
+
import type { ToolSession } from "./index";
|
|
22
|
+
import { ToolError, throwIfAborted } from "./tool-errors";
|
|
23
|
+
|
|
24
|
+
// Desktop actions cross the N-API boundary as i32; out-of-range JS numbers
|
|
25
|
+
// must fail closed here instead of truncating in the napi conversion.
|
|
26
|
+
const INT32_MIN = -2_147_483_648;
|
|
27
|
+
const INT32_MAX = 2_147_483_647;
|
|
28
|
+
|
|
29
|
+
const coordinateSchema = type("0 <= number.integer <= 2147483647");
|
|
30
|
+
const scrollDeltaSchema = type("-2147483648 <= number.integer <= 2147483647");
|
|
31
|
+
|
|
32
|
+
const pointSchema = type({
|
|
33
|
+
x: coordinateSchema.describe("x pixel coordinate"),
|
|
34
|
+
y: coordinateSchema.describe("y pixel coordinate"),
|
|
35
|
+
"+": "reject",
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const computerActionSchema = type({
|
|
39
|
+
type: type(
|
|
40
|
+
"'click' | 'double_click' | 'drag' | 'keypress' | 'move' | 'screenshot' | 'scroll' | 'type' | 'wait'",
|
|
41
|
+
).describe("action kind"),
|
|
42
|
+
"x?": coordinateSchema.describe(
|
|
43
|
+
"x pixel coordinate in the most recent screenshot (click, double_click, move, scroll)",
|
|
44
|
+
),
|
|
45
|
+
"y?": coordinateSchema.describe(
|
|
46
|
+
"y pixel coordinate in the most recent screenshot (click, double_click, move, scroll)",
|
|
47
|
+
),
|
|
48
|
+
"button?": type("'left' | 'right' | 'wheel' | 'back' | 'forward'").describe("mouse button; required for click"),
|
|
49
|
+
"path?": pointSchema.array().atLeastLength(2).describe("waypoints from press to release; required for drag"),
|
|
50
|
+
"keys?": type("string[] | null").describe(
|
|
51
|
+
"key names (e.g. CTRL, SHIFT, ENTER, A); required chord for keypress, optional held modifiers for pointer actions",
|
|
52
|
+
),
|
|
53
|
+
"scroll_x?": scrollDeltaSchema.describe("horizontal scroll delta in pixels; required for scroll"),
|
|
54
|
+
"scroll_y?": scrollDeltaSchema.describe(
|
|
55
|
+
"vertical scroll delta in pixels, positive scrolls content down; required for scroll",
|
|
56
|
+
),
|
|
57
|
+
"text?": type("string").describe("literal text to type; required for type"),
|
|
58
|
+
"+": "reject",
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const computerSchema = type({
|
|
62
|
+
"actions?": computerActionSchema
|
|
63
|
+
.array()
|
|
64
|
+
.describe("ordered actions executed as one batch; omit or pass [] to just capture a screenshot"),
|
|
65
|
+
"+": "reject",
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export type ComputerParams = typeof computerSchema.infer;
|
|
69
|
+
|
|
70
|
+
export interface ComputerToolDetails {
|
|
71
|
+
width: number;
|
|
72
|
+
height: number;
|
|
73
|
+
backend: DesktopCapture["backend"];
|
|
74
|
+
displayServer?: string;
|
|
75
|
+
capturePermission: string;
|
|
76
|
+
inputPermission: string;
|
|
77
|
+
displays: DesktopDisplay[];
|
|
78
|
+
capabilities?: DesktopCapabilities;
|
|
79
|
+
actions: ComputerAction["type"][];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type ComputerControllerFactory = (options: DesktopSessionOptions) => ComputerController;
|
|
83
|
+
|
|
84
|
+
function isInt32(value: unknown): value is number {
|
|
85
|
+
return typeof value === "number" && Number.isInteger(value) && value >= INT32_MIN && value <= INT32_MAX;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isCoordinate(value: unknown): value is number {
|
|
89
|
+
return isInt32(value) && value >= 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
type AllowedFields = Record<string, true>;
|
|
93
|
+
|
|
94
|
+
const POINT_FIELDS: AllowedFields = { x: true, y: true };
|
|
95
|
+
const MOUSE_BUTTONS: AllowedFields = { left: true, right: true, wheel: true, back: true, forward: true };
|
|
96
|
+
const ACTION_FIELDS: Record<ComputerAction["type"], AllowedFields> = {
|
|
97
|
+
click: { type: true, button: true, x: true, y: true, keys: true },
|
|
98
|
+
double_click: { type: true, x: true, y: true, keys: true },
|
|
99
|
+
drag: { type: true, path: true, keys: true },
|
|
100
|
+
keypress: { type: true, keys: true },
|
|
101
|
+
move: { type: true, x: true, y: true, keys: true },
|
|
102
|
+
screenshot: { type: true },
|
|
103
|
+
scroll: { type: true, x: true, y: true, scroll_x: true, scroll_y: true, keys: true },
|
|
104
|
+
type: { type: true, text: true },
|
|
105
|
+
wait: { type: true },
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
109
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function fieldsForAction(actionType: string): AllowedFields | undefined {
|
|
113
|
+
switch (actionType) {
|
|
114
|
+
case "click":
|
|
115
|
+
case "double_click":
|
|
116
|
+
case "drag":
|
|
117
|
+
case "keypress":
|
|
118
|
+
case "move":
|
|
119
|
+
case "screenshot":
|
|
120
|
+
case "scroll":
|
|
121
|
+
case "type":
|
|
122
|
+
case "wait":
|
|
123
|
+
return ACTION_FIELDS[actionType];
|
|
124
|
+
default:
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function hasOnlyFields(value: Record<string, unknown>, allowed: AllowedFields): boolean {
|
|
130
|
+
return Object.keys(value).every(key => allowed[key] === true);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function isPoint(value: unknown): value is { x: number; y: number } {
|
|
134
|
+
return isRecord(value) && hasOnlyFields(value, POINT_FIELDS) && isCoordinate(value.x) && isCoordinate(value.y);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function isStringArray(value: unknown): value is string[] {
|
|
138
|
+
return Array.isArray(value) && value.every(item => typeof item === "string");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function modifierBit(value: string): number {
|
|
142
|
+
switch (value.trim().toUpperCase()) {
|
|
143
|
+
case "CTRL":
|
|
144
|
+
case "CONTROL":
|
|
145
|
+
return 1;
|
|
146
|
+
case "SHIFT":
|
|
147
|
+
return 2;
|
|
148
|
+
case "ALT":
|
|
149
|
+
case "OPTION":
|
|
150
|
+
return 4;
|
|
151
|
+
case "META":
|
|
152
|
+
case "CMD":
|
|
153
|
+
case "COMMAND":
|
|
154
|
+
case "SUPER":
|
|
155
|
+
case "WINDOWS":
|
|
156
|
+
return 8;
|
|
157
|
+
default:
|
|
158
|
+
return 0;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function isModifierArray(value: unknown): value is string[] | null | undefined {
|
|
163
|
+
if (value == null) return true;
|
|
164
|
+
if (!isStringArray(value)) return false;
|
|
165
|
+
let seen = 0;
|
|
166
|
+
for (const entry of value) {
|
|
167
|
+
for (const component of entry.split("+")) {
|
|
168
|
+
const bit = modifierBit(component);
|
|
169
|
+
if (bit === 0 || (seen & bit) !== 0) return false;
|
|
170
|
+
seen |= bit;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function isKeypressArray(value: unknown): value is string[] {
|
|
177
|
+
return (
|
|
178
|
+
isStringArray(value) &&
|
|
179
|
+
value.length > 0 &&
|
|
180
|
+
value.every(key => key.split("+").every(component => component.trim().length > 0))
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function isComputerAction(value: unknown): value is ComputerAction {
|
|
185
|
+
if (!isRecord(value) || typeof value.type !== "string") return false;
|
|
186
|
+
const action = value;
|
|
187
|
+
const actionType = value.type;
|
|
188
|
+
const allowed = fieldsForAction(actionType);
|
|
189
|
+
if (!allowed || !hasOnlyFields(action, allowed)) return false;
|
|
190
|
+
switch (actionType) {
|
|
191
|
+
case "click":
|
|
192
|
+
return (
|
|
193
|
+
isCoordinate(action.x) &&
|
|
194
|
+
isCoordinate(action.y) &&
|
|
195
|
+
typeof action.button === "string" &&
|
|
196
|
+
MOUSE_BUTTONS[action.button] === true &&
|
|
197
|
+
isModifierArray(action.keys)
|
|
198
|
+
);
|
|
199
|
+
case "double_click":
|
|
200
|
+
return isCoordinate(action.x) && isCoordinate(action.y) && isModifierArray(action.keys);
|
|
201
|
+
case "drag":
|
|
202
|
+
return (
|
|
203
|
+
Array.isArray(action.path) &&
|
|
204
|
+
action.path.length >= 2 &&
|
|
205
|
+
action.path.every(isPoint) &&
|
|
206
|
+
isModifierArray(action.keys)
|
|
207
|
+
);
|
|
208
|
+
case "keypress":
|
|
209
|
+
return isKeypressArray(action.keys);
|
|
210
|
+
case "move":
|
|
211
|
+
return isCoordinate(action.x) && isCoordinate(action.y) && isModifierArray(action.keys);
|
|
212
|
+
case "screenshot":
|
|
213
|
+
case "wait":
|
|
214
|
+
return true;
|
|
215
|
+
case "scroll":
|
|
216
|
+
return (
|
|
217
|
+
isCoordinate(action.x) &&
|
|
218
|
+
isCoordinate(action.y) &&
|
|
219
|
+
isInt32(action.scroll_x) &&
|
|
220
|
+
isInt32(action.scroll_y) &&
|
|
221
|
+
isModifierArray(action.keys)
|
|
222
|
+
);
|
|
223
|
+
case "type":
|
|
224
|
+
return typeof action.text === "string";
|
|
225
|
+
default:
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function parseActions(value: unknown): ComputerAction[] {
|
|
231
|
+
// Missing or empty action batches degrade to a plain screenshot so a
|
|
232
|
+
// function-calling model can observe the screen before acting.
|
|
233
|
+
if (value === undefined) return [{ type: "screenshot" }];
|
|
234
|
+
if (!Array.isArray(value)) throw new ToolError("Computer call requires an array of actions");
|
|
235
|
+
if (value.length === 0) return [{ type: "screenshot" }];
|
|
236
|
+
if (!value.every(isComputerAction)) throw new ToolError("Computer call contains an invalid action");
|
|
237
|
+
return value;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function toDesktopAction(action: ComputerAction): DesktopAction {
|
|
241
|
+
switch (action.type) {
|
|
242
|
+
case "click":
|
|
243
|
+
return {
|
|
244
|
+
type: "click",
|
|
245
|
+
x: action.x,
|
|
246
|
+
y: action.y,
|
|
247
|
+
button: action.button,
|
|
248
|
+
...(action.keys ? { keys: action.keys } : {}),
|
|
249
|
+
};
|
|
250
|
+
case "double_click":
|
|
251
|
+
return {
|
|
252
|
+
type: "double_click",
|
|
253
|
+
x: action.x,
|
|
254
|
+
y: action.y,
|
|
255
|
+
...(action.keys ? { keys: action.keys } : {}),
|
|
256
|
+
};
|
|
257
|
+
case "drag":
|
|
258
|
+
return { type: "drag", path: action.path, ...(action.keys ? { keys: action.keys } : {}) };
|
|
259
|
+
case "keypress":
|
|
260
|
+
return { type: "keypress", keys: action.keys };
|
|
261
|
+
case "move":
|
|
262
|
+
return { type: "move", x: action.x, y: action.y, ...(action.keys ? { keys: action.keys } : {}) };
|
|
263
|
+
case "screenshot":
|
|
264
|
+
return { type: "screenshot" };
|
|
265
|
+
case "scroll":
|
|
266
|
+
return {
|
|
267
|
+
type: "scroll",
|
|
268
|
+
x: action.x,
|
|
269
|
+
y: action.y,
|
|
270
|
+
scroll_x: action.scroll_x,
|
|
271
|
+
scroll_y: action.scroll_y,
|
|
272
|
+
...(action.keys ? { keys: action.keys } : {}),
|
|
273
|
+
};
|
|
274
|
+
case "type":
|
|
275
|
+
return { type: "type", text: action.text };
|
|
276
|
+
case "wait":
|
|
277
|
+
return { type: "wait" };
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function callMetadata(context: AgentToolContext | undefined): ComputerToolCallMetadata | undefined {
|
|
282
|
+
const metadata = context?.toolCall?.providerMetadata;
|
|
283
|
+
return metadata?.type === "computer" ? metadata : undefined;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function computerApproval(args: unknown): ToolApprovalDecision {
|
|
287
|
+
const actions =
|
|
288
|
+
args && typeof args === "object" && "actions" in args ? (args as { actions?: unknown }).actions : undefined;
|
|
289
|
+
if (!Array.isArray(actions)) return "exec";
|
|
290
|
+
return actions.every(action => {
|
|
291
|
+
if (!action || typeof action !== "object") return false;
|
|
292
|
+
const actionType = (action as { type?: unknown }).type;
|
|
293
|
+
return actionType === "screenshot" || actionType === "wait";
|
|
294
|
+
})
|
|
295
|
+
? "read"
|
|
296
|
+
: "exec";
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function modifierSummary(keys: unknown): string {
|
|
300
|
+
return isStringArray(keys) && keys.length > 0 ? ` keys=${JSON.stringify(keys)}` : "";
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function approvalActionSummary(actions: unknown): string[] {
|
|
304
|
+
if (!Array.isArray(actions)) return ["Actions: unavailable"];
|
|
305
|
+
const lines = actions.slice(0, 12).map((value, index) => {
|
|
306
|
+
if (!value || typeof value !== "object") return `${index + 1}. invalid`;
|
|
307
|
+
const action = value as Record<string, unknown>;
|
|
308
|
+
const type = typeof action.type === "string" ? action.type : "invalid";
|
|
309
|
+
let detail: string;
|
|
310
|
+
switch (type) {
|
|
311
|
+
case "click":
|
|
312
|
+
detail = `click button=${String(action.button)} at (${String(action.x)}, ${String(action.y)})${modifierSummary(action.keys)}`;
|
|
313
|
+
break;
|
|
314
|
+
case "double_click":
|
|
315
|
+
detail = `double_click at (${String(action.x)}, ${String(action.y)})${modifierSummary(action.keys)}`;
|
|
316
|
+
break;
|
|
317
|
+
case "drag":
|
|
318
|
+
detail = `drag path=${Array.isArray(action.path) ? action.path.map(point => (isPoint(point) ? `(${point.x}, ${point.y})` : "invalid")).join(" -> ") : "invalid"}${modifierSummary(action.keys)}`;
|
|
319
|
+
break;
|
|
320
|
+
case "keypress":
|
|
321
|
+
detail = `keypress keys=${JSON.stringify(action.keys)}`;
|
|
322
|
+
break;
|
|
323
|
+
case "move":
|
|
324
|
+
detail = `move to (${String(action.x)}, ${String(action.y)})${modifierSummary(action.keys)}`;
|
|
325
|
+
break;
|
|
326
|
+
case "scroll":
|
|
327
|
+
detail = `scroll at (${String(action.x)}, ${String(action.y)}) delta=(${String(action.scroll_x)}, ${String(action.scroll_y)})${modifierSummary(action.keys)}`;
|
|
328
|
+
break;
|
|
329
|
+
case "type":
|
|
330
|
+
detail = `type text=${JSON.stringify(action.text)}`;
|
|
331
|
+
break;
|
|
332
|
+
case "screenshot":
|
|
333
|
+
case "wait":
|
|
334
|
+
detail = type;
|
|
335
|
+
break;
|
|
336
|
+
default:
|
|
337
|
+
detail = type;
|
|
338
|
+
}
|
|
339
|
+
return truncateForPrompt(sanitizeText(`${index + 1}. ${detail}`).replace(/[\r\n\t]+/g, " "), 240);
|
|
340
|
+
});
|
|
341
|
+
if (actions.length > 12) lines.push(`+${actions.length - 12} more actions`);
|
|
342
|
+
return truncateForPrompt(lines.join("\n"), 2_000).split("\n");
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export class ComputerTool implements AgentTool<typeof computerSchema, ComputerToolDetails> {
|
|
346
|
+
readonly name = "computer";
|
|
347
|
+
readonly native = { type: "computer" } as const;
|
|
348
|
+
readonly label = "Computer";
|
|
349
|
+
readonly loadMode = "essential" as const;
|
|
350
|
+
readonly concurrency = "exclusive" as const;
|
|
351
|
+
readonly summary = "Capture and control the host desktop through native OS APIs";
|
|
352
|
+
readonly parameters = computerSchema;
|
|
353
|
+
readonly strict = false;
|
|
354
|
+
readonly approval = computerApproval;
|
|
355
|
+
readonly formatApprovalDetails = (args: unknown): string[] => {
|
|
356
|
+
const actions = args && typeof args === "object" ? (args as { actions?: unknown }).actions : undefined;
|
|
357
|
+
return approvalActionSummary(actions);
|
|
358
|
+
};
|
|
359
|
+
readonly #controller: ComputerController;
|
|
360
|
+
readonly #unregisterOwner: () => void;
|
|
361
|
+
#closed = false;
|
|
362
|
+
#description?: string;
|
|
363
|
+
|
|
364
|
+
constructor(
|
|
365
|
+
readonly session: ToolSession,
|
|
366
|
+
createController: ComputerControllerFactory = options => new ComputerSupervisor(options),
|
|
367
|
+
) {
|
|
368
|
+
this.#controller = createController({
|
|
369
|
+
backend: session.settings.get("computer.backend"),
|
|
370
|
+
display: session.settings.get("computer.display"),
|
|
371
|
+
maxWidth: session.settings.get("computer.maxWidth"),
|
|
372
|
+
maxHeight: session.settings.get("computer.maxHeight"),
|
|
373
|
+
});
|
|
374
|
+
this.#unregisterOwner = registerComputerController(
|
|
375
|
+
session.getEvalKernelOwnerId?.() ?? undefined,
|
|
376
|
+
this.#controller,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
get description(): string {
|
|
380
|
+
this.#description ??= prompt.render(computerDescription);
|
|
381
|
+
return this.#description;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async execute(
|
|
385
|
+
_toolCallId: string,
|
|
386
|
+
params: ComputerParams,
|
|
387
|
+
signal?: AbortSignal,
|
|
388
|
+
_onUpdate?: AgentToolUpdateCallback<ComputerToolDetails>,
|
|
389
|
+
context?: AgentToolContext,
|
|
390
|
+
): Promise<AgentToolResult<ComputerToolDetails>> {
|
|
391
|
+
throwIfAborted(signal);
|
|
392
|
+
if (this.#closed) throw new ToolError("Computer session is closed");
|
|
393
|
+
const metadata = callMetadata(context);
|
|
394
|
+
const actions = parseActions(metadata?.actions ?? params.actions);
|
|
395
|
+
const pendingSafetyChecks: ComputerSafetyCheck[] = metadata?.pendingSafetyChecks ?? [];
|
|
396
|
+
if (pendingSafetyChecks.length > 0 && context?.providerSafetyApproved !== true) {
|
|
397
|
+
throw new ToolError("Provider safety checks require interactive approval before computer input");
|
|
398
|
+
}
|
|
399
|
+
const capture = await this.#controller.execute(actions.map(toDesktopAction), signal);
|
|
400
|
+
throwIfAborted(signal);
|
|
401
|
+
const data = Buffer.from(capture.data).toBase64();
|
|
402
|
+
return {
|
|
403
|
+
content: [{ type: "image", data, mimeType: "image/png", detail: "original" }],
|
|
404
|
+
details: {
|
|
405
|
+
width: capture.width,
|
|
406
|
+
height: capture.height,
|
|
407
|
+
backend: capture.backend,
|
|
408
|
+
displayServer: capture.displayServer,
|
|
409
|
+
capturePermission: capture.capturePermission,
|
|
410
|
+
inputPermission: capture.inputPermission,
|
|
411
|
+
displays: capture.displays,
|
|
412
|
+
capabilities: this.#controller.capabilities,
|
|
413
|
+
actions: actions.map(action => action.type),
|
|
414
|
+
},
|
|
415
|
+
...(metadata
|
|
416
|
+
? {
|
|
417
|
+
providerMetadata: {
|
|
418
|
+
type: "computer" as const,
|
|
419
|
+
screenshot: { type: "computer_screenshot" as const, image_url: `data:image/png;base64,${data}` },
|
|
420
|
+
acknowledgedSafetyChecks: pendingSafetyChecks,
|
|
421
|
+
},
|
|
422
|
+
}
|
|
423
|
+
: {}),
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async close(): Promise<void> {
|
|
428
|
+
if (this.#closed) return;
|
|
429
|
+
this.#closed = true;
|
|
430
|
+
this.#unregisterOwner();
|
|
431
|
+
await this.#controller.close();
|
|
432
|
+
}
|
|
433
|
+
}
|
package/src/tools/context.ts
CHANGED
|
@@ -13,6 +13,8 @@ declare module "@oh-my-pi/pi-agent-core" {
|
|
|
13
13
|
* wrapper must not re-prompt for the same action (explicit per-tool
|
|
14
14
|
* policies and overrides still apply). */
|
|
15
15
|
xdevApproved?: boolean;
|
|
16
|
+
/** Set only after an interactive prompt approves provider computer safety checks. */
|
|
17
|
+
providerSafetyApproved?: boolean;
|
|
16
18
|
}
|
|
17
19
|
}
|
|
18
20
|
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { Component } from "@oh-my-pi/pi-tui";
|
|
2
|
+
import { isRecord } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
4
|
+
import type { Theme } from "../modes/theme/theme";
|
|
5
|
+
import { renderStatusLine, WidthAwareText } from "../tui";
|
|
6
|
+
import {
|
|
7
|
+
formatArgsInline,
|
|
8
|
+
JSON_TREE_MAX_DEPTH_COLLAPSED,
|
|
9
|
+
JSON_TREE_MAX_DEPTH_EXPANDED,
|
|
10
|
+
JSON_TREE_MAX_LINES_COLLAPSED,
|
|
11
|
+
JSON_TREE_MAX_LINES_EXPANDED,
|
|
12
|
+
JSON_TREE_SCALAR_LEN_COLLAPSED,
|
|
13
|
+
JSON_TREE_SCALAR_LEN_EXPANDED,
|
|
14
|
+
renderJsonTreeLines,
|
|
15
|
+
} from "./json-tree";
|
|
16
|
+
import { formatExpandHint, replaceTabs, truncateToWidth } from "./render-utils";
|
|
17
|
+
|
|
18
|
+
/** Inputs rendered by the fallback card used when a tool has no bespoke renderer. */
|
|
19
|
+
export interface DefaultToolRenderInput {
|
|
20
|
+
/** Human-readable tool label. */
|
|
21
|
+
label: string;
|
|
22
|
+
/** Tool arguments, shown inline when collapsed and as a tree when expanded. */
|
|
23
|
+
args: unknown;
|
|
24
|
+
/** Settled or streaming result; omitted while only the call is available. */
|
|
25
|
+
result?: {
|
|
26
|
+
output: string;
|
|
27
|
+
isError?: boolean;
|
|
28
|
+
};
|
|
29
|
+
/** Current expansion and lifecycle state. */
|
|
30
|
+
options: RenderResultOptions;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Format one generic tool call/result card at the available content width. */
|
|
34
|
+
export function formatDefaultToolExecution(
|
|
35
|
+
input: DefaultToolRenderInput,
|
|
36
|
+
contentWidth: number,
|
|
37
|
+
uiTheme: Theme,
|
|
38
|
+
): string {
|
|
39
|
+
const lines: string[] = [];
|
|
40
|
+
const { options, result } = input;
|
|
41
|
+
const icon = options.isPartial
|
|
42
|
+
? options.spinnerFrame !== undefined
|
|
43
|
+
? "running"
|
|
44
|
+
: "pending"
|
|
45
|
+
: result?.isError
|
|
46
|
+
? "error"
|
|
47
|
+
: "done";
|
|
48
|
+
lines.push(renderStatusLine({ icon, spinnerFrame: options.spinnerFrame, title: input.label }, uiTheme));
|
|
49
|
+
|
|
50
|
+
const args = isRecord(input.args) ? input.args : undefined;
|
|
51
|
+
if (!options.expanded && args && Object.keys(args).length > 0) {
|
|
52
|
+
const inlineBudget = Math.max(20, contentWidth - Bun.stringWidth(uiTheme.tree.last) - 2);
|
|
53
|
+
const preview = formatArgsInline(args, inlineBudget);
|
|
54
|
+
if (preview) {
|
|
55
|
+
lines.push(` ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("dim", preview)}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (options.expanded && input.args !== undefined) {
|
|
60
|
+
lines.push("");
|
|
61
|
+
lines.push(uiTheme.fg("dim", "Args"));
|
|
62
|
+
const tree = renderJsonTreeLines(
|
|
63
|
+
input.args,
|
|
64
|
+
uiTheme,
|
|
65
|
+
JSON_TREE_MAX_DEPTH_EXPANDED,
|
|
66
|
+
JSON_TREE_MAX_LINES_EXPANDED,
|
|
67
|
+
JSON_TREE_SCALAR_LEN_EXPANDED,
|
|
68
|
+
);
|
|
69
|
+
lines.push(...tree.lines);
|
|
70
|
+
if (tree.truncated) {
|
|
71
|
+
lines.push(uiTheme.fg("dim", "…"));
|
|
72
|
+
}
|
|
73
|
+
lines.push("");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!result) {
|
|
77
|
+
return lines.join("\n");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const textContent = result.output.trimEnd();
|
|
81
|
+
if (!textContent) {
|
|
82
|
+
lines.push(uiTheme.fg("dim", "(no output)"));
|
|
83
|
+
return lines.join("\n");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (textContent.startsWith("{") || textContent.startsWith("[")) {
|
|
87
|
+
try {
|
|
88
|
+
const parsed = JSON.parse(textContent);
|
|
89
|
+
const maxDepth = options.expanded ? JSON_TREE_MAX_DEPTH_EXPANDED : JSON_TREE_MAX_DEPTH_COLLAPSED;
|
|
90
|
+
const maxLines = options.expanded ? JSON_TREE_MAX_LINES_EXPANDED : JSON_TREE_MAX_LINES_COLLAPSED;
|
|
91
|
+
const maxScalarLen = options.expanded ? JSON_TREE_SCALAR_LEN_EXPANDED : JSON_TREE_SCALAR_LEN_COLLAPSED;
|
|
92
|
+
const tree = renderJsonTreeLines(parsed, uiTheme, maxDepth, maxLines, maxScalarLen);
|
|
93
|
+
|
|
94
|
+
if (tree.lines.length > 0) {
|
|
95
|
+
lines.push(...tree.lines);
|
|
96
|
+
if (!options.expanded) {
|
|
97
|
+
lines.push(formatExpandHint(uiTheme, options.expanded, true));
|
|
98
|
+
} else if (tree.truncated) {
|
|
99
|
+
lines.push(uiTheme.fg("dim", "…"));
|
|
100
|
+
}
|
|
101
|
+
return lines.join("\n");
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
// Non-JSON output that starts with a bracket is rendered as plain text.
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const outputLines = textContent.split("\n");
|
|
109
|
+
const maxOutputLines = options.expanded ? 12 : 4;
|
|
110
|
+
const displayLines = outputLines.slice(0, maxOutputLines);
|
|
111
|
+
|
|
112
|
+
for (const line of displayLines) {
|
|
113
|
+
lines.push(uiTheme.fg("toolOutput", truncateToWidth(replaceTabs(line), contentWidth)));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (outputLines.length > maxOutputLines) {
|
|
117
|
+
const remaining = outputLines.length - maxOutputLines;
|
|
118
|
+
lines.push(
|
|
119
|
+
`${uiTheme.fg("dim", `… ${remaining} more lines`)} ${formatExpandHint(uiTheme, options.expanded, true)}`,
|
|
120
|
+
);
|
|
121
|
+
} else if (!options.expanded) {
|
|
122
|
+
lines.push(formatExpandHint(uiTheme, options.expanded, true));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return lines.join("\n");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Render the generic fallback as the state-tinted card used by direct custom tools. */
|
|
129
|
+
export function renderDefaultToolExecution(input: DefaultToolRenderInput, uiTheme: Theme): Component {
|
|
130
|
+
const component = new WidthAwareText(contentWidth => formatDefaultToolExecution(input, contentWidth, uiTheme), 1, 1);
|
|
131
|
+
const background = input.options.isPartial
|
|
132
|
+
? "toolPendingBg"
|
|
133
|
+
: input.result?.isError
|
|
134
|
+
? "toolErrorBg"
|
|
135
|
+
: "toolSuccessBg";
|
|
136
|
+
component.setCustomBgFn(text => uiTheme.bg(background, text));
|
|
137
|
+
component.setIgnoreTight(true);
|
|
138
|
+
return component;
|
|
139
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { EvalLanguage } from "../../eval/types";
|
|
2
|
+
import { formatJavaScriptForDisplay } from "./javascript";
|
|
3
|
+
import { formatJuliaForDisplay } from "./julia";
|
|
4
|
+
import { formatPythonForDisplay } from "./python";
|
|
5
|
+
import { formatRubyForDisplay } from "./ruby";
|
|
6
|
+
|
|
7
|
+
export * from "./javascript";
|
|
8
|
+
export * from "./julia";
|
|
9
|
+
export * from "./python";
|
|
10
|
+
export * from "./ruby";
|
|
11
|
+
|
|
12
|
+
/** Formats an arbitrary eval-code prefix for display without changing the executed source. */
|
|
13
|
+
export function formatEvalCodeForDisplay(source: string, language: EvalLanguage): string {
|
|
14
|
+
switch (language) {
|
|
15
|
+
case "js":
|
|
16
|
+
return formatJavaScriptForDisplay(source);
|
|
17
|
+
case "ruby":
|
|
18
|
+
return formatRubyForDisplay(source);
|
|
19
|
+
case "julia":
|
|
20
|
+
return formatJuliaForDisplay(source);
|
|
21
|
+
case "python":
|
|
22
|
+
return formatPythonForDisplay(source);
|
|
23
|
+
}
|
|
24
|
+
}
|