agent_win32
The Windows backend for the agent surface: it binds the
framework-neutral agent_core identity and
authorization model to a live Win32 window. It is the sibling of
agent_appkit, and reuses agent_core unchanged —
only this thin bridge is Windows-specific.
open(window)walks the live HWND hierarchy into aSurface— the controllable model an agent sees. The walk is a DFS overGetWindow(GW_CHILD / GW_HWNDNEXT).describe()returns a live snapshot of the nodes (Vec[UiNode]). A node is exposed — part of the curated surface — by giving it an agent id withset_agent_id. Untagged windows are still walked for tree completeness but areNotExposed, so actions on them are refused.- Authorized actions —
click,set_text, andfocusrun through theagent_coreauthorization brain. Text edits use optimistic-concurrency versioning, so a stale edit is rejected withVersionConflict. - Events —
emittranslates a fired control into anagent_coreverb and offers it to aSubscriber. - No GUI-toolkit dependency. Introspection is plain
user32extern fncalls (GetWindow/GetClassNameA/GetWindowTextA/ …), so it can describe any HWND tree, including one built with raw Win32 or any toolkit.
Curate, snapshot, act
Tag the controls the agent may see and touch, snapshot the window, then act.
The agent id is a stable NUL-terminated string literal — only its address is
held (via SetPropA), so pass a literal with static lifetime.
import "agent_win32/agent_win32" as agent;
import "agent_core/surface" as surface;
// 1. Curate: tag the controls the agent may see / act on.
agent::set_agent_id(button_hwnd, #str_ptr("btn_login\0"));
agent::set_agent_id(field_hwnd, #str_ptr("user_field\0"));
// 2. Snapshot the window (the READ path).
let surf: agent::Surface = agent::open(window_hwnd);
let nodes: vec::Vec[agent::UiNode] = surf.describe();
// each UiNode = { id, role, class_name, frame, is_hidden, text,
// actionable, parent }
// 3. Act (the WRITE path) — each call is authorized by agent_core first.
let _ = surf.click("btn_login"); // -> surface::Outcome
let v = surf.text_version("user_field");
let _ = surf.set_text("user_field", "alice", v); // optimistic concurrency
describe() reads each node's frame, hidden state, and caption now, so the
snapshot reflects the result of a preceding set_text. parent indexes back
into the returned Vec[UiNode] (None for the window root) so the flat list
reconstructs the tree.
The write path
Every mutation resolves the agent id to a node and asks agent_core first; the
real I/O runs only on Allowed. The result is a surface::Outcome
(Allowed / NotFound / NotExposed / NotActionable / VersionConflict).
click—authorize_action, thenSendMessage(BM_CLICK).set_text—authorize_text_writewith the version the agent last read, thenSetWindowTextAand a version bump. Passtext_version(id)as the base version; a racing edit yieldsVersionConflict.focus—authorize_read, thenSetFocus. This is the Win32 analogue of scroll-to-visible: focusing a control scrolls it into view in a scrollable parent.
let v: u64 = surf.text_version("user_field");
match surf.set_text("user_field", "alice", v) {
surface::Outcome::Allowed => { /* applied */ }
surface::Outcome::VersionConflict => { /* re-read and retry */ }
_ => { /* refused */ }
}
set_text mutates the version state, so its receiver is ref this; call it on
a var surface.
Role classification
The walk maps each window to the curated agent_core::Role by class name (plus
style bits): Edit → Input, Static → Text, ListBox / ComboBox →
List, and the single Button class splits by the low nibble of the window
style into push/checkbox/radio (Button) versus group boxes (Group). The
window root itself is role Window.
Platform notes
- Windows only. The package links
user32(declared inCplus.toml), which ships with Windows and is on the linker's default search path. See targets for cross-compilation. - No main-thread marshaling helper. Unlike the AppKit backend, Win32 needs
none: a cross-thread
SendMessageis delivered on the window's owning thread by the OS, so the gated actions are direct sends. - Flatter tree. Win32 controls are direct children of the window, so the child-window depth tracked by the DFS is shallow in practice.
Serving the surface
mcp_backend() returns a backend-neutral agent_core::backend::Backend vtable
(describe / click / set_text / navigate, where navigate is focus),
widening the integer Win32 rect to the f64 Rect the neutral node list uses.
Pair it with agent_mcp to expose the surface to an
external agent. To describe a C+-built GUI, see facet and
builder blocks.