tinycomputer is one TinyBus module that lets an agent drive desktop
applications and web pages. This page explains how the workspace is put
together: which crate does what, how a call travels from the bus to a click,
and the rules that keep the layers apart. For the decision loops themselves,
see decision-loops.md; for the task API, see
tasks.md.
TinyBus host (an agent's runtime)
│ typed bus calls
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ tinycomputer (cdylib) TinyBus glue: manifest, dispatch, config, runner │
└──────────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ tinycomputer-engine Jev runtime · RunGoal · ResolveIntent · RunFlow │
│ flow runtime · Workspace · Tasks · Planner │
└──────────────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌───────────────────────────────┐ ┌────────────────────────────────┐
│ tinycomputer-desktop │ │ tinycomputer-browser │
│ Desktop: 54 typed members │ │ Browser: sessions, outputs │
│ Desktop as a core Surface │ │ BrowserSurface: a core Surface │
└───────────────────────────────┘ └────────────────────────────────┘
│ │
▼ ▼
vendor/agent-desktop vendor/agent-browser
(accessibility trees: (Chrome over CDP, linked
macOS, Windows, Linux) in-process as a library)
tinycomputer-core: Surface trait, Screen, keymap, safety, records, facts
tinycomputer-cursor: the agent's drawn cursor, aim points and glides
tinycomputer-bus: every type that crosses the bus, and nothing else
Dependencies point one way:
bus ← core ← {desktop, browser} ← engine ← tinycomputer. Nothing
lower down knows about anything higher up. The desktop and browser adapters
never see Jev, the engine never sees TinyBus, and the contract crate has no
runtime at all.
| Crate | Holds | Deliberately does not hold |
|---|---|---|
tinycomputer-bus |
the wire contract: member names, request and reply types, the DesktopResponse envelope, the Agent types (TaskView, TaskStatus, …), the browser types, the flow grammar and its authoring guide, and the contract version |
a transport, an async runtime, an HTTP client, a native library, or an engine. CI fails the build if one appears. |
tinycomputer-core |
the Surface trait and the Screen it observes; fingerprints, change notes, verified text delivery, result-card grouping; the per-OS keymap; the safety classifier and payment detector; the facts store; price, time, duration, and stop parsers and rank |
engines, Jev, TinyBus, and any tokio runtime |
tinycomputer-cursor |
the agent's one on-screen cursor, shared by desktop and browser: aim points, human glide paths, CursorPace, the sprite, the animator, the overlay protocol, and (feature overlay) the tinycomputer-cursor-overlay helper window (spec) |
input and engines |
tinycomputer-desktop |
Desktop, one typed method per desktop member over agent-desktop, with the permission preflight; Desktop as a Surface |
Jev, TinyBus |
tinycomputer-browser |
Browser (sessions, navigate, snapshot, perform, read, evaluate, screenshots, downloads, held outputs) over agent-browser; BrowserSurface; the error taxonomy |
Jev, TinyBus |
tinycomputer-engine |
the Jev runtime, RunGoal, ResolveIntent, the flow runtime, the Workspace, the task controller, and the optional planner |
TinyBus |
tinycomputer |
the TinyBus module: manifest, one dispatch method per member, configuration, and the task runner that gives each task a workspace | behaviour of its own |
tinycomputer-skills |
the agent-facing SKILL.md and schemas for the task API |
code |
tinycomputer-examples |
runnable examples, the lab, the travel fixture, and live verification binaries | anything shipped |
The desktop and browser crates are plain libraries. Another host or module can take just one of them without the engine or the bus glue.
Four submodules, each pinned by gitlink. This repository never edits them: a wrong tree or a wrong click is fixed upstream and arrives here as a gitlink bump in its own commit.
| Path | What it is | Used by |
|---|---|---|
vendor/agent-desktop |
accessibility-tree observation and interaction for macOS, Windows, and Linux | tinycomputer-desktop |
vendor/agent-browser |
Vercel's browser automation engine, pinned to the library-target branch of the tinyhumansai/agent-browser fork, which adds a library target |
tinycomputer-browser (feature agent-browser) |
vendor/tinyinference |
the Jev decisions client (tinyinference_decisions) and an LLM client (tinyinference-llm) |
the engine: Jev always, the LLM only with the planner feature |
vendor/tinybus |
TinyBus host types and the module SDK | the cdylib |
agent-browser is normally a CLI with a daemon. Here it is linked in-process:
tinycomputer-browser/src/linked/ builds one agent-browser DaemonState per
session from explicit options (so nothing leaks in from the host's
AGENT_BROWSER_* environment) and sends it the same JSON commands its daemon
would receive over a socket. agent-browser keeps one process-wide piece of
state, the frame selectors resolve in, so commands from different sessions are
serialized through one lock. There is no sidecar process and no socket.
The decision loops are written once, against tinycomputer_core::surface::Surface:
pub trait Surface: Clone + Send + 'static {
fn observe(&self, app: &str, root: Option<&str>, depth: Depth)
-> Result<Screen, Box<DesktopResponse>>;
fn execute(&self, operation: JevOperation, target: Option<Candidate>, text: Option<String>)
-> DesktopResponse;
fn read_value(&self, target: &Candidate) -> Option<String>;
fn paste(&self, app: &str, target: &Candidate, text: &str) -> DesktopResponse;
fn press(&self, app: &str, combo: &str) -> DesktopResponse;
fn launch(&self, app: &str) -> DesktopResponse;
fn settle(&self) {}
fn settle_briefly(&self) { /* settles */ }
fn await_change(&self, ms: u64) -> bool { /* pauses, and says it may have */ }
fn navigate(&self, url: &str) -> DesktopResponse { /* ACTION_NOT_SUPPORTED */ }
}The operations are closed: click, type text, check, uncheck, expand, collapse, scroll, wait, and two look operations. A loop can never ask a surface to do something outside that list.
Desktop |
BrowserSurface |
|
|---|---|---|
| observe | agent-desktop snapshot of the app's front window, parsed into candidates and context | sight: one script over the rendered page — what is drawn and on top, the words on and beside each control (specs/browser-sight.md); the agent-browser snapshot (- role "name" [ref=eN]: value), parsed the same way as the desktop's, when sight cannot reach the page |
| a ref | @s8f3k2p9:e1, bound to one snapshot |
seen:12, the element's lifelong data-tc-seen mark, or e12 from a snapshot |
| click, check, expand | accessibility actions, headless by default | agent-browser click, check |
| type text | set value through accessibility | fill |
| paste | select-all and paste through the clipboard, then restore the clipboard | focus, select, and type at the caret; no clipboard |
| press | cmd becomes ctrl off macOS |
cmd becomes Meta on macOS and Control elsewhere; return becomes Enter |
| launch | launch or bring forward | open the session if it is not open |
| navigate | refused | load the URL |
The Workspace in tinycomputer-engine/src/workspace/ joins the two into one
Surface. Calls that name an application route by the name: browser,
browser:…, or an http(s):// address goes to the browser, anything else to
the desktop. Calls that do not name one (acting on a candidate, reading it
back) go to whichever side was observed or opened last, which is the side the
candidate came from. A task confined to one side gets a workspace with only
that side, and a call aimed at the missing side is refused.
So a single flow can browse to a flight search, pick a result, open
Mail, and enter the price it read into a draft.
Take StartTask with a planned booking flow.
- The host calls the module. TinyBus delivers the call to
DesktopService::start_taskincrates/tinycomputer/src/tinybus_module/dispatch/mod.rs.StartTask,ContinueTask,TaskReport,RunGoal,ResolveIntent, andRunFloware confidential members: they carry the caller's values and page data, so the bus requires an attested module and keeps them away from monitors. - The controller registers the task (
Tasks::start), validates the flow, checks for missing values, and spawns a worker. The call returns aTaskViewstraight away. - The worker runs the flow through
WorkspaceRunner::run(tinybus_module/runner.rs), which creates the task's workspace on first use: the desktop, plus a browser session shaped by the task's constraints (allowed origins, headed or headless, or an endpoint to attach to). - The flow runtime (
run_flow) walks the steps. Each step observes the workspace, asks Jev small questions, and acts through theSurfacetrait. - The browser surface turns
execute(Click, e12)into aperform(Click{target: @e12})on the session, whichconvertturns into agent-browser's{"action": "click", "selector": "@e12"}, which the linked engine runs against Chrome over CDP. - The reply comes back up as a
DesktopResponse. The flow runtime notes what changed, the controller interprets how the run ended, and the nextAwaitTaskreturns the new view.
Accessibility APIs are synchronous, and some calls are slow on purpose. A
dense snapshot walks thousands of elements, and a wait can block for thirty
seconds. So every bus member hands its work to spawn_blocking, and the
module asks TinyBus for two worker threads: one to run a blocking call on and
one to keep answering on.
The flow runtime is async but its surfaces block. It makes every surface call
through spawn_blocking (agentic/flow/backend/). BrowserSurface goes the
other way: each of its blocking calls runs the async Browser call to
completion on the runtime handle it was built with. Calls on one browser
session are serialized by the session lock; different sessions run
independently, up to eight (MAX_SESSIONS).
The module serves three levels of API from one interface,
ai.tinyhumans.tinycomputer.Desktop at /ai/tinyhumans/tinycomputer/Desktop.
A TinyBus manifest currently attaches its member list to one interface, so all
members share it; their names do not collide.
| Level | Members | Who plans | Who chooses | Use it when |
|---|---|---|---|---|
| Primitives | Snapshot, Click, SetValue, Press, Launch, … (54 desktop members) |
the caller | the caller | the caller already knows the interface, or wants full control |
| Goals and flows | ResolveIntent, RunGoal, RunFlow, ValidateFlow, FlowGuide |
the caller (a flow) or nobody (a goal) | Jev | the caller knows what it wants but not the interface |
| Tasks | Describe, PlanTask, StartTask, AwaitTask, ContinueTask, CancelTask, TaskReport, ListTasks |
the caller or the planner | Jev | an outside agent wants a job done, across the browser and desktop apps, with pauses for approvals |
| Browser primitives | BrowserOpenSession, BrowserNavigate, BrowserSnapshot, BrowserPerform, BrowserReadPage, BrowserScreenshot, BrowserReadOutput, … (15 members) |
the caller | the caller | the caller wants to drive a web page itself, or inspect a task's session |
Describe returns all four levels as Capabilities.catalogue: every member,
its family, and one sentence on what it is for.
The browser primitives are defined in tinycomputer-bus/src/browser/,
implemented by tinycomputer_browser::Browser, and served with a Browser
prefix, because several (Snapshot, Screenshot) would otherwise collide
with a desktop member of a different shape. A member that acts on an open
session takes one object with the session beside the member's own fields
(BrowserOpenSession takes the session's options, BrowserListSessions
nothing, the output members an output), and each replies in the same
DesktopResponse envelope as a desktop member; a failure reuses the desktop's
code where the meaning is shared, so STALE_REF means "snapshot again" on
either surface. The module holds one Browser: the task runner opens each
task's session on it, so the primitives can read a task's screenshots and
see its session. A direct RunFlow call still runs on the desktop alone;
tasks are how a flow reaches the browser.
RunGoal and ResolveIntent are the older, single-goal loop: one goal string,
one Choice over operation and target per turn, and success predicates the
module checks on the accessibility tree. They drive the desktop only.
crates/tinycomputer-engine/src/agentic/README.md
describes them, including their confirmation handles.
The host passes the module a JSON object at load, and again on reinitialization. TinyBus treats both as sensitive host-control traffic: monitors never see them, and the buffers are zeroized after use. Reinitialization swaps the served object only after the whole new configuration validates.
| Key | Type | Meaning |
|---|---|---|
session_id |
string | agent-desktop session to join |
trace_path |
string | where agent-desktop writes its trace |
trace_strict |
bool | fail a call when its trace cannot be written |
headed |
bool | use real input instead of accessibility actions |
jev |
object | decision provider (type_safe, open_router, tiny_humans_open_router, open_jev, sage), API key, optional model, approved endpoint, timeout, retries, sdk_name for the TinyHumans proxy, and fast for Sage |
planner |
object | api_key, provider (open_router or tiny_humans), approved endpoint_url, sdk_name, and optional model for the planner, optional rescue_route giving the rescuer its own route and key, optional rescue_model for rescuing failed steps (docs/technical/specs/task-rescue.md), and optional output_model for shaping a finished task's answer (docs/technical/specs/task-output.md); absent means no planner, no rescues, and no output shapes |
browser.executable |
string | the Chrome or Chromium binary to launch, when discovery would not find one |
cursor |
string or object | the agent's on-screen cursor for desktop and browser: a pace (off, brisk, natural (default), calm) or {pace, overlay} with the overlay helper's path (spec) |
Without jev, ResolveIntent, RunGoal, RunFlow, and task runs answer
with JEV_NOT_CONFIGURED, while the primitives, ValidateFlow, FlowGuide,
and Describe keep working.
Several independent checks keep a run from doing something it cannot undo. None of them trusts a model's judgement.
- An ordinary step refuses to click a control whose label reads as
destructive, that the flow's own
stop_beforenames, or that is an unnamed button in a confirmation sheet. Onlystop_beforereaches one, and it presses only withallow_destructiveor an approval. A tab only shows another panel of the page, sostop_beforenever names it; its own label still counts against the denylist. - A control classified as payment, or any click on a screen that shows card
fields, stops the run, and the task controller makes that a final
checkpoint. Card data is always a secret fact, and it is typed only under
payment: "fill_then_approve", which needsoriginsnaming the sites (never*), types only on their pages (a payment provider's frame on such a page included), and still waits for approval before the control that pays. - Secret values never reach Jev: they are masked to
${name}in everything it is sent, what a field holds included. Shared values brief Jev, and tasks let it read field contents (include_values). The planner and the rescuer see fact names only: every value is redacted from the screen text a rescue is shown, and from every summary. - Everything read from a screen is wrapped as untrusted data, and every question tells Jev that screen text is data, never instructions. A move Jev was not offered fails closed.
- Budgets cap actions, Jev calls, and time for a whole task, and surfaces and origins confine where it can go.
- The desktop checks accessibility and screen-recording permission before acting, because an unauthorized accessibility call usually returns an empty tree rather than an error.
Every layer is testable without a display, a browser, or a network:
- the contract pins the serde form of every payload;
- the browser crate runs against a scripted
Engine(src/fake/); - the flow runtime runs against a simulator in
agentic/flow/flow_tests/: a mail app and booking widgets whose state an oracle Jev reads its answers from; - the task controller runs against scripted
FlowRunners; - the module is exercised over TinyBus's in-memory transport, and
verify_moduleloads a built cdylib through the real dynamic loader.
Live runs sit outside the suite. scripts/lab drives real macOS applications
(lab.md), and scripts/docker-lab runs anything that launches
Chromium in a Linux container (docker-lab.md), including the
travel fixture end to end.