oui-spec 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +94 -122
  2. package/dist/{chunk-CVUUSPF4.js → chunk-BMCL2K74.js} +2 -2
  3. package/dist/chunk-BMCL2K74.js.map +1 -0
  4. package/dist/{chunk-MTMEW6MM.cjs → chunk-DCVVTP3V.cjs} +72 -23
  5. package/dist/chunk-DCVVTP3V.cjs.map +1 -0
  6. package/dist/chunk-F3SKRAGU.cjs +98 -0
  7. package/dist/chunk-F3SKRAGU.cjs.map +1 -0
  8. package/dist/{chunk-YDMGV72W.js → chunk-F3WKIRAU.js} +72 -23
  9. package/dist/chunk-F3WKIRAU.js.map +1 -0
  10. package/dist/chunk-IIZ6HL2L.js +89 -0
  11. package/dist/chunk-IIZ6HL2L.js.map +1 -0
  12. package/dist/chunk-KNWGQRXL.cjs +567 -0
  13. package/dist/chunk-KNWGQRXL.cjs.map +1 -0
  14. package/dist/chunk-NYCJZQSP.js +92 -0
  15. package/dist/chunk-NYCJZQSP.js.map +1 -0
  16. package/dist/chunk-ROGKEMLL.cjs +91 -0
  17. package/dist/chunk-ROGKEMLL.cjs.map +1 -0
  18. package/dist/chunk-TRNVXAB5.js +563 -0
  19. package/dist/chunk-TRNVXAB5.js.map +1 -0
  20. package/dist/{chunk-3DCIL4FV.cjs → chunk-ZKM4XWAB.cjs} +2 -2
  21. package/dist/chunk-ZKM4XWAB.cjs.map +1 -0
  22. package/dist/core/index.cjs +10 -6
  23. package/dist/core/index.d.cts +15 -8
  24. package/dist/core/index.d.ts +15 -8
  25. package/dist/core/index.js +3 -3
  26. package/dist/index.cjs +28 -12
  27. package/dist/index.d.cts +5 -4
  28. package/dist/index.d.ts +5 -4
  29. package/dist/index.js +5 -5
  30. package/dist/react/index.cjs +15 -3
  31. package/dist/react/index.d.cts +33 -50
  32. package/dist/react/index.d.ts +33 -50
  33. package/dist/react/index.js +1 -1
  34. package/dist/spec/index.cjs +2 -2
  35. package/dist/spec/index.d.cts +35 -7
  36. package/dist/spec/index.d.ts +35 -7
  37. package/dist/spec/index.js +1 -1
  38. package/dist/{define-surface-D6XPtoqu.d.cts → surface-runtime--2lZGB7Z.d.cts} +92 -2
  39. package/dist/{define-surface-DXBop-uB.d.ts → surface-runtime-Dj_nhd7r.d.ts} +92 -2
  40. package/dist/transport/index.cjs +4 -4
  41. package/dist/transport/index.d.cts +2 -2
  42. package/dist/transport/index.d.ts +2 -2
  43. package/dist/transport/index.js +2 -2
  44. package/dist/{websocket-CrI0yRNo.d.cts → websocket-YfWILdZ4.d.cts} +36 -18
  45. package/dist/{websocket-CiWvkaz_.d.ts → websocket-c5tIUWY5.d.ts} +36 -18
  46. package/package.json +5 -1
  47. package/spec/OUI-SPEC-v0.1.md +229 -155
  48. package/dist/chunk-2ON5IEXB.cjs +0 -167
  49. package/dist/chunk-2ON5IEXB.cjs.map +0 -1
  50. package/dist/chunk-3DCIL4FV.cjs.map +0 -1
  51. package/dist/chunk-5TQ6AVQH.cjs +0 -108
  52. package/dist/chunk-5TQ6AVQH.cjs.map +0 -1
  53. package/dist/chunk-CVUUSPF4.js.map +0 -1
  54. package/dist/chunk-HK7OXB2W.cjs +0 -157
  55. package/dist/chunk-HK7OXB2W.cjs.map +0 -1
  56. package/dist/chunk-ILUTAHUJ.js +0 -154
  57. package/dist/chunk-ILUTAHUJ.js.map +0 -1
  58. package/dist/chunk-MTMEW6MM.cjs.map +0 -1
  59. package/dist/chunk-O6EI77LD.js +0 -106
  60. package/dist/chunk-O6EI77LD.js.map +0 -1
  61. package/dist/chunk-UIGRLZOX.js +0 -164
  62. package/dist/chunk-UIGRLZOX.js.map +0 -1
  63. package/dist/chunk-YDMGV72W.js.map +0 -1
package/README.md CHANGED
@@ -36,37 +36,43 @@ OUI + UI App = any agent can control the UI
36
36
  flowchart TD
37
37
  Agent[Agent Runtime / LLM]
38
38
  Manifest[OUI Manifest]
39
- Dispatch[Dispatch Channel]
40
- Observe[Observation Channel]
39
+ Runtime[Client Surface Runtime]
41
40
  Surface[UI Surface]
42
41
  App[Application State]
43
42
 
44
43
  Agent -->|reads| Manifest
