@aiscene/shared 8.0.2 → 8.0.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/dist/es/agent-tools/agent-behavior-init-args.mjs +44 -0
- package/dist/es/agent-tools/base-tools.mjs +163 -0
- package/dist/es/agent-tools/chrome-path.mjs +50 -0
- package/dist/es/agent-tools/cli-report-session.mjs +78 -0
- package/dist/es/agent-tools/error-formatter.mjs +106 -0
- package/dist/es/agent-tools/index.mjs +9 -0
- package/dist/es/agent-tools/init-arg-utils.mjs +38 -0
- package/dist/es/agent-tools/observation-artifact.mjs +5 -0
- package/dist/es/agent-tools/observation-record.mjs +248 -0
- package/dist/es/agent-tools/tool-defaults.mjs +54 -0
- package/dist/es/agent-tools/tool-generator.mjs +529 -0
- package/dist/es/agent-tools/types.mjs +3 -0
- package/dist/es/agent-tools/user-prompt.mjs +66 -0
- package/dist/es/cli/cli-args.mjs +95 -0
- package/dist/es/cli/cli-error.mjs +24 -0
- package/dist/es/cli/cli-runner.mjs +10 -40
- package/dist/es/cli/index.mjs +4 -2
- package/dist/es/cli/interrupt.mjs +116 -0
- package/dist/es/cli/record-command.mjs +130 -0
- package/dist/es/cli/screenshot-file.mjs +24 -0
- package/dist/es/cli/verbose-ai-act.mjs +230 -0
- package/dist/es/cli/verbose-screenshot.mjs +134 -0
- package/dist/es/cli/verbose.mjs +404 -0
- package/dist/es/constants/example-code.mjs +2 -2
- package/dist/es/constants/index.mjs +6 -1
- package/dist/es/env/model-config-manager.mjs +3 -0
- package/dist/es/env/parse-model-config.mjs +6 -1
- package/dist/es/env/types.mjs +4 -1
- package/dist/es/env/utils.mjs +1 -9
- package/dist/es/extractor/index.mjs +2 -2
- package/dist/es/img/index.mjs +2 -2
- package/dist/es/img/info.mjs +58 -17
- package/dist/es/img/photon-loader.mjs +4 -0
- package/dist/es/key-alias-utils.mjs +19 -0
- package/dist/es/mcp/base-server.mjs +10 -9
- package/dist/es/mcp/base-tools.mjs +72 -4
- package/dist/es/mcp/cli-report-session.mjs +78 -0
- package/dist/es/mcp/error-formatter.mjs +19 -0
- package/dist/es/mcp/index.mjs +2 -0
- package/dist/es/mcp/init-arg-utils.mjs +38 -0
- package/dist/es/mcp/inject-report-html-plugin.mjs +1 -1
- package/dist/es/mcp/tool-generator.mjs +65 -17
- package/dist/es/node/fs.mjs +1 -1
- package/dist/es/recorder.mjs +249 -0
- package/dist/lib/agent-tools/agent-behavior-init-args.js +87 -0
- package/dist/lib/agent-tools/base-tools.js +197 -0
- package/dist/lib/agent-tools/chrome-path.js +87 -0
- package/dist/lib/agent-tools/cli-report-session.js +121 -0
- package/dist/lib/agent-tools/error-formatter.js +149 -0
- package/dist/lib/agent-tools/index.js +114 -0
- package/dist/lib/agent-tools/init-arg-utils.js +78 -0
- package/dist/lib/agent-tools/observation-artifact.js +42 -0
- package/dist/lib/agent-tools/observation-record.js +297 -0
- package/dist/lib/agent-tools/tool-defaults.js +97 -0
- package/dist/lib/agent-tools/tool-generator.js +569 -0
- package/dist/lib/agent-tools/types.js +40 -0
- package/dist/lib/agent-tools/user-prompt.js +103 -0
- package/dist/lib/cli/cli-args.js +138 -0
- package/dist/lib/cli/cli-error.js +61 -0
- package/dist/lib/cli/cli-runner.js +19 -46
- package/dist/lib/cli/index.js +8 -3
- package/dist/lib/cli/interrupt.js +156 -0
- package/dist/lib/cli/record-command.js +164 -0
- package/dist/lib/cli/screenshot-file.js +58 -0
- package/dist/lib/cli/verbose-ai-act.js +273 -0
- package/dist/lib/cli/verbose-screenshot.js +177 -0
- package/dist/lib/cli/verbose.js +465 -0
- package/dist/lib/constants/example-code.js +2 -2
- package/dist/lib/constants/index.js +23 -3
- package/dist/lib/env/model-config-manager.js +3 -0
- package/dist/lib/env/parse-model-config.js +6 -1
- package/dist/lib/env/types.js +9 -0
- package/dist/lib/env/utils.js +0 -11
- package/dist/lib/extractor/index.js +3 -0
- package/dist/lib/img/index.js +3 -0
- package/dist/lib/img/info.js +65 -18
- package/dist/lib/img/photon-loader.js +38 -0
- package/dist/lib/key-alias-utils.js +62 -0
- package/dist/lib/mcp/base-server.js +11 -10
- package/dist/lib/mcp/base-tools.js +74 -6
- package/dist/lib/mcp/cli-report-session.js +121 -0
- package/dist/lib/mcp/error-formatter.js +53 -0
- package/dist/lib/mcp/index.js +24 -10
- package/dist/lib/mcp/init-arg-utils.js +78 -0
- package/dist/lib/mcp/inject-report-html-plugin.js +1 -1
- package/dist/lib/mcp/tool-generator.js +74 -26
- package/dist/lib/node/fs.js +1 -1
- package/dist/lib/recorder.js +307 -0
- package/dist/types/agent-tools/agent-behavior-init-args.d.ts +17 -0
- package/dist/types/agent-tools/base-tools.d.ts +158 -0
- package/dist/types/agent-tools/chrome-path.d.ts +2 -0
- package/dist/types/agent-tools/cli-report-session.d.ts +12 -0
- package/dist/types/agent-tools/error-formatter.d.ts +30 -0
- package/dist/types/agent-tools/index.d.ts +9 -0
- package/dist/types/agent-tools/init-arg-utils.d.ts +13 -0
- package/dist/types/agent-tools/observation-artifact.d.ts +10 -0
- package/dist/types/agent-tools/observation-record.d.ts +38 -0
- package/dist/types/agent-tools/tool-defaults.d.ts +63 -0
- package/dist/types/agent-tools/tool-generator.d.ts +13 -0
- package/dist/types/agent-tools/types.d.ts +213 -0
- package/dist/types/agent-tools/user-prompt.d.ts +13 -0
- package/dist/types/cli/cli-args.d.ts +8 -0
- package/dist/types/cli/cli-error.d.ts +5 -0
- package/dist/types/cli/cli-runner.d.ts +4 -7
- package/dist/types/cli/index.d.ts +3 -1
- package/dist/types/cli/interrupt.d.ts +49 -0
- package/dist/types/cli/record-command.d.ts +3 -0
- package/dist/types/cli/screenshot-file.d.ts +10 -0
- package/dist/types/cli/verbose-ai-act.d.ts +44 -0
- package/dist/types/cli/verbose-screenshot.d.ts +10 -0
- package/dist/types/cli/verbose.d.ts +40 -0
- package/dist/types/constants/example-code.d.ts +1 -1
- package/dist/types/constants/index.d.ts +5 -0
- package/dist/types/env/types.d.ts +14 -5
- package/dist/types/env/utils.d.ts +0 -31
- package/dist/types/extractor/index.d.ts +1 -1
- package/dist/types/img/index.d.ts +1 -1
- package/dist/types/img/info.d.ts +7 -0
- package/dist/types/img/photon-loader.d.ts +2 -0
- package/dist/types/key-alias-utils.d.ts +9 -0
- package/dist/types/mcp/base-tools.d.ts +74 -5
- package/dist/types/mcp/cli-report-session.d.ts +12 -0
- package/dist/types/mcp/error-formatter.d.ts +12 -0
- package/dist/types/mcp/index.d.ts +2 -0
- package/dist/types/mcp/init-arg-utils.d.ts +13 -0
- package/dist/types/mcp/inject-report-html-plugin.d.ts +2 -2
- package/dist/types/mcp/launcher-helper.d.ts +3 -3
- package/dist/types/mcp/tool-generator.d.ts +3 -3
- package/dist/types/mcp/types.d.ts +10 -0
- package/dist/types/recorder.d.ts +113 -0
- package/package.json +1 -1
- package/src/agent-tools/agent-behavior-init-args.ts +109 -0
- package/src/agent-tools/base-tools.ts +399 -0
- package/src/agent-tools/chrome-path.ts +74 -0
- package/src/agent-tools/cli-report-session.ts +130 -0
- package/src/agent-tools/error-formatter.ts +177 -0
- package/src/agent-tools/index.ts +9 -0
- package/src/agent-tools/init-arg-utils.ts +105 -0
- package/src/agent-tools/observation-artifact.ts +29 -0
- package/src/agent-tools/observation-record.ts +331 -0
- package/src/agent-tools/tool-defaults.ts +119 -0
- package/src/agent-tools/tool-generator.ts +866 -0
- package/src/agent-tools/types.ts +250 -0
- package/src/agent-tools/user-prompt.ts +102 -0
- package/src/cli/cli-args.ts +173 -0
- package/src/cli/cli-error.ts +24 -0
- package/src/cli/cli-runner.ts +37 -56
- package/src/cli/index.ts +3 -7
- package/src/cli/interrupt.ts +207 -0
- package/src/cli/record-command.ts +177 -0
- package/src/cli/screenshot-file.ts +61 -0
- package/src/cli/verbose-ai-act.ts +387 -0
- package/src/cli/verbose-screenshot.ts +269 -0
- package/src/cli/verbose.ts +753 -0
- package/src/constants/example-code.ts +2 -2
- package/src/constants/index.ts +10 -0
- package/src/env/model-config-manager.ts +3 -0
- package/src/env/parse-model-config.ts +19 -1
- package/src/env/types.ts +13 -2
- package/src/env/utils.ts +0 -50
- package/src/extractor/index.ts +1 -1
- package/src/img/index.ts +14 -0
- package/src/img/info.ts +97 -45
- package/src/img/photon-loader.ts +5 -0
- package/src/img/transform.ts +261 -2
- package/src/key-alias-utils.ts +23 -0
- package/src/mcp/base-server.ts +10 -12
- package/src/mcp/base-tools.ts +218 -11
- package/src/mcp/cli-report-session.ts +130 -0
- package/src/mcp/error-formatter.ts +52 -0
- package/src/mcp/index.ts +2 -0
- package/src/mcp/init-arg-utils.ts +105 -0
- package/src/mcp/inject-report-html-plugin.ts +3 -3
- package/src/mcp/launcher-helper.ts +3 -3
- package/src/mcp/tool-generator.ts +98 -20
- package/src/mcp/types.ts +18 -1
- package/src/recorder.ts +625 -0
|
@@ -0,0 +1,866 @@
|
|
|
1
|
+
import { parseBase64 } from '@aiscene/shared/img';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import {
|
|
4
|
+
attachCliVerboseDumpListener,
|
|
5
|
+
emitCliVerboseEvent,
|
|
6
|
+
} from '../cli/verbose';
|
|
7
|
+
import {
|
|
8
|
+
getZodDescription,
|
|
9
|
+
getZodTypeName,
|
|
10
|
+
isMidsceneLocatorField,
|
|
11
|
+
unwrapZodField,
|
|
12
|
+
} from '../zod-schema-utils';
|
|
13
|
+
import { getErrorMessage } from './error-formatter';
|
|
14
|
+
import { resolveObservationArtifactAdapter } from './observation-artifact';
|
|
15
|
+
import { readUIObservationRecord } from './observation-record';
|
|
16
|
+
import type { ToolDefaults } from './tool-defaults';
|
|
17
|
+
import type {
|
|
18
|
+
ActionSpaceItem,
|
|
19
|
+
BaseAgent,
|
|
20
|
+
ToolCliMetadata,
|
|
21
|
+
ToolDefinition,
|
|
22
|
+
ToolResult,
|
|
23
|
+
ToolSchema,
|
|
24
|
+
} from './types';
|
|
25
|
+
import { composeUserPrompt, promptInputExtraSchema } from './user-prompt';
|
|
26
|
+
|
|
27
|
+
export { composeUserPrompt };
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Generate tool description from ActionSpaceItem.
|
|
31
|
+
* Format: "actionName action, description. Parameters: param1 (type) - desc; param2 (type) - desc"
|
|
32
|
+
*/
|
|
33
|
+
function describeActionForTool(action: ActionSpaceItem): string {
|
|
34
|
+
const actionDesc = action.description || `Execute ${action.name} action`;
|
|
35
|
+
|
|
36
|
+
if (!action.paramSchema) {
|
|
37
|
+
return `${action.name} action, ${actionDesc}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const shape = getZodObjectShape(action.paramSchema);
|
|
41
|
+
if (!shape) {
|
|
42
|
+
// Simple type schema
|
|
43
|
+
const typeName = getZodTypeName(action.paramSchema);
|
|
44
|
+
const description = getZodDescription(action.paramSchema as z.ZodTypeAny);
|
|
45
|
+
const paramDesc = description ? `${typeName} - ${description}` : typeName;
|
|
46
|
+
return `${action.name} action, ${actionDesc}. Parameter: ${paramDesc}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Object schema with multiple fields
|
|
50
|
+
const paramDescriptions: string[] = [];
|
|
51
|
+
for (const [key, field] of Object.entries(shape)) {
|
|
52
|
+
if (field && typeof field === 'object') {
|
|
53
|
+
const isFieldOptional =
|
|
54
|
+
typeof (field as { isOptional?: () => boolean }).isOptional ===
|
|
55
|
+
'function' && (field as { isOptional: () => boolean }).isOptional();
|
|
56
|
+
const typeName = getZodTypeName(field);
|
|
57
|
+
const description = getZodDescription(field as z.ZodTypeAny);
|
|
58
|
+
|
|
59
|
+
let paramStr = `${key}${isFieldOptional ? '?' : ''} (${typeName})`;
|
|
60
|
+
if (description) {
|
|
61
|
+
paramStr += ` - ${description}`;
|
|
62
|
+
}
|
|
63
|
+
paramDescriptions.push(paramStr);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (paramDescriptions.length === 0) {
|
|
68
|
+
return `${action.name} action, ${actionDesc}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return `${action.name} action, ${actionDesc}. Parameters: ${paramDescriptions.join('; ')}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Type guard: check if a Zod type is ZodOptional
|
|
76
|
+
*/
|
|
77
|
+
function isZodOptional(
|
|
78
|
+
value: z.ZodTypeAny,
|
|
79
|
+
): value is z.ZodOptional<z.ZodTypeAny> {
|
|
80
|
+
return '_def' in value && value._def?.typeName === 'ZodOptional';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Type guard: check if a Zod type is ZodObject
|
|
85
|
+
*/
|
|
86
|
+
function isZodObject(value: z.ZodTypeAny): value is z.ZodObject<z.ZodRawShape> {
|
|
87
|
+
return (
|
|
88
|
+
'_def' in value && value._def?.typeName === 'ZodObject' && 'shape' in value
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Unwrap ZodOptional to get inner type
|
|
94
|
+
*/
|
|
95
|
+
function unwrapOptional(value: z.ZodTypeAny): {
|
|
96
|
+
innerValue: z.ZodTypeAny;
|
|
97
|
+
isOptional: boolean;
|
|
98
|
+
} {
|
|
99
|
+
if (isZodOptional(value)) {
|
|
100
|
+
return { innerValue: value._def.innerType, isOptional: true };
|
|
101
|
+
}
|
|
102
|
+
return { innerValue: value, isOptional: false };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function getZodObjectShape(
|
|
106
|
+
value: z.ZodTypeAny | undefined,
|
|
107
|
+
): Record<string, z.ZodTypeAny> | undefined {
|
|
108
|
+
if (!value) {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const actualValue = unwrapZodField(value) as {
|
|
113
|
+
_def?: { typeName?: string; shape?: () => Record<string, z.ZodTypeAny> };
|
|
114
|
+
shape?: Record<string, z.ZodTypeAny>;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
if (actualValue._def?.typeName !== 'ZodObject') {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (typeof actualValue._def.shape === 'function') {
|
|
122
|
+
return actualValue._def.shape();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return actualValue.shape;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
129
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Transform a locate field schema to make its 'prompt' field optional
|
|
134
|
+
*/
|
|
135
|
+
function makePromptOptional(
|
|
136
|
+
shape: Record<string, z.ZodTypeAny>,
|
|
137
|
+
wrapInOptional: boolean,
|
|
138
|
+
description?: string | null,
|
|
139
|
+
): z.ZodTypeAny {
|
|
140
|
+
const newShape = { ...shape };
|
|
141
|
+
newShape.prompt = shape.prompt.optional();
|
|
142
|
+
|
|
143
|
+
let newSchema: z.ZodTypeAny = z.object(newShape).passthrough();
|
|
144
|
+
if (wrapInOptional) {
|
|
145
|
+
newSchema = newSchema.optional();
|
|
146
|
+
}
|
|
147
|
+
if (description) {
|
|
148
|
+
newSchema = newSchema.describe(description);
|
|
149
|
+
}
|
|
150
|
+
return newSchema;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Transform schema field to make locate.prompt optional if applicable
|
|
155
|
+
*/
|
|
156
|
+
function transformSchemaField(
|
|
157
|
+
key: string,
|
|
158
|
+
value: z.ZodTypeAny,
|
|
159
|
+
): [string, z.ZodTypeAny] {
|
|
160
|
+
const { innerValue, isOptional } = unwrapOptional(value);
|
|
161
|
+
const shape = getZodObjectShape(innerValue);
|
|
162
|
+
|
|
163
|
+
if (shape && isMidsceneLocatorField(innerValue)) {
|
|
164
|
+
return [
|
|
165
|
+
key,
|
|
166
|
+
makePromptOptional(shape, isOptional, getZodDescription(value)),
|
|
167
|
+
];
|
|
168
|
+
}
|
|
169
|
+
return [key, value];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Extract and transform schema from action's paramSchema.
|
|
174
|
+
*
|
|
175
|
+
* CLI tools expose parameters as named fields, so the only schema
|
|
176
|
+
* shapes we can surface are ZodObject (any number of fields) or undefined
|
|
177
|
+
* (the action takes no parameters). A primitive schema like `z.string()`
|
|
178
|
+
* silently degraded to leaking the ZodString instance's prototype methods
|
|
179
|
+
* as CLI flags — see https://github.com/web-infra-dev/midscene/issues/2313.
|
|
180
|
+
* Reject such schemas up front so the next author gets a loud error
|
|
181
|
+
* instead of a silent misconfiguration at runtime.
|
|
182
|
+
*/
|
|
183
|
+
function extractActionSchema(
|
|
184
|
+
paramSchema: z.ZodTypeAny | undefined,
|
|
185
|
+
actionName: string,
|
|
186
|
+
): Record<string, z.ZodTypeAny> {
|
|
187
|
+
if (!paramSchema) {
|
|
188
|
+
return {};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const shape = getZodObjectShape(paramSchema);
|
|
192
|
+
if (!shape) {
|
|
193
|
+
const typeName =
|
|
194
|
+
(paramSchema as unknown as { _def?: { typeName?: string } })?._def
|
|
195
|
+
?.typeName ?? 'unknown';
|
|
196
|
+
throw new Error(
|
|
197
|
+
`Action "${actionName}" declared a non-object paramSchema (${typeName}). CLI tool schemas must be a ZodObject (e.g. z.object({ uri: z.string() })) or undefined. Wrap primitive fields in an object schema.`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return Object.fromEntries(
|
|
202
|
+
Object.entries(shape).map(([key, value]) =>
|
|
203
|
+
transformSchemaField(key, value as z.ZodTypeAny),
|
|
204
|
+
),
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function getPromptText(prompt: unknown): string | undefined {
|
|
209
|
+
if (typeof prompt === 'string') {
|
|
210
|
+
return prompt;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (isRecord(prompt) && typeof prompt.prompt === 'string') {
|
|
214
|
+
return prompt.prompt;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function moveLocateExtrasIntoPrompt(
|
|
221
|
+
value: Record<string, unknown>,
|
|
222
|
+
locateFieldKeys: Set<string>,
|
|
223
|
+
): Record<string, unknown> {
|
|
224
|
+
const promptText = getPromptText(value.prompt);
|
|
225
|
+
if (!promptText) {
|
|
226
|
+
return value;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const normalizedPrompt: Record<string, unknown> = isRecord(value.prompt)
|
|
230
|
+
? { ...value.prompt }
|
|
231
|
+
: { prompt: promptText };
|
|
232
|
+
const normalizedLocate: Record<string, unknown> = {};
|
|
233
|
+
let movedExtraField = false;
|
|
234
|
+
|
|
235
|
+
for (const [key, fieldValue] of Object.entries(value)) {
|
|
236
|
+
if (key === 'prompt') {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (locateFieldKeys.has(key)) {
|
|
241
|
+
normalizedLocate[key] = fieldValue;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
movedExtraField = true;
|
|
246
|
+
if (!(key in normalizedPrompt)) {
|
|
247
|
+
normalizedPrompt[key] = fieldValue;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (!movedExtraField) {
|
|
252
|
+
return value;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return { ...normalizedLocate, prompt: normalizedPrompt };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function normalizeLocateLikeArg(
|
|
259
|
+
value: unknown,
|
|
260
|
+
fieldSchema: z.ZodTypeAny,
|
|
261
|
+
): unknown {
|
|
262
|
+
if (typeof value === 'string') {
|
|
263
|
+
return { prompt: value };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (!isRecord(value)) {
|
|
267
|
+
return value;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const shape = getZodObjectShape(fieldSchema);
|
|
271
|
+
if (!shape) {
|
|
272
|
+
return value;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return moveLocateExtrasIntoPrompt(value, new Set(Object.keys(shape)));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function normalizeActionArgs(
|
|
279
|
+
args: Record<string, unknown>,
|
|
280
|
+
paramSchema?: z.ZodTypeAny,
|
|
281
|
+
): Record<string, unknown> {
|
|
282
|
+
if (!paramSchema) {
|
|
283
|
+
return args;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const shape = getZodObjectShape(paramSchema);
|
|
287
|
+
if (!shape) {
|
|
288
|
+
return args;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return Object.fromEntries(
|
|
292
|
+
Object.entries(args).map(([key, value]) => {
|
|
293
|
+
const fieldSchema = shape[key] as z.ZodTypeAny | undefined;
|
|
294
|
+
if (!fieldSchema) {
|
|
295
|
+
return [key, value];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (isMidsceneLocatorField(fieldSchema)) {
|
|
299
|
+
return [key, normalizeLocateLikeArg(value, fieldSchema)];
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return [key, value];
|
|
303
|
+
}),
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Merge `defaults` into a single locate object without overwriting values the
|
|
309
|
+
* caller set explicitly. `deepThink` is a deprecated alias for `deepLocate`,
|
|
310
|
+
* so an explicit `deepThink` counts as `deepLocate` already being set.
|
|
311
|
+
*/
|
|
312
|
+
function mergeLocateDefaults(
|
|
313
|
+
locate: Record<string, unknown>,
|
|
314
|
+
defaults: Record<string, unknown>,
|
|
315
|
+
): Record<string, unknown> {
|
|
316
|
+
let merged: Record<string, unknown> | undefined;
|
|
317
|
+
for (const [key, value] of Object.entries(defaults)) {
|
|
318
|
+
if (locate[key] !== undefined) {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (key === 'deepLocate' && locate.deepThink !== undefined) {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
merged = merged ?? { ...locate };
|
|
325
|
+
merged[key] = value;
|
|
326
|
+
}
|
|
327
|
+
return merged ?? locate;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Apply `locateDefaults` to every locate-like field of an action's args.
|
|
332
|
+
* Generic over the default keys, so new behaviors need no changes here.
|
|
333
|
+
*/
|
|
334
|
+
function applyLocateDefaults(
|
|
335
|
+
args: Record<string, unknown>,
|
|
336
|
+
paramSchema: z.ZodTypeAny | undefined,
|
|
337
|
+
locateDefaults: Record<string, unknown>,
|
|
338
|
+
): Record<string, unknown> {
|
|
339
|
+
if (!paramSchema || Object.keys(locateDefaults).length === 0) {
|
|
340
|
+
return args;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const shape = getZodObjectShape(paramSchema);
|
|
344
|
+
if (!shape) {
|
|
345
|
+
return args;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return Object.fromEntries(
|
|
349
|
+
Object.entries(args).map(([key, value]) => {
|
|
350
|
+
const fieldSchema = shape[key] as z.ZodTypeAny | undefined;
|
|
351
|
+
if (
|
|
352
|
+
fieldSchema &&
|
|
353
|
+
isMidsceneLocatorField(fieldSchema) &&
|
|
354
|
+
isRecord(value)
|
|
355
|
+
) {
|
|
356
|
+
return [key, mergeLocateDefaults(value, locateDefaults)];
|
|
357
|
+
}
|
|
358
|
+
return [key, value];
|
|
359
|
+
}),
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Serialize args to human-readable description for AI action
|
|
365
|
+
*/
|
|
366
|
+
function serializeArgsToDescription(args: Record<string, unknown>): string {
|
|
367
|
+
try {
|
|
368
|
+
return Object.entries(args)
|
|
369
|
+
.map(([key, value]) => {
|
|
370
|
+
if (typeof value === 'object' && value !== null) {
|
|
371
|
+
try {
|
|
372
|
+
return `${key}: ${JSON.stringify(value)}`;
|
|
373
|
+
} catch {
|
|
374
|
+
// Circular reference or non-serializable object
|
|
375
|
+
return `${key}: [object]`;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return `${key}: "${value}"`;
|
|
379
|
+
})
|
|
380
|
+
.join(', ');
|
|
381
|
+
} catch (error: unknown) {
|
|
382
|
+
const errorMessage = getErrorMessage(error);
|
|
383
|
+
console.error('Error serializing args:', errorMessage);
|
|
384
|
+
return `[args serialization failed: ${errorMessage}]`;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Build action instruction as natural language for better AI planning flexibility.
|
|
390
|
+
* Natural language instructions allow the planner to adjust strategies on replanning,
|
|
391
|
+
* unlike rigid structured instructions that cause repeated identical failures.
|
|
392
|
+
*/
|
|
393
|
+
function buildActionInstruction(
|
|
394
|
+
actionName: string,
|
|
395
|
+
args: Record<string, unknown>,
|
|
396
|
+
): string {
|
|
397
|
+
const locatePrompt = isRecord(args.locate)
|
|
398
|
+
? getPromptText(args.locate.prompt)
|
|
399
|
+
: undefined;
|
|
400
|
+
|
|
401
|
+
switch (actionName) {
|
|
402
|
+
case 'Tap':
|
|
403
|
+
return locatePrompt ? `Tap on "${locatePrompt}"` : 'Tap';
|
|
404
|
+
case 'Input': {
|
|
405
|
+
const value = args.value ?? args.content ?? '';
|
|
406
|
+
return locatePrompt
|
|
407
|
+
? `Input "${value}" into "${locatePrompt}"`
|
|
408
|
+
: `Input "${value}"`;
|
|
409
|
+
}
|
|
410
|
+
case 'Scroll': {
|
|
411
|
+
const direction = args.direction ?? 'down';
|
|
412
|
+
return locatePrompt
|
|
413
|
+
? `Scroll ${direction} on "${locatePrompt}"`
|
|
414
|
+
: `Scroll ${direction}`;
|
|
415
|
+
}
|
|
416
|
+
case 'Hover':
|
|
417
|
+
return locatePrompt ? `Hover over "${locatePrompt}"` : 'Hover';
|
|
418
|
+
case 'KeyboardPress': {
|
|
419
|
+
const key = args.value ?? args.key ?? '';
|
|
420
|
+
return `Press key "${key}"`;
|
|
421
|
+
}
|
|
422
|
+
default: {
|
|
423
|
+
const argsDescription = serializeArgsToDescription(args);
|
|
424
|
+
return argsDescription ? `${actionName}: ${argsDescription}` : actionName;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async function executeAction(
|
|
430
|
+
agent: BaseAgent,
|
|
431
|
+
actionName: string,
|
|
432
|
+
args: Record<string, unknown>,
|
|
433
|
+
): Promise<unknown> {
|
|
434
|
+
if (agent.callActionInActionSpace) {
|
|
435
|
+
return agent.callActionInActionSpace(actionName, args);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (agent.aiAction) {
|
|
439
|
+
const instruction = buildActionInstruction(actionName, args);
|
|
440
|
+
return agent.aiAction(instruction);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
throw new Error(`Action "${actionName}" is not supported by this agent`);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Capture screenshot and return as tool result
|
|
448
|
+
*/
|
|
449
|
+
async function captureScreenshotResult(
|
|
450
|
+
agent: BaseAgent,
|
|
451
|
+
actionName: string,
|
|
452
|
+
actionResult?: unknown,
|
|
453
|
+
): Promise<ToolResult> {
|
|
454
|
+
const content: ToolResult['content'] = [
|
|
455
|
+
{ type: 'text', text: `Action "${actionName}" completed.` },
|
|
456
|
+
];
|
|
457
|
+
|
|
458
|
+
if (actionResult !== undefined) {
|
|
459
|
+
content.push({
|
|
460
|
+
type: 'text',
|
|
461
|
+
text: `Result: ${serializeActionResult(actionResult)}`,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
try {
|
|
466
|
+
const screenshot = await agent.page?.screenshotBase64();
|
|
467
|
+
if (!screenshot) {
|
|
468
|
+
return { content };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const { mimeType, body } = parseBase64(screenshot);
|
|
472
|
+
content.push({ type: 'image', data: body, mimeType });
|
|
473
|
+
return { content };
|
|
474
|
+
} catch (error: unknown) {
|
|
475
|
+
const errorMessage = getErrorMessage(error);
|
|
476
|
+
console.error('Error capturing screenshot:', errorMessage);
|
|
477
|
+
content[0] = {
|
|
478
|
+
type: 'text',
|
|
479
|
+
text: `Action "${actionName}" completed (screenshot unavailable: ${errorMessage})`,
|
|
480
|
+
};
|
|
481
|
+
return { content };
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function serializeActionResult(actionResult: unknown): string {
|
|
486
|
+
if (typeof actionResult === 'string') {
|
|
487
|
+
return actionResult;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
try {
|
|
491
|
+
return JSON.stringify(actionResult);
|
|
492
|
+
} catch {
|
|
493
|
+
return String(actionResult);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Create error result for tool handler
|
|
499
|
+
*/
|
|
500
|
+
function createErrorResult(message: string): ToolResult {
|
|
501
|
+
return {
|
|
502
|
+
content: [{ type: 'text', text: message }],
|
|
503
|
+
isError: true,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Capture screenshot and return as a non-error result with warning message.
|
|
509
|
+
* Used when an action fails but we want the AI agent to see the current state
|
|
510
|
+
* and decide how to recover, rather than treating it as a hard error (exit code 1).
|
|
511
|
+
*/
|
|
512
|
+
async function captureFailureResult(
|
|
513
|
+
agent: BaseAgent,
|
|
514
|
+
actionName: string,
|
|
515
|
+
errorMessage: string,
|
|
516
|
+
): Promise<ToolResult> {
|
|
517
|
+
const warningText = `Warning: Action "${actionName}" failed: ${errorMessage}. Check the screenshot below for the current page state and decide how to proceed.`;
|
|
518
|
+
try {
|
|
519
|
+
const screenshot = await agent.page?.screenshotBase64();
|
|
520
|
+
if (!screenshot) {
|
|
521
|
+
return {
|
|
522
|
+
content: [{ type: 'text', text: warningText }],
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
const { mimeType, body } = parseBase64(screenshot);
|
|
526
|
+
return {
|
|
527
|
+
content: [
|
|
528
|
+
{ type: 'text', text: warningText },
|
|
529
|
+
{ type: 'image', data: body, mimeType },
|
|
530
|
+
],
|
|
531
|
+
};
|
|
532
|
+
} catch {
|
|
533
|
+
return {
|
|
534
|
+
content: [{ type: 'text', text: warningText }],
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function mergeToolCliMetadata(
|
|
540
|
+
base?: ToolCliMetadata,
|
|
541
|
+
extra?: ToolCliMetadata,
|
|
542
|
+
): ToolCliMetadata | undefined {
|
|
543
|
+
const options = {
|
|
544
|
+
...(base?.options ?? {}),
|
|
545
|
+
...(extra?.options ?? {}),
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
const positionals = base?.positionals ?? extra?.positionals;
|
|
549
|
+
return Object.keys(options).length > 0 || positionals
|
|
550
|
+
? { options, positionals }
|
|
551
|
+
: undefined;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Converts DeviceAction from actionSpace into ToolDefinition.
|
|
556
|
+
* This is the core logic that removes need for hardcoded tool definitions
|
|
557
|
+
*/
|
|
558
|
+
export function generateToolsFromActionSpace(
|
|
559
|
+
actionSpace: ActionSpaceItem[],
|
|
560
|
+
getAgent: (args?: Record<string, unknown>) => Promise<BaseAgent>,
|
|
561
|
+
sanitizeArgs: (args: Record<string, unknown>) => Record<string, unknown> = (
|
|
562
|
+
args,
|
|
563
|
+
) => args,
|
|
564
|
+
initArgSchema: ToolSchema = {},
|
|
565
|
+
initArgCliMetadata?: ToolCliMetadata,
|
|
566
|
+
toolDefaults: ToolDefaults = {},
|
|
567
|
+
): ToolDefinition[] {
|
|
568
|
+
return actionSpace.map((action) => {
|
|
569
|
+
const schema = {
|
|
570
|
+
...extractActionSchema(action.paramSchema as z.ZodTypeAny, action.name),
|
|
571
|
+
...initArgSchema,
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
return {
|
|
575
|
+
name: action.name,
|
|
576
|
+
description: describeActionForTool(action),
|
|
577
|
+
schema,
|
|
578
|
+
cli: initArgCliMetadata,
|
|
579
|
+
handler: async (args: Record<string, unknown>) => {
|
|
580
|
+
try {
|
|
581
|
+
const agent = await getAgent(args);
|
|
582
|
+
emitCliVerboseEvent({
|
|
583
|
+
event: 'agent_ready',
|
|
584
|
+
tool: action.name,
|
|
585
|
+
});
|
|
586
|
+
const unsubscribeVerbose = attachCliVerboseDumpListener(agent, {
|
|
587
|
+
toolName: action.name,
|
|
588
|
+
});
|
|
589
|
+
try {
|
|
590
|
+
let normalizedArgs = normalizeActionArgs(
|
|
591
|
+
sanitizeArgs(args),
|
|
592
|
+
action.paramSchema,
|
|
593
|
+
);
|
|
594
|
+
if (toolDefaults.locate) {
|
|
595
|
+
normalizedArgs = applyLocateDefaults(
|
|
596
|
+
normalizedArgs,
|
|
597
|
+
action.paramSchema,
|
|
598
|
+
toolDefaults.locate,
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
let actionResult: unknown;
|
|
602
|
+
|
|
603
|
+
try {
|
|
604
|
+
actionResult = await executeAction(
|
|
605
|
+
agent,
|
|
606
|
+
action.name,
|
|
607
|
+
normalizedArgs,
|
|
608
|
+
);
|
|
609
|
+
} catch (error: unknown) {
|
|
610
|
+
const errorMessage = getErrorMessage(error);
|
|
611
|
+
console.error(
|
|
612
|
+
`Error executing action "${action.name}":`,
|
|
613
|
+
errorMessage,
|
|
614
|
+
);
|
|
615
|
+
// Return screenshot + warning instead of hard error,
|
|
616
|
+
// so the AI agent can see current state and decide to retry or adjust strategy
|
|
617
|
+
return await captureFailureResult(
|
|
618
|
+
agent,
|
|
619
|
+
action.name,
|
|
620
|
+
errorMessage,
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return await captureScreenshotResult(
|
|
625
|
+
agent,
|
|
626
|
+
action.name,
|
|
627
|
+
actionResult,
|
|
628
|
+
);
|
|
629
|
+
} finally {
|
|
630
|
+
unsubscribeVerbose();
|
|
631
|
+
}
|
|
632
|
+
} catch (error: unknown) {
|
|
633
|
+
// Connection/agent errors are still hard errors
|
|
634
|
+
const errorMessage = getErrorMessage(error);
|
|
635
|
+
console.error(`Error in handler for "${action.name}":`, errorMessage);
|
|
636
|
+
return createErrorResult(
|
|
637
|
+
`Failed to get agent or execute action "${action.name}": ${errorMessage}`,
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
},
|
|
641
|
+
};
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Generate common tools (screenshot, act)
|
|
647
|
+
*/
|
|
648
|
+
export function generateCommonTools(
|
|
649
|
+
getAgent: (args?: Record<string, unknown>) => Promise<BaseAgent>,
|
|
650
|
+
initArgSchema: ToolSchema = {},
|
|
651
|
+
initArgCliMetadata?: ToolCliMetadata,
|
|
652
|
+
toolDefaults: ToolDefaults = {},
|
|
653
|
+
): ToolDefinition[] {
|
|
654
|
+
return [
|
|
655
|
+
{
|
|
656
|
+
name: 'take_screenshot',
|
|
657
|
+
description: 'Capture screenshot of current page/screen',
|
|
658
|
+
schema: {
|
|
659
|
+
...initArgSchema,
|
|
660
|
+
},
|
|
661
|
+
cli: initArgCliMetadata,
|
|
662
|
+
handler: async (
|
|
663
|
+
args: Record<string, unknown> = {},
|
|
664
|
+
): Promise<ToolResult> => {
|
|
665
|
+
try {
|
|
666
|
+
const agent = await getAgent(args);
|
|
667
|
+
emitCliVerboseEvent({
|
|
668
|
+
event: 'agent_ready',
|
|
669
|
+
tool: 'take_screenshot',
|
|
670
|
+
});
|
|
671
|
+
const unsubscribeVerbose = attachCliVerboseDumpListener(agent, {
|
|
672
|
+
toolName: 'take_screenshot',
|
|
673
|
+
});
|
|
674
|
+
try {
|
|
675
|
+
const screenshot = await agent.page?.screenshotBase64();
|
|
676
|
+
if (!screenshot) {
|
|
677
|
+
return createErrorResult('Screenshot not available');
|
|
678
|
+
}
|
|
679
|
+
await agent.recordToReport?.('take_screenshot', {
|
|
680
|
+
screenshotBase64: screenshot,
|
|
681
|
+
});
|
|
682
|
+
const { mimeType, body } = parseBase64(screenshot);
|
|
683
|
+
return {
|
|
684
|
+
content: [{ type: 'image', data: body, mimeType }],
|
|
685
|
+
};
|
|
686
|
+
} finally {
|
|
687
|
+
unsubscribeVerbose();
|
|
688
|
+
}
|
|
689
|
+
} catch (error: unknown) {
|
|
690
|
+
const errorMessage = getErrorMessage(error);
|
|
691
|
+
console.error('Error taking screenshot:', errorMessage);
|
|
692
|
+
return createErrorResult(
|
|
693
|
+
`Failed to capture screenshot: ${errorMessage}`,
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
},
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
name: 'act',
|
|
700
|
+
description:
|
|
701
|
+
'Execute a natural language action. The AI will plan and perform multi-step operations in a single invocation, useful for transient UI interactions (e.g., Spotlight, dropdown menus) that disappear between separate commands.',
|
|
702
|
+
schema: {
|
|
703
|
+
prompt: z
|
|
704
|
+
.string()
|
|
705
|
+
.describe(
|
|
706
|
+
'Natural language description of the action to perform, e.g. "press Command+Space, type Safari, press Enter"',
|
|
707
|
+
),
|
|
708
|
+
deepLocate: z
|
|
709
|
+
.boolean()
|
|
710
|
+
.optional()
|
|
711
|
+
.describe(
|
|
712
|
+
'Use deep locate for every element this action targets. Improves precision for small or ambiguous targets at the cost of speed. Defaults to the server --deep-locate setting.',
|
|
713
|
+
),
|
|
714
|
+
deepThink: z
|
|
715
|
+
.boolean()
|
|
716
|
+
.optional()
|
|
717
|
+
.describe(
|
|
718
|
+
'Plan this action with deep thinking (richer context and sub-goal decomposition). Helps with complex multi-step instructions at the cost of speed. Defaults to the server --deep-think setting.',
|
|
719
|
+
),
|
|
720
|
+
fileChooserAllowedDir: z
|
|
721
|
+
.string()
|
|
722
|
+
.optional()
|
|
723
|
+
.describe(
|
|
724
|
+
'Directory that model-planned file uploads may access. Required when the prompt asks the model to upload files.',
|
|
725
|
+
),
|
|
726
|
+
...promptInputExtraSchema,
|
|
727
|
+
...initArgSchema,
|
|
728
|
+
},
|
|
729
|
+
cli: mergeToolCliMetadata(undefined, initArgCliMetadata),
|
|
730
|
+
handler: async (
|
|
731
|
+
args: Record<string, unknown> = {},
|
|
732
|
+
): Promise<ToolResult> => {
|
|
733
|
+
const prompt = args.prompt as string;
|
|
734
|
+
try {
|
|
735
|
+
const agent = await getAgent(args);
|
|
736
|
+
emitCliVerboseEvent({
|
|
737
|
+
event: 'agent_ready',
|
|
738
|
+
tool: 'act',
|
|
739
|
+
});
|
|
740
|
+
if (!agent.aiAction) {
|
|
741
|
+
return createErrorResult('act is not supported by this agent');
|
|
742
|
+
}
|
|
743
|
+
const unsubscribeVerbose = attachCliVerboseDumpListener(agent, {
|
|
744
|
+
toolName: 'act',
|
|
745
|
+
});
|
|
746
|
+
try {
|
|
747
|
+
// Start from the act defaults (deepThink off), overlay the server
|
|
748
|
+
// tool defaults, then let explicit per-call args win.
|
|
749
|
+
const actOptions: Record<string, unknown> = {
|
|
750
|
+
deepThink: false,
|
|
751
|
+
...toolDefaults.act,
|
|
752
|
+
};
|
|
753
|
+
if (args.deepLocate !== undefined) {
|
|
754
|
+
actOptions.deepLocate = args.deepLocate;
|
|
755
|
+
}
|
|
756
|
+
if (args.deepThink !== undefined) {
|
|
757
|
+
actOptions.deepThink = args.deepThink;
|
|
758
|
+
}
|
|
759
|
+
if (args.fileChooserAllowedDir !== undefined) {
|
|
760
|
+
actOptions.fileChooserAllowedDir = args.fileChooserAllowedDir;
|
|
761
|
+
}
|
|
762
|
+
const userPrompt = composeUserPrompt({
|
|
763
|
+
prompt,
|
|
764
|
+
image: args.image,
|
|
765
|
+
imageName: args.imageName,
|
|
766
|
+
convertHttpImage2Base64: args.convertHttpImage2Base64,
|
|
767
|
+
});
|
|
768
|
+
const result = await agent.aiAction(userPrompt, actOptions);
|
|
769
|
+
return await captureScreenshotResult(agent, 'act', result);
|
|
770
|
+
} finally {
|
|
771
|
+
unsubscribeVerbose();
|
|
772
|
+
}
|
|
773
|
+
} catch (error: unknown) {
|
|
774
|
+
const errorMessage = getErrorMessage(error);
|
|
775
|
+
console.error('Error executing act:', errorMessage);
|
|
776
|
+
return createErrorResult(`Failed to execute act: ${errorMessage}`);
|
|
777
|
+
}
|
|
778
|
+
},
|
|
779
|
+
},
|
|
780
|
+
{
|
|
781
|
+
name: 'assert',
|
|
782
|
+
description:
|
|
783
|
+
'Assert a natural language statement against the current page/screen or a saved UI observation record.',
|
|
784
|
+
schema: {
|
|
785
|
+
prompt: z
|
|
786
|
+
.string()
|
|
787
|
+
.describe(
|
|
788
|
+
'Natural language assertion to verify, e.g. "there is a login button visible"',
|
|
789
|
+
),
|
|
790
|
+
message: z
|
|
791
|
+
.string()
|
|
792
|
+
.optional()
|
|
793
|
+
.describe(
|
|
794
|
+
'Custom error message to throw when the assertion fails, e.g. "the login button should be visible".',
|
|
795
|
+
),
|
|
796
|
+
record: z
|
|
797
|
+
.string()
|
|
798
|
+
.optional()
|
|
799
|
+
.describe(
|
|
800
|
+
'Path to a JSON observation manifest created by the record command. Keep its adjacent image directory with it. When set, the assertion uses that ordered frame window instead of the current screen.',
|
|
801
|
+
),
|
|
802
|
+
...promptInputExtraSchema,
|
|
803
|
+
...initArgSchema,
|
|
804
|
+
},
|
|
805
|
+
cli: mergeToolCliMetadata(undefined, initArgCliMetadata),
|
|
806
|
+
handler: async (
|
|
807
|
+
args: Record<string, unknown> = {},
|
|
808
|
+
): Promise<ToolResult> => {
|
|
809
|
+
const prompt = args.prompt as string;
|
|
810
|
+
const message = args.message as string | undefined;
|
|
811
|
+
try {
|
|
812
|
+
const agent = await getAgent(args);
|
|
813
|
+
emitCliVerboseEvent({
|
|
814
|
+
event: 'agent_ready',
|
|
815
|
+
tool: 'assert',
|
|
816
|
+
});
|
|
817
|
+
if (!agent.aiAssert) {
|
|
818
|
+
return createErrorResult('assert is not supported by this agent');
|
|
819
|
+
}
|
|
820
|
+
const unsubscribeVerbose = attachCliVerboseDumpListener(agent, {
|
|
821
|
+
toolName: 'assert',
|
|
822
|
+
});
|
|
823
|
+
try {
|
|
824
|
+
const observationRecord =
|
|
825
|
+
typeof args.record === 'string'
|
|
826
|
+
? readUIObservationRecord(args.record)
|
|
827
|
+
: undefined;
|
|
828
|
+
const userPrompt = composeUserPrompt({
|
|
829
|
+
prompt,
|
|
830
|
+
image: args.image,
|
|
831
|
+
imageName: args.imageName,
|
|
832
|
+
convertHttpImage2Base64: args.convertHttpImage2Base64,
|
|
833
|
+
});
|
|
834
|
+
if (observationRecord) {
|
|
835
|
+
const observationArtifacts =
|
|
836
|
+
resolveObservationArtifactAdapter(agent);
|
|
837
|
+
if (!observationArtifacts) {
|
|
838
|
+
throw new Error(
|
|
839
|
+
'assert --record is not supported because this agent cannot load UI observations',
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
const observation =
|
|
843
|
+
observationArtifacts.loadRecord(observationRecord);
|
|
844
|
+
try {
|
|
845
|
+
await observation.aiAssert(userPrompt, message);
|
|
846
|
+
} finally {
|
|
847
|
+
await observation.dispose?.();
|
|
848
|
+
}
|
|
849
|
+
} else {
|
|
850
|
+
await agent.aiAssert(userPrompt, message);
|
|
851
|
+
}
|
|
852
|
+
return {
|
|
853
|
+
content: [{ type: 'text', text: 'Assertion passed.' }],
|
|
854
|
+
};
|
|
855
|
+
} finally {
|
|
856
|
+
unsubscribeVerbose();
|
|
857
|
+
}
|
|
858
|
+
} catch (error: unknown) {
|
|
859
|
+
const errorMessage = getErrorMessage(error);
|
|
860
|
+
console.error('Error executing assert:', errorMessage);
|
|
861
|
+
return createErrorResult(`Failed to execute assert: ${errorMessage}`);
|
|
862
|
+
}
|
|
863
|
+
},
|
|
864
|
+
},
|
|
865
|
+
];
|
|
866
|
+
}
|