@yolk-sdk/codemode 0.1.0-canary.96 → 0.1.0-canary.98
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/README.md +19 -7
- package/dist/catalog.d.mts +16 -2
- package/dist/catalog.d.mts.map +1 -1
- package/dist/catalog.mjs +39 -18
- package/dist/catalog.mjs.map +1 -1
- package/dist/executor.d.mts +22 -3
- package/dist/executor.d.mts.map +1 -1
- package/dist/node.d.mts +2 -1
- package/dist/node.d.mts.map +1 -1
- package/dist/node.mjs +183 -5
- package/dist/node.mjs.map +1 -1
- package/dist/tool.d.mts.map +1 -1
- package/dist/tool.mjs +8 -5
- package/dist/tool.mjs.map +1 -1
- package/package.json +3 -3
- package/src/catalog.ts +34 -3
- package/src/executor.ts +22 -3
- package/src/node.ts +217 -5
- package/src/tool.ts +10 -6
package/README.md
CHANGED
|
@@ -6,11 +6,11 @@ resolved tools, filters and aggregates their results, and returns only what matt
|
|
|
6
6
|
## Install
|
|
7
7
|
|
|
8
8
|
```bash
|
|
9
|
-
pnpm add @yolk-sdk/codemode@canary @yolk-sdk/agent@canary effect@4.0.0
|
|
9
|
+
pnpm add @yolk-sdk/codemode@canary @yolk-sdk/agent@canary effect@4.0.0
|
|
10
10
|
```
|
|
11
11
|
|
|
12
12
|
Canary APIs are unstable. Keep all `@yolk-sdk/*` packages on the same version.
|
|
13
|
-
Use the SDK's matching Effect version (`4.0.0
|
|
13
|
+
Use the SDK's matching Effect version (`4.0.0`) in host code.
|
|
14
14
|
Requires Node.js 22.19+ (the pi engine's minimum). `@yolk-sdk/codemode/node` is server-only.
|
|
15
15
|
|
|
16
16
|
## Subpaths
|
|
@@ -58,8 +58,20 @@ const program = Effect.gen(function* () {
|
|
|
58
58
|
truncated error, and usage. Nested results never reach the model; only the script output and
|
|
59
59
|
return value do.
|
|
60
60
|
- A call resolves to `structuredContent` for tools with an output schema and to the text content
|
|
61
|
-
otherwise; error results
|
|
62
|
-
|
|
61
|
+
otherwise; error results, `beforeNestedCall` failures, and calls past `maxNestedCalls` reject
|
|
62
|
+
with an `Error` whose message is `tools.<id>: <text>`, or `tools["<name>"]: <text>` for a tool
|
|
63
|
+
whose identifier an earlier tool already uses (the nested-call record keeps the raw text). Calls
|
|
64
|
+
still running when the script ends are cancelled and recorded as `cancelled`.
|
|
65
|
+
- Arguments make a JSON round trip: `undefined` keys and `null` optionals are absent, unknown keys
|
|
66
|
+
reject with a hint naming the allowed keys, and values JSON would silently change (`NaN`,
|
|
67
|
+
`Infinity`, invalid `Date`s, `undefined` array items, `Map`, `Set`, functions, class instances)
|
|
68
|
+
reject in the script before the call, with messages such as
|
|
69
|
+
`tools.search: argument at limit is NaN; pass a finite number or omit the key`. The pi executor
|
|
70
|
+
reads the argument once (getters and `toJSON` run once) into a checked JSON copy and sends that
|
|
71
|
+
copy; a valid `Date` passes as its ISO string. Arguments with more than 100,000 values or more
|
|
72
|
+
than 64 levels of nesting are rejected, never sent unchecked. Custom executors follow the same
|
|
73
|
+
rules (`CodeModeExecutorTool`) and label rejections with `CodeModeExecutorTool.callLabel`, which
|
|
74
|
+
`makeCodeModeTool` sets.
|
|
63
75
|
- The tool description lists the globals and the nested tools by namespace, with the module's
|
|
64
76
|
`ToolModule.description` under each heading. `codemode` + `listed` tools get TypeScript
|
|
65
77
|
declarations within `inlineBudget` (default 3,000 estimated tokens, filled fairly across
|
|
@@ -80,9 +92,9 @@ const program = Effect.gen(function* () {
|
|
|
80
92
|
keeps the 256 KiB-per-value and 1 MiB-total bounds by dropping offending writes. Transcript
|
|
81
93
|
`ToolResultMessage`s carry no tool name: pair each with its assistant tool call's name.
|
|
82
94
|
- `beforeNestedCall({ call, context })` runs before each nested call; a failure rejects that call in
|
|
83
|
-
the script with
|
|
84
|
-
- A nested call that is interrupted rejects with
|
|
85
|
-
unexpectedly
|
|
95
|
+
the script with `<label>: <message>` and records it as `error` without executing it.
|
|
96
|
+
- A nested call that is interrupted rejects with `<label> was cancelled.`; a defect rejects with
|
|
97
|
+
`<label> failed unexpectedly.` (the same label as above). If an executor misses its deadline, the tool returns a `timeout` failure
|
|
86
98
|
`timeoutMs` + 5 s after the start and aborts it.
|
|
87
99
|
|
|
88
100
|
## Limits
|
package/dist/catalog.d.mts
CHANGED
|
@@ -9,7 +9,13 @@ type CodeModeToolExposure = 'all' | 'listed' | 'search';
|
|
|
9
9
|
/** One nested tool as scripts see it. */
|
|
10
10
|
type CodeModeCatalogTool = {
|
|
11
11
|
/** Raw tool name; scripts may call `tools["<name>"](args)`. */readonly name: string; /** Identifier for `tools.<identifier>(args)` (invalid identifier characters become `_`). */
|
|
12
|
-
readonly identifier: string;
|
|
12
|
+
readonly identifier: string;
|
|
13
|
+
/**
|
|
14
|
+
* How scripts call this tool, used to prefix its rejections: `tools.<identifier>`, or
|
|
15
|
+
* `tools["<name>"]` when an earlier tool already has the identifier (see `codeModeCallLabels`;
|
|
16
|
+
* when the name is taken too, the tool is unreachable and the label only names it).
|
|
17
|
+
*/
|
|
18
|
+
readonly callLabel: string; /** The tool's `ToolModule.id`. */
|
|
13
19
|
readonly namespace: string; /** The module's `ToolModule.description`, when set. */
|
|
14
20
|
readonly namespaceDescription?: string;
|
|
15
21
|
readonly description: string;
|
|
@@ -18,6 +24,14 @@ type CodeModeCatalogTool = {
|
|
|
18
24
|
readonly structured: boolean;
|
|
19
25
|
readonly exposure: CodeModeToolExposure;
|
|
20
26
|
};
|
|
27
|
+
/**
|
|
28
|
+
* The call label of each tool name, in order, as pi binds `tools`: each tool takes
|
|
29
|
+
* `tools[<identifier>]` unless an earlier tool holds that key (first tool wins), then
|
|
30
|
+
* `tools[<name>]` if still free. The label is `tools.<identifier>` for the tool that holds its
|
|
31
|
+
* identifier, otherwise `tools["<name>"]`. The executor guard and the host rejection prefix share
|
|
32
|
+
* it, so both name a call the same way.
|
|
33
|
+
*/
|
|
34
|
+
declare const codeModeCallLabels: (names: ReadonlyArray<string>) => ReadonlyArray<string>;
|
|
21
35
|
/** Catalog of nested tools in resolution order. */
|
|
22
36
|
declare const codeModeCatalog: (tools: ReadonlyArray<NestedTool>) => ReadonlyArray<CodeModeCatalogTool>;
|
|
23
37
|
/** Default inline budget of the nested tool listing, in estimated tokens. */
|
|
@@ -56,5 +70,5 @@ declare const describeCodeModeTool: (tool: CodeModeCatalogTool) => string;
|
|
|
56
70
|
/** Finds a tool by identifier or raw name. */
|
|
57
71
|
declare const findCodeModeTool: (catalog: ReadonlyArray<CodeModeCatalogTool>, name: string) => CodeModeCatalogTool | undefined;
|
|
58
72
|
//#endregion
|
|
59
|
-
export { CodeModeCatalogTool, CodeModeDescriptionInput, CodeModeListing, CodeModeToolExposure, codeModeCatalog, defaultCodeModeInlineBudget, describeCodeModeTool, estimateCodeModeTokens, findCodeModeTool, renderCodeModeDescription, selectCodeModeListing };
|
|
73
|
+
export { CodeModeCatalogTool, CodeModeDescriptionInput, CodeModeListing, CodeModeToolExposure, codeModeCallLabels, codeModeCatalog, defaultCodeModeInlineBudget, describeCodeModeTool, estimateCodeModeTokens, findCodeModeTool, renderCodeModeDescription, selectCodeModeListing };
|
|
60
74
|
//# sourceMappingURL=catalog.d.mts.map
|
package/dist/catalog.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"catalog.d.mts","names":[],"sources":["../src/catalog.ts"],"mappings":";;;;;;AAcA;KAAY,oBAAA;;KAGA,mBAAA;EAHoB,wEAKrB,IAAA,UAFoB;EAAA,SAIpB,UAAA
|
|
1
|
+
{"version":3,"file":"catalog.d.mts","names":[],"sources":["../src/catalog.ts"],"mappings":";;;;;;AAcA;KAAY,oBAAA;;KAGA,mBAAA;EAHoB,wEAKrB,IAAA,UAFoB;EAAA,SAIpB,UAAA;EAYa;;;;;EAAA,SANb,SAAA,UANA;EAAA,SAQA,SAAA;EAAA,SAEA,oBAAA;EAAA,SACA,WAAA;EAAA,SACA,WAAA,EAAa,kBAAA,EAAA;EAAA,SAEb,YAAA,EAAc,kBAAA,EAAA;EAAA,SAEd,UAAA;EAAA,SACA,QAAA,EAAU,oBAAA;AAAA;;AAAoB;AAYzC;;;;;cAAa,kBAAA,GAAsB,KAAA,EAAO,aAAA,aAAwB,aAAa;;cAelE,eAAA,GACX,KAAA,EAAO,aAAA,CAAc,UAAA,MACpB,aAAA,CAAc,mBAAA;AAjB8D;AAAA,cAwClE,2BAAA;;cAGA,sBAAA,GAA0B,IAAY;AAAA,KA4DvC,eAAA;EAvFH;;;EAAA,SA2FE,MAAA,EAAQ,aAAA,CAAc,mBAAA,GA1FjB;EAAA,SA4FL,kBAAA,EAAoB,aAAA;AAAA;;;;;AA5FK;AAuBpC;cA8Ea,qBAAA,GACX,OAAA,EAAS,aAAA,CAAc,mBAAA,GACvB,MAAA,aACC,eAAA;AAAA,KAuFS,wBAAA;EAAA,SACD,KAAA,EAAO,aAAa,CAAC,UAAA;EAAA,SACrB,YAAA,WAvKE;EAAA,SAyKF,KAAA;AAAA;;AAzKwC;AA4DnD;;;;;cAuHa,yBAAA,GAA6B,KAA+B,EAAxB,wBAAwB;;cAwB5D,oBAAA,GAAwB,IAAyB,EAAnB,mBAAmB;;cAIjD,gBAAA,GACX,OAAA,EAAS,aAAA,CAAc,mBAAA,GACvB,IAAA,aACC,mBAAA"}
|
package/dist/catalog.mjs
CHANGED
|
@@ -3,23 +3,44 @@ import { renderDeclarations, renderToolOutputType, renderToolSample, toCodemodeI
|
|
|
3
3
|
import { toolDiscovery } from "@yolk-sdk/agent/protocol";
|
|
4
4
|
//#region src/catalog.ts
|
|
5
5
|
const textOutputSchema = { type: "string" };
|
|
6
|
+
/**
|
|
7
|
+
* The call label of each tool name, in order, as pi binds `tools`: each tool takes
|
|
8
|
+
* `tools[<identifier>]` unless an earlier tool holds that key (first tool wins), then
|
|
9
|
+
* `tools[<name>]` if still free. The label is `tools.<identifier>` for the tool that holds its
|
|
10
|
+
* identifier, otherwise `tools["<name>"]`. The executor guard and the host rejection prefix share
|
|
11
|
+
* it, so both name a call the same way.
|
|
12
|
+
*/
|
|
13
|
+
const codeModeCallLabels = (names) => {
|
|
14
|
+
const bound = /* @__PURE__ */ new Set();
|
|
15
|
+
return names.map((name) => {
|
|
16
|
+
const identifier = toCodemodeIdentifier(name);
|
|
17
|
+
const ownsIdentifier = !bound.has(identifier);
|
|
18
|
+
bound.add(identifier);
|
|
19
|
+
bound.add(name);
|
|
20
|
+
return ownsIdentifier ? `tools.${identifier}` : `tools[${JSON.stringify(name)}]`;
|
|
21
|
+
});
|
|
22
|
+
};
|
|
6
23
|
/** Catalog of nested tools in resolution order. */
|
|
7
|
-
const codeModeCatalog = (tools) =>
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
const codeModeCatalog = (tools) => {
|
|
25
|
+
const labels = codeModeCallLabels(tools.map(({ def }) => def.name));
|
|
26
|
+
return tools.map(({ def, moduleId, moduleDescription }, index) => {
|
|
27
|
+
const tool = {
|
|
28
|
+
name: def.name,
|
|
29
|
+
identifier: toCodemodeIdentifier(def.name),
|
|
30
|
+
callLabel: labels[index] ?? `tools[${JSON.stringify(def.name)}]`,
|
|
31
|
+
namespace: moduleId,
|
|
32
|
+
description: def.description,
|
|
33
|
+
inputSchema: def.parameters,
|
|
34
|
+
outputSchema: def.outputSchema ?? textOutputSchema,
|
|
35
|
+
structured: def.outputSchema !== void 0,
|
|
36
|
+
exposure: toolDiscovery(def) ?? "all"
|
|
37
|
+
};
|
|
38
|
+
return moduleDescription === void 0 ? tool : {
|
|
39
|
+
...tool,
|
|
40
|
+
namespaceDescription: moduleDescription
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
};
|
|
23
44
|
/** Default inline budget of the nested tool listing, in estimated tokens. */
|
|
24
45
|
const defaultCodeModeInlineBudget = 3e3;
|
|
25
46
|
/** Estimated tokens of a text: four characters per token, rounded up. */
|
|
@@ -88,7 +109,7 @@ const selectCodeModeListing = (catalog, budget) => {
|
|
|
88
109
|
const intro = [
|
|
89
110
|
"Run a JavaScript script that calls tools and returns only what matters.",
|
|
90
111
|
"`code` is the body of an async function: top-level `await` and `return` work. Write plain JavaScript; simple TypeScript annotations are stripped. There is no Node.js, network, filesystem, `require`, `fetch`, or timers.",
|
|
91
|
-
"Call tools as `await tools.<id>(args)`. A call resolves to the declared result type and rejects with an Error when the tool fails. Calls still running when the script ends are cancelled, so await every call (use `Promise.all` for parallel calls).",
|
|
112
|
+
"Call tools as `await tools.<id>(args)`. Arguments must be plain JSON (no `NaN`, `undefined` array items, `Map`, or functions). A call resolves to the declared result type and rejects with an Error whose message starts with `tools.<id>:` when the tool fails. Calls still running when the script ends are cancelled, so await every call (use `Promise.all` for parallel calls).",
|
|
92
113
|
"Only the script output and its return value come back to you: filter and aggregate inside the script and return a small JSON value."
|
|
93
114
|
].join("\n");
|
|
94
115
|
const globalLines = (store) => [
|
|
@@ -138,6 +159,6 @@ const describeCodeModeTool = (tool) => renderToolSample(declarationTool(tool));
|
|
|
138
159
|
/** Finds a tool by identifier or raw name. */
|
|
139
160
|
const findCodeModeTool = (catalog, name) => catalog.find((tool) => tool.identifier === name) ?? catalog.find((tool) => tool.name === name);
|
|
140
161
|
//#endregion
|
|
141
|
-
export { codeModeCatalog, defaultCodeModeInlineBudget, describeCodeModeTool, estimateCodeModeTokens, findCodeModeTool, renderCodeModeDescription, selectCodeModeListing };
|
|
162
|
+
export { codeModeCallLabels, codeModeCatalog, defaultCodeModeInlineBudget, describeCodeModeTool, estimateCodeModeTokens, findCodeModeTool, renderCodeModeDescription, selectCodeModeListing };
|
|
142
163
|
|
|
143
164
|
//# sourceMappingURL=catalog.mjs.map
|
package/dist/catalog.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"catalog.mjs","names":[],"sources":["../src/catalog.ts"],"sourcesContent":["import { Predicate } from 'effect'\nimport {\n renderDeclarations,\n renderToolOutputType,\n renderToolSample,\n toCodemodeIdentifier\n} from '@earendil-works/pi-codemode/declarations'\nimport { toolDiscovery } from '@yolk-sdk/agent/protocol'\nimport type { NestedTool } from '@yolk-sdk/agent/tools'\nimport type { CodeModeJsonSchema } from './executor.ts'\n\n/** How a nested tool reaches scripts: `all` tools are also direct model tools; `listed` and\n * `search` are the two discovery modes of `codemode`-only tools.\n */\nexport type CodeModeToolExposure = 'all' | 'listed' | 'search'\n\n/** One nested tool as scripts see it. */\nexport type CodeModeCatalogTool = {\n /** Raw tool name; scripts may call `tools[\"<name>\"](args)`. */\n readonly name: string\n /** Identifier for `tools.<identifier>(args)` (invalid identifier characters become `_`). */\n readonly identifier: string\n /** The tool's `ToolModule.id`. */\n readonly namespace: string\n /** The module's `ToolModule.description`, when set. */\n readonly namespaceDescription?: string\n readonly description: string\n readonly inputSchema: CodeModeJsonSchema\n /** The declared output schema, or `{ type: 'string' }` for text results. */\n readonly outputSchema: CodeModeJsonSchema\n /** True when the tool declares an output schema (calls resolve to `structuredContent`). */\n readonly structured: boolean\n readonly exposure: CodeModeToolExposure\n}\n\nconst textOutputSchema: CodeModeJsonSchema = { type: 'string' }\n\n/** Catalog of nested tools in resolution order. */\nexport const codeModeCatalog = (\n tools: ReadonlyArray<NestedTool>\n): ReadonlyArray<CodeModeCatalogTool> =>\n tools.map(({ def, moduleId, moduleDescription }) => {\n const tool: CodeModeCatalogTool = {\n name: def.name,\n identifier: toCodemodeIdentifier(def.name),\n namespace: moduleId,\n description: def.description,\n inputSchema: def.parameters,\n outputSchema: def.outputSchema ?? textOutputSchema,\n structured: def.outputSchema !== undefined,\n exposure: toolDiscovery(def) ?? 'all'\n }\n\n return moduleDescription === undefined\n ? tool\n : { ...tool, namespaceDescription: moduleDescription }\n })\n\n/** Default inline budget of the nested tool listing, in estimated tokens. */\nexport const defaultCodeModeInlineBudget = 3000\n\n/** Estimated tokens of a text: four characters per token, rounded up. */\nexport const estimateCodeModeTokens = (text: string): number => Math.ceil(text.length / 4)\n\nconst maxShortTypeChars = 60\n\nconst shortType = (schema: CodeModeJsonSchema): string => {\n const rendered = renderToolOutputType(schema)\n\n if (rendered.length <= maxShortTypeChars) return rendered\n\n if (!Predicate.isBoolean(schema)) {\n if (schema.type === 'array') return 'Array<unknown>'\n\n if (schema.type === 'object') return 'object'\n }\n\n return 'unknown'\n}\n\n// Declarations only read names, descriptions, and schemas; `execute` is never called.\nconst declarationTool = (tool: CodeModeCatalogTool) => ({\n name: tool.name,\n description: tool.description,\n inputSchema: tool.inputSchema,\n outputSchema: tool.outputSchema,\n execute: () => undefined\n})\n\nconst directToolLine = (tool: CodeModeCatalogTool) =>\n `- \\`tools.${tool.identifier}(args)\\` takes the arguments of the \\`${tool.name}\\` tool and resolves to \\`${shortType(tool.outputSchema)}\\`.`\n\nconst declarationMembers = (tools: ReadonlyArray<CodeModeCatalogTool>) =>\n renderDeclarations({ tools: tools.map(declarationTool) })\n\ntype Candidate = {\n readonly tool: CodeModeCatalogTool\n readonly index: number\n readonly cost: number\n}\n\nconst candidateCost = (tool: CodeModeCatalogTool) =>\n estimateCodeModeTokens(declarationMembers([tool]))\n\nconst groupByNamespace = <A extends { readonly tool: CodeModeCatalogTool }>(\n items: ReadonlyArray<A>\n): ReadonlyMap<string, ReadonlyArray<A>> => {\n const groups = new Map<string, Array<A>>()\n\n for (const item of items) {\n const group = groups.get(item.tool.namespace)\n\n if (group === undefined) {\n groups.set(item.tool.namespace, [item])\n } else {\n group.push(item)\n }\n }\n\n return groups\n}\n\nexport type CodeModeListing = {\n /** Tools placed in the description, in catalog order: every `all` tool, and the `listed` tools\n * that fit the budget.\n */\n readonly listed: ReadonlyArray<CodeModeCatalogTool>\n /** Namespaces (catalog order) with callable tools the description does not list. */\n readonly unlistedNamespaces: ReadonlyArray<string>\n}\n\n/**\n * Chooses the tools the description lists. `all` tools are always placed, outside the budget\n * (the model already has their declarations). `listed` tools fill `budget` estimated tokens fairly\n * across namespaces: each round, every namespace still in play places its cheapest remaining tool;\n * a namespace whose next tool does not fit drops out. `search` tools are never candidates.\n */\nexport const selectCodeModeListing = (\n catalog: ReadonlyArray<CodeModeCatalogTool>,\n budget: number\n): CodeModeListing => {\n const candidates = catalog.flatMap((tool, index): ReadonlyArray<Candidate> =>\n tool.exposure === 'listed' ? [{ tool, index, cost: candidateCost(tool) }] : []\n )\n\n const queues = new Map(\n [...groupByNamespace(candidates)].map(([namespace, group]) => [\n namespace,\n [...group].sort((left, right) => left.cost - right.cost || left.index - right.index)\n ])\n )\n\n const placed = new Set(catalog.flatMap((tool, index) => (tool.exposure === 'all' ? [index] : [])))\n\n const inPlay = new Set(queues.keys())\n let remaining = budget\n\n while (inPlay.size > 0) {\n for (const [namespace, queue] of queues) {\n if (!inPlay.has(namespace)) continue\n\n const next = queue.shift()\n\n if (next === undefined || next.cost > remaining) {\n inPlay.delete(namespace)\n continue\n }\n\n placed.add(next.index)\n remaining -= next.cost\n }\n }\n\n const unlisted = new Set(\n catalog.flatMap((tool, index) => (placed.has(index) ? [] : [tool.namespace]))\n )\n\n return {\n listed: catalog.filter((_, index) => placed.has(index)),\n unlistedNamespaces: [...new Set(catalog.map(tool => tool.namespace))].filter(namespace =>\n unlisted.has(namespace)\n )\n }\n}\n\nconst intro = [\n 'Run a JavaScript script that calls tools and returns only what matters.',\n '`code` is the body of an async function: top-level `await` and `return` work. Write plain JavaScript; simple TypeScript annotations are stripped. There is no Node.js, network, filesystem, `require`, `fetch`, or timers.',\n 'Call tools as `await tools.<id>(args)`. A call resolves to the declared result type and rejects with an Error when the tool fails. Calls still running when the script ends are cancelled, so await every call (use `Promise.all` for parallel calls).',\n 'Only the script output and its return value come back to you: filter and aggregate inside the script and return a small JSON value.'\n].join('\\n')\n\nconst globalLines = (store: boolean) => [\n '- `text(value)` and `console.log(...values)`: append text to the output.',\n '- `image(dataUrl)`: append a base64 image (a `data:` URL or `{ type: \"image\", data, mimeType }`).',\n '- `exit()`: end the script successfully.',\n '- `ALL_TOOLS`: `{ name, description }` of every callable tool.',\n '- `await searchTools(query, { limit?, namespace? })`: find tools by topic; resolves to `Array<{ name: string; description: string }>` (default limit 8).',\n '- `await describeTool(name)`: the description and TypeScript declaration of a tool, or `undefined`.',\n '- `await describeNamespace(name)`: `{ name, description?, tools: Array<{ name, description }> }` for a namespace, or `undefined`.',\n ...(store\n ? [\n '- `store(key, value)` and `load(key)`: keep small JSON values for later scripts; writes are saved only when the script succeeds.'\n ]\n : [])\n]\n\nconst namespaceSection = (namespace: string, tools: ReadonlyArray<CodeModeCatalogTool>) => {\n const declared = tools.filter(tool => tool.exposure !== 'all')\n const direct = tools.filter(tool => tool.exposure === 'all')\n\n const description = tools.find(\n tool => tool.namespaceDescription !== undefined\n )?.namespaceDescription\n\n return [\n `### ${namespace}`,\n ...(description === undefined ? [] : [description]),\n ...(declared.length > 0 ? ['```ts', declarationMembers(declared), '```'] : []),\n ...direct.map(directToolLine)\n ].join('\\n')\n}\n\n// Fixed text: never derived from `search` tools or from which tools did not fit the budget.\nconst unlistedToolsLine =\n 'More tools may be callable than are listed here: find them with `await searchTools(query, { namespace? })` and read one with `await describeTool(name)` or `await describeNamespace(name)` before calling it.'\n\nexport type CodeModeDescriptionInput = {\n readonly tools: ReadonlyArray<NestedTool>\n readonly inlineBudget?: number\n /** Mention `store()`/`load()` persistence. */\n readonly store?: boolean\n}\n\n/**\n * The code mode tool description: intro, globals one per line, then nested tools grouped by\n * namespace, then one fixed line pointing to `searchTools`/`describeTool`/`describeNamespace`.\n * `all` tools get one line each outside the budget; `listed` tools are declared within the inline\n * budget; `search` tools never contribute, so adding or removing them (even whole namespaces of\n * them) leaves the text byte-identical.\n */\nexport const renderCodeModeDescription = (input: CodeModeDescriptionInput): string => {\n const catalog = codeModeCatalog(input.tools)\n\n const listing = selectCodeModeListing(catalog, input.inlineBudget ?? defaultCodeModeInlineBudget)\n\n const sections = [...groupByNamespace(listing.listed.map(tool => ({ tool })))].map(\n ([namespace, items]) =>\n namespaceSection(\n namespace,\n items.map(item => item.tool)\n )\n )\n\n return [\n intro,\n ['Globals:', ...globalLines(input.store === true)].join('\\n'),\n sections.length === 0\n ? 'Nested tools: none are listed here.'\n : ['## Nested tools by namespace', ...sections].join('\\n\\n'),\n unlistedToolsLine\n ].join('\\n\\n')\n}\n\n/** `describeTool` text: the description and TypeScript declaration of one tool. */\nexport const describeCodeModeTool = (tool: CodeModeCatalogTool): string =>\n renderToolSample(declarationTool(tool))\n\n/** Finds a tool by identifier or raw name. */\nexport const findCodeModeTool = (\n catalog: ReadonlyArray<CodeModeCatalogTool>,\n name: string\n): CodeModeCatalogTool | undefined =>\n catalog.find(tool => tool.identifier === name) ?? catalog.find(tool => tool.name === name)\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"catalog.mjs","names":[],"sources":["../src/catalog.ts"],"sourcesContent":["import { Predicate } from 'effect'\nimport {\n renderDeclarations,\n renderToolOutputType,\n renderToolSample,\n toCodemodeIdentifier\n} from '@earendil-works/pi-codemode/declarations'\nimport { toolDiscovery } from '@yolk-sdk/agent/protocol'\nimport type { NestedTool } from '@yolk-sdk/agent/tools'\nimport type { CodeModeJsonSchema } from './executor.ts'\n\n/** How a nested tool reaches scripts: `all` tools are also direct model tools; `listed` and\n * `search` are the two discovery modes of `codemode`-only tools.\n */\nexport type CodeModeToolExposure = 'all' | 'listed' | 'search'\n\n/** One nested tool as scripts see it. */\nexport type CodeModeCatalogTool = {\n /** Raw tool name; scripts may call `tools[\"<name>\"](args)`. */\n readonly name: string\n /** Identifier for `tools.<identifier>(args)` (invalid identifier characters become `_`). */\n readonly identifier: string\n /**\n * How scripts call this tool, used to prefix its rejections: `tools.<identifier>`, or\n * `tools[\"<name>\"]` when an earlier tool already has the identifier (see `codeModeCallLabels`;\n * when the name is taken too, the tool is unreachable and the label only names it).\n */\n readonly callLabel: string\n /** The tool's `ToolModule.id`. */\n readonly namespace: string\n /** The module's `ToolModule.description`, when set. */\n readonly namespaceDescription?: string\n readonly description: string\n readonly inputSchema: CodeModeJsonSchema\n /** The declared output schema, or `{ type: 'string' }` for text results. */\n readonly outputSchema: CodeModeJsonSchema\n /** True when the tool declares an output schema (calls resolve to `structuredContent`). */\n readonly structured: boolean\n readonly exposure: CodeModeToolExposure\n}\n\nconst textOutputSchema: CodeModeJsonSchema = { type: 'string' }\n\n/**\n * The call label of each tool name, in order, as pi binds `tools`: each tool takes\n * `tools[<identifier>]` unless an earlier tool holds that key (first tool wins), then\n * `tools[<name>]` if still free. The label is `tools.<identifier>` for the tool that holds its\n * identifier, otherwise `tools[\"<name>\"]`. The executor guard and the host rejection prefix share\n * it, so both name a call the same way.\n */\nexport const codeModeCallLabels = (names: ReadonlyArray<string>): ReadonlyArray<string> => {\n const bound = new Set<string>()\n\n return names.map(name => {\n const identifier = toCodemodeIdentifier(name)\n const ownsIdentifier = !bound.has(identifier)\n\n bound.add(identifier)\n bound.add(name)\n\n return ownsIdentifier ? `tools.${identifier}` : `tools[${JSON.stringify(name)}]`\n })\n}\n\n/** Catalog of nested tools in resolution order. */\nexport const codeModeCatalog = (\n tools: ReadonlyArray<NestedTool>\n): ReadonlyArray<CodeModeCatalogTool> => {\n const labels = codeModeCallLabels(tools.map(({ def }) => def.name))\n\n return tools.map(({ def, moduleId, moduleDescription }, index) => {\n const tool: CodeModeCatalogTool = {\n name: def.name,\n identifier: toCodemodeIdentifier(def.name),\n callLabel: labels[index] ?? `tools[${JSON.stringify(def.name)}]`,\n namespace: moduleId,\n description: def.description,\n inputSchema: def.parameters,\n outputSchema: def.outputSchema ?? textOutputSchema,\n structured: def.outputSchema !== undefined,\n exposure: toolDiscovery(def) ?? 'all'\n }\n\n return moduleDescription === undefined\n ? tool\n : { ...tool, namespaceDescription: moduleDescription }\n })\n}\n\n/** Default inline budget of the nested tool listing, in estimated tokens. */\nexport const defaultCodeModeInlineBudget = 3000\n\n/** Estimated tokens of a text: four characters per token, rounded up. */\nexport const estimateCodeModeTokens = (text: string): number => Math.ceil(text.length / 4)\n\nconst maxShortTypeChars = 60\n\nconst shortType = (schema: CodeModeJsonSchema): string => {\n const rendered = renderToolOutputType(schema)\n\n if (rendered.length <= maxShortTypeChars) return rendered\n\n if (!Predicate.isBoolean(schema)) {\n if (schema.type === 'array') return 'Array<unknown>'\n\n if (schema.type === 'object') return 'object'\n }\n\n return 'unknown'\n}\n\n// Declarations only read names, descriptions, and schemas; `execute` is never called.\nconst declarationTool = (tool: CodeModeCatalogTool) => ({\n name: tool.name,\n description: tool.description,\n inputSchema: tool.inputSchema,\n outputSchema: tool.outputSchema,\n execute: () => undefined\n})\n\nconst directToolLine = (tool: CodeModeCatalogTool) =>\n `- \\`tools.${tool.identifier}(args)\\` takes the arguments of the \\`${tool.name}\\` tool and resolves to \\`${shortType(tool.outputSchema)}\\`.`\n\nconst declarationMembers = (tools: ReadonlyArray<CodeModeCatalogTool>) =>\n renderDeclarations({ tools: tools.map(declarationTool) })\n\ntype Candidate = {\n readonly tool: CodeModeCatalogTool\n readonly index: number\n readonly cost: number\n}\n\nconst candidateCost = (tool: CodeModeCatalogTool) =>\n estimateCodeModeTokens(declarationMembers([tool]))\n\nconst groupByNamespace = <A extends { readonly tool: CodeModeCatalogTool }>(\n items: ReadonlyArray<A>\n): ReadonlyMap<string, ReadonlyArray<A>> => {\n const groups = new Map<string, Array<A>>()\n\n for (const item of items) {\n const group = groups.get(item.tool.namespace)\n\n if (group === undefined) {\n groups.set(item.tool.namespace, [item])\n } else {\n group.push(item)\n }\n }\n\n return groups\n}\n\nexport type CodeModeListing = {\n /** Tools placed in the description, in catalog order: every `all` tool, and the `listed` tools\n * that fit the budget.\n */\n readonly listed: ReadonlyArray<CodeModeCatalogTool>\n /** Namespaces (catalog order) with callable tools the description does not list. */\n readonly unlistedNamespaces: ReadonlyArray<string>\n}\n\n/**\n * Chooses the tools the description lists. `all` tools are always placed, outside the budget\n * (the model already has their declarations). `listed` tools fill `budget` estimated tokens fairly\n * across namespaces: each round, every namespace still in play places its cheapest remaining tool;\n * a namespace whose next tool does not fit drops out. `search` tools are never candidates.\n */\nexport const selectCodeModeListing = (\n catalog: ReadonlyArray<CodeModeCatalogTool>,\n budget: number\n): CodeModeListing => {\n const candidates = catalog.flatMap((tool, index): ReadonlyArray<Candidate> =>\n tool.exposure === 'listed' ? [{ tool, index, cost: candidateCost(tool) }] : []\n )\n\n const queues = new Map(\n [...groupByNamespace(candidates)].map(([namespace, group]) => [\n namespace,\n [...group].sort((left, right) => left.cost - right.cost || left.index - right.index)\n ])\n )\n\n const placed = new Set(catalog.flatMap((tool, index) => (tool.exposure === 'all' ? [index] : [])))\n\n const inPlay = new Set(queues.keys())\n let remaining = budget\n\n while (inPlay.size > 0) {\n for (const [namespace, queue] of queues) {\n if (!inPlay.has(namespace)) continue\n\n const next = queue.shift()\n\n if (next === undefined || next.cost > remaining) {\n inPlay.delete(namespace)\n continue\n }\n\n placed.add(next.index)\n remaining -= next.cost\n }\n }\n\n const unlisted = new Set(\n catalog.flatMap((tool, index) => (placed.has(index) ? [] : [tool.namespace]))\n )\n\n return {\n listed: catalog.filter((_, index) => placed.has(index)),\n unlistedNamespaces: [...new Set(catalog.map(tool => tool.namespace))].filter(namespace =>\n unlisted.has(namespace)\n )\n }\n}\n\nconst intro = [\n 'Run a JavaScript script that calls tools and returns only what matters.',\n '`code` is the body of an async function: top-level `await` and `return` work. Write plain JavaScript; simple TypeScript annotations are stripped. There is no Node.js, network, filesystem, `require`, `fetch`, or timers.',\n 'Call tools as `await tools.<id>(args)`. Arguments must be plain JSON (no `NaN`, `undefined` array items, `Map`, or functions). A call resolves to the declared result type and rejects with an Error whose message starts with `tools.<id>:` when the tool fails. Calls still running when the script ends are cancelled, so await every call (use `Promise.all` for parallel calls).',\n 'Only the script output and its return value come back to you: filter and aggregate inside the script and return a small JSON value.'\n].join('\\n')\n\nconst globalLines = (store: boolean) => [\n '- `text(value)` and `console.log(...values)`: append text to the output.',\n '- `image(dataUrl)`: append a base64 image (a `data:` URL or `{ type: \"image\", data, mimeType }`).',\n '- `exit()`: end the script successfully.',\n '- `ALL_TOOLS`: `{ name, description }` of every callable tool.',\n '- `await searchTools(query, { limit?, namespace? })`: find tools by topic; resolves to `Array<{ name: string; description: string }>` (default limit 8).',\n '- `await describeTool(name)`: the description and TypeScript declaration of a tool, or `undefined`.',\n '- `await describeNamespace(name)`: `{ name, description?, tools: Array<{ name, description }> }` for a namespace, or `undefined`.',\n ...(store\n ? [\n '- `store(key, value)` and `load(key)`: keep small JSON values for later scripts; writes are saved only when the script succeeds.'\n ]\n : [])\n]\n\nconst namespaceSection = (namespace: string, tools: ReadonlyArray<CodeModeCatalogTool>) => {\n const declared = tools.filter(tool => tool.exposure !== 'all')\n const direct = tools.filter(tool => tool.exposure === 'all')\n\n const description = tools.find(\n tool => tool.namespaceDescription !== undefined\n )?.namespaceDescription\n\n return [\n `### ${namespace}`,\n ...(description === undefined ? [] : [description]),\n ...(declared.length > 0 ? ['```ts', declarationMembers(declared), '```'] : []),\n ...direct.map(directToolLine)\n ].join('\\n')\n}\n\n// Fixed text: never derived from `search` tools or from which tools did not fit the budget.\nconst unlistedToolsLine =\n 'More tools may be callable than are listed here: find them with `await searchTools(query, { namespace? })` and read one with `await describeTool(name)` or `await describeNamespace(name)` before calling it.'\n\nexport type CodeModeDescriptionInput = {\n readonly tools: ReadonlyArray<NestedTool>\n readonly inlineBudget?: number\n /** Mention `store()`/`load()` persistence. */\n readonly store?: boolean\n}\n\n/**\n * The code mode tool description: intro, globals one per line, then nested tools grouped by\n * namespace, then one fixed line pointing to `searchTools`/`describeTool`/`describeNamespace`.\n * `all` tools get one line each outside the budget; `listed` tools are declared within the inline\n * budget; `search` tools never contribute, so adding or removing them (even whole namespaces of\n * them) leaves the text byte-identical.\n */\nexport const renderCodeModeDescription = (input: CodeModeDescriptionInput): string => {\n const catalog = codeModeCatalog(input.tools)\n\n const listing = selectCodeModeListing(catalog, input.inlineBudget ?? defaultCodeModeInlineBudget)\n\n const sections = [...groupByNamespace(listing.listed.map(tool => ({ tool })))].map(\n ([namespace, items]) =>\n namespaceSection(\n namespace,\n items.map(item => item.tool)\n )\n )\n\n return [\n intro,\n ['Globals:', ...globalLines(input.store === true)].join('\\n'),\n sections.length === 0\n ? 'Nested tools: none are listed here.'\n : ['## Nested tools by namespace', ...sections].join('\\n\\n'),\n unlistedToolsLine\n ].join('\\n\\n')\n}\n\n/** `describeTool` text: the description and TypeScript declaration of one tool. */\nexport const describeCodeModeTool = (tool: CodeModeCatalogTool): string =>\n renderToolSample(declarationTool(tool))\n\n/** Finds a tool by identifier or raw name. */\nexport const findCodeModeTool = (\n catalog: ReadonlyArray<CodeModeCatalogTool>,\n name: string\n): CodeModeCatalogTool | undefined =>\n catalog.find(tool => tool.identifier === name) ?? catalog.find(tool => tool.name === name)\n"],"mappings":";;;;AAyCA,MAAM,mBAAuC,EAAE,MAAM,SAAS;;;;;;;;AAS9D,MAAa,sBAAsB,UAAwD;CACzF,MAAM,wBAAQ,IAAI,IAAY;CAE9B,OAAO,MAAM,KAAI,SAAQ;EACvB,MAAM,aAAa,qBAAqB,IAAI;EAC5C,MAAM,iBAAiB,CAAC,MAAM,IAAI,UAAU;EAE5C,MAAM,IAAI,UAAU;EACpB,MAAM,IAAI,IAAI;EAEd,OAAO,iBAAiB,SAAS,eAAe,SAAS,KAAK,UAAU,IAAI,EAAE;CAChF,CAAC;AACH;;AAGA,MAAa,mBACX,UACuC;CACvC,MAAM,SAAS,mBAAmB,MAAM,KAAK,EAAE,UAAU,IAAI,IAAI,CAAC;CAElE,OAAO,MAAM,KAAK,EAAE,KAAK,UAAU,qBAAqB,UAAU;EAChE,MAAM,OAA4B;GAChC,MAAM,IAAI;GACV,YAAY,qBAAqB,IAAI,IAAI;GACzC,WAAW,OAAO,UAAU,SAAS,KAAK,UAAU,IAAI,IAAI,EAAE;GAC9D,WAAW;GACX,aAAa,IAAI;GACjB,aAAa,IAAI;GACjB,cAAc,IAAI,gBAAgB;GAClC,YAAY,IAAI,iBAAiB,KAAA;GACjC,UAAU,cAAc,GAAG,KAAK;EAClC;EAEA,OAAO,sBAAsB,KAAA,IACzB,OACA;GAAE,GAAG;GAAM,sBAAsB;EAAkB;CACzD,CAAC;AACH;;AAGA,MAAa,8BAA8B;;AAG3C,MAAa,0BAA0B,SAAyB,KAAK,KAAK,KAAK,SAAS,CAAC;AAEzF,MAAM,oBAAoB;AAE1B,MAAM,aAAa,WAAuC;CACxD,MAAM,WAAW,qBAAqB,MAAM;CAE5C,IAAI,SAAS,UAAU,mBAAmB,OAAO;CAEjD,IAAI,CAAC,UAAU,UAAU,MAAM,GAAG;EAChC,IAAI,OAAO,SAAS,SAAS,OAAO;EAEpC,IAAI,OAAO,SAAS,UAAU,OAAO;CACvC;CAEA,OAAO;AACT;AAGA,MAAM,mBAAmB,UAA+B;CACtD,MAAM,KAAK;CACX,aAAa,KAAK;CAClB,aAAa,KAAK;CAClB,cAAc,KAAK;CACnB,eAAe,KAAA;AACjB;AAEA,MAAM,kBAAkB,SACtB,aAAa,KAAK,WAAW,wCAAwC,KAAK,KAAK,4BAA4B,UAAU,KAAK,YAAY,EAAE;AAE1I,MAAM,sBAAsB,UAC1B,mBAAmB,EAAE,OAAO,MAAM,IAAI,eAAe,EAAE,CAAC;AAQ1D,MAAM,iBAAiB,SACrB,uBAAuB,mBAAmB,CAAC,IAAI,CAAC,CAAC;AAEnD,MAAM,oBACJ,UAC0C;CAC1C,MAAM,yBAAS,IAAI,IAAsB;CAEzC,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,QAAQ,OAAO,IAAI,KAAK,KAAK,SAAS;EAE5C,IAAI,UAAU,KAAA,GACZ,OAAO,IAAI,KAAK,KAAK,WAAW,CAAC,IAAI,CAAC;OAEtC,MAAM,KAAK,IAAI;CAEnB;CAEA,OAAO;AACT;;;;;;;AAiBA,MAAa,yBACX,SACA,WACoB;CACpB,MAAM,aAAa,QAAQ,SAAS,MAAM,UACxC,KAAK,aAAa,WAAW,CAAC;EAAE;EAAM;EAAO,MAAM,cAAc,IAAI;CAAE,CAAC,IAAI,CAAC,CAC/E;CAEA,MAAM,SAAS,IAAI,IACjB,CAAC,GAAG,iBAAiB,UAAU,CAAC,EAAE,KAAK,CAAC,WAAW,WAAW,CAC5D,WACA,CAAC,GAAG,KAAK,EAAE,MAAM,MAAM,UAAU,KAAK,OAAO,MAAM,QAAQ,KAAK,QAAQ,MAAM,KAAK,CACrF,CAAC,CACH;CAEA,MAAM,SAAS,IAAI,IAAI,QAAQ,SAAS,MAAM,UAAW,KAAK,aAAa,QAAQ,CAAC,KAAK,IAAI,CAAC,CAAE,CAAC;CAEjG,MAAM,SAAS,IAAI,IAAI,OAAO,KAAK,CAAC;CACpC,IAAI,YAAY;CAEhB,OAAO,OAAO,OAAO,GACnB,KAAK,MAAM,CAAC,WAAW,UAAU,QAAQ;EACvC,IAAI,CAAC,OAAO,IAAI,SAAS,GAAG;EAE5B,MAAM,OAAO,MAAM,MAAM;EAEzB,IAAI,SAAS,KAAA,KAAa,KAAK,OAAO,WAAW;GAC/C,OAAO,OAAO,SAAS;GACvB;EACF;EAEA,OAAO,IAAI,KAAK,KAAK;EACrB,aAAa,KAAK;CACpB;CAGF,MAAM,WAAW,IAAI,IACnB,QAAQ,SAAS,MAAM,UAAW,OAAO,IAAI,KAAK,IAAI,CAAC,IAAI,CAAC,KAAK,SAAS,CAAE,CAC9E;CAEA,OAAO;EACL,QAAQ,QAAQ,QAAQ,GAAG,UAAU,OAAO,IAAI,KAAK,CAAC;EACtD,oBAAoB,CAAC,GAAG,IAAI,IAAI,QAAQ,KAAI,SAAQ,KAAK,SAAS,CAAC,CAAC,EAAE,QAAO,cAC3E,SAAS,IAAI,SAAS,CACxB;CACF;AACF;AAEA,MAAM,QAAQ;CACZ;CACA;CACA;CACA;AACF,EAAE,KAAK,IAAI;AAEX,MAAM,eAAe,UAAmB;CACtC;CACA;CACA;CACA;CACA;CACA;CACA;CACA,GAAI,QACA,CACE,kIACF,IACA,CAAC;AACP;AAEA,MAAM,oBAAoB,WAAmB,UAA8C;CACzF,MAAM,WAAW,MAAM,QAAO,SAAQ,KAAK,aAAa,KAAK;CAC7D,MAAM,SAAS,MAAM,QAAO,SAAQ,KAAK,aAAa,KAAK;CAE3D,MAAM,cAAc,MAAM,MACxB,SAAQ,KAAK,yBAAyB,KAAA,CACxC,GAAG;CAEH,OAAO;EACL,OAAO;EACP,GAAI,gBAAgB,KAAA,IAAY,CAAC,IAAI,CAAC,WAAW;EACjD,GAAI,SAAS,SAAS,IAAI;GAAC;GAAS,mBAAmB,QAAQ;GAAG;EAAK,IAAI,CAAC;EAC5E,GAAG,OAAO,IAAI,cAAc;CAC9B,EAAE,KAAK,IAAI;AACb;AAGA,MAAM,oBACJ;;;;;;;;AAgBF,MAAa,6BAA6B,UAA4C;CAKpF,MAAM,WAAW,CAAC,GAAG,iBAFL,sBAFA,gBAAgB,MAAM,KAEM,GAAG,MAAM,gBAAA,GAET,EAAE,OAAO,KAAI,UAAS,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,KAC5E,CAAC,WAAW,WACX,iBACE,WACA,MAAM,KAAI,SAAQ,KAAK,IAAI,CAC7B,CACJ;CAEA,OAAO;EACL;EACA,CAAC,YAAY,GAAG,YAAY,MAAM,UAAU,IAAI,CAAC,EAAE,KAAK,IAAI;EAC5D,SAAS,WAAW,IAChB,wCACA,CAAC,gCAAgC,GAAG,QAAQ,EAAE,KAAK,MAAM;EAC7D;CACF,EAAE,KAAK,MAAM;AACf;;AAGA,MAAa,wBAAwB,SACnC,iBAAiB,gBAAgB,IAAI,CAAC;;AAGxC,MAAa,oBACX,SACA,SAEA,QAAQ,MAAK,SAAQ,KAAK,eAAe,IAAI,KAAK,QAAQ,MAAK,SAAQ,KAAK,SAAS,IAAI"}
|
package/dist/executor.d.mts
CHANGED
|
@@ -13,20 +13,38 @@ type CodeModeToolContext = {
|
|
|
13
13
|
* A function a script can call. Tools are called as `tools.<identifier>(args)` and
|
|
14
14
|
* `tools["<name>"](args)`; the identifier replaces characters that are not valid in a JavaScript
|
|
15
15
|
* identifier with `_`. Arguments and results make a JSON round trip; a rejection surfaces in the
|
|
16
|
-
* script as an `Error` with the same message
|
|
16
|
+
* script as an `Error` with the same message (`makeCodeModeTool` rejects with
|
|
17
|
+
* `<call label>: <message>`, the label being `tools.<identifier>` or `tools["<name>"]`).
|
|
18
|
+
*
|
|
19
|
+
* `args` is the JSON value of the script's first argument (`undefined` when there is none), read
|
|
20
|
+
* once the way `JSON.stringify` reads it (getters and `toJSON` run once); the checked value is the
|
|
21
|
+
* one sent. Executors reject, inside the script and without calling `execute`, values that JSON
|
|
22
|
+
* would silently change instead of coercing them: non-finite numbers, invalid `Date`s,
|
|
23
|
+
* `undefined`/function/symbol array items and holes, function/symbol/bigint values, cycles, and
|
|
24
|
+
* non-plain objects without `toJSON` (`Map`, `Set`, `RegExp`, `Error`, class instances), including
|
|
25
|
+
* inside `toJSON` results. `undefined`-valued object keys count as absent, as JSON drops them; a
|
|
26
|
+
* valid `Date` passes as its ISO string. An argument too large or deep to check completely is
|
|
27
|
+
* rejected, never sent unchecked (the pi executor: more than 100000 values or 64 levels). A
|
|
28
|
+
* rejection names the call the way scripts reach it, as `makeCodeModeTool` does: the first tool
|
|
29
|
+
* with an identifier is `tools.<identifier>`, a later one `tools["<name>"]`.
|
|
17
30
|
*/
|
|
18
31
|
type CodeModeExecutorTool = {
|
|
19
32
|
readonly name: string;
|
|
20
33
|
readonly description?: string;
|
|
21
34
|
readonly inputSchema?: CodeModeJsonSchema;
|
|
22
35
|
readonly outputSchema?: CodeModeJsonSchema;
|
|
36
|
+
/** Diagnostic call label for rejection messages: `tools.<identifier>`, or `tools["<name>"]`
|
|
37
|
+
* when an earlier tool holds the identifier (a tool whose name is taken too is unreachable and
|
|
38
|
+
* the label only names it). `makeCodeModeTool` sets it; executors fall back to pi's binding rule.
|
|
39
|
+
*/
|
|
40
|
+
readonly callLabel?: string;
|
|
23
41
|
readonly execute: (args: unknown, context: CodeModeToolContext) => Promise<unknown>;
|
|
24
42
|
};
|
|
25
43
|
/**
|
|
26
44
|
* A top-level function (`name`) or namespace member (`namespace.member`) for scripts. Globals are
|
|
27
45
|
* host helpers: they are not tools and are not recorded as nested calls.
|
|
28
46
|
*/
|
|
29
|
-
type CodeModeExecutorGlobal = CodeModeExecutorTool & {
|
|
47
|
+
type CodeModeExecutorGlobal = Omit<CodeModeExecutorTool, 'callLabel'> & {
|
|
30
48
|
/** `execute` receives every call argument as an array instead of the first one. */readonly spread?: boolean; /** TypeScript parameter list and return type for declarations. */
|
|
31
49
|
readonly signature?: string;
|
|
32
50
|
};
|
|
@@ -76,7 +94,8 @@ type CodeModeExecuteOptions = {
|
|
|
76
94
|
* may carry TypeScript annotations: executors strip them or report a `script` error. The promise
|
|
77
95
|
* resolves for every script outcome, including failures, timeouts, and aborts; a rejection is
|
|
78
96
|
* treated as a `sandbox` failure. Running tool calls must be cancelled through their signal when
|
|
79
|
-
* the script ends.
|
|
97
|
+
* the script ends. Tool arguments follow the JSON rules of `CodeModeExecutorTool`: pass their JSON
|
|
98
|
+
* value and reject what JSON would silently change.
|
|
80
99
|
*
|
|
81
100
|
* `@yolk-sdk/codemode/node` provides `makePiCodeModeExecutor`; hosts on other runtimes supply their
|
|
82
101
|
* own engine behind this interface.
|
package/dist/executor.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"executor.d.mts","names":[],"sources":["../src/executor.ts"],"mappings":";;;;;KAIY,kBAAA,GAAqB,cAAc;AAA/C;AAAA,KAGY,aAAA,GAAgB,QAAA,CAAS,MAAA,SAAe,MAAA,CAAO,IAAA;AAAA,KAE/C,mBAAA;EALmC,uGAOpC,MAAA,EAAQ,WAAW;AAAA
|
|
1
|
+
{"version":3,"file":"executor.d.mts","names":[],"sources":["../src/executor.ts"],"mappings":";;;;;KAIY,kBAAA,GAAqB,cAAc;AAA/C;AAAA,KAGY,aAAA,GAAgB,QAAA,CAAS,MAAA,SAAe,MAAA,CAAO,IAAA;AAAA,KAE/C,mBAAA;EALmC,uGAOpC,MAAA,EAAQ,WAAW;AAAA;;;;;;;;;;;;;AAJiC;AAE/D;;;;AAE8B;AAsB9B;KAAY,oBAAA;EAAA,SACD,IAAA;EAAA,SACA,WAAA;EAAA,SACA,WAAA,GAAc,kBAAA;EAAA,SACd,YAAA,GAAe,kBAAA;EAM2C;;;;EAAA,SAD1D,SAAA;EAAA,SACA,OAAA,GAAU,IAAA,WAAe,OAAA,EAAS,mBAAA,KAAwB,OAAA;AAAA;;;;;KAOzD,sBAAA,GAAyB,IAAI,CAAC,oBAAA;EAPG,4FASlC,MAAA,YAT0D;EAAA,SAW1D,SAAA;AAAA;AAJX;AAAA,KAQY,kBAAA;EAAA,SACG,IAAA;EAAA,SAAuB,IAAA;AAAA;EAAA,SACvB,IAAA;EAAA,SAAwB,IAAA;EAAA,SAAuB,QAAA;AAAA;AAN1C;AAIpB;;;;;AAJoB,KAcR,iBAAA;AAAA,KAEA,aAAA;EAAA,SACD,IAAA,EAAM,iBAAiB;EAAA,SACvB,OAAA;EAAA,SACA,KAAA;AAAA;AALX;AAAA,KASY,mBAAA;EAAA,SACD,GAAA,EAAK,aAAA;EAAA,SACL,MAAA,EAAQ,aAAa;AAAA;AAAA,KAGpB,uBAAA;EAAA,SACD,EAAA;WAEA,KAAA;EAAA,SACA,KAAA,GAAQ,aAAA,EAfF;EAAA,SAiBN,MAAA,EAAQ,aAAA,CAAc,kBAAA,GAftB;EAAA,SAiBA,WAAA,GAAc,mBAAA;AAAA;AAAA,KAGb,sBAAA;EAAA,SACD,KAAA,EAAO,aAAA,CAAc,oBAAA;EAAA,SACrB,OAAA,EAAS,aAAA,CAAc,sBAAA,GAhBF;EAAA,SAkBrB,SAAA,UAnBK;EAAA,SAqBL,gBAAA,UApBQ;EAAA,SAsBR,KAAA,EAAO,aAAA;EAAA,SACP,MAAA,EAAQ,WAAA;AAAA;;;;;;;;;;;;KAcP,gBAAA;EAAA,SACD,OAAA,GACP,IAAA,UACA,OAAA,EAAS,sBAAA,KACN,OAAA,CAAQ,uBAAA;AAAA"}
|
package/dist/node.d.mts
CHANGED
|
@@ -20,7 +20,8 @@ type PiCodeModeExecutorOptions = {
|
|
|
20
20
|
/**
|
|
21
21
|
* A `CodeModeExecutor` on `@earendil-works/pi-codemode`: every execution gets a fresh QuickJS VM
|
|
22
22
|
* (WebAssembly) in a fresh worker thread, closed when the execution ends. Applies the timeout,
|
|
23
|
-
* heap cap, abort signal, and store, strips TypeScript annotations first,
|
|
23
|
+
* heap cap, abort signal, and store, strips TypeScript annotations first, rejects tool arguments
|
|
24
|
+
* that JSON would silently change inside the script (see `CodeModeExecutorTool`), and caps concurrent
|
|
24
25
|
* executions per executor; create one executor per process (module scope) so the cap is
|
|
25
26
|
* per process.
|
|
26
27
|
*
|
package/dist/node.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node.d.mts","names":[],"sources":["../src/node.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"node.d.mts","names":[],"sources":["../src/node.ts"],"mappings":";;;;;cAsBa,8BAAA;AAAA,KAED,yBAAA;EAF+B;;;AAAA;EAAA,SAOhC,IAAA,GAAO,kBAAA,GAAqB,OAAA,CAAQ,kBAAA;EALV;;;;EAAA,SAU1B,SAAA,YAAqB,GAAA,EAAA;EAAA,SAErB,uBAAA;AAAA;;;;;;;;;AAAuB;AA8alC;;;cAAa,sBAAA,GACX,OAAA,GAAS,yBAAA,KACR,gBA6EF"}
|
package/dist/node.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { codeModeCallLabels } from "./catalog.mjs";
|
|
1
2
|
import * as nodeModule from "node:module";
|
|
2
3
|
import { Option, Predicate } from "effect";
|
|
3
4
|
import * as Schema from "effect/Schema";
|
|
@@ -42,6 +43,182 @@ const makeExecutionSlots = (max) => {
|
|
|
42
43
|
};
|
|
43
44
|
const wrapperPrefix = "async function __yolkCodeMode() {\n";
|
|
44
45
|
const wrapperSuffix = "\n}";
|
|
46
|
+
/**
|
|
47
|
+
* Evaluated in the VM before the script runs: `(tools, labels) => guardedTools`, with `labels` the
|
|
48
|
+
* `[name, callLabel]` pairs of the tools in order (`codeModeCallLabels`). Each call walks its first
|
|
49
|
+
* argument once, the way `JSON.stringify` reads it (each own enumerable string key and each array
|
|
50
|
+
* index read once, so getters run once; a callable `toJSON` is called once with the key, `''` at
|
|
51
|
+
* the root, and its result is walked at the same path), and builds a fresh copy (null-prototype
|
|
52
|
+
* objects and arrays) that is what the call sends. It rejects with a `TypeError` naming the call
|
|
53
|
+
* label and path when JSON would change the value instead of carrying it: non-finite numbers,
|
|
54
|
+
* invalid `Date`s, `undefined`/function/symbol array items and holes, function/symbol/bigint
|
|
55
|
+
* values, cycles, and objects that are neither plain (prototype `Object.prototype` or `null`) nor
|
|
56
|
+
* arrays and have no `toJSON`. An argument with more than `maxArgumentValues` values or nested more
|
|
57
|
+
* than `maxArgumentDepth` levels is rejected too, never sent unchecked. `undefined`-valued keys are
|
|
58
|
+
* left out (absent, as JSON drops them); the same object twice (not a cycle) is copied twice. An
|
|
59
|
+
* error thrown by a getter or `toJSON` rejects the call with that error. A rejected call never
|
|
60
|
+
* reaches the host. Every member of pi's `tools` is wrapped; unknown members fall through to pi's
|
|
61
|
+
* proxy (close-match suggestions). Not a security boundary (`globalThis.tools` is unguarded):
|
|
62
|
+
* arguments are still decoded by the host.
|
|
63
|
+
*/
|
|
64
|
+
const toolsGuardSource = `(function (tools, labels) {
|
|
65
|
+
// Captured before the script runs, so a script that changes built-ins cannot change the checks.
|
|
66
|
+
const { apply, getPrototypeOf, setPrototypeOf } = Reflect
|
|
67
|
+
const isFinite = Number.isFinite
|
|
68
|
+
const isArray = Array.isArray
|
|
69
|
+
const { keys, create, defineProperty } = Object
|
|
70
|
+
const getTime = Date.prototype.getTime
|
|
71
|
+
const TypeErrorCtor = TypeError
|
|
72
|
+
const SetCtor = Set
|
|
73
|
+
const setAdd = Set.prototype.add
|
|
74
|
+
const setHas = Set.prototype.has
|
|
75
|
+
const setDelete = Set.prototype.delete
|
|
76
|
+
const ObjectProto = Object.prototype
|
|
77
|
+
const { trunc, min } = Math
|
|
78
|
+
const stringify = JSON.stringify
|
|
79
|
+
const plainJson = 'pass plain JSON (objects, arrays, strings, finite numbers, booleans, null)'
|
|
80
|
+
const maxValues = 100000
|
|
81
|
+
const maxDepth = 64
|
|
82
|
+
class Rejected {
|
|
83
|
+
constructor(message) {
|
|
84
|
+
defineProperty(this, 'message', { value: message })
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const keyPath = (path, key) =>
|
|
88
|
+
/^[A-Za-z_$][\\w$]*$/.test(key)
|
|
89
|
+
? (path === '' ? key : path + '.' + key)
|
|
90
|
+
: path + '[' + stringify(key) + ']'
|
|
91
|
+
const typeName = value => {
|
|
92
|
+
const proto = getPrototypeOf(value)
|
|
93
|
+
const name = proto && typeof proto.constructor === 'function' ? proto.constructor.name : ''
|
|
94
|
+
return name ? (/^[AEIOU]/.test(name) ? 'an ' : 'a ') + name : 'a non-plain object'
|
|
95
|
+
}
|
|
96
|
+
// The JSON copy of a value already read from its holder at key (undefined: absent), or throws
|
|
97
|
+
// Rejected with why JSON would not carry it unchanged.
|
|
98
|
+
const snapshot = (read, key, path, slot, walk) => {
|
|
99
|
+
const at = path === '' ? 'is ' : 'at ' + path + ' is '
|
|
100
|
+
if (++walk.values > maxValues) {
|
|
101
|
+
throw new Rejected('has more than ' + maxValues + ' values; split the work across calls')
|
|
102
|
+
}
|
|
103
|
+
let value = read
|
|
104
|
+
const kind = typeof value
|
|
105
|
+
if ((kind === 'object' && value !== null) || kind === 'function' || kind === 'bigint') {
|
|
106
|
+
// The intrinsic getTime reads a Date's internal time (and throws for anything else).
|
|
107
|
+
let time
|
|
108
|
+
try {
|
|
109
|
+
time = apply(getTime, value, [])
|
|
110
|
+
} catch {
|
|
111
|
+
time = undefined
|
|
112
|
+
}
|
|
113
|
+
if (time !== undefined && !isFinite(time)) {
|
|
114
|
+
throw new Rejected(at + 'an invalid Date; pass a valid Date or an ISO string')
|
|
115
|
+
}
|
|
116
|
+
const toJSON = value.toJSON
|
|
117
|
+
if (typeof toJSON === 'function') value = apply(toJSON, value, [key])
|
|
118
|
+
}
|
|
119
|
+
switch (typeof value) {
|
|
120
|
+
case 'string':
|
|
121
|
+
case 'boolean':
|
|
122
|
+
return value
|
|
123
|
+
case 'number':
|
|
124
|
+
if (isFinite(value)) return value
|
|
125
|
+
throw new Rejected(
|
|
126
|
+
at + value + '; pass a finite number' + (slot === 'key' ? ' or omit the key' : '')
|
|
127
|
+
)
|
|
128
|
+
case 'undefined':
|
|
129
|
+
if (slot === 'item') throw new Rejected(at + 'undefined; arrays cannot hold undefined')
|
|
130
|
+
return undefined
|
|
131
|
+
case 'bigint':
|
|
132
|
+
case 'function':
|
|
133
|
+
case 'symbol':
|
|
134
|
+
throw new Rejected(at + 'a ' + typeof value + '; ' + plainJson)
|
|
135
|
+
}
|
|
136
|
+
if (value === null) return null
|
|
137
|
+
if (apply(setHas, walk.ancestors, [value])) {
|
|
138
|
+
throw new Rejected(at + 'a circular reference; ' + plainJson)
|
|
139
|
+
}
|
|
140
|
+
if (walk.depth >= maxDepth) {
|
|
141
|
+
throw new Rejected('at ' + path + ' is nested more than ' + maxDepth + ' levels deep')
|
|
142
|
+
}
|
|
143
|
+
const array = isArray(value)
|
|
144
|
+
if (!array) {
|
|
145
|
+
const proto = getPrototypeOf(value)
|
|
146
|
+
if (proto !== ObjectProto && proto !== null) {
|
|
147
|
+
throw new Rejected(at + typeName(value) + '; ' + plainJson)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
apply(setAdd, walk.ancestors, [value])
|
|
151
|
+
walk.depth++
|
|
152
|
+
let copy
|
|
153
|
+
if (array) {
|
|
154
|
+
// LengthOfArrayLike, read once, as JSON.stringify does.
|
|
155
|
+
// Unary plus is ToNumber: one coercion, and it throws for a bigint as JSON.stringify does.
|
|
156
|
+
const raw = +value.length
|
|
157
|
+
const length = raw > 0 ? min(trunc(raw), 9007199254740991) : 0
|
|
158
|
+
// No prototype: no inherited toJSON or index setter can change the checked copy when pi
|
|
159
|
+
// serializes it (still an array to JSON.stringify).
|
|
160
|
+
copy = []
|
|
161
|
+
setPrototypeOf(copy, null)
|
|
162
|
+
for (let i = 0; i < length; i++) {
|
|
163
|
+
copy[i] = snapshot(value[i], '' + i, path + '[' + i + ']', 'item', walk)
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
copy = create(null)
|
|
167
|
+
// Indexed over the fresh key array: no iterator a script could replace.
|
|
168
|
+
const names = keys(value)
|
|
169
|
+
for (let i = 0; i < names.length; i++) {
|
|
170
|
+
const name = names[i]
|
|
171
|
+
const item = snapshot(value[name], name, keyPath(path, name), 'key', walk)
|
|
172
|
+
if (item !== undefined) copy[name] = item
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
walk.depth--
|
|
176
|
+
apply(setDelete, walk.ancestors, [value])
|
|
177
|
+
return copy
|
|
178
|
+
}
|
|
179
|
+
const wrap = (label, call) => async (...args) => {
|
|
180
|
+
let json
|
|
181
|
+
try {
|
|
182
|
+
json = snapshot(args[0], '', '', 'root', { values: 0, depth: 0, ancestors: new SetCtor() })
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (error instanceof Rejected) throw new TypeErrorCtor(label + ': argument ' + error.message)
|
|
185
|
+
throw error
|
|
186
|
+
}
|
|
187
|
+
return call(json)
|
|
188
|
+
}
|
|
189
|
+
// Each tool's function sits at tools[name] unless an earlier tool holds that key, in which case
|
|
190
|
+
// that earlier function is already wrapped.
|
|
191
|
+
const wrappers = new Map()
|
|
192
|
+
for (const [name, label] of labels) {
|
|
193
|
+
const call = name in tools ? tools[name] : undefined
|
|
194
|
+
if (typeof call === 'function' && !wrappers.has(call)) wrappers.set(call, wrap(label, call))
|
|
195
|
+
}
|
|
196
|
+
const guarded = Object.create(null)
|
|
197
|
+
for (const key of Object.keys(tools)) {
|
|
198
|
+
const call = tools[key]
|
|
199
|
+
if (!wrappers.has(call)) wrappers.set(call, wrap('tools[' + JSON.stringify(key) + ']', call))
|
|
200
|
+
guarded[key] = wrappers.get(call)
|
|
201
|
+
}
|
|
202
|
+
return new Proxy(Object.freeze(guarded), {
|
|
203
|
+
get: (target, key) => (key in target ? target[key] : tools[key])
|
|
204
|
+
})
|
|
205
|
+
})`;
|
|
206
|
+
const guardPrefix = "return (async (tools, console) => {";
|
|
207
|
+
/** JSON as a JavaScript expression (U+2028/U+2029 escaped for older parsers). */
|
|
208
|
+
const jsonSource = (value) => JSON.stringify(value).replace(/[\u2028\u2029]/g, (char) => char === "\u2028" ? "\\u2028" : "\\u2029");
|
|
209
|
+
const guardSuffix = (tools) => {
|
|
210
|
+
const labels = codeModeCallLabels(tools.map((tool) => tool.name));
|
|
211
|
+
return `\n})(${toolsGuardSource}(tools, ${jsonSource(tools.map((tool, index) => [tool.name, tool.callLabel ?? labels[index] ?? `tools[${JSON.stringify(tool.name)}]`]))}), console)`;
|
|
212
|
+
};
|
|
213
|
+
const lineTerminators = /\r\n?|[\n\u2028\u2029]/g;
|
|
214
|
+
/** Lines of the script as QuickJS counts them (at least; never fewer). */
|
|
215
|
+
const scriptLineCount = (code) => (code.match(lineTerminators)?.length ?? 0) + 1;
|
|
216
|
+
const scriptFrame = /codemode\.js:(\d+):\d+/;
|
|
217
|
+
/** Drops stack frames of the wrapper and guard, which sit below the script's last line. */
|
|
218
|
+
const withoutWrapperFrames = (stack, scriptLines) => stack.split("\n").filter((line) => {
|
|
219
|
+
const frame = scriptFrame.exec(line);
|
|
220
|
+
return frame === null || Number(frame[1]) <= scriptLines;
|
|
221
|
+
}).join("\n");
|
|
45
222
|
const ErrorWithCode = Schema.Struct({ code: Schema.String });
|
|
46
223
|
const errorCode = (error) => Option.getOrUndefined(Option.map(Schema.decodeUnknownOption(ErrorWithCode)(error), (e) => e.code));
|
|
47
224
|
const syntaxLocation = (error) => {
|
|
@@ -114,7 +291,7 @@ const StoreWrites = Schema.Struct({
|
|
|
114
291
|
delete: Schema.Array(Schema.String)
|
|
115
292
|
});
|
|
116
293
|
const decodeStoreWrites = Schema.decodeUnknownOption(StoreWrites);
|
|
117
|
-
const fromPiResult = (result) => {
|
|
294
|
+
const fromPiResult = (result, scriptLines) => {
|
|
118
295
|
if (result.ok) {
|
|
119
296
|
const storeWrites = decodeStoreWrites(result.storeWrites);
|
|
120
297
|
if (Option.isNone(storeWrites)) return failure({
|
|
@@ -134,7 +311,7 @@ const fromPiResult = (result) => {
|
|
|
134
311
|
kind,
|
|
135
312
|
message: name !== void 0 && name.length > 0 && !message.startsWith(name) ? `${name}: ${message}` : message
|
|
136
313
|
};
|
|
137
|
-
if (stack !== void 0) error.stack = stack;
|
|
314
|
+
if (stack !== void 0) error.stack = withoutWrapperFrames(stack, scriptLines);
|
|
138
315
|
return {
|
|
139
316
|
ok: false,
|
|
140
317
|
error,
|
|
@@ -145,7 +322,8 @@ const describeError = (error) => error instanceof Error ? error.message : String
|
|
|
145
322
|
/**
|
|
146
323
|
* A `CodeModeExecutor` on `@earendil-works/pi-codemode`: every execution gets a fresh QuickJS VM
|
|
147
324
|
* (WebAssembly) in a fresh worker thread, closed when the execution ends. Applies the timeout,
|
|
148
|
-
* heap cap, abort signal, and store, strips TypeScript annotations first,
|
|
325
|
+
* heap cap, abort signal, and store, strips TypeScript annotations first, rejects tool arguments
|
|
326
|
+
* that JSON would silently change inside the script (see `CodeModeExecutorTool`), and caps concurrent
|
|
149
327
|
* executions per executor; create one executor per process (module scope) so the cap is
|
|
150
328
|
* per process.
|
|
151
329
|
*
|
|
@@ -182,11 +360,11 @@ const makePiCodeModeExecutor = (options = {}) => {
|
|
|
182
360
|
});
|
|
183
361
|
}
|
|
184
362
|
try {
|
|
185
|
-
return fromPiResult(await sandbox.execute(stripped.code
|
|
363
|
+
return fromPiResult(await sandbox.execute(`${guardPrefix}${stripped.code}${guardSuffix(execution.tools)}`, {
|
|
186
364
|
signal: execution.signal,
|
|
187
365
|
store: execution.store,
|
|
188
366
|
timeoutMs
|
|
189
|
-
}));
|
|
367
|
+
}), scriptLineCount(stripped.code));
|
|
190
368
|
} finally {
|
|
191
369
|
await sandbox.close();
|
|
192
370
|
}
|