# KORMo-Web-4B Harness Specification Running a GUI agent requires more than the chat template. This file covers the parts the template does not: what goes into each message, how the screenshot and history are managed, how actions are parsed and executed, and how feedback is written back to the model. **Match this spec as closely as you can.** The model was trained on this exact layout, and deviating from it (for example by sending several past screenshots, dropping the tab list, or using pixel coordinates) measurably hurts it. A runnable reference implementation is in [`examples/agent_playwright.py`](examples/agent_playwright.py). --- ## 1. What you need | Piece | File | Role | |---|---|---| | Weights + tokenizer/processor | `model.safetensors`, `tokenizer*.json`, `processor_config.json` | Qwen3.5 VL architecture | | Chat template | [`chat_template.jinja`](chat_template.jinja) | Renders tools, ``, XML tool calls, `` | | System prompt | [`system_prompt.md`](system_prompt.md) | Sent verbatim as the `system` message | | Tool schema | [`tools.json`](tools.json) | The 22 tools, passed as `tools=` | | Sampling | [`generation_config.json`](generation_config.json) | `temperature 0.6 · top_p 0.95 · top_k 20` | | Harness | **this file** | Message layout, parsing, execution, feedback | --- ## 2. Conversation layout ``` system system_prompt.md (tools are injected by the chat template) user {task} … ← screenshot only if this is the current step assistant …\n\n… tool {feedback}\n… assistant … tool {feedback}\n … screenshot: ← newest step only ``` - **Full history is kept.** Every previous assistant turn stays in the context with its full `` and ``, and so does every tool feedback and observation. Nothing is truncated; the 50-step episodes in our evaluation stayed inside a 131k-token context. - **Only one screenshot is sent:** the newest one. When a new observation is appended, the screenshot is removed from the previous observation, but its text is kept. - **Tool results use `role: "tool"`.** The chat template wraps consecutive tool messages into a single `user` turn as `…`. - **Thinking is on.** Pass `chat_template_kwargs={"enable_thinking": true}`; the generation prompt then opens with `\n`. ### 2.1 Observation block ``` screen size: 1280 x 720 Current tab: 0 Available tabs: - Tab 0 (active): https://www.google.com/ - Tab 1: https://chatgpt.com/ url: https://www.google.com/ screenshot: ``` - `screen size` is the viewport size in pixels, which is also the screenshot size. - List every open tab with its 0-based index, and mark the active tab with `(active)`. - Cut URLs longer than 300 characters to their first 300 characters followed by `…`. Non-ASCII characters, such as Korean, are shown decoded rather than percent-encoded. - For past observations, drop the `screenshot:` line and the image. The block then ends right after the `url:` line. ### 2.2 First user message ``` {task text} … ``` --- ## 3. Model output and parsing Each assistant turn contains an optional reasoning block followed by **exactly one** tool call in Qwen XML form: ``` I can see the search box at the top of the page. I have not searched yet … My plan: 1. … 2. … 498 101 ``` Parsing rules: 1. Remove everything up to and including ``. 2. The remaining text must contain exactly one `…`. 3. Read `` and each `\nVALUE\n`. 4. If the schema types a parameter as `string` (for example `text`, `answer`, `url`, `path`), keep its value as raw text. Decode every other value with `json.loads` (numbers, the `keys` array, `index`). 5. Check the parsed call against `tools.json` (required parameters, no unknown names). If parsing fails, do not act; reply with the failure as feedback (see §5). Do **not** enable vLLM's server-side tool parser unless it supports this XML format. Parsing the raw text yourself is the simplest option. --- ## 4. Action space (22 tools) **Coordinates `x, y, x2, y2` are normalized to 0–1000** relative to the screenshot. Convert them to pixels with `px = x / 1000 × width` and `py = y / 1000 × height`. **Scroll deltas are signed pixels** and are not normalized. | Tool | Arguments | Execute as | |---|---|---| | `click` / `double_click` / `right_click` | `x, y` | mouse click at the point | | `hover` | `x, y` | move the mouse without clicking | | `focus` | `x, y` | focus the element at the point | | `drag` | `x, y, x2, y2` | press, move, release | | `type` | `text` | type into the **currently focused** input; never clears it first | | `clear` | – | clear the **currently focused** input (Ctrl+A, Backspace) | | `hotkey` | `keys: [..]` | press the keys together, modifiers first, e.g. `["Control","a"]` | | `scroll` | `scroll_dy`, `scroll_dx?` | mouse wheel by those pixels (positive = down / right) | | `wait` | `seconds` | sleep | | `goto` | `url` | navigate the active tab | | `go_back` / `go_forward` | – | browser history | | `new_tab` / `switch_tab(index)` / `close_tab` | – / `index` / – | tab management | | `upload` | `path` | set the file on the open file picker | | `finished` | `answer?` | **terminal** – the task is done; `answer` is the final answer | | `fail` | – | **terminal** – the task cannot be done | | `EXIT` | – | **terminal** – explicit stop | | `unknown` | – | no-op; reply `No action executed.` | Two execution details from training: - If an action opens a new tab, for example a `target=_blank` link, **switch to the new tab automatically**. - `type` and `clear` work only on the focused element. The model learned to click or focus the input first. --- ## 5. Feedback strings The text before `` in each tool message tells the model what happened. These are the patterns used in training; staying close to them helps. ``` Succeed: `click` on type=text "Search" at (498, 101) executed. Note: no visible navigation or new tab detected. Succeed: `click` on

"Product title …" at (294, 314) executed. Page navigated to https://example.com/item/1. Succeed: `click` on "Menu" at (139, 342) executed. New tab opened (tab 1). Succeed: `type` typed "KAIST graduate credit exchange". Succeed: `hotkey` ['Enter'] executed. Page navigated to https://… Succeed: `scroll` by (dx=0, dy=504) executed. Succeed: waited 10.0 seconds. Succeed: `goto` navigated to https://chatgpt.com/. Succeed: `new_tab` opened blank tab (tab 1). Succeed: `switch_tab` switched to tab 0 (https://…). Failed: `goto` execution failed: Page.goto: Timeout 60000ms exceeded. ``` - Coordinates in feedback are the model's **normalized** values, not pixels. - The element description (` type=… role=… "text"`) is optional, but it helps. - On failure, use `Failed: \`{tool}\` execution failed: {reason}`. --- ## 6. Recommended settings | Setting | Value | |---|---| | Sampling | `temperature 0.6`, `top_p 0.95`, `top_k 20`, `repetition_penalty 1.0`, `presence_penalty 0` | | `max_tokens` per step | 16384 (thinking on) | | Context (`--max-model-len`) | 131072 | | Viewport | 1280 × 720 (77% of training trajectories; the rest were 1280×1000, 1366×768, 1440×900 and 1536×864) | | Images per request | 1 (`--limit-mm-per-prompt '{"image":1,"video":0}'`) | | Max steps | 50 | All scores in the README were measured with these settings.