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