feat(android): add state conditions and measured gesture feedback
Add optional ensure_state, gesture region and final value feedback shared by Flash and Pro. Preserve completed inputs when verification fails and stop repeated ineffective strategies. Regenerate pinned tool schemas for the new optional arguments.
This commit is contained in:
1 parent
6a69a595ef
commit
4ea7939d9c
20 files changed
+2779
-33
No files matched your search
@@ -88,6 +88,8 @@ Responses 消息交给工具、下游解析器或正式裁定前,必须具有
|
|||||||
|
|
||||||
`drag` 支持明确的归一化或原图像素端点,默认 300ms,不自动过冲。可根据当前缩放下的两个真实数值刻度校准 thumb 位置,或将 pan 刻度移至固定指针;返回真实派发端点、比例及端点取整误差。校准假定本次局部刻度线性,换缩放须重新读取锚点;计算回执保留 pending_observation,实际选中值及业务目标仍须通过动作后的观察验证。像素分辨率不足时先放大,不宣称任意时间精度。
|
`drag` 支持明确的归一化或原图像素端点,默认 300ms,不自动过冲。可根据当前缩放下的两个真实数值刻度校准 thumb 位置,或将 pan 刻度移至固定指针;返回真实派发端点、比例及端点取整误差。校准假定本次局部刻度线性,换缩放须重新读取锚点;计算回执保留 pending_observation,实际选中值及业务目标仍须通过动作后的观察验证。像素分辨率不足时先放大,不宣称任意时间精度。
|
||||||
|
|
||||||
|
状态动作使用显式 `control_action.ensure_state`:仅以真实原生 checked/selected、文本/描述或数值判断,已满足则跳过,否则写入一次并有界只读核验。缺失状态不当作 false;核验失败保留完成的输入,不自动再切换。drag/pinch/touch_gesture 的可选 region 在首个输入前约束所有触点路径。drag 或整个 action_sequence 的可选 feedback 绑定唯一持久数值读数,在输入前确认可读,整组结束后有界等待稳定并报告实际值、目标、残差和允许误差;连续两次同策略未减小误差则拒绝第三次输入,要求重新观察、缩放或改变有效移动距离。记录仅存于当前设备执行实例,缺失读数不记作零,不共享到新任务。计算精度、实际数值条件和最终业务验收分开;无原生读数的自绘控件仍依靠视觉证据,不能宣称任意精度。详见[原生动作说明](docs/android-native-actions.md)。
|
||||||
|
|
||||||
任务耗时复用现有队列、工作进程控制文件与验收 trace,网页、MCP 和报告使用同一只读投影。分别列出后台排队、等待设备/资源、暂停与示范、执行与整理、最终验收及报告导出;总历时包含这些等待,不代表模型速度。缺少时间记录显示未知,不将旧任务缺失的分项补成零。运行结束后不继续累计执行耗时,报告导出单独计时。
|
任务耗时复用现有队列、工作进程控制文件与验收 trace,网页、MCP 和报告使用同一只读投影。分别列出后台排队、等待设备/资源、暂停与示范、执行与整理、最终验收及报告导出;总历时包含这些等待,不代表模型速度。缺少时间记录显示未知,不将旧任务缺失的分项补成零。运行结束后不继续累计执行耗时,报告导出单独计时。
|
||||||
|
|
||||||
指导接收不保证赶在终态前消费。保留接收时点、执行器消费回执及后续动作记录的区别;`consumed` 仅证明执行器已读取,不能宣称模型已应用或验收已通过。终态仍未领取的指导投影为 `unapplied`,领取后未留下消费回执的为 `unconfirmed`,不改写原始文件。网页详细查询复用原指导入口,旧数组响应保持兼容。
|
指导接收不保证赶在终态前消费。保留接收时点、执行器消费回执及后续动作记录的区别;`consumed` 仅证明执行器已读取,不能宣称模型已应用或验收已通过。终态仍未领取的指导投影为 `unapplied`,领取后未留下消费回执的为 `unconfirmed`,不改写原始文件。网页详细查询复用原指导入口,旧数组响应保持兼容。
|
||||||
|
|||||||
@@ -623,6 +623,24 @@ describe('extractActionExtraParams', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('native action Chinese presentation', () => {
|
describe('native action Chinese presentation', () => {
|
||||||
|
it('renders actual state and final value verification in Chinese without changing literal evidence', () => {
|
||||||
|
const action = { action: 'action_sequence', steps: [{ action: 'control_action', request: {
|
||||||
|
action: 'click', selector: { resource_id: 'demo:id/player' },
|
||||||
|
ensure_state: { state: 'content_desc', expected_text: '播放' }
|
||||||
|
} }], feedback: {
|
||||||
|
selector: { resource_id: 'demo:id/clock' }, value_format: 'clock', target_value: 63737, tolerance: 5
|
||||||
|
}, value_feedback: { after_value: 63737, residual: 0, within_tolerance: true }, verification: 'value_matched' };
|
||||||
|
const original = JSON.stringify(action);
|
||||||
|
const params = extractActionExtraParams(action);
|
||||||
|
const steps = JSON.parse(params.find(p => p.key === '动作步骤')!.value);
|
||||||
|
expect(steps[0]['原生请求']['操作']).toBe('原生点击');
|
||||||
|
expect(steps[0]['原生请求']['确保控件状态']['等待条件']).toBe('控件描述匹配');
|
||||||
|
expect(steps[0]['原生请求']['确保控件状态']['预期文本']).toBe('播放');
|
||||||
|
expect(JSON.parse(params.find(p => p.key === '实际值核验')!.value)['读数格式']).toBe('时分秒文本');
|
||||||
|
expect(params).toContain({ key: '验证状态', value: '实际值符合目标' });
|
||||||
|
expect(JSON.stringify(action)).toBe(original);
|
||||||
|
});
|
||||||
|
|
||||||
it('renders a pinch trace in Chinese without modifying the recorded gesture', () => {
|
it('renders a pinch trace in Chinese without modifying the recorded gesture', () => {
|
||||||
const action = {
|
const action = {
|
||||||
name: 'pinch', args: {
|
name: 'pinch', args: {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const ACTION_LABELS = new Map(Object.entries({
|
|||||||
control_action: '操作原生控件',
|
control_action: '操作原生控件',
|
||||||
wait_until: '等待控件条件',
|
wait_until: '等待控件条件',
|
||||||
scroll_to: '滚动查找控件',
|
scroll_to: '滚动查找控件',
|
||||||
action_sequence: '动作序列'
|
action_sequence: '动作序列', drag: '拖动屏幕'
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const CONTROL_LABELS = new Map(Object.entries({
|
const CONTROL_LABELS = new Map(Object.entries({
|
||||||
@@ -40,6 +40,20 @@ const PARAM_LABELS = new Map(Object.entries({
|
|||||||
poll_interval_ms: '检查间隔(毫秒)', stable_ms: '稳定时长(毫秒)',
|
poll_interval_ms: '检查间隔(毫秒)', stable_ms: '稳定时长(毫秒)',
|
||||||
direction: '滚动方向', max_scrolls: '最多滚动次数', settle_ms: '停稳时长(毫秒)',
|
direction: '滚动方向', max_scrolls: '最多滚动次数', settle_ms: '停稳时长(毫秒)',
|
||||||
require_visible: '可见条件', unless_visible: '跳过条件',
|
require_visible: '可见条件', unless_visible: '跳过条件',
|
||||||
|
ensure_state: '确保控件状态', verify_timeout_ms: '状态核验超时(毫秒)',
|
||||||
|
feedback: '实际值核验', value_feedback: '数值反馈', value_format: '读数格式',
|
||||||
|
before_value: '调整前数值', after_value: '调整后数值', target_value: '目标数值',
|
||||||
|
before_observation: '调整前观测', after_observation: '调整后观测',
|
||||||
|
node: '原生节点', samples: '采样次数', stable_for_ms: '已稳定时长(毫秒)',
|
||||||
|
residual: '实际残差', changed: '读数已变化', improved: '误差已减小',
|
||||||
|
within_tolerance: '符合允许误差', consecutive_no_progress: '连续无效次数',
|
||||||
|
region: '手势操作区域', start: '起点', end: '终点',
|
||||||
|
calibration: '刻度校准', axis: '刻度轴', mode: '拖动方式', anchors: '观测刻度',
|
||||||
|
position: '刻度位置', indicator_position: '固定指针位置',
|
||||||
|
min_movement_px: '已测最小有效距离(像素)', movement_px: '计划移动距离(像素)',
|
||||||
|
pixel_error_budget: '允许像素误差', value_per_pixel: '每像素数值',
|
||||||
|
rounding_error_value: '取整数值误差', anchor_coordinate_space: '刻度坐标空间',
|
||||||
|
target_position_pixel: '目标刻度像素位置', request: '原生请求',
|
||||||
steps: '动作步骤', action: '操作', args: '参数',
|
steps: '动作步骤', action: '操作', args: '参数',
|
||||||
time_in_ms: '等待时长(毫秒)', target_description: '目标说明',
|
time_in_ms: '等待时长(毫秒)', target_description: '目标说明',
|
||||||
execution: '执行状态', verification: '验证状态'
|
execution: '执行状态', verification: '验证状态'
|
||||||
@@ -50,7 +64,7 @@ const ENUM_LABELS: Record<string, Map<string, string>> = {
|
|||||||
state: new Map(Object.entries({
|
state: new Map(Object.entries({
|
||||||
visible: '可见', absent: '消失', enabled: '可用', disabled: '禁用',
|
visible: '可见', absent: '消失', enabled: '可用', disabled: '禁用',
|
||||||
checked: '已勾选', unchecked: '未勾选', selected: '已选中',
|
checked: '已勾选', unchecked: '未勾选', selected: '已选中',
|
||||||
unselected: '未选中', text: '文本匹配', value: '数值匹配', stable: '持续稳定'
|
unselected: '未选中', text: '文本匹配', content_desc: '控件描述匹配', value: '数值匹配', stable: '持续稳定'
|
||||||
})),
|
})),
|
||||||
direction: new Map(Object.entries({
|
direction: new Map(Object.entries({
|
||||||
forward: '向前', backward: '向后', up: '向上', down: '向下', left: '向左', right: '向右'
|
forward: '向前', backward: '向后', up: '向上', down: '向下', left: '向左', right: '向右'
|
||||||
@@ -59,7 +73,15 @@ const ENUM_LABELS: Record<string, Map<string, string>> = {
|
|||||||
not_started: '未开始', observed: '已观测', completed: '已完成',
|
not_started: '未开始', observed: '已观测', completed: '已完成',
|
||||||
partial: '部分完成', cancelled: '已取消', unknown: '结果未知'
|
partial: '部分完成', cancelled: '已取消', unknown: '结果未知'
|
||||||
})),
|
})),
|
||||||
verification: new Map(Object.entries({ pending_observation: '等待观测' }))
|
mode: new Map(Object.entries({ thumb: '移动滑块', pan: '平移刻度' })),
|
||||||
|
value_format: new Map(Object.entries({ range: '原生数值范围', number: '数字文本', clock: '时分秒文本' })),
|
||||||
|
anchor_coordinate_space: new Map(Object.entries({ normalized: '归一化坐标', pixel: '像素坐标' })),
|
||||||
|
verification: new Map(Object.entries({
|
||||||
|
pending_observation: '等待观测', condition_satisfied: '状态条件已满足',
|
||||||
|
condition_not_met: '状态条件未满足', value_matched: '实际值符合目标',
|
||||||
|
value_not_reached: '实际值未达目标', unavailable: '无法核验',
|
||||||
|
strategy_change_required: '需要更换调整策略'
|
||||||
|
}))
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getNativeActionTitle(name: string, args: any = {}): string | null {
|
export function getNativeActionTitle(name: string, args: any = {}): string | null {
|
||||||
@@ -73,7 +95,7 @@ export function getNativeActionTitle(name: string, args: any = {}): string | nul
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getNativeActionArgs(args: any): any {
|
export function getNativeActionArgs(args: any): any {
|
||||||
return args?.action === 'control_action' && args.args ? args.args : (args ?? {});
|
return args?.action === 'control_action' ? (args.request ?? args.args ?? args) : (args ?? {});
|
||||||
}
|
}
|
||||||
|
|
||||||
function localizedValue(value: any, key: string, controlAction = false): any {
|
function localizedValue(value: any, key: string, controlAction = false): any {
|
||||||
@@ -81,7 +103,7 @@ function localizedValue(value: any, key: string, controlAction = false): any {
|
|||||||
if (value && typeof value === 'object') {
|
if (value && typeof value === 'object') {
|
||||||
return Object.fromEntries(Object.entries(value).map(([field, item]) => [
|
return Object.fromEntries(Object.entries(value).map(([field, item]) => [
|
||||||
PARAM_LABELS.get(field) ?? field,
|
PARAM_LABELS.get(field) ?? field,
|
||||||
localizedValue(item, field, field === 'action' ? controlAction : field === 'args' && value.action === 'control_action')
|
localizedValue(item, field, field === 'action' ? controlAction : (field === 'args' || field === 'request') && value.action === 'control_action')
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ You are an autonomous UI execution agent. Accomplish the user's objective using
|
|||||||
|
|
||||||
**Objective: {{ goal }}**
|
**Objective: {{ goal }}**
|
||||||
|
|
||||||
|
{% if "drag" in available_tools %}- **数值目标的闭环**:先观察真实手势区域,为 drag/pinch/touch_gesture 填 `drag.region`,不要把日期栏、导航栏当作时间轴。若原生可读取持久数值,为 drag 或 action_sequence 绑定 `action_sequence.feedback`(唯一稳定 selector、value_format、target_value、tolerance);时分秒用 clock,单位为当天秒数。组合“拖动→暂停”时只在整个序列上绑定 feedback,末尾读稳定值,不能在拖动中途的浮层上核验。已验证的同一持久读数在允许误差内就是此数值条件的证据;日期、暂停等其它要求仍分别核对,不能无依据地改称误差无法判断。连续无效反馈会拒绝第三次同策略输入;放大并重新读刻度、改为足够距离或原生设值,不能靠微改坐标绕过。calibration.tolerance 给出几何误差预算;min_movement_px 只填写实际测得的起效距离,不猜固定阈值。没有可读的持久数值时保留视觉证据与不确定项,不捏造 selector 或读数。{% endif %}
|
||||||
|
{% if "control_action" in available_tools %}- **先状态后动作**:对于暂停、勾选等目标状态,先 read_control 确认其真实原生语义,再用 control_action.ensure_state;已符合时跳过,否则只操作一次并有界核验。可按观察到的 checked/selected、text 或 content_desc 判断;不能假设 selected 就表示暂停。selector 不包含动作会改变的文本/描述。拖动可能恢复播放,把确保暂停放在拖动后同一动作序列中;状态核验失败时重新观察,不重复盲点切换。{% endif %}
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# 1. COGNITIVE PROTOCOL
|
# 1. COGNITIVE PROTOCOL
|
||||||
|
|||||||
@@ -263,7 +263,9 @@ REPORT_TASK_STATUS_TOOL = ToolDeclaration(
|
|||||||
"properties": {
|
"properties": {
|
||||||
"report_note_key": {
|
"report_note_key": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "If the requested final report is already saved with save_note, name that note here. ARTEMIS uses its exact body as the final answer and output.md, instead of generating a second conflicting report. Omit when explanation itself is the complete report.",
|
"pattern": r"^[\w-]{0,128}$",
|
||||||
|
"maxLength": 128,
|
||||||
|
"description": "Copy the exact plain note name ONLY if the requested final report was saved with save_note. Otherwise use the empty string (or omit); never supply placeholders or paths such as /not/requested. ARTEMIS uses the saved note's exact body as the final report.",
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class GestureModel(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
Point = Annotated[list[StrictInt], Field(min_length=2, max_length=2)]
|
Point = Annotated[list[StrictInt], Field(min_length=2, max_length=2)]
|
||||||
|
Region = Annotated[list[StrictInt], Field(min_length=4, max_length=4)]
|
||||||
Duration = Annotated[int, Field(strict=True, ge=1, le=10000)]
|
Duration = Annotated[int, Field(strict=True, ge=1, le=10000)]
|
||||||
CoordinateSpace = Literal["normalized", "pixel"]
|
CoordinateSpace = Literal["normalized", "pixel"]
|
||||||
|
|
||||||
@@ -25,3 +26,29 @@ def pixel_point(point: list[int], space: CoordinateSpace, width: int, height: in
|
|||||||
if space == "pixel":
|
if space == "pixel":
|
||||||
return list(point)
|
return list(point)
|
||||||
return [min(size - 1, p * size // 1000) for p, size in zip(point, [width, height], strict=True)]
|
return [min(size - 1, p * size // 1000) for p, size in zip(point, [width, height], strict=True)]
|
||||||
|
|
||||||
|
|
||||||
|
def pixel_region(region: list[int], space: CoordinateSpace, width: int, height: int) -> list[int]:
|
||||||
|
"""Observed rectangle; right/bottom are exclusive, like Android bounds."""
|
||||||
|
left, top, right, bottom = region
|
||||||
|
max_x, max_y = (1000, 1000) if space == "normalized" else (width, height)
|
||||||
|
if not (0 <= left < right <= max_x and 0 <= top < bottom <= max_y):
|
||||||
|
raise ValueError(
|
||||||
|
"region must be a nonempty on-screen [left, top, right, bottom] rectangle."
|
||||||
|
)
|
||||||
|
if space == "pixel":
|
||||||
|
return list(region)
|
||||||
|
return [
|
||||||
|
left * width // 1000,
|
||||||
|
top * height // 1000,
|
||||||
|
right * width // 1000,
|
||||||
|
bottom * height // 1000,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def require_in_region(points: list[list[int]], region: list[int]) -> None:
|
||||||
|
left, top, right, bottom = region
|
||||||
|
if any(not (left <= x < right and top <= y < bottom) for x, y in points):
|
||||||
|
raise ValueError(
|
||||||
|
"Gesture leaves the observed region; re-observe the actual gesture surface. No input executed."
|
||||||
|
)
|
||||||
@@ -70,6 +70,8 @@ from pydantic import BaseModel, Field, create_model
|
|||||||
from artemis.core.tool_declaration import ToolDeclaration
|
from artemis.core.tool_declaration import ToolDeclaration
|
||||||
from artemis.mcp.action_types import ActionResult
|
from artemis.mcp.action_types import ActionResult
|
||||||
from artemis.mcp.gestures import CoordinateSpace, DragCalibration, GestureStep
|
from artemis.mcp.gestures import CoordinateSpace, DragCalibration, GestureStep
|
||||||
|
from artemis.mcp.action_parameters import Region
|
||||||
|
from artemis.mcp.value_feedback import ValueFeedback
|
||||||
from artemis.mcp.native_actions import NATIVE_REQUESTS, READ_ONLY_ACTIONS
|
from artemis.mcp.native_actions import NATIVE_REQUESTS, READ_ONLY_ACTIONS
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -225,6 +227,8 @@ async def _wire_drag(actuator: Any, a: dict[str, Any]) -> ActionResult:
|
|||||||
|
|
||||||
|
|
||||||
async def _wire_action_sequence(actuator: Any, a: dict[str, Any]) -> ActionResult:
|
async def _wire_action_sequence(actuator: Any, a: dict[str, Any]) -> ActionResult:
|
||||||
|
if a.get("feedback") is not None:
|
||||||
|
return await actuator.action_sequence(a["steps"], feedback=a["feedback"])
|
||||||
return await actuator.action_sequence(a["steps"])
|
return await actuator.action_sequence(a["steps"])
|
||||||
|
|
||||||
|
|
||||||
@@ -298,7 +302,13 @@ _SPECS: tuple[ActionSpec, ...] = (
|
|||||||
"A prior screenshot does not prove the panel is still open. Use click.unless_visible "
|
"A prior screenshot does not prove the panel is still open. Use click.unless_visible "
|
||||||
"and drag.require_visible with an observed panel resource_id to avoid toggling an "
|
"and drag.require_visible with an observed panel resource_id to avoid toggling an "
|
||||||
"open panel closed or dragging its underlying content. Never invent resource IDs. "
|
"open panel closed or dragging its underlying content. Never invent resource IDs. "
|
||||||
"Verify actual ticks/handle endpoints before claiming maximum or minimum."
|
"Verify actual ticks/handle endpoints before claiming maximum or minimum. "
|
||||||
|
"For a measurable target, bind feedback to its persistent native readout: "
|
||||||
|
"final feedback is read AFTER ALL members, including any pause, finish. Never use a release-time tooltip. "
|
||||||
|
"Use control_action.request.ensure_state for observed checked/selected/text/value state, "
|
||||||
|
"so an already satisfied state is skipped rather than toggled. A playback icon alone "
|
||||||
|
"does not prove a native boolean state: inspect it first. Feedback blocks the third "
|
||||||
|
"consecutive ineffective strategy; zoom or increase the meaningful movement, then re-observe."
|
||||||
),
|
),
|
||||||
params=(
|
params=(
|
||||||
ParamSpec(
|
ParamSpec(
|
||||||
@@ -306,11 +316,20 @@ _SPECS: tuple[ActionSpec, ...] = (
|
|||||||
list[GestureStep],
|
list[GestureStep],
|
||||||
"Ordered UI preparation, click, wait, drag, pinch, touch_gesture, control_action and wait_until members. control_action members wrap their native selector/action arguments in request. Inline text is limited to 1024 characters; use a standalone control_action for long edits. Entire encoded sequence stays within the existing 16KiB argument safety bound.",
|
"Ordered UI preparation, click, wait, drag, pinch, touch_gesture, control_action and wait_until members. control_action members wrap their native selector/action arguments in request. Inline text is limited to 1024 characters; use a standalone control_action for long edits. Entire encoded sequence stays within the existing 16KiB argument safety bound.",
|
||||||
),
|
),
|
||||||
|
ParamSpec(
|
||||||
|
"feedback",
|
||||||
|
ValueFeedback | None,
|
||||||
|
"Optional actual value/target comparison after the WHOLE sequence. clock uses seconds since midnight; exact bound and stable observation are checked locally, not inferred from dispatch.",
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
wire=WireDialect(
|
wire=WireDialect(
|
||||||
description="Execute validated short gesture steps consecutively; stop at the first failure.",
|
description="Execute validated short gesture steps consecutively; stop at the first failure.",
|
||||||
params=(ParamSpec("steps", list[GestureStep]),),
|
params=(
|
||||||
|
ParamSpec("steps", list[GestureStep]),
|
||||||
|
ParamSpec("feedback", ValueFeedback | None, required=False),
|
||||||
|
),
|
||||||
bind=_wire_action_sequence,
|
bind=_wire_action_sequence,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -350,6 +369,18 @@ _SPECS: tuple[ActionSpec, ...] = (
|
|||||||
"coordinate_space", CoordinateSpace, required=False, default="normalized"
|
"coordinate_space", CoordinateSpace, required=False, default="normalized"
|
||||||
),
|
),
|
||||||
ParamSpec("duration_ms", int, required=False, default=300),
|
ParamSpec("duration_ms", int, required=False, default=300),
|
||||||
|
ParamSpec(
|
||||||
|
"region",
|
||||||
|
Region | None,
|
||||||
|
"Observed gesture surface rectangle, in coordinate_space. Keep both endpoints inside.",
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
ParamSpec(
|
||||||
|
"feedback",
|
||||||
|
ValueFeedback | None,
|
||||||
|
"Read the persistent actual value after the drag. For drag then pause, put feedback on action_sequence instead.",
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
ParamSpec(
|
ParamSpec(
|
||||||
"calibration",
|
"calibration",
|
||||||
DragCalibration | None,
|
DragCalibration | None,
|
||||||
@@ -374,6 +405,8 @@ _SPECS: tuple[ActionSpec, ...] = (
|
|||||||
"coordinate_space", CoordinateSpace, required=False, default="normalized"
|
"coordinate_space", CoordinateSpace, required=False, default="normalized"
|
||||||
),
|
),
|
||||||
ParamSpec("duration_ms", int, required=False, default=300),
|
ParamSpec("duration_ms", int, required=False, default=300),
|
||||||
|
ParamSpec("region", Region | None, required=False),
|
||||||
|
ParamSpec("feedback", ValueFeedback | None, required=False),
|
||||||
ParamSpec("calibration", DragCalibration | None, required=False),
|
ParamSpec("calibration", DragCalibration | None, required=False),
|
||||||
ParamSpec("require_visible", str | None, required=False),
|
ParamSpec("require_visible", str | None, required=False),
|
||||||
),
|
),
|
||||||
@@ -853,6 +886,11 @@ _NATIVE_DESCRIPTIONS = {
|
|||||||
"control_action": (
|
"control_action": (
|
||||||
"[ACTION] Perform ONE supported native action on a freshly resolved unique control. "
|
"[ACTION] Perform ONE supported native action on a freshly resolved unique control. "
|
||||||
"Read_control first; copy selector exactly and optionally expected_token to reject stale evidence. "
|
"Read_control first; copy selector exactly and optionally expected_token to reject stale evidence. "
|
||||||
|
"For a desired state use ensure_state (checked/selected/text/content_desc/value), then "
|
||||||
|
"verify_timeout_ms: skip an already satisfied state, otherwise write ONCE and poll read-only. "
|
||||||
|
"Copy observed state semantics; do not assume selected means paused. Bind a stable selector "
|
||||||
|
"excluding text/content_desc that the action changes. A failed postcondition does not undo "
|
||||||
|
"the completed input and must not cause an automatic second toggle. "
|
||||||
"action=set_progress requires a finite in-range value; set_selection requires selection_start/end "
|
"action=set_progress requires a finite in-range value; set_selection requires selection_start/end "
|
||||||
"in UTF-16 code units (never split an emoji). set_text and replace_selection require text, "
|
"in UTF-16 code units (never split an emoji). set_text and replace_selection require text, "
|
||||||
"including empty text; whitespace is preserved. Other actions take no text/value/selection. "
|
"including empty text; whitespace is preserved. Other actions take no text/value/selection. "
|
||||||
|
|||||||
@@ -108,6 +108,9 @@ class AdbActuator:
|
|||||||
):
|
):
|
||||||
self.ctx = ctx
|
self.ctx = ctx
|
||||||
self.controller = controller or UnifiedMobileController(ctx)
|
self.controller = controller or UnifiedMobileController(ctx)
|
||||||
|
from artemis.mcp.value_feedback import ValueFeedbackRunner
|
||||||
|
|
||||||
|
self._value_feedback = ValueFeedbackRunner()
|
||||||
|
|
||||||
# --- Contract --------------------------------------------------------------------
|
# --- Contract --------------------------------------------------------------------
|
||||||
|
|
||||||
@@ -183,6 +186,8 @@ class AdbActuator:
|
|||||||
else "possible"
|
else "possible"
|
||||||
)
|
)
|
||||||
if receipt.get("success"):
|
if receipt.get("success"):
|
||||||
|
if name in {"pinch", "touch_gesture"} and receipt.get("execution") == "completed":
|
||||||
|
self._value_feedback.history.clear()
|
||||||
return ActionResult.success(
|
return ActionResult.success(
|
||||||
name,
|
name,
|
||||||
"Native operation completed. Inspect the receipt and current app state; business verification remains separate.",
|
"Native operation completed. Inspect the receipt and current app state; business verification remains separate.",
|
||||||
@@ -224,10 +229,24 @@ class AdbActuator:
|
|||||||
from artemis.mcp.gestures import DragRequest, plan_drag
|
from artemis.mcp.gestures import DragRequest, plan_drag
|
||||||
|
|
||||||
try:
|
try:
|
||||||
planned = plan_drag(DragRequest.model_validate(kwargs), *self._dims())
|
request = DragRequest.model_validate(kwargs)
|
||||||
|
planned = plan_drag(request, *self._dims())
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
return ActionResult.failure("drag", str(exc), code=ActionCode.INVALID_ARGS)
|
return ActionResult.failure("drag", str(exc), code=ActionCode.INVALID_ARGS)
|
||||||
return await self._dispatch_drag(planned)
|
if request.feedback is not None and "read_control" not in self.capabilities():
|
||||||
|
return ActionResult.failure(
|
||||||
|
"drag",
|
||||||
|
"Value feedback requires native read_control; no input executed.",
|
||||||
|
code=ActionCode.UNSUPPORTED,
|
||||||
|
metadata={"execution": "not_started", "side_effect": "none"},
|
||||||
|
)
|
||||||
|
return await self._value_feedback.run(
|
||||||
|
request.feedback,
|
||||||
|
[planned],
|
||||||
|
"drag",
|
||||||
|
self.controller.native_request,
|
||||||
|
lambda: self._dispatch_drag(planned),
|
||||||
|
)
|
||||||
|
|
||||||
async def _dispatch_drag(self, planned: dict) -> ActionResult:
|
async def _dispatch_drag(self, planned: dict) -> ActionResult:
|
||||||
metadata = dict(planned)
|
metadata = dict(planned)
|
||||||
@@ -374,19 +393,18 @@ class AdbActuator:
|
|||||||
f"Panel target {step['target']} did not become visible within {step['timeout_ms']}ms after one opener tap. No opener retry; re-observe before continuing."
|
f"Panel target {step['target']} did not become visible within {step['timeout_ms']}ms after one opener tap. No opener retry; re-observe before continuing."
|
||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
async def action_sequence(self, steps: list[Any]) -> ActionResult:
|
async def action_sequence(
|
||||||
|
self, steps: list[Any], feedback: dict[str, Any] | None = None
|
||||||
|
) -> ActionResult:
|
||||||
"""Execute once; optional visibility guards read live UI without model turns."""
|
"""Execute once; optional visibility guards read live UI without model turns."""
|
||||||
import time
|
|
||||||
|
|
||||||
from artemis.config.agent import load_agent_config
|
from artemis.config.agent import load_agent_config
|
||||||
from artemis.data_engine.trace import TraceSpan
|
|
||||||
from artemis.mcp.gestures import plan_sequence, validate_sequence
|
from artemis.mcp.gestures import plan_sequence, validate_sequence
|
||||||
from artemis.runtime.task_control import TaskControl, has_pending_instruction
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
limit = load_agent_config().pro.execution.max_burst_actions
|
limit = load_agent_config().pro.execution.max_burst_actions
|
||||||
|
request = validate_sequence({"steps": steps, "feedback": feedback}, limit)
|
||||||
planned = plan_sequence(
|
planned = plan_sequence(
|
||||||
validate_sequence({"steps": steps}, limit),
|
request,
|
||||||
*self._dims(),
|
*self._dims(),
|
||||||
limit=limit,
|
limit=limit,
|
||||||
)
|
)
|
||||||
@@ -406,6 +424,28 @@ class AdbActuator:
|
|||||||
code=ActionCode.UNSUPPORTED,
|
code=ActionCode.UNSUPPORTED,
|
||||||
metadata={"execution": "not_started", "side_effect": "none"},
|
metadata={"execution": "not_started", "side_effect": "none"},
|
||||||
)
|
)
|
||||||
|
if request.feedback is not None and "read_control" not in self.capabilities():
|
||||||
|
return ActionResult.failure(
|
||||||
|
"action_sequence",
|
||||||
|
"Value feedback requires native read_control; no input executed.",
|
||||||
|
code=ActionCode.UNSUPPORTED,
|
||||||
|
metadata={"execution": "not_started", "side_effect": "none"},
|
||||||
|
)
|
||||||
|
return await self._value_feedback.run(
|
||||||
|
request.feedback,
|
||||||
|
planned,
|
||||||
|
"action_sequence",
|
||||||
|
self.controller.native_request,
|
||||||
|
lambda: self._execute_sequence(planned),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _execute_sequence(self, planned: list[dict]) -> ActionResult:
|
||||||
|
import time
|
||||||
|
|
||||||
|
from artemis.data_engine.trace import TraceSpan
|
||||||
|
from artemis.mcp.native_actions import NATIVE_ACTIONS
|
||||||
|
from artemis.runtime.task_control import TaskControl, has_pending_instruction
|
||||||
|
|
||||||
started = time.monotonic()
|
started = time.monotonic()
|
||||||
receipts = [
|
receipts = [
|
||||||
{**step, "index": i + 1, "execution": "not_started"} for i, step in enumerate(planned)
|
{**step, "index": i + 1, "execution": "not_started"} for i, step in enumerate(planned)
|
||||||
@@ -484,8 +524,12 @@ class AdbActuator:
|
|||||||
error = None if result.ok else result.message
|
error = None if result.ok else result.message
|
||||||
receipt.update(result.metadata)
|
receipt.update(result.metadata)
|
||||||
receipt["native_execution"] = result.metadata.get("execution", "unknown")
|
receipt["native_execution"] = result.metadata.get("execution", "unknown")
|
||||||
|
member_execution = (
|
||||||
|
"skipped"
|
||||||
|
if result.ok and result.metadata.get("skipped")
|
||||||
|
else receipt["native_execution"]
|
||||||
|
)
|
||||||
if not result.ok:
|
if not result.ok:
|
||||||
member_execution = result.metadata.get("execution", "unknown")
|
|
||||||
failed_code = result.code
|
failed_code = result.code
|
||||||
elif step["action"] == "drag":
|
elif step["action"] == "drag":
|
||||||
result = await self._dispatch_drag(step)
|
result = await self._dispatch_drag(step)
|
||||||
@@ -503,6 +547,7 @@ class AdbActuator:
|
|||||||
member_execution
|
member_execution
|
||||||
if member_execution
|
if member_execution
|
||||||
in {"skipped", "not_started", "cancelled", "observed"}
|
in {"skipped", "not_started", "cancelled", "observed"}
|
||||||
|
or receipt.get("native_execution") == "completed"
|
||||||
or not error
|
or not error
|
||||||
else "unknown"
|
else "unknown"
|
||||||
),
|
),
|
||||||
@@ -553,7 +598,7 @@ class AdbActuator:
|
|||||||
)
|
)
|
||||||
return ActionResult.success(
|
return ActionResult.success(
|
||||||
"action_sequence",
|
"action_sequence",
|
||||||
f"Executed {completed} gesture steps consecutively; skipped {skipped} unnecessary UI-opening steps. Verify the intended result in the final screen.",
|
f"Executed {completed} gesture steps consecutively; skipped {skipped} already satisfied steps. Verify the intended result in the final screen.",
|
||||||
metadata=metadata,
|
metadata=metadata,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -105,7 +105,9 @@ class Actuator(Protocol):
|
|||||||
|
|
||||||
async def scroll_to(self, **kwargs: Any) -> ActionResult: ...
|
async def scroll_to(self, **kwargs: Any) -> ActionResult: ...
|
||||||
|
|
||||||
async def action_sequence(self, steps: list[Any]) -> ActionResult: ...
|
async def action_sequence(
|
||||||
|
self, steps: list[Any], feedback: dict[str, Any] | None = None
|
||||||
|
) -> ActionResult: ...
|
||||||
|
|
||||||
async def manage_app(self, action: str, app_name: str) -> ActionResult: ...
|
async def manage_app(self, action: str, app_name: str) -> ActionResult: ...
|
||||||
|
|
||||||
|
|||||||
+42
-3
@@ -14,7 +14,10 @@ from artemis.mcp.action_parameters import (
|
|||||||
Duration,
|
Duration,
|
||||||
GestureModel,
|
GestureModel,
|
||||||
Point,
|
Point,
|
||||||
|
Region,
|
||||||
pixel_point,
|
pixel_point,
|
||||||
|
pixel_region,
|
||||||
|
require_in_region,
|
||||||
)
|
)
|
||||||
from artemis.mcp.native_actions import (
|
from artemis.mcp.native_actions import (
|
||||||
ControlActionRequest,
|
ControlActionRequest,
|
||||||
@@ -24,6 +27,7 @@ from artemis.mcp.native_actions import (
|
|||||||
plan_pinch,
|
plan_pinch,
|
||||||
plan_touch,
|
plan_touch,
|
||||||
)
|
)
|
||||||
|
from artemis.mcp.value_feedback import ValueFeedback
|
||||||
|
|
||||||
|
|
||||||
class ScaleAnchor(GestureModel):
|
class ScaleAnchor(GestureModel):
|
||||||
@@ -43,9 +47,19 @@ class DragCalibration(GestureModel):
|
|||||||
indicator_position: float | None = Field(
|
indicator_position: float | None = Field(
|
||||||
default=None, description="Required for pan: the fixed indicator's observed axis position."
|
default=None, description="Required for pan: the fixed indicator's observed axis position."
|
||||||
)
|
)
|
||||||
|
tolerance: float | None = Field(
|
||||||
|
default=None,
|
||||||
|
ge=0,
|
||||||
|
description="Allowed value error. Reject a plan whose pixel rounding exceeds it; this is geometric error only, not proof of application precision.",
|
||||||
|
)
|
||||||
|
min_movement_px: float | None = Field(
|
||||||
|
default=None,
|
||||||
|
gt=0,
|
||||||
|
description="Previously measured minimum effective drag distance in pixels. If this calibrated move is smaller, refuse input and zoom first. Never guess a device-wide threshold.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DragRequest(GestureModel):
|
class DragGeometry(GestureModel):
|
||||||
start: Point
|
start: Point
|
||||||
target_description: str = Field(min_length=1)
|
target_description: str = Field(min_length=1)
|
||||||
end: Point | None = Field(
|
end: Point | None = Field(
|
||||||
@@ -53,6 +67,10 @@ class DragRequest(GestureModel):
|
|||||||
)
|
)
|
||||||
coordinate_space: CoordinateSpace = "normalized"
|
coordinate_space: CoordinateSpace = "normalized"
|
||||||
duration_ms: Duration = 300
|
duration_ms: Duration = 300
|
||||||
|
region: Region | None = Field(
|
||||||
|
default=None,
|
||||||
|
description="Observed gesture surface rectangle in coordinate_space, excluding date/navigation rails and unrelated overlays.",
|
||||||
|
)
|
||||||
calibration: DragCalibration | None = Field(
|
calibration: DragCalibration | None = Field(
|
||||||
default=None, description="Computes the endpoint from observed anchors; then set end=null."
|
default=None, description="Computes the endpoint from observed anchors; then set end=null."
|
||||||
)
|
)
|
||||||
@@ -72,6 +90,10 @@ class DragRequest(GestureModel):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class DragRequest(DragGeometry):
|
||||||
|
feedback: ValueFeedback | None = None
|
||||||
|
|
||||||
|
|
||||||
class ClickStep(GestureModel):
|
class ClickStep(GestureModel):
|
||||||
action: Literal["click"]
|
action: Literal["click"]
|
||||||
target: Point | Annotated[str, Field(min_length=1, max_length=512)] = Field(
|
target: Point | Annotated[str, Field(min_length=1, max_length=512)] = Field(
|
||||||
@@ -95,7 +117,7 @@ class ClickStep(GestureModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DragStep(DragRequest):
|
class DragStep(DragGeometry):
|
||||||
action: Literal["drag"]
|
action: Literal["drag"]
|
||||||
|
|
||||||
|
|
||||||
@@ -170,6 +192,7 @@ GestureStep = (
|
|||||||
|
|
||||||
class SequenceRequest(GestureModel):
|
class SequenceRequest(GestureModel):
|
||||||
steps: list[GestureStep] = Field(min_length=1, max_length=10)
|
steps: list[GestureStep] = Field(min_length=1, max_length=10)
|
||||||
|
feedback: ValueFeedback | None = None
|
||||||
|
|
||||||
|
|
||||||
def validate_sequence(payload: dict, limit: int) -> SequenceRequest:
|
def validate_sequence(payload: dict, limit: int) -> SequenceRequest:
|
||||||
@@ -183,7 +206,7 @@ def validate_sequence(payload: dict, limit: int) -> SequenceRequest:
|
|||||||
return SequenceRequest.model_validate(payload)
|
return SequenceRequest.model_validate(payload)
|
||||||
|
|
||||||
|
|
||||||
def plan_drag(request: DragRequest, width: int, height: int) -> dict:
|
def plan_drag(request: DragGeometry, width: int, height: int) -> dict:
|
||||||
start = pixel_point(request.start, request.coordinate_space, width, height)
|
start = pixel_point(request.start, request.coordinate_space, width, height)
|
||||||
calibration = None
|
calibration = None
|
||||||
if request.calibration is None:
|
if request.calibration is None:
|
||||||
@@ -237,8 +260,23 @@ def plan_drag(request: DragRequest, width: int, height: int) -> dict:
|
|||||||
"rounding_error_value": abs(end[axis] - endpoint) * abs(slope),
|
"rounding_error_value": abs(end[axis] - endpoint) * abs(slope),
|
||||||
"verification": "pending_observation",
|
"verification": "pending_observation",
|
||||||
}
|
}
|
||||||
|
calibration["movement_px"] = abs(end[axis] - start[axis])
|
||||||
|
if scale.tolerance is not None:
|
||||||
|
calibration["pixel_error_budget"] = scale.tolerance / abs(slope)
|
||||||
|
if calibration["rounding_error_value"] > scale.tolerance:
|
||||||
|
raise ValueError(
|
||||||
|
"Pixel rounding exceeds the requested tolerance; zoom and re-observe anchors before input."
|
||||||
|
)
|
||||||
|
if scale.min_movement_px is not None and calibration["movement_px"] < scale.min_movement_px:
|
||||||
|
raise ValueError(
|
||||||
|
"Planned movement is below the observed minimum effective distance; zoom and re-observe anchors. No input executed."
|
||||||
|
)
|
||||||
if start == end:
|
if start == end:
|
||||||
raise ValueError("Drag has no pixel movement; zoom further or choose a different endpoint.")
|
raise ValueError("Drag has no pixel movement; zoom further or choose a different endpoint.")
|
||||||
|
region = None
|
||||||
|
if request.region is not None:
|
||||||
|
region = pixel_region(request.region, request.coordinate_space, width, height)
|
||||||
|
require_in_region([start, end], region)
|
||||||
return {
|
return {
|
||||||
"action": "drag",
|
"action": "drag",
|
||||||
"coordinates": [*start, *end],
|
"coordinates": [*start, *end],
|
||||||
@@ -250,6 +288,7 @@ def plan_drag(request: DragRequest, width: int, height: int) -> dict:
|
|||||||
],
|
],
|
||||||
"duration_ms": request.duration_ms,
|
"duration_ms": request.duration_ms,
|
||||||
"target_description": request.target_description,
|
"target_description": request.target_description,
|
||||||
|
**({"region": region} if region is not None else {}),
|
||||||
**({"require_visible": request.require_visible} if request.require_visible else {}),
|
**({"require_visible": request.require_visible} if request.require_visible else {}),
|
||||||
**({"calibration": calibration} if calibration else {}),
|
**({"calibration": calibration} if calibration else {}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ from artemis.mcp.action_parameters import (
|
|||||||
Duration,
|
Duration,
|
||||||
GestureModel,
|
GestureModel,
|
||||||
Point,
|
Point,
|
||||||
|
Region,
|
||||||
pixel_point,
|
pixel_point,
|
||||||
|
pixel_region,
|
||||||
|
require_in_region,
|
||||||
)
|
)
|
||||||
|
|
||||||
ShortText = Annotated[str, Field(min_length=1, max_length=512)]
|
ShortText = Annotated[str, Field(min_length=1, max_length=512)]
|
||||||
@@ -71,6 +74,11 @@ class ControlActionRequest(ReadControlRequest):
|
|||||||
text: Annotated[str, StringConstraints(strip_whitespace=False, max_length=65536)] | None = None
|
text: Annotated[str, StringConstraints(strip_whitespace=False, max_length=65536)] | None = None
|
||||||
selection_start: Annotated[int, Field(strict=True, ge=0)] | None = None
|
selection_start: Annotated[int, Field(strict=True, ge=0)] | None = None
|
||||||
selection_end: Annotated[int, Field(strict=True, ge=0)] | None = None
|
selection_end: Annotated[int, Field(strict=True, ge=0)] | None = None
|
||||||
|
ensure_state: "ControlCondition | None" = Field(
|
||||||
|
default=None,
|
||||||
|
description="Desired observed state: skip an already satisfied action; otherwise write ONCE and verify by read-only polling. Never infer state from an icon or missing attributes.",
|
||||||
|
)
|
||||||
|
verify_timeout_ms: Annotated[int, Field(strict=True, ge=1, le=3000)] = 1500
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def action_arguments(self):
|
def action_arguments(self):
|
||||||
@@ -106,6 +114,10 @@ class TouchGestureRequest(GestureModel):
|
|||||||
coordinate_space: CoordinateSpace = "normalized"
|
coordinate_space: CoordinateSpace = "normalized"
|
||||||
target_description: ShortText
|
target_description: ShortText
|
||||||
require_visible: ShortText | None = None
|
require_visible: ShortText | None = None
|
||||||
|
region: Region | None = Field(
|
||||||
|
default=None,
|
||||||
|
description="Observed gesture surface bounds in coordinate_space; every finger path must stay inside it.",
|
||||||
|
)
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def continuous_timeline(self):
|
def continuous_timeline(self):
|
||||||
@@ -130,6 +142,10 @@ class PinchRequest(GestureModel):
|
|||||||
coordinate_space: CoordinateSpace = "normalized"
|
coordinate_space: CoordinateSpace = "normalized"
|
||||||
target_description: ShortText
|
target_description: ShortText
|
||||||
require_visible: ShortText | None = None
|
require_visible: ShortText | None = None
|
||||||
|
region: Region | None = Field(
|
||||||
|
default=None,
|
||||||
|
description="Observed zoom surface bounds in coordinate_space; not the whole screen or date rail. Both fingers must remain inside.",
|
||||||
|
)
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def meaningful_pinch(self):
|
def meaningful_pinch(self):
|
||||||
@@ -149,6 +165,7 @@ class WaitUntilRequest(ReadControlRequest):
|
|||||||
"selected",
|
"selected",
|
||||||
"unselected",
|
"unselected",
|
||||||
"text",
|
"text",
|
||||||
|
"content_desc",
|
||||||
"value",
|
"value",
|
||||||
"stable",
|
"stable",
|
||||||
]
|
]
|
||||||
@@ -163,8 +180,8 @@ class WaitUntilRequest(ReadControlRequest):
|
|||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def expected_arguments(self):
|
def expected_arguments(self):
|
||||||
if (self.expected_text is not None) != (self.state == "text"):
|
if (self.expected_text is not None) != (self.state in {"text", "content_desc"}):
|
||||||
raise ValueError("Only state=text requires expected_text (exact match).")
|
raise ValueError("Only state=text/content_desc requires expected_text (exact match).")
|
||||||
if (self.expected_value is not None) != (self.state == "value"):
|
if (self.expected_value is not None) != (self.state == "value"):
|
||||||
raise ValueError("Only state=value requires expected_value.")
|
raise ValueError("Only state=value requires expected_value.")
|
||||||
if self.state != "value" and self.tolerance != 0:
|
if self.state != "value" and self.tolerance != 0:
|
||||||
@@ -174,6 +191,26 @@ class WaitUntilRequest(ReadControlRequest):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class ControlCondition(GestureModel):
|
||||||
|
state: Literal[
|
||||||
|
"checked", "unchecked", "selected", "unselected", "text", "content_desc", "value"
|
||||||
|
]
|
||||||
|
expected_text: (
|
||||||
|
Annotated[str, StringConstraints(strip_whitespace=False, max_length=4096)] | None
|
||||||
|
) = None
|
||||||
|
expected_value: float | None = None
|
||||||
|
tolerance: Annotated[float, Field(ge=0)] = 0
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
def expected_arguments(self):
|
||||||
|
# Reuse the wait predicate contract without duplicating its semantics.
|
||||||
|
WaitUntilRequest(selector={"class_name": "condition"}, **self.model_dump())
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
ControlActionRequest.model_rebuild()
|
||||||
|
|
||||||
|
|
||||||
class ScrollToRequest(GestureModel):
|
class ScrollToRequest(GestureModel):
|
||||||
container: ControlSelector
|
container: ControlSelector
|
||||||
target: ControlSelector
|
target: ControlSelector
|
||||||
@@ -222,6 +259,10 @@ def plan_touch(request: TouchGestureRequest, width: int, height: int) -> dict:
|
|||||||
}
|
}
|
||||||
for frame in request.frames
|
for frame in request.frames
|
||||||
]
|
]
|
||||||
|
region = None
|
||||||
|
if request.region is not None:
|
||||||
|
region = pixel_region(request.region, request.coordinate_space, width, height)
|
||||||
|
require_in_region([p for frame in frames for p in frame["points"]], region)
|
||||||
# Reject trajectories that merge two fingers, including between keyframes.
|
# Reject trajectories that merge two fingers, including between keyframes.
|
||||||
for before, after in zip(frames, frames[1:]):
|
for before, after in zip(frames, frames[1:]):
|
||||||
for first in range(len(before["points"])):
|
for first in range(len(before["points"])):
|
||||||
@@ -240,6 +281,7 @@ def plan_touch(request: TouchGestureRequest, width: int, height: int) -> dict:
|
|||||||
"coordinate_space": "pixel",
|
"coordinate_space": "pixel",
|
||||||
"screen_size": [width, height],
|
"screen_size": [width, height],
|
||||||
"target_description": request.target_description,
|
"target_description": request.target_description,
|
||||||
|
**({"region": region} if region is not None else {}),
|
||||||
**({"require_visible": request.require_visible} if request.require_visible else {}),
|
**({"require_visible": request.require_visible} if request.require_visible else {}),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,6 +313,9 @@ def plan_pinch(request: PinchRequest, width: int, height: int) -> dict:
|
|||||||
coordinate_space="pixel",
|
coordinate_space="pixel",
|
||||||
target_description=request.target_description,
|
target_description=request.target_description,
|
||||||
require_visible=request.require_visible,
|
require_visible=request.require_visible,
|
||||||
|
region=pixel_region(request.region, request.coordinate_space, width, height)
|
||||||
|
if request.region is not None
|
||||||
|
else None,
|
||||||
),
|
),
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
|||||||
@@ -158,10 +158,10 @@ async def _pause(deadline: float, interval: float) -> None:
|
|||||||
|
|
||||||
def _condition(request: WaitUntilRequest, node: dict) -> bool:
|
def _condition(request: WaitUntilRequest, node: dict) -> bool:
|
||||||
state = request.state
|
state = request.state
|
||||||
if state == "text":
|
if state in {"text", "content_desc"}:
|
||||||
if node.get("password") is True or not isinstance(node.get("text"), str):
|
if node.get("password") is True or not isinstance(node.get(state), str):
|
||||||
raise ValueError("An exact readable native text value is required.")
|
raise ValueError("An exact readable native text value is required.")
|
||||||
return node["text"] == request.expected_text
|
return node[state] == request.expected_text
|
||||||
if state == "value":
|
if state == "value":
|
||||||
native_range = _range(node)
|
native_range = _range(node)
|
||||||
if native_range is None or request.expected_value is None:
|
if native_range is None or request.expected_value is None:
|
||||||
@@ -268,6 +268,23 @@ async def _control(request: ControlActionRequest, native: NativeCaller) -> dict:
|
|||||||
if not inspection["success"]:
|
if not inspection["success"]:
|
||||||
return _result("control_action", inspection)
|
return _result("control_action", inspection)
|
||||||
node = inspection["node"]
|
node = inspection["node"]
|
||||||
|
condition = None
|
||||||
|
if request.ensure_state is not None:
|
||||||
|
condition = WaitUntilRequest(
|
||||||
|
selector=request.selector,
|
||||||
|
timeout_ms=request.verify_timeout_ms,
|
||||||
|
**request.ensure_state.model_dump(),
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
satisfied = _condition(condition, node)
|
||||||
|
except ValueError as exc:
|
||||||
|
return _result(
|
||||||
|
"control_action", _failure("INVALID_STATE", str(exc)), preflight=inspection
|
||||||
|
)
|
||||||
|
if satisfied:
|
||||||
|
return _result(
|
||||||
|
"control_action", inspection, skipped=True, verification="condition_satisfied"
|
||||||
|
)
|
||||||
invalid = _write_guard(node, request.action, request.expected_token)
|
invalid = _write_guard(node, request.action, request.expected_token)
|
||||||
if invalid is None and request.action == "set_progress":
|
if invalid is None and request.action == "set_progress":
|
||||||
native_range = _range(node)
|
native_range = _range(node)
|
||||||
@@ -284,9 +301,30 @@ async def _control(request: ControlActionRequest, native: NativeCaller) -> dict:
|
|||||||
)
|
)
|
||||||
if invalid is not None:
|
if invalid is not None:
|
||||||
return _result("control_action", invalid, preflight=inspection, node_before=node)
|
return _result("control_action", invalid, preflight=inspection, node_before=node)
|
||||||
args = request.model_dump(mode="json", exclude_none=True)
|
args = request.model_dump(
|
||||||
|
mode="json", exclude_none=True, exclude={"ensure_state", "verify_timeout_ms"}
|
||||||
|
)
|
||||||
args["expected_token"] = node["token"]
|
args["expected_token"] = node["token"]
|
||||||
receipt = await _call(native, "control_action", args)
|
receipt = await _call(native, "control_action", args)
|
||||||
|
if receipt["success"] and condition is not None:
|
||||||
|
verification = await _wait(condition, native)
|
||||||
|
outcome = dict(receipt)
|
||||||
|
if not verification["success"]:
|
||||||
|
outcome.update(
|
||||||
|
success=False,
|
||||||
|
code=verification["code"],
|
||||||
|
error="The input completed, but its requested state was not verified. Do not toggle again blindly.",
|
||||||
|
)
|
||||||
|
return _result(
|
||||||
|
"control_action",
|
||||||
|
outcome,
|
||||||
|
native_receipt=receipt,
|
||||||
|
preflight=inspection,
|
||||||
|
postcondition=verification,
|
||||||
|
node_before=node,
|
||||||
|
node_after=verification.get("node"),
|
||||||
|
verification="condition_satisfied" if verification["success"] else "condition_not_met",
|
||||||
|
)
|
||||||
return _result(
|
return _result(
|
||||||
"control_action",
|
"control_action",
|
||||||
receipt,
|
receipt,
|
||||||
|
|||||||
@@ -0,0 +1,284 @@
|
|||||||
|
"""Optional, task-local feedback for numeric adjustments; never retries an input."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import re
|
||||||
|
from collections.abc import Awaitable, Callable
|
||||||
|
from asyncio import sleep
|
||||||
|
from time import monotonic
|
||||||
|
from typing import Annotated, Literal
|
||||||
|
|
||||||
|
from pydantic import Field, model_validator
|
||||||
|
|
||||||
|
from artemis.mcp.action_parameters import GestureModel
|
||||||
|
from artemis.mcp.action_types import ActionCode, ActionResult
|
||||||
|
from artemis.mcp.native_actions import ControlSelector
|
||||||
|
from artemis.mcp.native_executor import NativeCaller, execute_native
|
||||||
|
|
||||||
|
|
||||||
|
class ValueFeedback(GestureModel):
|
||||||
|
selector: ControlSelector = Field(
|
||||||
|
description="Unique, observed persistent value readout, not a transient drag tooltip. Copy its stable selector, excluding changing text."
|
||||||
|
)
|
||||||
|
value_format: Literal["range", "number", "clock"] = Field(
|
||||||
|
description="range reads native range.current; number requires an entire numeric text; clock requires exactly one HH:MM:SS in readable text and returns seconds since midnight."
|
||||||
|
)
|
||||||
|
target_value: float
|
||||||
|
tolerance: Annotated[float, Field(ge=0)]
|
||||||
|
stable_ms: Annotated[int, Field(strict=True, ge=100, le=5000)] = 1000
|
||||||
|
timeout_ms: Annotated[int, Field(strict=True, ge=200, le=10000)] = 3000
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
def bounded_observation(self):
|
||||||
|
if self.stable_ms >= self.timeout_ms:
|
||||||
|
raise ValueError("stable_ms must be smaller than timeout_ms.")
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
def read_value(node: dict, value_format: str) -> float:
|
||||||
|
if node.get("password") is True or node.get("visible") is not True:
|
||||||
|
raise ValueError("The value must be visible, readable and non-password.")
|
||||||
|
if value_format == "range":
|
||||||
|
native_range = node.get("range")
|
||||||
|
if not isinstance(native_range, dict):
|
||||||
|
raise ValueError("No native numeric range; do not guess a value from the gesture.")
|
||||||
|
values = [native_range.get(k) for k in ("min", "current", "max")]
|
||||||
|
if any(type(v) not in (int, float) or not math.isfinite(v) for v in values):
|
||||||
|
raise ValueError("Invalid native numeric range.")
|
||||||
|
low, current, high = values
|
||||||
|
if not low <= current <= high:
|
||||||
|
raise ValueError("Invalid native numeric range bounds.")
|
||||||
|
return float(current)
|
||||||
|
text = node.get("text")
|
||||||
|
if not isinstance(text, str) or len(text) > 4096:
|
||||||
|
raise ValueError("The selected node has no bounded readable text.")
|
||||||
|
if value_format == "clock":
|
||||||
|
matches = re.findall(r"(?<![\d:])([01]\d|2[0-3]):([0-5]\d):([0-5]\d)(?![\d:])", text)
|
||||||
|
if len(matches) != 1:
|
||||||
|
raise ValueError(
|
||||||
|
"clock requires exactly one unambiguous HH:MM:SS; select its persistent readout."
|
||||||
|
)
|
||||||
|
h, m, s = map(int, matches[0])
|
||||||
|
return float(h * 3600 + m * 60 + s)
|
||||||
|
if not re.fullmatch(r"[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?", text.strip()):
|
||||||
|
raise ValueError("number requires an entire numeric text, without units or other numbers.")
|
||||||
|
value = float(text)
|
||||||
|
if not math.isfinite(value):
|
||||||
|
raise ValueError("The observed value must be finite.")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def strategy_key(planned: list[dict]) -> str:
|
||||||
|
"""Ignore tiny coordinate jitter; a new scale, axis or meaningful span is a new strategy."""
|
||||||
|
strategy = []
|
||||||
|
for step in planned:
|
||||||
|
if step["action"] == "drag":
|
||||||
|
x1, y1, x2, y2 = step["coordinates"]
|
||||||
|
dx, dy = x2 - x1, y2 - y1
|
||||||
|
calibration = step.get("calibration", {})
|
||||||
|
strategy.append(
|
||||||
|
[
|
||||||
|
"drag",
|
||||||
|
"x" if abs(dx) >= abs(dy) else "y",
|
||||||
|
1 if (dx if abs(dx) >= abs(dy) else dy) > 0 else -1,
|
||||||
|
math.hypot(dx, dy),
|
||||||
|
step["duration_ms"],
|
||||||
|
round(calibration.get("value_per_pixel", 0), 8),
|
||||||
|
step.get("region"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
strategy.append(step)
|
||||||
|
return json.dumps(strategy, sort_keys=True, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
def same_strategy(left: str, right: str) -> bool:
|
||||||
|
"""Compare relative changes; crossing an arbitrary pixel bucket is not a new strategy."""
|
||||||
|
before, after = json.loads(left), json.loads(right)
|
||||||
|
if len(before) != len(after):
|
||||||
|
return False
|
||||||
|
for old, new in zip(before, after, strict=True):
|
||||||
|
if isinstance(old, list) and isinstance(new, list) and old[0] == new[0] == "drag":
|
||||||
|
if old[1:3] != new[1:3] or old[6] != new[6]:
|
||||||
|
return False
|
||||||
|
if any(not 0.5 < new[i] / old[i] < 2 for i in (3, 4)):
|
||||||
|
return False
|
||||||
|
if not math.isclose(old[5], new[5], rel_tol=0.05):
|
||||||
|
return False
|
||||||
|
elif old != new:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class ValueFeedbackRunner:
|
||||||
|
"""Bounded memory owned by one device actuator/task, never persisted or shared."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.history: dict[str, tuple[str, float, int]] = {}
|
||||||
|
|
||||||
|
async def _observe(
|
||||||
|
self, request: ValueFeedback, native: NativeCaller, *, stable: bool
|
||||||
|
) -> tuple[float, dict]:
|
||||||
|
deadline = monotonic() + request.timeout_ms / 1000
|
||||||
|
previous = None
|
||||||
|
since = monotonic()
|
||||||
|
samples = 0
|
||||||
|
async with asyncio.timeout(request.timeout_ms / 1000):
|
||||||
|
while monotonic() < deadline:
|
||||||
|
receipt = await execute_native(
|
||||||
|
"read_control",
|
||||||
|
{"selector": request.selector.model_dump(exclude_none=True)},
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
native,
|
||||||
|
)
|
||||||
|
if monotonic() >= deadline:
|
||||||
|
raise TimeoutError("The value observation arrived after the feedback deadline.")
|
||||||
|
if not receipt["success"]:
|
||||||
|
if stable and receipt.get("code") in {
|
||||||
|
"NOT_FOUND",
|
||||||
|
"READ_FAILED",
|
||||||
|
"UNAVAILABLE",
|
||||||
|
}:
|
||||||
|
previous = None
|
||||||
|
since = monotonic()
|
||||||
|
await sleep(min(0.1, max(0, deadline - monotonic())))
|
||||||
|
continue
|
||||||
|
raise ValueError(
|
||||||
|
f"Value read failed: {receipt.get('code')}; no value inferred."
|
||||||
|
)
|
||||||
|
value = read_value(receipt["node"], request.value_format)
|
||||||
|
samples += 1
|
||||||
|
evidence = {
|
||||||
|
"node": receipt["node"],
|
||||||
|
"samples": samples,
|
||||||
|
"stable_for_ms": round((monotonic() - since) * 1000)
|
||||||
|
if stable and value == previous
|
||||||
|
else 0,
|
||||||
|
}
|
||||||
|
if not stable:
|
||||||
|
return value, evidence
|
||||||
|
if value != previous:
|
||||||
|
previous, since = value, monotonic()
|
||||||
|
elif monotonic() - since >= request.stable_ms / 1000:
|
||||||
|
return value, evidence
|
||||||
|
await sleep(min(0.1, max(0, deadline - monotonic())))
|
||||||
|
raise TimeoutError("The value did not settle within the feedback deadline.")
|
||||||
|
|
||||||
|
async def run(
|
||||||
|
self,
|
||||||
|
request: ValueFeedback | None,
|
||||||
|
planned: list[dict],
|
||||||
|
action: str,
|
||||||
|
native: NativeCaller,
|
||||||
|
operation: Callable[[], Awaitable[ActionResult]],
|
||||||
|
) -> ActionResult:
|
||||||
|
if request is None:
|
||||||
|
return await operation()
|
||||||
|
key = json.dumps(
|
||||||
|
[
|
||||||
|
request.selector.model_dump(),
|
||||||
|
request.value_format,
|
||||||
|
request.target_value,
|
||||||
|
request.tolerance,
|
||||||
|
],
|
||||||
|
sort_keys=True,
|
||||||
|
)
|
||||||
|
strategy = strategy_key(planned)
|
||||||
|
try:
|
||||||
|
before, before_observation = await self._observe(request, native, stable=False)
|
||||||
|
except (ValueError, TimeoutError) as exc:
|
||||||
|
self.history.pop(key, None)
|
||||||
|
return ActionResult.failure(
|
||||||
|
action,
|
||||||
|
f"Feedback unavailable; no input executed: {exc}",
|
||||||
|
code=ActionCode.TARGET_NOT_FOUND,
|
||||||
|
metadata={
|
||||||
|
"execution": "not_started",
|
||||||
|
"side_effect": "none",
|
||||||
|
"verification": "unavailable",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
previous = self.history.get(key)
|
||||||
|
stalled = (
|
||||||
|
previous[2]
|
||||||
|
if previous and previous[1] == before and same_strategy(previous[0], strategy)
|
||||||
|
else 0
|
||||||
|
)
|
||||||
|
feedback = {
|
||||||
|
"selector": request.selector.model_dump(exclude_none=True),
|
||||||
|
"before_value": before,
|
||||||
|
"before_observation": before_observation,
|
||||||
|
"target_value": request.target_value,
|
||||||
|
"tolerance": request.tolerance,
|
||||||
|
"value_format": request.value_format,
|
||||||
|
}
|
||||||
|
if stalled >= 2:
|
||||||
|
return ActionResult.failure(
|
||||||
|
action,
|
||||||
|
"NO_PROGRESS: two adjustments did not reduce the actual residual. No further input executed. Re-observe the gesture surface and scale; zoom, change the meaningful drag span or use a native setter. Do not repeat the same small drag.",
|
||||||
|
metadata={
|
||||||
|
"execution": "not_started",
|
||||||
|
"side_effect": "none",
|
||||||
|
"verification": "strategy_change_required",
|
||||||
|
"value_feedback": {**feedback, "consecutive_no_progress": stalled},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
result = await operation()
|
||||||
|
if not result.ok:
|
||||||
|
self.history.pop(key, None)
|
||||||
|
return result
|
||||||
|
try:
|
||||||
|
after, after_observation = await self._observe(request, native, stable=True)
|
||||||
|
except (ValueError, TimeoutError) as exc:
|
||||||
|
self.history.pop(key, None)
|
||||||
|
return result.model_copy(
|
||||||
|
update={
|
||||||
|
"ok": False,
|
||||||
|
"code": ActionCode.TIMEOUT
|
||||||
|
if isinstance(exc, TimeoutError)
|
||||||
|
else ActionCode.TARGET_NOT_FOUND,
|
||||||
|
"message": f"Input completed; value feedback unavailable: {exc}. Do not replay the input blindly.",
|
||||||
|
"metadata": {
|
||||||
|
**result.metadata,
|
||||||
|
"execution": "completed",
|
||||||
|
"verification": "unavailable",
|
||||||
|
"value_feedback": feedback,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
residual = after - request.target_value
|
||||||
|
matched = abs(residual) <= request.tolerance
|
||||||
|
improved = abs(residual) < abs(before - request.target_value) - 1e-9
|
||||||
|
stalled = 0 if matched or improved else stalled + 1
|
||||||
|
if len(self.history) >= 32:
|
||||||
|
self.history.pop(next(iter(self.history)))
|
||||||
|
self.history[key] = (strategy, after, stalled)
|
||||||
|
feedback.update(
|
||||||
|
after_observation=after_observation,
|
||||||
|
after_value=after,
|
||||||
|
residual=residual,
|
||||||
|
changed=after != before,
|
||||||
|
improved=improved,
|
||||||
|
within_tolerance=matched,
|
||||||
|
consecutive_no_progress=stalled,
|
||||||
|
)
|
||||||
|
return result.model_copy(
|
||||||
|
update={
|
||||||
|
"ok": matched,
|
||||||
|
"code": ActionCode.OK if matched else ActionCode.DEVICE_ERROR,
|
||||||
|
"message": f"Input completed. Observed value={after:g}, target={request.target_value:g}, residual={residual:g}, tolerance={request.tolerance:g}. "
|
||||||
|
+ (
|
||||||
|
"Value condition met; complete task acceptance separately."
|
||||||
|
if matched
|
||||||
|
else "Value condition not met. Use this observed residual to change strategy; never infer success from dispatch."
|
||||||
|
),
|
||||||
|
"metadata": {
|
||||||
|
**result.metadata,
|
||||||
|
"execution": "completed",
|
||||||
|
"verification": "value_matched" if matched else "value_not_reached",
|
||||||
|
"value_feedback": feedback,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -37,6 +37,24 @@ Flash、普通 Pro 和探索共用同一套动作参数、助手执行入口与
|
|||||||
|
|
||||||
## 时间轴精度
|
## 时间轴精度
|
||||||
|
|
||||||
|
精确调整可绑定可选的 `feedback`:`selector` 指向原生可读取、唯一且持久的真实选中值,`value_format` 为 `range`(原生范围)、`number`(完整数字文本)或 `clock`(文本中唯一的 HH:MM:SS,换算为当天秒数),并提供同单位的 `target_value` 与 `tolerance`。不要选择录像卡片、视频水印或短暂拖动浮层,也不要在 selector 中包含会变化的读数文本。无法建立真实绑定时继续视觉观测,不能编造控件或把手势派发当作读值。
|
||||||
|
|
||||||
|
单独拖动把 feedback 放在 `drag` 参数上;“拖动后暂停”等组合把它放在 `action_sequence` 顶层,不放在 drag 成员上。输入前确认可读,整组成功后再等待读数连续稳定 `stable_ms`(默认 1000,范围 100–5000),总观察不超过 `timeout_ms`(默认 3000,上限 10000,必须大于稳定时长)。时钟稳定时长应覆盖其刷新周期。回执含 before_value、after_value、residual、within_tolerance 和连续无效次数;数值条件满足与最终业务验收分开。输入完成但读数失败或未达标仍保留完成回执,不会自动重发。
|
||||||
|
|
||||||
|
同一执行实例内,同读数、目标和策略连续两次没有减小实际误差,第三次拒绝输入,提示重新观察并放大、改变有效移动距离或使用原生设值。微改坐标不算换策略;新刻度比例、轴向或明显不同的距离可重新尝试。读数改变或任务实例更换后重新判断。此守卫依赖显式 feedback,不声称能够自动理解所有自绘控件。
|
||||||
|
|
||||||
|
所有手指路径可以通过 `region=[left,top,right,bottom]` 限定到已观察到的手势区域;坐标单位跟随 coordinate_space,右/下边界不包含在区域内。拖动、缩放或多指轨迹离开区域时整组在首个输入前拒绝;区域只验证几何边界,不证明窗口未被遮挡。calibration.tolerance 检查像素取整误差,回执给出每像素数值和允许像素误差;min_movement_px 只能填实测起效距离,过小则拒绝并要求放大。几何误差不包括读图、惯性与 APP 的内部吸附误差。
|
||||||
|
|
||||||
|
暂停或勾选等状态可用 `control_action.ensure_state`,支持 checked/unchecked、selected/unselected、text/content_desc 精确匹配及 value/tolerance。先 read_control 确认其语义,稳定 selector 不包含会改变的文字/描述。达到状态时跳过;未达到时仅操作一次,再只读等待 verify_timeout_ms(默认 1500,上限 3000)。状态缺失不当作 false,核验失败保留已完成输入,不继续盲点切换。不能假定 selected 就表示暂停。
|
||||||
|
|
||||||
|
例如,已观察确认资源 ID `player` 的描述为“播放”时表示暂停,可将以下成员放在拖动后:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"action":"control_action","request":{"selector":{"resource_id":"player"},"action":"click","ensure_state":{"state":"content_desc","expected_text":"播放"}}}
|
||||||
|
```
|
||||||
|
|
||||||
|
示例 ID 和状态含义必须换成实际观察结果。绑定时间反馈示例为 `{"selector":{"resource_id":"observed_current_time"},"value_format":"clock","target_value":63737,"tolerance":5}`,对应目标 17:42:17±5 秒;不用于日期验收或跨日时间差。
|
||||||
|
|
||||||
双指距离变化不一定等于 APP 比例尺变化:先小幅缩放,再读取实际刻度,建立像素与时间的局部对应。平移后等待停稳,读取残差再修正;播放中应先暂停后验收。
|
双指距离变化不一定等于 APP 比例尺变化:先小幅缩放,再读取实际刻度,建立像素与时间的局部对应。平移后等待停稳,读取残差再修正;播放中应先暂停后验收。
|
||||||
|
|
||||||
如果常驻读数通过标签、节点角色或层级明确绑定到固定指针当前选时,且在暂停或独立停稳观测中持续显示、与刻度一致,可以直接作为秒级证据,无须再要求粗像素插值独立达到同样精度。事件卡片、录像水印和短暂拖动浮层不能代替当前选时。真实冲突、来源不明或缺少稳定证据时继续保留不确定结论;没有可信读数时仍用刻度插值并计入读图误差。
|
如果常驻读数通过标签、节点角色或层级明确绑定到固定指针当前选时,且在暂停或独立停稳观测中持续显示、与刻度一致,可以直接作为秒级证据,无须再要求粗像素插值独立达到同样精度。事件卡片、录像水印和短暂拖动浮层不能代替当前选时。真实冲突、来源不明或缺少稳定证据时继续保留不确定结论;没有可信读数时仍用刻度插值并计入读图误差。
|
||||||
|
|||||||
@@ -37,6 +37,24 @@ Flash、普通 Pro 和探索共用同一套动作参数、助手执行入口与
|
|||||||
|
|
||||||
## 时间轴精度
|
## 时间轴精度
|
||||||
|
|
||||||
|
精确调整可绑定可选的 `feedback`:`selector` 指向原生可读取、唯一且持久的真实选中值,`value_format` 为 `range`(原生范围)、`number`(完整数字文本)或 `clock`(文本中唯一的 HH:MM:SS,换算为当天秒数),并提供同单位的 `target_value` 与 `tolerance`。不要选择录像卡片、视频水印或短暂拖动浮层,也不要在 selector 中包含会变化的读数文本。无法建立真实绑定时继续视觉观测,不能编造控件或把手势派发当作读值。
|
||||||
|
|
||||||
|
单独拖动把 feedback 放在 `drag` 参数上;“拖动后暂停”等组合把它放在 `action_sequence` 顶层,不放在 drag 成员上。输入前确认可读,整组成功后再等待读数连续稳定 `stable_ms`(默认 1000,范围 100–5000),总观察不超过 `timeout_ms`(默认 3000,上限 10000,必须大于稳定时长)。时钟稳定时长应覆盖其刷新周期。回执含 before_value、after_value、residual、within_tolerance 和连续无效次数;数值条件满足与最终业务验收分开。输入完成但读数失败或未达标仍保留完成回执,不会自动重发。
|
||||||
|
|
||||||
|
同一执行实例内,同读数、目标和策略连续两次没有减小实际误差,第三次拒绝输入,提示重新观察并放大、改变有效移动距离或使用原生设值。微改坐标不算换策略;新刻度比例、轴向或明显不同的距离可重新尝试。读数改变或任务实例更换后重新判断。此守卫依赖显式 feedback,不声称能够自动理解所有自绘控件。
|
||||||
|
|
||||||
|
所有手指路径可以通过 `region=[left,top,right,bottom]` 限定到已观察到的手势区域;坐标单位跟随 coordinate_space,右/下边界不包含在区域内。拖动、缩放或多指轨迹离开区域时整组在首个输入前拒绝;区域只验证几何边界,不证明窗口未被遮挡。calibration.tolerance 检查像素取整误差,回执给出每像素数值和允许像素误差;min_movement_px 只能填实测起效距离,过小则拒绝并要求放大。几何误差不包括读图、惯性与 APP 的内部吸附误差。
|
||||||
|
|
||||||
|
暂停或勾选等状态可用 `control_action.ensure_state`,支持 checked/unchecked、selected/unselected、text/content_desc 精确匹配及 value/tolerance。先 read_control 确认其语义,稳定 selector 不包含会改变的文字/描述。达到状态时跳过;未达到时仅操作一次,再只读等待 verify_timeout_ms(默认 1500,上限 3000)。状态缺失不当作 false,核验失败保留已完成输入,不继续盲点切换。不能假定 selected 就表示暂停。
|
||||||
|
|
||||||
|
例如,已观察确认资源 ID `player` 的描述为“播放”时表示暂停,可将以下成员放在拖动后:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"action":"control_action","request":{"selector":{"resource_id":"player"},"action":"click","ensure_state":{"state":"content_desc","expected_text":"播放"}}}
|
||||||
|
```
|
||||||
|
|
||||||
|
示例 ID 和状态含义必须换成实际观察结果。绑定时间反馈示例为 `{"selector":{"resource_id":"observed_current_time"},"value_format":"clock","target_value":63737,"tolerance":5}`,对应目标 17:42:17±5 秒;不用于日期验收或跨日时间差。
|
||||||
|
|
||||||
双指距离变化不一定等于 APP 比例尺变化:先小幅缩放,再读取实际刻度,建立像素与时间的局部对应。平移后等待停稳,读取残差再修正;播放中应先暂停后验收。
|
双指距离变化不一定等于 APP 比例尺变化:先小幅缩放,再读取实际刻度,建立像素与时间的局部对应。平移后等待停稳,读取残差再修正;播放中应先暂停后验收。
|
||||||
|
|
||||||
如果常驻读数通过标签、节点角色或层级明确绑定到固定指针当前选时,且在暂停或独立停稳观测中持续显示、与刻度一致,可以直接作为秒级证据,无须再要求粗像素插值独立达到同样精度。事件卡片、录像水印和短暂拖动浮层不能代替当前选时。真实冲突、来源不明或缺少稳定证据时继续保留不确定结论;没有可信读数时仍用刻度插值并计入读图误差。
|
如果常驻读数通过标签、节点角色或层级明确绑定到固定指针当前选时,且在暂停或独立停稳观测中持续显示、与刻度一致,可以直接作为秒级证据,无须再要求粗像素插值独立达到同样精度。事件卡片、录像水印和短暂拖动浮层不能代替当前选时。真实冲突、来源不明或缺少稳定证据时继续保留不确定结论;没有可信读数时仍用刻度插值并计入读图误差。
|
||||||
+525
-1
@@ -103,6 +103,60 @@
|
|||||||
"title": "ControlActionStep",
|
"title": "ControlActionStep",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"ControlCondition": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"state": {
|
||||||
|
"enum": [
|
||||||
|
"checked",
|
||||||
|
"unchecked",
|
||||||
|
"selected",
|
||||||
|
"unselected",
|
||||||
|
"text",
|
||||||
|
"content_desc",
|
||||||
|
"value"
|
||||||
|
],
|
||||||
|
"title": "State",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"expected_text": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"maxLength": 4096,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Expected Text"
|
||||||
|
},
|
||||||
|
"expected_value": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Expected Value"
|
||||||
|
},
|
||||||
|
"tolerance": {
|
||||||
|
"default": 0,
|
||||||
|
"minimum": 0,
|
||||||
|
"title": "Tolerance",
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"state"
|
||||||
|
],
|
||||||
|
"title": "ControlCondition",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ControlSelector": {
|
"ControlSelector": {
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"description": "Every supplied attribute is an exact AND match against a fresh native node.",
|
"description": "Every supplied attribute is an exact AND match against a fresh native node.",
|
||||||
@@ -242,6 +296,34 @@
|
|||||||
"default": null,
|
"default": null,
|
||||||
"description": "Required for pan: the fixed indicator's observed axis position.",
|
"description": "Required for pan: the fixed indicator's observed axis position.",
|
||||||
"title": "Indicator Position"
|
"title": "Indicator Position"
|
||||||
|
},
|
||||||
|
"tolerance": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "Allowed value error. Reject a plan whose pixel rounding exceeds it; this is geometric error only, not proof of application precision.",
|
||||||
|
"title": "Tolerance"
|
||||||
|
},
|
||||||
|
"min_movement_px": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"exclusiveMinimum": 0,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "Previously measured minimum effective drag distance in pixels. If this calibrated move is smaller, refuse input and zoom first. Never guess a device-wide threshold.",
|
||||||
|
"title": "Min Movement Px"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -304,6 +386,24 @@
|
|||||||
"title": "Duration Ms",
|
"title": "Duration Ms",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"region": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"maxItems": 4,
|
||||||
|
"minItems": 4,
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "Observed gesture surface rectangle in coordinate_space, excluding date/navigation rails and unrelated overlays.",
|
||||||
|
"title": "Region"
|
||||||
|
},
|
||||||
"calibration": {
|
"calibration": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -464,6 +564,24 @@
|
|||||||
"default": null,
|
"default": null,
|
||||||
"title": "Require Visible"
|
"title": "Require Visible"
|
||||||
},
|
},
|
||||||
|
"region": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"maxItems": 4,
|
||||||
|
"minItems": 4,
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "Observed zoom surface bounds in coordinate_space; not the whole screen or date rail. Both fingers must remain inside.",
|
||||||
|
"title": "Region"
|
||||||
|
},
|
||||||
"action": {
|
"action": {
|
||||||
"const": "pinch",
|
"const": "pinch",
|
||||||
"title": "Action",
|
"title": "Action",
|
||||||
@@ -591,6 +709,25 @@
|
|||||||
],
|
],
|
||||||
"default": null,
|
"default": null,
|
||||||
"title": "Selection End"
|
"title": "Selection End"
|
||||||
|
},
|
||||||
|
"ensure_state": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/ControlCondition"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "Desired observed state: skip an already satisfied action; otherwise write ONCE and verify by read-only polling. Never infer state from an icon or missing attributes."
|
||||||
|
},
|
||||||
|
"verify_timeout_ms": {
|
||||||
|
"default": 1500,
|
||||||
|
"maximum": 3000,
|
||||||
|
"minimum": 1,
|
||||||
|
"title": "Verify Timeout Ms",
|
||||||
|
"type": "integer"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -672,6 +809,24 @@
|
|||||||
"default": null,
|
"default": null,
|
||||||
"title": "Require Visible"
|
"title": "Require Visible"
|
||||||
},
|
},
|
||||||
|
"region": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"maxItems": 4,
|
||||||
|
"minItems": 4,
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "Observed gesture surface bounds in coordinate_space; every finger path must stay inside it.",
|
||||||
|
"title": "Region"
|
||||||
|
},
|
||||||
"action": {
|
"action": {
|
||||||
"const": "touch_gesture",
|
"const": "touch_gesture",
|
||||||
"title": "Action",
|
"title": "Action",
|
||||||
@@ -686,6 +841,56 @@
|
|||||||
"title": "TouchGestureStep",
|
"title": "TouchGestureStep",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"ValueFeedback": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"selector": {
|
||||||
|
"$ref": "#/$defs/ControlSelector",
|
||||||
|
"description": "Unique, observed persistent value readout, not a transient drag tooltip. Copy its stable selector, excluding changing text."
|
||||||
|
},
|
||||||
|
"value_format": {
|
||||||
|
"description": "range reads native range.current; number requires an entire numeric text; clock requires exactly one HH:MM:SS in readable text and returns seconds since midnight.",
|
||||||
|
"enum": [
|
||||||
|
"range",
|
||||||
|
"number",
|
||||||
|
"clock"
|
||||||
|
],
|
||||||
|
"title": "Value Format",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"target_value": {
|
||||||
|
"title": "Target Value",
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"tolerance": {
|
||||||
|
"minimum": 0,
|
||||||
|
"title": "Tolerance",
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"stable_ms": {
|
||||||
|
"default": 1000,
|
||||||
|
"maximum": 5000,
|
||||||
|
"minimum": 100,
|
||||||
|
"title": "Stable Ms",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"timeout_ms": {
|
||||||
|
"default": 3000,
|
||||||
|
"maximum": 10000,
|
||||||
|
"minimum": 200,
|
||||||
|
"title": "Timeout Ms",
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"selector",
|
||||||
|
"value_format",
|
||||||
|
"target_value",
|
||||||
|
"tolerance"
|
||||||
|
],
|
||||||
|
"title": "ValueFeedback",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"WaitStep": {
|
"WaitStep": {
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -725,6 +930,7 @@
|
|||||||
"selected",
|
"selected",
|
||||||
"unselected",
|
"unselected",
|
||||||
"text",
|
"text",
|
||||||
|
"content_desc",
|
||||||
"value",
|
"value",
|
||||||
"stable"
|
"stable"
|
||||||
],
|
],
|
||||||
@@ -830,6 +1036,17 @@
|
|||||||
},
|
},
|
||||||
"title": "Steps",
|
"title": "Steps",
|
||||||
"type": "array"
|
"type": "array"
|
||||||
|
},
|
||||||
|
"feedback": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/ValueFeedback"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -931,6 +1148,100 @@
|
|||||||
"description": "Drag with exact coordinates or an observed two-anchor calibration.",
|
"description": "Drag with exact coordinates or an observed two-anchor calibration.",
|
||||||
"inputSchema": {
|
"inputSchema": {
|
||||||
"$defs": {
|
"$defs": {
|
||||||
|
"ControlSelector": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"description": "Every supplied attribute is an exact AND match against a fresh native node.",
|
||||||
|
"properties": {
|
||||||
|
"resource_id": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"maxLength": 512,
|
||||||
|
"minLength": 1,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Resource Id"
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"maxLength": 4096,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Text"
|
||||||
|
},
|
||||||
|
"content_desc": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"maxLength": 512,
|
||||||
|
"minLength": 1,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Content Desc"
|
||||||
|
},
|
||||||
|
"class_name": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"maxLength": 512,
|
||||||
|
"minLength": 1,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Class Name"
|
||||||
|
},
|
||||||
|
"package_name": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"maxLength": 512,
|
||||||
|
"minLength": 1,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Package Name"
|
||||||
|
},
|
||||||
|
"bounds": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"maxItems": 4,
|
||||||
|
"minItems": 4,
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Bounds"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "ControlSelector",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"DragCalibration": {
|
"DragCalibration": {
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -976,6 +1287,34 @@
|
|||||||
"default": null,
|
"default": null,
|
||||||
"description": "Required for pan: the fixed indicator's observed axis position.",
|
"description": "Required for pan: the fixed indicator's observed axis position.",
|
||||||
"title": "Indicator Position"
|
"title": "Indicator Position"
|
||||||
|
},
|
||||||
|
"tolerance": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "Allowed value error. Reject a plan whose pixel rounding exceeds it; this is geometric error only, not proof of application precision.",
|
||||||
|
"title": "Tolerance"
|
||||||
|
},
|
||||||
|
"min_movement_px": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"exclusiveMinimum": 0,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "Previously measured minimum effective drag distance in pixels. If this calibrated move is smaller, refuse input and zoom first. Never guess a device-wide threshold.",
|
||||||
|
"title": "Min Movement Px"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -1007,6 +1346,56 @@
|
|||||||
],
|
],
|
||||||
"title": "ScaleAnchor",
|
"title": "ScaleAnchor",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
},
|
||||||
|
"ValueFeedback": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"selector": {
|
||||||
|
"$ref": "#/$defs/ControlSelector",
|
||||||
|
"description": "Unique, observed persistent value readout, not a transient drag tooltip. Copy its stable selector, excluding changing text."
|
||||||
|
},
|
||||||
|
"value_format": {
|
||||||
|
"description": "range reads native range.current; number requires an entire numeric text; clock requires exactly one HH:MM:SS in readable text and returns seconds since midnight.",
|
||||||
|
"enum": [
|
||||||
|
"range",
|
||||||
|
"number",
|
||||||
|
"clock"
|
||||||
|
],
|
||||||
|
"title": "Value Format",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"target_value": {
|
||||||
|
"title": "Target Value",
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"tolerance": {
|
||||||
|
"minimum": 0,
|
||||||
|
"title": "Tolerance",
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"stable_ms": {
|
||||||
|
"default": 1000,
|
||||||
|
"maximum": 5000,
|
||||||
|
"minimum": 100,
|
||||||
|
"title": "Stable Ms",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"timeout_ms": {
|
||||||
|
"default": 3000,
|
||||||
|
"maximum": 10000,
|
||||||
|
"minimum": 200,
|
||||||
|
"title": "Timeout Ms",
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"selector",
|
||||||
|
"value_format",
|
||||||
|
"target_value",
|
||||||
|
"tolerance"
|
||||||
|
],
|
||||||
|
"title": "ValueFeedback",
|
||||||
|
"type": "object"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -1050,6 +1439,34 @@
|
|||||||
"title": "Duration Ms",
|
"title": "Duration Ms",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"region": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"maxItems": 4,
|
||||||
|
"minItems": 4,
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Region"
|
||||||
|
},
|
||||||
|
"feedback": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/ValueFeedback"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null
|
||||||
|
},
|
||||||
"calibration": {
|
"calibration": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -2582,6 +2999,23 @@
|
|||||||
],
|
],
|
||||||
"default": null,
|
"default": null,
|
||||||
"title": "Require Visible"
|
"title": "Require Visible"
|
||||||
|
},
|
||||||
|
"region": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"maxItems": 4,
|
||||||
|
"minItems": 4,
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Region"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -2756,6 +3190,23 @@
|
|||||||
],
|
],
|
||||||
"default": null,
|
"default": null,
|
||||||
"title": "Require Visible"
|
"title": "Require Visible"
|
||||||
|
},
|
||||||
|
"region": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"maxItems": 4,
|
||||||
|
"minItems": 4,
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Region"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -3054,9 +3505,63 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"control_action": {
|
"control_action": {
|
||||||
"description": "[ACTION] Perform ONE supported native action on a freshly resolved unique control. Read_control first; copy selector exactly and optionally expected_token to reject stale evidence. action=set_progress requires a finite in-range value; set_selection requires selection_start/end in UTF-16 code units (never split an emoji). set_text and replace_selection require text, including empty text; whitespace is preserved. Other actions take no text/value/selection. ime_enter invokes the advertised IME submit action. Unsupported actions fail without coordinate fallback or replay. Focus or selection changes may move the target; before subsequent edits, use wait_until state=stable when needed. STALE means no action started; observe again before rebuilding the request. Never resend an unknown write. An accepted action is not business verification; inspect the returned/current state.",
|
"description": "[ACTION] Perform ONE supported native action on a freshly resolved unique control. Read_control first; copy selector exactly and optionally expected_token to reject stale evidence. For a desired state use ensure_state (checked/selected/text/content_desc/value), then verify_timeout_ms: skip an already satisfied state, otherwise write ONCE and poll read-only. Copy observed state semantics; do not assume selected means paused. Bind a stable selector excluding text/content_desc that the action changes. A failed postcondition does not undo the completed input and must not cause an automatic second toggle. action=set_progress requires a finite in-range value; set_selection requires selection_start/end in UTF-16 code units (never split an emoji). set_text and replace_selection require text, including empty text; whitespace is preserved. Other actions take no text/value/selection. ime_enter invokes the advertised IME submit action. Unsupported actions fail without coordinate fallback or replay. Focus or selection changes may move the target; before subsequent edits, use wait_until state=stable when needed. STALE means no action started; observe again before rebuilding the request. Never resend an unknown write. An accepted action is not business verification; inspect the returned/current state.",
|
||||||
"inputSchema": {
|
"inputSchema": {
|
||||||
"$defs": {
|
"$defs": {
|
||||||
|
"ControlCondition": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"state": {
|
||||||
|
"enum": [
|
||||||
|
"checked",
|
||||||
|
"unchecked",
|
||||||
|
"selected",
|
||||||
|
"unselected",
|
||||||
|
"text",
|
||||||
|
"content_desc",
|
||||||
|
"value"
|
||||||
|
],
|
||||||
|
"title": "State",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"expected_text": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"maxLength": 4096,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Expected Text"
|
||||||
|
},
|
||||||
|
"expected_value": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Expected Value"
|
||||||
|
},
|
||||||
|
"tolerance": {
|
||||||
|
"default": 0,
|
||||||
|
"minimum": 0,
|
||||||
|
"title": "Tolerance",
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"state"
|
||||||
|
],
|
||||||
|
"title": "ControlCondition",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ControlSelector": {
|
"ControlSelector": {
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"description": "Every supplied attribute is an exact AND match against a fresh native node.",
|
"description": "Every supplied attribute is an exact AND match against a fresh native node.",
|
||||||
@@ -3240,6 +3745,24 @@
|
|||||||
],
|
],
|
||||||
"default": null,
|
"default": null,
|
||||||
"title": "Selection End"
|
"title": "Selection End"
|
||||||
|
},
|
||||||
|
"ensure_state": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/ControlCondition"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null
|
||||||
|
},
|
||||||
|
"verify_timeout_ms": {
|
||||||
|
"default": 1500,
|
||||||
|
"maximum": 3000,
|
||||||
|
"minimum": 1,
|
||||||
|
"title": "Verify Timeout Ms",
|
||||||
|
"type": "integer"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -3452,6 +3975,7 @@
|
|||||||
"selected",
|
"selected",
|
||||||
"unselected",
|
"unselected",
|
||||||
"text",
|
"text",
|
||||||
|
"content_desc",
|
||||||
"value",
|
"value",
|
||||||
"stable"
|
"stable"
|
||||||
],
|
],
|
||||||
|
|||||||
+602
-2
@@ -54,7 +54,7 @@
|
|||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": "action_sequence",
|
"name": "action_sequence",
|
||||||
"description": "[ACTION] General Android composer: execute ensure_visible, click, wait and drag steps in ONE call, in order, without model turns between members; explicit visibility guards may read the live UI locally. Use for temporary overlays: open the control, wait briefly, then operate it before it disappears. Prefer ensure_visible(target=observed panel control ID, opener=observed UI opener ID) followed by click/drag. This checks live UI, skips opening an already-visible panel, or taps its opener ONCE and waits up to timeout_ms (default 1500, max 3000) for the target. Each preparation counts as one member. Include it again in EVERY panel-operation call, even when the previous screenshot showed the panel open; do not split preparation and operation into different model turns. It never retries a business action. Copy all resource IDs VERBATIM, including ':id/' separators, never ':id:'. If preparation fails, re-observe/correct the ID; never guess fallback coordinates. Legacy click.unless_visible is ONLY for an opener, NEVER for an operation such as a +/- button: visible means SKIP, not require-visible. For clicks prefer target=the exact observed resource_id string: live bounds are resolved locally, refusing absent or ambiguous targets. Otherwise use observed [x,y] in the declared coordinate_space; do not confuse original screenshot pixels with 0..1000 normalized coordinates. Element indices are not accepted. Each click/drag needs target_description. All members are validated before any input. The configured fast-action limit applies. The first failure stops the remainder; receipts identify completed and unexecuted members. Never replay the whole sequence after a partial result. Every wait counts as a member. click.times (1..10) performs consecutive taps within ONE member, like the ordinary click tool; prefer this over expanding repeated +/- taps into many members/model turns. Each resource-targeted tap is revalidated locally; missing targets stop remaining taps and receipts preserve completed_taps. For maximum/minimum prefer observed native +/- controls, confirming that further same-direction adjustment stops changing handle/ticks. An estimated drag endpoint can be imprecise. For every auto-hiding adjustment, including the reverse direction, keep opener/wait/operation in one call. A prior screenshot does not prove the panel is still open. Use click.unless_visible and drag.require_visible with an observed panel resource_id to avoid toggling an open panel closed or dragging its underlying content. Never invent resource IDs. Verify actual ticks/handle endpoints before claiming maximum or minimum.",
|
"description": "[ACTION] General Android composer: execute ensure_visible, click, wait and drag steps in ONE call, in order, without model turns between members; explicit visibility guards may read the live UI locally. Use for temporary overlays: open the control, wait briefly, then operate it before it disappears. Prefer ensure_visible(target=observed panel control ID, opener=observed UI opener ID) followed by click/drag. This checks live UI, skips opening an already-visible panel, or taps its opener ONCE and waits up to timeout_ms (default 1500, max 3000) for the target. Each preparation counts as one member. Include it again in EVERY panel-operation call, even when the previous screenshot showed the panel open; do not split preparation and operation into different model turns. It never retries a business action. Copy all resource IDs VERBATIM, including ':id/' separators, never ':id:'. If preparation fails, re-observe/correct the ID; never guess fallback coordinates. Legacy click.unless_visible is ONLY for an opener, NEVER for an operation such as a +/- button: visible means SKIP, not require-visible. For clicks prefer target=the exact observed resource_id string: live bounds are resolved locally, refusing absent or ambiguous targets. Otherwise use observed [x,y] in the declared coordinate_space; do not confuse original screenshot pixels with 0..1000 normalized coordinates. Element indices are not accepted. Each click/drag needs target_description. All members are validated before any input. The configured fast-action limit applies. The first failure stops the remainder; receipts identify completed and unexecuted members. Never replay the whole sequence after a partial result. Every wait counts as a member. click.times (1..10) performs consecutive taps within ONE member, like the ordinary click tool; prefer this over expanding repeated +/- taps into many members/model turns. Each resource-targeted tap is revalidated locally; missing targets stop remaining taps and receipts preserve completed_taps. For maximum/minimum prefer observed native +/- controls, confirming that further same-direction adjustment stops changing handle/ticks. An estimated drag endpoint can be imprecise. For every auto-hiding adjustment, including the reverse direction, keep opener/wait/operation in one call. A prior screenshot does not prove the panel is still open. Use click.unless_visible and drag.require_visible with an observed panel resource_id to avoid toggling an open panel closed or dragging its underlying content. Never invent resource IDs. Verify actual ticks/handle endpoints before claiming maximum or minimum. For a measurable target, bind feedback to its persistent native readout: final feedback is read AFTER ALL members, including any pause, finish. Never use a release-time tooltip. Use control_action.request.ensure_state for observed checked/selected/text/value state, so an already satisfied state is skipped rather than toggled. A playback icon alone does not prove a native boolean state: inspect it first. Feedback blocks the third consecutive ineffective strategy; zoom or increase the meaningful movement, then re-observe.",
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"steps": {
|
"steps": {
|
||||||
@@ -193,6 +193,24 @@
|
|||||||
"title": "Duration Ms",
|
"title": "Duration Ms",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"region": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"maxItems": 4,
|
||||||
|
"minItems": 4,
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "Observed gesture surface rectangle in coordinate_space, excluding date/navigation rails and unrelated overlays.",
|
||||||
|
"title": "Region"
|
||||||
|
},
|
||||||
"calibration": {
|
"calibration": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -258,6 +276,34 @@
|
|||||||
"default": null,
|
"default": null,
|
||||||
"description": "Required for pan: the fixed indicator's observed axis position.",
|
"description": "Required for pan: the fixed indicator's observed axis position.",
|
||||||
"title": "Indicator Position"
|
"title": "Indicator Position"
|
||||||
|
},
|
||||||
|
"tolerance": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "Allowed value error. Reject a plan whose pixel rounding exceeds it; this is geometric error only, not proof of application precision.",
|
||||||
|
"title": "Tolerance"
|
||||||
|
},
|
||||||
|
"min_movement_px": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"exclusiveMinimum": 0,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "Previously measured minimum effective drag distance in pixels. If this calibrated move is smaller, refuse input and zoom first. Never guess a device-wide threshold.",
|
||||||
|
"title": "Min Movement Px"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -446,6 +492,24 @@
|
|||||||
"default": null,
|
"default": null,
|
||||||
"title": "Require Visible"
|
"title": "Require Visible"
|
||||||
},
|
},
|
||||||
|
"region": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"maxItems": 4,
|
||||||
|
"minItems": 4,
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "Observed zoom surface bounds in coordinate_space; not the whole screen or date rail. Both fingers must remain inside.",
|
||||||
|
"title": "Region"
|
||||||
|
},
|
||||||
"action": {
|
"action": {
|
||||||
"const": "pinch",
|
"const": "pinch",
|
||||||
"title": "Action",
|
"title": "Action",
|
||||||
@@ -531,6 +595,24 @@
|
|||||||
"default": null,
|
"default": null,
|
||||||
"title": "Require Visible"
|
"title": "Require Visible"
|
||||||
},
|
},
|
||||||
|
"region": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"maxItems": 4,
|
||||||
|
"minItems": 4,
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "Observed gesture surface bounds in coordinate_space; every finger path must stay inside it.",
|
||||||
|
"title": "Region"
|
||||||
|
},
|
||||||
"action": {
|
"action": {
|
||||||
"const": "touch_gesture",
|
"const": "touch_gesture",
|
||||||
"title": "Action",
|
"title": "Action",
|
||||||
@@ -734,6 +816,76 @@
|
|||||||
],
|
],
|
||||||
"default": null,
|
"default": null,
|
||||||
"title": "Selection End"
|
"title": "Selection End"
|
||||||
|
},
|
||||||
|
"ensure_state": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"state": {
|
||||||
|
"enum": [
|
||||||
|
"checked",
|
||||||
|
"unchecked",
|
||||||
|
"selected",
|
||||||
|
"unselected",
|
||||||
|
"text",
|
||||||
|
"content_desc",
|
||||||
|
"value"
|
||||||
|
],
|
||||||
|
"title": "State",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"expected_text": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"maxLength": 4096,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Expected Text"
|
||||||
|
},
|
||||||
|
"expected_value": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Expected Value"
|
||||||
|
},
|
||||||
|
"tolerance": {
|
||||||
|
"default": 0,
|
||||||
|
"minimum": 0,
|
||||||
|
"title": "Tolerance",
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"state"
|
||||||
|
],
|
||||||
|
"title": "ControlCondition",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "Desired observed state: skip an already satisfied action; otherwise write ONCE and verify by read-only polling. Never infer state from an icon or missing attributes."
|
||||||
|
},
|
||||||
|
"verify_timeout_ms": {
|
||||||
|
"default": 1500,
|
||||||
|
"maximum": 3000,
|
||||||
|
"minimum": 1,
|
||||||
|
"title": "Verify Timeout Ms",
|
||||||
|
"type": "integer"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -859,6 +1011,7 @@
|
|||||||
"selected",
|
"selected",
|
||||||
"unselected",
|
"unselected",
|
||||||
"text",
|
"text",
|
||||||
|
"content_desc",
|
||||||
"value",
|
"value",
|
||||||
"stable"
|
"stable"
|
||||||
],
|
],
|
||||||
@@ -936,6 +1089,155 @@
|
|||||||
"maxItems": 10,
|
"maxItems": 10,
|
||||||
"minItems": 1,
|
"minItems": 1,
|
||||||
"type": "array"
|
"type": "array"
|
||||||
|
},
|
||||||
|
"feedback": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"selector": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"description": "Unique, observed persistent value readout, not a transient drag tooltip. Copy its stable selector, excluding changing text.",
|
||||||
|
"properties": {
|
||||||
|
"resource_id": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"maxLength": 512,
|
||||||
|
"minLength": 1,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Resource Id"
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"maxLength": 4096,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Text"
|
||||||
|
},
|
||||||
|
"content_desc": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"maxLength": 512,
|
||||||
|
"minLength": 1,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Content Desc"
|
||||||
|
},
|
||||||
|
"class_name": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"maxLength": 512,
|
||||||
|
"minLength": 1,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Class Name"
|
||||||
|
},
|
||||||
|
"package_name": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"maxLength": 512,
|
||||||
|
"minLength": 1,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Package Name"
|
||||||
|
},
|
||||||
|
"bounds": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"maxItems": 4,
|
||||||
|
"minItems": 4,
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Bounds"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "ControlSelector",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"value_format": {
|
||||||
|
"description": "range reads native range.current; number requires an entire numeric text; clock requires exactly one HH:MM:SS in readable text and returns seconds since midnight.",
|
||||||
|
"enum": [
|
||||||
|
"range",
|
||||||
|
"number",
|
||||||
|
"clock"
|
||||||
|
],
|
||||||
|
"title": "Value Format",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"target_value": {
|
||||||
|
"title": "Target Value",
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"tolerance": {
|
||||||
|
"minimum": 0,
|
||||||
|
"title": "Tolerance",
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"stable_ms": {
|
||||||
|
"default": 1000,
|
||||||
|
"maximum": 5000,
|
||||||
|
"minimum": 100,
|
||||||
|
"title": "Stable Ms",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"timeout_ms": {
|
||||||
|
"default": 3000,
|
||||||
|
"maximum": 10000,
|
||||||
|
"minimum": 200,
|
||||||
|
"title": "Timeout Ms",
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"selector",
|
||||||
|
"value_format",
|
||||||
|
"target_value",
|
||||||
|
"tolerance"
|
||||||
|
],
|
||||||
|
"title": "ValueFeedback",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "Optional actual value/target comparison after the WHOLE sequence. clock uses seconds since midnight; exact bound and stable observation are checked locally, not inferred from dispatch."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -990,6 +1292,172 @@
|
|||||||
"default": 300,
|
"default": 300,
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"region": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"maxItems": 4,
|
||||||
|
"minItems": 4,
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "Observed gesture surface rectangle, in coordinate_space. Keep both endpoints inside."
|
||||||
|
},
|
||||||
|
"feedback": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"selector": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"description": "Unique, observed persistent value readout, not a transient drag tooltip. Copy its stable selector, excluding changing text.",
|
||||||
|
"properties": {
|
||||||
|
"resource_id": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"maxLength": 512,
|
||||||
|
"minLength": 1,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Resource Id"
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"maxLength": 4096,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Text"
|
||||||
|
},
|
||||||
|
"content_desc": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"maxLength": 512,
|
||||||
|
"minLength": 1,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Content Desc"
|
||||||
|
},
|
||||||
|
"class_name": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"maxLength": 512,
|
||||||
|
"minLength": 1,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Class Name"
|
||||||
|
},
|
||||||
|
"package_name": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"maxLength": 512,
|
||||||
|
"minLength": 1,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Package Name"
|
||||||
|
},
|
||||||
|
"bounds": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"maxItems": 4,
|
||||||
|
"minItems": 4,
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Bounds"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "ControlSelector",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"value_format": {
|
||||||
|
"description": "range reads native range.current; number requires an entire numeric text; clock requires exactly one HH:MM:SS in readable text and returns seconds since midnight.",
|
||||||
|
"enum": [
|
||||||
|
"range",
|
||||||
|
"number",
|
||||||
|
"clock"
|
||||||
|
],
|
||||||
|
"title": "Value Format",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"target_value": {
|
||||||
|
"title": "Target Value",
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"tolerance": {
|
||||||
|
"minimum": 0,
|
||||||
|
"title": "Tolerance",
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"stable_ms": {
|
||||||
|
"default": 1000,
|
||||||
|
"maximum": 5000,
|
||||||
|
"minimum": 100,
|
||||||
|
"title": "Stable Ms",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"timeout_ms": {
|
||||||
|
"default": 3000,
|
||||||
|
"maximum": 10000,
|
||||||
|
"minimum": 200,
|
||||||
|
"title": "Timeout Ms",
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"selector",
|
||||||
|
"value_format",
|
||||||
|
"target_value",
|
||||||
|
"tolerance"
|
||||||
|
],
|
||||||
|
"title": "ValueFeedback",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "Read the persistent actual value after the drag. For drag then pause, put feedback on action_sequence instead."
|
||||||
|
},
|
||||||
"calibration": {
|
"calibration": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -1055,6 +1523,34 @@
|
|||||||
"default": null,
|
"default": null,
|
||||||
"description": "Required for pan: the fixed indicator's observed axis position.",
|
"description": "Required for pan: the fixed indicator's observed axis position.",
|
||||||
"title": "Indicator Position"
|
"title": "Indicator Position"
|
||||||
|
},
|
||||||
|
"tolerance": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "Allowed value error. Reject a plan whose pixel rounding exceeds it; this is geometric error only, not proof of application precision.",
|
||||||
|
"title": "Tolerance"
|
||||||
|
},
|
||||||
|
"min_movement_px": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"exclusiveMinimum": 0,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "Previously measured minimum effective drag distance in pixels. If this calibrated move is smaller, refuse input and zoom first. Never guess a device-wide threshold.",
|
||||||
|
"title": "Min Movement Px"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -1162,6 +1658,23 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"default": null
|
"default": null
|
||||||
|
},
|
||||||
|
"region": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"maxItems": 4,
|
||||||
|
"minItems": 4,
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "Observed zoom surface bounds in coordinate_space; not the whole screen or date rail. Both fingers must remain inside."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -1239,6 +1752,23 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"default": null
|
"default": null
|
||||||
|
},
|
||||||
|
"region": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"maxItems": 4,
|
||||||
|
"minItems": 4,
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "Observed gesture surface bounds in coordinate_space; every finger path must stay inside it."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -1355,7 +1885,7 @@
|
|||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": "control_action",
|
"name": "control_action",
|
||||||
"description": "[ACTION] Perform ONE supported native action on a freshly resolved unique control. Read_control first; copy selector exactly and optionally expected_token to reject stale evidence. action=set_progress requires a finite in-range value; set_selection requires selection_start/end in UTF-16 code units (never split an emoji). set_text and replace_selection require text, including empty text; whitespace is preserved. Other actions take no text/value/selection. ime_enter invokes the advertised IME submit action. Unsupported actions fail without coordinate fallback or replay. Focus or selection changes may move the target; before subsequent edits, use wait_until state=stable when needed. STALE means no action started; observe again before rebuilding the request. Never resend an unknown write. An accepted action is not business verification; inspect the returned/current state.",
|
"description": "[ACTION] Perform ONE supported native action on a freshly resolved unique control. Read_control first; copy selector exactly and optionally expected_token to reject stale evidence. For a desired state use ensure_state (checked/selected/text/content_desc/value), then verify_timeout_ms: skip an already satisfied state, otherwise write ONCE and poll read-only. Copy observed state semantics; do not assume selected means paused. Bind a stable selector excluding text/content_desc that the action changes. A failed postcondition does not undo the completed input and must not cause an automatic second toggle. action=set_progress requires a finite in-range value; set_selection requires selection_start/end in UTF-16 code units (never split an emoji). set_text and replace_selection require text, including empty text; whitespace is preserved. Other actions take no text/value/selection. ime_enter invokes the advertised IME submit action. Unsupported actions fail without coordinate fallback or replay. Focus or selection changes may move the target; before subsequent edits, use wait_until state=stable when needed. STALE means no action started; observe again before rebuilding the request. Never resend an unknown write. An accepted action is not business verification; inspect the returned/current state.",
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"selector": {
|
"selector": {
|
||||||
@@ -1523,6 +2053,75 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"default": null
|
"default": null
|
||||||
|
},
|
||||||
|
"ensure_state": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"state": {
|
||||||
|
"enum": [
|
||||||
|
"checked",
|
||||||
|
"unchecked",
|
||||||
|
"selected",
|
||||||
|
"unselected",
|
||||||
|
"text",
|
||||||
|
"content_desc",
|
||||||
|
"value"
|
||||||
|
],
|
||||||
|
"title": "State",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"expected_text": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"maxLength": 4096,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Expected Text"
|
||||||
|
},
|
||||||
|
"expected_value": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Expected Value"
|
||||||
|
},
|
||||||
|
"tolerance": {
|
||||||
|
"default": 0,
|
||||||
|
"minimum": 0,
|
||||||
|
"title": "Tolerance",
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"state"
|
||||||
|
],
|
||||||
|
"title": "ControlCondition",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "Desired observed state: skip an already satisfied action; otherwise write ONCE and verify by read-only polling. Never infer state from an icon or missing attributes."
|
||||||
|
},
|
||||||
|
"verify_timeout_ms": {
|
||||||
|
"default": 1500,
|
||||||
|
"maximum": 3000,
|
||||||
|
"minimum": 1,
|
||||||
|
"type": "integer"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -1638,6 +2237,7 @@
|
|||||||
"selected",
|
"selected",
|
||||||
"unselected",
|
"unselected",
|
||||||
"text",
|
"text",
|
||||||
|
"content_desc",
|
||||||
"value",
|
"value",
|
||||||
"stable"
|
"stable"
|
||||||
],
|
],
|
||||||
|
|||||||
+602
-2
@@ -284,6 +284,172 @@
|
|||||||
"default": 300,
|
"default": 300,
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"region": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"maxItems": 4,
|
||||||
|
"minItems": 4,
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "Observed gesture surface rectangle, in coordinate_space. Keep both endpoints inside."
|
||||||
|
},
|
||||||
|
"feedback": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"selector": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"description": "Unique, observed persistent value readout, not a transient drag tooltip. Copy its stable selector, excluding changing text.",
|
||||||
|
"properties": {
|
||||||
|
"resource_id": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"maxLength": 512,
|
||||||
|
"minLength": 1,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Resource Id"
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"maxLength": 4096,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Text"
|
||||||
|
},
|
||||||
|
"content_desc": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"maxLength": 512,
|
||||||
|
"minLength": 1,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Content Desc"
|
||||||
|
},
|
||||||
|
"class_name": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"maxLength": 512,
|
||||||
|
"minLength": 1,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Class Name"
|
||||||
|
},
|
||||||
|
"package_name": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"maxLength": 512,
|
||||||
|
"minLength": 1,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Package Name"
|
||||||
|
},
|
||||||
|
"bounds": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"maxItems": 4,
|
||||||
|
"minItems": 4,
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Bounds"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "ControlSelector",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"value_format": {
|
||||||
|
"description": "range reads native range.current; number requires an entire numeric text; clock requires exactly one HH:MM:SS in readable text and returns seconds since midnight.",
|
||||||
|
"enum": [
|
||||||
|
"range",
|
||||||
|
"number",
|
||||||
|
"clock"
|
||||||
|
],
|
||||||
|
"title": "Value Format",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"target_value": {
|
||||||
|
"title": "Target Value",
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"tolerance": {
|
||||||
|
"minimum": 0,
|
||||||
|
"title": "Tolerance",
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"stable_ms": {
|
||||||
|
"default": 1000,
|
||||||
|
"maximum": 5000,
|
||||||
|
"minimum": 100,
|
||||||
|
"title": "Stable Ms",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"timeout_ms": {
|
||||||
|
"default": 3000,
|
||||||
|
"maximum": 10000,
|
||||||
|
"minimum": 200,
|
||||||
|
"title": "Timeout Ms",
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"selector",
|
||||||
|
"value_format",
|
||||||
|
"target_value",
|
||||||
|
"tolerance"
|
||||||
|
],
|
||||||
|
"title": "ValueFeedback",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "Read the persistent actual value after the drag. For drag then pause, put feedback on action_sequence instead."
|
||||||
|
},
|
||||||
"calibration": {
|
"calibration": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -349,6 +515,34 @@
|
|||||||
"default": null,
|
"default": null,
|
||||||
"description": "Required for pan: the fixed indicator's observed axis position.",
|
"description": "Required for pan: the fixed indicator's observed axis position.",
|
||||||
"title": "Indicator Position"
|
"title": "Indicator Position"
|
||||||
|
},
|
||||||
|
"tolerance": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "Allowed value error. Reject a plan whose pixel rounding exceeds it; this is geometric error only, not proof of application precision.",
|
||||||
|
"title": "Tolerance"
|
||||||
|
},
|
||||||
|
"min_movement_px": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"exclusiveMinimum": 0,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "Previously measured minimum effective drag distance in pixels. If this calibrated move is smaller, refuse input and zoom first. Never guess a device-wide threshold.",
|
||||||
|
"title": "Min Movement Px"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -392,7 +586,7 @@
|
|||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": "action_sequence",
|
"name": "action_sequence",
|
||||||
"description": "[ACTION] General Android composer: execute ensure_visible, click, wait and drag steps in ONE call, in order, without model turns between members; explicit visibility guards may read the live UI locally. Use for temporary overlays: open the control, wait briefly, then operate it before it disappears. Prefer ensure_visible(target=observed panel control ID, opener=observed UI opener ID) followed by click/drag. This checks live UI, skips opening an already-visible panel, or taps its opener ONCE and waits up to timeout_ms (default 1500, max 3000) for the target. Each preparation counts as one member. Include it again in EVERY panel-operation call, even when the previous screenshot showed the panel open; do not split preparation and operation into different model turns. It never retries a business action. Copy all resource IDs VERBATIM, including ':id/' separators, never ':id:'. If preparation fails, re-observe/correct the ID; never guess fallback coordinates. Legacy click.unless_visible is ONLY for an opener, NEVER for an operation such as a +/- button: visible means SKIP, not require-visible. For clicks prefer target=the exact observed resource_id string: live bounds are resolved locally, refusing absent or ambiguous targets. Otherwise use observed [x,y] in the declared coordinate_space; do not confuse original screenshot pixels with 0..1000 normalized coordinates. Element indices are not accepted. Each click/drag needs target_description. All members are validated before any input. The configured fast-action limit applies. The first failure stops the remainder; receipts identify completed and unexecuted members. Never replay the whole sequence after a partial result. Every wait counts as a member. click.times (1..10) performs consecutive taps within ONE member, like the ordinary click tool; prefer this over expanding repeated +/- taps into many members/model turns. Each resource-targeted tap is revalidated locally; missing targets stop remaining taps and receipts preserve completed_taps. For maximum/minimum prefer observed native +/- controls, confirming that further same-direction adjustment stops changing handle/ticks. An estimated drag endpoint can be imprecise. For every auto-hiding adjustment, including the reverse direction, keep opener/wait/operation in one call. A prior screenshot does not prove the panel is still open. Use click.unless_visible and drag.require_visible with an observed panel resource_id to avoid toggling an open panel closed or dragging its underlying content. Never invent resource IDs. Verify actual ticks/handle endpoints before claiming maximum or minimum.",
|
"description": "[ACTION] General Android composer: execute ensure_visible, click, wait and drag steps in ONE call, in order, without model turns between members; explicit visibility guards may read the live UI locally. Use for temporary overlays: open the control, wait briefly, then operate it before it disappears. Prefer ensure_visible(target=observed panel control ID, opener=observed UI opener ID) followed by click/drag. This checks live UI, skips opening an already-visible panel, or taps its opener ONCE and waits up to timeout_ms (default 1500, max 3000) for the target. Each preparation counts as one member. Include it again in EVERY panel-operation call, even when the previous screenshot showed the panel open; do not split preparation and operation into different model turns. It never retries a business action. Copy all resource IDs VERBATIM, including ':id/' separators, never ':id:'. If preparation fails, re-observe/correct the ID; never guess fallback coordinates. Legacy click.unless_visible is ONLY for an opener, NEVER for an operation such as a +/- button: visible means SKIP, not require-visible. For clicks prefer target=the exact observed resource_id string: live bounds are resolved locally, refusing absent or ambiguous targets. Otherwise use observed [x,y] in the declared coordinate_space; do not confuse original screenshot pixels with 0..1000 normalized coordinates. Element indices are not accepted. Each click/drag needs target_description. All members are validated before any input. The configured fast-action limit applies. The first failure stops the remainder; receipts identify completed and unexecuted members. Never replay the whole sequence after a partial result. Every wait counts as a member. click.times (1..10) performs consecutive taps within ONE member, like the ordinary click tool; prefer this over expanding repeated +/- taps into many members/model turns. Each resource-targeted tap is revalidated locally; missing targets stop remaining taps and receipts preserve completed_taps. For maximum/minimum prefer observed native +/- controls, confirming that further same-direction adjustment stops changing handle/ticks. An estimated drag endpoint can be imprecise. For every auto-hiding adjustment, including the reverse direction, keep opener/wait/operation in one call. A prior screenshot does not prove the panel is still open. Use click.unless_visible and drag.require_visible with an observed panel resource_id to avoid toggling an open panel closed or dragging its underlying content. Never invent resource IDs. Verify actual ticks/handle endpoints before claiming maximum or minimum. For a measurable target, bind feedback to its persistent native readout: final feedback is read AFTER ALL members, including any pause, finish. Never use a release-time tooltip. Use control_action.request.ensure_state for observed checked/selected/text/value state, so an already satisfied state is skipped rather than toggled. A playback icon alone does not prove a native boolean state: inspect it first. Feedback blocks the third consecutive ineffective strategy; zoom or increase the meaningful movement, then re-observe.",
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"steps": {
|
"steps": {
|
||||||
@@ -531,6 +725,24 @@
|
|||||||
"title": "Duration Ms",
|
"title": "Duration Ms",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"region": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"maxItems": 4,
|
||||||
|
"minItems": 4,
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "Observed gesture surface rectangle in coordinate_space, excluding date/navigation rails and unrelated overlays.",
|
||||||
|
"title": "Region"
|
||||||
|
},
|
||||||
"calibration": {
|
"calibration": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -596,6 +808,34 @@
|
|||||||
"default": null,
|
"default": null,
|
||||||
"description": "Required for pan: the fixed indicator's observed axis position.",
|
"description": "Required for pan: the fixed indicator's observed axis position.",
|
||||||
"title": "Indicator Position"
|
"title": "Indicator Position"
|
||||||
|
},
|
||||||
|
"tolerance": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "Allowed value error. Reject a plan whose pixel rounding exceeds it; this is geometric error only, not proof of application precision.",
|
||||||
|
"title": "Tolerance"
|
||||||
|
},
|
||||||
|
"min_movement_px": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"exclusiveMinimum": 0,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "Previously measured minimum effective drag distance in pixels. If this calibrated move is smaller, refuse input and zoom first. Never guess a device-wide threshold.",
|
||||||
|
"title": "Min Movement Px"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -784,6 +1024,24 @@
|
|||||||
"default": null,
|
"default": null,
|
||||||
"title": "Require Visible"
|
"title": "Require Visible"
|
||||||
},
|
},
|
||||||
|
"region": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"maxItems": 4,
|
||||||
|
"minItems": 4,
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "Observed zoom surface bounds in coordinate_space; not the whole screen or date rail. Both fingers must remain inside.",
|
||||||
|
"title": "Region"
|
||||||
|
},
|
||||||
"action": {
|
"action": {
|
||||||
"const": "pinch",
|
"const": "pinch",
|
||||||
"title": "Action",
|
"title": "Action",
|
||||||
@@ -869,6 +1127,24 @@
|
|||||||
"default": null,
|
"default": null,
|
||||||
"title": "Require Visible"
|
"title": "Require Visible"
|
||||||
},
|
},
|
||||||
|
"region": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"maxItems": 4,
|
||||||
|
"minItems": 4,
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "Observed gesture surface bounds in coordinate_space; every finger path must stay inside it.",
|
||||||
|
"title": "Region"
|
||||||
|
},
|
||||||
"action": {
|
"action": {
|
||||||
"const": "touch_gesture",
|
"const": "touch_gesture",
|
||||||
"title": "Action",
|
"title": "Action",
|
||||||
@@ -1072,6 +1348,76 @@
|
|||||||
],
|
],
|
||||||
"default": null,
|
"default": null,
|
||||||
"title": "Selection End"
|
"title": "Selection End"
|
||||||
|
},
|
||||||
|
"ensure_state": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"state": {
|
||||||
|
"enum": [
|
||||||
|
"checked",
|
||||||
|
"unchecked",
|
||||||
|
"selected",
|
||||||
|
"unselected",
|
||||||
|
"text",
|
||||||
|
"content_desc",
|
||||||
|
"value"
|
||||||
|
],
|
||||||
|
"title": "State",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"expected_text": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"maxLength": 4096,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Expected Text"
|
||||||
|
},
|
||||||
|
"expected_value": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Expected Value"
|
||||||
|
},
|
||||||
|
"tolerance": {
|
||||||
|
"default": 0,
|
||||||
|
"minimum": 0,
|
||||||
|
"title": "Tolerance",
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"state"
|
||||||
|
],
|
||||||
|
"title": "ControlCondition",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "Desired observed state: skip an already satisfied action; otherwise write ONCE and verify by read-only polling. Never infer state from an icon or missing attributes."
|
||||||
|
},
|
||||||
|
"verify_timeout_ms": {
|
||||||
|
"default": 1500,
|
||||||
|
"maximum": 3000,
|
||||||
|
"minimum": 1,
|
||||||
|
"title": "Verify Timeout Ms",
|
||||||
|
"type": "integer"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -1197,6 +1543,7 @@
|
|||||||
"selected",
|
"selected",
|
||||||
"unselected",
|
"unselected",
|
||||||
"text",
|
"text",
|
||||||
|
"content_desc",
|
||||||
"value",
|
"value",
|
||||||
"stable"
|
"stable"
|
||||||
],
|
],
|
||||||
@@ -1274,6 +1621,155 @@
|
|||||||
"maxItems": 10,
|
"maxItems": 10,
|
||||||
"minItems": 1,
|
"minItems": 1,
|
||||||
"type": "array"
|
"type": "array"
|
||||||
|
},
|
||||||
|
"feedback": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"selector": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"description": "Unique, observed persistent value readout, not a transient drag tooltip. Copy its stable selector, excluding changing text.",
|
||||||
|
"properties": {
|
||||||
|
"resource_id": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"maxLength": 512,
|
||||||
|
"minLength": 1,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Resource Id"
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"maxLength": 4096,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Text"
|
||||||
|
},
|
||||||
|
"content_desc": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"maxLength": 512,
|
||||||
|
"minLength": 1,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Content Desc"
|
||||||
|
},
|
||||||
|
"class_name": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"maxLength": 512,
|
||||||
|
"minLength": 1,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Class Name"
|
||||||
|
},
|
||||||
|
"package_name": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"maxLength": 512,
|
||||||
|
"minLength": 1,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Package Name"
|
||||||
|
},
|
||||||
|
"bounds": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"maxItems": 4,
|
||||||
|
"minItems": 4,
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Bounds"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "ControlSelector",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"value_format": {
|
||||||
|
"description": "range reads native range.current; number requires an entire numeric text; clock requires exactly one HH:MM:SS in readable text and returns seconds since midnight.",
|
||||||
|
"enum": [
|
||||||
|
"range",
|
||||||
|
"number",
|
||||||
|
"clock"
|
||||||
|
],
|
||||||
|
"title": "Value Format",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"target_value": {
|
||||||
|
"title": "Target Value",
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"tolerance": {
|
||||||
|
"minimum": 0,
|
||||||
|
"title": "Tolerance",
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"stable_ms": {
|
||||||
|
"default": 1000,
|
||||||
|
"maximum": 5000,
|
||||||
|
"minimum": 100,
|
||||||
|
"title": "Stable Ms",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"timeout_ms": {
|
||||||
|
"default": 3000,
|
||||||
|
"maximum": 10000,
|
||||||
|
"minimum": 200,
|
||||||
|
"title": "Timeout Ms",
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"selector",
|
||||||
|
"value_format",
|
||||||
|
"target_value",
|
||||||
|
"tolerance"
|
||||||
|
],
|
||||||
|
"title": "ValueFeedback",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "Optional actual value/target comparison after the WHOLE sequence. clock uses seconds since midnight; exact bound and stable observation are checked locally, not inferred from dispatch."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -1423,6 +1919,23 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"default": null
|
"default": null
|
||||||
|
},
|
||||||
|
"region": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"maxItems": 4,
|
||||||
|
"minItems": 4,
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "Observed zoom surface bounds in coordinate_space; not the whole screen or date rail. Both fingers must remain inside."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -1500,6 +2013,23 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"default": null
|
"default": null
|
||||||
|
},
|
||||||
|
"region": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"maxItems": 4,
|
||||||
|
"minItems": 4,
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "Observed gesture surface bounds in coordinate_space; every finger path must stay inside it."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -1616,7 +2146,7 @@
|
|||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": "control_action",
|
"name": "control_action",
|
||||||
"description": "[ACTION] Perform ONE supported native action on a freshly resolved unique control. Read_control first; copy selector exactly and optionally expected_token to reject stale evidence. action=set_progress requires a finite in-range value; set_selection requires selection_start/end in UTF-16 code units (never split an emoji). set_text and replace_selection require text, including empty text; whitespace is preserved. Other actions take no text/value/selection. ime_enter invokes the advertised IME submit action. Unsupported actions fail without coordinate fallback or replay. Focus or selection changes may move the target; before subsequent edits, use wait_until state=stable when needed. STALE means no action started; observe again before rebuilding the request. Never resend an unknown write. An accepted action is not business verification; inspect the returned/current state.",
|
"description": "[ACTION] Perform ONE supported native action on a freshly resolved unique control. Read_control first; copy selector exactly and optionally expected_token to reject stale evidence. For a desired state use ensure_state (checked/selected/text/content_desc/value), then verify_timeout_ms: skip an already satisfied state, otherwise write ONCE and poll read-only. Copy observed state semantics; do not assume selected means paused. Bind a stable selector excluding text/content_desc that the action changes. A failed postcondition does not undo the completed input and must not cause an automatic second toggle. action=set_progress requires a finite in-range value; set_selection requires selection_start/end in UTF-16 code units (never split an emoji). set_text and replace_selection require text, including empty text; whitespace is preserved. Other actions take no text/value/selection. ime_enter invokes the advertised IME submit action. Unsupported actions fail without coordinate fallback or replay. Focus or selection changes may move the target; before subsequent edits, use wait_until state=stable when needed. STALE means no action started; observe again before rebuilding the request. Never resend an unknown write. An accepted action is not business verification; inspect the returned/current state.",
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"selector": {
|
"selector": {
|
||||||
@@ -1784,6 +2314,75 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"default": null
|
"default": null
|
||||||
|
},
|
||||||
|
"ensure_state": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"state": {
|
||||||
|
"enum": [
|
||||||
|
"checked",
|
||||||
|
"unchecked",
|
||||||
|
"selected",
|
||||||
|
"unselected",
|
||||||
|
"text",
|
||||||
|
"content_desc",
|
||||||
|
"value"
|
||||||
|
],
|
||||||
|
"title": "State",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"expected_text": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"maxLength": 4096,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Expected Text"
|
||||||
|
},
|
||||||
|
"expected_value": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Expected Value"
|
||||||
|
},
|
||||||
|
"tolerance": {
|
||||||
|
"default": 0,
|
||||||
|
"minimum": 0,
|
||||||
|
"title": "Tolerance",
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"state"
|
||||||
|
],
|
||||||
|
"title": "ControlCondition",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"description": "Desired observed state: skip an already satisfied action; otherwise write ONCE and verify by read-only polling. Never infer state from an icon or missing attributes."
|
||||||
|
},
|
||||||
|
"verify_timeout_ms": {
|
||||||
|
"default": 1500,
|
||||||
|
"maximum": 3000,
|
||||||
|
"minimum": 1,
|
||||||
|
"type": "integer"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -1899,6 +2498,7 @@
|
|||||||
"selected",
|
"selected",
|
||||||
"unselected",
|
"unselected",
|
||||||
"text",
|
"text",
|
||||||
|
"content_desc",
|
||||||
"value",
|
"value",
|
||||||
"stable"
|
"stable"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -71,6 +71,81 @@ async def run(name, args, native):
|
|||||||
return await execute_native(name, args, 400, 800, native)
|
return await execute_native(name, args, 400, 800, native)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"condition,fields",
|
||||||
|
[
|
||||||
|
({"state": "unchecked"}, {"checked": False}),
|
||||||
|
({"state": "text", "expected_text": " exact "}, {"text": " exact "}),
|
||||||
|
({"state": "content_desc", "expected_text": "播放"}, {"content_desc": "播放"}),
|
||||||
|
({"state": "value", "expected_value": 3.0}, {}),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_ensure_state_already_satisfied_never_toggles(condition, fields):
|
||||||
|
native = AsyncMock(return_value=observed(**fields))
|
||||||
|
result = await run(
|
||||||
|
"control_action",
|
||||||
|
{"selector": SELECTOR, "action": "click", "ensure_state": condition},
|
||||||
|
native,
|
||||||
|
)
|
||||||
|
assert result["success"] and result["skipped"]
|
||||||
|
assert result["execution"] == "observed"
|
||||||
|
assert [call.args[0] for call in native.await_args_list] == ["inspect_control"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_ensure_state_missing_native_boolean_refuses_input():
|
||||||
|
native = AsyncMock(return_value=observed(checked=None))
|
||||||
|
result = await run(
|
||||||
|
"control_action",
|
||||||
|
{"selector": SELECTOR, "action": "click", "ensure_state": {"state": "unchecked"}},
|
||||||
|
native,
|
||||||
|
)
|
||||||
|
assert not result["success"] and result["execution"] == "not_started"
|
||||||
|
native.assert_awaited_once()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_ensure_state_writes_once_then_reads_actual_state():
|
||||||
|
native = AsyncMock(side_effect=[observed(checked=True), completed(), observed(checked=False)])
|
||||||
|
result = await run(
|
||||||
|
"control_action",
|
||||||
|
{"selector": SELECTOR, "action": "click", "ensure_state": {"state": "unchecked"}},
|
||||||
|
native,
|
||||||
|
)
|
||||||
|
assert result["success"] and result["verification"] == "condition_satisfied"
|
||||||
|
assert [call.args[0] for call in native.await_args_list] == [
|
||||||
|
"inspect_control",
|
||||||
|
"control_action",
|
||||||
|
"inspect_control",
|
||||||
|
]
|
||||||
|
args = native.await_args_list[1].args[1]
|
||||||
|
assert "ensure_state" not in args and "verify_timeout_ms" not in args
|
||||||
|
assert args["expected_token"] == TOKEN
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_failed_postcondition_preserves_completed_input_without_retoggling(clock):
|
||||||
|
async def native(command, args, **kwargs):
|
||||||
|
return completed() if command == "control_action" else observed(checked=True)
|
||||||
|
|
||||||
|
native = AsyncMock(side_effect=native)
|
||||||
|
result = await run(
|
||||||
|
"control_action",
|
||||||
|
{
|
||||||
|
"selector": SELECTOR,
|
||||||
|
"action": "click",
|
||||||
|
"ensure_state": {"state": "unchecked"},
|
||||||
|
"verify_timeout_ms": 400,
|
||||||
|
},
|
||||||
|
native,
|
||||||
|
)
|
||||||
|
assert not result["success"] and result["code"] == "TIMEOUT"
|
||||||
|
assert result["execution"] == "completed"
|
||||||
|
assert result["native_receipt"]["success"]
|
||||||
|
assert sum(c.args[0] == "control_action" for c in native.await_args_list) == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.parametrize("name", ["pinch", "touch_gesture"])
|
@pytest.mark.parametrize("name", ["pinch", "touch_gesture"])
|
||||||
async def test_all_gesture_geometry_is_validated_before_a_single_native_dispatch(name):
|
async def test_all_gesture_geometry_is_validated_before_a_single_native_dispatch(name):
|
||||||
|
|||||||
@@ -0,0 +1,346 @@
|
|||||||
|
"""Regress real actuator dispatch, state verification and value convergence without devices."""
|
||||||
|
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from artemis.mcp import value_feedback
|
||||||
|
from artemis.mcp.action_manifest import DEVICE_ACTIONS
|
||||||
|
from artemis.mcp.actuators import MockActuator
|
||||||
|
from artemis.mcp.gestures import DragRequest, plan_drag
|
||||||
|
from artemis.mcp.native_actions import PinchRequest, plan_pinch
|
||||||
|
from artemis.mcp.value_feedback import read_value
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def deterministic_clock(monkeypatch):
|
||||||
|
now = [0.0]
|
||||||
|
|
||||||
|
async def sleep(seconds):
|
||||||
|
now[0] += seconds
|
||||||
|
|
||||||
|
monkeypatch.setattr(value_feedback, "monotonic", lambda: now[0])
|
||||||
|
monkeypatch.setattr(value_feedback, "sleep", sleep)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"artemis.config.agent.load_agent_config",
|
||||||
|
lambda: SimpleNamespace(
|
||||||
|
pro=SimpleNamespace(execution=SimpleNamespace(max_burst_actions=10))
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_actuator(*, current="100", after="100", readable=True):
|
||||||
|
actuator = MockActuator(width=720, height=1600, capabilities=DEVICE_ACTIONS)
|
||||||
|
state = {"text": current, "playing": False}
|
||||||
|
events = []
|
||||||
|
|
||||||
|
async def native(command, args, **kwargs):
|
||||||
|
events.append(command)
|
||||||
|
if command == "control_action":
|
||||||
|
state["playing"] = not state["playing"]
|
||||||
|
return {"success": True, "execution": "completed", "code": "OK"}
|
||||||
|
is_player = args["selector"]["resource_id"] == "player"
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"execution": "observed",
|
||||||
|
"code": "OK",
|
||||||
|
"node": {
|
||||||
|
"token": "a" * 64,
|
||||||
|
"content_signature": "b" * 64,
|
||||||
|
"visible": True,
|
||||||
|
"enabled": True,
|
||||||
|
"password": False,
|
||||||
|
"actions": ["click"],
|
||||||
|
"text": state["text"] if readable else None,
|
||||||
|
"content_desc": ("暂停" if state["playing"] else "播放")
|
||||||
|
if is_player
|
||||||
|
else "readout",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
async def swipe(*args):
|
||||||
|
events.append("swipe")
|
||||||
|
state.update(text=after, playing=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
actuator.controller.native_request = AsyncMock(side_effect=native)
|
||||||
|
actuator.controller.swipe_coords = AsyncMock(side_effect=swipe)
|
||||||
|
actuator.controller.tap_at = AsyncMock(return_value=SimpleNamespace(error=None))
|
||||||
|
return actuator, state, events
|
||||||
|
|
||||||
|
|
||||||
|
def feedback(**overrides):
|
||||||
|
return {
|
||||||
|
"selector": {"resource_id": "readout"},
|
||||||
|
"value_format": "number",
|
||||||
|
"target_value": 90.0,
|
||||||
|
"tolerance": 1.0,
|
||||||
|
"stable_ms": 100,
|
||||||
|
"timeout_ms": 500,
|
||||||
|
**overrides,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def drag(**overrides):
|
||||||
|
return {
|
||||||
|
"start": [300, 1050],
|
||||||
|
"end": [325, 1050],
|
||||||
|
"coordinate_space": "pixel",
|
||||||
|
"target_description": "observed timeline",
|
||||||
|
**overrides,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_two_ineffective_attempts_block_third_but_allow_meaningful_strategy_change():
|
||||||
|
actuator, _, _ = setup_actuator()
|
||||||
|
for _ in range(2):
|
||||||
|
result = await actuator.drag(**drag(feedback=feedback()))
|
||||||
|
assert not result.ok and result.metadata["execution"] == "completed"
|
||||||
|
assert result.metadata["value_feedback"]["after_value"] == 100
|
||||||
|
result = await actuator.drag(**drag(feedback=feedback()))
|
||||||
|
assert not result.ok and "NO_PROGRESS" in result.message
|
||||||
|
assert result.metadata["execution"] == "not_started"
|
||||||
|
assert actuator.controller.swipe_coords.await_count == 2
|
||||||
|
# Tiny endpoint jitter is the same strategy, not an escape from the guard.
|
||||||
|
await actuator.drag(**drag(end=[328, 1050], feedback=feedback()))
|
||||||
|
assert actuator.controller.swipe_coords.await_count == 2
|
||||||
|
await actuator.drag(**drag(end=[424, 1050], feedback=feedback()))
|
||||||
|
assert actuator.controller.swipe_coords.await_count == 3
|
||||||
|
# History belongs to this actuator/task, not a global cache.
|
||||||
|
other, _, _ = setup_actuator()
|
||||||
|
await other.drag(**drag(feedback=feedback()))
|
||||||
|
assert other.controller.swipe_coords.await_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_changed_actual_value_resets_stalled_history():
|
||||||
|
actuator, state, _ = setup_actuator()
|
||||||
|
for _ in range(2):
|
||||||
|
await actuator.drag(**drag(feedback=feedback()))
|
||||||
|
state["text"] = "95"
|
||||||
|
await actuator.drag(**drag(feedback=feedback()))
|
||||||
|
assert actuator.controller.swipe_coords.await_count == 3
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_one_pixel_change_across_power_of_two_is_still_the_same_strategy():
|
||||||
|
actuator, _, _ = setup_actuator()
|
||||||
|
for endpoint in (331, 332, 333):
|
||||||
|
await actuator.drag(**drag(end=[endpoint, 1050], feedback=feedback()))
|
||||||
|
assert actuator.controller.swipe_coords.await_count == 2
|
||||||
|
# A substantially slower gesture is a meaningful new approach.
|
||||||
|
await actuator.drag(**drag(end=[333, 1050], duration_ms=1500, feedback=feedback()))
|
||||||
|
assert actuator.controller.swipe_coords.await_count == 3
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_sequence_reads_final_value_only_after_pause_and_skips_redundant_toggle():
|
||||||
|
actuator, state, events = setup_actuator(
|
||||||
|
current="11-02 周一 17:43:12", after="11-02 周一 17:42:17"
|
||||||
|
)
|
||||||
|
pause = {
|
||||||
|
"action": "control_action",
|
||||||
|
"request": {
|
||||||
|
"action": "click",
|
||||||
|
"selector": {"resource_id": "player"},
|
||||||
|
"ensure_state": {"state": "content_desc", "expected_text": "播放"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
result = await actuator.action_sequence(
|
||||||
|
[pause, {"action": "drag", **drag()}, pause],
|
||||||
|
feedback=feedback(
|
||||||
|
value_format="clock", target_value=17.0 * 3600 + 42 * 60 + 17, tolerance=5.0
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assert result.ok and not state["playing"]
|
||||||
|
assert result.metadata["skipped_steps"] == 1
|
||||||
|
assert result.metadata["value_feedback"]["residual"] == 0
|
||||||
|
observation = result.metadata["value_feedback"]["after_observation"]
|
||||||
|
assert observation["node"]["text"] == "11-02 周一 17:42:17"
|
||||||
|
assert observation["stable_for_ms"] >= 100 and observation["samples"] >= 2
|
||||||
|
assert result.metadata["verification"] == "value_matched"
|
||||||
|
assert events.count("control_action") == 1
|
||||||
|
assert events.index("swipe") < events.index("control_action")
|
||||||
|
assert events[-1] == "inspect_control"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_missing_value_refuses_input_and_postwrite_read_failure_preserves_receipt():
|
||||||
|
actuator, _, _ = setup_actuator(readable=False)
|
||||||
|
result = await actuator.drag(**drag(feedback=feedback()))
|
||||||
|
assert not result.ok and result.metadata["execution"] == "not_started"
|
||||||
|
actuator.controller.swipe_coords.assert_not_awaited()
|
||||||
|
actuator, _, _ = setup_actuator(after="unreadable")
|
||||||
|
result = await actuator.drag(**drag(feedback=feedback()))
|
||||||
|
assert not result.ok and result.metadata["execution"] == "completed"
|
||||||
|
assert result.metadata["coordinates"] == [300, 1050, 325, 1050]
|
||||||
|
assert result.metadata["verification"] == "unavailable"
|
||||||
|
actuator.controller.swipe_coords.assert_awaited_once()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_native_feedback_unavailable_has_no_coordinate_fallback():
|
||||||
|
actuator = MockActuator(capabilities=DEVICE_ACTIONS - {"read_control", "wait_until"})
|
||||||
|
actuator.controller.swipe_coords = AsyncMock()
|
||||||
|
result = await actuator.drag(**drag(feedback=feedback()))
|
||||||
|
assert not result.ok and result.code == "UNSUPPORTED"
|
||||||
|
actuator.controller.swipe_coords.assert_not_awaited()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_whole_sequence_rejects_bad_region_before_any_click():
|
||||||
|
actuator, _, _ = setup_actuator()
|
||||||
|
result = await actuator.action_sequence(
|
||||||
|
[
|
||||||
|
{"action": "click", "target": [50, 50], "target_description": "opener"},
|
||||||
|
{"action": "drag", **drag(region=[400, 970, 650, 1118])},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
assert not result.ok
|
||||||
|
actuator.controller.tap_at.assert_not_awaited()
|
||||||
|
actuator.controller.swipe_coords.assert_not_awaited()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_failed_state_verification_keeps_completed_members_and_stops_next_input():
|
||||||
|
actuator, _, _ = setup_actuator()
|
||||||
|
|
||||||
|
async def unchanged(command, args, **kwargs):
|
||||||
|
if command == "control_action":
|
||||||
|
return {"success": True, "execution": "completed", "code": "OK"}
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"execution": "observed",
|
||||||
|
"code": "OK",
|
||||||
|
"node": {
|
||||||
|
"token": "a" * 64,
|
||||||
|
"visible": True,
|
||||||
|
"enabled": True,
|
||||||
|
"text": "100",
|
||||||
|
"content_desc": "暂停",
|
||||||
|
"actions": ["click"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
actuator.controller.native_request = AsyncMock(side_effect=unchanged)
|
||||||
|
result = await actuator.action_sequence(
|
||||||
|
[
|
||||||
|
{"action": "drag", **drag()},
|
||||||
|
{
|
||||||
|
"action": "control_action",
|
||||||
|
"request": {
|
||||||
|
"action": "click",
|
||||||
|
"selector": {"resource_id": "player"},
|
||||||
|
"ensure_state": {"state": "content_desc", "expected_text": "播放"},
|
||||||
|
"verify_timeout_ms": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{"action": "click", "target": [50, 50], "target_description": "must not execute"},
|
||||||
|
],
|
||||||
|
feedback=feedback(),
|
||||||
|
)
|
||||||
|
assert not result.ok and result.metadata["execution"] == "partial"
|
||||||
|
assert [r["execution"] for r in result.metadata["sequence_results"]] == [
|
||||||
|
"completed",
|
||||||
|
"completed",
|
||||||
|
"not_started",
|
||||||
|
]
|
||||||
|
assert result.metadata["sequence_results"][1]["verification"] == "condition_not_met"
|
||||||
|
actuator.controller.tap_at.assert_not_awaited()
|
||||||
|
actuator.controller.swipe_coords.assert_awaited_once()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_transient_post_action_read_failure_can_settle_without_replaying_input():
|
||||||
|
actuator, _, events = setup_actuator(after="90")
|
||||||
|
original = actuator.controller.native_request.side_effect
|
||||||
|
failed = False
|
||||||
|
|
||||||
|
async def transient(command, args, **kwargs):
|
||||||
|
nonlocal failed
|
||||||
|
if "swipe" in events and not failed:
|
||||||
|
failed = True
|
||||||
|
return {"success": False, "execution": "not_started", "code": "READ_FAILED"}
|
||||||
|
return await original(command, args, **kwargs)
|
||||||
|
|
||||||
|
actuator.controller.native_request = AsyncMock(side_effect=transient)
|
||||||
|
result = await actuator.drag(**drag(feedback=feedback()))
|
||||||
|
assert result.ok and result.metadata["value_feedback"]["after_value"] == 90
|
||||||
|
actuator.controller.swipe_coords.assert_awaited_once()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_feedback_timeout_never_repeats_a_completed_input(monkeypatch):
|
||||||
|
actuator, state, _ = setup_actuator()
|
||||||
|
original = value_feedback.sleep
|
||||||
|
|
||||||
|
async def moving(seconds):
|
||||||
|
state["text"] = str(float(state["text"]) + 1)
|
||||||
|
await original(seconds)
|
||||||
|
|
||||||
|
monkeypatch.setattr(value_feedback, "sleep", moving)
|
||||||
|
result = await actuator.drag(**drag(feedback=feedback()))
|
||||||
|
assert not result.ok and result.code == "TIMEOUT"
|
||||||
|
assert result.metadata["execution"] == "completed"
|
||||||
|
assert result.metadata["verification"] == "unavailable"
|
||||||
|
actuator.controller.swipe_coords.assert_awaited_once()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"text", ["17:42:17 / 17:43:12", "25:42:17", "117:42:17", "17:42:170", "17:42"]
|
||||||
|
)
|
||||||
|
def test_clock_readout_rejects_ambiguous_or_invalid_times(text):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
read_value({"visible": True, "text": text}, "clock")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"value_format,node",
|
||||||
|
[
|
||||||
|
("number", {"text": "0"}),
|
||||||
|
("clock", {"text": "00:00:00"}),
|
||||||
|
("range", {"range": {"min": 0, "current": 0, "max": 10}}),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_zero_is_an_actual_value(value_format, node):
|
||||||
|
assert read_value({"visible": True, **node}, value_format) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_precision_guard_uses_observed_minimum_and_reports_geometric_budget():
|
||||||
|
calibration = {
|
||||||
|
"axis": "x",
|
||||||
|
"mode": "thumb",
|
||||||
|
"anchors": [{"position": 100.0, "value": 0.0}, {"position": 200.0, "value": 1000.0}],
|
||||||
|
"target_value": 125.0,
|
||||||
|
"tolerance": 1.0,
|
||||||
|
}
|
||||||
|
request = {
|
||||||
|
"start": [100, 1050],
|
||||||
|
"coordinate_space": "pixel",
|
||||||
|
"target_description": "observed scale",
|
||||||
|
"calibration": calibration,
|
||||||
|
}
|
||||||
|
with pytest.raises(ValueError, match="rounding"):
|
||||||
|
plan_drag(DragRequest.model_validate(request), 720, 1600)
|
||||||
|
calibration.update(tolerance=5.0, min_movement_px=20.0)
|
||||||
|
with pytest.raises(ValueError, match="minimum effective"):
|
||||||
|
plan_drag(DragRequest.model_validate(request), 720, 1600)
|
||||||
|
calibration.update(min_movement_px=10.0)
|
||||||
|
plan = plan_drag(DragRequest.model_validate(request), 720, 1600)
|
||||||
|
assert plan["calibration"]["pixel_error_budget"] == 0.5
|
||||||
|
assert plan["calibration"]["rounding_error_value"] == 5
|
||||||
|
|
||||||
|
|
||||||
|
def test_both_pinch_fingers_must_stay_in_observed_surface():
|
||||||
|
request = PinchRequest(
|
||||||
|
center=[360, 656],
|
||||||
|
start_span=40,
|
||||||
|
end_span=80,
|
||||||
|
coordinate_space="pixel",
|
||||||
|
target_description="observed timeline",
|
||||||
|
region=[200, 970, 650, 1118],
|
||||||
|
)
|
||||||
|
with pytest.raises(ValueError, match="region"):
|
||||||
|
plan_pinch(request, 720, 1600)
|
||||||
Reference in new issue
Block a user