C+
Packages · View as Markdown

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 a Surface — the controllable model an agent sees. The walk is a DFS over GetWindow(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 with set_agent_id. Untagged windows are still walked for tree completeness but are NotExposed, so actions on them are refused.
  • Authorized actionsclick, set_text, and focus run through the agent_core authorization brain. Text edits use optimistic-concurrency versioning, so a stale edit is rejected with VersionConflict.
  • Eventsemit translates a fired control into an agent_core verb and offers it to a Subscriber.
  • No GUI-toolkit dependency. Introspection is plain user32 extern fn calls (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).

  • clickauthorize_action, then SendMessage(BM_CLICK).
  • set_textauthorize_text_write with the version the agent last read, then SetWindowTextA and a version bump. Pass text_version(id) as the base version; a racing edit yields VersionConflict.
  • focusauthorize_read, then SetFocus. 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): EditInput, StaticText, ListBox / ComboBoxList, 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 in Cplus.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 SendMessage is 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.