@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 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-rc.115
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-rc.115`) in host code.
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 reject with an `Error` carrying their text. Calls still running when
62
- the script ends are cancelled and recorded as `cancelled`.
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 the message and records it as `error` without executing it.
84
- - A nested call that is interrupted rejects with `was cancelled`; a defect rejects with `failed
85
- unexpectedly`. If an executor misses its deadline, the tool returns a `timeout` failure
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
@@ -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; /** The tool's `ToolModule.id`. */
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
@@ -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,UAMa;EAAA,SAJb,SAAA,UASU;EAAA,SAPV,oBAAA;EAAA,SACA,WAAA;EAAA,SACA,WAAA,EAAa,kBAAA,EANb;EAAA,SAQA,YAAA,EAAc,kBAAA,EAJd;EAAA,SAMA,UAAA;EAAA,SACA,QAAA,EAAU,oBAAA;AAAA;;cAMR,eAAA,GACX,KAAA,EAAO,aAAA,CAAc,UAAA,MACpB,aAAA,CAAc,mBAAA;;cAmBJ,2BAAA;;cAGA,sBAAA,GAA0B,IAAY;AAAA,KA4DvC,eAAA;EApFC;;;EAAA,SAwFF,MAAA,EAAQ,aAAA,CAAc,mBAAA,GAvFxB;EAAA,SAyFE,kBAAA,EAAoB,aAAA;AAAA;;;;;;;cASlB,qBAAA,GACX,OAAA,EAAS,aAAA,CAAc,mBAAA,GACvB,MAAA,aACC,eAAA;AAAA,KAuFS,wBAAA;EAAA,SACD,KAAA,EAAO,aAAa,CAAC,UAAA;EAAA,SACrB,YAAA,WA1K6B;EAAA,SA4K7B,KAAA;AAAA;AA5K6B;AAGxC;;;;AAAmD;AA4DnD;AA/DwC,cAsL3B,yBAAA,GAA6B,KAA+B,EAAxB,wBAAwB;;cAwB5D,oBAAA,GAAwB,IAAyB,EAAnB,mBAAmB;;cAIjD,gBAAA,GACX,OAAA,EAAS,aAAA,CAAc,mBAAA,GACvB,IAAA,aACC,mBAAA"}
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) => tools.map(({ def, moduleId, moduleDescription }) => {
8
- const tool = {
9
- name: def.name,
10
- identifier: toCodemodeIdentifier(def.name),
11
- namespace: moduleId,
12
- description: def.description,
13
- inputSchema: def.parameters,
14
- outputSchema: def.outputSchema ?? textOutputSchema,
15
- structured: def.outputSchema !== void 0,
16
- exposure: toolDiscovery(def) ?? "all"
17
- };
18
- return moduleDescription === void 0 ? tool : {
19
- ...tool,
20
- namespaceDescription: moduleDescription
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
@@ -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":"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"}
@@ -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.
@@ -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;;;;;;;KASlB,oBAAA;EAAA,SACD,IAAA;EAAA,SACA,WAAA;EAAA,SACA,WAAA,GAAc,kBAAA;EAAA,SACd,YAAA,GAAe,kBAAA;EAAA,SACf,OAAA,GAAU,IAAA,WAAe,OAAA,EAAS,mBAAA,KAAwB,OAAA;AAAA;AAhBrE;;;;AAAA,KAuBY,sBAAA,GAAyB,oBAAoB;EAZ7C,4FAcD,MAAA;WAEA,SAAA;AAAA;;KAIC,kBAAA;EAAA,SACG,IAAA;EAAA,SAAuB,IAAA;AAAA;EAAA,SACvB,IAAA;EAAA,SAAwB,IAAA;EAAA,SAAuB,QAAA;AAAA;;;;;;;KAQlD,iBAAA;AAAA,KAEA,aAAA;EAAA,SACD,IAAA,EAAM,iBAAiB;EAAA,SACvB,OAAA;EAAA,SACA,KAAA;AAAA;;KAIC,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,EA3BmB;EAAA,SA6B3B,MAAA,EAAQ,aAAA,CAAc,kBAAA,GA5BM;EAAA,SA8B5B,WAAA,GAAc,mBAAA;AAAA;AAAA,KAGb,sBAAA;EAAA,SACD,KAAA,EAAO,aAAA,CAAc,oBAAA;EAAA,SACrB,OAAA,EAAS,aAAA,CAAc,sBAAA;WAEvB,SAAA,UA7BkB;EAAA,SA+BlB,gBAAA,UA7Bc;EAAA,SA+Bd,KAAA,EAAO,aAAA;EAAA,SACP,MAAA,EAAQ,WAAA;AAAA;;;;;AA7BH;AAIhB;;;;;KAsCY,gBAAA;EAAA,SACD,OAAA,GACP,IAAA,UACA,OAAA,EAAS,sBAAA,KACN,OAAA,CAAQ,uBAAA;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, and caps concurrent
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
  *
@@ -1 +1 @@
1
- {"version":3,"file":"node.d.mts","names":[],"sources":["../src/node.ts"],"mappings":";;;;;cAqBa,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;AA4NlC;;cAAa,sBAAA,GACX,OAAA,GAAS,yBAAA,KACR,gBA4EF"}
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, and caps concurrent
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
  }