@obenor/mcp 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.
- package/bundle/kernel-worker.mjs +6091 -720
- package/bundle/obenor-kernel-mt.mjs +1 -1
- package/bundle/obenor-kernel.mjs +1 -1
- package/bundle/runner.js +181 -0
- package/bundle/server.mjs +4240 -429
- package/package.json +2 -2
package/bundle/runner.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// ../script/src/runner.ts
|
|
2
|
+
import { parentPort } from "node:worker_threads";
|
|
3
|
+
|
|
4
|
+
// ../script/src/protocol.ts
|
|
5
|
+
var SCRIPT_CALLS = [
|
|
6
|
+
"add",
|
|
7
|
+
"update",
|
|
8
|
+
"remove",
|
|
9
|
+
"suppress",
|
|
10
|
+
"parameter",
|
|
11
|
+
"parameters",
|
|
12
|
+
"features",
|
|
13
|
+
"feature",
|
|
14
|
+
"bodies",
|
|
15
|
+
"measure",
|
|
16
|
+
"build",
|
|
17
|
+
"id",
|
|
18
|
+
"material",
|
|
19
|
+
"raycast",
|
|
20
|
+
"topology"
|
|
21
|
+
];
|
|
22
|
+
var SCRIPT_LIMITS = {
|
|
23
|
+
/** Wall clock. Long enough to build a real model, short enough to notice. */
|
|
24
|
+
deadlineMs: 3e4,
|
|
25
|
+
/** Document calls. A script that needs more than this is a program. */
|
|
26
|
+
maxCalls: 5e3,
|
|
27
|
+
/** Characters of source. */
|
|
28
|
+
maxSource: 2e5,
|
|
29
|
+
/** Characters of printed output kept. The rest is counted and dropped. */
|
|
30
|
+
maxPrint: 2e4,
|
|
31
|
+
/**
|
|
32
|
+
* Rays in one `raycast`. The one limit a batch verb needs of its own.
|
|
33
|
+
*
|
|
34
|
+
* `maxCalls` counts CALLS, and it is the only thing standing between a script
|
|
35
|
+
* and unbounded kernel work - which a verb that takes an array quietly walks
|
|
36
|
+
* around, because a million rays in one call is one call. So the array is
|
|
37
|
+
* capped too, at the same order as the whole call budget: a script may ask
|
|
38
|
+
* for as much geometry in one batch as it was ever allowed to ask for in
|
|
39
|
+
* total, and no more.
|
|
40
|
+
*
|
|
41
|
+
* RAYS AND NOT KERNEL CALLS, which is the honest reading of it. The work a
|
|
42
|
+
* batch does is rays x distinct PLACEMENTS - an assembly of forty occurrences
|
|
43
|
+
* turns five thousand rays into two hundred thousand traversals - so this
|
|
44
|
+
* bounds the argument rather than the cost. That is deliberate and not an
|
|
45
|
+
* oversight: the cost is already bounded by the DEADLINE, which is the limit
|
|
46
|
+
* that actually protects a caller from a slow answer, and a cap that varied
|
|
47
|
+
* with the model would refuse a sampling pass on a big assembly that a small
|
|
48
|
+
* one allows, which is the kind of limit nobody can plan around.
|
|
49
|
+
*/
|
|
50
|
+
maxRays: 5e3,
|
|
51
|
+
/**
|
|
52
|
+
* Faces or edges one `topology` call may answer with, per kind.
|
|
53
|
+
*
|
|
54
|
+
* A ceiling on the ANSWER and not a refusal of the question: a body with
|
|
55
|
+
* three thousand faces is a real body, and a script that asks about it should
|
|
56
|
+
* get the first `limit` of them and be told how many it did not get, rather
|
|
57
|
+
* than an error it can do nothing about. The default `limit` is far below
|
|
58
|
+
* this, because the common case is a panel with six faces and the uncommon
|
|
59
|
+
* one is an import - and a script that means it says so.
|
|
60
|
+
*/
|
|
61
|
+
maxTopologyRows: 2e4
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// ../script/src/runtime.ts
|
|
65
|
+
var PREAMBLE_LINES = 3;
|
|
66
|
+
var WRAPPER = "obenorScript";
|
|
67
|
+
function lineOf(error) {
|
|
68
|
+
const stack = error instanceof Error ? error.stack : null;
|
|
69
|
+
if (typeof stack !== "string") return null;
|
|
70
|
+
for (const frame of stack.split("\n")) {
|
|
71
|
+
if (!frame.includes(WRAPPER)) continue;
|
|
72
|
+
const at = /:(\d+):\d+\)?\s*$/u.exec(frame.trim());
|
|
73
|
+
if (at === null) continue;
|
|
74
|
+
const line = Number(at[1]) - PREAMBLE_LINES;
|
|
75
|
+
return line > 0 ? line : null;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
function format(value) {
|
|
80
|
+
if (value === void 0) return "undefined";
|
|
81
|
+
try {
|
|
82
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
83
|
+
return JSON.stringify(value, (_key, held) => {
|
|
84
|
+
if (typeof held !== "object" || held === null) return held;
|
|
85
|
+
if (seen.has(held)) return "[circular]";
|
|
86
|
+
seen.add(held);
|
|
87
|
+
return held;
|
|
88
|
+
}) ?? String(value);
|
|
89
|
+
} catch {
|
|
90
|
+
return String(value);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function transportable(value) {
|
|
94
|
+
try {
|
|
95
|
+
structuredClone(value);
|
|
96
|
+
return value;
|
|
97
|
+
} catch {
|
|
98
|
+
return format(value);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function compile(source) {
|
|
102
|
+
const AsyncFunction = Object.getPrototypeOf(async () => {
|
|
103
|
+
}).constructor;
|
|
104
|
+
return new AsyncFunction(
|
|
105
|
+
"api",
|
|
106
|
+
"print",
|
|
107
|
+
`return (async function ${WRAPPER}() {
|
|
108
|
+
${source}
|
|
109
|
+
})();`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
function startRunner(port2) {
|
|
113
|
+
const pending = /* @__PURE__ */ new Map();
|
|
114
|
+
let nextId = 1;
|
|
115
|
+
let calls = 0;
|
|
116
|
+
let maxCalls = SCRIPT_LIMITS.maxCalls;
|
|
117
|
+
const ask = (call, args) => {
|
|
118
|
+
calls += 1;
|
|
119
|
+
if (calls > maxCalls) {
|
|
120
|
+
return Promise.reject(new Error(
|
|
121
|
+
`this script has made ${calls} document calls, which is past the limit of ${maxCalls}. Something is looping.`
|
|
122
|
+
));
|
|
123
|
+
}
|
|
124
|
+
const id = nextId++;
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
pending.set(id, { resolve, reject });
|
|
127
|
+
port2.post({ kind: "call", id, call, args });
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
const api = {};
|
|
131
|
+
for (const call of SCRIPT_CALLS) {
|
|
132
|
+
api[call] = (...args) => ask(call, args);
|
|
133
|
+
}
|
|
134
|
+
const print = (...parts) => {
|
|
135
|
+
port2.post({
|
|
136
|
+
kind: "print",
|
|
137
|
+
text: parts.map((part) => typeof part === "string" ? part : format(part)).join(" ")
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
const run = async (source) => {
|
|
141
|
+
let done;
|
|
142
|
+
try {
|
|
143
|
+
const value = await compile(source)(api, print);
|
|
144
|
+
done = { kind: "done", ok: true, value: transportable(value), line: null };
|
|
145
|
+
} catch (error) {
|
|
146
|
+
done = {
|
|
147
|
+
kind: "done",
|
|
148
|
+
ok: false,
|
|
149
|
+
error: error instanceof Error ? error.message : String(error),
|
|
150
|
+
line: lineOf(error)
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
port2.post(done);
|
|
154
|
+
};
|
|
155
|
+
port2.listen((message) => {
|
|
156
|
+
if (message.kind === "start") {
|
|
157
|
+
maxCalls = message.maxCalls;
|
|
158
|
+
void run(message.source);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const held = pending.get(message.id);
|
|
162
|
+
if (held === void 0) return;
|
|
163
|
+
pending.delete(message.id);
|
|
164
|
+
if (message.ok) held.resolve(message.value);
|
|
165
|
+
else held.reject(new Error(message.error ?? "the document refused, without saying why"));
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ../script/src/runner.ts
|
|
170
|
+
var port = parentPort;
|
|
171
|
+
if (port === null) throw new Error("the script runner must be started as a worker");
|
|
172
|
+
startRunner({
|
|
173
|
+
post: (message) => {
|
|
174
|
+
port.postMessage(message);
|
|
175
|
+
},
|
|
176
|
+
listen: (handler) => {
|
|
177
|
+
port.on("message", (message) => {
|
|
178
|
+
handler(message);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
});
|