/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // PR-RUNTIME-CU — the model-facing `computer` tool + its dispatch seam. // // This is platform-agnostic: the actual host input/capture is done by an // injected `CuDispatchBackend` (the desktop app spawns the signed Swift helper // and implements this interface). The tool owns the Path 18 obligations that // are OS-independent: per-action TCC re-check (S12), coordinate authority stays // runtime-side (S15), a closed typed-error surface (S17), and AbortSignal // threading (S18). The backend owns the actual AX/capture dispatch. import { z } from 'zod'; import { CU_TOOL_ACTION_TYPES, COMPUTER_USE_WITHHELD_VALUE, computerUseModelCallArgs, isComputerUseErrorCode, isCuMutatingAction, isCuObservingAction, type CuAction, type CuPoint, type CuToolActionType, type ComputerUseErrorCode, type ComputerUseWindowIdentity, } from '@maka/core/computer-use'; import { redactSecrets } from '@maka/core/redaction'; import { renderObservationForModel } from './computer-use-observation-text.js'; import type { MakaTool } from './tool-runtime.js'; import { bindCuaActionToObservation, bindCuaSemanticActionToObservation, CuaFrameState, fingerprintCuaAction, fingerprintCuaSemanticAction, type CuaActionRejectionReason, type CuaBoundAction, type CuaObservationSnapshot, } from './cua-frame-state.js'; import { CuaSessionState, type CuaActionLease, type CuaSessionActionBlockReason, type CuaSessionSnapshot, } from './cua-session-state.js'; /** * `scroll_amount` has no declared unit at the tool boundary ("Amount for * scroll", 0..100) while both executors declare pages. Fixed here so the two * ends cannot disagree silently. */ const SCROLL_UNITS_PER_PAGE = 10; const COMPUTER_USE_CATEGORY = 'computer_use'; import { adaptToCuAction, computerParams, snapshotComputerParams, summarize, summarizeEvidence, coordinate, text, type ComputerParams, type ComputerSummaryAction, } from './computer-use-codec.js'; import type { CuDispatchBackend, CuDispatchOutcome, CuObservation, CuLaunchedApp, CuObservedElement, CuOverlayHook, CuOverlayHookContext, CuPresentationFence, CuRunContext, CuRunResult, CuSemanticAction, } from './computer-use-types.js'; export { adaptToCuAction, snapshotComputerParams } from './computer-use-codec.js'; export type { CuAppSummary, CuDispatchBackend, CuDispatchEvidence, CuDispatchOutcome, CuObservedElement, CuObservation, CuOverlayHook, CuOverlayHookContext, CuPresentationFence, CuRunContext, CuRunResult, CuScreenshot, CuSemanticAction, } from './computer-use-types.js'; // Function-tool JSON schemas require an object at the top level. // Keep the wire schema as one top-level object, then apply the strict // discriminated union above immediately at execution. /** * The schema the model is actually held to. * * Exported for one reason: `computerParams` — the strict union this narrows to * — is not what the SDK validates against, and a test that only exercises the * union proves nothing about what a model can send. `window_action` shipped * that way. Its fields were added to the union, its tests passed against the * union, and a real-machine probe called the backend directly and moved a * window. This schema is `.strict()` and had no `window_action`, `position` or * `size` in it, so every call a model made was rejected by the SDK before * reaching the tool — invisible to the debug journal, which wraps `impl`. The * action was unusable from the day it was added and nothing said so. * * `computer-use-schema-parity.test.ts` is what holds the two schemas against * each other, and it is the only thing that can tell them apart. */ export const computerWireParams = z .object({ action: z .enum(CU_TOOL_ACTION_TYPES as unknown as [string, ...string[]]) .describe( 'Operation to perform. Required fields by action: list_apps takes an optional app to filter by — pass the name you were given ("TextEdit", "文本编辑") and it returns the matching app ids, which is far cheaper than listing everything; without it only apps that currently have a window are listed; launch_app requires app; observe/screenshot require app or window_id, and observe takes an optional menu to open one menu bar menu and an optional query to show only the matching part of a large window; click_element requires observation_id and element_id; set_value requires observation_id, element_id, and value; select_text/secondary_action require observation_id, element_id, and text; scroll_element requires observation_id, element_id, and scroll_direction, with optional scroll_amount; element_sequence requires observation_id and steps, where each step names a control by the label it shows and optionally its role — prefer it whenever several controls must be operated in order, since it costs one call instead of one per control; window_action requires observation_id, element_id and window_action (move, resize or minimize), with position for move and size for resize — element_id is the window itself, which is the first element of the observation, and position is in screen points, the same space the observation reports its window bounds and displays in, so moving a window to the left edge of a screen means that display x with y unchanged. Raw key and coordinate actions remain in this provider schema for compatibility, but the shipping maka-cu host refuses them.', ), // "Exact" was already in this description and was not enough. On a real // desktop chain the model asked for "Calculator" and got nothing, because // the app is named 计算器 — macOS reports the localized display name and // that name is the identity. It recovered by calling list_apps, at the cost // of a round trip this sentence can save. app: z .string() .min(1) .max(512) .optional() .describe( 'The application to look at: either a bundle id like com.apple.calculator, or the name a person would ' + 'use for it — "Calculator", "计算器", "Visual Studio Code" all resolve. A name that matches two running ' + 'applications comes back as ambiguous with both ids, rather than one of them being picked for you. ' + 'Required for observe unless window_id is supplied.', ), window_id: z .number() .int() .positive() .optional() .describe('Exact window_id from list_apps or observe.'), include_screenshot: z .boolean() .optional() .describe( 'For observe, also capture a picture of the window. Defaults to false: the element ' + 'list is what element actions need, and capturing the picture is the slow part. ' + 'Pass true only when the pixels need visual interpretation or the element list ' + 'does not describe a control. A screenshot does not enable coordinate input.', ), menu: z .string() .min(1) .max(256) .optional() .describe( 'For observe: the title of one menu bar menu to open, exactly as the observation lists it ' + '("文件", "Format"). An observation lists the menu titles when the executor walks the menu bar; ' + "this lists one menu's commands, and they can then be clicked with click_element like any other " + 'element. Most of what an application can do is a menu command and nothing in the window reaches it. ' + 'Open the one menu you need — the whole menu bar is several times the size of the window. A command ' + 'shown as disabled cannot be pressed: it needs its application in front, which Computer Use does not do. ' + 'An observation that answers menu_bar=unavailable came from an executor that does not report the menu ' + 'bar at all, and no menu command is reachable there however the argument is spelled.', ), wait_for_text: z .string() .min(1) .max(256) .optional() .describe( 'For wait: return as soon as this text appears in the window you last observed, instead of after a fixed ' + 'delay. Use it after an action that opens something — a sheet, a panel, a dialog — and name text you expect ' + 'it to contain. `duration` becomes the deadline (default 5s). Far better than guessing how long to sleep. ' + "It matches a control's name as well as its value, and returns immediately if the text is already there, " + 'so name something that is not on screen yet — the title of the sheet you are opening, not the button you ' + 'just pressed.', ), wait_for_text_gone: z .string() .min(1) .max(256) .optional() .describe( 'For wait: the mirror of wait_for_text — return as soon as this text is no longer in the window you last ' + 'observed. Use it after dismissing something, or while a progress indicator is up.', ), query: z .string() .min(1) .max(256) .optional() .describe( 'For observe: show only elements whose label, value or role contains this text, plus the elements containing them. ' + 'Element ids are unchanged, so anything found can be acted on directly. Use it on a large window — a Finder ' + 'window is about 1,200 elements and a VS Code window about 1,000, and most of that is a file list or page text ' + 'you did not ask for. Observe without a query first if you do not know what the window holds.', ), observation_id: z .string() .min(1) .max(256) .optional() .describe( 'Required for every action that targets an observed element or coordinate. Copy it exactly from the immediately preceding observe or fresh observation result.', ), element_id: z .string() .min(1) .max(256) .optional() .describe( 'Required for click_element, set_value, select_text, and secondary_action. Copy the exact element_id from the same observation_id.', ), value: text .optional() .describe('Required only for set_value. The complete replacement value to write.'), coordinate: coordinate .optional() .describe( 'Required for coordinate pointer actions. Coordinates are in the referenced observation screenshot.', ), start_coordinate: coordinate.optional().describe('Required only for left_click_drag.'), text: text .optional() .describe( 'Required for select_text, secondary_action, press_key, type, key, and hold_key. ' + 'For secondary_action it must be one of the names the element itself advertises — an observation writes them ' + 'after the label as "+show_menu,raise", and an element with none offers nothing beyond a plain click_element.', ), scroll_direction: z .enum(['up', 'down', 'left', 'right']) .optional() .describe('Direction for scroll and scroll_element.'), scroll_amount: z .number() .int() .min(0) .max(100) .optional() .describe( `Amount for scroll and scroll_element, in tenths of a page (${SCROLL_UNITS_PER_PAGE} = one page).`, ), duration: z .number() .min(0) .max(60) .optional() .describe('Duration in seconds for wait or hold_key.'), window_action: z .enum(['move', 'resize', 'minimize']) .optional() .describe( 'Required for window_action. Moving or resizing a window is its own verb because dragging its title bar ' + 'is a coordinate action, and a window Computer Use drives is behind something else, so the drag is refused. ' + 'This is not, and it does not bring the application forward. ' + // The one action here that cannot be taken back. Measured: the moment // it succeeds, list_apps reports windowCount 0 for that application // and observe answers target_missing — a minimized window is not in // the window list, so there is nothing left to address. A model that // does not know this minimises a window to get it out of the way and // then cannot put it back or even see that it is still there. 'minimize is one-way: a minimized window leaves the window list, so nothing here can restore it and ' + 'observing it afterwards fails. Only the person at the machine can bring it back, from the Dock. ' + 'Do not minimize a window to get it out of the way — move it instead.', ), position: z // Signed, because a second display is a real place: one measured here sits // at (-193, -1080) in the space the observation reports. Refusing a // negative would make half the desktop unaddressable. .tuple([z.number().int(), z.number().int()]) .optional() .describe( "Required for window_action=move: [x, y] of the window's top-left in screen points, the same space the " + 'observation reports window bounds and displays in.', ), size: z .tuple([z.number().int().positive(), z.number().int().positive()]) .optional() .describe('Required for window_action=resize: [width, height] in points.'), steps: z .array( z .object({ label: z.string().min(1).max(256), role: z.string().min(1).max(64).optional(), do: z.enum(['click', 'set_value']).optional(), value: text.optional(), }) .strict(), ) .min(1) .max(12) .optional() .describe( 'Required only for element_sequence. Each step names a control by the label it shows in the observation (and its role when the label alone is ambiguous). ' + '`do` defaults to click; use set_value with `value` to write into a field. The host re-observes before every step, so labels — not element_ids — are what carry across.', ), region: z .tuple([ z.number().int().nonnegative(), z.number().int().nonnegative(), z.number().int().nonnegative(), z.number().int().nonnegative(), ]) .optional() .describe('Required only for zoom: [x1, y1, x2, y2] in the referenced observation.'), }) .strict(); /** * Raw result of the `computer` tool. `text` is the S16-safe summary the runtime * records to session history (via coerceResultContent's text-only projection: * this object has no `kind`, so only `text` survives). `screenshot`, when * present, feeds the local presentation layer and, only for explicitly visual * actions, `toModelOutput`. It never enters `text`, so the bounded frame base64 * stays out of session history. */ interface ComputerToolResult { text: string; modelText?: string; error?: ComputerUseErrorCode; failureClass?: 'ambiguous_target'; screenshot?: { base64: string; mimeType: string }; includeScreenshotInModelOutput?: boolean; } export const COMPUTER_USE_MODEL_SCREENSHOT_POLICY = { list_apps: 'never', launch_app: 'never', observe: 'explicit', click_element: 'never', set_value: 'never', select_text: 'never', secondary_action: 'never', scroll_element: 'never', window_action: 'never', element_sequence: 'never', press_key: 'never', screenshot: 'always', cursor_position: 'never', mouse_move: 'always', left_click: 'always', right_click: 'always', middle_click: 'always', double_click: 'always', triple_click: 'always', left_mouse_down: 'always', left_mouse_up: 'always', left_click_drag: 'always', type: 'always', key: 'always', hold_key: 'always', scroll: 'always', wait: 'never', zoom: 'always', } as const satisfies Record; function shouldSendScreenshotToModel(input: ComputerParams): boolean { const policy = COMPUTER_USE_MODEL_SCREENSHOT_POLICY[input.action]; return ( policy === 'always' || (policy === 'explicit' && input.action === 'observe' && input.include_screenshot === true) ); } export interface ComputerUseToolSet extends Array { clearSession(sessionId: string): void; sessionEvents: { snapshot(sessionId: string): CuaSessionSnapshot; physicalUserIntervened(sessionId: string): CuaSessionSnapshot; interventionDebounceElapsed(sessionId: string): CuaSessionSnapshot; reobserveRequired(sessionId: string): CuaSessionSnapshot; screenLocked(sessionId: string): CuaSessionSnapshot; screenUnlocked(sessionId: string): CuaSessionSnapshot; blockedUrlDetected(sessionId: string): CuaSessionSnapshot; userStopped(sessionId: string): CuaSessionSnapshot; dynamicContentChanged(sessionId: string): CuaSessionSnapshot; }; } /** * Failures after which a fresh observation is the right thing to hand back. * * Only those that mean "the frame you were holding has moved on". A refusal * like `user_intervened` or `screen_locked` is a latch with a deliberate * release — the user has to stop typing, the screen has to be unlocked — and * observing on its behalf would quietly open it. Those keep their old shape: * no observation, and the model has to come back and ask. */ const REOBSERVABLE_FAILURES = new Set([ 'target_changed', 'target_missing', 'target_occluded', 'ambiguous_target', 'page_target_changed', 'stale_frame', 'stale_epoch', 'duplicate_action', 'invalid_coordinate', ]); /** * Whether a name the user used names this application. * * Containment both ways, because macOS reports `NSRunningApplication`'s * localized name and that is often shorter than what a person calls the app. * Visual Studio Code answers `"Code"`: a one-way `name.includes(query)` finds * nothing for "Visual Studio Code", and a real run got `app_count: 0` for an * application that was running with a window, then recovered by picking the id * out of the `apps_with_windows` list — a round trip for a name that was right. * * The reverse direction is floored at three characters so that a short name * cannot match most queries: `"Go"` inside "Google Chrome" is a coincidence, * `"Code"` inside "Visual Studio Code" is the application. */ function matchesAppQuery(candidate: string, query: string): boolean { if (candidate.includes(query)) return true; return candidate.length >= 3 && query.includes(candidate); } function shouldReobserveAfter(outcome: CuRunResult['outcome']): boolean { return outcome.ok || REOBSERVABLE_FAILURES.has(outcome.error); } /** * The pointer-shaped stand-in a semantic action shows the presentation layer. * * The cursor overlay and the mirror speak in clicks and coordinates; a semantic * action has an element. This is the same translation the single-action path * already does inline, named so a sequence can reuse it. */ function summarySemanticAction(action: CuSemanticAction, binding: CuaBoundAction): CuAction { const coordinate = binding.sourceCoordinate ?? { x: 0, y: 0 }; return action.type === 'set_value' ? { type: 'type', text: action.value } : { type: 'left_click', coordinate }; } function observationText(observation: CuObservation): string { return renderObservationForModel(observation); } /** * What the model asked to see, carried on the observation the executor * returned. * * Both of these were advertised in the tool description as facts of the format * and neither was produced by the only executor there was. `query` is filtered * by the renderer from `observation.query` and the executor was expected to * echo it; it did not, so the filter never ran, and the header — which is where * a filtered view announces itself — said nothing either. Stamping the request * makes the filter the host's own work, which is where it already was. * * `menu` cannot be synthesized: opening a menu is a walk of the menu bar and * only the executor can do it. So it is named as missing instead. An * unanswerable request that says nothing is the failure mode this whole surface * exists to remove. */ function withRequestedView( observation: CuObservation, request: { query?: string; menu?: string }, ): CuObservation { const menuAsked = request.menu !== undefined; const menuReturned = observation.elements.some((element) => element.role === 'AXMenuBar'); return { ...observation, ...(request.query !== undefined && observation.query === undefined ? { query: request.query } : {}), ...(menuAsked && !menuReturned ? { menu: { ...observation.menu, unavailable: true } } : {}), }; } function persistedObservationText(observation: CuObservation): string { return JSON.stringify({ observation_id: observation.observationId, app_id: observation.appId, pid: observation.pid, window_id: observation.windowId, element_count: observation.elements.length, screenshot: observation.screenshot ? { mime_type: observation.screenshot.mimeType, width_px: observation.screenshot.widthPx, height_px: observation.screenshot.heightPx, } : undefined, }); } /** * How long the runtime waits for a presentation to say it has finished, after * the action has already been dispatched. * * This is not a second opinion on `CuPresentationFence.readyTimeoutMs`, and it * is deliberately much shorter than one. The two bound different, sequential * intervals: `readyTimeoutMs` covers the whole motion, from the moment the * cursor is sent until the release gate opens and the action is allowed to * dispatch. This one starts only after `onActionEnd`, so all it has to cover is * the landing — the tail of a motion the completion has already truncated, plus * the click pulse. * * Nothing said so, and nothing checked it, which is why it read as a constant * chosen twice. `apps/desktop/src/main/__tests__/cursor-engine.test.ts` now * measures the longest tail the real engine can leave and asserts it fits here. */ export const DEFAULT_PRESENTATION_FINISHED_TIMEOUT_MS = 1_500; /** * The action minus any target the host has not confirmed. * * `app` and `window_id` may accompany an element action as redundant hints. * They are accepted so a careful model is not rejected for supplying them, but * until the observation they name is the active frame they are unverified * claims — and the approval summary built from these arguments is what a person * reads before allowing the action. */ function stripUnverifiedTargetHints(input: T): T { if (!('observation_id' in input)) return input; if (!('app' in input) && !('window_id' in input)) return input; const { app: _app, window_id: _windowId, ...rest } = input as T & { app?: string; window_id?: number; }; return rest as T; } /** * Says so when a redundant target hint disagrees with the frame the action is * bound to. * * A hint that agrees is free — dispatch resolves through the observation * either way. A hint that disagrees means the model believes it is driving a * different window than the one it is about to act on, and ignoring it quietly * would let it keep that belief through every retry. */ function targetHintConflict( input: ComputerParams, record: { appId?: string; appAlias?: string; windowId?: number }, ): ComputerToolResult | undefined { const hinted = input as ComputerParams & { app?: string; window_id?: number }; if ( hinted.app !== undefined && record.appId !== undefined && hinted.app !== record.appId && // The name that resolved this observation is not a contradiction of it. hinted.app !== record.appAlias ) { return { error: 'target_mismatch', text: `maka_computer.${input.action} failed: target_mismatch — this observation is of ${record.appId}, not ${hinted.app}. Observe the app you mean, then act on an element from that observation.`, }; } if ( hinted.window_id !== undefined && record.windowId !== undefined && hinted.window_id !== record.windowId ) { return { error: 'target_mismatch', text: `maka_computer.${input.action} failed: target_mismatch — this observation is of window ${record.windowId}, not ${hinted.window_id}. Observe that window, then act on an element from that observation.`, }; } return undefined; } /** * Says so when an argument holds a placeholder from the model's own call * record instead of a value. * * The record the model reads back withholds screen-derived and typed values and * leaves a shape in their place. Those shapes are legal strings: `value` and * `text` are `z.string().max(8000)` with no lower bound and no pattern, so * `""` passes the wire schema and the strict union both, and a model * replaying its own `set_value` would type those characters into the user's * field. Nothing here reads the argument's contents beyond matching that shape. * * Every string argument, not the three that were named. The named list was * written when `value`, `text` and `steps[].value` were the only arguments a * shape could reach; `observe`'s `query` and `menu` and `wait`'s * `wait_for_text` are plain strings the schema accepts too, and a placeholder * there is the quieter failure of the two — a model that filtered a window with * `query:"下载"`, replayed `query:""` and read `showing 0 of 1200` had * been told the control does not exist. Those arguments no longer come back as * shapes, and this is what makes that a property of the tool rather than of one * map staying in step with another. */ function withheldValueReplayed(input: ComputerParams): ComputerToolResult | undefined { const offending: string[] = []; for (const [key, held] of Object.entries(input as Record)) { if (typeof held === 'string' && COMPUTER_USE_WITHHELD_VALUE.test(held)) { offending.push(key); continue; } if (!Array.isArray(held)) continue; for (const entry of held) { if (entry === null || typeof entry !== 'object') continue; for (const [member, value] of Object.entries(entry as Record)) { const label = `${key}[].${member}`; if ( typeof value === 'string' && COMPUTER_USE_WITHHELD_VALUE.test(value) && !offending.includes(label) ) { offending.push(label); } } } } if (offending.length === 0) return undefined; return { error: 'withheld_value_replayed', text: `maka_computer.${input.action} failed: withheld_value_replayed — ` + `${offending.join(', ')} holds a placeholder from your own call record, not text. ` + 'Your earlier calls are recorded with typed and screen-derived values replaced by their ' + 'shape, because those values belong to the user. Nothing was sent. Send the text you mean.', }; } /** * One line of the Computer Use debug journal. * * Everything about a call that is normally projected away before anyone can * read it back: the arguments exactly as the model sent them, and the result * exactly as it was returned. The stored record is a deliberately redacted * summary — right for an audit row, useless when the question is "what did the * model actually send", which is a question that has now cost two sessions. * * Off unless the host passes a sink. */ export interface CuDebugRecord { ts: number; sessionId: string; turnId: string; toolCallId: string; /** Verbatim model arguments, before any parse or projection. */ rawArgs: unknown; /** What the model will read back as its own call. */ modelFacingArgs: unknown; /** Full result text, untruncated. */ resultText?: string; /** The longer text the model sees, when it differs from the stored one. */ resultModelText?: string; error?: string; durationMs: number; } export function buildComputerUseTools(deps: { backend: CuDispatchBackend; overlay?: CuOverlayHook; /** * Whether the machine is locked right now. * * Consulted before every action and every observation, because a locked * screen is the user saying they have left: past that point the agent would * be driving and reading a machine its owner deliberately closed off, with * nobody watching and no way to intervene. Refusing produces the * `screen_locked` session state, which is what keeps the refusal from having * to be re-derived on every subsequent call — and which only * `sessionEvents.screenUnlocked` can leave. * * Takes the session so the host can record which sessions it will have to * release when the machine comes back. Without that, a session whose very * first call landed on a locked screen would latch `screen_locked` while * being unknown to the host, and would stay latched for the rest of its life. * * Absent means the host cannot answer the question, not that the machine is * unlocked; there is no lock guard at all in that case. */ screenLocked?: (context: { sessionId: string }) => boolean | Promise; presentationReadyTimeoutMs?: number; presentationFinishedTimeoutMs?: number; /** Diagnostics only. Never on by default, never able to change an outcome. */ debug?: (record: CuDebugRecord) => void; }): ComputerUseToolSet { const presentationReadyTimeoutMs = deps.presentationReadyTimeoutMs ?? 1_000; const presentationFinishedTimeoutMs = deps.presentationFinishedTimeoutMs ?? DEFAULT_PRESENTATION_FINISHED_TIMEOUT_MS; const invocationQueues = new Map>(); const presentationWaiters = new Map void>>(); const presentationQueueWaiters = new Map void>>(); const presentationGenerations = new Map(); const pendingInvocationTurns = new Map>(); let presentationQueue = Promise.resolve(); let accessibilityPermissionRequested = false; interface SessionObservationRecord { turnId: string; state: CuaFrameState; backendObservationId?: string; appId?: string; /** The non-canonical name that resolved this observation, if any. */ appAlias?: string; windowId?: number; elements?: Map; /** From the last observation: the windows stacked above the target. */ obscuringRects?: Array<{ x: number; y: number; width: number; height: number }>; } const observations = new Map(); interface SessionStateRecord { turnId?: string; state: CuaSessionState; } const sessionStates = new Map(); function sessionState(sessionId: string, turnId?: string): CuaSessionState { const current = sessionStates.get(sessionId); if (current) { if (turnId === undefined || current.turnId === turnId) { return current.state; } if (current.turnId === undefined) { current.turnId = turnId; return current.state; } } const created = new CuaSessionState(sessionId); sessionStates.set(sessionId, { ...(turnId === undefined ? {} : { turnId }), state: created, }); return created; } function sessionObservation(sessionId: string, turnId: string): SessionObservationRecord { const current = observations.get(sessionId); if (current?.turnId === turnId) return current; if (current) sessionState(sessionId, turnId).reobserveRequired(); const next = { turnId, state: new CuaFrameState() }; observations.set(sessionId, next); return next; } function trackPendingInvocation(sessionId: string, turnId: string): () => void { const turns = pendingInvocationTurns.get(sessionId) ?? new Set(); turns.add(turnId); pendingInvocationTurns.set(sessionId, turns); return () => { turns.delete(turnId); if (turns.size === 0) pendingInvocationTurns.delete(sessionId); }; } function invalidateObservation(sessionId: string): void { const record = observations.get(sessionId); if (!record) return; record.state.invalidate(); record.backendObservationId = undefined; record.elements = undefined; } // `unsupported_action` carries two facts that call for opposite next moves. // The executor uses it for "this element does not offer that", which means // pick another element. Every use in this file means the other thing: the // capability is absent from this build, so no element and no window makes it // appear, and retrying is guaranteed to fail the same way. The code is shared // (it is fixed in `COMPUTER_USE_ERROR_CODES`), so the sentence has to be what // tells the two apart. const MISSING_CAPABILITY = 'this Computer Use build does not provide that capability at all, so retrying it, ' + 'or retrying it against another target, will fail the same way.'; // A refusal that names only the reason teaches the model the reason, which is // a host state machine label it cannot act on. Every one of these has exactly // one call that clears it, and the ones that have none have to say so — a // model that is not told a refusal is terminal re-sends it. Measured on real // traces: `reobserve_required` came back 13 times in one session, and the // model never once answered it with `observe`, because nothing said to. const SESSION_BLOCK_RECOVERY: Record = { no_active_frame: 'no observation is active yet. Call action:"observe" with an app or window_id first, ' + 'then quote the observation_id it returns.', reobserve_required: 'observation consumed; call action:"observe" before the next coordinate or element action.', user_intervened: 'the user was at the keyboard or the pointer, so nothing was sent. ' + 'It clears on its own once they have been idle briefly; until then every call is ' + 'refused the same way, observe included. Wait, then call action:"observe" for a ' + 'current observation and send the action again.', screen_locked: 'the screen is locked, so nothing on it can be read or driven. ' + 'Do not retry; tell the user to unlock the screen.', blocked_url: 'this target is refused for the rest of this session, and so is every other one — ' + 'no further computer action of any kind will be accepted. Report that Computer Use is ' + 'off limits for this session and continue without it.', user_stopped: 'the user stopped computer use for this session. Do not send any further computer action; ' + 'report that it was stopped.', }; // Reasons the frame layer produces. `invalid_binding` and `action_not_claimed` // are internal rejection labels rather than Computer Use error codes, so they // travel to the model as `stale_frame` — but they still need their own // sentence, because "look again" is the answer to all three and nothing in // the bare code said it. const BINDING_FAILURE_RECOVERY: Record = { invalid_binding: 'the observation_id sent with this action is not one this session handed out. ' + 'Call action:"observe" and quote the observation_id from its header line verbatim.', no_active_frame: 'no observation is active yet. Call action:"observe" with an app or window_id first, ' + 'then quote the observation_id it returns.', stale_epoch: 'the screen moved on after the observation this action quotes. ' + 'Call action:"observe" again and re-pick the element from the new observation.', stale_frame: 'the observation this action quotes is no longer the current one. ' + 'Call action:"observe" again and re-pick the element from the new observation.', duplicate_action: 'this exact action was already sent against this observation and was not sent twice. ' + 'Call action:"observe" to see whether it took effect instead of sending it again.', retired_action: 'this exact action was already refused against this observation, and nothing was dispatched ' + 'either time, so the window is as it was and observing again would show the same thing. ' + 'Address a different element, or use a different action on this one.', action_not_claimed: 'this action was not registered against the observation it quotes. ' + 'Call action:"observe" and send the action again with the observation_id it returns.', target_missing: 'that element is not in the window any more. ' + 'Call action:"observe" and pick an element_id from the new observation.', target_changed: 'the window changed under this action, so it was not sent. ' + 'Call action:"observe" and decide again from what it shows.', capture_failed: 'the window could not be read, so the outcome of this action is unknown. ' + 'Call action:"observe" before sending anything else.', }; function sessionFailure( reason: CuaSessionActionBlockReason, action?: string, ): ComputerToolResult { const tool = action ? `maka_computer.${action}` : 'maka_computer'; return { text: `${tool} failed: ${reason} — ${SESSION_BLOCK_RECOVERY[reason]}`, error: reason, }; } function validateActionLease( state: CuaSessionState, lease: CuaActionLease, ): ComputerToolResult | undefined { const validation = state.validateLease(lease); return validation.ok ? undefined : sessionFailure(validation.reason); } function applyTypedOutcomeState(state: CuaSessionState, outcome: CuDispatchOutcome): void { if (outcome.ok) return; switch (outcome.error) { case 'user_intervened': // The driver currently exposes no trustworthy debounce deadline. // Re-observe immediately instead of entering an unrecoverable // intervention_debounce state. state.reobserveRequired(); return; case 'screen_locked': state.screenLocked(); return; case 'blocked_url': state.blockedUrlDetected(); return; case 'outcome_unknown': case 'service_unavailable': case 'service_mismatch': state.reobserveRequired(); return; default: return; } } function toObservationSnapshot(observation: CuObservation): CuaObservationSnapshot { const screenshotWidth = observation.screenshot?.widthPx; const screenshotHeight = observation.screenshot?.heightPx; const sourceBoundsPx = observation.sourceBoundsPx ?? (screenshotWidth !== undefined && screenshotHeight !== undefined ? { x: 0, y: 0, width: screenshotWidth, height: screenshotHeight } : undefined); const width = sourceBoundsPx?.width ?? screenshotWidth; const height = sourceBoundsPx?.height ?? screenshotHeight; const target: ComputerUseWindowIdentity = { pid: observation.pid, windowId: observation.windowId, appName: observation.appId, ...(observation.windowTitle ? { title: observation.windowTitle } : {}), ...(observation.bundleId ? { bundleId: observation.bundleId } : {}), ...(observation.windowBounds ? { bounds: observation.windowBounds } : {}), ...(sourceBoundsPx ? { sourceBoundsPx } : {}), ...(observation.zIndex === undefined ? {} : { zIndex: observation.zIndex }), ...(observation.contentFingerprint ? { contentFingerprint: observation.contentFingerprint } : {}), ...(observation.page ? { page: observation.page } : {}), }; const displays = observation.displays ?? (width !== undefined && height !== undefined ? [ { displayId: `window:${observation.pid}:${observation.windowId}`, logicalBounds: { x: 0, y: 0, width, height }, sourceBoundsPx: { x: 0, y: 0, width, height }, scaleFactor: 1, }, ] : []); return { capturedAt: observation.capturedAt ?? Date.now(), ...(width !== undefined ? { screenshotWidthPx: width } : {}), ...(height !== undefined ? { screenshotHeightPx: height } : {}), displays, target, }; } function registerObservation( record: SessionObservationRecord, observation: CuObservation, ): CuObservation { const normalized = { ...observation, elements: observation.elements.map((element) => ({ ...element, identity: element.identity ?? { role: element.role, ...(element.label ? { label: element.label } : {}), ...(element.value !== undefined ? { value: element.value } : {}), }, })), }; const frame = record.state.observe(toObservationSnapshot(normalized)); record.backendObservationId = observation.observationId; // Read before `record.appId` is overwritten: the alias survives a fresh // observation of the same application. Only `observe` knows the name the // model used, and every dispatch takes a fresh observation afterwards — // clearing it there would make `Dictionary` work once and answer // `target_mismatch` on the next call. const carriedAlias = record.appId === observation.appId ? record.appAlias : undefined; record.appId = observation.appId; record.appAlias = observation.appAlias ?? carriedAlias; record.windowId = observation.windowId; record.obscuringRects = observation.obscuringRects; record.elements = new Map(normalized.elements.map((element) => [element.elementId, element])); return { ...normalized, observationId: frame.frameId }; } type BindingFailureReason = | CuaActionRejectionReason | 'target_missing' | 'target_changed' | 'capture_failed'; function bindingFailure(reason: BindingFailureReason, action?: string): ComputerToolResult { // `retired_action` is an internal distinction, not a twenty-ninth word for // the model: it is the same fact as `duplicate_action` with a different // recovery, and the recovery is the sentence, not the code. const error: ComputerUseErrorCode = isComputerUseErrorCode(reason) ? reason : reason === 'retired_action' ? 'duplicate_action' : 'stale_frame'; const tool = action ? `maka_computer.${action}` : 'maka_computer'; return { text: `${tool} failed: ${error} — ${BINDING_FAILURE_RECOVERY[reason]}`, error, }; } function preservePartialDelivery(result: CuRunResult): CuRunResult { if ( result.outcome.ok || result.outcome.error === 'outcome_unknown' || (result.outcome.completedSubSteps ?? 0) === 0 ) { return result; } return { ...result, outcome: { ...result.outcome, error: 'outcome_unknown', message: 'computer action was partially delivered; final state is unknown. ' + 'Do not send it again — part of it already landed and repeating it can apply that part twice. ' + 'Call action:"observe" first and check what actually took effect.', }, }; } function hasUncertainDeliveredOutcome(result: CuRunResult | undefined): result is CuRunResult { return ( result !== undefined && !result.outcome.ok && (result.outcome.error === 'outcome_unknown' || (result.outcome.completedSubSteps ?? 0) > 0) ); } function deliveredWithoutFreshObservation( action: ComputerSummaryAction, result: CuRunResult, includeScreenshotInModelOutput = false, ): ComputerToolResult { const evidence = summarizeEvidence(result.outcome.evidence); const hostEvidence = summarizeEvidence(result.outcome.evidence, 'host'); const screenshot = result.screenshot; return { text: `maka_computer.${action.type} failed: outcome_unknown${hostEvidence}` + ' — the action reached the executor but a required fresh observation was unavailable, ' + 'so whether it took effect is not known. Do not send it again: it may already have ' + 'landed and repeating it would apply it twice. Call action:"observe" first and check ' + 'whether it took effect; send it again only if the observation shows it did not.', modelText: `maka_computer.${action.type} failed: outcome_unknown${evidence}` + ' — the action reached the executor and may already have taken effect, but that could ' + 'not be confirmed. Do not send it again: repeating it can apply it twice. Call ' + 'action:"observe" first and check whether it took effect; send it again only if the ' + 'observation shows it did not.', error: 'outcome_unknown', ...(screenshot ? { screenshot: { base64: screenshot.base64, mimeType: screenshot.mimeType, }, includeScreenshotInModelOutput, } : {}), }; } /** * The frame the mirror shows, when the dispatch itself did not produce one. * * `presentToPip` reads `result.screenshot ?? result.observation?.screenshot`, * and the executor attaches both only on its success arm. Every failure arm — * `outcome_unknown`, `dispatch_refused`, `target_occluded`, `stale_frame`, * `target_changed`, `reobserve_required` — returns an outcome and nothing * else, so the mirror had nothing to draw. Across 30 traces the split was * exact: 11 runs where the mirror appeared all had at least one success that * carried a screenshot; the 19 where it never appeared had none. * * The frame exists either way. A refused action is followed here by a full * observation captured with a screenshot — the one that becomes the "Fresh * observation:" tail the model reads. It was simply never handed to the * overlay, which left the mirror blank at exactly the moment a person most * wants to look at it: the turn that went wrong. */ function withMirrorFrame( result: CuRunResult, freshObservation: CuObservation | undefined, ): CuRunResult { if (!freshObservation?.screenshot) return result; if (result.screenshot || result.observation?.screenshot) return result; return { ...result, observation: freshObservation }; } function claimBoundAction( record: SessionObservationRecord, observationId: string, action: CuAction | CuSemanticAction, ): CuaBoundAction | { rejection: BindingFailureReason } { const active = record.state.activeObservation(); const semantic = action.type === 'click_element' || action.type === 'set_value' || action.type === 'select_text' || action.type === 'press_key' || action.type === 'scroll_element' || action.type === 'window_action' || action.type === 'secondary_action'; const semanticAction = semantic ? (action as CuSemanticAction) : undefined; const semanticValue = semanticAction?.type === 'set_value' ? semanticAction.value : semanticAction?.type === 'select_text' ? semanticAction.text : semanticAction?.type === 'secondary_action' ? semanticAction.action : semanticAction?.type === 'press_key' ? semanticAction.key : semanticAction?.type === 'scroll_element' ? `${semanticAction.direction}:${semanticAction.pages ?? 1}` : semanticAction?.type === 'window_action' ? `${semanticAction.action}:${ semanticAction.position ? `${semanticAction.position.x},${semanticAction.position.y}` : semanticAction.size ? `${semanticAction.size.width}x${semanticAction.size.height}` : '' }` : undefined; const elementId = semanticAction && 'elementId' in semanticAction ? semanticAction.elementId : undefined; const fingerprint = semanticAction ? fingerprintCuaSemanticAction(action.type, elementId, semanticValue) : fingerprintCuaAction(action as CuAction); if ( record.state.isConsumed({ frameId: observationId, epoch: active?.epoch ?? 0 }, fingerprint) ) { return { rejection: record.state.wasRetired( { frameId: observationId, epoch: active?.epoch ?? 0 }, fingerprint, ) ? 'retired_action' : 'duplicate_action', }; } if (!active) return { rejection: 'no_active_frame' }; if (observationId !== active.frameId) return { rejection: 'stale_frame' }; const bound = semanticAction ? bindCuaSemanticActionToObservation(active, { type: semanticAction.type, elementId, value: semanticValue, // Carried so the agent cursor can travel to the element before the // action fires; dispatch itself stays element-addressed. ...(elementId && record.elements?.get(elementId)?.frame ? { elementFrame: record.elements.get(elementId)!.frame! } : {}), }) : bindCuaActionToObservation(active, action as CuAction); if (!bound) return { rejection: 'target_missing' }; const claim = record.state.claimAction(bound); return claim.ok ? bound : { rejection: claim.reason }; } function consumeBoundAction( record: SessionObservationRecord, action: CuaBoundAction, ): BindingFailureReason | undefined { const confirmation = record.state.confirmAction(action); record.backendObservationId = undefined; record.elements = undefined; return confirmation.ok ? undefined : confirmation.reason; } /** * The frame's refusal, plus the executor's own when it had one. * * Frame bookkeeping can fail after the executor has already answered: the * epoch moves while a dispatch is in flight, and the confirmation that * follows is rejected. Returning only the frame's word threw away the one * sentence saying why the action itself was refused, so a model handed * `stale_frame` did the only thing it says to do — observe, re-pick the same * element, and collect the identical `dispatch_refused` it was never shown. * The executor's account leads, because it is the one that says what to do * differently; the frame's is the reason the retry has to be re-observed. */ function refusalAfterDispatch( reason: BindingFailureReason, result: CuRunResult | undefined, action: string, ): ComputerToolResult { if (!result || result.outcome.ok) return bindingFailure(reason, action); const executor = result.outcome; return { error: executor.error, text: `maka_computer.${action} failed: ${executor.error} — ${executor.message} ` + `The observation it quoted has also moved on: ${BINDING_FAILURE_RECOVERY[reason]}`, }; } /** * A refusal the executor states it never dispatched. * * `path` is what the executor did, not what it was asked to do, and `"none"` * is its word for "nothing reached the target" — `maka.cu/2` §6.5. It is * absent rather than defaulted when a backend does not say, so a backend that * forgets falls back to the cautious behaviour instead of claiming this one. * * Nothing about the window changed, so the frame the action was quoted * against is still a description of what is there. */ function dispatchedNothing(result: CuRunResult | undefined): boolean { return result?.outcome.ok === false && result.outcome.evidence?.path === 'none'; } /** Retire the action but keep the frame, for a refusal that never ran. */ function retireBoundAction( record: SessionObservationRecord, action: CuaBoundAction, ): BindingFailureReason | undefined { const retirement = record.state.retireAction(action); return retirement.ok ? undefined : retirement.reason; } /** * Turn the name a person used into the id the executor takes. * * Returns `undefined` when there is nothing to do — no name, a name that * could already be an id, or no way to look one up — and the caller passes * the original through. A lookup that fails is not an error here: the * executor has its own account of an app it cannot find, and that account is * better than one invented from an empty list. */ async function resolveAppName( app: string | undefined, signal: AbortSignal, ): Promise<{ app: string } | { ambiguous: string[] } | undefined> { // A dot is what a bundle id has and a display name does not. Anything that // could already be an id goes through untouched, so an executor that knows // ids this host has never seen keeps working. if (!app || app.includes('.') || !deps.backend.listApps) return undefined; const query = app.trim().toLowerCase(); if (query.length === 0) return undefined; let apps: Awaited>>; try { apps = await deps.backend.listApps(signal); } catch { return undefined; } const hits = apps.filter((candidate) => [candidate.appId, candidate.name].some( (name) => name && matchesAppQuery(name.toLowerCase(), query), ), ); if (hits.length === 0) return undefined; if (hits.length === 1) return { app: hits[0]!.appId }; // Two applications answering to one name is the model's to settle, not the // host's: picking one silently would drive the wrong window and report // success for it. const withWindows = hits.filter((candidate) => (candidate.windowCount ?? 0) > 0); if (withWindows.length === 1) return { app: withWindows[0]!.appId }; return { ambiguous: hits.slice(0, 8).map((candidate) => candidate.appId) }; } async function freshFullObservation( state: CuaSessionState, record: SessionObservationRecord, result: CuRunResult, signal: AbortSignal, context: CuRunContext, ): Promise { const observationLease = state.beforeObservation(); if (!observationLease.ok) return undefined; let captured = result.observation; if ( (!captured || (!captured.screenshot && !result.screenshot)) && deps.backend.captureObservation && record.appId && record.windowId ) { try { captured = await deps.backend.captureObservation( { app: record.appId, windowId: record.windowId, includeScreenshot: true, }, signal, context, ); } catch (error) { // A pictureless post-dispatch observation is still authoritative state // for the model. The mirror is best-effort, so failing its richer // recapture must not discard that state or turn a completed action into // an unknown outcome. if (!captured) throw error; } } const fresh = captured && result.screenshot && !captured.screenshot ? { ...captured, screenshot: result.screenshot } : captured; if (!fresh || !state.validateObservationLease(observationLease.lease).ok) { return undefined; } const registered = registerObservation(record, fresh); const snapshot = state.freshObservationSucceeded(); return snapshot.status === 'active' ? registered : undefined; } async function withInvocationQueue( sessionId: string, signal: AbortSignal, operation: () => Promise, ): Promise { const previous = invocationQueues.get(sessionId) ?? Promise.resolve(); let release!: () => void; const gate = new Promise((resolve) => { release = resolve; }); const current = previous.then(() => gate); invocationQueues.set(sessionId, current); await previous; try { if (signal.aborted) throw new Error('aborted'); return await operation(); } finally { release(); if (invocationQueues.get(sessionId) === current) { invocationQueues.delete(sessionId); } } } function presentationScreenPoint(boundAction: CuaBoundAction | undefined): CuPoint | undefined { // An element action is aimed at an element, not at a coordinate, so it // carries the point directly. Only a coordinate action has a screenshot // pixel to map back onto the screen. if (boundAction?.presentationScreenPoint) return boundAction.presentationScreenPoint; const source = boundAction?.sourceStartCoordinate ?? boundAction?.sourceCoordinate; const sourceBounds = boundAction?.target.sourceBoundsPx; const windowBounds = boundAction?.target.bounds; if (!source || !sourceBounds || !windowBounds) return undefined; if (sourceBounds.width <= 0 || sourceBounds.height <= 0) return undefined; return { x: windowBounds.x + (source.x / sourceBounds.width) * windowBounds.width, y: windowBounds.y + (source.y / sourceBounds.height) * windowBounds.height, }; } async function waitForPresentationReady( fence: CuPresentationFence | undefined, signal: AbortSignal, sessionId: string, ): Promise { if (!fence) return; if (signal.aborted) throw signal.reason ?? new Error('aborted'); await new Promise((resolve, reject) => { let settled = false; const finish = (error?: unknown) => { if (settled) return; settled = true; clearTimeout(timer); signal.removeEventListener('abort', onAbort); const waiters = presentationWaiters.get(sessionId); waiters?.delete(wake); if (waiters?.size === 0) presentationWaiters.delete(sessionId); if (error) reject(error); else resolve(); }; // The producer's own worst case wins when it has one. A backstop shorter // than the time the presentation needs is not a safety net, it is a // second, disagreeing opinion — and it resolves the action mid-motion, // which is the thing the ready gate exists to prevent. const timer = setTimeout( finish, Math.max(fence.readyTimeoutMs ?? 0, presentationReadyTimeoutMs), ); const onAbort = () => finish(signal.reason ?? new Error('aborted')); const wake = () => finish(); const waiters = presentationWaiters.get(sessionId) ?? new Set(); waiters.add(wake); presentationWaiters.set(sessionId, waiters); signal.addEventListener('abort', onAbort, { once: true }); fence.readyForInteraction.then( () => finish(), () => finish(), ); }); } async function waitForPresentationFinished( fence: CuPresentationFence | undefined, ): Promise { if (!fence) return; await new Promise((resolve) => { let settled = false; const finish = () => { if (settled) return; settled = true; clearTimeout(timer); resolve(); }; const timer = setTimeout(finish, presentationFinishedTimeoutMs); fence.finished.then(finish, finish); }); } async function runWithPresentation( action: CuAction, context: CuRunContext, signal: AbortSignal, dispatch: () => Promise, beforeDispatch?: () => ComputerToolResult | undefined, invocationGeneration = 0, ): Promise<{ result?: CuRunResult; blocked?: ComputerToolResult; finish(result?: CuRunResult): void; }> { let releasePresentation!: () => void; const previousPresentation = presentationQueue; const presentationGate = new Promise((resolve) => { releasePresentation = resolve; }); if (deps.overlay) { presentationQueue = previousPresentation.then(() => presentationGate); if ((presentationGenerations.get(context.sessionId) ?? 0) !== invocationGeneration) { releasePresentation(); return { blocked: sessionFailure('user_stopped'), finish: () => {}, }; } let queuedCancelled = false; await Promise.race([ previousPresentation, new Promise((resolve) => { const cancel = () => { queuedCancelled = true; resolve(); }; const waiters = presentationQueueWaiters.get(context.sessionId) ?? new Set(); waiters.add(cancel); presentationQueueWaiters.set(context.sessionId, waiters); if ((presentationGenerations.get(context.sessionId) ?? 0) !== invocationGeneration) { cancel(); } void previousPresentation.finally(() => { waiters.delete(cancel); if (waiters.size === 0) { presentationQueueWaiters.delete(context.sessionId); } }); }), ]); if ( queuedCancelled || (presentationGenerations.get(context.sessionId) ?? 0) !== invocationGeneration ) { releasePresentation(); return { blocked: sessionFailure('user_stopped'), finish: () => {}, }; } } const cursorPoint = context.boundAction ? presentationScreenPoint(context.boundAction) : undefined; // `requireTarget` uses { pid: -1, windowId: -1 } as its miss sentinel, and // -1 is not undefined — an unguarded field would hand `window:-1:0` to the // reorder and rely on it throwing. const targetWindowId = context.boundAction?.target.windowId; const overlayContext: CuOverlayHookContext = { sessionId: context.sessionId, toolCallId: context.toolCallId, ...(cursorPoint ? { presentationScreenPoint: cursorPoint } : {}), ...(Number.isInteger(targetWindowId) && (targetWindowId as number) > 0 ? { targetWindowId: targetWindowId as number } : {}), }; let fence: CuPresentationFence | undefined; try { fence = deps.overlay?.onActionBegin(action, overlayContext) ?? undefined; void fence?.finished.catch(() => {}); } catch { fence = undefined; } let finished = false; const finish = (result?: CuRunResult) => { if (finished) return; finished = true; let endPromise: Promise; try { endPromise = Promise.resolve( deps.overlay?.onActionEnd?.(action, result, overlayContext), ).then( () => undefined, () => undefined, ); } catch { // Presentation is best-effort and cannot change execution outcome. endPromise = Promise.resolve(); } void endPromise .then(() => waitForPresentationFinished(fence)) .finally(() => releasePresentation?.()); }; try { if (fence) { await waitForPresentationReady(fence, signal, context.sessionId); } const blocked = beforeDispatch?.(); if (blocked) { finish(); return { blocked, finish }; } const result = await dispatch(); return { result, finish }; } catch (error) { finish(); throw error; } } const tool: MakaTool = { name: 'maka_computer', displayName: 'Maka Computer', // The kind every other builtin declares, and the reason `'computer'` is on // the wire at all. Without it the renderer had to recognise this tool by // name, which is the recognition-by-string the kind exists to replace. activityKind: 'computer', description: 'Maka semantic computer harness. Use action=observe to read the current computer state before acting, then use the same function ' + 'for semantic element actions, exact Electron page actions, wait, screenshots, or another observation. Every successful mutating action returns a fresh screenshot when available ' + 'and controlled path/effect/verified evidence; inspect that new state before retrying or continuing. ' + // "The retained background mutation paths are native Accessibility element // actions and exact Electron page semantic actions" named two host // dispatch implementations. Neither is a thing the model selects, so // there was no behaviour it could change on reading it. What it can act // on is which action to reach for. 'Everything here runs without bringing the target application to the front. ' + 'Prefer click_element or set_value using an element_id from the immediately preceding observation. ' + 'An observation is a header line of observation_id/app/pid/window_id followed by one line per element, ' + 'indented to show containment: " \\"