gesso-framework 0.4.1 → 0.5.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.
@@ -0,0 +1,600 @@
1
+ //#region src/channel/ChannelSchema.ts
2
+ /**
3
+ * The schemas, beside the tokens rather than on them.
4
+ *
5
+ * A token is a plain object both threads import and nothing should
6
+ * grow on, and its interface is a public type that a schema field would
7
+ * widen for every application, described or not. Keyed weakly, so a
8
+ * token the application drops takes its schema with it.
9
+ */
10
+ const schemas = /* @__PURE__ */ new WeakMap();
11
+ /**
12
+ * Attaches a schema to a token.
13
+ *
14
+ * `gesso-vite-plugin` calls this at the bottom of each contract module
15
+ * it reads, so it runs once, when the module is first imported. Call it
16
+ * yourself to describe a channel the plugin does not see. A second call
17
+ * for the same token replaces the first, which is what a hot-replaced
18
+ * contract module does.
19
+ */
20
+ function describeChannel(token, schema) {
21
+ schemas.set(token, schema);
22
+ }
23
+ /** The schema attached to a token, or undefined when nothing described it. */
24
+ function channelSchema(token) {
25
+ return schemas.get(token);
26
+ }
27
+ //#endregion
28
+ //#region src/agent/binary.ts
29
+ /**
30
+ * Turns the base64 an agent wrote back into the bytes a command takes.
31
+ *
32
+ * A command may carry an `ArrayBuffer` or a typed array, which an agent
33
+ * cannot write: it speaks JSON. `gesso-vite-plugin` describes such a
34
+ * field as a base64 string tagged `x-gesso-binary` with the type the
35
+ * command expects, the agent sends the string, and this walks the
36
+ * arguments beside their schema and puts the bytes back before the
37
+ * command is sent, so the application receives what its own components
38
+ * would have sent it.
39
+ *
40
+ * Only what the schema tags is touched, and a value that is not a
41
+ * string where bytes are expected is left for the command to see: the
42
+ * validator has already said whether it was the right shape.
43
+ */
44
+ function decodeBinary(schema, value, root) {
45
+ if (schema === void 0) return value;
46
+ const ref = schema.$ref;
47
+ if (typeof ref === "string") {
48
+ const target = root.$defs?.[ref.replace(/^#\/\$defs\//, "")];
49
+ return decodeBinary(target, value, root);
50
+ }
51
+ const kind = schema["x-gesso-binary"];
52
+ if (typeof kind === "string") return typeof value === "string" ? bytesOf(value, kind) : value;
53
+ if (Array.isArray(schema.anyOf)) {
54
+ const binary = schema.anyOf.find((option) => typeof option["x-gesso-binary"] === "string");
55
+ return binary !== void 0 && typeof value === "string" ? decodeBinary(binary, value, root) : value;
56
+ }
57
+ if (Array.isArray(value)) {
58
+ const prefix = schema.prefixItems ?? [];
59
+ const items = schema.items;
60
+ return value.map((item, index) => decodeBinary(index < prefix.length ? prefix[index] : typeof items === "object" ? items : void 0, item, root));
61
+ }
62
+ if (value !== null && typeof value === "object") {
63
+ const properties = schema.properties ?? {};
64
+ const out = {};
65
+ for (const [key, field] of Object.entries(value)) out[key] = decodeBinary(properties[key], field, root);
66
+ return out;
67
+ }
68
+ return value;
69
+ }
70
+ const TYPED = {
71
+ Int8Array,
72
+ Uint8Array,
73
+ Uint8ClampedArray,
74
+ Int16Array,
75
+ Uint16Array,
76
+ Int32Array,
77
+ Uint32Array,
78
+ Float32Array,
79
+ Float64Array,
80
+ BigInt64Array,
81
+ BigUint64Array
82
+ };
83
+ function bytesOf(base64, kind) {
84
+ const text = atob(base64);
85
+ const bytes = new Uint8Array(text.length);
86
+ for (let index = 0; index < text.length; index++) bytes[index] = text.charCodeAt(index);
87
+ if (kind === "ArrayBuffer") return bytes.buffer;
88
+ const Typed = TYPED[kind];
89
+ return Typed === void 0 || Typed === Uint8Array ? bytes : new Typed(bytes.buffer);
90
+ }
91
+ //#endregion
92
+ //#region src/agent/validate.ts
93
+ /**
94
+ * Checks a value against the JSON Schema a channel description uses.
95
+ *
96
+ * Not a general validator. It understands the keywords
97
+ * `gesso-vite-plugin` writes (`type`, `enum`, `const`, `anyOf`,
98
+ * `items`, `prefixItems`, `minItems`, `properties`, `required`,
99
+ * `additionalProperties`, `not` and `$ref` into `$defs`), and passes
100
+ * anything else. That is enough to stop the case that matters: an
101
+ * agent sending `"3"` where a command takes a number, which the
102
+ * command would accept, store, and fail on much later and somewhere
103
+ * else.
104
+ *
105
+ * Returns the first problem as a sentence naming where it is, written
106
+ * for the agent that sent the value so it can correct itself, or null.
107
+ */
108
+ function validate(schema, value, root = schema, at = "input") {
109
+ const ref = schema.$ref;
110
+ if (typeof ref === "string") {
111
+ const name = ref.replace(/^#\/\$defs\//, "");
112
+ const target = root.$defs?.[name];
113
+ return target === void 0 ? null : validate(target, value, root, at);
114
+ }
115
+ if (schema.not !== void 0 && validate(schema.not, value, root, at) === null) return `${at} must not be given.`;
116
+ if ("const" in schema && value !== schema.const) return `${at} must be ${JSON.stringify(schema.const)}.`;
117
+ if (Array.isArray(schema.enum) && !schema.enum.includes(value)) return `${at} must be one of ${schema.enum.map((option) => JSON.stringify(option)).join(", ")}.`;
118
+ if (Array.isArray(schema.anyOf)) {
119
+ const options = schema.anyOf;
120
+ if (options.every((option) => validate(option, value, root, at) !== null)) return `${at} does not match any of the ${options.length} shapes it may take.`;
121
+ return null;
122
+ }
123
+ const type = schema.type;
124
+ if (typeof type === "string" && !hasType(value, type)) return `${at} must be ${article(type)}, not ${describe(value)}.`;
125
+ if (Array.isArray(value)) return validateArray(schema, value, root, at);
126
+ if (type === "object" && value !== null && typeof value === "object") return validateObject(schema, value, root, at);
127
+ return null;
128
+ }
129
+ function validateArray(schema, value, root, at) {
130
+ const prefix = schema.prefixItems ?? [];
131
+ if (typeof schema.minItems === "number" && value.length < schema.minItems) return `${at} must have at least ${schema.minItems} items.`;
132
+ for (let index = 0; index < value.length; index++) {
133
+ const item = index < prefix.length ? prefix[index] : schema.items;
134
+ if (item === false) return `${at} must have at most ${prefix.length} items.`;
135
+ if (item !== void 0 && item !== true) {
136
+ const problem = validate(item, value[index], root, `${at}[${index}]`);
137
+ if (problem !== null) return problem;
138
+ }
139
+ }
140
+ return null;
141
+ }
142
+ function validateObject(schema, value, root, at) {
143
+ const properties = schema.properties ?? {};
144
+ for (const key of schema.required ?? []) if (value[key] === void 0) return `${at}.${key} is required.`;
145
+ for (const [key, property] of Object.entries(value)) {
146
+ const declared = properties[key];
147
+ if (declared !== void 0) {
148
+ const problem = property === void 0 ? null : validate(declared, property, root, `${at}.${key}`);
149
+ if (problem !== null) return problem;
150
+ continue;
151
+ }
152
+ const extra = schema.additionalProperties;
153
+ if (extra === false) {
154
+ const known = Object.keys(properties);
155
+ return `${at}.${key} is not expected${known.length > 0 ? `; the fields are ${known.join(", ")}` : ""}.`;
156
+ }
157
+ if (extra !== void 0 && extra !== true) {
158
+ const problem = validate(extra, property, root, `${at}.${key}`);
159
+ if (problem !== null) return problem;
160
+ }
161
+ }
162
+ return null;
163
+ }
164
+ function hasType(value, type) {
165
+ switch (type) {
166
+ case "string": return typeof value === "string";
167
+ case "number": return typeof value === "number" && Number.isFinite(value);
168
+ case "integer": return Number.isInteger(value);
169
+ case "boolean": return typeof value === "boolean";
170
+ case "null": return value === null;
171
+ case "array": return Array.isArray(value);
172
+ case "object": return value !== null && typeof value === "object" && !Array.isArray(value);
173
+ default: return true;
174
+ }
175
+ }
176
+ function article(type) {
177
+ return /^[aeiou]/.test(type) ? `an ${type}` : `a ${type}`;
178
+ }
179
+ function describe(value) {
180
+ if (value === null) return "null";
181
+ if (Array.isArray(value)) return "an array";
182
+ return typeof value === "object" ? "an object" : `${typeof value} ${JSON.stringify(value)}`;
183
+ }
184
+ //#endregion
185
+ //#region src/agent/AgentSurface.ts
186
+ /** The longest tool name MCP clients accept. */
187
+ const MAX_NAME = 64;
188
+ function agentSurface(channels, options = {}) {
189
+ const quietMs = options.quietMs ?? 50;
190
+ const settleMs = options.settleMs ?? 1e3;
191
+ const subscriptions = [];
192
+ const entries = [];
193
+ const tools = [];
194
+ const handlers = /* @__PURE__ */ new Map();
195
+ let lastChange = 0;
196
+ let following = false;
197
+ /**
198
+ * Subscribed on first use rather than at construction, so a surface
199
+ * built at startup and never called costs the application nothing:
200
+ * a view key may be a cold observable doing real work per subscriber.
201
+ */
202
+ const follow = () => {
203
+ if (following) return;
204
+ following = true;
205
+ for (const entry of entries) for (const [key, observable] of Object.entries(entry.served.source.view)) subscriptions.push(observable.subscribe((value) => {
206
+ entry.view[key] = value;
207
+ lastChange = Date.now();
208
+ }));
209
+ };
210
+ const settle = async () => {
211
+ const start = Date.now();
212
+ for (;;) {
213
+ await new Promise((resolve) => setTimeout(resolve, quietMs));
214
+ const now = Date.now();
215
+ if (now - lastChange >= quietMs || now - start >= settleMs) return;
216
+ }
217
+ };
218
+ const add = (tool, handler) => {
219
+ if (handlers.has(tool.name)) throw new Error(`Two tools would be called '${tool.name}'. A channel and command name pair has to be unique once joined with an underscore, and a command cannot be called \`view\`.`);
220
+ tools.push(tool);
221
+ handlers.set(tool.name, handler);
222
+ };
223
+ for (const served of channels) {
224
+ const schema = channelSchema(served.token);
225
+ const name = toolName(served.token.name);
226
+ const commands = /* @__PURE__ */ new Map();
227
+ for (const command of Object.keys(served.source.commands ?? {})) {
228
+ const described = schema?.commands[command];
229
+ if (described?.hidden !== true) commands.set(command, described ?? null);
230
+ }
231
+ const entry = {
232
+ served,
233
+ name,
234
+ description: schema?.description,
235
+ viewSchema: schema?.view,
236
+ view: { ...served.token.initial },
237
+ commands
238
+ };
239
+ entries.push(entry);
240
+ const about = entry.description === void 0 ? "" : `\n\n${entry.description}`;
241
+ add({
242
+ name: toolName(`${served.token.name}_view`),
243
+ title: `Read ${served.token.name}`,
244
+ description: `What the ${served.token.name} channel currently holds.${about}`,
245
+ inputSchema: {
246
+ type: "object",
247
+ properties: {},
248
+ additionalProperties: false
249
+ },
250
+ ...entry.viewSchema === void 0 ? {} : { outputSchema: entry.viewSchema },
251
+ annotations: {
252
+ readOnlyHint: true,
253
+ openWorldHint: false
254
+ }
255
+ }, async () => {
256
+ follow();
257
+ return viewResult(entry, `The ${served.token.name} view.`);
258
+ });
259
+ for (const [command, described] of commands) add(commandTool(entry, command, described), (args) => send(entry, command, described, args));
260
+ }
261
+ const send = async (entry, command, described, args) => {
262
+ follow();
263
+ const channel = entry.served.token.name;
264
+ const input = args ?? {};
265
+ let positional;
266
+ if (described === null) {
267
+ const list = input.arguments ?? [];
268
+ if (!Array.isArray(list)) return failure("arguments must be an array of the values the command takes, in order.");
269
+ positional = list;
270
+ } else {
271
+ const problem = validate(described.input, input);
272
+ if (problem !== null) return failure(problem);
273
+ const properties = described.input.properties ?? {};
274
+ positional = described.parameters.map((parameter) => decodeBinary(properties[parameter], input[parameter], described.input));
275
+ if (described.rest === true) {
276
+ const spread = positional.pop();
277
+ positional.push(...Array.isArray(spread) ? spread : []);
278
+ }
279
+ while (positional.length > 0 && positional[positional.length - 1] === void 0) positional.pop();
280
+ if (described.confirm === true) {
281
+ if (options.confirm === void 0) return failure(`${channel}.${command} asks for a person to approve it, and this application has given agents no way to ask. Ask the person to do it themselves.`);
282
+ if (!await options.confirm({
283
+ channel,
284
+ command,
285
+ description: described.description,
286
+ arguments: input,
287
+ destructive: described.destructive === true
288
+ })) return failure(`The person declined ${channel}.${command}. Do not send it again unless they ask.`);
289
+ }
290
+ }
291
+ const handler = entry.served.source.commands?.[command];
292
+ if (handler === void 0) return failure(`${channel} has no command ${command}.`);
293
+ try {
294
+ handler(...positional);
295
+ } catch (error) {
296
+ return failure(`${channel}.${command} failed: ${error instanceof Error ? error.message : String(error)}`);
297
+ }
298
+ await settle();
299
+ return viewResult(entry, `Sent ${command} to ${channel}. The view afterwards:`);
300
+ };
301
+ return {
302
+ tools: () => tools,
303
+ call: async (name, args) => {
304
+ const handler = handlers.get(name);
305
+ if (handler === void 0) return failure(`There is no tool called ${name}. The tools are ${[...handlers.keys()].join(", ")}.`);
306
+ return handler(args);
307
+ },
308
+ resources: () => entries.map((entry) => ({
309
+ uri: resourceUri(entry.served.token.name),
310
+ name: entry.served.token.name,
311
+ ...entry.description === void 0 ? {} : { description: entry.description },
312
+ mimeType: "application/json"
313
+ })),
314
+ read: (uri) => {
315
+ const entry = entries.find((candidate) => resourceUri(candidate.served.token.name) === uri);
316
+ if (entry === void 0) return;
317
+ follow();
318
+ return { ...entry.view };
319
+ },
320
+ dispose: () => {
321
+ for (const subscription of subscriptions) subscription.unsubscribe();
322
+ subscriptions.length = 0;
323
+ following = false;
324
+ }
325
+ };
326
+ }
327
+ /** The URI of a channel's view. */
328
+ function resourceUri(channel) {
329
+ return `gesso://${encodeURIComponent(channel)}/view`;
330
+ }
331
+ function commandTool(entry, command, described) {
332
+ const channel = entry.served.token.name;
333
+ const destructive = described?.destructive === true;
334
+ const lines = [described?.description ?? `Sends ${command} to the ${channel} channel.`];
335
+ if (described === null) lines.push(`This command was not described, so its arguments are a positional list. Read ${toolName(`${channel}_view`)} first to see what the channel holds.`);
336
+ if (described?.confirm === true) lines.push("The person is asked to approve this before it is sent.");
337
+ lines.push("Returns the view after the command has taken effect.");
338
+ return {
339
+ name: toolName(`${channel}_${command}`),
340
+ title: `${channel}: ${command}`,
341
+ description: lines.join("\n\n"),
342
+ inputSchema: described?.input ?? {
343
+ type: "object",
344
+ properties: { arguments: {
345
+ type: "array",
346
+ description: "The arguments, in order."
347
+ } },
348
+ additionalProperties: false
349
+ },
350
+ ...entry.viewSchema === void 0 ? {} : { outputSchema: entry.viewSchema },
351
+ annotations: {
352
+ readOnlyHint: false,
353
+ ...destructive ? { destructiveHint: true } : { destructiveHint: false },
354
+ ...described?.idempotent === true ? { idempotentHint: true } : {},
355
+ openWorldHint: false
356
+ }
357
+ };
358
+ }
359
+ /**
360
+ * A tool name MCP clients accept: letters, digits, underscores and
361
+ * dashes, at most 64 characters. A channel name is the application's
362
+ * own string, so anything else in it becomes an underscore, and one
363
+ * too long is an error at startup rather than a tool a client drops.
364
+ */
365
+ function toolName(raw) {
366
+ const name = raw.replace(/[^A-Za-z0-9_-]/g, "_");
367
+ if (name.length > MAX_NAME) throw new Error(`The tool name '${name}' is longer than ${MAX_NAME} characters. Shorten the channel's name.`);
368
+ return name;
369
+ }
370
+ function viewResult(entry, lead) {
371
+ const view = { ...entry.view };
372
+ return {
373
+ content: [{
374
+ type: "text",
375
+ text: `${lead}\n${JSON.stringify(view, null, 2)}`
376
+ }],
377
+ structuredContent: view,
378
+ isError: false
379
+ };
380
+ }
381
+ function failure(text) {
382
+ return {
383
+ content: [{
384
+ type: "text",
385
+ text
386
+ }],
387
+ isError: true
388
+ };
389
+ }
390
+ //#endregion
391
+ //#region src/agent/remote.ts
392
+ /**
393
+ * An agent surface across a thread.
394
+ *
395
+ * A web application's channels are served in workers, and an agent
396
+ * reaches the page, so the surface has to cross from one to the other.
397
+ * The worker that serves channels answers a `gesso:agent` port with its
398
+ * own surface (`serveAgentPort`); the page holds the other end as a
399
+ * surface of its own (`remoteSurface`); and a thread that knows several
400
+ * such ports, the render worker with its channel workers behind it,
401
+ * offers them as one (`combineSurfaces`).
402
+ *
403
+ * The four operations cross as they are. MCP itself is spoken only at
404
+ * the end that faces the agent, so nothing in a worker parses JSON-RPC.
405
+ */
406
+ /** The port key a thread answers with its agent surface. */
407
+ const AGENT_PORT = "gesso:agent";
408
+ /**
409
+ * Answers a port with a surface.
410
+ *
411
+ * The surface is made on the first request rather than when the port
412
+ * arrives: a port is opened by a page that may never ask anything, and
413
+ * a surface subscribes to every view key once it is used. It is given
414
+ * a `confirm` that asks the far end of this port, so a command marked
415
+ * `@confirm` reaches the person wherever they are; the far end answers
416
+ * no when it has no way to ask.
417
+ */
418
+ function serveAgentPort(port, makeSurface) {
419
+ let surface;
420
+ let nextConfirm = 1;
421
+ const asking = /* @__PURE__ */ new Map();
422
+ const confirm = (request) => new Promise((resolve) => {
423
+ const id = nextConfirm++;
424
+ asking.set(id, resolve);
425
+ port.postMessage({
426
+ confirm: id,
427
+ request
428
+ });
429
+ });
430
+ port.onmessage = async (event) => {
431
+ const data = event.data;
432
+ if ("confirm" in data && "approved" in data) {
433
+ asking.get(data.confirm)?.(data.approved);
434
+ asking.delete(data.confirm);
435
+ return;
436
+ }
437
+ const request = data;
438
+ if (typeof request?.id !== "number") return;
439
+ surface ??= makeSurface(confirm);
440
+ try {
441
+ port.postMessage({
442
+ id: request.id,
443
+ result: await answer(surface, request)
444
+ });
445
+ } catch (error) {
446
+ port.postMessage({
447
+ id: request.id,
448
+ error: error instanceof Error ? error.message : String(error)
449
+ });
450
+ }
451
+ };
452
+ return () => {
453
+ port.onmessage = null;
454
+ surface?.dispose?.();
455
+ for (const resolve of asking.values()) resolve(false);
456
+ asking.clear();
457
+ };
458
+ }
459
+ function answer(surface, request) {
460
+ switch (request.op) {
461
+ case "tools": return surface.tools();
462
+ case "resources": return surface.resources();
463
+ case "call": return surface.call(request.name, request.args);
464
+ case "read": return surface.read(request.uri) ?? null;
465
+ }
466
+ }
467
+ /**
468
+ * The surface at the other end of a port.
469
+ *
470
+ * A request nobody answers within `timeoutMs` (default 2000) is taken
471
+ * as a thread with nothing to offer: a worker that serves no channels
472
+ * never installs the answering side, and an agent asking the whole
473
+ * application should hear about the threads that do rather than wait
474
+ * on one that does not.
475
+ */
476
+ function remoteSurface(port, options = {}) {
477
+ const timeoutMs = options.timeoutMs ?? 2e3;
478
+ let next = 1;
479
+ const pending = /* @__PURE__ */ new Map();
480
+ port.onmessage = (event) => {
481
+ const question = event.data;
482
+ if (question !== null && typeof question === "object" && "request" in question) {
483
+ Promise.resolve(options.confirm?.(question.request) ?? false).then((approved) => port.postMessage({
484
+ confirm: question.confirm,
485
+ approved
486
+ }), () => port.postMessage({
487
+ confirm: question.confirm,
488
+ approved: false
489
+ }));
490
+ return;
491
+ }
492
+ const response = event.data;
493
+ const waiting = pending.get(response?.id);
494
+ if (waiting === void 0) return;
495
+ pending.delete(response.id);
496
+ if ("error" in response) waiting.reject(new Error(response.error));
497
+ else waiting.resolve(response.result);
498
+ };
499
+ const ask = (request, fallback, timeout = timeoutMs) => new Promise((resolve, reject) => {
500
+ const id = next++;
501
+ const timer = setTimeout(() => {
502
+ pending.delete(id);
503
+ resolve(fallback);
504
+ }, timeout);
505
+ pending.set(id, {
506
+ resolve: (value) => {
507
+ clearTimeout(timer);
508
+ resolve(value);
509
+ },
510
+ reject: (error) => {
511
+ clearTimeout(timer);
512
+ reject(error);
513
+ }
514
+ });
515
+ port.postMessage({
516
+ ...request,
517
+ id
518
+ });
519
+ });
520
+ return {
521
+ tools: () => ask({ op: "tools" }, []),
522
+ resources: () => ask({ op: "resources" }, []),
523
+ call: (name, args) => ask({
524
+ op: "call",
525
+ name,
526
+ args
527
+ }, {
528
+ content: [{
529
+ type: "text",
530
+ text: `${name} did not answer in time.`
531
+ }],
532
+ isError: true
533
+ }, Math.max(timeoutMs, 12e4)),
534
+ read: async (uri) => await ask({
535
+ op: "read",
536
+ uri
537
+ }, null) ?? void 0
538
+ };
539
+ }
540
+ /**
541
+ * Several surfaces as one: tools and resources listed together, a call
542
+ * or a read sent to whichever surface listed it. When two list the same
543
+ * name, the first keeps it, as the order of the threads is the order
544
+ * the application registered them in.
545
+ */
546
+ function combineSurfaces(surfaces) {
547
+ const toolOwners = /* @__PURE__ */ new Map();
548
+ const resourceOwners = /* @__PURE__ */ new Map();
549
+ const tools = async () => {
550
+ const lists = await Promise.all(surfaces.map((surface) => surface.tools()));
551
+ toolOwners.clear();
552
+ const out = [];
553
+ lists.forEach((list, index) => {
554
+ for (const tool of list) if (!toolOwners.has(tool.name)) {
555
+ toolOwners.set(tool.name, surfaces[index]);
556
+ out.push(tool);
557
+ }
558
+ });
559
+ return out;
560
+ };
561
+ const resources = async () => {
562
+ const lists = await Promise.all(surfaces.map((surface) => surface.resources()));
563
+ resourceOwners.clear();
564
+ const out = [];
565
+ lists.forEach((list, index) => {
566
+ for (const resource of list) if (!resourceOwners.has(resource.uri)) {
567
+ resourceOwners.set(resource.uri, surfaces[index]);
568
+ out.push(resource);
569
+ }
570
+ });
571
+ return out;
572
+ };
573
+ return {
574
+ tools,
575
+ resources,
576
+ call: async (name, args) => {
577
+ if (!toolOwners.has(name)) await tools();
578
+ const owner = toolOwners.get(name);
579
+ if (owner === void 0) return {
580
+ content: [{
581
+ type: "text",
582
+ text: `There is no tool called ${name}. The tools are ${[...toolOwners.keys()].join(", ")}.`
583
+ }],
584
+ isError: true
585
+ };
586
+ return owner.call(name, args);
587
+ },
588
+ read: async (uri) => {
589
+ if (!resourceOwners.has(uri)) await resources();
590
+ return resourceOwners.get(uri)?.read(uri);
591
+ },
592
+ dispose: () => {
593
+ for (const surface of surfaces) surface.dispose?.();
594
+ }
595
+ };
596
+ }
597
+ //#endregion
598
+ export { agentSurface as a, channelSchema as c, serveAgentPort as i, describeChannel as l, combineSurfaces as n, resourceUri as o, remoteSurface as r, validate as s, AGENT_PORT as t };
599
+
600
+ //# sourceMappingURL=remote-xxij8zXe.js.map