@alisio/core 0.5.2 → 0.7.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 +42 -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/eval/canonical.d.ts +14 -0
- package/dist/eval/canonical.js +40 -0
- package/dist/eval/cassette.d.ts +180 -0
- package/dist/eval/cassette.js +424 -0
- package/dist/eval/compare.d.ts +147 -0
- package/dist/eval/compare.js +436 -0
- package/dist/eval/engine.d.ts +144 -0
- package/dist/eval/engine.js +728 -0
- package/dist/eval/export.d.ts +40 -0
- package/dist/eval/export.js +90 -0
- package/dist/eval/fingerprint.d.ts +88 -0
- package/dist/eval/fingerprint.js +186 -0
- package/dist/eval/normalize.d.ts +68 -0
- package/dist/eval/normalize.js +138 -0
- package/dist/eval/paths.d.ts +11 -0
- package/dist/eval/paths.js +50 -0
- package/dist/eval/plan.d.ts +26 -0
- package/dist/eval/plan.js +44 -0
- package/dist/eval/report.d.ts +53 -0
- package/dist/eval/report.js +258 -0
- package/dist/eval/run-suite.d.ts +68 -0
- package/dist/eval/run-suite.js +94 -0
- package/dist/eval/secrets.d.ts +12 -0
- package/dist/eval/secrets.js +45 -0
- package/dist/eval/stats.d.ts +17 -0
- package/dist/eval/stats.js +50 -0
- package/dist/eval/suite.d.ts +20 -0
- package/dist/eval/suite.js +165 -0
- package/dist/eval/variants.d.ts +35 -0
- package/dist/eval/variants.js +137 -0
- package/dist/eval/verifiers.d.ts +49 -0
- package/dist/eval/verifiers.js +264 -0
- 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 +27 -0
- package/dist/forge/javascript/hosts/worker-host.js +116 -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 +339 -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 +23 -1
- package/dist/index.js +23 -1
- package/dist/permissions/decide.d.ts +42 -0
- package/dist/permissions/decide.js +24 -0
- package/dist/plan/exit-plan.js +2 -1
- 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/ids.d.ts +21 -0
- package/dist/runtime/ids.js +41 -1
- 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 +106 -2
- 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,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
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `GuideProvider` (spec §8.3, D12, Lazy Load): layer 2 of the incremental context. Each topic is
|
|
3
|
+
* generated on request from the live state of the engines, so a guide never describes a limit, a
|
|
4
|
+
* package or a guarantee the engine does not have right now. Nothing is built at construction.
|
|
5
|
+
* Every topic is at most 4 KiB and carries an `etag` (truncated SHA-256 of its content); a caller
|
|
6
|
+
* that already holds the same etag gets `{ unchanged: true }`.
|
|
7
|
+
*/
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { jsApiGuide, jsPatternsGuide, jsToolsGuide, } from "./javascript.js";
|
|
10
|
+
import { pythonDashboardsGuide, pythonDataGuide, pythonEnvironmentGuide, pythonMlGuide, pythonRuntimeGuide, } from "./python.js";
|
|
11
|
+
export const JS_GUIDE_TOPICS = ["js.api", "js.patterns", "js.tools"];
|
|
12
|
+
export const PYTHON_GUIDE_TOPICS = [
|
|
13
|
+
"python.runtime",
|
|
14
|
+
"python.data",
|
|
15
|
+
"python.ml",
|
|
16
|
+
"python.dashboards",
|
|
17
|
+
"python.environment",
|
|
18
|
+
];
|
|
19
|
+
export const GUIDE_TOPICS = [...JS_GUIDE_TOPICS, ...PYTHON_GUIDE_TOPICS];
|
|
20
|
+
/** Hard bound of one topic (spec §8.3). */
|
|
21
|
+
export const GUIDE_MAX_BYTES = 4096;
|
|
22
|
+
/** Characters of the SHA-256 kept as the etag. */
|
|
23
|
+
export const ETAG_LENGTH = 16;
|
|
24
|
+
export class UnknownGuideTopicError extends Error {
|
|
25
|
+
constructor(topic, available) {
|
|
26
|
+
super(`Unknown guide topic "${topic}". Available: ${available.join(", ")}`);
|
|
27
|
+
this.name = "UnknownGuideTopicError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export const etagOf = (content) => createHash("sha256").update(content).digest("hex").slice(0, ETAG_LENGTH);
|
|
31
|
+
/** Cuts to the byte bound on a character boundary, saying so. */
|
|
32
|
+
function bounded(content) {
|
|
33
|
+
if (Buffer.byteLength(content) <= GUIDE_MAX_BYTES)
|
|
34
|
+
return content;
|
|
35
|
+
const note = "\n[guide cut at 4 KiB]";
|
|
36
|
+
let out = content;
|
|
37
|
+
while (Buffer.byteLength(out) + Buffer.byteLength(note) > GUIDE_MAX_BYTES)
|
|
38
|
+
out = out.slice(0, -64);
|
|
39
|
+
return out + note;
|
|
40
|
+
}
|
|
41
|
+
export class GuideProvider {
|
|
42
|
+
#sources;
|
|
43
|
+
constructor(sources) {
|
|
44
|
+
this.#sources = sources;
|
|
45
|
+
}
|
|
46
|
+
/** The topics that exist now: the JavaScript ones with a ready engine, the Python ones with `python_run`. */
|
|
47
|
+
topics() {
|
|
48
|
+
return [
|
|
49
|
+
...(this.#sources.javascript ? JS_GUIDE_TOPICS : []),
|
|
50
|
+
...(this.#sources.python ? PYTHON_GUIDE_TOPICS : []),
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
async get(request) {
|
|
54
|
+
const available = this.topics();
|
|
55
|
+
const topic = available.find((candidate) => candidate === request.topic);
|
|
56
|
+
if (!topic)
|
|
57
|
+
throw new UnknownGuideTopicError(request.topic, available);
|
|
58
|
+
const content = bounded(await this.#render(topic, request));
|
|
59
|
+
const etag = etagOf(content);
|
|
60
|
+
if (request.ifNoneMatch !== undefined && request.ifNoneMatch === etag)
|
|
61
|
+
return { unchanged: true, etag };
|
|
62
|
+
return { unchanged: false, topic, etag, content };
|
|
63
|
+
}
|
|
64
|
+
async #render(topic, request) {
|
|
65
|
+
switch (topic) {
|
|
66
|
+
case "js.api":
|
|
67
|
+
return jsApiGuide(await this.#js());
|
|
68
|
+
case "js.patterns":
|
|
69
|
+
return jsPatternsGuide(await this.#js());
|
|
70
|
+
case "js.tools":
|
|
71
|
+
return jsToolsGuide(request.catalog);
|
|
72
|
+
case "python.runtime":
|
|
73
|
+
return pythonRuntimeGuide(await this.#py());
|
|
74
|
+
case "python.data":
|
|
75
|
+
return pythonDataGuide(await this.#py());
|
|
76
|
+
case "python.ml":
|
|
77
|
+
return pythonMlGuide(await this.#py());
|
|
78
|
+
case "python.dashboards":
|
|
79
|
+
return pythonDashboardsGuide(await this.#py());
|
|
80
|
+
case "python.environment":
|
|
81
|
+
return pythonEnvironmentGuide(await this.#py());
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
#js() {
|
|
85
|
+
const source = this.#sources.javascript;
|
|
86
|
+
if (!source)
|
|
87
|
+
throw new Error("the JavaScript engine is not available");
|
|
88
|
+
return source();
|
|
89
|
+
}
|
|
90
|
+
#py() {
|
|
91
|
+
const source = this.#sources.python;
|
|
92
|
+
if (!source)
|
|
93
|
+
throw new Error("python_run is not available");
|
|
94
|
+
return source();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { PythonBackend, PythonBackendId } from "../python/backend.ts";
|
|
2
|
+
import { type IsolationPolicy, type PythonRuntimeSetting } from "../python/backend-selector.ts";
|
|
3
|
+
import type { PythonEnvironment } from "./python.ts";
|
|
4
|
+
export interface PythonEnvironmentDeps {
|
|
5
|
+
backends: Partial<Record<PythonBackendId, PythonBackend>>;
|
|
6
|
+
/** `analysis.runtime`, read live. */
|
|
7
|
+
runtime(): PythonRuntimeSetting;
|
|
8
|
+
/** `analysis.isolation`, read live. */
|
|
9
|
+
isolation(): IsolationPolicy;
|
|
10
|
+
/** The extras installed in the managed virtual environment (`analysis`, `science`). */
|
|
11
|
+
managedExtras(): Promise<readonly string[]>;
|
|
12
|
+
/** `analysis.smartDashboard`. */
|
|
13
|
+
smartDashboard(): boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function probePythonEnvironment(deps: PythonEnvironmentDeps, signal?: AbortSignal): Promise<PythonEnvironment>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gathers the `PythonEnvironment` snapshot the Python guides are written from: the backend the
|
|
3
|
+
* selector would choose RIGHT NOW (same chain, same settings as `python_run`, no script known) and
|
|
4
|
+
* the packages that backend really has. It probes readiness like a call would and never starts a
|
|
5
|
+
* script or installs anything.
|
|
6
|
+
*/
|
|
7
|
+
import { EXTRAS_DISTRIBUTIONS } from "../../analysis/requirements.js";
|
|
8
|
+
import { isForgeError } from "../errors.js";
|
|
9
|
+
import { createPythonBackendSelector, neededBackends, } from "../python/backend-selector.js";
|
|
10
|
+
import { PYODIDE_PROFILE_ORDER, PYODIDE_PROFILES } from "../python/pyodide-manifest.js";
|
|
11
|
+
const selector = createPythonBackendSelector();
|
|
12
|
+
async function packagesOf(backend, deps) {
|
|
13
|
+
if (backend.id === "wasm") {
|
|
14
|
+
const installed = (await backend.describe()).profiles ?? [];
|
|
15
|
+
const top = PYODIDE_PROFILE_ORDER.filter((profile) => installed.includes(profile)).at(-1);
|
|
16
|
+
return top
|
|
17
|
+
? {
|
|
18
|
+
known: true,
|
|
19
|
+
packages: [...PYODIDE_PROFILES[top]],
|
|
20
|
+
detail: `Pyodide profile "${top}" is installed (profiles: ${installed.join(", ")}).`,
|
|
21
|
+
}
|
|
22
|
+
: { known: true, packages: [], detail: "No Pyodide package profile is installed (base)." };
|
|
23
|
+
}
|
|
24
|
+
if (backend.id === "managed") {
|
|
25
|
+
const extras = await deps.managedExtras();
|
|
26
|
+
const top = extras.includes("science")
|
|
27
|
+
? "science"
|
|
28
|
+
: extras.includes("analysis")
|
|
29
|
+
? "analysis"
|
|
30
|
+
: undefined;
|
|
31
|
+
return top
|
|
32
|
+
? {
|
|
33
|
+
known: true,
|
|
34
|
+
packages: [...EXTRAS_DISTRIBUTIONS[top]],
|
|
35
|
+
detail: `The "${top}" extras are installed in the managed environment.`,
|
|
36
|
+
}
|
|
37
|
+
: {
|
|
38
|
+
known: true,
|
|
39
|
+
packages: [],
|
|
40
|
+
detail: "No extras are installed; pass `extras` to ask the user to install them.",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
known: false,
|
|
45
|
+
detail: "A container has the packages of its image; scripts cannot install anything (no network).",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export async function probePythonEnvironment(deps, signal) {
|
|
49
|
+
const runtime = deps.runtime();
|
|
50
|
+
const isolation = deps.isolation();
|
|
51
|
+
const smartDashboard = deps.smartDashboard();
|
|
52
|
+
const wanted = neededBackends({ runtime, isolation });
|
|
53
|
+
const readiness = {};
|
|
54
|
+
const networkDenied = {};
|
|
55
|
+
await Promise.all(wanted.map(async (id) => {
|
|
56
|
+
const backend = deps.backends[id];
|
|
57
|
+
readiness[id] = backend
|
|
58
|
+
? await backend.check(signal)
|
|
59
|
+
: { ready: false, reason: `${id} is not available here` };
|
|
60
|
+
if (backend && readiness[id]?.ready && isolation === "strict")
|
|
61
|
+
networkDenied[id] = (await backend.describe()).capabilities.networkDeniedByOS;
|
|
62
|
+
}));
|
|
63
|
+
try {
|
|
64
|
+
const chosen = selector.select({ runtime, isolation, readiness, networkDenied });
|
|
65
|
+
const backend = deps.backends[chosen.engine];
|
|
66
|
+
if (!backend)
|
|
67
|
+
return { available: false, reason: `${chosen.engine} is not available here`, smartDashboard };
|
|
68
|
+
return {
|
|
69
|
+
available: true,
|
|
70
|
+
backend: chosen.engine,
|
|
71
|
+
reason: chosen.reason,
|
|
72
|
+
packages: await packagesOf(backend, deps),
|
|
73
|
+
smartDashboard,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (!isForgeError(error))
|
|
78
|
+
throw error;
|
|
79
|
+
return { available: false, reason: error.message, smartDashboard };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Python topics of `forge_guide` (spec §8.3; Python spec §4.3, §6, §7, §8): `python.runtime`,
|
|
3
|
+
* `python.data`, `python.ml`, `python.dashboards` and `python.environment`. Pure functions of a
|
|
4
|
+
* `PythonEnvironment` snapshot: the backend the selector would choose right now and what that
|
|
5
|
+
* backend really provides. No guide ever claims a guarantee the chosen backend does not give.
|
|
6
|
+
*/
|
|
7
|
+
import type { PythonBackendId } from "../python/backend.ts";
|
|
8
|
+
export type PackageKnowledge =
|
|
9
|
+
/** The packages this backend provides right now (a distribution name each). */
|
|
10
|
+
{
|
|
11
|
+
known: true;
|
|
12
|
+
packages: string[];
|
|
13
|
+
detail: string;
|
|
14
|
+
}
|
|
15
|
+
/** The backend's packages cannot be listed from here (a container image). */
|
|
16
|
+
| {
|
|
17
|
+
known: false;
|
|
18
|
+
detail: string;
|
|
19
|
+
};
|
|
20
|
+
export type PythonEnvironment = {
|
|
21
|
+
available: true;
|
|
22
|
+
backend: PythonBackendId;
|
|
23
|
+
/** Why the selector chose it (its own `reason`). */
|
|
24
|
+
reason: string;
|
|
25
|
+
packages: PackageKnowledge;
|
|
26
|
+
/** `analysis.smartDashboard`: `dashboard_generate` exists. */
|
|
27
|
+
smartDashboard: boolean;
|
|
28
|
+
} | {
|
|
29
|
+
available: false;
|
|
30
|
+
/** The selector's message: every backend's reason and the shortest fix. */
|
|
31
|
+
reason: string;
|
|
32
|
+
smartDashboard: boolean;
|
|
33
|
+
};
|
|
34
|
+
export declare function pythonRuntimeGuide(env: PythonEnvironment): string;
|
|
35
|
+
export declare function pythonDataGuide(env: PythonEnvironment): string;
|
|
36
|
+
export declare function pythonMlGuide(env: PythonEnvironment): string;
|
|
37
|
+
export declare function pythonDashboardsGuide(env: PythonEnvironment): string;
|
|
38
|
+
export declare function pythonEnvironmentGuide(env: PythonEnvironment): string;
|