Hyperbolic Gateway

Kuang Grade Mark Eleven: Orthogonal Primitives & Workflow Recipes

This partition documents the primitive layer: a small set of orthogonal verbs (observe Β· act Β· wait Β· expect Β· find Β· extract Β· collect Β· state Β· network Β· context) that the older flat tool list is being consolidated onto. Prefer these for any non-trivial page: they remove blind sleep, remove guessy selectors, and return compact deltas instead of dumping the DOM.

All primitives run on the TIER-1 humanlike path (real input events; no page DOM mutation beyond Mark Eleven's own data-mark-id stamp), so they do not trip isTrusted: false bot sensors.

1. The Primitive Map

Tier map (mirrors the [TIER n] prefixes on the advertised descriptions): TIER 1 Β· prefer β€” this whole map (observe, act, wait_for, expect, find, extract, collect_all, page_state, diff); TIER 2 Β· fallback β€” take_screenshot, snapshot_rendered_page, press_key; TIER 3 Β· use deliberately β€” evaluate_javascript (powerful, just not the default). Every other tool is unprefixed (the only tool for the job).

Primitive Job Returns Replaces
observe(as, region, budget, cursor, include_offscreen) One observation model with projections {text, total, offset, next_cursor, truncated} or structured rows get_page_markdown + snapshot_rendered_page + list_marks
act(mark_id | selector | x+y, action, text, secret, clear_first, await_conditions, timeout_ms, dry_run) Unified humanlike action on a mark ref, CSS selector, or coordinates (+ optional post-conditions). clear_first replaces a pre-filled value instead of appending Delta report click_mark / type_mark / click_element / type_text / hover + a following wait_for
wait_for(conditions[], require, timeout_ms, poll_ms) Declarative readiness gate {ok, waitedMs, attempts, results[]} every sleep / "did it load?" guess
expect(conditions[]) Immediate assertion (no polling) {ok, results[]} with per-condition evidence agent-side guessing before proceeding
find(role, name, contains, regex, tag, selector, limit) Semantic element search β†’ refs rows carrying mark ids hand-written CSS/XPath
extract(kind, ref, selector, limit) Structured read: list | table | tables | app_data | schema_org (type filters JSON-LD by @type) {rows} / {headers, rows, records} / embedded JSON / typed JSON-LD items regex over HTML
collect_all(selector, ref, scroll, max_scrolls, max) Scroll a lazy/virtualized list to the end, dedupe rows rows + scroll diagnostics a manual scroll-and-harvest sweep
llm_surface(url) / llm_fetch(url, as) / read_url(url, max_chars?, cursor?) Read a URL as Markdown β€” the site's own (/llms.txt, <link rel=alternate> or a real <a href="….md"> link, or negotiation), else a detached render. read_url is the one-call composite; llm_surface.recommended is null when there is nothing fetchable (follow next) surface report / {as, url, scope, scope_mismatch, contentType, bytes, content, via} / {via, as, scope, total, offset, next_cursor, truncated, content, reason?} rendering a docs page just to read its text
search_web(query, engine?, engines?, url_template?, max_results?) Multi-engine web search in a detached view with automatic fallback ranked {rank, title, url, snippet} manual SERP navigation
deep_crawl(url, max_depth?, max_pages?, match_patterns?, …) Autonomous BFS crawl; distill + index each page to Cortex (cheap llm path first) per-page summary + cortexIndexed turn-by-turn navigate-and-harvest
research_topic(query, max_sources?, …) Search β†’ gather top sources β†’ distill β†’ index; extractive cited highlights sources + highlights + cortexIndexed manual search-and-read across tabs
page_state() / diff(since) Cheap monotonic change signature state / field-by-field diff re-reading the whole page
bridge_health() Liveness + capability + revision + input transport {version, revision, capabilities[], input, window} assuming the bridge is up
export_har(pattern, limit, save) HAR 1.2 export of captured traffic HAR JSON or file path ad-hoc curl replay

Observation projections (observe.as)

markdown (TIER-1 harvest with inline [#id] marks) Β· text (innerText, region-scoped) Β· ax (accessibility tree) Β· som (numbered marks table) Β· list Β· table Β· app_data (embedded __NEXT_DATA__ / JSON-LD / inline state blobs).

components is the decision-ready projection: one line per component with its role, name, value, state, region (nearest landmark) and the actions it accepts β€” {click}, {type}, {select}, empty when disabled β€” grouped under the landmark it belongs to. Ids are mark ids, so a decision is executed with act(mark_id=…) and verified like any other action. A target list for click therefore only ever contains clickable components.

markdown reads the composed DOM β€” open shadow roots included, with live form values baked in and password values reduced to a length β€” so a web-component form reads as a form, not as empty host tags. Its envelope states how complete the read is: reader (which extraction path produced the body), extracted_chars / page_chars (characters of page text, so the ratio means something) and shadow_roots. A partly-visible page says so and points at text or ax; a page with no text says why.

Use budget + the returned next_cursor to page a very large page instead of truncating it β€” never let a single read exceed the contraction cap. markdown returns a 25,000-character default view (budget: 0 for the whole construct); text/ax return everything unless budgeted. Paging is exact because the construct carries no timestamp β€” two reads of an unchanged page are byte-identical.

2. Refs = Marks

There is no second element-addressing scheme to learn: a ref is a mark id ([#7] in observe as=markdown, the number in observe(as:"som"), the mark field from find/extract). Marks remain the on-the-wire format, so act(mark_id=…) / find β†’ act are drop-in. When you have no mark (e.g. an off-list pseudo-element), the same act accepts selector="…" or x=/y= coordinates β€” you never need a separate click_* tool. act re-resolves the element's live centre from the DOM each time, so a mark stays usable across layout shifts. Mark ids are re-assigned on every scan β€” re-scan after the page changes.

3. Wait Condition Vocabulary

wait_for and the implicit act(await_conditions=[...]) share one vocabulary:

Condition Meaning
{"type":"selector","selector":".card","visible":true} element present (and visible)
{"type":"gone","selector":".spinner"} element removed
{"type":"text","text":"Results"} or {"regex":"Β£\\d+"} body text contains (regex)
{"type":"url","pattern":"/flights/"} URL matches
{"type":"role_name","role":"button","name":"Search"} accessible role + name present
{"type":"count","selector":".card","min":5,"equals":10,"max":12} element count bound
{"type":"network_idle","quietMs":800} no in-flight requests for a quiet window
{"type":"stable","target":"marks|height|text|nodes","holdMs":400} lazily-rendered content stopped growing
{"type":"revision","version":N} page revision reached (see Β§5)

require:"any" (default) or "all". Warning: skeleton placeholders can satisfy a naive selector/count early (e.g. Skyscanner stamps [data-testid="ticket"] on skeletons). Use stable (or count+min) β€” a single-poll wait_for is a common false positive.

4. Structured Errors

New endpoints return a stable machine code (HTTP 400) alongside the message:

Code Meaning Recovery
STALE_REF the mark/ref no longer resolves re-scan (observe) and re-issue
TIMEOUT wait_for exhausted its budget widen timeout_ms, loosen conditions, check the diagnostics
NO_TARGET nothing matched the selector/ref verify with find, or observe(region=…)
BAD_REQUEST malformed arguments fix the call
NOT_SUPPORTED capability not negotiated check bridge_health().capabilities

5. Workflow Recipes

R1 β€” Harvest a lazy / infinite list (the one that used to stall)

collect_all(selector=".result", scroll="container"|"window", max_scrolls=25, max=500)

collect_all auto-detects the repeating item container, scrolls until three consecutive scrolls add nothing, and dedupes by text+href. It returns scroll diagnostics (scrollTop / scrollHeight / clientHeight / visibility) so a stall explains itself. For whole-page infinite scroll pass scroll:"window". Sanity-check with wait_for([{"type":"stable","target":"marks","holdMs":400}]) first.

R2 β€” Act and wait in one round trip

act(mark_id=12, action="click",
    await_conditions=[{"type":"url","pattern":"/results"}],
    timeout_ms=15000)

Returns what changed (urlChanged, marks/revision before/after) so you usually need no follow-up read. Add dry_run:true to resolve the target without acting, and secret:true when typing credentials so the value is [redacted] in every log.

R3 β€” Authenticated context switch (profiles)

context_save(name="airline")     # snapshot cookies + storage now
context_open(name="airline")     # later: restore it (also on another tab)
context_list() / context_delete(name)

context_* are the profile verbs; the legacy save_session / restore_session are hidden aliases.

R4 β€” Change-detection loop (no full re-reads)

s = page_state()                              # {revision, url, marks, nodeCount, height, scrollY, textLen, textHash}
... do something ...
diff(since = s.revision)                      # field-by-field {from,to} for exactly what moved

R5 β€” Action-correlated network capture

get_network_traffic(since="last_action")      # only requests fired by the last mutating action
export_har(pattern="graphql", save=true)      # HAR 1.2 β†’ data/har/har_<ts>.har

since accepts an epoch-ms timestamp, a revision, or 'last_action'. get_network_traffic also accepts revision: for revision-correlated windows.

R6 β€” Type into a pre-filled field or autocomplete (combobox)

Typing appends by default. clear_first does a trusted Ctrl+A + Backspace first, so the value is replaced β€” no synthetic el.value write, so React/Vue controlled inputs still update.

act(mark_id = origin, action = "type", text = "London", clear_first = true)

Autocompletes (React selects that only commit on option choice):

act(mark_id = origin, action = "type", text = "Lon", clear_first = true)
wait_for(conditions = [{type: "selector", selector = "[role=option]"}, {type: "stable", target = "text"}])
observe(as = "som", region = "<the list>")        # read the option marks
act(mark_id = <option mark>, action = "click")    # COMMIT by clicking the option β€” free text is often ignored

If a click appears to do nothing, do not just re-click it. Cheap checks first:

diff(since = <revision>)      # did anything move at all?
observe(as = "markdown")      # read the form/validation state β€” an invalid field blocks submit
expect(conditions = [{type: "selector", selector = "#result"}])

A form silently rejected by validation (e.g. a corrupted combobox value) looks exactly like a dropped click, and re-clicking it forever never helps. Note the transport is usually CDP (focus-independent), so a "lost focus" window is rarely the real cause β€” check bridge_health().input.transport before blaming focus.

R7 β€” Web components: the host is not the control

A custom element (<faceplate-text-input>, <my-text-field>) is a wrapper; the <input> that takes focus and text lives in its shadow root. Selectors now resolve to the control a mouse would hit, including through open shadow roots, so:

act(selector = "#email", action = "type", text = "a@b.com", clear_first = true)
act(selector = "my-text-input", action = "type", text = "a@b.com")   # resolves to the inner input

Both address the real input. Watch the returned target.field.tag β€” if it is a hyphenated tag instead of input, the selector landed on a host with no control inside (wrong element, or a closed shadow root) and you should switch to coordinates from a screenshot. Marks work here too: Set-of-Marks, find and act(mark_id=…) all traverse open shadow roots, and observe(as:"markdown") badges shadow controls with [#id] like any other element.

R8 β€” Read it back; a success string is not evidence

action="type" is now verified: the control's value is read before and after, so a no-op comes back as ok:false with error.code = "TYPE_NO_EFFECT". The typing evidence tells you which failure you have:

R9 β€” the decision loop (optional; only when decide is configured)

A decision model picks which component; Kuang executes and proves it. One round trip per step:

1  observe(as = "components")                      # role, name, value, state, region, actions; ids are mark ids
2  decide(state = <goal + that table>, questions)   # ONE call: operation + targets + is_done, in parallel
3  act(mark_id = <chosen id>, action = <operation>) # verified: box, focus, typing.landed
4  check the delta / typing verdict, then repeat from 1

Question set (keep every head narrow β€” a target list for click must only contain clickable components):

{ "operation":   { "type": "choice", "instructions": "What is the next operation?",
                   "criteria": { "click": "Actuate a control…", "type_text": "Type into a field…",
                                 "done": "The goal is already satisfied…", "blocked": "A CAPTCHA, consent or login wall…" } },
  "click_target": { "type": "choice", "instructions": "Which component to click?",
                    "criteria": { "9": "Sign in button", "12": "Reject optional cookies" } },
  "is_done":      { "type": "noul", "instructions": "Does the visible state satisfy the goal?" } }

Three rules that matter more than the plumbing:

  1. choice takes criteria, not options. TypeSafe reports a wrong shape as 422 "Upstream request failed: Endpoint is unavailable." β€” which looks like an auth failure. It isn't.
  2. Every option needs a real description. Bare option keys cost accuracy: a local model went 5/10 β†’ 7/10 with descriptions, and a blocked option described as "a wall" stopped being answered as click.
  3. Execute the head that matches the chosen operation, and only when it exists. done/blocked need no target. Never turn an answer into a selector, coordinate or script β€” it is a component id, nothing more.

decide is absent unless a backend is configured. GET /decisions returns the last 50 calls (backend, latency, questions, answers, or the typed failure) so a loop can be inspected after the fact.

Never trust an answer blindly β€” guard it, then eliminate.

decide(state, questions, guard = {threshold: 0.55, components}, eliminate = {max_attempts: 3})
  accepted:true   β†’ act(mark_id = target) β†’ verify (delta / typing.landed)
  accepted:false  β†’ exhausted:true, fallback:"agent", reasons[] β†’ YOU decide; no action was taken

The guard rejects: confidence below the threshold, a target that was not in the offered list, a target whose actions do not include the operation, and a done answer that contradicts the operation. Each rejection removes that option and re-asks, so the model cannot repeat the same mistake; when the options run out the call returns fallback:"agent". The ladder is model β†’ eliminate + retry β†’ agent. If an act afterwards fails (TYPE_NO_EFFECT, no delta), treat the chosen step as wrong: re-observe, pick the next candidate, and hand over to a human if the page is a wall.

Who writes the typed text? Not Jev β€” it only decides. The agent supplies the string (act(mark_id, action:"type", text:"…")), because Kuang ships no generation model. jev-ultrafast embeds a small LLM for this; ours is the calling agent.

Human-in-the-loop is a question, not an action. Ask the model whether a human is required, and let the runner act on the probability:

{ "handoff":     { "type": "noul",   "instructions": "Does this require a HUMAN β€” CAPTCHA, 2FA/one-time code, payment, identity check?" },
  "handoff_for": { "type": "choice", "instructions": "What should the human do?",
                   "criteria": { "captcha": "Solve the CAPTCHA", "otp": "Enter the emailed code",
                                 "consent": "Decide the consent prompt", "payment": "Complete payment" } } }
handoff.noul >= 0.6 β†’ request_human_intervention(reason = handoff_for.choice)

The model never calls HIL itself, and the mechanical signals (TYPE_NO_EFFECT after the input ladder, blocked, an unchanged delta) stay valid reasons to hand over even when the model disagrees.

Sensitive gates outrank the model. Before any model call, Kuang scans the state β€” and, for guarded calls, the live page text (include_page_text, on by default with guard/eliminate) β€” for age-verification copy ("verify your age/birthday", "birthday to continue", "18 or older", "adult content", "mature content") and for CAPTCHA / "verify you are human" copy. If found, decide returns HTTP 409 HUMAN_REQUIRED (with gate and the matched lines), spends no model call, does not eliminate or retry, and raises the HIL request itself, so the human is actually asked rather than merely advised. A Date of birth field in an ordinary signup form is not a gate and does not trigger it.

If it cannot be determined, the agent decides. A model answer of blocked with no matching gate copy is a normal decision (with a hint): the agent may call request_human_intervention itself, or choose another step. Human attention is spent only on a boundary Kuang can name and quote.

Seeing the page. decide(include_screenshot: true) attaches a viewport JPEG as a TypeSafe image asset(resized into its 280–1568 px limits). Caveat: the OpenCode gateway strips the TypeSafe-Danger-Zone-Images header, so images only work when the endpoint is api.typesafe.ai; otherwise Kuang retries without the image and reports that it did. act also returns a warning when a click target sits behind a gate β€” a warning, not a refusal, because a human may have asked for that click.

Driving a UI. drive({goal, values}) runs observe β†’ decide β†’ act in code, one decision call per step, and stops with a typed status: done, blocked, human_required, needs_agent, failed, max_steps. needs_input asks you for a field's text (Jev never writes it) β€” supply it in values and call again. dry_run plans the first step. The same loop as a runner script: scripts/drive-example.mjs. The guarded-call page scan is why this works on /r/petite: the component table only shows "Not Now" / "Verify", while the page says "Verify your birthday to continue".

6. Input Delivery & Background Rendering (operational guarantees)

7. Discovery

Call bridge_health() once per session: {status, app, version, revision, capabilities[], window:{visible,focused}, input:{transport}, lastActionAt}. capabilities advertises which primitives this build negotiated (observe:v2, wait_for, extract:list,table,app_data, collect_all, find, expect, act, page_state, diff, network_since, export_har, contexts, marks_offscreen, llm_surface, llm_fetch, read_url).

8. Known Limits