oui-spec 0.1.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.
- package/README.md +161 -104
- package/dist/{chunk-CVUUSPF4.js → chunk-BMCL2K74.js} +2 -2
- package/dist/chunk-BMCL2K74.js.map +1 -0
- package/dist/chunk-DCVVTP3V.cjs +155 -0
- package/dist/chunk-DCVVTP3V.cjs.map +1 -0
- package/dist/chunk-F3SKRAGU.cjs +98 -0
- package/dist/chunk-F3SKRAGU.cjs.map +1 -0
- package/dist/chunk-F3WKIRAU.js +153 -0
- package/dist/chunk-F3WKIRAU.js.map +1 -0
- package/dist/chunk-IIZ6HL2L.js +89 -0
- package/dist/chunk-IIZ6HL2L.js.map +1 -0
- package/dist/chunk-KNWGQRXL.cjs +567 -0
- package/dist/chunk-KNWGQRXL.cjs.map +1 -0
- package/dist/chunk-NYCJZQSP.js +92 -0
- package/dist/chunk-NYCJZQSP.js.map +1 -0
- package/dist/chunk-ROGKEMLL.cjs +91 -0
- package/dist/chunk-ROGKEMLL.cjs.map +1 -0
- package/dist/chunk-TRNVXAB5.js +563 -0
- package/dist/chunk-TRNVXAB5.js.map +1 -0
- package/dist/{chunk-3DCIL4FV.cjs → chunk-ZKM4XWAB.cjs} +2 -2
- package/dist/chunk-ZKM4XWAB.cjs.map +1 -0
- package/dist/core/index.cjs +13 -4
- package/dist/core/index.d.cts +123 -102
- package/dist/core/index.d.ts +123 -102
- package/dist/core/index.js +3 -2
- package/dist/index.cjs +31 -10
- package/dist/index.d.cts +7 -4
- package/dist/index.d.ts +7 -4
- package/dist/index.js +5 -4
- package/dist/react/index.cjs +15 -3
- package/dist/react/index.d.cts +33 -50
- package/dist/react/index.d.ts +33 -50
- package/dist/react/index.js +1 -1
- package/dist/spec/index.cjs +2 -2
- package/dist/spec/index.d.cts +35 -7
- package/dist/spec/index.d.ts +35 -7
- package/dist/spec/index.js +1 -1
- package/dist/surface-runtime--2lZGB7Z.d.cts +241 -0
- package/dist/surface-runtime-Dj_nhd7r.d.ts +241 -0
- package/dist/transport/index.cjs +4 -3
- package/dist/transport/index.d.cts +4 -77
- package/dist/transport/index.d.ts +4 -77
- package/dist/transport/index.js +2 -1
- package/dist/websocket-YfWILdZ4.d.cts +96 -0
- package/dist/websocket-c5tIUWY5.d.ts +96 -0
- package/package.json +8 -3
- package/spec/OUI-SPEC-v0.1.md +229 -155
- package/dist/chunk-2ON5IEXB.cjs +0 -167
- package/dist/chunk-2ON5IEXB.cjs.map +0 -1
- package/dist/chunk-3DCIL4FV.cjs.map +0 -1
- package/dist/chunk-7EXSMPGR.js +0 -207
- package/dist/chunk-7EXSMPGR.js.map +0 -1
- package/dist/chunk-CVUUSPF4.js.map +0 -1
- package/dist/chunk-JO5LGOB6.js +0 -88
- package/dist/chunk-JO5LGOB6.js.map +0 -1
- package/dist/chunk-LEXIU2MH.cjs +0 -90
- package/dist/chunk-LEXIU2MH.cjs.map +0 -1
- package/dist/chunk-UIGRLZOX.js +0 -164
- package/dist/chunk-UIGRLZOX.js.map +0 -1
- package/dist/chunk-WZXUKJ7E.cjs +0 -210
- package/dist/chunk-WZXUKJ7E.cjs.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
|
-
|
|
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 -->|
|
|
46
|
-
|
|
47
|
-
Surface -->|
|
|
48
|
-
App -->|
|
|
49
|
-
|
|
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
|
-
###
|
|
51
|
+
### Every Action Is Answered
|
|
54
52
|
|
|
55
|
-
|
|
53
|
+
Every action request carries a `requestId`, and the client answers it exactly
|
|
54
|
+
once with an `OUIActionResult` (spec §7.3):
|
|
56
55
|
|
|
57
|
-
|
|
|
58
|
-
|
|
|
59
|
-
|
|
|
60
|
-
|
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
67
|
-
-
|
|
68
|
-
|
|
69
|
-
|
|
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,76 +124,146 @@ export const counterSurface = defineSurface({
|
|
|
118
124
|
});
|
|
119
125
|
```
|
|
120
126
|
|
|
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:
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
// App.tsx
|
|
133
|
+
import { createSurfaceRuntime } from "oui-spec/core";
|
|
134
|
+
import { SurfaceRuntimeProvider } from "oui-spec/react";
|
|
135
|
+
|
|
136
|
+
const runtime = createSurfaceRuntime({ socket }); // your connection, your auth
|
|
137
|
+
|
|
138
|
+
export function App() {
|
|
139
|
+
return (
|
|
140
|
+
<SurfaceRuntimeProvider runtime={runtime}>
|
|
141
|
+
<CounterPage />
|
|
142
|
+
</SurfaceRuntimeProvider>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
121
147
|
```tsx
|
|
122
148
|
// CounterPage.tsx
|
|
123
149
|
import { useSurface, useObservation } from "oui-spec/react";
|
|
124
150
|
import { counterSurface } from "./counter.surface";
|
|
125
151
|
|
|
126
|
-
function CounterPage(
|
|
152
|
+
function CounterPage() {
|
|
127
153
|
const [count, setCount] = useState(0);
|
|
128
154
|
|
|
129
|
-
|
|
155
|
+
// Mounted while the page is. The runtime answers requests for it.
|
|
156
|
+
const surface = useSurface({
|
|
130
157
|
surface: counterSurface,
|
|
131
158
|
context: { count, setCount },
|
|
132
|
-
send: transport.send,
|
|
133
159
|
});
|
|
134
160
|
|
|
135
|
-
|
|
136
|
-
useObservation(
|
|
137
|
-
counterSurface.id,
|
|
138
|
-
"current_value",
|
|
139
|
-
{ value: count },
|
|
140
|
-
transport.send,
|
|
141
|
-
);
|
|
161
|
+
useObservation(surface, "current_value", { value: count });
|
|
142
162
|
|
|
143
163
|
return <div>Count: {count}</div>;
|
|
144
164
|
}
|
|
145
165
|
```
|
|
146
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
|
+
|
|
147
172
|
That's it. The agent now sees a `counter` surface with two tools (`increment`, `reset`) and one live observation (`current_value`).
|
|
148
173
|
|
|
149
174
|
---
|
|
150
175
|
|
|
151
|
-
##
|
|
176
|
+
## The Integration Contract
|
|
152
177
|
|
|
153
|
-
|
|
178
|
+
OUI owns the protocol. You own the connection and the content. That split is
|
|
179
|
+
enforced by the type system, not by documentation.
|
|
154
180
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
181
|
+
### What OUI owns — and you cannot reimplement
|
|
182
|
+
|
|
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` |
|
|
189
|
+
|
|
190
|
+
`OUIInstance.transport` is a branded `OwnedTransport`. Only `createOUI`
|
|
191
|
+
produces one, so a transport you construct by hand cannot be substituted for
|
|
192
|
+
it. This is deliberate: an early integrator satisfied every type in this
|
|
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.
|
|
196
|
+
|
|
197
|
+
### What you own
|
|
198
|
+
|
|
199
|
+
| | Owned by the integrator |
|
|
200
|
+
| ------------------- | -------------------------------------------- |
|
|
201
|
+
| Connection | The `socket` — auth, reconnection, lifecycle |
|
|
202
|
+
| Surfaces | What exists, via `defineSurface` |
|
|
203
|
+
| Action schemas | `input` / `output` JSON Schema per action |
|
|
204
|
+
| Observation schemas | `schema` per observation |
|
|
205
|
+
| Handlers | What actually executes |
|
|
206
|
+
|
|
207
|
+
### Requirements enforced at compile time
|
|
208
|
+
|
|
209
|
+
`createOUI` will not compile unless every requirement is met:
|
|
160
210
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
211
|
+
```typescript
|
|
212
|
+
createOUI({ socket, surfaces: [alpha, alpha] });
|
|
213
|
+
// ✗ Duplicate surface id: "alpha". Registration is keyed by id,
|
|
214
|
+
// so the second would silently replace the first.
|
|
215
|
+
|
|
216
|
+
createOUI({ socket, surfaces: [] });
|
|
217
|
+
// ✗ createOUI requires at least one surface. An instance with no
|
|
218
|
+
// surfaces registers nothing and dispatches nowhere.
|
|
164
219
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
Note over Browser: Async work begins
|
|
220
|
+
createOUI({ surfaces: [alpha] });
|
|
221
|
+
// ✗ Property 'socket' is missing.
|
|
168
222
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
Browser->>WS: oui:observation (interim update)
|
|
172
|
-
end
|
|
223
|
+
createOUI({ socket, surfaces: [{ id: "x", name: "X" /* ... */ }] });
|
|
224
|
+
// ✗ Property '[OUI_BRAND]' is missing — surfaces must come from defineSurface.
|
|
173
225
|
|
|
174
|
-
|
|
175
|
-
|
|
226
|
+
const mine: OwnedTransport = createWebSocketTransport(socket);
|
|
227
|
+
// ✗ Property '[OUI_BRAND]' is missing in type 'OUITransport'.
|
|
176
228
|
```
|
|
177
229
|
|
|
178
|
-
|
|
230
|
+
Duplicate action and observation ids within a surface are validated by
|
|
231
|
+
`defineSurface`. Surface-level rules are checked by the type system, so they
|
|
232
|
+
fail in your editor rather than at render time in a browser after a deploy.
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Architecture
|
|
179
237
|
|
|
180
|
-
###
|
|
238
|
+
### Request and Result
|
|
181
239
|
|
|
182
|
-
|
|
240
|
+
```mermaid
|
|
241
|
+
sequenceDiagram
|
|
242
|
+
participant Agent as Agent Runtime
|
|
243
|
+
participant WS as WebSocket Server
|
|
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
|
|
252
|
+
```
|
|
183
253
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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`.
|
|
187
260
|
|
|
188
|
-
|
|
261
|
+
### Long operations
|
|
189
262
|
|
|
190
|
-
|
|
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.
|
|
191
267
|
|
|
192
268
|
---
|
|
193
269
|
|
|
@@ -484,55 +560,27 @@ export const datavizSurface = defineSurface<DataVizContext>({
|
|
|
484
560
|
### Using the Surface in React
|
|
485
561
|
|
|
486
562
|
```tsx
|
|
487
|
-
// DataVizPage.tsx
|
|
563
|
+
// DataVizPage.tsx — rendered inside a SurfaceRuntimeProvider
|
|
488
564
|
import { useSurface, useObservation } from "oui-spec/react";
|
|
489
|
-
import { createWebSocketTransport } from "oui-spec/transport";
|
|
490
565
|
import { datavizSurface } from "./dataviz.surface";
|
|
491
566
|
|
|
492
567
|
export function DataVizPage() {
|
|
493
568
|
const { state, updateState, api } = useDataVizWizard();
|
|
494
|
-
const transport = useTransport(); // your app's transport setup
|
|
495
569
|
|
|
496
|
-
const
|
|
570
|
+
const surface = useSurface({
|
|
497
571
|
surface: datavizSurface,
|
|
498
572
|
context: { state, updateState, api },
|
|
499
|
-
send: transport.pushObservation,
|
|
500
|
-
active: true,
|
|
501
573
|
});
|
|
502
574
|
|
|
503
|
-
//
|
|
504
|
-
useObservation(
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
renderedChartUrl: state.renderedChart?.imageUrl,
|
|
513
|
-
},
|
|
514
|
-
transport.pushObservation,
|
|
515
|
-
);
|
|
516
|
-
|
|
517
|
-
useObservation(
|
|
518
|
-
datavizSurface.id,
|
|
519
|
-
"available_datasets",
|
|
520
|
-
state.datasets,
|
|
521
|
-
transport.pushObservation,
|
|
522
|
-
);
|
|
523
|
-
|
|
524
|
-
// Wire transport to surface
|
|
525
|
-
useEffect(() => {
|
|
526
|
-
return transport.onAction((surfaceId, actionId, params) => {
|
|
527
|
-
handleActionRequest({
|
|
528
|
-
surfaceId,
|
|
529
|
-
actionId,
|
|
530
|
-
params,
|
|
531
|
-
requestId: "",
|
|
532
|
-
timestamp: Date.now(),
|
|
533
|
-
});
|
|
534
|
-
});
|
|
535
|
-
}, [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);
|
|
536
584
|
|
|
537
585
|
return <DataVizWizardUI state={state} />;
|
|
538
586
|
}
|
|
@@ -564,19 +612,25 @@ The agent can orchestrate the full wizard: select data → configure chart → f
|
|
|
564
612
|
The `oui-spec` package exposes subpath exports for granular imports:
|
|
565
613
|
|
|
566
614
|
```typescript
|
|
567
|
-
import { defineSurface } from "oui-spec/core";
|
|
568
|
-
import { useSurface } from "oui-spec/react";
|
|
569
|
-
import {
|
|
615
|
+
import { createOUI, defineSurface } from "oui-spec/core";
|
|
616
|
+
import { useSurface, useObservation } from "oui-spec/react";
|
|
617
|
+
import type { SocketLike } from "oui-spec/transport";
|
|
570
618
|
import type { OUISurface, OUIAction } from "oui-spec/spec";
|
|
571
619
|
|
|
572
620
|
// Or import everything from the root
|
|
573
|
-
import { defineSurface, useSurface
|
|
621
|
+
import { createOUI, defineSurface, useSurface } from "oui-spec";
|
|
574
622
|
```
|
|
575
623
|
|
|
624
|
+
`oui-spec/transport` also exports `createWebSocketTransport` and
|
|
625
|
+
`createDirectTransportPair`. These exist for tests and for embedding OUI in a
|
|
626
|
+
runtime that owns its own wiring — they are **not** the integration path, and
|
|
627
|
+
they return a plain `OUITransport` rather than the branded `OwnedTransport` that
|
|
628
|
+
OUI's own APIs accept. If you are integrating an application, use `createOUI`.
|
|
629
|
+
|
|
576
630
|
| Subpath | Description |
|
|
577
631
|
| -------------------- | ---------------------------------------------------------------------------- |
|
|
578
632
|
| `oui-spec/spec` | TypeScript types + JSON Schema for the OUI specification. Zero runtime deps. |
|
|
579
|
-
| `oui-spec/core` | `defineSurface()`, manifest extraction, action execution
|
|
633
|
+
| `oui-spec/core` | `createOUI()`, `defineSurface()`, manifest extraction, action execution. |
|
|
580
634
|
| `oui-spec/react` | `useSurface()` hook, `useObservation()` helper. React bindings. |
|
|
581
635
|
| `oui-spec/transport` | Two-channel transport layer. WebSocket (Socket.IO), direct (in-memory). |
|
|
582
636
|
|
|
@@ -616,7 +670,7 @@ They even share JSON Schema for parameter validation — a deliberate design cho
|
|
|
616
670
|
| **Async operations** | Poll screenshots | Poll DOM | Not built-in | Declarative polling runtime |
|
|
617
671
|
| **Consent model** | Everything visible | Everything in DOM | Server declares tools | App declares surface |
|
|
618
672
|
| **Framework coupling** | None | Tight (HTML-specific) | None | None (spec is universal) |
|
|
619
|
-
| **Complex workflows** | Dozens of screenshots | Dozens of selectors | Multiple tool calls | Multiple
|
|
673
|
+
| **Complex workflows** | Dozens of screenshots | Dozens of selectors | Multiple tool calls | Multiple requests, each answered |
|
|
620
674
|
|
|
621
675
|
---
|
|
622
676
|
|
|
@@ -625,13 +679,14 @@ They even share JSON Schema for parameter validation — a deliberate design cho
|
|
|
625
679
|
1. **App-declared, not inferred.** The application explicitly states what it supports. Agents don't guess from pixels or DOM.
|
|
626
680
|
2. **Semantic, not structural.** Actions are named operations (`render_chart`), not DOM paths (`click #btn-render`).
|
|
627
681
|
3. **Typed end-to-end.** Inputs and outputs use JSON Schema. The agent knows exactly what to send and expect.
|
|
628
|
-
4. **
|
|
629
|
-
5. **
|
|
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.
|
|
630
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.
|
|
631
685
|
7. **Observable.** The agent reads relevant state without screenshots. The app pushes state changes when they happen.
|
|
632
686
|
8. **Consent-based.** The app controls what's exposed. Users and developers decide what agents can do.
|
|
633
687
|
9. **Composable.** Multiple surfaces coexist. A complex app exposes many surfaces (one per feature/page).
|
|
634
688
|
10. **Complementary.** OUI doesn't replace OpenAPI, MCP, or any other standard. It fills the "UI control" gap they leave open.
|
|
689
|
+
11. **Unbypassable.** A contract an integrator can satisfy without using is not a contract. OUI brands the values it owns with a `unique symbol`, so structural typing cannot be used to substitute a hand-rolled transport or surface. Requirements fail at compile time, with the offending id named.
|
|
635
690
|
|
|
636
691
|
---
|
|
637
692
|
|
|
@@ -646,6 +701,8 @@ They even share JSON Schema for parameter validation — a deliberate design cho
|
|
|
646
701
|
- [x] `oui-spec/core` — `defineSurface()` + manifest extraction
|
|
647
702
|
- [x] `oui-spec/react` — `useSurface()` hook + observation helpers
|
|
648
703
|
- [x] `oui-spec/transport` — WebSocket + Direct transports
|
|
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
|
|
649
706
|
- [ ] `oui-spec/devtools` — Surface inspector / debugger
|
|
650
707
|
- [ ] `oui-spec/vue` — Vue bindings
|
|
651
708
|
- [ ] `oui-spec/validator` — Runtime schema validation
|
|
@@ -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"]}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/transport/websocket.ts
|
|
4
|
+
function createWebSocketTransport(socket, config) {
|
|
5
|
+
const ns = config?.namespace ?? "oui";
|
|
6
|
+
const buffer = [];
|
|
7
|
+
const maxBuffer = config?.maxBufferSize ?? 100;
|
|
8
|
+
const shouldBuffer = config?.bufferWhileDisconnected ?? true;
|
|
9
|
+
let connected = socket.connected ?? false;
|
|
10
|
+
const connectionHandlers = [];
|
|
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", () => {
|
|
23
|
+
connected = true;
|
|
24
|
+
if (shouldBuffer) {
|
|
25
|
+
while (buffer.length > 0) {
|
|
26
|
+
const msg = buffer.shift();
|
|
27
|
+
socket.emit(msg.event, msg.data);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
connectionHandlers.forEach((h) => h(true));
|
|
31
|
+
});
|
|
32
|
+
listen("disconnect", () => {
|
|
33
|
+
connected = false;
|
|
34
|
+
connectionHandlers.forEach((h) => h(false));
|
|
35
|
+
});
|
|
36
|
+
function emit(event, data) {
|
|
37
|
+
if (connected) {
|
|
38
|
+
socket.emit(event, data);
|
|
39
|
+
} else if (shouldBuffer && buffer.length < maxBuffer) {
|
|
40
|
+
buffer.push({ event, data });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
// ─── Dispatch ───────────────────────────────────────────────
|
|
45
|
+
dispatch(request) {
|
|
46
|
+
emit(`${ns}:dispatch`, request);
|
|
47
|
+
},
|
|
48
|
+
onAction(handler) {
|
|
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
|
+
);
|
|
78
|
+
},
|
|
79
|
+
// ─── Observation Channel ────────────────────────────────────
|
|
80
|
+
pushObservation(update) {
|
|
81
|
+
emit(`${ns}:observation`, update);
|
|
82
|
+
},
|
|
83
|
+
onObservation(handler) {
|
|
84
|
+
return listen(
|
|
85
|
+
`${ns}:observation`,
|
|
86
|
+
(data) => handler(data)
|
|
87
|
+
);
|
|
88
|
+
},
|
|
89
|
+
// ─── Surface Lifecycle ──────────────────────────────────────
|
|
90
|
+
registerSurface(surface) {
|
|
91
|
+
emit(`${ns}:surface:register`, { surface, timestamp: Date.now() });
|
|
92
|
+
},
|
|
93
|
+
deregisterSurface(surfaceId) {
|
|
94
|
+
emit(`${ns}:surface:deregister`, { surfaceId, timestamp: Date.now() });
|
|
95
|
+
},
|
|
96
|
+
onSurfaceRegister(handler) {
|
|
97
|
+
return listen(
|
|
98
|
+
`${ns}:surface:register`,
|
|
99
|
+
(data) => handler(data.surface)
|
|
100
|
+
);
|
|
101
|
+
},
|
|
102
|
+
onSurfaceDeregister(handler) {
|
|
103
|
+
return listen(
|
|
104
|
+
`${ns}:surface:deregister`,
|
|
105
|
+
(data) => handler(data.surfaceId)
|
|
106
|
+
);
|
|
107
|
+
},
|
|
108
|
+
// ─── Connection ─────────────────────────────────────────────
|
|
109
|
+
get connected() {
|
|
110
|
+
return connected;
|
|
111
|
+
},
|
|
112
|
+
async connect() {
|
|
113
|
+
if (connected) return;
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
const timeout = setTimeout(() => {
|
|
116
|
+
reject(
|
|
117
|
+
new Error(
|
|
118
|
+
`OUI transport connect timeout (${config?.connectTimeoutMs ?? 1e4}ms)`
|
|
119
|
+
)
|
|
120
|
+
);
|
|
121
|
+
}, config?.connectTimeoutMs ?? 1e4);
|
|
122
|
+
socket.once("connect", () => {
|
|
123
|
+
clearTimeout(timeout);
|
|
124
|
+
resolve();
|
|
125
|
+
});
|
|
126
|
+
if (typeof socket.connect === "function") {
|
|
127
|
+
socket.connect();
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
},
|
|
131
|
+
disconnect() {
|
|
132
|
+
if (typeof socket.disconnect === "function") {
|
|
133
|
+
socket.disconnect();
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
onConnectionChange(handler) {
|
|
137
|
+
connectionHandlers.push(handler);
|
|
138
|
+
return () => {
|
|
139
|
+
const idx = connectionHandlers.indexOf(handler);
|
|
140
|
+
if (idx >= 0) connectionHandlers.splice(idx, 1);
|
|
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;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
exports.createWebSocketTransport = createWebSocketTransport;
|
|
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"]}
|