oui-spec 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +94 -122
  2. package/dist/{chunk-CVUUSPF4.js → chunk-BMCL2K74.js} +2 -2
  3. package/dist/chunk-BMCL2K74.js.map +1 -0
  4. package/dist/{chunk-MTMEW6MM.cjs → chunk-DCVVTP3V.cjs} +72 -23
  5. package/dist/chunk-DCVVTP3V.cjs.map +1 -0
  6. package/dist/chunk-DGMKSJTY.cjs +573 -0
  7. package/dist/chunk-DGMKSJTY.cjs.map +1 -0
  8. package/dist/chunk-F3SKRAGU.cjs +98 -0
  9. package/dist/chunk-F3SKRAGU.cjs.map +1 -0
  10. package/dist/{chunk-YDMGV72W.js → chunk-F3WKIRAU.js} +72 -23
  11. package/dist/chunk-F3WKIRAU.js.map +1 -0
  12. package/dist/chunk-GXTNZ44I.js +569 -0
  13. package/dist/chunk-GXTNZ44I.js.map +1 -0
  14. package/dist/chunk-IIZ6HL2L.js +89 -0
  15. package/dist/chunk-IIZ6HL2L.js.map +1 -0
  16. package/dist/chunk-NYCJZQSP.js +92 -0
  17. package/dist/chunk-NYCJZQSP.js.map +1 -0
  18. package/dist/chunk-ROGKEMLL.cjs +91 -0
  19. package/dist/chunk-ROGKEMLL.cjs.map +1 -0
  20. package/dist/{chunk-3DCIL4FV.cjs → chunk-ZKM4XWAB.cjs} +2 -2
  21. package/dist/chunk-ZKM4XWAB.cjs.map +1 -0
  22. package/dist/core/index.cjs +10 -6
  23. package/dist/core/index.d.cts +15 -8
  24. package/dist/core/index.d.ts +15 -8
  25. package/dist/core/index.js +3 -3
  26. package/dist/index.cjs +28 -12
  27. package/dist/index.d.cts +5 -4
  28. package/dist/index.d.ts +5 -4
  29. package/dist/index.js +5 -5
  30. package/dist/react/index.cjs +15 -3
  31. package/dist/react/index.d.cts +33 -50
  32. package/dist/react/index.d.ts +33 -50
  33. package/dist/react/index.js +1 -1
  34. package/dist/spec/index.cjs +2 -2
  35. package/dist/spec/index.d.cts +35 -7
  36. package/dist/spec/index.d.ts +35 -7
  37. package/dist/spec/index.js +1 -1
  38. package/dist/{define-surface-D6XPtoqu.d.cts → surface-runtime-BzIKgL_I.d.cts} +102 -2
  39. package/dist/{define-surface-DXBop-uB.d.ts → surface-runtime-DeByfbKg.d.ts} +102 -2
  40. package/dist/transport/index.cjs +4 -4
  41. package/dist/transport/index.d.cts +2 -2
  42. package/dist/transport/index.d.ts +2 -2
  43. package/dist/transport/index.js +2 -2
  44. package/dist/{websocket-CrI0yRNo.d.cts → websocket-YfWILdZ4.d.cts} +36 -18
  45. package/dist/{websocket-CiWvkaz_.d.ts → websocket-c5tIUWY5.d.ts} +36 -18
  46. package/package.json +5 -1
  47. package/spec/OUI-SPEC-v0.1.md +229 -155
  48. package/dist/chunk-2ON5IEXB.cjs +0 -167
  49. package/dist/chunk-2ON5IEXB.cjs.map +0 -1
  50. package/dist/chunk-3DCIL4FV.cjs.map +0 -1
  51. package/dist/chunk-5TQ6AVQH.cjs +0 -108
  52. package/dist/chunk-5TQ6AVQH.cjs.map +0 -1
  53. package/dist/chunk-CVUUSPF4.js.map +0 -1
  54. package/dist/chunk-HK7OXB2W.cjs +0 -157
  55. package/dist/chunk-HK7OXB2W.cjs.map +0 -1
  56. package/dist/chunk-ILUTAHUJ.js +0 -154
  57. package/dist/chunk-ILUTAHUJ.js.map +0 -1
  58. package/dist/chunk-MTMEW6MM.cjs.map +0 -1
  59. package/dist/chunk-O6EI77LD.js +0 -106
  60. package/dist/chunk-O6EI77LD.js.map +0 -1
  61. package/dist/chunk-UIGRLZOX.js +0 -164
  62. package/dist/chunk-UIGRLZOX.js.map +0 -1
  63. package/dist/chunk-YDMGV72W.js.map +0 -1