45
- Agent -->|fire-and-forget action| Dispatch
46
- Dispatch -->|WebSocket| Surface
47
- Surface -->|executes handler| App
48
- App -->|state changes| Surface
49
- Surface -->|pushes observations| Observe
50
- Observe -->|WebSocket| Agent
44
+ Agent -->|action request + requestId| Runtime
45
+ Runtime -->|runs handler| Surface
46
+ Surface -->|changes| App
47
+ App -->|UI settles| Runtime
48
+ Runtime -->|action result + surfaces after it| Agent
51
49
  ```
52
50
 
53
- ### Fully Async. Fully Stateless. No Waiting.
51
+ ### Every Action Is Answered
54
52
 
55
- OUI uses **two one-way channels** — not request/response. This is the critical architectural decision:
53
+ Every action request carries a `requestId`, and the client answers it exactly
54
+ once with an `OUIActionResult` (spec §7.3):
56
55
 
57
- | Channel | Direction | Purpose |
58
- | --------------- | --------------- | ------------------------------------ |
59
- | **Dispatch** | Server → Client | Fire action instructions at the UI |
60
- | **Observation** | Client → Server | Push state updates back to the agent |
56
+ | Event | Direction | Carries |
57
+ | -------------------- | ---------------- | ---------------------------------------------------------------------------------------------------- |
58
+ | `{ns}:dispatch` | runtime → client | `OUIActionRequest` — `requestId`, `surfaceId`, `actionId`, `params` |
59
+ | `{ns}:action:result` | client → runtime | `OUIActionResult` — success or error, and the client's surfaces and observations once the UI settled |
60
+ | `{ns}:observation` | client → runtime | state updates as they happen |
61
61
 
62
- The agent runtime (e.g., a Lambda function) dispatches an action and **terminates immediately**. It does not wait for a response. The browser executes the action, starts any async work, and pushes observation updates over the observation channel as results arrive. The agent picks up those observations on its next invocation.
62
+ The result includes what the client can do **after** the action. An action can
63
+ change that: navigating unmounts one page's surfaces and mounts another's. An
64
+ agent that only knew the capability set from before the action would be acting
65
+ on the page it just left.
63
66
 
64
- This means:
67
+ An async action (`async: true`) is acknowledged at once with `interim: true`,
68
+ and answered a second time, under the same `requestId`, when its polling
69
+ finishes. Progress in between is published as observations.
65
70
 
66
- - **No Lambda waiting** — no 30-second timeouts holding a connection open
67
- - **No request/response correlation** — no correlation IDs, no pending promises
68
- - **No WebSocket held open by the server** — the server writes and disconnects
69
- - **Long-running operations work naturally** — the UI polls or subscribes and pushes status updates over time
71
+ An earlier version of this package had no result channel: dispatch was
72
+ fire-and-forget and results were expected to arrive as observations. An agent
73
+ could not tell whether an action ran, and its integrator reported success
74
+ before the browser had done anything. The spec always required a result; the
75
+ package now implements it.
70
76
 
71
77
  ---
72
78
 
@@ -118,48 +124,51 @@ export const counterSurface = defineSurface({
118
124
  });
119
125
  ```
120
126
 
121
- Then instantiate OUI. **`createOUI` is the only supported integration path** — it owns the wire so you never implement the protocol yourself:
127
+ Then create **one surface runtime per client** and provide it at the root. The
128
+ runtime keeps the client's own record of what is mounted, answers every request
129
+ that arrives on the socket, and produces the snapshot an agent works from:
122
130
 
123
- ```typescript
124
- // oui.ts
125
- import { createOUI } from "oui-spec/core";
126
- import { counterSurface } from "./counter.surface";
127
- import { socket } from "./my-socket"; // your connection, your auth, your lifecycle
131
+ ```tsx
132
+ // App.tsx
133
+ import { createSurfaceRuntime } from "oui-spec/core";
134
+ import { SurfaceRuntimeProvider } from "oui-spec/react";
128
135
 
129
- export const oui = createOUI({
130
- socket, // you own the connection
131
- surfaces: [counterSurface], // you own what exists
132
- });
136
+ const runtime = createSurfaceRuntime({ socket }); // your connection, your auth
133
137
 
134
- await oui.start(); // registers every surface and connects
138
+ export function App() {
139
+ return (
140
+ <SurfaceRuntimeProvider runtime={runtime}>
141
+ <CounterPage />
142
+ </SurfaceRuntimeProvider>
143
+ );
144
+ }
135
145
  ```
136
146
 
137
147
  ```tsx
138
148
  // CounterPage.tsx
139
149
  import { useSurface, useObservation } from "oui-spec/react";
140
150
  import { counterSurface } from "./counter.surface";
141
- import { oui } from "./oui";
142
151
 
