oui-spec 0.1.0 → 0.2.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 +95 -10
- package/dist/chunk-5TQ6AVQH.cjs +108 -0
- package/dist/chunk-5TQ6AVQH.cjs.map +1 -0
- package/dist/{chunk-LEXIU2MH.cjs → chunk-HK7OXB2W.cjs} +74 -7
- package/dist/chunk-HK7OXB2W.cjs.map +1 -0
- package/dist/{chunk-JO5LGOB6.js → chunk-ILUTAHUJ.js} +74 -8
- package/dist/chunk-ILUTAHUJ.js.map +1 -0
- package/dist/{chunk-WZXUKJ7E.cjs → chunk-MTMEW6MM.cjs} +2 -106
- package/dist/chunk-MTMEW6MM.cjs.map +1 -0
- package/dist/chunk-O6EI77LD.js +106 -0
- package/dist/chunk-O6EI77LD.js.map +1 -0
- package/dist/{chunk-7EXSMPGR.js → chunk-YDMGV72W.js} +3 -106
- package/dist/chunk-YDMGV72W.js.map +1 -0
- package/dist/core/index.cjs +7 -2
- package/dist/core/index.d.cts +116 -102
- package/dist/core/index.d.ts +116 -102
- package/dist/core/index.js +2 -1
- package/dist/define-surface-D6XPtoqu.d.cts +151 -0
- package/dist/define-surface-DXBop-uB.d.ts +151 -0
- package/dist/index.cjs +10 -5
- package/dist/index.d.cts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -2
- package/dist/react/index.d.cts +1 -1
- package/dist/react/index.d.ts +1 -1
- 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-CiWvkaz_.d.ts +78 -0
- package/dist/websocket-CrI0yRNo.d.cts +78 -0
- package/package.json +4 -3
- package/dist/chunk-7EXSMPGR.js.map +0 -1
- package/dist/chunk-JO5LGOB6.js.map +0 -1
- package/dist/chunk-LEXIU2MH.cjs.map +0 -1
- package/dist/chunk-WZXUKJ7E.cjs.map +0 -1
package/README.md
CHANGED
|
@@ -118,26 +118,42 @@ export const counterSurface = defineSurface({
|
|
|
118
118
|
});
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
+
Then instantiate OUI. **`createOUI` is the only supported integration path** — it owns the wire so you never implement the protocol yourself:
|
|
122
|
+
|
|
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
|
|
128
|
+
|
|
129
|
+
export const oui = createOUI({
|
|
130
|
+
socket, // you own the connection
|
|
131
|
+
surfaces: [counterSurface], // you own what exists
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
await oui.start(); // registers every surface and connects
|
|
135
|
+
```
|
|
136
|
+
|
|
121
137
|
```tsx
|
|
122
138
|
// CounterPage.tsx
|
|
123
139
|
import { useSurface, useObservation } from "oui-spec/react";
|
|
124
140
|
import { counterSurface } from "./counter.surface";
|
|
141
|
+
import { oui } from "./oui";
|
|
125
142
|
|
|
126
|
-
function CounterPage(
|
|
143
|
+
function CounterPage() {
|
|
127
144
|
const [count, setCount] = useState(0);
|
|
128
145
|
|
|
129
|
-
|
|
146
|
+
useSurface({
|
|
130
147
|
surface: counterSurface,
|
|
131
148
|
context: { count, setCount },
|
|
132
|
-
send: transport.send,
|
|
149
|
+
send: oui.transport.send,
|
|
133
150
|
});
|
|
134
151
|
|
|
135
|
-
// Push observation whenever count changes
|
|
136
152
|
useObservation(
|
|
137
153
|
counterSurface.id,
|
|
138
154
|
"current_value",
|
|
139
155
|
{ value: count },
|
|
140
|
-
transport.send,
|
|
156
|
+
oui.transport.send,
|
|
141
157
|
);
|
|
142
158
|
|
|
143
159
|
return <div>Count: {count}</div>;
|
|
@@ -148,6 +164,67 @@ That's it. The agent now sees a `counter` surface with two tools (`increment`, `
|
|
|
148
164
|
|
|
149
165
|
---
|
|
150
166
|
|
|
167
|
+
## The Integration Contract
|
|
168
|
+
|
|
169
|
+
OUI owns the protocol. You own the connection and the content. That split is
|
|
170
|
+
enforced by the type system, not by documentation.
|
|
171
|
+
|
|
172
|
+
### What OUI owns — and you cannot reimplement
|
|
173
|
+
|
|
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 |
|
|
180
|
+
|
|
181
|
+
`OUIInstance.transport` is a branded `OwnedTransport`. Only `createOUI`
|
|
182
|
+
produces one, so a transport you construct by hand cannot be substituted for
|
|
183
|
+
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.
|
|
188
|
+
|
|
189
|
+
### What you own
|
|
190
|
+
|
|
191
|
+
| | Owned by the integrator |
|
|
192
|
+
| ------------------- | -------------------------------------------- |
|
|
193
|
+
| Connection | The `socket` — auth, reconnection, lifecycle |
|
|
194
|
+
| Surfaces | What exists, via `defineSurface` |
|
|
195
|
+
| Action schemas | `input` / `output` JSON Schema per action |
|
|
196
|
+
| Observation schemas | `schema` per observation |
|
|
197
|
+
| Handlers | What actually executes |
|
|
198
|
+
|
|
199
|
+
### Requirements enforced at compile time
|
|
200
|
+
|
|
201
|
+
`createOUI` will not compile unless every requirement is met:
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
createOUI({ socket, surfaces: [alpha, alpha] });
|
|
205
|
+
// ✗ Duplicate surface id: "alpha". Registration is keyed by id,
|
|
206
|
+
// so the second would silently replace the first.
|
|
207
|
+
|
|
208
|
+
createOUI({ socket, surfaces: [] });
|
|
209
|
+
// ✗ createOUI requires at least one surface. An instance with no
|
|
210
|
+
// surfaces registers nothing and dispatches nowhere.
|
|
211
|
+
|
|
212
|
+
createOUI({ surfaces: [alpha] });
|
|
213
|
+
// ✗ Property 'socket' is missing.
|
|
214
|
+
|
|
215
|
+
createOUI({ socket, surfaces: [{ id: "x", name: "X" /* ... */ }] });
|
|
216
|
+
// ✗ Property '[OUI_BRAND]' is missing — surfaces must come from defineSurface.
|
|
217
|
+
|
|
218
|
+
const mine: OwnedTransport = createWebSocketTransport(socket);
|
|
219
|
+
// ✗ Property '[OUI_BRAND]' is missing in type 'OUITransport'.
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Duplicate action and observation ids within a surface are validated by
|
|
223
|
+
`defineSurface`. Surface-level rules are checked by the type system, so they
|
|
224
|
+
fail in your editor rather than at render time in a browser after a deploy.
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
151
228
|
## Architecture
|
|
152
229
|
|
|
153
230
|
### Two One-Way Channels
|
|
@@ -564,19 +641,25 @@ The agent can orchestrate the full wizard: select data → configure chart → f
|
|
|
564
641
|
The `oui-spec` package exposes subpath exports for granular imports:
|
|
565
642
|
|
|
566
643
|
```typescript
|
|
567
|
-
import { defineSurface } from "oui-spec/core";
|
|
568
|
-
import { useSurface } from "oui-spec/react";
|
|
569
|
-
import {
|
|
644
|
+
import { createOUI, defineSurface } from "oui-spec/core";
|
|
645
|
+
import { useSurface, useObservation } from "oui-spec/react";
|
|
646
|
+
import type { SocketLike } from "oui-spec/transport";
|
|
570
647
|
import type { OUISurface, OUIAction } from "oui-spec/spec";
|
|
571
648
|
|
|
572
649
|
// Or import everything from the root
|
|
573
|
-
import { defineSurface, useSurface
|
|
650
|
+
import { createOUI, defineSurface, useSurface } from "oui-spec";
|
|
574
651
|
```
|
|
575
652
|
|
|
653
|
+
`oui-spec/transport` also exports `createWebSocketTransport` and
|
|
654
|
+
`createDirectTransportPair`. These exist for tests and for embedding OUI in a
|
|
655
|
+
runtime that owns its own wiring — they are **not** the integration path, and
|
|
656
|
+
they return a plain `OUITransport` rather than the branded `OwnedTransport` that
|
|
657
|
+
OUI's own APIs accept. If you are integrating an application, use `createOUI`.
|
|
658
|
+
|
|
576
659
|
| Subpath | Description |
|
|
577
660
|
| -------------------- | ---------------------------------------------------------------------------- |
|
|
578
661
|
| `oui-spec/spec` | TypeScript types + JSON Schema for the OUI specification. Zero runtime deps. |
|
|
579
|
-
| `oui-spec/core` | `defineSurface()`, manifest extraction, action execution
|
|
662
|
+
| `oui-spec/core` | `createOUI()`, `defineSurface()`, manifest extraction, action execution. |
|
|
580
663
|
| `oui-spec/react` | `useSurface()` hook, `useObservation()` helper. React bindings. |
|
|
581
664
|
| `oui-spec/transport` | Two-channel transport layer. WebSocket (Socket.IO), direct (in-memory). |
|
|
582
665
|
|
|
@@ -632,6 +715,7 @@ They even share JSON Schema for parameter validation — a deliberate design cho
|
|
|
632
715
|
8. **Consent-based.** The app controls what's exposed. Users and developers decide what agents can do.
|
|
633
716
|
9. **Composable.** Multiple surfaces coexist. A complex app exposes many surfaces (one per feature/page).
|
|
634
717
|
10. **Complementary.** OUI doesn't replace OpenAPI, MCP, or any other standard. It fills the "UI control" gap they leave open.
|
|
718
|
+
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
719
|
|
|
636
720
|
---
|
|
637
721
|
|
|
@@ -646,6 +730,7 @@ They even share JSON Schema for parameter validation — a deliberate design cho
|
|
|
646
730
|
- [x] `oui-spec/core` — `defineSurface()` + manifest extraction
|
|
647
731
|
- [x] `oui-spec/react` — `useSurface()` hook + observation helpers
|
|
648
732
|
- [x] `oui-spec/transport` — WebSocket + Direct transports
|
|
733
|
+
- [x] `createOUI()` — gated instantiation; integration requirements enforced at compile time
|
|
649
734
|
- [ ] `oui-spec/devtools` — Surface inspector / debugger
|
|
650
735
|
- [ ] `oui-spec/vue` — Vue bindings
|
|
651
736
|
- [ ] `oui-spec/validator` — Runtime schema validation
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/transport/direct.ts
|
|
4
|
+
function createDirectTransportPair() {
|
|
5
|
+
const actionHandlers = [];
|
|
6
|
+
const observationHandlers = [];
|
|
7
|
+
const surfaceRegisterHandlers = [];
|
|
8
|
+
const surfaceDeregisterHandlers = [];
|
|
9
|
+
const server = {
|
|
10
|
+
dispatch(surfaceId, actionId, params) {
|
|
11
|
+
actionHandlers.forEach((h) => h(surfaceId, actionId, params));
|
|
12
|
+
},
|
|
13
|
+
onAction() {
|
|
14
|
+
return () => {
|
|
15
|
+
};
|
|
16
|
+
},
|
|
17
|
+
// Server doesn't receive dispatches
|
|
18
|
+
pushObservation() {
|
|
19
|
+
},
|
|
20
|
+
// Server doesn't push observations
|
|
21
|
+
onObservation(handler) {
|
|
22
|
+
observationHandlers.push(handler);
|
|
23
|
+
return () => {
|
|
24
|
+
const i = observationHandlers.indexOf(handler);
|
|
25
|
+
if (i >= 0) observationHandlers.splice(i, 1);
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
registerSurface() {
|
|
29
|
+
},
|
|
30
|
+
deregisterSurface() {
|
|
31
|
+
},
|
|
32
|
+
onSurfaceRegister(handler) {
|
|
33
|
+
surfaceRegisterHandlers.push(handler);
|
|
34
|
+
return () => {
|
|
35
|
+
const i = surfaceRegisterHandlers.indexOf(handler);
|
|
36
|
+
if (i >= 0) surfaceRegisterHandlers.splice(i, 1);
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
onSurfaceDeregister(handler) {
|
|
40
|
+
surfaceDeregisterHandlers.push(handler);
|
|
41
|
+
return () => {
|
|
42
|
+
const i = surfaceDeregisterHandlers.indexOf(handler);
|
|
43
|
+
if (i >= 0) surfaceDeregisterHandlers.splice(i, 1);
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
get connected() {
|
|
47
|
+
return true;
|
|
48
|
+
},
|
|
49
|
+
async connect() {
|
|
50
|
+
},
|
|
51
|
+
disconnect() {
|
|
52
|
+
},
|
|
53
|
+
onConnectionChange() {
|
|
54
|
+
return () => {
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const client = {
|
|
59
|
+
dispatch() {
|
|
60
|
+
},
|
|
61
|
+
// Client doesn't dispatch
|
|
62
|
+
onAction(handler) {
|
|
63
|
+
actionHandlers.push(handler);
|
|
64
|
+
return () => {
|
|
65
|
+
const i = actionHandlers.indexOf(handler);
|
|
66
|
+
if (i >= 0) actionHandlers.splice(i, 1);
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
pushObservation(update) {
|
|
70
|
+
observationHandlers.forEach((h) => h(update));
|
|
71
|
+
},
|
|
72
|
+
onObservation() {
|
|
73
|
+
return () => {
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
// Client doesn't receive observations
|
|
77
|
+
registerSurface(surface) {
|
|
78
|
+
surfaceRegisterHandlers.forEach((h) => h(surface));
|
|
79
|
+
},
|
|
80
|
+
deregisterSurface(surfaceId) {
|
|
81
|
+
surfaceDeregisterHandlers.forEach((h) => h(surfaceId));
|
|
82
|
+
},
|
|
83
|
+
onSurfaceRegister() {
|
|
84
|
+
return () => {
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
onSurfaceDeregister() {
|
|
88
|
+
return () => {
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
get connected() {
|
|
92
|
+
return true;
|
|
93
|
+
},
|
|
94
|
+
async connect() {
|
|
95
|
+
},
|
|
96
|
+
disconnect() {
|
|
97
|
+
},
|
|
98
|
+
onConnectionChange() {
|
|
99
|
+
return () => {
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
return { server, client };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
exports.createDirectTransportPair = createDirectTransportPair;
|
|
107
|
+
//# sourceMappingURL=chunk-5TQ6AVQH.cjs.map
|
|
108
|
+
//# sourceMappingURL=chunk-5TQ6AVQH.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/transport/direct.ts"],"names":[],"mappings":";;;AAWO,SAAS,yBAAA,GAA4E;AAC1F,EAAA,MAAM,iBAAqC,EAAC;AAC5C,EAAA,MAAM,sBAA+C,EAAC;AACtD,EAAA,MAAM,0BAA0D,EAAC;AACjE,EAAA,MAAM,4BAAyD,EAAC;AAEhE,EAAA,MAAM,MAAA,GAAuB;AAAA,IAC3B,QAAA,CAAS,SAAA,EAAW,QAAA,EAAU,MAAA,EAAQ;AAEpC,MAAA,cAAA,CAAe,QAAQ,CAAA,CAAA,KAAK,CAAA,CAAE,SAAA,EAAW,QAAA,EAAU,MAAM,CAAC,CAAA;AAAA,IAC5D,CAAA;AAAA,IACA,QAAA,GAAW;AAAE,MAAA,OAAO,MAAM;AAAA,MAAC,CAAA;AAAA,IAAG,CAAA;AAAA;AAAA,IAC9B,eAAA,GAAkB;AAAA,IAAC,CAAA;AAAA;AAAA,IACnB,cAAc,OAAA,EAAS;AACrB,MAAA,mBAAA,CAAoB,KAAK,OAAO,CAAA;AAChC,MAAA,OAAO,MAAM;AAAE,QAAA,MAAM,CAAA,GAAI,mBAAA,CAAoB,OAAA,CAAQ,OAAO,CAAA;AAAG,QAAA,IAAI,CAAA,IAAK,CAAA,EAAG,mBAAA,CAAoB,MAAA,CAAO,GAAG,CAAC,CAAA;AAAA,MAAG,CAAA;AAAA,IAC/G,CAAA;AAAA,IACA,eAAA,GAAkB;AAAA,IAAC,CAAA;AAAA,IACnB,iBAAA,GAAoB;AAAA,IAAC,CAAA;AAAA,IACrB,kBAAkB,OAAA,EAAS;AACzB,MAAA,uBAAA,CAAwB,KAAK,OAAO,CAAA;AACpC,MAAA,OAAO,MAAM;AAAE,QAAA,MAAM,CAAA,GAAI,uBAAA,CAAwB,OAAA,CAAQ,OAAO,CAAA;AAAG,QAAA,IAAI,CAAA,IAAK,CAAA,EAAG,uBAAA,CAAwB,MAAA,CAAO,GAAG,CAAC,CAAA;AAAA,MAAG,CAAA;AAAA,IACvH,CAAA;AAAA,IACA,oBAAoB,OAAA,EAAS;AAC3B,MAAA,yBAAA,CAA0B,KAAK,OAAO,CAAA;AACtC,MAAA,OAAO,MAAM;AAAE,QAAA,MAAM,CAAA,GAAI,yBAAA,CAA0B,OAAA,CAAQ,OAAO,CAAA;AAAG,QAAA,IAAI,CAAA,IAAK,CAAA,EAAG,yBAAA,CAA0B,MAAA,CAAO,GAAG,CAAC,CAAA;AAAA,MAAG,CAAA;AAAA,IAC3H,CAAA;AAAA,IACA,IAAI,SAAA,GAAY;AAAE,MAAA,OAAO,IAAA;AAAA,IAAM,CAAA;AAAA,IAC/B,MAAM,OAAA,GAAU;AAAA,IAAC,CAAA;AAAA,IACjB,UAAA,GAAa;AAAA,IAAC,CAAA;AAAA,IACd,kBAAA,GAAqB;AAAE,MAAA,OAAO,MAAM;AAAA,MAAC,CAAA;AAAA,IAAG;AAAA,GAC1C;AAEA,EAAA,MAAM,MAAA,GAAuB;AAAA,IAC3B,QAAA,GAAW;AAAA,IAAC,CAAA;AAAA;AAAA,IACZ,SAAS,OAAA,EAAS;AAChB,MAAA,cAAA,CAAe,KAAK,OAAO,CAAA;AAC3B,MAAA,OAAO,MAAM;AAAE,QAAA,MAAM,CAAA,GAAI,cAAA,CAAe,OAAA,CAAQ,OAAO,CAAA;AAAG,QAAA,IAAI,CAAA,IAAK,CAAA,EAAG,cAAA,CAAe,MAAA,CAAO,GAAG,CAAC,CAAA;AAAA,MAAG,CAAA;AAAA,IACrG,CAAA;AAAA,IACA,gBAAgB,MAAA,EAA8B;AAE5C,MAAA,mBAAA,CAAoB,OAAA,CAAQ,CAAA,CAAA,KAAK,CAAA,CAAE,MAAM,CAAC,CAAA;AAAA,IAC5C,CAAA;AAAA,IACA,aAAA,GAAgB;AAAE,MAAA,OAAO,MAAM;AAAA,MAAC,CAAA;AAAA,IAAG,CAAA;AAAA;AAAA,IACnC,gBAAgB,OAAA,EAAS;AACvB,MAAA,uBAAA,CAAwB,OAAA,CAAQ,CAAA,CAAA,KAAK,CAAA,CAAE,OAAO,CAAC,CAAA;AAAA,IACjD,CAAA;AAAA,IACA,kBAAkB,SAAA,EAAW;AAC3B,MAAA,yBAAA,CAA0B,OAAA,CAAQ,CAAA,CAAA,KAAK,CAAA,CAAE,SAAS,CAAC,CAAA;AAAA,IACrD,CAAA;AAAA,IACA,iBAAA,GAAoB;AAAE,MAAA,OAAO,MAAM;AAAA,MAAC,CAAA;AAAA,IAAG,CAAA;AAAA,IACvC,mBAAA,GAAsB;AAAE,MAAA,OAAO,MAAM;AAAA,MAAC,CAAA;AAAA,IAAG,CAAA;AAAA,IACzC,IAAI,SAAA,GAAY;AAAE,MAAA,OAAO,IAAA;AAAA,IAAM,CAAA;AAAA,IAC/B,MAAM,OAAA,GAAU;AAAA,IAAC,CAAA;AAAA,IACjB,UAAA,GAAa;AAAA,IAAC,CAAA;AAAA,IACd,kBAAA,GAAqB;AAAE,MAAA,OAAO,MAAM;AAAA,MAAC,CAAA;AAAA,IAAG;AAAA,GAC1C;AAEA,EAAA,OAAO,EAAE,QAAQ,MAAA,EAAO;AAC1B","file":"chunk-5TQ6AVQH.cjs","sourcesContent":["import type { OUITransport, OUIActionHandler, OUIObservationHandler } from './types.js';\nimport type { OUISurface, OUIObservationUpdate } from '../spec/index.js';\n\n/**\n * Direct transport — for same-process communication.\n * Used for API surfaces (where the agent runtime and surface are in the same Lambda)\n * and for testing (where you want synchronous, in-memory communication).\n *\n * Creates a paired (server, client) transport where messages sent on one\n * side are immediately received on the other.\n */\nexport function createDirectTransportPair(): { server: OUITransport; client: OUITransport } {\n const actionHandlers: OUIActionHandler[] = [];\n const observationHandlers: OUIObservationHandler[] = [];\n const surfaceRegisterHandlers: Array<(s: OUISurface) => void> = [];\n const surfaceDeregisterHandlers: Array<(id: string) => void> = [];\n\n const server: OUITransport = {\n dispatch(surfaceId, actionId, params) {\n // Server dispatches → client receives\n actionHandlers.forEach(h => h(surfaceId, actionId, params));\n },\n onAction() { return () => {}; }, // Server doesn't receive dispatches\n pushObservation() {}, // Server doesn't push observations\n onObservation(handler) {\n observationHandlers.push(handler);\n return () => { const i = observationHandlers.indexOf(handler); if (i >= 0) observationHandlers.splice(i, 1); };\n },\n registerSurface() {},\n deregisterSurface() {},\n onSurfaceRegister(handler) {\n surfaceRegisterHandlers.push(handler);\n return () => { const i = surfaceRegisterHandlers.indexOf(handler); if (i >= 0) surfaceRegisterHandlers.splice(i, 1); };\n },\n onSurfaceDeregister(handler) {\n surfaceDeregisterHandlers.push(handler);\n return () => { const i = surfaceDeregisterHandlers.indexOf(handler); if (i >= 0) surfaceDeregisterHandlers.splice(i, 1); };\n },\n get connected() { return true; },\n async connect() {},\n disconnect() {},\n onConnectionChange() { return () => {}; },\n };\n\n const client: OUITransport = {\n dispatch() {}, // Client doesn't dispatch\n onAction(handler) {\n actionHandlers.push(handler);\n return () => { const i = actionHandlers.indexOf(handler); if (i >= 0) actionHandlers.splice(i, 1); };\n },\n pushObservation(update: OUIObservationUpdate) {\n // Client pushes → server receives\n observationHandlers.forEach(h => h(update));\n },\n onObservation() { return () => {}; }, // Client doesn't receive observations\n registerSurface(surface) {\n surfaceRegisterHandlers.forEach(h => h(surface));\n },\n deregisterSurface(surfaceId) {\n surfaceDeregisterHandlers.forEach(h => h(surfaceId));\n },\n onSurfaceRegister() { return () => {}; },\n onSurfaceDeregister() { return () => {}; },\n get connected() { return true; },\n async connect() {},\n disconnect() {},\n onConnectionChange() { return () => {}; },\n };\n\n return { server, client };\n}\n"]}
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var chunkMTMEW6MM_cjs = require('./chunk-MTMEW6MM.cjs');
|
|
4
|
+
|
|
5
|
+
// src/core/brand.ts
|
|
6
|
+
function brand(value) {
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
|
|
3
10
|
// src/core/define-surface.ts
|
|
4
11
|
function defineSurface(definition) {
|
|
5
12
|
const actionIds = /* @__PURE__ */ new Set();
|
|
6
13
|
for (const action of definition.actions) {
|
|
7
14
|
if (actionIds.has(action.id)) {
|
|
8
|
-
throw new Error(
|
|
15
|
+
throw new Error(
|
|
16
|
+
`[OUI] Duplicate action ID "${action.id}" in surface "${definition.id}"`
|
|
17
|
+
);
|
|
9
18
|
}
|
|
10
19
|
actionIds.add(action.id);
|
|
11
20
|
}
|
|
@@ -13,12 +22,14 @@ function defineSurface(definition) {
|
|
|
13
22
|
const obsIds = /* @__PURE__ */ new Set();
|
|
14
23
|
for (const obs of definition.observations) {
|
|
15
24
|
if (obsIds.has(obs.id)) {
|
|
16
|
-
throw new Error(
|
|
25
|
+
throw new Error(
|
|
26
|
+
`[OUI] Duplicate observation ID "${obs.id}" in surface "${definition.id}"`
|
|
27
|
+
);
|
|
17
28
|
}
|
|
18
29
|
obsIds.add(obs.id);
|
|
19
30
|
}
|
|
20
31
|
}
|
|
21
|
-
return {
|
|
32
|
+
return brand({
|
|
22
33
|
...definition,
|
|
23
34
|
/** Extract the manifest (schema only, no handlers) for transmission to the agent runtime */
|
|
24
35
|
toManifest() {
|
|
@@ -37,7 +48,13 @@ function defineSurface(definition) {
|
|
|
37
48
|
async executeAction(actionId, params, context) {
|
|
38
49
|
const action = definition.actions.find((a) => a.id === actionId);
|
|
39
50
|
if (!action) {
|
|
40
|
-
return {
|
|
51
|
+
return {
|
|
52
|
+
success: false,
|
|
53
|
+
error: {
|
|
54
|
+
code: "ACTION_NOT_FOUND",
|
|
55
|
+
message: `Action "${actionId}" not found on surface "${definition.id}"`
|
|
56
|
+
}
|
|
57
|
+
};
|
|
41
58
|
}
|
|
42
59
|
try {
|
|
43
60
|
return await action.handler(params, context);
|
|
@@ -60,7 +77,7 @@ function defineSurface(definition) {
|
|
|
60
77
|
const action = definition.actions.find((a) => a.id === actionId);
|
|
61
78
|
return action?.polling;
|
|
62
79
|
}
|
|
63
|
-
};
|
|
80
|
+
});
|
|
64
81
|
}
|
|
65
82
|
function actionToManifest(action) {
|
|
66
83
|
return {
|
|
@@ -85,6 +102,56 @@ function obsToManifest(obs) {
|
|
|
85
102
|
};
|
|
86
103
|
}
|
|
87
104
|
|
|
105
|
+
// src/core/create-oui.ts
|
|
106
|
+
function createOUI(config) {
|
|
107
|
+
const {
|
|
108
|
+
socket,
|
|
109
|
+
surfaces,
|
|
110
|
+
transport: transportConfig
|
|
111
|
+
} = config;
|
|
112
|
+
if (!socket) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
"[OUI] createOUI requires a socket. OUI owns the wire; the integrator owns the connection."
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
if (!Array.isArray(surfaces) || surfaces.length === 0) {
|
|
118
|
+
throw new Error("[OUI] createOUI requires at least one surface.");
|
|
119
|
+
}
|
|
120
|
+
const seen = /* @__PURE__ */ new Set();
|
|
121
|
+
for (const surface of surfaces) {
|
|
122
|
+
if (seen.has(surface.id)) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`[OUI] Duplicate surface id "${surface.id}". Registration is keyed by id, so the second would replace the first.`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
seen.add(surface.id);
|
|
128
|
+
}
|
|
129
|
+
const transport = brand(
|
|
130
|
+
chunkMTMEW6MM_cjs.createWebSocketTransport(socket, transportConfig)
|
|
131
|
+
);
|
|
132
|
+
const byId = new Map(surfaces.map((s) => [s.id, s]));
|
|
133
|
+
return {
|
|
134
|
+
transport,
|
|
135
|
+
manifests: surfaces.map((s) => s.toManifest()),
|
|
136
|
+
async start() {
|
|
137
|
+
await transport.connect();
|
|
138
|
+
for (const surface of surfaces) {
|
|
139
|
+
transport.registerSurface(surface.toManifest());
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
stop() {
|
|
143
|
+
for (const surface of surfaces) {
|
|
144
|
+
transport.deregisterSurface(surface.id);
|
|
145
|
+
}
|
|
146
|
+
transport.disconnect();
|
|
147
|
+
},
|
|
148
|
+
getSurface(id) {
|
|
149
|
+
return byId.get(id);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
exports.createOUI = createOUI;
|
|
88
155
|
exports.defineSurface = defineSurface;
|
|
89
|
-
//# sourceMappingURL=chunk-
|
|
90
|
-
//# sourceMappingURL=chunk-
|
|
156
|
+
//# sourceMappingURL=chunk-HK7OXB2W.cjs.map
|
|
157
|
+
//# sourceMappingURL=chunk-HK7OXB2W.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/brand.ts","../src/core/define-surface.ts","../src/core/create-oui.ts"],"names":["createWebSocketTransport"],"mappings":"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","file":"chunk-HK7OXB2W.cjs","sourcesContent":["/**\n * Nominal branding for values OUI must construct itself.\n *\n * Structural typing means any object with the right shape satisfies an\n * interface. That is how Closure Studio ended up hand-rolling OUI's transport:\n * `SocketLike` was importable, `OUITransport` was a plain interface, so an\n * integrator could satisfy every type without ever calling OUI. The result was\n * a second implementation of the wire — hardcoded namespace, re-declared\n * payload types on the server, and a dead `action:result` mapping alongside the\n * observation channel that already carried results.\n *\n * A unique symbol cannot be produced outside this module. Branding the types\n * OUI owns turns \"you should use our constructor\" into \"you cannot compile\n * without it\".\n */\n\ndeclare const OUI_BRAND: unique symbol;\n\n/** Marks a value as constructed by OUI itself. */\nexport interface OUIBranded<TKind extends string> {\n readonly [OUI_BRAND]: TKind;\n}\n\n/**\n * Attach the brand at runtime. The property does not exist at runtime — the\n * symbol is declare-only — so this is a type-level assertion with no cost.\n */\nexport function brand<TKind extends string, T>(\n value: T,\n): T & OUIBranded<TKind> {\n return value as T & OUIBranded<TKind>;\n}\n","import type {\n OUISurface,\n OUIAction,\n OUIObservation,\n OUIActivation,\n OUIActionPolling,\n JSONSchema,\n} from \"../spec/index.js\";\nimport { brand, type OUIBranded } from \"./brand.js\";\n\n/**\n * Configuration for defining an OUI surface with handlers.\n * This is the \"integration file\" an app provides — it declares both\n * the schema (what the agent sees) and the implementation (what executes).\n */\nexport interface SurfaceDefinition<TContext = unknown> {\n id: string;\n name: string;\n description: string;\n version?: string;\n activation?: OUIActivation;\n actions: ActionDefinition<TContext>[];\n observations?: ObservationDefinition[];\n metadata?: Record<string, unknown>;\n}\n\n/**\n * Polling config with an optional resolve handler.\n * The spec's OUIActionPolling is the manifest shape (no functions).\n * The core extends it with the resolve implementation for the runtime.\n */\nexport interface ActionPollingConfig<\n TContext = unknown,\n> extends OUIActionPolling {\n /**\n * Called on each poll interval. Receives the dispatch result from the handler\n * plus the current context. Return { done: true, data } to stop polling,\n * or { done: false, data } to continue.\n */\n resolve?: (\n dispatchResult: unknown,\n context: TContext,\n ) => Promise<{ done: boolean; data: unknown }>;\n}\n\n/**\n * An action definition includes both the schema (for the agent) and\n * the handler (for execution). This is the single source of truth —\n * no separate YAML, no separate listener, no drift possible.\n */\nexport interface ActionDefinition<TContext = unknown> {\n id: string;\n description: string;\n input: JSONSchema;\n output?: JSONSchema;\n confirm?: boolean;\n async?: boolean;\n\n /**\n * Polling/subscription config for async actions.\n * After the handler returns, the OUI runtime starts polling or subscribing\n * and pushes observation updates until the operation completes.\n * Extends the spec's OUIActionPolling with an optional resolve handler.\n */\n polling?: ActionPollingConfig<TContext>;\n\n usage?: string;\n preconditions?: string;\n estimatedDuration?: string;\n tags?: string[];\n\n /**\n * The handler that executes when the agent invokes this action.\n * Receives typed params and a context object (app-specific state/utilities).\n * Returns a result that the agent sees as the tool output.\n */\n handler: (\n params: Record<string, unknown>,\n context: TContext,\n ) => Promise<ActionHandlerResult>;\n}\n\nexport interface ActionHandlerResult {\n success: boolean;\n data?: unknown;\n error?: { code: string; message: string; details?: unknown };\n /** For async actions: metadata passed to the polling resolver */\n dispatchMeta?: Record<string, unknown>;\n}\n\nexport interface ObservationDefinition {\n id: string;\n description: string;\n schema: JSONSchema;\n /** Function that returns the current observation value */\n getValue?: () => unknown;\n}\n\n/**\n * Define an OUI surface — the single entry point for integrating\n * an application feature with the OUI protocol.\n *\n * This function validates the definition and returns a typed surface\n * object that can be registered with an agent runtime or transport layer.\n *\n * @example\n * ```typescript\n * const mySurface = defineSurface({\n * id: 'my-feature',\n * name: 'My Feature',\n * description: 'Does something cool',\n * actions: [\n * {\n * id: 'do_thing',\n * description: 'Does the thing',\n * input: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },\n * handler: async (params) => {\n * const result = await doTheThing(params.name);\n * return { success: true, data: result };\n * },\n * },\n * ],\n * });\n * ```\n */\nexport function defineSurface<\n TContext = unknown,\n const TId extends string = string,\n>(\n definition: SurfaceDefinition<TContext> & { id: TId },\n): DefinedSurface<TContext> & { id: TId } {\n // Validate: no duplicate action IDs\n const actionIds = new Set<string>();\n for (const action of definition.actions) {\n if (actionIds.has(action.id)) {\n throw new Error(\n `[OUI] Duplicate action ID \"${action.id}\" in surface \"${definition.id}\"`,\n );\n }\n actionIds.add(action.id);\n }\n\n // Validate: no duplicate observation IDs\n if (definition.observations) {\n const obsIds = new Set<string>();\n for (const obs of definition.observations) {\n if (obsIds.has(obs.id)) {\n throw new Error(\n `[OUI] Duplicate observation ID \"${obs.id}\" in surface \"${definition.id}\"`,\n );\n }\n obsIds.add(obs.id);\n }\n }\n\n return brand<\"surface\", unknown>({\n ...definition,\n\n /** Extract the manifest (schema only, no handlers) for transmission to the agent runtime */\n toManifest(): OUISurface {\n return {\n id: definition.id,\n name: definition.name,\n description: definition.description,\n version: definition.version,\n activation: definition.activation,\n actions: definition.actions.map(actionToManifest),\n observations: definition.observations?.map(obsToManifest),\n metadata: definition.metadata,\n };\n },\n\n /** Execute an action by ID with the given params and context */\n async executeAction(\n actionId: string,\n params: Record<string, unknown>,\n context: TContext,\n ): Promise<ActionHandlerResult> {\n const action = definition.actions.find((a) => a.id === actionId);\n if (!action) {\n return {\n success: false,\n error: {\n code: \"ACTION_NOT_FOUND\",\n message: `Action \"${actionId}\" not found on surface \"${definition.id}\"`,\n },\n };\n }\n try {\n return await action.handler(params, context);\n } catch (err) {\n return {\n success: false,\n error: {\n code: \"ACTION_EXECUTION_ERROR\",\n message: err instanceof Error ? err.message : String(err),\n },\n };\n }\n },\n\n /** Get all action IDs */\n getActionIds(): string[] {\n return definition.actions.map((a) => a.id);\n },\n\n /** Get the polling config for an action (if async) */\n getPollingConfig(actionId: string): OUIActionPolling | undefined {\n const action = definition.actions.find((a) => a.id === actionId);\n return action?.polling;\n },\n }) as DefinedSurface<TContext> & { id: TId };\n}\n\n/** A fully defined surface with both manifest extraction and execution capabilities */\n/**\n * Branded so it can only come from `defineSurface`.\n *\n * Without the brand a plain object literal satisfies this interface\n * structurally, which is how an integrator can assemble a surface that never\n * ran defineSurface's duplicate-id checks and never produced a real manifest.\n */\nexport interface DefinedSurface<TContext = unknown>\n extends SurfaceDefinition<TContext>, OUIBranded<\"surface\"> {\n toManifest(): OUISurface;\n executeAction(\n actionId: string,\n params: Record<string, unknown>,\n context: TContext,\n ): Promise<ActionHandlerResult>;\n getActionIds(): string[];\n /** Get the polling config for an action (if async) */\n getPollingConfig(actionId: string): OUIActionPolling | undefined;\n}\n\n// ─── Internal helpers ────────────────────────────────────────────────────────\n\nfunction actionToManifest<T>(action: ActionDefinition<T>): OUIAction {\n return {\n id: action.id,\n description: action.description,\n input: action.input,\n output: action.output,\n confirm: action.confirm,\n async: action.async,\n polling: action.polling,\n usage: action.usage,\n preconditions: action.preconditions,\n estimatedDuration: action.estimatedDuration,\n tags: action.tags,\n };\n}\n\nfunction obsToManifest(obs: ObservationDefinition): OUIObservation {\n return {\n id: obs.id,\n description: obs.description,\n schema: obs.schema,\n };\n}\n","/**\n * createOUI — the only supported way to instantiate OUI.\n *\n * WHY THIS EXISTS\n *\n * OUI shipped a correct protocol and a correct transport, and its first\n * integrator used neither. studio-ui imported only the `SocketLike` TYPE from\n * oui-spec and rebuilt the wire by hand: a literal map of protocol types to\n * socket names, `oui:` hardcoded so the configurable namespace was unreachable,\n * payload interfaces re-declared on the server, and an `action:result` channel\n * mapped but never sent — duplicating the observation channel that already\n * carries results.\n *\n * Nothing was wrong with the spec. Nothing enforced it. Structural typing meant\n * every hand-rolled piece type-checked, and both ends agreed only by the\n * coincidence of matching string literals. Rename one and the other keeps\n * compiling and silently stops receiving.\n *\n * So the requirements are gated here:\n *\n * OUI owns the wire. The integrator provides a socket; it cannot provide a\n * transport, because `OUITransport` values are branded and only this module\n * brands them.\n *\n * OUI owns the result path. Results return through the observation channel,\n * as `use-surface` already implements. An integrator cannot add a parallel\n * channel because it cannot construct a transport to add one to.\n *\n * The integrator owns content. Surfaces, action schemas, observation schemas\n * and handlers all arrive through `defineSurface`, whose output is branded —\n * so a plain object shaped like a surface will not compile.\n *\n * Violations fail at compile time with a message naming the offending id.\n */\nimport type { OUITransport, OUITransportConfig } from \"../transport/types.js\";\nimport type { SocketLike } from \"../transport/websocket.js\";\nimport { createWebSocketTransport } from \"../transport/websocket.js\";\nimport type { DefinedSurface } from \"./define-surface.js\";\nimport { brand, type OUIBranded } from \"./brand.js\";\nimport type { OUIConfigError, ValidateSurfaceIds } from \"./validate.js\";\nimport type { OUISurface } from \"../spec/index.js\";\n\n/** A transport OUI constructed. Integrators cannot produce one. */\nexport type OwnedTransport = OUITransport & OUIBranded<\"transport\">;\n\n/** Surface ids as literal types, for the duplicate check. */\ntype SurfaceIds<T extends readonly DefinedSurface<never>[]> = {\n [K in keyof T]: T[K][\"id\"];\n};\n\nexport interface OUIConfig<TSurfaces extends readonly DefinedSurface<never>[]> {\n /**\n * The socket OUI will speak over. The integrator owns the connection —\n * auth, reconnection, lifecycle — and OUI owns what travels on it.\n */\n socket: SocketLike;\n\n /**\n * Surfaces produced by `defineSurface`. A plain object with the same shape\n * will not compile: DefinedSurface is branded.\n */\n surfaces: TSurfaces;\n\n /**\n * Wire configuration. `namespace` defaults to 'oui' and is the supported way\n * to change the event prefix — hardcoding it at the call site is what made\n * this option unreachable in the first integration.\n */\n transport?: Omit<OUITransportConfig, \"namespace\"> & { namespace?: string };\n}\n\n/** A live OUI instance. */\nexport interface OUIInstance {\n /** The transport OUI owns. Exposed for wiring, not for reimplementation. */\n readonly transport: OwnedTransport;\n\n /** Manifests for every registered surface, as the agent sees them. */\n readonly manifests: readonly OUISurface[];\n\n /** Register every surface and connect. */\n start(): Promise<void>;\n\n /** Deregister every surface and disconnect. */\n stop(): void;\n\n /** Look up a registered surface by id. */\n getSurface(id: string): DefinedSurface<never> | undefined;\n}\n\n/**\n * Instantiate OUI.\n *\n * `const` on the type parameter captures surface ids as literal types, which is\n * what allows duplicate detection to name the offending id rather than failing\n * with an unassignable-to-never error.\n */\nexport function createOUI<\n const TSurfaces extends readonly DefinedSurface<never>[],\n>(\n config: OUIConfig<TSurfaces> & ValidateSurfaceIds<SurfaceIds<TSurfaces>>,\n): OUIInstance {\n const {\n socket,\n surfaces,\n transport: transportConfig,\n } = config as OUIConfig<TSurfaces>;\n\n // Defensive, not decorative: the compile-time checks only bind when a caller\n // passes literals. A surface list assembled at runtime, or JS with no types\n // at all, reaches here unchecked.\n if (!socket) {\n throw new Error(\n \"[OUI] createOUI requires a socket. OUI owns the wire; the integrator owns the connection.\",\n );\n }\n if (!Array.isArray(surfaces) || surfaces.length === 0) {\n throw new Error(\"[OUI] createOUI requires at least one surface.\");\n }\n\n const seen = new Set<string>();\n for (const surface of surfaces) {\n if (seen.has(surface.id)) {\n throw new Error(\n `[OUI] Duplicate surface id \"${surface.id}\". Registration is keyed by id, so the second would replace the first.`,\n );\n }\n seen.add(surface.id);\n }\n\n const transport = brand<\"transport\", OUITransport>(\n createWebSocketTransport(socket, transportConfig),\n );\n\n const byId = new Map(surfaces.map((s) => [s.id, s] as const));\n\n return {\n transport,\n manifests: surfaces.map((s) => s.toManifest()),\n\n async start() {\n await transport.connect();\n for (const surface of surfaces) {\n transport.registerSurface(surface.toManifest());\n }\n },\n\n stop() {\n for (const surface of surfaces) {\n transport.deregisterSurface(surface.id);\n }\n transport.disconnect();\n },\n\n getSurface(id) {\n return byId.get(id);\n },\n };\n}\n\n/** Re-exported so integrators can name the error type in tests. */\nexport type { OUIConfigError };\n"]}
|
|
@@ -1,9 +1,18 @@
|
|
|
1
|
+
import { createWebSocketTransport } from './chunk-YDMGV72W.js';
|
|
2
|
+
|
|
3
|
+
// src/core/brand.ts
|
|
4
|
+
function brand(value) {
|
|
5
|
+
return value;
|
|
6
|
+
}
|
|
7
|
+
|
|
1
8
|
// src/core/define-surface.ts
|
|
2
9
|
function defineSurface(definition) {
|
|
3
10
|
const actionIds = /* @__PURE__ */ new Set();
|
|
4
11
|
for (const action of definition.actions) {
|
|
5
12
|
if (actionIds.has(action.id)) {
|
|
6
|
-
throw new Error(
|
|
13
|
+
throw new Error(
|
|
14
|
+
`[OUI] Duplicate action ID "${action.id}" in surface "${definition.id}"`
|
|
15
|
+
);
|
|
7
16
|
}
|
|
8
17
|
actionIds.add(action.id);
|
|
9
18
|
}
|
|
@@ -11,12 +20,14 @@ function defineSurface(definition) {
|
|
|
11
20
|
const obsIds = /* @__PURE__ */ new Set();
|
|
12
21
|
for (const obs of definition.observations) {
|
|
13
22
|
if (obsIds.has(obs.id)) {
|
|
14
|
-
throw new Error(
|
|
23
|
+
throw new Error(
|
|
24
|
+
`[OUI] Duplicate observation ID "${obs.id}" in surface "${definition.id}"`
|
|
25
|
+
);
|
|
15
26
|
}
|
|
16
27
|
obsIds.add(obs.id);
|
|
17
28
|
}
|
|
18
29
|
}
|
|
19
|
-
return {
|
|
30
|
+
return brand({
|
|
20
31
|
...definition,
|
|
21
32
|
/** Extract the manifest (schema only, no handlers) for transmission to the agent runtime */
|
|
22
33
|
toManifest() {
|
|
@@ -35,7 +46,13 @@ function defineSurface(definition) {
|
|
|
35
46
|
async executeAction(actionId, params, context) {
|
|
36
47
|
const action = definition.actions.find((a) => a.id === actionId);
|
|
37
48
|
if (!action) {
|
|
38
|
-
return {
|
|
49
|
+
return {
|
|
50
|
+
success: false,
|
|
51
|
+
error: {
|
|
52
|
+
code: "ACTION_NOT_FOUND",
|
|
53
|
+
message: `Action "${actionId}" not found on surface "${definition.id}"`
|
|
54
|
+
}
|
|
55
|
+
};
|
|
39
56
|
}
|
|
40
57
|
try {
|
|
41
58
|
return await action.handler(params, context);
|
|
@@ -58,7 +75,7 @@ function defineSurface(definition) {
|
|
|
58
75
|
const action = definition.actions.find((a) => a.id === actionId);
|
|
59
76
|
return action?.polling;
|
|
60
77
|
}
|
|
61
|
-
};
|
|
78
|
+
});
|
|
62
79
|
}
|
|
63
80
|
function actionToManifest(action) {
|
|
64
81
|
return {
|
|
@@ -83,6 +100,55 @@ function obsToManifest(obs) {
|
|
|
83
100
|
};
|
|
84
101
|
}
|
|
85
102
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
103
|
+
// src/core/create-oui.ts
|
|
104
|
+
function createOUI(config) {
|
|
105
|
+
const {
|
|
106
|
+
socket,
|
|
107
|
+
surfaces,
|
|
108
|
+
transport: transportConfig
|
|
109
|
+
} = config;
|
|
110
|
+
if (!socket) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
"[OUI] createOUI requires a socket. OUI owns the wire; the integrator owns the connection."
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
if (!Array.isArray(surfaces) || surfaces.length === 0) {
|
|
116
|
+
throw new Error("[OUI] createOUI requires at least one surface.");
|
|
117
|
+
}
|
|
118
|
+
const seen = /* @__PURE__ */ new Set();
|
|
119
|
+
for (const surface of surfaces) {
|
|
120
|
+
if (seen.has(surface.id)) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`[OUI] Duplicate surface id "${surface.id}". Registration is keyed by id, so the second would replace the first.`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
seen.add(surface.id);
|
|
126
|
+
}
|
|
127
|
+
const transport = brand(
|
|
128
|
+
createWebSocketTransport(socket, transportConfig)
|
|
129
|
+
);
|
|
130
|
+
const byId = new Map(surfaces.map((s) => [s.id, s]));
|
|
131
|
+
return {
|
|
132
|
+
transport,
|
|
133
|
+
manifests: surfaces.map((s) => s.toManifest()),
|
|
134
|
+
async start() {
|
|
135
|
+
await transport.connect();
|
|
136
|
+
for (const surface of surfaces) {
|
|
137
|
+
transport.registerSurface(surface.toManifest());
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
stop() {
|
|
141
|
+
for (const surface of surfaces) {
|
|
142
|
+
transport.deregisterSurface(surface.id);
|
|
143
|
+
}
|
|
144
|
+
transport.disconnect();
|
|
145
|
+
},
|
|
146
|
+
getSurface(id) {
|
|
147
|
+
return byId.get(id);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export { createOUI, defineSurface };
|
|
153
|
+
//# sourceMappingURL=chunk-ILUTAHUJ.js.map
|
|
154
|
+
//# sourceMappingURL=chunk-ILUTAHUJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/brand.ts","../src/core/define-surface.ts","../src/core/create-oui.ts"],"names":[],"mappings":"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","file":"chunk-ILUTAHUJ.js","sourcesContent":["/**\n * Nominal branding for values OUI must construct itself.\n *\n * Structural typing means any object with the right shape satisfies an\n * interface. That is how Closure Studio ended up hand-rolling OUI's transport:\n * `SocketLike` was importable, `OUITransport` was a plain interface, so an\n * integrator could satisfy every type without ever calling OUI. The result was\n * a second implementation of the wire — hardcoded namespace, re-declared\n * payload types on the server, and a dead `action:result` mapping alongside the\n * observation channel that already carried results.\n *\n * A unique symbol cannot be produced outside this module. Branding the types\n * OUI owns turns \"you should use our constructor\" into \"you cannot compile\n * without it\".\n */\n\ndeclare const OUI_BRAND: unique symbol;\n\n/** Marks a value as constructed by OUI itself. */\nexport interface OUIBranded<TKind extends string> {\n readonly [OUI_BRAND]: TKind;\n}\n\n/**\n * Attach the brand at runtime. The property does not exist at runtime — the\n * symbol is declare-only — so this is a type-level assertion with no cost.\n */\nexport function brand<TKind extends string, T>(\n value: T,\n): T & OUIBranded<TKind> {\n return value as T & OUIBranded<TKind>;\n}\n","import type {\n OUISurface,\n OUIAction,\n OUIObservation,\n OUIActivation,\n OUIActionPolling,\n JSONSchema,\n} from \"../spec/index.js\";\nimport { brand, type OUIBranded } from \"./brand.js\";\n\n/**\n * Configuration for defining an OUI surface with handlers.\n * This is the \"integration file\" an app provides — it declares both\n * the schema (what the agent sees) and the implementation (what executes).\n */\nexport interface SurfaceDefinition<TContext = unknown> {\n id: string;\n name: string;\n description: string;\n version?: string;\n activation?: OUIActivation;\n actions: ActionDefinition<TContext>[];\n observations?: ObservationDefinition[];\n metadata?: Record<string, unknown>;\n}\n\n/**\n * Polling config with an optional resolve handler.\n * The spec's OUIActionPolling is the manifest shape (no functions).\n * The core extends it with the resolve implementation for the runtime.\n */\nexport interface ActionPollingConfig<\n TContext = unknown,\n> extends OUIActionPolling {\n /**\n * Called on each poll interval. Receives the dispatch result from the handler\n * plus the current context. Return { done: true, data } to stop polling,\n * or { done: false, data } to continue.\n */\n resolve?: (\n dispatchResult: unknown,\n context: TContext,\n ) => Promise<{ done: boolean; data: unknown }>;\n}\n\n/**\n * An action definition includes both the schema (for the agent) and\n * the handler (for execution). This is the single source of truth —\n * no separate YAML, no separate listener, no drift possible.\n */\nexport interface ActionDefinition<TContext = unknown> {\n id: string;\n description: string;\n input: JSONSchema;\n output?: JSONSchema;\n confirm?: boolean;\n async?: boolean;\n\n /**\n * Polling/subscription config for async actions.\n * After the handler returns, the OUI runtime starts polling or subscribing\n * and pushes observation updates until the operation completes.\n * Extends the spec's OUIActionPolling with an optional resolve handler.\n */\n polling?: ActionPollingConfig<TContext>;\n\n usage?: string;\n preconditions?: string;\n estimatedDuration?: string;\n tags?: string[];\n\n /**\n * The handler that executes when the agent invokes this action.\n * Receives typed params and a context object (app-specific state/utilities).\n * Returns a result that the agent sees as the tool output.\n */\n handler: (\n params: Record<string, unknown>,\n context: TContext,\n ) => Promise<ActionHandlerResult>;\n}\n\nexport interface ActionHandlerResult {\n success: boolean;\n data?: unknown;\n error?: { code: string; message: string; details?: unknown };\n /** For async actions: metadata passed to the polling resolver */\n dispatchMeta?: Record<string, unknown>;\n}\n\nexport interface ObservationDefinition {\n id: string;\n description: string;\n schema: JSONSchema;\n /** Function that returns the current observation value */\n getValue?: () => unknown;\n}\n\n/**\n * Define an OUI surface — the single entry point for integrating\n * an application feature with the OUI protocol.\n *\n * This function validates the definition and returns a typed surface\n * object that can be registered with an agent runtime or transport layer.\n *\n * @example\n * ```typescript\n * const mySurface = defineSurface({\n * id: 'my-feature',\n * name: 'My Feature',\n * description: 'Does something cool',\n * actions: [\n * {\n * id: 'do_thing',\n * description: 'Does the thing',\n * input: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },\n * handler: async (params) => {\n * const result = await doTheThing(params.name);\n * return { success: true, data: result };\n * },\n * },\n * ],\n * });\n * ```\n */\nexport function defineSurface<\n TContext = unknown,\n const TId extends string = string,\n>(\n definition: SurfaceDefinition<TContext> & { id: TId },\n): DefinedSurface<TContext> & { id: TId } {\n // Validate: no duplicate action IDs\n const actionIds = new Set<string>();\n for (const action of definition.actions) {\n if (actionIds.has(action.id)) {\n throw new Error(\n `[OUI] Duplicate action ID \"${action.id}\" in surface \"${definition.id}\"`,\n );\n }\n actionIds.add(action.id);\n }\n\n // Validate: no duplicate observation IDs\n if (definition.observations) {\n const obsIds = new Set<string>();\n for (const obs of definition.observations) {\n if (obsIds.has(obs.id)) {\n throw new Error(\n `[OUI] Duplicate observation ID \"${obs.id}\" in surface \"${definition.id}\"`,\n );\n }\n obsIds.add(obs.id);\n }\n }\n\n return brand<\"surface\", unknown>({\n ...definition,\n\n /** Extract the manifest (schema only, no handlers) for transmission to the agent runtime */\n toManifest(): OUISurface {\n return {\n id: definition.id,\n name: definition.name,\n description: definition.description,\n version: definition.version,\n activation: definition.activation,\n actions: definition.actions.map(actionToManifest),\n observations: definition.observations?.map(obsToManifest),\n metadata: definition.metadata,\n };\n },\n\n /** Execute an action by ID with the given params and context */\n async executeAction(\n actionId: string,\n params: Record<string, unknown>,\n context: TContext,\n ): Promise<ActionHandlerResult> {\n const action = definition.actions.find((a) => a.id === actionId);\n if (!action) {\n return {\n success: false,\n error: {\n code: \"ACTION_NOT_FOUND\",\n message: `Action \"${actionId}\" not found on surface \"${definition.id}\"`,\n },\n };\n }\n try {\n return await action.handler(params, context);\n } catch (err) {\n return {\n success: false,\n error: {\n code: \"ACTION_EXECUTION_ERROR\",\n message: err instanceof Error ? err.message : String(err),\n },\n };\n }\n },\n\n /** Get all action IDs */\n getActionIds(): string[] {\n return definition.actions.map((a) => a.id);\n },\n\n /** Get the polling config for an action (if async) */\n getPollingConfig(actionId: string): OUIActionPolling | undefined {\n const action = definition.actions.find((a) => a.id === actionId);\n return action?.polling;\n },\n }) as DefinedSurface<TContext> & { id: TId };\n}\n\n/** A fully defined surface with both manifest extraction and execution capabilities */\n/**\n * Branded so it can only come from `defineSurface`.\n *\n * Without the brand a plain object literal satisfies this interface\n * structurally, which is how an integrator can assemble a surface that never\n * ran defineSurface's duplicate-id checks and never produced a real manifest.\n */\nexport interface DefinedSurface<TContext = unknown>\n extends SurfaceDefinition<TContext>, OUIBranded<\"surface\"> {\n toManifest(): OUISurface;\n executeAction(\n actionId: string,\n params: Record<string, unknown>,\n context: TContext,\n ): Promise<ActionHandlerResult>;\n getActionIds(): string[];\n /** Get the polling config for an action (if async) */\n getPollingConfig(actionId: string): OUIActionPolling | undefined;\n}\n\n// ─── Internal helpers ────────────────────────────────────────────────────────\n\nfunction actionToManifest<T>(action: ActionDefinition<T>): OUIAction {\n return {\n id: action.id,\n description: action.description,\n input: action.input,\n output: action.output,\n confirm: action.confirm,\n async: action.async,\n polling: action.polling,\n usage: action.usage,\n preconditions: action.preconditions,\n estimatedDuration: action.estimatedDuration,\n tags: action.tags,\n };\n}\n\nfunction obsToManifest(obs: ObservationDefinition): OUIObservation {\n return {\n id: obs.id,\n description: obs.description,\n schema: obs.schema,\n };\n}\n","/**\n * createOUI — the only supported way to instantiate OUI.\n *\n * WHY THIS EXISTS\n *\n * OUI shipped a correct protocol and a correct transport, and its first\n * integrator used neither. studio-ui imported only the `SocketLike` TYPE from\n * oui-spec and rebuilt the wire by hand: a literal map of protocol types to\n * socket names, `oui:` hardcoded so the configurable namespace was unreachable,\n * payload interfaces re-declared on the server, and an `action:result` channel\n * mapped but never sent — duplicating the observation channel that already\n * carries results.\n *\n * Nothing was wrong with the spec. Nothing enforced it. Structural typing meant\n * every hand-rolled piece type-checked, and both ends agreed only by the\n * coincidence of matching string literals. Rename one and the other keeps\n * compiling and silently stops receiving.\n *\n * So the requirements are gated here:\n *\n * OUI owns the wire. The integrator provides a socket; it cannot provide a\n * transport, because `OUITransport` values are branded and only this module\n * brands them.\n *\n * OUI owns the result path. Results return through the observation channel,\n * as `use-surface` already implements. An integrator cannot add a parallel\n * channel because it cannot construct a transport to add one to.\n *\n * The integrator owns content. Surfaces, action schemas, observation schemas\n * and handlers all arrive through `defineSurface`, whose output is branded —\n * so a plain object shaped like a surface will not compile.\n *\n * Violations fail at compile time with a message naming the offending id.\n */\nimport type { OUITransport, OUITransportConfig } from \"../transport/types.js\";\nimport type { SocketLike } from \"../transport/websocket.js\";\nimport { createWebSocketTransport } from \"../transport/websocket.js\";\nimport type { DefinedSurface } from \"./define-surface.js\";\nimport { brand, type OUIBranded } from \"./brand.js\";\nimport type { OUIConfigError, ValidateSurfaceIds } from \"./validate.js\";\nimport type { OUISurface } from \"../spec/index.js\";\n\n/** A transport OUI constructed. Integrators cannot produce one. */\nexport type OwnedTransport = OUITransport & OUIBranded<\"transport\">;\n\n/** Surface ids as literal types, for the duplicate check. */\ntype SurfaceIds<T extends readonly DefinedSurface<never>[]> = {\n [K in keyof T]: T[K][\"id\"];\n};\n\nexport interface OUIConfig<TSurfaces extends readonly DefinedSurface<never>[]> {\n /**\n * The socket OUI will speak over. The integrator owns the connection —\n * auth, reconnection, lifecycle — and OUI owns what travels on it.\n */\n socket: SocketLike;\n\n /**\n * Surfaces produced by `defineSurface`. A plain object with the same shape\n * will not compile: DefinedSurface is branded.\n */\n surfaces: TSurfaces;\n\n /**\n * Wire configuration. `namespace` defaults to 'oui' and is the supported way\n * to change the event prefix — hardcoding it at the call site is what made\n * this option unreachable in the first integration.\n */\n transport?: Omit<OUITransportConfig, \"namespace\"> & { namespace?: string };\n}\n\n/** A live OUI instance. */\nexport interface OUIInstance {\n /** The transport OUI owns. Exposed for wiring, not for reimplementation. */\n readonly transport: OwnedTransport;\n\n /** Manifests for every registered surface, as the agent sees them. */\n readonly manifests: readonly OUISurface[];\n\n /** Register every surface and connect. */\n start(): Promise<void>;\n\n /** Deregister every surface and disconnect. */\n stop(): void;\n\n /** Look up a registered surface by id. */\n getSurface(id: string): DefinedSurface<never> | undefined;\n}\n\n/**\n * Instantiate OUI.\n *\n * `const` on the type parameter captures surface ids as literal types, which is\n * what allows duplicate detection to name the offending id rather than failing\n * with an unassignable-to-never error.\n */\nexport function createOUI<\n const TSurfaces extends readonly DefinedSurface<never>[],\n>(\n config: OUIConfig<TSurfaces> & ValidateSurfaceIds<SurfaceIds<TSurfaces>>,\n): OUIInstance {\n const {\n socket,\n surfaces,\n transport: transportConfig,\n } = config as OUIConfig<TSurfaces>;\n\n // Defensive, not decorative: the compile-time checks only bind when a caller\n // passes literals. A surface list assembled at runtime, or JS with no types\n // at all, reaches here unchecked.\n if (!socket) {\n throw new Error(\n \"[OUI] createOUI requires a socket. OUI owns the wire; the integrator owns the connection.\",\n );\n }\n if (!Array.isArray(surfaces) || surfaces.length === 0) {\n throw new Error(\"[OUI] createOUI requires at least one surface.\");\n }\n\n const seen = new Set<string>();\n for (const surface of surfaces) {\n if (seen.has(surface.id)) {\n throw new Error(\n `[OUI] Duplicate surface id \"${surface.id}\". Registration is keyed by id, so the second would replace the first.`,\n );\n }\n seen.add(surface.id);\n }\n\n const transport = brand<\"transport\", OUITransport>(\n createWebSocketTransport(socket, transportConfig),\n );\n\n const byId = new Map(surfaces.map((s) => [s.id, s] as const));\n\n return {\n transport,\n manifests: surfaces.map((s) => s.toManifest()),\n\n async start() {\n await transport.connect();\n for (const surface of surfaces) {\n transport.registerSurface(surface.toManifest());\n }\n },\n\n stop() {\n for (const surface of surfaces) {\n transport.deregisterSurface(surface.id);\n }\n transport.disconnect();\n },\n\n getSurface(id) {\n return byId.get(id);\n },\n };\n}\n\n/** Re-exported so integrators can name the error type in tests. */\nexport type { OUIConfigError };\n"]}
|