@alisio/core 0.5.2 → 0.6.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/dist/analysis/janitor.d.ts +48 -0
- package/dist/analysis/janitor.js +170 -18
- package/dist/analysis/jobs.d.ts +11 -2
- package/dist/analysis/jobs.js +13 -0
- package/dist/analysis/oci.d.ts +11 -2
- package/dist/analysis/oci.js +20 -4
- package/dist/analysis/python/sources.js +2 -1
- package/dist/analysis/rerun.d.ts +1 -1
- package/dist/analysis/status.d.ts +3 -1
- package/dist/analysis/status.js +3 -0
- package/dist/application.d.ts +28 -2
- package/dist/application.js +189 -4
- package/dist/changelog/data.js +26 -0
- package/dist/config.d.ts +110 -2
- package/dist/config.js +188 -6
- package/dist/core/call-internals.d.ts +22 -0
- package/dist/core/call-internals.js +9 -0
- package/dist/core/contracts.d.ts +2 -2
- package/dist/core/runner.js +111 -38
- package/dist/forge/broker.d.ts +51 -0
- package/dist/forge/broker.js +244 -0
- package/dist/forge/budget.d.ts +27 -0
- package/dist/forge/budget.js +60 -0
- package/dist/forge/catalog-view.d.ts +24 -0
- package/dist/forge/catalog-view.js +43 -0
- package/dist/forge/errors.d.ts +26 -0
- package/dist/forge/errors.js +49 -0
- package/dist/forge/guides/javascript.d.ts +25 -0
- package/dist/forge/guides/javascript.js +90 -0
- package/dist/forge/guides/provider.d.ts +42 -0
- package/dist/forge/guides/provider.js +96 -0
- package/dist/forge/guides/python-environment.d.ts +15 -0
- package/dist/forge/guides/python-environment.js +81 -0
- package/dist/forge/guides/python.d.ts +38 -0
- package/dist/forge/guides/python.js +170 -0
- package/dist/forge/javascript/engine.d.ts +44 -0
- package/dist/forge/javascript/engine.js +229 -0
- package/dist/forge/javascript/hosts/factory.d.ts +25 -0
- package/dist/forge/javascript/hosts/factory.js +21 -0
- package/dist/forge/javascript/hosts/framing.d.ts +17 -0
- package/dist/forge/javascript/hosts/framing.js +49 -0
- package/dist/forge/javascript/hosts/isolate-main.d.ts +9 -0
- package/dist/forge/javascript/hosts/isolate-main.js +63 -0
- package/dist/forge/javascript/hosts/process-entry.d.ts +1 -0
- package/dist/forge/javascript/hosts/process-entry.js +3 -0
- package/dist/forge/javascript/hosts/process-host.d.ts +33 -0
- package/dist/forge/javascript/hosts/process-host.js +160 -0
- package/dist/forge/javascript/hosts/worker-entry.d.ts +1 -0
- package/dist/forge/javascript/hosts/worker-entry.js +3 -0
- package/dist/forge/javascript/hosts/worker-host.d.ts +18 -0
- package/dist/forge/javascript/hosts/worker-host.js +101 -0
- package/dist/forge/javascript/prelude.d.ts +2 -0
- package/dist/forge/javascript/prelude.js +333 -0
- package/dist/forge/javascript/probe.d.ts +2 -0
- package/dist/forge/javascript/probe.js +53 -0
- package/dist/forge/javascript/runtime-manifest.d.ts +14 -0
- package/dist/forge/javascript/runtime-manifest.js +14 -0
- package/dist/forge/javascript/vm-runner.d.ts +21 -0
- package/dist/forge/javascript/vm-runner.js +301 -0
- package/dist/forge/javascript/wasm-asset.d.ts +8 -0
- package/dist/forge/javascript/wasm-asset.js +8 -0
- package/dist/forge/javascript/wasm.d.ts +15 -0
- package/dist/forge/javascript/wasm.js +54 -0
- package/dist/forge/journal.d.ts +105 -0
- package/dist/forge/journal.js +174 -0
- package/dist/forge/kernel.d.ts +76 -0
- package/dist/forge/kernel.js +338 -0
- package/dist/forge/limits.d.ts +38 -0
- package/dist/forge/limits.js +73 -0
- package/dist/forge/nested.d.ts +96 -0
- package/dist/forge/nested.js +214 -0
- package/dist/forge/ports.d.ts +120 -0
- package/dist/forge/ports.js +1 -0
- package/dist/forge/progress.d.ts +24 -0
- package/dist/forge/progress.js +76 -0
- package/dist/forge/protocol.d.ts +110 -0
- package/dist/forge/protocol.js +171 -0
- package/dist/forge/python/backend-selector.d.ts +38 -0
- package/dist/forge/python/backend-selector.js +111 -0
- package/dist/forge/python/backend.d.ts +89 -0
- package/dist/forge/python/backend.js +1 -0
- package/dist/forge/python/engine.d.ts +84 -0
- package/dist/forge/python/engine.js +279 -0
- package/dist/forge/python/frame-parser.d.ts +24 -0
- package/dist/forge/python/frame-parser.js +107 -0
- package/dist/forge/python/frame-transport.d.ts +40 -0
- package/dist/forge/python/frame-transport.js +101 -0
- package/dist/forge/python/import-scan.d.ts +22 -0
- package/dist/forge/python/import-scan.js +133 -0
- package/dist/forge/python/managed-backend.d.ts +30 -0
- package/dist/forge/python/managed-backend.js +111 -0
- package/dist/forge/python/oci-backend.d.ts +27 -0
- package/dist/forge/python/oci-backend.js +84 -0
- package/dist/forge/python/pyodide-backend.d.ts +40 -0
- package/dist/forge/python/pyodide-backend.js +289 -0
- package/dist/forge/python/pyodide-install.d.ts +41 -0
- package/dist/forge/python/pyodide-install.js +167 -0
- package/dist/forge/python/pyodide-manifest.d.ts +42 -0
- package/dist/forge/python/pyodide-manifest.js +376 -0
- package/dist/forge/python/pyodide-runner-source.d.ts +2 -0
- package/dist/forge/python/pyodide-runner-source.js +3 -0
- package/dist/forge/python/rss.d.ts +2 -0
- package/dist/forge/python/rss.js +33 -0
- package/dist/forge/python/scratch-copy.d.ts +6 -0
- package/dist/forge/python/scratch-copy.js +61 -0
- package/dist/forge/python/settle.d.ts +6 -0
- package/dist/forge/python/settle.js +21 -0
- package/dist/forge/python/status.d.ts +8 -0
- package/dist/forge/python/status.js +15 -0
- package/dist/forge/python/wasm-node.d.ts +59 -0
- package/dist/forge/python/wasm-node.js +220 -0
- package/dist/forge/python/wiring.d.ts +39 -0
- package/dist/forge/python/wiring.js +47 -0
- package/dist/forge/registry.d.ts +26 -0
- package/dist/forge/registry.js +83 -0
- package/dist/forge/result.d.ts +58 -0
- package/dist/forge/result.js +86 -0
- package/dist/forge/routing-rule.d.ts +19 -0
- package/dist/forge/routing-rule.js +31 -0
- package/dist/forge/runtime.d.ts +64 -0
- package/dist/forge/runtime.js +88 -0
- package/dist/forge/scratch-files.d.ts +36 -0
- package/dist/forge/scratch-files.js +94 -0
- package/dist/forge/scratch-provider.d.ts +41 -0
- package/dist/forge/scratch-provider.js +79 -0
- package/dist/forge/scratch-quota.d.ts +8 -0
- package/dist/forge/scratch-quota.js +25 -0
- package/dist/forge/selector.d.ts +35 -0
- package/dist/forge/selector.js +51 -0
- package/dist/forge/session-dir.d.ts +59 -0
- package/dist/forge/session-dir.js +239 -0
- package/dist/forge/session-path.d.ts +31 -0
- package/dist/forge/session-path.js +61 -0
- package/dist/forge/slots.d.ts +16 -0
- package/dist/forge/slots.js +47 -0
- package/dist/forge/state-delta.d.ts +54 -0
- package/dist/forge/state-delta.js +99 -0
- package/dist/forge/state-machine.d.ts +20 -0
- package/dist/forge/state-machine.js +40 -0
- package/dist/forge/state-store.d.ts +25 -0
- package/dist/forge/state-store.js +50 -0
- package/dist/forge/task-log.d.ts +9 -0
- package/dist/forge/task-log.js +42 -0
- package/dist/forge/verdict.d.ts +20 -0
- package/dist/forge/verdict.js +28 -0
- package/dist/forge-isolate.d.ts +6 -0
- package/dist/forge-isolate.js +6 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/permissions/decide.d.ts +42 -0
- package/dist/permissions/decide.js +24 -0
- package/dist/resources/context.d.ts +2 -1
- package/dist/resources/context.js +2 -1
- package/dist/runtime/access.d.ts +34 -0
- package/dist/runtime/access.js +80 -0
- package/dist/runtime/process.d.ts +2 -1
- package/dist/runtime/process.js +5 -5
- package/dist/runtime/store.d.ts +11 -0
- package/dist/runtime/store.js +103 -0
- package/dist/tools/analysis.d.ts +25 -2
- package/dist/tools/analysis.js +315 -267
- package/dist/tools/execute.js +6 -7
- package/dist/tools/forge-guide.d.ts +6 -0
- package/dist/tools/forge-guide.js +60 -0
- package/dist/tools/js-run.d.ts +19 -0
- package/dist/tools/js-run.js +126 -0
- package/dist/tools/standard.d.ts +7 -1
- package/dist/tools/standard.js +44 -43
- package/package.json +7 -2
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The capability broker (spec §4.2, §6.4): the kernel's side of the guest's messages. A script sees
|
|
3
|
+
* proxy functions; every real decision happens here, with the numbers the HOST keeps:
|
|
4
|
+
*
|
|
5
|
+
* - nested calls go through the run's nested-call dispatcher (the same gate as a direct call, no
|
|
6
|
+
* new approvals, D7/D-24), paced by the `BudgetLedger` (total, concurrent, credits);
|
|
7
|
+
* - sizes, output and state are bounded again no matter what the prelude checked;
|
|
8
|
+
* - every nested call is journaled and reported as `ForgeProgress`.
|
|
9
|
+
*
|
|
10
|
+
* Errors that mean "the guest broke a limit" are thrown as `ForgeError`: the engine ends the
|
|
11
|
+
* execution with that code. Errors of one tool call are answered to the guest as `ok: false`.
|
|
12
|
+
*/
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
import { describeTool, searchCatalog } from "./catalog-view.js";
|
|
15
|
+
import { ForgeError } from "./errors.js";
|
|
16
|
+
import { clipText } from "./result.js";
|
|
17
|
+
/** Maximum catalog lookups per execution; they are cheap but not free. */
|
|
18
|
+
export const MAX_CATALOG_REQUESTS = 200;
|
|
19
|
+
/** Progress messages one execution may report; the rest is dropped (counted, not stored). */
|
|
20
|
+
export const MAX_PROGRESS_MESSAGES = 10_000;
|
|
21
|
+
/** Characters of one progress message the host keeps. */
|
|
22
|
+
export const MAX_PROGRESS_CHARS = 500;
|
|
23
|
+
/** Bytes of state operations one execution may queue (the commit enforces the stored total). */
|
|
24
|
+
export const MAX_STATE_DELTA_BYTES = 1024 * 1024;
|
|
25
|
+
const DENIED = new Set(["PERMISSION_DENIED", "APPROVAL_REQUIRED"]);
|
|
26
|
+
const sha256 = (text) => createHash("sha256").update(text).digest("hex");
|
|
27
|
+
const positionOf = (nestedCallId) => Number(nestedCallId.slice(nestedCallId.lastIndexOf(":") + 1));
|
|
28
|
+
export class CapabilityBroker {
|
|
29
|
+
#deps;
|
|
30
|
+
#now;
|
|
31
|
+
output = [];
|
|
32
|
+
stats = { total: 0, failed: 0, denied: 0 };
|
|
33
|
+
#inflight = new Set();
|
|
34
|
+
#send = () => { };
|
|
35
|
+
/** Progress messages dropped past `MAX_PROGRESS_MESSAGES` (reported in the journal metrics). */
|
|
36
|
+
progressDropped = 0;
|
|
37
|
+
#progressSeen = 0;
|
|
38
|
+
#catalogRequests = 0;
|
|
39
|
+
#stateBytes = 0;
|
|
40
|
+
constructor(deps) {
|
|
41
|
+
this.#deps = deps;
|
|
42
|
+
this.#now = deps.now ?? Date.now;
|
|
43
|
+
}
|
|
44
|
+
bind(send) {
|
|
45
|
+
this.#send = send;
|
|
46
|
+
}
|
|
47
|
+
/** Resolves when every call the guest started has finished (or the timeout passed). */
|
|
48
|
+
async drain(timeoutMs) {
|
|
49
|
+
if (this.#inflight.size === 0)
|
|
50
|
+
return;
|
|
51
|
+
await Promise.race([
|
|
52
|
+
Promise.allSettled([...this.#inflight]),
|
|
53
|
+
new Promise((resolve) => setTimeout(resolve, timeoutMs).unref?.()),
|
|
54
|
+
]);
|
|
55
|
+
}
|
|
56
|
+
async receive(message) {
|
|
57
|
+
switch (message.type) {
|
|
58
|
+
case "ready":
|
|
59
|
+
return { type: "grant", payload: this.#deps.ledger.grant() };
|
|
60
|
+
case "progress": {
|
|
61
|
+
if (++this.#progressSeen > MAX_PROGRESS_MESSAGES) {
|
|
62
|
+
if (this.progressDropped++ === 0)
|
|
63
|
+
this.#deps.log(`progress: more than ${MAX_PROGRESS_MESSAGES} messages; the rest is dropped`);
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
const text = clipText(message.payload.message, MAX_PROGRESS_CHARS).text;
|
|
67
|
+
this.#deps.log(`progress: ${text}`);
|
|
68
|
+
this.#deps.update({
|
|
69
|
+
kind: "forge",
|
|
70
|
+
executionId: this.#deps.executionId,
|
|
71
|
+
engine: this.#deps.engine,
|
|
72
|
+
phase: "running",
|
|
73
|
+
message: text,
|
|
74
|
+
...(message.payload.completed !== undefined
|
|
75
|
+
? { completed: message.payload.completed }
|
|
76
|
+
: {}),
|
|
77
|
+
...(message.payload.total !== undefined ? { total: message.payload.total } : {}),
|
|
78
|
+
});
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
case "output":
|
|
82
|
+
this.#recordOutput(message.payload);
|
|
83
|
+
return undefined;
|
|
84
|
+
case "state.set":
|
|
85
|
+
this.#state().set(message.payload.key, message.payload.value);
|
|
86
|
+
this.#countState(message.payload.key, message.payload.value);
|
|
87
|
+
return undefined;
|
|
88
|
+
case "state.delete":
|
|
89
|
+
this.#state().delete(message.payload.key);
|
|
90
|
+
this.#countState(message.payload.key, null);
|
|
91
|
+
return undefined;
|
|
92
|
+
case "catalog":
|
|
93
|
+
return this.#catalog(message.payload);
|
|
94
|
+
case "call": {
|
|
95
|
+
// Not awaited by the engine's dispatch: calls of one script run concurrently.
|
|
96
|
+
const job = this.#call(message.payload);
|
|
97
|
+
this.#inflight.add(job);
|
|
98
|
+
const settled = () => this.#inflight.delete(job);
|
|
99
|
+
job.then(settled, settled);
|
|
100
|
+
await job;
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
default:
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
#state() {
|
|
108
|
+
const state = this.#deps.state;
|
|
109
|
+
if (!state)
|
|
110
|
+
throw new ForgeError("INVALID_INPUT", "stored state is switched off (forge.state.enabled)");
|
|
111
|
+
return state;
|
|
112
|
+
}
|
|
113
|
+
#countState(key, value) {
|
|
114
|
+
this.#stateBytes += key.length + JSON.stringify(value).length;
|
|
115
|
+
if (this.#stateBytes > MAX_STATE_DELTA_BYTES)
|
|
116
|
+
throw new ForgeError("BUDGET_EXCEEDED", "this execution changed too much stored state");
|
|
117
|
+
}
|
|
118
|
+
#recordOutput(item) {
|
|
119
|
+
const bytes = Buffer.byteLength(JSON.stringify(item), "utf8");
|
|
120
|
+
this.#deps.ledger.recordOutput(bytes);
|
|
121
|
+
this.output.push(item);
|
|
122
|
+
if (item.type === "text")
|
|
123
|
+
this.#deps.log(item.text.replace(/\n$/, ""));
|
|
124
|
+
}
|
|
125
|
+
#catalog(payload) {
|
|
126
|
+
if (++this.#catalogRequests > MAX_CATALOG_REQUESTS)
|
|
127
|
+
throw new ForgeError("BUDGET_EXCEEDED", `catalog lookups are limited to ${MAX_CATALOG_REQUESTS} per execution`);
|
|
128
|
+
const tools = this.#deps.dispatcher.catalog();
|
|
129
|
+
const entries = payload.op === "search"
|
|
130
|
+
? searchCatalog(tools, payload.query, payload.limit)
|
|
131
|
+
: [describeTool(tools, payload.query)].filter((entry) => entry !== undefined);
|
|
132
|
+
return { type: "catalog.result", payload: { requestId: payload.requestId, entries } };
|
|
133
|
+
}
|
|
134
|
+
async #call(payload) {
|
|
135
|
+
const { ledger, journal, executionId, limits } = this.#deps;
|
|
136
|
+
// Only names the registry has are ever recorded or broadcast; anything else is a placeholder.
|
|
137
|
+
const tool = this.#deps.dispatcher.knows(payload.tool)
|
|
138
|
+
? payload.tool
|
|
139
|
+
: `unknown:${sha256(payload.tool).slice(0, 12)}`;
|
|
140
|
+
// The host's own count: a call without a credit, or past the total, ends the execution.
|
|
141
|
+
ledger.startCall();
|
|
142
|
+
this.stats.total++;
|
|
143
|
+
const startedAt = this.#now();
|
|
144
|
+
const progress = (status, extra = {}) => ({
|
|
145
|
+
kind: "forge",
|
|
146
|
+
executionId,
|
|
147
|
+
engine: this.#deps.engine,
|
|
148
|
+
phase: "call",
|
|
149
|
+
call: { callId: payload.callId, tool, status, ...extra },
|
|
150
|
+
});
|
|
151
|
+
let journalId;
|
|
152
|
+
let ended = false;
|
|
153
|
+
const end = (status, errorCode) => {
|
|
154
|
+
if (journalId && !ended) {
|
|
155
|
+
ended = true;
|
|
156
|
+
journal.endCall(executionId, journalId, {
|
|
157
|
+
status,
|
|
158
|
+
durationMs: this.#now() - startedAt,
|
|
159
|
+
...(errorCode ? { errorCode } : {}),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
let reply;
|
|
164
|
+
try {
|
|
165
|
+
const inputJson = JSON.stringify(payload.input ?? {});
|
|
166
|
+
if (Buffer.byteLength(inputJson, "utf8") > limits.maxCallInputBytes) {
|
|
167
|
+
reply = this.#error(payload.callId, "TOOL_INPUT_INVALID", `tool input is larger than ${limits.maxCallInputBytes} bytes`);
|
|
168
|
+
this.stats.failed++;
|
|
169
|
+
this.#deps.terminal(progress("failed"));
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
this.#deps.log(`call ${tool}`);
|
|
173
|
+
this.#deps.update(progress("started"));
|
|
174
|
+
const outcome = await this.#deps.dispatcher.call({
|
|
175
|
+
name: payload.tool,
|
|
176
|
+
input: payload.input,
|
|
177
|
+
signal: this.#deps.signal,
|
|
178
|
+
onStart: (start) => {
|
|
179
|
+
journalId = journal.startCall({
|
|
180
|
+
executionId,
|
|
181
|
+
n: positionOf(start.nestedCallId),
|
|
182
|
+
toolName: start.name,
|
|
183
|
+
effect: start.effect,
|
|
184
|
+
inputSha256: sha256(inputJson),
|
|
185
|
+
paths: start.paths,
|
|
186
|
+
});
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
if (outcome.ok) {
|
|
190
|
+
const valueJson = JSON.stringify(outcome.value) ?? "null";
|
|
191
|
+
if (Buffer.byteLength(valueJson, "utf8") > limits.maxCallResponseBytes) {
|
|
192
|
+
reply = this.#error(payload.callId, "OUTPUT_LIMIT", `${payload.tool} returned ${Buffer.byteLength(valueJson, "utf8")} bytes, over the ${limits.maxCallResponseBytes} byte limit; ask for less (a smaller range, a filter, a page)`);
|
|
193
|
+
this.stats.failed++;
|
|
194
|
+
end("failed", "OUTPUT_LIMIT");
|
|
195
|
+
this.#deps.terminal(progress("failed", { durationMs: this.#now() - startedAt }));
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
reply = {
|
|
199
|
+
type: "result",
|
|
200
|
+
payload: { callId: payload.callId, ok: true, value: outcome.value ?? null },
|
|
201
|
+
};
|
|
202
|
+
end("succeeded");
|
|
203
|
+
this.#deps.terminal(progress("succeeded", { durationMs: this.#now() - startedAt }));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
if (!journalId) {
|
|
208
|
+
// Refused before it started (denied, unknown, invalid): still on the record.
|
|
209
|
+
journalId = journal.startCall({
|
|
210
|
+
executionId,
|
|
211
|
+
n: positionOf(outcome.nestedCallId),
|
|
212
|
+
toolName: tool,
|
|
213
|
+
effect: (outcome.effect ?? "external"),
|
|
214
|
+
inputSha256: sha256(inputJson),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
const denied = DENIED.has(outcome.code);
|
|
218
|
+
if (denied)
|
|
219
|
+
this.stats.denied++;
|
|
220
|
+
else
|
|
221
|
+
this.stats.failed++;
|
|
222
|
+
end("failed", outcome.code);
|
|
223
|
+
this.#deps.log(`call ${tool} failed: ${outcome.code}`);
|
|
224
|
+
this.#deps.terminal(progress(denied ? "denied" : "failed", { durationMs: this.#now() - startedAt }));
|
|
225
|
+
reply = this.#error(payload.callId, outcome.code, outcome.message);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
end("failed", "TOOL_FAILED");
|
|
231
|
+
this.stats.failed++;
|
|
232
|
+
this.#deps.terminal(progress("failed", { durationMs: this.#now() - startedAt }));
|
|
233
|
+
reply = this.#error(payload.callId, "TOOL_FAILED", error instanceof Error ? error.message : String(error));
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
ledger.finishCall();
|
|
237
|
+
}
|
|
238
|
+
this.#send(reply);
|
|
239
|
+
this.#send({ type: "grant", payload: ledger.grant() });
|
|
240
|
+
}
|
|
241
|
+
#error(callId, code, message) {
|
|
242
|
+
return { type: "result", payload: { callId, ok: false, error: { code, message } } };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface BudgetLimits {
|
|
2
|
+
maxToolCalls: number;
|
|
3
|
+
maxConcurrency: number;
|
|
4
|
+
maxRetainedOutputBytes: number;
|
|
5
|
+
}
|
|
6
|
+
export declare class BudgetLedger {
|
|
7
|
+
#private;
|
|
8
|
+
constructor(limits: BudgetLimits);
|
|
9
|
+
get stats(): {
|
|
10
|
+
started: number;
|
|
11
|
+
active: number;
|
|
12
|
+
outputBytes: number;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Tops the guest's credits up to what it may still use and returns the increment to send in a
|
|
16
|
+
* `grant` message. Call credits never exceed the free concurrency slots or the remaining total.
|
|
17
|
+
*/
|
|
18
|
+
grant(): {
|
|
19
|
+
calls: number;
|
|
20
|
+
bytes: number;
|
|
21
|
+
};
|
|
22
|
+
/** Spends a credit for a new nested call or throws `BUDGET_EXCEEDED` without changing state. */
|
|
23
|
+
startCall(): void;
|
|
24
|
+
finishCall(): void;
|
|
25
|
+
/** Accounts output bytes, or throws `OUTPUT_LIMIT` without partial accounting. */
|
|
26
|
+
recordOutput(bytes: number): void;
|
|
27
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-execution budget: total and concurrent nested calls, the credits granted to the guest
|
|
3
|
+
* (backpressure, spec §5.4) and retained output bytes. The host enforces these numbers even when
|
|
4
|
+
* the guest prelude checks them too. Pure: no I/O.
|
|
5
|
+
*/
|
|
6
|
+
import { ForgeError } from "./errors.js";
|
|
7
|
+
export class BudgetLedger {
|
|
8
|
+
#limits;
|
|
9
|
+
#started = 0;
|
|
10
|
+
#active = 0;
|
|
11
|
+
#outputBytes = 0;
|
|
12
|
+
#callCredits = 0;
|
|
13
|
+
#byteCredits = 0;
|
|
14
|
+
constructor(limits) {
|
|
15
|
+
this.#limits = limits;
|
|
16
|
+
}
|
|
17
|
+
get stats() {
|
|
18
|
+
return { started: this.#started, active: this.#active, outputBytes: this.#outputBytes };
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Tops the guest's credits up to what it may still use and returns the increment to send in a
|
|
22
|
+
* `grant` message. Call credits never exceed the free concurrency slots or the remaining total.
|
|
23
|
+
*/
|
|
24
|
+
grant() {
|
|
25
|
+
const callTarget = Math.max(0, Math.min(this.#limits.maxConcurrency - this.#active, this.#limits.maxToolCalls - this.#started));
|
|
26
|
+
const calls = Math.max(0, callTarget - this.#callCredits);
|
|
27
|
+
this.#callCredits += calls;
|
|
28
|
+
const bytes = this.#limits.maxRetainedOutputBytes - this.#outputBytes - this.#byteCredits;
|
|
29
|
+
this.#byteCredits += bytes;
|
|
30
|
+
return { calls, bytes };
|
|
31
|
+
}
|
|
32
|
+
/** Spends a credit for a new nested call or throws `BUDGET_EXCEEDED` without changing state. */
|
|
33
|
+
startCall() {
|
|
34
|
+
if (this.#started >= this.#limits.maxToolCalls) {
|
|
35
|
+
throw new ForgeError("BUDGET_EXCEEDED", `Tool call limit reached (${this.#limits.maxToolCalls})`);
|
|
36
|
+
}
|
|
37
|
+
if (this.#active >= this.#limits.maxConcurrency) {
|
|
38
|
+
throw new ForgeError("BUDGET_EXCEEDED", `Concurrent tool call limit reached (${this.#limits.maxConcurrency})`);
|
|
39
|
+
}
|
|
40
|
+
if (this.#callCredits <= 0) {
|
|
41
|
+
throw new ForgeError("BUDGET_EXCEEDED", "Tool call without a granted credit");
|
|
42
|
+
}
|
|
43
|
+
this.#callCredits--;
|
|
44
|
+
this.#started++;
|
|
45
|
+
this.#active++;
|
|
46
|
+
}
|
|
47
|
+
finishCall() {
|
|
48
|
+
if (this.#active <= 0)
|
|
49
|
+
throw new Error("finishCall without an active call");
|
|
50
|
+
this.#active--;
|
|
51
|
+
}
|
|
52
|
+
/** Accounts output bytes, or throws `OUTPUT_LIMIT` without partial accounting. */
|
|
53
|
+
recordOutput(bytes) {
|
|
54
|
+
if (this.#outputBytes + bytes > this.#limits.maxRetainedOutputBytes) {
|
|
55
|
+
throw new ForgeError("OUTPUT_LIMIT", `Retained output limit reached (${this.#limits.maxRetainedOutputBytes} bytes)`);
|
|
56
|
+
}
|
|
57
|
+
this.#outputBytes += bytes;
|
|
58
|
+
this.#byteCredits = Math.max(0, this.#byteCredits - bytes);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `catalog.search` and `catalog.describe` show a script (spec §6.2, D13): the tools the run
|
|
3
|
+
* lets it call right now, found by name or description, never the whole prompt-sized catalog.
|
|
4
|
+
* Pure over the tool definitions.
|
|
5
|
+
*/
|
|
6
|
+
import type { ToolDefinition } from "@alisio/sdk";
|
|
7
|
+
export declare const CATALOG_DEFAULT_LIMIT = 10;
|
|
8
|
+
export declare const CATALOG_MAX_LIMIT = 25;
|
|
9
|
+
export interface CatalogEntry {
|
|
10
|
+
name: string;
|
|
11
|
+
effect: string;
|
|
12
|
+
summary: string;
|
|
13
|
+
}
|
|
14
|
+
export interface CatalogDetail {
|
|
15
|
+
name: string;
|
|
16
|
+
effect: string;
|
|
17
|
+
description: string;
|
|
18
|
+
inputSchema: unknown;
|
|
19
|
+
}
|
|
20
|
+
/** The first sentence of a description, bounded. */
|
|
21
|
+
export declare function summarize(description: string): string;
|
|
22
|
+
/** Tools whose name or description holds every term of the query (all of them for an empty query). */
|
|
23
|
+
export declare function searchCatalog(tools: readonly ToolDefinition[], query: string, limit?: number): CatalogEntry[];
|
|
24
|
+
export declare function describeTool(tools: readonly ToolDefinition[], name: string): CatalogDetail | undefined;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { effectOf } from "../permissions/decide.js";
|
|
2
|
+
export const CATALOG_DEFAULT_LIMIT = 10;
|
|
3
|
+
export const CATALOG_MAX_LIMIT = 25;
|
|
4
|
+
const SUMMARY_CHARS = 160;
|
|
5
|
+
/** The first sentence of a description, bounded. */
|
|
6
|
+
export function summarize(description) {
|
|
7
|
+
const flat = description.replace(/\s+/g, " ").trim();
|
|
8
|
+
const end = flat.search(/[.!?](\s|$)/);
|
|
9
|
+
const sentence = end >= 0 ? flat.slice(0, end + 1) : flat;
|
|
10
|
+
return sentence.length > SUMMARY_CHARS ? `${sentence.slice(0, SUMMARY_CHARS - 1)}…` : sentence;
|
|
11
|
+
}
|
|
12
|
+
const terms = (query) => query
|
|
13
|
+
.toLowerCase()
|
|
14
|
+
.split(/[^a-z0-9_]+/)
|
|
15
|
+
.filter(Boolean);
|
|
16
|
+
/** Tools whose name or description holds every term of the query (all of them for an empty query). */
|
|
17
|
+
export function searchCatalog(tools, query, limit = CATALOG_DEFAULT_LIMIT) {
|
|
18
|
+
const wanted = terms(query);
|
|
19
|
+
const bounded = Math.max(1, Math.min(Math.floor(limit), CATALOG_MAX_LIMIT));
|
|
20
|
+
return tools
|
|
21
|
+
.filter((tool) => {
|
|
22
|
+
const haystack = `${tool.name} ${tool.description}`.toLowerCase();
|
|
23
|
+
return wanted.every((term) => haystack.includes(term));
|
|
24
|
+
})
|
|
25
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
26
|
+
.slice(0, bounded)
|
|
27
|
+
.map((tool) => ({
|
|
28
|
+
name: tool.name,
|
|
29
|
+
effect: effectOf(tool),
|
|
30
|
+
summary: summarize(tool.description),
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
export function describeTool(tools, name) {
|
|
34
|
+
const tool = tools.find((candidate) => candidate.name === name);
|
|
35
|
+
if (!tool)
|
|
36
|
+
return undefined;
|
|
37
|
+
return {
|
|
38
|
+
name: tool.name,
|
|
39
|
+
effect: effectOf(tool),
|
|
40
|
+
description: tool.description,
|
|
41
|
+
inputSchema: tool.inputSchema,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forja error codes and the error type that carries them. Pure: no I/O. The Python specification
|
|
3
|
+
* adds its own codes with the same shape.
|
|
4
|
+
*/
|
|
5
|
+
export declare const FORGE_ERROR_CODES: readonly ["INVALID_INPUT", "ENGINE_UNAVAILABLE", "SCRIPT_SYNTAX", "SCRIPT_RUNTIME", "PERMISSION_DENIED", "APPROVAL_REQUIRED", "TOOL_UNAVAILABLE", "TOOL_INPUT_INVALID", "TOOL_FAILED", "BUDGET_EXCEEDED", "OUTPUT_LIMIT", "MEMORY_LIMIT", "STACK_LIMIT", "TIMEOUT", "CANCELLED", "STATE_CONFLICT", "PROTOCOL_VIOLATION", "UNRESOLVABLE_PROMISE", "ISOLATE_FAILURE",
|
|
6
|
+
/** Python: the artifact publisher refused the files the script left (symlink, size, count). */
|
|
7
|
+
"PUBLICATION_REJECTED"];
|
|
8
|
+
export type ForgeErrorCode = (typeof FORGE_ERROR_CODES)[number];
|
|
9
|
+
/** Points the model at the guide topic that explains how to recover from an error. */
|
|
10
|
+
export interface GuideHint {
|
|
11
|
+
topic: string;
|
|
12
|
+
reason: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The one-line pointer the model reads (spec §8.4): the reason and the guide topic to call, never the
|
|
16
|
+
* guide itself. An anchor (`js.patterns#filtering`) names a section of the topic.
|
|
17
|
+
*/
|
|
18
|
+
export declare function renderHint(hint: GuideHint): string;
|
|
19
|
+
export declare class ForgeError extends Error {
|
|
20
|
+
readonly code: ForgeErrorCode;
|
|
21
|
+
readonly hint?: GuideHint;
|
|
22
|
+
constructor(code: ForgeErrorCode, message: string, options?: {
|
|
23
|
+
hint?: GuideHint;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export declare function isForgeError(value: unknown): value is ForgeError;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forja error codes and the error type that carries them. Pure: no I/O. The Python specification
|
|
3
|
+
* adds its own codes with the same shape.
|
|
4
|
+
*/
|
|
5
|
+
export const FORGE_ERROR_CODES = [
|
|
6
|
+
"INVALID_INPUT",
|
|
7
|
+
"ENGINE_UNAVAILABLE",
|
|
8
|
+
"SCRIPT_SYNTAX",
|
|
9
|
+
"SCRIPT_RUNTIME",
|
|
10
|
+
"PERMISSION_DENIED",
|
|
11
|
+
"APPROVAL_REQUIRED",
|
|
12
|
+
"TOOL_UNAVAILABLE",
|
|
13
|
+
"TOOL_INPUT_INVALID",
|
|
14
|
+
"TOOL_FAILED",
|
|
15
|
+
"BUDGET_EXCEEDED",
|
|
16
|
+
"OUTPUT_LIMIT",
|
|
17
|
+
"MEMORY_LIMIT",
|
|
18
|
+
"STACK_LIMIT",
|
|
19
|
+
"TIMEOUT",
|
|
20
|
+
"CANCELLED",
|
|
21
|
+
"STATE_CONFLICT",
|
|
22
|
+
"PROTOCOL_VIOLATION",
|
|
23
|
+
"UNRESOLVABLE_PROMISE",
|
|
24
|
+
"ISOLATE_FAILURE",
|
|
25
|
+
/** Python: the artifact publisher refused the files the script left (symlink, size, count). */
|
|
26
|
+
"PUBLICATION_REJECTED",
|
|
27
|
+
];
|
|
28
|
+
/**
|
|
29
|
+
* The one-line pointer the model reads (spec §8.4): the reason and the guide topic to call, never the
|
|
30
|
+
* guide itself. An anchor (`js.patterns#filtering`) names a section of the topic.
|
|
31
|
+
*/
|
|
32
|
+
export function renderHint(hint) {
|
|
33
|
+
const [topic = hint.topic, anchor] = hint.topic.split("#");
|
|
34
|
+
return `${hint.reason} (forge_guide topic "${topic}"${anchor ? `, section #${anchor}` : ""})`;
|
|
35
|
+
}
|
|
36
|
+
export class ForgeError extends Error {
|
|
37
|
+
code;
|
|
38
|
+
hint;
|
|
39
|
+
constructor(code, message, options = {}) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.name = "ForgeError";
|
|
42
|
+
this.code = code;
|
|
43
|
+
if (options.hint)
|
|
44
|
+
this.hint = options.hint;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function isForgeError(value) {
|
|
48
|
+
return value instanceof ForgeError;
|
|
49
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The JavaScript topics of `forge_guide` (spec §8.3): `js.api`, `js.patterns` and `js.tools`. Pure
|
|
3
|
+
* functions of the engine's live state (effective limits, host) and, for `js.tools`, of the tools
|
|
4
|
+
* a script can call right now. No I/O here; the provider gathers the state.
|
|
5
|
+
*/
|
|
6
|
+
import type { IsolationLevel } from "@alisio/sdk";
|
|
7
|
+
import type { ForgeLimits } from "../limits.ts";
|
|
8
|
+
/** What the guide needs to know about the running JavaScript engine. */
|
|
9
|
+
export interface JavascriptGuideState {
|
|
10
|
+
/** Engine build, e.g. `quickjs-wasi@3.6.2`. */
|
|
11
|
+
version: string;
|
|
12
|
+
isolation: IsolationLevel;
|
|
13
|
+
host: "worker" | "process";
|
|
14
|
+
limits: ForgeLimits;
|
|
15
|
+
}
|
|
16
|
+
/** One line of the permission-filtered catalog summary. */
|
|
17
|
+
export interface CatalogSummaryEntry {
|
|
18
|
+
name: string;
|
|
19
|
+
effect: string;
|
|
20
|
+
}
|
|
21
|
+
/** The most entries `js.tools` lists (spec §8.3). */
|
|
22
|
+
export declare const TOOLS_GUIDE_MAX_ENTRIES = 40;
|
|
23
|
+
export declare function jsApiGuide(state: JavascriptGuideState): string;
|
|
24
|
+
export declare function jsPatternsGuide(state: JavascriptGuideState): string;
|
|
25
|
+
export declare function jsToolsGuide(catalog: readonly CatalogSummaryEntry[] | undefined): string;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/** The most entries `js.tools` lists (spec §8.3). */
|
|
2
|
+
export const TOOLS_GUIDE_MAX_ENTRIES = 40;
|
|
3
|
+
const KiB = 1024;
|
|
4
|
+
const MiB = 1024 * KiB;
|
|
5
|
+
const size = (bytes) => bytes >= MiB && bytes % MiB === 0
|
|
6
|
+
? `${bytes / MiB} MiB`
|
|
7
|
+
: bytes >= KiB
|
|
8
|
+
? `${Math.round(bytes / KiB)} KiB`
|
|
9
|
+
: `${bytes} B`;
|
|
10
|
+
const seconds = (ms) => `${Math.round((ms / 1000) * 10) / 10} s`;
|
|
11
|
+
export function jsApiGuide(state) {
|
|
12
|
+
const l = state.limits;
|
|
13
|
+
const where = state.host === "worker" ? "a worker thread of this process" : "a child process of this program";
|
|
14
|
+
return [
|
|
15
|
+
`# js_run API (${state.version})`,
|
|
16
|
+
"",
|
|
17
|
+
`Isolation: ${state.isolation}. A fresh VM per run, hosted in ${where}. No imports, require, network, files, timers or WebAssembly. The body is an async function: use await and \`return\` a small JSON value.`,
|
|
18
|
+
"",
|
|
19
|
+
"## Globals",
|
|
20
|
+
"- `tools.call(name, input?)`: call another tool; resolves to its JSON result (or its text), rejects with an error that has `code` and `message`. `callTool` is the same.",
|
|
21
|
+
"- `catalog.search(query, {limit?})`: `[{name, effect, summary}]`. `catalog.describe(name)`: `{name, effect, description, inputSchema}`. Call it before using a tool you do not know.",
|
|
22
|
+
"- `state.get(key)`, `state.set(key, value)`, `state.delete(key)`: small JSON kept for later runs of this session; committed only if the run succeeds.",
|
|
23
|
+
"- `text(value)`: explicit output for the model. `progress({message, completed?, total?})`: live progress. `console.log/warn/error` go to the output too.",
|
|
24
|
+
"- `parallel.map(items, fn, {concurrency?})`: results in input order; no new item starts after the first failure and it rejects with that error. `parallel.settle(...)`: every result as `{ok:true,value}` or `{ok:false,error:{code,message}}`.",
|
|
25
|
+
"",
|
|
26
|
+
"## Effective limits now",
|
|
27
|
+
`- Time ${seconds(l.timeoutMs)} per run (\`timeoutMs\` can only lower it); heap ${size(l.memoryBytes)}; stack ${size(l.stackBytes)}; source ${size(l.maxSourceBytes)}.`,
|
|
28
|
+
`- Nested calls: ${l.maxToolCalls} per run, ${l.maxConcurrency} at once (\`Promise.all\` does not avoid this); input ${size(l.maxCallInputBytes)}, response ${size(l.maxCallResponseBytes)} each.`,
|
|
29
|
+
`- Returned to you: at most ${l.maxOutputChars} characters (\`OUTPUT_LIMIT\` beyond that); retained output ${size(l.maxRetainedOutputBytes)}.`,
|
|
30
|
+
"",
|
|
31
|
+
"## Permissions",
|
|
32
|
+
"Nested calls pass the same gate as a direct call of this session and never open an approval: a tool that needs one fails with `APPROVAL_REQUIRED`; call it directly instead. `js.tools` lists what is callable now.",
|
|
33
|
+
"",
|
|
34
|
+
"## Errors you can catch",
|
|
35
|
+
"`TOOL_UNAVAILABLE`, `TOOL_INPUT_INVALID`, `TOOL_FAILED`, `PERMISSION_DENIED`, `APPROVAL_REQUIRED`, `BUDGET_EXCEEDED`, `OUTPUT_LIMIT`. Anything else a script throws is `SCRIPT_RUNTIME`.",
|
|
36
|
+
].join("\n");
|
|
37
|
+
}
|
|
38
|
+
export function jsPatternsGuide(state) {
|
|
39
|
+
const l = state.limits;
|
|
40
|
+
return [
|
|
41
|
+
"# js_run patterns",
|
|
42
|
+
"",
|
|
43
|
+
"## #filtering",
|
|
44
|
+
`Filter, aggregate or count INSIDE the script and return the result, not the data: the model sees at most ${l.maxOutputChars} characters.`,
|
|
45
|
+
"```js",
|
|
46
|
+
'const rows = await tools.call("<tool>", { /* input */ }); // check the shape with catalog.describe',
|
|
47
|
+
"const big = rows.filter((r) => r.size > 1000);",
|
|
48
|
+
"return { total: rows.length, big: big.length, sample: big.slice(0, 5) };",
|
|
49
|
+
"```",
|
|
50
|
+
"",
|
|
51
|
+
"## #parallel",
|
|
52
|
+
`Use \`parallel.map(items, fn, {concurrency: 4})\` for independent reads: up to ${l.maxConcurrency} run at once, ${l.maxToolCalls} in total. It stops at the first failure. Use \`parallel.settle\` when partial results are fine and report the failed ones.`,
|
|
53
|
+
"```js",
|
|
54
|
+
'const results = await parallel.settle(paths, (p) => tools.call("read_file", { path: p, limit: 50 }));',
|
|
55
|
+
"return results.map((r, i) => (r.ok ? { path: paths[i], ok: true } : { path: paths[i], error: r.error.code }));",
|
|
56
|
+
"```",
|
|
57
|
+
"",
|
|
58
|
+
"## #pagination",
|
|
59
|
+
"Loop until the tool says there is no more, and keep only what you need from each page. Count calls: the budget is shared by the whole run.",
|
|
60
|
+
"",
|
|
61
|
+
"## #state",
|
|
62
|
+
"`state.set('cursor', value)` survives to the next run, but only when this run succeeds. Keep it small JSON; large data belongs in a scratchpad file.",
|
|
63
|
+
"",
|
|
64
|
+
"## #files",
|
|
65
|
+
"Scripts cannot touch files. To keep a long script, write it to the scratchpad with the file tools and run it with `js_run` `codePath` (a .js file); edit it between runs instead of resending the code. Intermediate results go to scratchpad files the same way.",
|
|
66
|
+
"",
|
|
67
|
+
"## #errors",
|
|
68
|
+
"Wrap risky calls in try/catch and return `{ error: e.code }`. A script that throws ends the run; one that returns a value is a success even if some calls failed (check `calls` in the result).",
|
|
69
|
+
].join("\n");
|
|
70
|
+
}
|
|
71
|
+
export function jsToolsGuide(catalog) {
|
|
72
|
+
const head = ["# Tools a script can call now", ""];
|
|
73
|
+
if (!catalog)
|
|
74
|
+
return [
|
|
75
|
+
...head,
|
|
76
|
+
"The tool list depends on the live session and is not available here. Inside a script, `await catalog.search('')` lists what is callable and `await catalog.describe(name)` gives the schema.",
|
|
77
|
+
].join("\n");
|
|
78
|
+
const sorted = [...catalog].sort((a, b) => a.name.localeCompare(b.name));
|
|
79
|
+
const shown = sorted.slice(0, TOOLS_GUIDE_MAX_ENTRIES);
|
|
80
|
+
const more = sorted.length - shown.length;
|
|
81
|
+
return [
|
|
82
|
+
...head,
|
|
83
|
+
`Only tools this session's permissions allow without asking are listed (${sorted.length}). Others fail with \`APPROVAL_REQUIRED\` or \`PERMISSION_DENIED\`; call those directly.`,
|
|
84
|
+
"",
|
|
85
|
+
...shown.map((tool) => `- ${tool.name} (${tool.effect})`),
|
|
86
|
+
...(more > 0 ? [`- ... and ${more} more: use \`catalog.search(query)\``] : []),
|
|
87
|
+
"",
|
|
88
|
+
"Details and input schemas: `await catalog.describe(name)` inside the script. Never reachable from a script: `js_run`, `forge_guide`, `task`, `task_wait`, `send_message`, `ask_user_question`. `python_run` only when this run allows analysis or process effects.",
|
|
89
|
+
].join("\n");
|
|
90
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type CatalogSummaryEntry, type JavascriptGuideState } from "./javascript.ts";
|
|
2
|
+
import { type PythonEnvironment } from "./python.ts";
|
|
3
|
+
export declare const JS_GUIDE_TOPICS: readonly ["js.api", "js.patterns", "js.tools"];
|
|
4
|
+
export declare const PYTHON_GUIDE_TOPICS: readonly ["python.runtime", "python.data", "python.ml", "python.dashboards", "python.environment"];
|
|
5
|
+
export declare const GUIDE_TOPICS: readonly ["js.api", "js.patterns", "js.tools", "python.runtime", "python.data", "python.ml", "python.dashboards", "python.environment"];
|
|
6
|
+
export type GuideTopic = (typeof GUIDE_TOPICS)[number];
|
|
7
|
+
/** Hard bound of one topic (spec §8.3). */
|
|
8
|
+
export declare const GUIDE_MAX_BYTES = 4096;
|
|
9
|
+
/** Characters of the SHA-256 kept as the etag. */
|
|
10
|
+
export declare const ETAG_LENGTH = 16;
|
|
11
|
+
export interface GuideSources {
|
|
12
|
+
/** Present when the JavaScript engine is ready; its absence removes the `js.*` topics. */
|
|
13
|
+
javascript?: () => Promise<JavascriptGuideState>;
|
|
14
|
+
/** Present when `python_run` is registered; its absence removes the `python.*` topics. */
|
|
15
|
+
python?: () => Promise<PythonEnvironment>;
|
|
16
|
+
}
|
|
17
|
+
export type GuideResponse = {
|
|
18
|
+
unchanged: true;
|
|
19
|
+
etag: string;
|
|
20
|
+
} | {
|
|
21
|
+
unchanged: false;
|
|
22
|
+
topic: GuideTopic;
|
|
23
|
+
etag: string;
|
|
24
|
+
content: string;
|
|
25
|
+
};
|
|
26
|
+
export interface GuideRequest {
|
|
27
|
+
topic: string;
|
|
28
|
+
ifNoneMatch?: string | undefined;
|
|
29
|
+
/** The tools a script can call right now (for `js.tools`). */
|
|
30
|
+
catalog?: readonly CatalogSummaryEntry[] | undefined;
|
|
31
|
+
}
|
|
32
|
+
export declare class UnknownGuideTopicError extends Error {
|
|
33
|
+
constructor(topic: string, available: readonly string[]);
|
|
34
|
+
}
|
|
35
|
+
export declare const etagOf: (content: string) => string;
|
|
36
|
+
export declare class GuideProvider {
|
|
37
|
+
#private;
|
|
38
|
+
constructor(sources: GuideSources);
|
|
39
|
+
/** The topics that exist now: the JavaScript ones with a ready engine, the Python ones with `python_run`. */
|
|
40
|
+
topics(): GuideTopic[];
|
|
41
|
+
get(request: GuideRequest): Promise<GuideResponse>;
|
|
42
|
+
}
|