143
152
  function CounterPage() {
144
153
  const [count, setCount] = useState(0);
145
154
 
146
- useSurface({
155
+ // Mounted while the page is. The runtime answers requests for it.
156
+ const surface = useSurface({
147
157
  surface: counterSurface,
148
158
  context: { count, setCount },
149
- send: oui.transport.send,
150
159
  });
151
160
 
152
- useObservation(
153
- counterSurface.id,
154
- "current_value",
155
- { value: count },
156
- oui.transport.send,
157
- );
161
+ useObservation(surface, "current_value", { value: count });
158
162
 
159
163
  return <div>Count: {count}</div>;
160
164
  }
161
165
  ```
162
166
 
167
+ `runtime.snapshot()` returns `{ surfaces, observations }` for everything
168
+ mounted. An agent runtime that keeps no registry of its own can take it with
169
+ each request (for example, with each chat turn); set `announce: false` and the
170
+ runtime sends nothing but results over the socket.
171
+
163
172
  That's it. The agent now sees a `counter` surface with two tools (`increment`, `reset`) and one live observation (`current_value`).
164
173
 
165
174
  ---
@@ -171,20 +180,19 @@ enforced by the type system, not by documentation.
171
180
 
172
181
  ### What OUI owns — and you cannot reimplement
173
182
 
174
- | | Owned by OUI |
175
- | ------------------ | ------------------------------------------------------------------------------------------------ |
176
- | Socket event names | `{ns}:dispatch`, `{ns}:observation`, `{ns}:surface:register`, `{ns}:surface:deregister` |
177
- | Namespace prefix | `transport.namespace`, default `'oui'` |
178
- | Payload shapes | `OUIActionRequest`, `OUIObservationUpdate`, `OUISurfaceRegistration`, `OUISurfaceDeregistration` |
179
- | Result routing | Action results return through the **observation channel** — there is no separate result event |
183
+ | | Owned by OUI |
184
+ | ------------------ | ------------------------------------------------------------------------------------------------------------------- |
185
+ | Socket event names | `{ns}:dispatch`, `{ns}:action:result`, `{ns}:observation`, `{ns}:surface:register`, `{ns}:surface:deregister` |
186
+ | Namespace prefix | `transport.namespace`, default `'oui'` |
187
+ | Payload shapes | `OUIActionRequest`, `OUIActionResult`, `OUIObservationUpdate`, `OUISurfaceRegistration`, `OUISurfaceDeregistration` |
188
+ | Result routing | One `{ns}:action:result` per request, correlated by `requestId` |
180
189
 
181
190
  `OUIInstance.transport` is a branded `OwnedTransport`. Only `createOUI`
182
191
  produces one, so a transport you construct by hand cannot be substituted for
183
192
  it. This is deliberate: an early integrator satisfied every type in this
184
- package without calling any of its runtime, rebuilt the wire by hand, hardcoded
185
- the namespace, and added a redundant result channel beside the observation
186
- channel that already carried results. Both ends then agreed only by the
187
- coincidence of matching string literals.
193
+ package without calling any of its runtime, rebuilt the wire by hand and
194
+ hardcoded the namespace. Both ends then agreed only by the coincidence of
195
+ matching string literals.
188
196
 
189
197
  ### What you own
190
198
 
@@ -227,44 +235,35 @@ fail in your editor rather than at render time in a browser after a deploy.
227
235
 
228
236
  ## Architecture
229
237
 
230
- ### Two One-Way Channels
238
+ ### Request and Result
231
239
 
232
240
  ```mermaid
233
241
  sequenceDiagram
234
- participant Lambda as Agent Lambda
242
+ participant Agent as Agent Runtime
235
243
  participant WS as WebSocket Server
236
- participant Browser as Browser / UI
237
-
238
- Note over Lambda: Agent decides to act
239
- Lambda->>WS: dispatch(surfaceId, actionId, params)
240
- Note over Lambda: Lambda terminates immediately
241
-
242
- WS->>Browser: oui:dispatch event
243
- Note over Browser: Handler executes
244
- Note over Browser: Async work begins
245
-
246
- loop Polling / Subscribe
247
- Browser->>Browser: Check job status
248
- Browser->>WS: oui:observation (interim update)
249
- end
250
-
251
- Browser->>WS: oui:observation (final result)
252
- Note over Lambda: Next invocation reads observations
244
+ participant Browser as Browser (surface runtime)
245
+
246
+ Agent->>WS: oui:dispatch { requestId, surfaceId, actionId, params }
247
+ WS->>Browser: oui:dispatch
248
+ Note over Browser: Handler runs once (de-duplicated by requestId)
249
+ Note over Browser: Waits until the UI settles
250
+ Browser->>WS: oui:action:result { requestId, success, data, surfaces, observations }
251
+ WS->>Agent: result for requestId
253
252
  ```
254
253
 
255
- The dispatch channel carries action instructions **downward** (server → client). The observation channel carries state updates **upward** (client → server). They never mix. There is no concept of "waiting for a response."
256
-
257
- ### Why Not Request/Response?
258
-
259
- In a typical agent architecture:
254
+ The client waits for the UI to settle before answering: nothing mounted,
255
+ unmounted or changed an observation for a quiet window (250 ms by default), and
256
+ no hold open. A component whose work mounting cannot show, such as a route's
257
+ code or a page's data still loading, keeps the result waiting with
258
+ `useSurfaceHold(isLoading)`. If the deadline passes first, the result says
259
+ `settled: false`.
260
260
 
261
- 1. The agent runs in a Lambda (or similar serverless function)
262
- 2. The Lambda has a 30-second timeout
263
- 3. The UI operation might take 2 minutes (file upload, ML inference, user confirmation)
261
+ ### Long operations
264
262
 
265
- Request/response forces the Lambda to hold a connection open until the operation completes. That either times out or requires expensive long-running compute.
266
-
267
- OUI's two-channel model means the Lambda does O(1) work — write a message to the dispatch channel and exit. The browser handles the rest on its own timeline and pushes results as observations. The agent reads those observations whenever it runs next.
263
+ The result of an async action is its acknowledgment: the job was started, and
264
+ here is its id. The agent is not held until a two-minute render finishes. The
265
+ client polls or subscribes, publishes progress as observations, and sends the
266
+ request's final result when the job ends.
268
267
 
269
268
  ---
270
269
 
@@ -561,55 +560,27 @@ export const datavizSurface = defineSurface<DataVizContext>({
561
560
  ### Using the Surface in React
562
561
 
563
562
  ```tsx
564
- // DataVizPage.tsx
563
+ // DataVizPage.tsx — rendered inside a SurfaceRuntimeProvider
565
564
  import { useSurface, useObservation } from "oui-spec/react";
566
- import { createWebSocketTransport } from "oui-spec/transport";
567
565
  import { datavizSurface } from "./dataviz.surface";
568
566
 
569
567
  export function DataVizPage() {
570
568
  const { state, updateState, api } = useDataVizWizard();
571
- const transport = useTransport(); // your app's transport setup
572
569
 
573
- const { handleActionRequest } = useSurface({
570
+ const surface = useSurface({
574
571
  surface: datavizSurface,
575
572
  context: { state, updateState, api },
576
- send: transport.pushObservation,
577
- active: true,
578
573
  });
579
574
 
580
- // Push observations whenever relevant state changes
581
- useObservation(
582
- datavizSurface.id,
583
- "wizard_state",
584
- {
585
- step: state.step,
586
- selectedDatasetId: state.selectedDataset?.id,
587
- chartConfig: state.chartConfig,
588
- filters: state.filters,
589
- renderedChartUrl: state.renderedChart?.imageUrl,
590
- },
591
- transport.pushObservation,
592
- );
593
-
594
- useObservation(
595
- datavizSurface.id,
596
- "available_datasets",
597
- state.datasets,
598
- transport.pushObservation,
599
- );
600
-
601
- // Wire transport to surface
602
- useEffect(() => {
603
- return transport.onAction((surfaceId, actionId, params) => {
604
- handleActionRequest({
605
- surfaceId,
606
- actionId,
607
- params,
608
- requestId: "",
609
- timestamp: Date.now(),
610
- });
611
- });
612
- }, [handleActionRequest, transport]);
575
+ // Keep observations equal to the state they describe
576
+ useObservation(surface, "wizard_state", {
577
+ step: state.step,
578
+ selectedDatasetId: state.selectedDataset?.id,
579
+ chartConfig: state.chartConfig,
580
+ filters: state.filters,
581
+ renderedChartUrl: state.renderedChart?.imageUrl,
582
+ });
583
+ useObservation(surface, "available_datasets", state.datasets);
613
584
 
614
585
  return <DataVizWizardUI state={state} />;
615
586
  }
@@ -699,7 +670,7 @@ They even share JSON Schema for parameter validation — a deliberate design cho
699
670
  | **Async operations** | Poll screenshots | Poll DOM | Not built-in | Declarative polling runtime |
700
671
  | **Consent model** | Everything visible | Everything in DOM | Server declares tools | App declares surface |
701
672
  | **Framework coupling** | None | Tight (HTML-specific) | None | None (spec is universal) |
702
- | **Complex workflows** | Dozens of screenshots | Dozens of selectors | Multiple tool calls | Multiple dispatches + observations |
673
+ | **Complex workflows** | Dozens of screenshots | Dozens of selectors | Multiple tool calls | Multiple requests, each answered |
703
674
 
704
675
  ---
705
676
 
@@ -708,8 +679,8 @@ They even share JSON Schema for parameter validation — a deliberate design cho
708
679
  1. **App-declared, not inferred.** The application explicitly states what it supports. Agents don't guess from pixels or DOM.
709
680
  2. **Semantic, not structural.** Actions are named operations (`render_chart`), not DOM paths (`click #btn-render`).
710
681
  3. **Typed end-to-end.** Inputs and outputs use JSON Schema. The agent knows exactly what to send and expect.
711
- 4. **Fully async.** Dispatch is fire-and-forget. Results arrive as observations. No blocking, no timeouts.
712
- 5. **Two channels, one direction each.** Dispatch goes down. Observations go up. Never mixed.
682
+ 4. **Every action is answered.** One result per request, correlated by `requestId`, carrying what the client can do after it. Long jobs are acknowledged at once and answered again when they finish.
683
+ 5. **The client is the record.** The client's surface runtime knows what is mounted; an agent can take its snapshot rather than trust a registry that can drift from it.
713
684
  6. **Framework-agnostic.** The spec works with React, Vue, Svelte, vanilla JS, native apps — anything that can handle a function call and emit an event.
714
685
  7. **Observable.** The agent reads relevant state without screenshots. The app pushes state changes when they happen.
715
686
  8. **Consent-based.** The app controls what's exposed. Users and developers decide what agents can do.
@@ -731,6 +702,7 @@ They even share JSON Schema for parameter validation — a deliberate design cho
731
702
  - [x] `oui-spec/react` — `useSurface()` hook + observation helpers
732
703
  - [x] `oui-spec/transport` — WebSocket + Direct transports
733
704
  - [x] `createOUI()` — gated instantiation; integration requirements enforced at compile time
705
+ - [x] `createSurfaceRuntime()` — client-side record of mounted surfaces; every request answered with a result and the snapshot after it
734
706
  - [ ] `oui-spec/devtools` — Surface inspector / debugger
735
707
  - [ ] `oui-spec/vue` — Vue bindings
736
708
  - [ ] `oui-spec/validator` — Runtime schema validation
@@ -8,5 +8,5 @@ var OUI_PROTOCOL_EVENTS = {
8
8
  };
9
9
 
10
10
  export { OUI_PROTOCOL_EVENTS };
11
- //# sourceMappingURL=chunk-CVUUSPF4.js.map
12
- //# sourceMappingURL=chunk-CVUUSPF4.js.map
11
+ //# sourceMappingURL=chunk-BMCL2K74.js.map
12
+ //# sourceMappingURL=chunk-BMCL2K74.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/spec/types.ts"],"names":[],"mappings":";AAiUO,IAAM,mBAAA,GAAsB;AAAA,EACjC,gBAAA,EAAkB,kBAAA;AAAA,EAClB,kBAAA,EAAoB,oBAAA;AAAA,EACpB,cAAA,EAAgB,gBAAA;AAAA,EAChB,aAAA,EAAe,eAAA;AAAA,EACf,kBAAA,EAAoB;AACtB","file":"chunk-BMCL2K74.js","sourcesContent":["/**\n * OUI — Open UI Specification\n *\n * The machine-readable contract for agent-controllable user interfaces.\n * Any UI application that implements this interface can be controlled by\n * any OUI-compatible agent runtime.\n */\n\n/** JSON Schema type (subset used by OUI) */\nexport type JSONSchema = {\n type?: string | string[];\n properties?: Record<string, JSONSchema>;\n items?: JSONSchema;\n required?: string[];\n enum?: (string | number | boolean)[];\n description?: string;\n default?: unknown;\n oneOf?: JSONSchema[];\n anyOf?: JSONSchema[];\n allOf?: JSONSchema[];\n const?: unknown;\n minimum?: number;\n maximum?: number;\n minLength?: number;\n maxLength?: number;\n pattern?: string;\n format?: string;\n additionalProperties?: boolean | JSONSchema;\n};\n\n/**\n * A Surface is a controllable boundary within an application.\n * It declares what an agent can do (actions), what it can observe (state),\n * and when it's available (activation conditions).\n *\n * Think of it as \"one page\" or \"one feature\" of an application, described\n * in a way that an agent can understand and operate.\n */\nexport interface OUISurface {\n /** Unique identifier for this surface (e.g., 'dataviz-wizard', 'document-editor') */\n id: string;\n\n /** Human-readable name */\n name: string;\n\n /** Description of what this surface does — included in agent context */\n description: string;\n\n /** Version of this surface's contract (semver) */\n version?: string;\n\n /** Actions the agent can invoke on this surface */\n actions: OUIAction[];\n\n /** Observable state the agent can read */\n observations?: OUIObservation[];\n\n /** When this surface is active/available */\n activation?: OUIActivation;\n\n /** Additional metadata for agent context */\n metadata?: Record<string, unknown>;\n}\n\n/**\n * Declarative polling configuration for async actions.\n * The OUI runtime in the browser manages the polling lifecycle automatically\n * after the action handler returns its dispatch result.\n */\nexport interface OUIActionPolling {\n /** How often to check (milliseconds) */\n intervalMs: number;\n\n /** Max poll attempts before giving up (optional) */\n maxAttempts?: number;\n\n /** Max total duration before giving up in ms (alternative to maxAttempts) */\n maxDurationMs?: number;\n\n /**\n * Subscribe to a realtime event instead of polling.\n * If provided, the runtime listens for this event instead of using setInterval.\n * More efficient when realtime infrastructure exists.\n */\n subscribe?: {\n /** Event name to listen for */\n event: string;\n /**\n * Filter to match the event. Values prefixed with '$dispatchResult.'\n * are resolved from the handler's return value.\n * Example: { jobId: '$dispatchResult.jobId' }\n */\n filter?: Record<string, unknown>;\n };\n}\n\n/**\n * An Action is a single operation an agent can perform on a surface.\n * Each action becomes a \"tool\" that the LLM can call.\n *\n * The action's handler executes in the UI (or API) and returns a result\n * that the agent sees as the tool's output.\n */\nexport interface OUIAction {\n /** Unique action identifier (becomes the tool name) */\n id: string;\n\n /** Human-readable description — this is what the LLM reads to decide when to use it */\n description: string;\n\n /** JSON Schema for the action's input parameters */\n input: JSONSchema;\n\n /** JSON Schema for the action's return value (optional — agent still gets the result) */\n output?: JSONSchema;\n\n /** If true, the agent runtime should confirm with the user before executing */\n confirm?: boolean;\n\n /** If true, the action is async — returns immediately with a job/tracking ID */\n async?: boolean;\n\n /** Declarative polling configuration — runtime manages the polling lifecycle after dispatch */\n polling?: OUIActionPolling;\n\n /** Human-readable hint about when this action is appropriate to use */\n usage?: string;\n\n /**\n * Preconditions that should be met before this action is called.\n * Expressed as a description (for the LLM) not as executable logic.\n */\n preconditions?: string;\n\n /** Estimated duration hint (e.g., \"instant\", \"2-5s\", \"30-120s\") */\n estimatedDuration?: string;\n\n /** Tags for categorization */\n tags?: string[];\n}\n\n/**\n * An Observation is a piece of application state that the agent can read.\n * Observations are pushed to the agent in real-time as the app state changes,\n * giving it situational awareness without needing screenshots.\n */\nexport interface OUIObservation {\n /** Unique identifier for this observation */\n id: string;\n\n /** Description of what this observation represents */\n description: string;\n\n /** JSON Schema for the observation's value */\n schema: JSONSchema;\n\n /** How frequently this observation updates */\n updateFrequency?: \"realtime\" | \"on-change\" | \"polling\";\n}\n\n/**\n * Activation conditions — when a surface is available to the agent.\n * A surface might only be active on a specific page, when certain\n * entities are loaded, or under specific application states.\n */\nexport interface OUIActivation {\n /** URL path(s) where this surface is active */\n routes?: string | string[];\n\n /** Entity type/ID that must be present for this surface to activate */\n entity?: {\n type: string;\n id?: string;\n };\n\n /** Custom predicate description (for documentation, not execution) */\n condition?: string;\n}\n\n// ─── Protocol Types ──────────────────────────────────────────────────────────\n\n/**\n * A request from the agent runtime to execute an action on a surface.\n */\nexport interface OUIActionRequest {\n /** Unique request ID for correlation */\n requestId: string;\n\n /** Which surface this request targets */\n surfaceId: string;\n\n /** Which action to invoke */\n actionId: string;\n\n /** The action's input parameters (validated against the action's input schema) */\n params: Record<string, unknown>;\n\n /** Timestamp of the request */\n timestamp: number;\n}\n\n/**\n * The result of an action execution, returned from the surface to the agent.\n */\nexport interface OUIActionResult {\n /** Correlates to the request */\n requestId: string;\n\n /** Whether the action succeeded */\n success: boolean;\n\n /** The action's return data (if successful) */\n data?: unknown;\n\n /** Error information (if failed) */\n error?: {\n code: string;\n message: string;\n details?: unknown;\n };\n\n /** Progress indicator for interim polling results (0-100) */\n progress?: number;\n\n /** Whether this is an interim update (polling still in progress) or final */\n interim?: boolean;\n\n /** Duration of execution in milliseconds */\n durationMs?: number;\n\n /** Timestamp of the result */\n timestamp: number;\n\n /**\n * The surfaces active on the client once the action's effects settled.\n *\n * An action can change what the agent is able to do next: navigating\n * unmounts one page's surfaces and mounts another's. Without this, the agent\n * would act on the capability set it had before the action, which is the\n * set the action just replaced.\n */\n surfaces?: OUISurface[];\n\n /** Latest observation values per surface once the action's effects settled. */\n observations?: OUIObservationSnapshot;\n\n /**\n * False when the client's deadline passed before its UI settled, so\n * `surfaces` and `observations` may still be changing. Absent or true\n * otherwise.\n */\n settled?: boolean;\n}\n\n/** Latest observation values, keyed by surface id and then observation id. */\nexport type OUIObservationSnapshot = Record<string, Record<string, unknown>>;\n\n/**\n * What a client can do right now: the surfaces it has mounted and their latest\n * observation values. A client sends this to its agent runtime (for example,\n * with each turn), and returns it on every action result.\n */\nexport interface OUISurfaceSnapshot {\n surfaces: OUISurface[];\n observations: OUIObservationSnapshot;\n}\n\n/**\n * An observation update pushed from the surface to the agent.\n */\nexport interface OUIObservationUpdate {\n /** Which surface this observation belongs to */\n surfaceId: string;\n\n /** Which observation updated */\n observationId: string;\n\n /** The new value */\n value: unknown;\n\n /** Timestamp of the update */\n timestamp: number;\n}\n\n/**\n * Surface registration event — sent when a surface becomes active.\n */\nexport interface OUISurfaceRegistration {\n /** The full surface manifest */\n surface: OUISurface;\n\n /** Timestamp of registration */\n timestamp: number;\n}\n\n/**\n * Surface deregistration event — sent when a surface becomes inactive.\n */\nexport interface OUISurfaceDeregistration {\n /** Which surface is being deregistered */\n surfaceId: string;\n\n /** Timestamp */\n timestamp: number;\n}\n\n// ─── Transport Protocol Events ───────────────────────────────────────────────\n\n/**\n * All protocol events exchanged between agent runtime and surfaces.\n */\nexport type OUIProtocolEvent =\n | { type: \"surface:register\"; payload: OUISurfaceRegistration }\n | { type: \"surface:deregister\"; payload: OUISurfaceDeregistration }\n | { type: \"action:request\"; payload: OUIActionRequest }\n | { type: \"action:result\"; payload: OUIActionResult }\n | { type: \"observation:update\"; payload: OUIObservationUpdate };\n\n/**\n * Protocol event type strings (for type guards and routing).\n */\nexport const OUI_PROTOCOL_EVENTS = {\n SURFACE_REGISTER: \"surface:register\",\n SURFACE_DEREGISTER: \"surface:deregister\",\n ACTION_REQUEST: \"action:request\",\n ACTION_RESULT: \"action:result\",\n OBSERVATION_UPDATE: \"observation:update\",\n} as const;\n"]}
@@ -8,17 +8,28 @@ function createWebSocketTransport(socket, config) {
8
8
  const shouldBuffer = config?.bufferWhileDisconnected ?? true;
9
9
  let connected = socket.connected ?? false;
10
10
  const connectionHandlers = [];
11
- socket.on("connect", () => {
11
+ const attached = [];
12
+ function listen(event, handler) {
13
+ socket.on(event, handler);
14
+ const entry = { event, handler };
15
+ attached.push(entry);
16
+ return () => {
17
+ socket.off(event, handler);
18
+ const i = attached.indexOf(entry);
19
+ if (i >= 0) attached.splice(i, 1);
20
+ };
21
+ }
22
+ listen("connect", () => {
12
23
  connected = true;
13
- connectionHandlers.forEach((h) => h(true));
14
24
  if (shouldBuffer) {
15
25
  while (buffer.length > 0) {
16
26
  const msg = buffer.shift();
17
27
  socket.emit(msg.event, msg.data);
18
28
  }
19
29
  }
30
+ connectionHandlers.forEach((h) => h(true));
20
31
  });
21
- socket.on("disconnect", () => {
32
+ listen("disconnect", () => {
22
33
  connected = false;
23
34
  connectionHandlers.forEach((h) => h(false));
24
35
  });
@@ -30,25 +41,50 @@ function createWebSocketTransport(socket, config) {
30
41
  }
31
42
  }
32
43
  return {
33
- // ─── Dispatch Channel ───────────────────────────────────────
34
- dispatch(surfaceId, actionId, params) {
35
- emit(`${ns}:dispatch`, { surfaceId, actionId, params, timestamp: Date.now() });
44
+ // ─── Dispatch ───────────────────────────────────────────────
45
+ dispatch(request) {
46
+ emit(`${ns}:dispatch`, request);
36
47
  },
37
48
  onAction(handler) {
38
- const listener = (data) => {
39
- handler(data.surfaceId, data.actionId, data.params);
40
- };
41
- socket.on(`${ns}:dispatch`, listener);
42
- return () => socket.off(`${ns}:dispatch`, listener);
49
+ return listen(
50
+ `${ns}:dispatch`,
51
+ (data) => {
52
+ if (!data || typeof data.requestId !== "string" || !data.requestId || typeof data.surfaceId !== "string" || typeof data.actionId !== "string") {
53
+ console.warn(
54
+ "[OUI] Dropped a dispatch without requestId/surfaceId/actionId",
55
+ data
56
+ );
57
+ return;
58
+ }
59
+ handler({
60
+ requestId: data.requestId,
61
+ surfaceId: data.surfaceId,
62
+ actionId: data.actionId,
63
+ params: data.params ?? {},
64
+ timestamp: data.timestamp ?? Date.now()
65
+ });
66
+ }
67
+ );
68
+ },
69
+ // ─── Result ─────────────────────────────────────────────────
70
+ sendResult(result) {
71
+ emit(`${ns}:action:result`, result);
72
+ },
73
+ onResult(handler) {
74
+ return listen(
75
+ `${ns}:action:result`,
76
+ (data) => handler(data)
77
+ );
43
78
  },
44
79
  // ─── Observation Channel ────────────────────────────────────
45
80
  pushObservation(update) {
46
81
  emit(`${ns}:observation`, update);
47
82
  },
48
83
  onObservation(handler) {
49
- const listener = (data) => handler(data);
50
- socket.on(`${ns}:observation`, listener);
51
- return () => socket.off(`${ns}:observation`, listener);
84
+ return listen(
85
+ `${ns}:observation`,
86
+ (data) => handler(data)
87
+ );
52
88
  },
53
89
  // ─── Surface Lifecycle ──────────────────────────────────────
54
90
  registerSurface(surface) {
@@ -58,14 +94,16 @@ function createWebSocketTransport(socket, config) {
58
94
  emit(`${ns}:surface:deregister`, { surfaceId, timestamp: Date.now() });
59
95
  },
60
96
  onSurfaceRegister(handler) {
61
- const listener = (data) => handler(data.surface);
62
- socket.on(`${ns}:surface:register`, listener);
63
- return () => socket.off(`${ns}:surface:register`, listener);
97
+ return listen(
98
+ `${ns}:surface:register`,
99
+ (data) => handler(data.surface)
100
+ );
64
101
  },
65
102
  onSurfaceDeregister(handler) {
66
- const listener = (data) => handler(data.surfaceId);
67
- socket.on(`${ns}:surface:deregister`, listener);
68
- return () => socket.off(`${ns}:surface:deregister`, listener);
103
+ return listen(
104
+ `${ns}:surface:deregister`,
105
+ (data) => handler(data.surfaceId)
106
+ );
69
107
  },
70
108
  // ─── Connection ─────────────────────────────────────────────
71
109
  get connected() {
@@ -75,7 +113,11 @@ function createWebSocketTransport(socket, config) {
75
113
  if (connected) return;
76
114
  return new Promise((resolve, reject) => {
77
115
  const timeout = setTimeout(() => {
78
- reject(new Error(`OUI transport connect timeout (${config?.connectTimeoutMs ?? 1e4}ms)`));
116
+ reject(
117
+ new Error(
118
+ `OUI transport connect timeout (${config?.connectTimeoutMs ?? 1e4}ms)`
119
+ )
120
+ );
79
121
  }, config?.connectTimeoutMs ?? 1e4);
80
122
  socket.once("connect", () => {
81
123
  clearTimeout(timeout);
@@ -97,10 +139,17 @@ function createWebSocketTransport(socket, config) {
97
139
  const idx = connectionHandlers.indexOf(handler);
98
140
  if (idx >= 0) connectionHandlers.splice(idx, 1);
99
141
  };
142
+ },
143
+ dispose() {
144
+ for (const { event, handler } of attached.splice(0)) {
145
+ socket.off(event, handler);
146
+ }
147
+ connectionHandlers.length = 0;
148
+ buffer.length = 0;
100
149
  }
101
150
  };
102
151
  }
103
152
 
104
153
  exports.createWebSocketTransport = createWebSocketTransport;
105
- //# sourceMappingURL=chunk-MTMEW6MM.cjs.map
106
- //# sourceMappingURL=chunk-MTMEW6MM.cjs.map
154
+ //# sourceMappingURL=chunk-DCVVTP3V.cjs.map
155
+ //# sourceMappingURL=chunk-DCVVTP3V.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/transport/websocket.ts"],"names":[],"mappings":"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","file":"chunk-DCVVTP3V.cjs","sourcesContent":["import type {\n OUITransport,\n OUITransportConfig,\n OUIActionHandler,\n OUIObservationHandler,\n OUIResultHandler,\n} from \"./types.js\";\nimport type {\n OUISurface,\n OUIObservationUpdate,\n OUIActionRequest,\n OUIActionResult,\n} from \"../spec/index.js\";\n\n/**\n * WebSocket transport using Socket.IO.\n * Maps OUI protocol events to socket events with a configurable namespace prefix.\n *\n * Events:\n * {ns}:dispatch — action request (runtime → client)\n * {ns}:action:result — action result (client → runtime)\n * {ns}:observation — observation (client → runtime)\n * {ns}:surface:register — registration (client → runtime)\n * {ns}:surface:deregister — deregistration (client → runtime)\n */\nexport function createWebSocketTransport(\n socket: SocketLike,\n config?: OUITransportConfig,\n): OUITransport {\n const ns = config?.namespace ?? \"oui\";\n const buffer: Array<{ event: string; data: unknown }> = [];\n const maxBuffer = config?.maxBufferSize ?? 100;\n const shouldBuffer = config?.bufferWhileDisconnected ?? true;\n\n let connected = socket.connected ?? false;\n const connectionHandlers: Array<(c: boolean) => void> = [];\n\n // Every listener this transport attaches, so dispose() can remove exactly\n // these and nothing the integrator attached to the same socket.\n const attached: Array<{ event: string; handler: (...args: any[]) => void }> =\n [];\n function listen(\n event: string,\n handler: (...args: any[]) => void,\n ): () => void {\n socket.on(event, handler);\n const entry = { event, handler };\n attached.push(entry);\n return () => {\n socket.off(event, handler);\n const i = attached.indexOf(entry);\n if (i >= 0) attached.splice(i, 1);\n };\n }\n\n listen(\"connect\", () => {\n connected = true;\n // Flush before announcing: anything queued while offline was sent earlier\n // than whatever a connection handler is about to send.\n if (shouldBuffer) {\n while (buffer.length > 0) {\n const msg = buffer.shift()!;\n socket.emit(msg.event, msg.data);\n }\n }\n connectionHandlers.forEach((h) => h(true));\n });\n\n listen(\"disconnect\", () => {\n connected = false;\n connectionHandlers.forEach((h) => h(false));\n });\n\n function emit(event: string, data: unknown) {\n if (connected) {\n socket.emit(event, data);\n } else if (shouldBuffer && buffer.length < maxBuffer) {\n buffer.push({ event, data });\n }\n }\n\n return {\n // ─── Dispatch ───────────────────────────────────────────────\n dispatch(request: OUIActionRequest) {\n emit(`${ns}:dispatch`, request);\n },\n\n onAction(handler: OUIActionHandler) {\n return listen(\n `${ns}:dispatch`,\n (data: Partial<OUIActionRequest> | undefined) => {\n // A request without a requestId cannot be answered, and answering is\n // the contract. Refuse it loudly rather than run an action whose\n // result has nowhere to go.\n if (\n !data ||\n typeof data.requestId !== \"string\" ||\n !data.requestId ||\n typeof data.surfaceId !== \"string\" ||\n typeof data.actionId !== \"string\"\n ) {\n console.warn(\n \"[OUI] Dropped a dispatch without requestId/surfaceId/actionId\",\n data,\n );\n return;\n }\n handler({\n requestId: data.requestId,\n surfaceId: data.surfaceId,\n actionId: data.actionId,\n params: data.params ?? {},\n timestamp: data.timestamp ?? Date.now(),\n });\n },\n );\n },\n\n // ─── Result ─────────────────────────────────────────────────\n sendResult(result: OUIActionResult) {\n emit(`${ns}:action:result`, result);\n },\n\n onResult(handler: OUIResultHandler) {\n return listen(`${ns}:action:result`, (data: OUIActionResult) =>\n handler(data),\n );\n },\n\n // ─── Observation Channel ────────────────────────────────────\n pushObservation(update: OUIObservationUpdate) {\n emit(`${ns}:observation`, update);\n },\n\n onObservation(handler: OUIObservationHandler) {\n return listen(`${ns}:observation`, (data: OUIObservationUpdate) =>\n handler(data),\n );\n },\n\n // ─── Surface Lifecycle ──────────────────────────────────────\n registerSurface(surface: OUISurface) {\n emit(`${ns}:surface:register`, { surface, timestamp: Date.now() });\n },\n\n deregisterSurface(surfaceId: string) {\n emit(`${ns}:surface:deregister`, { surfaceId, timestamp: Date.now() });\n },\n\n onSurfaceRegister(handler: (surface: OUISurface) => void) {\n return listen(`${ns}:surface:register`, (data: { surface: OUISurface }) =>\n handler(data.surface),\n );\n },\n\n onSurfaceDeregister(handler: (surfaceId: string) => void) {\n return listen(`${ns}:surface:deregister`, (data: { surfaceId: string }) =>\n handler(data.surfaceId),\n );\n },\n\n // ─── Connection ─────────────────────────────────────────────\n get connected() {\n return connected;\n },\n\n async connect() {\n if (connected) return;\n return new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => {\n reject(\n new Error(\n `OUI transport connect timeout (${config?.connectTimeoutMs ?? 10000}ms)`,\n ),\n );\n }, config?.connectTimeoutMs ?? 10000);\n\n socket.once(\"connect\", () => {\n clearTimeout(timeout);\n resolve();\n });\n\n if (typeof socket.connect === \"function\") {\n socket.connect();\n }\n });\n },\n\n disconnect() {\n if (typeof socket.disconnect === \"function\") {\n socket.disconnect();\n }\n },\n\n onConnectionChange(handler: (c: boolean) => void) {\n connectionHandlers.push(handler);\n return () => {\n const idx = connectionHandlers.indexOf(handler);\n if (idx >= 0) connectionHandlers.splice(idx, 1);\n };\n },\n\n dispose() {\n for (const { event, handler } of attached.splice(0)) {\n socket.off(event, handler);\n }\n connectionHandlers.length = 0;\n buffer.length = 0;\n },\n };\n}\n\n/**\n * Minimal socket interface — compatible with Socket.IO client or server socket.\n * Only the methods OUI actually uses.\n */\nexport interface SocketLike {\n readonly connected?: boolean;\n emit(event: string, data: unknown): void;\n on(event: string, handler: (...args: any[]) => void): void;\n off(event: string, handler: (...args: any[]) => void): void;\n once(event: string, handler: (...args: any[]) => void): void;\n connect?(): void;\n disconnect?(): void;\n}\n"]}
@@ -0,0 +1,98 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+
5
+ // src/react/use-surface.ts
6
+ var SurfaceRuntimeContext = react.createContext(null);
7
+ function SurfaceRuntimeProvider({
8
+ runtime,
9
+ children
10
+ }) {
11
+ return react.createElement(
12
+ SurfaceRuntimeContext.Provider,
13
+ { value: runtime },
14
+ children
15
+ );
16
+ }
17
+ function useSurfaceRuntime() {
18
+ const runtime = react.useContext(SurfaceRuntimeContext);
19
+ if (!runtime) {
20
+ throw new Error(
21
+ "[OUI] useSurface needs a SurfaceRuntimeProvider above it. Create one runtime per client with createSurfaceRuntime() and provide it at the root."
22
+ );
23
+ }
24
+ return runtime;
25
+ }
26
+ var localRequestSeq = 0;
27
+ function useSurface(options) {
28
+ const { surface, context, active = true } = options;
29
+ const runtime = useSurfaceRuntime();
30
+ const contextRef = react.useRef(context);
31
+ contextRef.current = context;
32
+ const mountedRef = react.useRef(null);
33
+ const pendingRef = react.useRef(/* @__PURE__ */ new Map());
34
+ react.useEffect(() => {
35
+ if (!active) return;
36
+ const mounted = runtime.mount(surface, () => contextRef.current);
37
+ mountedRef.current = mounted;
38
+ for (const [observationId, value] of pendingRef.current) {
39
+ mounted.pushObservation(observationId, value);
40
+ }
41
+ pendingRef.current.clear();
42
+ return () => {
43
+ mounted.unmount();
44
+ if (mountedRef.current === mounted) mountedRef.current = null;
45
+ };
46
+ }, [runtime, surface, active]);
47
+ const pushObservation = react.useCallback(
48
+ (observationId, value) => {
49
+ const mounted = mountedRef.current;
50
+ if (mounted) mounted.pushObservation(observationId, value);
51
+ else pendingRef.current.set(observationId, value);
52
+ },
53
+ []
54
+ );
55
+ const execute = react.useCallback(
56
+ (actionId, params = {}) => runtime.execute({
57
+ requestId: `local_${Date.now()}_${++localRequestSeq}`,
58
+ surfaceId: surface.id,
59
+ actionId,
60
+ params,
61
+ timestamp: Date.now()
62
+ }),
63
+ [runtime, surface.id]
64
+ );
65
+ return react.useMemo(
66
+ () => ({ surfaceId: surface.id, pushObservation, execute }),
67
+ [surface.id, pushObservation, execute]
68
+ );
69
+ }
70
+ function useObservation(surface, observationId, value) {
71
+ const { pushObservation } = surface;
72
+ let serialized;
73
+ try {
74
+ serialized = JSON.stringify(value) ?? "undefined";
75
+ } catch {
76
+ serialized = String(value);
77
+ }
78
+ const valueRef = react.useRef(value);
79
+ valueRef.current = value;
80
+ react.useEffect(() => {
81
+ pushObservation(observationId, valueRef.current);
82
+ }, [pushObservation, observationId, serialized]);
83
+ }
84
+ function useSurfaceHold(busy) {
85
+ const runtime = useSurfaceRuntime();
86
+ react.useEffect(() => {
87
+ if (!busy) return;
88
+ return runtime.hold();
89
+ }, [runtime, busy]);
90
+ }
91
+
92
+ exports.SurfaceRuntimeProvider = SurfaceRuntimeProvider;
93
+ exports.useObservation = useObservation;
94
+ exports.useSurface = useSurface;
95
+ exports.useSurfaceHold = useSurfaceHold;
96
+ exports.useSurfaceRuntime = useSurfaceRuntime;
97
+ //# sourceMappingURL=chunk-F3SKRAGU.cjs.map
98
+ //# sourceMappingURL=chunk-F3SKRAGU.cjs.map