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.
- package/CHANGELOG.md +114 -0
- package/README.md +2 -0
- package/dist/ChannelProtocol-ByNoHujM.d.ts +247 -0
- package/dist/{FunctionComponent-CgwLKE5d.d.ts → FunctionComponent-fYMdePtH.d.ts} +3 -247
- package/dist/agent/index.d.ts +317 -0
- package/dist/agent/index.js +184 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/app-BvlIO1G9.js +326 -0
- package/dist/app-BvlIO1G9.js.map +1 -0
- package/dist/{index-C9FAI_Kt.d.ts → index-BDM_gzzZ.d.ts} +377 -268
- package/dist/index.d.ts +5 -3
- package/dist/index.js +974 -92
- package/dist/index.js.map +1 -1
- package/dist/jsx/jsx-runtime.d.ts +1 -1
- package/dist/{persisted-CsTPnjkc.js → persisted-pix1fS1D.js} +6 -1
- package/dist/{persisted-CsTPnjkc.js.map → persisted-pix1fS1D.js.map} +1 -1
- package/dist/remote-xxij8zXe.js +600 -0
- package/dist/remote-xxij8zXe.js.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/ui-U39HjNFA.d.ts +517 -0
- package/dist/webmcp-CJVFvAHe.js +77 -0
- package/dist/webmcp-CJVFvAHe.js.map +1 -0
- package/dist/worker/index.d.ts +4 -2
- package/dist/worker/index.js +1 -1
- package/package.json +12 -4
|
@@ -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
|