Screen tools
Let the agent point at and click UI elements — the server locates, your handlers act.
Screen share lets the agent see; the screen tools let it act: "click the export button", "highlight the field to fill in next". The split is strict — the server finds the element, your app touches it.
Four slots, added to an agent's tools like any other:
import {
screenClickElementTool, screenHighlightElementTool, screenHighlightBoxTool,
clicked, notClicked, landedOnControl, landedOnEstimate, notShown,
} from 'cosmo-ai/tool/screen';
const agent = client.agent({
instructions: 'help the user drive their machine',
tools: [
screenLocateTool(request => grabScreen(request)),
screenClickElementTool(({ element, action }) => {
if (!canControlTheDesktop()) return notClicked('I need accessibility access');
press(element.frame, action);
return clicked;
}),
],
});In Python the locator opt-in is the screen_locate_tool(capture) factory (there is no hand-writable bare kind) and the renderers are factory functions from cosmo_ai.tools:
from cosmo_ai.tools import (
ScreenClickOutcome,
ScreenClickTarget,
screen_click_element_tool,
screen_locate_tool,
)
def on_click(target: ScreenClickTarget) -> ScreenClickOutcome: # sync or async
if not can_control_the_desktop():
return ScreenClickOutcome(clicked=False, reason="I need accessibility access")
press(target.element.frame, target.action) # button, single or double
return ScreenClickOutcome(clicked=True)
agent = client.agent(
instructions="help the user drive their machine",
tools=[screen_locate_tool(grab_screen), screen_click_element_tool(on_click)],
)screenLocateTool(capture)is a server-tool opt-in that carries configuration, because unlike the zero-config ones the locator has to be told how to see.capturereturns aScreenCapture: a JPEG of the current screen plus a list ofScreenElements — indexed regions with arole, aframe, and optionaltitle/label/value. Build it from whatever your platform offers: the accessibility tree, your own component registry, a DOM walk. Declaring it is what asks for the locator, and it is the one entry here the model never calls — the server does, mid-cosmo_screen_locate.screenClickElementTool(onClick)receives aScreenClickRequest— the resolvedelement, thecaptureit came from, and theaction(button, single or double). ReturnclickedornotClicked(reason).screenHighlightElementTool(onHighlight)highlights a located element instead of touching it — a coach-mark, a tour overlay.interactionsays which gesture to suggest (pointer,click,double_click,drag_show, …). ReturnlandedOnControl, ornotShown(reason).screenHighlightBoxTool(onHighlight)highlights a box the model located itself, as fractions of the shared surface. No capture and no lookup, so it draws immediately, which makes it the default way to point. ReturnlandedOnControlwhen you snapped onto a real control,landedOnEstimatewhen all you had was the model's box.
Both highlights answer in one shape, { shown, exact }, so the model runs the same check either way. exact: false is what sends it to the locator — and from a located element the answer is always landedOnControl, since the grounder picked it out of a real accessibility list.
Your capture handler receives a ScreenCaptureRequest; it carries no options today and exists so future capture options can arrive without changing your handler's signature. Captures serve cosmo_screen_locate alone — the vision locators read the session's own recent frames (a live track, or a sent ClientImage), never this handler's captures.
Walk through the flow
- The model calls
cosmo_screen_locatewith a description ("the blue Export button"). - The server calls your
captureand receives the screenshot + element list. - A vision model resolves the description against both and returns candidates to the model, each carrying an opaque
found_elementhandle. - The model picks one — using the conversation, which the grounder cannot see — and passes that handle to
cosmo_sdk_screen_click_elementorcosmo_sdk_screen_highlight_element. - The SDK resolves the handle back to the element it names and calls your handler with it.
Your app never parses model output and never does visual matching. It executes a concrete element plus an action, which keeps deciding what to touch on the server and actually touching it in code you wrote.
A handle names one element of one capture, so it means nothing beside any other, and there are no fields in it to edit. If the screen moved on and the capture expired — or the model assembled a handle rather than passing one back — the SDK declines with a reason the agent can say out loud rather than touching the wrong thing.
Guard real actions
screenClickElementTool performs real actions, so treat it like any other privileged surface:
- Only include elements in
capturethat the agent may legitimately touch — the element list is an allowlist by construction. Omit destructive controls, or returnnotClicked(...)for them. - Prefer
screenHighlightElementToolplus a human click for anything irreversible. - Clicking is gated by workspace policy on top of whatever you declare: a session without desktop control starts without
cosmo_sdk_screen_click_elementand reports the drop onready.rejected_tools. - Every call lands in the tool event stream, so the session timeline records what the agent touched and when.
Available in all three SDKs. The spelling follows each language — TypeScript's screenClickElementTool / screenHighlightElementTool / screenHighlightBoxTool with the screenLocateTool(capture) opt-in; Swift's .screenLocateTool(capture:) and its ScreenClickTool / ScreenHighlightTool / ScreenHighlightBoxTool; Python's screen_locate_tool(capture) / screen_click_element_tool / screen_highlight_element_tool / screen_highlight_box_tool in cosmo_ai.tools — but the names on the wire, the model-facing text, and the decode and reply shapes are identical, pinned by the shared client-tool vectors. Supplying the capture handler is the host's job in every one; the SDK owns everything either side of it.