fix: guard transient controls and verify timeline endpoints
This commit is contained in:
1 parent
479a185192
commit
00e62a663a
29 files changed
+1150
-90
No files matched your search
+72
-16
@@ -186,8 +186,11 @@ _TARGET_DESCRIPTION = (
|
||||
|
||||
|
||||
async def _wire_click(actuator: Any, a: dict[str, Any]) -> ActionResult:
|
||||
guard = (
|
||||
{"expected_resource_id": a["expected_resource_id"]} if a.get("expected_resource_id") else {}
|
||||
)
|
||||
return await actuator.click(
|
||||
int(a["target"][0]), int(a["target"][1]), times=a["times"], delay_ms=a["delay_ms"]
|
||||
int(a["target"][0]), int(a["target"][1]), times=a["times"], delay_ms=a["delay_ms"], **guard
|
||||
)
|
||||
|
||||
|
||||
@@ -259,15 +262,30 @@ _SPECS: tuple[ActionSpec, ...] = (
|
||||
operator=OperatorDialect(
|
||||
description=(
|
||||
"[ACTION] Execute a short sequence of click, wait and drag steps in ONE call, "
|
||||
"in order, without model turns or screenshots between members. Use for "
|
||||
"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. Use observed coordinates, or positions learned from an "
|
||||
"earlier observation of the same restored layout; element indices are not "
|
||||
"accepted. Each click/drag needs target_description. All members are "
|
||||
"it disappears. 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. "
|
||||
"Verify the final screen before claiming the intended 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."
|
||||
),
|
||||
params=(
|
||||
ParamSpec("steps", list[GestureStep], "Ordered click, wait and drag members."),
|
||||
@@ -286,8 +304,12 @@ _SPECS: tuple[ActionSpec, ...] = (
|
||||
"[ACTION] Precisely drag a local slider or timeline. Use explicit start/end "
|
||||
"or calibration from TWO observed numeric scale anchors. coordinate_space "
|
||||
"is normalized (0..1000) or pixel (the current screenshot's actual pixels); "
|
||||
"pixel avoids quantization on a zoomed scale. Default duration_ms=300: "
|
||||
"slower is not always more precise and may trigger long-press recognition. "
|
||||
"pixel avoids quantization on a zoomed scale. Default duration_ms=300. "
|
||||
"If a panned timeline coasts past the release position, try a slower pan "
|
||||
"(e.g. 1500ms), then wait for its ticks to stop before measuring the residual. "
|
||||
"A transient release-time bubble can disagree with the final fixed-pointer "
|
||||
"scale; do not treat it as final position proof. A permanently visible "
|
||||
"timeline can be panned independently of its zoom popup. "
|
||||
"Do not overshoot the endpoint automatically. For calibration, axis=x/y; "
|
||||
"mode=thumb moves the handle to target_value, mode=pan shifts the scale "
|
||||
"under indicator_position. Use one numeric unit, e.g. seconds since midnight, "
|
||||
@@ -317,6 +339,12 @@ _SPECS: tuple[ActionSpec, ...] = (
|
||||
"Observed numeric anchors that compute the endpoint. When supplied, end MUST be null; otherwise set calibration null.",
|
||||
required=False,
|
||||
),
|
||||
ParamSpec(
|
||||
"require_visible",
|
||||
str | None,
|
||||
"Previously observed panel resource_id; refuse the drag if it is no longer visible.",
|
||||
required=False,
|
||||
),
|
||||
),
|
||||
),
|
||||
wire=WireDialect(
|
||||
@@ -330,6 +358,7 @@ _SPECS: tuple[ActionSpec, ...] = (
|
||||
),
|
||||
ParamSpec("duration_ms", int, required=False, default=300),
|
||||
ParamSpec("calibration", DragCalibration | None, required=False),
|
||||
ParamSpec("require_visible", str | None, required=False),
|
||||
),
|
||||
bind=_wire_drag,
|
||||
),
|
||||
@@ -339,7 +368,10 @@ _SPECS: tuple[ActionSpec, ...] = (
|
||||
operator=OperatorDialect(
|
||||
description=(
|
||||
"[ACTION] Click on the target location on the screen (supports element"
|
||||
" index or absolute normalized coordinates)."
|
||||
" index or absolute normalized coordinates). Indexed resource targets are checked "
|
||||
"against live bounds before input; missing/moved targets refuse the tap. For "
|
||||
"floating or auto-hiding panels, use a guarded action_sequence with opener and "
|
||||
"target in the same call; an open panel in a screenshot may already have expired."
|
||||
),
|
||||
params=(
|
||||
ParamSpec(
|
||||
@@ -379,6 +411,12 @@ _SPECS: tuple[ActionSpec, ...] = (
|
||||
ParamSpec("target", list[int]),
|
||||
ParamSpec("times", int, required=False, default=1),
|
||||
ParamSpec("delay_ms", int, required=False, default=100),
|
||||
ParamSpec(
|
||||
"expected_resource_id",
|
||||
str | None,
|
||||
"Observed index resource ID; refuse a stale tap outside its live bounds.",
|
||||
required=False,
|
||||
),
|
||||
),
|
||||
bind=_wire_click,
|
||||
),
|
||||
@@ -386,7 +424,8 @@ _SPECS: tuple[ActionSpec, ...] = (
|
||||
"target: the agent dialect accepts an element index or a coordinate pair"
|
||||
" (an index is resolved client-side against the indexed element list); the"
|
||||
" wire takes coordinates only. target_description: recorded, never sent"
|
||||
" to the wire; required only for coordinate targets."
|
||||
" to the wire; required only for coordinate targets. Indexed resource identity is "
|
||||
"sent as expected_resource_id for live bounds validation; coordinate-only calls omit it."
|
||||
),
|
||||
),
|
||||
ActionSpec(
|
||||
@@ -809,7 +848,7 @@ def exception_prefix(action: str) -> str:
|
||||
|
||||
|
||||
@cache
|
||||
def _operator_args_model(name: str):
|
||||
def _operator_args_model(name: str, sequence_limit: int | None = None):
|
||||
"""Builds (once) the pydantic argument model for an operator shell."""
|
||||
dialect = ACTION_SPECS[name].operator
|
||||
if dialect is None:
|
||||
@@ -817,7 +856,12 @@ def _operator_args_model(name: str):
|
||||
fields: dict[str, Any] = {}
|
||||
for p in dialect.params:
|
||||
if p.required:
|
||||
fields[p.name] = (p.annotation, Field(description=p.description))
|
||||
bounds = (
|
||||
{"min_length": 1, "max_length": sequence_limit or 10}
|
||||
if name == "action_sequence"
|
||||
else {}
|
||||
)
|
||||
fields[p.name] = (p.annotation, Field(description=p.description, **bounds))
|
||||
else:
|
||||
fields[p.name] = (
|
||||
p.annotation,
|
||||
@@ -826,7 +870,7 @@ def _operator_args_model(name: str):
|
||||
return create_model(name, **fields)
|
||||
|
||||
|
||||
def operator_shell_tool(name: str) -> StructuredTool:
|
||||
def operator_shell_tool(name: str, *, sequence_limit: int | None = None) -> StructuredTool:
|
||||
"""Builds the Operator's declaration-only shell for one action.
|
||||
|
||||
The body is never executed by the Operator loop -- action calls are translated
|
||||
@@ -838,8 +882,13 @@ def operator_shell_tool(name: str) -> StructuredTool:
|
||||
raise ValueError(f"Action '{name}' has no operator dialect.")
|
||||
return StructuredTool(
|
||||
name=name,
|
||||
description=dialect.description,
|
||||
args_schema=_operator_args_model(name),
|
||||
description=dialect.description
|
||||
+ (
|
||||
f" This task allows at most {sequence_limit} members per call, including waits."
|
||||
if name == "action_sequence" and sequence_limit is not None
|
||||
else ""
|
||||
),
|
||||
args_schema=_operator_args_model(name, sequence_limit),
|
||||
func=lambda **kwargs: "Action Recorded",
|
||||
)
|
||||
|
||||
@@ -900,7 +949,7 @@ def _projected_parameters(dialect: OperatorDialect, name: str) -> dict[str, Any]
|
||||
|
||||
|
||||
@cache
|
||||
def tool_declaration(name: str) -> ToolDeclaration:
|
||||
def tool_declaration(name: str, *, sequence_limit: int | None = None) -> ToolDeclaration:
|
||||
"""Builds the Flash ``ToolDeclaration`` for one action.
|
||||
|
||||
A shared action is the operator shell projected onto JSON (same description,
|
||||
@@ -908,6 +957,13 @@ def tool_declaration(name: str) -> ToolDeclaration:
|
||||
``DeclarationDialect``.
|
||||
"""
|
||||
spec = ACTION_SPECS[name]
|
||||
if name == "action_sequence" and sequence_limit is not None:
|
||||
shell = operator_shell_tool(name, sequence_limit=sequence_limit)
|
||||
return ToolDeclaration(
|
||||
name=name,
|
||||
description=shell.description,
|
||||
parameters=convert_to_openai_tool(shell)["function"]["parameters"],
|
||||
)
|
||||
if spec.declaration is not None:
|
||||
return ToolDeclaration(
|
||||
name=name,
|
||||
|
||||
Reference in new issue
Block a user