@@ -0,0 +1,569 @@
1
+ import { createWebSocketTransport } from './chunk-F3WKIRAU.js';
2
+
3
+ // src/core/brand.ts
4
+ function brand(value) {
5
+ return value;
6
+ }
7
+
8
+ // src/core/define-surface.ts
9
+ function defineSurface(definition) {
10
+ const actionIds = /* @__PURE__ */ new Set();
11
+ for (const action of definition.actions) {
12
+ if (actionIds.has(action.id)) {
13
+ throw new Error(
14
+ `[OUI] Duplicate action ID "${action.id}" in surface "${definition.id}"`
15
+ );
16
+ }
17
+ actionIds.add(action.id);
18
+ }
19
+ if (definition.observations) {
20
+ const obsIds = /* @__PURE__ */ new Set();
21
+ for (const obs of definition.observations) {
22
+ if (obsIds.has(obs.id)) {
23
+ throw new Error(
24
+ `[OUI] Duplicate observation ID "${obs.id}" in surface "${definition.id}"`
25
+ );
26
+ }
27
+ obsIds.add(obs.id);
28
+ }
29
+ }
30
+ return brand({
31
+ ...definition,
32
+ /** Extract the manifest (schema only, no handlers) for transmission to the agent runtime */
33
+ toManifest() {
34
+ return {
35
+ id: definition.id,
36
+ name: definition.name,
37
+ description: definition.description,
38
+ version: definition.version,
39
+ activation: definition.activation,
40
+ actions: definition.actions.map(actionToManifest),
41
+ observations: definition.observations?.map(obsToManifest),
42
+ metadata: definition.metadata
43
+ };
44
+ },
45
+ /** Execute an action by ID with the given params and context */
46
+ async executeAction(actionId, params, context) {
47
+ const action = definition.actions.find((a) => a.id === actionId);
48
+ if (!action) {
49
+ return {
50
+ success: false,
51
+ error: {
52
+ code: "ACTION_NOT_FOUND",
53
+ message: `Action "${actionId}" not found on surface "${definition.id}"`
54
+ }
55
+ };
56
+ }
57
+ try {
58
+ return await action.handler(params, context);
59
+ } catch (err) {
60
+ return {
61
+ success: false,
62
+ error: {
63
+ code: "ACTION_EXECUTION_ERROR",
64
+ message: err instanceof Error ? err.message : String(err)
65
+ }
66
+ };
67
+ }
68
+ },
69
+ /** Get all action IDs */
70
+ getActionIds() {
71
+ return definition.actions.map((a) => a.id);
72
+ },
73
+ /** Get the polling config for an action (if async) */
74
+ getPollingConfig(actionId) {
75
+ const action = definition.actions.find((a) => a.id === actionId);
76
+ return action?.polling;
77
+ }
78
+ });
79
+ }
80
+ function actionToManifest(action) {
81
+ return {
82
+ id: action.id,
83
+ description: action.description,
84
+ input: action.input,
85
+ output: action.output,
86
+ confirm: action.confirm,
87
+ async: action.async,
88
+ polling: action.polling,
89
+ usage: action.usage,
90
+ preconditions: action.preconditions,
91
+ estimatedDuration: action.estimatedDuration,
92
+ tags: action.tags
93
+ };
94
+ }
95
+ function obsToManifest(obs) {
96
+ return {
97
+ id: obs.id,
98
+ description: obs.description,
99
+ schema: obs.schema
100
+ };
101
+ }
102
+
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.dispose();
145
+ },
146
+ getSurface(id) {
147
+ return byId.get(id);
148
+ }
149
+ };
150
+ }
151
+
152
+ // src/core/surface-runtime.ts
153
+ var DEFAULT_QUIET_MS = 250;
154
+ var DEFAULT_SETTLE_TIMEOUT_MS = 5e3;
155
+ var DEFAULT_DEDUPE_WINDOW = 500;
156
+ var SETTLE_TICK_MS = 50;
157
+ function createSurfaceRuntime(options = {}) {
158
+ const announce = options.announce ?? true;
159
+ const quietMs = options.settle?.quietMs ?? DEFAULT_QUIET_MS;
160
+ const settleTimeoutMs = options.settle?.timeoutMs ?? DEFAULT_SETTLE_TIMEOUT_MS;
161
+ const dedupeWindow = options.dedupeWindow ?? DEFAULT_DEDUPE_WINDOW;
162
+ let nextKey = 1;
163
+ const entries = [];
164
+ const observations = /* @__PURE__ */ new Map();
165
+ const serializedObservations = /* @__PURE__ */ new Map();
166
+ const holds = /* @__PURE__ */ new Set();
167
+ let nextHold = 1;
168
+ const results = /* @__PURE__ */ new Map();
169
+ const pollers = /* @__PURE__ */ new Map();
170
+ const listeners = /* @__PURE__ */ new Set();
171
+ const acks = /* @__PURE__ */ new Map();
172
+ function ackFor(requestId) {
173
+ let ack = acks.get(requestId);
174
+ if (!ack) {
175
+ let resolve;
176
+ const promise = new Promise((r) => resolve = r);
177
+ ack = { promise, resolve };
178
+ acks.set(requestId, ack);
179
+ }
180
+ return ack;
181
+ }
182
+ function acknowledged(requestId) {
183
+ return ackFor(requestId).promise;
184
+ }
185
+ let lastChangeAt = Date.now();
186
+ let socket = null;
187
+ let transport = null;
188
+ let detachTransport = [];
189
+ function changed(settles = true) {
190
+ if (settles) lastChangeAt = Date.now();
191
+ for (const l of [...listeners]) l();
192
+ }
193
+ function activeFor(surfaceId) {
194
+ for (let i = entries.length - 1; i >= 0; i--) {
195
+ if (entries[i].surface.id === surfaceId) return entries[i];
196
+ }
197
+ return void 0;
198
+ }
199
+ function activeEntries() {
200
+ const seen = /* @__PURE__ */ new Set();
201
+ const out = [];
202
+ for (const e of entries) {
203
+ if (seen.has(e.surface.id)) continue;
204
+ seen.add(e.surface.id);
205
+ out.push(activeFor(e.surface.id));
206
+ }
207
+ return out;
208
+ }
209
+ function announceSurface(entry) {
210
+ if (!announce || !transport) return;
211
+ transport.registerSurface(entry.surface.toManifest());
212
+ const obs = observations.get(entry.surface.id);
213
+ if (obs) {
214
+ for (const [observationId, value] of obs) {
215
+ transport.pushObservation({
216
+ surfaceId: entry.surface.id,
217
+ observationId,
218
+ value,
219
+ timestamp: Date.now()
220
+ });
221
+ }
222
+ }
223
+ }
224
+ function announceAll() {
225
+ for (const e of activeEntries()) announceSurface(e);
226
+ }
227
+ function setObservation(surfaceId, observationId, value, settles = true) {
228
+ const key = `${surfaceId}\0${observationId}`;
229
+ let serialized;
230
+ try {
231
+ serialized = JSON.stringify(value) ?? "undefined";
232
+ } catch {
233
+ serialized = String(value);
234
+ }
235
+ if (serializedObservations.get(key) === serialized) return;
236
+ serializedObservations.set(key, serialized);
237
+ let obs = observations.get(surfaceId);
238
+ if (!obs) {
239
+ obs = /* @__PURE__ */ new Map();
240
+ observations.set(surfaceId, obs);
241
+ }
242
+ obs.set(observationId, toJsonSafe(value));
243
+ if (announce && transport) {
244
+ transport.pushObservation({
245
+ surfaceId,
246
+ observationId,
247
+ value,
248
+ timestamp: Date.now()
249
+ });
250
+ }
251
+ changed(settles);
252
+ }
253
+ function forgetSurface(surfaceId) {
254
+ observations.delete(surfaceId);
255
+ for (const key of [...serializedObservations.keys()]) {
256
+ if (key.startsWith(`${surfaceId}\0`))
257
+ serializedObservations.delete(key);
258
+ }
259
+ for (const [key, timer] of pollers) {
260
+ if (key.startsWith(`${surfaceId}\0`)) {
261
+ clearInterval(timer);
262
+ pollers.delete(key);
263
+ }
264
+ }
265
+ }
266
+ function startPolling(requestId, surfaceId, actionId, polling, dispatchResult) {
267
+ const finish = async (success, data, error) => {
268
+ await acknowledged(requestId);
269
+ const snap = snapshot();
270
+ transport?.sendResult({
271
+ requestId,
272
+ success,
273
+ ...data !== void 0 ? { data: toJsonSafe(data) } : {},
274
+ ...error ? { error } : {},
275
+ interim: false,
276
+ timestamp: Date.now(),
277
+ surfaces: snap.surfaces,
278
+ observations: snap.observations
279
+ });
280
+ acks.delete(requestId);
281
+ };
282
+ const observationId = `${surfaceId}:${actionId}:status`;
283
+ const pollerKey = `${surfaceId}\0${actionId}`;
284
+ if (polling.subscribe) {
285
+ setObservation(
286
+ surfaceId,
287
+ observationId,
288
+ { status: "dispatched", dispatchResult, interim: true },
289
+ false
290
+ );
291
+ return;
292
+ }
293
+ if (!polling.resolve) {
294
+ setObservation(
295
+ surfaceId,
296
+ observationId,
297
+ { status: "dispatched", dispatchResult, interim: false },
298
+ false
299
+ );
300
+ return;
301
+ }
302
+ const existing = pollers.get(pollerKey);
303
+ if (existing) clearInterval(existing);
304
+ setObservation(
305
+ surfaceId,
306
+ observationId,
307
+ { status: "polling", dispatchResult, interim: true },
308
+ false
309
+ );
310
+ const maxAttempts = polling.maxAttempts ?? Infinity;
311
+ const maxDuration = polling.maxDurationMs ?? Infinity;
312
+ const startedAt = Date.now();
313
+ let attempts = 0;
314
+ const timer = setInterval(async () => {
315
+ attempts++;
316
+ if (attempts > maxAttempts || Date.now() - startedAt > maxDuration) {
317
+ clearInterval(timer);
318
+ pollers.delete(pollerKey);
319
+ setObservation(
320
+ surfaceId,
321
+ observationId,
322
+ { status: "timeout", dispatchResult, interim: false },
323
+ false
324
+ );
325
+ void finish(false, void 0, {
326
+ code: "TIMEOUT",
327
+ message: `"${actionId}" did not finish within its polling limits.`
328
+ });
329
+ return;
330
+ }
331
+ const entry = activeFor(surfaceId);
332
+ const action = entry?.surface.actions.find((a) => a.id === actionId);
333
+ if (!entry || !action?.polling?.resolve) {
334
+ clearInterval(timer);
335
+ pollers.delete(pollerKey);
336
+ return;
337
+ }
338
+ try {
339
+ const r = await action.polling.resolve(
340
+ dispatchResult,
341
+ entry.getContext()
342
+ );
343
+ setObservation(
344
+ surfaceId,
345
+ observationId,
346
+ {
347
+ ...r.data,
348
+ interim: !r.done
349
+ },
350
+ false
351
+ );
352
+ if (r.done) {
353
+ clearInterval(timer);
354
+ pollers.delete(pollerKey);
355
+ void finish(true, r.data);
356
+ }
357
+ } catch (err) {
358
+ setObservation(
359
+ surfaceId,
360
+ observationId,
361
+ { status: "poll_error", error: String(err), interim: true },
362
+ false
363
+ );
364
+ }
365
+ }, polling.intervalMs);
366
+ pollers.set(pollerKey, timer);
367
+ }
368
+ async function waitUntilSettled(since) {
369
+ const deadline = since + settleTimeoutMs;
370
+ for (; ; ) {
371
+ const now = Date.now();
372
+ const quietFor = now - Math.max(since, lastChangeAt);
373
+ if (holds.size === 0 && quietFor >= quietMs) return true;
374
+ if (now >= deadline) return false;
375
+ await sleep(Math.min(SETTLE_TICK_MS, deadline - now));
376
+ }
377
+ }
378
+ async function run(request) {
379
+ const startedAt = Date.now();
380
+ const entry = activeFor(request.surfaceId);
381
+ let result;
382
+ let interim = false;
383
+ if (!entry) {
384
+ const mounted = activeEntries().map((e) => e.surface.id);
385
+ result = {
386
+ success: false,
387
+ error: {
388
+ code: "SURFACE_NOT_MOUNTED",
389
+ message: `Surface "${request.surfaceId}" is not on screen. Mounted surfaces: ${mounted.length ? mounted.join(", ") : "none"}.`
390
+ }
391
+ };
392
+ } else {
393
+ result = await entry.surface.executeAction(
394
+ request.actionId,
395
+ request.params ?? {},
396
+ entry.getContext()
397
+ );
398
+ if (result.success) {
399
+ const action = entry.surface.actions.find(
400
+ (a) => a.id === request.actionId
401
+ );
402
+ if (action?.polling) {
403
+ interim = true;
404
+ startPolling(
405
+ request.requestId,
406
+ entry.surface.id,
407
+ action.id,
408
+ action.polling,
409
+ result.data ?? result.dispatchMeta
410
+ );
411
+ }
412
+ }
413
+ }
414
+ const settled = await waitUntilSettled(startedAt);
415
+ const snap = snapshot();
416
+ return {
417
+ requestId: request.requestId,
418
+ success: result.success,
419
+ ...result.data !== void 0 ? { data: toJsonSafe(result.data) } : {},
420
+ ...result.error ? { error: toJsonSafe(result.error) } : {},
421
+ ...interim ? { interim: true } : {},
422
+ durationMs: Date.now() - startedAt,
423
+ timestamp: Date.now(),
424
+ surfaces: snap.surfaces,
425
+ observations: snap.observations,
426
+ settled
427
+ };
428
+ }
429
+ function execute(request) {
430
+ const seen = results.get(request.requestId);
431
+ if (seen) return seen;
432
+ const pending = run(request);
433
+ results.set(request.requestId, pending);
434
+ while (results.size > dedupeWindow) {
435
+ const oldest = results.keys().next().value;
436
+ results.delete(oldest);
437
+ }
438
+ return pending;
439
+ }
440
+ function snapshot() {
441
+ const surfaces = [];
442
+ const obs = {};
443
+ for (const e of activeEntries()) {
444
+ surfaces.push(e.surface.toManifest());
445
+ const values = observations.get(e.surface.id);
446
+ if (values && values.size > 0)
447
+ obs[e.surface.id] = Object.fromEntries(values);
448
+ }
449
+ return { surfaces, observations: obs };
450
+ }
451
+ function attach(next) {
452
+ if (next === socket) return;
453
+ for (const off of detachTransport) off();
454
+ detachTransport = [];
455
+ transport?.dispose();
456
+ transport = null;
457
+ socket = next;
458
+ if (!next) return;
459
+ const t = createWebSocketTransport(next, options.transport);
460
+ transport = t;
461
+ detachTransport.push(
462
+ t.onAction((request) => {
463
+ if (options.accept && !options.accept(request)) {
464
+ console.warn(
465
+ `[OUI] Refused a request the client does not accept: ${request.surfaceId}.${request.actionId} (${request.requestId})`
466
+ );
467
+ return;
468
+ }
469
+ void execute(request).then((result) => {
470
+ transport?.sendResult(result);
471
+ const ack = ackFor(request.requestId);
472
+ ack.resolve();
473
+ if (!result.interim) acks.delete(request.requestId);
474
+ });
475
+ })
476
+ );
477
+ if (announce) {
478
+ detachTransport.push(
479
+ t.onConnectionChange((connected) => {
480
+ if (connected) announceAll();
481
+ })
482
+ );
483
+ if (t.connected) announceAll();
484
+ }
485
+ }
486
+ if (options.socket) attach(options.socket);
487
+ function mount(surface, getContext) {
488
+ const entry = {
489
+ key: nextKey++,
490
+ surface,
491
+ getContext
492
+ };
493
+ entries.push(entry);
494
+ announceSurface(entry);
495
+ changed();
496
+ let mounted = true;
497
+ return {
498
+ surfaceId: surface.id,
499
+ pushObservation(observationId, value) {
500
+ if (!mounted || activeFor(surface.id) !== entry) return;
501
+ setObservation(surface.id, observationId, value);
502
+ },
503
+ unmount() {
504
+ if (!mounted) return;
505
+ mounted = false;
506
+ const i = entries.indexOf(entry);
507
+ if (i >= 0) entries.splice(i, 1);
508
+ const replacement = activeFor(surface.id);
509
+ if (replacement) {
510
+ announceSurface(replacement);
511
+ } else {
512
+ forgetSurface(surface.id);
513
+ if (announce && transport) transport.deregisterSurface(surface.id);
514
+ }
515
+ changed();
516
+ }
517
+ };
518
+ }
519
+ return {
520
+ mount,
521
+ attach,
522
+ snapshot,
523
+ execute,
524
+ hold() {
525
+ const id = nextHold++;
526
+ holds.add(id);
527
+ changed();
528
+ let released = false;
529
+ return () => {
530
+ if (released) return;
531
+ released = true;
532
+ holds.delete(id);
533
+ changed();
534
+ };
535
+ },
536
+ subscribe(listener) {
537
+ listeners.add(listener);
538
+ return () => {
539
+ listeners.delete(listener);
540
+ };
541
+ },
542
+ dispose() {
543
+ for (const timer of pollers.values()) clearInterval(timer);
544
+ pollers.clear();
545
+ attach(null);
546
+ entries.length = 0;
547
+ observations.clear();
548
+ serializedObservations.clear();
549
+ holds.clear();
550
+ listeners.clear();
551
+ acks.clear();
552
+ }
553
+ };
554
+ }
555
+ function sleep(ms) {
556
+ return new Promise((resolve) => setTimeout(resolve, ms));
557
+ }
558
+ function toJsonSafe(value) {
559
+ if (value === void 0) return void 0;
560
+ try {
561
+ return JSON.parse(JSON.stringify(value));
562
+ } catch {
563
+ return String(value);
564
+ }
565
+ }
566
+
567
+ export { createOUI, createSurfaceRuntime, defineSurface };
568
+ //# sourceMappingURL=chunk-GXTNZ44I.js.map
569
+ //# sourceMappingURL=chunk-GXTNZ44I.js.map