From f90975f00640b90fa04bee5538e51f532d2e1ccc Mon Sep 17 00:00:00 2001 From: SirKentut <81878031+SirKentut@users.noreply.github.com> Date: Sun, 14 Jun 2026 07:49:00 -0700 Subject: [PATCH] feat(webapp-template): ship agent bridge (window.OPENSWARM_APP) as a hook, not a doc Add agentBridge.ts and import it FIRST in index.tsx so the bridge is installed on the window before any app code runs. The visitor / door / doorbell analogy: - The visitor is the agent. It shows up wanting to operate the app: read the rules, see the controls, take an action, check what happened. - The door is window.OPENSWARM_APP. A single, always-present surface the visitor can knock on: describe() to learn the app, getState() to read a snapshot, invoke(name, args) to act. - The doorbell is register({ rules, controls, getState, invoke }). The app presses it on mount to say "I'm home, here's how to talk to me." Until it does, describe()/getState() answer { __ready: false } so the visitor knows the app is still booting and waits, instead of concluding nobody lives here. Why a hook and not just an .md: A markdown instruction ("please expose your controls on window.X") is a request the app can forget, half-implement, or drift from, and nothing fails when it does. The bridge ships WITH the template and is imported before app code, so the door exists from first paint whether or not the author thought about agents. The app's only job is to press the doorbell once; it never wires the plumbing, so it cannot get the plumbing wrong. The contract is executable, not aspirational, which is the difference between every generated app being agent-operable by default and hoping each one remembered to be. --- .../frontend/src/agentBridge.ts | 99 +++++++++++++++++++ .../webapp_template/frontend/src/index.tsx | 4 + 2 files changed, 103 insertions(+) create mode 100644 backend/apps/outputs/webapp_template/frontend/src/agentBridge.ts diff --git a/backend/apps/outputs/webapp_template/frontend/src/agentBridge.ts b/backend/apps/outputs/webapp_template/frontend/src/agentBridge.ts new file mode 100644 index 00000000..4d29ad05 --- /dev/null +++ b/backend/apps/outputs/webapp_template/frontend/src/agentBridge.ts @@ -0,0 +1,99 @@ +// window.OPENSWARM_APP - the agent bridge, shipped with the template so it +// EXISTS from first paint, before any app-specific code runs (index.tsx imports +// this first). An app makes itself agent-operable by calling +// window.OPENSWARM_APP.register({ rules, controls, getState, invoke }) on mount; +// it never has to wire up the plumbing, so it cannot forget it. Until the app +// registers, describe()/getState() report { __ready: false } so the agent knows +// the app is still booting (and waits) instead of declaring it bridge-less. + +export type AgentControl = { + name: string; + args?: Record; + description?: string; + keys?: string; // optional key hint, e.g. "Space = flap", "WASD to move" +}; + +export type AgentRegistration = { + rules?: string; // what the app is and its objective, plain prose + controls: AgentControl[] | (() => AgentControl[]); // a function for dynamic controls + getState?: () => unknown; // small JSON snapshot, used to verify an action landed + invoke: (name: string, args?: Record) => unknown; +}; + +type Bridge = { + __openswarm: true; + __ready: boolean; + __rev: number; + register: (api: AgentRegistration) => void; + refresh: () => void; // bump __rev after dynamic controls change so the agent re-reads + describe: () => unknown; + getState: () => unknown; + invoke: (name: string, args?: Record) => unknown; +}; + +declare global { + interface Window { + OPENSWARM_APP?: Bridge; + } +} + +let registration: AgentRegistration | null = null; + +function resolveControls(): AgentControl[] { + if (!registration) return []; + const c = registration.controls; + try { + return typeof c === 'function' ? c() || [] : c || []; + } catch { + return []; + } +} + +const bridge: Bridge = { + __openswarm: true, + __ready: false, + __rev: 0, + register(api: AgentRegistration) { + registration = api; + bridge.__ready = true; + bridge.__rev += 1; + }, + refresh() { + bridge.__rev += 1; + }, + describe() { + if (!bridge.__ready || !registration) { + return { __ready: false, __rev: bridge.__rev }; + } + return { + rules: registration.rules || '', + controls: resolveControls(), + __rev: bridge.__rev, + }; + }, + getState() { + if (!bridge.__ready || !registration) { + return { __ready: false, __rev: bridge.__rev }; + } + let state: unknown = {}; + try { + state = registration.getState ? registration.getState() : {}; + } catch (e) { + return { __error__: String((e as Error)?.message || e), __rev: bridge.__rev }; + } + // Carry __rev alongside the app's own state so the agent can detect a + // controls change with a single getState, without re-describing every turn. + if (state && typeof state === 'object' && !Array.isArray(state)) { + return { ...(state as Record), __rev: bridge.__rev }; + } + return { value: state, __rev: bridge.__rev }; + }, + invoke(name: string, args?: Record) { + if (!bridge.__ready || !registration) { + throw 'OPENSWARM_APP not registered yet'; + } + return registration.invoke(name, args || {}); + }, +}; + +window.OPENSWARM_APP = bridge; diff --git a/backend/apps/outputs/webapp_template/frontend/src/index.tsx b/backend/apps/outputs/webapp_template/frontend/src/index.tsx index 6c7c991f..3925ad74 100644 --- a/backend/apps/outputs/webapp_template/frontend/src/index.tsx +++ b/backend/apps/outputs/webapp_template/frontend/src/index.tsx @@ -1,3 +1,7 @@ +// Install window.OPENSWARM_APP BEFORE anything else so the agent bridge exists +// from first paint, even while React + the app are still mounting. The app fills +// it in by calling window.OPENSWARM_APP.register(...) on mount. +import './agentBridge'; import React from 'react'; import { createRoot } from 'react-dom/client'; import Main from './app/Main';